@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +39 -23
  2. package/dist/application/task-lifecycle/advance.js +4 -254
  3. package/dist/application/task-lifecycle/gates.js +0 -50
  4. package/dist/application/task-lifecycle/observe.js +2 -11
  5. package/dist/commands/init-upgrade.js +1 -32
  6. package/dist/commands/init.js +3 -94
  7. package/dist/executors/shell-executor.js +91 -4
  8. package/dist/executors/shell-write-guard.js +8 -26
  9. package/dist/shared/operator/capabilities.js +42 -72
  10. package/dist/task/source-prepare/index.js +0 -2
  11. package/dist/task/source-prepare/parse-intent.js +10 -58
  12. package/dist/task/source-prepare/prepare.js +16 -180
  13. package/dist/task/source-prepare/reference-integrity.js +2 -18
  14. package/dist/worker/console/app-data.js +0 -2
  15. package/dist/worker/console/chat/chat-event-store.js +25 -190
  16. package/dist/worker/console/chat/instruction-skills.js +217 -0
  17. package/dist/worker/console/chat/pi-console-config.js +32 -250
  18. package/dist/worker/console/chat/pi-runtime.js +71 -625
  19. package/dist/worker/console/chat/resource-loader.js +4 -5
  20. package/dist/worker/console/chat/routes.js +146 -324
  21. package/dist/worker/console/chat/runtime-context.js +12 -48
  22. package/dist/worker/console/chat/runtime-selection.js +0 -59
  23. package/dist/worker/console/chat/shortcuts.js +0 -1
  24. package/dist/worker/console/chat/tool-adapter.js +3 -9
  25. package/dist/worker/console/chat/tools.js +1 -5
  26. package/dist/worker/console/operator-actions.js +68 -559
  27. package/dist/worker/console/server.js +15 -8
  28. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  29. package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
  30. package/dist/worker/console/static/index.html +2 -2
  31. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
  32. package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
  33. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
  34. package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
  35. package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
  36. package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
  37. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
  38. package/dist/worker/delivery/final-verification.js +5 -13
  39. package/dist/worker/delivery/package.js +19 -31
  40. package/dist/worker/delivery/verification-bundle.js +4 -6
  41. package/dist/worker/observe/static/operator-chrome.css +2 -5
  42. package/dist/worker/observe/static/operator-chrome.js +1 -6
  43. package/dist/worker/observe/static/styles.css +9 -39
  44. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
  45. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  46. package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
  47. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  48. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  49. package/dist/workflows/dag/backend-test-scenario-param.js +552 -124
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  51. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  52. package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
  53. package/dist/workflows/dag/init-hybrid.js +49 -37
  54. package/dist/workflows/dag/types.js +7 -0
  55. package/dist/workflows/dag/workspace-checkpoint.js +27 -8
  56. package/docs/templates/backend-test-dag.json +33 -30
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +1 -3
  60. package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
  61. package/skills/loop-agent/references/task-workflow.md +0 -4
  62. package/dist/shared/resilient-git.js +0 -133
  63. package/dist/task/source-prepare/artifact-meta.js +0 -137
  64. package/dist/task/source-prepare/semantic-intake.js +0 -404
  65. package/dist/worker/console/dag-execution-receipt.js +0 -380
  66. package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
  67. package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
  68. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
  69. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
