@tea-agent/loop-agent 0.9.0 → 0.10.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 +42 -11
- package/README.md +20 -0
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/dag-reconcile-run.js +118 -0
- package/dist/commands/init.js +12 -3
- package/dist/governance/manifest-types.js +4 -0
- package/dist/worker/cli.js +216 -0
- package/dist/worker/closeout/apply.js +73 -0
- package/dist/worker/closeout/preview.js +30 -0
- package/dist/worker/delivery/final-verification.js +158 -0
- package/dist/worker/delivery/git-transaction.js +354 -0
- package/dist/worker/delivery/package.js +449 -0
- package/dist/worker/feature/decision-loader.js +68 -0
- package/dist/worker/feature/discover.js +14 -0
- package/dist/worker/feature/next-action.js +74 -0
- package/dist/worker/feature/reducer.js +133 -0
- package/dist/worker/feature/review.js +502 -0
- package/dist/worker/feature/run.js +313 -0
- package/dist/worker/feature/types.js +1 -0
- package/dist/worker/follow-up/approve.js +270 -0
- package/dist/worker/follow-up/factory.js +234 -0
- package/dist/worker/follow-up/paths.js +25 -0
- package/dist/worker/follow-up/policy.js +26 -0
- package/dist/worker/follow-up/schema.js +93 -0
- package/dist/worker/follow-up/store.js +96 -0
- package/dist/worker/metrics/projector.js +139 -0
- package/dist/worker/observability/read-model.js +256 -15
- package/dist/worker/observe/paths.js +17 -5
- package/dist/worker/observe/static/app.js +443 -61
- package/dist/worker/observe/static/index.html +3 -1
- package/dist/worker/observe/static/styles.css +86 -19
- package/dist/worker/pool/run-store.js +14 -2
- package/dist/worker/pool/validation.js +59 -0
- package/dist/worker/report/morning-report.js +41 -6
- package/dist/worker/run-task/run-task.js +1 -1
- package/dist/worker/runner/run-ready.js +19 -5
- package/dist/workflows/dag/init-hybrid.js +3 -1
- package/dist/workflows/dag/lifecycle.js +146 -0
- package/dist/workflows/dag/node-execution.js +3 -0
- package/dist/workflows/dag/prompt.js +16 -0
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/runner.js +133 -104
- package/dist/workflows/dag/types.js +3 -0
- package/docs/README.md +17 -0
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/design/README.md +11 -5
- package/docs/exec-plans/active/README.md +1 -1
- package/docs/exec-plans/completed/README.md +8 -1
- package/docs/loop-agent-harness.md +45 -2
- package/docs/progress/README.md +2 -0
- package/docs/reports/README.md +10 -0
- package/docs/templates/agent-dag-report.schema.json +5 -3
- package/docs/templates/harness.schema.json +7 -2
- package/docs/templates/init-evolution-review.md +4 -2
- package/docs/verification-matrix.md +7 -0
- package/harness.json +4 -3
- package/package.json +4 -2
- package/scripts/check-product-line-docs.sh +7 -3
- package/skills/init-capability-evolution/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +21 -0
- package/skills/loop-agent/references/hybrid-dag.md +4 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rename, } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { hostname as localHostname } from "node:os";
|
|
3
4
|
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
4
5
|
import { parseDagSpec } from "./types.js";
|
|
5
6
|
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
@@ -161,10 +162,98 @@ export async function humanApprovalArtifactExists(runDir, nodeId) {
|
|
|
161
162
|
return false;
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
export function assessDagRunLiveness(input) {
|
|
166
|
+
if (input.state.status !== "running")
|
|
167
|
+
return { status: "unknown" };
|
|
168
|
+
const runner = input.state.runner;
|
|
169
|
+
if (!runner)
|
|
170
|
+
return { status: "unknown" };
|
|
171
|
+
if (runner.hostname !== (input.hostname ?? localHostname()))
|
|
172
|
+
return { status: "unknown-host" };
|
|
173
|
+
const isAlive = input.isProcessAlive ?? ((pid) => {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(pid, 0);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (!isAlive(runner.pid))
|
|
183
|
+
return { status: "orphaned", runnerAlive: false };
|
|
184
|
+
const heartbeatMs = Date.parse(runner.heartbeatAt);
|
|
185
|
+
const nowMs = (input.now ?? new Date()).getTime();
|
|
186
|
+
if (!Number.isNaN(heartbeatMs) && nowMs - heartbeatMs > (input.staleThresholdMs ?? 90_000)) {
|
|
187
|
+
return { status: "stale", runnerAlive: true };
|
|
188
|
+
}
|
|
189
|
+
const activeNode = Object.values(input.state.nodes).find((node) => node.status === "RUNNING");
|
|
190
|
+
const nodeActivityMs = Date.parse(activeNode?.lastActivityAt ?? activeNode?.startedAt ?? "");
|
|
191
|
+
if (!Number.isNaN(nodeActivityMs) && nowMs - nodeActivityMs > (input.nodeQuietThresholdMs ?? 300_000)) {
|
|
192
|
+
return { status: "node-quiet", runnerAlive: true };
|
|
193
|
+
}
|
|
194
|
+
return { status: "active", runnerAlive: true };
|
|
195
|
+
}
|
|
196
|
+
export function deriveDagRunEffectiveStatus(input) {
|
|
197
|
+
if (input.state.status === "superseded")
|
|
198
|
+
return "superseded";
|
|
199
|
+
if (input.state.status === "abandoned")
|
|
200
|
+
return "abandoned";
|
|
201
|
+
if (input.lifecycle === "paused")
|
|
202
|
+
return "paused";
|
|
203
|
+
if (input.lifecycle === "completed") {
|
|
204
|
+
return input.state.status === "finished" ? "finished" : "failed";
|
|
205
|
+
}
|
|
206
|
+
if (input.state.status === "pending")
|
|
207
|
+
return "pending";
|
|
208
|
+
if (isTerminalDagRunStatus(input.state.status)) {
|
|
209
|
+
return input.state.status === "finished" ? "finished" : "failed";
|
|
210
|
+
}
|
|
211
|
+
if (input.liveness === "orphaned" || input.liveness === "stale")
|
|
212
|
+
return "interrupted";
|
|
213
|
+
if (input.liveness === "node-quiet")
|
|
214
|
+
return "running-quiet";
|
|
215
|
+
if (input.liveness === "unknown-host")
|
|
216
|
+
return "remote-unknown";
|
|
217
|
+
if (input.liveness === "active")
|
|
218
|
+
return "running";
|
|
219
|
+
return "unknown";
|
|
220
|
+
}
|
|
221
|
+
export function assessDagRunRecoveryEligibility(input) {
|
|
222
|
+
const reasons = [];
|
|
223
|
+
const canResume = input.lifecycle === "active"
|
|
224
|
+
&& input.state.status === "running"
|
|
225
|
+
&& Boolean(input.state.humanDecisionNodeId)
|
|
226
|
+
&& Boolean(input.hasHumanApproval);
|
|
227
|
+
if (!canResume)
|
|
228
|
+
reasons.push("standard-resume-preconditions-not-met");
|
|
229
|
+
let canReconcile = true;
|
|
230
|
+
if (input.lifecycle === "completed" || isTerminalDagRunStatus(input.state.status)) {
|
|
231
|
+
canReconcile = false;
|
|
232
|
+
reasons.push("run-already-terminal");
|
|
233
|
+
}
|
|
234
|
+
if (input.lifecycle === "active"
|
|
235
|
+
&& ["active", "node-quiet", "stale", "unknown-host", "unknown"].includes(input.liveness)) {
|
|
236
|
+
canReconcile = false;
|
|
237
|
+
reasons.push("runner-not-proven-dead-or-stopped");
|
|
238
|
+
}
|
|
239
|
+
if (input.lifecycle === "paused" && input.state.status !== "paused") {
|
|
240
|
+
reasons.push("lifecycle-status-mismatch");
|
|
241
|
+
}
|
|
242
|
+
if (!input.state.runner)
|
|
243
|
+
reasons.push("missing-runner-metadata");
|
|
244
|
+
return {
|
|
245
|
+
canResume,
|
|
246
|
+
canReconcile,
|
|
247
|
+
allowedActions: canReconcile ? ["supersede", "abandon"] : [],
|
|
248
|
+
reasons: [...new Set(reasons)],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
164
251
|
export const TERMINAL_RUN_STATUSES = new Set([
|
|
165
252
|
"finished",
|
|
166
253
|
"failed",
|
|
167
254
|
"partial_failed",
|
|
255
|
+
"superseded",
|
|
256
|
+
"abandoned",
|
|
168
257
|
]);
|
|
169
258
|
export function isTerminalDagRunStatus(status) {
|
|
170
259
|
return TERMINAL_RUN_STATUSES.has(status);
|
|
@@ -248,6 +337,31 @@ export async function detectDagRunHealthIssues(input) {
|
|
|
248
337
|
advisoryAction: "Use dag doctor; approve/reject/resume may fail until lifecycle facts are consistent.",
|
|
249
338
|
});
|
|
250
339
|
}
|
|
340
|
+
const liveness = assessDagRunLiveness({ state });
|
|
341
|
+
if (liveness.status === "orphaned") {
|
|
342
|
+
issues.push({
|
|
343
|
+
code: "runner-process-missing",
|
|
344
|
+
severity: "error",
|
|
345
|
+
message: `Runner PID ${state.runner?.pid} is not alive on host ${state.runner?.hostname}`,
|
|
346
|
+
advisoryAction: "Treat this run as orphaned; inspect artifacts and start a new run instead of resuming it.",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
else if (liveness.status === "stale") {
|
|
350
|
+
issues.push({
|
|
351
|
+
code: "runner-heartbeat-stale",
|
|
352
|
+
severity: "warning",
|
|
353
|
+
message: `Runner heartbeat is stale since ${state.runner?.heartbeatAt}`,
|
|
354
|
+
advisoryAction: "Inspect node artifacts and process liveness before stopping or retrying.",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
else if (liveness.status === "node-quiet") {
|
|
358
|
+
issues.push({
|
|
359
|
+
code: "node-activity-quiet",
|
|
360
|
+
severity: "warning",
|
|
361
|
+
message: "Runner heartbeat is fresh but the current RUNNING node has produced no state activity for more than 5 minutes",
|
|
362
|
+
advisoryAction: "Inspect the node session events and executor logs before deciding whether to wait or abort.",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
251
365
|
if (lifecycle === "active" &&
|
|
252
366
|
state.status === "running" &&
|
|
253
367
|
state.humanDecisionNodeId) {
|
|
@@ -340,6 +454,14 @@ export async function buildDagOperatorRunSummary(entry) {
|
|
|
340
454
|
pendingNodes: [],
|
|
341
455
|
finishedNodes: [],
|
|
342
456
|
healthIssues,
|
|
457
|
+
effectiveStatus: entry.lifecycle === "paused" ? "paused" : "unknown",
|
|
458
|
+
stateConsistent: false,
|
|
459
|
+
recoveryEligibility: {
|
|
460
|
+
canResume: false,
|
|
461
|
+
canReconcile: false,
|
|
462
|
+
allowedActions: [],
|
|
463
|
+
reasons: ["missing-state-json"],
|
|
464
|
+
},
|
|
343
465
|
nextRecommendedAction: deriveOperatorNextAction({
|
|
344
466
|
lifecycle: entry.lifecycle,
|
|
345
467
|
state: {
|
|
@@ -363,9 +485,22 @@ export async function buildDagOperatorRunSummary(entry) {
|
|
|
363
485
|
runDir: entry.runDir,
|
|
364
486
|
state,
|
|
365
487
|
});
|
|
488
|
+
const liveness = assessDagRunLiveness({ state });
|
|
366
489
|
const hasHumanApproval = state.humanDecisionNodeId
|
|
367
490
|
? await humanApprovalArtifactExists(entry.runDir, state.humanDecisionNodeId)
|
|
368
491
|
: false;
|
|
492
|
+
const effectiveStatus = deriveDagRunEffectiveStatus({
|
|
493
|
+
lifecycle: entry.lifecycle,
|
|
494
|
+
state,
|
|
495
|
+
liveness: liveness.status,
|
|
496
|
+
});
|
|
497
|
+
const stateConsistent = !healthIssues.some((issue) => ["lifecycle-status-mismatch", "non-terminal-in-completed"].includes(issue.code));
|
|
498
|
+
const recoveryEligibility = assessDagRunRecoveryEligibility({
|
|
499
|
+
lifecycle: entry.lifecycle,
|
|
500
|
+
state,
|
|
501
|
+
liveness: liveness.status,
|
|
502
|
+
hasHumanApproval,
|
|
503
|
+
});
|
|
369
504
|
return {
|
|
370
505
|
runId: state.runId,
|
|
371
506
|
title: state.title,
|
|
@@ -380,6 +515,10 @@ export async function buildDagOperatorRunSummary(entry) {
|
|
|
380
515
|
pendingNodes: listPendingNodeIds(state),
|
|
381
516
|
finishedNodes: listFinishedNodeIds(state),
|
|
382
517
|
healthIssues,
|
|
518
|
+
liveness: liveness.status,
|
|
519
|
+
effectiveStatus,
|
|
520
|
+
stateConsistent,
|
|
521
|
+
recoveryEligibility,
|
|
383
522
|
nextRecommendedAction: deriveOperatorNextAction({
|
|
384
523
|
lifecycle: entry.lifecycle,
|
|
385
524
|
state,
|
|
@@ -552,6 +691,13 @@ async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
|
552
691
|
"",
|
|
553
692
|
`- run id: ${runId}`,
|
|
554
693
|
`- lifecycle: ${located.lifecycle}`,
|
|
694
|
+
`- effective status: ${summary.effectiveStatus}`,
|
|
695
|
+
`- raw status: ${summary.status}`,
|
|
696
|
+
`- state consistent: ${summary.stateConsistent ? "yes" : "no"}`,
|
|
697
|
+
`- liveness: ${summary.liveness ?? "unknown"}`,
|
|
698
|
+
`- can resume: ${summary.recoveryEligibility.canResume ? "yes" : "no"}`,
|
|
699
|
+
`- can reconcile: ${summary.recoveryEligibility.canReconcile ? "yes" : "no"}`,
|
|
700
|
+
`- health issues: ${summary.healthIssues.length > 0 ? summary.healthIssues.map((issue) => `${issue.code}: ${issue.message}`).join("; ") : "none"}`,
|
|
555
701
|
`- failed node: ${failure.nodeId ?? "-"}`,
|
|
556
702
|
`- raw failure: ${rawFailureCategory ?? "-"}`,
|
|
557
703
|
`- normalized category: ${normalizedCategory}`,
|
|
@@ -113,6 +113,7 @@ export async function executeDagNode(input) {
|
|
|
113
113
|
const node = state.nodes[nodeId];
|
|
114
114
|
node.status = "RUNNING";
|
|
115
115
|
node.startedAt = new Date().toISOString();
|
|
116
|
+
node.lastActivityAt = node.startedAt;
|
|
116
117
|
if (task.shell?.verifyEvidence) {
|
|
117
118
|
node.verifyEvidence = task.shell.verifyEvidence;
|
|
118
119
|
}
|
|
@@ -140,6 +141,7 @@ export async function executeDagNode(input) {
|
|
|
140
141
|
node.stderr = result.stderr;
|
|
141
142
|
node.failureCategory = result.failureCategory;
|
|
142
143
|
node.finishedAt = new Date().toISOString();
|
|
144
|
+
node.lastActivityAt = node.finishedAt;
|
|
143
145
|
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
144
146
|
}
|
|
145
147
|
catch (error) {
|
|
@@ -202,6 +204,7 @@ export async function executeDagNode(input) {
|
|
|
202
204
|
await notifyNodeObserver(input.observer, "onNodeOutput", nodeId, state, outputChunk);
|
|
203
205
|
}
|
|
204
206
|
node.finishedAt = new Date().toISOString();
|
|
207
|
+
node.lastActivityAt = node.finishedAt;
|
|
205
208
|
node.status = result.ok ? "FINISHED" : "ERROR";
|
|
206
209
|
if (result.ok) {
|
|
207
210
|
const decisionRecord = await recordDecisionEnvelopeForNode({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_DAG_OUTPUT_LANGUAGE, } from "./types.js";
|
|
1
2
|
import { formatStdoutPreview, formatUpstreamArtifactPointerMap, } from "./upstream-artifacts.js";
|
|
2
3
|
export const MAX_UPSTREAM_CHARS = 2_000;
|
|
3
4
|
/** Shared bullets for DAG authoring templates, docs, and planner-node envelopes. */
|
|
@@ -41,6 +42,20 @@ function formatResolvedSkillInstructions(instructions) {
|
|
|
41
42
|
.map((instruction) => instruction.promptText)
|
|
42
43
|
.join("\n\n---\n\n");
|
|
43
44
|
}
|
|
45
|
+
export function formatOutputLanguageBlock(language = DEFAULT_DAG_OUTPUT_LANGUAGE) {
|
|
46
|
+
if (language === "en") {
|
|
47
|
+
return [
|
|
48
|
+
"Write prose, analysis, reports, summaries, and documentation in English.",
|
|
49
|
+
"Keep code, commands, paths, identifiers, JSON keys, exact protocol tokens, verdict lines, and output-contract literals unchanged.",
|
|
50
|
+
"If the node task explicitly requires another language, follow the explicit task requirement.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
"使用简体中文撰写说明、分析、报告、总结和文档正文。",
|
|
55
|
+
"代码、命令、路径、标识符、JSON 字段、精确协议 token、VERDICT 行以及 output contract 中要求的字面量保持原样,不要翻译。",
|
|
56
|
+
"如果当前节点任务明确要求其他语言,以节点的明确要求为准。",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
44
59
|
export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHARS) {
|
|
45
60
|
const sections = [];
|
|
46
61
|
for (const depId of task.depends_on) {
|
|
@@ -91,6 +106,7 @@ export function buildDagNodePromptEnvelope(input) {
|
|
|
91
106
|
`<dag_objective>\n${objective}\n</dag_objective>`,
|
|
92
107
|
`<success_criteria>\n${successCriteria}\n</success_criteria>`,
|
|
93
108
|
`<global_constraints>\n${globalConstraints}\n</global_constraints>`,
|
|
109
|
+
`<output_language>\n${formatOutputLanguageBlock(spec.outputLanguage)}\n</output_language>`,
|
|
94
110
|
[
|
|
95
111
|
"<node_contract>",
|
|
96
112
|
`Role: ${role}`,
|
|
@@ -20,6 +20,8 @@ const dagRunStatusSchema = z.enum([
|
|
|
20
20
|
"partial_failed",
|
|
21
21
|
"failed",
|
|
22
22
|
"paused",
|
|
23
|
+
"superseded",
|
|
24
|
+
"abandoned",
|
|
23
25
|
]);
|
|
24
26
|
const dagRecoveryActionSchema = z.enum(DAG_RECOVERY_ACTIONS);
|
|
25
27
|
const dagProductLineFailureCategorySchema = z.enum(dagProductLineFailureCategoryValues);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { hostname } from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
4
5
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
@@ -12,7 +13,7 @@ import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeD
|
|
|
12
13
|
import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
|
|
13
14
|
import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
|
|
14
15
|
import { topoSortToRanks } from "./topo.js";
|
|
15
|
-
import { parseDagSpec, } from "./types.js";
|
|
16
|
+
import { parseDagSpec, resolveModelForTask, } from "./types.js";
|
|
16
17
|
import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
|
|
17
18
|
import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
|
|
18
19
|
import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
|
|
@@ -109,6 +110,9 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
109
110
|
status: "PENDING",
|
|
110
111
|
executor: task.executor,
|
|
111
112
|
complexity: task.complexity,
|
|
113
|
+
...(resolveModelForTask(task, spec.executorModels)
|
|
114
|
+
? { model: resolveModelForTask(task, spec.executorModels) }
|
|
115
|
+
: {}),
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
return {
|
|
@@ -117,6 +121,12 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
117
121
|
runId,
|
|
118
122
|
cwd: opts.cwd,
|
|
119
123
|
startedAt: new Date().toISOString(),
|
|
124
|
+
runner: {
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
hostname: hostname(),
|
|
127
|
+
startedAt: new Date().toISOString(),
|
|
128
|
+
heartbeatAt: new Date().toISOString(),
|
|
129
|
+
},
|
|
120
130
|
status: opts.initOnly || opts.dryRun ? "pending" : "running",
|
|
121
131
|
ranks,
|
|
122
132
|
nodes,
|
|
@@ -213,6 +223,13 @@ export async function resumeDagRun(opts) {
|
|
|
213
223
|
}
|
|
214
224
|
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
215
225
|
state.status = "running";
|
|
226
|
+
const resumedAt = new Date().toISOString();
|
|
227
|
+
state.runner = {
|
|
228
|
+
pid: process.pid,
|
|
229
|
+
hostname: hostname(),
|
|
230
|
+
startedAt: resumedAt,
|
|
231
|
+
heartbeatAt: resumedAt,
|
|
232
|
+
};
|
|
216
233
|
const activeRunDir = getDagRunDir(opts.cwd, "active", state.runId);
|
|
217
234
|
const completedRunDir = getDagRunDir(opts.cwd, "completed", state.runId);
|
|
218
235
|
const pausedRunDir = getDagRunDir(opts.cwd, "paused", state.runId);
|
|
@@ -243,124 +260,136 @@ async function executeDagCheckpoint(input) {
|
|
|
243
260
|
stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
|
|
244
261
|
await stateWriteQueue;
|
|
245
262
|
};
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
const heartbeatTimer = setInterval(() => {
|
|
264
|
+
if (!state.runner)
|
|
265
|
+
return;
|
|
266
|
+
state.runner.heartbeatAt = new Date().toISOString();
|
|
267
|
+
void persistState().catch(() => { });
|
|
268
|
+
}, 15_000);
|
|
269
|
+
heartbeatTimer.unref();
|
|
270
|
+
try {
|
|
271
|
+
const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
|
|
272
|
+
const baseExecuteNode = input.executeNode ??
|
|
273
|
+
createDagNodeExecutor({
|
|
274
|
+
runDir,
|
|
275
|
+
runId: state.runId,
|
|
276
|
+
spec,
|
|
260
277
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
278
|
+
const executeDynamicNode = async (dynamicInput) => {
|
|
279
|
+
const { task } = dynamicInput;
|
|
280
|
+
if (task.dynamicExpansion) {
|
|
281
|
+
return executeDynamicMapExpansion({
|
|
282
|
+
...dynamicInput,
|
|
283
|
+
expansion: task.dynamicExpansion,
|
|
284
|
+
executeDynamicNode,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (task.dynamicCondition) {
|
|
288
|
+
return executeDynamicCondition({
|
|
289
|
+
task,
|
|
290
|
+
condition: task.dynamicCondition,
|
|
291
|
+
tasksById: dynamicInput.tasksById,
|
|
292
|
+
state: dynamicInput.state,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (task.dynamicLoopUntil) {
|
|
296
|
+
return executeDynamicLoopUntil({
|
|
297
|
+
...dynamicInput,
|
|
298
|
+
loop: task.dynamicLoopUntil,
|
|
299
|
+
executeDynamicNode,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return executeDynamicReduction({
|
|
264
303
|
task,
|
|
265
|
-
|
|
266
|
-
tasksById: dynamicInput.tasksById,
|
|
304
|
+
reduction: task.dynamicReduction,
|
|
267
305
|
state: dynamicInput.state,
|
|
306
|
+
runDir: dynamicInput.runDir,
|
|
268
307
|
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
loop: task.dynamicLoopUntil,
|
|
274
|
-
executeDynamicNode,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
return executeDynamicReduction({
|
|
278
|
-
task,
|
|
279
|
-
reduction: task.dynamicReduction,
|
|
280
|
-
state: dynamicInput.state,
|
|
281
|
-
runDir: dynamicInput.runDir,
|
|
282
|
-
});
|
|
283
|
-
};
|
|
284
|
-
let pausedByNodeId;
|
|
285
|
-
while (true) {
|
|
286
|
-
pausedByNodeId = await executeDagRanksOnce({
|
|
287
|
-
state,
|
|
288
|
-
ranks,
|
|
289
|
-
tasksById,
|
|
290
|
-
maxConcurrent,
|
|
291
|
-
persistState,
|
|
292
|
-
createExecuteNodeForRank: (rankCursorNodeIds) => buildRankAwareExecuteNode({
|
|
293
|
-
baseExecuteNode,
|
|
294
|
-
customExecuteNode: input.executeNode,
|
|
295
|
-
rankCursorNodeIds,
|
|
296
|
-
tasksById,
|
|
297
|
-
meta: { runDir, runId: state.runId, spec },
|
|
298
|
-
}),
|
|
299
|
-
executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
|
|
300
|
-
nodeId,
|
|
301
|
-
tasksById,
|
|
308
|
+
};
|
|
309
|
+
let pausedByNodeId;
|
|
310
|
+
while (true) {
|
|
311
|
+
pausedByNodeId = await executeDagRanksOnce({
|
|
302
312
|
state,
|
|
313
|
+
ranks,
|
|
314
|
+
tasksById,
|
|
315
|
+
maxConcurrent,
|
|
316
|
+
persistState,
|
|
317
|
+
createExecuteNodeForRank: (rankCursorNodeIds) => buildRankAwareExecuteNode({
|
|
318
|
+
baseExecuteNode,
|
|
319
|
+
customExecuteNode: input.executeNode,
|
|
320
|
+
rankCursorNodeIds,
|
|
321
|
+
tasksById,
|
|
322
|
+
meta: { runDir, runId: state.runId, spec },
|
|
323
|
+
}),
|
|
324
|
+
executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
|
|
325
|
+
nodeId,
|
|
326
|
+
tasksById,
|
|
327
|
+
state,
|
|
328
|
+
spec,
|
|
329
|
+
cwd,
|
|
330
|
+
runDir,
|
|
331
|
+
executeNode,
|
|
332
|
+
executeDynamicNode,
|
|
333
|
+
observer: input.observer,
|
|
334
|
+
persistState,
|
|
335
|
+
onPause,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
if (pausedByNodeId) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
const convergenceDecision = await runConvergencePassController({
|
|
303
342
|
spec,
|
|
304
|
-
|
|
343
|
+
state,
|
|
344
|
+
tasksById,
|
|
305
345
|
runDir,
|
|
306
|
-
|
|
307
|
-
executeDynamicNode,
|
|
308
|
-
observer: input.observer,
|
|
346
|
+
cwd,
|
|
309
347
|
persistState,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
348
|
+
});
|
|
349
|
+
if (convergenceDecision.pausedByNodeId) {
|
|
350
|
+
pausedByNodeId = convergenceDecision.pausedByNodeId;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
if (convergenceDecision.retry) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
314
356
|
break;
|
|
315
357
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
358
|
+
state.finishedAt = new Date().toISOString();
|
|
359
|
+
const runDirBeforeTransfer = runDir;
|
|
360
|
+
if (pausedByNodeId) {
|
|
361
|
+
state.status = "paused";
|
|
362
|
+
await persistState();
|
|
363
|
+
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
364
|
+
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
finalizeTerminalRunStatus(state, spec.tasks.length);
|
|
368
|
+
await persistState();
|
|
369
|
+
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
370
|
+
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
371
|
+
}
|
|
372
|
+
await relocateRunArtifactPaths({
|
|
320
373
|
runDir,
|
|
321
|
-
|
|
322
|
-
|
|
374
|
+
oldRunDir: runDirBeforeTransfer,
|
|
375
|
+
nodes: state.nodes,
|
|
323
376
|
});
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
if (convergenceDecision.retry) {
|
|
329
|
-
continue;
|
|
377
|
+
if (state.convergence) {
|
|
378
|
+
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
330
379
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
finalizeTerminalRunStatus(state, spec.tasks.length);
|
|
343
|
-
await persistState();
|
|
344
|
-
await notifyRunObserver(input.observer, "onRunFinish", state);
|
|
345
|
-
runDir = await moveToCompletedRunDir(runDir, completedRunDir);
|
|
380
|
+
await persistState({ allowCompletedFactsWrite: true });
|
|
381
|
+
return {
|
|
382
|
+
title: spec.title,
|
|
383
|
+
runId: state.runId,
|
|
384
|
+
status: state.status,
|
|
385
|
+
ranks,
|
|
386
|
+
nodes: state.nodes,
|
|
387
|
+
runDir,
|
|
388
|
+
};
|
|
346
389
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
oldRunDir: runDirBeforeTransfer,
|
|
350
|
-
nodes: state.nodes,
|
|
351
|
-
});
|
|
352
|
-
if (state.convergence) {
|
|
353
|
-
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
390
|
+
finally {
|
|
391
|
+
clearInterval(heartbeatTimer);
|
|
354
392
|
}
|
|
355
|
-
await persistState({ allowCompletedFactsWrite: true });
|
|
356
|
-
return {
|
|
357
|
-
title: spec.title,
|
|
358
|
-
runId: state.runId,
|
|
359
|
-
status: state.status,
|
|
360
|
-
ranks,
|
|
361
|
-
nodes: state.nodes,
|
|
362
|
-
runDir,
|
|
363
|
-
};
|
|
364
393
|
}
|
|
365
394
|
async function notifyRunObserver(observer, event, state) {
|
|
366
395
|
try {
|
|
@@ -234,9 +234,12 @@ export const dagExecutorModelsSchema = z
|
|
|
234
234
|
pi: dagModelsSchema.optional(),
|
|
235
235
|
})
|
|
236
236
|
.partial();
|
|
237
|
+
export const dagOutputLanguageSchema = z.enum(["zh-CN", "en"]);
|
|
238
|
+
export const DEFAULT_DAG_OUTPUT_LANGUAGE = "zh-CN";
|
|
237
239
|
export const dagSpecSchema = z.object({
|
|
238
240
|
version: dagVersionSchema,
|
|
239
241
|
title: z.string().min(1),
|
|
242
|
+
outputLanguage: dagOutputLanguageSchema.optional(),
|
|
240
243
|
objective: z.string().optional(),
|
|
241
244
|
successCriteria: z.array(z.string()).optional(),
|
|
242
245
|
globalConstraints: z.array(z.string()).optional(),
|
package/docs/README.md
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
- `verification-matrix.md` — 验证命令选择
|
|
13
13
|
- `production-readiness.md` — Production Readiness v0.1 范围、证据与 DAG hardening 标准
|
|
14
14
|
- `loop-agent-harness.md` — runtime 与 command surface 概览
|
|
15
|
+
- `exec-plans/completed/2026-07-11-m2-01-feature-review-model.md` — 第二月 Feature read model 与 `feature review` 已完成实施合同
|
|
16
|
+
- `exec-plans/completed/2026-07-12-m2-02-feature-run-orchestration.md` — 第二月 Feature run 薄编排已完成实施合同
|
|
17
|
+
- `exec-plans/completed/2026-07-12-m2-03-follow-up-tracer-bullet.md` — 第二月 ProductBug Follow-up tracer bullet 已完成合同
|
|
18
|
+
- `exec-plans/completed/2026-07-12-m2-04-follow-up-hardening.md` — 第二月 Follow-up 全分类与事务故障注入已完成合同
|
|
19
|
+
- `exec-plans/completed/2026-07-12-m2-05-git-transaction.md` — 第二月显式授权 Git checkpoint 与失败恢复已完成合同
|
|
20
|
+
- `exec-plans/completed/2026-07-12-m2-06-delivery-closeout-preview.md` — 第二月 Delivery、Acceptance Coverage 与 Closeout preview 已完成合同
|
|
21
|
+
- `exec-plans/completed/2026-07-12-m2-07-feature-decision-metrics.md` — 第二月 Feature 决策体验与 metrics 已完成合同
|
|
22
|
+
- `exec-plans/completed/2026-07-12-m2-08-closeout-dogfood-release.md` — 第二月 Closeout apply、真实 dogfood 与发布证据已完成合同
|
|
15
23
|
- `agent-dag-runner.md` — Agent DAG runner 指南
|
|
16
24
|
- `cursor-executor-usage.md` — Cursor executor 用法
|
|
17
25
|
- `dynamic-workflow-dag-engine-roadmap.md` — Dynamic Workflow DAG Engine 路线图与适配分析
|
|
@@ -41,6 +49,15 @@
|
|
|
41
49
|
- `reports/2026-07-11-command-performance-followups.md` — Observe 快照复用、输出缓冲和 reference index 遍历优化的验证记录
|
|
42
50
|
- `reports/2026-07-11-observe-terminal-dag-kpi.md` — 终态 DAG 被误计为活跃数的修复记录
|
|
43
51
|
- `reports/2026-07-12-observe-warm-console-redesign.md` — 暖白运行控制台视觉重构与桌面验证记录
|
|
52
|
+
- `reports/2026-07-12-dag-state-reconciliation.md` — DAG 实际状态判定、安全收口命令与历史失联运行处理记录
|
|
53
|
+
- `reports/2026-07-12-dag-state-reconciliation-init-evolution-review.md` — DAG 状态收口能力对初始化 surface 的影响审查
|
|
54
|
+
- `reports/2026-07-12-observe-dag-node-model.md` — Dashboard DAG 节点模型显示与历史运行兼容修复记录
|
|
55
|
+
- `reports/2026-07-12-observe-dag-node-model-init-evolution-review.md` — 节点模型持久化与 Observe 投影对初始化 surface 的影响审查
|
|
56
|
+
- `reports/2026-07-12-dag-output-language.md` — Agent DAG 默认中文输出与可配置语言规则实现记录
|
|
57
|
+
- `reports/2026-07-12-dag-output-language-init-evolution-review.md` — DAG 输出语言配置对初始化 surface 的影响审查
|
|
58
|
+
- `reports/2026-07-12-init-evolution-strict-report-range.md` — 严格 init 演化校验拒绝无关历史报告的修复与验证证据
|
|
59
|
+
- `reports/2026-07-12-dag-liveness-recovery.md` — DAG runner 心跳、孤儿运行诊断与 Observe 活跃筛选的修复记录
|
|
60
|
+
- `reports/2026-07-12-dag-liveness-init-evolution-review.md` — DAG 活性元数据对初始化投影和旧项目兼容性的审查
|
|
44
61
|
- `decisions/README.md` — 架构决策
|
|
45
62
|
- `skills/README.md` — repo-local skill registry and vetting notes
|
|
46
63
|
- `templates/` — 可复用的规划、报告与 DAG 模板
|
|
@@ -77,8 +77,9 @@ Governance (scripts/check-*.sh, src/governance/)
|
|
|
77
77
|
### Infrastructure / Store
|
|
78
78
|
|
|
79
79
|
- **位置**:`src/infrastructure/harness/**`(按计划逐步引入);过渡期部分逻辑仍在 `src/workflows/dag/lifecycle.ts`、`src/records/**`。
|
|
80
|
-
- **职责**:`.harness/tasks`、`.harness/dag-runs`、`.harness/runs`、loop state 的集中读写;completed run facts 只读约束。
|
|
81
|
-
-
|
|
80
|
+
- **职责**:`.harness/tasks`、`.harness/dag-runs`、`.harness/runs`、loop state 的集中读写;completed run facts 只读约束。
|
|
81
|
+
- **DAG recovery mutation**:`dag reconcile-run` 是显式 operator 边界;默认仅检查,只有给出 action + reason 且 runner 已证明停止时才能保存原始快照、写 terminal reconciliation facts 并迁移 lifecycle。Observe、status 和 doctor 始终只读。
|
|
82
|
+
- **禁止**:把 raw path mutation 扩散给 runner、loop action 或 command handler。
|
|
82
83
|
|
|
83
84
|
### Governance
|
|
84
85
|
|
package/docs/design/README.md
CHANGED
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
## 文档
|
|
6
6
|
|
|
7
|
-
| 文档 | 用途 |
|
|
8
|
-
|---|---|
|
|
9
|
-
|
|
|
7
|
+
| 文档 | 用途 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `DESIGN-cursor.md` | Observe 暖白运行控制台采用的 Cursor 风格视觉参考与设计 token |
|
|
10
|
+
| `DESIGN-lovable.md` | Lovable 风格的暖色视觉系统参考 |
|
|
11
|
+
| `产品线共享知识库.md` | 产品线文档仓库作为上游事实源 |
|
|
10
12
|
| `研发模式.md` | 10 个工作日 Feature 团队工作流 |
|
|
11
13
|
| `六个月规划.md` | 六个月路线图与目标架构 |
|
|
12
14
|
| `taskspec-to-loop-agent-mapping.md` | 将产品线 TaskSpec 适配为 `loop-agent` task 的契约 |
|
|
13
15
|
| `state-and-failure-taxonomy.md` | 文档、Task Pool、DAG、Loop 共用的 canonical status 与 failure taxonomy |
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
+
| `archive/2026-07-07-agent-worker-plan.md` | 已归档:Worker 落地计划(基线 0.3.0,M1-M7 物化/调用/Task Pool/batch/morning report 路线图;已随 0.8.0 发布实现于 `src/worker/`) |
|
|
17
|
+
| `archive/2026-07-07-taskspec-plan.md` | 已归档:TaskSpec v0.1 schema 设计(基线 0.3.0,TaskSpec/AcceptanceSpec/TaskGraphSpec schema 与 materialize 规则;已实现于 `src/worker/task-spec/`) |
|
|
16
18
|
| `archive/2026-07-10-下一阶段任务-功能开发完成.md` | 已归档:真实样本、失败硬化与 QA 闭环的功能性开发完成记录 |
|
|
19
|
+
| `archive/2026-07-10-observe-ui.md` | 已归档:agent-worker 本地只读可观测 UI v0 设计(事件流 / SSE / stale 诊断、事件契约与验收边界;OBS-001~010 已完成,0.9.0 暖白重设计在其上演进) |
|
|
20
|
+
| `archive/2026-07-10-observe-ui-optimization.md` | 已归档:Observe UI 中文化、DAG-first 总览、进行中 DAG 置顶、read-model enrichment、Pi `session-events.jsonl` 落盘与过程时间线(UI-1~UI-9 已实现) |
|
|
21
|
+
| `archive/2026-07-10-observe-ui-goal.md` | 已归档:OBS-001~010 逐任务进度看板(全部 ✅ done) |
|
|
17
22
|
| `archive/2026-07-11-第一月规划.md` | 已归档:首月落地计划(0.8.0 发布 + Round 1/2/3 累计 20 runs / 10 success / 50% / 两次 owner morning decision,第一个月闭环目标全部达成) |
|
|
18
23
|
| `archive/2026-07-11-第一月wbs.md` | 已归档:首月 WBS 与分工(与第一月规划配套,任务编号与估算为历史记录) |
|
|
24
|
+
| `archive/2026-07-12-第二月规划.md` | 已归档:第二月本地 Feature 交付闭环计划(M2-01~08 全部完成;F-2026-002 完整 Closeout、F-2026-003 真实失败恢复闭环、月末硬指标全达成;完成审计见 `docs/reports/2026-07-12-m2-completion-audit.md`) |
|
|
19
25
|
|
|
20
26
|
## 命名约定(2026-07-09 起)
|
|
21
27
|
|