@tea-agent/loop-agent 0.9.0 → 0.10.0-alpha.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 (183) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +92 -11
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +4 -14
  5. package/dist/application/dag/generate-task-dag.js +32 -2
  6. package/dist/application/dag/run-dag.js +1 -27
  7. package/dist/application/dag/validate-dag.js +2 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +7 -11
  10. package/dist/cli/program.js +9 -21
  11. package/dist/commands/cursor-prompt.js +42 -82
  12. package/dist/commands/dag-approve.js +36 -0
  13. package/dist/commands/dag-reconcile-run.js +118 -0
  14. package/dist/commands/delegate.js +75 -77
  15. package/dist/commands/doctor.js +0 -18
  16. package/dist/commands/init.js +60 -40
  17. package/dist/commands/instructions.js +7 -10
  18. package/dist/commands/loop.js +4 -20
  19. package/dist/executors/config-core.js +0 -51
  20. package/dist/executors/dag-pi-executor.js +1 -1
  21. package/dist/executors/dag.js +0 -1
  22. package/dist/executors/index.js +0 -2
  23. package/dist/executors/model-routing.js +9 -9
  24. package/dist/executors/shell-executor.js +1 -1
  25. package/dist/governance/checks.js +6 -3
  26. package/dist/governance/manifest-types.js +33 -2
  27. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  28. package/dist/records/harvest.js +2 -23
  29. package/dist/records/one-shot-runs.js +1 -1
  30. package/dist/shared/artifacts-core.js +24 -5
  31. package/dist/shared/output-truncation.js +37 -0
  32. package/dist/shared/package-metadata.js +353 -0
  33. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  34. package/dist/sidecars/cursor-prompt/index.js +3 -0
  35. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  36. package/dist/task/config-types.js +29 -12
  37. package/dist/task/delegate.js +9 -21
  38. package/dist/task/runtime.js +1 -2
  39. package/dist/worker/cli.js +243 -0
  40. package/dist/worker/closeout/apply.js +73 -0
  41. package/dist/worker/closeout/preview.js +30 -0
  42. package/dist/worker/delivery/final-verification.js +194 -0
  43. package/dist/worker/delivery/git-transaction.js +354 -0
  44. package/dist/worker/delivery/package.js +502 -0
  45. package/dist/worker/feature/decision-loader.js +68 -0
  46. package/dist/worker/feature/discover.js +14 -0
  47. package/dist/worker/feature/next-action.js +74 -0
  48. package/dist/worker/feature/reducer.js +133 -0
  49. package/dist/worker/feature/review.js +502 -0
  50. package/dist/worker/feature/run.js +365 -0
  51. package/dist/worker/feature/types.js +1 -0
  52. package/dist/worker/follow-up/approve.js +270 -0
  53. package/dist/worker/follow-up/factory.js +234 -0
  54. package/dist/worker/follow-up/paths.js +25 -0
  55. package/dist/worker/follow-up/policy.js +26 -0
  56. package/dist/worker/follow-up/schema.js +93 -0
  57. package/dist/worker/follow-up/store.js +96 -0
  58. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  59. package/dist/worker/metrics/projector.js +139 -0
  60. package/dist/worker/observability/read-model.js +282 -15
  61. package/dist/worker/observe/paths.js +17 -5
  62. package/dist/worker/observe/static/app.js +443 -61
  63. package/dist/worker/observe/static/index.html +3 -1
  64. package/dist/worker/observe/static/styles.css +85 -18
  65. package/dist/worker/pool/run-store.js +14 -2
  66. package/dist/worker/pool/validation.js +59 -0
  67. package/dist/worker/preflight.js +49 -1
  68. package/dist/worker/report/morning-report.js +41 -6
  69. package/dist/worker/run-task/run-task.js +23 -13
  70. package/dist/worker/runner/run-ready.js +89 -11
  71. package/dist/worker/task-spec/schema.js +0 -1
  72. package/dist/workflows/dag/convergence/controller.js +1 -1
  73. package/dist/workflows/dag/executor-registry.js +0 -2
  74. package/dist/workflows/dag/governance-profile.js +10 -0
  75. package/dist/workflows/dag/init-hybrid.js +601 -26
  76. package/dist/workflows/dag/lifecycle.js +146 -0
  77. package/dist/workflows/dag/node-execution.js +64 -7
  78. package/dist/workflows/dag/prompt.js +16 -0
  79. package/dist/workflows/dag/report.js +2 -0
  80. package/dist/workflows/dag/runner.js +176 -119
  81. package/dist/workflows/dag/scheduler.js +7 -2
  82. package/dist/workflows/dag/skill-snapshot.js +527 -0
  83. package/dist/workflows/dag/types.js +45 -9
  84. package/dist/workflows/dag/validate.js +5 -8
  85. package/dist/workflows/loop/actions/dag-action.js +0 -2
  86. package/dist/workflows/loop/actions/shared.js +1 -1
  87. package/dist/workflows/loop/actions.js +14 -31
  88. package/dist/workflows/loop/benchmark.js +1 -1
  89. package/dist/workflows/loop/index.js +1 -1
  90. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  91. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  92. package/docs/README.md +35 -16
  93. package/docs/agent-dag-recovery-playbook.md +1 -1
  94. package/docs/architecture/README.md +26 -0
  95. package/docs/architecture/dag-execution.md +134 -0
  96. package/docs/architecture/evolution.md +52 -0
  97. package/docs/architecture/facts-and-state.md +58 -0
  98. package/docs/architecture/runtime-boundaries.md +41 -15
  99. package/docs/architecture/system-overview.md +93 -0
  100. package/docs/architecture/worker-and-feature.md +81 -0
  101. package/docs/cursor-prompt-sidecar.md +36 -0
  102. package/docs/decisions/README.md +13 -1
  103. package/docs/design/README.md +39 -13
  104. package/docs/development-principles.md +1 -1
  105. package/docs/exec-plans/active/README.md +2 -2
  106. package/docs/exec-plans/completed/README.md +14 -1
  107. package/docs/feature-workflow.md +44 -4
  108. package/docs/init-surface.manifest.json +63 -1
  109. package/docs/loop-agent-harness.md +65 -3
  110. package/docs/progress/README.md +27 -0
  111. package/docs/reports/README.md +74 -8
  112. package/docs/skills/README.md +2 -1
  113. package/docs/skills/vetted-skill-registry.md +2 -1
  114. package/docs/templates/agent-dag-report.schema.json +4 -2
  115. package/docs/templates/agent-dag.base.json +0 -5
  116. package/docs/templates/agent-dag.final-verification.json +0 -5
  117. package/docs/templates/agent-dag.schema.json +1 -2
  118. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  119. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  120. package/docs/templates/backend-test-dag.json +213 -0
  121. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  122. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  123. package/docs/templates/frontend-design-contract.md +33 -0
  124. package/docs/templates/frontend-task-constraints.md +25 -0
  125. package/docs/templates/frontend-task-requirement.md +61 -0
  126. package/docs/templates/harness.schema.json +8 -5
  127. package/docs/templates/hybrid-dag.json +1 -6
  128. package/docs/templates/init-evolution-review.md +4 -2
  129. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  130. package/docs/templates/product-line/task.yaml +0 -1
  131. package/docs/templates/worker-dogfood-evidence.md +28 -0
  132. package/docs/templates/worker-dogfood-setup.md +20 -0
  133. package/docs/verification-matrix.md +17 -0
  134. package/examples/decision-gate-agent-dag.json +87 -33
  135. package/examples/example-dag.json +0 -5
  136. package/examples/hybrid-loop-agent-dag.json +0 -5
  137. package/harness.json +6 -11
  138. package/package.json +22 -44
  139. package/scripts/check-product-line-docs.sh +10 -3
  140. package/skills/agent-worker/SKILL.md +37 -0
  141. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  142. package/skills/frontend-design-review/SKILL.md +59 -0
  143. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  144. package/skills/frontend-implementation/SKILL.md +48 -0
  145. package/skills/frontend-implementation/references/code-standards.md +34 -0
  146. package/skills/frontend-implementation/references/design-spec.md +46 -0
  147. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  148. package/skills/frontend-review/SKILL.md +53 -0
  149. package/skills/frontend-review/references/review-findings.md +42 -0
  150. package/skills/frontend-verification/SKILL.md +40 -0
  151. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  152. package/skills/grill-me/SKILL.md +10 -0
  153. package/skills/grill-with-docs/SKILL.md +88 -0
  154. package/skills/grill-with-docs/adr-format.md +47 -0
  155. package/skills/grill-with-docs/context-format.md +60 -0
  156. package/skills/init-capability-evolution/SKILL.md +1 -0
  157. package/skills/loop-agent/SKILL.md +11 -9
  158. package/skills/loop-agent/references/command-reference.md +28 -15
  159. package/skills/loop-agent/references/docs-converge.md +126 -0
  160. package/skills/loop-agent/references/harness-policy.md +7 -7
  161. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  162. package/skills/loop-agent/references/long-running-loop.md +4 -6
  163. package/skills/loop-agent/references/multi-worktree.md +6 -6
  164. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  165. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  166. package/skills/using-git-worktrees/SKILL.md +215 -0
  167. package/dist/commands/cursor-worker.js +0 -43
  168. package/dist/cursor-worker-entry.js +0 -8
  169. package/dist/executors/cursor-artifacts.js +0 -33
  170. package/dist/executors/cursor-execution-log.js +0 -81
  171. package/dist/executors/cursor-executor-artifacts.js +0 -134
  172. package/dist/executors/cursor-run.js +0 -115
  173. package/dist/executors/cursor-tool.js +0 -94
  174. package/dist/executors/cursor-worker-client.js +0 -223
  175. package/dist/executors/cursor-worker-protocol.js +0 -18
  176. package/dist/executors/cursor-worker-server.js +0 -54
  177. package/dist/executors/cursor-worker.js +0 -3
  178. package/dist/executors/cursor.js +0 -6
  179. package/dist/executors/dag-cursor-executor.js +0 -87
  180. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  181. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  182. package/docs/cursor-executor-usage.md +0 -25
  183. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -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
 
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
+
243
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,14 +1,27 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
3
4
  export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
