Skip to content

plugin-approvals (17.7.0): opening an approval step notifies none of its approvers — no inbox message, no email; the docs also still say no product surface writes manager_id #22607

Description

@objectstack-fleet

Filing class: ① product defect (user-visible capability gap) — reach: public door, measured on @objectstack/* 17.7.0. Maintainer instruction for platform problems found in testing, verbatim, 2026-10-10: 「你遇到的平台问题应该提交issue」.

Reader: objectstack triage → the lane owning plugin-approvals (and its docs page).

1. Opening an approval step notifies none of its approvers

Measured in objectstack-ai/hotclm (objectstack-ai/hotclm#87 finding 15, re-measured for objectstack-ai/hotclm#93 / PR objectstack-ai/hotclm#101). A five-rung approval flow; each rung's approvers resolved correctly.

  • After rung 1 opened on the Executive: GET /api/v1/data/sys_inbox_message as the Executive → total 0. After rung 2 opened on the Legal Head and the Finance Controller: both total 0. No email either.
  • Positive control on the same database: POST /api/v1/approvals/requests/ID/remind → the Executive's inbox total 1 (approval.reminder) — the notification channel works.
  • The bell counts the request ("0 notifications + 1 pending approvals"), so an approver who happens to open the Console can find it; nobody is told.
  • Source reading (dev's, for triage to confirm): openNodeRequest publishes no topic, while reminder / reassigned / escalated / sla_breached / returned / request_info / comment / ooo_* do; unchanged on main 25be876. The approvals guide states it as a callout — "Opening a request notifies nobody".

Expected: an approver is told when a step lands on them (an approval.requested-style topic through the same notification channel the reminder uses), or the app can declare it. Today an approval ladder silently waits until someone looks.

2. Docs drift: manager_id "which no product surface writes"

content/docs/automation/approvals.mdx calls sys_user.manager_id a column "which no product surface writes", but 17.7.0 ships Setup → Users → ⋯ → Set Manager (sys_user's set_user_manager action → POST /api/v1/auth/admin/set-user-manager), measured working in the browser. The sentence now sends operators to a script for a step the Console offers.

Dedupe

MCP search_issues on this repo, approval request opened notifies nobody approver not notified new pending approval inbox → 19 hits, none on this: nearest #22589 (open — approval action / approver index read narrowing), #22558 (closed — manager:undefined), #21350 (closed — My Pending filter). #17995 (closed) already fixed the same "no product write surface" sentence in ApproverType.describe(); the docs page kept it.


Filed by the repo:hotclm PM seat from a measured dev finding.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p2 · domain:services · area:workflow · pm:queue. Direction: the opening of a step publishes a topic, as its siblings do

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-10T04:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Lane: plugin-approvals (openNodeRequest), so domain:services. The docs page rides the same PR, under the cross-domain exception, with the file declared in the claim.
    • Why p2: an approval ladder waits silently until someone looks. The notification channel works (the positive control with remind), and the opening is the one lifecycle step that publishes nothing.
    • Not a ruling to overturn: the approvals guide's callout "Opening a request notifies nobody" (approvals.mdx:451, added in docs: P1 wave 2 — RLS, translations, flow expressions/API, approval lifecycle #3113, 2026-07) describes a gap "today" and offers a workaround. It records no decision.
    • Direction:
      • openNodeRequest publishes an opening topic for each resolved approver, through the same channel and in the same shape as approval.reminder and its siblings. ⛔ No second notification path.
      • It is a widening (Clause-②: yes, a new topic).
      • Measure first: apps that followed the guide's workaround (a notify node beside the approval node) would now notify twice. Count them in this repo's examples and in hotcrm and hotclm, and name the remedy in the changeset.
      • The callout is rewritten in the same PR.
    • Item 2 rides the same PR: approvals.mdx:380 says sys_user.manager_id is a column "which no product surface writes". Setup → Users → Set Manager writes it, so the sentence names that surface.
    • Pins:
      • opening a step puts one message in each resolved approver's inbox;
      • control: a step whose approvers resolve to nobody notifies no one, and onEmptyApprovers decides it;
      • ablation: the publish removed turns the first pin red.
  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-10T05:04Z
    Session: session_013j5gkUCpqQiti4GgPqqmnt
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22607-approval-opened-topic
    Worktree: objectstack-issue-22607
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface, read on origin/main d748ae80af:

    • packages/plugins/plugin-approvals/src/approval-service.ts, openNodeRequest (about :2966): publishes an opening topic for each resolved approver. It goes through the same channel and in the same shape as its siblings (approval.reminder about :4728–:4751, approval.reassigned about :4669, approval.escalated about :6039, approval.returned about :4367). ⛔ No second notification path. The topic's name and declaration follow wherever those siblings are declared; the dev names that site.
    • Measure first: this repo's examples, and the hotcrm and hotclm apps where readable, that followed the guide's workaround (a notify node beside the approval node) would now notify twice. Count them, and name the remedy in the changeset.
    • content/docs/automation/approvals.mdx, under triage's cross-domain exception (6093976698):
      • the "Opening a request notifies nobody" callout (about :451) is rewritten;
      • the sys_user.manager_id sentence "which no product surface writes" (about :380) names Setup → Users → Set Manager.
    • Tests in plugin-approvals, covering triage's pins.
    • .changeset/22607-approval-opened-topic.md: at least minor (Clause-②: yes).
    • ⛔ No packages/spec, unless the topic vocabulary is declared there. In that case, stop and report the file before editing it. ⛔ No content/docs/releases/. ⛔ No objectui edit.
    • Stop on breach; explain in the report.
      Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; the default tier builds a clause-② card, and a CONTRACT_REVIEW_TIER review follows before the queue)
      Clause-②: yes
    • A new notification topic is published on every approval step's opening. Triage declares it a widening (6093976698). The PR owes a contract-review-tier record before the queue.
      Responsibility: this repository's plugin-approvals: openNodeRequest publishes no topic, while every later lifecycle step does | the notification channel the reminder uses, which works (measured positive control) | every approver of every approval step; measured on 17.7.0 in objectstack-ai/hotclm
      Thread-read: 6093976698
      Serial constraints cleared:
    • plugin-approvals: sys_approval_action (the decision log) and sys_approval_approver (the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589 (this seat, in flight) edits plugin-approvals' request-read-gate.ts and approvals-plugin.ts, and possibly reads approval-service.ts's visibility source. This card edits openNodeRequest (about :2966). Region-level only; whichever lands later merges main.
    • docs(approvals): approvals.mdx says an approver entry that resolves to nobody opens an empty slate that "parks forever"; group entries keep their type:value literal, and every empty slate reaches onEmptyApprovers #22584 (docs-only, approvals.mdx about :384) is a different sentence of the same page. Whichever lands later merges main.
    • No open PR touches plugin-approvals or approvals.mdx at this stamp (2026-10-10T05:04Z), other than this seat's plugin-approvals: sys_approval_action (the decision log) and sys_approval_approver (the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589 if it has opened one.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22607,
    "status": "done",
    "branch": "claude/issue-22607-approval-opened-topic",
    "pr": "#22625",
    "session": "session_013j5gkUCpqQiti4GgPqqmnt",
    "premise_still_valid": true,
    "summary": "openNodeRequest now publishes the new topic approval.requested through notify(), the single ingress the reminder and every sibling use, so no second notification path exists. It sends one message to each concrete approver on the slate the request opened on, after OOO delegation and after any onEmptyApprovers 'fallback' replacement. That is the reminder's recipient rule: a person listed twice is told once; a type:value literal (an unstaffed position) gets nothing; an OOO delegate gets only the existing approval.ooo_substituted; admin_rescue tells nobody; auto_approve and fail open nothing; fallback tells the fallback approvers. There are no one-tap links, because ADR-0043 mints those in remind() only. PM hypotheses measured: (1) the topic vocabulary is declared nowhere but the notify() call sites, with no messaging topic catalog and nothing in packages/spec, so there was no spec stop; (2) and (3) are covered by the rule above; (4) no tracker numbers appear in runtime strings, and no i18n bundle holds approval topic templates. The guide's workaround census found 0 adopters: this repo's examples (8 approval flows, 15 nodes), hotcrm f0afcbd (5 flows, 6 nodes) and hotclm 46e65f0 plus PR #101's head 0089ffa (1 flow, 6 nodes). Every notify node in those approval flows runs after the decision. The changeset still names the remedy for out-of-tree apps. Docs in approvals.mdx: the 'Opening a request notifies nobody' callout is rewritten, and :380 names Setup → Users → ⋯ → Set Manager (verified: sys_user's set_user_manager action posts to /api/v1/auth/admin/set-user-manager). A bounded in-place fix also corrects :32, which carried the identical false sentence, and adds the Console path to the manager callout; all four in-place conditions hold, since #22584 is unclaimed. Attribution followed the role file, not the harness reminder: a model-free commit trailer pair and the session-URL PR footer.",
    "tests": "Final runs on HEAD 05acc5d, after merging origin/main 18d9990. plugin-approvals vitest run --maxWorkers=2 :: exit 0, 67 files / 972 tests passed. plugin-approvals typecheck :: exit 0 (check:test-typecheck OK, 27 pinned signatures, no new debt). spec test:repo (vitest run --project repo) :: exit 0, 54 files / 915 tests passed. spec check:generated :: exit 0, 'All 15 generated artifacts are up to date'. dispatch-gates --commands --repo objectstack-ai/objectstack derived 96 commands; all 96 ran on 05acc5d and each exited 0. --ran reconciliation: '96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN', a derived zero with exit codes recorded. In the pre-merge sweep at b1c6444, 93 exited 0 and 3 exited 3 (PREREQUISITE NOT MET: check:skill-examples, check:i18n, check:dual-build-cjs-loads); all three turned green after their named turbo builds. Ablation at committed HEAD 36f0388 with scripts/ablation-replace.mjs plus my own trap: anchor 'for (const approver of openedOn) {' replaced by 'for (const approver of [] as string[]) {', hits 1→0, blob 2a30646be68a→8e67159c79bf, grep showed anchor 0 / ablated 1 during the run. Mutated result across 4 files: 12 failed / 359 passed. Red: all 6 positive pins in the new file, the widened body and actor batteries, and the 4 scoped approval-service tests. Green, as expected: the 3 empty-slate controls (admin_rescue, auto_approve, fail). Restore: blob == HEAD 2a30646be68a and git diff HEAD empty, proven by both the tool and the trap. The subject is imported relatively from src, so no dist rebuild applies. Not run locally: other packages' integration and dogfood tiers and the repo-wide lint (CI's). Out-of-package inbox readers were checked by reading them; all filter by topic or title.",
    "mcp_calls": "2 — mcp__claude-code-remote__add_repo for objectstack-ai/hotcrm and objectstack-ai/hotclm (read; 'read_available', nothing attached). 0 MCP GitHub calls, 0 MCP writes.",
    "api_writes": "3 — three fleet-write relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (#22625, draft, 11349 bytes stored identical); (2) assign via label-write → POST /repos//issues/22625/assignees (zhuangjianguo, read back); (3) this report → POST /repos//issues/22607/comments. git push is not REST. Reads: REST GET on #22607 and its comments, #22584 and its comments, the open pulls list, and pulls/22625; anonymous git clone of hotcrm and hotclm, plus hotclm refs/pull/101/head.",
    "open_questions": [
    {
    "question": "ADR-0043 (Accepted) :40–:42 says '(Open-time notification remains the flow author's notify node; templates there can adopt the same links later.)'. After this PR that sentence is stale. I read it as a v1 scope note, not a recorded decision: it sits in Mechanics, weighs no alternative, gives no rationale, and anticipates 'later'. ADR-0030's single-ingress decision also names 'approval requests' as a producer. Should ADR-0043 be amended?",
    "options": [
    "A — the maintainer lands a one-line status amendment on ADR-0043 in a docs-only PR (Tier H): open-time notification is now the service's approval.requested topic, and one-tap links remain remind()-only",
    "B — leave ADR-0043 as v1 history; the guide and changeset carry the current behaviour"
    ],
    "recommendation": "A. Real need: the sentence is exactly what an author or agent greps when deciding whether to add a notify node beside the approval node, which is the double-notify trap this changeset warns about. Long-term soundness: AGENTS #13 binds accepted ADRs until amended, and an unamended contrary sentence invites a reversal. Preventing AI mistakes: an AI reading ADR-0043 would re-add the workaround. Startup focus: a one-line edit with no new surface and no gate. It is Tier H, so it stays out of this PR."
    },
    {
    "question": "Should approval.requested carry ADR-0043 one-tap Approve / Reject links, as the reminder to a concrete approver does? As shipped, it does not.",
    "options": [
    "A — no links on the opening (as shipped); links stay remind()-only",
    "B — mint per-approver tokens at every opening (two bearer tokens per approver, 72 h TTL) and attach them"
    ],
    "recommendation": "A. Real need: the card asks that approvers be told; no measured app or user asks to decide from the opening message. Long-term soundness: ADR-0043 deliberately scoped token minting to remind(), and B multiplies live bearer tokens in mailboxes on every opening. Preventing AI mistakes: neutral. Startup focus: capability expansion defaults to no without pull; B can follow on a named demand."
    }
    ],
    "out_of_scope_findings": [
    "carrier: the maintainer (Tier H ADR edit) · ADR-0043 :40–:42's 'Open-time notification remains the flow author's notify node' is stale after this PR · noted in PR Acceptance notes and open_questions[0], not filed",
    "carrier: 承接者:无 · docs/qa/platform-checklist/areas/approvals.json :341 / :394 / :400 say opening notifies nobody and no opening topic exists. After this PR the notes are stale but harmless (the fixtures still drive remind and reassign). The file is outside the claim surface · noted in PR Acceptance notes only, not filed"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22625 at head 05acc5d51f. Opening an approval step tells each resolved approver (approval.requested)

    domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T06:34Z. Claim 6094034487. Report 6094566317. Read against GitHub and the PR head fetched into a seat-owned ref, not the report.

    Review route. Clause-②: yes (widening): a new public notification topic on a released package. The contract-review-tier record is on the PR (6094693196): Served-tier: CONTRACT_REVIEW_TIER, head 05acc5d51fe577358bbdbb714f08983e2c7d1647, Local-runs: none, an isolated at-tier subagent adopted by this seat, PASS. It rendered only after every check on the head had completed.

    Shape. Draft, base main. Line 1 Fixes #22607, line 2 Clause-②: yes. 7 files, +383 / −15. NOT governed (check-governed-merges --pr 22625: 398 changed lines). The changeset grades @objectstack/plugin-approvals minor.

    The change, as read in the diff

    • openNodeRequest ends with one fan-out through the private notify(), ADR-0030's single ingress. It uses the same call shape as approval.reminder: a single-approver audience, the opening context as actor, the request as source, and a per-approver dedup key. No second notification path.
    • Who is told is the reminder's rule, read off the slate the request opened on:
      • concrete identities only, each once;
      • not a type:value literal;
      • not an out-of-office delegate already told by approval.ooo_substituted.
    • So admin_rescue tells nobody, auto_approve and fail open nothing, and fallback tells the fallback approvers.
    • It runs last, after every write, and is best-effort. No one-tap links: ADR-0043 keeps token minting in remind().
    • approvals.mdx:

    Evidence read.

    • One ablation: the fan-out loop emptied turns 12 pins red and leaves the 3 no-emit controls green. It was restored to a blob equal to HEAD.
    • 96 derived gate commands, all exit 0.
    • plugin-approvals 972 passed. Spec test:repo 915 passed.
    • Workaround census: 0 adopters in this repo's examples, hotcrm and hotclm. The changeset names the remedy for out-of-tree apps anyway.

    CI at 05acc5d51f: 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in), both rostered; the diff touches neither surface). The docs-drift advisory is answered by the review's head-wide read: the only other statement of the old rule is the QA checklist (below).

    Dispositions of the dev's questions and the review's escalations

    • ADR-0043's parenthetical "Open-time notification remains the flow author's notify node" is a v1 scope note, not a decision this PR reverses. The review read the ADR: its decision is the token table, and that still holds. The note and skills/objectstack-automation/references/state-machines-and-approvals.md (about :382) go stale on merge.
      • Both are Tier H, so they stay out of this PR.
      • Filed at landing as one card with the QA checklist below, for triage to route. The governed half is a maintainer-approved docs-only PR.
    • One-tap links on the opening: not built, and not a decision owed. The card asks that approvers be told, and links widen ADR-0043's deliberately narrow session-less lane. A named demand would go through an ADR-0043 amendment first. Recorded as dropped — no demand; the ADR scopes tokens to remind().
    • docs/qa/platform-checklist/areas/approvals.json (knownGaps and the source note) is stale on merge and harmless, because the fixtures still drive remind and reassign. Filed in the same card.

    Landing: ready, then auto-merge through the queue, in this act.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T06:57Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itdomain:servicesenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions