@tea-agent/loop-agent 0.26.0 → 0.26.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 (170) hide show
  1. package/CHANGELOG.md +1032 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/cli/command-definitions.js +25 -10
  5. package/dist/cli/help.js +4 -3
  6. package/dist/cli/program.js +43 -17
  7. package/dist/commands/cursor-prompt.js +6 -6
  8. package/dist/commands/import-prd.js +7 -2
  9. package/dist/commands/init.js +7 -5
  10. package/dist/commands/loop-benchmark.js +11 -11
  11. package/dist/commands/pi-reuse-benchmark.js +16 -16
  12. package/dist/commands/task-source-prepare.js +468 -0
  13. package/dist/executors/dag-pi-executor.js +40 -5
  14. package/dist/executors/shell-write-guard.js +161 -25
  15. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  16. package/dist/task/source-prepare/build-draft.js +215 -0
  17. package/dist/task/source-prepare/completeness.js +195 -0
  18. package/dist/task/source-prepare/index.js +7 -0
  19. package/dist/task/source-prepare/parse-intent.js +373 -0
  20. package/dist/task/source-prepare/path-policy.js +197 -0
  21. package/dist/task/source-prepare/prepare.js +506 -0
  22. package/dist/task/source-prepare/reference-integrity.js +274 -0
  23. package/dist/task/source-prepare/types.js +7 -0
  24. package/dist/worker/observe/static/copy.js +67 -67
  25. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  26. package/dist/worker/observe/static/dag-layout.js +83 -83
  27. package/dist/worker/observe/static/dom.js +220 -220
  28. package/dist/worker/observe/static/relations.js +133 -133
  29. package/dist/worker/observe/static/router.js +93 -93
  30. package/dist/worker/observe/static/run-processing.js +148 -148
  31. package/dist/worker/observe/static/views/batch.js +227 -227
  32. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  33. package/dist/worker/observe/static/views/failures.js +143 -143
  34. package/dist/worker/observe/static/views/feature.js +492 -492
  35. package/dist/worker/observe/static/views/run.js +453 -453
  36. package/dist/worker/observe/static/views/shell.js +7 -7
  37. package/dist/worker/observe/static/views/timeline.js +163 -163
  38. package/dist/workflows/dag/canvas-observer.js +275 -275
  39. package/docs/skills/README.md +7 -7
  40. package/docs/templates/adr.md +60 -60
  41. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  42. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  43. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  44. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  45. package/docs/templates/agent-dag-report.schema.json +473 -473
  46. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  47. package/docs/templates/backend-test-result.schema.json +99 -99
  48. package/docs/templates/feature-spec.md +53 -53
  49. package/docs/templates/frontend-design-contract.md +42 -42
  50. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  51. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  52. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  53. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  54. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  55. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  56. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  57. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  58. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  59. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  60. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  61. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  62. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  63. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  64. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  65. package/docs/templates/frontend-eval/metrics.md +138 -138
  66. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  67. package/docs/templates/frontend-task-constraints.md +35 -35
  68. package/docs/templates/frontend-task-requirement.md +70 -70
  69. package/docs/templates/init-evolution-review.md +35 -35
  70. package/docs/templates/init-managed-agents.md +10 -5
  71. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  72. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  73. package/docs/templates/knowledge-sync-dag.json +178 -178
  74. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  75. package/docs/templates/product-line/closeout.yaml +9 -9
  76. package/docs/templates/product-line/design.md +13 -13
  77. package/docs/templates/product-line/links.md +10 -10
  78. package/docs/templates/product-line/requirement.md +17 -17
  79. package/docs/templates/product-line/test-plan.md +7 -7
  80. package/docs/templates/production-readiness-checklist.md +57 -57
  81. package/docs/templates/project-start-checklist.md +9 -9
  82. package/docs/templates/qa-report.md +48 -48
  83. package/docs/templates/sprint-contract.md +29 -29
  84. package/docs/templates/worker-dogfood-evidence.md +80 -80
  85. package/docs/templates/worker-dogfood-setup.md +68 -68
  86. package/package.json +1 -1
  87. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  88. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  89. package/scripts/kb-graph-materialize.mjs +105 -105
  90. package/scripts/kb-graph-promote.mjs +164 -164
  91. package/scripts/kb-query.mjs +554 -554
  92. package/skills/ai-engineering-context/SKILL.md +48 -48
  93. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  94. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  95. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  96. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  97. package/skills/analyze-product-dependencies/references/example.md +76 -76
  98. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  99. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  100. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  101. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  102. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  103. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  104. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  105. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  106. package/skills/analyze-product-requirements/SKILL.md +90 -90
  107. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  108. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  109. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  110. package/skills/analyze-product-requirements/references/example.md +86 -86
  111. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  112. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  113. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  114. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  115. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  116. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  117. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  118. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  119. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  120. package/skills/browser-tools/browser-content.js +103 -103
  121. package/skills/browser-tools/browser-cookies.js +35 -35
  122. package/skills/browser-tools/browser-eval.js +53 -53
  123. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  124. package/skills/browser-tools/browser-nav.js +44 -44
  125. package/skills/browser-tools/browser-pick.js +162 -162
  126. package/skills/browser-tools/browser-screenshot.js +34 -34
  127. package/skills/browser-tools/browser-start.js +86 -86
  128. package/skills/browser-tools/package-lock.json +2556 -2556
  129. package/skills/browser-tools/package.json +19 -19
  130. package/skills/code-review-core/SKILL.md +20 -20
  131. package/skills/codebase-scout/SKILL.md +19 -19
  132. package/skills/grill-me/SKILL.md +10 -10
  133. package/skills/loop-agent/SKILL.md +5 -2
  134. package/skills/loop-agent/references/README.md +67 -67
  135. package/skills/loop-agent/references/command-reference.md +17 -15
  136. package/skills/loop-agent/references/docs-converge.md +126 -126
  137. package/skills/loop-agent/references/harness-policy.md +3 -4
  138. package/skills/loop-agent/references/learned/README.md +21 -21
  139. package/skills/loop-agent/references/long-running-loop.md +57 -57
  140. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  141. package/skills/loop-agent/references/pi-prompt.md +23 -23
  142. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  143. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  144. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  145. package/skills/loop-agent/references/task-workflow.md +7 -5
  146. package/skills/playwright-cli/SKILL.md +420 -420
  147. package/skills/playwright-cli/references/element-attributes.md +23 -23
  148. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  149. package/skills/playwright-cli/references/request-mocking.md +87 -87
  150. package/skills/playwright-cli/references/running-code.md +241 -241
  151. package/skills/playwright-cli/references/session-management.md +225 -225
  152. package/skills/playwright-cli/references/storage-state.md +275 -275
  153. package/skills/playwright-cli/references/test-generation.md +433 -433
  154. package/skills/playwright-cli/references/tracing.md +139 -139
  155. package/skills/playwright-cli/references/video-recording.md +143 -143
  156. package/skills/requesting-code-review/SKILL.md +101 -101
  157. package/skills/requesting-code-review/code-reviewer.md +168 -168
  158. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  159. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  160. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  161. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  162. package/skills/systematic-debugging/find-polluter.sh +63 -63
  163. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  164. package/skills/systematic-debugging/test-academic.md +14 -14
  165. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  166. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  167. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  168. package/skills/using-git-worktrees/SKILL.md +215 -215
  169. package/skills/verification-before-completion/SKILL.md +154 -154
  170. package/skills/webapp-testing/SKILL.md +19 -19
