@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -16
- package/dist/application/task-lifecycle/advance.js +309 -9
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/application/task-lifecycle/plan-transitions.js +13 -21
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/dag-pi-executor.js +18 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +98 -44
- 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 +229 -18
- 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/interview/grill-me.js +7 -6
- package/dist/worker/console/operator-actions.js +785 -85
- package/dist/worker/console/prd-intake-bridge.js +393 -0
- package/dist/worker/console/recovery-cta.js +35 -6
- 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-BpuHmlSP.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +1 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +57 -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/observability/read-model.js +3 -0
- 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/failure-category.js +3 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +49 -55
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/retry-policy.js +44 -11
- package/dist/workflows/dag/runner.js +6 -0
- package/dist/workflows/dag/scheduler.js +49 -1
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/validate.js +16 -2
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/agent-dag.schema.json +4 -4
- 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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console PRD Intake Bridge (design 2026-08-11-console-prd-intake-bridge).
|
|
3
|
+
* Maps task-side intake artifacts / projected source into Console DraftStore
|
|
4
|
+
* fields. Does not run Semantic Intake itself — that stays in task advance.
|
|
5
|
+
*/
|
|
6
|
+
import { access, readFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getTaskPaths } from "../../task/runtime.js";
|
|
9
|
+
import { SEMANTIC_INTAKE_ARTIFACT_REL, } from "../../task/source-prepare/semantic-intake.js";
|
|
10
|
+
import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
|
|
11
|
+
import { emptyDraft } from "./interview/grill-me.js";
|
|
12
|
+
import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
|
|
13
|
+
function asStringList(value) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [];
|
|
16
|
+
return value
|
|
17
|
+
.map((item) => {
|
|
18
|
+
if (typeof item === "string")
|
|
19
|
+
return item.trim();
|
|
20
|
+
if (item && typeof item === "object") {
|
|
21
|
+
const record = item;
|
|
22
|
+
if (typeof record.text === "string")
|
|
23
|
+
return record.text.trim();
|
|
24
|
+
if (typeof record.message === "string")
|
|
25
|
+
return record.message.trim();
|
|
26
|
+
}
|
|
27
|
+
return String(item ?? "").trim();
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
function asAcceptance(value) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
36
|
+
const item = value[i];
|
|
37
|
+
if (typeof item === "string" && item.trim()) {
|
|
38
|
+
out.push({ id: `AC-${i + 1}`, text: item.trim() });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (item && typeof item === "object") {
|
|
42
|
+
const record = item;
|
|
43
|
+
const text = typeof record.text === "string" ? record.text.trim() : "";
|
|
44
|
+
if (!text)
|
|
45
|
+
continue;
|
|
46
|
+
const id = typeof record.id === "string" && record.id.trim()
|
|
47
|
+
? record.id.trim()
|
|
48
|
+
: `AC-${i + 1}`;
|
|
49
|
+
out.push({ id, text });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function warningList(json) {
|
|
55
|
+
if (!json || typeof json !== "object")
|
|
56
|
+
return [];
|
|
57
|
+
const root = json;
|
|
58
|
+
const raw = Array.isArray(root.warnings)
|
|
59
|
+
? root.warnings
|
|
60
|
+
: Array.isArray(root.result?.warnings)
|
|
61
|
+
? root.result?.warnings
|
|
62
|
+
: [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const item of raw) {
|
|
65
|
+
if (!item || typeof item !== "object")
|
|
66
|
+
continue;
|
|
67
|
+
const record = item;
|
|
68
|
+
const code = typeof record.code === "string" && record.code.trim()
|
|
69
|
+
? record.code.trim()
|
|
70
|
+
: "UNKNOWN";
|
|
71
|
+
const message = typeof record.message === "string" ? record.message : code;
|
|
72
|
+
out.push({ code, message });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function lifecycleFromAdvanceJson(json) {
|
|
77
|
+
if (!json || typeof json !== "object")
|
|
78
|
+
return undefined;
|
|
79
|
+
const root = json;
|
|
80
|
+
const value = root.result?.lifecycleState ?? root.lifecycleState;
|
|
81
|
+
return typeof value === "string" ? value : undefined;
|
|
82
|
+
}
|
|
83
|
+
async function readJsonIfExists(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
await access(filePath);
|
|
86
|
+
const raw = await readFile(filePath, "utf8");
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function hasRequirementStructure(draft) {
|
|
94
|
+
const req = draft.requirement ?? {};
|
|
95
|
+
return Boolean(typeof req.objective === "string" &&
|
|
96
|
+
req.objective.trim() &&
|
|
97
|
+
Array.isArray(req.scope) &&
|
|
98
|
+
req.scope.length > 0 &&
|
|
99
|
+
Array.isArray(req.acceptanceCriteria) &&
|
|
100
|
+
req.acceptanceCriteria.length > 0);
|
|
101
|
+
}
|
|
102
|
+
/** Normalize path / verify lists from operator params (arrays or newline/comma text). */
|
|
103
|
+
export function parseEngineeringBoundaryFromParams(p) {
|
|
104
|
+
const asList = (value) => {
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return value
|
|
107
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
110
|
+
if (typeof value === "string") {
|
|
111
|
+
return value
|
|
112
|
+
.split(/[\n,;,;]+/)
|
|
113
|
+
.map((s) => s.trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
};
|
|
118
|
+
const allowedPaths = asList(p.allowedPaths ?? p.allowedPath);
|
|
119
|
+
const forbiddenPaths = asList(p.forbiddenPaths ?? p.forbiddenPath);
|
|
120
|
+
const verifyRaw = p.verifyCommands ?? p.verify;
|
|
121
|
+
const verifyCommands = [];
|
|
122
|
+
if (Array.isArray(verifyRaw)) {
|
|
123
|
+
for (let i = 0; i < verifyRaw.length; i += 1) {
|
|
124
|
+
const item = verifyRaw[i];
|
|
125
|
+
if (typeof item === "string" && item.trim()) {
|
|
126
|
+
const trimmed = item.trim();
|
|
127
|
+
const idx = trimmed.indexOf(":");
|
|
128
|
+
if (idx > 0 && idx < trimmed.length - 1 && !trimmed.slice(0, idx).includes(" ")) {
|
|
129
|
+
verifyCommands.push({
|
|
130
|
+
label: trimmed.slice(0, idx),
|
|
131
|
+
command: trimmed.slice(idx + 1).trim(),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
verifyCommands.push({
|
|
136
|
+
label: `verify-${i + 1}`,
|
|
137
|
+
command: trimmed,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (item && typeof item === "object") {
|
|
142
|
+
const rec = item;
|
|
143
|
+
const command = typeof rec.command === "string" ? rec.command.trim() : "";
|
|
144
|
+
if (!command)
|
|
145
|
+
continue;
|
|
146
|
+
verifyCommands.push({
|
|
147
|
+
label: typeof rec.label === "string" && rec.label.trim()
|
|
148
|
+
? rec.label.trim()
|
|
149
|
+
: `verify-${i + 1}`,
|
|
150
|
+
command,
|
|
151
|
+
...(typeof rec.timeoutMs === "number"
|
|
152
|
+
? { timeoutMs: rec.timeoutMs }
|
|
153
|
+
: {}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (typeof verifyRaw === "string" && verifyRaw.trim()) {
|
|
159
|
+
for (const [i, line] of asList(verifyRaw).entries()) {
|
|
160
|
+
const idx = line.indexOf(":");
|
|
161
|
+
if (idx > 0 && idx < line.length - 1 && !line.slice(0, idx).includes(" ")) {
|
|
162
|
+
verifyCommands.push({
|
|
163
|
+
label: line.slice(0, idx),
|
|
164
|
+
command: line.slice(idx + 1).trim(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
verifyCommands.push({ label: `verify-${i + 1}`, command: line });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...(allowedPaths.length > 0 ? { allowedPaths } : {}),
|
|
174
|
+
...(forbiddenPaths.length > 0 ? { forbiddenPaths } : {}),
|
|
175
|
+
...(verifyCommands.length > 0 ? { verifyCommands } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function appendEngineeringCliArgs(args, boundary) {
|
|
179
|
+
for (const pathGlob of boundary.allowedPaths ?? []) {
|
|
180
|
+
args.push("--allowed-path", pathGlob);
|
|
181
|
+
}
|
|
182
|
+
for (const pathGlob of boundary.forbiddenPaths ?? []) {
|
|
183
|
+
args.push("--forbidden-path", pathGlob);
|
|
184
|
+
}
|
|
185
|
+
for (const cmd of boundary.verifyCommands ?? []) {
|
|
186
|
+
args.push("--verify", `${cmd.label}:${cmd.command}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function applyEngineeringBoundaryToDraft(draft, boundary) {
|
|
190
|
+
if (!(boundary.allowedPaths?.length) &&
|
|
191
|
+
!(boundary.forbiddenPaths?.length) &&
|
|
192
|
+
!(boundary.verifyCommands?.length)) {
|
|
193
|
+
return draft;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...draft,
|
|
197
|
+
constraints: {
|
|
198
|
+
invariants: draft.constraints?.invariants ?? [],
|
|
199
|
+
allowedPaths: boundary.allowedPaths?.length
|
|
200
|
+
? boundary.allowedPaths
|
|
201
|
+
: (draft.constraints?.allowedPaths ?? []),
|
|
202
|
+
forbiddenPaths: boundary.forbiddenPaths?.length
|
|
203
|
+
? boundary.forbiddenPaths
|
|
204
|
+
: (draft.constraints?.forbiddenPaths ?? []),
|
|
205
|
+
},
|
|
206
|
+
verification: {
|
|
207
|
+
commands: boundary.verifyCommands?.length
|
|
208
|
+
? boundary.verifyCommands
|
|
209
|
+
: (draft.verification?.commands ?? []),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export async function buildDraftFromTaskIntake(input) {
|
|
214
|
+
const taskKind = normalizeConsoleWorkflowKind(input.taskKind, "standard");
|
|
215
|
+
const paths = getTaskPaths(input.repoRoot, input.taskId);
|
|
216
|
+
const warnings = warningList(input.advanceJson);
|
|
217
|
+
const lifecycleState = lifecycleFromAdvanceJson(input.advanceJson);
|
|
218
|
+
const semanticApplied = warnings.some((w) => w.code === "SEMANTIC_INTAKE_APPLIED");
|
|
219
|
+
const semanticFailed = warnings.find((w) => w.code === "SEMANTIC_INTAKE_PI_FAILED" ||
|
|
220
|
+
w.code === "SEMANTIC_INTAKE_INVALID_OUTPUT" ||
|
|
221
|
+
w.code === "SEMANTIC_INTAKE_FAILED" ||
|
|
222
|
+
w.code === "SEMANTIC_INTAKE_NO_DOCUMENTS" ||
|
|
223
|
+
w.code === "SEMANTIC_INTAKE_REFUSED_NON_EXECUTABLE");
|
|
224
|
+
const semanticSkipped = warnings.find((w) => w.code === "SEMANTIC_INTAKE_SKIPPED");
|
|
225
|
+
const artifactPath = path.join(paths.taskDir, SEMANTIC_INTAKE_ARTIFACT_REL);
|
|
226
|
+
const artifact = await readJsonIfExists(artifactPath);
|
|
227
|
+
let draft = {
|
|
228
|
+
...emptyDraft(input.taskId, input.title, { taskKind }),
|
|
229
|
+
requirement: {
|
|
230
|
+
objective: input.title,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
let parseStatus = "pending";
|
|
234
|
+
let semanticIntake = {
|
|
235
|
+
applied: false,
|
|
236
|
+
code: semanticFailed?.code ?? semanticSkipped?.code,
|
|
237
|
+
message: semanticFailed?.message ?? semanticSkipped?.message,
|
|
238
|
+
};
|
|
239
|
+
const semanticReq = artifact?.draft?.requirement ??
|
|
240
|
+
(artifact?.semantic
|
|
241
|
+
? {
|
|
242
|
+
objective: artifact.semantic.objective,
|
|
243
|
+
scope: artifact.semantic.scope,
|
|
244
|
+
nonGoals: artifact.semantic.nonGoals,
|
|
245
|
+
acceptanceCriteria: artifact.semantic.acceptanceCriteria,
|
|
246
|
+
}
|
|
247
|
+
: undefined);
|
|
248
|
+
// An artifact is authoritative only when this exact advance invocation reports
|
|
249
|
+
// SEMANTIC_INTAKE_APPLIED. Tasks may import newer immutable PRD revisions while
|
|
250
|
+
// an older per-task semantic artifact remains on disk; never project it as the
|
|
251
|
+
// current Console draft.
|
|
252
|
+
if (semanticApplied && semanticReq?.objective?.trim()) {
|
|
253
|
+
const title = artifact?.draft?.title?.trim() ||
|
|
254
|
+
artifact?.semantic?.title?.trim() ||
|
|
255
|
+
input.title;
|
|
256
|
+
draft = {
|
|
257
|
+
...draft,
|
|
258
|
+
title,
|
|
259
|
+
requirement: {
|
|
260
|
+
objective: semanticReq.objective.trim(),
|
|
261
|
+
scope: asStringList(semanticReq.scope),
|
|
262
|
+
nonGoals: asStringList(semanticReq.nonGoals),
|
|
263
|
+
acceptanceCriteria: asAcceptance(semanticReq.acceptanceCriteria),
|
|
264
|
+
},
|
|
265
|
+
openQuestions: asStringList(artifact?.draft?.openQuestions ?? artifact?.semantic?.openQuestions) || undefined,
|
|
266
|
+
assumptions: asStringList(artifact?.draft?.assumptions ?? artifact?.semantic?.assumptions) || undefined,
|
|
267
|
+
references: artifact?.draft?.references,
|
|
268
|
+
};
|
|
269
|
+
parseStatus = "semantic";
|
|
270
|
+
semanticIntake = {
|
|
271
|
+
applied: true,
|
|
272
|
+
code: "SEMANTIC_INTAKE_APPLIED",
|
|
273
|
+
message: "structured from artifacts/intake/semantic-draft.json",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Deterministic path. When the current intake is incomplete/failed/skipped,
|
|
278
|
+
// prefer the newest imported reference over an older projected 需求.md.
|
|
279
|
+
// Successful deterministic projection still prefers canonical 需求.md.
|
|
280
|
+
const projectedRequirementPath = path.join(paths.sourceDir, "需求.md");
|
|
281
|
+
const referenceCandidates = [];
|
|
282
|
+
try {
|
|
283
|
+
const manifestPath = path.join(paths.sourceDir, "source-manifest.json");
|
|
284
|
+
const manifest = await readJsonIfExists(manifestPath);
|
|
285
|
+
for (const doc of manifest?.documents ?? []) {
|
|
286
|
+
if ((doc.role === "requirement" || doc.role === "acceptance") &&
|
|
287
|
+
typeof doc.materializedPath === "string") {
|
|
288
|
+
referenceCandidates.push({
|
|
289
|
+
filePath: path.join(paths.sourceDir, doc.materializedPath),
|
|
290
|
+
importedAt: typeof doc.importedAt === "string" ? doc.importedAt : "",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// ignore manifest read errors
|
|
297
|
+
}
|
|
298
|
+
referenceCandidates.sort((left, right) => right.importedAt.localeCompare(left.importedAt));
|
|
299
|
+
const preferImportedReferences = Boolean(semanticFailed ||
|
|
300
|
+
semanticSkipped ||
|
|
301
|
+
lifecycleState === "intake-incomplete");
|
|
302
|
+
const candidates = preferImportedReferences
|
|
303
|
+
? [
|
|
304
|
+
...referenceCandidates.map((candidate) => candidate.filePath),
|
|
305
|
+
projectedRequirementPath,
|
|
306
|
+
]
|
|
307
|
+
: [
|
|
308
|
+
projectedRequirementPath,
|
|
309
|
+
...referenceCandidates.map((candidate) => candidate.filePath),
|
|
310
|
+
];
|
|
311
|
+
for (const filePath of candidates) {
|
|
312
|
+
try {
|
|
313
|
+
const markdown = await readFile(filePath, "utf8");
|
|
314
|
+
const facts = extractRequirementFactsFromMarkdown(markdown);
|
|
315
|
+
if (!facts.objective?.trim())
|
|
316
|
+
continue;
|
|
317
|
+
draft = {
|
|
318
|
+
...draft,
|
|
319
|
+
title: facts.title?.trim() || input.title,
|
|
320
|
+
requirement: {
|
|
321
|
+
objective: facts.objective.trim(),
|
|
322
|
+
scope: facts.scope,
|
|
323
|
+
nonGoals: facts.nonGoals,
|
|
324
|
+
acceptanceCriteria: facts.acceptanceCriteria,
|
|
325
|
+
},
|
|
326
|
+
openQuestions: facts.openQuestions.length > 0
|
|
327
|
+
? facts.openQuestions
|
|
328
|
+
: undefined,
|
|
329
|
+
assumptions: facts.assumptions.length > 0
|
|
330
|
+
? facts.assumptions
|
|
331
|
+
: undefined,
|
|
332
|
+
};
|
|
333
|
+
parseStatus = hasRequirementStructure(draft)
|
|
334
|
+
? "deterministic"
|
|
335
|
+
: "incomplete";
|
|
336
|
+
if (hasRequirementStructure(draft))
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// try next candidate
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Fallback: title-only objective (never invent scope/AC templates).
|
|
344
|
+
if (!draft.requirement?.objective?.trim()) {
|
|
345
|
+
draft = {
|
|
346
|
+
...draft,
|
|
347
|
+
requirement: { objective: input.title },
|
|
348
|
+
};
|
|
349
|
+
parseStatus = "incomplete";
|
|
350
|
+
}
|
|
351
|
+
else if (parseStatus === "pending") {
|
|
352
|
+
parseStatus = hasRequirementStructure(draft)
|
|
353
|
+
? "deterministic"
|
|
354
|
+
: "incomplete";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Engineering gaps remain as draft gaps; do not invent paths/AC templates.
|
|
358
|
+
const gaps = warnings.filter((w) => w.code === "EMPTY_ALLOWED_PATHS" ||
|
|
359
|
+
w.code === "EMPTY_ACCEPTANCE" ||
|
|
360
|
+
w.code === "EMPTY_OBJECTIVE" ||
|
|
361
|
+
w.code === "EMPTY_FORBIDDEN_PATHS" ||
|
|
362
|
+
w.code === "INTAKE_INCOMPLETE" ||
|
|
363
|
+
w.code === "BOUNDARY_REQUIRED" ||
|
|
364
|
+
w.code.startsWith("SEMANTIC_INTAKE_") ||
|
|
365
|
+
w.code === "NO_PARSEABLE_REQUIREMENT");
|
|
366
|
+
if (semanticFailed && !semanticIntake.applied) {
|
|
367
|
+
if (!hasRequirementStructure(draft)) {
|
|
368
|
+
parseStatus = "failed";
|
|
369
|
+
}
|
|
370
|
+
semanticIntake = {
|
|
371
|
+
applied: false,
|
|
372
|
+
code: semanticFailed.code,
|
|
373
|
+
message: semanticFailed.message,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
else if (!hasRequirementStructure(draft) && parseStatus !== "semantic") {
|
|
377
|
+
parseStatus = "incomplete";
|
|
378
|
+
}
|
|
379
|
+
if (input.engineering) {
|
|
380
|
+
draft = applyEngineeringBoundaryToDraft(draft, input.engineering);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
draft,
|
|
384
|
+
parseStatus,
|
|
385
|
+
semanticIntake,
|
|
386
|
+
gaps,
|
|
387
|
+
lifecycleState,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/** True when requirement structure is filled enough that Skip should not invent AC/scope. */
|
|
391
|
+
export function requirementStructurallyReady(draft) {
|
|
392
|
+
return hasRequirementStructure(draft);
|
|
393
|
+
}
|
|
@@ -37,9 +37,10 @@ const CTA = {
|
|
|
37
37
|
id: "reconcile",
|
|
38
38
|
label: "对账 / reconcile",
|
|
39
39
|
requiresReason: true,
|
|
40
|
+
action: "dagReconcileRun",
|
|
40
41
|
lane: "dag",
|
|
41
|
-
guide: "
|
|
42
|
-
commandHint: "loop-agent dag reconcile-run --run-id <run-id> --reason <structured-reason>",
|
|
42
|
+
guide: "Orphan/中断时先对账归档(默认 supersede);runner 仍存活时勿盲 supersede",
|
|
43
|
+
commandHint: "loop-agent dag reconcile-run --run-id <run-id> --action supersede --reason <structured-reason>",
|
|
43
44
|
},
|
|
44
45
|
regenerate: {
|
|
45
46
|
id: "regenerate",
|
|
@@ -97,7 +98,9 @@ const MATRIX = {
|
|
|
97
98
|
"resume",
|
|
98
99
|
"regenerate",
|
|
99
100
|
],
|
|
100
|
-
|
|
101
|
+
// Stall/quiet/unknown: observe + doctor only — do not offer supersede while
|
|
102
|
+
// runner may still be alive (eligibility fail-closes, but UI should not tease).
|
|
103
|
+
"orphan-unknown": ["report", "doctor"],
|
|
101
104
|
"terminal-failed": [
|
|
102
105
|
"report",
|
|
103
106
|
"doctor",
|
|
@@ -137,8 +140,9 @@ export function partitionRecoveryCtas(ctas) {
|
|
|
137
140
|
}
|
|
138
141
|
/**
|
|
139
142
|
* Console only shows CTAs that actually dispatch an operator action.
|
|
140
|
-
* Pure CLI-hint rows (regenerate / decision / resume
|
|
141
|
-
*
|
|
143
|
+
* Pure CLI-hint rows (regenerate / decision / resume) stay in the policy
|
|
144
|
+
* matrix for docs, but must not appear as dead buttons. reconcile is wired
|
|
145
|
+
* to dagReconcileRun (structured reason required).
|
|
142
146
|
*/
|
|
143
147
|
export function isConsoleExecutableRecoveryCta(cta) {
|
|
144
148
|
return typeof cta.action === "string" && cta.action.trim().length > 0;
|
|
@@ -167,6 +171,8 @@ export function recommendedRecoveryCtaId(factClass) {
|
|
|
167
171
|
case "pause-on-human":
|
|
168
172
|
return pick("doctor", "report");
|
|
169
173
|
case "needs-reconcile":
|
|
174
|
+
// Prefer doctor first; reconcile is available when operator confirms reason.
|
|
175
|
+
return pick("doctor", "reconcile", "report");
|
|
170
176
|
case "runtime-mismatch":
|
|
171
177
|
case "contract-drift":
|
|
172
178
|
return pick("doctor", "report");
|
|
@@ -201,6 +207,7 @@ export function classifyRecoveryFact(input) {
|
|
|
201
207
|
const code = (input.errorCode ?? "").toLowerCase();
|
|
202
208
|
const state = (input.operationState ?? "").toLowerCase();
|
|
203
209
|
const dag = (input.dagStatus ?? "").toLowerCase();
|
|
210
|
+
const failure = (input.failureCategory ?? "").toLowerCase();
|
|
204
211
|
if (code.includes("validation") ||
|
|
205
212
|
code.includes("invalid") ||
|
|
206
213
|
state.includes("validation")) {
|
|
@@ -225,15 +232,37 @@ export function classifyRecoveryFact(input) {
|
|
|
225
232
|
if (code.includes("verif") || state.includes("verif")) {
|
|
226
233
|
return "verification-failed";
|
|
227
234
|
}
|
|
235
|
+
// Controller interrupt / orphaned mid-run: reconcile first (not blind rerun).
|
|
236
|
+
if (failure === "controller-interrupted" ||
|
|
237
|
+
code.includes("controller-interrupted") ||
|
|
238
|
+
code.includes("interrupted") ||
|
|
239
|
+
dag === "interrupted" ||
|
|
240
|
+
dag === "orphaned" ||
|
|
241
|
+
state.includes("controller-interrupted") ||
|
|
242
|
+
state.includes("interrupted")) {
|
|
243
|
+
return "needs-reconcile";
|
|
244
|
+
}
|
|
245
|
+
// Live stall / quiet while runner lease may still be fresh — observe/doctor only
|
|
246
|
+
// (Console executable CTAs: report + doctor; reconcile stays CLI-hint).
|
|
247
|
+
if (dag === "running-suspected-stall" ||
|
|
248
|
+
dag === "running-quiet" ||
|
|
249
|
+
dag === "suspected-stall" ||
|
|
250
|
+
dag.includes("suspected-stall") ||
|
|
251
|
+
state.includes("suspected-stall")) {
|
|
252
|
+
return "orphan-unknown";
|
|
253
|
+
}
|
|
228
254
|
if (state === "succeeded" ||
|
|
229
255
|
state === "completed" ||
|
|
256
|
+
state === "finished" ||
|
|
230
257
|
dag === "succeeded" ||
|
|
231
|
-
dag === "completed"
|
|
258
|
+
dag === "completed" ||
|
|
259
|
+
dag === "finished") {
|
|
232
260
|
return "terminal-succeeded";
|
|
233
261
|
}
|
|
234
262
|
if (state === "failed" ||
|
|
235
263
|
state.includes("fail") ||
|
|
236
264
|
dag === "failed" ||
|
|
265
|
+
dag === "partial_failed" ||
|
|
237
266
|
code.includes("fail")) {
|
|
238
267
|
return "terminal-failed";
|
|
239
268
|
}
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
|
|
7
7
|
import { openConsoleAppData } from "./app-data.js";
|
|
8
8
|
import { DagConfirmationStore } from "./dag-confirmation.js";
|
|
9
|
+
import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
|
|
9
10
|
import { DraftStore } from "./draft-store.js";
|
|
10
11
|
import { AssessmentStore } from "./interview/assessment.js";
|
|
11
12
|
import { InterviewSessionStore } from "./interview/session.js";
|
|
@@ -59,6 +60,7 @@ export async function createConsoleServer(options) {
|
|
|
59
60
|
});
|
|
60
61
|
const drafts = new DraftStore(appData);
|
|
61
62
|
const confirmations = new DagConfirmationStore(appData, bootToken.confirmationToken);
|
|
63
|
+
const dagExecutionReceipts = new DagExecutionReceiptStore(appData);
|
|
62
64
|
const interviews = new InterviewSessionStore(appData);
|
|
63
65
|
const assessments = new AssessmentStore(appData);
|
|
64
66
|
const readiness = await probePiReadiness({
|
|
@@ -94,6 +96,7 @@ export async function createConsoleServer(options) {
|
|
|
94
96
|
events,
|
|
95
97
|
drafts,
|
|
96
98
|
confirmations,
|
|
99
|
+
dagExecutionReceipts,
|
|
97
100
|
humanGateSecret: bootToken.confirmationToken,
|
|
98
101
|
interviews,
|
|
99
102
|
assessments,
|
|
@@ -111,16 +114,18 @@ export async function createConsoleServer(options) {
|
|
|
111
114
|
// custom-tool execute() calls dispatch into the operator action layer (Gate 3).
|
|
112
115
|
// Chat session dir is dedicated (app-data/chat-sessions), NOT user default.
|
|
113
116
|
const chatStore = new ChatSessionStore(appData);
|
|
114
|
-
|
|
117
|
+
// M5.1: every Console boot owns a fresh event-log generation. A browser
|
|
118
|
+
// reconnecting with an old SSE cursor must reconcile from snapshot rather
|
|
119
|
+
// than silently consume a different process' event namespace.
|
|
120
|
+
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
121
|
+
const chatEvents = createChatEventStore({ appData, epoch: chatEventEpoch });
|
|
115
122
|
const chatOperationLinker = createChatOperationLinker({
|
|
116
123
|
chatEvents,
|
|
117
124
|
operations,
|
|
118
125
|
operationEvents: events,
|
|
119
126
|
});
|
|
120
|
-
const skillsDir = resolveLoopAgentSkillsDir(repoRoot);
|
|
121
127
|
const chatRuntime = new ConsolePiRuntime({
|
|
122
128
|
cwd: repoRoot,
|
|
123
|
-
skillsDir,
|
|
124
129
|
sessionDir: appData.chatSessions,
|
|
125
130
|
actionContext,
|
|
126
131
|
});
|
|
@@ -137,7 +142,6 @@ export async function createConsoleServer(options) {
|
|
|
137
142
|
bootToken,
|
|
138
143
|
getConsoleOrigin,
|
|
139
144
|
allowNonLoopbackAccess: !loopbackBind,
|
|
140
|
-
skillsDir,
|
|
141
145
|
};
|
|
142
146
|
const nightAuxTicker = new NightAuxTicker({
|
|
143
147
|
repoRoot,
|
|
@@ -292,17 +296,6 @@ export async function createConsoleServer(options) {
|
|
|
292
296
|
});
|
|
293
297
|
});
|
|
294
298
|
}
|
|
295
|
-
/** Resolve the loop-agent repo skills/ directory (source repo or npm pack asset). */
|
|
296
|
-
function resolveLoopAgentSkillsDir(repoRoot) {
|
|
297
|
-
// When running from dist (npm pack), skills/ is a sibling of dist/ at the
|
|
298
|
-
// package root. When running from source (tsx), it's the repo root.
|
|
299
|
-
const fromDist = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "skills");
|
|
300
|
-
const fromRepo = path.resolve(repoRoot, "skills");
|
|
301
|
-
// Prefer whichever exists; dist path wins if present (npm-installed runtime).
|
|
302
|
-
if (existsSync(fromDist))
|
|
303
|
-
return fromDist;
|
|
304
|
-
return fromRepo;
|
|
305
|
-
}
|
|
306
299
|
function requestSource(req) {
|
|
307
300
|
const forwarded = req.headers["x-forwarded-for"];
|
|
308
301
|
if (typeof forwarded === "string" && forwarded.trim()) {
|