@intentius/chant 0.124.24 → 0.124.25
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/main.js +21 -7
- package/dist/cli/mcp/workspace-tools.d.ts +3 -3
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +27 -4
- package/dist/effect-receipt.d.ts +13 -0
- package/dist/effect-receipt.d.ts.map +1 -1
- package/dist/effect-receipt.js +19 -0
- package/dist/lexicon-module.d.ts +7 -1
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +7 -2
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.js +4 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +64 -5
- package/dist/op/receipt-store.d.ts +5 -0
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/receipt-store.js +4 -3
- package/dist/op/step-output-ref.d.ts +26 -18
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/step-output-ref.js +86 -63
- package/dist/wave-runtime.mjs +213 -213
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/agent-cli.js +3 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/dist/workspace/conformance/writer.js +1 -1
- package/dist/workspace/decide.d.ts +51 -9
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decide.js +125 -14
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +20 -11
- package/dist/workspace/point-answer.schema.json +16 -1
- package/dist/workspace/points-cli.d.ts +5 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points-cli.js +16 -7
- package/dist/workspace/points-write.schema.json +39 -4
- package/dist/workspace/points.schema.json +42 -3
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -1
- package/dist/workspace/records-cli.d.ts +12 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +27 -7
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records-write.js +2 -3
- package/dist/workspace/records.d.ts +10 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +16 -3
- package/dist/workspace/records.schema.json +2 -1
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.js +2 -1
- package/dist/workspace/trust/verify.d.ts +22 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/trust/verify.js +94 -14
- package/dist/workspace/write-scope.d.ts +3 -1
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -4
- package/package.json +1 -1
- package/src/cli/compiled-cli.e2e.test.ts +45 -1
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +21 -7
- package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +27 -4
- package/src/effect-receipt.ts +22 -0
- package/src/lexicon-module.ts +9 -2
- package/src/lifecycle/receipt-plan.test.ts +9 -0
- package/src/lifecycle/receipt-plan.ts +4 -1
- package/src/op/effect-step.test.ts +95 -1
- package/src/op/local-executor.test.ts +65 -0
- package/src/op/local-executor.ts +68 -5
- package/src/op/receipt-store.ts +13 -3
- package/src/op/step-output-ref.test.ts +68 -24
- package/src/op/step-output-ref.ts +85 -65
- package/src/workspace/agent-cli.ts +2 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/src/workspace/conformance/writer.ts +1 -1
- package/src/workspace/decide.test.ts +87 -3
- package/src/workspace/decide.ts +142 -17
- package/src/workspace/lineage-upgrade.test.ts +21 -0
- package/src/workspace/lineage-upgrade.ts +19 -11
- package/src/workspace/point-answer.schema.json +16 -1
- package/src/workspace/points-cli.ts +16 -8
- package/src/workspace/points-write.schema.json +39 -4
- package/src/workspace/points.schema.json +42 -3
- package/src/workspace/reason-codes.ts +2 -1
- package/src/workspace/record-assets.test.ts +14 -0
- package/src/workspace/records-cli.ts +28 -7
- package/src/workspace/records-write-commands.e2e.test.ts +32 -0
- package/src/workspace/records-write.ts +2 -3
- package/src/workspace/records.schema.json +2 -1
- package/src/workspace/records.ts +17 -4
- package/src/workspace/trust/trust.test.ts +105 -0
- package/src/workspace/trust/verify-cli.ts +2 -1
- package/src/workspace/trust/verify.ts +106 -13
- package/src/workspace/write-scope.test.ts +18 -0
- package/src/workspace/write-scope.ts +16 -5
|
@@ -131,8 +131,9 @@ export const SEAL_REASON_CODES = [
|
|
|
131
131
|
"seal-unverifiable",
|
|
132
132
|
];
|
|
133
133
|
/**
|
|
134
|
-
* A warning
|
|
135
|
-
*
|
|
134
|
+
* A warning `records` raises with the policy at base, never by a write: about
|
|
135
|
+
* a record's own seal (#2688), or a ratified record's quorum (#2878). Closed,
|
|
136
|
+
* like the reason codes.
|
|
136
137
|
*/
|
|
137
138
|
export const SEAL_WARNING_CODES = [
|
|
138
139
|
/**
|
|
@@ -142,6 +143,13 @@ export const SEAL_WARNING_CODES = [
|
|
|
142
143
|
* fails. The record is still read, and still valid.
|
|
143
144
|
*/
|
|
144
145
|
"record-unattested",
|
|
146
|
+
/**
|
|
147
|
+
* The record is in its kind's ratified state (`reviews.ratified`, #2873),
|
|
148
|
+
* and its quorum, computed as `records` computes it, is not met: it was
|
|
149
|
+
* written to that state by hand, past the check of `records new` and
|
|
150
|
+
* `amend`. The record is still read, and still valid (#2878).
|
|
151
|
+
*/
|
|
152
|
+
"record-ratified-below-quorum",
|
|
145
153
|
];
|
|
146
154
|
/**
|
|
147
155
|
* The top-level field that holds a record's author seal (#2688), on a kind
|
|
@@ -547,12 +555,17 @@ export async function readSessionSubjects(loaded, root, source) {
|
|
|
547
555
|
* Import the kind file at `path` through the path loader lexicons use (#2520).
|
|
548
556
|
* The kind is registered under a name no lexicon package can have (npm names
|
|
549
557
|
* hold no `:`), imported and unregistered again, so no lexicon lookup sees it.
|
|
558
|
+
*
|
|
559
|
+
* A kind file is a data-only ES module, so a `.mjs`, `.cjs` or `.js` one loads
|
|
560
|
+
* with plain node and only a TypeScript one registers tsx (#3276). That keeps
|
|
561
|
+
* `workspace ls`, `status` and `records` from paying for tsx's loader thread
|
|
562
|
+
* on every read.
|
|
550
563
|
*/
|
|
551
564
|
export async function importKindModule(path) {
|
|
552
565
|
const name = `record-kind:${path}`;
|
|
553
566
|
registerLexiconDeclarations([{ name, module: path }], dirname(path));
|
|
554
567
|
try {
|
|
555
|
-
const mod = await importLexiconModule(name);
|
|
568
|
+
const mod = await importLexiconModule(name, { typeScript: !/\.[cm]?js$/.test(path) });
|
|
556
569
|
if (!mod)
|
|
557
570
|
throw new Error(`${path} was not registered`);
|
|
558
571
|
return mod;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-cli.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAOzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"verify-cli.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAOzD,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB7E"}
|
|
@@ -22,6 +22,7 @@ export async function runWorkspaceVerify(ctx) {
|
|
|
22
22
|
}
|
|
23
23
|
const report = verifyChange({
|
|
24
24
|
repo,
|
|
25
|
+
cwd: process.cwd(),
|
|
25
26
|
base: args.base,
|
|
26
27
|
head: args.head,
|
|
27
28
|
require: args.require === "attested" || args.require === "attested-runs" ? args.require : undefined,
|
|
@@ -50,7 +51,7 @@ function printReport(r) {
|
|
|
50
51
|
for (const c of r.runs.commits)
|
|
51
52
|
lines.push(` ${c.commit.slice(0, 8)} agent run ${c.runs.join(", ") || c.run}: ${c.attested ? "attested" : "not attested"}, ${c.reason}`);
|
|
52
53
|
for (const w of r.protectedWrites)
|
|
53
|
-
lines.push(` protected write ${w.commit.slice(0, 8)} ${w.paths.join(", ")}: ${w.allowed ? "allowed" : "refused"}, ${w.reason}`);
|
|
54
|
+
lines.push(` protected write ${w.commit.slice(0, 8)} ${w.paths.join(", ")}${w.blocks ? ` (${w.blocks.join(", ")})` : ""}: ${w.allowed ? "allowed" : "refused"}, ${w.reason}`);
|
|
54
55
|
if (r.rotation && "to" in r.rotation)
|
|
55
56
|
lines.push(` signer set: version ${r.rotation.from} to ${r.rotation.to}, signed by ${r.rotation.signedBy.join(", ")}`);
|
|
56
57
|
for (const n of r.notes)
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* that touches those files must be attested by a signer the base policy
|
|
9
9
|
* trusts, and by an admin when the base policy grants the admin role.
|
|
10
10
|
*
|
|
11
|
+
* The write scope is policy too (#3079): when the workspace declaration at
|
|
12
|
+
* base has a `writeScope` or `agents` block, a commit that changes either
|
|
13
|
+
* block, or removes the declaration, is a protected write on the same terms.
|
|
14
|
+
* Other edits to the declaration are not. The declaration is found from
|
|
15
|
+
* `cwd`, as `chant workspace check --changes` finds it.
|
|
16
|
+
*
|
|
11
17
|
* Attestation is opt-in (#2525 rule 6). Without a signers file at base the
|
|
12
18
|
* check passes and says so, unless `--require attested` asks for more.
|
|
13
19
|
*
|
|
@@ -36,6 +42,8 @@ export interface CommitVerdict extends RecordProvenance {
|
|
|
36
42
|
export interface ProtectedWrite {
|
|
37
43
|
commit: string;
|
|
38
44
|
paths: string[];
|
|
45
|
+
/** For a declaration write (#3079): the scope blocks the commit changes. */
|
|
46
|
+
blocks?: ScopeBlock[];
|
|
39
47
|
level: ProvenanceLevel;
|
|
40
48
|
principal?: string;
|
|
41
49
|
allowed: boolean;
|
|
@@ -61,6 +69,16 @@ export interface ChangeReport {
|
|
|
61
69
|
/** Protected paths whose content differs between the merge base and head. */
|
|
62
70
|
protectedChanged: string[];
|
|
63
71
|
protectedWrites: ProtectedWrite[];
|
|
72
|
+
/**
|
|
73
|
+
* The workspace declaration at base, when it has a scope block (#3079): its
|
|
74
|
+
* path from the repository root, the blocks it has, and the blocks that
|
|
75
|
+
* differ between the merge base and head.
|
|
76
|
+
*/
|
|
77
|
+
declaration: {
|
|
78
|
+
path: string;
|
|
79
|
+
blocks: ScopeBlock[];
|
|
80
|
+
changed: ScopeBlock[];
|
|
81
|
+
} | null;
|
|
64
82
|
/**
|
|
65
83
|
* When the change edits the signer set (#2553): the version it proposes and
|
|
66
84
|
* who signed it, or, when the new set is refused, the version it had to
|
|
@@ -81,6 +99,8 @@ export interface ChangeReport {
|
|
|
81
99
|
}
|
|
82
100
|
export interface VerifyOptions {
|
|
83
101
|
repo: string;
|
|
102
|
+
/** Where to look for the workspace declaration, as `check --changes` does; the repository root by default. */
|
|
103
|
+
cwd?: string;
|
|
84
104
|
base?: string;
|
|
85
105
|
head?: string;
|
|
86
106
|
require?: VerifyRequirement;
|
|
@@ -91,5 +111,7 @@ export interface VerifyOptions {
|
|
|
91
111
|
* changes. Merges count only for what they change beyond every parent.
|
|
92
112
|
*/
|
|
93
113
|
export declare function commitsTouching(repo: string, range: string, paths: string[]): Map<string, string[]>;
|
|
114
|
+
/** The declaration blocks whose change is a protected write (#3079). */
|
|
115
|
+
export type ScopeBlock = "writeScope" | "agents";
|
|
94
116
|
export declare function verifyChange(opts: VerifyOptions): ChangeReport;
|
|
95
117
|
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/workspace/trust/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* that touches those files must be attested by a signer the base policy
|
|
9
9
|
* trusts, and by an admin when the base policy grants the admin role.
|
|
10
10
|
*
|
|
11
|
+
* The write scope is policy too (#3079): when the workspace declaration at
|
|
12
|
+
* base has a `writeScope` or `agents` block, a commit that changes either
|
|
13
|
+
* block, or removes the declaration, is a protected write on the same terms.
|
|
14
|
+
* Other edits to the declaration are not. The declaration is found from
|
|
15
|
+
* `cwd`, as `chant workspace check --changes` finds it.
|
|
16
|
+
*
|
|
11
17
|
* Attestation is opt-in (#2525 rule 6). Without a signers file at base the
|
|
12
18
|
* check passes and says so, unless `--require attested` asks for more.
|
|
13
19
|
*
|
|
@@ -21,6 +27,11 @@
|
|
|
21
27
|
* `--require`, `runs` reports and nothing fails (studio-035 d).
|
|
22
28
|
*/
|
|
23
29
|
import { execFileSync } from "node:child_process";
|
|
30
|
+
import { posix } from "node:path";
|
|
31
|
+
import { isDeepStrictEqual } from "node:util";
|
|
32
|
+
import { DECLARATION_FILES, WorkspaceReadError } from "../declaration.js";
|
|
33
|
+
import { parseJsonText } from "../jsonc.js";
|
|
34
|
+
import { locateWorkspace } from "../which-chant.js";
|
|
24
35
|
import { policyAtBase, commitProvenance, resolveBase } from "./provenance.js";
|
|
25
36
|
import { policyWriters, protectedPaths } from "./policy.js";
|
|
26
37
|
import { fileAt, nextVersion, rotationPath, signerHistory } from "./rotation.js";
|
|
@@ -45,6 +56,47 @@ export function commitsTouching(repo, range, paths) {
|
|
|
45
56
|
}
|
|
46
57
|
return out;
|
|
47
58
|
}
|
|
59
|
+
const SCOPE_BLOCKS = ["writeScope", "agents"];
|
|
60
|
+
function scopeState(repo, rev, dir) {
|
|
61
|
+
const present = DECLARATION_FILES.map((name) => ({ name, text: fileAt(repo, rev, posix.join(dir, name)) })).filter((f) => f.text !== undefined);
|
|
62
|
+
if (present.length === 0)
|
|
63
|
+
return { blocks: {} };
|
|
64
|
+
if (present.length > 1)
|
|
65
|
+
return { unreadable: present.map((f) => f.text).join("\0") };
|
|
66
|
+
const parsed = parseJsonText(present[0].text, { jsonc: present[0].name.endsWith(".jsonc") });
|
|
67
|
+
if (!parsed.ok || typeof parsed.value !== "object" || parsed.value === null || Array.isArray(parsed.value))
|
|
68
|
+
return { unreadable: present[0].text };
|
|
69
|
+
const obj = parsed.value;
|
|
70
|
+
return { blocks: Object.fromEntries(SCOPE_BLOCKS.filter((b) => obj[b] !== undefined).map((b) => [b, obj[b]])) };
|
|
71
|
+
}
|
|
72
|
+
/** The scope blocks that differ between two states; all of them when either can't be read and the texts differ. */
|
|
73
|
+
function changedBlocks(a, b) {
|
|
74
|
+
if ("unreadable" in a || "unreadable" in b) {
|
|
75
|
+
return "unreadable" in a && "unreadable" in b && a.unreadable === b.unreadable ? [] : [...SCOPE_BLOCKS];
|
|
76
|
+
}
|
|
77
|
+
return SCOPE_BLOCKS.filter((k) => !isDeepStrictEqual(a.blocks[k], b.blocks[k]));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The directory, from the repository root, of the declaration found from
|
|
81
|
+
* `cwd` at `base`, with the scope blocks it has there. Null when there is no
|
|
82
|
+
* declaration, or one with neither block: then nothing in it is protected. A
|
|
83
|
+
* declaration that can't be read at base counts as having both.
|
|
84
|
+
*/
|
|
85
|
+
function scopedDeclarationAt(repo, cwd, base) {
|
|
86
|
+
let dir;
|
|
87
|
+
try {
|
|
88
|
+
const root = locateWorkspace(cwd, base).root;
|
|
89
|
+
dir = root === "." ? "" : root;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof WorkspaceReadError)
|
|
93
|
+
return null;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const state = scopeState(repo, base, dir);
|
|
97
|
+
const blocks = "unreadable" in state ? [...SCOPE_BLOCKS] : SCOPE_BLOCKS.filter((b) => b in state.blocks);
|
|
98
|
+
return blocks.length > 0 ? { dir, blocks } : null;
|
|
99
|
+
}
|
|
48
100
|
/** Whether a merge commit changes nothing beyond what its parents bring. */
|
|
49
101
|
function mergeWithoutOwnChanges(repo, commit, parents) {
|
|
50
102
|
if (parents < 2)
|
|
@@ -73,6 +125,7 @@ export function verifyChange(opts) {
|
|
|
73
125
|
protectedPaths: protectedPaths(policy),
|
|
74
126
|
protectedChanged: [],
|
|
75
127
|
protectedWrites: [],
|
|
128
|
+
declaration: null,
|
|
76
129
|
rotation: null,
|
|
77
130
|
notes: [],
|
|
78
131
|
failures: [],
|
|
@@ -99,7 +152,8 @@ export function verifyChange(opts) {
|
|
|
99
152
|
}
|
|
100
153
|
// Every commit in the change, oldest first, with its parents.
|
|
101
154
|
const listing = git(repo, ["rev-list", "--reverse", "--parents", `${base.commit}..${head}`]).split("\n").filter(Boolean);
|
|
102
|
-
const
|
|
155
|
+
const parentList = new Map(listing.map((l) => { const [c, ...p] = l.split(" "); return [c, p]; }));
|
|
156
|
+
const parentsOf = new Map([...parentList].map(([c, p]) => [c, p.length]));
|
|
103
157
|
report.runs = changeRuns(repo, [...parentsOf.keys()], policy.runners);
|
|
104
158
|
if (opts.require === "attested-runs") {
|
|
105
159
|
if (policy.runners.length === 0 && report.runs.commits.length > 0) {
|
|
@@ -145,24 +199,50 @@ export function verifyChange(opts) {
|
|
|
145
199
|
report.commits.push(v);
|
|
146
200
|
}
|
|
147
201
|
// Protected writes: every commit that touches the policy needs a writer's signature.
|
|
202
|
+
const writers = policyWriters(policy);
|
|
203
|
+
const judge = (commit, paths, blocks) => {
|
|
204
|
+
const v = verdicts.get(commit) ?? { ...commitProvenance(repo, policy, commit, opts.attestors), commit, subject: "" };
|
|
205
|
+
const allowed = v.level === "attested" && v.principal !== undefined && writers.has(v.principal);
|
|
206
|
+
const who = policy.roles.admin?.length ? "an admin the policy at base names" : "a signer the policy at base lists";
|
|
207
|
+
const what = blocks ? `${paths.join(", ")} (${blocks.join(", ")})` : paths.join(", ");
|
|
208
|
+
const reason = allowed
|
|
209
|
+
? `signed by ${v.principal}, ${who.replace(/^an? /, "")}`
|
|
210
|
+
: v.level === "attested"
|
|
211
|
+
? `signed by ${v.principal}, who is not ${who}`
|
|
212
|
+
: `${what} is protected and needs a signature by ${who}: ${v.reason}`;
|
|
213
|
+
report.protectedWrites.push({ commit, paths, ...(blocks ? { blocks } : {}), level: v.level, ...(v.principal ? { principal: v.principal } : {}), allowed, reason });
|
|
214
|
+
if (!allowed)
|
|
215
|
+
report.failures.push(`${commit.slice(0, 8)} changes ${what}: ${reason}`);
|
|
216
|
+
};
|
|
148
217
|
if (changedPaths.length > 0) {
|
|
149
|
-
const writers = policyWriters(policy);
|
|
150
218
|
const touching = commitsTouching(repo, `${base.commit}..${head}`, report.protectedPaths);
|
|
151
219
|
if (touching.size === 0) {
|
|
152
220
|
report.failures.push(`the policy files ${changedPaths.join(", ")} differ from base, and no commit in the change accounts for it`);
|
|
153
221
|
}
|
|
154
|
-
for (const [commit, paths] of touching)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
222
|
+
for (const [commit, paths] of touching)
|
|
223
|
+
judge(commit, paths);
|
|
224
|
+
}
|
|
225
|
+
// The write scope in the declaration at base (#3079): a commit that changes writeScope or agents.
|
|
226
|
+
const scoped = scopedDeclarationAt(repo, opts.cwd ?? repo, base.commit);
|
|
227
|
+
if (scoped) {
|
|
228
|
+
const files = DECLARATION_FILES.map((name) => posix.join(scoped.dir, name));
|
|
229
|
+
const changed = changedBlocks(scopeState(repo, mergeBase, scoped.dir), scopeState(repo, head, scoped.dir));
|
|
230
|
+
report.declaration = { path: files.find((f) => fileAt(repo, base.commit, f) !== undefined) ?? files[0], blocks: scoped.blocks, changed };
|
|
231
|
+
if (changed.length > 0) {
|
|
232
|
+
let accounted = false;
|
|
233
|
+
for (const [commit, paths] of commitsTouching(repo, `${base.commit}..${head}`, files)) {
|
|
234
|
+
const after = scopeState(repo, commit, scoped.dir);
|
|
235
|
+
// A merge changes a block only when it differs from every parent.
|
|
236
|
+
const per = (parentList.get(commit) ?? []).map((p) => new Set(changedBlocks(scopeState(repo, p, scoped.dir), after)));
|
|
237
|
+
const blocks = SCOPE_BLOCKS.filter((b) => per.length > 0 && per.every((s) => s.has(b)));
|
|
238
|
+
if (blocks.length === 0)
|
|
239
|
+
continue;
|
|
240
|
+
accounted = true;
|
|
241
|
+
judge(commit, paths, blocks);
|
|
242
|
+
}
|
|
243
|
+
if (!accounted) {
|
|
244
|
+
report.failures.push(`${changed.join(" and ")} in ${report.declaration.path} differ from base, and no commit in the change accounts for it`);
|
|
245
|
+
}
|
|
166
246
|
}
|
|
167
247
|
}
|
|
168
248
|
// A new signer set must be signed by a threshold of the set at base (#2553).
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* `write-scope-class-unknown` until the plugin is installed at the pinned
|
|
38
38
|
* version or the entry is removed.
|
|
39
39
|
*/
|
|
40
|
-
import { type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
|
|
40
|
+
import { WorkspaceReadError, type AgentDeclaration, type ClassScope, type Declaration, type PrincipalClass, type ProtectedPath, type WriteVerb } from "./declaration.js";
|
|
41
41
|
import { classesOf, roleGrants, type ClassRegistry } from "./principal-classes.js";
|
|
42
42
|
import { type TrustPolicy } from "./trust/policy.js";
|
|
43
43
|
import { AGENT_TRAILER } from "./trailers.js";
|
|
@@ -164,6 +164,8 @@ export interface ScopeSource {
|
|
|
164
164
|
policy: TrustPolicy;
|
|
165
165
|
/** The principal classes: the core four and those the declaration's pins supply, path pins read where the declaration was (#3080). */
|
|
166
166
|
classes: ClassRegistry;
|
|
167
|
+
/** Why the working tree's declaration couldn't be read when `declaration` is null: declaration-missing, or declaration-invalid with the problem (#3433). */
|
|
168
|
+
failure?: WorkspaceReadError;
|
|
167
169
|
}
|
|
168
170
|
/**
|
|
169
171
|
* The scope for a write from `cwd`: the declaration and the trust policy at
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,OAAO,
|
|
1
|
+
{"version":3,"file":"write-scope.d.ts","sourceRoot":"","sources":["../../src/workspace/write-scope.ts"],"names":[],"mappings":"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"}
|
|
@@ -275,17 +275,22 @@ export function judgePath(declaration, writer, path, change) {
|
|
|
275
275
|
message: `${who(writer)} may not write ${path}${allowance}: writeScope.${writer.class}.protected lists ${guarded.path}`,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
|
-
|
|
278
|
+
/** The declaration at `at` (the working tree without it), or the read error that says why there is none. */
|
|
279
|
+
function readOrFailure(cwd, at) {
|
|
279
280
|
try {
|
|
280
281
|
const located = locateWorkspace(cwd, at);
|
|
281
282
|
return { declaration: readDeclaration(located.tree), rootOnDisk: located.rootOnDisk, root: located.root, tree: located.tree };
|
|
282
283
|
}
|
|
283
284
|
catch (err) {
|
|
284
285
|
if (err instanceof WorkspaceReadError)
|
|
285
|
-
return
|
|
286
|
+
return err;
|
|
286
287
|
throw err;
|
|
287
288
|
}
|
|
288
289
|
}
|
|
290
|
+
function readAt(cwd, at) {
|
|
291
|
+
const read = readOrFailure(cwd, at);
|
|
292
|
+
return read instanceof WorkspaceReadError ? null : read;
|
|
293
|
+
}
|
|
289
294
|
/**
|
|
290
295
|
* The scope for a write from `cwd`: the declaration and the trust policy at
|
|
291
296
|
* the base revision (`origin/HEAD`, `main` or `master`), so a write can't
|
|
@@ -306,8 +311,10 @@ export function scopeSource(cwd, at) {
|
|
|
306
311
|
if (atBase)
|
|
307
312
|
return { ...withClasses(atBase), from: "base", policy };
|
|
308
313
|
}
|
|
309
|
-
const here =
|
|
310
|
-
|
|
314
|
+
const here = readOrFailure(cwd);
|
|
315
|
+
if (here instanceof WorkspaceReadError)
|
|
316
|
+
return { declaration: null, from: null, rootOnDisk: null, root: null, policy, classes: coreClassRegistry(), failure: here };
|
|
317
|
+
return { ...withClasses(here), from: "working-tree", policy };
|
|
311
318
|
}
|
|
312
319
|
const toPosix = (p) => (sep === "/" ? p : p.split(sep).join("/"));
|
|
313
320
|
/**
|
package/package.json
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* Each run is traced through a load hook that NODE_OPTIONS hands to the CLI's
|
|
8
8
|
* node, so the test can say what loaded: none of chant's TypeScript source,
|
|
9
9
|
* ever, and tsx only for a command that imports TypeScript (here the
|
|
10
|
-
* lexicon and the project source).
|
|
10
|
+
* lexicon and the project source). tsx's ESM hooks run on a thread of their
|
|
11
|
+
* own, about 200 MB of the process (#3276), so a workspace read must not
|
|
12
|
+
* register it either.
|
|
11
13
|
*
|
|
12
14
|
* End to end because it packs, installs from the registry and spawns the CLI.
|
|
13
15
|
*/
|
|
@@ -20,12 +22,14 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
|
20
22
|
|
|
21
23
|
const coreRoot = resolve(import.meta.dirname, "..", "..");
|
|
22
24
|
const renderRoot = resolve(coreRoot, "..", "..", "lexicons", "render");
|
|
25
|
+
const referenceDecisions = resolve(coreRoot, "..", "..", "reference-workspace", "decisions");
|
|
23
26
|
const version = (JSON.parse(readFileSync(join(coreRoot, "package.json"), "utf-8")) as { version: string }).version;
|
|
24
27
|
|
|
25
28
|
let scratch: string;
|
|
26
29
|
let project: string;
|
|
27
30
|
let cjsProject: string;
|
|
28
31
|
let waves: string;
|
|
32
|
+
let workspace: string;
|
|
29
33
|
let chant: string;
|
|
30
34
|
let tracer: string;
|
|
31
35
|
|
|
@@ -126,6 +130,31 @@ beforeAll(() => {
|
|
|
126
130
|
writeFileSync(join(waves, ".gitignore"), ".chant/\n");
|
|
127
131
|
git("add", "-A");
|
|
128
132
|
git("commit", "-qm", "ignore");
|
|
133
|
+
|
|
134
|
+
// A workspace whose declaration names a .mjs record kind, with a member
|
|
135
|
+
// that has a chant.config.ts, for the workspace reads below (#3276).
|
|
136
|
+
workspace = join(scratch, "workspace");
|
|
137
|
+
mkdirSync(join(workspace, "decisions"), { recursive: true });
|
|
138
|
+
mkdirSync(join(workspace, "app"));
|
|
139
|
+
for (const file of ["decision.kind.mjs", "decision.schema.json", "ref-001-how-the-app-is-deployed.md"]) {
|
|
140
|
+
writeFileSync(join(workspace, "decisions", file), readFileSync(join(referenceDecisions, file)));
|
|
141
|
+
}
|
|
142
|
+
writeFileSync(
|
|
143
|
+
join(workspace, "chant.workspace.json"),
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
name: "e2e",
|
|
146
|
+
schema: 1,
|
|
147
|
+
members: [{ name: "app", dir: "app", kind: "chant" }],
|
|
148
|
+
records: [{ kind: "decisions/decision.kind.mjs" }],
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
writeFileSync(join(workspace, "app", "chant.config.ts"), `export default { lexicons: [] as string[] };\n`);
|
|
152
|
+
symlinkSync(join(project, "node_modules"), join(workspace, "node_modules"), "dir");
|
|
153
|
+
const wsGit = (...args: string[]) => execFileSync("git", ["-c", "user.email=e2e@chant", "-c", "user.name=e2e", ...args], { cwd: workspace, stdio: "ignore" });
|
|
154
|
+
wsGit("init", "-q", "-b", "main");
|
|
155
|
+
writeFileSync(join(workspace, ".gitignore"), "node_modules\n");
|
|
156
|
+
wsGit("add", "-A");
|
|
157
|
+
wsGit("commit", "-qm", "workspace");
|
|
129
158
|
}, 600_000);
|
|
130
159
|
|
|
131
160
|
afterAll(() => {
|
|
@@ -148,6 +177,21 @@ describe("the installed chant CLI (#3878)", () => {
|
|
|
148
177
|
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
149
178
|
});
|
|
150
179
|
|
|
180
|
+
test("workspace ls reads a .mjs record kind from a member with a chant.config.ts without loading tsx", () => {
|
|
181
|
+
const r = run(join(workspace, "app"), ["workspace", "ls", "--json"]);
|
|
182
|
+
expect(r.status, r.stderr).toBe(0);
|
|
183
|
+
const ls = JSON.parse(r.stdout) as { workspace: { records: Array<{ name: string; kind: string | null }> } };
|
|
184
|
+
expect(ls.workspace.records.map((k) => k.kind)).toEqual(["decision"]);
|
|
185
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("workspace records reads the declared .mjs kind's records without loading tsx", () => {
|
|
189
|
+
const r = run(join(workspace, "app"), ["workspace", "records", "--json"]);
|
|
190
|
+
expect(r.status, r.stderr).toBe(0);
|
|
191
|
+
expect(r.stdout).toContain("ref-001");
|
|
192
|
+
expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0 });
|
|
193
|
+
});
|
|
194
|
+
|
|
151
195
|
test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
|
|
152
196
|
const r = run(project, ["build", "src", "-o", "out/render.json"]);
|
|
153
197
|
expect(r.status, r.stderr).toBe(0);
|
package/src/cli/main.test.ts
CHANGED
|
@@ -1003,3 +1003,13 @@ describe("chant workspace check --generated (#2641)", () => {
|
|
|
1003
1003
|
expect(() => parseArgs(["workspace", "check", "--generated=yes"])).toThrow();
|
|
1004
1004
|
});
|
|
1005
1005
|
});
|
|
1006
|
+
|
|
1007
|
+
describe("chant --help", () => {
|
|
1008
|
+
test("lists every top-level command in the registry", () => {
|
|
1009
|
+
const source = readFileSync(join(import.meta.dirname, "main.ts"), "utf-8");
|
|
1010
|
+
const help = source.slice(source.indexOf("function printHelp("), source.indexOf("Options:", source.indexOf("function printHelp(")));
|
|
1011
|
+
const listed = new Set([...help.matchAll(/^ {2}([a-z][a-z-]*)\b/gm)].map((m) => m[1]));
|
|
1012
|
+
const commands = new Set(commandRegistry.map((def) => def.name.split(" ")[0]));
|
|
1013
|
+
expect([...commands].filter((name) => !listed.has(name))).toEqual([]);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
package/src/cli/main.ts
CHANGED
|
@@ -167,7 +167,12 @@ Commands:
|
|
|
167
167
|
okf emits an OKF v0.2 knowledge bundle — one markdown
|
|
168
168
|
concept per entity + index.md; -o <dir> writes the
|
|
169
169
|
bundle tree, otherwise JSON path→content on stdout)
|
|
170
|
-
|
|
170
|
+
search "<query>" Answer an estate question with a few matching rows
|
|
171
|
+
instead of a graph dump (kind:, tag:, attr:, ->/<- edge
|
|
172
|
+
terms; --live or --at <ref> with --env <name> reads the
|
|
173
|
+
estate or a recorded snapshot; --show <attr>,
|
|
174
|
+
--check-live, --check-snapshot, --fail-on-drift)
|
|
175
|
+
generate Generate typed code from sources chant.config declares
|
|
171
176
|
(k8s.crds, helm.charts) into src/generated/<lexicon>/
|
|
172
177
|
(codegen.outDir); --check exits 1 when the committed
|
|
173
178
|
output no longer matches its sources, as build does
|
|
@@ -402,9 +407,11 @@ Workspace (level 1, #2524):
|
|
|
402
407
|
state transitions, new verdicts, new supersessions
|
|
403
408
|
and changed pins. A session id compares the commits
|
|
404
409
|
the session opened and closed at
|
|
405
|
-
workspace records pin <path>
|
|
410
|
+
workspace records pin <path> [--kind <kind file>]
|
|
406
411
|
Print the path from the workspace root and the
|
|
407
|
-
sha256 of a file, for a decision's evidence pin
|
|
412
|
+
sha256 of a file, for a decision's evidence pin. The
|
|
413
|
+
root is the kind file's workspace with --kind, else
|
|
414
|
+
the workspace above the current directory
|
|
408
415
|
workspace records new [<kind file>] --from <file|-> [--prefix <prefix>] [--sign [<key file>]] [--dry-run]
|
|
409
416
|
Write one new record in the kind's directory from
|
|
410
417
|
the JSON fields given, after validating them as
|
|
@@ -559,6 +566,11 @@ Workspace (level 1, #2524):
|
|
|
559
566
|
workspace points retract <id> --by <name>... [--note <text>] [--kind <kind file>] [--dry-run]
|
|
560
567
|
Take an answer back: the question is open for people
|
|
561
568
|
again, and the answer stays in its retractions
|
|
569
|
+
workspace points withdraw <id> --by <name>... [--note <text>] [--kind <kind file>] [--dry-run]
|
|
570
|
+
Close an open question that no longer applies, such as
|
|
571
|
+
one about a work item its author changed: points --open
|
|
572
|
+
leaves it out, nobody answers it, and asking it again
|
|
573
|
+
opens it again (#2888)
|
|
562
574
|
workspace pin <path> [--json]
|
|
563
575
|
Print the integrity value that pins the plugin at
|
|
564
576
|
<path>, to put in a path pin of chant.workspace.json.
|
|
@@ -1226,16 +1238,18 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1226
1238
|
|
|
1227
1239
|
// Workspace reads (#2524). Imported on first use, so a level-0 command never
|
|
1228
1240
|
// loads anything under workspace/ (#2525 rule 5, pinned by #2526's goldens).
|
|
1229
|
-
|
|
1241
|
+
// #3276 — records, points, ls and status read the declaration and never the
|
|
1242
|
+
// config of the member they run in, so they skip its load (and tsx with it).
|
|
1243
|
+
{ name: "workspace records", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/records-cli")).runWorkspaceRecords(ctx) },
|
|
1230
1244
|
// ws-058 (#2739) — decision points and the questions asked of them; never calls a model.
|
|
1231
|
-
{ name: "workspace points", handler: async (ctx) => (await import("../workspace/points-cli")).runWorkspacePoints(ctx) },
|
|
1245
|
+
{ name: "workspace points", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/points-cli")).runWorkspacePoints(ctx) },
|
|
1232
1246
|
{ name: "workspace init", handler: async (ctx) => (await import("../workspace/init")).runWorkspaceInit(ctx) },
|
|
1233
|
-
{ name: "workspace ls", handler: async (ctx) => (await import("../workspace/ls")).runWorkspaceLs(ctx) },
|
|
1247
|
+
{ name: "workspace ls", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ls")).runWorkspaceLs(ctx) },
|
|
1234
1248
|
// #2732 — the work lease: claim, renew and release a work item.
|
|
1235
1249
|
{ name: "workspace work", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/work-cli")).runWorkspaceWork(ctx) },
|
|
1236
1250
|
// #3033 — the agent run record: what each run cost and which commits it made. chant records runs and never starts one.
|
|
1237
1251
|
{ name: "workspace runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/runs-cli")).runWorkspaceRuns(ctx) },
|
|
1238
|
-
{ name: "workspace status", handler: async (ctx) => (await import("../workspace/status")).runWorkspaceStatus(ctx) },
|
|
1252
|
+
{ name: "workspace status", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/status")).runWorkspaceStatus(ctx) },
|
|
1239
1253
|
{ name: "workspace lineage", handler: async (ctx) => (await import("../workspace/lineage-cli")).runWorkspaceLineage(ctx) },
|
|
1240
1254
|
{ name: "workspace upgrade", handler: async (ctx) => (await import("../workspace/lineage-upgrade-cli")).runWorkspaceUpgrade(ctx) },
|
|
1241
1255
|
// #2551 — adopt a lineage, the hash index it matches against, and the versions of a family of workspaces.
|
|
@@ -85,7 +85,7 @@ describe("which servers have the workspace tools", () => {
|
|
|
85
85
|
const { tools } = (await rpc(server(cwd), "tools/list")) as { tools: { name: string }[] };
|
|
86
86
|
expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(names));
|
|
87
87
|
}
|
|
88
|
-
expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract"]);
|
|
88
|
+
expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract", "points-withdraw"]);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
test("a server outside any workspace, or given none, does not", async () => {
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* read to the workspace root's pinned chant included (ws-021), and keeps
|
|
16
16
|
* anything a command prints away from the protocol on stdout.
|
|
17
17
|
* - The writes (`records-new`, `records-amend`, `records-review`,
|
|
18
|
-
* `records-close`, `points-answer`, `points-retract`)
|
|
19
|
-
* `chant workspace records new|amend|review|close` and
|
|
20
|
-
* answer|retract` call, and return the
|
|
18
|
+
* `records-close`, `points-answer`, `points-retract`, `points-withdraw`)
|
|
19
|
+
* call the functions `chant workspace records new|amend|review|close` and
|
|
20
|
+
* `points answer|retract|withdraw` call, and return the
|
|
21
21
|
* same JSON result. They keep every rule
|
|
22
22
|
* the CLI keeps: the kind's schema, a closed record never changing, an
|
|
23
23
|
* approved one changing only as its approval rule allows, a dissent needing
|
|
@@ -165,7 +165,7 @@ export const workspaceReadTools: ToolDefinition[] = [
|
|
|
165
165
|
{
|
|
166
166
|
name: "workspace-points",
|
|
167
167
|
description:
|
|
168
|
-
"The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
|
|
168
|
+
"The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed, and not withdrawn; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
|
|
169
169
|
inputSchema: {
|
|
170
170
|
type: "object",
|
|
171
171
|
properties: {
|
|
@@ -326,6 +326,23 @@ export const workspaceWriteTools: ToolDefinition[] = [
|
|
|
326
326
|
required: ["id", "by"],
|
|
327
327
|
},
|
|
328
328
|
},
|
|
329
|
+
{
|
|
330
|
+
name: "points-withdraw",
|
|
331
|
+
description:
|
|
332
|
+
"Close an open decision point question that no longer applies, such as one about a work item its author changed, which a run asks about anew: chant workspace points withdraw (#2888). The question keeps its state, with who withdrew it, when and why; workspace-points open leaves it out, and points-answer refuses it. Asking it again opens it again. Withdrawing answers nothing, so it needs no quorum. An answered question is not withdrawn: points-retract takes the answer back. " +
|
|
333
|
+
PROTOCOL,
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
id: { type: "string", description: "The question's id, as workspace-points lists it." },
|
|
338
|
+
by: { type: "array", items: { type: "string" }, description: "Who withdraws the question (--by)." },
|
|
339
|
+
note: { type: "string", description: "Why, such as the question that asks about the subject now (--note)." },
|
|
340
|
+
kind: { type: "string", description: "The answer kind file. Without it, the declared answer kinds." },
|
|
341
|
+
dryRun: dryRunProp,
|
|
342
|
+
},
|
|
343
|
+
required: ["id", "by"],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
329
346
|
];
|
|
330
347
|
|
|
331
348
|
/** A tool call that can't be made as given: the client gets it as an error result, and nothing runs. */
|
|
@@ -581,6 +598,12 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
|
|
|
581
598
|
if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must list each person who takes the answer back");
|
|
582
599
|
return retractAnswer({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
|
|
583
600
|
},
|
|
601
|
+
"points-withdraw": async (params) => {
|
|
602
|
+
const { withdrawQuestion } = await import("../../workspace/decide");
|
|
603
|
+
const by = params.by;
|
|
604
|
+
if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must name who withdraws the question");
|
|
605
|
+
return withdrawQuestion({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
|
|
606
|
+
},
|
|
584
607
|
"records-close": async (params) => {
|
|
585
608
|
const w = await write();
|
|
586
609
|
const { closeRecord, RECORDS_CLOSE_SCHEMA_ID } = await import("../../workspace/records-close");
|