@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
@@ -35,7 +35,9 @@
35
35
  <h2>运行总览</h2>
36
36
  <p>聚焦正在推进的 DAG、需要处理的风险与近期运行记录。</p>
37
37
  </div>
38
- <section id="dashboard-kpi" class="kpi-grid panel panel-kpi" aria-label="运行摘要"></section>
38
+ <div id="dashboard-repo" class="repo-banner" aria-label="当前仓库"></div>
39
+ <section id="dashboard-features" class="panel panel-features" aria-label="Feature 决策"></section>
40
+ <section id="dashboard-kpi" class="panel panel-kpi" aria-label="运行摘要"></section>
39
41
  <div class="dashboard-workspace">
40
42
  <section id="dashboard-active-dags" class="panel panel-primary"></section>
41
43
  <aside id="dashboard-risk" class="panel panel-risk" aria-label="运行风险"></aside>
@@ -238,12 +238,22 @@ td:first-child,
238
238
 
239
239
  .view-dashboard { display: grid; gap: var(--space-4); }
240
240
 
241
+ .repo-banner { display: flex; align-items: center; gap: var(--space-3); padding: 10px 14px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-lg); background: var(--surface); }
242
+ .repo-banner > i { color: var(--muted); font-size: 18px; }
243
+ .repo-banner-text { display: grid; gap: 2px; min-width: 0; }
244
+ .repo-banner-name { color: var(--ink); font-size: 14px; font-weight: 700; overflow-wrap: anywhere; }
245
+ .repo-banner-path { color: var(--muted); font-size: 11px; overflow-wrap: anywhere; }
246
+
241
247
  .panel-kpi { padding: 0; background: transparent; border: 0; }
242
248
 
