Skip to content

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

@objectstack-fleet

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/clone answers 404 {"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 main at 6bff748b (showcase, objectstack dev, stock composition), twice on two separate cold boots; objectui console built at the pinned dd3f7e1b.

Reproduction

  1. Boot the showcase (OS_PORT=PORT objectstack dev --ui --seed-admin -p PORT -d file:…) and sign in as the seeded admin.
  2. POST /api/v1/automation/showcase_urgent_task_alert/clone with body {"name":"qa_urgent_alert_clone","label":"QA urgent alert clone"}.
  3. Expected: a 2xx carrying the cloned flow and FLOW_CLONE_NOTICE. Actual: 404 ENDPOINT_NOT_FOUND; GET /api/v1/automation/qa_urgent_alert_clone → 404 RESOURCE_NOT_FOUND.
  4. Positive control on the same prefix, same session: POST /api/v1/automation/showcase_notify_owner/toggle {"enabled":true} → 200.
  5. UI: Setup → Packaged Automation → Clone on any flow row → legal name + label → Create clone → the dialog's alert reads "Not found"; the network trace shows POST /api/v1/automation/(flow)/clone → 404.

Mechanism (read from source at 6bff748b)

  • The domain arm exists: packages/runtime/src/domains/automation.ts, POST /:name/clone (body validation, 404 / 409 probes, cloneFlowDefinition, registerFlow).
  • The HTTP bridge never routes to it: registerAutomationRoutes in packages/runtime/src/dispatcher-plugin.ts mounts each /automation route explicitly (/:name, /:name/trigger, /:name/toggle, /:name/runs…), and has no /:name/clone mount — git log -S "/clone" -- packages/runtime/src/dispatcher-plugin.ts finds none ever. Hono's not-found answers before the dispatcher runs.
  • packages/runtime/src/domains/automation-flow-clone.test.ts stays green because it calls the domain handler directly; no dogfood test drives the clone over HTTP (the checklist's dispatcher-vs-hono-route trap).
  • The route is also absent from 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

  • Acceptance card 验收测试:包内元数据定制(ADR-0126)——流程/动作的停用+克隆 全链路人工确认 #12438 rows B1 and B2 cannot pass on any build that ships this bridge; the clone half of the §7.1 promise is unreachable.
  • Every refusal that prescribes the clone points at the dead door: the ledger FLOW_DISABLED message ("…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.mdx and content/docs/capabilities/integrations.mdx promise "clone your own".
  • The clone's own persistence question (FOLLOW-UPS §8a D18 — engine-only registration, restart survival, Studio reachability) could not be measured at all; it stays open behind this card.

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

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: automation — a packaged flow can be cloned to customize it | 缺项 (POST /automation/:name/clone exists 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) | P1

    Triage: 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 pass

    Triage: lands in packages/runtime/src/dispatcher-plugin.ts (registerAutomationRoutes) and route-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-route trap.

    Direction.

    • Mount POST /:name/clone beside /:name/toggle in registerAutomationRoutes, and add it to route-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.
    • 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.
  2. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): 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: registerAutomationRoutes only (the /:name/clone mount at both the plain and the environment-scoped base, and any other unmounted /automation arm the sweep finds).
    • packages/runtime/src/route-ledger.ts: the /automation rows 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/qa dogfood or a packages/runtime integration test, whichever already boots the dispatcher bridge.
    • .changeset/20676-*.md for @objectstack/runtime.
    • Added in the same round (patch round 1): packages/qa/dogfood/test/authz-conformance.matrix.ts (the docblock figure for route-ledger.ts's row count) and packages/qa/dogfood/test/authz-probe-blind-spot.census.ts (the route-ledger.ts probe 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.ts is 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 --tier at 0d9349fe: 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, main 0d9349fe.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 through dist/; 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 no major bump.",
    "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"
    ]
    }

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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: toggleFlow writes 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: toggleFlow writes the ADR-0126 §7.2 activation ledger first — one deployment-wide sys_metadata_activation row 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, which getFlowRuntimeStates() 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"
    }

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20779 at 99b3cfa4 (POST /automation/:name/clone mounted at both bases)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T07:00Z

    • Contract review of record: 5905918083 on the PR, CONTRACT_REVIEW_TIER, head 99b3cfa4, PASS. The seat spot-checked the reviewer's readings before posting.
    • Checklist:
      • Draft, base main, first line Fixes #20676, second line Clause-②: 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/runtime patch changeset; no content/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_state reads clean.
    • Patch round 1 (one round): the dogfood red on 0b1c343e was this PR's own. Two census pins read route-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.
    • Out-of-scope findings:
    • Next: land through the queue. At the merge, the Fixes closes this card, and the seat removes pm:dispatched in the same act.

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20779 → 96e724475c (POST /automation/:name/clone is served over HTTP)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T07:19Z


    Generated by Claude Code

  8. added a commit that references this issue on Oct 7, 2026
    96e7244
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:clipriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions