@tea-agent/loop-agent 0.35.1-beta.2 → 0.35.1

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 (48) hide show
  1. package/AGENTS.md +0 -2
  2. package/CHANGELOG.md +25 -24
  3. package/bin/loop-agent.js +1 -37
  4. package/dist/application/dag/generate-task-dag.js +4 -1
  5. package/dist/application/task-lifecycle/advance.js +14 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/commands/task-advance.js +1 -0
  8. package/dist/executors/dag-pi-executor.js +0 -44
  9. package/dist/shared/package-metadata.js +0 -42
  10. package/dist/task/config-types.js +2 -0
  11. package/dist/task/contract/project.js +3 -0
  12. package/dist/task/contract/schema.js +1 -0
  13. package/dist/task/source-prepare/build-draft.js +7 -0
  14. package/dist/task/source-prepare/semantic-intake.js +37 -10
  15. package/dist/task/task-demand-routing.js +10 -0
  16. package/dist/worker/console/operator-actions.js +72 -6
  17. package/dist/worker/console/prd-intake-bridge.js +10 -3
  18. package/dist/worker/console/prd-reference-discovery.js +124 -0
  19. package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
  20. package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
  24. package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
  25. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  26. package/dist/worker/observability/read-model.js +0 -20
  27. package/dist/worker/preflight.js +1 -2
  28. package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
  29. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  30. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  32. package/dist/workflows/dag/frontend-repair.js +18 -219
  33. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  34. package/dist/workflows/dag/init-hybrid.js +26 -41
  35. package/dist/workflows/dag/node-execution.js +0 -89
  36. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  37. package/dist/workflows/dag/runner.js +11 -245
  38. package/dist/workflows/dag/scheduler.js +3 -257
  39. package/dist/workflows/dag/types.js +2 -130
  40. package/package.json +2 -2
  41. package/dist/build-stamp.json +0 -6
  42. package/dist/workflows/dag/contract-output-registry.js +0 -14
  43. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  44. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  45. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  46. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  47. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  48. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
@@ -1,125 +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
- ]);
29
- export const frontendPrewriteResultV1Schema = z
30
- .object({
31
- schemaVersion: z.literal(1),
32
- schemaId: z.literal(FRONTEND_PREWRITE_RESULT_SCHEMA_ID),
33
- classification: z.enum([
34
- "accepted",
35
- "accepted-normalized",
36
- "retryable-invalid",
37
- "blocked",
38
- ]),
39
- failureCode: frontendPrewriteFailureCodeSchema.nullable(),
40
- normalizationActions: z.array(frontendNormalizationActionSchema),
41
- candidateRawSha256: z.string().regex(/^[a-f0-9]{64}$/),
42
- candidateJsonSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
43
- canonicalSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
44
- selectedPlanNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
45
- sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
46
- writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
47
- retryCount: z.number().int().min(0).max(1),
48
- maxRetryCount: z.literal(1),
49
- failureReason: z.string().nullable(),
50
- digestAlgorithm: z.literal("sha256"),
51
- })
52
- .strict()
53
- .superRefine((value, ctx) => {
54
- const acceptedLike = value.classification === "accepted" ||
55
- value.classification === "accepted-normalized";
56
- const failed = value.classification === "retryable-invalid" ||
57
- value.classification === "blocked";
58
- if (acceptedLike) {
59
- if (value.failureReason !== null)
60
- ctx.addIssue({
61
- code: "custom",
62
- message: "failureReason must be null for accepted/accepted-normalized",
63
- path: ["failureReason"],
64
- });
65
- if (value.failureCode !== null)
66
- ctx.addIssue({
67
- code: "custom",
68
- message: "failureCode must be null for accepted/accepted-normalized",
69
- path: ["failureCode"],
70
- });
71
- if (!value.candidateJsonSha256 || !value.canonicalSha256)
72
- ctx.addIssue({
73
- code: "custom",
74
- message: "candidateJsonSha256 and canonicalSha256 are required for accepted/accepted-normalized",
75
- path: ["candidateJsonSha256"],
76
- });
77
- }
78
- if (value.classification === "accepted" && value.normalizationActions.length !== 0)
79
- ctx.addIssue({
80
- code: "custom",
81
- message: "accepted must have empty normalizationActions",
82
- path: ["normalizationActions"],
83
- });
84
- if (value.classification === "accepted-normalized" &&
85
- value.normalizationActions.length === 0)
86
- ctx.addIssue({
87
- code: "custom",
88
- message: "accepted-normalized must have non-empty normalizationActions",
89
- path: ["normalizationActions"],
90
- });
91
- if (failed) {
92
- if (value.normalizationActions.length !== 0)
93
- ctx.addIssue({
94
- code: "custom",
95
- message: "retryable-invalid/blocked must have empty normalizationActions",
96
- path: ["normalizationActions"],
97
- });
98
- if (!value.failureReason)
99
- ctx.addIssue({
100
- code: "custom",
101
- message: "failureReason must be non-empty for retryable-invalid/blocked",
102
- path: ["failureReason"],
103
- });
104
- if (value.failureCode === null)
105
- ctx.addIssue({
106
- code: "custom",
107
- message: "failureCode must be non-null for retryable-invalid/blocked",
108
- path: ["failureCode"],
109
- });
110
- }
111
- });
112
- export const frontendWriterAdmissionSchema = z
113
- .object({
114
- schemaVersion: z.literal(1),
115
- writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
116
- decision: z.enum(["authorized", "denied"]),
117
- sourceArtifact: z.string().min(1),
118
- artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
119
- checkedAt: z.string().min(1),
120
- reason: z.string().nullable(),
121
- })
122
- .strict();
123
7
  async function selectNode(runDir, primary, fallbacks) {
124
8
  const candidates = [primary, ...fallbacks.filter((id) => id !== primary)];
125
9
  const rejections = [];
@@ -153,10 +37,6 @@ async function readNodeText(runDir, nodeId) {
153
37
  throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
154
38
  return text;
155
39
  }
156
- async function readNodeRawText(runDir, nodeId) {
157
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
158
- return record.assistantText ?? record.stdout ?? "";
159
- }
160
40
  function firstNonEmptyVerdictLine(text) {
161
41
  const lines = text
162
42
  .split(/\r?\n/)
@@ -252,141 +132,16 @@ async function checkOpenspecReadEvidence(input) {
252
132
  }
253
133
  return [...matched];
254
134
  }
255
- async function finalizePrewrite(input, pending) {
256
- const sourceBindingSha256 = input.sourceBinding
257
- ? deterministicSha256(input.sourceBinding)
258
- : null;
259
- const writeSetDigest = input.config.implementationWriteSet
260
- ? deterministicSha256([...new Set(input.config.implementationWriteSet)].sort())
261
- : null;
262
- const prewriteResult = frontendPrewriteResultV1Schema.parse({
263
- schemaVersion: 1,
264
- schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
265
- classification: pending.classification,
266
- normalizationActions: pending.normalizationActions,
267
- candidateRawSha256: pending.candidateRawSha256,
268
- candidateJsonSha256: pending.candidateJsonSha256,
269
- canonicalSha256: pending.canonicalSha256,
270
- selectedPlanNodeId: pending.planNodeId,
271
- sourceBindingSha256,
272
- writeSetDigest,
273
- retryCount: 0,
274
- maxRetryCount: 1,
275
- failureReason: pending.failureReason,
276
- failureCode: pending.failureCode,
277
- digestAlgorithm: "sha256",
278
- });
279
- const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.config.outputDir, FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME), prewriteResult);
280
- return {
281
- ok: true,
282
- classification: pending.classification,
283
- planNodeId: pending.planNodeId,
284
- reviewNodeId: pending.reviewNodeId,
285
- verdict: pending.verdict,
286
- mockStrategy: pending.mockStrategy,
287
- artifact: pending.artifact,
288
- prewriteResult: {
289
- path: written.path,
290
- sha256: written.sha256,
291
- schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
292
- },
293
- failureReason: pending.failureReason,
294
- failureCode: pending.failureCode,
295
- normalizationActions: pending.normalizationActions,
296
- candidateRawSha256: pending.candidateRawSha256,
297
- candidateJsonSha256: pending.candidateJsonSha256,
298
- canonicalSha256: pending.canonicalSha256,
299
- openspecReadPaths: pending.openspecReadPaths,
300
- openspecCandidatePaths: pending.openspecCandidatePaths,
301
- };
302
- }
303
135
  export async function runFrontendPrewriteGate(input) {
304
136
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
305
- // selectNode throws for a scheduling defect (no FINISHED plan/review). That
306
- // remains an ERROR and is intentionally not classified into prewrite-result.
307
- const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
308
- const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
309
- const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
310
- const basePending = {
311
- planNodeId,
312
- reviewNodeId,
313
- verdict: "",
314
- candidateRawSha256,
315
- candidateJsonSha256: null,
316
- canonicalSha256: null,
317
- normalizationActions: [],
318
- mockStrategy: undefined,
319
- artifact: undefined,
320
- failureReason: null,
321
- failureCode: null,
322
- classification: "accepted",
323
- openspecReadPaths: [],
324
- openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
325
- };
326
- let planText;
327
- try {
328
- planText = await readNodeText(input.runDir, planNodeId);
329
- }
330
- catch (error) {
331
- return finalizePrewrite(input, {
332
- ...basePending,
333
- classification: "retryable-invalid",
334
- failureReason: error instanceof Error ? error.message : String(error),
335
- failureCode: "plan-text-unreadable",
336
- });
337
- }
338
- let reviewText;
339
- try {
340
- reviewText = await readNodeText(input.runDir, reviewNodeId);
341
- }
342
- catch (error) {
343
- return finalizePrewrite(input, {
344
- ...basePending,
345
- classification: "blocked",
346
- failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${error instanceof Error ? error.message : String(error)}`,
347
- failureCode: "review-text-unreadable",
348
- });
349
- }
350
- const verdict = firstNonEmptyVerdictLine(reviewText);
351
- if (verdict !== "VERDICT: pass") {
352
- return finalizePrewrite(input, {
353
- ...basePending,
354
- verdict,
355
- classification: "blocked",
356
- failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`,
357
- failureCode: "verdict-not-pass",
358
- });
359
- }
360
- const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
361
- if (missingIds.length > 0) {
362
- return finalizePrewrite(input, {
363
- ...basePending,
364
- verdict,
365
- classification: "blocked",
366
- failureReason: `frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`,
367
- failureCode: "missing-requirement-ids",
368
- });
369
- }
370
137
  if (workspaceRoot) {
371
138
  if (input.config.requireSourceFreshness) {
372
- if (!input.sourceBinding) {
139
+ if (!input.sourceBinding)
373
140
  throw new Error("frontend prewrite gate requires sourceBinding for freshness check");
374
- }
375
- try {
376
- await assertFrontendSourceBindingFresh({
377
- workspaceRoot,
378
- binding: input.sourceBinding,
379
- });
380
- }
381
- catch (error) {
382
- return finalizePrewrite(input, {
383
- ...basePending,
384
- verdict,
385
- classification: "blocked",
386
- failureReason: error instanceof Error ? error.message : String(error),
387
- failureCode: "source-binding-not-fresh",
388
- });
389
- }
141
+ await assertFrontendSourceBindingFresh({
142
+ workspaceRoot,
143
+ binding: input.sourceBinding,
144
+ });
390
145
  }
391
146
  let hasGitMetadata = true;
392
147
  try {
@@ -404,34 +159,31 @@ export async function runFrontendPrewriteGate(input) {
404
159
  workspaceRoot,
405
160
  });
406
161
  }
407
- // Candidate -> canonical conversion. The gate consumes the selected
408
- // plan/revision assistant output directly and is the only conversion point.
409
- let analysis;
410
- try {
411
- analysis = await analyzeFrontendImplementationContract({
412
- runDir: input.runDir,
413
- fromNodeId: planNodeId,
414
- sourceBinding: input.sourceBinding,
415
- });
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"}`);
416
169
  }
417
- catch (error) {
418
- if (error instanceof FrontendContractFailure) {
419
- return finalizePrewrite(input, {
420
- ...basePending,
421
- verdict,
422
- classification: error.kind,
423
- failureReason: error.message,
424
- failureCode: error.kind === "retryable-invalid"
425
- ? "contract-invalid"
426
- : "contract-blocked",
427
- candidateJsonSha256: error.candidateJsonSha256,
428
- normalizationActions: [],
429
- });
430
- }
431
- 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(", ")}`);
432
173
  }
433
- const mockStrategy = analysis.canonical.mockApi.strategy;
434
- 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)) {
435
187
  const quotedAllowed = input.config.allowedMockStrategies
436
188
  .map((strategy) => JSON.stringify(strategy))
437
189
  .join(", ");
@@ -440,71 +192,30 @@ export async function runFrontendPrewriteGate(input) {
440
192
  const reason = forcedNotNeeded
441
193
  ? "the task is auto/not-required or lacks authorized deterministic Mock verification commands"
442
194
  : `allowed strategies are [${quotedAllowed}]`;
443
- return finalizePrewrite(input, {
444
- ...basePending,
445
- verdict,
446
- candidateJsonSha256: analysis.candidateJsonSha256,
447
- normalizationActions: [],
448
- mockStrategy,
449
- classification: "blocked",
450
- 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.`,
451
- failureCode: "mock-strategy-outside-allowed",
452
- });
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.`);
453
196
  }
454
- // Mock command binding (main): a non-not-needed strategy can only be proven
455
- // by the DAG's frozen Mock verification commands. Block before any write is
456
- // authorized when none were materialized at generation time.
457
- 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" &&
458
203
  (input.config.mockCommandLabels?.length ?? 0) === 0) {
459
- return finalizePrewrite(input, {
460
- ...basePending,
461
- verdict,
462
- candidateJsonSha256: analysis.candidateJsonSha256,
463
- normalizationActions: [],
464
- mockStrategy,
465
- classification: "blocked",
466
- 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`,
467
- failureCode: "mock-strategy-no-verification-commands",
468
- });
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`);
469
205
  }
470
206
  if (input.config.implementationWriteSet) {
471
207
  const writeSet = new Set(input.config.implementationWriteSet);
472
- const contractTargets = new Set(analysis.canonical.targets.files);
208
+ const contractTargets = new Set(contract.targets.files);
473
209
  const uncoveredTargets = [...contractTargets].filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
474
210
  if (uncoveredTargets.length > 0) {
475
- return finalizePrewrite(input, {
476
- ...basePending,
477
- verdict,
478
- candidateJsonSha256: analysis.candidateJsonSha256,
479
- normalizationActions: [],
480
- mockStrategy,
481
- classification: "blocked",
482
- failureReason: `frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
483
- failureCode: "target-outside-write-set",
484
- });
211
+ throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
485
212
  }
