Skip to content

On a README-configured install the approval ladder sticks forever at its first rung — manager_review resolves to manager:undefined, locks the contract, and no approver is told anything is waiting - #101

Draft
objectstack-fleet[bot] wants to merge 1 commit into
mainfrom
claude/issue-93-approval-ladder-rung-1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #93

What changed

  • README.md: a new section, "Rung 1 of the approval ladder is a manager", in the operator setup.
    • Rung 1 is the direct manager of the contract's owner (not the lawyer who pressed Send for Approval), read from sys_user.manager_id. No seed, position or permission set fills that column.
    • Where to set it: Setup → Users → the user → ⋯ → Set Manager, for each business requester and for anyone else who launches contracts. Any other account will do, and the manager needs nothing more: approve_contract is in clm_requester.
    • What skipping it costs: the request waits on manager:undefined (upstream plugin-approvals: a manager approver whose owner has no manager_id lands as the literal manager:undefined in pending_approvers, where the documented contract says the request opens with an empty slate (17.7.0) objectstack#22558) with the contract locked. Setting the manager afterwards does not reach a request that is already open. Only a platform admin's Override moves it.
    • What approvers are told: no message and no email when a rung opens. The bell counts their pending approvals and Waiting on Me lists them.
    • The intro sentence of "Users are not seeded" now names the manager step. The scripted section adds POST /api/v1/auth/admin/set-user-manager and the data API's 403 on manager_id.
  • scripts/demo.mjs: the closing operator note has a new step 2 naming the manager step. The header now says "two setup steps", and the old step 2 is step 3.
  • src/flows/contract-approval.flow.ts: comments only. The compiled flows are byte-identical to 46e65f0 (proof below).
    • The manager_review comment said the engine reads the submitter's manager. It reads the owner's (record[value] ?? record.owner_id, as the header already said, and as measured below: the submitter was Legal Counsel 1 and rung 1 opened on the owner's manager).
    • RUNG_POLICY now records the 17.7.0 reading of the admin override (in the Console now, no longer API-only) and why no fallback is authored.

The three decisions the card asked for

  1. README operator setup names the manager step. Done. It is the fix for the README-configured install: with managers set, rung 1 resolves and the ladder climbs (browser evidence below).
  2. F5 keeps onEmptyApprovers: 'admin_rescue' on manager_review; no 'fallback'.
    • 'fallback' is refused without a fallbackApprovers list (checkFallbackApproversPairing), and that list has to name who approves a contract whose owner has no manager. DESIGN.md §06 F5 and §04 do not name anyone.
    • §03 fixes the ladder at five named rungs, and §13 Q3 sends extra approvers to the customer overlay.
    • The platform's other suggestion, a second { type: 'org_membership_level', value: 'owner' } entry, would seat the org owner as a silent co-approver on rung 1.
    • With the README step done, the empty slate does not occur on a configured install. Where it still occurs, the 17.7.0 Console gives the platform admin an in-product recovery (Override Approve / Reject / Reassign / Recall, measured below). Recorded as a non-blocking question in the report, with a recommendation to keep it this way.
  3. Telling an approver a rung opened is the platform's job, not F5's. Reported, no workaround.
    • The approvals service is the only component that knows the concrete slate. It resolves positions, managers, out-of-office delegates, fallbacks and cross-org screening.
    • It already publishes notification topics for approval.reminder, .reassigned, .escalated, .sla_breached, .returned, .request_info, .comment and .ooo_*. It publishes none from openNodeRequest: read in @objectstack/plugin-approvals@17.7.0 dist/index.js, and the same on objectstack main @ 25be876.
    • An app-side notify node per rung would have to re-derive that slate: the messaging recipient resolver has no position: or manager selector. That is a second copy of the routing that the F5 header refuses to keep, and it would mis-address under OOO delegation and fallback. So none is added.
    • The platform guide's own "Opening a request notifies nobody" callout suggests exactly that notify node. It is not taken, per the lane ruling (platform problems are fixed in the platform).
    • The platform report is at the foot of this PR.

The literal manager:undefined is the platform's (objectstack-ai/objectstack#22558) and is untouched here.

Browser evidence: Chromium 1194, 1440×900, pnpm demo --port 3493, @objectstack/* 17.7.0

Setup.

  • Boot is clean: Plugins: 41 loaded, no degraded, no no such table.
  • The README's eleven accounts were created through POST /api/v1/auth/admin/create-user, positions through POST /api/v1/data/sys_user_position (all 201), and requesters' clm_requester through sys_user_permission_set (201). Then pnpm demo ran again.
  • Owners were handed over: owner_id 43 / 43 / 34.

Before: the README as it stands on main (no manager set).

  • Legal Counsel 1 opened SUP-2026-0001 (owner Business Requester 2) and clicked Send for Approval → POST /api/v1/actions/clm_contract/send_for_approval → 200.
  • GET /api/v1/approvals/requests?object=clm_contract&recordId=m9_O4fSzq52HrAfz → current_step "manager_review", pending_approvers ["manager:undefined"].
  • The server logged approver 'manager:undefined' expanded to nobody and resolved to no concrete approver — the request is decidable only by a privileged admin.
  • The record page shows Locked for approval. As the platform admin, Setup → Approvals → Approvals Inbox → All → that request renders Override Approve · Override Reject · Override Reassign · Override Recall.

The new README step, in the browser.

  • As the dev admin: Setup → Users → Business Requester 1 → ⋯ → Set Manager → picked "Executive" → Confirm → POST /api/v1/auth/admin/set-user-manager → 200 {"userId":"y2Vm…","managerId":"JOzK…"} and the toast "Manager updated".
  • The scripted route set requesters 2 and 3: POST /api/v1/auth/admin/set-user-manager → 200 each.
  • The data API refuses the column: PATCH /api/v1/data/sys_user/ID {"manager_id":…} → 403 PERMISSION_DENIED "None of the submitted fields (manager_id) are editable on 'sys_user' via the data API (ADR-0092). Editable fields: name, image, locale."
  • The SUP-2026-0001 request still read pending_approvers ["manager:undefined"] after its owner's manager was set. That is the README's "does not reach a request that is already open".

After: rung 1 resolves and the ladder climbs, with no admin override.

  • MSA-2026-0001 (owner Business Requester 1). Legal Counsel 1 accepted its one open deviation (API) and clicked Send for Approval → 200.
  • The request opened current_step "manager_review", pending_approvers ["JOzKz5aTf9zlockrUqUO6HHm4bMLKCpe"], the Executive.
  • The Executive signed in → 我的合同 › Waiting on Me (/_console/apps/app.objectstack.hotclm/component/approvals/inbox) → My Pending 1 → "Contract Approval Ladder · Direct Manager · Master Services Agreement — Aurora Systems" → Approve → Confirm → POST /api/v1/approvals/requests/areq_6209…/approve → 200, toast "Approved."
  • The next rung opened: current_step "legal_finance_joint", pending_approvers = [Legal Head, Finance Controller].
  • On the head build (0089ffa): ICA-2026-0004 (owner Business Requester 1, no route flags). Legal Counsel 2 clicked Send for Approval → rung 1 on the Executive → Executive Approve → 200. The contract read status "approved" and approval_status "approved", and the owner's inbox held clm_contract_approved: Contract approved: Independent Contractor Agreement — Camille Beauchamp.

What an approver is told when a rung opens (card finding 15).

  • GET /api/v1/data/sys_inbox_message read total 0 for the Executive after rung 1 opened, and 0 for the Legal Head and the Finance Controller after rung 2 opened.
  • Positive control: the submitter's POST /api/v1/approvals/requests/areq_6209…/remind → 200 put approval.reminder: Approval reminder in the Executive's inbox (total 1). So messaging to this approver works; the open simply emits nothing.
  • The Console does show a pull signal: the Legal Head's bell read badge 1, "0 notifications + 1 pending approvals".

Browser console. Pre-existing noise only:

Gates, at 0089ffa (one green tail each, exit codes captured to files before any pipe)

validate=0  lint=0  typecheck=0  i18n=0   (HEAD 0089ffa)
$ pnpm validate
  ✓ Validation passed (1034ms)
$ pnpm lint
  6 suggestion(s) (964ms)        ← the six approval-approvers-may-resolve-empty infos, the same six as on main
$ pnpm typecheck
> tsc --noEmit                   (exit 0, no output)
$ pnpm lint:i18n-gate
✓ i18n gate
  COVERAGE : 0 missing keys across 2 locale(s)

Comment-only proof. git archive 46e65f0 and git archive 0089ffa were each built with objectstack build. flows is identical (contract_approval 8697 bytes, equal). The only differing top-level key is runtimeModule, whose bundle differs only in the two edited comment blocks and its temp entry filename.

Acceptance notes

  • PM assumption (1), partly falsified: "only POST /api/v1/auth/admin/set-user-manager can set it". On 17.7.0 the Console has a Set Manager action (Setup → Users → the user → ⋯) that posts to that endpoint. The README names the button first and the endpoint for scripts. The rest holds: the README never asked for the step, and with managers set the ladder works.
  • PM assumption (2), measured: see decision 3 and the platform report below. Nothing in DESIGN.md §06 makes F5 notify an approver when a rung opens. §05 makes 待我处理 the platform approvals inbox, which the bell also counts.
  • PM assumption (3): src/objects/contract.hook.ts untouched (PR The intake wizard's "Submit now" fails for every requester — FLOW_FAILED, the edit-window RLS policy refuses the post-submit row, while the header Submit button on the same draft succeeds #91 holds it). The ladder was entered through the send_for_approval header action, in_review → in_approval.
  • Platform doc drift, noted, not filed (no carrier): objectstack's Approvals guide says sys_user.manager_id is a column "which no product surface writes" (content/docs/automation/approvals.mdx). 17.7.0 ships the Set Manager action, platform-objects sys_user set_user_manager.
  • The approval ladder already notifies the owner on the terminal edges (notify_approved / notify_rejected), measured above. Nothing changes there.

Platform report (for the seat to file upstream, per the lane ruling; nothing filed from here)

  • Symptom: when an approval node opens a request, the approvers it resolved receive no sys_inbox_message and no email. On every other request event (reminder, reassign, escalation, return, request-info, comment, OOO) they do.
  • Minimal repro: a flow with one approval node { type: 'user', value: 'ID' }. Trigger it and read GET /api/v1/data/sys_inbox_message as that user: total 0. Call POST /api/v1/approvals/requests/ID/remind as the submitter, and the same read answers 1 (approval.reminder).
  • Expected capability: openNodeRequest publishes an "approval requested" topic to the concrete slate it just resolved, with approve/reject action links like remind() does.
  • Version: @objectstack/plugin-approvals 17.7.0. Upstream main @ 25be876 has no such topic either (packages/plugins/plugin-approvals/src/approval-service.ts).
  • Dedupe words: openNodeRequest notify, approval.requested, Opening a request notifies nobody.

Generated by Claude Code

…needs

On an install set up from the README alone, every contract sent for
approval opened rung 1 on `manager:undefined` and stayed there, locked:
rung 1 resolves the contract owner's `sys_user.manager_id`, and nothing
in the operator setup asked anyone to set it.

The README now has a "Rung 1 of the approval ladder is a manager"
section (Setup -> Users -> Set Manager for every business requester, the
admin endpoint for scripts, what an unset manager costs and how a
platform admin recovers it, and that no rung sends its approver a
message when it opens), and the `pnpm demo` closing note carries the
step. F5 keeps `onEmptyApprovers: 'admin_rescue'`: a `fallback` would
need a fallbackApprovers list naming who takes an unstaffed rung, which
DESIGN.md does not decide. The manager_review comment said the engine
reads the submitter's manager; it reads the owner's, as the header says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants