@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/SKILL.md +9 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
  5. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  6. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  7. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  8. package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
  9. package/bridges/codex-cli-bridge/capability.json +1 -1
  10. package/capability.json +1 -1
  11. package/package.json +1 -1
  12. package/references/modes/commit-guard.md +2 -2
  13. package/references/modes/control-bytes.md +91 -0
  14. package/references/modes/core-evidence.md +1 -1
  15. package/references/modes/flow-writer.md +16 -4
  16. package/references/modes/procedures.md +5 -1
  17. package/references/modes/review-state.md +3 -3
  18. package/references/modes/robustness-brief.md +61 -0
  19. package/references/modes/velocity.md +2 -0
  20. package/references/robustness-literals.json +107 -0
  21. package/tools/changed-surface.mjs +1 -1
  22. package/tools/commands.mjs +14 -0
  23. package/tools/commit-guard.mjs +24 -12
  24. package/tools/control-bytes.mjs +264 -0
  25. package/tools/core-evidence.mjs +33 -20
  26. package/tools/coverage-check.mjs +1 -1
  27. package/tools/dispatch-store-read.mjs +269 -0
  28. package/tools/dispatch-store.mjs +21 -357
  29. package/tools/flow-check-git-lane.mjs +2 -1
  30. package/tools/flow-delta-proof.mjs +4 -4
  31. package/tools/flow-record-shape.mjs +44 -2
  32. package/tools/flow-round-gates.mjs +151 -0
  33. package/tools/flow-store-read.mjs +1 -1
  34. package/tools/flow-writer.mjs +235 -82
  35. package/tools/fold-scope.mjs +7 -3
  36. package/tools/fs-read-nofollow.mjs +56 -4
  37. package/tools/git-env.mjs +120 -0
  38. package/tools/grounding.mjs +4 -3
  39. package/tools/held-session.mjs +165 -0
  40. package/tools/plan-shape-facts.mjs +23 -10
  41. package/tools/plan-shape.mjs +8 -0
  42. package/tools/procedures.mjs +47 -4
  43. package/tools/repo-lex.mjs +29 -0
  44. package/tools/review-state.mjs +127 -39
  45. package/tools/robustness-brief.mjs +209 -0
  46. package/tools/robustness-literals.mjs +101 -0
  47. package/tools/sandbox-masks.mjs +2 -1
  48. package/tools/set-flow.mjs +2 -1
  49. package/tools/source-size-scope.mjs +19 -25
  50. package/tools/tracked-tree-census.mjs +3 -3
  51. package/tools/velocity-profile.mjs +5 -2
  52. package/tools/worktree-handoff-return.mjs +2 -2
  53. package/tools/worktrees.mjs +2 -2
