@intentius/chant 0.95.1 → 0.96.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.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +9 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/run-live.d.ts +127 -0
- package/dist/op/run-live.d.ts.map +1 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/workspace/checks/diagrams.d.ts +5 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +5 -5
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +50 -7
- package/dist/workspace/intent-cli.d.ts +6 -0
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +145 -0
- package/dist/workspace/intent-record.d.ts.map +1 -0
- package/dist/workspace/intent.d.ts +46 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +2 -2
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/patch.d.ts +122 -0
- package/dist/workspace/patch.d.ts.map +1 -0
- package/dist/workspace/points-cli.d.ts +8 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-decided.d.ts +36 -0
- package/dist/workspace/record-decided.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +8 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +79 -12
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/main.test.ts +16 -0
- package/src/cli/main.ts +34 -3
- package/src/cli/registry.ts +9 -1
- package/src/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/shell.ts +4 -1
- package/src/op/index.ts +2 -0
- package/src/op/local-executor.ts +51 -3
- package/src/op/operator.ts +2 -11
- package/src/op/run-live.test.ts +111 -0
- package/src/op/run-live.ts +305 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/steward-points.test.ts +81 -0
- package/src/workspace/checks/diagrams.test.ts +24 -0
- package/src/workspace/checks/diagrams.ts +10 -3
- package/src/workspace/declaration.schema.json +50 -7
- package/src/workspace/declaration.test.ts +18 -0
- package/src/workspace/declaration.ts +7 -7
- package/src/workspace/intent-cli.ts +47 -1
- package/src/workspace/intent-record.schema.json +529 -0
- package/src/workspace/intent-record.test.ts +257 -0
- package/src/workspace/intent-record.ts +371 -0
- package/src/workspace/intent.schema.json +33 -1
- package/src/workspace/intent.ts +29 -12
- package/src/workspace/ls-contract.test.ts +14 -0
- package/src/workspace/ls.schema.json +3 -3
- package/src/workspace/ls.ts +3 -3
- package/src/workspace/patch.schema.json +321 -0
- package/src/workspace/patch.test.ts +198 -0
- package/src/workspace/patch.ts +377 -0
- package/src/workspace/points-cli.ts +18 -0
- package/src/workspace/read-contract.test.ts +29 -1
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-decided.ts +107 -0
- package/src/workspace/records-cli.ts +12 -0
- package/src/workspace/records.schema.json +15 -0
- package/src/workspace/status-contract.test.ts +76 -2
- package/src/workspace/status-stewards.ts +149 -27
- 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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* run
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
67
|
-
|
|
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
|
|
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
|
|
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":"AAAA
|
|
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
package/src/cli/main.test.ts
CHANGED
|
@@ -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
|
-
|
|
382
|
-
if (
|
|
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") },
|
package/src/cli/registry.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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,
|
package/src/op/local-executor.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/src/op/operator.ts
CHANGED
|
@@ -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
|
-
|
|
346
|
-
|
|
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
|
+
});
|