@tea-agent/loop-agent 0.28.2-beta.1 → 0.28.3

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 (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +37 -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 +538 -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 +90 -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 +412 -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/dist/workflows/dag/frontend-implementation-contract.js +2 -102
  58. package/docs/architecture/runtime-boundaries.md +9 -0
  59. package/docs/init-surface.manifest.json +9 -2
  60. package/docs/templates/harness.schema.json +107 -0
  61. package/docs/templates/init-managed-agents.md +18 -8
  62. package/harness.json +22 -0
  63. package/package.json +1 -1
  64. package/skills/loop-agent/SKILL.md +28 -36
  65. package/skills/loop-agent/references/command-reference.md +40 -16
  66. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  67. package/dist/worker/console/static/assets/index-CNO7n6qB.js +0 -29
@@ -0,0 +1,539 @@
1
+ import path from "node:path";
2
+ import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
3
+ import { addSchedule, cancelScheduledReservation, prepareAdmission, } from "./admission.js";
4
+ import { getScheduleStatus, listScheduleSnapshots, submitSchedule, transitionSchedule, } from "./lifecycle.js";
5
+ import { diagnoseScheduler, formatSchedulerDoctorHuman } from "./doctor.js";
6
+ import { readAdmission, readLedgerEvents, recoverAllPendingJournals, } from "./store.js";
7
+ import { discardNightSchedule, harvestNightSchedule } from "./night-harvest.js";
8
+ import { schedulerTick } from "./scheduler-loop.js";
9
+ import { scheduleStatusSchema, SchedulerError, } from "./types.js";
10
+ import { parseOnceLocalToUtc } from "./trigger.js";
11
+ /**
12
+ * Register `agent-worker scheduler` + `admission` commands.
13
+ * Phase 2: prepare + add + cancel reservation. Live tick lands in Phase 3.
14
+ */
15
+ export function registerSchedulerCommands(program) {
16
+ const admission = program
17
+ .command("admission")
18
+ .description("Night Scheduler admission prepare / inspect (daytime planning)");
19
+ admission
20
+ .command("prepare")
21
+ .description("Create schedule worktree, materialize/validate DAG to writeSet gate, freeze admission")
22
+ .requiredOption("--repo <repo-root>", "Control repo root")
23
+ .requiredOption("--feature-dir <path>", "Feature directory")
24
+ .requiredOption("--task-id <task-id>", "TaskSpec id")
25
+ .requiredOption("--at <local-time>", 'Local execute time "YYYY-MM-DD HH:mm"')
26
+ .option("--tz <timezone>", "IANA timezone for --at", "Asia/Shanghai")
27
+ .option("--task-card <ref>", "Task card / work item ref")
28
+ .option("--loop-agent-bin <path>", "Published loop-agent binary")
29
+ .option("--json", "Emit JSON")
30
+ .action(async (options) => {
31
+ try {
32
+ const controlRepoRoot = path.resolve(options.repo);
33
+ const client = new LoopAgentClient({
34
+ loopAgentBin: options.loopAgentBin ?? "loop-agent",
35
+ artifactRoot: path.join(controlRepoRoot, ".harness", "task-pool", "scheduler", "client-artifacts"),
36
+ resolveIdentity: true,
37
+ });
38
+ const result = await prepareAdmission({
39
+ controlRepoRoot,
40
+ featureDir: options.featureDir,
41
+ taskId: options.taskId,
42
+ at: options.at,
43
+ timezone: options.tz,
44
+ ...(options.taskCard ? { taskCard: options.taskCard } : {}),
45
+ client,
46
+ });
47
+ emit(options.json, result, () => formatReviewPacketHuman(result.reviewPacket));
48
+ }
49
+ catch (error) {
50
+ handleSchedulerCliError(error, options.json === true);
51
+ }
52
+ });
53
+ admission
54
+ .command("show")
55
+ .description("Show frozen admission for a schedule")
56
+ .argument("<schedule-id>", "Schedule id")
57
+ .requiredOption("--repo <repo-root>", "Control repo root")
58
+ .option("--json", "Emit JSON")
59
+ .action(async (scheduleId, options) => {
60
+ try {
61
+ const admission = await readAdmission(path.resolve(options.repo), scheduleId);
62
+ if (!admission) {
63
+ throw new SchedulerError("scheduler-not-found", `admission not found: ${scheduleId}`);
64
+ }
65
+ emit(options.json, admission, () => `${JSON.stringify(admission, null, 2)}\n`);
66
+ }
67
+ catch (error) {
68
+ handleSchedulerCliError(error, options.json === true);
69
+ }
70
+ });
71
+ const scheduler = program
72
+ .command("scheduler")
73
+ .description("Night Scheduler facts, lifecycle, admission commit, and doctor");
74
+ scheduler
75
+ .command("add")
76
+ .description("Approve frozen gate receipt and reserve Task Pool Queued (no DAG execute)")
77
+ .argument("<schedule-id>", "Schedule id from admission prepare")
78
+ .requiredOption("--repo <repo-root>", "Control repo root")
79
+ .requiredOption("--approve-gate <token>", "Gate token from admission review packet")
80
+ .option("--json", "Emit JSON")
81
+ .action(async (scheduleId, options) => {
82
+ try {
83
+ const result = await addSchedule({
84
+ controlRepoRoot: path.resolve(options.repo),
85
+ scheduleId,
86
+ approveGate: options.approveGate,
87
+ });
88
+ emit(options.json, result, () => formatScheduleHuman(result.schedule));
89
+ }
90
+ catch (error) {
91
+ handleSchedulerCliError(error, options.json === true);
92
+ }
93
+ });
94
+ scheduler
95
+ .command("submit")
96
+ .description("Create a submitted schedule fact (planning intent; no admission yet)")
97
+ .requiredOption("--repo <repo-root>", "Control repo root")
98
+ .requiredOption("--feature-id <feature-id>", "Feature id")
99
+ .requiredOption("--task-id <task-id>", "Task id")
100
+ .option("--feature-dir <path>", "Feature directory relative to repo")
101
+ .option("--task-card <ref>", "Task card / work item ref (e.g. UCP-LOOP#11)")
102
+ .option("--at <local-time>", 'Local execute time "YYYY-MM-DD HH:mm"')
103
+ .option("--tz <timezone>", "IANA timezone for --at", "Asia/Shanghai")
104
+ .option("--json", "Emit JSON")
105
+ .action(async (options) => {
106
+ try {
107
+ const controlRepoRoot = path.resolve(options.repo);
108
+ const trigger = options.at
109
+ ? parseOnceLocalToUtc({
110
+ localAt: options.at,
111
+ timezone: options.tz ?? "Asia/Shanghai",
112
+ })
113
+ : undefined;
114
+ const result = await submitSchedule({
115
+ controlRepoRoot,
116
+ featureId: options.featureId,
117
+ taskId: options.taskId,
118
+ ...(options.featureDir ? { featureDir: options.featureDir } : {}),
119
+ ...(options.taskCard ? { workItemRef: options.taskCard } : {}),
120
+ ...(trigger
121
+ ? {
122
+ trigger: {
123
+ type: "once",
124
+ executeAtUtc: trigger.executeAtUtc,
125
+ displayTimezone: trigger.displayTimezone,
126
+ ...(trigger.latestStartAtUtc
127
+ ? { latestStartAtUtc: trigger.latestStartAtUtc }
128
+ : {}),
129
+ misfirePolicy: trigger.misfirePolicy,
130
+ },
131
+ }
132
+ : {}),
133
+ });
134
+ emit(options.json, result.schedule, () => formatScheduleHuman(result.schedule));
135
+ }
136
+ catch (error) {
137
+ handleSchedulerCliError(error, options.json === true);
138
+ }
139
+ });
140
+ scheduler
141
+ .command("list")
142
+ .description("List current schedule snapshots")
143
+ .requiredOption("--repo <repo-root>", "Control repo root")
144
+ .option("--status <status>", "Filter by current status (comma-separated allowed)")
145
+ .option("--json", "Emit JSON")
146
+ .action(async (options) => {
147
+ try {
148
+ const controlRepoRoot = path.resolve(options.repo);
149
+ const statusFilter = parseStatusFilter(options.status);
150
+ const schedules = await listScheduleSnapshots({
151
+ controlRepoRoot,
152
+ ...(statusFilter ? { status: statusFilter } : {}),
153
+ });
154
+ if (options.json) {
155
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, schedules }, null, 2)}\n`);
156
+ return;
157
+ }
158
+ process.stdout.write(formatScheduleListHuman(schedules));
159
+ }
160
+ catch (error) {
161
+ handleSchedulerCliError(error, options.json === true);
162
+ }
163
+ });
164
+ scheduler
165
+ .command("status")
166
+ .description("Show one schedule current snapshot")
167
+ .argument("<schedule-id>", "Schedule id")
168
+ .requiredOption("--repo <repo-root>", "Control repo root")
169
+ .option("--json", "Emit JSON")
170
+ .action(async (scheduleId, options) => {
171
+ try {
172
+ const schedule = await getScheduleStatus({
173
+ controlRepoRoot: path.resolve(options.repo),
174
+ scheduleId,
175
+ });
176
+ emit(options.json, schedule, () => formatScheduleHuman(schedule));
177
+ }
178
+ catch (error) {
179
+ handleSchedulerCliError(error, options.json === true);
180
+ }
181
+ });
182
+ scheduler
183
+ .command("ledger")
184
+ .description("Query append-only schedule lifecycle ledger")
185
+ .requiredOption("--repo <repo-root>", "Control repo root")
186
+ .option("--schedule-id <id>", "Filter by schedule id")
187
+ .option("--to-status <status>", "Filter by toStatus")
188
+ .option("--json", "Emit JSON")
189
+ .action(async (options) => {
190
+ try {
191
+ const events = await readLedgerEvents(path.resolve(options.repo), {
192
+ ...(options.scheduleId ? { scheduleId: options.scheduleId } : {}),
193
+ ...(options.toStatus ? { toStatus: options.toStatus } : {}),
194
+ });
195
+ if (options.json) {
196
+ process.stdout.write(`${JSON.stringify({ schemaVersion: 1, events }, null, 2)}\n`);
197
+ return;
198
+ }
199
+ process.stdout.write(formatLedgerHuman(events));
200
+ }
201
+ catch (error) {
202
+ handleSchedulerCliError(error, options.json === true);
203
+ }
204
+ });
205
+ scheduler
206
+ .command("cancel")
207
+ .description("Cancel a non-running schedule and release Queued reservation when bound")
208
+ .argument("<schedule-id>", "Schedule id")
209
+ .requiredOption("--repo <repo-root>", "Control repo root")
210
+ .requiredOption("--reason <reason>", "Cancel reason")
211
+ .option("--json", "Emit JSON")
212
+ .action(async (scheduleId, options) => {
213
+ try {
214
+ const schedule = await cancelScheduledReservation({
215
+ controlRepoRoot: path.resolve(options.repo),
216
+ scheduleId,
217
+ reason: options.reason,
218
+ });
219
+ emit(options.json, schedule, () => formatScheduleHuman(schedule));
220
+ }
221
+ catch (error) {
222
+ handleSchedulerCliError(error, options.json === true);
223
+ }
224
+ });
225
+ scheduler
226
+ .command("transition")
227
+ .description("Internal/test helper: apply a legal lifecycle transition (no dispatch)")
228
+ .argument("<schedule-id>", "Schedule id")
229
+ .requiredOption("--repo <repo-root>", "Control repo root")
230
+ .requiredOption("--to <status>", "Target status")
231
+ .option("--reason <reason>", "Reason text")
232
+ .option("--json", "Emit JSON")
233
+ .action(async (scheduleId, options) => {
234
+ try {
235
+ const toStatus = scheduleStatusSchema.parse(options.to);
236
+ const result = await transitionSchedule({
237
+ controlRepoRoot: path.resolve(options.repo),
238
+ scheduleId,
239
+ toStatus,
240
+ ...(options.reason ? { reason: options.reason } : {}),
241
+ });
242
+ emit(options.json, result.schedule, () => formatScheduleHuman(result.schedule));
243
+ }
244
+ catch (error) {
245
+ handleSchedulerCliError(error, options.json === true);
246
+ }
247
+ });
248
+ scheduler
249
+ .command("tick")
250
+ .description("Claim due once schedules and dispatch prepared execution (OS cron entry)")
251
+ .requiredOption("--repo <repo-root>", "Control repo root")
252
+ .option("--loop-agent-bin <path>", "Published loop-agent binary")
253
+ .option("--max-claims <n>", "Max schedules to claim this tick", "1")
254
+ .option("--json", "Emit JSON")
255
+ .action(async (options) => {
256
+ try {
257
+ const controlRepoRoot = path.resolve(options.repo);
258
+ const client = new LoopAgentClient({
259
+ loopAgentBin: options.loopAgentBin ?? "loop-agent",
260
+ artifactRoot: path.join(controlRepoRoot, ".harness", "task-pool", "scheduler", "client-artifacts"),
261
+ resolveIdentity: true,
262
+ });
263
+ const maxClaims = Number(options.maxClaims ?? "1");
264
+ const result = await schedulerTick({
265
+ controlRepoRoot,
266
+ client,
267
+ maxClaims: Number.isFinite(maxClaims) ? maxClaims : 1,
268
+ });
269
+ if (options.json) {
270
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
271
+ return;
272
+ }
273
+ process.stdout.write(formatTickHuman(result));
274
+ }
275
+ catch (error) {
276
+ handleSchedulerCliError(error, options.json === true);
277
+ }
278
+ });
279
+ scheduler
280
+ .command("harvest")
281
+ .description("Exact-base fast-forward night branch into base (succeeded + pending-harvest only)")
282
+ .argument("<schedule-id>", "Schedule id")
283
+ .requiredOption("--repo <repo-root>", "Control repo root")
284
+ .option("--keep-worktree", "Keep night worktree/branch after harvest")
285
+ .option("--json", "Emit JSON")
286
+ .action(async (scheduleId, options) => {
287
+ try {
288
+ const result = await harvestNightSchedule({
289
+ controlRepoRoot: path.resolve(options.repo),
290
+ scheduleId,
291
+ keepWorktree: options.keepWorktree === true,
292
+ });
293
+ if (!result.ok) {
294
+ if (options.json) {
295
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
296
+ }
297
+ else {
298
+ process.stderr.write(`harvest blocked (${result.code}): ${result.message}\nnext: ${result.nextAction}\n`);
299
+ }
300
+ process.exitCode = 1;
301
+ return;
302
+ }
303
+ emit(options.json, result, () => [
304
+ `HARVESTED ${result.scheduleId}`,
305
+ ` base: ${result.baseBranch}@${result.mergedCommit.slice(0, 12)}`,
306
+ ` night: ${result.nightBranch}`,
307
+ ` ff: yes`,
308
+ ` worktree: ${result.worktreeRemoved ? "removed" : "kept"}`,
309
+ ` branch: ${result.branchDeleted ? "deleted" : "kept"}`,
310
+ "",
311
+ ].join("\n"));
312
+ }
313
+ catch (error) {
314
+ handleSchedulerCliError(error, options.json === true);
315
+ }
316
+ });
317
+ scheduler
318
+ .command("discard")
319
+ .description("Discard night worktree/branch after evidence retained (failed needs --force)")
320
+ .argument("<schedule-id>", "Schedule id")
321
+ .requiredOption("--repo <repo-root>", "Control repo root")
322
+ .requiredOption("--reason <reason>", "Why this schedule is discarded")
323
+ .option("--force", "Required for failed/human_required schedules")
324
+ .option("--json", "Emit JSON")
325
+ .action(async (scheduleId, options) => {
326
+ try {
327
+ const result = await discardNightSchedule({
328
+ controlRepoRoot: path.resolve(options.repo),
329
+ scheduleId,
330
+ reason: options.reason,
331
+ force: options.force === true,
332
+ });
333
+ if (!result.ok) {
334
+ if (options.json) {
335
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
336
+ }
337
+ else {
338
+ process.stderr.write(`discard blocked (${result.code}): ${result.message}\nnext: ${result.nextAction}\n`);
339
+ }
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ emit(options.json, result, () => [
344
+ `DISCARDED ${result.scheduleId}`,
345
+ ` reason: ${result.reason}`,
346
+ ` worktree: ${result.worktreeRemoved ? "removed" : "kept/missing"}`,
347
+ ` branch: ${result.branchDeleted ? "deleted" : "kept/missing"}`,
348
+ "",
349
+ ].join("\n"));
350
+ }
351
+ catch (error) {
352
+ handleSchedulerCliError(error, options.json === true);
353
+ }
354
+ });
355
+ scheduler
356
+ .command("doctor")
357
+ .description("Read-only Night Scheduler consistency diagnosis")
358
+ .requiredOption("--repo <repo-root>", "Control repo root")
359
+ .option("--json", "Emit JSON")
360
+ .option("--recover-journals", "Replay pending transition journals before diagnosis")
361
+ .action(async (options) => {
362
+ try {
363
+ const controlRepoRoot = path.resolve(options.repo);
364
+ if (options.recoverJournals) {
365
+ await recoverAllPendingJournals(controlRepoRoot);
366
+ }
367
+ const report = await diagnoseScheduler(controlRepoRoot);
368
+ if (options.json) {
369
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
370
+ return;
371
+ }
372
+ process.stdout.write(formatSchedulerDoctorHuman(report));
373
+ if (!report.ok)
374
+ process.exitCode = 1;
375
+ }
376
+ catch (error) {
377
+ handleSchedulerCliError(error, options.json === true);
378
+ }
379
+ });
380
+ }
381
+ function parseStatusFilter(raw) {
382
+ if (!raw)
383
+ return undefined;
384
+ const parts = raw
385
+ .split(",")
386
+ .map((part) => part.trim())
387
+ .filter(Boolean);
388
+ const parsed = parts.map((part) => scheduleStatusSchema.parse(part));
389
+ if (parsed.length === 0)
390
+ return undefined;
391
+ if (parsed.length === 1)
392
+ return parsed[0];
393
+ return parsed;
394
+ }
395
+ function emit(json, value, human) {
396
+ if (json) {
397
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
398
+ return;
399
+ }
400
+ process.stdout.write(human());
401
+ }
402
+ function formatTickHuman(result) {
403
+ const lines = [
404
+ `Night Scheduler tick @ ${result.at}`,
405
+ ` claimed: ${result.claimed.length ? result.claimed.join(", ") : "(none)"}`,
406
+ ` dispatched: ${result.dispatched.length}`,
407
+ ];
408
+ for (const item of result.dispatched) {
409
+ lines.push(` - ${item.schedule.id} → ${item.schedule.status}`);
410
+ }
411
+ if (result.recovery?.length) {
412
+ lines.push(" recovery:");
413
+ for (const item of result.recovery) {
414
+ lines.push(` - ${item.scheduleId}: ${item.action} (${item.detail})`);
415
+ }
416
+ }
417
+ if (result.waiting.length) {
418
+ lines.push(" waiting:");
419
+ for (const item of result.waiting) {
420
+ lines.push(` - ${item.scheduleId}: ${item.reason}`);
421
+ }
422
+ }
423
+ if (result.skipped.length) {
424
+ lines.push(" skipped:");
425
+ for (const item of result.skipped) {
426
+ lines.push(` - ${item.scheduleId}: ${item.reason}`);
427
+ }
428
+ }
429
+ lines.push("");
430
+ return lines.join("\n");
431
+ }
432
+ function formatReviewPacketHuman(packet) {
433
+ const lines = [
434
+ "ADMISSION REVIEW PACKET",
435
+ ` schedule: ${packet.scheduleId}`,
436
+ ` task: ${packet.featureId}/${packet.taskId}`,
437
+ ` executeAt: ${packet.executeAtUtc}`,
438
+ ` worktree: ${packet.worktreePath}`,
439
+ ` branch: ${packet.branch}`,
440
+ ` base: ${packet.baseBranch}@${packet.baseCommit.slice(0, 12)}`,
441
+ ` gate: ${packet.gateToken}`,
442
+ ` admission: ${packet.admissionPath}`,
443
+ ];
444
+ if (packet.harnessTaskId)
445
+ lines.push(` harness: ${packet.harnessTaskId}`);
446
+ if (packet.taskCard)
447
+ lines.push(` card: ${packet.taskCard}`);
448
+ if (packet.writeSet?.length) {
449
+ lines.push(" writeSet:");
450
+ for (const entry of packet.writeSet)
451
+ lines.push(` - ${entry}`);
452
+ }
453
+ lines.push("", "Next: review writeSet, then:", ` agent-worker scheduler add ${packet.scheduleId} --approve-gate ${JSON.stringify(packet.gateToken)} --repo .`, "");
454
+ return lines.join("\n");
455
+ }
456
+ function formatScheduleHuman(schedule) {
457
+ const lines = [
458
+ `SCHEDULE ${schedule.id}`,
459
+ ` task: ${schedule.featureId}/${schedule.taskId}`,
460
+ ` status: ${schedule.status} (rev ${schedule.revision})`,
461
+ ];
462
+ if (schedule.trigger?.executeAtUtc) {
463
+ lines.push(` plan: ${schedule.trigger.executeAtUtc}${schedule.trigger.displayTimezone ? ` (${schedule.trigger.displayTimezone})` : ""}`);
464
+ }
465
+ if (schedule.traceability?.workItemRef) {
466
+ lines.push(` card: ${schedule.traceability.workItemRef}`);
467
+ }
468
+ if (schedule.isolation?.worktreePath) {
469
+ lines.push(` worktree: ${schedule.isolation.worktreePath}`);
470
+ }
471
+ if (schedule.isolation?.mergeState) {
472
+ lines.push(` merge: ${schedule.isolation.mergeState}`);
473
+ }
474
+ lines.push("");
475
+ return `${lines.join("\n")}\n`;
476
+ }
477
+ function formatScheduleListHuman(schedules) {
478
+ if (schedules.length === 0) {
479
+ return "No schedules.\n";
480
+ }
481
+ const header = "SCHEDULE TASK PLAN TIME STATUS MERGE";
482
+ const lines = [header, "-".repeat(header.length)];
483
+ for (const schedule of schedules) {
484
+ const plan = schedule.trigger?.executeAtUtc ?? "-";
485
+ const merge = schedule.isolation?.mergeState ?? "-";
486
+ const task = `${schedule.featureId}/${schedule.taskId}`;
487
+ lines.push([
488
+ pad(schedule.id, 32),
489
+ pad(task, 25),
490
+ pad(plan, 24),
491
+ pad(schedule.status, 16),
492
+ merge,
493
+ ].join(" "));
494
+ }
495
+ lines.push("");
496
+ return `${lines.join("\n")}\n`;
497
+ }
498
+ function formatLedgerHuman(events) {
499
+ if (events.length === 0)
500
+ return "No ledger events.\n";
501
+ const lines = [
502
+ "AT SCHEDULE TRANSITION EVENT",
503
+ ];
504
+ for (const event of events) {
505
+ const transition = `${event.fromStatus ?? "∅"}→${event.toStatus}`;
506
+ lines.push([
507
+ pad(event.at, 25),
508
+ pad(event.scheduleId, 29),
509
+ pad(transition, 23),
510
+ event.event,
511
+ ].join(" "));
512
+ if (event.reason) {
513
+ lines.push(` reason: ${event.reason}`);
514
+ }
515
+ }
516
+ lines.push("");
517
+ return `${lines.join("\n")}\n`;
518
+ }
519
+ function pad(value, width) {
520
+ if (value.length >= width)
521
+ return value.slice(0, width);
522
+ return value + " ".repeat(width - value.length);
523
+ }
524
+ function handleSchedulerCliError(error, json) {
525
+ const message = error instanceof Error ? error.message : String(error);
526
+ const code = error instanceof SchedulerError ? error.code : "scheduler-cli-error";
527
+ if (json) {
528
+ process.stdout.write(`${JSON.stringify({
529
+ schemaVersion: 1,
530
+ ok: false,
531
+ code,
532
+ message,
533
+ }, null, 2)}\n`);
534
+ }
535
+ else {
536
+ process.stderr.write(`${message}\n`);
537
+ }
538
+ process.exitCode = 1;
539
+ }