@@ -0,0 +1,506 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat } from "node:fs/promises";
3
+ import { ContractMutationError, applyTaskContract, logicalRevisionForState, observeTaskContractState, projectConstraintsMarkdown, projectRequirementMarkdown, sha256OfCanonicalJson, validateDraftForTask, } from "../contract/index.js";
4
+ import { getTaskContractPaths } from "../contract/paths.js";
5
+ import { loadTaskConfig } from "../runtime.js";
6
+ import { buildPrepareDraft } from "./build-draft.js";
7
+ import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, } from "./completeness.js";
8
+ import { extractRequirementFactsFromMarkdown, fillMissingRequirementFacts, mergeRequirementFacts, } from "./parse-intent.js";
9
+ import { draftReferencesFromManifest, validateImportedPrdReferences, } from "./reference-integrity.js";
10
+ import { readSourceManifest } from "../source-references.js";
11
+ import { PREPARE_MAX_FILE_BYTES } from "./types.js";
12
+ async function fileNonEmpty(filePath) {
13
+ try {
14
+ const stats = await lstat(filePath);
15
+ return stats.isFile() && stats.size > 0;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function loadExistingTaskConfig(repoRoot, taskId) {
22
+ try {
23
+ return await loadTaskConfig(repoRoot, taskId);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function mutationOutcome(code) {
30
+ switch (code) {
31
+ case "REVISION_CONFLICT":
32
+ case "OBSERVED_HASH_CONFLICT":
33
+ case "REQUEST_ID_REUSE_CONFLICT":
34
+ case "LEASE_HELD":
35
+ return "conflict";
36
+ case "NOT_FOUND":
37
+ return "not-found";
38
+ case "TRANSACTION_INCOMPLETE":
39
+ case "OPERATION_NEEDS_RECONCILE":
40
+ return "needs-reconcile";
41
+ case "EXTERNALLY_MODIFIED":
42
+ case "NOT_MANAGED":
43
+ case "BINDING_DRIFT":
44
+ return "blocked";
45
+ case "INVALID_INPUT":
46
+ case "PATH_OUTSIDE_REPO":
47
+ case "UNSUPPORTED_SCHEMA_VERSION":
48
+ return "invalid";
49
+ default:
50
+ return "rejected";
51
+ }
52
+ }
53
+ function buildNextSteps(input) {
54
+ const next = [];
55
+ if (!input.hasImportedPrd) {
56
+ next.push(`loop-agent import-prd ${input.taskId} --file <prd.md> --role requirement`);
57
+ }
58
+ if (input.gaps.some((g) => g.code === "EMPTY_ALLOWED_PATHS")) {
59
+ next.push("补 --allowed-path <glob>");
60
+ }
61
+ if (input.gaps.some((g) => g.code === "EMPTY_ACCEPTANCE")) {
62
+ next.push("补 --ac <id:text> 或完善 PRD 验收标准章节");
63
+ }
64
+ if (input.mode === "dry-run" && input.ok) {
65
+ next.push(`loop-agent task source prepare ${input.taskId} --use-imported-prd ... --apply --json`);
66
+ }
67
+ if (input.ok && input.mode === "apply") {
68
+ next.push(`loop-agent task contract show ${input.taskId} --json`);
69
+ next.push(`loop-agent dag run-task ${input.taskId} --profile auto --strict-models`);
70
+ next.push("审查 DAG writeSet 后再 run-dag");
71
+ }
72
+ if (input.gaps.some((g) => g.code === "DIRTY_SOURCE")) {
73
+ next.push("loop-agent task contract show|diff|adopt|recover");
74
+ next.push("或显式 --force-overwrite-source 后重跑 prepare");
75
+ }
76
+ if (input.gaps.some((g) => g.code === "TRANSACTION_INCOMPLETE")) {
77
+ next.push(`loop-agent task contract recover --task ${input.taskId}`);
78
+ }
79
+ return next;
80
+ }
81
+ function emptyObservedHashPlaceholder() {
82
+ return "0".repeat(64);
83
+ }
84
+ /**
85
+ * Core prepare orchestration: facts → thin Draft → gates → optional apply.
86
+ * No LLM. Production md writes only via applyTaskContract projection.
87
+ */
88
+ export async function prepareTaskSource(input) {
89
+ const { repoRoot, taskId } = input;
90
+ const contractPaths = getTaskContractPaths(repoRoot, taskId);
91
+ const state = await observeTaskContractState(repoRoot, taskId);
92
+ if (state.observationErrors.some((e) => e.code === "NOT_FOUND")) {
93
+ return {
94
+ ok: false,
95
+ outcome: "not-found",
96
+ mode: input.mode,
97
+ taskId,
98
+ gaps: [
99
+ {
100
+ code: "TASK_NOT_FOUND",
101
+ level: "blocking",
102
+ message: `task not found: ${taskId}`,
103
+ },
104
+ ],
105
+ risks: [],
106
+ next: [`loop-agent new-task ${taskId} "<title>"`],
107
+ error: {
108
+ code: "NOT_FOUND",
109
+ message: `task not found: ${taskId}`,
110
+ },
111
+ };
112
+ }
113
+ const existingTaskConfig = await loadExistingTaskConfig(repoRoot, taskId);
114
+ const sourceFilesPresent = {
115
+ requirement: await fileNonEmpty(contractPaths.requirementPath),
116
+ constraints: await fileNonEmpty(contractPaths.constraintsPath),
117
+ };
118
+ // Contract may record referenceManifestSha256 while observe only reports
119
+ // EXTERNALLY_MODIFIED after the manifest file disappears (hash already drifts).
120
+ // That is a non-fresh observation condition force must not bypass.
121
+ if (state.ref?.sourceHashes.referenceManifestSha256) {
122
+ const manifest = await readSourceManifest(contractPaths.sourceDir);
123
+ if (!manifest) {
124
+ state.observationErrors = [
125
+ ...state.observationErrors,
126
+ {
127
+ code: "MANIFEST_MISSING",
128
+ path: contractPaths.sourceManifestPath,
129
+ message: "contract records referenceManifestSha256 but source-manifest.json is missing",
130
+ },
131
+ ];
132
+ }
133
+ }
134
+ let baseDraft;
135
+ let facts;
136
+ let sourceIntegrity = null;
137
+ let hasImportedPrd = false;
138
+ let references;
139
+ if (input.intent.kind === "draft") {
140
+ baseDraft = input.intent.draft;
141
+ if (baseDraft.taskId !== taskId) {
142
+ return {
143
+ ok: false,
144
+ outcome: "invalid",
145
+ mode: input.mode,
146
+ taskId,
147
+ gaps: [
148
+ {
149
+ code: "DRAFT_TASK_ID_MISMATCH",
150
+ level: "blocking",
151
+ message: `draft.taskId "${baseDraft.taskId}" does not match ${taskId}`,
152
+ },
153
+ ],
154
+ risks: [],
155
+ next: [],
156
+ error: {
157
+ code: "INVALID_INPUT",
158
+ message: `draft.taskId "${baseDraft.taskId}" does not match ${taskId}`,
159
+ },
160
+ };
161
+ }
162
+ // Still verify any references in draft against manifest when present.
163
+ if (baseDraft.references?.length) {
164
+ sourceIntegrity = await validateImportedPrdReferences({
165
+ repoRoot,
166
+ taskId,
167
+ requireParseableRequirement: false,
168
+ });
169
+ hasImportedPrd = sourceIntegrity.documents.length > 0;
170
+ const allowedRefs = new Set(sourceIntegrity.documents.map((document) => document.materializedPath));
171
+ for (const ref of baseDraft.references) {
172
+ const normalizedRef = ref.ref.replace(/\\/g, "/");
173
+ if (!allowedRefs.has(normalizedRef)) {
174
+ sourceIntegrity.issues.push({
175
+ code: "REFERENCE_NOT_IN_MANIFEST",
176
+ level: "blocking",
177
+ message: `draft reference not found in source-manifest: ${ref.ref}`,
178
+ path: ref.ref,
179
+ });
180
+ sourceIntegrity.ok = false;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ else {
186
+ const importedFactParts = [];
187
+ let supplementalFacts;
188
+ if (input.intent.useImportedPrd) {
189
+ sourceIntegrity = await validateImportedPrdReferences({
190
+ repoRoot,
191
+ taskId,
192
+ requireParseableRequirement: false,
193
+ });
194
+ hasImportedPrd = true;
195
+ if (sourceIntegrity.ok) {
196
+ references = draftReferencesFromManifest(sourceIntegrity.documents);
197
+ let acStart = 1;
198
+ for (const doc of sourceIntegrity.parseableDocuments) {
199
+ if (!doc.content)
200
+ continue;
201
+ const extracted = extractRequirementFactsFromMarkdown(doc.content, {
202
+ sourceLabel: doc.materializedPath,
203
+ acCounterStart: acStart,
204
+ });
205
+ acStart += extracted.acceptanceCriteria.filter((ac) => /^AC-\d+$/i.test(ac.id)).length;
206
+ // Better: count all auto ids; keep simple sequential across docs.
207
+ const maxAuto = extracted.acceptanceCriteria.reduce((max, ac) => {
208
+ const m = /^AC-(\d+)$/i.exec(ac.id);
209
+ if (!m)
210
+ return max;
211
+ return Math.max(max, Number(m[1]));
212
+ }, acStart - 1);
213
+ acStart = maxAuto + 1;
214
+ importedFactParts.push(extracted);
215
+ }
216
+ }
217
+ }
218
+ if (input.intent.text) {
219
+ if (Buffer.byteLength(input.intent.text, "utf-8") > PREPARE_MAX_FILE_BYTES) {
220
+ return {
221
+ ok: false,
222
+ outcome: "invalid",
223
+ mode: input.mode,
224
+ taskId,
225
+ gaps: [
226
+ {
227
+ code: "INPUT_TOO_LARGE",
228
+ level: "blocking",
229
+ message: `text input exceeds ${PREPARE_MAX_FILE_BYTES} bytes`,
230
+ },
231
+ ],
232
+ risks: [],
233
+ next: [],
234
+ error: {
235
+ code: "INPUT_TOO_LARGE",
236
+ message: `text input exceeds ${PREPARE_MAX_FILE_BYTES} bytes`,
237
+ },
238
+ };
239
+ }
240
+ supplementalFacts = extractRequirementFactsFromMarkdown(input.intent.text, {
241
+ sourceLabel: input.intent.textLabel ?? "supplemental-text",
242
+ });
243
+ }
244
+ const importedFacts = importedFactParts.length > 0
245
+ ? mergeRequirementFacts(importedFactParts)
246
+ : undefined;
247
+ const mergedSupplemental = supplementalFacts
248
+ ? mergeRequirementFacts([supplementalFacts])
249
+ : undefined;
250
+ if (importedFacts && mergedSupplemental) {
251
+ facts = fillMissingRequirementFacts(importedFacts, mergedSupplemental);
252
+ }
253
+ else {
254
+ facts = importedFacts ?? mergedSupplemental;
255
+ }
256
+ }
257
+ const built = buildPrepareDraft({
258
+ taskId,
259
+ existingTaskConfig,
260
+ baseDraft,
261
+ facts,
262
+ flags: input.flags,
263
+ references,
264
+ });
265
+ const { gaps, risks } = listPrepareGaps({
266
+ draft: built.draft,
267
+ state,
268
+ sourceIntegrity,
269
+ pathAssessment: built.pathAssessment,
270
+ hasImportedPrd,
271
+ acceptanceConflicts: built.acceptanceConflicts,
272
+ sourceFilesPresent,
273
+ forceOverwriteSource: input.forceOverwriteSource,
274
+ });
275
+ // If --use-imported-prd and no parseable requirement and no AC from flags/text → blocking
276
+ if (input.intent.kind === "facts" &&
277
+ input.intent.useImportedPrd &&
278
+ sourceIntegrity &&
279
+ !sourceIntegrity.parseableDocuments.some((d) => d.role === "requirement") &&
280
+ built.draft.requirement.acceptanceCriteria.length === 0 &&
281
+ !input.flags.acceptanceCriteria?.length &&
282
+ !input.intent.text) {
283
+ gaps.push({
284
+ code: "NO_PARSEABLE_REQUIREMENT",
285
+ level: "blocking",
286
+ message: "no parseable requirement document and no --ac / supplemental text; import a requirement-role PRD or pass flags",
287
+ });
288
+ }
289
+ const validation = await validateDraftForTask({
290
+ repoRoot,
291
+ taskId,
292
+ draft: built.draft,
293
+ });
294
+ if (!validation.ok) {
295
+ for (const message of validation.errors) {
296
+ gaps.push({
297
+ code: "DRAFT_VALIDATION",
298
+ level: "blocking",
299
+ message,
300
+ });
301
+ }
302
+ }
303
+ const requirementPreview = projectRequirementMarkdown(built.draft);
304
+ const constraintsPreview = projectConstraintsMarkdown(built.draft);
305
+ const projected = {
306
+ requirementPath: "source/需求.md",
307
+ constraintsPath: "source/执行约束.md",
308
+ requirementPreview,
309
+ constraintsPreview,
310
+ };
311
+ const blocking = hasBlockingGaps(gaps) ||
312
+ hasBlockingRisks(risks) ||
313
+ !validation.ok;
314
+ const warnings = gaps
315
+ .filter((g) => g.level === "warning")
316
+ .map((g) => ({ code: g.code, message: g.message }));
317
+ const next = buildNextSteps({
318
+ taskId,
319
+ hasImportedPrd,
320
+ mode: input.mode,
321
+ ok: !blocking,
322
+ gaps,
323
+ });
324
+ if (input.mode !== "apply") {
325
+ return {
326
+ ok: !blocking,
327
+ outcome: blocking ? "invalid" : "succeeded",
328
+ mode: "dry-run",
329
+ taskId,
330
+ taskKind: built.draft.taskKind,
331
+ title: built.draft.title,
332
+ draft: built.draft,
333
+ gaps,
334
+ risks,
335
+ projected,
336
+ next,
337
+ contractStatus: state.effectiveStatus,
338
+ warnings,
339
+ ...(blocking
340
+ ? {
341
+ error: {
342
+ code: "INVALID_INPUT",
343
+ message: "task source draft has blocking gaps",
344
+ details: {
345
+ gaps: gaps
346
+ .filter((g) => g.level === "blocking")
347
+ .map((g) => g.code),
348
+ },
349
+ },
350
+ }
351
+ : {}),
352
+ };
353
+ }
354
+ // apply path — prioritize state-machine outcomes over generic invalid gates
355
+ if (state.effectiveStatus === "transaction-incomplete" ||
356
+ gaps.some((g) => g.code === "TRANSACTION_INCOMPLETE")) {
357
+ return {
358
+ ok: false,
359
+ outcome: "needs-reconcile",
360
+ mode: "apply",
361
+ taskId,
362
+ taskKind: built.draft.taskKind,
363
+ title: built.draft.title,
364
+ draft: built.draft,
365
+ gaps,
366
+ risks,
367
+ projected,
368
+ next: [`loop-agent task contract recover --task ${taskId}`],
369
+ contractStatus: state.effectiveStatus,
370
+ warnings,
371
+ error: {
372
+ code: "TRANSACTION_INCOMPLETE",
373
+ message: "unfinished transaction; run task contract recover first",
374
+ },
375
+ };
376
+ }
377
+ if (blocking) {
378
+ const blockingCodes = gaps
379
+ .filter((g) => g.level === "blocking")
380
+ .map((g) => g.code);
381
+ const outcome = blockingCodes.includes("DIRTY_SOURCE")
382
+ ? "blocked"
383
+ : "invalid";
384
+ return {
385
+ ok: false,
386
+ outcome,
387
+ mode: "apply",
388
+ taskId,
389
+ taskKind: built.draft.taskKind,
390
+ title: built.draft.title,
391
+ draft: built.draft,
392
+ gaps,
393
+ risks,
394
+ projected,
395
+ next,
396
+ contractStatus: state.effectiveStatus,
397
+ warnings,
398
+ error: {
399
+ code: outcome === "blocked" ? "DIRTY_SOURCE" : "INVALID_INPUT",
400
+ message: "task source draft has blocking gaps",
401
+ details: {
402
+ gaps: blockingCodes,
403
+ },
404
+ },
405
+ };
406
+ }
407
+ const expectedRevision = logicalRevisionForState(state);
408
+ const expectedObservedHash = state.observedCanonicalHash ??
409
+ (expectedRevision === 0 ? emptyObservedHashPlaceholder() : null);
410
+ if (!expectedObservedHash) {
411
+ return {
412
+ ok: false,
413
+ outcome: "conflict",
414
+ mode: "apply",
415
+ taskId,
416
+ draft: built.draft,
417
+ gaps,
418
+ risks,
419
+ projected,
420
+ next: [`loop-agent task contract doctor --task ${taskId}`],
421
+ error: {
422
+ code: "OBSERVED_HASH_CONFLICT",
423
+ message: "unable to determine expectedObservedHash",
424
+ },
425
+ };
426
+ }
427
+ // Dirty without force already blocked above. Fresh legacy is allowed.
428
+ void isFreshLegacyUnversioned;
429
+ const requestId = input.requestId?.trim() || `prepare-${taskId}-${randomUUID()}`;
430
+ const requestPayloadSha256 = sha256OfCanonicalJson(built.draft);
431
+ try {
432
+ const applyResult = await applyTaskContract({
433
+ repoRoot,
434
+ taskId,
435
+ draft: built.draft,
436
+ expectedRevision,
437
+ expectedObservedHash,
438
+ requestId,
439
+ requestPayloadSha256,
440
+ payloadSha256Trusted: true,
441
+ });
442
+ return {
443
+ ok: true,
444
+ outcome: "succeeded",
445
+ mode: "apply",
446
+ taskId,
447
+ taskKind: built.draft.taskKind,
448
+ title: built.draft.title,
449
+ draft: built.draft,
450
+ gaps,
451
+ risks,
452
+ projected,
453
+ apply: applyResult,
454
+ next: buildNextSteps({
455
+ taskId,
456
+ hasImportedPrd,
457
+ mode: "apply",
458
+ ok: true,
459
+ gaps,
460
+ }),
461
+ contractStatus: "managed",
462
+ warnings,
463
+ };
464
+ }
465
+ catch (error) {
466
+ if (error instanceof ContractMutationError) {
467
+ const outcome = mutationOutcome(error.code);
468
+ return {
469
+ ok: false,
470
+ outcome,
471
+ mode: "apply",
472
+ taskId,
473
+ taskKind: built.draft.taskKind,
474
+ title: built.draft.title,
475
+ draft: built.draft,
476
+ gaps,
477
+ risks,
478
+ projected,
479
+ next: [
480
+ `loop-agent task contract show ${taskId} --json`,
481
+ `loop-agent task contract doctor --task ${taskId}`,
482
+ ],
483
+ error: {
484
+ code: error.code,
485
+ message: error.message,
486
+ details: error.details,
487
+ },
488
+ };
489
+ }
490
+ return {
491
+ ok: false,
492
+ outcome: "rejected",
493
+ mode: "apply",
494
+ taskId,
495
+ draft: built.draft,
496
+ gaps,
497
+ risks,
498
+ projected,
499
+ next: [],
500
+ error: {
501
+ code: "INTERNAL_ERROR",
502
+ message: error instanceof Error ? error.message : String(error),
503
+ },
504
+ };
505
+ }
506
+ }