Skip to content

feat(spec)!: refuse at publish a save_notice that a suspending node reaches - #28838

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-9kerqb
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-9kerqb

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA

Fixes #28806

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28787 — skills/** does not teach save_notice yet; that open card carries the teaching, which should state this placement rule beside the trigger rule

维护者速读

  • BREAKING(收窄):记录触发的流程里,如果 save_notice 节点能沿连线从 approval、wait、screen 或 approval_revise 之后到达,现在发布时就会被拒绝,返回 INVALID_METADATA 422,位置在 nodes.<i>.type。以前它能通过发布,但每次审批决定都会让流程卡住(RESUME_FAILED)。
  • 拒绝文案会点名那个挂起节点,并告诉作者:把 save_notice 挪到它前面,或者在它后面改用 notify 节点。
  • 按席位裁定,只要有一条路径经过挂起节点能到达,就拒绝,包括回环;放在挂起之前的 save_notice 照常通过。
  • 挂起节点集合直接复用已有的 FLOW_PAUSE_CAPABLE_NODE_TYPES。subflow、map 只在子流程挂起时才挂起,发布时看不到,所以不计入,仍由运行时按名字拒绝。
  • 已发布的流程没有这种形状:showcase 唯一的 save_notice 前面没有挂起节点,HotCRM main 里没有 save_notice。
  • Metadata-protocol 实测为 narrowed(派单预计 none):按 AGENTS.md,作者能写的值被拒就是 narrowed,因此 spec 是 minor。详见下文和报告的 deviations。
  • 没有需要你拍板的事项。合并前由席位做契约复核。

What changes

Spec: the placement door refuses a save_notice that a suspending node reaches (packages/spec/src/automation/save-notice-placement.ts). After the trigger check passes, the door walks the flow's top-level edges. It takes every node that a node of an always-suspending type reaches from its successors, where that suspending node is itself reachable from the start. A save_notice in that set is refused:

A save_notice node tells the person who made the write that fired this run what the save set in motion, so it runs only while the run is still inside that write. This one is reached after approval node 'ask' suspends the run. A suspended run resumes after the write has already answered, so on that path the node is refused by name at run time, and after an approval the decision cannot finish its run. Move the save_notice ahead of 'ask', or tell the person after it with a notify node.

  • Which nodes suspend. The door reuses FLOW_PAUSE_CAPABLE_NODE_TYPES (screen, wait, approval, approval_revise), the list of types that pause on every execution. flow.zod.ts passes it in, so the door's module imports nothing back from flow.zod.ts.
    • subflow and map declare supportsPause: true but pause only when the child flow they name pauses. That is a different record, which the parse does not hold, so they are not counted. That is the same classification ruling D made for region bodies. A save_notice after one still meets the run-time refusal by name.
  • Reachability. Any path counts, per the seat ruling, including mixed graphs and loops. An edge after the approval that loops back to before the notice makes the notice reachable from the approval.
  • Region bodies. A region body cannot hold a pausing node (the region rule refuses one). So a save_notice inside a loop / parallel / try_catch body is judged by its top-level container, and the refusal is anchored at the nested node's own path.
  • Shape. ctx.addIssue({ code: 'custom' }) at nodes.<i>.type, which the metadata door answers as INVALID_METADATA 422, the same as the trigger case. objectstack validate, the save door and registerFlow refuse it alike.

Test re-anchored: plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts (#28786). Its flow placed the second save_notice right after the approval, which is the shape this PR refuses, so registerFlow now throws in its setup. The test keeps its purpose:

  • The approval is now asked in a subflow (start → save_notice → subflow(approval) → save_notice). The parse accepts that graph. The parent suspends with its child, is persisted in sys_automation_run, and resumes from the store when the decision completes the child.
  • The resumed save_notice still refuses by name. Through a subflow the decision answers the approvals' existing subflow-parent strand: resumed: true, with resumeFailure: { code: 'RESUME_FAILED', runId: <parent>, status: 'stranded' } and resumeError naming the refusal. It does not throw. That is existing behaviour, read and not changed.
  • A second case pins this PR's refusal at registerFlow on the original automation: save_notice follow-ups from #28251 — the array triggerType refusal wording, a persisted approval round-trip test, and the lint node-type lists #28786 graph.

Why

#28806: an AI writing "notify after approval" with save_notice produced a graph that published and then stranded every decision. The author now learns at publish time.

Verification

  • Red first. All 8 new refusal pins in spec/src/automation/save-notice-node.test.ts were red before the change (8 failed, 21 passed of 29) and green after (29/29). The 3 controls passed on both sides.
  • Door pins (metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts, automation: a save_notice placed after an approval, wait or screen node passes publish and then strands every decision with RESUME_FAILED #28806 block). In publish and draft mode they assert { code: 'INVALID_METADATA', status: 422 }, [['custom', 'nodes.2.type']], the sentence verbatim and nothing stored. The control (notice ahead of the approval) is stored.
  • Ablations, each red and then restored (spec source through scripts/ablation-replace.mjs, restore proven by blob == HEAD; dist by hand, restore proven by blob hash):
  • Shipped flows. examples/** and packages/dogfood: one save_notice (examples/app-showcase showcase_task_completed: start → script → save_notice → notify → end), with no suspending node. HotCRM origin/main (a598256): no save_notice.
  • Gates. dispatch-gates.mjs --commands derived 84; --ran reconciles 84 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.
  • Suites. Full vitest run of @objectstack/spec, @objectstack/metadata-protocol, @objectstack/plugin-approvals, @objectstack/service-automation, @objectstack/trigger-record-change and @objectstack/example-showcase: exit 0 each. The round-trip file's two cases run in about 0.3 s of test time.
  • Dogfood battery (owed by the (narrowing) arm), on head bb9723f5 under os-verify-lock.sh: closure build VERDICT command-exit 0; pnpm --filter @objectstack/dogfood test VERDICT command-exit 0, Test Files 380 passed and 3 skipped (383), Tests 3114 passed and 28 skipped (3142), no failure.
  • eslint --no-inline-config over packages/spec, packages/metadata-protocol and packages/plugins/plugin-approvals exits 0.

Acceptance notes

  • Metadata-protocol: narrowed, not the none the order expected. AGENTS.md defines "a value refused" as narrowed, and a flow graph an author could save before is now refused at the save door. The seat-1 contract review on fix(metadata-protocol)!: refuse an object whose datasource names no datasource at save and publish #26912 ruled the same way for a save door refusing what it stored before. Under ruling A the level follows the kind, so @objectstack/spec is minor. No other package's shipped files move.
  • The RESUME_FAILED / stranded-decision posture, the save-notice contract and the wire notice are unchanged. No ui/** and no hotcrm changes.
  • A plugin-registered pausing type is not in the list (the namespace is open), so a save_notice after one still meets the run-time refusal.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: no generated artifact moves (check-generated and the spec artifact gates pass on this head).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

A save_notice reachable along the flow's edges from a node that always
suspends the run (screen, wait, approval, approval_revise) is refused at
the flow parse, naming that node. On that path the write has already
answered, so the node can only refuse at run time and, after an approval,
the decision strands its run.

The persisted round-trip test now asks its approval through a subflow,
the shape the parse cannot see, and pins the parse refusal at registerFlow.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 6 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via FlowSchema (symbol, a top-level const))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v18/18-1.mdx (via FlowSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2a4c0034ed828e7b2203bcf120e484c52e3b6088 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0d38f4407e18802119b2cb28bac5dfba9f193332 — the merge of head bb9723f5d03561fa6aa5a69e11af7b4777b27712 into base 2a4c0034ed828e7b2203bcf120e484c52e3b6088, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0d38f4407e18802119b2cb28bac5dfba9f193332 && git checkout 0d38f4407e18802119b2cb28bac5dfba9f193332
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a4c0034ed828e7b2203bcf120e484c52e3b6088 bb9723f5d03561fa6aa5a69e11af7b4777b27712 && git checkout -B drift-repro 2a4c0034ed828e7b2203bcf120e484c52e3b6088 && git merge --no-ff bb9723f5d03561fa6aa5a69e11af7b4777b27712

node scripts/docs-audit/affected-docs.mjs --json 2a4c0034ed828e7b2203bcf120e484c52e3b6088

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2a4c0034ed828e7b2203bcf120e484c52e3b6088 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bb9723f5d03561fa6aa5a69e11af7b4777b27712
Local-runs: none
Scope: packages/spec/src/automation/save-notice-placement.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/automation/save-notice-node.test.ts, packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts, packages/plugins/plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts + packages/spec/src/automation/control-flow.zod.ts (collectFlowGraphs, the region path shape), packages/spec/src/automation/flow-region-pause-and-end.test.ts (the ruled FLOW_PAUSE_CAPABLE_NODE_TYPES set), packages/services/service-automation/src/builtin/save-notice-node.ts (the run-time refusal by name), packages/services/service-automation/src/builtin/subflow-node.ts and builtin/map-node.ts (the linked-runs pause the parse cannot see), packages/services/service-automation/src/run-resume.ts (resumeInternal, takeSubflowParentStrand), packages/plugins/plugin-approvals/src/approval-service.ts (resumeRecordedOutcome, decideAndResume), packages/plugins/plugin-approvals/src/subflow-hosted-approval-strand.test.ts and stranded-run-repairability.test.ts (where the two strand postures stay pinned), packages/types/src/stranded-decision.ts, packages/lint/src/lint-flow-patterns.ts, examples/app-showcase/src/automation/flows/index.ts, docs/qa/platform-checklist/areas/automation.json, hotcrm origin/main at a5982564. Net diff read against merge-base 663b7c08ee. Reviewed at 2026-11-01T23:28Z.

① Derived judgments

  • Accept-set, FlowSchema (published). A record-triggered flow with a save_notice node reachable along the top-level edges from a screen, wait, approval or approval_revise node that the start reaches is now refused with one custom issue at nodes.N.type (region member: nodes.N.config.body.nodes.M.type, anchored on the nested node, judged by its top-level container). Right. The set is FLOW_PAUSE_CAPABLE_NODE_TYPES handed in from flow.zod.ts, the ruled always-suspending set (ruling D, decision batch 🔗 Broken links detected in documentation #153 item 1), the same set the region rule judges by; save-notice-placement.ts imports nothing back from flow.zod.ts.
  • Refusal order. The suspension walk runs only after the trigger check passes, so a node earns one refusal, never two. Right.
  • Graph walk, read in the code. BFS with a seen set from the start, then from each reachable pausing node's successors (not itself), so loops terminate, a loop back from after the approval to before the notice counts, a self-loop on the pause node is harmless, an orphan pausing node that nothing reaches is not counted, and edge labels and conditions are ignored, so the reject branch counts too. Malformed members (no string id/type, an edge with no string source/target) are skipped, not read through, so the parse cannot throw out of safeParse. Region bodies: graph.path[1] is the raw top-level index, which matches collectFlowGraphs indexing over the RAW list at any depth (loop/parallel branch/try_catch slot alike), and a pausing node cannot stand in a region, so the container is the right proxy. Right. The refusal names pauses[0] in node-array order: one suspending node, by type and id, with the two prescriptions (move ahead of it, or notify after it). The text carries no tracker number. Right.
  • "Any path" over the card's "every path". A mixed graph (decision → approval → tell beside decision → tell) is refused although one of its paths works. The seat ruled this in the claim (6564282443) and the header states why: a static graph cannot rule the suspended path out, and the other path strands every decision. Right; ruled. The author's fix is two save_notice nodes, which the refusal's prescription implies.
  • Door shape. ctx.addIssue({ code: 'custom' }) is answered by the metadata door as INVALID_METADATA 422 in publish and draft mode, nothing stored, control stored: pinned in the 422 face inventory. registerFlow refuses the same graph (pinned in the round-trip file). Right.
  • Public surface. saveNoticePlacementRefusals gains a required parameter, but it is not an export of @objectstack/spec (absent from api-surface/automation.json and export-origins; its only caller is flow.zod.ts). No export, type or generated artifact moves; Check Changeset, Spec property liveness and Type Check · source gates are green on the head. Right.
  • Shipped flows. git grep save_notice over the head outside tests and prose finds one authored flow, showcase_task_completed (start → summarize(script) → tell_saver → notify → end, no suspending node); packages/dogfood carries none; hotcrm origin/main (a5982564) carries none. The QA checklist row (revision 12) names the same showcase node. Nothing shipped is refused.
  • Run-time contract. save-notice-node.ts, stranded-decision.ts, approval-service.ts and the engine are untouched by the diff (file list is six paths). The stranded-decision posture and RESUME_FAILED did not move, as the card forbids. Right.

② Semver level

  • .changeset/28806-save-notice-after-suspension-refused.md: '@objectstack/spec': minor, Clause-②: no (narrowing), Metadata-protocol: narrowed, BREAKING banner, ADR-0087 not-required (no-migration-prescription) marker. The PR body carries the same two lines and the feat(spec)! title.
  • Metadata-protocol: narrowed over the claim's expected none: right. AGENTS.md Post-Task Checklist 3: "a value refused or a requirement tightened is narrowed". A flow graph the save door stored before is refused now; that is a requirement tightened on what an author can write. The claim itself said "none (expected; the dev measures and declares)", so the dev measured and declared. The level follows the kind alone: a kind takes at least minor on a package the diff moves, so spec minor is the floor and the ceiling here (no export removed, no key re-shaped). Only @objectstack/spec ships source; the metadata-protocol and plugin-approvals paths are tests. Matches the diff.
  • Clause-②: no (narrowing): right. It is the arm the card and the claim ordered, (narrowing) is BREAKING per the checklist, the changeset carries the banner and its ADR-0087 marker, and the migration is the prescription in the refusal itself (no key is removed, so no FROM → TO mapping or tombstone is owed).

③ Boundary flags

④ Design judgment

  • Is the narrowing the right kind? The published spec on main already declared the refusal: the save_notice descriptor says "before any node that suspends the run", the placement header said the resumed leg "refuses the node by name there", and the executor refuses a run holding no channel. The capability (tell the writer after an approval) was never delivered on that leg; it refused at the worst moment, after a durable decision, as a 500-class strand. The PR makes the spec's own refusal real at the parse. It takes nothing away that was delivered. Right kind of narrowing.
  • The subflow and map hole, tried. start → save_notice → subflow(child with approval) → save_notice still parses, and the dev's own rewritten test proves what happens at run time: the parent suspends at the subflow node (correlation: subflow:CHILD_RUN_ID), the decision resumes the child, bubbleToParent resumes the parent from the store, the parent's save_notice refuses by name, the parent lands on the engine's stranded exit, and decide answers 200 with resumed: true plus resumeFailure: RESUME_FAILED naming the parent, instead of throwing. So on that shape every decision still strands the parent run, and the card's trap stays open for an author who routes the approval through a subflow or a map item. Judged against the tree: (a) FLOW_PAUSE_CAPABLE_NODE_TYPES excludes subflow and map by maintainer ruling D, and its docblock says adding either back "is a ruled decision, not a fix"; (b) refusing by type would also refuse start → subflow(synchronous child) → save_notice, which runs correctly today, so the parse-time door genuinely cannot see this; (c) the run-time refusal by name still meets it, and the strand is reported in the ruled [Decision] Must a resume failure reach the CALLER in a shape it can act on? — the family question triage reserved, now that its instance cards have each measured their own half and stopped at the same contract #16472 shape (resumeFailure on the answer, error log from bubbleToParent), not swallowed; (d) the card's direction and the claim's file surface scope the fix to the placement door. Not a must-fix. Should-fix, as a follow-up card, not this PR: the remaining trap is an AI-authoring trap of the same class the card names (Prime Directive 10 files a card for one), and the place that CAN see it is where the child record is in hand: registerFlow/activation (subflow-guards.ts already resolves subflowTargets there) or packages/lint over the whole app's flows. The dev recorded the hole in the acceptance notes and the changeset's "Not refused" line but filed no card. The seat files it on landing.
  • Plugin-registered pausing types. Not in the list (open namespace, ADR-0018); met at run time by name. Same boundary the region rule declares. Right as is.
  • Previously stored flows. A stored flow of the refused shape would now fail its parse on rehydration. None is shipped (①), save_notice landed in this release window, and the changeset says so under "Who moves". Accepted.
  • Nothing else breaks. Nested regions, parallel branches and try_catch slots resolve to the same top-level container; a dangling edge target walks to a dead end; the walk is O(P·(N+E)); the refusal text and both prescriptions are verbatim in the spec, door and registerFlow pins; check:doc-authoring's no-tracker-number rule holds in the runtime string.

Must-fix: none.
Should-fix: one, the follow-up card for the subflow/map-routed approval (④), owed by the seat at landing, not by this PR.

Check-runs on the head at review time: 26 success, 13 skipped, 0 failure, 27 still in progress (Test Core 1–8, Test UI 1–16, Type Check UI, Type Check · consumer gates, Type Check · workspace). The completed gates that decide this face are green: Check Changeset, Spec property liveness, Type Check · source gates, Lint · ESLint, Lint · PM dispatch-gates, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate. The seat lands only once the remainder is green.

Implemented-by: claude/project-thread-9kerqb
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28838 · 2026-11-01T23:40Z

Head-sha: bb9723f5d03561fa6aa5a69e11af7b4777b27712

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28806,与派单 pm-orders/server/order-28806.md 一致。文件 6 个;认领 6564282443 申报面之外有两处(下第 4 项)。
  2. 前提实测:dev 在 663b7c08(含 70943dd6)上红先、绿后,消融后还原;复用既有 FLOW_PAUSE_CAPABLE_NODE_TYPES,未另拼挂起节点表。
  3. 门禁:本 head check-runs 57 success、15 skipped(路径未命中)、0 失败或待跑。dev 本地 84 条派生门禁全部 exit 0,0 NOT-MEASURED;六个包测试通过;dogfood 380 文件、3114 测试通过。
  4. 接受的偏差:
  5. 范围外去向:复核 should-fix:经 subflow / map 子流程挂起后的 save_notice 仍能发布并卡住父运行(解析期按类型看不到,规则 D 不计这两类)。已立卡 automation: a save_notice after a subflow or map whose child suspends passes publish and strands every decision of the parent run #28840(p3,扩展既有注册门,不新增门禁),pm:blocked 待本 PR 合入。Skills-docs:见 skills(automation): teach the save notice — the save_notice flow node and ctx.notice in hooks (#28251) #28787。
  6. 落地条款:packages/spec/src 路径 + 收窄 ⇒ CONTRACT_REVIEW_TIER 复核 PASS(6565504502,Head-sha 与现 head 一致),Clause-②: no (narrowing),needs:contract-review 已摘。semver:spec minor,与 changeset 一致(Check Changeset 本 head success)。check-governed-merges --pr 28838:NOT governed,456 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

automation: a save_notice placed after an approval, wait or screen node passes publish and then strands every decision with RESUME_FAILED

1 participant