@@ -0,0 +1,101 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
4
+
5
+ export const KINDS = Object.freeze(['env', 'flag', 'ref', 'syscall', 'errno', 'state', 'argv']);
6
+ const KIND_SET = new Set(KINDS);
7
+ const TOP_KEYS = Object.freeze(['schema', 'version', 'classes']);
8
+ const CLASS_KEYS = Object.freeze(['id', 'prove', 'members']);
9
+ const MEMBER_KEYS = Object.freeze(['literal', 'kind', 'note', 'source']);
10
+ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
11
+ const ADR = /^AD-\d+$/u;
12
+ const CONTROL = /[\p{Cc}\u2028\u2029\uFFFD]/u;
13
+ const SHIPPED_LIST = fileURLToPath(new URL('../references/robustness-literals.json', import.meta.url));
14
+
15
+ const refuse = (code, detail) => {
16
+ throw Object.assign(new Error(`robustness-literals: ${code}${detail ? ` — ${detail}` : ''}`), { code });
17
+ };
18
+ const requireObject = (value, label) => {
19
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) refuse('type', `${label} must be an object`);
20
+ };
21
+ const requireKeys = (value, keys, label) => {
22
+ requireObject(value, label);
23
+ const unknown = Object.keys(value).find((key) => !keys.includes(key));
24
+ if (unknown !== undefined) refuse('unknown-key', `${label}.${unknown}`);
25
+ const missing = keys.find((key) => !Object.hasOwn(value, key));
26
+ if (missing !== undefined) refuse('missing-key', `${label}.${missing}`);
27
+ };
28
+ const requireString = (value, label) => {
29
+ if (typeof value !== 'string') refuse('type', `${label} must be a string`);
30
+ if (value.length === 0) refuse('empty-string', label);
31
+ if (CONTROL.test(value)) refuse('control', label);
32
+ };
33
+ const isRepoSource = (source) => !source.startsWith('/') && !source.includes('\\') &&
34
+ !source.split('/').includes('..') && (source.includes('/') || source.includes('.'));
35
+ const freezeList = (list) => Object.freeze({
36
+ ...list,
37
+ classes: Object.freeze(list.classes.map((entry) => Object.freeze({
38
+ ...entry,
39
+ members: Object.freeze(entry.members.map((member) => Object.freeze(member))),
40
+ }))),
41
+ });
42
+
43
+ const validateList = (value) => {
44
+ requireKeys(value, TOP_KEYS, 'list');
45
+ if (value.schema !== 1) refuse('schema', 'schema must be 1');
46
+ if (!Number.isSafeInteger(value.version) || value.version < 1) refuse('version', 'version must be an integer >= 1');
47
+ if (!Array.isArray(value.classes)) refuse('type', 'classes must be an array');
48
+ if (value.classes.length === 0) refuse('empty-class', 'classes must not be empty');
49
+ const ids = new Set();
50
+ for (const [classIndex, entry] of value.classes.entries()) {
51
+ const label = `classes[${classIndex}]`;
52
+ requireKeys(entry, CLASS_KEYS, label);
53
+ requireString(entry.id, `${label}.id`);
54
+ if (!SLUG.test(entry.id)) refuse('class-id', entry.id);
55
+ if (ids.has(entry.id)) refuse('duplicate-class', entry.id);
56
+ ids.add(entry.id);
57
+ requireString(entry.prove, `${label}.prove`);
58
+ if (!Array.isArray(entry.members)) refuse('type', `${label}.members must be an array`);
59
+ if (entry.members.length === 0) refuse('empty-class', entry.id);
60
+ const literals = new Set();
61
+ for (const [memberIndex, member] of entry.members.entries()) {
62
+ const memberLabel = `${label}.members[${memberIndex}]`;
63
+ requireKeys(member, MEMBER_KEYS, memberLabel);
64
+ for (const key of MEMBER_KEYS) requireString(member[key], `${memberLabel}.${key}`);
65
+ if (literals.has(member.literal)) refuse('duplicate-literal', `${entry.id}:${member.literal}`);
66
+ literals.add(member.literal);
67
+ if (!KIND_SET.has(member.kind)) refuse('kind', member.kind);
68
+ if (!ADR.test(member.source) && !SLUG.test(member.source) && !isRepoSource(member.source)) refuse('source', member.source);
69
+ }
70
+ }
71
+ return freezeList(value);
72
+ };
73
+
74
+ export const readRobustnessLiterals = (path, io = {}) => {
75
+ const read = readRegularFileNoFollow(path, io);
76
+ if (read.outcome !== 'ok') refuse('type', `${path} must be a readable regular file (${read.className ?? read.code ?? read.outcome})`);
77
+ try {
78
+ return validateList(JSON.parse(read.content));
79
+ } catch (error) {
80
+ if (error?.code) throw error;
81
+ refuse('type', `${path} is not valid JSON (${error.message})`);
82
+ }
83
+ };
84
+
85
+ export const readShippedRobustnessLiterals = (deps = {}) =>
86
+ readRobustnessLiterals((deps.resolvePath ?? (() => SHIPPED_LIST))(), deps.io);
87
+
88
+ export const parseRobustTag = (text) => {
89
+ const matches = [...String(text ?? '').matchAll(/robust:(\S*)/gu)];
90
+ if (matches.length === 0) return { classes: [] };
91
+ if (matches.length > 1) return { refusal: 'multiple-tags' };
92
+ if (matches[0][1] === '') return { refusal: 'no-class' };
93
+ const classes = matches[0][1].split(',');
94
+ if (classes.some((entry) => entry === '')) return { refusal: 'empty-class' };
95
+ if (new Set(classes).size !== classes.length) return { refusal: 'duplicate-class' };
96
+ if (classes.some((entry) => !SLUG.test(entry))) return { refusal: 'invalid-class' };
97
+ return { classes };
98
+ };
99
+
100
+ export const classIds = (list) => Object.freeze(list.classes.map((entry) => entry.id));
101
+ export const digestOf = (list) => createHash('sha256').update(JSON.stringify(list.classes)).digest('hex');
@@ -42,6 +42,7 @@ import { isDirectRun } from './direct-run.mjs';
42
42
  import { fail } from './orchestration-config.mjs';
