agent-doc-system 1.4.0 → 1.5.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/CHANGELOG.md +23 -1
- package/INSTALL_FOR_AGENTS.md +1 -1
- package/README.md +5 -1
- package/bin/agentdoc.mjs +16 -1
- package/bin/commands.mjs +15 -0
- package/bin/usage.txt +4 -0
- package/core/cache/compiled.mjs +34 -0
- package/core/cache/parses.mjs +33 -0
- package/core/codes.mjs +11 -0
- package/core/compile.mjs +25 -1
- package/core/context/index.mjs +173 -0
- package/core/context/symbols.mjs +81 -0
- package/core/fsx.mjs +50 -3
- package/core/graph.mjs +1 -1
- package/core/query.mjs +58 -7
- package/core/session/results.mjs +138 -0
- package/core/snapshot.mjs +123 -0
- package/docs/CI.md +1 -1
- package/docs/CONTEXT.md +136 -0
- package/docs/EVALS.md +124 -0
- package/docs/IMPLEMENTATION-PLAN.md +310 -0
- package/docs/QUERY.md +23 -5
- package/docs/ci/github-actions.yml +1 -1
- package/evals/agent/codex-driver.mjs +32 -0
- package/evals/agent/optimize.mjs +48 -0
- package/evals/agent/results/smoke-gpt-6.1-sol.json +81 -0
- package/evals/agent/run.mjs +163 -0
- package/evals/agent/tasks/corpus.mjs +53 -0
- package/evals/agent/tasks/pins.json +32 -0
- package/package.json +9 -5
- package/schemas/context.schema.json +222 -0
- package/skills/agent-doc-system/SKILL.md +6 -0
- package/templates/ci.yml +1 -1
package/core/fsx.mjs
CHANGED
|
@@ -70,11 +70,19 @@ export class Repo {
|
|
|
70
70
|
this.root = path.resolve(root);
|
|
71
71
|
this.readPaths = new Map();
|
|
72
72
|
this.probes = new Map();
|
|
73
|
+
this.probeRecords = new Map();
|
|
73
74
|
this.walks = new Map();
|
|
75
|
+
this.walkInputs = new Map();
|
|
74
76
|
this.listings = new Map();
|
|
75
77
|
this.nestedUnitRoots = new Map();
|
|
76
78
|
this.generatedDirs = new Set(opts.generatedDirs || DEFAULT_GENERATED_DIRS);
|
|
77
79
|
this.noGit = Boolean(opts.noGit);
|
|
80
|
+
this.snapshotReads = Boolean(opts.snapshotReads);
|
|
81
|
+
this.byteCache = new Map();
|
|
82
|
+
this.textCache = new Map();
|
|
83
|
+
this.jsonCache = new Map();
|
|
84
|
+
this.parseCache = opts.parseCache;
|
|
85
|
+
this.metrics = { physicalReads: 0, bytesRead: 0, readCacheHits: 0, jsonParses: 0, jsonCacheHits: 0, sharedJsonHits: 0 };
|
|
78
86
|
// Per-Repo git caches. undefined = not asked yet; null = no answer.
|
|
79
87
|
this._hasGit = undefined;
|
|
80
88
|
this._prefix = undefined;
|
|
@@ -148,15 +156,18 @@ export class Repo {
|
|
|
148
156
|
exists(rel) {
|
|
149
157
|
const r = !this.isIgnored(rel) && fs.existsSync(this.abs(rel));
|
|
150
158
|
this.probes.set(rel, r);
|
|
159
|
+
this.probeRecords.set("exists:" + rel, { kind: "exists", path: rel, value: r });
|
|
151
160
|
return r;
|
|
152
161
|
}
|
|
153
162
|
isDir(rel) {
|
|
154
163
|
try {
|
|
155
164
|
const r = !this.isIgnored(rel) && fs.statSync(this.abs(rel)).isDirectory();
|
|
156
165
|
this.probes.set(rel, r);
|
|
166
|
+
this.probeRecords.set("isDir:" + rel, { kind: "isDir", path: rel, value: r });
|
|
157
167
|
return r;
|
|
158
168
|
} catch {
|
|
159
169
|
this.probes.set(rel, false);
|
|
170
|
+
this.probeRecords.set("isDir:" + rel, { kind: "isDir", path: rel, value: false });
|
|
160
171
|
return false;
|
|
161
172
|
}
|
|
162
173
|
}
|
|
@@ -172,9 +183,15 @@ export class Repo {
|
|
|
172
183
|
{ path: rel }
|
|
173
184
|
);
|
|
174
185
|
}
|
|
175
|
-
let buf;
|
|
186
|
+
let buf = this.snapshotReads ? this.byteCache.get(rel) : undefined;
|
|
187
|
+
if (buf) this.metrics.readCacheHits++;
|
|
176
188
|
try {
|
|
177
|
-
|
|
189
|
+
if (!buf) {
|
|
190
|
+
buf = fs.readFileSync(a);
|
|
191
|
+
this.metrics.physicalReads++;
|
|
192
|
+
this.metrics.bytesRead += buf.length;
|
|
193
|
+
if (this.snapshotReads) this.byteCache.set(rel, buf);
|
|
194
|
+
}
|
|
178
195
|
} catch {
|
|
179
196
|
throw new AgentDocError(CODES.PATH_UNRESOLVED, "path does not resolve inside the repository: " + rel, { path: rel });
|
|
180
197
|
}
|
|
@@ -183,6 +200,7 @@ export class Repo {
|
|
|
183
200
|
}
|
|
184
201
|
readText(rel, opts = {}) {
|
|
185
202
|
const buf = this.readBytes(rel, opts);
|
|
203
|
+
if (this.snapshotReads && this.textCache.has(rel)) return this.textCache.get(rel);
|
|
186
204
|
let text;
|
|
187
205
|
try {
|
|
188
206
|
text = new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
@@ -190,10 +208,26 @@ export class Repo {
|
|
|
190
208
|
throw new AgentDocError(CODES.UTF8, "file is not valid UTF-8: " + rel, { path: rel });
|
|
191
209
|
}
|
|
192
210
|
if (text.startsWith("")) text = text.slice(1);
|
|
211
|
+
if (this.snapshotReads) this.textCache.set(rel, text);
|
|
193
212
|
return text;
|
|
194
213
|
}
|
|
195
214
|
readJson(rel) {
|
|
196
|
-
|
|
215
|
+
const text = this.readText(rel);
|
|
216
|
+
if (this.snapshotReads && this.jsonCache.has(rel)) {
|
|
217
|
+
this.metrics.jsonCacheHits++;
|
|
218
|
+
return structuredClone(this.jsonCache.get(rel));
|
|
219
|
+
}
|
|
220
|
+
const shared = this.parseCache?.get(text);
|
|
221
|
+
if (shared !== undefined) {
|
|
222
|
+
this.metrics.sharedJsonHits++;
|
|
223
|
+
if (this.snapshotReads) this.jsonCache.set(rel, structuredClone(shared));
|
|
224
|
+
return shared;
|
|
225
|
+
}
|
|
226
|
+
this.metrics.jsonParses++;
|
|
227
|
+
const value = JSON.parse(text);
|
|
228
|
+
this.parseCache?.set(text, value);
|
|
229
|
+
if (this.snapshotReads) this.jsonCache.set(rel, structuredClone(value));
|
|
230
|
+
return value;
|
|
197
231
|
}
|
|
198
232
|
readJsonUntracked(rel) {
|
|
199
233
|
return JSON.parse(this.readText(rel, { track: false }));
|
|
@@ -215,6 +249,10 @@ export class Repo {
|
|
|
215
249
|
continue;
|
|
216
250
|
}
|
|
217
251
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
252
|
+
if (this.snapshotReads) {
|
|
253
|
+
const rel = path.relative(this.root, dir).split(path.sep).join("/") || ".";
|
|
254
|
+
this.walkInputs.set(rel, this.directoryState(rel, entries));
|
|
255
|
+
}
|
|
218
256
|
for (const ent of entries) {
|
|
219
257
|
if (this.generatedDirs.has(ent.name)) continue;
|
|
220
258
|
const full = path.join(dir, ent.name);
|
|
@@ -232,6 +270,15 @@ export class Repo {
|
|
|
232
270
|
this.walks.set(relDir || ".", out);
|
|
233
271
|
return out;
|
|
234
272
|
}
|
|
273
|
+
// Raw visited-directory state, independent of extractor filter closures.
|
|
274
|
+
// Those closures may depend on discovery state which evolves during compile.
|
|
275
|
+
directoryState(relDir, entries = fs.readdirSync(this.abs(relDir), { withFileTypes: true })) {
|
|
276
|
+
const prefix = relDir && relDir !== "." ? relDir + "/" : "";
|
|
277
|
+
return entries
|
|
278
|
+
.filter(e => !this.generatedDirs.has(e.name) && !this.isIgnored(prefix + e.name))
|
|
279
|
+
.map(e => [e.name, e.isSymbolicLink() ? "link" : e.isDirectory() ? "dir" : "file"])
|
|
280
|
+
.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
281
|
+
}
|
|
235
282
|
// Per-unit scans use the discovery result, never filesystem heuristics.
|
|
236
283
|
// Keep walk() repository-wide for discovery, contracts and authored inputs.
|
|
237
284
|
ownsUnitPath(unitRoot, rel) {
|
package/core/graph.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { assertDeterministic, structuralProjection } from "./determinism.mjs";
|
|
|
9
9
|
import { sha256Hex } from "./fsx.mjs";
|
|
10
10
|
|
|
11
11
|
export const COMPILER_NAME = "agentdoc-compiler";
|
|
12
|
-
export const COMPILER_VERSION = "1.
|
|
12
|
+
export const COMPILER_VERSION = "1.5.0";
|
|
13
13
|
export const GRAPH_SCHEMA_VERSION = "agentdoc.dev/graph/v1";
|
|
14
14
|
|
|
15
15
|
// Resolve symbolic provenance subjects to RFC 6901 pointers into the graph.
|
package/core/query.mjs
CHANGED
|
@@ -130,7 +130,7 @@ function contractsOf(graph) {
|
|
|
130
130
|
|
|
131
131
|
export function queryContext(graph, needle, opts = {}) {
|
|
132
132
|
const budget = { ...DEFAULT_BUDGET, ...(opts.budget || {}) };
|
|
133
|
-
const idx = new ContextIndex(graph);
|
|
133
|
+
const idx = opts.index || new ContextIndex(graph);
|
|
134
134
|
const entity = idx.resolve(needle);
|
|
135
135
|
if (!entity) {
|
|
136
136
|
throw new AgentDocError(
|
|
@@ -379,8 +379,21 @@ export function renderContext(ctx, opts = {}) {
|
|
|
379
379
|
if (ctx.placement.domains.length) L.push("- domain: " + ctx.placement.domains.join(", "));
|
|
380
380
|
if (ctx.apis.provided.length) L.push("- provides: " + ctx.apis.provided.map((a) => a.api).join(", "));
|
|
381
381
|
if (ctx.apis.consumed.length) L.push("- consumes: " + ctx.apis.consumed.map((a) => a.api + " (via " + a.provider + ")").join(", "));
|
|
382
|
+
if (ctx.apis.provider) L.push("- API provider: " + ctx.apis.provider);
|
|
383
|
+
if (ctx.apis.consumers.length) L.push("- API consumers: " + ctx.apis.consumers.join(", "));
|
|
384
|
+
if (ctx.apis.contract) L.push("- API contract: " + JSON.stringify(ctx.apis.contract));
|
|
382
385
|
if (ctx.resources.used.length) L.push("- uses resource: " + ctx.resources.used.join(", "));
|
|
383
|
-
if (ctx.
|
|
386
|
+
if (ctx.resources.usedBy.length) L.push("- resource users: " + ctx.resources.usedBy.join(", "));
|
|
387
|
+
if (ctx.relations.items.length) {
|
|
388
|
+
L.push("- relations:");
|
|
389
|
+
for (const r of ctx.relations.items) {
|
|
390
|
+
L.push(" - " + r.direction + " " + r.type + " " + r.other + " [" + r.evidenceClass + "]" +
|
|
391
|
+
(r.attributes ? " " + JSON.stringify(r.attributes) : ""));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (ctx.events.items.length) L.push("- events: " + ctx.events.items.map((x) => x.pattern + " [" + x.resolution + "]" +
|
|
395
|
+
" producers=" + JSON.stringify(x.producers) + " consumers=" + JSON.stringify(x.consumers) +
|
|
396
|
+
(x.contract ? " contract=" + JSON.stringify(x.contract) : "")).join(", "));
|
|
384
397
|
if (ctx.externalDependencies.items.length) L.push("- external: " + ctx.externalDependencies.items.map((x) => x.name + " (" + x.role + ")").join(", "));
|
|
385
398
|
if (ctx.capabilities.items.length) L.push("- capabilities: " + ctx.capabilities.items.map((x) => x.name).join(", "));
|
|
386
399
|
for (const key of ["constraints", "docs", "runbooks"]) {
|
|
@@ -395,13 +408,36 @@ export function renderContext(ctx, opts = {}) {
|
|
|
395
408
|
}
|
|
396
409
|
if (ctx.verification.items.length) {
|
|
397
410
|
L.push("- verify:");
|
|
398
|
-
for (const v of ctx.verification.items) L.push(" - [" + v.tier + "] " + v.command
|
|
411
|
+
for (const v of ctx.verification.items) L.push(" - [" + v.tier + "] " + v.command + " (" + v.id +
|
|
412
|
+
(v.configPaths?.length ? "; config: " + v.configPaths.join(", ") : "") + ")");
|
|
413
|
+
}
|
|
414
|
+
if (ctx.globalGates.length) {
|
|
415
|
+
L.push("- repository gates (required in addition to scoped checks):");
|
|
416
|
+
for (const v of ctx.globalGates) L.push(" - [" + v.tier + "] " + v.command + " (" + v.id + ")");
|
|
399
417
|
}
|
|
400
418
|
if (ctx.journeys.items.length) L.push("- journeys: " + ctx.journeys.items.map((j) => j.id).join(", "));
|
|
401
|
-
if (ctx.
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
if (ctx.journeyPeers.length) L.push("- journey peers: " + ctx.journeyPeers.join(", "));
|
|
420
|
+
if (ctx.contracts.items.length) {
|
|
421
|
+
L.push("- contracts:");
|
|
422
|
+
for (const c of ctx.contracts.items) L.push(" - " + JSON.stringify(c));
|
|
423
|
+
}
|
|
424
|
+
if (ctx.authority.assertions.items.length) {
|
|
425
|
+
L.push("- assertions:");
|
|
426
|
+
for (const a of ctx.authority.assertions.items) {
|
|
427
|
+
L.push(" - " + a.key + " [" + a.evidenceClass + "/" + a.confidence + "/" + a.status + "]: " +
|
|
428
|
+
JSON.stringify(a.value) + (a.semantics ? " (" + a.semantics + ")" : ""));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const [settled, title] of [
|
|
432
|
+
[false, "CONFLICTS (do not assume one is true)"],
|
|
433
|
+
[true, "RESOLVED CONFLICTS (use the recorded election; re-check its review condition)"],
|
|
434
|
+
]) {
|
|
435
|
+
const conflicts = ctx.authority.conflicts.items.filter((c) => (c.status !== "unresolved") === settled);
|
|
436
|
+
if (!conflicts.length) continue;
|
|
437
|
+
L.push("- " + title + ":");
|
|
438
|
+
for (const c of conflicts) {
|
|
404
439
|
L.push(" - " + c.key + " [" + c.kind + "/" + c.status + "] " + (c.detail || ""));
|
|
440
|
+
if (c.election) L.push(" - election: " + JSON.stringify(c.election));
|
|
405
441
|
for (const a of c.assertions) L.push(" - " + a.evidenceClass + "/" + a.confidence + ": " + JSON.stringify(a.value));
|
|
406
442
|
}
|
|
407
443
|
}
|
|
@@ -421,9 +457,24 @@ export function renderContext(ctx, opts = {}) {
|
|
|
421
457
|
const obs = ctx.authority.observations;
|
|
422
458
|
if (obs.length) {
|
|
423
459
|
L.push("- RUNTIME OBSERVATIONS:");
|
|
424
|
-
for (const o of obs)
|
|
460
|
+
for (const o of obs) {
|
|
461
|
+
L.push(" - " + o.environment + " via " + o.sourceSystem + " at " + o.capturedAt + " (evidence " + o.evidenceBundle + ")");
|
|
462
|
+
for (const f of o.facts) L.push(" - " + f.key + ": " + JSON.stringify(f.value) +
|
|
463
|
+
(f.semantics ? " (" + f.semantics + ")" : ""));
|
|
464
|
+
}
|
|
425
465
|
}
|
|
426
466
|
const neighbours = ctx.neighbours.filter((n) => n.ref !== e.ref);
|
|
427
467
|
if (neighbours.length) L.push("- neighbours: " + neighbours.map((n) => n.ref).join(", "));
|
|
468
|
+
if (ctx.provenance.items.length) {
|
|
469
|
+
L.push("- evidence:");
|
|
470
|
+
for (const p of ctx.provenance.items) L.push(" - " + p.id + " " + p.path + " [" + p.class + "] " + p.contentHash);
|
|
471
|
+
}
|
|
472
|
+
if (ctx.truncated.length) {
|
|
473
|
+
L.push("- INCOMPLETE CONTEXT (request a larger --budget before treating these sections as complete):");
|
|
474
|
+
for (const name of ctx.truncated) {
|
|
475
|
+
const section = name.split(".").reduce((value, key) => value[key], ctx);
|
|
476
|
+
L.push(" - " + name + " [truncated: " + section.items.length + " of " + section.total + "]");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
428
479
|
return L.join("\n");
|
|
429
480
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CODES } from "../codes.mjs";
|
|
2
|
+
// Imported runner evidence is scoped to one repository snapshot and execution
|
|
3
|
+
// context. This module never executes a check or promotes a recommendation into
|
|
4
|
+
// a success. Any changed graph or evidence invalidates the imported result.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { compileSnapshot } from "../snapshot.mjs";
|
|
9
|
+
import { canonicalPath } from "../paths.mjs";
|
|
10
|
+
import { observationFreshness } from "../observations.mjs";
|
|
11
|
+
import { assertNoSecretsInText } from "../secrets.mjs";
|
|
12
|
+
|
|
13
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
14
|
+
const graphHash = (graph) => digest(JSON.stringify(graph));
|
|
15
|
+
function reject(message) { throw Object.assign(new Error(message), { code: CODES.SESSION_RESULT }); }
|
|
16
|
+
function object(value, keys, name) {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) reject(name + " must be an object");
|
|
18
|
+
for (const key of Object.keys(value)) if (!keys.includes(key)) reject("unknown " + name + " field: " + key);
|
|
19
|
+
}
|
|
20
|
+
function nonempty(value, name) {
|
|
21
|
+
if (typeof value !== "string" || !value.trim()) reject(name + " must be a nonempty string");
|
|
22
|
+
}
|
|
23
|
+
function sessionFile(root, session) {
|
|
24
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(session || "")) reject("invalid session reference");
|
|
25
|
+
return path.join(path.resolve(root), ".agentdoc", "sessions", "check-results", session + ".json");
|
|
26
|
+
}
|
|
27
|
+
function repositoryPath(root, value, directory = false) {
|
|
28
|
+
const rel = canonicalPath(value);
|
|
29
|
+
if (rel === null || typeof value !== "string" || !value) reject("execution paths must stay inside the repository");
|
|
30
|
+
let real;
|
|
31
|
+
try {
|
|
32
|
+
real = fs.realpathSync(path.join(root, rel));
|
|
33
|
+
const stat = fs.statSync(real);
|
|
34
|
+
if (directory ? !stat.isDirectory() : !stat.isFile()) reject("wrong execution path kind: " + rel);
|
|
35
|
+
} catch { reject("execution path unavailable: " + rel); }
|
|
36
|
+
const base = fs.realpathSync(root);
|
|
37
|
+
if (real !== base && !real.startsWith(base + path.sep)) reject("execution path escapes repository: " + rel);
|
|
38
|
+
return { rel, real };
|
|
39
|
+
}
|
|
40
|
+
function current(root, snapshot) {
|
|
41
|
+
if (!snapshot || !/^[a-f0-9]{64}$/.test(snapshot.graphHash || "")) reject("snapshot.graphHash must identify the full graph");
|
|
42
|
+
const res = compileSnapshot(root);
|
|
43
|
+
if (res.errors.length) reject("cannot validate results against a failing compilation");
|
|
44
|
+
if (snapshot.graphHash !== graphHash(res.graph)) reject("snapshot changed; imported checks must be run again");
|
|
45
|
+
if (observationFreshness(res.graph.observations).some((o) => o.stale)) reject("runtime observation expired; refresh context before importing checks");
|
|
46
|
+
return res.graph;
|
|
47
|
+
}
|
|
48
|
+
function execution(result, root, graph) {
|
|
49
|
+
object(result, ["checkId", "command", "cwd", "status", "exitCode", "runner", "evidence"], "result");
|
|
50
|
+
nonempty(result.checkId, "checkId");
|
|
51
|
+
nonempty(result.command, "command");
|
|
52
|
+
const check = [...graph.verification, ...graph.gates].find((v) => v.id === result.checkId);
|
|
53
|
+
if (!check || check.command !== result.command) reject("result must name the exact registered check and command");
|
|
54
|
+
const cwd = repositoryPath(root, result.cwd, true).rel;
|
|
55
|
+
if (!["succeeded", "failed"].includes(result.status) || !Number.isSafeInteger(result.exitCode) || result.exitCode < 0 ||
|
|
56
|
+
(result.status === "succeeded") !== (result.exitCode === 0)) reject("execution status must agree with its exit code");
|
|
57
|
+
object(result.runner, ["name", "version"], "runner");
|
|
58
|
+
nonempty(result.runner.name, "runner.name"); nonempty(result.runner.version, "runner.version");
|
|
59
|
+
object(result.evidence, ["path", "sha256"], "evidence");
|
|
60
|
+
if (!/^[a-f0-9]{64}$/.test(result.evidence.sha256 || "")) reject("evidence requires a SHA-256 digest");
|
|
61
|
+
const evidencePath = repositoryPath(root, result.evidence.path);
|
|
62
|
+
const bytes = fs.readFileSync(evidencePath.real);
|
|
63
|
+
if (digest(bytes) !== result.evidence.sha256) reject("execution evidence changed");
|
|
64
|
+
let report;
|
|
65
|
+
try { report = JSON.parse(bytes.toString("utf8")); } catch { reject("execution evidence must be a JSON runner report"); }
|
|
66
|
+
if (!report || typeof report !== "object" || report.snapshot?.graphHash !== graphHash(graph) ||
|
|
67
|
+
report.checkId !== result.checkId || report.command !== result.command || report.cwd !== cwd ||
|
|
68
|
+
report.status !== result.status || report.exitCode !== result.exitCode ||
|
|
69
|
+
report.runner?.name !== result.runner.name || report.runner?.version !== result.runner.version) reject("runner report does not corroborate the imported result");
|
|
70
|
+
return { ...result, cwd, runner: { ...result.runner }, evidence: { path: evidencePath.rel, sha256: result.evidence.sha256 } };
|
|
71
|
+
}
|
|
72
|
+
function read(root, session) {
|
|
73
|
+
const file = sessionFile(root, session);
|
|
74
|
+
if (!fs.existsSync(file)) return null;
|
|
75
|
+
try {
|
|
76
|
+
const stored = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
77
|
+
if (stored.digest !== digest(JSON.stringify(stored.data)) || stored.data.repository !== fs.realpathSync(root) ||
|
|
78
|
+
stored.data.session !== session || stored.data.schemaVersion !== "agentdoc.dev/check-results/v1" || !Array.isArray(stored.data.results)) reject("invalid stored results");
|
|
79
|
+
return stored.data;
|
|
80
|
+
} catch { reject("stored session results are invalid; forget the session and rerun its checks"); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function importCheckResults(root, request) {
|
|
84
|
+
object(request, ["session", "snapshot", "results"], "import");
|
|
85
|
+
const file = sessionFile(root, request.session);
|
|
86
|
+
if (!Array.isArray(request.results) || !request.results.length) reject("results must be a nonempty execution list");
|
|
87
|
+
const graph = current(root, request.snapshot);
|
|
88
|
+
const incoming = request.results.map((r) => execution(r, root, graph));
|
|
89
|
+
const previous = read(root, request.session);
|
|
90
|
+
const combined = new Map();
|
|
91
|
+
if (previous?.graphHash === request.snapshot.graphHash) {
|
|
92
|
+
for (const r of previous.results) {
|
|
93
|
+
try { const valid = execution(r, root, graph); combined.set(JSON.stringify([valid.checkId, valid.command, valid.cwd, valid.runner]), valid); }
|
|
94
|
+
catch { /* An old evidence bundle may have been replaced by a rerun. */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const r of incoming) combined.set(JSON.stringify([r.checkId, r.command, r.cwd, r.runner]), r);
|
|
98
|
+
const data = { schemaVersion: "agentdoc.dev/check-results/v1", repository: fs.realpathSync(root), session: request.session,
|
|
99
|
+
graphHash: request.snapshot.graphHash, results: [...combined.values()] };
|
|
100
|
+
const serialized = JSON.stringify({ data, digest: digest(JSON.stringify(data)) });
|
|
101
|
+
assertNoSecretsInText(serialized, "session check results");
|
|
102
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
103
|
+
const temp = file + "." + randomUUID() + ".tmp";
|
|
104
|
+
try { fs.writeFileSync(temp, serialized, { mode: 0o600 }); fs.renameSync(temp, file); }
|
|
105
|
+
finally { fs.rmSync(temp, { force: true }); }
|
|
106
|
+
return { session: request.session, graphHash: data.graphHash, results: data.results };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validResultsForSnapshot(root, session, snapshot) {
|
|
110
|
+
const data = read(root, session);
|
|
111
|
+
if (!data || data.graphHash !== snapshot?.graphHash) return [];
|
|
112
|
+
let graph;
|
|
113
|
+
try { graph = current(root, snapshot); } catch { return []; }
|
|
114
|
+
const valid = [];
|
|
115
|
+
for (const result of data.results) {
|
|
116
|
+
try { valid.push(execution(result, root, graph)); } catch { /* Expired or changed evidence is not a valid result. */ }
|
|
117
|
+
}
|
|
118
|
+
return valid;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function forgetSession(root, session) {
|
|
122
|
+
const file = sessionFile(root, session);
|
|
123
|
+
const existed = fs.existsSync(file);
|
|
124
|
+
fs.rmSync(file, { force: true });
|
|
125
|
+
return existed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function clearSessions(root) {
|
|
129
|
+
const dir = path.dirname(sessionFile(root, "session"));
|
|
130
|
+
if (!fs.existsSync(dir)) return 0;
|
|
131
|
+
let removed = 0;
|
|
132
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
133
|
+
if (entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}\.json$/.test(entry.name)) {
|
|
134
|
+
fs.unlinkSync(path.join(dir, entry.name)); removed++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return removed;
|
|
138
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Conservative persistent compilation reuse. Every visible file is hashed;
|
|
2
|
+
// this deliberately over-invalidates rather than inventing extractor-level
|
|
3
|
+
// dependencies. Full compile remains the oracle and is always available.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Repo, sha256Hex } from "./fsx.mjs";
|
|
8
|
+
import { compile, CompileResult } from "./compile.mjs";
|
|
9
|
+
import { loadConfig, loadSchemaBundle } from "./descriptors.mjs";
|
|
10
|
+
import { AgentDocError, CODES } from "./codes.mjs";
|
|
11
|
+
import { readCompiledCache, writeCompiledCache } from "./cache/compiled.mjs";
|
|
12
|
+
import { ParseCache } from "./cache/parses.mjs";
|
|
13
|
+
|
|
14
|
+
const implementationRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
|
|
16
|
+
function implementationHash() {
|
|
17
|
+
const records = [];
|
|
18
|
+
for (const dir of ["core", "adapters", "schemas"]) {
|
|
19
|
+
const walk = (base) => {
|
|
20
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
21
|
+
const file = path.join(base, entry.name);
|
|
22
|
+
if (entry.isDirectory()) walk(file);
|
|
23
|
+
else if (entry.isFile()) records.push([path.relative(implementationRoot, file), sha256Hex(fs.readFileSync(file))]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
walk(path.join(implementationRoot, dir));
|
|
27
|
+
}
|
|
28
|
+
return sha256Hex(JSON.stringify(records));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A running process keeps its imported implementation even if files on disk
|
|
32
|
+
// are replaced by an upgrade. Never label old code's result with the new hash.
|
|
33
|
+
const implementationVersion = implementationHash();
|
|
34
|
+
const sharedParses = new ParseCache();
|
|
35
|
+
|
|
36
|
+
function capture(root, opts) {
|
|
37
|
+
const schema = loadSchemaBundle();
|
|
38
|
+
const cfg = loadConfig(new Repo(root, { noGit: true }), schema.bundle);
|
|
39
|
+
const repo = new Repo(root, { noGit: opts.noGit, generatedDirs: cfg.discovery.generatedDirs || opts.generatedDirs, snapshotReads: true });
|
|
40
|
+
const files = repo.walk("").filter((file) => file !== cfg.output.graph);
|
|
41
|
+
const content = files.map((file) => [file, sha256Hex(repo.readBytes(file))]);
|
|
42
|
+
// Empty directories and path kinds are captured too: exists/isDir/listDir
|
|
43
|
+
// probes may affect resolution even when there is no readable file yet.
|
|
44
|
+
let symlinks = false;
|
|
45
|
+
const listings = [];
|
|
46
|
+
const visit = (dir) => {
|
|
47
|
+
if (repo.isIgnored(dir)) return;
|
|
48
|
+
const entries = fs.readdirSync(repo.abs(dir), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
const visible = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const rel = dir === "." ? entry.name : dir + "/" + entry.name;
|
|
52
|
+
if (repo.generatedDirs.has(entry.name) || repo.isIgnored(rel)) continue;
|
|
53
|
+
visible.push([entry.name, entry.isSymbolicLink() ? "link" : entry.isDirectory() ? "dir" : "file"]);
|
|
54
|
+
if (entry.isSymbolicLink()) symlinks = true;
|
|
55
|
+
if (entry.isDirectory()) visit(rel);
|
|
56
|
+
}
|
|
57
|
+
listings.push([dir, visible]);
|
|
58
|
+
};
|
|
59
|
+
visit(".");
|
|
60
|
+
const state = {
|
|
61
|
+
version: 1, implementation: implementationVersion, schemas: schema.versionInputs, noGit: Boolean(opts.noGit),
|
|
62
|
+
generatedDirs: [...repo.generatedDirs].sort(), commit: repo.headCommit(),
|
|
63
|
+
status: repo.statusRecords()?.filter((r) => r.xy !== "!!" && r.path !== cfg.output.graph) || null, content, listings,
|
|
64
|
+
};
|
|
65
|
+
return { key: sha256Hex(JSON.stringify(state)), repo, cfg, files: new Set(files), digests: new Map(content), symlinks };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read-only result consumer API. Extraction-only fields (ctx/discovery) are
|
|
69
|
+
* available on a miss only; callers needing those must use compile().
|
|
70
|
+
* Time-dependent observation gates must run after every call, including hits.
|
|
71
|
+
*/
|
|
72
|
+
export function compileSnapshot(root, opts = {}) {
|
|
73
|
+
const started = performance.now();
|
|
74
|
+
const file = path.join(path.resolve(root), ".agentdoc", "cache", "compiled-v1.json");
|
|
75
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
76
|
+
let before;
|
|
77
|
+
try { before = capture(root, opts); }
|
|
78
|
+
catch { return compile(root, { ...opts, snapshotReads: false }); }
|
|
79
|
+
// Explicit authored paths can follow a symlink even though discovery does
|
|
80
|
+
// not. Without a target manifest, reuse would miss changes outside the walk.
|
|
81
|
+
if (before.symlinks) return compile(root, { ...opts, snapshotReads: false });
|
|
82
|
+
const cached = opts.cache === false ? null : readCompiledCache(file, before.key);
|
|
83
|
+
let result;
|
|
84
|
+
if (cached) {
|
|
85
|
+
try {
|
|
86
|
+
const graph = JSON.parse(cached.serialized);
|
|
87
|
+
const invalid = loadSchemaBundle().bundle.validator("agentdoc.dev/schema/graph.schema.json").validate(graph);
|
|
88
|
+
if (!invalid) {
|
|
89
|
+
result = new CompileResult();
|
|
90
|
+
Object.assign(result, { graph, serialized: cached.serialized, stats: cached.stats, diagnostics: graph.diagnostics,
|
|
91
|
+
cfg: before.cfg, repo: before.repo, graphPath: before.cfg.output.graph });
|
|
92
|
+
}
|
|
93
|
+
} catch { /* corrupt cache: compile instead */ }
|
|
94
|
+
}
|
|
95
|
+
const hit = Boolean(result);
|
|
96
|
+
result ||= compile(root, { ...opts, snapshotReads: true, telemetry: true, parseCache: opts.parseCache || sharedParses });
|
|
97
|
+
if (!hit && result.repo) {
|
|
98
|
+
const excluded = [...result.repo.readPaths.keys()].some((p) => !before.files.has(p)) ||
|
|
99
|
+
[...result.repo.probes.keys()].some((p) => p.split("/").some((seg) => before.repo.generatedDirs.has(seg)) || p === before.cfg.output.graph);
|
|
100
|
+
if (excluded) return compile(root, { ...opts, snapshotReads: false });
|
|
101
|
+
}
|
|
102
|
+
let after;
|
|
103
|
+
try { after = capture(root, opts); }
|
|
104
|
+
catch { continue; }
|
|
105
|
+
if (before.key !== after.key) continue;
|
|
106
|
+
if (!hit && result.repo) {
|
|
107
|
+
if ([...result.repo.readPaths].some(([p, digest]) => after.digests.get(p) !== digest)) continue;
|
|
108
|
+
if ([...result.repo.probeRecords.values()].some((probe) => after.repo[probe.kind](probe.path) !== probe.value)) continue;
|
|
109
|
+
if ([...result.repo.listings].some(([p, names]) => JSON.stringify(after.repo.listDir(p)) !== JSON.stringify(names))) continue;
|
|
110
|
+
if ([...result.repo.walkInputs].some(([dir, entries]) => JSON.stringify(after.repo.directoryState(dir)) !== JSON.stringify(entries))) continue;
|
|
111
|
+
}
|
|
112
|
+
if (!hit && !result.errors.length && opts.cache !== false) writeCompiledCache(file, {
|
|
113
|
+
key: before.key, serialized: result.serialized, stats: result.stats,
|
|
114
|
+
});
|
|
115
|
+
// Consumers read evidence from the captured bytes, including on cache hits.
|
|
116
|
+
result.repo = before.repo;
|
|
117
|
+
result.cache = { hit, key: before.key, mode: "whole-compile", persisted: opts.cache !== false };
|
|
118
|
+
result.telemetry = { ...(result.telemetry || {}), durationMs: performance.now() - started,
|
|
119
|
+
captureReads: before.repo.metrics.physicalReads + after.repo.metrics.physicalReads };
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
throw new AgentDocError(CODES.SNAPSHOT_UNSTABLE, "repository changed during context capture; retry once edits have settled");
|
|
123
|
+
}
|
package/docs/CI.md
CHANGED
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
with: { fetch-depth: 0 }
|
|
21
21
|
- uses: actions/setup-node@v4
|
|
22
22
|
with: { node-version: '24' }
|
|
23
|
-
# Pin a tag for reproducibility, e.g. github:lbframe/agent-doc-system#v1.
|
|
23
|
+
# Pin a tag for reproducibility, e.g. github:lbframe/agent-doc-system#v1.5.0 —
|
|
24
24
|
# a compiler upgrade intentionally invalidates every committed graph.
|
|
25
25
|
- run: npm install -g github:lbframe/agent-doc-system
|
|
26
26
|
- run: agentdoc validate
|
package/docs/CONTEXT.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Task context protocol
|
|
2
|
+
|
|
3
|
+
`prepare`, `expand` and `refresh` accept one JSON request on stdin (or `--request file`) and emit one compact JSON packet plus a newline. They operate on the current working tree and do not require a committed graph. They never execute repository verification commands.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
printf '%s\n' '{"task":"Change the engine API","paths":["crates/engine"],"intent":"change-contract","budget":{"tokens":4096,"encoding":"o200k_base"}}' | agentdoc prepare
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Intents are `implement` (default), `change-contract`, `debug-runtime` and `review`. The first implementation conservatively retains the same obligation closure across intents. The packet contains typed, versioned records, evidence references, selected checks, analysis limitations and stale runtime observation IDs. It does not infer that unsupported or dynamic dependencies are absent.
|
|
10
|
+
|
|
11
|
+
## Budget and continuation
|
|
12
|
+
|
|
13
|
+
The exact serialized output, including its final newline, metadata and continuation, is counted using pinned `js-tiktoken@1.0.21`. Select `o200k_base` or `cl100k_base` explicitly to match the encoding used by your model; AgentDoc does not guess model tokenization. The supported budget is 256–1,000,000 tokens. A budget too small for the envelope or next indivisible record fails with `AGENTDOC_CONTEXT_BUDGET` and must be increased.
|
|
14
|
+
|
|
15
|
+
`delivery.partial` and `delivery.criticalOmitted` report incomplete delivery. Critical obligations are selected first, but a partial page must never be interpreted as the complete check or constraint list. Follow `continuation` until it is null:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{"packet":"<64-character packet reference>","offset":8,"tokens":8192}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Pass that object to `agentdoc expand`. Continuations are tied to the original request, snapshot and protocol. A changed graph or runtime freshness state rejects expansion with `AGENTDOC_CONTEXT_STALE`; use `refresh` or `prepare`. Larger budgets allow reading a long constraint document without silently shortening it.
|
|
22
|
+
|
|
23
|
+
## Session refresh
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{"packet":"<previous packet reference>","acknowledged":["<retained record ID>"]}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Send this to `agentdoc refresh`. Acknowledge only records still present in the agent's active context. Without acknowledgements, all records are delivered again. `reset:true` also requests full delivery. Refresh emits new or changed records, explicit retractions and active critical obligations. Critical records are repeated even if acknowledged. The resulting delta has its own packet reference and continuation.
|
|
30
|
+
|
|
31
|
+
The harness must apply retractions, replace changed versions and manage its active context. Merely appending delta messages does not bound the model's history. Following a lost or compacted session, request a reset. A changed snapshot conservatively invalidates prior execution evidence.
|
|
32
|
+
|
|
33
|
+
Packets are stored locally under `.agentdoc/context/`, excluded from compiler inputs. Treat references as local to a repository. The JavaScript API exposes `forget(root, packet)` to remove a retained packet. At most 64 packets are retained per repository; older continuations may require preparing again. The caller can delete packets sooner; packet files are not published with the repository.
|
|
34
|
+
|
|
35
|
+
## Checks and evidence
|
|
36
|
+
|
|
37
|
+
A recommended shell command is not an execution result. In this initial protocol, checks expose `execution.status:"requires-context"` and `cwd:null` when the compiler has not established a working directory. Resolve that context from the check's owner and evidence before executing it. Identical command text for different checks remains distinct.
|
|
38
|
+
|
|
39
|
+
Constraint and document records carry their content and a content fingerprint. Other records carry graph provenance identifiers or paths. Facts retain their authority data; contradictory claims are not merged into one assumed truth. Stale observations are marked explicitly and must not be treated as current production evidence.
|
|
40
|
+
|
|
41
|
+
## Cache and compatibility
|
|
42
|
+
|
|
43
|
+
`cache:false` in a prepare request bypasses persistent compilation reuse. The full compiler remains the correctness reference. Cache reuse is conservative and does not imply extractor-level incremental recompilation. Protocol version `agentdoc.dev/context/v1` is separate from graph version `agentdoc.dev/graph/v1`.
|
|
44
|
+
|
|
45
|
+
Existing `query` and `impact` keep their established freshness policies. See [QUERY.md](QUERY.md) for those commands.
|
|
46
|
+
|
|
47
|
+
## Importing check execution results from JavaScript
|
|
48
|
+
|
|
49
|
+
The result API accepts reports produced by the harness after execution. It does
|
|
50
|
+
not run the command or independently observe its execution. Each report must name
|
|
51
|
+
a registered check and its exact command, an explicit repository-relative working
|
|
52
|
+
directory, a runner name/version, and a status matching its exit code:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"snapshot": { "graphHash": "<graphHash from the prepared packet>" },
|
|
57
|
+
"checkId": "<registered check ID>",
|
|
58
|
+
"command": "<exact registered command>",
|
|
59
|
+
"cwd": "service",
|
|
60
|
+
"status": "succeeded",
|
|
61
|
+
"exitCode": 0,
|
|
62
|
+
"runner": { "name": "<harness name>", "version": "<harness version>" }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `failed` with a positive integer exit code for a failed execution. A
|
|
67
|
+
recommendation, missing working directory or disagreement between the status and
|
|
68
|
+
exit code is refused. Store the report under `.agentdoc/executions/` so writing
|
|
69
|
+
execution evidence does not itself change the source snapshot. The report's
|
|
70
|
+
`graphHash` must be the snapshot actually used for the run, not a new snapshot
|
|
71
|
+
captured after unrelated edits.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import fs from 'node:fs';
|
|
75
|
+
import path from 'node:path';
|
|
76
|
+
import { createHash } from 'node:crypto';
|
|
77
|
+
import {
|
|
78
|
+
importCheckResults, validResultsForSnapshot, forgetSession, clearSessions,
|
|
79
|
+
} from 'agent-doc-system/core/session/results.mjs';
|
|
80
|
+
|
|
81
|
+
// root is the repository root; packet is its prepared context packet.
|
|
82
|
+
const reportPath = '.agentdoc/executions/check.json';
|
|
83
|
+
const bytes = fs.readFileSync(path.join(root, reportPath));
|
|
84
|
+
const report = JSON.parse(bytes.toString('utf8'));
|
|
85
|
+
const { checkId, command, cwd, status, exitCode, runner } = report;
|
|
86
|
+
importCheckResults(root, {
|
|
87
|
+
session: 'change-engine',
|
|
88
|
+
snapshot: packet.snapshot,
|
|
89
|
+
results: [{ checkId, command, cwd, status, exitCode, runner, evidence: {
|
|
90
|
+
path: reportPath,
|
|
91
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
92
|
+
} }],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const results = validResultsForSnapshot(root, 'change-engine', packet.snapshot);
|
|
96
|
+
const successes = results.filter(result => result.status === 'succeeded');
|
|
97
|
+
forgetSession(root, 'change-engine'); // Returns whether a result record existed.
|
|
98
|
+
clearSessions(root); // Removes all retained result sessions; returns their count.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Import verifies the current graph, registered command and hash-pinned JSON report.
|
|
102
|
+
Reads verify them again: a changed graph, changed evidence, unavailable execution
|
|
103
|
+
directory or expired runtime observation invalidates prior results. Both valid
|
|
104
|
+
successes and valid failures are returned; the harness must inspect `status`.
|
|
105
|
+
All import validation errors use `AGENTDOC_SESSION_RESULT`.
|
|
106
|
+
|
|
107
|
+
Session names contain 1–64 ASCII letters, digits, underscores or hyphens and start
|
|
108
|
+
with a letter or digit. Result records live under
|
|
109
|
+
`.agentdoc/sessions/check-results/`, independently of context packets. Cleanup
|
|
110
|
+
removes result records only; it preserves context packets and execution reports.
|
|
111
|
+
Use the context API's `forget(root, packet)` separately when dropping a packet.
|
|
112
|
+
|
|
113
|
+
## TypeScript symbol candidates from JavaScript
|
|
114
|
+
|
|
115
|
+
The optional symbol experiment indexes only explicitly requested TypeScript
|
|
116
|
+
files. It returns declarations and reference candidates with source line/column
|
|
117
|
+
locations and a SHA-256 content fingerprint:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
import { indexTypeScriptSymbols } from 'agent-doc-system/core/context/symbols.mjs';
|
|
121
|
+
|
|
122
|
+
const candidates = indexTypeScriptSymbols(root, ['src/api.ts', 'src/consumer.ts']);
|
|
123
|
+
for (const file of candidates.files) {
|
|
124
|
+
// file.path, file.sha256, file.declarations, file.references
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Supported paths end in `.ts`, `.tsx`, `.mts` or `.cts`; paths escaping the
|
|
129
|
+
repository, including through a symlink, are refused. The experiment masks
|
|
130
|
+
comments and quoted text, but does not resolve lexical scopes, imports,
|
|
131
|
+
overloads, dynamic references, regex literals or template interpolation. Its
|
|
132
|
+
output always declares `coverage.partial:true` and
|
|
133
|
+
`coverage.checkSelection:"unchanged"`. Use candidates to navigate source, never
|
|
134
|
+
to infer that an unlisted dependency is absent or to remove a required check.
|
|
135
|
+
The experiment is a JavaScript API and is not enabled by the default context
|
|
136
|
+
commands.
|