4
5
  export async function preflightTargetRepo(input) {
5
6
  const commands = [];
7
+ const identity = resolveControllerIdentity(input.client);
8
+ const expectationFailure = controllerIdentityExpectationFailure(identity, input.expectation);
9
+ if (expectationFailure) {
10
+ return {
11
+ ok: false,
12
+ code: expectationFailure.code,
13
+ message: expectationFailure.message,
14
+ commands,
15
+ controllerIdentity: identity,
16
+ };
17
+ }
6
18
  if (!(await exists(path.join(input.repoRoot, "harness.json")))) {
7
19
  return {
8
20
  ok: false,
9
21
  code: "target-not-initialized",
10
22
  message: "target repo is missing harness.json",
11
23
  commands,
24
+ controllerIdentity: identity,
12
25
  };
13
26
  }
14
27
  if (!(await exists(path.join(input.repoRoot, ".harness")))) {
@@ -17,6 +30,7 @@ export async function preflightTargetRepo(input) {
17
30
  code: "target-not-initialized",
18
31
  message: "target repo is missing .harness",
19
32
  commands,
33
+ controllerIdentity: identity,
20
34
  };
21
35
  }
22
36
  const version = await input.client.run(["--version"], {
@@ -30,8 +44,36 @@ export async function preflightTargetRepo(input) {
30
44
  code: "version-failed",
31
45
  message: "loop-agent --version failed",
32
46
  commands,
47
+ controllerIdentity: identity,
33
48
  };
34
49
  }
50
+ const reportedVersion = version.stdout.trim();
51
+ let controllerIdentity = identity
52
+ ? { ...identity, reportedVersion }
53
+ : undefined;
54
+ if (identity && reportedVersion !== identity.packageVersion) {
55
+ return {
56
+ ok: false,
57
+ code: "reported-version-mismatch",
58
+ message: `reported version ${reportedVersion} does not match package version ${identity.packageVersion}`,
59
+ commands,
60
+ controllerIdentity,
61
+ };
62
+ }
63
+ if (identity && input.client.observeReportedVersion) {
64
+ try {
65
+ controllerIdentity = input.client.observeReportedVersion(reportedVersion) ?? controllerIdentity;
66
+ }
67
+ catch (error) {
68
+ return {
69
+ ok: false,
70
+ code: "reported-version-mismatch",
71
+ message: error instanceof Error ? error.message : String(error),
72
+ commands,
73
+ controllerIdentity,
74
+ };
75
+ }
76
+ }
35
77
  const inspect = await input.client.run(["inspect"], {
36
78
  cwd: input.repoRoot,
37
79
  artifactName: "inspect",
@@ -44,6 +86,7 @@ export async function preflightTargetRepo(input) {
44
86
  code: "inspect-failed",
45
87
  message: "loop-agent inspect failed or did not return JSON",
46
88
  commands,
89
+ controllerIdentity,
47
90
  };
48
91
  }
49
92
  const docsAudit = await input.client.run(["docs", "audit"], {
@@ -57,6 +100,7 @@ export async function preflightTargetRepo(input) {
57
100
  code: "docs-audit-failed",
58
101
  message: "loop-agent docs audit failed",
59
102
  commands,
103
+ controllerIdentity,
60
104
  };
61
105
  }
62
106
  const gitStatus = await input.client.runExternal("git", ["status", "--short", "--branch"], {
@@ -70,6 +114,7 @@ export async function preflightTargetRepo(input) {
70
114
  code: "git-status-failed",
71
115
  message: "git status --short --branch failed",
72
116
  commands,
117
+ controllerIdentity,
73
118
  };
74
119
  }
75
120
  if (input.runCheckRepo) {
@@ -86,15 +131,18 @@ export async function preflightTargetRepo(input) {
86
131
  code: "check-repo-failed",
87
132
  message: `${[command, ...args].join(" ")} failed`,
88
133
  commands,
134
+ controllerIdentity,
89
135
  };
90
136
  }
91
137
  }
92
138
  return {
93
139
  ok: true,
94
- version: version.stdout.trim(),
140
+ version: reportedVersion,
141
+ reportedVersion: controllerIdentity?.reportedVersion,
95
142
  inspect: inspect.json,
96
143
  gitStatus: gitStatus.stdout,
97
144
  commands,
145
+ controllerIdentity,
98
146
  };
99
147
  }
100
148
  async function exists(filePath) {
@@ -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 });
@@ -3,12 +3,18 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { formatDuration, noopProgressReporter } from "../progress-reporter.js";
5
5
  import { redactForPreview, truncatePreview } from "../observability/events.js";
6
+ import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
6
7
  import { materializeTaskSpec, } from "../materialize/harness-task-materializer.js";
7
8
  import { preflightTargetRepo } from "../preflight.js";
8
9
  import { getTaskPoolRoot } from "../pool/run-store.js";
9
10
  export const DEFAULT_RUN_DAG_TIMEOUT_MS = 1_800_000;
10
11
  export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
11
12
  export async function runTaskSpec(options) {
13
+ let controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity);
14
+ const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, options.controllerExpectation);
15
+ if (identityFailure) {
16
+ throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
17
+ }
12
18
  const now = options.now ?? new Date();
13
19
  const workerRunId = options.workerRunId ?? buildWorkerRunId(options.taskSpec.id, now);
14
20
  const commands = [];
@@ -26,11 +32,13 @@ export async function runTaskSpec(options) {
26
32
  repoRoot: options.repoRoot,
27
33
  client,
28
34
  ...(typeof options.preflight === "object" ? options.preflight : {}),
35
+ ...(options.controllerExpectation ? { expectation: options.controllerExpectation } : {}),
29
36
  });
