@tea-agent/loop-agent 0.42.0-next.2 → 0.42.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +242 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +942 -187
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +173 -7
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +30 -19
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +677 -492
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +433 -126
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -124,6 +124,13 @@ export function isEphemeralToolCachePath(filePath) {
|
|
|
124
124
|
if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
|
+
// vite/vitest runtime caches live under node_modules/.vite (e.g.
|
|
128
|
+
// node_modules/.vite/vitest/results.json). node_modules is the gitignored
|
|
129
|
+
// dependency tree — tool cache writes there are never a writer's product
|
|
130
|
+
// change and must not trip the write guard.
|
|
131
|
+
if (normalized.startsWith("node_modules/.vite/")) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
127
134
|
if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
|
|
128
135
|
return true;
|
|
129
136
|
if (/\.(?:pyc|pyo)$/.test(normalized))
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
|
|
4
|
+
import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
|
|
5
|
+
import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
|
|
6
|
+
import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
|
|
3
7
|
import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
|
|
4
8
|
export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
|
|
5
9
|
function operationFile(appData, operationId) {
|
|
@@ -15,16 +19,21 @@ export function computeActionPayloadHash(action, actionParams) {
|
|
|
15
19
|
export function newOperationId() {
|
|
16
20
|
return `op_${randomBytes(12).toString("hex")}`;
|
|
17
21
|
}
|
|
22
|
+
const sharedWriteChains = new Map();
|
|
18
23
|
export class OperationStore {
|
|
19
24
|
appData;
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
* (Windows EPERM) or last-write-wins lost patches.
|
|
24
|
-
*/
|
|
25
|
-
writeChains = new Map();
|
|
25
|
+
/** Shared per-store-root, per-key write lock. Multiple OperationStore instances
|
|
26
|
+
* in one worker process must observe the same CAS critical section. */
|
|
27
|
+
writeChains;
|
|
26
28
|
constructor(appData) {
|
|
27
29
|
this.appData = appData;
|
|
30
|
+
const root = path.resolve(appData.operations);
|
|
31
|
+
let chains = sharedWriteChains.get(root);
|
|
32
|
+
if (!chains) {
|
|
33
|
+
chains = new Map();
|
|
34
|
+
sharedWriteChains.set(root, chains);
|
|
35
|
+
}
|
|
36
|
+
this.writeChains = chains;
|
|
28
37
|
}
|
|
29
38
|
serialize(key, op) {
|
|
30
39
|
const prev = this.writeChains.get(key) ?? Promise.resolve();
|
|
@@ -66,6 +75,7 @@ export class OperationStore {
|
|
|
66
75
|
payloadHash,
|
|
67
76
|
state: "queued",
|
|
68
77
|
createdAt: now,
|
|
78
|
+
revision: 1,
|
|
69
79
|
taskId: input.taskId,
|
|
70
80
|
cliArgs: input.cliArgs,
|
|
71
81
|
...(input.externalCommand
|
|
@@ -77,11 +87,61 @@ export class OperationStore {
|
|
|
77
87
|
return { kind: "created", operation };
|
|
78
88
|
});
|
|
79
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Idempotently materialize a split child/integration operation with a
|
|
92
|
+
* caller-specified deterministic operationId (from the split manifest).
|
|
93
|
+
* Unlike create() (random op_<hex> id) and save/update (no create), this is
|
|
94
|
+
* the durable cross-restart idempotency boundary for the production
|
|
95
|
+
* FrontendSplitTaskMaterializer backend: an existing operation file with the
|
|
96
|
+
* same taskId + payloadHash is returned as {created:false}; any drift fails
|
|
97
|
+
* closed instead of overwriting or silently diverging.
|
|
98
|
+
*/
|
|
99
|
+
async ensureSplitMaterializedOperation(input) {
|
|
100
|
+
return this.serialize(input.operationId, async () => {
|
|
101
|
+
const existing = await this.get(input.operationId);
|
|
102
|
+
if (existing) {
|
|
103
|
+
if (existing.taskId !== input.taskId ||
|
|
104
|
+
existing.payloadHash !== input.payloadHash) {
|
|
105
|
+
throw new Error("frontend split operation materialization payload drift");
|
|
106
|
+
}
|
|
107
|
+
return { operation: existing, created: false };
|
|
108
|
+
}
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
const operation = {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
operationId: input.operationId,
|
|
113
|
+
clientRequestId: `split:${input.operationId}`,
|
|
114
|
+
action: input.action,
|
|
115
|
+
actionParams: input.actionParams,
|
|
116
|
+
payloadHash: input.payloadHash,
|
|
117
|
+
state: "queued",
|
|
118
|
+
createdAt: now,
|
|
119
|
+
revision: 1,
|
|
120
|
+
taskId: input.taskId,
|
|
121
|
+
...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
|
|
122
|
+
};
|
|
123
|
+
await writeSecureJson(operationFile(this.appData, input.operationId), operation);
|
|
124
|
+
return { operation, created: true };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
80
127
|
async get(operationId) {
|
|
81
128
|
return readJsonIfExists(operationFile(this.appData, operationId));
|
|
82
129
|
}
|
|
83
130
|
async save(operation) {
|
|
84
131
|
return this.serialize(operation.operationId, async () => {
|
|
132
|
+
const current = await this.get(operation.operationId);
|
|
133
|
+
if (current?.revision !== undefined && operation.revision !== current.revision)
|
|
134
|
+
throw new Error("operation revision may not be changed by save");
|
|
135
|
+
if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
|
|
136
|
+
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
137
|
+
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
138
|
+
return current;
|
|
139
|
+
}
|
|
140
|
+
if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
|
|
141
|
+
if (!current || JSON.stringify(operation) !== JSON.stringify(current))
|
|
142
|
+
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
143
|
+
return current;
|
|
144
|
+
}
|
|
85
145
|
await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
|
|
86
146
|
return operation;
|
|
87
147
|
});
|
|
@@ -92,6 +152,10 @@ export class OperationStore {
|
|
|
92
152
|
if (!current) {
|
|
93
153
|
throw new Error(`operation not found: ${operationId}`);
|
|
94
154
|
}
|
|
155
|
+
if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
156
|
+
throw new Error("human-decision projection may only be mutated by canonical CAS");
|
|
157
|
+
if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
|
|
158
|
+
throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
|
|
95
159
|
const next = {
|
|
96
160
|
...current,
|
|
97
161
|
...patch,
|
|
@@ -106,6 +170,162 @@ export class OperationStore {
|
|
|
106
170
|
return next;
|
|
107
171
|
});
|
|
108
172
|
}
|
|
173
|
+
async materializeFrontendHumanDecision(input) {
|
|
174
|
+
return this.serialize(input.operationId, async () => {
|
|
175
|
+
const current = await this.get(input.operationId);
|
|
176
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
|
|
177
|
+
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
178
|
+
return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
|
|
179
|
+
}
|
|
180
|
+
const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
|
|
181
|
+
const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
|
|
182
|
+
if (result.outcome !== "applied")
|
|
183
|
+
return result;
|
|
184
|
+
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
|
|
185
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async consumeFrontendHumanDecision(input) {
|
|
190
|
+
return this.serialize(input.operationId, async () => {
|
|
191
|
+
const current = await this.get(input.operationId);
|
|
192
|
+
if (!current || current.revision === undefined || !current.frontendHumanDecision) {
|
|
193
|
+
const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
194
|
+
return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
|
|
195
|
+
}
|
|
196
|
+
const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
197
|
+
if (current.revision !== input.expectedRecordRevision)
|
|
198
|
+
return { outcome: "rejected", reason: "record revision conflict", projection };
|
|
199
|
+
const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
200
|
+
if (result.outcome !== "applied")
|
|
201
|
+
return result;
|
|
202
|
+
const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
|
|
203
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async initializeFrontendHumanApprovedSplit(input) {
|
|
208
|
+
return this.serialize(input.operationId, async () => {
|
|
209
|
+
const current = await this.get(input.operationId);
|
|
210
|
+
const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
|
|
211
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
|
|
212
|
+
return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
|
|
213
|
+
const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
|
|
214
|
+
if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
|
|
215
|
+
return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
|
|
216
|
+
const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
|
|
217
|
+
if (receipt.kind !== "split-materialization-approval")
|
|
218
|
+
return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
|
|
219
|
+
if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
|
|
220
|
+
return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
|
|
221
|
+
const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
|
|
222
|
+
if (consumed.outcome !== "applied")
|
|
223
|
+
return consumed;
|
|
224
|
+
const initialized = initializeFrontendSplitParent({
|
|
225
|
+
execution: input.execution,
|
|
226
|
+
manifest: input.manifest,
|
|
227
|
+
expectedOperationRevision: input.execution.operationRevision,
|
|
228
|
+
successorOperationRevision: consumed.projection.revision,
|
|
229
|
+
envelopeTargetDigest: humanProjection.envelope.targetDigest,
|
|
230
|
+
activeWriterOperationId: input.activeWriterOperationId,
|
|
231
|
+
activeProviderOperationId: input.activeProviderOperationId,
|
|
232
|
+
authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
|
|
233
|
+
});
|
|
234
|
+
if (initialized.outcome !== "applied")
|
|
235
|
+
return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
|
|
236
|
+
const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
|
|
237
|
+
const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
|
|
238
|
+
const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
|
|
239
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
240
|
+
return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async compareAndSwapFrontendSplit(input) {
|
|
244
|
+
return this.serialize(input.operationId, async () => {
|
|
245
|
+
const current = await this.get(input.operationId);
|
|
246
|
+
if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
|
|
247
|
+
return { outcome: "conflict" };
|
|
248
|
+
const currentProjection = current.frontendSplit;
|
|
249
|
+
if (currentProjection) {
|
|
250
|
+
if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
|
|
251
|
+
return { outcome: "conflict" };
|
|
252
|
+
}
|
|
253
|
+
else if (input.expectedExecutionDigest || input.expectedParentDigest)
|
|
254
|
+
return { outcome: "conflict" };
|
|
255
|
+
const next = currentProjection
|
|
256
|
+
? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
|
|
257
|
+
: frontendSplitProjectionSchema.parse(input.next);
|
|
258
|
+
if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
|
|
259
|
+
throw new Error("split CAS next revision or operation binding mismatch");
|
|
260
|
+
const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
|
|
261
|
+
const record = { ...current, revision: next.revision, frontendSplit: next, state };
|
|
262
|
+
await writeSecureJson(operationFile(this.appData, input.operationId), record);
|
|
263
|
+
return { outcome: "applied", projection: next };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async loadChildCloseoutFacts(input) {
|
|
267
|
+
const factsDir = input.closeoutRef.ref;
|
|
268
|
+
const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
|
|
269
|
+
if (facts === undefined)
|
|
270
|
+
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
|
|
271
|
+
const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
|
|
272
|
+
if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
|
|
273
|
+
throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
|
|
274
|
+
const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
|
|
275
|
+
const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
|
|
276
|
+
return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
|
|
277
|
+
}
|
|
278
|
+
async settleFrontendSplitAggregate(input) {
|
|
279
|
+
return this.serialize(input.operationId, async () => {
|
|
280
|
+
const record = await this.get(input.operationId);
|
|
281
|
+
if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
|
|
282
|
+
return { outcome: "conflict" };
|
|
283
|
+
const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
|
|
284
|
+
if (authoritative.parent.operationId !== input.operationId
|
|
285
|
+
|| authoritative.revision !== input.expectedRevision
|
|
286
|
+
|| authoritative.execution.digest !== input.expectedExecutionDigest
|
|
287
|
+
|| authoritative.parent.digest !== input.expectedParentDigest
|
|
288
|
+
|| authoritative.manifest.digest !== input.expectedManifestDigest
|
|
289
|
+
|| authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
|
|
290
|
+
|| authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
|
|
291
|
+
return { outcome: "conflict" };
|
|
292
|
+
if (!authoritative.candidateBinding)
|
|
293
|
+
return { outcome: "conflict" };
|
|
294
|
+
const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
|
|
295
|
+
if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
|
|
296
|
+
return { outcome: "conflict" };
|
|
297
|
+
const transaction = await input.prepareAggregate({
|
|
298
|
+
operationId: authoritative.parent.operationId,
|
|
299
|
+
aggregate: expectedAggregate,
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
|
|
303
|
+
const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
|
|
304
|
+
const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
|
|
305
|
+
const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
|
|
306
|
+
await transaction.commit({
|
|
307
|
+
parentRecordPath: operationFile(this.appData, record.operationId),
|
|
308
|
+
parentRecord: nextRecord,
|
|
309
|
+
});
|
|
310
|
+
return { outcome: "applied", projection: next };
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
await transaction.rollback();
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async initializeFrontendSplit(expectedRevision, projectionValue) {
|
|
319
|
+
const projection = frontendSplitProjectionSchema.parse(projectionValue);
|
|
320
|
+
return this.serialize(projection.parent.operationId, async () => {
|
|
321
|
+
const current = await this.get(projection.parent.operationId);
|
|
322
|
+
if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
|
|
323
|
+
return { outcome: "conflict" };
|
|
324
|
+
const record = { ...current, revision: projection.revision, frontendSplit: projection };
|
|
325
|
+
await writeSecureJson(operationFile(this.appData, current.operationId), record);
|
|
326
|
+
return { outcome: "applied", projection };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
109
329
|
async list() {
|
|
110
330
|
const files = await listJsonFiles(this.appData.operations);
|
|
111
331
|
const ops = [];
|
|
@@ -47,10 +47,22 @@ const RAW_TO_NORMALIZED = {
|
|
|
47
47
|
"verify-failure": "validation",
|
|
48
48
|
"test-failure": "validation",
|
|
49
49
|
"verification-plan-stale": "validation",
|
|
50
|
+
"read-burst": "validation",
|
|
51
|
+
// Writer persisted real file changes but the final provider call hit a
|
|
52
|
+
// context overflow; verification never ran. Distinct from empty-output so
|
|
53
|
+
// the report shows partial success instead of a spec issue.
|
|
54
|
+
"partial-success-with-context-overflow": "validation",
|
|
55
|
+
// Writer burned an excessive token budget (unconverged read-edit-test loop)
|
|
56
|
+
// and still failed; distinct from empty-output for accurate reporting.
|
|
57
|
+
"writer-budget-exhausted": "validation",
|
|
50
58
|
network: "executor",
|
|
51
59
|
quota: "executor",
|
|
52
60
|
"rate-limit": "executor",
|
|
53
61
|
unavailable: "executor",
|
|
62
|
+
// Context window overflow (400 request-too-large): the session needs
|
|
63
|
+
// compaction / fresh context before retrying — an executor-side
|
|
64
|
+
// environment condition, not a plan-quality defect.
|
|
65
|
+
"context-overflow": "executor",
|
|
54
66
|
// Deterministic capability/policy refusal: missing verified launcher JS
|
|
55
67
|
// entry, capability allowlist rejection, or missing bounded write tool
|
|
56
68
|
// profile. These are environment/config defects the operator repairs
|
|
@@ -12,6 +12,19 @@ export const OPENSPEC_SPEC_DIRS = [
|
|
|
12
12
|
"openspec/project-specs",
|
|
13
13
|
"ai_workspace",
|
|
14
14
|
];
|
|
15
|
+
/** Default roots used when a frontend task does not declare specRoots. */
|
|
16
|
+
export const DEFAULT_FRONTEND_SPEC_ROOTS = OPENSPEC_SPEC_DIRS;
|
|
17
|
+
/** A task-owned spec root must be a normalized, repo-relative POSIX directory. */
|
|
18
|
+
export function isValidFrontendSpecRoot(value) {
|
|
19
|
+
return (Boolean(value) &&
|
|
20
|
+
!value.includes("\\") &&
|
|
21
|
+
!value.startsWith("/") &&
|
|
22
|
+
!value.includes("//") &&
|
|
23
|
+
!value.split("/").some((segment) => !segment ||
|
|
24
|
+
segment === "." ||
|
|
25
|
+
segment === ".." ||
|
|
26
|
+
/\s/.test(segment)));
|
|
27
|
+
}
|
|
15
28
|
/** Allowed spec file extensions (case-insensitive). */
|
|
16
29
|
export const OPENSPEC_SPEC_EXT_RE = /\.(md|mdx|json|yaml|yml)$/i;
|
|
17
30
|
/**
|
|
@@ -43,14 +56,17 @@ export function isGovernanceSubtreePath(filePath, governanceRoot) {
|
|
|
43
56
|
* directories. `filePath` must be a repo-relative POSIX path (backslashes are
|
|
44
57
|
* normalised internally). Directory names are case-sensitive.
|
|
45
58
|
*/
|
|
46
|
-
export function
|
|
59
|
+
export function isFrontendSpecPath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
|
|
47
60
|
const normalized = filePath.replaceAll("\\", "/");
|
|
48
|
-
for (const dir of
|
|
61
|
+
for (const dir of specRoots) {
|
|
49
62
|
if (normalized === dir || normalized.startsWith(dir + "/"))
|
|
50
63
|
return true;
|
|
51
64
|
}
|
|
52
65
|
return false;
|
|
53
66
|
}
|
|
67
|
+
export function isOpenspecSpecPath(filePath) {
|
|
68
|
+
return isFrontendSpecPath(filePath);
|
|
69
|
+
}
|
|
54
70
|
/**
|
|
55
71
|
* Returns `true` when `filename` (the basename only, not a full path) has an
|
|
56
72
|
* allowed frontend specification extension.
|
|
@@ -59,10 +75,13 @@ export function isValidOpenspecExtension(filename) {
|
|
|
59
75
|
return OPENSPEC_SPEC_EXT_RE.test(filename);
|
|
60
76
|
}
|
|
61
77
|
/** Returns `true` for a supported file inside a canonical openspec spec dir. */
|
|
62
|
-
export function
|
|
63
|
-
return (
|
|
78
|
+
export function isFrontendSpecFilePath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
|
|
79
|
+
return (isFrontendSpecPath(filePath, specRoots) &&
|
|
64
80
|
isValidOpenspecExtension(filePath.replaceAll("\\", "/")));
|
|
65
81
|
}
|
|
82
|
+
export function isOpenspecSpecFilePath(filePath) {
|
|
83
|
+
return isFrontendSpecFilePath(filePath);
|
|
84
|
+
}
|
|
66
85
|
/**
|
|
67
86
|
* Returns `true` when free-form search input references a canonical frontend
|
|
68
87
|
* specification directory with exact lowercase directory names.
|
|
@@ -123,3 +142,50 @@ export function extractTaskSourceOpenspecPaths(markdown, governanceRoot = DEFAUL
|
|
|
123
142
|
!isGovernanceSubtreePath(token, governanceRoot))
|
|
124
143
|
.sort();
|
|
125
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract explicit specification-file citations for a task's frozen spec roots.
|
|
147
|
+
* This is intentionally path-only: source preparation decides which roots are
|
|
148
|
+
* normative, while DAG execution only consumes that frozen configuration.
|
|
149
|
+
*/
|
|
150
|
+
export function extractTaskSourceFrontendSpecPaths(markdown, specRoots, governanceRoot = DEFAULT_OPENSPEC_GOVERNANCE_ROOT) {
|
|
151
|
+
if (!markdown)
|
|
152
|
+
return [];
|
|
153
|
+
const raw = new Set();
|
|
154
|
+
const collect = (token) => {
|
|
155
|
+
const normalized = normalizeCitationToken(token);
|
|
156
|
+
if (normalized)
|
|
157
|
+
raw.add(normalized);
|
|
158
|
+
};
|
|
159
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g))
|
|
160
|
+
collect(match[1] ?? "");
|
|
161
|
+
for (const match of markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g))
|
|
162
|
+
collect(match[1] ?? "");
|
|
163
|
+
for (const root of specRoots) {
|
|
164
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
165
|
+
for (const match of markdown.matchAll(new RegExp(`(?:^|[\\s\"'([>])(${escaped}/[A-Za-z0-9._/-]+)`, "g"))) {
|
|
166
|
+
collect(match[1] ?? "");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [...raw]
|
|
170
|
+
.filter((token) => isFrontendSpecFilePath(token, specRoots) && !isGovernanceSubtreePath(token, governanceRoot))
|
|
171
|
+
.sort();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Source-preparation helper for explicitly named normative directories. A root
|
|
175
|
+
* is inferred only on a line that declares a spec/规范/design-system intent;
|
|
176
|
+
* arbitrary Markdown links never silently broaden task configuration.
|
|
177
|
+
*/
|
|
178
|
+
export function extractDeclaredFrontendSpecRoots(markdown) {
|
|
179
|
+
const roots = new Set();
|
|
180
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
181
|
+
if (!/(?:规范|规约|标准|设计系统|\bspec(?:ification)?\b|\bdesign[ -]?system\b|\bguidelines?\b|\brules?\b)/i.test(line))
|
|
182
|
+
continue;
|
|
183
|
+
for (const match of line.matchAll(/`([^`]+)`|\[[^\]]+\]\(([^)\s]+)\)/g)) {
|
|
184
|
+
const raw = (match[1] ?? match[2] ?? "").replaceAll("\\", "/").replace(/\/+$/, "");
|
|
185
|
+
if (!raw || !isValidFrontendSpecRoot(raw))
|
|
186
|
+
continue;
|
|
187
|
+
roots.add(isValidOpenspecExtension(raw) ? raw.split("/").slice(0, -1).join("/") : raw);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [...roots].filter(isValidFrontendSpecRoot).sort();
|
|
191
|
+
}
|
|
@@ -2240,6 +2240,13 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
2240
2240
|
source: "loop-agent",
|
|
2241
2241
|
note: "server-side read-only run-facts check (ineligible plan / explicit rerun-task recommendation) replaces the browser Human Gate (2026-08-11).",
|
|
2242
2242
|
},
|
|
2243
|
+
{
|
|
2244
|
+
command: "loop-agent dag follow-up",
|
|
2245
|
+
coverage: "excluded",
|
|
2246
|
+
action: null,
|
|
2247
|
+
source: "loop-agent",
|
|
2248
|
+
exclusionReason: "Frontend review recovery helper is a direct advanced CLI composition (create/import task and optional advance), not an Operator action endpoint.",
|
|
2249
|
+
},
|
|
2243
2250
|
{
|
|
2244
2251
|
command: "loop-agent dag reconcile-tasks",
|
|
2245
2252
|
coverage: "human-gated-required",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
|
|
3
3
|
export const taskFlowSchema = z.enum([
|
|
4
4
|
"auto",
|
|
5
5
|
"micro",
|
|
@@ -77,13 +77,27 @@ export const frontendOpenspecPolicySchema = z.enum(["cited", "scan-strict"]);
|
|
|
77
77
|
*/
|
|
78
78
|
export const frontendOpenspecConfigSchema = z.object({
|
|
79
79
|
policy: frontendOpenspecPolicySchema.optional().default("cited"),
|
|
80
|
+
/**
|
|
81
|
+
* Repo-relative directories containing frontend normative files. These are
|
|
82
|
+
* frozen in task.json during contract preparation, before DAG generation.
|
|
83
|
+
*/
|
|
84
|
+
specRoots: z
|
|
85
|
+
.array(z.string().refine((candidate) => isValidFrontendSpecRoot(candidate), "frontendOpenspec.specRoots entries must be normalized repo-relative directories"))
|
|
86
|
+
.optional(),
|
|
80
87
|
requiredReadPaths: z
|
|
81
|
-
.array(z
|
|
82
|
-
.string()
|
|
83
|
-
.refine((candidate) => !candidate.includes("\\") &&
|
|
84
|
-
isOpenspecSpecFilePath(candidate), "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under openspec/schemas/, openspec/project-specs/, or ai_workspace/"))
|
|
88
|
+
.array(z.string())
|
|
85
89
|
.optional()
|
|
86
90
|
.default([]),
|
|
91
|
+
}).superRefine((value, ctx) => {
|
|
92
|
+
const roots = value.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
|
|
93
|
+
if (new Set(roots).size !== roots.length) {
|
|
94
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.specRoots must not contain duplicates", path: ["specRoots"] });
|
|
95
|
+
}
|
|
96
|
+
for (const required of value.requiredReadPaths) {
|
|
97
|
+
if (!isFrontendSpecFilePath(required, roots)) {
|
|
98
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under frontendOpenspec.specRoots", path: ["requiredReadPaths"] });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
87
101
|
});
|
|
88
102
|
/** Batch limits and gate profile for the browser-driven frontend test DAG.
|
|
89
103
|
* Token fields are post-case stop thresholds, not model-provider hard caps. */
|
|
@@ -281,6 +295,21 @@ const taskConfigObjectSchema = z.object({
|
|
|
281
295
|
maxFixLoops: z.number().int().min(0).optional().default(2),
|
|
282
296
|
/** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
|
|
283
297
|
convergence: convergenceConfigSchema.optional().default({ enabled: false }),
|
|
298
|
+
/** Frontend recovery continuation quota. Bounds how many times a failed
|
|
299
|
+
* frontend run may auto-recover via M6 recovery operations (1 = current
|
|
300
|
+
* default; raise to 2-3 for more tolerance of transient verify failures). */
|
|
301
|
+
frontendRecovery: z
|
|
302
|
+
.object({
|
|
303
|
+
maxContinuations: z
|
|
304
|
+
.number()
|
|
305
|
+
.int()
|
|
306
|
+
.min(1)
|
|
307
|
+
.max(5)
|
|
308
|
+
.optional()
|
|
309
|
+
.default(1),
|
|
310
|
+
})
|
|
311
|
+
.strict()
|
|
312
|
+
.optional(),
|
|
284
313
|
/** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
|
|
285
314
|
loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
|
|
286
315
|
.optional()
|
|
@@ -90,6 +90,10 @@ export async function adoptTaskContract(input) {
|
|
|
90
90
|
requirementBytes: bytes.requirementBytes,
|
|
91
91
|
constraintsBytes: bytes.constraintsBytes,
|
|
92
92
|
referenceManifestBytes: bytes.referenceManifestBytes,
|
|
93
|
+
...(state.ref?.sourceHashes.requirementLedgerSha256 &&
|
|
94
|
+
bytes.requirementLedgerBytes !== null
|
|
95
|
+
? { requirementLedgerBytes: bytes.requirementLedgerBytes }
|
|
96
|
+
: {}),
|
|
93
97
|
});
|
|
94
98
|
const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
|
|
95
99
|
const canonicalHash = computeCanonicalHash({
|
|
@@ -45,6 +45,10 @@ export async function refreshManagedContractAfterImport(input) {
|
|
|
45
45
|
requirementBytes: bytes.requirementBytes,
|
|
46
46
|
constraintsBytes: bytes.constraintsBytes,
|
|
47
47
|
referenceManifestBytes: bytes.referenceManifestBytes,
|
|
48
|
+
...(state2.ref.sourceHashes.requirementLedgerSha256 &&
|
|
49
|
+
bytes.requirementLedgerBytes !== null
|
|
50
|
+
? { requirementLedgerBytes: bytes.requirementLedgerBytes }
|
|
51
|
+
: {}),
|
|
48
52
|
});
|
|
49
53
|
const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
|
|
50
54
|
const canonicalHash = computeCanonicalHash({
|
|
@@ -184,6 +184,9 @@ export function mergeManagedTaskConfigFields(existing, draft) {
|
|
|
184
184
|
command: cmd.command,
|
|
185
185
|
...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
|
|
186
186
|
})),
|
|
187
|
+
...(draft.frontendOpenspec !== undefined
|
|
188
|
+
? { frontendOpenspec: draft.frontendOpenspec }
|
|
189
|
+
: {}),
|
|
187
190
|
};
|
|
188
191
|
if (draft.featureId !== undefined) {
|
|
189
192
|
next.featureId = draft.featureId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { backendTestConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
|
|
2
|
+
import { backendTestConfigSchema, frontendOpenspecConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
|
|
3
3
|
import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
|
|
4
4
|
const sha256Schema = z
|
|
5
5
|
.string()
|
|
@@ -74,6 +74,7 @@ export const taskContractDraftSchema = z
|
|
|
74
74
|
openQuestions: z.array(z.string()).optional(),
|
|
75
75
|
assumptions: z.array(z.string()).optional(),
|
|
76
76
|
hardConstraints: z.array(z.string()).optional(),
|
|
77
|
+
frontendOpenspec: frontendOpenspecConfigSchema.optional(),
|
|
77
78
|
interviewAssessmentRef: z
|
|
78
79
|
.object({
|
|
79
80
|
draftSha256: z.string().min(1),
|