Repository navigation
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
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
enhancement·priority:p2·domain:services·area:workflow·pm:queue. Direction: the opening of a step publishes a topic, as its siblings doTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T04:56Z. ⛔ Not a claim, ⛔ not a dispatch.- Lane:
plugin-approvals(openNodeRequest), sodomain: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:
openNodeRequestpublishes an opening topic for each resolved approver, through the same channel and in the same shape asapproval.reminderand 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
notifynode 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:380sayssys_user.manager_idis 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
onEmptyApproversdecides it; - ablation: the publish removed turns the first pin red.
- Lane:
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-10T05:04Z
Session:session_013j5gkUCpqQiti4GgPqqmnt
Account:zhuangjianguo(the seat's linked user asGET /useranswers 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 onorigin/maind748ae80af: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.reminderabout:4728–:4751,approval.reassignedabout:4669,approval.escalatedabout:6039,approval.returnedabout: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
notifynode 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_idsentence "which no product surface writes" (about:380) names Setup → Users → Set Manager.
- the "Opening a request notifies nobody" callout (about
- Tests in
plugin-approvals, covering triage's pins. .changeset/22607-approval-opened-topic.md: at leastminor(Clause-②: yes).- ⛔ No
packages/spec, unless the topic vocabulary is declared there. In that case, stop and report the file before editing it. ⛔ Nocontent/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 aCONTRACT_REVIEW_TIERreview 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) andsys_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) editsplugin-approvals'request-read-gate.tsandapprovals-plugin.ts, and possibly readsapproval-service.ts's visibility source. This card editsopenNodeRequest(about:2966). Region-level only; whichever lands later mergesmain. - docs(approvals):
approvals.mdxsays an approver entry that resolves to nobody opens an empty slate that "parks forever"; group entries keep theirtype:valueliteral, and every empty slate reachesonEmptyApprovers#22584 (docs-only,approvals.mdxabout:384) is a different sentence of the same page. Whichever lands later mergesmain. - No open PR touches
plugin-approvalsorapprovals.mdxat this stamp (2026-10-10T05:04Z), other than this seat's plugin-approvals:sys_approval_action(the decision log) andsys_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
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-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-approvalsvitest 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
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22625 at head
05acc5d51f. Opening an approval step tells each resolved approver (approval.requested)domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T06:34Z. Claim6094034487. Report6094566317. 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, head05acc5d51fe577358bbdbb714f08983e2c7d1647,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 1Fixes #22607, line 2Clause-②: yes. 7 files, +383 / −15. NOT governed (check-governed-merges --pr 22625: 398 changed lines). The changeset grades@objectstack/plugin-approvalsminor.The change, as read in the diff
openNodeRequestends with one fan-out through the privatenotify(), ADR-0030's single ingress. It uses the same call shape asapproval.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:valueliteral; - not an out-of-office delegate already told by
approval.ooo_substituted.
- So
admin_rescuetells nobody,auto_approveandfailopen nothing, andfallbacktells 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:- the "Opening a request notifies nobody" callout is rewritten, with the double-notify remedy;
- the
manager_idsentences name Setup → Users → ⋯ → Set Manager. The review verified theset_user_managerrow action onsys_user. - docs(approvals):
approvals.mdxsays an approver entry that resolves to nobody opens an empty slate that "parks forever"; group entries keep theirtype:valueliteral, and every empty slate reachesonEmptyApprovers#22584's sentence is untouched.
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-approvals972 passed. Spectest:repo915 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(knownGapsand thesourcenote) 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
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T06:57Z- PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625 merged through the queue as
e8c6666870, read onorigin/main:openNodeRequestpublishesapproval.requested. ItsFixes #22607closed this cardcompleted.pm:dispatchedand the assignee were cleared in this act. - What landed (
@objectstack/plugin-approvals,minor):- opening an approval step tells each concrete approver on the slate it opened on, once, on
approval.requested, through the samenotify()ingress as the reminder; - an unstaffed slot tells nobody, and
onEmptyApproversdecides that case; - an out-of-office delegate gets only
approval.ooo_substituted; - no one-tap links (ADR-0043 keeps them in
remind()); approvals.mdxis rewritten, and it names Setup → Users → ⋯ → Set Manager.
- opening an approval step tells each concrete approver on the slate it opened on, once, on
- For apps: a flow with a
notifynode beside its approval node only to tell the approvers now tells them twice. Remove it. The census found no such flow in this repo's examples, hotcrm or hotclm. - Filed in this act: approvals: three texts still say opening a request notifies nobody, after
approval.requested(#22607): ADR-0043's v1 note, the automation skill's approval advice, and the QA checklist's known gap #22631. Three texts still say an opening notifies nobody: ADR-0043's v1 note and the automation skill's advice (both Tier H), and the QA checklist's known gap. It goes to triage.
Generated by Claude Code
- PR feat(plugin-approvals): opening an approval step tells each resolved approver (approval.requested) #22625 merged through the queue as
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.GET /api/v1/data/sys_inbox_messageas the Executive →total 0. After rung 2 opened on the Legal Head and the Finance Controller: bothtotal 0. No email either.POST /api/v1/approvals/requests/ID/remind→ the Executive's inboxtotal 1(approval.reminder) — the notification channel works.openNodeRequestpublishes no topic, while reminder / reassigned / escalated / sla_breached / returned / request_info / comment / ooo_* do; unchanged onmain25be876. 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.mdxcallssys_user.manager_ida column "which no product surface writes", but 17.7.0 ships Setup → Users → ⋯ → Set Manager (sys_user'sset_user_manageraction →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_issueson 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 inApproverType.describe(); the docs page kept it.Filed by the
repo:hotclmPM seat from a measured dev finding.