@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.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 (101) hide show
  1. package/CHANGELOG.md +67 -16
  2. package/dist/application/task-lifecycle/advance.js +309 -9
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  6. package/dist/commands/init-upgrade.js +32 -1
  7. package/dist/commands/init.js +94 -3
  8. package/dist/executors/dag-pi-executor.js +18 -3
  9. package/dist/executors/shell-executor.js +4 -91
  10. package/dist/executors/shell-write-guard.js +26 -8
  11. package/dist/shared/operator/capabilities.js +98 -44
  12. package/dist/shared/resilient-git.js +133 -0
  13. package/dist/task/source-prepare/artifact-meta.js +137 -0
  14. package/dist/task/source-prepare/index.js +2 -0
  15. package/dist/task/source-prepare/parse-intent.js +58 -10
  16. package/dist/task/source-prepare/prepare.js +229 -18
  17. package/dist/task/source-prepare/reference-integrity.js +18 -2
  18. package/dist/task/source-prepare/semantic-intake.js +404 -0
  19. package/dist/worker/console/app-data.js +2 -0
  20. package/dist/worker/console/chat/chat-event-store.js +190 -25
  21. package/dist/worker/console/chat/model-resolver.js +17 -0
  22. package/dist/worker/console/chat/pi-console-config.js +250 -32
  23. package/dist/worker/console/chat/pi-runtime.js +1007 -188
  24. package/dist/worker/console/chat/resource-loader.js +5 -4
  25. package/dist/worker/console/chat/routes.js +495 -157
  26. package/dist/worker/console/chat/runtime-context.js +48 -12
  27. package/dist/worker/console/chat/runtime-selection.js +59 -0
  28. package/dist/worker/console/chat/session-store.js +39 -0
  29. package/dist/worker/console/chat/shortcuts.js +1 -0
  30. package/dist/worker/console/chat/tool-adapter.js +9 -3
  31. package/dist/worker/console/chat/tools.js +5 -1
  32. package/dist/worker/console/dag-execution-receipt.js +380 -0
  33. package/dist/worker/console/interview/grill-me.js +7 -6
  34. package/dist/worker/console/operator-actions.js +785 -85
  35. package/dist/worker/console/prd-intake-bridge.js +393 -0
  36. package/dist/worker/console/recovery-cta.js +35 -6
  37. package/dist/worker/console/server.js +8 -15
  38. package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
  39. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +1 -0
  42. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  45. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  46. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  47. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  48. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  49. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  50. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  51. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  52. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
  53. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
  54. package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
  55. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  56. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  57. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  58. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  59. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  60. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  61. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
  62. package/dist/worker/delivery/final-verification.js +13 -5
  63. package/dist/worker/delivery/package.js +31 -19
  64. package/dist/worker/delivery/verification-bundle.js +6 -4
  65. package/dist/worker/observability/read-model.js +3 -0
  66. package/dist/worker/observe/static/operator-chrome.css +5 -2
  67. package/dist/worker/observe/static/operator-chrome.js +6 -1
  68. package/dist/worker/observe/static/styles.css +39 -9
  69. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
  70. package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
  71. package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
  72. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  73. package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
  74. package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
  75. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  76. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  77. package/dist/workflows/dag/failure-category.js +3 -0
  78. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  79. package/dist/workflows/dag/init-hybrid.js +49 -55
  80. package/dist/workflows/dag/node-execution.js +3 -2
  81. package/dist/workflows/dag/retry-policy.js +44 -11
  82. package/dist/workflows/dag/runner.js +6 -0
  83. package/dist/workflows/dag/scheduler.js +49 -1
  84. package/dist/workflows/dag/types.js +0 -7
  85. package/dist/workflows/dag/validate.js +16 -2
  86. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  87. package/docs/templates/agent-dag.schema.json +4 -4
  88. package/docs/templates/backend-test-dag.json +29 -32
  89. package/harness.json +1 -1
  90. package/package.json +1 -1
  91. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  92. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  93. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  94. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  95. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  96. package/skills/loop-agent/references/command-reference.md +3 -1
  97. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  98. package/skills/loop-agent/references/task-workflow.md +4 -0
  99. package/dist/worker/console/chat/instruction-skills.js +0 -217
  100. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  101. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -1,6 +1,19 @@
