@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0

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.
Files changed (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -61
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +13 -373
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +1 -6
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -1,126 +1,9 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { z } from "zod";
4
3
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
5
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
4
+ import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
6
5
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
7
6
  import { pathMatchesPattern } from "../../shared/git-progress.js";
8
- export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
9
- export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
10
- /**
11
- * Machine-readable failure classification for blocked/retryable-invalid
12
- * prewrite outcomes. Kept alongside the human-readable failureReason so
13
- * downstream routing (repair prompting, dashboards, retry policies) can act on
14
- * a stable code instead of parsing prose.
15
- */
16
- export const frontendPrewriteFailureCodeSchema = z.enum([
17
- "plan-text-unreadable",
18
- "review-text-unreadable",
19
- "verdict-not-pass",
20
- "missing-requirement-ids",
21
- "source-binding-not-fresh",
22
- "contract-invalid",
23
- "contract-blocked",
24
- "mock-strategy-outside-allowed",
25
- "mock-strategy-no-verification-commands",
26
- "target-outside-write-set",
27
- "verification-target-outside-write-set",
28
- "openspec-not-read",
29
- ]);
30
- export const frontendPrewriteResultV1Schema = z
31
- .object({
32
- schemaVersion: z.literal(1),
33
- schemaId: z.literal(FRONTEND_PREWRITE_RESULT_SCHEMA_ID),
34
- classification: z.enum([
35
- "accepted",
36
- "accepted-normalized",
37
- "retryable-invalid",
38
- "blocked",
39
- ]),
40
- failureCode: frontendPrewriteFailureCodeSchema.nullable(),
41
- normalizationActions: z.array(frontendNormalizationActionSchema),
42
- candidateRawSha256: z.string().regex(/^[a-f0-9]{64}$/),
43
- candidateJsonSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
44
- canonicalSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
45
- selectedPlanNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
46
- sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
47
- writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
48
- retryCount: z.number().int().min(0).max(1),
49
- maxRetryCount: z.literal(1),
50
- failureReason: z.string().nullable(),
51
- digestAlgorithm: z.literal("sha256"),
52
- })
53
- .strict()
54
- .superRefine((value, ctx) => {
55
- const acceptedLike = value.classification === "accepted" ||
56
- value.classification === "accepted-normalized";
57
- const failed = value.classification === "retryable-invalid" ||
58
- value.classification === "blocked";
59
- if (acceptedLike) {
60
- if (value.failureReason !== null)
61
- ctx.addIssue({
62
- code: "custom",
63
- message: "failureReason must be null for accepted/accepted-normalized",
64
- path: ["failureReason"],
65
- });
66
- if (value.failureCode !== null)
67
- ctx.addIssue({
68
- code: "custom",
69
- message: "failureCode must be null for accepted/accepted-normalized",
70
- path: ["failureCode"],
71
- });
72
- if (!value.candidateJsonSha256 || !value.canonicalSha256)
73
- ctx.addIssue({
74
- code: "custom",
75
- message: "candidateJsonSha256 and canonicalSha256 are required for accepted/accepted-normalized",
76
- path: ["candidateJsonSha256"],
77
- });
78
- }
79
- if (value.classification === "accepted" && value.normalizationActions.length !== 0)
80
- ctx.addIssue({
81
- code: "custom",
82
- message: "accepted must have empty normalizationActions",
83
- path: ["normalizationActions"],
84
- });
85
- if (value.classification === "accepted-normalized" &&
86
- value.normalizationActions.length === 0)
87
- ctx.addIssue({
88
- code: "custom",
89
- message: "accepted-normalized must have non-empty normalizationActions",
90
- path: ["normalizationActions"],
91
- });
92
- if (failed) {
93
- if (value.normalizationActions.length !== 0)
94
- ctx.addIssue({
95
- code: "custom",
96
- message: "retryable-invalid/blocked must have empty normalizationActions",
97
- path: ["normalizationActions"],
98
- });
99
- if (!value.failureReason)
100
- ctx.addIssue({
101
- code: "custom",
102
- message: "failureReason must be non-empty for retryable-invalid/blocked",
103
- path: ["failureReason"],
104
- });
105
- if (value.failureCode === null)
106
- ctx.addIssue({
107
- code: "custom",
108
- message: "failureCode must be non-null for retryable-invalid/blocked",
109
- path: ["failureCode"],
110
- });
111
- }
112
- });
113
- export const frontendWriterAdmissionSchema = z
114
- .object({
115
- schemaVersion: z.literal(1),
116
- writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
117
- decision: z.enum(["authorized", "denied"]),
118
- sourceArtifact: z.string().min(1),
119
- artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
120
- checkedAt: z.string().min(1),
121
- reason: z.string().nullable(),
122
- })
123
- .strict();
124
7
  async function selectNode(runDir, primary, fallbacks) {
125
8
  const candidates = [primary, ...fallbacks.filter((id) => id !== primary)];
126
9
  const rejections = [];
@@ -154,10 +37,6 @@ async function readNodeText(runDir, nodeId) {
154
37
  throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
155
38
  return text;
156
39
  }
157
- async function readNodeRawText(runDir, nodeId) {
158
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
159
- return record.assistantText ?? record.stdout ?? "";
160
- }
161
40
  function firstNonEmptyVerdictLine(text) {
162
41
  const lines = text
163
42
  .split(/\r?\n/)
@@ -253,141 +132,16 @@ async function checkOpenspecReadEvidence(input) {
253
132
  }
254
133
  return [...matched];
255
134
  }
256
- async function finalizePrewrite(input, pending) {
257
- const sourceBindingSha256 = input.sourceBinding
258
- ? deterministicSha256(input.sourceBinding)
259
- : null;
260
- const writeSetDigest = input.config.implementationWriteSet
261
- ? deterministicSha256([...new Set(input.config.implementationWriteSet)].sort())
262
- : null;
263
- const prewriteResult = frontendPrewriteResultV1Schema.parse({
264
- schemaVersion: 1,
265
- schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
266
- classification: pending.classification,
267
- normalizationActions: pending.normalizationActions,
268
- candidateRawSha256: pending.candidateRawSha256,
269
- candidateJsonSha256: pending.candidateJsonSha256,
270
- canonicalSha256: pending.canonicalSha256,
271
- selectedPlanNodeId: pending.planNodeId,
272
- sourceBindingSha256,
273
- writeSetDigest,
274
- retryCount: 0,
275
- maxRetryCount: 1,
276
- failureReason: pending.failureReason,
277
- failureCode: pending.failureCode,
278
- digestAlgorithm: "sha256",
279
- });
280
- const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.config.outputDir, FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME), prewriteResult);
281
- return {
282
- ok: true,
283
- classification: pending.classification,
284
- planNodeId: pending.planNodeId,
285
- reviewNodeId: pending.reviewNodeId,
286
- verdict: pending.verdict,
287
- mockStrategy: pending.mockStrategy,
288
- artifact: pending.artifact,
289
- prewriteResult: {
290
- path: written.path,
291
- sha256: written.sha256,
292
- schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
293
- },
294
- failureReason: pending.failureReason,
295
- failureCode: pending.failureCode,
296
- normalizationActions: pending.normalizationActions,
297
- candidateRawSha256: pending.candidateRawSha256,
298
- candidateJsonSha256: pending.candidateJsonSha256,
299
- canonicalSha256: pending.canonicalSha256,
300
- openspecReadPaths: pending.openspecReadPaths,
301
- openspecCandidatePaths: pending.openspecCandidatePaths,
302
- };
303
- }
304
135
  export async function runFrontendPrewriteGate(input) {
305
136
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
306
- // selectNode throws for a scheduling defect (no FINISHED plan/review). That
307
- // remains an ERROR and is intentionally not classified into prewrite-result.
308
- const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
309
- const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
310
- const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
311
- const basePending = {
312
- planNodeId,
313
- reviewNodeId,
314
- verdict: "",
315
- candidateRawSha256,
316
- candidateJsonSha256: null,
317
- canonicalSha256: null,
318
- normalizationActions: [],
319
- mockStrategy: undefined,
320
- artifact: undefined,
321
- failureReason: null,
322
- failureCode: null,
323
- classification: "accepted",
324
- openspecReadPaths: [],
325
- openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
326
- };
327
- let planText;
328
- try {
329
- planText = await readNodeText(input.runDir, planNodeId);
330
- }
331
- catch (error) {
332
- return finalizePrewrite(input, {
333
- ...basePending,
334
- classification: "retryable-invalid",
335
- failureReason: error instanceof Error ? error.message : String(error),
336
- failureCode: "plan-text-unreadable",
337
- });
338
- }
339
- let reviewText;
340
- try {
341
- reviewText = await readNodeText(input.runDir, reviewNodeId);
342
- }
343
- catch (error) {
344
- return finalizePrewrite(input, {
345
- ...basePending,
346
- classification: "blocked",
347
- failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${error instanceof Error ? error.message : String(error)}`,
348
- failureCode: "review-text-unreadable",
349
- });
350
- }
351
- const verdict = firstNonEmptyVerdictLine(reviewText);
352
- if (verdict !== "VERDICT: pass") {
353
- return finalizePrewrite(input, {
354
- ...basePending,
355
- verdict,
356
- classification: "blocked",
357
- failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`,
358
- failureCode: "verdict-not-pass",
359
- });
360
- }
361
- const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
362
- if (missingIds.length > 0) {
363
- return finalizePrewrite(input, {
364
- ...basePending,
365
- verdict,
366
- classification: "blocked",
367
- failureReason: `frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`,
368
- failureCode: "missing-requirement-ids",
369
- });
370
- }
371
137
  if (workspaceRoot) {
372
138
  if (input.config.requireSourceFreshness) {
373
- if (!input.sourceBinding) {
139
+ if (!input.sourceBinding)
374
140
  throw new Error("frontend prewrite gate requires sourceBinding for freshness check");
375
- }
376
- try {
377
- await assertFrontendSourceBindingFresh({
378
- workspaceRoot,
379
- binding: input.sourceBinding,
380
- });
381
- }
382
- catch (error) {
383
- return finalizePrewrite(input, {
384
- ...basePending,
385
- verdict,
386
- classification: "blocked",
387
- failureReason: error instanceof Error ? error.message : String(error),
388
- failureCode: "source-binding-not-fresh",
389
- });
390
- }
141
+ await assertFrontendSourceBindingFresh({
142
+ workspaceRoot,
143
+ binding: input.sourceBinding,
144
+ });
391
145
  }
392
146
  let hasGitMetadata = true;
393
147
  try {
@@ -405,34 +159,31 @@ export async function runFrontendPrewriteGate(input) {
405
159
  workspaceRoot,
406
160
  });
407
161
  }
408
- // Candidate -> canonical conversion. The gate consumes the selected
409
- // plan/revision assistant output directly and is the only conversion point.
410
- let analysis;
411
- try {
412
- analysis = await analyzeFrontendImplementationContract({
413
- runDir: input.runDir,
414
- fromNodeId: planNodeId,
415
- sourceBinding: input.sourceBinding,
416
- });
162
+ const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
163
+ const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
164
+ const planText = await readNodeText(input.runDir, planNodeId);
165
+ const reviewText = await readNodeText(input.runDir, reviewNodeId);
166
+ const verdict = firstNonEmptyVerdictLine(reviewText);
167
+ if (verdict !== "VERDICT: pass") {
168
+ throw new Error(`frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`);
417
169
  }
418
- catch (error) {
419
- if (error instanceof FrontendContractFailure) {
420
- return finalizePrewrite(input, {
421
- ...basePending,
422
- verdict,
423
- classification: error.kind,
424
- failureReason: error.message,
425
- failureCode: error.kind === "retryable-invalid"
426
- ? "contract-invalid"
427
- : "contract-blocked",
428
- candidateJsonSha256: error.candidateJsonSha256,
429
- normalizationActions: [],
430
- });
431
- }
432
- throw error;
170
+ const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
171
+ if (missingIds.length > 0) {
172
+ throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
433
173
  }
434
- const mockStrategy = analysis.canonical.mockApi.strategy;
435
- if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
174
+ // Always rematerialize from the selected plan node. Reusing a previous
175
+ // contracts/*.json would accept stale contracts when a higher-priority
176
+ // FINISHED primary is present but invalid (see composite-shell invalid-primary).
177
+ const artifact = await materializeFrontendImplementationContract({
178
+ runDir: input.runDir,
179
+ fromNodeId: planNodeId,
180
+ artifactName: input.config.artifactName,
181
+ outputDir: input.config.outputDir,
182
+ sourceBinding: input.sourceBinding,
183
+ });
184
+ const raw = JSON.parse(await readFile(artifact.path, "utf8"));
185
+ const contract = frontendImplementationContractSchema.parse(raw);
186
+ if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
436
187
  const quotedAllowed = input.config.allowedMockStrategies
437
188
  .map((strategy) => JSON.stringify(strategy))
438
189
  .join(", ");
@@ -441,67 +192,30 @@ export async function runFrontendPrewriteGate(input) {
441
192
  const reason = forcedNotNeeded
442
193
  ? "the task is auto/not-required or lacks authorized deterministic Mock verification commands"
443
194
  : `allowed strategies are [${quotedAllowed}]`;
444
- return finalizePrewrite(input, {
445
- ...basePending,
446
- verdict,
447
- candidateJsonSha256: analysis.candidateJsonSha256,
448
- normalizationActions: [],
449
- mockStrategy,
450
- classification: "blocked",
451
- failureReason: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`,
452
- failureCode: "mock-strategy-outside-allowed",
453
- });
195
+ throw new Error(`frontend contract mock strategy "${contract.mockApi.strategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`);
454
196
  }
455
- // Mock command binding (main): a non-not-needed strategy can only be proven
456
- // by the DAG's frozen Mock verification commands. Block before any write is
457
- // authorized when none were materialized at generation time.
458
- if (mockStrategy !== "not-needed" &&
197
+ // Fail early instead of at the verification trace: a non-not-needed strategy
198
+ // can only be proven by executing the DAG's frozen Mock verification
199
+ // commands. If none were materialized at generation time the contract would
200
+ // fail "trace: selected Mock strategy requires successful Mock verification
201
+ // commands" after implementation, so block before any write is authorized.
202
+ if (contract.mockApi.strategy !== "not-needed" &&
459
203
  (input.config.mockCommandLabels?.length ?? 0) === 0) {
460
- return finalizePrewrite(input, {
461
- ...basePending,
462
- verdict,
463
- candidateJsonSha256: analysis.candidateJsonSha256,
464
- normalizationActions: [],
465
- mockStrategy,
466
- classification: "blocked",
467
- failureReason: `frontend prewrite gate blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`,
468
- failureCode: "mock-strategy-no-verification-commands",
469
- });
204
+ throw new Error(`frontend prewrite gate blocked: contract selected Mock strategy ${contract.mockApi.strategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`);
470
205
  }
471
206
  if (input.config.implementationWriteSet) {
472
207
  const writeSet = new Set(input.config.implementationWriteSet);
473
- const contractTargets = new Set(analysis.canonical.targets.files);
208
+ const contractTargets = new Set(contract.targets.files);
474
209
  const uncoveredTargets = [...contractTargets].filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
475
210
  if (uncoveredTargets.length > 0) {
476
- return finalizePrewrite(input, {
477
- ...basePending,
478
- verdict,
479
- candidateJsonSha256: analysis.candidateJsonSha256,
480
- normalizationActions: [],
481
- mockStrategy,
482
- classification: "blocked",
483
- failureReason: `frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
484
- failureCode: "target-outside-write-set",
485
- });
211
+ throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
486
212
  }
487
- const nonStaticVTs = analysis.canonical.verificationTargets.filter((vt) => vt.type !== "static");
213
+ const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
488
214
  const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
489
215
  if (uncoveredVTs.length > 0) {
490
- return finalizePrewrite(input, {
491
- ...basePending,
492
- verdict,
493
- candidateJsonSha256: analysis.candidateJsonSha256,
494
- normalizationActions: [],
495
- mockStrategy,
496
- classification: "blocked",
497
- failureReason: `frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
498
- failureCode: "verification-target-outside-write-set",
499
- });
216
+ throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
500
217
  }
501
218
  }
502
- // Effective plan/review must actually read the candidate openspec paths when
503
- // any exist. This fail-closes BEFORE the canonical contract is materialized
504
- // so a writer is never authorized on top of unread normative evidence.
505
219
  const candidatePaths = input.config.openspecCandidatePaths ?? [];
506
220
  const openspecReadPaths = await checkOpenspecReadEvidence({
507
221
  runDir: input.runDir,
@@ -510,82 +224,36 @@ export async function runFrontendPrewriteGate(input) {
510
224
  reviewNodeId,
511
225
  repoRoot: workspaceRoot ?? process.cwd(),
512
226
  });
513
- if (candidatePaths.length > 0) {
514
- const readSet = new Set(openspecReadPaths);
515
- const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
516
- if (unread.length > 0) {
517
- return finalizePrewrite(input, {
518
- ...basePending,
519
- verdict,
520
- candidateJsonSha256: analysis.candidateJsonSha256,
521
- normalizationActions: [],
522
- mockStrategy,
523
- classification: "retryable-invalid",
524
- failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun.`,
525
- failureCode: "openspec-not-read",
526
- openspecReadPaths,
527
- openspecCandidatePaths: candidatePaths,
528
- });
529
- }
530
- }
531
- // Materialize the canonical contract only after every governance check passed.
532
- const artifact = await writeFrontendImplementationContractArtifact({
533
- runDir: input.runDir,
534
- outputDir: input.config.outputDir,
535
- artifactName: input.config.artifactName,
536
- canonical: analysis.canonical,
537
- });
538
- const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
539
- return finalizePrewrite(input, {
540
- ...basePending,
227
+ // OpenSpec is advisory design evidence, not an authorization boundary.
228
+ // Model read behavior is nondeterministic, and a missed read must not block
229
+ // a contract/schema/write-set-authorized implementation. Preserve any
230
+ // successful evidence for review context, while allowing unavailable
231
+ // evidence to be reported downstream.
232
+ return {
233
+ ok: true,
234
+ planNodeId,
235
+ reviewNodeId,
541
236
  verdict,
542
- classification,
543
- failureReason: null,
544
- candidateJsonSha256: analysis.candidateJsonSha256,
545
- canonicalSha256: artifact.sha256,
546
- normalizationActions: analysis.normalizationActions,
547
- mockStrategy,
237
+ mockStrategy: contract.mockApi.strategy,
548
238
  artifact,
549
239
  openspecReadPaths,
550
240
  openspecCandidatePaths: candidatePaths,
551
- });
241
+ };
552
242
  }
553
243
  export function formatFrontendPrewriteGateStdout(result) {
554
- const lines = [];
555
- if (result.classification === "accepted" ||
556
- result.classification === "accepted-normalized") {
557
- lines.push("Frontend prewrite gate: pass");
558
- }
559
- else if (result.classification === "blocked") {
560
- lines.push("Frontend prewrite gate: blocked");
561
- }
562
- else {
563
- lines.push("Frontend prewrite gate: retryable-invalid");
564
- }
565
- lines.push(`Classification: ${result.classification}`);
566
- lines.push(`Plan: ${result.planNodeId}`);
567
- lines.push(`Review: ${result.reviewNodeId}`);
568
- if (result.mockStrategy)
569
- lines.push(`Mock strategy: ${result.mockStrategy}`);
570
- if (result.artifact) {
571
- lines.push(`Structured artifact: ${result.artifact.path}`);
572
- lines.push(`Schema: ${result.artifact.schemaId}`);
573
- lines.push(`SHA-256: ${result.artifact.sha256}`);
574
- }
575
- lines.push(`Prewrite result: ${result.prewriteResult.path}`);
576
- if (result.failureReason)
577
- lines.push(`Failure reason: ${result.failureReason}`);
244
+ const lines = [
245
+ "Frontend prewrite gate: pass",
246
+ `Plan: ${result.planNodeId}`,
247
+ `Review: ${result.reviewNodeId}`,
248
+ `Mock strategy: ${result.mockStrategy}`,
249
+ `Structured artifact: ${result.artifact.path}`,
250
+ `Schema: ${result.artifact.schemaId}`,
251
+ `SHA-256: ${result.artifact.sha256}`,
252
+ ];
578
253
  if (result.openspecReadPaths.length > 0) {
579
254
  lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
580
255
  }
581
- if (result.openspecCandidatePaths.length > 0) {
582
- const readSet = new Set(result.openspecReadPaths);
583
- const unread = result.openspecCandidatePaths.filter((candidate) => !readSet.has(candidate));
584
- if (unread.length > 0) {
585
- lines.push(`openspec unread: ${unread.join(", ")}`);
586
- }
587
- }
588
- else {
256
+ else if (result.openspecCandidatePaths.length === 0) {
589
257
  lines.push("openspec: unavailable");
590
258
  }
591
259
  return lines.join("\n");