@tea-agent/loop-agent 0.28.1 → 0.28.2

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 (66) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +11 -1
  4. package/dist/cli/command-definitions.js +2 -1
  5. package/dist/commands/client-recovery.js +111 -8
  6. package/dist/commands/dag-init-hybrid.js +1 -1
  7. package/dist/commands/init-upgrade.js +2479 -0
  8. package/dist/commands/init.js +120 -9
  9. package/dist/governance/manifest-types.js +65 -0
  10. package/dist/shared/operator/capabilities.js +350 -2
  11. package/dist/task/worktree.js +256 -39
  12. package/dist/worker/cli.js +22 -12
  13. package/dist/worker/console/chat/workspace-landing.js +16 -6
  14. package/dist/worker/console/observe-health-match.js +2 -0
  15. package/dist/worker/console/observe-link.js +4 -0
  16. package/dist/worker/console/operator-actions.js +183 -4
  17. package/dist/worker/console/operator-selection.js +13 -0
  18. package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
  19. package/dist/worker/console/static/index.html +1 -1
  20. package/dist/worker/observe/health.js +1 -0
  21. package/dist/worker/observe/night-jobs.js +104 -0
  22. package/dist/worker/observe/routes.js +48 -0
  23. package/dist/worker/observe/static/app.js +3 -0
  24. package/dist/worker/observe/static/constants.js +1 -0
  25. package/dist/worker/observe/static/index.html +47 -0
  26. package/dist/worker/observe/static/router.js +10 -0
  27. package/dist/worker/observe/static/shell-chrome.js +1 -0
  28. package/dist/worker/observe/static/views/night.js +201 -0
  29. package/dist/worker/report/morning-report.js +56 -16
  30. package/dist/worker/run-task/execute-prepared-task.js +153 -0
  31. package/dist/worker/runner/single-task-attempt.js +147 -0
  32. package/dist/worker/scheduler/admission.js +536 -0
  33. package/dist/worker/scheduler/auto-followup.js +99 -0
  34. package/dist/worker/scheduler/cli.js +539 -0
  35. package/dist/worker/scheduler/dispatcher.js +503 -0
  36. package/dist/worker/scheduler/doctor.js +346 -0
  37. package/dist/worker/scheduler/evidence.js +170 -0
  38. package/dist/worker/scheduler/git-base.js +52 -0
  39. package/dist/worker/scheduler/index.js +23 -0
  40. package/dist/worker/scheduler/lease.js +114 -0
  41. package/dist/worker/scheduler/lifecycle.js +348 -0
  42. package/dist/worker/scheduler/lock.js +80 -0
  43. package/dist/worker/scheduler/morning-window.js +161 -0
  44. package/dist/worker/scheduler/night-git-finalizer.js +88 -0
  45. package/dist/worker/scheduler/night-harvest.js +421 -0
  46. package/dist/worker/scheduler/paths.js +84 -0
  47. package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
  48. package/dist/worker/scheduler/recovery.js +277 -0
  49. package/dist/worker/scheduler/reservation.js +146 -0
  50. package/dist/worker/scheduler/retry.js +199 -0
  51. package/dist/worker/scheduler/scheduler-loop.js +272 -0
  52. package/dist/worker/scheduler/store.js +275 -0
  53. package/dist/worker/scheduler/traceability.js +54 -0
  54. package/dist/worker/scheduler/trigger.js +258 -0
  55. package/dist/worker/scheduler/types.js +369 -0
  56. package/dist/worker/scheduler/workspace-adapter.js +91 -0
  57. package/docs/architecture/runtime-boundaries.md +9 -0
  58. package/docs/init-surface.manifest.json +9 -2
  59. package/docs/templates/harness.schema.json +107 -0
  60. package/docs/templates/init-managed-agents.md +18 -8
  61. package/harness.json +22 -0
  62. package/package.json +1 -1
  63. package/skills/loop-agent/SKILL.md +28 -36
  64. package/skills/loop-agent/references/command-reference.md +40 -16
  65. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  66. package/dist/worker/console/static/assets/index-CNO7n6qB.js +0 -29
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Loop 操作台 · Operator Console</title>
7
- <script type="module" crossorigin src="/assets/index-CNO7n6qB.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BfRgtLF4.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Yyn3ynVv.css">
9
9
  </head>
