@sabaiway/agent-workflow-kit 10.5.0 → 11.0.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 +139 -0
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +5 -1
- package/references/modes/review-state.md +3 -3
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +24 -12
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +269 -0
- package/tools/dispatch-store.mjs +21 -357
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/held-session.mjs +165 -0
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +47 -4
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +127 -39
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- package/tools/worktrees.mjs +2 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// no-follow read without a cycle. flow-store-read.mjs re-exports everything here, so every
|
|
6
6
|
// existing consumer keeps its import site. Dependency-free, Node >= 22.
|
|
7
7
|
|
|
8
|
-
import { readFileSync, lstatSync, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
|
|
8
|
+
import { readFileSync, readSync, lstatSync, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
|
|
9
9
|
|
|
10
10
|
// Local no-follow lstat (null ONLY on a true ENOENT).
|
|
11
11
|
export const lstatNoFollowRead = (path, lstat = lstatSync) => {
|
|
@@ -24,6 +24,14 @@ export const describeNonRegular = (st) =>
|
|
|
24
24
|
// ignoreBOM: a BOM must surface as malformed line 1, not vanish and get rewritten without it.
|
|
25
25
|
const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
26
26
|
const hasOpenFlag = (v) => typeof v === 'number' && v !== 0;
|
|
27
|
+
const errCode = (err) => (err && err.code) || (err && err.message) || null;
|
|
28
|
+
// The open failures with a NAME of their own (shared by both readers); null for every other.
|
|
29
|
+
const openFailure = (err) => {
|
|
30
|
+
if (err && err.code === 'ENOENT') return { outcome: 'absent' };
|
|
31
|
+
if (err && err.code === 'ELOOP') return { outcome: 'foreign', className: 'symlink', isDirectory: false };
|
|
32
|
+
if (err && err.code === 'EISDIR') return { outcome: 'foreign', className: 'directory', isDirectory: true };
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
27
35
|
|
|
28
36
|
// The ONE race-free read for store/lock bytes: open O_NOFOLLOW|O_NONBLOCK, fstat the DESCRIPTOR,
|
|
29
37
|
// read through it, decode fatally — a pathname swapped after the open cannot change what the fd
|
|
@@ -89,9 +97,8 @@ const readNoFollowCore = (path, io, closeBox) => {
|
|
|
89
97
|
}
|
|
90
98
|
return { outcome: 'ok', content, dev: st.dev, ino: st.ino };
|
|
91
99
|
} catch (err) {
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
if (err && err.code === 'EISDIR') return { outcome: 'foreign', className: 'directory', isDirectory: true };
|
|
100
|
+
const known = openFailure(err);
|
|
101
|
+
if (known !== null) return known;
|
|
95
102
|
try {
|
|
96
103
|
const st = lstatNoFollowRead(path, lstat);
|
|
97
104
|
if (st && !st.isFile()) return { outcome: 'foreign', className: describeNonRegular(st), isDirectory: st.isDirectory() };
|
|
@@ -108,6 +115,51 @@ const readNoFollowCore = (path, io, closeBox) => {
|
|
|
108
115
|
}
|
|
109
116
|
};
|
|
110
117
|
|
|
118
|
+
// readFileBytesNoFollowCapped(path, cap, io?) → ok { bytes } | over-cap { cap } | absent | foreign |
|
|
119
|
+
// error — the control-byte gate's read: open O_NOFOLLOW|O_NONBLOCK, fstat the DESCRIPTOR for its
|
|
120
|
+
// kind, read on it in bounded chunks until EOF or cap+1 bytes; the fstat SIZE is never consulted (a
|
|
121
|
+
// file growing after the fstat still refuses); no decode (a lone 0xE9 is ok bytes); no lstat fallback.
|
|
122
|
+
// ONE skeleton: no open without both flags; fstat the DESCRIPTOR; the body runs on a regular file only.
|
|
123
|
+
const READ_CHUNK = 64 * 1024;
|
|
124
|
+
const withRegularDescriptor = (path, io, body) => {
|
|
125
|
+
const consts = io.constants ?? fsConstants;
|
|
126
|
+
const open = io.open ?? openSync;
|
|
127
|
+
const fstat = io.fstat ?? fstatSync;
|
|
128
|
+
const close = io.close ?? closeSync;
|
|
129
|
+
if (!hasOpenFlag(consts.O_NONBLOCK) || !hasOpenFlag(consts.O_NOFOLLOW)) {
|
|
130
|
+
return { outcome: 'error', code: 'this platform exposes no usable O_NOFOLLOW|O_NONBLOCK open flags — refusing to open (fail closed)' };
|
|
131
|
+
}
|
|
132
|
+
let fd = null;
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
fd = open(path, (consts.O_RDONLY ?? 0) | consts.O_NOFOLLOW | consts.O_NONBLOCK);
|
|
136
|
+
const st = fstat(fd);
|
|
137
|
+
result = st.isFile() ? body(fd) : { outcome: 'foreign', className: describeNonRegular(st), isDirectory: st.isDirectory() };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
result = openFailure(err) ?? { outcome: 'error', code: errCode(err) ?? 'read failed' };
|
|
140
|
+
} finally {
|
|
141
|
+
if (fd !== null) {
|
|
142
|
+
try { close(fd); } catch (err) { result = { outcome: 'error', code: `descriptor close failed (${errCode(err) ?? 'close failed'}) — fail closed` }; }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
// probeRegularFileNoFollow(path, io?) → ok | absent | foreign | error: the kind check alone, no read.
|
|
148
|
+
export const probeRegularFileNoFollow = (path, io = {}) => withRegularDescriptor(path, io, () => ({ outcome: 'ok' }));
|
|
149
|
+
export const readFileBytesNoFollowCapped = (path, cap, io = {}) => withRegularDescriptor(path, io, (fd) => {
|
|
150
|
+
const read = io.read ?? readSync;
|
|
151
|
+
const chunks = [];
|
|
152
|
+
let total = 0;
|
|
153
|
+
while (total <= cap) {
|
|
154
|
+
const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK, cap + 1 - total));
|
|
155
|
+
const n = read(fd, chunk, 0, chunk.length, null);
|
|
156
|
+
if (n === 0) break;
|
|
157
|
+
chunks.push(chunk.subarray(0, n));
|
|
158
|
+
total += n;
|
|
159
|
+
}
|
|
160
|
+
return total > cap ? { outcome: 'over-cap', cap } : { outcome: 'ok', bytes: Buffer.concat(chunks, total) };
|
|
161
|
+
});
|
|
162
|
+
|
|
111
163
|
// readFileBytesNoFollow(path, io?) → { outcome: 'ok', bytes } | absent | foreign | error — the
|
|
112
164
|
// BYTES twin of the reader above for consumers whose domain is byte offsets (the Phase-4
|
|
113
165
|
// receipt-deadline watermark, the finding-manifest digest domain): keepFd internally so the raw
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// git-env.mjs — the kit's git-location leaf: WHICH repository a git spawn will talk to, judged by
|
|
2
|
+
// AGREEMENT, never by a variable ban (measured 2026-08-31, git 2.43: an ambient GIT_DIR made three
|
|
3
|
+
// consumers judge ANOTHER repository in silence; a killed git read as "not a git work tree", a pass).
|
|
4
|
+
// The six-state table, the GIT_* strip, the hermetic env, the ls-files parser. A pure LEAF.
|
|
5
|
+
|
|
6
|
+
import { realpathSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
export const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
11
|
+
export const GIT_LOCATION_STATES = Object.freeze(['work-tree', 'not-a-repository', 'error', 'redirected', 'no-work-tree', 'env-only']);
|
|
12
|
+
const LOCATION_VARIABLES = 'GIT_DIR / GIT_WORK_TREE / GIT_COMMON_DIR';
|
|
13
|
+
|
|
14
|
+
// ASCII-case-insensitive prefix (a case-insensitive host honours `git_dir` as GIT_DIR) — an ASCII
|
|
15
|
+
// class, never toUpperCase(): Unicode case folding would also strip a foreign `g<dotless-i>t_dir`.
|
|
16
|
+
export const stripGitLocationEnv = (env) => Object.fromEntries(Object.entries(env).filter(([key]) => !/^[Gg][Ii][Tt]_/.test(key)));
|
|
17
|
+
|
|
18
|
+
// The test fixtures' env, never a consumer's: the strip plus every host-reaching knob pinned.
|
|
19
|
+
export const hermeticGitEnv = (env, home) => ({
|
|
20
|
+
...stripGitLocationEnv(env),
|
|
21
|
+
HOME: home, XDG_CONFIG_HOME: home, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null',
|
|
22
|
+
GIT_CONFIG_NOSYSTEM: '1', GIT_ATTR_NOSYSTEM: '1', LC_ALL: 'C',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// An env naming no PATH (a partial override, a test's `{}`) borrows the process PATH; one naming a PATH is honoured as is.
|
|
26
|
+
export const withGitPath = (env) => (env.PATH === undefined && process.env.PATH !== undefined ? { ...env, PATH: process.env.PATH } : env);
|
|
27
|
+
|
|
28
|
+
// `ls-files -s -z` records: mode, object, stage, TAB, then the path BYTES up to the NUL — undecoded.
|
|
29
|
+
export const parseIndexEntries = (buf) => {
|
|
30
|
+
const entries = [];
|
|
31
|
+
for (let start = 0; start < buf.length;) {
|
|
32
|
+
let end = buf.indexOf(0, start);
|
|
33
|
+
if (end === -1) end = buf.length;
|
|
34
|
+
const record = buf.subarray(start, end);
|
|
35
|
+
const tab = record.indexOf(0x09);
|
|
36
|
+
if (tab !== -1) {
|
|
37
|
+
const [mode, sha, stage] = record.subarray(0, tab).toString('utf8').split(' ');
|
|
38
|
+
entries.push({ mode, sha, stage: Number(stage), path: record.subarray(tab + 1) });
|
|
39
|
+
}
|
|
40
|
+
start = end + 1;
|
|
41
|
+
}
|
|
42
|
+
return entries;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const NOT_A_REPOSITORY = /not a git repository/u;
|
|
46
|
+
const NO_WORK_TREE = /must be run in a work tree/u;
|
|
47
|
+
|
|
48
|
+
// ONE rev-parse identity per spawn → ok { line } | not-a-repository | no-work-tree | error (a throw,
|
|
49
|
+
// ENOENT, a signal, any other exit, an EMPTY answer). The answer is the stdout minus exactly one
|
|
50
|
+
// terminating LF — never a split: a path may carry a newline. LC_ALL=C pins git's own wording.
|
|
51
|
+
const probe = (arg, { cwd, env, spawn }) => {
|
|
52
|
+
let r;
|
|
53
|
+
try {
|
|
54
|
+
r = spawn('git', ['rev-parse', arg], { cwd, env: { ...withGitPath(env), LC_ALL: 'C' }, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { kind: 'error', cause: `git threw synchronously (${err?.message ?? err})` };
|
|
57
|
+
}
|
|
58
|
+
if (r == null || typeof r !== 'object') return { kind: 'error', cause: 'the git runner returned no result' };
|
|
59
|
+
if (r.error) return { kind: 'error', cause: `git could not run (${r.error.code ?? r.error.message ?? r.error})` };
|
|
60
|
+
if (r.signal) return { kind: 'error', cause: `git rev-parse ${arg} was killed by ${r.signal}` };
|
|
61
|
+
const stderr = String(r.stderr ?? '').trim();
|
|
62
|
+
if (r.status === 128 && NOT_A_REPOSITORY.test(stderr)) return { kind: 'not-a-repository', cause: stderr };
|
|
63
|
+
if (r.status === 128 && NO_WORK_TREE.test(stderr)) return { kind: 'no-work-tree', cause: stderr };
|
|
64
|
+
if (r.status !== 0) return { kind: 'error', cause: `git rev-parse ${arg} exited ${r.status}${stderr ? ` (${stderr})` : ''}` };
|
|
65
|
+
const line = String(r.stdout ?? '').replace(/\n$/u, '');
|
|
66
|
+
if (line.length === 0) return { kind: 'error', cause: `git rev-parse ${arg} answered nothing` };
|
|
67
|
+
return { kind: 'ok', line };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const identity = (cwd, line, realpath) => {
|
|
71
|
+
try { return { ok: true, path: realpath(resolve(cwd, line)) }; } catch (err) { return { ok: false, cause: `${line} cannot be resolved (${err?.code ?? err?.message ?? err})` }; }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const located = (state, cause, facts = {}) => ({ state, cause, top: null, gitDir: null, commonDir: null, ...facts });
|
|
75
|
+
|
|
76
|
+
// resolveGitLocation(cwd, { spawn, env, realpath }) → { state, cause, top, gitDir, commonDir }: the
|
|
77
|
+
// REALPATH identity of the git dir, the common dir and the work-tree top, ambient env vs the
|
|
78
|
+
// GIT_-stripped discovery from cwd. Only `work-tree` proceeds.
|
|
79
|
+
export const resolveGitLocation = (cwd, deps = {}) => {
|
|
80
|
+
const spawn = deps.spawn ?? spawnSync;
|
|
81
|
+
const env = deps.env ?? process.env;
|
|
82
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
83
|
+
const ambient = { cwd, env, spawn };
|
|
84
|
+
const stripped = { cwd, env: stripGitLocationEnv(env), spawn };
|
|
85
|
+
// Precedence (fail closed): the GIT DIR probes decide whether each discovery reaches a repository;
|
|
86
|
+
// the COMMON DIR and TOP probes then owe ok (or no-work-tree for the top) — anything else is error.
|
|
87
|
+
const a = probe('--absolute-git-dir', ambient);
|
|
88
|
+
const s = probe('--absolute-git-dir', stripped);
|
|
89
|
+
if (a.kind === 'error' || a.kind === 'no-work-tree') return located('error', a.cause);
|
|
90
|
+
if (s.kind === 'error' || s.kind === 'no-work-tree') return located('error', `under the GIT_-stripped discovery from ${cwd}: ${s.cause}`);
|
|
91
|
+
if (a.kind === 'not-a-repository' && s.kind === 'not-a-repository') return located('not-a-repository', a.cause);
|
|
92
|
+
if (a.kind === 'not-a-repository') return located('redirected', `the ambient git environment (${LOCATION_VARIABLES}) reaches no repository (${a.cause}) while the discovery from ${cwd} does`);
|
|
93
|
+
if (s.kind === 'not-a-repository') return located('env-only', `the ambient git environment (${LOCATION_VARIABLES}) reaches the git dir ${a.line} while the GIT_-stripped discovery from ${cwd} reaches no repository (${s.cause})`);
|
|
94
|
+
const ids = {};
|
|
95
|
+
const ac = probe('--git-common-dir', ambient);
|
|
96
|
+
const sc = probe('--git-common-dir', stripped);
|
|
97
|
+
if (ac.kind !== 'ok') return located('error', `the common dir under the ambient git environment: ${ac.cause}`);
|
|
98
|
+
if (sc.kind !== 'ok') return located('error', `the common dir under the GIT_-stripped discovery: ${sc.cause}`);
|
|
99
|
+
for (const [name, ax, sx] of [['git dir', a, s], ['common dir', ac, sc]]) {
|
|
100
|
+
const x = identity(cwd, ax.line, realpath);
|
|
101
|
+
const y = identity(cwd, sx.line, realpath);
|
|
102
|
+
if (!x.ok) return located('error', `the ${name} under the ambient git environment: ${x.cause}`);
|
|
103
|
+
if (!y.ok) return located('error', `the ${name} under the GIT_-stripped discovery: ${y.cause}`);
|
|
104
|
+
if (x.path !== y.path) return located('redirected', `the ${name} differs: the ambient git environment (${LOCATION_VARIABLES}) resolves it to ${x.path}, the discovery from ${cwd} to ${y.path}`);
|
|
105
|
+
ids[name] = x.path;
|
|
106
|
+
}
|
|
107
|
+
const at = probe('--show-toplevel', ambient);
|
|
108
|
+
const st = probe('--show-toplevel', stripped);
|
|
109
|
+
if (at.kind === 'error' || at.kind === 'not-a-repository') return located('error', at.cause);
|
|
110
|
+
if (st.kind === 'error' || st.kind === 'not-a-repository') return located('error', `under the GIT_-stripped discovery from ${cwd}: ${st.cause}`);
|
|
111
|
+
if (at.kind === 'no-work-tree' && st.kind === 'no-work-tree') return located('no-work-tree', `the git dir ${ids['git dir']} agrees but git names no work tree (${at.cause})`);
|
|
112
|
+
if (at.kind === 'no-work-tree') return located('redirected', `the ambient git environment (${LOCATION_VARIABLES}) names no work tree (${at.cause}) while the discovery from ${cwd} reaches one`);
|
|
113
|
+
if (st.kind === 'no-work-tree') return located('redirected', `the ambient git environment (${LOCATION_VARIABLES}) reaches a work tree at ${at.line} while the discovery from ${cwd} names none (${st.cause})`);
|
|
114
|
+
const x = identity(cwd, at.line, realpath);
|
|
115
|
+
const y = identity(cwd, st.line, realpath);
|
|
116
|
+
if (!x.ok) return located('error', `the work-tree top under the ambient git environment: ${x.cause}`);
|
|
117
|
+
if (!y.ok) return located('error', `the work-tree top under the GIT_-stripped discovery: ${y.cause}`);
|
|
118
|
+
if (x.path !== y.path) return located('redirected', `the work-tree top differs: the ambient git environment (${LOCATION_VARIABLES}) resolves it to ${x.path}, the discovery from ${cwd} to ${y.path}`);
|
|
119
|
+
return located('work-tree', null, { top: at.line, gitDir: ids['git dir'], commonDir: ids['common dir'] });
|
|
120
|
+
};
|
package/tools/grounding.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
33
33
|
import { isDirectRun } from './direct-run.mjs';
|
|
34
34
|
import { fail } from './orchestration-config.mjs';
|
|
35
35
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
36
|
+
import { stripGitLocationEnv } from './git-env.mjs';
|
|
36
37
|
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
37
38
|
// READ core only — never autonomy-write.mjs (the import-split invariant).
|
|
38
39
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
@@ -163,9 +164,9 @@ const systemTempRoots = () => [...new Set([tmpdir(), process.env.TMPDIR, '/tmp']
|
|
|
163
164
|
|
|
164
165
|
const gitLine = (args, cwd) => {
|
|
165
166
|
// Ambient GIT_* location vars (GIT_DIR / GIT_WORK_TREE / …) would let rev-parse prove a FOREIGN
|
|
166
|
-
// tree — every location answer must come from cwd alone, for every gitLine consumer.
|
|
167
|
-
|
|
168
|
-
const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', windowsHide: true });
|
|
167
|
+
// tree — every location answer must come from cwd alone, for every gitLine consumer. The strip
|
|
168
|
+
// is the git-env leaf's (GIT_* only — never the test fixtures' hermetic pins).
|
|
169
|
+
const r = spawnSync('git', args, { cwd, env: stripGitLocationEnv(process.env), encoding: 'utf8', windowsHide: true });
|
|
169
170
|
return r.error || r.status == null ? null : { status: r.status, stdout: r.stdout ?? '' };
|
|
170
171
|
};
|
|
171
172
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { escapeForDisplay, isRenderableLine, shellQuoteArg } from './repo-lex.mjs';
|
|
2
|
+
import { wrapperCmdFor } from './detect-backends.mjs';
|
|
3
|
+
import { DISPLAY_ALIASES } from './recipes.mjs';
|
|
4
|
+
|
|
5
|
+
const CODE_STEP = 'code';
|
|
6
|
+
const FOLD_KIND = 'fold';
|
|
7
|
+
const RETURN_KIND = 'return';
|
|
8
|
+
const EXECUTE_BACKEND = 'codex-cli-bridge';
|
|
9
|
+
const UNPOPULATED_HELD_ID = '<held id>';
|
|
10
|
+
|
|
11
|
+
export const HELD_RECEIPT_BACKEND = DISPLAY_ALIASES[EXECUTE_BACKEND];
|
|
12
|
+
export const HELD_EXECUTE_WRAPPER = wrapperCmdFor(EXECUTE_BACKEND, 'execute');
|
|
13
|
+
|
|
14
|
+
const readsInsideEpoch = (dispatch, head) => {
|
|
15
|
+
if (head.state === 'unborn') return true;
|
|
16
|
+
const instant = Date.parse(dispatch.timestamp);
|
|
17
|
+
return Number.isFinite(instant) && Math.floor(instant / 1000) > head.seconds;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const updatesThread = (threads, nonce, update) =>
|
|
21
|
+
threads.map((thread) => (thread.nonce === nonce ? { ...thread, ...update } : thread));
|
|
22
|
+
|
|
23
|
+
const findsDispatch = (dispatches, nonce) => dispatches.find((dispatch) => dispatch.nonce === nonce) ?? null;
|
|
24
|
+
const findsReturn = (returns, nonce) => returns.find((returned) => returned.nonce === nonce) ?? null;
|
|
25
|
+
const hasCoveredReplacement = (returned, degrades) =>
|
|
26
|
+
degrades.some((degrade) => degrade.fingerprint === returned.postTreeDigest);
|
|
27
|
+
const collectsRetryChainNonces = (dispatches, nonce, collected = new Set()) => {
|
|
28
|
+
if (nonce === null || collected.has(nonce)) return collected;
|
|
29
|
+
const next = new Set(collected).add(nonce);
|
|
30
|
+
return collectsRetryChainNonces(dispatches, findsDispatch(dispatches, nonce)?.retryOf ?? null, next);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const accumulatesFacts = (degrades) => (state, record) => {
|
|
34
|
+
if (record.kind === 'dispatch' && state.epochNonces.has(record.nonce)) {
|
|
35
|
+
const expectedId = state.heldId;
|
|
36
|
+
return {
|
|
37
|
+
...state,
|
|
38
|
+
dispatches: [...state.dispatches, record],
|
|
39
|
+
threads: [...state.threads, { nonce: record.nonce, expectedId, actualId: null, status: 'OPEN' }],
|
|
40
|
+
open: record.baselineClean === false ? [...state.open, { nonce: record.nonce, expectedId }] : state.open,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (record.kind === RETURN_KIND && state.epochNonces.has(record.nonce)) {
|
|
44
|
+
const dispatch = findsDispatch(state.dispatches, record.nonce);
|
|
45
|
+
if (dispatch === null) return state;
|
|
46
|
+
const expectedId = state.threads.find((thread) => thread.nonce === record.nonce)?.expectedId ?? null;
|
|
47
|
+
const status = record.sessionId === null ? 'FAILED'
|
|
48
|
+
: expectedId === null ? 'FIRST'
|
|
49
|
+
: dispatch.retryOf !== null || record.sessionId === expectedId ? 'CONTINUED'
|
|
50
|
+
: 'SUBSTITUTED';
|
|
51
|
+
const substitution = status === 'SUBSTITUTED' && dispatch.baselineClean === false
|
|
52
|
+
&& !hasCoveredReplacement(record, degrades)
|
|
53
|
+
? {
|
|
54
|
+
nonce: record.nonce, expectedId, actualId: record.sessionId,
|
|
55
|
+
postTreeDigest: record.postTreeDigest, folded: false,
|
|
56
|
+
}
|
|
57
|
+
: null;
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
returns: [...state.returns, record],
|
|
61
|
+
threads: updatesThread(state.threads, record.nonce, { actualId: record.sessionId, status }),
|
|
62
|
+
open: state.open.filter((thread) => thread.nonce !== record.nonce),
|
|
63
|
+
substitutions: substitution === null
|
|
64
|
+
? state.substitutions
|
|
65
|
+
: [...state.substitutions, substitution],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (record.kind === FOLD_KIND && state.epochNonces.has(record.nonce)) {
|
|
69
|
+
const dispatch = findsDispatch(state.dispatches, record.nonce);
|
|
70
|
+
const returned = findsReturn(state.returns, record.nonce);
|
|
71
|
+
if (dispatch === null || returned === null || returned.sessionId === null) return state;
|
|
72
|
+
const establishes = state.heldId === null;
|
|
73
|
+
const retries = dispatch.retryOf !== null;
|
|
74
|
+
const replaces = returned.sessionId !== state.heldId && hasCoveredReplacement(returned, degrades);
|
|
75
|
+
const retryChain = retries ? collectsRetryChainNonces(state.dispatches, dispatch.retryOf) : new Set();
|
|
76
|
+
const foldedSubstitutions = state.substitutions.map((substitution) =>
|
|
77
|
+
(substitution.nonce === record.nonce ? { ...substitution, folded: true } : substitution));
|
|
78
|
+
const substitutions = retries
|
|
79
|
+
? foldedSubstitutions.filter((substitution) => !retryChain.has(substitution.nonce))
|
|
80
|
+
: foldedSubstitutions;
|
|
81
|
+
if (establishes || retries || replaces) return { ...state, heldId: returned.sessionId, substitutions };
|
|
82
|
+
return { ...state, substitutions };
|
|
83
|
+
}
|
|
84
|
+
return state;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const countsHeldFolds = (records, epochNonces, heldId) => {
|
|
88
|
+
if (heldId === null) return 0;
|
|
89
|
+
return records.filter((record) => {
|
|
90
|
+
if (record.kind !== FOLD_KIND || !epochNonces.has(record.nonce)) return false;
|
|
91
|
+
return records.find((candidate) => candidate.kind === RETURN_KIND && candidate.nonce === record.nonce)?.sessionId === heldId;
|
|
92
|
+
}).length;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const judgeHeldSession = (records, { head, backend, degrades = [] }) => {
|
|
96
|
+
if (head.state === 'error') {
|
|
97
|
+
return { state: 'error', cause: 'head', reason: head.reason, heldId: null, folds: 0, substitution: null, threads: [], open: [] };
|
|
98
|
+
}
|
|
99
|
+
const dispatches = records.filter((record) =>
|
|
100
|
+
record.kind === 'dispatch'
|
|
101
|
+
&& record.stepClass === CODE_STEP
|
|
102
|
+
&& record.backend === backend
|
|
103
|
+
&& readsInsideEpoch(record, head));
|
|
104
|
+
const epochNonces = new Set(dispatches.map((dispatch) => dispatch.nonce));
|
|
105
|
+
const initial = { epochNonces, dispatches: [], returns: [], heldId: null, substitutions: [], threads: [], open: [] };
|
|
106
|
+
const accumulated = records.reduce(accumulatesFacts(degrades), initial);
|
|
107
|
+
return {
|
|
108
|
+
state: 'ok',
|
|
109
|
+
heldId: accumulated.heldId,
|
|
110
|
+
folds: countsHeldFolds(records, epochNonces, accumulated.heldId),
|
|
111
|
+
substitution: accumulated.substitutions[0] ?? null,
|
|
112
|
+
threads: accumulated.threads,
|
|
113
|
+
open: accumulated.open,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const judgeLedger = (ledger, { backend, degrades = [] }) => {
|
|
118
|
+
if (ledger.state === 'absent') {
|
|
119
|
+
return { state: 'absent', heldId: null, folds: 0, substitution: null, threads: [], open: [] };
|
|
120
|
+
}
|
|
121
|
+
if (ledger.state === 'error') {
|
|
122
|
+
return { state: 'error', cause: 'ledger', reason: ledger.reason, heldId: null, folds: 0, substitution: null, threads: [], open: [] };
|
|
123
|
+
}
|
|
124
|
+
return judgeHeldSession(ledger.records, { head: ledger.head, backend, degrades });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const decideHeldSession = (facts) => {
|
|
128
|
+
if (facts.state === 'absent') {
|
|
129
|
+
const reason = 'no delegation ledger was recorded';
|
|
130
|
+
return { code: 0, reason, line: `held session: none — ${reason}` };
|
|
131
|
+
}
|
|
132
|
+
if (facts.state === 'error') {
|
|
133
|
+
return { code: 1, reason: facts.reason, line: `held session: unavailable — ${facts.reason}` };
|
|
134
|
+
}
|
|
135
|
+
if (facts.substitution !== null) {
|
|
136
|
+
const { nonce, expectedId, actualId } = facts.substitution;
|
|
137
|
+
const reason = `delegated code thread "${nonce}" substituted held session "${escapeForDisplay(expectedId)}" with "${escapeForDisplay(actualId)}"`;
|
|
138
|
+
return { code: 1, reason, line: `held session: SUBSTITUTED — ${reason}` };
|
|
139
|
+
}
|
|
140
|
+
if (facts.heldId === null) return { code: 0, reason: 'no folded code thread in the commit epoch carries a session yet', line: 'held session: none' };
|
|
141
|
+
const heldId = escapeForDisplay(facts.heldId);
|
|
142
|
+
return { code: 0, reason: `held session "${heldId}" is continuous`, line: `held session: ${heldId} — ${facts.folds} fold(s) rode it` };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const describesLaneCaveat = (facts) => {
|
|
146
|
+
if (facts.state === 'absent') return decideHeldSession(facts).reason;
|
|
147
|
+
if (facts.state === 'error') return decideHeldSession(facts).reason;
|
|
148
|
+
if (facts.substitution !== null) return decideHeldSession(facts).reason;
|
|
149
|
+
if (facts.heldId === null) return decideHeldSession(facts).reason;
|
|
150
|
+
if (!isRenderableLine(facts.heldId)) return 'the held session id cannot be rendered on one command line';
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const foldLaneLines = (facts) => {
|
|
155
|
+
const caveat = describesLaneCaveat(facts);
|
|
156
|
+
const heldId = caveat === null ? shellQuoteArg(facts.heldId) : UNPOPULATED_HELD_ID;
|
|
157
|
+
const lines = [
|
|
158
|
+
'Fold lane (execute = delegated) — a fold rides the delegate\'s HELD session:',
|
|
159
|
+
` run: ${HELD_EXECUTE_WRAPPER} --resume ${heldId} --nonce <nonce> <fold-brief>`,
|
|
160
|
+
' the fold brief is a dispatch file carrying the finding and the accepted fold; dispatch open precedes the run, then dispatch return and fold follow it',
|
|
161
|
+
' the orchestrator runs the suites, verifies the returned diff, re-mints the red-proofs and owns the commit',
|
|
162
|
+
' a fresh session is a forbidden substitution; a retry of a failed thread or a recorded execute degrade is the exception; the wrapper sidecar is never read',
|
|
163
|
+
];
|
|
164
|
+
return caveat === null ? lines : [...lines, ` caveat: ${caveat}`];
|
|
165
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstatSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
4
|
+
import { classIds, readShippedRobustnessLiterals } from './robustness-literals.mjs';
|
|
4
5
|
import { segmentPrefixOf, validateSourceSizeConfig } from './source-size-config.mjs';
|
|
5
6
|
import { getLineCount, isSweep, resolveAnchorCandidates, unique } from './plan-shape.mjs';
|
|
6
7
|
|
|
@@ -83,6 +84,14 @@ const loadPractice = (root) => {
|
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
const loadRobustClasses = (deps) => {
|
|
88
|
+
try {
|
|
89
|
+
return classIds(readShippedRobustnessLiterals(deps));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw usageError(`robustness-literals list is unreadable (${error.message})`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
86
95
|
const isInScope = (path, config) => Boolean(config &&
|
|
87
96
|
config.roots.some((root) => segmentPrefixOf(root, path)) &&
|
|
88
97
|
!config.exclude.some((excluded) => segmentPrefixOf(excluded, path)) &&
|
|
@@ -111,6 +120,12 @@ const compileGlob = (pattern) => {
|
|
|
111
120
|
}
|
|
112
121
|
};
|
|
113
122
|
|
|
123
|
+
const expandPatterns = (repoFiles, patterns) => Object.fromEntries(patterns.map((pattern) => {
|
|
124
|
+
const compiled = compileGlob(pattern);
|
|
125
|
+
if (!compiled) throw planError(`unsupported glob in plan path: ${pattern}`);
|
|
126
|
+
return [pattern, repoFiles.filter((path) => compiled.test(path))];
|
|
127
|
+
}));
|
|
128
|
+
|
|
114
129
|
const matchFilesEntry = (entry, ownedPath) => {
|
|
115
130
|
const normalized = entry.replace(/^\.\//, '').replace(/\/$/, '');
|
|
116
131
|
if (normalized === '' || normalized === '.') return { known: true, matches: true };
|
|
@@ -185,20 +200,18 @@ const describePath = (root, path, practice, repoFiles, packageCache) => {
|
|
|
185
200
|
};
|
|
186
201
|
};
|
|
187
202
|
|
|
188
|
-
export const openRepo = (root) => {
|
|
203
|
+
export const openRepo = (root, { robustnessDeps } = {}) => {
|
|
189
204
|
const repoRoot = realpathSync(root);
|
|
190
|
-
return { repoRoot, repoFiles: walkRegularFiles(repoRoot), practice: loadPractice(repoRoot), packageCache: new Map() };
|
|
205
|
+
return { repoRoot, repoFiles: walkRegularFiles(repoRoot), practice: loadPractice(repoRoot), robustClasses: loadRobustClasses(robustnessDeps), packageCache: new Map() };
|
|
191
206
|
};
|
|
192
207
|
|
|
193
|
-
export const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return [pattern, repoFiles.filter((path) => compiled.test(path))];
|
|
199
|
-
}));
|
|
208
|
+
export const expandSweepPaths = (root, patterns) => expandPatterns(walkRegularFiles(realpathSync(root)), patterns);
|
|
209
|
+
|
|
210
|
+
export const buildFacts = (root, { paths = [], robustnessDeps, repo = openRepo(root, { robustnessDeps }) } = {}) => {
|
|
211
|
+
const { repoRoot, repoFiles, practice, robustClasses, packageCache } = repo;
|
|
212
|
+
const expansions = expandPatterns(repoFiles, paths.filter(isSweep));
|
|
200
213
|
const allPaths = unique([...paths, ...Object.values(expansions).flat()]);
|
|
201
214
|
const pathFacts = Object.fromEntries(allPaths.map((path) => [path, describePath(repoRoot, path, practice, repoFiles, packageCache)]));
|
|
202
215
|
const candidates = (suffix, precedingPaths = []) => resolveAnchorCandidates(repoFiles, suffix, precedingPaths);
|
|
203
|
-
return { capDeclared: practice.capDeclared, cap: practice.cap, pathFacts, expansions, repoFiles, candidates };
|
|
216
|
+
return { capDeclared: practice.capDeclared, cap: practice.cap, robustClasses, pathFacts, expansions, repoFiles, candidates };
|
|
204
217
|
};
|
package/tools/plan-shape.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tokenizeMarkdown } from '../references/scripts/markdown-blocks.mjs';
|
|
2
|
+
import { parseRobustTag } from './robustness-literals.mjs';
|
|
2
3
|
|
|
3
4
|
export const PLAN_TITLE_PREFIX = '# Plan: ';
|
|
4
5
|
export const PLAN_HEADINGS = Object.freeze([
|
|
@@ -243,8 +244,15 @@ const getCurrentLines = (rows, facts, verbs) => rows
|
|
|
243
244
|
const checkAuthoring = (parsed, facts) => {
|
|
244
245
|
const findings = [];
|
|
245
246
|
const bullets = getVerificationBullets(parsed.document);
|
|
247
|
+
const robustClasses = new Set(facts.robustClasses ?? []);
|
|
246
248
|
for (const [index, row] of parsed.rows.entries()) {
|
|
247
249
|
if (!row.valid) continue;
|
|
250
|
+
const robust = parseRobustTag(row.responsibility);
|
|
251
|
+
const unknownRobustClass = robust.classes?.find((classId) => !robustClasses.has(classId));
|
|
252
|
+
if (robust.refusal || unknownRobustClass !== undefined) {
|
|
253
|
+
const reason = robust.refusal ? `tag grammar refused ${robust.refusal}` : `class ${unknownRobustClass} is not in the shipped list`;
|
|
254
|
+
findings.push(makeFinding(row.line, 'robust-class', `${row.id} robust tag: ${reason}`, row.id));
|
|
255
|
+
}
|
|
248
256
|
const paths = getConcretePaths(row, facts);
|
|
249
257
|
const expectedKind = row.verb === 'create' ? 'absent' : 'regular';
|
|
250
258
|
if (paths.length === 0 && isSweep(row.path)) findings.push(makeFinding(row.line, 'kind', `${row.id} sweep resolves to no regular path`, row.id));
|
package/tools/procedures.mjs
CHANGED
|
@@ -46,6 +46,8 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from
|
|
|
46
46
|
// orchestration-write (the import-split test pins both direct rules; the TRANSITIVE claim is
|
|
47
47
|
// structural — test/read-graph-purity.test.mjs pins it).
|
|
48
48
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
49
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
50
|
+
import { foldLaneLines, HELD_RECEIPT_BACKEND, judgeLedger } from './held-session.mjs';
|
|
49
51
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
50
52
|
import { readRegistration } from './mcp-registration.mjs';
|
|
51
53
|
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
@@ -422,6 +424,7 @@ const foldScopeAdvice = (activity, config, plans) => {
|
|
|
422
424
|
// only what no canon can: the POPULATED checker commands, and the session register they read. The
|
|
423
425
|
// register is NEVER defaulted by the tool — naming it here is the whole point of the block.
|
|
424
426
|
export const SPEC_CHECK_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'spec-check-cli.mjs');
|
|
427
|
+
export const ROBUSTNESS_BRIEF_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'robustness-brief.mjs');
|
|
425
428
|
const SPEC_OPS_REGISTER = `${PLANS_REL}/spec-ops.list`;
|
|
426
429
|
const specCheckAdvice = (activity) => {
|
|
427
430
|
if (activity !== 'plan-execution') return [];
|
|
@@ -433,6 +436,16 @@ const specCheckAdvice = (activity) => {
|
|
|
433
436
|
];
|
|
434
437
|
};
|
|
435
438
|
|
|
439
|
+
const robustnessBriefAdvice = (activity, plans) => {
|
|
440
|
+
if (activity !== 'plan-execution') return [];
|
|
441
|
+
const operand = populatedPlan(plans);
|
|
442
|
+
return [
|
|
443
|
+
'Robustness brief (every tagged ledger row) — generate the concrete literals before dispatch:',
|
|
444
|
+
` • node ${renderToolPath(ROBUSTNESS_BRIEF_TOOL)} --plan ${operand ?? '<plan-file>'}`,
|
|
445
|
+
...planDiscoveryCaveat(plans, '--plan', 'populate --plan with the plan in flight.', operand === null ? ['--plan'] : []),
|
|
446
|
+
];
|
|
447
|
+
};
|
|
448
|
+
|
|
436
449
|
// The cost-lane advisory block (cost-tiered execution — orchestration.md §5 canon, paraphrased
|
|
437
450
|
// at the point of use like reviewLoopAdvice paraphrases procedures.md Fold + loop / orchestration §4). Rendered UNCONDITIONALLY for
|
|
438
451
|
// every activity — the lanes route EVERY step, review-backed or not (unlike reviewLoopAdvice,
|
|
@@ -579,7 +592,22 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
579
592
|
return lines;
|
|
580
593
|
};
|
|
581
594
|
|
|
582
|
-
const
|
|
595
|
+
const foldLaneAdvice = ({
|
|
596
|
+
activity,
|
|
597
|
+
slots,
|
|
598
|
+
cwd,
|
|
599
|
+
env,
|
|
600
|
+
resolveStore,
|
|
601
|
+
readStore,
|
|
602
|
+
audit,
|
|
603
|
+
readHead,
|
|
604
|
+
}) => {
|
|
605
|
+
if (activity !== 'plan-execution' || slots.find((slot) => slot.slot === 'execute')?.recipe !== 'delegated') return [];
|
|
606
|
+
const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
|
|
607
|
+
return foldLaneLines(judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades: [] }));
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck, robustnessBrief, foldLane }) => {
|
|
583
611
|
const lines = [
|
|
584
612
|
section,
|
|
585
613
|
'',
|
|
@@ -599,6 +627,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
599
627
|
for (const line of dispatchForm({ activity, slot: s.slot, state: v.state })) lines.push(` ${line}`);
|
|
600
628
|
}
|
|
601
629
|
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
630
|
+
if (s.slot === 'execute' && foldLane.length) lines.push('', ...foldLane);
|
|
602
631
|
}
|
|
603
632
|
if ((flowHalves ?? []).length) lines.push('', ...flowHalves);
|
|
604
633
|
const autonomyBlock = autonomyAdvice(activity, autonomy);
|
|
@@ -610,6 +639,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
610
639
|
if (advice.length) lines.push('', ...advice);
|
|
611
640
|
if (foldScope.length) lines.push('', ...foldScope);
|
|
612
641
|
if (specCheck.length) lines.push('', ...specCheck);
|
|
642
|
+
if (robustnessBrief.length) lines.push('', ...robustnessBrief);
|
|
613
643
|
lines.push('', ...costLanesAdvice());
|
|
614
644
|
if (declaredPractice.length) lines.push('', ...declaredPractice);
|
|
615
645
|
if (warnings.length) {
|
|
@@ -619,7 +649,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
619
649
|
return lines.join('\n');
|
|
620
650
|
};
|
|
621
651
|
|
|
622
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck }) => ({
|
|
652
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck, robustnessBrief, foldLane }) => ({
|
|
623
653
|
activity,
|
|
624
654
|
section,
|
|
625
655
|
slots: Object.fromEntries(
|
|
@@ -640,6 +670,8 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
640
670
|
foldScope,
|
|
641
671
|
// ADDITIVE (spec layer 2b): the spec-store block, structured (empty outside plan-execution).
|
|
642
672
|
specCheck,
|
|
673
|
+
robustnessBrief,
|
|
674
|
+
foldLane,
|
|
643
675
|
// ADDITIVE (AD-044 Plan 4): the per-activity autonomy block, structured (empty when unresolvable).
|
|
644
676
|
autonomy: autonomyAdvice(activity, autonomy),
|
|
645
677
|
// ADDITIVE (D-17 U1): the SAME composed lines the human render prints — one array, two renders, so
|
|
@@ -733,9 +765,20 @@ export const main = (argv, ctx = {}) => {
|
|
|
733
765
|
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
734
766
|
const foldScope = foldScopeAdvice(activity, config, plans);
|
|
735
767
|
const specCheck = specCheckAdvice(activity);
|
|
768
|
+
const robustnessBrief = robustnessBriefAdvice(activity, plans);
|
|
769
|
+
const foldLane = foldLaneAdvice({
|
|
770
|
+
activity,
|
|
771
|
+
slots,
|
|
772
|
+
cwd,
|
|
773
|
+
env,
|
|
774
|
+
resolveStore: ctx.resolveDelegationStorePath,
|
|
775
|
+
readStore: ctx.readDelegationStore,
|
|
776
|
+
audit: ctx.auditDelegationStoreSemantics,
|
|
777
|
+
readHead: ctx.readHeadInstant,
|
|
778
|
+
});
|
|
736
779
|
const stdout = json
|
|
737
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck }), null, 2)
|
|
738
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck });
|
|
780
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck, robustnessBrief, foldLane }), null, 2)
|
|
781
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck, robustnessBrief, foldLane });
|
|
739
782
|
if (autonomy?.error) {
|
|
740
783
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
741
784
|
}
|
package/tools/repo-lex.mjs
CHANGED
|
@@ -39,6 +39,35 @@ const LINE_BREAKING_ALL = new RegExp(LINE_BREAKING_SOURCE, 'gu');
|
|
|
39
39
|
export const isRenderableLine = (value) => typeof value === 'string' && !LINE_BREAKING.test(value);
|
|
40
40
|
export const escapeForDisplay = (value) => String(value).replace(LINE_BREAKING_ALL, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
41
41
|
|
|
42
|
+
// The control-byte gate's predicate over ONE byte (an integer 0..255, a Buffer element): C0 minus
|
|
43
|
+
// TAB, LF and CR, plus DEL. Bytes at or above 0x80 are never judged here (UTF-8 validity and C1
|
|
44
|
+
// controls are the gate's stated non-goals). ONE home, beside the artifact-path rule below, which
|
|
45
|
+
// keeps refusing TAB, LF and CR as its bash twins do.
|
|
46
|
+
export const isRefusedControlByte = (byte) => byte <= 0x08 || byte === 0x0b || byte === 0x0c || (byte >= 0x0e && byte <= 0x1f) || byte === 0x7f;
|
|
47
|
+
|
|
48
|
+
// The injective renderer of a path's raw BYTES for a report line: bytes decode as UTF-8 where a
|
|
49
|
+
// sequence is valid, every LINE_BREAKING code point and a valid U+FFFD render as \u{XXXX}, an invalid
|
|
50
|
+
// byte as \xNN and a backslash as \\ — so two names differing only in an unrenderable byte render differently,
|
|
51
|
+
// a name carrying a raw byte differs from a name spelling its escape, and no output line carries a
|
|
52
|
+
// raw control byte or U+FFFD. Separate from escapeForDisplay, whose string-rendering consumers keep their contract.
|
|
53
|
+
const utf8SequenceLength = (lead) => (lead < 0x80 ? 1 : (lead & 0xe0) === 0xc0 ? 2 : (lead & 0xf0) === 0xe0 ? 3 : (lead & 0xf8) === 0xf0 ? 4 : 0);
|
|
54
|
+
export const renderPathForDisplay = (bytes) => {
|
|
55
|
+
let out = '';
|
|
56
|
+
for (let i = 0; i < bytes.length;) {
|
|
57
|
+
const length = utf8SequenceLength(bytes[i]);
|
|
58
|
+
const slice = length > 0 && i + length <= bytes.length ? bytes.subarray(i, i + length) : null;
|
|
59
|
+
const decoded = slice === null ? null : slice.toString('utf8');
|
|
60
|
+
if (decoded === null || !Buffer.from(decoded, 'utf8').equals(slice)) {
|
|
61
|
+
out += `\\x${bytes[i].toString(16).padStart(2, '0')}`;
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out += decoded === '\\' ? '\\\\' : LINE_BREAKING.test(decoded) || decoded.codePointAt(0) === 0xfffd ? `\\u{${decoded.codePointAt(0).toString(16)}}` : decoded;
|
|
66
|
+
i += length;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
};
|
|
70
|
+
|
|
42
71
|
// The receipt encoder's carriability rule for an artifact path (S21), the JS twin of the wrappers'
|
|
43
72
|
// refuse_uncarriable_artifact_byte: a quote, a backslash, a C0 control or DEL — deliberately NOT
|
|
44
73
|
// \p{Cc} (C1 is the declared residual, and the two normalizations are parity-pinned on this set).
|