30
37
  if (!preflight.ok) {
31
38
  progress.note(`preflight FAILED: ${preflight.code}`);
32
39
  throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
33
40
  }
41
+ controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
34
42
  });
35
43
  progress.step(`preflight ok in ${formatDuration(Date.now() - preflightStartedAt)}`);
36
44
  }
@@ -66,7 +74,6 @@ export async function runTaskSpec(options) {
66
74
  dagPath,
67
75
  "--cwd",
68
76
  options.repoRoot,
69
- ...noCursorArgs(options.taskSpec),
70
77
  ], true, undefined, eventCtx);
71
78
  });
72
79
  progress.step(`dag generated in ${formatDuration(Date.now() - dagGenStartedAt)}`);
@@ -90,7 +97,6 @@ export async function runTaskSpec(options) {
90
97
  "--strict-governance",
91
98
  "--spine-task",
92
99
  materializeManifest.harnessTaskId,
93
- ...forbidCursorArgs(options.taskSpec),
94
100
  ], true, undefined, eventCtx);
95
101
  });
96
102
  progress.step(`dag validated in ${formatDuration(Date.now() - validateStartedAt)}`);
@@ -111,7 +117,6 @@ export async function runTaskSpec(options) {
111
117
  "--events-jsonl",
112
118
  dagEventsPath,
113
119
  ...maxConcurrentArgs(options.taskSpec),
114
- ...noCursorArgs(options.taskSpec),
115
120
  ], resolveRunDagTimeoutMs(options.taskSpec), eventCtx);