@@ -1,133 +0,0 @@
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
- }
@@ -1,137 +0,0 @@
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,404 +0,0 @@
1
- /**
2
- * P2 Semantic Intake: one-shot, read-only Pi structuring of imported PRDs
3
- * into TaskContractDraftV1. Does not write source/需求.md directly — caller
4
- * re-enters prepare with kind:"draft". Engineering paths/verify never come
5
- * from the model; only from flags / existing task config.
6
- */
7
- import { mkdir, readFile, writeFile } from "node:fs/promises";
8
- import path from "node:path";
9
- import { z } from "zod";
10
- import { executePiStep } from "../../executors/pi-executor.js";
11
- import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
12
- import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
13
- import { loadHarnessManifest } from "../../governance/harness.js";
14
- import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
15
- import { getTaskPaths } from "../runtime.js";
16
- import { detectProductArtifactMeta } from "./artifact-meta.js";
17
- export const SEMANTIC_INTAKE_ARTIFACT_REL = "artifacts/intake/semantic-draft.json";
18
- export const SEMANTIC_INTAKE_ATTEMPT_MARKER = "artifacts/intake/semantic-intake-attempted.json";
19
- const productArtifactBlockers = new Set([
20
- "PRODUCT_ANALYSIS_NOT_EXECUTABLE",
21
- "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
22
- "PRODUCT_REQUIREMENT_PENDING",
23
- "PRODUCT_ARTIFACT_NOT_EXECUTABLE",
24
- ]);
25
- const requirementDraftSchema = z.object({
26
- objective: z.string().min(1),
27
- scope: z.array(z.string()).default([]),
28
- nonGoals: z.array(z.string()).default([]),
29
- acceptanceCriteria: z
30
- .array(z.object({
31
- id: z.string().min(1),
32
- text: z.string().min(1),
33
- provenance: z
34
- .enum(["explicit", "derived", "inferred"])
35
- .optional()
36
- .default("derived"),
37
- }))
38
- .min(1),
39
- openQuestions: z.array(z.string()).optional(),
40
- assumptions: z.array(z.string()).optional(),
41
- title: z.string().optional(),
42
- });
43
- export function isSemanticIntakeDisabled(env = process.env) {
44
- const raw = (env.LOOP_AGENT_SEMANTIC_INTAKE ?? "").trim().toLowerCase();
45
- if (raw === "0" || raw === "false" || raw === "off" || raw === "no") {
46
- return true;
47
- }
48
- return false;
49
- }
50
- /**
51
- * When deterministic parse leaves soft intake gaps that look like "structure
52
- * problem" (not product-analysis / pending), allow one Pi structuring pass.
53
- */
54
- export function evaluateSemanticIntakeEligibility(input) {
55
- if (input.disabled) {
56
- return { eligible: false, reason: "semantic intake disabled" };
57
- }
58
- if (input.alreadyAttempted && !input.force) {
59
- return { eligible: false, reason: "semantic intake already attempted" };
60
- }
61
- if (!input.hasImportedPrd && !input.force) {
62
- return { eligible: false, reason: "no imported PRD" };
63
- }
64
- const blocking = input.gaps.filter((g) => g.level === "blocking");
65
- const codes = new Set(input.gaps.map((g) => g.code));
66
- const blockingCodes = new Set(blocking.map((g) => g.code));
67
- for (const code of productArtifactBlockers) {
68
- if (blockingCodes.has(code)) {
69
- return {
70
- eligible: false,
71
- reason: `product-artifact blocker ${code} requires human product work, not LLM structuring`,
72
- };
73
- }
74
- }
75
- // Engineering-only gaps are not fixed by semantic intake.
76
- const onlyEngineering = blocking.length > 0 &&
77
- blocking.every((g) => g.code === "EMPTY_ALLOWED_PATHS" ||
78
- g.code === "EMPTY_TASK_KIND" ||
79
- g.code === "MISSING_FEATURE_ID" ||
80
- g.code === "PATH_POLICY" ||
81
- g.code.startsWith("UNSAFE_") ||
82
- g.code === "ALLOWED_FORBIDDEN_OVERLAP");
83
- if (onlyEngineering) {
84
- return {
85
- eligible: false,
86
- reason: "only engineering boundary gaps; provide --allowed-path / flags",
87
- };
88
- }
89
- const structural = codes.has("SEMANTIC_INTAKE_RECOMMENDED") ||
90
- blockingCodes.has("EMPTY_ACCEPTANCE") ||
91
- blockingCodes.has("EMPTY_OBJECTIVE") ||
92
- blockingCodes.has("NO_PARSEABLE_REQUIREMENT");
93
- if (!structural && !input.force) {
94
- return {
95
- eligible: false,
96
- reason: "no structural requirement gaps for semantic intake",
97
- };
98
- }
99
- return { eligible: true, reason: "structural intake gaps with imported sources" };
100
- }
101
- export function extractJsonObject(text) {
102
- const fenced = /```(?:json)?\s*([\s\S]*?)```/iu.exec(text);
103
- const candidate = fenced?.[1]?.trim() || text.trim();
104
- const start = candidate.indexOf("{");
105
- const end = candidate.lastIndexOf("}");
106
- if (start < 0 || end <= start) {
107
- throw new Error("no JSON object found in model output");
108
- }
109
- try {
110
- return JSON.parse(candidate.slice(start, end + 1));
111
- }
112
- catch (error) {
113
- const message = error instanceof Error ? error.message : String(error);
114
- throw new Error(`invalid JSON object in model output: ${message}`);
115
- }
116
- }
117
- export function parseSemanticRequirementDraft(text) {
118
- const raw = extractJsonObject(text);
119
- return requirementDraftSchema.parse(raw);
120
- }
121
- /** Reject drafts that invent product behavior when only inferred ACs exist. */
122
- export function assertSemanticDraftAcceptable(draft) {
123
- const inferredOnly = draft.acceptanceCriteria.length > 0 &&
124
- draft.acceptanceCriteria.every((ac) => ac.provenance === "inferred");
125
- if (inferredOnly) {
126
- throw new Error("semantic intake produced only inferred acceptance criteria; refuse to invent product behavior");
127
- }
128
- if (!draft.objective.trim()) {
129
- throw new Error("semantic intake objective is empty");
130
- }
131
- }
132
- export function buildDraftFromSemanticIntake(input) {
133
- const allowedPaths = input.flags.allowedPaths?.length
134
- ? [
135
- ...(input.existingAllowedPaths ?? []),
136
- ...input.flags.allowedPaths,
137
- ]
138
- : (input.existingAllowedPaths ?? input.flags.allowedPaths ?? []);
139
- const forbiddenPaths = input.flags.forbiddenPaths?.length
140
- ? [
141
- ...(input.existingForbiddenPaths ?? []),
142
- ...input.flags.forbiddenPaths,
143
- ]
144
- : (input.existingForbiddenPaths ?? input.flags.forbiddenPaths ?? []);
145
- const verifyCommands = input.flags.verifyCommands?.length
146
- ? input.flags.verifyCommands
147
- : (input.existingVerify ?? []);
148
- const draft = {
149
- schemaVersion: TASK_CONTRACT_DRAFT_SCHEMA_VERSION,
150
- taskId: input.taskId,
151
- title: input.semantic.title?.trim() || input.title,
152
- taskKind: input.flags.taskKind || input.taskKind || "standard",
153
- requirement: {
154
- objective: input.semantic.objective.trim(),
155
- scope: input.semantic.scope.map((s) => s.trim()).filter(Boolean),
156
- nonGoals: input.semantic.nonGoals.map((s) => s.trim()).filter(Boolean),
157
- acceptanceCriteria: input.semantic.acceptanceCriteria.map((ac) => ({
158
- id: ac.id.trim(),
159
- text: ac.text.trim(),
160
- })),
161
- },
162
- constraints: {
163
- invariants: input.flags.invariants ?? [],
164
- allowedPaths: [...new Set(allowedPaths.map((p) => p.trim()).filter(Boolean))],
165
- forbiddenPaths: [
166
- ...new Set(forbiddenPaths.map((p) => p.trim()).filter(Boolean)),
167
- ],
168
- ...(input.flags.allowedRoots
169
- ? { allowedRoots: input.flags.allowedRoots }
170
- : {}),
171
- },
172
- verification: {
173
- commands: verifyCommands
174
- .filter((c) => c.label && c.command)
175
- .map((c) => ({
176
- label: c.label,
177
- command: c.command,
178
- ...(c.timeoutMs !== undefined ? { timeoutMs: c.timeoutMs } : {}),
179
- })),
180
- },
181
- };
182
- if (input.featureId || input.flags.featureId) {
183
- draft.featureId = input.flags.featureId ?? input.featureId;
184
- }
185
- if (input.references?.length)
186
- draft.references = input.references;
187
- if (input.semantic.openQuestions?.length) {
188
- draft.openQuestions = input.semantic.openQuestions;
189
- }
190
- if (input.semantic.assumptions?.length) {
191
- draft.assumptions = input.semantic.assumptions;
192
- }
193
- return draft;
194
- }
195
- function buildPrompt(input) {
196
- const bodies = input.documents
197
- .map((doc, index) => {
198
- const meta = detectProductArtifactMeta(doc.content);
199
- const metaLine = meta.recognized
200
- ? `artifact_type=${meta.artifactType} status=${meta.requirementStatus ?? meta.analysisStatus ?? "n/a"}`
201
- : "artifact_type=unknown";
202
- const clipped = doc.content.length > 24_000
203
- ? `${doc.content.slice(0, 24_000)}\n\n[truncated]`
204
- : doc.content;
205
- return [
206
- `### Document ${index + 1}`,
207
- `role: ${doc.role}`,
208
- `path: ${doc.materializedPath}`,
209
- metaLine,
210
- "```markdown",
211
- clipped,
212
- "```",
213
- ].join("\n");
214
- })
215
- .join("\n\n");
216
- return [
217
- "You are a read-only requirement structurer for loop-agent task intake.",
218
- "Convert imported product documents into a thin requirement draft JSON.",
219
- "Do NOT invent product behavior that is not supported by the documents.",
220
- "Do NOT emit engineering fields (allowedPaths, forbiddenPaths, verifyCommands, taskKind).",
221
- "Do NOT rewrite or suggest editing the original PRD files.",
222
- "Prefer explicit text; use provenance derived for rephrasing; use inferred only when clearly implied — if you must invent ACs, leave acceptanceCriteria empty so the system can stop for clarification.",
223
- "Return exactly one JSON object, no markdown prose outside optional fence:",
224
- JSON.stringify({
225
- title: "optional string",
226
- objective: "required string",
227
- scope: ["bullet"],
228
- nonGoals: ["bullet"],
229
- acceptanceCriteria: [
230
- {
231
- id: "AC-001",
232
- text: "observable criterion",
233
- provenance: "explicit|derived|inferred",
234
- },
235
- ],
236
- openQuestions: ["optional"],
237
- assumptions: ["optional"],
238
- }, null, 2),
239
- `taskId: ${input.taskId}`,
240
- `fallbackTitle: ${input.title}`,
241
- "Documents:",
242
- bodies,
243
- ].join("\n\n");
244
- }
245
- export async function readSemanticIntakeAttempted(repoRoot, taskId) {
246
- const paths = getTaskPaths(repoRoot, taskId);
247
- const marker = path.join(paths.taskDir, SEMANTIC_INTAKE_ATTEMPT_MARKER);
248
- try {
249
- await readFile(marker, "utf-8");
250
- return true;
251
- }
252
- catch {
253
- return false;
254
- }
255
- }
256
- async function writeAttemptMarker(repoRoot, taskId, payload) {
257
- const paths = getTaskPaths(repoRoot, taskId);
258
- const dir = path.join(paths.taskDir, "artifacts", "intake");
259
- await mkdir(dir, { recursive: true });
260
- const marker = path.join(paths.taskDir, SEMANTIC_INTAKE_ATTEMPT_MARKER);
261
- await writeFile(marker, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
262
- return marker;
263
- }
264
- /**
265
- * Run at most one semantic intake attempt for a task (caller must also latch
266
- * per advance invocation). Writes artifacts under taskDir/artifacts/intake/.
267
- */
268
- export async function runSemanticIntake(input) {
269
- const paths = getTaskPaths(input.repoRoot, input.taskId);
270
- const intakeDir = path.join(paths.taskDir, "artifacts", "intake");
271
- await mkdir(intakeDir, { recursive: true });
272
- const usableDocs = input.documents.filter((d) => d.content?.trim() && d.role !== "design");
273
- if (usableDocs.length === 0) {
274
- await writeAttemptMarker(input.repoRoot, input.taskId, {
275
- ok: false,
276
- code: "SEMANTIC_INTAKE_NO_DOCUMENTS",
277
- at: new Date().toISOString(),
278
- });
279
- return {
280
- ok: false,
281
- code: "SEMANTIC_INTAKE_NO_DOCUMENTS",
282
- message: "no imported document content available for semantic intake",
283
- };
284
- }
285
- // Refuse pure analysis/clarification-only suites (no requirement body).
286
- const onlyNonExecutable = usableDocs.every((d) => {
287
- const meta = detectProductArtifactMeta(d.content);
288
- return (meta.artifactType === "product-analysis" ||
289
- meta.artifactType === "requirement-clarification" ||
290
- d.role === "analysis" ||
291
- d.role === "clarification");
292
- });
293
- if (onlyNonExecutable) {
294
- await writeAttemptMarker(input.repoRoot, input.taskId, {
295
- ok: false,
296
- code: "SEMANTIC_INTAKE_REFUSED_NON_EXECUTABLE",
297
- at: new Date().toISOString(),
298
- });
299
- return {
300
- ok: false,
301
- code: "SEMANTIC_INTAKE_REFUSED_NON_EXECUTABLE",
302
- message: "documents are analysis/clarification only; use complete product-requirement.md",
303
- };
304
- }
305
- let assistantText;
306
- if (input.fixtureAssistantText !== undefined) {
307
- assistantText = input.fixtureAssistantText;
308
- }
309
- else {
310
- const execute = input.executePi ?? executePiStep;
311
- const manifest = await loadHarnessManifest(input.repoRoot);
312
- const models = resolveExecutorModelMatrices(manifest);
313
- const model = models.pi.MED;
314
- const attached = usableDocs
315
- .map((d) => d.absolutePath)
316
- .filter((p) => Boolean(p));
317
- const result = await execute({
318
- attachedFiles: attached,
319
- modelConfig: resolveDagPiModelConfig(model),
320
- prompt: buildPrompt({
321
- taskId: input.taskId,
322
- title: input.title,
323
- documents: usableDocs,
324
- }),
325
- repoRoot: input.repoRoot,
326
- step: "analyze",
327
- toolNames: ["read", "grep", "find", "ls"],
328
- userMessage: "Structure the imported requirement documents into the required JSON only.",
329
- validateOutput: (text) => {
330
- try {
331
- const parsed = parseSemanticRequirementDraft(text);
332
- assertSemanticDraftAcceptable(parsed);
333
- return [];
334
- }
335
- catch (error) {
336
- return [error instanceof Error ? error.message : String(error)];
337
- }
338
- },
339
- });
340
- if (!result.ok) {
341
- await writeAttemptMarker(input.repoRoot, input.taskId, {
342
- ok: false,
343
- code: "SEMANTIC_INTAKE_PI_FAILED",
344
- failureCategory: result.failureCategory,
345
- at: new Date().toISOString(),
346
- });
347
- return {
348
- ok: false,
349
- code: "SEMANTIC_INTAKE_PI_FAILED",
350
- message: `semantic intake pi failed: ${result.failureCategory}: ${result.stderr || result.assistantText}`.slice(0, 2000),
351
- };
352
- }
353
- assistantText = result.assistantText;
354
- }
355
- try {
356
- const semantic = parseSemanticRequirementDraft(assistantText);
357
- assertSemanticDraftAcceptable(semantic);
358
- const draft = buildDraftFromSemanticIntake({
359
- taskId: input.taskId,
360
- title: input.title,
361
- taskKind: input.taskKind ?? "standard",
362
- featureId: input.featureId,
363
- semantic,
364
- flags: input.flags,
365
- existingAllowedPaths: input.existingAllowedPaths,
366
- existingForbiddenPaths: input.existingForbiddenPaths,
367
- existingVerify: input.existingVerify,
368
- references: usableDocs.map((d) => ({
369
- role: d.role,
370
- ref: d.materializedPath.replace(/\\/g, "/"),
371
- })),
372
- });
373
- const artifactPath = path.join(paths.taskDir, SEMANTIC_INTAKE_ARTIFACT_REL);
374
- await writeFile(artifactPath, `${JSON.stringify({
375
- schemaVersion: 1,
376
- kind: "semantic-intake-draft",
377
- taskId: input.taskId,
378
- createdAt: new Date().toISOString(),
379
- semantic,
380
- draft,
381
- }, null, 2)}\n`, "utf-8");
382
- await writeAttemptMarker(input.repoRoot, input.taskId, {
383
- ok: true,
384
- code: "SEMANTIC_INTAKE_OK",
385
- artifactPath: SEMANTIC_INTAKE_ARTIFACT_REL,
386
- at: new Date().toISOString(),
387
- });
388
- return { ok: true, draft, artifactPath, semantic };
389
- }
390
- catch (error) {
391
- const message = error instanceof Error ? error.message : String(error);
392
- await writeAttemptMarker(input.repoRoot, input.taskId, {
393
- ok: false,
394
- code: "SEMANTIC_INTAKE_INVALID_OUTPUT",
395
- message,
396
- at: new Date().toISOString(),
397
- });
398
- return {
399
- ok: false,
400
- code: "SEMANTIC_INTAKE_INVALID_OUTPUT",
401
- message,
402
- };
403
- }
404
- }