@intentius/chant 0.95.1 → 0.97.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 (79) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/registry.d.ts +9 -1
  3. package/dist/cli/registry.d.ts.map +1 -1
  4. package/dist/lifecycle/run-ledger.d.ts +2 -0
  5. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  6. package/dist/op/activities/shell.d.ts.map +1 -1
  7. package/dist/op/index.d.ts +2 -0
  8. package/dist/op/index.d.ts.map +1 -1
  9. package/dist/op/local-executor.d.ts.map +1 -1
  10. package/dist/op/operator.d.ts.map +1 -1
  11. package/dist/op/run-live.d.ts +127 -0
  12. package/dist/op/run-live.d.ts.map +1 -0
  13. package/dist/op/runtime.d.ts +2 -0
  14. package/dist/op/runtime.d.ts.map +1 -1
  15. package/dist/workspace/checks/diagrams.d.ts +5 -0
  16. package/dist/workspace/checks/diagrams.d.ts.map +1 -1
  17. package/dist/workspace/declaration.d.ts +5 -5
  18. package/dist/workspace/declaration.d.ts.map +1 -1
  19. package/dist/workspace/declaration.schema.json +50 -7
  20. package/dist/workspace/intent-cli.d.ts +6 -0
  21. package/dist/workspace/intent-cli.d.ts.map +1 -1
  22. package/dist/workspace/intent-record.d.ts +145 -0
  23. package/dist/workspace/intent-record.d.ts.map +1 -0
  24. package/dist/workspace/intent.d.ts +46 -1
  25. package/dist/workspace/intent.d.ts.map +1 -1
  26. package/dist/workspace/ls.d.ts +2 -2
  27. package/dist/workspace/ls.d.ts.map +1 -1
  28. package/dist/workspace/patch.d.ts +122 -0
  29. package/dist/workspace/patch.d.ts.map +1 -0
  30. package/dist/workspace/points-cli.d.ts +8 -0
  31. package/dist/workspace/points-cli.d.ts.map +1 -1
  32. package/dist/workspace/reason-codes.d.ts +2 -0
  33. package/dist/workspace/reason-codes.d.ts.map +1 -1
  34. package/dist/workspace/record-decided.d.ts +36 -0
  35. package/dist/workspace/record-decided.d.ts.map +1 -0
  36. package/dist/workspace/records-cli.d.ts +8 -0
  37. package/dist/workspace/records-cli.d.ts.map +1 -1
  38. package/dist/workspace/status-stewards.d.ts +79 -12
  39. package/dist/workspace/status-stewards.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/main.test.ts +16 -0
  42. package/src/cli/main.ts +34 -3
  43. package/src/cli/registry.ts +9 -1
  44. package/src/lifecycle/run-ledger.ts +7 -1
  45. package/src/op/activities/shell.ts +4 -1
  46. package/src/op/index.ts +2 -0
  47. package/src/op/local-executor.ts +51 -3
  48. package/src/op/operator.ts +2 -11
  49. package/src/op/run-live.test.ts +111 -0
  50. package/src/op/run-live.ts +305 -0
  51. package/src/op/runtime.ts +2 -0
  52. package/src/op/steward-points.test.ts +81 -0
  53. package/src/workspace/checks/diagrams.test.ts +24 -0
  54. package/src/workspace/checks/diagrams.ts +10 -3
  55. package/src/workspace/declaration.schema.json +50 -7
  56. package/src/workspace/declaration.test.ts +18 -0
  57. package/src/workspace/declaration.ts +7 -7
  58. package/src/workspace/intent-cli.ts +47 -1
  59. package/src/workspace/intent-record.schema.json +529 -0
  60. package/src/workspace/intent-record.test.ts +257 -0
  61. package/src/workspace/intent-record.ts +371 -0
  62. package/src/workspace/intent.schema.json +33 -1
  63. package/src/workspace/intent.ts +29 -12
  64. package/src/workspace/ls-contract.test.ts +14 -0
  65. package/src/workspace/ls.schema.json +3 -3
  66. package/src/workspace/ls.ts +3 -3
  67. package/src/workspace/patch.schema.json +321 -0
  68. package/src/workspace/patch.test.ts +198 -0
  69. package/src/workspace/patch.ts +377 -0
  70. package/src/workspace/points-cli.ts +18 -0
  71. package/src/workspace/read-contract.test.ts +29 -1
  72. package/src/workspace/reason-codes.test.ts +2 -0
  73. package/src/workspace/reason-codes.ts +3 -0
  74. package/src/workspace/record-decided.ts +107 -0
  75. package/src/workspace/records-cli.ts +12 -0
  76. package/src/workspace/records.schema.json +15 -0
  77. package/src/workspace/status-contract.test.ts +76 -2
  78. package/src/workspace/status-stewards.ts +149 -27
  79. package/src/workspace/status.schema.json +69 -6
