@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.0
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 +38 -17
- 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-executor.js +4 -91
- 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/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- 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/session-store.js +39 -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-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-CMHovlqG.js +32 -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/landing-density.js +23 -0
- 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/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -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/useOverlayFocus.js +84 -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/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -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 +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- 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 +34 -46
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- 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,133 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, constants } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Thrown only for spawn/infrastructure failures or exhausted eligible startup retries. */
|
|
5
|
+
export class ResilientGitCommandError extends Error {
|
|
6
|
+
diagnostics;
|
|
7
|
+
constructor(input) {
|
|
8
|
+
const last = input.attempts.at(-1);
|
|
9
|
+
super(`git ${input.command} unavailable after ${input.attempts.length} attempts (exit code=${last?.exitCode ?? "unavailable"}, exit code hex=${last?.exitCodeHex ?? "unavailable"}, signal=${last?.signal ?? "unavailable"}, cwd=${path.resolve(input.cwd)}): ${last?.detail ?? "unknown infrastructure error"}`);
|
|
10
|
+
this.name = "ResilientGitCommandError";
|
|
11
|
+
this.diagnostics = { schemaVersion: 1, ...input };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isResilientGitCommandError(error) {
|
|
15
|
+
return error instanceof ResilientGitCommandError;
|
|
16
|
+
}
|
|
17
|
+
export function formatWindowsExitCode(exitCode) {
|
|
18
|
+
return exitCode === undefined
|
|
19
|
+
? undefined
|
|
20
|
+
: `0x${(exitCode >>> 0).toString(16).padStart(8, "0").toUpperCase()}`;
|
|
21
|
+
}
|
|
22
|
+
export function isWindowsDllInitializationFailure(platform, exitCode) {
|
|
23
|
+
return platform === "win32" && exitCode !== undefined && (exitCode >>> 0) === 0xc0000142;
|
|
24
|
+
}
|
|
25
|
+
/** Only returns the original executable and its sibling within the same Git installation. */
|
|
26
|
+
export function deriveSameInstallationGitCandidates(primary) {
|
|
27
|
+
const normalized = path.win32.normalize(primary);
|
|
28
|
+
const lower = normalized.toLowerCase();
|
|
29
|
+
let sibling;
|
|
30
|
+
if (lower.endsWith("\\mingw64\\bin\\git.exe")) {
|
|
31
|
+
const root = path.win32.resolve(path.win32.dirname(normalized), "..", "..");
|
|
32
|
+
sibling = path.win32.join(root, "cmd", "git.exe");
|
|
33
|
+
}
|
|
34
|
+
else if (lower.endsWith("\\cmd\\git.exe")) {
|
|
35
|
+
const root = path.win32.resolve(path.win32.dirname(normalized), "..");
|
|
36
|
+
sibling = path.win32.join(root, "mingw64", "bin", "git.exe");
|
|
37
|
+
}
|
|
38
|
+
return [...new Set([normalized, ...(sibling ? [sibling] : [])])];
|
|
39
|
+
}
|
|
40
|
+
const windowsCandidateCache = new Map();
|
|
41
|
+
export async function resolveGitExecutableCandidates(platform, env) {
|
|
42
|
+
if (platform !== "win32")
|
|
43
|
+
return ["git"];
|
|
44
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
45
|
+
const cached = windowsCandidateCache.get(pathValue);
|
|
46
|
+
if (cached)
|
|
47
|
+
return cached;
|
|
48
|
+
const resolved = resolveWindowsGitExecutableCandidates(pathValue);
|
|
49
|
+
windowsCandidateCache.set(pathValue, resolved);
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
async function resolveWindowsGitExecutableCandidates(pathValue) {
|
|
53
|
+
for (const raw of pathValue.split(path.delimiter)) {
|
|
54
|
+
const entry = raw.trim().replace(/^"|"$/g, "");
|
|
55
|
+
if (!entry)
|
|
56
|
+
continue;
|
|
57
|
+
const candidate = path.win32.join(entry, "git.exe");
|
|
58
|
+
try {
|
|
59
|
+
await access(candidate, constants.X_OK);
|
|
60
|
+
const candidates = [];
|
|
61
|
+
for (const executable of deriveSameInstallationGitCandidates(candidate)) {
|
|
62
|
+
try {
|
|
63
|
+
await access(executable, constants.X_OK);
|
|
64
|
+
candidates.push(executable);
|
|
65
|
+
}
|
|
66
|
+
catch { /* optional sibling */ }
|
|
67
|
+
}
|
|
68
|
+
if (candidates.length)
|
|
69
|
+
return candidates;
|
|
70
|
+
}
|
|
71
|
+
catch { /* continue PATH search */ }
|
|
72
|
+
}
|
|
73
|
+
return ["git"];
|
|
74
|
+
}
|
|
75
|
+
const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
76
|
+
const bounded = (text) => text.length <= 1000 ? text : `${text.slice(0, 1000)}...[truncated]`;
|
|
77
|
+
/**
|
|
78
|
+
* Execute a read-only Git command with bounded recovery for Windows startup failures.
|
|
79
|
+
* Completed non-zero commands are deliberately returned to preserve caller semantics.
|
|
80
|
+
*/
|
|
81
|
+
export async function runResilientGitCommand(options, dependencies = {}) {
|
|
82
|
+
const platform = dependencies.platform ?? process.platform;
|
|
83
|
+
const env = { ...(dependencies.env ?? process.env), GIT_OPTIONAL_LOCKS: "0" };
|
|
84
|
+
const candidates = await (dependencies.resolveExecutableCandidates ?? resolveGitExecutableCandidates)(platform, env);
|
|
85
|
+
const executables = candidates.length ? candidates : ["git"];
|
|
86
|
+
const maxAttempts = Math.max(1, options.attempts ?? 3);
|
|
87
|
+
const delay = Math.max(0, options.retryDelayMs ?? 250);
|
|
88
|
+
const execute = dependencies.runAttempt ?? spawnGitAttempt;
|
|
89
|
+
const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
90
|
+
const now = dependencies.now ?? Date.now;
|
|
91
|
+
const attemptDiagnostics = [];
|
|
92
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
93
|
+
const executable = executables[(attempt - 1) % executables.length];
|
|
94
|
+
const startedAt = now();
|
|
95
|
+
try {
|
|
96
|
+
const result = await execute({ executable, args: options.args, cwd: options.cwd, env, windowsHide: true, maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER });
|
|
97
|
+
if (result.code === 0)
|
|
98
|
+
return result;
|
|
99
|
+
const dllFailure = isWindowsDllInitializationFailure(platform, result.code);
|
|
100
|
+
const emptyStartupFailure = result.stdout.length === 0 && result.stderr.length === 0;
|
|
101
|
+
const eligible = platform === "win32" && (dllFailure || emptyStartupFailure);
|
|
102
|
+
if (!eligible)
|
|
103
|
+
return result;
|
|
104
|
+
attemptDiagnostics.push({
|
|
105
|
+
attempt, executable, exitCode: result.code, ...(formatWindowsExitCode(result.code) ? { exitCodeHex: formatWindowsExitCode(result.code) } : {}), signal: result.signal,
|
|
106
|
+
durationMs: Math.max(0, now() - startedAt), detail: bounded("no stdout/stderr"),
|
|
107
|
+
transientKind: dllFailure ? "windows-dll-init-failed" : "empty-output-startup-failure",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const detail = bounded(error instanceof Error ? error.message : String(error));
|
|
112
|
+
attemptDiagnostics.push({ attempt, executable, signal: null, durationMs: Math.max(0, now() - startedAt), detail });
|
|
113
|
+
}
|
|
114
|
+
if (attempt < maxAttempts && delay > 0)
|
|
115
|
+
await sleep(Math.min(4_000, delay * 2 ** (attempt - 1)));
|
|
116
|
+
}
|
|
117
|
+
throw new ResilientGitCommandError({ cwd: options.cwd, command: options.args.join(" "), platform, executableCandidates: executables, attempts: attemptDiagnostics });
|
|
118
|
+
}
|
|
119
|
+
function spawnGitAttempt(input) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const child = spawn(input.executable, input.args, { cwd: input.cwd, env: input.env, windowsHide: input.windowsHide, stdio: ["ignore", "pipe", "pipe"] });
|
|
122
|
+
let stdout = "";
|
|
123
|
+
let stderr = "";
|
|
124
|
+
const append = (current, chunk) => {
|
|
125
|
+
const next = current + String(chunk);
|
|
126
|
+
return next.length > input.maxBuffer ? next.slice(0, input.maxBuffer) : next;
|
|
127
|
+
};
|
|
128
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
129
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
130
|
+
child.on("error", reject);
|
|
131
|
+
child.on("close", (code, signal) => resolve({ code: code ?? 1, signal, stdout, stderr }));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -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
|
};
|