@tea-agent/loop-agent 0.33.6-beta.0 → 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.
- package/CHANGELOG.md +29 -4
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +625 -71
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +324 -146
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
- package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +2 -27
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +12 -20
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +10 -10
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- 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([
|
|
3
|
-
|
|
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]
|
|
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]
|
|
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
|
|
137
|
+
next.push(`loop-agent task advance ${input.taskId} --prd <product-requirement.md> --allowed-path <glob> --json`);
|
|
136
138
|
}
|
|
137
|
-
if (
|
|
138
|
-
next.push("
|
|
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 (
|
|
141
|
-
next.push("
|
|
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
|
|
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 (
|
|
152
|
-
|
|
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 (
|
|
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
|
-
:
|
|
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
|
-
:
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
:
|
|
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:
|
|
506
|
-
|
|
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,
|
|
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 (
|
|
242
|
+
if (loadContent) {
|
|
227
243
|
totalParseBytes += hashResult.bytes;
|
|
228
244
|
if (totalParseBytes > PREPARE_MAX_TOTAL_BYTES) {
|
|
229
245
|
issues.push({
|