@@ -14,13 +14,25 @@
14
14
  * - each Op's last run is the newest record in its run ledger,
15
15
  * `<env>/runs__<op>.jsonl` under the member's ledger prefix, where `<env>`
16
16
  * is the Op's own `labels.Env` or `local`;
17
+ * - an Op's `inFlight` is the run of it in flight now, from the record a
18
+ * run keeps beside the checkout's git directory while it runs
19
+ * (`../op/run-live.ts`): its phase and step, the phases it has finished and
20
+ * its newest activity lines. Null once the run's ledger record is written,
21
+ * or when the process that wrote it is gone;
17
22
  * - `waiting` lists the open decision points the steward waits on (#2749):
18
- * each Op whose newest run is the steward's own and stopped on a question,
19
- * as the run ledger records it. The question's state now is `points`'s.
20
- * A run is the steward's when its record names it (`steward`, written for
21
- * a run started in the steward's turn or under `CHANT_STEWARD`), so this
22
- * also covers the member's other Ops, those no steward lists, that such a
23
- * run belongs to, such as an Op the steward's process starts itself;
23
+ * each Op whose newest run is the steward's own and stopped on a question
24
+ * that is still open. The question's state is read now, the way `points`
25
+ * reads it (`readQuestionStates`), not copied from the run ledger, which
26
+ * keeps the state the run stopped with. A question answered since, or
27
+ * whose record is gone, is not waited on; nor is one whose Op has a newer
28
+ * run in flight that has not written its record yet: the Op's lease or one
29
+ * of its work leases was taken after the waiting run ended
30
+ * (`newerRunInFlight`). When the questions can't be read, the ledger's
31
+ * state stands, as the operator's does. A run is the steward's when its
32
+ * record names it (`steward`, written for a run started in the steward's
33
+ * turn or under `CHANT_STEWARD`), so this also covers the member's other
34
+ * Ops, those no steward lists, that such a run belongs to, such as an Op
35
+ * the steward's process starts itself;
24
36
  * - `lease` is the local steward's own lease ref
25
37
  * (`refs/chant/lease/[<prefix>]_stewards/<name>`), present while a
26
38
  * `chant operator --steward` holds it or until it expires;
@@ -55,16 +67,64 @@ export interface StatusStewardRun {
55
67
  op: string;
56
68
  approve: string;
57
69
  } | null;
58
- /** The open decision point the run stopped on, for a `waiting` run (#2749). */
70
+ /**
71
+ * The decision point the run stopped on, for a `waiting` run (#2749). Its
72
+ * state is the question's now, `answered` once a person has answered it.
73
+ */
59
74
  point: StatusStewardWait | null;
75
+ /** Each phase in execution order, with its wall-clock time, or the sum of its steps' for a record that kept none. */
76
+ phases: StatusStewardPhase[];
77
+ }
78
+ /** A phase of a run: its verdict and how long it took. */
79
+ export interface StatusStewardPhase {
80
+ name: string;
81
+ status: "ok" | "fail" | "skipped";
82
+ durationMs: number;
83
+ }
84
+ /**
85
+ * The run of an Op in flight now (../op/run-live.ts): what it is doing while
86
+ * its ledger record is not yet written.
87
+ */
88
+ export interface StatusStewardInFlight {
89
+ id: string;
90
+ started: string;
91
+ /** When the run last changed its record. */
92
+ updated: string;
93
+ steward: string | null;
94
+ /** The work item the run's lease holds, once it is claimed. */
95
+ item: string | null;
96
+ phase: {
97
+ name: string;
98
+ started: string;
99
+ } | null;
100
+ step: {
101
+ name: string;
102
+ fn: string;
103
+ started: string;
104
+ } | null;
105
+ /** The phases it has finished, in order. */
106
+ phases: StatusStewardPhase[];
107
+ /** The newest activity lines its steps reported; `total` counts every line of the run. */
108
+ activity: {
109
+ total: number;
110
+ lines: {
111
+ seq: number;
112
+ at: string | null;
113
+ text: string;
114
+ }[];
115
+ };
60
116
  }