43
43
  import { isNeverCommittableStat, shellQuoteArg } from './review-state.mjs';
44
44
  import { assertContainedRealPath } from './fs-safe.mjs';
45
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
45
46
 
46
47
  export const MASKS_FENCE_START = '# >>> agent-workflow sandbox-masks — managed block, fully REPLACED by the kit sandbox-masks lane; do not hand-edit inside >>>';
47
48
  export const MASKS_FENCE_END = '# <<< agent-workflow sandbox-masks <<<';
@@ -59,7 +60,7 @@ const gitLine = (args, cwd) => {
59
60
  // The UNFILTERED untracked walk: --others WITHOUT --exclude-standard, so a mask already hidden by
60
61
  // an earlier apply stays visible to a rerun (the full-block replace re-derives it, codex).
61
62
  const listUntrackedUnfilteredZ = (root) => {
62
- const r = spawnSync('git', ['ls-files', '--others', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true });
63
+ const r = spawnSync('git', ['ls-files', '--others', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
63
64
  if (r.error || r.status !== 0) return null;
64
65
  return r.stdout.split('\0').filter(Boolean);
65
66
  };
@@ -50,6 +50,7 @@ import { loadDeclaration } from './run-gates.mjs';
50
50
  import { compareSemver } from './semver-lite.mjs';
51
51
  import { lexicalRepoRelative } from './core-evidence.mjs';
52
52
  import { readAuthoritativeVersion } from './manifest/validate.mjs';
53
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
53
54
 
54
55
  const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
55
56
  const GATES_REL = 'docs/ai/gates.json';
@@ -197,7 +198,7 @@ const meetsVersionFloor = (version, floor) => {
197
198
  return cmp !== null && cmp >= 0;
198
199
  };
199
200
 
200
- const defaultRunGit = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: 64 * 1024 * 1024, windowsHide: true });
201
+ const defaultRunGit = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
201
202
 
202
203
  const gitToplevel = (cwd, runGit) => {
203
204
  const r = runGit(['rev-parse', '--show-toplevel'], cwd);
@@ -18,37 +18,18 @@ import { readFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { spawnSync } from 'node:child_process';
20
20
  import { configFail, configPathFor, scopeFail } from './source-size-refusal.mjs';
21
+ import { GIT_MAX_BUFFER, parseIndexEntries, resolveGitLocation, withGitPath } from './git-env.mjs';
21
22
 
22
23
  const LF = 0x0a;
23
24
  const CR = 0x0d;
24
- const TAB = 0x09;
25
25
  const SLASH = 0x2f;
26
26
 
27
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
28
27
  const SYMLINK_MODE = '120000';
29
28
  const GITLINK_MODE = '160000';
30
29
 
31
- // Raw bytes in, raw bytes out: `ls-files -s -z` emits mode, object and stage, then a TAB, then the
32
- // path, then a NUL — and the path half is never decoded before it has been matched, so a name
33
- // carrying a tab or a newline survives intact (splitting on lines would mangle it).
34
- // The object id is carried through rather than dropped: the record already holds it, and a consumer
35
- // that must read what the index HOLDS (not what the worktree happens to show) needs exactly that.
36
- const parseIndexEntries = (buf) => {
37
- const entries = [];
38
- let start = 0;
39
- while (start < buf.length) {
40
- let end = buf.indexOf(0, start);
41
- if (end === -1) end = buf.length;
42
- const record = buf.subarray(start, end);
43
- const tab = record.indexOf(TAB);
44
- if (tab !== -1) {
45
- const head = record.subarray(0, tab).toString('utf8').split(' ');
46
- entries.push({ mode: head[0], sha: head[1], stage: Number(head[2]), path: record.subarray(tab + 1) });
47
- }
48
- start = end + 1;
49
- }
50
- return entries;
51
- };
30
+ // The `ls-files -s -z` record parser lives in the git leaf (raw bytes in, raw path bytes out, the
31
+ // object id carried through so a consumer reading what the index HOLDS has it) — one home shared
32
+ // with the control-byte gate.
52
33
 
53
34
  // A path whose bytes are not valid UTF-8 round-trips to something DIFFERENT (the replacement char is
54
35
  // lossy) — that is the whole test, and it is exact.
@@ -65,11 +46,24 @@ const bufSegmentPrefix = (path, prefix) =>
65
46
  const bufEndsWith = (path, suffix) =>
66
47
  path.length >= suffix.length && path.subarray(path.length - suffix.length).equals(suffix);
67
48
 
49
+ // The enumeration-error class (exit 2) also refuses every git LOCATION but a work tree, by its own
50
+ // state name — a redirecting GIT_DIR would otherwise enumerate ANOTHER repository's index in
51
+ // silence — and a runner that throws, which is an answer here, never a raw stack.
68
52
  export const enumerateIndex = (cwd, deps = {}) => {
69
53
  const spawn = deps.spawn ?? spawnSync;
70
- const result = spawn('git', ['ls-files', '-s', '-z'], { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
54
+ const env = deps.env ?? process.env;
55
+ const location = resolveGitLocation(cwd, { spawn, env });
56
+ if (location.state !== 'work-tree') {
57
+ throw configFail(`the git index could not be enumerated in ${cwd} (the git location is ${location.state}: ${location.cause}) — the declared scope is unknown, so nothing is judged`);
58
+ }
59
+ let result;
60
+ try {
61
+ result = spawn('git', ['ls-files', '-s', '-z'], { cwd, env: withGitPath(env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
62
+ } catch (err) {
63
+ result = { error: err };
64
+ }
71
65
  if (result.error || result.status !== 0) {
72
- const why = result.error ? result.error.message : `git exited ${result.status}`;
66
+ const why = result.error ? result.error.message : result.signal ? `git was killed by ${result.signal}` : `git exited ${result.status}`;
73
67
  throw configFail(`the git index could not be enumerated in ${cwd} (${why}) — the declared scope is unknown, so nothing is judged`);
74
68
  }
75
69
  return parseIndexEntries(result.stdout);
@@ -1,6 +1,6 @@
1
1
  // tracked-tree-census.mjs — how much of a project's TRACKED tree the changed-line coverage domain
2
2
  // can actually assess, in the closed vocabulary that domain already speaks. A LEAF: it imports the
3
- // classification and nothing else, and it spawns exactly one read-only `git ls-files`.
3
+ // changed-surface classification and the shared git-spawn buffer from the git-location leaf, and it spawns exactly one read-only `git ls-files`.
4
4
  //
5
5
  // Why this exists: the coverage checker's domain is `.mjs/.cjs/.js` by design, and on a TS project
6
6
  // that domain is a rounding error of the tree. Certifying it and calling the flow optimal is the
@@ -10,9 +10,9 @@
10
10
  // Read-only: never writes, never commits. Dependency-free, Node >= 22. No side effects on import.
11
11
 
12
12
  import { spawnSync } from 'node:child_process';
13
- import { classifyChangedPath } from './changed-surface.mjs';
14
13
 
15
- const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a large tracked tree; never truncate
14
+ import { classifyChangedPath } from './changed-surface.mjs';
15
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
16
16
 
17
17
  export const CENSUS_VERDICT = Object.freeze({ NARROW: 'domain-narrow', WITHIN_DOMAIN: 'within-domain' });
18
18
 
@@ -133,7 +133,7 @@ export const SHELL_READONLY = Object.freeze([
133
133
  // seed time from the RUNNING tool's own location — resolved-absolute, so a moved or reinstalled
134
134
  // skill leaves a stale rule that FAIL-SAFE prompts again (never a silent widening).
135
135
  //
136
- // Membership (13, frozen): the read-only kit tools plus run-gates.mjs — which is NOT read-only but
136
+ // Membership (14, frozen): the read-only kit tools plus run-gates.mjs — which is NOT read-only but
137
137
  // project-exec (it runs the project's OWN declared gates.json commands), so it seeds as ONE exact
138
138
  // byte-string pinned to this project root (`--cwd <resolved root>`): a wildcard would be BROADER
139
139
  // than the AD-037 hook boundary (`--cwd <dir>` executes an arbitrary OTHER project's gates.json)
@@ -163,6 +163,9 @@ export const KIT_READONLY_TOOLS = Object.freeze([
163
163
  // The round table the procedures advisor names every review round; its path argument is resolved
164
164
  // and compared, never opened as a pattern, so it rides the settings-level posture like the rest.
165
165
  'tools/review-rounds-cli.mjs',
166
+ // The control-byte gate: a pure reader over the work tree, run by hand before the matrix runs it.
167
+ 'tools/control-bytes.mjs',
168
+ 'tools/robustness-brief.mjs',
166
169
  ]);
167
170
  // Writer previews: ONLY writers whose ARG-FREE invocation is a documented dry-run ("Default is
168
171
  // --dry-run" in their usage) seed an EXACT preview byte-string — every --apply/--write/--yes keeps
@@ -780,7 +783,7 @@ export const screenAllowlistEntry = (pattern) => {
780
783
  };
781
784
 
782
785
  /**
783
- * Derive the opt-in kit-tools tier for a project: 12 wildcard entries (resolved-absolute script
786
+ * Derive the opt-in kit-tools tier for a project: 13 wildcard entries (resolved-absolute script
784
787
  * path + args wildcard), ONE exact run-gates entry pinned to the resolved project root, and the
785
788
  * writer-preview exact dry-run byte-strings. Pure derivation — a stale path simply prompts again.
786
789
  */
@@ -34,12 +34,14 @@
34
34
  import { spawnSync } from 'node:child_process';
35
35
  import { lstatSync, readdirSync, realpathSync } from 'node:fs';
36
36
  import { createHash } from 'node:crypto';
37
+
37
38
  import { findSatelliteEntry, readSatelliteIdentity, WORKTREES_STOP } from './satellite-locator.mjs';
38
39
  import { locateProvisionRecordSection, parseProvisionRecord, displayValue } from './worktrees-record.mjs';
39
40
  import { readFileBytesNoFollow } from './fs-read-nofollow.mjs';
40
41
  import { computeNumerator } from './dispatch-record.mjs';
41
42
  import { resolveRepoRoot, formatRatio, buildObservationRecord } from './observation-builder.mjs';
42
43
  import { appendDelegationRecord, DELEGATION_STORE_STOP } from './dispatch-store.mjs';
44
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
43
45
 
44
46
  // The worktrees slug grammar, repeated here so the CLI can refuse a malformed slug as USAGE before
45
47
  // any probe echoes it — the locator's own refusal interpolates the slug into a terminal message.
@@ -57,8 +59,6 @@ const PROVENANCE = 'self-reported';
57
59
  const REGULAR_MODES = new Set(['100644', '100755']);
58
60
  const RECORD_HEADING = '## Provision record';
59
61
 
60
- const GIT_MAX_BUFFER = 64 * 1024 * 1024;
61
-
62
62
  const defaultGit = (args, cwd) => {
63
63
  const r = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true, maxBuffer: GIT_MAX_BUFFER });
64
64
  return {
@@ -16,6 +16,7 @@ import { join, dirname, basename, resolve, relative, isAbsolute, sep } from 'nod
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { spawnSync } from 'node:child_process';
18
18
  import { randomBytes } from 'node:crypto';
19
+
19
20
  import { MCP_JSON_REL, SERVER_NAME, SETTINGS_REL, withoutRegistration } from './mcp-registration.mjs';
20
21
  import {
21
22
  KIT_OWN_PATHS, KNOWN_FOOTPRINT, expandGlob, normalizeSlashes, isDirPattern, isGlobPattern,
@@ -25,6 +26,7 @@ import { isDirectRun } from './direct-run.mjs';
25
26
  import { isScratchPlanName, plansInFlight, PLANS_REL, shellQuoteArg } from './review-state.mjs';
26
27
  import { writeContainedFileAtomic } from './atomic-write.mjs';
27
28
  import { assertContainedRealPath } from './fs-safe.mjs';
29
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
28
30
  import { isFinalCapableDeclaration } from './run-gates.mjs';
29
31
  import {
30
32
  WORKTREES_STOP, stop, EXIT, handoffBasename, recordValue, hasControlByte, displayValue,
@@ -76,8 +78,6 @@ const TRACKED_PIN_DECLARATION =
76
78
  const TRANSFER_EXCLUSIONS = Object.freeze([':!docs/ai', ':!docs/plans']);
77
79
  const PREPARE_LOCK_BASENAME = 'aw-prepare-lock';
78
80
 
79
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
80
-
81
81
  const USAGE = [
82
82
  'usage: worktrees.mjs <subcommand> [args]',
83
83
  '',