@tea-agent/loop-agent 0.33.5 → 0.33.6

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 (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/application/task-lifecycle/advance.js +254 -4
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/commands/init-upgrade.js +32 -1
  6. package/dist/commands/init.js +94 -3
  7. package/dist/executors/shell-write-guard.js +26 -8
  8. package/dist/shared/operator/capabilities.js +72 -42
  9. package/dist/shared/resilient-git.js +133 -0
  10. package/dist/task/source-prepare/artifact-meta.js +137 -0
  11. package/dist/task/source-prepare/index.js +2 -0
  12. package/dist/task/source-prepare/parse-intent.js +58 -10
  13. package/dist/task/source-prepare/prepare.js +180 -16
  14. package/dist/task/source-prepare/reference-integrity.js +18 -2
  15. package/dist/task/source-prepare/semantic-intake.js +404 -0
  16. package/dist/worker/console/app-data.js +2 -0
  17. package/dist/worker/console/chat/chat-event-store.js +190 -25
  18. package/dist/worker/console/chat/pi-console-config.js +250 -32
  19. package/dist/worker/console/chat/pi-runtime.js +625 -71
  20. package/dist/worker/console/chat/resource-loader.js +5 -4
  21. package/dist/worker/console/chat/routes.js +324 -146
  22. package/dist/worker/console/chat/runtime-context.js +48 -12
  23. package/dist/worker/console/chat/runtime-selection.js +59 -0
  24. package/dist/worker/console/chat/shortcuts.js +1 -0
  25. package/dist/worker/console/chat/tool-adapter.js +9 -3
  26. package/dist/worker/console/chat/tools.js +5 -1
  27. package/dist/worker/console/dag-execution-receipt.js +380 -0
  28. package/dist/worker/console/operator-actions.js +559 -68
  29. package/dist/worker/console/server.js +8 -15
  30. package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
  31. package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
  32. package/dist/worker/console/static/index.html +2 -2
  33. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  34. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  35. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  36. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
  37. package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
  38. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  39. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  40. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  41. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  42. package/dist/worker/delivery/final-verification.js +13 -5
  43. package/dist/worker/delivery/package.js +31 -19
  44. package/dist/worker/delivery/verification-bundle.js +6 -4
  45. package/dist/worker/observe/static/operator-chrome.css +5 -2
  46. package/dist/worker/observe/static/operator-chrome.js +6 -1
  47. package/dist/worker/observe/static/styles.css +39 -9
  48. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  49. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  50. package/harness.json +1 -1
  51. package/package.json +1 -1
  52. package/skills/loop-agent/references/command-reference.md +3 -1
  53. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  54. package/skills/loop-agent/references/task-workflow.md +4 -0
  55. package/dist/worker/console/chat/instruction-skills.js +0 -217
  56. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  57. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Product Analysis / Product Requirement artifact frontmatter detection.
3
+ * Deterministic; no LLM. Used to route intake errors and parseable roles.
4
+ */
5
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u;
6
+ function parseYamlishScalar(raw) {
7
+ const trimmed = raw.trim();
8
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
9
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
10
+ return trimmed.slice(1, -1).trim();
11
+ }
12
+ return trimmed;
13
+ }
14
+ function parseSimpleFrontmatter(block) {
15
+ const out = {};
16
+ for (const line of block.split(/\r?\n/)) {
17
+ const match = /^([A-Za-z0-9_-]+)\s*:\s*(.+?)\s*$/u.exec(line);
18
+ if (!match)
19
+ continue;
20
+ out[match[1]] = parseYamlishScalar(match[2]);
21
+ }
22
+ return out;
23
+ }
24
+ function normalizeArtifactType(raw) {
25
+ if (!raw)
26
+ return "unknown";
27
+ const value = raw.trim().toLowerCase();
28
+ if (value === "product-requirement" || value === "product_requirement") {
29
+ return "product-requirement";
30
+ }
31
+ if (value === "product-analysis" || value === "product_analysis") {
32
+ return "product-analysis";
33
+ }
34
+ if (value === "requirement-clarification" ||
35
+ value === "requirement_clarification" ||
36
+ value === "product-clarification") {
37
+ return "requirement-clarification";
38
+ }
39
+ return "unknown";
40
+ }
41
+ /**
42
+ * Detect Product Analysis V3/V4 style artifact metadata from markdown body.
43
+ */
44
+ export function detectProductArtifactMeta(markdown) {
45
+ const match = FRONTMATTER_RE.exec(markdown);
46
+ if (!match) {
47
+ return { artifactType: "unknown", recognized: false };
48
+ }
49
+ const fields = parseSimpleFrontmatter(match[1]);
50
+ const artifactType = normalizeArtifactType(fields.artifact_type);
51
+ const recognized = artifactType !== "unknown" || Boolean(fields.artifact_version);
52
+ return {
53
+ artifactType,
54
+ ...(fields.artifact_version
55
+ ? { artifactVersion: fields.artifact_version }
56
+ : {}),
57
+ ...(fields.requirement_status
58
+ ? { requirementStatus: fields.requirement_status }
59
+ : {}),
60
+ ...(fields.analysis_status
61
+ ? { analysisStatus: fields.analysis_status }
62
+ : {}),
63
+ ...(fields.requirement_id ? { requirementId: fields.requirement_id } : {}),
64
+ ...(fields.analysis_scope ? { analysisScope: fields.analysis_scope } : {}),
65
+ recognized,
66
+ };
67
+ }
68
+ /**
69
+ * Infer import role from file path basename when --role is omitted.
70
+ */
71
+ export function inferPrdImportRoleFromPath(filePath) {
72
+ const base = filePath
73
+ .replace(/\\/g, "/")
74
+ .split("/")
75
+ .pop()
76
+ ?.toLowerCase()
77
+ .replace(/\.(md|markdown|txt)$/u, "") ?? "";
78
+ if (base === "product-analysis" ||
79
+ base.endsWith("-product-analysis") ||
80
+ base.includes("product-analysis")) {
81
+ return "analysis";
82
+ }
83
+ if (base === "requirement-clarification" ||
84
+ base.endsWith("-requirement-clarification") ||
85
+ base.includes("requirement-clarification") ||
86
+ base.includes("clarification")) {
87
+ return "clarification";
88
+ }
89
+ if (base === "product-requirement" ||
90
+ base.endsWith("-product-requirement") ||
91
+ base.includes("product-requirement")) {
92
+ return "requirement";
93
+ }
94
+ if (base.includes("acceptance"))
95
+ return "acceptance";
96
+ return "requirement";
97
+ }
98
+ /**
99
+ * Prefer path inference; if markdown frontmatter is available and more specific, use it.
100
+ */
101
+ export function resolvePrdImportRole(input) {
102
+ if (input.explicitRole?.trim())
103
+ return input.explicitRole.trim();
104
+ if (input.markdown) {
105
+ const meta = detectProductArtifactMeta(input.markdown);
106
+ if (meta.artifactType === "product-analysis")
107
+ return "analysis";
108
+ if (meta.artifactType === "requirement-clarification") {
109
+ return "clarification";
110
+ }
111
+ if (meta.artifactType === "product-requirement")
112
+ return "requirement";
113
+ }
114
+ return inferPrdImportRoleFromPath(input.filePath);
115
+ }
116
+ /** Roles that may contribute requirement facts (objective/AC/scope). */
117
+ export const REQUIREMENT_FACT_ROLES = new Set(["requirement", "acceptance"]);
118
+ /** Roles that are archival references only (not fact sources). */
119
+ export const REFERENCE_ONLY_ROLES = new Set(["analysis", "clarification", "design"]);
120
+ export function isIntakeSoftGapCode(code) {
121
+ return (code === "EMPTY_ACCEPTANCE" ||
122
+ code === "EMPTY_OBJECTIVE" ||
123
+ code === "EMPTY_ALLOWED_PATHS" ||
124
+ code === "EMPTY_TASK_KIND" ||
125
+ code === "NO_PARSEABLE_REQUIREMENT" ||
126
+ code === "PRODUCT_ANALYSIS_NOT_EXECUTABLE" ||
127
+ code === "PRODUCT_REQUIREMENT_PENDING" ||
128
+ code === "PRODUCT_CLARIFICATION_NOT_EXECUTABLE" ||
129
+ code === "PRODUCT_ARTIFACT_NOT_EXECUTABLE" ||
130
+ code === "MISSING_FEATURE_ID" ||
131
+ code === "SEMANTIC_INTAKE_RECOMMENDED" ||
132
+ code === "SEMANTIC_INTAKE_FAILED" ||
133
+ code === "SEMANTIC_INTAKE_INVALID_OUTPUT" ||
134
+ code === "SEMANTIC_INTAKE_PI_FAILED" ||
135
+ code === "SEMANTIC_INTAKE_NO_DOCUMENTS" ||
136
+ code === "SEMANTIC_INTAKE_REFUSED_NON_EXECUTABLE");
137
+ }
@@ -1,6 +1,8 @@
1
1
  export * from "./types.js";