61
- /** An open decision point a steward's run stopped on (#2749), as the run ledger records it. */
117
+ /** A decision point a steward's run stopped on (#2749). */
62
118
  export interface StatusStewardWait {
63
119
  /** The answer record's id: what `points --open` lists and `points answer` names. */
64
120
  id: string;
65
121
  point: string;
66
- /** The question's state when the run stopped. */
67
- state: "escalated" | "proposed";
122
+ /**
123
+ * The question's state now, as `points` reads it. The state the run
124
+ * stopped with when the questions can't be read or its record is gone.
125
+ * Never `answered` in a steward's `waiting`.
126
+ */
127
+ state: "escalated" | "proposed" | "answered";
68
128
  path: string;
69
129
  subject: string | null;
70
130
  since: string;
@@ -104,6 +164,8 @@ export interface StatusStewardOp {
104
164
  env: string;
105
165
  /** The newest run in its run ledger, or null when it has none. */
106
166
  lastRun: StatusStewardRun | null;
167
+ /** The run in flight now, or null when none is. */
168
+ inFlight: StatusStewardInFlight | null;
107
169
  /** For a ConvergeOp, its newest tick on the converge ledger (#2778); null for any other Op, or before its first tick. */
108
170
  lastTick: StatusStewardTick | null;
109
171
  /** Whether the Op changes the checkout, and so runs under a work lease on a branch of its own (#2748). */
@@ -172,7 +234,8 @@ export interface StatusSteward {
172
234
  ops: StatusStewardOp[];
173
235
  /**
174
236
  * The open decision points the steward waits on (#2749): each Op whose
175
- * newest run is the steward's own and stopped on a question. Its declared
237
+ * newest run is the steward's own and stopped on a question still open,
238
+ * with no newer run of the Op in flight. Its declared
176
239
  * Ops come first, in `ops` order, then any other Op of the member whose
177
240
  * newest run names the steward, by Op name.
178
241
  */
@@ -188,6 +251,8 @@ export interface MemberStewards {
188
251
  message: string;
189
252
  }[];
190
253
  }
254
+ /** Every question's state in the workspace by id, or null when they can't all be read. */
255
+ export type ReadQuestionStates = (cwd: string) => Promise<Map<string, string> | null>;
191
256
  /**
192
257
  * The stewards declared in one member, for `env`. `memberDir` is absolute.
193
258
  * Only a member of kind `chant` with a config of its own is read.
@@ -197,5 +262,7 @@ export declare function readMemberStewards(memberDir: string, env: string, now:
197
262
  name: string;
198
263
  broker: string | null;
199
264
  }[];
200
- } | null): Promise<MemberStewards>;
265
+ } | null, deps?: {
266
+ readQuestions?: ReadQuestionStates;
267
+ }): Promise<MemberStewards>;
201
268
  //# sourceMappingURL=status-stewards.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status-stewards.d.ts","sourceRoot":"","sources":["../../src/workspace/status-stewards.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status-stewards.d.ts","sourceRoot":"","sources":["../../src/workspace/status-stewards.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.95.1",
3
+ "version": "0.97.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -756,6 +756,7 @@ describe("workspace init and ls (#2534)", () => {
756
756
  "workspace lint",
757
757
  "workspace ls",
758
758
  "workspace member-run",
759
+ "workspace patch",
759
760
  "workspace points",
760
761
  "workspace records",
761
762
  "workspace status",
@@ -773,6 +774,21 @@ describe("workspace init and ls (#2534)", () => {
773
774
  expect(() => parseArgs(["workspace", "work", "claim", "W-001", "--holder"])).toThrow(/--holder needs a value/);
774
775
  });
775
776
 
777
+ test("graph --intent takes --record in place of a region", () => {
778
+ expect(parseArgs(["workspace", "graph", "--intent", "--record", "studio-008", "--json"])).toMatchObject({ intent: "", record: "studio-008", json: true });
779
+ expect(parseArgs(["workspace", "graph", "--intent", "app/server.mjs"])).toMatchObject({ intent: "app/server.mjs" });
780
+ expect(() => parseArgs(["workspace", "graph", "--intent", "--json"])).toThrow(/--intent needs a region/);
781
+ expect(() => parseArgs(["workspace", "graph", "--intent", "--record"])).toThrow(/--record needs a record id/);
782
+ });
783
+
784
+ test("patch takes a range, --path and --max-bytes", () => {
785
+ const patch = parseArgs(["workspace", "patch", "main...chant/work/W-001", "--path", "app", "--path", "docs/a.md", "--max-bytes", "4096", "--json"]);
786
+ expect(patch).toMatchObject({ command: "workspace", path: "patch", extraPositional: "main...chant/work/W-001", paths: ["app", "docs/a.md"], maxBytes: 4096, json: true });
787
+ expect(resolveCommand(patch, commandRegistry)?.def.name).toBe("workspace patch");
788
+ expect(() => parseArgs(["workspace", "patch", "HEAD", "--max-bytes", "0"])).toThrow(/--max-bytes needs a whole number/);
789
+ expect(parseArgs(["workspace", "patch", "--worktree", "--path", "docs/a.md", "--json"])).toMatchObject({ command: "workspace", path: "patch", worktree: true, paths: ["docs/a.md"], json: true });
790
+ });
791
+
776
792
  test("run takes --work <id> for an Op with a work lease (#2748)", () => {
777
793
  expect(parseArgs(["run", "dispatch", "--work", "W-001", "--holder", "box/dispatch@h"])).toMatchObject({ command: "run", path: "dispatch", work: "W-001", holder: "box/dispatch@h" });
778
794
  expect(() => parseArgs(["run", "dispatch", "--work"])).toThrow(/--work needs a work item id/);
package/src/cli/main.ts CHANGED
@@ -377,9 +377,28 @@ export function parseArgs(args: string[]): ParsedArgs {
377
377
  // `chant workspace graph --composites` (#2662)
378
378
  result.composites = true;
379
379
  } else if (arg === "--intent") {
380
- // `chant workspace graph --intent <path[:start-end]>` (#2651)
381
- result.intent = args[++i];
382
- if (!result.intent || result.intent.startsWith("-")) throw new Error("--intent needs a region: --intent <path[:start-end]>");
380
+ // `chant workspace graph --intent <path[:start-end]>` (#2651), or `--intent --record <id>` with no region.
381
+ const value = args[i + 1];
382
+ if (value !== undefined && !value.startsWith("-")) result.intent = args[++i];
383
+ else if (args.includes("--record")) result.intent = "";
384
+ else throw new Error("--intent needs a region: --intent <path[:start-end]>, or --intent --record <id>");
385
+ } else if (arg === "--record") {
386
+ // `chant workspace graph --intent --record <id>`
387
+ result.record = args[++i];
388
+ if (!result.record || result.record.startsWith("-")) throw new Error("--record needs a record id: --record <id>");
389
+ } else if (arg === "--path") {
390
+ // `chant workspace patch <range> --path <p>`, repeatable
391
+ const value = args[++i];
392
+ if (!value || value.startsWith("-")) throw new Error("--path needs a path: --path <path>");
393
+ (result.paths ??= []).push(value);
394
+ } else if (arg === "--worktree") {
395
+ // `chant workspace patch [<commit>] --worktree`
396
+ result.worktree = true;
397
+ } else if (arg === "--max-bytes") {
398
+ // `chant workspace patch <range> --max-bytes <n>`
399
+ const value = Number(args[++i]);
400
+ if (!Number.isInteger(value) || value < 1) throw new Error("--max-bytes needs a whole number of bytes above 0");
401
+ result.maxBytes = value;
383
402
  } else if (arg === "--changes") {
384
403
  // `chant workspace check --changes <base>..<head>` (#2773)
385
404
  result.changes = args[++i];
@@ -923,6 +942,12 @@ Workspace (level 1, #2524):
923
942
  --work item and its decisions) lists it in
924
943
  out_of_scope. The declaration's changes block sets
925
944
  the severity (warn by default) and ignore globs
945
+ workspace patch <base>..<head>|<base>...<head>|<commit> [--path <p>...] [--max-bytes <n>] [--json]
946
+ workspace patch [<commit>] --worktree [--path <p>...] [--max-bytes <n>] [--json]
947
+ The hunks of a diff, file by file: a range, a work
948
+ branch, or one commit against its first parent. Each
949
+ file's hunk text stops at --max-bytes (64 KiB) and
950
+ says it was truncated
926
951
  workspace build [dir] [--member <name>] [-o <dir>] [--dry-run]
927
952
  Build every chant member and example project, each with
928
953
  its own chant, one process per toolchain. -o <dir>
@@ -952,6 +977,10 @@ Workspace (level 1, #2524):
952
977
  touched it, the decisions whose constrains cover it,
953
978
  the artifacts they pin, and findings with closed codes.
954
979
  Without --kind, every record kind the declaration names
980
+ workspace graph --intent --record <id> [--at <rev>] [--kind <kind file>...] [--json]
981
+ One decision's intent walk over every path: and member:
982
+ entry it constrains: the commits in its window, each
983
+ own, worked, within-other or unexplained, with counts
955
984
 
956
985
  Lifecycle (alias: lc):
957
986
  lifecycle snapshot <env> Query API, save metadata to orphan branch
@@ -1442,6 +1471,8 @@ export const commandRegistry: CommandDef[] = [
1442
1471
  { name: "workspace upgrade", handler: async (ctx) => (await import("../workspace/lineage-upgrade-cli")).runWorkspaceUpgrade(ctx) },
1443
1472
  // #2641 — workspace check reads member configs statically and never runs one.
1444
1473
  { name: "workspace check", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/lineage-check")).runWorkspaceCheck(ctx) },
1474
+ // The hunks of a diff, for a reader that runs no git of its own.
1475
+ { name: "workspace patch", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/patch")).runWorkspacePatch(ctx) },
1445
1476
  // #2537 — per-member commands. Each member runs under its own chant, one
1446
1477
  // process per toolchain identity; `member-run` is that process's entry.
1447
1478
  { name: "workspace build", handler: async (ctx) => (await import("../workspace/member-commands")).runWorkspaceMembers(ctx, "build") },
@@ -303,8 +303,16 @@ export interface ParsedArgs {
303
303
  noCache?: boolean;
304
304
  /** `chant workspace graph --composites` (#2662): print each composite instance with the components that can deploy it. */
305
305
  composites?: boolean;
306
- /** `chant workspace graph --intent <path[:start-end]>` (#2651): the region the intent graph is over. */
306
+ /** `chant workspace graph --intent <path[:start-end]>` (#2651): the region the intent graph is over. Empty with `--record`. */
307
307
  intent?: string;
308
+ /** `chant workspace graph --intent --record <id>`: walk one decision record over every entry its constrains lists. */
309
+ record?: string;
310
+ /** `chant workspace patch <range> --path <p>`: only these paths, from the workspace root. Repeatable. */
311
+ paths?: string[];
312
+ /** `chant workspace patch <range> --max-bytes <n>`: the most hunk text printed for one file. */
313
+ maxBytes?: number;
314
+ /** `chant workspace patch [<commit>] --worktree`: the working tree against the commit (HEAD unless given). */
315
+ worktree?: boolean;
308
316
  /** `chant workspace check --changes <base>..<head>` (#2773): the diff the forward coverage check maps to records. */
309
317
  changes?: string;
310
318
  /** `chant workspace check --changes ... --severity off|warn|fail` (#2773): in place of the declaration's `changes.severity`. */
@@ -87,6 +87,8 @@ export function buildRunRecord(
87
87
  gate?: OpRunRecord["gate"];
88
88
  point?: OpRunRecord["point"];
89
89
  steward?: string;
90
+ /** Each phase's wall-clock time, by name, as the executor measured it. */
91
+ phaseDurations?: Record<string, number>;
90
92
  },
91
93
  ): OpRunRecordInput {
92
94
  const phases: OpRunPhaseRecord[] = [];
@@ -113,7 +115,11 @@ export function buildRunRecord(
113
115
  });
114
116
  if (record.outcome) outcomes[record.outcome.name] = record.outcome.value;
115
117
  }