10
10
  <body>
@@ -19,6 +19,7 @@ export function deriveObserveRouteCapabilities(routes) {
19
19
  feature: routeExists(routes, "GET", "/api/features/example-feature/tasks/example-task"),
20
20
  workerRun: routeExists(routes, "GET", "/api/runs/example-run"),
21
21
  batch: routeExists(routes, "GET", "/api/batches/example-batch"),
22
+ nightJobs: routeExists(routes, "GET", "/api/night-jobs"),
22
23
  };
23
24
  }
24
25
  export function resolveObservePackageIdentity(fromFileUrl = import.meta.url) {
@@ -0,0 +1,104 @@
1
+ import { diagnoseScheduler } from "../scheduler/doctor.js";
2
+ import { buildNightMorningWindow, } from "../scheduler/morning-window.js";
3
+ import { listSchedules, readAdmission, readExecution, readLedgerEvents, readSchedule, } from "../scheduler/store.js";
4
+ import { resolveNextAction } from "../scheduler/morning-window.js";
5
+ export async function buildNightJobsList(controlRepoRoot, now = new Date()) {
6
+ const schedules = await listSchedules(controlRepoRoot);
7
+ const rows = schedules
8
+ .map((schedule) => toSummary(schedule))
9
+ .sort((a, b) => (b.executeAtUtc ?? "").localeCompare(a.executeAtUtc ?? ""));
10
+ return {
11
+ schemaVersion: 1,
12
+ generatedAt: now.toISOString(),
13
+ schedules: rows,
14
+ summary: {
15
+ total: rows.length,
16
+ active: rows.filter((row) => [
17
+ "submitted",
18
+ "validating",
19
+ "validated",
20
+ "scheduled",
21
+ "waiting",
22
+ "dispatching",
23
+ "running",
24
+ "cancel_requested",
25
+ ].includes(row.status)).length,
26
+ succeeded: rows.filter((row) => row.status === "succeeded").length,
27
+ failed: rows.filter((row) => row.status === "failed").length,
28
+ humanRequired: rows.filter((row) => row.status === "human_required")
29
+ .length,
30
+ pendingHarvest: rows.filter((row) => row.mergeState === "pending-harvest")
31
+ .length,
32
+ },
33
+ };
34
+ }
35
+ export async function buildNightJobDetail(controlRepoRoot, scheduleId, now = new Date()) {
36
+ const schedule = await readSchedule(controlRepoRoot, scheduleId);
37
+ if (!schedule)
38
+ return null;
39
+ const admission = await readAdmission(controlRepoRoot, scheduleId);
40
+ let execution;
41
+ if (schedule.currentExecutionId) {
42
+ execution =
43
+ (await readExecution(controlRepoRoot, schedule.currentExecutionId)) ??
44
+ undefined;
45
+ }
46
+ const ledger = (await readLedgerEvents(controlRepoRoot, { scheduleId }))
47
+ .slice(-50)
48
+ .map((event) => event);
49
+ return {
50
+ schemaVersion: 1,
51
+ generatedAt: now.toISOString(),
52
+ schedule,
53
+ ...(admission ? { admission: redactAdmission(admission) } : {}),
54
+ ...(execution ? { execution } : {}),
55
+ ledger,
56
+ nextAction: resolveNextAction(schedule),
57
+ };
58
+ }
59
+ export async function buildNightJobsMorning(controlRepoRoot, query) {
60
+ return buildNightMorningWindow({
61
+ controlRepoRoot,
62
+ ...(query.date ? { date: query.date } : {}),
63
+ ...(query.timezone ? { timezone: query.timezone } : {}),
64
+ ...(query.now ? { now: query.now } : {}),
65
+ });
66
+ }
67
+ export async function buildNightJobsDoctor(controlRepoRoot) {
68
+ return diagnoseScheduler(controlRepoRoot);
69
+ }
70
+ function redactAdmission(admission) {
71
+ const { controllerIdentity: _controllerIdentity, gateApproval, ...rest } = admission;
72
+ void _controllerIdentity;
73
+ if (!gateApproval)
74
+ return rest;
75
+ const { token: _token, ...safeGateApproval } = gateApproval;
76
+ void _token;
77
+ return { ...rest, gateApproval: safeGateApproval };
78
+ }
79
+ function toSummary(schedule) {
80
+ return {
81
+ scheduleId: schedule.id,
82
+ featureId: schedule.featureId,
83
+ taskId: schedule.taskId,
84
+ status: schedule.status,
85
+ ...(schedule.trigger?.executeAtUtc
86
+ ? { executeAtUtc: schedule.trigger.executeAtUtc }
87
+ : {}),
88
+ ...(schedule.isolation?.mergeState
89
+ ? { mergeState: schedule.isolation.mergeState }
90
+ : {}),
91
+ ...(schedule.isolation?.worktreePath
92
+ ? { worktreePath: schedule.isolation.worktreePath }
93
+ : {}),
94
+ ...(schedule.isolation?.branch
95
+ ? { branch: schedule.isolation.branch }
96
+ : {}),
97
+ ...(schedule.traceability?.workItemRef
98
+ ? { taskCard: schedule.traceability.workItemRef }
99
+ : {}),
100
+ currentExecutionId: schedule.currentExecutionId ?? null,
101
+ nextAction: resolveNextAction(schedule),
102
+ waitingReason: schedule.waitingReason ?? null,
103
+ };
104
+ }
@@ -15,6 +15,7 @@ import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview,
15
15
  import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
16
16
  import { buildDagNodeInput } from "./node-input.js";
17
17
  import { buildObserveHealthV1 } from "./health.js";
18
+ import { buildNightJobDetail, buildNightJobsDoctor, buildNightJobsList, buildNightJobsMorning, } from "./night-jobs.js";
18
19
  const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
19
20
  export function createObserveSnapshotCache() {
20
21
  return { expiresAt: 0 };
@@ -113,6 +114,23 @@ export const ROUTES = [
113
114
  pattern: /^\/api\/events\/stream$/,
114
115
  handler: handleEventStream,
115
116
  },
117
+ // Night Scheduler Inspect (Phase 5) — read-only projections over scheduler facts.
118
+ { method: "GET", pattern: /^\/api\/night-jobs$/, handler: handleNightJobs },
119
+ {
120
+ method: "GET",
121
+ pattern: /^\/api\/night-jobs\/morning$/,
122
+ handler: handleNightJobsMorning,
123
+ },
124
+ {
125
+ method: "GET",
126
+ pattern: /^\/api\/night-jobs\/doctor$/,
127
+ handler: handleNightJobsDoctor,
128
+ },
129
+ {
130
+ method: "GET",
131
+ pattern: /^\/api\/night-jobs\/([^/]+)$/,
132
+ handler: handleNightJobById,
133
+ },
116
134
  ];
117
135
  export function matchObserveGetRoute(pathname) {
118
136
  for (const route of ROUTES) {
@@ -236,6 +254,36 @@ async function handleSnapshot(_req, res, _match, ctx) {
236
254
  const snapshot = await getSnapshot(ctx);
237
255
  sendJson(res, 200, snapshot);
238
256
  }
257
+ async function handleNightJobs(_req, res, _match, ctx) {
258
+ const body = await buildNightJobsList(ctx.repoRoot);
259
+ sendJson(res, 200, body);
260
+ }
261
+ async function handleNightJobsMorning(_req, res, match, ctx) {
262
+ const date = match.query.get("date") ?? undefined;
263
+ const timezone = match.query.get("timezone") ?? match.query.get("tz") ?? undefined;
264
+ const body = await buildNightJobsMorning(ctx.repoRoot, {
265
+ ...(date ? { date } : {}),
266
+ ...(timezone ? { timezone } : {}),
267
+ });
268
+ sendJson(res, 200, body);
269
+ }
270
+ async function handleNightJobsDoctor(_req, res, _match, ctx) {
271
+ const body = await buildNightJobsDoctor(ctx.repoRoot);
272
+ sendJson(res, 200, body);
273
+ }
274
+ async function handleNightJobById(_req, res, match, ctx) {
275
+ const scheduleId = match.params.id;
276
+ if (!scheduleId) {
277
+ sendJson(res, 400, { error: "schedule id required" });
278
+ return;
279
+ }
280
+ const body = await buildNightJobDetail(ctx.repoRoot, scheduleId);
281
+ if (!body) {
282
+ sendJson(res, 404, { error: "Night schedule not found" });
283
+ return;
284
+ }
285
+ sendJson(res, 200, body);
286
+ }
239
287
  async function handleBatches(_req, res, _match, ctx) {
240
288
  const snapshot = await getSnapshot(ctx);
241
289
  sendJson(res, 200, snapshot.batches);
@@ -55,6 +55,7 @@ import { startTaskPolling } from "./views/task.js";
55
55
  import { startRunPolling } from "./views/run.js";
56
56
  import { startDagPolling } from "./views/dag.js";
57
57
  import { renderDagGraph } from "./views/dag-graph.js";
58
+ import { renderNightJobs } from "./views/night.js";
58
59
  import {
59
60
  selectDagNode,
60
61
  closeDagInspector,
@@ -128,6 +129,8 @@ function route() {
128
129
  void renderFailures();
129
130
  } else if (r.view === "pool") {
130
131
  startPoolView();
132
+ } else if (r.view === "night") {
133
+ void renderNightJobs(r.scheduleId);
131
134
  } else if (r.view === "task") {
132
135
  startTaskPolling(r.featureId, r.taskId);
133
136
  } else if (r.view === "feature") {
@@ -22,6 +22,7 @@ export const UI_TEXT = {
22
22
  dashboard: "总览",
23
23
  dagRuns: "DAG 运行",
24
24
  failures: "异常 Task",
25
+ nightJobs: "夜间任务",
25
26
  batchDetail: "批次详情",
26
27
  runDetail: "运行详情",
27
28
  dagDetail: "DAG 详情",
@@ -37,6 +37,10 @@
37
37
  ><i class="ri-alarm-warning-line" aria-hidden="true"></i
38
38
  ><span>异常 Task</span></a
39
39
  >
40
+ <a href="#/night" class="nav-link"
41
+ ><i class="ri-moon-clear-line" aria-hidden="true"></i
42
+ ><span>夜间任务</span></a
43
+ >
40
44
  </nav>
41
45
  <div class="header-meta">
42
46
  <i class="ri-live-line" aria-hidden="true"></i>
@@ -181,6 +185,49 @@
181
185
  ></aside>
182
186
  </section>
183
187
 
188
+ <section data-view="night" id="view-night" class="view" hidden>
189
+ <header class="page-header">
190
+ <div class="view-heading page-header-heading">
191
+ <p class="view-kicker">Night Scheduler</p>
192
+ <h2>夜间任务</h2>
193
+ </div>
194
+ <div class="cta-row">
195
+ <button
196
+ type="button"
197
+ id="night-refresh-btn"
198
+ class="btn btn-secondary"
199
+ >
200
+ 刷新
201
+ </button>
202
+ <a class="btn btn-secondary" href="/?workspace=night"
203
+ >打开 Operate 面板</a
204
+ >
205
+ </div>
206
+ </header>
207
+ <section
208
+ id="night-summary"
209
+ class="panel panel-kpi"
210
+ aria-label="夜间摘要"
211
+ ></section>
212
+ <section
213
+ id="night-list"
214
+ class="panel panel-table"
215
+ aria-label="夜间计划列表"
216
+ ></section>
217
+ <section
218
+ id="night-detail"
219
+ class="panel"
220
+ aria-label="计划详情"
221
+ hidden
222
+ ></section>
223
+ <section
224
+ id="night-morning"
225
+ class="panel"
226
+ aria-label="早晨窗口"
227
+ ></section>
228
+ <p id="night-empty" class="empty-state" hidden></p>
229
+ </section>
230
+
184
231
  <section data-view="failures" id="view-failures" class="view" hidden>
185
232
  <div class="view-heading">
186
233
  <p class="view-kicker">Attention Queue</p>
@@ -39,6 +39,16 @@ export function parseHashRoute(hashValue) {
39
39
  if (hash === "/failures") {
40
40
  return { view: "failures" };
41
41
  }
42
+ if (hash === "/night") {
43
+ return { view: "night" };
44
+ }
45
+ const nightMatch = /^\/night\/([^/]+)$/.exec(hash);
46
+ if (nightMatch) {
47
+ return {
48
+ view: "night",
49
+ scheduleId: decodeURIComponent(nightMatch[1]),
50
+ };
51
+ }
42
52
  if (hash === "/dags") {
43
53
  return { view: "dashboard", scrollTo: "dags" };
44
54
  }
@@ -15,6 +15,7 @@ export function showView(name) {
15
15
  let activeHref = "#/";
16
16
  if (name === "failures") activeHref = "#/failures";
17
17
  else if (name === "pool" || name === "task") activeHref = "#/pool";
18
+ else if (name === "night") activeHref = "#/night";
18
19
  else if (name === "feature") activeHref = "#/";
19
20
  document.querySelectorAll(".nav-link").forEach((link) => {
20
21
  link.classList.toggle(
@@ -0,0 +1,201 @@
1
+ /** Observe UI — Night Scheduler Inspect view (Phase 5). */
2
+ import { UI_TEXT } from "../constants.js";
3
+ import { clearNode, el, buildTable } from "../dom.js";
4
+ import { badge, formatTs, truncateText } from "../format.js";
5
+ import { navigate } from "../router.js";
6
+ import { fetchJson } from "../api.js";
7
+ import { mountViewState, renderViewState } from "../state.js";
8
+ import {
9
+ showView,
10
+ setBreadcrumb,
11
+ updateHeaderRefresh,
12
+ } from "../shell-chrome.js";
13
+
14
+ let nightBound = false;
15
+
16
+ function bindNightToolbar() {
17
+ if (nightBound) return;
18
+ const btn = document.getElementById("night-refresh-btn");
19
+ if (!btn) return;
20
+ nightBound = true;
21
+ btn.addEventListener("click", () => {
22
+ void renderNightJobs();
23
+ });
24
+ }
25
+
26
+ function summaryCards(summary) {
27
+ const wrap = el("div", "kpi-grid");
28
+ const items = [
29
+ ["全部", summary.total ?? 0],
30
+ ["活跃", summary.active ?? 0],
31
+ ["成功", summary.succeeded ?? 0],
32
+ ["失败", summary.failed ?? 0],
33
+ ["需人工", summary.humanRequired ?? 0],
34
+ ["待 harvest", summary.pendingHarvest ?? 0],
35
+ ];
36
+ for (const [label, value] of items) {
37
+ const card = el("div", "kpi-card");
38
+ card.appendChild(el("div", "kpi-label", label));
39
+ card.appendChild(el("div", "kpi-value", String(value)));
40
+ wrap.appendChild(card);
41
+ }
42
+ return wrap;
43
+ }
44
+
45
+ export async function renderNightJobs(scheduleId) {
46
+ showView("night");
47
+ bindNightToolbar();
48
+ const crumbs = [
49
+ { label: UI_TEXT.dashboard, href: "#/" },
50
+ { label: UI_TEXT.nightJobs, href: "#/night" },
51
+ ];
52
+ if (scheduleId) crumbs.push({ label: scheduleId });
53
+ setBreadcrumb(crumbs);
54
+
55
+ const listEl = document.getElementById("night-list");
56
+ const summaryEl = document.getElementById("night-summary");
57
+ const detailEl = document.getElementById("night-detail");
58
+ const morningEl = document.getElementById("night-morning");
59
+ const emptyEl = document.getElementById("night-empty");
60
+ clearNode(listEl);
61
+ clearNode(summaryEl);
62
+ clearNode(detailEl);
63
+ clearNode(morningEl);
64
+ if (emptyEl) {
65
+ emptyEl.hidden = true;
66
+ clearNode(emptyEl);
67
+ }
68
+ if (listEl) mountViewState(listEl, "loading", "加载夜间计划…");
69
+
70
+ const [listBody, morningBody] = await Promise.all([
71
+ fetchJson("/api/night-jobs"),
72
+ fetchJson("/api/night-jobs/morning"),
73
+ ]);
74
+ if (!listBody) {
75
+ if (listEl) mountViewState(listEl, "error", "无法加载 /api/night-jobs。");
76
+ return;
77
+ }
78
+ updateHeaderRefresh(listBody.generatedAt);
79
+
80
+ if (summaryEl) {
81
+ summaryEl.appendChild(summaryCards(listBody.summary ?? {}));
82
+ }
83
+
84
+ const schedules = listBody.schedules ?? [];
85
+ if (schedules.length === 0) {
86
+ clearNode(listEl);
87
+ if (emptyEl) {
88
+ emptyEl.hidden = false;
89
+ mountViewState(
90
+ emptyEl,
91
+ "empty",
92
+ "当前没有 Night Scheduler 计划。请用 Operate 面板 admission prepare / scheduler add。",
93
+ );
94
+ } else if (listEl) {
95
+ mountViewState(listEl, "empty", "当前没有 Night Scheduler 计划。");
96
+ }
97
+ } else if (listEl) {
98
+ clearNode(listEl);
99
+ const rows = schedules.map((row) => ({
100
+ className: `row-${row.status}`,
101
+ onClick: () => navigate(`#/night/${encodeURIComponent(row.scheduleId)}`),
102
+ cells: [
103
+ row.scheduleId ?? "—",
104
+ `${row.featureId ?? "—"} / ${row.taskId ?? "—"}`,
105
+ badge(row.status),
106
+ row.mergeState ? badge(row.mergeState) : "—",
107
+ row.executeAtUtc ? formatTs(row.executeAtUtc) : "—",
108
+ row.taskCard ?? "—",
109
+ truncateText(row.nextAction ?? "—", 64),
110
+ ],
111
+ }));
112
+ listEl.appendChild(
113
+ buildTable(
114
+ ["Schedule", "Task", "Status", "Merge", "Plan UTC", "Card", "Next"],
115
+ rows,
116
+ ),
117
+ );
118
+ }
119
+
120
+ if (morningEl && morningBody) {
121
+ const title = el("h3", null, `早晨窗口 ${morningBody.date ?? ""}`);
122
+ morningEl.appendChild(title);
123
+ const meta = el(
124
+ "p",
125
+ "muted",
126
+ `timezone=${morningBody.timezone ?? "—"} · generated=${morningBody.generatedAt ?? "—"}`,
127
+ );
128
+ morningEl.appendChild(meta);
129
+ const s = morningBody.summary ?? {};
130
+ morningEl.appendChild(
131
+ el(
132
+ "p",
133
+ null,
134
+ `total=${s.total ?? 0} succeeded=${s.succeeded ?? 0} failed=${s.failed ?? 0} human_required=${s.humanRequired ?? 0} pending-harvest=${s.pendingHarvest ?? 0}`,
135
+ ),
136
+ );
137
+ }
138
+
139
+ if (scheduleId && detailEl) {
140
+ detailEl.hidden = false;
141
+ mountViewState(detailEl, "loading", `加载 ${scheduleId}…`);
142
+ const detail = await fetchJson(
143
+ `/api/night-jobs/${encodeURIComponent(scheduleId)}`,
144
+ );
145
+ if (!detail) {
146
+ renderViewState(detailEl, "error", "计划不存在或加载失败。");
147
+ return;
148
+ }
149
+ clearNode(detailEl);
150
+ const schedule = detail.schedule ?? {};
151
+ detailEl.appendChild(el("h3", null, schedule.id ?? scheduleId));
152
+ detailEl.appendChild(
153
+ el(
154
+ "p",
155
+ null,
156
+ `${schedule.featureId ?? "—"} / ${schedule.taskId ?? "—"} · ${schedule.status ?? "—"}`,
157
+ ),
158
+ );
159
+ detailEl.appendChild(el("p", "muted", `next: ${detail.nextAction ?? "—"}`));
160
+ if (schedule.isolation) {
161
+ detailEl.appendChild(
162
+ el(
163
+ "pre",
164
+ "cmd",
165
+ JSON.stringify(
166
+ {
167
+ branch: schedule.isolation.branch,
168
+ worktreePath: schedule.isolation.worktreePath,
169
+ baseCommit: schedule.isolation.baseCommit,
170
+ mergeState: schedule.isolation.mergeState,
171
+ },
172
+ null,
173
+ 2,
174
+ ),
175
+ ),
176
+ );
177
+ }
178
+ if (detail.execution) {
179
+ detailEl.appendChild(el("h4", null, "Current execution"));
180
+ detailEl.appendChild(
181
+ el(
182
+ "pre",
183
+ "cmd",
184
+ JSON.stringify(detail.execution, null, 2).slice(0, 4000),
185
+ ),
186
+ );
187
+ }
188
+ if (Array.isArray(detail.ledger) && detail.ledger.length) {
189
+ detailEl.appendChild(el("h4", null, "Ledger (latest)"));
190
+ detailEl.appendChild(
191
+ el(
192
+ "pre",
193
+ "cmd",
194
+ JSON.stringify(detail.ledger.slice(-10), null, 2).slice(0, 4000),
195
+ ),
196
+ );
197
+ }
198
+ } else if (detailEl) {
199
+ detailEl.hidden = true;
200
+ }
201
+ }
@@ -4,8 +4,17 @@ import YAML from "yaml";
4
4
  import { followUpActionCardSchema } from "../follow-up/schema.js";
5
5
  import { readFollowUpIndex, resolveRepoFile } from "../follow-up/store.js";
6
6
  import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
7
- import { readValidTaskPoolRuns, readValidTaskPoolStates } from "../pool/validation.js";
7
+ import { readValidTaskPoolRuns, readValidTaskPoolStates, } from "../pool/validation.js";
8
8
  export async function renderMorningReport(options) {
9
+ if (options.window === "night") {
10
+ const { buildNightMorningWindow, renderNightMorningMarkdown } = await import("../scheduler/morning-window.js");
11
+ const night = await buildNightMorningWindow({
12
+ controlRepoRoot: options.repoRoot,
13
+ ...(options.date ? { date: options.date } : {}),
14
+ ...(options.timezone ? { timezone: options.timezone } : {}),
15
+ });
16
+ return renderNightMorningMarkdown(night);
17
+ }
9
18
  const { features } = await loadFeatureDecisionModels(options.repoRoot);
10
19
  const allRuns = (await readValidTaskPoolRuns(options.repoRoot)).records;
11
20
  const runs = allRuns.filter((run) => !options.batchRunId || run.batchRunId === options.batchRunId);
@@ -20,20 +29,49 @@ export async function renderMorningReport(options) {
20
29
  "",
21
30
  "## Feature Decision Summary",
22
31
  "",
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
- const selected = feature.planning?.selected[0];
27
- const deferred = feature.planning?.deferred ?? [];
28
- const blocked = feature.planning?.blocked ?? [];
29
- const deferredText = deferred.length > 0
30
- ? deferred.map((item) => `${item.taskId} (${item.reasonCode})`).join(", ")
31
- : "none";
32
- const blockedText = blocked.length > 0
33
- ? blocked.map((item) => `${item.taskId} (${item.reasonCode}${item.blockedBy.length ? `; blockedBy ${item.blockedBy.join(",")}` : ""})`).join(", ")
34
- : "none";
35
- return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Task: ${selected ? `${selected.taskId} (${selected.priority})` : "none"}`, `- Deferred: ${deferredText}`, `- Blocked: ${blockedText}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
36
- })),
32
+ ...(features.length === 0
33
+ ? [
34
+ "- Status: no Feature Packet discovered",
35
+ "- Next Action: add or locate a Feature Packet",
36
+ "- Why: no shared Feature read model is available",
37
+ "- Evidence: none",
38
+ ]
39
+ : features.flatMap((feature) => {
40
+ const why = feature.blockingItems[0]?.message ??
41
+ `${feature.summary.tasksSucceeded}/${feature.summary.tasksTotal} tasks; ${feature.summary.requiredAcCovered}/${feature.summary.requiredAcTotal} required AC`;
42
+ const evidence = [
43
+ feature.evidence.delivery,
44
+ feature.evidence.closeout,
45
+ feature.evidence.morningReport,
46
+ feature.evidence.observeSnapshot,
47
+ ...feature.blockingItems.flatMap((item) => item.evidence),
48
+ ].find(Boolean) ?? "none";
49
+ const selected = feature.planning?.selected[0];
50
+ const deferred = feature.planning?.deferred ?? [];
51
+ const blocked = feature.planning?.blocked ?? [];
52
+ const deferredText = deferred.length > 0
53
+ ? deferred
54
+ .map((item) => `${item.taskId} (${item.reasonCode})`)
55
+ .join(", ")
56
+ : "none";
57
+ const blockedText = blocked.length > 0
58
+ ? blocked
59
+ .map((item) => `${item.taskId} (${item.reasonCode}${item.blockedBy.length ? `; blockedBy ${item.blockedBy.join(",")}` : ""})`)
60
+ .join(", ")
61
+ : "none";
62
+ return [
63
+ `### ${feature.featureId}`,
64
+ "",
65
+ `- Status: ${feature.status}`,
66
+ `- Next Task: ${selected ? `${selected.taskId} (${selected.priority})` : "none"}`,
67
+ `- Deferred: ${deferredText}`,
68
+ `- Blocked: ${blockedText}`,
69
+ `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`,
70
+ `- Why: ${why}`,
71
+ `- Evidence: ${evidence}`,
72
+ "",
73
+ ];
74
+ })),
37
75
  "## Summary",
38
76
  "",
39
77
  `- Total: ${total}`,
@@ -66,7 +104,9 @@ async function followUpSummary(run, repoRoot) {
66
104
  return undefined;
67
105
  try {
68
106
  const index = await readFollowUpIndex(repoRoot, run.featureId);
69
- const entry = index.entries.find((candidate) => candidate.workerRunId === run.workerRunId && candidate.status !== "Superseded" && candidate.status !== "Rejected");
107
+ const entry = index.entries.find((candidate) => candidate.workerRunId === run.workerRunId &&
108
+ candidate.status !== "Superseded" &&
109
+ candidate.status !== "Rejected");
70
110
  if (!entry)
71
111
  return undefined;
72
112
  if (entry.kind === "TaskDraft") {