486
- const nonStaticVTs = analysis.canonical.verificationTargets.filter((vt) => vt.type !== "static");
213
+ const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
487
214
  const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
488
215
  if (uncoveredVTs.length > 0) {
489
- return finalizePrewrite(input, {
490
- ...basePending,
491
- verdict,
492
- candidateJsonSha256: analysis.candidateJsonSha256,
493
- normalizationActions: [],
494
- mockStrategy,
495
- classification: "blocked",
496
- failureReason: `frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
497
- failureCode: "verification-target-outside-write-set",
498
- });
216
+ throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
499
217
  }
500
218
  }
501
- // Materialize the canonical contract only after every governance check passed.
502
- const artifact = await writeFrontendImplementationContractArtifact({
503
- runDir: input.runDir,
504
- outputDir: input.config.outputDir,
505
- artifactName: input.config.artifactName,
506
- canonical: analysis.canonical,
507
- });
508
219
  const candidatePaths = input.config.openspecCandidatePaths ?? [];
509
220
  const openspecReadPaths = await checkOpenspecReadEvidence({
510
221
  runDir: input.runDir,
@@ -513,46 +224,32 @@ export async function runFrontendPrewriteGate(input) {
513
224
  reviewNodeId,
514
225
  repoRoot: workspaceRoot ?? process.cwd(),
515
226
  });
516
- const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
517
- return finalizePrewrite(input, {
518
- ...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,
519
236
  verdict,
520
- classification,
521
- failureReason: null,
522
- candidateJsonSha256: analysis.candidateJsonSha256,
523
- canonicalSha256: artifact.sha256,
524
- normalizationActions: analysis.normalizationActions,
525
- mockStrategy,
237
+ mockStrategy: contract.mockApi.strategy,
526
238
  artifact,
527
239
  openspecReadPaths,
528
240
  openspecCandidatePaths: candidatePaths,
529
- });
241
+ };
530
242
  }
531
243
  export function formatFrontendPrewriteGateStdout(result) {
532
- const lines = [];
533
- if (result.classification === "accepted" ||
534
- result.classification === "accepted-normalized") {
535
- lines.push("Frontend prewrite gate: pass");
536
- }
537
- else if (result.classification === "blocked") {
538
- lines.push("Frontend prewrite gate: blocked");
539
- }
540
- else {
541
- lines.push("Frontend prewrite gate: retryable-invalid");
542
- }
543
- lines.push(`Classification: ${result.classification}`);
544
- lines.push(`Plan: ${result.planNodeId}`);
545
- lines.push(`Review: ${result.reviewNodeId}`);
546
- if (result.mockStrategy)
547
- lines.push(`Mock strategy: ${result.mockStrategy}`);
548
- if (result.artifact) {
549
- lines.push(`Structured artifact: ${result.artifact.path}`);
550
- lines.push(`Schema: ${result.artifact.schemaId}`);
551
- lines.push(`SHA-256: ${result.artifact.sha256}`);
552
- }
553
- lines.push(`Prewrite result: ${result.prewriteResult.path}`);
554
- if (result.failureReason)
555
- 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
+ ];
556
253
  if (result.openspecReadPaths.length > 0) {
557
254
  lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
558
255
  }