1
1
  const HEADING_ALIASES = {
2
- objective: new Set(["目标", "objective", "问题与目标"]),
3
- scope: new Set(["范围", "scope", "in scope"]),
2
+ objective: new Set([
3
+ "目标",
4
+ "objective",
5
+ "问题与目标",
6
+ "业务目标",
7
+ "需求概述",
8
+ ]),
9
+ scope: new Set([
10
+ "范围",
11
+ "scope",
12
+ "in scope",
13
+ "需求范围",
14
+ "已确认范围",
15
+ "已确认需求",
16
+ ]),
4
17
  nonGoals: new Set(["非目标", "out of scope", "non-goals", "non goals"]),
5
18
  acceptance: new Set(["验收标准", "acceptance criteria", "acceptance"]),
6
19
  constraints: new Set(["约束", "constraints", "不变式", "invariants"]),
@@ -8,6 +21,8 @@ const HEADING_ALIASES = {
8
21
  assumptions: new Set(["假设", "assumptions"]),
9
22
  };
10
23
  const OBJECTIVE_MAX_CHARS = 1200;
24
+ /** Product Requirement V3/V4 style embedded AC heading, e.g. AC-FE-001 / AC-BE-12 / AC-001. */
25
+ const EMBEDDED_AC_HEADING = /^(AC(?:[-_](?:FE|BE|UI|API))?[-_]?\d+(?:[-_]\d+)?)\s*[::]?\s*(.*)$/iu;
11
26
  function stripFencedAndComments(text) {
12
27
  // Remove fenced code blocks and HTML comments so headings inside them are ignored.
13
28
  return text
@@ -18,10 +33,28 @@ function stripFencedAndComments(text) {
18
33
  function normalizeHeadingText(raw) {
19
34
  return raw
20
35
  .trim()
36
+ // Strip leading section numbers: "2. 业务目标" / "3.1 已确认范围" / "3.2、非目标"
37
+ .replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "")
21
38
  .toLowerCase()
22
39
  .replace(/[::]\s*$/u, "")
23
40
  .replace(/\s+/g, " ");
24
41
  }
42
+ function normalizeAcceptanceId(raw) {
43
+ return raw
44
+ .trim()
45
+ .toUpperCase()
46
+ .replace(/_/g, "-")
47
+ .replace(/^(AC)(FE|BE|UI|API)(?=-|\d)/u, "$1-$2");
48
+ }
49
+ function parseEmbeddedAcHeading(headingText) {
50
+ const normalized = headingText.trim().replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "");
51
+ const match = EMBEDDED_AC_HEADING.exec(normalized);
52
+ if (!match)
53
+ return null;
54
+ const id = normalizeAcceptanceId(match[1]);
55
+ const text = match[2]?.trim() || id;
56
+ return { id, text };
57
+ }
25
58
  function classifyHeading(text) {
26
59
  const normalized = normalizeHeadingText(text);
27
60
  for (const [key, aliases] of Object.entries(HEADING_ALIASES)) {
@@ -48,14 +81,17 @@ function parseAcceptanceLine(line, nextId) {
48
81
  const body = (checklist?.[1] ?? bullet?.[1] ?? line.trim()).trim();
49
82
  if (!body)
50
83
  return null;
84
+ const embedded = parseEmbeddedAcHeading(body);
85
+ if (embedded)
86
+ return embedded;
51
87
  const withId = /^(AC[-_]?\d+)\s*[::]\s*(.+)$/iu.exec(body);
52
88
  if (withId) {
53
- return { id: withId[1].toUpperCase().replace("_", "-"), text: withId[2].trim() };
89
+ return { id: normalizeAcceptanceId(withId[1]), text: withId[2].trim() };
54
90
  }
55
91
  const bareAc = /^(AC[-_]?\d+)\s+(.+)$/iu.exec(body);
56
92
  if (bareAc) {
57
93
  return {
58
- id: bareAc[1].toUpperCase().replace("_", "-"),
94
+ id: normalizeAcceptanceId(bareAc[1]),
59
95
  text: bareAc[2].trim(),
60
96
  };
61
97
  }
@@ -114,9 +150,27 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
114
150
  const sections = [];
115
151
  let current = { key: "other", lines: [] };
116
152
  sections.push(current);
153
+ let acCounter = options?.acCounterStart ?? 1;
154
+ const nextId = () => {
155
+ const id = `AC-${String(acCounter).padStart(3, "0")}`;
156
+ acCounter += 1;
157
+ return id;
158
+ };
117
159
  for (const line of lines) {
118
160
  const heading = isAtxHeading(line);
119
161
  if (heading) {
162
+ // Product Requirement V3/V4 embeds formal AC as headings (#### AC-FE-001 ...).
163
+ // Capture the heading as a single AC; GWT body lists under it stay non-AC.
164
+ const embeddedAc = parseEmbeddedAcHeading(heading.text);
165
+ if (embeddedAc) {
166
+ sections.push({
167
+ key: "acceptance",
168
+ lines: [`- ${embeddedAc.id}: ${embeddedAc.text}`],
169
+ });
170
+ current = { key: "other", lines: [] };
171
+ sections.push(current);
172
+ continue;
173
+ }
120
174
  const key = classifyHeading(heading.text);
121
175
  current = { key, lines: [] };
122
176
  sections.push(current);
@@ -124,12 +178,6 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
124
178
  }
125
179
  current.lines.push(line);
126
180
  }
127
- let acCounter = options?.acCounterStart ?? 1;
128
- const nextId = () => {
129
- const id = `AC-${String(acCounter).padStart(3, "0")}`;
130
- acCounter += 1;
131
- return id;
132
- };
133
181
  let objective;
134
182
  const scope = [];
135
183
  const nonGoals = [];
@@ -6,6 +6,7 @@ import { getTaskContractPaths } from "../contract/paths.js";
6
6
  import { loadTaskConfig } from "../runtime.js";
7
7
  import { buildPrepareDraft } from "./build-draft.js";
8
8
  import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, } from "./completeness.js";
9
+ import { detectProductArtifactMeta, isIntakeSoftGapCode, } from "./artifact-meta.js";
9
10
  import { extractRequirementFactsFromMarkdown, fillMissingRequirementFacts, mergeRequirementFacts, } from "./parse-intent.js";
10
11
  import { draftReferencesFromManifest, validateImportedPrdReferences, } from "./reference-integrity.js";
11
12
  import { readSourceManifest } from "../source-references.js";
@@ -131,31 +132,139 @@ function mutationOutcome(code) {
131
132
  }
132
133
  function buildNextSteps(input) {
133
134
  const next = [];
135
+ const codes = new Set(input.gaps.map((g) => g.code));
134
136
  if (!input.hasImportedPrd) {
135
- next.push(`loop-agent import-prd ${input.taskId} --file <prd.md> --role requirement`);
137
+ next.push(`loop-agent task advance ${input.taskId} --prd <product-requirement.md> --allowed-path <glob> --json`);
136
138
  }
137
- if (input.gaps.some((g) => g.code === "EMPTY_ALLOWED_PATHS")) {
138
- next.push(" --allowed-path <glob>");
139
+ if (codes.has("PRODUCT_ANALYSIS_NOT_EXECUTABLE")) {
140
+ next.push("使用同目录 product-requirement.md 作为 --prd(role=requirement);product-analysis.md 仅作引用(--role analysis)");
141
+ next.push("禁止改写原始 product-analysis / product-requirement 产物,禁止在原 PRD 目录生成兼容 Markdown 或 reviewed-draft.json");
139
142
  }
140
- if (input.gaps.some((g) => g.code === "EMPTY_ACCEPTANCE")) {
141
- next.push(" --ac <id:text> 或完善 PRD 验收标准章节");
143
+ if (codes.has("PRODUCT_CLARIFICATION_NOT_EXECUTABLE")) {
144
+ next.push("requirement-clarification.md 是决策追溯,不是可执行 PRD;请导入 complete product-requirement.md");
145
+ }
146
+ if (codes.has("PRODUCT_REQUIREMENT_PENDING")) {
147
+ next.push("先完成 analyze-product-requirements 澄清并将 product-requirement.md 标为 complete,再 task advance --prd");
148
+ }
149
+ if (codes.has("EMPTY_ALLOWED_PATHS")) {
150
+ next.push("补 --allowed-path <glob>(工程边界必须显式给出)");
151
+ }
152
+ if (codes.has("EMPTY_ACCEPTANCE") || codes.has("EMPTY_OBJECTIVE")) {
153
+ next.push("PRD 真缺验收标准/目标时:补完整 product-requirement(Given/When/Then 或 AC 标题),或显式 --from-text;不要自动生成 sidecar draft JSON");
154
+ }
155
+ if (codes.has("SEMANTIC_INTAKE_RECOMMENDED")) {
156
+ next.push("文档语义完整但结构非标准:整理为 product-requirement 或标准「目标/范围/非目标/验收标准」Markdown;不要在原 PRD 目录生成兼容 md/json");
157
+ }
158
+ if (codes.has("SEMANTIC_INTAKE_FAILED") || codes.has("SEMANTIC_INTAKE_INVALID_OUTPUT")) {
159
+ next.push("semantic intake 未能产出可接受 draft:补全 product-requirement 或标准验收标准后重试;禁止在原 PRD 目录生成 sidecar JSON");
142
160
  }
143
161
  if (input.mode === "dry-run" && input.ok) {
144
- next.push(`loop-agent task source prepare ${input.taskId} --use-imported-prd ... --apply --json`);
162
+ next.push(`loop-agent task advance ${input.taskId} --allowed-path <glob> --json`);
145
163
  }
146
164
  if (input.ok && input.mode === "apply") {
147
165
  next.push(`loop-agent task status ${input.taskId} --json`);
148
166
  next.push(`loop-agent task advance ${input.taskId} --profile auto --json`);
149
167
  next.push("审查 DAG writeSet 后再 task advance --approve-gate ...");
150
168
  }
151
- if (input.gaps.some((g) => g.code === "DIRTY_SOURCE" || g.code === "MANAGED_PROJECTION_INCOMPLETE")) {
152
- next.push(`loop-agent task advance ${input.taskId} --from-draft <reviewed-draft.json> --json`);
169
+ if (codes.has("DIRTY_SOURCE") ||
170
+ codes.has("MANAGED_PROJECTION_INCOMPLETE")) {
171
+ next.push(`高级恢复(仅人工 reviewed draft):loop-agent task advance ${input.taskId} --from-draft <reviewed-draft.json> --json`);
172
+ next.push("--from-draft 不是 EMPTY_ACCEPTANCE 的默认恢复路径;禁止在原 PRD 目录自动生成 draft JSON");
153
173
  }
154
- if (input.gaps.some((g) => g.code === "TRANSACTION_INCOMPLETE")) {
174
+ if (codes.has("TRANSACTION_INCOMPLETE")) {
155
175
  next.push(`loop-agent task advance ${input.taskId} --json`);
156
176
  }
157
177
  return next;
158
178
  }
179
+ function appendProductArtifactGaps(input) {
180
+ if (!input.sourceIntegrity)
181
+ return;
182
+ const docs = input.sourceIntegrity.documents.filter((d) => d.content);
183
+ if (docs.length === 0)
184
+ return;
185
+ let sawAnalysisOnly = false;
186
+ let sawClarificationOnly = false;
187
+ let sawPendingRequirement = false;
188
+ let sawExecutableRequirement = false;
189
+ let sawUnknownNarrative = false;
190
+ for (const doc of docs) {
191
+ const meta = detectProductArtifactMeta(doc.content);
192
+ if (meta.artifactType === "product-analysis") {
193
+ sawAnalysisOnly = true;
194
+ continue;
195
+ }
196
+ if (meta.artifactType === "requirement-clarification") {
197
+ sawClarificationOnly = true;
198
+ continue;
199
+ }
200
+ if (meta.artifactType === "product-requirement") {
201
+ if (meta.requirementStatus &&
202
+ meta.requirementStatus.trim().toLowerCase() === "pending") {
203
+ sawPendingRequirement = true;
204
+ }
205
+ else {
206
+ sawExecutableRequirement = true;
207
+ }
208
+ continue;
209
+ }
210
+ if (doc.role === "requirement" &&
211
+ meta.artifactType === "unknown" &&
212
+ !input.hasAcceptance) {
213
+ sawUnknownNarrative = true;
214
+ }
215
+ }
216
+ const hasRequirementRole = input.sourceIntegrity.parseableDocuments.some((d) => d.role === "requirement" || d.role === "acceptance");
217
+ if (!hasRequirementRole && sawAnalysisOnly) {
218
+ input.gaps.push({
219
+ code: "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
220
+ level: "blocking",
221
+ field: "acceptanceCriteria",
222
+ message: "imported document is product-analysis (no formal AC by design); use product-requirement.md as --prd requirement",
223
+ });
224
+ }
225
+ if (!hasRequirementRole &&
226
+ sawClarificationOnly &&
227
+ !sawExecutableRequirement) {
228
+ input.gaps.push({
229
+ code: "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
230
+ level: "blocking",
231
+ field: "acceptanceCriteria",
232
+ message: "imported document is requirement-clarification (decision log only); use complete product-requirement.md",
233
+ });
234
+ }
235
+ if (sawPendingRequirement && !input.hasAcceptance) {
236
+ input.gaps.push({
237
+ code: "PRODUCT_REQUIREMENT_PENDING",
238
+ level: "blocking",
239
+ field: "acceptanceCriteria",
240
+ message: "product-requirement is still pending clarification; complete analyze-product-requirements before task advance",
241
+ });
242
+ }
243
+ if (sawUnknownNarrative &&
244
+ !input.hasAcceptance &&
245
+ !sawPendingRequirement &&
246
+ !sawAnalysisOnly) {
247
+ input.gaps.push({
248
+ code: "SEMANTIC_INTAKE_RECOMMENDED",
249
+ level: "warning",
250
+ message: "requirement body lacks structured AC headings; use product-requirement or standard Markdown sections — do not auto-write sidecar drafts",
251
+ });
252
+ }
253
+ const precise = new Set([
254
+ "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
255
+ "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
256
+ "PRODUCT_REQUIREMENT_PENDING",
257
+ ]);
258
+ if (input.gaps.some((g) => precise.has(g.code))) {
259
+ for (const gap of input.gaps) {
260
+ if (gap.code === "EMPTY_ACCEPTANCE") {
261
+ gap.level = "warning";
262
+ gap.message =
263
+ "acceptance criteria empty (superseded by product-artifact diagnostic)";
264
+ }
265
+ }
266
+ }
267
+ }
159
268
  function emptyObservedHashPlaceholder() {
160
269
  return "0".repeat(64);
161
270
  }
@@ -222,7 +331,19 @@ export async function prepareTaskSource(input) {
222
331
  constraintsPath: contractPaths.constraintsPath,
223
332
  });
224
333
  if (projection.ok) {
225
- baseDraft = projection.draft;
334
+ baseDraft = input.intent.replaceManagedRequirement
335
+ ? {
336
+ ...projection.draft,
337
+ requirement: {
338
+ objective: "",
339
+ scope: [],
340
+ nonGoals: [],
341
+ acceptanceCriteria: [],
342
+ },
343
+ openQuestions: [],
344
+ assumptions: [],
345
+ }
346
+ : projection.draft;
226
347
  }
227
348
  else {
228
349
  managedProjectionError = projection.message;
@@ -287,7 +408,30 @@ export async function prepareTaskSource(input) {
287
408
  taskId,
288
409
  requireParseableRequirement: false,
289
410
  });
290
- hasImportedPrd = true;
411
+ const requestedPaths = new Set((input.intent.importedPrdPaths ?? [])
412
+ .map((value) => value.replace(/\\/g, "/").trim())
413
+ .filter(Boolean));
414
+ if (requestedPaths.size > 0) {
415
+ const selectedDocuments = sourceIntegrity.documents.filter((document) => requestedPaths.has(document.materializedPath.replace(/\\/g, "/")));
416
+ const selectedPaths = new Set(selectedDocuments.map((document) => document.materializedPath.replace(/\\/g, "/")));
417
+ for (const requestedPath of requestedPaths) {
418
+ if (selectedPaths.has(requestedPath))
419
+ continue;
420
+ sourceIntegrity.issues.push({
421
+ code: "REFERENCE_NOT_IN_MANIFEST",
422
+ level: "blocking",
423
+ message: `current import batch reference not found in source-manifest: ${requestedPath}`,
424
+ path: requestedPath,
425
+ });
426
+ sourceIntegrity.ok = false;
427
+ }
428
+ sourceIntegrity = {
429
+ ...sourceIntegrity,
430
+ documents: selectedDocuments,
431
+ parseableDocuments: selectedDocuments.filter((document) => document.parseable),
432
+ };
433
+ }
434
+ hasImportedPrd = sourceIntegrity.documents.length > 0;
291
435
  if (sourceIntegrity.ok) {
292
436
  references = draftReferencesFromManifest(sourceIntegrity.documents);
293
437
  let acStart = 1;
@@ -389,13 +533,48 @@ export async function prepareTaskSource(input) {
389
533
  message: "no parseable requirement document and no --ac / supplemental text; import a requirement-role PRD or pass flags",
390
534
  });
391
535
  }
536
+ // Product Analysis / pending Product Requirement diagnostics (precise gaps).
537
+ if (input.intent.kind === "facts" && input.intent.useImportedPrd) {
538
+ appendProductArtifactGaps({
539
+ gaps,
540
+ sourceIntegrity,
541
+ hasAcceptance: built.draft.requirement.acceptanceCriteria.length > 0,
542
+ });
543
+ }
392
544
  const validation = await validateDraftForTask({
393
545
  repoRoot,
394
546
  taskId,
395
547
  draft: built.draft,
396
548
  });
397
549
  if (!validation.ok) {
550
+ const productArtifactBlocking = gaps.some((gap) => [
551
+ "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
552
+ "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
553
+ "PRODUCT_REQUIREMENT_PENDING",
554
+ ].includes(gap.code));
555
+ const hasEmptyAcceptance = gaps.some((gap) => gap.code === "EMPTY_ACCEPTANCE");
398
556
  for (const message of validation.errors) {
557
+ const isAcceptanceSchema = /acceptance criterion|acceptanceCriteria/i.test(message);
558
+ // Prefer product-artifact diagnostics over generic schema AC errors.
559
+ if (productArtifactBlocking && isAcceptanceSchema) {
560
+ gaps.push({
561
+ code: "DRAFT_VALIDATION",
562
+ level: "warning",
563
+ message: `${message} (superseded by product-artifact diagnostic)`,
564
+ });
565
+ continue;
566
+ }
567
+ // Soft-intake path: completeness already has EMPTY_ACCEPTANCE (soft).
568
+ // Do not add a hard DRAFT_VALIDATION twin that flips softIntake=false
569
+ // and blocks one-shot Semantic Intake for free-form PRDs.
570
+ if (hasEmptyAcceptance && isAcceptanceSchema) {
571
+ gaps.push({
572
+ code: "DRAFT_VALIDATION",
573
+ level: "warning",
574
+ message: `${message} (soft intake; EMPTY_ACCEPTANCE primary)`,
575
+ });
576
+ continue;
577
+ }
399
578
  gaps.push({
400
579
  code: "DRAFT_VALIDATION",
401
580
  level: "blocking",
@@ -428,13 +607,32 @@ export async function prepareTaskSource(input) {
428
607
  .filter((gap) => gap.level === "blocking")
429
608
  .map((gap) => gap.code);
430
609
  const transactionIncomplete = blockingCodes.includes("TRANSACTION_INCOMPLETE");
610
+ const productArtifactCodes = [
611
+ "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
612
+ "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
613
+ "PRODUCT_REQUIREMENT_PENDING",
614
+ "PRODUCT_ARTIFACT_NOT_EXECUTABLE",
615
+ ];
616
+ const primaryBlockingCode = blockingCodes.find((code) => productArtifactCodes.includes(code)) ??
617
+ blockingCodes.find((code) => code !== "EMPTY_ACCEPTANCE") ??
618
+ blockingCodes[0] ??
619
+ "INVALID_INPUT";
620
+ // Soft intake only when PRD was imported (or product-artifact diagnostics apply).
621
+ // Bare missing flags without import stay hard-invalid.
622
+ const softIntakeOnly = blocking &&
623
+ blockingCodes.length > 0 &&
624
+ blockingCodes.every((code) => isIntakeSoftGapCode(code)) &&
625
+ (hasImportedPrd ||
626
+ blockingCodes.some((code) => productArtifactCodes.includes(code)));
431
627
  if (input.mode !== "apply") {
432
628
  return {
433
629
  ok: !blocking,
434
630
  outcome: blocking
435
631
  ? transactionIncomplete
436
632
  ? "needs-reconcile"
437
- : "invalid"
633
+ : softIntakeOnly
634
+ ? "blocked"
635
+ : "invalid"
438
636
  : "succeeded",
439
637
  mode: "dry-run",
440
638
  taskId,
@@ -452,9 +650,13 @@ export async function prepareTaskSource(input) {
452
650
  error: {
453
651
  code: transactionIncomplete
454
652
  ? "TRANSACTION_INCOMPLETE"
455
- : "INVALID_INPUT",
456
- message: "task source draft has blocking gaps",
457
- details: { gaps: blockingCodes },
653
+ : softIntakeOnly
654
+ ? primaryBlockingCode
655
+ : "INVALID_INPUT",
656
+ message: softIntakeOnly
657
+ ? "PRD imported or facts present but intake incomplete; DAG not generated"
658
+ : "task source draft has blocking gaps",
659
+ details: { gaps: blockingCodes, softIntake: softIntakeOnly },
458
660
  },
459
661
  }
460
662
  : {}),
@@ -486,7 +688,9 @@ export async function prepareTaskSource(input) {
486
688
  if (blocking) {
487
689
  const outcome = blockingCodes.includes("DIRTY_SOURCE")
488
690
  ? "blocked"
489
- : "invalid";
691
+ : softIntakeOnly
692
+ ? "blocked"
693
+ : "invalid";
490
694
  return {
491
695
  ok: false,
492
696
  outcome,
@@ -502,10 +706,17 @@ export async function prepareTaskSource(input) {
502
706
  contractStatus: state.effectiveStatus,
503
707
  warnings,
504
708
  error: {
505
- code: outcome === "blocked" ? "DIRTY_SOURCE" : "INVALID_INPUT",
506
- message: "task source draft has blocking gaps",
709
+ code: blockingCodes.includes("DIRTY_SOURCE")
710
+ ? "DIRTY_SOURCE"
711
+ : softIntakeOnly
712
+ ? primaryBlockingCode
713
+ : "INVALID_INPUT",
714
+ message: softIntakeOnly
715
+ ? "PRD imported or facts present but intake incomplete; DAG not generated"
716
+ : "task source draft has blocking gaps",
507
717
  details: {
508
718
  gaps: blockingCodes,
719
+ softIntake: softIntakeOnly,
509
720
  },
510
721
  },
511
722
  };
@@ -5,7 +5,16 @@ import { getTaskPaths } from "../runtime.js";
5
5
  import { readSourceManifest, } from "../source-references.js";
6
6
  import { PREPARE_MAX_FILE_BYTES, PREPARE_MAX_PARSE_DOCS, PREPARE_MAX_TOTAL_BYTES, } from "./types.js";
7
7
  const PARSEABLE_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
8
+ /** Roles that contribute requirement facts (objective/AC/scope). */
8
9
  const PARSEABLE_ROLES = new Set(["requirement", "acceptance"]);
10
+ /** Roles whose body is loaded for artifact meta / diagnostics (not fact extraction). */
11
+ const CONTENT_LOAD_ROLES = new Set([
12
+ "requirement",
13
+ "acceptance",
14
+ "analysis",
15
+ "clarification",
16
+ "design",
17
+ ]);
9
18
  function isSafeRelativeReferencePath(materializedPath) {
10
19
  const normalized = materializedPath.replace(/\\/g, "/").trim();
11
20
  if (!normalized)
@@ -52,6 +61,12 @@ function isParseableDoc(doc) {
52
61
  return false;
53
62
  return PARSEABLE_ROLES.has(doc.role);
54
63
  }
64
+ function shouldLoadContent(doc) {
65
+ const ext = path.extname(doc.materializedPath).toLowerCase();
66
+ if (!PARSEABLE_EXTENSIONS.has(ext))
67
+ return false;
68
+ return CONTENT_LOAD_ROLES.has(doc.role) || PARSEABLE_ROLES.has(doc.role);
69
+ }
55
70
  /**
56
71
  * Validate source-manifest + reference files before deriving a draft.
57
72
  * Fail-closed on taskId/path/hash/symlink/size issues.
@@ -170,6 +185,7 @@ export async function validateImportedPrdReferences(input) {
170
185
  continue;
171
186
  }
172
187
  const parseable = isParseableDoc(doc);
188
+ const loadContent = shouldLoadContent(doc);
173
189
  if (parseable) {
174
190
  parseableCount += 1;
175
191
  if (parseableCount > PREPARE_MAX_PARSE_DOCS) {
@@ -183,7 +199,7 @@ export async function validateImportedPrdReferences(input) {
183
199
  }
184
200
  let hashResult;
185
201
  try {
186
- hashResult = await streamSha256AndBytes(absolutePath, parseable ? PREPARE_MAX_FILE_BYTES : undefined);
202
+ hashResult = await streamSha256AndBytes(absolutePath, loadContent ? PREPARE_MAX_FILE_BYTES : undefined);
187
203
  }
188
204
  catch (error) {
189
205
  if (error &&
@@ -223,7 +239,7 @@ export async function validateImportedPrdReferences(input) {
223
239
  });
224
240
  }
225
241
  let content;
226
- if (parseable) {
242
+ if (loadContent) {
227
243
  totalParseBytes += hashResult.bytes;
228
244
  if (totalParseBytes > PREPARE_MAX_TOTAL_BYTES) {
229
245
  issues.push({