Repository navigation
runtime: POST /api/v1/automation/:name/clone is not mounted on the HTTP server — every flow clone, from the API and from the Setup packaged-automation page, answers 404 ENDPOINT_NOT_FOUND #20676
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: automation — a packaged flow can be cloned to customize it | 缺项 (
POST /automation/:name/cloneexists as a domain arm but is never mounted by the HTTP bridge, so every clone, from the API and from Setup's Clone dialog, answers 404) | P1Triage: first grade —
bug·priority:p1·domain:cli·area:workflow·pm:queue. Direction: mount the route, ledger it, and pin it over HTTP. Sweep the sibling arms in the same passTriage: lands in
packages/runtime/src/dispatcher-plugin.ts(registerAutomationRoutes) androute-ledger.ts⇒domain:cli(runtime). It is a finding of acceptance run #20674.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T17:59Z. ⛔ Not a claim, ⛔ not a dispatch.Why p1. Under ADR-0126 the packaged base is locked, and cloning (§7.1) is the prescribed way to customize a packaged flow. That door answers 404 for every caller and every body, so the customization path is broken end to end, and Setup's Clone dialog only ever says "Not found". Its unit test stays green because it calls the handler directly: the checklist's
dispatcher-vs-hono-routetrap.Direction.
- Mount
POST /:name/clonebeside/:name/toggleinregisterAutomationRoutes, and add it toroute-ledger.ts. - An HTTP-level pin (dogfood/verify boot, ⛔ not the handler directly):
- a legal clone answers 2xx with
FLOW_CLONE_NOTICE, and the clone reads back; - an illegal name answers 4xx;
- an anonymous caller is refused.
- a legal clone answers 2xx with
- Sweep, same pass. Diff
packages/runtime/src/domains/automation.ts's arms against the bridge's mounts, and mount or report any other unmounted arm. ⛔ It is a one-time sweep plus pins, not a new ledger-vs-live gate (新增门禁默认否). - Release: a shipped endpoint that 404s, so this is recommended before the next release.
- Mount
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 29, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1, dispatched first at the maintainer's direction in this session's chat (「20679 20676 优先」)
Session:session_01VvcEokUG1tvVxkceYfR5XB
Account:huangyiirene
Branch:claude/issue-20676-mount-flow-clone
Worktree:objectstack-issue-20676
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/runtime/src/dispatcher-plugin.ts:registerAutomationRoutesonly (the/:name/clonemount at both the plain and the environment-scoped base, and any other unmounted/automationarm the sweep finds).packages/runtime/src/route-ledger.ts: the/automationrows only.- An HTTP-level pin that boots the real composition (a dogfood or verify boot, ⛔ not the domain handler called directly). Its expected home is
packages/qadogfood or apackages/runtimeintegration test, whichever already boots the dispatcher bridge. .changeset/20676-*.mdfor@objectstack/runtime.- Added in the same round (patch round 1):
packages/qa/dogfood/test/authz-conformance.matrix.ts(the docblock figure forroute-ledger.ts's row count) andpackages/qa/dogfood/test/authz-probe-blind-spot.census.ts(theroute-ledger.tsprobe row's population and reach). Both census pins read the ledger's row count, which the new clone row moves. They are re-derived by the census file's own method, and the blind spot stays unchanged. - ⛔
packages/runtime/src/domains/automation.tsis NOT on this surface: the domain arm exists. If the sweep finds a defect in an arm itself, the dev reports it and leaves the file alone.
Stop on breach and explain in the report
Container & model:M,mode:subagent,model: default tier (opus)(dispatch-gates --tierat0d9349fe: no path-derived mandate; floor sonnet · default opus · ceiling fable).
Clause-②: no
Clause-② reading: the clone door is already declared, by ADR-0126 §7.1, the domain arm, the Setup page and the docs. Mounting it brings the server back to that declaration and widens no published contract. Any OTHER arm the sweep mounts gets the same test in the report: declared already ⇒no; a door nobody declared ⇒ the dev reports it and does not mount it.
Thread-read: 5895748555
Serial constraints cleared:No open PR touches dispatcher-plugin.ts or route-ledger.ts (file lists of all 10 open PRs read in this act). #20679 (this seat, same round) edits packages/runtime/src/domains/automation.ts, a disjoint file. #20726 (domain:services, claim 5903568667) edits the toggle description in automation.ts and service-automation engine.ts, both disjoint.Taken 2026-09-30T03:50Z,
main0d9349fe.
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20676,
"status": "done",
"branch": "claude/issue-20676-mount-flow-clone",
"pr": "#20779",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "The premise holds on origin/main f284ab2. registerAutomationRoutes (packages/runtime/src/dispatcher-plugin.ts) mounted no /:name/clone: git grep clone had 0 hits, while the control /:name/toggle was at line 1569. POST ${base}/automation/:name/clone is now mounted beside toggle, so the plain and the environment-scoped base both get it from one line. route-ledger.ts gains a server-only POST /automation/:name/clone row, and the census moved 81 to 82. There is no client.automation.clone and the gap ratchet is 0, so the row's rationale mirrors the POST /actions/_activation row: Setup calls the API directly. The HTTP pins are a dogfood bootStack(CRM, automation) test and a runtime plugin-hono-server integration test with scoping on, for the scoped twin. The sweep compared every handleAutomationRequest arm with every bridge mount, and clone was the only unmounted arm; no undeclared door was found. automation.ts is untouched. The assumption-5 check is only half true: the FLOW_DISABLED text and the Setup copy now reach a live door, but the 'edit in Studio' half of integrations.mdx is still false. The clone is engine-only (D18, measured below).",
"tests": "HEAD 0b1c343 unless noted. (1) Pins before the merge at 00b5b7c: runtime src/dispatcher-plugin.automation-clone-mount.integration.test.ts 4/4; dogfood test/automation-flow-clone-door.dogfood.test.ts 5/5 (anonymous 401 UNAUTHENTICATED; legal clone 200 with data.notice === FLOW_CLONE_NOTICE, status draft, and GET read-back 200; illegal name 400 VALIDATION_FAILED; missing name 400; taken name 409 RESOURCE_CONFLICT). (2) ABLATION, with the fix committed first. scripts/ablation-replace.mjs changed anchor 'automation/:name/clone' to 'automation/:name/clone-ablated-20676' (anchor 1 to 0, blob b6dc62c9 to f8f968e2). Runtime was rebuilt, and ablation-dist-preflight found the marker in dist/index.js and dist/index.cjs. Runtime pin RED 2 failed / 2 passed: both clone cases returned 404 {code:ENDPOINT_NOT_FOUND}, and both controls stayed green. Dogfood pin RED 5/5, all 404 ENDPOINT_NOT_FOUND, the card's symptom. route-ledger-live-mount-parity RED 2/8: 'POST /automation/:name/clone — LEDGERED BUT NOT MOUNTED' and the ablated mount unledgered. RESTORE: ablation-replace blob == HEAD b6dc62c9 with git diff HEAD empty; the shell belt trap hash-compared disk == HEAD; whole-tree porcelain empty. Runtime rebuilt; preflight --absent: marker absent from all 6 built files. Re-run: runtime 4/4, dogfood 21/21 (clone pin + parity + automation-toggle-tenant-scope). (3) Full runtime suite 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2' at d663c2f: Test Files 291 passed, Tests 4204 passed, 1 skipped. The delta from d663c2f to 0b1c343 is one string in the ledger note. (4) At 0b1c343: runtime route-ledger.conformance, automation-api-contract-mounts, the clone-mount pin and domains/automation-flow-clone, 4 files / 31 passed; dogfood clone pin + parity, 2 files / 13 passed. (5) pnpm --filter @objectstack/runtime typecheck (tsc --noEmit plus check:test-typecheck 'OK ... held') and pnpm --filter @objectstack/dogfood typecheck: both exit 0 at 0b1c343. tsc --listFiles contains each new test file (1 hit each). (6) Lint as a proven narrowing, not pnpm lint. eslint --no-inline-config --format json on the 4 touched TS files returned 4 results, 0 errors, 0 warnings. Population comes from eslint's own config: --print-config gives 6/6/5/5 rules, so no file is ignored. Invariance: eslint.config.mjs has no parserOptions.project and no projectService, and its only fs reads are scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, both untouched. Full-tree pnpm lint is left to CI. (7) One-off D18 measurement at 00b5b7c; the throwaway file was deleted and never committed. Clone 200, then GET /meta/flow/CLONE 404 while the source returned 200, then a cold boot on the same databaseFile gave GET /automation/CLONE 404 while the source returned 200.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #20779; relay run 36671230607); (2) label-write --assign → POST /repos//issues/20779/assignees [huangyiirene] (relay run 36671281560; read back matches); (3) this os-dev-report comment → POST /repos//issues/20676/comments. git push is not counted (not REST). Zero label writes: the dispatch named no PR labels, and skip-changeset does not apply because @objectstack/runtime publishes.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured over HTTP on bootStack(CRM, automation, databaseFile) at 00b5b7c. POST /api/v1/automation/crm_convert_lead_wizard/clone answers 200. GET /api/v1/meta/flow/CLONE then answers 404 RESOURCE_NOT_FOUND while the source answers 200. After a cold boot on the same DB file, GET /api/v1/automation/CLONE answers 404 while the source answers 200. · evidence: the clone arm registers only through automationService.registerFlow (domains/automation.ts clone arm; flow-clone.ts), with no sys_metadata write. The clone is therefore invisible to Studio's /meta surface and lost on restart. Contract text: content/docs/capabilities/integrations.mdx says 'switch it off and clone your own to edit in Studio', and ADR-0126 §7.1 says 'an ordinary org/install-owned flow'. This is FOLLOW-UPS.md §8a D18, whose 'restart survival unknown' is now measured. Seam: spec:ADR-0126 §7.1 clone → runtime:domains/automation.ts clone arm (registerFlow only) | renderer:objectui StudioDesignSurface / PackagedAutomationPage · dedupe words: flow clone persistence; clone engine-only registerFlow; clone lost on restart; clone not in meta flow; D18",
"carrier: 承接者:无 · noted, not filed. packages/qa/dogfood/test/authz-conformance.matrix.ts, the /automation enforcement prose, still says 'four gated flow writes'. isFlowAuthoringWrite has gated clone as a fifth since #12156. This is prose drift only; the gate is pinned in domains/automation-flow-clone.test.ts. It is in the PR's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed. docs/qa/platform-checklist/FOLLOW-UPS.md §8a D22 ('POST /automation/:name/clone is unledgered') becomes stale when PR #20779 lands. The file is outside this card's surface and was left untouched."
],
"gates": {
"dispatch-gates --ran (67 derived at 0b1c343, exit-coded record)": "✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3).",
"all 67 derived commands at 0b1c343": "exit 0 each (first pass at d663c2f: 64 exit 0; check:doc-authoring exit 1 = real, fixed in 0b1c343; check-plugin-teardown-shape --self-test exit 3 = shallow clone, fixture commit 621a487 fetched; check:dual-build-cjs-loads exit 3 = 8 packages had no dist, built)",
"pnpm check:route-ledger-census": "✓ check:route-ledger-census — all 1 census sentence(s) match their arrays. (reads 82, array holds 82)",
"pnpm check:doc-authoring": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 794 pinned site(s) across 227 file(s), 92071 string(s) read in 1260 parsed source(s), no growth, no burn-down unrecorded.",
"pnpm check:nul-bytes": "check-nul-bytes: OK (scanned 9395 text file(s) -- 9395 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
"pnpm check:test-source-alias": "check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep throughdist/; 51 published subpath(s) resolved through every alias table.",
"pnpm check:cross-package-test-inputs": "OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob",
"pnpm check:dual-build-cjs-loads": "✓ check:dual-build-cjs-loads — 105 published require entry point(s) across 66 package(s) load; 701 emitted CommonJS file(s) parse",
"pnpm check:type-check-debt": "check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 84.7s, 53 raw tsc error(s) total, none above its recorded number.",
"pnpm check:type-check-coverage": "check-type-check-coverage: OK — 76/80 workspace packages type-checked (plus the root), 4 in the DEBT ledger",
"pnpm check:engine-double-contract": "check-engine-double-contract: OK — 904 pinned, 129 in the DEBT ledger, 3 exempt.",
"node scripts/check-adr-0087-registration.mjs --base origin/main": "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-changeset-no-major.mjs --base origin/main": "✓ This diff introduces nomajorbump.",
"node scripts/check-plugin-teardown-shape.mjs --self-test": "✓ check-plugin-teardown-shape self-test: 48 cases pass",
"pnpm --filter @objectstack/runtime typecheck && pnpm --filter @objectstack/dogfood typecheck": "os-verify-lock: VERDICT command-exit 0 (at 0b1c343)",
"pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2": "Test Files 291 passed (291) · Tests 4204 passed | 1 skipped (4205) · VERDICT command-exit 0 (at d663c2f)",
"pnpm lint": "NOT RUN full-tree: a proven narrowing instead (see tests (6)); the full run is left to CI"
},
"deviations": [
"Base f284ab2, not the dispatch tip 0d9349f: origin/main had moved by 2 unrelated commits (ADR-0053 doc and service-analytics) when the worktree was cut.",
"The HTTP pin is TWO files. The dogfood bootStack pin covers the plain base. bootStack mounts the dispatcher without project scoping, so the environment-scoped twin is pinned in a runtime plugin-hono-server integration test instead. Both files are inside the claim's pin surface.",
"Merged origin/main once before the PR (d663c2f, one spec commit, no overlap). main later moved 3 more commits (to 91e8fa1: plugin-dev and trigger-schedule docs re-anchors plus a release script, no overlap with runtime, dogfood or spec). I did not re-merge, because the final gate run was reading the tree.",
"The full runtime suite ran at d663c2f, not at the final head 0b1c343. The only delta is one string in the ledger note (a tracker id removed for check:doc-authoring); every ledger-reading suite and the pins re-ran at 0b1c343.",
"The first typecheck attempt was not a measurement: it overlapped my own gate runner's check:type-check-debt re-measure, which was rebuilding dist in the same worktree, and it produced TS7016. It was re-run clean at 0b1c343.",
"Fetched commit 621a487 (--depth=1) into the shared object store so check-plugin-teardown-shape --self-test could read its pinned fixture on this shallow clone.",
"pnpm lint was not run full-tree; the proven narrowing (three pieces of evidence) is recorded instead.",
"Commit trailers use the dispatch's model-free pair, and the PR footer uses the AGENTS.md session-URL form, not the harness reminder's model-named trailer and robot footer. AGENTS.md takes precedence.",
"A throwaway dogfood test file was used for the one-off D18 measurement, then deleted and never committed."
],
"files_changed": [
"packages/runtime/src/dispatcher-plugin.ts",
"packages/runtime/src/route-ledger.ts",
"packages/runtime/src/dispatcher-plugin.automation-clone-mount.integration.test.ts",
"packages/qa/dogfood/test/automation-flow-clone-door.dogfood.test.ts",
".changeset/20676-mount-flow-clone.md"
]
}objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20676,
"status": "done",
"branch": "claude/issue-20676-mount-flow-clone",
"pr": "#20779",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Patch round 1 (plus the two items folded into it). The new head is 99b3cfa, pushed and equal to origin. (1) The Dogfood Regression Gate red is fixed. The new clone ledger row moved route-ledger.ts from 81 to 82 rows. I re-derived the census by its own method: population and reach move together, 81/81 to 82/82; the blind spot stays 0; keys stay 21. The row, its controls, and the census prose at line 109 now say 82, and the matrix docblock now says (82 rows / 21 domains). (2) Bounded in-place fix in authz-conformance.matrix.ts: the /automation enforcement prose said 'four gated flow writes' and now names five, clone included. (3) The positive control in the scoped-mount pin now drives the /:name/trigger execution door instead of /:name/toggle. It has the same shape and the same domain-wide anonymous floor, and it sits outside every authoring gate, so it holds in either landing order with PR #20780. (4) Bounded in-place fix in route-ledger.ts: the toggle row's note claimed toggleFlow writes an in-process map; it now states the activation-ledger write. main was merged twice this round, to be554a8 and then 99b3cfa. Recorded, and it changes nothing here: the maintainer's #20761 ruling (a clone is written server-side as a tenant-authored copy) is #20761's stage 2, and this PR's mount stays as it is.",
"census_before_after": {
"packages/runtime/src/route-ledger.ts probe row": "population 81 → 82 · reachable 81 → 82 · blindSpot 0 → 0 · keys 21 → 21 · controls route 81 → 82, domain 81 → 82, RouteLedgerEntry 2 → 2",
"before_reading": "origin/main 9ad6544, route-ledger.ts blob: occurrence counts route 81, domain 81, distinct domain values 21. That matches the recorded 81/81, which was green on main.",
"after_reading": "deriveProbeFileCensus() at be554a8 (before the edit): population 82, reachable 82, controls 82/82/2, and 1 mismatching row (this one). At 5518c80, ab7d501 and 99b3cfa after the edit: 82/82, 0 mismatching rows. BLIND_SPOT_TOTAL_STATIC 67 / RUNTIME 72 unchanged. Every other census row unchanged.",
"matrix_docblock": "(81 rows / 21 domains) → (82 rows / 21 domains)",
"census_prose_line_109": "81 rows over 21 domains → 82 rows over 21 domains"
},
"pr_body_carry_for_seat": [
"Acceptance notes: replace the 'authz-conformance.matrix.ts ... still names four' line with this one. Fixed here (bounded in-place fix, patch round 1): the /automation enforcement prose in packages/qa/dogfood/test/authz-conformance.matrix.ts said 'four gated flow writes' and now names five, adding the ADR-0126 §7.1 clone POST /:name/clone. Evidence: isFlowAuthoringWrite in packages/runtime/src/domains/automation.ts returns true for exactly five route shapes: POST / (parts.length 0), POST /:name/toggle, POST /:name/clone, PUT /:name and DELETE /:name (parts.length 1).",
"Acceptance notes, new line. Fixed here (bounded in-place fix, patch round 1): the note on route-ledger.ts's POST /automation/:name/toggle row. BEFORE: 'The enabled bit is not a ROW, so no organization wall scopes it:toggleFlowwrites an in-process map keyed by flow name only,getFlowRuntimeStates()reads it with no caller and no organization, and the automation service is ONE instance per environment'. AFTER: 'No organization wall scopes the enabled bit:toggleFlowwrites the ADR-0126 §7.2 activation ledger first — one deployment-widesys_metadata_activationrow per flow, keyed by(metadata_type, name), carrying the flow's package id and no organization column — and only then updates the engine's in-process projection, whichgetFlowRuntimeStates()reads with no caller and no organization; the automation service is ONE instance per environment'. Evidence: service-automation engine.ts toggleFlow calls flowActivationStore.setActive before flowLedgerDisabled is updated; core metadata-activation-store.ts has columns metadata_type / name / package_id / active and matches on (metadata_type, name). 'Packaged flows only' was not added: that is #20780's behaviour.",
"Pins section: the scoped-mount pin's positive control is now POST /:name/trigger (it was /:name/toggle).",
"Verification section, new head 99b3cfa: runtime pins 4 files / 31 tests; runtime and dogfood typecheck green; full dogfood package 141 files passed and 1 skipped (142), 1155 tests passed and 3 skipped; dispatch-gates --ran reconciles 67 of 67 with 0 NOT-MEASURED (a derived zero); check:route-ledger-census reads 82 and the array holds 82."
],
"item1_choice": "I chose a different mounted door, POST /automation/:name/trigger, over toggling a packaged flow. Reasons: (a) This composition (plugin-hono-server plus the dispatcher, no service plugins) has no automation service and no packages, so 'a packaged flow' cannot be expressed. The flow name there has no provenance. (b) I measured #20780's diff. It moves the customer-flow refusal into the engine's toggleFlow and changes only docs in the domain arm, so the anonymous floor is still the domain's first statement. The old control was therefore already answered 401 in either order, but it read a door whose semantics are in flight. (c) Trigger has the identical two-segment POST shape and the same domain-wide floor, sits outside every authoring gate, and #20780 does not touch it. The control still proves that the dispatcher answered: 401 UNAUTHENTICATED is minted only inside dispatch(), and the negative control still gets the transport's 404. No assertion on #20780's behaviour was added.",
"tests": "At head 99b3cfa unless noted. (1) Runtime: 'vitest run --project local --maxWorkers=2' over src/dispatcher-plugin.automation-clone-mount.integration.test.ts, src/route-ledger.conformance.test.ts, src/automation-api-contract-mounts.test.ts and src/domains/automation-flow-clone.test.ts gave Test Files 4 passed (4), Tests 31 passed (31). Then 'pnpm --filter @objectstack/runtime typecheck' (check:test-typecheck: OK ... held) and 'pnpm --filter @objectstack/dogfood typecheck', both exit 0, under one lock hold with VERDICT command-exit 0. (2) Full dogfood package: 'pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2', which is the test script's 'vitest run' via exec, with no bare '--'. Result: Test Files 141 passed | 1 skipped (142), Tests 1155 passed | 3 skipped (1158), Duration 875.40s, os-verify-lock VERDICT command-exit 0. (3) The two formerly red files run verbosely: test/authz-conformance.test.ts and test/authz-probe-blind-spot.test.ts, 2 files / 88 tests passed. Passing: 'every figure the docblock states equals what its ledger holds TODAY', 'the anchor is the PATH, so the dated drift note is NOT pinned as a present-tense claim', 'packages/runtime/src/route-ledger.ts — population, reach and blind spot are unchanged', and 'packages/runtime/src/route-ledger.ts — every positive control is still present in THAT file'. (4) SHARD-3 FILE: test/authz-probe-blind-spot.test.ts. How it was read: 'vitest list --shard=k/3 --filesOnly' ignores --shard (it returned 142 files for every k), so that reading measured nothing. I emulated vitest 4.1.11's own BaseSequencer.shard instead (sha1 of the root-relative path, sorted, calculateShardRange) over the 142 files; no dogfood test file was added or removed by these commits. It puts authz-conformance.test.ts and route-ledger-live-mount-parity on 1/3, automation-flow-clone-door on 2/3, and authz-probe-blind-spot.test.ts on 3/3. That matches the CI reading (red on shards 1 and 3, one file each), which serves as the control. (5) Census derivation, with the numbers in census_before_after. (6) check:route-ledger-census: 'OK packages/runtime/src/route-ledger.ts :: ROUTE_LEDGER — the census sentence above ROUTE_LEDGER (reads 82, array holds 82)'. (7) Before any edit, both census pins were measured red by the census's own derivation at be554a8 (derived 82 vs recorded 81), which reproduces the CI failure. All of them are green after the edit.",
"mcp_calls": "0 — no MCP GitHub tool called this round",
"api_writes": "1 this round, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches as objectstack-fleet[bot]): this os-dev-report comment, POST /repos//issues/20676/comments. The PR body was not edited, per the write-once rule; the lines to carry are in pr_body_carry_for_seat. git push is not counted: 4 pushes (be554a8 merge, 5518c80, ab7d501, 99b3cfa merge). Round 0's 3 writes are unchanged.",
"open_questions": [],
"out_of_scope_findings": [
"Unchanged from round 0, still open for filing: class: a · reach: public door, measured over HTTP (clone 200, then GET /api/v1/meta/flow/CLONE 404 while the source answers 200; after a cold boot on the same DB file, GET /api/v1/automation/CLONE 404 while the source answers 200) · evidence: the clone arm registers only through registerFlow and writes no sys_metadata row (FOLLOW-UPS §8a D18). The maintainer's #20761 ruling (5904938166, a server-side tenant-authored copy, #20761 stage 2) is the carrier, so if #20761 already covers it, fold it there. · dedupe words: flow clone persistence; clone engine-only registerFlow; clone lost on restart; clone not in meta flow; D18",
"carrier: 承接者:无 · noted, not filed. docs/qa/platform-checklist/FOLLOW-UPS.md §8a D22 ('POST /automation/:name/clone is unledgered') becomes stale when PR #20779 lands. The file is outside this card's surface and was left untouched."
],
"gates": {
"dispatch-gates --ran (67 derived at 99b3cfa, exit-coded record)": "✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3).",
"all 67 derived commands at 99b3cfa": "exit 0 each",
"pnpm check:route-ledger-census": "OK packages/runtime/src/route-ledger.ts :: ROUTE_LEDGER — the census sentence above ROUTE_LEDGER (reads 82, array holds 82)",
"pnpm check:doc-authoring": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 794 pinned site(s) across 227 file(s), 92074 string(s) read in 1260 parsed source(s), no growth, no burn-down unrecorded.",
"pnpm check:nul-bytes": "check-nul-bytes: OK (scanned 9406 text file(s) -- 9406 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
"pnpm check:dual-build-cjs-loads": "✓ check:dual-build-cjs-loads — 105 published require entry point(s) across 66 package(s) load; 701 emitted CommonJS file(s) parse",
"pnpm check:type-check-debt": "check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 101.3s, 53 raw tsc error(s) total, none above its recorded number.",
"node scripts/check-plugin-teardown-shape.mjs --self-test": "✓ check-plugin-teardown-shape self-test: 48 cases pass",
"pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2": "Test Files 141 passed | 1 skipped (142) · Tests 1155 passed | 3 skipped (1158) · VERDICT command-exit 0",
"runtime pins + typecheck (one lock hold)": "Test Files 4 passed (4) · Tests 31 passed (31) · both typechecks exit 0 · VERDICT command-exit 0",
"pnpm lint": "NOT RUN full-tree: the proven narrowing from round 0 stands. This round touched 2 more TS files (the census and matrix, both data/prose edits in files already inside the eslint population) and 2 already-linted files; the full-tree run is left to CI."
},
"deviations": [
"Worktree recreated on the existing branch after a fetch (base recorded as 0b1c343). No new branch or PR.",
"main was merged twice this round: be554a8 (7 commits) at the start, and 99b3cfa (2 commits: an ADR doc and spec liveness JSON) before the final-head runs. main has since moved 2 more commits, to 7a09eee (objectql no-operator filter door, a skills doc), with no overlap with this PR. I did not re-merge, because that would reopen every final-head reading; CI and the queue rebuild against current main.",
"I stopped my own first full-dogfood run (at 5518c80, before the two folded-in items arrived) with kill -TERM on its recorded process group 32335, which I started with setsid. The run had to be repeated at the final head anyway, and stopping it released the shared lock. The group read zero processes afterward. The first kill attempt was refused by guard-process-kill.sh because a grep shared its command line; the kill was re-issued alone.",
"lint: eslint was not re-run on the 4 TS files this round touched (a test control change and prose in the ledger, census and matrix). The round-0 proven narrowing covered 2 of them; the full-tree run is left to CI.",
"Shard membership comes from an emulation of vitest's own shard function, because 'vitest list --shard --filesOnly' ignores --shard. The CI shard reading serves as its control.",
"The census prose at line 109 was updated along with the row: it is a maintained present-tense figure beside the REST one (83), even though no pin reads it."
],
"files_changed": [
"packages/qa/dogfood/test/authz-probe-blind-spot.census.ts",
"packages/qa/dogfood/test/authz-conformance.matrix.ts",
"packages/runtime/src/dispatcher-plugin.automation-clone-mount.integration.test.ts",
"packages/runtime/src/route-ledger.ts"
],
"head": "99b3cfa42a64f82aee7caa3bf80ad50e09624b79"
}objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT: PR #20779 at
99b3cfa4(POST /automation/:name/clonemounted at both bases)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T07:00Z- Contract review of record:
5905918083on the PR,CONTRACT_REVIEW_TIER, head99b3cfa4, PASS. The seat spot-checked the reviewer's readings before posting. - Checklist:
- Draft, base
main, first lineFixes #20676, second lineClause-②: no, and no other closing keyword in the body (scanned). - 7 files, +318 / −8, all inside the claim's surface as amended in patch round 1: the two dogfood census files were added there before they were edited.
- A
@objectstack/runtimepatchchangeset; nocontent/docs/releases/edit. check-governed-merges --pr 20779: NOT governed.- 35 check runs on the head, latest per name: 32 success and 3 skipped.
mergeable_statereadsclean.
- Draft, base
- Patch round 1 (one round): the dogfood red on
0b1c343ewas this PR's own. Two census pins readroute-ledger.ts's row count. They were re-derived by the census file's own method: population and reach move together, 81/81 to 82/82, and the blind spot stays 0. It is not a ratchet bump.- The round also carried two bounded in-place fixes: the matrix's "five gated flow writes", and the toggle row's activation-ledger note.
- It also moved the scoped pin's control to
/:name/trigger, so the pin holds whichever of this PR and PR fix(service-automation)!: the toggle door switches packaged flows only; a customer flow is refused, naming its status switch (#20726) #20780 lands first.
- Out-of-scope findings:
- D18 (the clone is engine-only: it is lost on a cold boot and absent from
/meta, measured over HTTP): folded into automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 as evidence for its stage 2 (5905846738). The maintainer's ruling there pins "a clone of a shipped flow is saved as a tenant row". Not a separate card. - FOLLOW-UPS.md §8a D22 goes stale when this lands: Acceptance notes, no carrier. Noted, not filed.
- D18 (the clone is engine-only: it is lost on a cold boot and absent from
- Next: land through the queue. At the merge, the
Fixescloses this card, and the seat removespm:dispatchedin the same act.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded: PR #20779 →
96e724475c(POST /automation/:name/cloneis served over HTTP)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T07:19Z- Landing reading:
96e724475c476d018c2b6d13dcd117ade14d0aa0is onorigin/mainas a single-parent squash. Content:packages/runtime/src/dispatcher-plugin.tsonorigin/maincarriesautomation/:name/cloneon 1 line; the controlautomation/:name/togglealso reads 1 line. - Closure: the merge did not close this card by itself, although
closed_by_pull_requestsnames PR fix(runtime): mount POST /automation/:name/clone on the dispatcher bridge, at both bases #20779 (MERGED). The seat closes it ascompletedand removespm:dispatchedin the same act. - Carried on:
- the clone's persistence (FOLLOW-UPS §8a D18) rides automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761's stage 2 (
5905846738); - the "packaged flows only" wording on the toggle row lands with PR fix(service-automation)!: the toggle door switches packaged flows only; a customer flow is refused, naming its status switch (#20726) #20780 if it merges second.
- the clone's persistence (FOLLOW-UPS §8a D18) rides automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761's stage 2 (
Generated by Claude Code
- Landing reading:
- added a commit that references this issue
on Oct 7, 2026
QA-source: #20674 · automation.packaged-flow-clone-contract · c1, c4
QA-source: #20674 · automation.setup-packaged-automation-board · c7
What happens
The ADR-0126 §7.1 flow clone cannot be performed on a running server.
POST /api/v1/automation/:name/cloneanswers404 {"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}for every body (legal, empty, missing name, missing label, same name, illegal name), for every source (existing or not) and for every caller (admin and anonymous alike). The Setup → Packaged Automation page's Clone dialog therefore shows "Not found" for every submission and never shows the post-clone notice.Measured on
mainat6bff748b(showcase,objectstack dev, stock composition), twice on two separate cold boots; objectui console built at the pinneddd3f7e1b.Reproduction
OS_PORT=PORT objectstack dev --ui --seed-admin -p PORT -d file:…) and sign in as the seeded admin.POST /api/v1/automation/showcase_urgent_task_alert/clonewith body{"name":"qa_urgent_alert_clone","label":"QA urgent alert clone"}.FLOW_CLONE_NOTICE. Actual: 404ENDPOINT_NOT_FOUND;GET /api/v1/automation/qa_urgent_alert_clone→ 404RESOURCE_NOT_FOUND.POST /api/v1/automation/showcase_notify_owner/toggle {"enabled":true}→ 200.POST /api/v1/automation/(flow)/clone→ 404.Mechanism (read from source at
6bff748b)packages/runtime/src/domains/automation.ts,POST /:name/clone(body validation, 404 / 409 probes,cloneFlowDefinition,registerFlow).registerAutomationRoutesinpackages/runtime/src/dispatcher-plugin.tsmounts each/automationroute explicitly (/:name,/:name/trigger,/:name/toggle,/:name/runs…), and has no/:name/clonemount —git log -S "/clone" -- packages/runtime/src/dispatcher-plugin.tsfinds none ever. Hono's not-found answers before the dispatcher runs.packages/runtime/src/domains/automation-flow-clone.test.tsstays green because it calls the domain handler directly; no dogfood test drives the clone over HTTP (the checklist'sdispatcher-vs-hono-routetrap).packages/runtime/src/route-ledger.ts(docs/qa/platform-checklist/FOLLOW-UPS.md §8a D22), so no ledger-vs-live parity check could flag the missing mount.Consequences
FLOW_DISABLEDmessage ("…or run a clone of it under a new name.") and the Setup page copy ("clone a flow under a new name to customize it").content/docs/build-without-code.mdxandcontent/docs/capabilities/integrations.mdxpromise "clone your own".Expected
The live server serves
POST /api/v1/automation/:name/clone(and its environment-scoped twin) through the dispatcher, with the arms the domain handler already implements, and a live HTTP pin covers it.Full evidence chain: #20674 (F-1).
Generated by Claude Code