116
121
  });
117
122
  progress.step(`run-dag finished in ${formatDuration(Date.now() - runDagStartedAt)}`);
@@ -156,7 +161,7 @@ export async function runTaskSpec(options) {
156
161
  taskArtifactsDir,
157
162
  eventCtx,
158
163
  });
159
- if (reportDecision.succeeded) {
164
+ if (reportDecision.succeeded && !options.skipSuccessFinalization) {
160
165
  await runObservedStep(eventCtx, "promote-run", async () => {
161
166
  progress.step("promote run");
162
167
  await runRequiredCommand(options.repoRoot, client, "promote-run", ["promote-run", materializeManifest.harnessTaskId, "--run-id", workerRunId], true, undefined, eventCtx);
@@ -180,6 +185,7 @@ export async function runTaskSpec(options) {
180
185
  reportDecision,
181
186
  commands,
182
187
  ...(failureArtifacts ? { failureArtifacts } : {}),
188
+ ...(controllerIdentity ? { controllerIdentity } : {}),
183
189
  };
184
190
  await writeFile(runRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
185
191
  emit(progress, {
@@ -407,6 +413,12 @@ class RecordingRunTaskClient {
407
413
  this.delegate = delegate;
408
414
  this.records = records;
409
415
  }
416
+ getIdentity() {
417
+ return this.delegate.getIdentity?.();
418
+ }
419
+ observeReportedVersion(reportedVersion) {
420
+ return this.delegate.observeReportedVersion?.(reportedVersion);
421
+ }
410
422
  async run(args, options) {
411
423
  const result = await this.delegate.run(args, options);
412
424
  this.records.push({
@@ -452,6 +464,12 @@ class ObservedRunTaskClient {
452
464
  this.delegate = delegate;
453
465
  this.eventCtx = eventCtx;
454
466
  }
467
+ getIdentity() {
468
+ return this.delegate.getIdentity();
469
+ }
470
+ observeReportedVersion(reportedVersion) {
471
+ return this.delegate.observeReportedVersion(reportedVersion);
472
+ }
455
473
  run(args, options) {
456
474
  return this.runObserved(args, options, (observedOptions) => this.delegate.run(args, observedOptions));
457
475
  }
@@ -544,12 +562,6 @@ function commandRecordName(args, artifactName) {
544
562
  return "new-task";
545
563
  return artifactName;
546
564
  }
547
- function noCursorArgs(taskSpec) {
548
- return taskSpec.loop_agent.no_cursor ? ["--no-cursor"] : [];
549
- }
550
- function forbidCursorArgs(taskSpec) {
551
- return taskSpec.loop_agent.no_cursor ? ["--forbid-executor", "cursor"] : [];
552
- }
553
565
  function maxConcurrentArgs(taskSpec) {
554
566
  return ["--max-concurrent", String(taskSpec.loop_agent.max_concurrent)];
555
567
  }
@@ -563,14 +575,12 @@ function strictModelsArgs(piModel) {
563
575
  }
564
576
  /**
565
577
  * Rewrite `executorModels.pi.{LOW,MED,HIGH}` in an already-generated DAG JSON
566
- * so every pi executor node resolves to the smoke override model. Cursor / shell
567
- * slots are left untouched.
578
+ * so every pi executor node resolves to the smoke override model.
568
579
  */
569
580
  export async function applyPiModelOverride(dagPath, piModel) {
570
581
  const raw = await readFile(dagPath, "utf-8");
571
582
  const spec = JSON.parse(raw);
572
583
  spec.executorModels = {
573
- ...(spec.executorModels?.cursor ? { cursor: spec.executorModels.cursor } : {}),
574
584
  pi: {
575
585
  LOW: piModel,
576
586
  MED: piModel,