243
- .kpi-grid {
249
+ .kpi-group { margin: 0; padding: 0; border: 0; display: grid; gap: var(--space-3); }
250
+ .kpi-group + .kpi-group { margin-top: var(--space-3); }
251
+ .kpi-group-legend { padding: 0; color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
252
+
253
+ .kpi-grid {
244
254
  display: grid;
245
255
  grid-template-columns: repeat(6, minmax(0, 1fr));
246
- overflow: hidden;
256
+ overflow: visible;
247
257
  border: 1px solid var(--hairline-strong);
248
258
  border-radius: var(--radius-lg);
249
259
  background: var(--surface);
@@ -259,8 +269,15 @@ td:first-child,
259
269
 
260
270
  .kpi-card:first-child { border-left: 0; }
261
271
  .kpi-label { margin-bottom: 9px; color: var(--muted); font-size: 11px; font-weight: 650; }
272
+ .kpi-value-row { display: flex; align-items: baseline; gap: 6px; }
262
273
  .kpi-value { color: var(--ink); font-size: 26px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
263
274
 
275
+ .kpi-help { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--muted-soft); cursor: help; font-size: 13px; line-height: 1; }
276
+ .kpi-help:hover, .kpi-help:focus-visible { color: var(--ink); background: var(--surface-muted); outline: none; }
277
+ .kpi-help:focus-visible { box-shadow: 0 0 0 2px var(--hairline-strong); }
278
+ .kpi-tip { position: absolute; left: 12px; right: 12px; top: calc(100% - 8px); z-index: 10; max-width: 280px; padding: 8px 10px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-md); background: var(--surface-ink); color: var(--canvas); font-size: 11px; font-weight: 400; line-height: 1.5; box-shadow: var(--shadow-panel); opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--transition-fast); }
279
+ .kpi-help:hover + .kpi-tip, .kpi-help:focus-visible + .kpi-tip { opacity: 1; visibility: visible; }
280
+
264
281
  .dashboard-workspace,
265
282
  .dashboard-secondary {
266
283
  display: grid;
@@ -272,19 +289,29 @@ td:first-child,
272
289
  .dashboard-secondary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
273
290
 
274
291
  .panel-risk {
275
- display: grid;
276
- grid-template-columns: minmax(130px, .28fr) minmax(0, 1fr) auto;
277
- align-items: center;
278
- gap: var(--space-4);
292
+ display: block;
279
293
  background: var(--canvas-soft);
280
294
  }
281
295
 
282
296
  .panel-risk-attention { border-color: color-mix(in srgb, var(--red) 50%, var(--hairline)); background: #fff8f9; }
283
- .panel-risk .section-heading { margin: 0; }
284
- .risk-total { margin: 0; color: var(--amber); font-size: 38px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
297
+ .panel-risk .section-heading { margin: 0 0 var(--space-3); }
298
+ .risk-blocks { display: grid; gap: var(--space-4); }
299
+ .risk-block { display: grid; grid-template-columns: minmax(120px, .25fr) minmax(0, 1fr); align-items: start; gap: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--hairline); }
300
+ .risk-block:first-child { padding-top: 0; border-top: 0; }
301
+ .risk-block-label { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
302
+ .risk-block-dag .risk-block-label { color: var(--ink); }
303
+ .risk-block-worker .risk-block-label { color: var(--ink); }
304
+ .risk-total { margin: 0; color: var(--amber); font-size: 32px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
285
305
  .panel-risk-attention .risk-total { color: var(--red); }
286
306
  .risk-summary { margin: 0; color: var(--body); font-size: 12px; line-height: 1.6; }
287
- .panel-risk .risk-empty { grid-column: 2 / -1; }
307
+ .panel-risk .risk-empty { margin: 0; }
308
+
309
+ .projection-fault { display: flex; align-items: flex-start; gap: var(--space-3); padding: 14px 16px; border: 1px solid color-mix(in srgb, var(--red) 50%, var(--hairline)); border-radius: var(--radius-lg); background: #fff8f9; }
310
+ .projection-fault > i { color: var(--red); font-size: 20px; line-height: 1.4; }
311
+ .projection-fault-body { display: grid; gap: 4px; min-width: 0; }
312
+ .projection-fault-title { margin: 0; color: var(--ink); font-size: 14px; font-weight: 700; }
313
+ .projection-fault-message { margin: 0; color: var(--body); font-size: 12px; overflow-wrap: anywhere; }
314
+ .projection-fault-at { margin: 0; color: var(--muted); font-size: 11px; }
288
315
 
289
316
  .risk-link,
290
317
  .active-dag-link,
@@ -383,7 +410,7 @@ tr.clickable:focus-within { outline: 2px solid var(--orange); outline-offset: -2
383
410
  bottom: 24px;
384
411
  display: grid;
385
412
  grid-template-rows: auto minmax(0, 1fr);
386
- width: min(440px, calc(100vw - 48px));
413
+ width: min(var(--dag-inspector-width, 440px), calc(100vw - 48px));
387
414
  min-width: 0;
388
415
  padding: 0;
389
416
  overflow: hidden;
@@ -393,9 +420,37 @@ tr.clickable:focus-within { outline: 2px solid var(--orange); outline-offset: -2
393
420
  pointer-events: none;
394
421
  transform: translateX(calc(100% + 24px));
395
422
  visibility: hidden;
396
- transition: transform var(--transition-base), opacity var(--transition-base);
423
+ transition: transform var(--transition-base), opacity var(--transition-base), visibility 0s linear 240ms;
424
+ }
425
+ .dag-inspector.is-open { opacity: 1; pointer-events: auto; transform: translateX(0); visibility: visible; transition-delay: 0s, 0s, 0s; }
426
+ .dag-inspector-resize-handle {
427
+ position: absolute;
428
+ z-index: 1;
429
+ top: 0;
430
+ bottom: 0;
431
+ left: 0;
432
+ width: 18px;
433
+ cursor: col-resize;
434
+ touch-action: none;
435
+ outline: none;
436
+ }
437
+ .dag-inspector-resize-handle::before {
438
+ content: "";
439
+ position: absolute;
440
+ top: 50%;
441
+ left: 7px;
442
+ width: 3px;
443
+ height: 52px;
444
+ transform: translateY(-50%);
445
+ border-radius: 2px;
446
+ background: var(--hairline-strong);
447
+ opacity: 0;
448
+ transition: opacity var(--transition-base), background var(--transition-base);
397
449
  }
398
- .dag-inspector.is-open { opacity: 1; pointer-events: auto; transform: translateX(0); visibility: visible; }
450
+ .dag-inspector-resize-handle:hover::before,
451
+ .dag-inspector-resize-handle:focus-visible::before { background: var(--orange); opacity: 1; }
452
+ .dag-inspector-resize-handle:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
453
+ body.is-resizing-dag-inspector { cursor: col-resize; user-select: none; }
399
454
  .dag-inspector-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3); align-items: start; padding: var(--space-4) var(--space-4) var(--space-3); border-bottom: 1px solid var(--hairline); background: var(--canvas-soft); }
400
455
  .dag-inspector-title { display: grid; gap: 6px; min-width: 0; }
401
456
  .dag-inspector-title h3 { margin: 0; font-size: 14px; }
@@ -489,11 +544,23 @@ tr.clickable:focus-within { outline: 2px solid var(--orange); outline-offset: -2
489
544
  .rank-node-id { color: var(--ink); font-size: 11px; }
490
545
  .rank-node-dur { color: var(--muted); font-size: 11px; }
491
546
 
492
- .process-timeline { position: relative; display: grid; gap: 0; margin: 0; padding: var(--space-2) 0 0; list-style: none; }
493
- .process-timeline-item { position: relative; display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: var(--space-3); padding: 10px 0 10px 13px; border-left: 1px solid var(--hairline-strong); }
494
- .process-timeline-item::before { position: absolute; width: 7px; height: 7px; margin-left: -17px; border: 2px solid var(--surface); border-radius: 50%; background: var(--lavender); content: ""; }
495
- .process-timeline-time { color: var(--muted); font-size: 10px; }
496
- .process-timeline-label { color: var(--body); font-size: 12px; overflow-wrap: anywhere; }
547
+ .process-timeline-note { margin: 0 0 var(--space-2); color: var(--muted); font-size: 11px; }
548
+ .process-timeline { display: grid; gap: 0; margin: 0; padding: var(--space-2) 0; list-style: none; }
549
+ .process-timeline-item { display: grid; grid-template-columns: 20px minmax(0, 1fr); align-items: stretch; min-height: 34px; }
550
+ .process-timeline-item.has-time { grid-template-columns: 20px 92px minmax(0, 1fr); }
551
+ .process-timeline-rail { position: relative; display: grid; place-items: center; min-height: 34px; }
552
+ .process-timeline-rail::before { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--hairline-strong); content: ""; }
553
+ .process-timeline-item.is-first .process-timeline-rail::before { top: 50%; }
554
+ .process-timeline-item.is-last .process-timeline-rail::before { bottom: 50%; }
555
+ .process-timeline-dot { position: relative; z-index: 1; width: 8px; height: 8px; border: 2px solid var(--surface); border-radius: 50%; background: var(--muted-soft); }
556
+ .process-timeline-lifecycle .process-timeline-dot { background: var(--lavender); }
557
+ .process-timeline-tool-start .process-timeline-dot { background: var(--orange); }
558
+ .process-timeline-tool-end .process-timeline-dot { background: var(--green); }
559
+ .process-timeline-assistant .process-timeline-dot { background: var(--blue); }
560
+ .process-timeline-protocol .process-timeline-dot { background: var(--muted-soft); }
561
+ .process-timeline-time { padding: 9px 0; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
562
+ .process-timeline-label { min-width: 0; padding: 8px 0 8px var(--space-2); color: var(--body); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
563
+ .process-timeline-protocol .process-timeline-label { color: var(--muted); font-size: 11px; }
497
564
 
498
565
  .markdown-output { display: grid; gap: var(--space-3); color: var(--body); font-size: 13px; line-height: 1.65; }
499
566
  .markdown-output > :first-child { margin-top: 0; }
@@ -579,9 +646,9 @@ tr.clickable:focus-within { outline: 2px solid var(--orange); outline-offset: -2
579
646
  .kpi-card:nth-child(1), .kpi-card:nth-child(2) { border-top: 0; }
580
647
  .kpi-card:nth-child(odd) { border-left: 0; }
581
648
  .dag-inspector { top: 76px; right: 12px; bottom: 12px; width: calc(100vw - 24px); }
649
+ .dag-inspector-resize-handle { display: none; }
582
650
  .dashboard-secondary { grid-template-columns: 1fr; }
583
- .panel-risk { grid-template-columns: 1fr; gap: var(--space-2); }
584
- .panel-risk .risk-empty { grid-column: auto; }
651
+ .risk-block { grid-template-columns: 1fr; gap: var(--space-2); }
585
652
  .panel { padding: var(--space-4); }
586
653
  .panel-table { overflow-x: auto; padding: 0; }
587
654
  table { min-width: 680px; }
@@ -1,4 +1,5 @@
1
- import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  export const TASK_POOL_RELATIVE_ROOT = ".harness/task-pool";
4
5
  export function getTaskPoolRoot(repoRoot) {
@@ -19,6 +20,10 @@ export async function recordTaskPoolRun(input) {
19
20
  await writeTaskPoolState(input.repoRoot, stateFromRun(input.run));
20
21
  await appendJsonlFile(getEventsJsonlPath(input.repoRoot), eventFromRun(input.run));
21
22
  }
23
+ export async function appendTaskPoolEvent(repoRoot, event) {
24
+ await ensurePoolDirs(repoRoot);
25
+ await appendJsonlFile(getEventsJsonlPath(repoRoot), event);
26
+ }
22
27
  export async function findRunByWorkerRunId(repoRoot, workerRunId) {
23
28
  const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
24
29
  return runs.find((run) => run.workerRunId === workerRunId);
@@ -99,7 +104,14 @@ export async function readAllTaskPoolStates(repoRoot) {
99
104
  export async function writeTaskPoolState(repoRoot, state) {
100
105
  const statePath = getTaskStatePath(repoRoot, state.taskId);
101
106
  await mkdir(path.dirname(statePath), { recursive: true });
102
- await writeFile(statePath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
107
+ const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${randomUUID()}.tmp`);
108
+ try {
109
+ await writeFile(tempPath, `${JSON.stringify({ schemaVersion: 1, ...state }, null, 2)}\n`, "utf-8");
110
+ await rename(tempPath, statePath);
111
+ }
112
+ finally {
113
+ await unlink(tempPath).catch(() => { });
114
+ }
103
115
  }
104
116
  export async function readJsonlFile(filePath) {
105
117
  try {
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getRunsJsonlPath, getTaskPoolRoot } from "./run-store.js";
5
+ export const taskPoolRunFactSchema = z.object({
6
+ schemaVersion: z.literal(1), batchRunId: z.string().min(1), workerRunId: z.string().min(1), taskId: z.string().min(1), featureId: z.string().min(1),
7
+ status: z.enum(["succeeded", "failed", "run-error"]), recordedAt: z.string().datetime(),
8
+ }).passthrough();
9
+ export const taskPoolStateFactSchema = z.object({
10
+ taskId: z.string().min(1), status: z.enum(["Draft", "Ready", "Queued", "Running", "AgentCompleted", "VerificationRunning", "HumanReview", "Done", "Failed", "Blocked", "Abandoned"]), updatedAt: z.string().datetime(),
11
+ }).passthrough();
12
+ export async function readValidTaskPoolRuns(repoRoot) {
13
+ const records = [];
14
+ const warnings = [];
15
+ try {
16
+ const raw = await readFile(getRunsJsonlPath(repoRoot), "utf-8");
17
+ for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries())
18
+ try {
19
+ const parsed = taskPoolRunFactSchema.safeParse(JSON.parse(line));
20
+ if (parsed.success)
21
+ records.push(parsed.data);
22
+ else
23
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is semantically invalid`);
24
+ }
25
+ catch {
26
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is corrupt`);
27
+ }
28
+ }
29
+ catch (error) {
30
+ if (!isNotFound(error))
31
+ warnings.push("Task Pool runs.jsonl is unreadable");
32
+ }
33
+ return { records, warnings };
34
+ }
35
+ export async function readValidTaskPoolStates(repoRoot) {
36
+ const records = {};
37
+ const warnings = [];
38
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
39
+ try {
40
+ for (const entry of await readdir(stateDir))
41
+ if (entry.endsWith(".json"))
42
+ try {
43
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
44
+ if (parsed.success && parsed.data.taskId === entry.slice(0, -5))
45
+ records[parsed.data.taskId] = parsed.data;
46
+ else
47
+ warnings.push(`Task Pool state is semantically invalid: ${entry}`);
48
+ }
49
+ catch {
50
+ warnings.push(`Task Pool state is corrupt: ${entry}`);
51
+ }
52
+ }
53
+ catch (error) {
54
+ if (!isNotFound(error))
55
+ warnings.push("Task Pool states are unreadable");
56
+ }
57
+ return { records, warnings };
58
+ }
59
+ function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
@@ -1,17 +1,30 @@
1
- import { access, writeFile, mkdir } from "node:fs/promises";
1
+ import { access, writeFile, mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { getRunsJsonlPath, readAllTaskPoolStates, readJsonlFile, } from "../pool/run-store.js";
3
+ import YAML from "yaml";
4
+ import { followUpActionCardSchema } from "../follow-up/schema.js";
5
+ import { readFollowUpIndex, resolveRepoFile } from "../follow-up/store.js";
6
+ import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
7
+ import { readValidTaskPoolRuns, readValidTaskPoolStates } from "../pool/validation.js";
4
8
  export async function renderMorningReport(options) {
5
- const runs = (await readJsonlFile(getRunsJsonlPath(options.repoRoot))).filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
9
+ const { features } = await loadFeatureDecisionModels(options.repoRoot);
10
+ const allRuns = (await readValidTaskPoolRuns(options.repoRoot)).records;
11
+ const runs = allRuns.filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
6
12
  const total = runs.length;
7
13
  const succeeded = runs.filter((run) => run.status === "succeeded").length;
8
14
  const failed = runs.filter((run) => run.status === "failed").length;
9
15
  const followUps = runs.filter((run) => run.failure).length;
10
- const states = await readAllTaskPoolStates(options.repoRoot);
16
+ const states = (await readValidTaskPoolStates(options.repoRoot)).records;
11
17
  const blocked = Object.values(states).filter((state) => state.status === "Blocked").length;
12
18
  const lines = [
13
19
  "# Nightly Worker Report",
14
20
  "",
21
+ "## Feature Decision Summary",
22
+ "",
23
+ ...(features.length === 0 ? ["- Status: no Feature Packet discovered", "- Next Action: add or locate a Feature Packet", "- Why: no shared Feature read model is available", "- Evidence: none"] : features.flatMap((feature) => {
24
+ const why = feature.blockingItems[0]?.message ?? `${feature.summary.tasksSucceeded}/${feature.summary.tasksTotal} tasks; ${feature.summary.requiredAcCovered}/${feature.summary.requiredAcTotal} required AC`;
25
+ const evidence = [feature.evidence.delivery, feature.evidence.closeout, feature.evidence.morningReport, feature.evidence.observeSnapshot, ...feature.blockingItems.flatMap((item) => item.evidence)].find(Boolean) ?? "none";
26
+ return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
27
+ })),
15
28
  "## Summary",
16
29
  "",
17
30
  `- Total: ${total}`,
@@ -26,16 +39,38 @@ export async function renderMorningReport(options) {
26
39
  "|---|---|---|---|---|---|",
27
40
  ];
28
41
  for (const run of runs) {
29
- lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
42
+ const followUp = await followUpSummary(run, options.repoRoot);
43
+ lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${followUp ?? run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
30
44
  }
31
45
  if (followUps > 0) {
32
46
  lines.push("", "## Human Actions", "");
33
47
  for (const run of runs.filter((candidate) => candidate.failure)) {
34
- lines.push(`- ${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`);
48
+ const followUp = await followUpSummary(run, options.repoRoot);
49
+ lines.push(`- ${followUp ?? `${run.failure?.derivedFollowUpTaskId}: ${run.failure?.recommendedFollowUpKind} for ${run.taskId}`}`);
35
50
  }
36
51
  }
37
52
  return `${lines.join("\n")}\n`;
38
53
  }
54
+ async function followUpSummary(run, repoRoot) {
55
+ if (!run.failure)
56
+ return undefined;
57
+ try {
58
+ const index = await readFollowUpIndex(repoRoot, run.featureId);
59
+ const entry = index.entries.find((candidate) => candidate.workerRunId === run.workerRunId && candidate.status !== "Superseded" && candidate.status !== "Rejected");
60
+ if (!entry)
61
+ return undefined;
62
+ if (entry.kind === "TaskDraft") {
63
+ return entry.status === "Approved"
64
+ ? `${entry.followUpId}: approved as ${entry.proposedTaskId}`
65
+ : `${entry.followUpId}: approve ${entry.proposedTaskId}`;
66
+ }
67
+ const card = followUpActionCardSchema.parse(YAML.parse(await readFile(await resolveRepoFile(repoRoot, entry.draftPath), "utf-8")));
68
+ return `${entry.followUpId}: ${card.command ?? card.label}`;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
39
74
  export async function writeMorningReport(input) {
40
75
  const markdown = await renderMorningReport(input);
41
76
  await mkdir(path.dirname(input.outputPath), { recursive: true });
@@ -156,7 +156,7 @@ export async function runTaskSpec(options) {
156
156
  taskArtifactsDir,
157
157
  eventCtx,
158
158
  });
159
- if (reportDecision.succeeded) {
159
+ if (reportDecision.succeeded && !options.skipSuccessFinalization) {
160
160
  await runObservedStep(eventCtx, "promote-run", async () => {
161
161
  progress.step("promote run");
162
162
  await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
@@ -66,6 +66,7 @@ export async function runReadyTasks(options) {
66
66
  status: "reused",
67
67
  runRecordPath: existing.runRecordPath,
68
68
  });
69
+ await options.onTaskFinalized?.({ status: "reused", taskSpec, workerRunId, ...(existing.runRecordPath ? { runRecordPath: existing.runRecordPath } : {}) });
69
70
  continue;
70
71
  }
71
72
  }
@@ -106,10 +107,12 @@ export async function runReadyTasks(options) {
106
107
  client: options.client,
107
108
  now,
108
109
  workerRunId,
109
- preflight: {
110
- runCheckRepo: options.runCheckRepo,
111
- ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
112
- },
110
+ preflight: options.skipTaskPreflight
111
+ ? false
112
+ : {
113
+ runCheckRepo: options.runCheckRepo,
114
+ ...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
115
+ },
113
116
  progress,
114
117
  ...(options.piModel ? { piModel: options.piModel } : {}),
115
118
  });
@@ -128,6 +131,7 @@ export async function runReadyTasks(options) {
128
131
  status: "failed",
129
132
  message,
130
133
  });
134
+ let recorded = false;
131
135
  try {
132
136
  await recordTaskPoolRun({
133
137
  repoRoot: options.repoRoot,
@@ -144,16 +148,20 @@ export async function runReadyTasks(options) {
144
148
  ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
145
149
  },
146
150
  });
151
+ recorded = true;
147
152
  }
148
153
  catch (recordError) {
154
+ await options.onTaskFinalized?.({ status: "run-error", taskSpec, workerRunId });
149
155
  tasks.push({
150
156
  taskId,
151
157
  workerRunId,
152
158
  status: "record-error",
153
159
  error: errorMessage(recordError),
154
160
  });
155
- continue;
161
+ break;
156
162
  }
163
+ if (recorded)
164
+ await options.onTaskFinalized?.({ status: "run-error", taskSpec, workerRunId });
157
165
  tasks.push({
158
166
  taskId,
159
167
  workerRunId,
@@ -207,8 +215,10 @@ export async function runReadyTasks(options) {
207
215
  ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
208
216
  ...(result.failureArtifacts ? { failureArtifacts: result.failureArtifacts } : {}),
209
217
  };
218
+ let recorded = false;
210
219
  try {
211
220
  await recordTaskPoolRun({ repoRoot: options.repoRoot, run });
221
+ recorded = true;
212
222
  tasks.push({
213
223
  taskId,
214
224
  workerRunId: result.workerRunId,
@@ -217,6 +227,7 @@ export async function runReadyTasks(options) {
217
227
  });
218
228
  }
219
229
  catch (error) {
230
+ await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
220
231
  tasks.push({
221
232
  taskId,
222
233
  workerRunId: result.workerRunId,
@@ -224,7 +235,10 @@ export async function runReadyTasks(options) {
224
235
  error: errorMessage(error),
225
236
  runRecordPath: result.runRecordPath,
226
237
  });
238
+ break;
227
239
  }
240
+ if (recorded)
241
+ await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
228
242
  }
229
243
  const summary = summarize(tasks);
230
244
  const batchRunPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "batch-run.json");
@@ -2,7 +2,7 @@ import { access, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { assertValidDagSpec } from "./validate.js";
5
- import { DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
5
+ import { DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
6
6
  import { pathMatchesPattern } from "../../shared/git-progress.js";
7
7
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
8
8
  import { resolveAdapter } from "../../adapters/index.js";
@@ -448,6 +448,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
448
448
  constraintPath,
449
449
  requirementMarkdown,
450
450
  constraintMarkdown,
451
+ outputLanguage: manifest.workflowPolicy.dag.outputLanguage,
451
452
  referenceDocuments,
452
453
  taskConfig,
453
454
  enabledExecutors: resolveEnabledExecutors(manifest.executors),
@@ -524,6 +525,7 @@ export function buildStandardHybridDagFromTask(sources) {
524
525
  const spec = {
525
526
  version: 2,
526
527
  title: `Hybrid DAG: ${taskConfig.title}`,
528
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
527
529
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
528
530
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
529
531
  globalConstraints,