@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +42 -11
  2. package/README.md +20 -0
  3. package/dist/cli/command-definitions.js +7 -0
  4. package/dist/cli/program.js +6 -1
  5. package/dist/commands/dag-reconcile-run.js +118 -0
  6. package/dist/commands/init.js +12 -3
  7. package/dist/governance/manifest-types.js +4 -0
  8. package/dist/worker/cli.js +216 -0
  9. package/dist/worker/closeout/apply.js +73 -0
  10. package/dist/worker/closeout/preview.js +30 -0
  11. package/dist/worker/delivery/final-verification.js +158 -0
  12. package/dist/worker/delivery/git-transaction.js +354 -0
  13. package/dist/worker/delivery/package.js +449 -0
  14. package/dist/worker/feature/decision-loader.js +68 -0
  15. package/dist/worker/feature/discover.js +14 -0
  16. package/dist/worker/feature/next-action.js +74 -0
  17. package/dist/worker/feature/reducer.js +133 -0
  18. package/dist/worker/feature/review.js +502 -0
  19. package/dist/worker/feature/run.js +313 -0
  20. package/dist/worker/feature/types.js +1 -0
  21. package/dist/worker/follow-up/approve.js +270 -0
  22. package/dist/worker/follow-up/factory.js +234 -0
  23. package/dist/worker/follow-up/paths.js +25 -0
  24. package/dist/worker/follow-up/policy.js +26 -0
  25. package/dist/worker/follow-up/schema.js +93 -0
  26. package/dist/worker/follow-up/store.js +96 -0
  27. package/dist/worker/metrics/projector.js +139 -0
  28. package/dist/worker/observability/read-model.js +256 -15
  29. package/dist/worker/observe/paths.js +17 -5
  30. package/dist/worker/observe/static/app.js +443 -61
  31. package/dist/worker/observe/static/index.html +3 -1
  32. package/dist/worker/observe/static/styles.css +86 -19
  33. package/dist/worker/pool/run-store.js +14 -2
  34. package/dist/worker/pool/validation.js +59 -0
  35. package/dist/worker/report/morning-report.js +41 -6
  36. package/dist/worker/run-task/run-task.js +1 -1
  37. package/dist/worker/runner/run-ready.js +19 -5
  38. package/dist/workflows/dag/init-hybrid.js +3 -1
  39. package/dist/workflows/dag/lifecycle.js +146 -0
  40. package/dist/workflows/dag/node-execution.js +3 -0
  41. package/dist/workflows/dag/prompt.js +16 -0
  42. package/dist/workflows/dag/report.js +2 -0
  43. package/dist/workflows/dag/runner.js +133 -104
  44. package/dist/workflows/dag/types.js +3 -0
  45. package/docs/README.md +17 -0
  46. package/docs/agent-dag-recovery-playbook.md +1 -1
  47. package/docs/architecture/runtime-boundaries.md +3 -2
  48. package/docs/design/README.md +11 -5
  49. package/docs/exec-plans/active/README.md +1 -1
  50. package/docs/exec-plans/completed/README.md +8 -1
  51. package/docs/loop-agent-harness.md +45 -2
  52. package/docs/progress/README.md +2 -0
  53. package/docs/reports/README.md +10 -0
  54. package/docs/templates/agent-dag-report.schema.json +5 -3
  55. package/docs/templates/harness.schema.json +7 -2
  56. package/docs/templates/init-evolution-review.md +4 -2
  57. package/docs/verification-matrix.md +7 -0
  58. package/harness.json +4 -3
  59. package/package.json +4 -2
  60. package/scripts/check-product-line-docs.sh +7 -3
  61. package/skills/init-capability-evolution/SKILL.md +1 -0
  62. package/skills/loop-agent/references/command-reference.md +21 -0
  63. 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 tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
247
- const baseExecuteNode = input.executeNode ??
248
- createDagNodeExecutor({
249
- runDir,
250
- runId: state.runId,
251
- spec,
252
- });
253
- const executeDynamicNode = async (dynamicInput) => {
254
- const { task } = dynamicInput;
255
- if (task.dynamicExpansion) {
256
- return executeDynamicMapExpansion({
257
- ...dynamicInput,
258
- expansion: task.dynamicExpansion,
259
- executeDynamicNode,
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
- if (task.dynamicCondition) {
263
- return executeDynamicCondition({
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
- condition: task.dynamicCondition,
266
- tasksById: dynamicInput.tasksById,
304
+ reduction: task.dynamicReduction,
267
305
  state: dynamicInput.state,
306
+ runDir: dynamicInput.runDir,
268
307
  });
269
- }
270
- if (task.dynamicLoopUntil) {
271
- return executeDynamicLoopUntil({
272
- ...dynamicInput,
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
- cwd,
343
+ state,
344
+ tasksById,
305
345
  runDir,
306
- executeNode,
307
- executeDynamicNode,
308
- observer: input.observer,
346
+ cwd,
309
347
  persistState,
310
- onPause,
311
- }),
312
- });
313
- if (pausedByNodeId) {
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
- const convergenceDecision = await runConvergencePassController({
317
- spec,
318
- state,
319
- tasksById,
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
- cwd,
322
- persistState,
374
+ oldRunDir: runDirBeforeTransfer,
375
+ nodes: state.nodes,
323
376
  });
324
- if (convergenceDecision.pausedByNodeId) {
325
- pausedByNodeId = convergenceDecision.pausedByNodeId;
326
- break;
327
- }
328
- if (convergenceDecision.retry) {
329
- continue;
377
+ if (state.convergence) {
378
+ relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
330
379
  }
331
- break;
332
- }
333
- state.finishedAt = new Date().toISOString();
334
- const runDirBeforeTransfer = runDir;
335
- if (pausedByNodeId) {
336
- state.status = "paused";
337
- await persistState();
338
- await notifyRunObserver(input.observer, "onRunFinish", state);
339
- runDir = await moveToPausedRunDir(runDir, pausedRunDir);
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
- await relocateRunArtifactPaths({
348
- runDir,
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 模板
@@ -15,7 +15,7 @@ product_line_failure_category
15
15
  recommended_follow_up
16
16
  ```
17
17
 
18
- Product-line taxonomy 定义见 `design/state-and-failure-taxonomy.md`。
18
+ Product-line taxonomy 定义见 `docs/design/state-and-failure-taxonomy.md`。
19
19
 
20
20
  **非目标(本 playbook 不覆盖、runner 不实现):**
21
21
 
@@ -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
- - **禁止**:把 raw path mutation 扩散给 runner、loop action command handler
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 并迁移 lifecycleObserve、status 和 doctor 始终只读。
82
+ - **禁止**:把 raw path mutation 扩散给 runner、loop action 或 command handler。
82
83
 
83
84
  ### Governance
84
85
 
@@ -4,18 +4,24 @@
4
4
 
5
5
  ## 文档
6
6
 
7
- | 文档 | 用途 |
8
- |---|---|
9
- | `产品线共享知识库.md` | 产品线文档仓库作为上游事实源 |
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
- | `observe-ui.md` | 已实现的 agent-worker 本地只读可观测 UI(事件流 / SSE / stale 诊断)设计、事件契约与验收边界 |
15
- | `observe-ui-optimization.md` | 2026-07-10:**已实现** Observe UI 中文化、DAG-first 总览、进行中 DAG 置顶、read-model enrichment、Pi `session-events.jsonl` 落盘与过程时间线(UI-1~UI-9) |
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