@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +11 -1
- package/dist/cli/command-definitions.js +2 -1
- package/dist/commands/client-recovery.js +111 -8
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/init-upgrade.js +2479 -0
- package/dist/commands/init.js +120 -9
- package/dist/governance/manifest-types.js +65 -0
- package/dist/shared/operator/capabilities.js +350 -2
- package/dist/task/worktree.js +256 -39
- package/dist/worker/cli.js +22 -12
- package/dist/worker/console/chat/workspace-landing.js +16 -6
- package/dist/worker/console/observe-health-match.js +2 -0
- package/dist/worker/console/observe-link.js +4 -0
- package/dist/worker/console/operator-actions.js +183 -4
- package/dist/worker/console/operator-selection.js +13 -0
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/observe/health.js +1 -0
- package/dist/worker/observe/night-jobs.js +104 -0
- package/dist/worker/observe/routes.js +48 -0
- package/dist/worker/observe/static/app.js +3 -0
- package/dist/worker/observe/static/constants.js +1 -0
- package/dist/worker/observe/static/index.html +47 -0
- package/dist/worker/observe/static/router.js +10 -0
- package/dist/worker/observe/static/shell-chrome.js +1 -0
- package/dist/worker/observe/static/views/night.js +201 -0
- package/dist/worker/report/morning-report.js +56 -16
- package/dist/worker/run-task/execute-prepared-task.js +153 -0
- package/dist/worker/runner/single-task-attempt.js +147 -0
- package/dist/worker/scheduler/admission.js +536 -0
- package/dist/worker/scheduler/auto-followup.js +99 -0
- package/dist/worker/scheduler/cli.js +539 -0
- package/dist/worker/scheduler/dispatcher.js +503 -0
- package/dist/worker/scheduler/doctor.js +346 -0
- package/dist/worker/scheduler/evidence.js +170 -0
- package/dist/worker/scheduler/git-base.js +52 -0
- package/dist/worker/scheduler/index.js +23 -0
- package/dist/worker/scheduler/lease.js +114 -0
- package/dist/worker/scheduler/lifecycle.js +348 -0
- package/dist/worker/scheduler/lock.js +80 -0
- package/dist/worker/scheduler/morning-window.js +161 -0
- package/dist/worker/scheduler/night-git-finalizer.js +88 -0
- package/dist/worker/scheduler/night-harvest.js +421 -0
- package/dist/worker/scheduler/paths.js +84 -0
- package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
- package/dist/worker/scheduler/recovery.js +277 -0
- package/dist/worker/scheduler/reservation.js +146 -0
- package/dist/worker/scheduler/retry.js +199 -0
- package/dist/worker/scheduler/scheduler-loop.js +272 -0
- package/dist/worker/scheduler/store.js +275 -0
- package/dist/worker/scheduler/traceability.js +54 -0
- package/dist/worker/scheduler/trigger.js +258 -0
- package/dist/worker/scheduler/types.js +369 -0
- package/dist/worker/scheduler/workspace-adapter.js +91 -0
- package/docs/architecture/runtime-boundaries.md +9 -0
- package/docs/init-surface.manifest.json +9 -2
- package/docs/templates/harness.schema.json +107 -0
- package/docs/templates/init-managed-agents.md +18 -8
- package/harness.json +22 -0
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +28 -36
- package/skills/loop-agent/references/command-reference.md +40 -16
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- 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-
|
|
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") {
|
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 &&
|
|
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") {
|