2
2
  export * from "./path-policy.js";
3
3
  export * from "./parse-intent.js";
4
+ export * from "./artifact-meta.js";
5
+ export * from "./semantic-intake.js";
4
6
  export * from "./reference-integrity.js";
5
7
  export * from "./completeness.js";
6
8
  export * from "./build-draft.js";
@@ -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
  }
@@ -389,13 +498,36 @@ export async function prepareTaskSource(input) {
389
498
  message: "no parseable requirement document and no --ac / supplemental text; import a requirement-role PRD or pass flags",
390
499
  });
391
500
  }
501
+ // Product Analysis / pending Product Requirement diagnostics (precise gaps).
502
+ if (input.intent.kind === "facts" && input.intent.useImportedPrd) {
503
+ appendProductArtifactGaps({
504
+ gaps,
505
+ sourceIntegrity,
506
+ hasAcceptance: built.draft.requirement.acceptanceCriteria.length > 0,
507
+ });
508
+ }
392
509
  const validation = await validateDraftForTask({
393
510
  repoRoot,
394
511
  taskId,
395
512
  draft: built.draft,
396
513
  });
397
514
  if (!validation.ok) {
515
+ const productArtifactBlocking = gaps.some((gap) => [
516
+ "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
517
+ "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
518
+ "PRODUCT_REQUIREMENT_PENDING",
519
+ ].includes(gap.code));
398
520
  for (const message of validation.errors) {
521
+ const isAcceptanceSchema = /acceptance criterion|acceptanceCriteria/i.test(message);
522
+ // Prefer product-artifact diagnostics over generic schema AC errors.
523
+ if (productArtifactBlocking && isAcceptanceSchema) {
524
+ gaps.push({
525
+ code: "DRAFT_VALIDATION",
526
+ level: "warning",
527
+ message: `${message} (superseded by product-artifact diagnostic)`,
528
+ });
529
+ continue;
530
+ }
399
531
  gaps.push({
400
532
  code: "DRAFT_VALIDATION",
401
533
  level: "blocking",
@@ -428,13 +560,32 @@ export async function prepareTaskSource(input) {
428
560
  .filter((gap) => gap.level === "blocking")
429
561
  .map((gap) => gap.code);
430
562
  const transactionIncomplete = blockingCodes.includes("TRANSACTION_INCOMPLETE");
563
+ const productArtifactCodes = [
564
+ "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
565
+ "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
566
+ "PRODUCT_REQUIREMENT_PENDING",
567
+ "PRODUCT_ARTIFACT_NOT_EXECUTABLE",
568
+ ];
569
+ const primaryBlockingCode = blockingCodes.find((code) => productArtifactCodes.includes(code)) ??
570
+ blockingCodes.find((code) => code !== "EMPTY_ACCEPTANCE") ??
571
+ blockingCodes[0] ??
572
+ "INVALID_INPUT";
573
+ // Soft intake only when PRD was imported (or product-artifact diagnostics apply).
574
+ // Bare missing flags without import stay hard-invalid.
575
+ const softIntakeOnly = blocking &&
576
+ blockingCodes.length > 0 &&
577
+ blockingCodes.every((code) => isIntakeSoftGapCode(code)) &&
578
+ (hasImportedPrd ||
579
+ blockingCodes.some((code) => productArtifactCodes.includes(code)));
431
580
  if (input.mode !== "apply") {
432
581
  return {
433
582
  ok: !blocking,
434
583
  outcome: blocking
435
584
  ? transactionIncomplete
436
585
  ? "needs-reconcile"
437
- : "invalid"
586
+ : softIntakeOnly
587
+ ? "blocked"
588
+ : "invalid"
438
589
  : "succeeded",
439
590
  mode: "dry-run",
440
591
  taskId,
@@ -452,9 +603,13 @@ export async function prepareTaskSource(input) {
452
603
  error: {
453
604
  code: transactionIncomplete
454
605
  ? "TRANSACTION_INCOMPLETE"
455
- : "INVALID_INPUT",
456
- message: "task source draft has blocking gaps",
457
- details: { gaps: blockingCodes },
606
+ : softIntakeOnly
607
+ ? primaryBlockingCode
608
+ : "INVALID_INPUT",
609
+ message: softIntakeOnly
610
+ ? "PRD imported or facts present but intake incomplete; DAG not generated"
611
+ : "task source draft has blocking gaps",
612
+ details: { gaps: blockingCodes, softIntake: softIntakeOnly },
458
613
  },
459
614
  }
460
615
  : {}),
@@ -486,7 +641,9 @@ export async function prepareTaskSource(input) {
486
641
  if (blocking) {
487
642
  const outcome = blockingCodes.includes("DIRTY_SOURCE")
488
643
  ? "blocked"
489
- : "invalid";
644
+ : softIntakeOnly
645
+ ? "blocked"
646
+ : "invalid";
490
647
  return {
491
648
  ok: false,
492
649
  outcome,
@@ -502,10 +659,17 @@ export async function prepareTaskSource(input) {
502
659
  contractStatus: state.effectiveStatus,
503
660
  warnings,
504
661
  error: {
505
- code: outcome === "blocked" ? "DIRTY_SOURCE" : "INVALID_INPUT",
506
- message: "task source draft has blocking gaps",
662
+ code: blockingCodes.includes("DIRTY_SOURCE")
663
+ ? "DIRTY_SOURCE"
664
+ : softIntakeOnly
665
+ ? primaryBlockingCode
666
+ : "INVALID_INPUT",
667
+ message: softIntakeOnly
668
+ ? "PRD imported or facts present but intake incomplete; DAG not generated"
669
+ : "task source draft has blocking gaps",
507
670
  details: {
508
671
  gaps: blockingCodes,
672
+ softIntake: softIntakeOnly,
509
673
  },
510
674
  },
511
675
  };
@@ -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({