@intentius/chant 0.93.0 → 0.95.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/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +14 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/operator.d.ts +90 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts +84 -0
- package/dist/op/steward-beside.d.ts.map +1 -0
- package/dist/op/steward.d.ts +87 -2
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/box-intent.d.ts +85 -0
- package/dist/workspace/box-intent.d.ts.map +1 -0
- package/dist/workspace/box-services.d.ts +31 -0
- package/dist/workspace/box-services.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +5 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts +14 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +11 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +5 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +43 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +62 -1
- package/dist/workspace/graph-cache.d.ts +168 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -5
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +5 -5
- package/dist/workspace/kind-readers.d.ts +39 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +29 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +15 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-run.d.ts +2 -0
- package/dist/workspace/member-run.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +3 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +5 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +10 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +4 -2
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +8 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +31 -9
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +20 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +26 -0
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +7 -3
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +97 -0
- package/src/cli/handlers/operator.ts +53 -11
- package/src/cli/handlers/run.test.ts +71 -0
- package/src/cli/handlers/run.ts +65 -7
- package/src/cli/main.ts +21 -9
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/registry.ts +2 -0
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- package/src/cli/static-config-read.test.ts +8 -2
- package/src/meta/source-is-text.test.ts +21 -3
- package/src/okf.test.ts +6 -1
- package/src/op/activities/decide.test.ts +8 -0
- package/src/op/builders.ts +14 -3
- package/src/op/index.ts +8 -2
- package/src/op/operator.ts +264 -16
- package/src/op/steward-beside.test.ts +267 -0
- package/src/op/steward-beside.ts +219 -0
- package/src/op/steward-points.test.ts +112 -1
- package/src/op/steward.ts +135 -3
- package/src/workspace/box-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -0
- package/src/workspace/box-services.test.ts +129 -0
- package/src/workspace/box-services.ts +51 -0
- package/src/workspace/chant-migrations.ts +5 -0
- package/src/workspace/check.schema.json +7 -3
- package/src/workspace/checks/boxes.test.ts +3 -1
- package/src/workspace/checks/boxes.ts +66 -0
- package/src/workspace/checks.ts +12 -1
- package/src/workspace/compose-graph.test.ts +1 -0
- package/src/workspace/compose-graph.ts +11 -0
- package/src/workspace/composites.test.ts +1 -1
- package/src/workspace/composites.ts +12 -5
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -2
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +62 -1
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +26 -0
- package/src/workspace/graph-cache.test.ts +343 -0
- package/src/workspace/graph-cache.ts +409 -0
- package/src/workspace/graph-cli.ts +93 -26
- package/src/workspace/graph-contract.test.ts +129 -6
- package/src/workspace/graph.schema.json +21 -1
- package/src/workspace/intent-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/kind-readers.e2e.test.ts +68 -0
- package/src/workspace/kind-readers.test.ts +134 -0
- package/src/workspace/kind-readers.ts +111 -0
- package/src/workspace/kinds.test.ts +49 -0
- package/src/workspace/kinds.ts +59 -2
- package/src/workspace/member-commands.test.ts +15 -0
- package/src/workspace/member-commands.ts +47 -7
- package/src/workspace/member-run.ts +10 -2
- package/src/workspace/points-cli.ts +3 -0
- package/src/workspace/points.schema.json +18 -0
- package/src/workspace/points.test.ts +15 -14
- package/src/workspace/points.ts +4 -16
- package/src/workspace/read-contract.test.ts +8 -0
- package/src/workspace/reason-codes.test.ts +7 -7
- package/src/workspace/reason-codes.ts +6 -1
- package/src/workspace/records-amend.schema.json +1 -0
- package/src/workspace/records-cli.ts +34 -13
- package/src/workspace/records-contract.test.ts +2 -1
- package/src/workspace/records-formats.test.ts +15 -15
- package/src/workspace/records-new.schema.json +1 -0
- package/src/workspace/records-quorum.test.ts +10 -3
- package/src/workspace/records-sessions-write.test.ts +2 -1
- package/src/workspace/records-since.test.ts +8 -7
- package/src/workspace/records-write.test.ts +101 -1
- package/src/workspace/records-write.ts +50 -6
- package/src/workspace/records.test.ts +1 -1
- package/src/workspace/records.ts +22 -5
- package/src/workspace/status-contract.test.ts +32 -1
- package/src/workspace/status-stewards.ts +88 -9
- package/src/workspace/status.schema.json +66 -4
- package/src/workspace/status.ts +32 -0
- package/src/workspace/trust/record-seal.test.ts +26 -2
- package/src/workspace/work-evidence.schema.json +1 -0
- package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +17 -0
- package/src/workspace/workspace-kinds.schema.json +26 -0
|
@@ -11,6 +11,7 @@ const discoverOpsMock = vi.fn();
|
|
|
11
11
|
// default is what every other call sees.
|
|
12
12
|
const discoverStewardsMock = vi.fn().mockResolvedValue({ stewards: new Map(), errors: [], conflicts: [] });
|
|
13
13
|
const acquireStewardTurnMock = vi.fn();
|
|
14
|
+
const holdBesideLeaseMock = vi.fn();
|
|
14
15
|
const releaseLeaseMock = vi.fn();
|
|
15
16
|
const loadChantConfigMock = vi.fn();
|
|
16
17
|
const writeFileSyncMock = vi.fn();
|
|
@@ -33,6 +34,10 @@ vi.mock("../../op/operator", async (importOriginal) => {
|
|
|
33
34
|
const actual = await importOriginal<typeof import("../../op/operator")>();
|
|
34
35
|
return { ...actual, acquireStewardTurn: (...args: unknown[]) => acquireStewardTurnMock(...args) };
|
|
35
36
|
});
|
|
37
|
+
vi.mock("../../op/steward-beside", async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal<typeof import("../../op/steward-beside")>();
|
|
39
|
+
return { ...actual, holdBesideLease: (...args: unknown[]) => holdBesideLeaseMock(...args) };
|
|
40
|
+
});
|
|
36
41
|
vi.mock("../../lifecycle/lease", async (importOriginal) => {
|
|
37
42
|
const actual = await importOriginal<typeof import("../../lifecycle/lease")>();
|
|
38
43
|
return { ...actual, releaseLease: (...args: unknown[]) => releaseLeaseMock(...args) };
|
|
@@ -438,6 +443,72 @@ describe("runOp: a steward's turn (#2750)", () => {
|
|
|
438
443
|
});
|
|
439
444
|
});
|
|
440
445
|
|
|
446
|
+
/**
|
|
447
|
+
* chant #2861: `chant run <op>` for an Op a steward runs beside its turns
|
|
448
|
+
* takes the Op's own lease, as `--holder`, and never the steward's turn.
|
|
449
|
+
* `holdBesideLease` is stubbed; `op/steward-beside.test.ts` runs it against a
|
|
450
|
+
* real repo.
|
|
451
|
+
*/
|
|
452
|
+
describe("runOp: an Op beside a steward's turns (#2861)", () => {
|
|
453
|
+
function stewardBeside(opName: string, name: string) {
|
|
454
|
+
return {
|
|
455
|
+
stewards: new Map([[name, {
|
|
456
|
+
declaration: {
|
|
457
|
+
kind: "Chant::Steward", name, ops: [{ name: "converge", phases: [] }, { name: opName, phases: [] }],
|
|
458
|
+
beside: [{ op: opName, ready: null }],
|
|
459
|
+
form: { default: "local", environments: {} }, capabilities: [], vault: null,
|
|
460
|
+
},
|
|
461
|
+
filePath: "ops/steward.op.ts", exportName: "steward",
|
|
462
|
+
}]]),
|
|
463
|
+
errors: [], conflicts: [],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
beforeEach(() => {
|
|
468
|
+
discoverOpsMock.mockReset();
|
|
469
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
470
|
+
loadPluginsMock.mockReset().mockResolvedValue([]);
|
|
471
|
+
acquireStewardTurnMock.mockReset();
|
|
472
|
+
holdBesideLeaseMock.mockReset();
|
|
473
|
+
releaseLeaseMock.mockReset().mockResolvedValue(true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("takes the Op's lease as --holder, not the turn, runs, and releases it once the run is over", async () => {
|
|
477
|
+
discoverOpsMock.mockResolvedValue({
|
|
478
|
+
ops: new Map([localOp("dispatch", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
479
|
+
errors: [],
|
|
480
|
+
});
|
|
481
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardBeside("dispatch", "box-steward"));
|
|
482
|
+
const release = vi.fn(async () => {});
|
|
483
|
+
holdBesideLeaseMock.mockResolvedValue({ acquired: true, lease: { op: "dispatch", holder: "h", token: "t1", acquiredAt: "x", expiresAt: "y" }, release });
|
|
484
|
+
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
485
|
+
const exit = await runOp({ args: makeArgs({ path: "dispatch", holder: "box-steward/dispatch@op1" }), plugins: [], serializers: [] });
|
|
486
|
+
stderrWrite.mockRestore();
|
|
487
|
+
|
|
488
|
+
expect(exit).toBe(0);
|
|
489
|
+
expect(acquireStewardTurnMock).not.toHaveBeenCalled();
|
|
490
|
+
expect(holdBesideLeaseMock).toHaveBeenCalledWith("dispatch", "box-steward/dispatch@op1");
|
|
491
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("a run of it in progress: refused, naming the holder, nothing runs", async () => {
|
|
495
|
+
discoverOpsMock.mockResolvedValue({
|
|
496
|
+
ops: new Map([localOp("dispatch", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
497
|
+
errors: [],
|
|
498
|
+
});
|
|
499
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardBeside("dispatch", "box-steward"));
|
|
500
|
+
holdBesideLeaseMock.mockResolvedValue({ acquired: false, heldBy: "box-steward/dispatch@op1" });
|
|
501
|
+
const stderr = makeStderrSpy();
|
|
502
|
+
const exit = await runOp({ args: makeArgs({ path: "dispatch" }), plugins: [], serializers: [] });
|
|
503
|
+
|
|
504
|
+
expect(exit).toBe(1);
|
|
505
|
+
const out = stderr.join("\n");
|
|
506
|
+
expect(out).toContain(`runs beside steward "box-steward"'s turns`);
|
|
507
|
+
expect(out).toContain("box-steward/dispatch@op1");
|
|
508
|
+
expect(acquireStewardTurnMock).not.toHaveBeenCalled();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
441
512
|
/**
|
|
442
513
|
* chant #2243 — a gated run's exit code is the one thing `--gated-exit`
|
|
443
514
|
* remaps, and the gate is reported where CI can see it without opening a log.
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -17,7 +17,8 @@ import { recordGateApproval } from "./operator";
|
|
|
17
17
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
18
18
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
19
19
|
import type { CommandContext } from "../registry";
|
|
20
|
-
import { stewardFormFor, stewardTurnLeaseName, DEFAULT_STEWARD_ENV, type StewardDeclaration } from "../../op/steward";
|
|
20
|
+
import { stewardBesideFor, stewardFormFor, stewardTurnLeaseName, DEFAULT_STEWARD_ENV, type StewardDeclaration } from "../../op/steward";
|
|
21
|
+
import { holdBesideLease } from "../../op/steward-beside";
|
|
21
22
|
import { acquireStewardTurn, STEWARD_TURN_WAIT_MS } from "../../op/operator";
|
|
22
23
|
import { releaseLease, currentHolderId, type AcquireLeaseResult } from "../../lifecycle/lease";
|
|
23
24
|
import { StaleLockError } from "../../lifecycle/git";
|
|
@@ -557,6 +558,12 @@ type StewardTurnGate =
|
|
|
557
558
|
* hand. Ctrl-C already means "abort my own run" here; it must not also mean
|
|
558
559
|
* "give up waiting for someone else's turn".
|
|
559
560
|
*
|
|
561
|
+
* An Op the steward runs beside its turns (#2861) takes the Op's own lease
|
|
562
|
+
* instead, renewed for as long as the run lasts, and never the turn: the run
|
|
563
|
+
* is one at a time with the runs the operator starts of it, and holds none
|
|
564
|
+
* of the steward's other Ops up. Held already, it refuses at once, naming
|
|
565
|
+
* the holder.
|
|
566
|
+
*
|
|
560
567
|
* `undefined` (via the caller checking `owner`) when no steward lists this
|
|
561
568
|
* op — the ordinary, untouched path.
|
|
562
569
|
*/
|
|
@@ -585,6 +592,8 @@ async function stewardTurnGate(opName: string, ctx: CommandContext): Promise<Ste
|
|
|
585
592
|
};
|
|
586
593
|
}
|
|
587
594
|
|
|
595
|
+
if (stewardBesideFor(owner, opName)) return besideLeaseGate(opName, owner.name, ctx);
|
|
596
|
+
|
|
588
597
|
const holder = currentHolderId();
|
|
589
598
|
let turn: AcquireLeaseResult;
|
|
590
599
|
try {
|
|
@@ -619,6 +628,36 @@ async function stewardTurnGate(opName: string, ctx: CommandContext): Promise<Ste
|
|
|
619
628
|
return { ok: true, release: async () => { await releaseLease(stewardTurnLeaseName(stewardName), holder, lease.token).catch(() => false); } };
|
|
620
629
|
}
|
|
621
630
|
|
|
631
|
+
/**
|
|
632
|
+
* The lease a `chant run` of an Op beside a steward's turns holds (#2861):
|
|
633
|
+
* the Op's own, as `--holder` (the operator passes `<steward>/<op>@<its
|
|
634
|
+
* holder>`) or this process.
|
|
635
|
+
*/
|
|
636
|
+
async function besideLeaseGate(opName: string, steward: string, ctx: CommandContext): Promise<StewardTurnGate> {
|
|
637
|
+
const holder = ctx.args.holder ?? currentHolderId();
|
|
638
|
+
let held;
|
|
639
|
+
try {
|
|
640
|
+
held = await holdBesideLease(opName, holder);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
return {
|
|
643
|
+
ok: false,
|
|
644
|
+
message: `Op "${opName}" runs beside steward "${steward}"'s turns, and its lease could not be read`,
|
|
645
|
+
hint:
|
|
646
|
+
`${err instanceof StaleLockError ? err.message : err instanceof Error ? err.message : String(err)} ` +
|
|
647
|
+
`This can happen when a process was killed mid-write; check refs/chant/lease/${opName} for a stale lock.`,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (!held.acquired) {
|
|
651
|
+
return {
|
|
652
|
+
ok: false,
|
|
653
|
+
message: `Op "${opName}" runs beside steward "${steward}"'s turns, one run at a time, and a run of it is in progress` +
|
|
654
|
+
(held.heldBy ? ` (held by ${held.heldBy})` : ""),
|
|
655
|
+
hint: `Wait for that run to end and re-run \`chant run ${opName}\`; \`chant workspace status --json\` lists it under the steward. Its lease expires on its own if the process holding it died.`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
return { ok: true, release: held.release };
|
|
659
|
+
}
|
|
660
|
+
|
|
622
661
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
623
662
|
if (ctx.args.generate) {
|
|
624
663
|
if (ctx.args.components) {
|
|
@@ -975,20 +1014,35 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
975
1014
|
// runs under; it is taken where the run executes, which for a hosted
|
|
976
1015
|
// runtime is not this process.
|
|
977
1016
|
const work = await resolveRunWork(ctx, config, runtime.name);
|
|
978
|
-
if (work === null)
|
|
1017
|
+
if (work === null) {
|
|
1018
|
+
await stewardGate?.release?.();
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
979
1021
|
|
|
980
1022
|
// `--progress-json` streams one NDJSON StepRecord per settled step, fed by
|
|
981
1023
|
// whatever the runtime reports through `progress`.
|
|
982
1024
|
const progress = ctx.args.progressJson ? ndjsonProgressSink<StepRecord>() : undefined;
|
|
983
1025
|
|
|
984
|
-
// Ctrl-C
|
|
985
|
-
//
|
|
1026
|
+
// Ctrl-C, or a SIGTERM (the signal a steward's operator sends this process
|
|
1027
|
+
// when it runs beside the steward's turns and the operator itself stops,
|
|
1028
|
+
// and what a plain `kill <pid>` sends with no signal named), aborts
|
|
1029
|
+
// in-flight activities (kills their child processes) instead of orphaning
|
|
1030
|
+
// them. `process.on`, not `.once` (chant#2872's shape, here too): a second
|
|
1031
|
+
// signal arriving before the `finally` below has released this run's lease
|
|
1032
|
+
// (`stewardGate`, for a beside run) must not fall through to Node's default
|
|
1033
|
+
// disposition and skip that release. `stopping` makes a repeat a no-op; the
|
|
1034
|
+
// listeners are removed only after the release, at the very end of
|
|
1035
|
+
// `finally`, not before it.
|
|
986
1036
|
const controller = new AbortController();
|
|
1037
|
+
let stopping = false;
|
|
987
1038
|
const onSigint = () => {
|
|
1039
|
+
if (stopping) return;
|
|
1040
|
+
stopping = true;
|
|
988
1041
|
console.error(formatWarning({ message: "interrupted — stopping Op" }));
|
|
989
1042
|
controller.abort();
|
|
990
1043
|
};
|
|
991
|
-
process.
|
|
1044
|
+
process.on("SIGINT", onSigint);
|
|
1045
|
+
process.on("SIGTERM", onSigint);
|
|
992
1046
|
|
|
993
1047
|
try {
|
|
994
1048
|
const handle = await runtime.start(config, {
|
|
@@ -1065,10 +1119,14 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
1065
1119
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
1066
1120
|
return 1;
|
|
1067
1121
|
} finally {
|
|
1068
|
-
process.removeListener("SIGINT", onSigint);
|
|
1069
1122
|
// The turn is over, win or lose — free it so the steward's next round,
|
|
1070
|
-
// or the next hand run, doesn't wait out this run's own TTL (#2750).
|
|
1123
|
+
// or the next hand run, doesn't wait out this run's own TTL (#2750). The
|
|
1124
|
+
// listeners stay registered until after this release (chant#2872): it is
|
|
1125
|
+
// itself async, and removing them first would let a repeat signal in
|
|
1126
|
+
// that window fall through to Node's default disposition and skip it.
|
|
1071
1127
|
await stewardGate?.release?.();
|
|
1128
|
+
process.removeListener("SIGINT", onSigint);
|
|
1129
|
+
process.removeListener("SIGTERM", onSigint);
|
|
1072
1130
|
}
|
|
1073
1131
|
}
|
|
1074
1132
|
|
package/src/cli/main.ts
CHANGED
|
@@ -90,6 +90,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
90
90
|
"--no-release-record",
|
|
91
91
|
"--fold",
|
|
92
92
|
"--no-fold",
|
|
93
|
+
"--no-cache",
|
|
93
94
|
"--sandbox",
|
|
94
95
|
"--yes",
|
|
95
96
|
"--confirm-prod",
|
|
@@ -481,6 +482,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
481
482
|
result.noReleaseRecord = true;
|
|
482
483
|
} else if (arg === "--fold") {
|
|
483
484
|
result.fold = true;
|
|
485
|
+
} else if (arg === "--no-cache") {
|
|
486
|
+
// `chant workspace graph --no-cache` (#2876): read every member.
|
|
487
|
+
result.noCache = true;
|
|
484
488
|
} else if (arg === "--no-fold") {
|
|
485
489
|
// chant #1134 — fold is the default build path; this is the explicit
|
|
486
490
|
// opt-out, and like --fold it beats chant.config.ts's build.fold.
|
|
@@ -748,7 +752,9 @@ Ops:
|
|
|
748
752
|
CronJob invokers use this instead of the daemon).
|
|
749
753
|
--steward [<name>] runs a declared steward's local
|
|
750
754
|
form instead (#2731): its scheduled Ops on their
|
|
751
|
-
crons, under the steward's own lease
|
|
755
|
+
crons, under the steward's own lease, and its
|
|
756
|
+
beside Ops as chant run processes of their own
|
|
757
|
+
(#2861)
|
|
752
758
|
operator status Last tick, outcomes, and pending gates per
|
|
753
759
|
ConvergeOp, read from the chant/lifecycle orphan
|
|
754
760
|
branch alone — no daemon needs to be running
|
|
@@ -841,10 +847,12 @@ Workspace (level 1, #2524):
|
|
|
841
847
|
Set top-level fields of one record. A closed record
|
|
842
848
|
never changes, and an approved one changes only its
|
|
843
849
|
state (upward), pins and reviews; anything else is
|
|
844
|
-
refused with amend-supersede-instead.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
850
|
+
refused with amend-supersede-instead. Moving a
|
|
851
|
+
record to its kind's ratified state (ratified for
|
|
852
|
+
decisions) is refused with ratify-quorum-not-met
|
|
853
|
+
until its quorum is met. --sign seals the author
|
|
854
|
+
again; without it an amendment removes the author
|
|
855
|
+
seal and says so. Prints {path, id, changed}
|
|
848
856
|
workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--sign [<key file>]] [--dry-run]
|
|
849
857
|
Append a review to one record, dated and bound to
|
|
850
858
|
the digest of the record text. A dissent needs
|
|
@@ -931,7 +939,10 @@ Workspace (level 1, #2524):
|
|
|
931
939
|
with <member>/<id> ids and groups.byMember: the
|
|
932
940
|
read-contract document. --at <rev> runs each member's
|
|
933
941
|
source as it was at that commit; --kind adds the
|
|
934
|
-
records' asset and constrains links
|
|
942
|
+
records' asset and constrains links. A member whose
|
|
943
|
+
source and toolchain are unchanged is served from
|
|
944
|
+
the cache in $CHANT_CACHE_DIR or ~/.cache/chant;
|
|
945
|
+
--no-cache reads every member
|
|
935
946
|
workspace graph --composites [--at <rev>] [--member <name>] [-o <file>]
|
|
936
947
|
Each composite instance the members declare, with the
|
|
937
948
|
components whose contract can deploy it; an instance
|
|
@@ -1284,9 +1295,10 @@ async function loadPluginsOrExit(path: string): Promise<import("../lexicon").Lex
|
|
|
1284
1295
|
* #2700 — whether `path` is the root of a declared workspace and holds no
|
|
1285
1296
|
* lexicon of its own: no `lexicons` in a root config, and no lexicon import in
|
|
1286
1297
|
* the root's source, which leaves the members' directories out (#2527). A
|
|
1287
|
-
* generated
|
|
1288
|
-
* `chant serve mcp` starts there instead of
|
|
1289
|
-
* detected", and serves core with the chant
|
|
1298
|
+
* generated app repo can be this shape: its lexicons are all under a
|
|
1299
|
+
* delivery directory, not the root. `chant serve mcp` starts there instead of
|
|
1300
|
+
* refusing with "No lexicon detected", and serves core with the chant
|
|
1301
|
+
* members' lexicons.
|
|
1290
1302
|
*
|
|
1291
1303
|
* A directory with no workspace declaration costs only `findWorkspaceRoot`'s
|
|
1292
1304
|
* existence checks, so a level-0 project reaches `loadPluginsOrExit` as before.
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* #2700 — the lexicons `chant serve mcp` loads at the root of a declared
|
|
3
3
|
* workspace that has no lexicon of its own.
|
|
4
4
|
*
|
|
5
|
-
* Such a root (a generated
|
|
6
|
-
*
|
|
7
|
-
* there got none of chant's tools. The server now starts with core's
|
|
8
|
-
* resources, and with the lexicons of the workspace's members of
|
|
9
|
-
* each read from that member's own config the way `chant build`
|
|
10
|
-
* member's directory reads it.
|
|
5
|
+
* Such a root (a generated app repo, say, whose lexicons are all under a
|
|
6
|
+
* delivery directory) used to refuse with "No lexicon detected", so an agent
|
|
7
|
+
* started there got none of chant's tools. The server now starts with core's
|
|
8
|
+
* tools and resources, and with the lexicons of the workspace's members of
|
|
9
|
+
* kind chant, each read from that member's own config the way `chant build`
|
|
10
|
+
* in the member's directory reads it.
|
|
11
11
|
*
|
|
12
12
|
* Loading is best effort, one member and one lexicon at a time: a member whose
|
|
13
13
|
* config does not load, or a lexicon this chant cannot import, is left out and
|
|
@@ -195,7 +195,7 @@ export const workspaceWriteTools: ToolDefinition[] = [
|
|
|
195
195
|
{
|
|
196
196
|
name: "records-amend",
|
|
197
197
|
description:
|
|
198
|
-
"Set top-level fields of a record: chant workspace records amend. A closed record never changes, and an approved one changes only in its state, evidence and reviews: anything else, its reasoning included, is a new record that supersedes it. A source block given in the fields records that the change came through MCP. " +
|
|
198
|
+
"Set top-level fields of a record: chant workspace records amend. A closed record never changes, and an approved one changes only in its state, evidence and reviews: anything else, its reasoning included, is a new record that supersedes it. A record moves to its kind's ratified state only once its review quorum is met (ratify-quorum-not-met). A source block given in the fields records that the change came through MCP. " +
|
|
199
199
|
PROTOCOL,
|
|
200
200
|
inputSchema: {
|
|
201
201
|
type: "object",
|
package/src/cli/registry.ts
CHANGED
|
@@ -299,6 +299,8 @@ export interface ParsedArgs {
|
|
|
299
299
|
subject?: string;
|
|
300
300
|
/** `chant workspace points answer <id> --answer <value>` (#2739): the people's answer. */
|
|
301
301
|
answer?: string;
|
|
302
|
+
/** `chant workspace graph --no-cache` (#2876): read every member, bypassing the per-member cache. */
|
|
303
|
+
noCache?: boolean;
|
|
302
304
|
/** `chant workspace graph --composites` (#2662): print each composite instance with the components that can deploy it. */
|
|
303
305
|
composites?: boolean;
|
|
304
306
|
/** `chant workspace graph --intent <path[:start-end]>` (#2651): the region the intent graph is over. */
|
|
@@ -73,8 +73,8 @@ describe("chant serve mcp at a workspace root with no lexicon of its own (#2700)
|
|
|
73
73
|
expect(init.instructions).toContain("Member delivery (delivery/) declares docker.");
|
|
74
74
|
}, TIMEOUT);
|
|
75
75
|
|
|
76
|
-
test("a
|
|
77
|
-
const root = join(scratch, "
|
|
76
|
+
test("a lexiconless root, lexicons only in a member, serves core and the lexicons that load", () => {
|
|
77
|
+
const root = join(scratch, "lexiconless-shaped");
|
|
78
78
|
mkdirSync(join(root, ".git"), { recursive: true });
|
|
79
79
|
mkdirSync(join(root, "delivery"), { recursive: true });
|
|
80
80
|
mkdirSync(join(root, "app"), { recursive: true });
|
|
@@ -87,9 +87,9 @@ describe("chant serve mcp at a workspace root with no lexicon of its own (#2700)
|
|
|
87
87
|
],
|
|
88
88
|
pins: [],
|
|
89
89
|
}));
|
|
90
|
-
// `
|
|
91
|
-
//
|
|
92
|
-
writeFileSync(join(root, "delivery", "chant.config.ts"), 'export default { lexicons: ["docker", "
|
|
90
|
+
// `acme` names a lexicon package that isn't installed here: it is named
|
|
91
|
+
// as not loaded and the rest is served.
|
|
92
|
+
writeFileSync(join(root, "delivery", "chant.config.ts"), 'export default { lexicons: ["docker", "acme"] };\n');
|
|
93
93
|
writeFileSync(join(root, "app", "server.js"), "// not chant\n");
|
|
94
94
|
|
|
95
95
|
const { status, stderr, init, tools } = initializeAndList(root);
|
|
@@ -97,7 +97,7 @@ describe("chant serve mcp at a workspace root with no lexicon of its own (#2700)
|
|
|
97
97
|
for (const name of CORE_TOOLS) expect(tools).toContain(name);
|
|
98
98
|
expect(tools).toContain("docker:diff");
|
|
99
99
|
expect(init.instructions).toContain('workspace "t"');
|
|
100
|
-
expect(init.instructions).toContain("Lexicon
|
|
100
|
+
expect(init.instructions).toContain("Lexicon acme did not load");
|
|
101
101
|
expect(init.instructions).toContain("Lexicon tools and resources served: docker.");
|
|
102
102
|
}, TIMEOUT);
|
|
103
103
|
|
|
@@ -72,7 +72,9 @@ afterEach(() => {
|
|
|
72
72
|
function chant(args: string[], cwd: string): string {
|
|
73
73
|
const result = spawnSync(process.execPath, ["--import", tsxLoader, mainTs, ...args], {
|
|
74
74
|
cwd,
|
|
75
|
-
|
|
75
|
+
// TMPDIR passes through so tsx caches under the run's temp directory
|
|
76
|
+
// (chant#2864), not the machine-wide default this scrubbed env would give.
|
|
77
|
+
env: { PATH: process.env.PATH ?? "", HOME: home, TMPDIR: tmpdir() },
|
|
76
78
|
encoding: "utf-8",
|
|
77
79
|
input: "n\n",
|
|
78
80
|
timeout: 60_000,
|
|
@@ -105,7 +107,11 @@ describe("commands that read path lexicons statically run no project code", () =
|
|
|
105
107
|
|
|
106
108
|
test("a config it cannot read is reported, and still not run", () => {
|
|
107
109
|
writeConfig(`process.env.CI ? ["github"] : [{ name: "github", module: "./gh/index.ts" }]`);
|
|
108
|
-
|
|
110
|
+
// `import --agents` rather than `dev onboard`: with the config unread,
|
|
111
|
+
// github is treated as a package, and onboard then patches the chant
|
|
112
|
+
// repo it runs from (its CI workflow, chant#2863). Both commands print
|
|
113
|
+
// the notice from the same recordProjectLexicons read.
|
|
114
|
+
const output = chant(["import", "--agents", "--lexicon", "github", "--output", join(root, "out")], project);
|
|
109
115
|
expect(output).toContain("could not read the lexicons in");
|
|
110
116
|
expect(output).toContain("treated as a package");
|
|
111
117
|
expect(existsSync(marker)).toBe(false);
|
|
@@ -27,11 +27,28 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
*/
|
|
28
28
|
const SRC = fileURLToPath(new URL("../", import.meta.url));
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Run `read`, or return undefined when the path is gone. Other test files
|
|
32
|
+
* create and delete fixtures under src while this one walks it
|
|
33
|
+
* (lint/config.test.ts's `__test_config__`, chant#2870), so an entry listed a
|
|
34
|
+
* moment ago may no longer exist.
|
|
35
|
+
*/
|
|
36
|
+
function unlessGone<T>(read: () => T): T | undefined {
|
|
37
|
+
try {
|
|
38
|
+
return read();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/** Every TypeScript file under core's src, tests and fixtures included. */
|
|
31
46
|
function sourceFiles(dir: string): string[] {
|
|
32
|
-
return readdirSync(dir).flatMap((entry) => {
|
|
47
|
+
return (unlessGone(() => readdirSync(dir)) ?? []).flatMap((entry) => {
|
|
33
48
|
const path = join(dir, entry);
|
|
34
|
-
|
|
49
|
+
const stat = unlessGone(() => statSync(path));
|
|
50
|
+
if (stat === undefined) return [];
|
|
51
|
+
if (stat.isDirectory()) return entry === "node_modules" ? [] : sourceFiles(path);
|
|
35
52
|
return /\.(ts|mts|cts)$/.test(entry) ? [path] : [];
|
|
36
53
|
});
|
|
37
54
|
}
|
|
@@ -40,7 +57,8 @@ describe("source files are text, not binary (chant#2280)", () => {
|
|
|
40
57
|
test("no shipped source file contains a NUL byte", () => {
|
|
41
58
|
const offenders: string[] = [];
|
|
42
59
|
for (const file of sourceFiles(SRC)) {
|
|
43
|
-
const buf = readFileSync(file);
|
|
60
|
+
const buf = unlessGone(() => readFileSync(file));
|
|
61
|
+
if (buf === undefined) continue;
|
|
44
62
|
const at = buf.indexOf(0);
|
|
45
63
|
if (at !== -1) offenders.push(`${relative(SRC, file)} (first at byte ${at})`);
|
|
46
64
|
}
|
package/src/okf.test.ts
CHANGED
|
@@ -155,7 +155,12 @@ const exampleCases: Array<[string, string]> = [
|
|
|
155
155
|
|
|
156
156
|
describe("buildOkfBundle over shipped examples", () => {
|
|
157
157
|
for (const [name, rel] of exampleCases) {
|
|
158
|
-
test
|
|
158
|
+
// The test's own `expect` names the snapshot after this test. The global
|
|
159
|
+
// one names it after whichever test is current, so when a slow discover
|
|
160
|
+
// outlives its timeout and the next case starts, a late call records
|
|
161
|
+
// under the next case's name and the checkout gains a "... 2" snapshot
|
|
162
|
+
// (chant#2863).
|
|
163
|
+
test(`${name} emits a conformant, snapshot-stable bundle`, async ({ expect }) => {
|
|
159
164
|
const projectPath = resolve(repoRoot, rel);
|
|
160
165
|
const result = await discover(projectPath);
|
|
161
166
|
expect(result.errors).toEqual([]);
|
|
@@ -65,6 +65,14 @@ afterAll(async () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
describe("decide against the stub (#2740)", () => {
|
|
68
|
+
test("points --json carries each point's criteria, as the points file declares it (#2853)", async () => {
|
|
69
|
+
const doc = await workspacePoints({ cwd: root });
|
|
70
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
71
|
+
expect(doc.points.find((p) => p.name === "triage")?.criteria).toEqual({ true: "A person looks at it today.", false: "It can wait." });
|
|
72
|
+
expect(doc.points.find((p) => p.name === "route")?.criteria).toEqual({ platform: "The platform team.", app: "The app team.", docs: "The docs team." });
|
|
73
|
+
expect(doc.points.find((p) => p.name === "effort")?.criteria).toEqual(["low", "medium", "high"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
68
76
|
test("a noul: the model's answer at the threshold is recorded as proposed, and the run waits on it", async () => {
|
|
69
77
|
script = { triage: { type: "noul", noul: 0.91 } };
|
|
70
78
|
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 5, "record.risky": true }, subject: "T-1" }));
|
package/src/op/builders.ts
CHANGED
|
@@ -916,10 +916,21 @@ export const spriteApplyNetworkPolicy = (args: {
|
|
|
916
916
|
return activity("spriteApplyNetworkPolicy", rest, profile ?? "fastIdempotent");
|
|
917
917
|
};
|
|
918
918
|
|
|
919
|
-
/**
|
|
919
|
+
/**
|
|
920
|
+
* Reconcile a sprite's background services (create-or-update, optionally
|
|
921
|
+
* start). With an `id`, through the Sprites API and its `services`; without
|
|
922
|
+
* one, inside the sprite through sprite-env, applying the box block's
|
|
923
|
+
* services (`box: true`, #2880): `only` names some of them, `start` starts
|
|
924
|
+
* the applied ones that are not running, `restart` restarts the converged
|
|
925
|
+
* ones. Defaults to the `fastIdempotent` profile (override via `profile`).
|
|
926
|
+
*/
|
|
920
927
|
export const spriteApplyServices = (args: {
|
|
921
|
-
id
|
|
922
|
-
|
|
928
|
+
id?: string;
|
|
929
|
+
box?: boolean;
|
|
930
|
+
only?: string[];
|
|
931
|
+
restart?: boolean;
|
|
932
|
+
spriteEnv?: string;
|
|
933
|
+
services?: Array<{
|
|
923
934
|
name: string;
|
|
924
935
|
cmd: string;
|
|
925
936
|
args?: string[];
|
package/src/op/index.ts
CHANGED
|
@@ -109,15 +109,21 @@ export type {
|
|
|
109
109
|
export {
|
|
110
110
|
discoverConvergeOps, runOperatorRound, runOperatorForever, formatRoundLine,
|
|
111
111
|
formatSignalLine, DEFAULT_OPERATOR_INTERVAL_MS, acquireStewardLease,
|
|
112
|
-
acquireStewardTurn, STEWARD_TURN_WAIT_MS,
|
|
112
|
+
acquireStewardTurn, STEWARD_TURN_WAIT_MS, createBesideState, waitForBesideRuns, stopBesideRuns,
|
|
113
113
|
} from "./operator";
|
|
114
|
+
export type { BesideState } from "./operator";
|
|
115
|
+
export {
|
|
116
|
+
spawnBesideRun, inProcessBesideLauncher, holdBesideLease, askReady, DEFAULT_READY_TIMEOUT_MS,
|
|
117
|
+
} from "./steward-beside";
|
|
118
|
+
export type { BesideStart, BesideExit, BesideHandle, BesideLauncher, BesideWhy, HeldBesideLease, ReadyAnswer } from "./steward-beside";
|
|
114
119
|
export {
|
|
115
120
|
declareSteward, isStewardDeclaration, stewardFormFor, stewardOpConfig, normaliseStewardForm, stewardLeaseName,
|
|
116
|
-
stewardTurnLeaseName,
|
|
121
|
+
stewardTurnLeaseName, stewardBesideOf, stewardBesideFor, stewardTurnOps, readinessKeys,
|
|
117
122
|
STEWARD_KIND, STEWARD_FORMS, STEWARD_NAME_PATTERN, DEFAULT_STEWARD_ENV,
|
|
118
123
|
} from "./steward";
|
|
119
124
|
export type {
|
|
120
125
|
StewardDeclaration, StewardDeclarationConfig, StewardForm, StewardFormSpec, StewardOpInput,
|
|
126
|
+
StewardBeside, StewardBesideInput,
|
|
121
127
|
} from "./steward";
|
|
122
128
|
export { discoverStewards } from "./discover";
|
|
123
129
|
export {
|