116
- for (const phase of phases) phase.status = phaseStatus(phase.steps);
118
+ for (const phase of phases) {
119
+ phase.status = phaseStatus(phase.steps);
120
+ const ms = times.phaseDurations?.[phase.name];
121
+ if (ms !== undefined) phase.durationMs = ms;
122
+ }
117
123
 
118
124
  return {
119
125
  ...(times.id !== undefined ? { id: times.id } : {}),
@@ -2,6 +2,7 @@ import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { GateWait, asPendingGate } from "../gate-wait";
4
4
  import type { PendingGateRecord } from "../../lifecycle/gate-ledger";
5
+ import { liveRunEnv } from "../run-live";
5
6
 
6
7
  const execAsync = promisify(exec);
7
8
 
@@ -116,7 +117,9 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
116
117
  try {
117
118
  const { stdout, stderr } = await execAsync(args.cmd, {
118
119
  cwd: args.cwd,
119
- env: { ...process.env, ...args.env },
120
+ // In a run that keeps an in-flight record, CHANT_RUN_ACTIVITY names the
121
+ // file the command appends activity lines to (../run-live.ts).
122
+ env: { ...process.env, ...liveRunEnv(), ...args.env },
120
123
  maxBuffer: MAX_STDOUT_BYTES,
121
124
  signal,
122
125
  });
package/src/op/index.ts CHANGED
@@ -135,6 +135,8 @@ export {
135
135
  currentStewardTurn, enterStewardTurn, setStewardTurn, resetStewardTurn, STEWARD_ENV,
136
136
  } from "./steward-turn";
137
137
  export type { StewardTurn } from "./steward-turn";
138
+ export { reportRunActivity, readInFlightRun, RUN_ACTIVITY_ENV, RUN_ID_ENV } from "./run-live";
139
+ export type { InFlightRun, InFlightRecord, InFlightPhase, InFlightActivityLine } from "./run-live";
138
140
  export type { DiscoveredSteward, StewardDiscoveryResult } from "./discover";
139
141
  export type {
140
142
  OperatorTickEvent, OperatorRoundOptions, OperatorLoopOptions,
@@ -38,6 +38,8 @@ import type { OpRunRecord } from "./runtime";
38
38
  import { randomUUID } from "node:crypto";
39
39
  import { RunWorkLease, LEASE_LOST, workLeaseProblems, workLeaseNeedsRunItem, type WorkLeaseRunResult } from "./work-lease-run";
40
40
  import { WORK_LEASE_STEP_ID } from "./types";
41
+ import { LiveRun, currentLiveRun, withLiveRun } from "./run-live";
42
+ import { runEnvOf } from "../lifecycle/run-ledger";
41
43
 
42
44
  export { parseDuration } from "./duration";
43
45
 
@@ -398,6 +400,15 @@ async function runStep(
398
400
  : Infinity;
399
401
  const nonRetryable = profile.retry?.nonRetryableErrorTypes ?? [];
400
402
 
403
+ // The in-flight record names the step while it runs.
404
+ const ended = currentLiveRun()?.stepStarted(step.fn, step.id);
405
+ try {
406
+ return await attempts();
407
+ } finally {
408
+ ended?.();
409
+ }
410
+
411
+ async function attempts(): Promise<RanStep> {
401
412
  let lastErr: unknown;
402
413
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
403
414
  try {
@@ -435,6 +446,7 @@ async function runStep(
435
446
  }
436
447
  }
437
448
  return { record: { ...base, status: "fail", durationMs: Date.now() - start, error: errMessage(lastErr) } };
449
+ }
438
450
  }
439
451
 
440
452
  // ── Effect steps (#1834) ──────────────────────────────────────────────────────
@@ -518,6 +530,7 @@ async function workBoundary(
518
530
  return "unclaimed";
519
531
  }
520
532
  resultsById.set(WORK_LEASE_STEP_ID, claimed.lease);
533
+ currentLiveRun()?.item(claimed.lease.item);
521
534
  pushRecord(sink, ctx, {
522
535
  phase: phaseName,
523
536
  fn: "workLease:claim",
@@ -1026,9 +1039,22 @@ export async function runOpLocally(
1026
1039
  const turn = currentStewardTurn();
1027
1040
  const steward = options.steward ?? turn?.steward;
1028
1041
  const restore = turn && turn.run !== runId ? enterStewardTurn({ ...turn, run: runId }) : undefined;
1042
+ // A run that writes to the ledger also says what it is doing while it runs
1043
+ // (./run-live.ts), and removes that once its record is written.
1044
+ const live = options.ledger
1045
+ ? await LiveRun.open({
1046
+ cwd: options.ledger.cwd ?? options.cwd ?? process.cwd(),
1047
+ env: runEnvOf(config),
1048
+ op: config.name,
1049
+ id: runId,
1050
+ started: options.now ?? new Date().toISOString(),
1051
+ ...(steward ? { steward } : {}),
1052
+ })
1053
+ : undefined;
1029
1054
  try {
1030
- return await runOpInTurn(config, activities, profiles, signal, { ...options, runId, ...(steward ? { steward } : {}) });
1055
+ return await withLiveRun(live, () => runOpInTurn(config, activities, profiles, signal, { ...options, runId, ...(steward ? { steward } : {}) }));
1031
1056
  } finally {
1057
+ live?.close();
1032
1058
  restore?.();
1033
1059
  }
1034
1060
  }
@@ -1126,6 +1152,20 @@ async function runOpInTurn(
1126
1152
 
1127
1153
  const records: StepRecord[] = [];
1128
1154
  const start = Date.now();
1155
+ // Each phase's wall-clock time, for the run record and the in-flight one.
1156
+ const phaseDurations: Record<string, number> = {};
1157
+ const timedPhase = async (phase: PhaseDefinition, ctx: GateContext, phaseSignal: AbortSignal | undefined): Promise<StepRecord[]> => {
1158
+ const live = currentLiveRun();
1159
+ const t0 = Date.now();
1160
+ live?.phaseStarted(phase.name);
1161
+ try {
1162
+ const ran = await runPhase(phase, activities, profiles, resultsById, ctx, phaseSignal);
1163
+ live?.phaseEnded(phase.name, phaseVerdict(ran), Date.now() - t0);
1164
+ return ran;
1165
+ } finally {
1166
+ phaseDurations[phase.name] = (phaseDurations[phase.name] ?? 0) + (Date.now() - t0);
1167
+ }
1168
+ };
1129
1169
  const startedAt = options.now ?? new Date(start).toISOString();
1130
1170
 
1131
1171
  /**
@@ -1148,6 +1188,7 @@ async function runOpInTurn(
1148
1188
  ...(gate ? { gate: { name: gate.gate, since: gate.timestamp, ...(gate.op !== config.name ? { op: gate.op } : {}) } } : {}),
1149
1189
  ...(point ? { point: { ...point, since: ended } } : {}),
1150
1190
  ...(options.steward ? { steward: options.steward } : {}),
1191
+ phaseDurations,
1151
1192
  });
1152
1193
  const record: OpRunRecord = { version: 1, ...input, id: runId };
1153
1194
  if (!options.ledger) return record;
@@ -1167,7 +1208,7 @@ async function runOpInTurn(
1167
1208
  try {
1168
1209
  for (const phase of config.phases) {
1169
1210
  if (signal?.aborted) throw new PhaseFailure([]);
1170
- records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, stepSignal)));
1211
+ records.push(...(await timedPhase(phase, gates, stepSignal)));
1171
1212
  }
1172
1213
  // The last renewal before the run is recorded (#2748): a run whose lease
1173
1214
  // was lost is never recorded as done.
@@ -1269,7 +1310,7 @@ async function runOpInTurn(
1269
1310
  if (!signal?.aborted) {
1270
1311
  for (const phase of [...(config.onFailure ?? [])].reverse()) {
1271
1312
  try {
1272
- records.push(...(await runPhase(phase, activities, profiles, resultsById, compensation, signal)));
1313
+ records.push(...(await timedPhase(phase, compensation, signal)));
1273
1314
  } catch (compErr) {
1274
1315
  if (compErr instanceof PhaseFailure || compErr instanceof GateStop || compErr instanceof PointStop) {
1275
1316
  records.push(...compErr.records);
@@ -1316,6 +1357,13 @@ async function runOpInTurn(
1316
1357
  };
1317
1358
  }
1318
1359
 
1360
+ /** A finished phase's verdict, as the run ledger folds it: `fail`, `skipped` when every step was, else `ok`. */
1361
+ function phaseVerdict(records: readonly StepRecord[]): "ok" | "fail" | "skipped" {
1362
+ if (records.some((r) => r.status === "fail")) return "fail";
1363
+ if (records.length > 0 && records.every((r) => r.status === "skipped")) return "skipped";
1364
+ return "ok";
1365
+ }
1366
+
1319
1367
  function errMessage(err: unknown): string {
1320
1368
  return err instanceof Error ? err.message : String(err);
1321
1369
  }
@@ -342,17 +342,8 @@ async function stewardGates(steward: StewardDeclaration, opts: OperatorRoundOpti
342
342
 
343
343
  /** Every question's state in the workspace, by id, or null when the workspace's points can't be read. */
344
344
  async function readQuestionStates(cwd: string): Promise<Map<string, string> | null> {
345
- try {
346
- const { workspacePoints } = await import("../workspace/points-cli");
347
- const doc = await workspacePoints({ cwd });
348
- if (!("questions" in doc)) return null;
349
- // An answer kind that could not be read might hold the question: say
350
- // nothing rather than resume a run whose question may still be open.
351
- if (doc.sources.some((s) => s.reason !== null)) return null;
352
- return new Map(doc.questions.map((q) => [q.id, q.state]));
353
- } catch {
354
- return null;
355
- }
345
+ const { readQuestionStates: read } = await import("../workspace/points-cli");
346
+ return read(cwd);
356
347
  }
357
348
 
358
349
  /** Take or renew a local steward's own lease (#2731). */
@@ -0,0 +1,111 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { appendFileSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, describe, expect, test } from "vitest";
6
+ import { LiveRun, inFlightPaths, liveRunEnv, readInFlightRun, reportRunActivity, withLiveRun, RUN_ACTIVITY_ENV } from "./run-live";
7
+ import { runOpLocally } from "./local-executor";
8
+ import { readRunLedger } from "../lifecycle/run-ledger";
9
+ import type { OpConfig } from "./types";
10
+
11
+ const dirs: string[] = [];
12
+ afterEach(() => {
13
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
14
+ });
15
+
16
+ function checkout(): string {
17
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "run-live-")));
18
+ dirs.push(dir);
19
+ execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir });
20
+ execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init"], { cwd: dir });
21
+ return dir;
22
+ }
23
+
24
+ describe("the in-flight run record", () => {
25
+ test("is kept under the git directory and read back with the newest activity lines", async () => {
26
+ const dir = checkout();
27
+ const live = (await LiveRun.open({ cwd: dir, env: "local", op: "build", id: "r1", started: "2026-09-27T10:00:00.000Z", steward: "s" }))!;
28
+ expect(live.activityFile.startsWith(join(dir, ".git", "chant", "runs"))).toBe(true);
29
+ live.phaseStarted("Pick");
30
+ live.phaseEnded("Pick", "ok", 12);
31
+ live.phaseStarted("Build");
32
+ const done = live.stepStarted("shellCmd", "build");
33
+ live.item("W-1");
34
+ for (let i = 1; i <= 25; i++) appendFileSync(live.activityFile, `line ${i}\n`);
35
+
36
+ const run = (await readInFlightRun(dir, "local", "build"))!;
37
+ expect(run).toMatchObject({
38
+ id: "r1",
39
+ steward: "s",
40
+ item: "W-1",
41
+ phase: { name: "Build" },
42
+ step: { name: "build", fn: "shellCmd" },
43
+ phases: [{ name: "Pick", status: "ok", durationMs: 12 }],
44
+ });
45
+ expect(run.activity.total).toBe(25);
46
+ expect(run.activity.lines).toHaveLength(20);
47
+ expect(run.activity.lines[0]).toEqual({ seq: 6, at: null, text: "line 6" });
48
+
49
+ done();
50
+ expect((await readInFlightRun(dir, "local", "build"))!.step).toBeNull();
51
+ live.close();
52
+ expect(await readInFlightRun(dir, "local", "build")).toBeNull();
53
+ });
54
+
55
+ test("a record left by a process that is gone is not in flight", async () => {
56
+ const dir = checkout();
57
+ const paths = (await inFlightPaths(dir, "local", "build"))!;
58
+ const live = (await LiveRun.open({ cwd: dir, env: "local", op: "build", id: "r1", started: "2026-09-27T10:00:00.000Z" }))!;
59
+ const rec = JSON.parse(readFileSync(paths.record, "utf8"));
60
+ writeFileSync(paths.record, JSON.stringify({ ...rec, pid: 2 ** 22 + 12345 }));
61
+ expect(await readInFlightRun(dir, "local", "build")).toBeNull();
62
+ live.close();
63
+ });
64
+
65
+ test("outside a checkout there is none, and a report is a no-op", async () => {
66
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "run-live-none-")));
67
+ dirs.push(dir);
68
+ expect(await LiveRun.open({ cwd: dir, env: "local", op: "x", id: "r", started: "now" })).toBeUndefined();
69
+ expect(liveRunEnv()).toEqual({});
70
+ expect(() => reportRunActivity("nothing")).not.toThrow();
71
+ });
72
+
73
+ test("a run hands its steps the activity file, and removes it once its ledger record is written", async () => {
74
+ const dir = checkout();
75
+ let seen: Record<string, string> = {};
76
+ let during: Awaited<ReturnType<typeof readInFlightRun>> = null;
77
+ const op = {
78
+ name: "build",
79
+ overview: "build",
80
+ phases: [
81
+ {
82
+ name: "Build",
83
+ steps: [{ kind: "activity", fn: "work", id: "build", args: {} }],
84
+ },
85
+ ],
86
+ } as unknown as OpConfig;
87
+ const activities = new Map([
88
+ [
89
+ "work",
90
+ async () => {
91
+ seen = liveRunEnv();
92
+ reportRunActivity("Edit kit/lobby/box-side.mjs\n");
93
+ during = await readInFlightRun(dir, "local", "build");
94
+ return {};
95
+ },
96
+ ],
97
+ ]);
98
+ const result = await runOpLocally(op, activities as never, {}, undefined, { runId: "r2", ledger: { cwd: dir }, cwd: dir });
99
+ expect(result.status).toBe("ok");
100
+ expect(seen[RUN_ACTIVITY_ENV]).toMatch(/local__build\.activity\.jsonl$/);
101
+ expect(during).toMatchObject({ id: "r2", phase: { name: "Build" }, step: { name: "build" } });
102
+ expect(during!.activity.lines.map((l) => l.text)).toEqual(["Edit kit/lobby/box-side.mjs"]);
103
+ expect(existsSync(seen[RUN_ACTIVITY_ENV])).toBe(false);
104
+ const { records } = await readRunLedger("local", "build", { cwd: dir });
105
+ expect(records.at(-1)!.phases[0]).toMatchObject({ name: "Build", status: "ok", durationMs: expect.any(Number) });
106
+ });
107
+
108
+ test("withLiveRun without a run just calls through", async () => {
109
+ expect(await withLiveRun(undefined, async () => liveRunEnv())).toEqual({});
110
+ });
111
+ });