@sabaiway/agent-workflow-kit 5.6.0 → 5.8.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 +92 -0
  2. package/README.md +2 -2
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +26 -17
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -5
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +13 -4
  7. package/bridges/antigravity-cli-bridge/bin/agy.sh +7 -4
  8. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +24 -0
  9. package/bridges/antigravity-cli-bridge/capability.json +3 -3
  10. package/bridges/antigravity-cli-bridge/references/driving-agy.md +9 -8
  11. package/bridges/antigravity-cli-bridge/references/models-and-flags.md +31 -14
  12. package/bridges/antigravity-cli-bridge/setup/README.md +4 -3
  13. package/capability.json +1 -1
  14. package/package.json +1 -1
  15. package/references/hooks/gate-approve.mjs +7 -1
  16. package/references/modes/doc-parity.md +1 -1
  17. package/references/modes/gates.md +16 -3
  18. package/references/modes/grounding.md +4 -3
  19. package/references/modes/recommendations.md +3 -0
  20. package/references/modes/review-state.md +1 -1
  21. package/references/modes/setup.md +18 -2
  22. package/references/modes/upgrade.md +38 -18
  23. package/references/scripts/migrate-gates-branches.test.mjs +146 -1
  24. package/references/scripts/migrate-gates.mjs +295 -60
  25. package/references/scripts/migrate-gates.test.mjs +206 -14
  26. package/references/shared/deploy-tail.md +1 -1
  27. package/references/templates/gates.json +1 -1
  28. package/tools/ack-write.mjs +20 -11
  29. package/tools/atomic-write.mjs +71 -18
  30. package/tools/checker-claim.mjs +100 -0
  31. package/tools/coverage-producer.mjs +43 -6
  32. package/tools/direct-run.mjs +76 -0
  33. package/tools/doc-parity.mjs +34 -3
  34. package/tools/engine-source.mjs +12 -8
  35. package/tools/ensure-configs.mjs +141 -0
  36. package/tools/ensure-ops.mjs +284 -0
  37. package/tools/ensure-vocabulary.mjs +71 -0
  38. package/tools/gates-declaration.mjs +23 -10
  39. package/tools/gates-init.mjs +6 -3
  40. package/tools/grounding.mjs +105 -16
  41. package/tools/hide-footprint.mjs +21 -3
  42. package/tools/lens-region.mjs +74 -23
  43. package/tools/orchestration-config.mjs +5 -3
  44. package/tools/orchestration-write.mjs +7 -0
  45. package/tools/recommendations.mjs +315 -66
  46. package/tools/refresh-parity.mjs +263 -0
  47. package/tools/run-gates.mjs +8 -5
  48. package/tools/setup-backends.mjs +88 -77
  49. package/tools/source-size-check.mjs +6 -16
  50. package/tools/source-size-core.mjs +7 -1
  51. package/tools/source-size-gate-cmd.mjs +18 -46
  52. package/tools/tracked-tree-census.mjs +102 -0
  53. package/tools/upgrade-runlist.mjs +92 -0
@@ -1,55 +1,27 @@
1
- // source-size-gate-cmd.mjs — whether a declared gate cmd IS this checker. Mirrors the SHAPE of the
2
- // review-dependent matcher (gates-declaration.mjs) without joining either of its arrays: this gate is
3
- // neither a final core check nor review-dependent.
1
+ // source-size-gate-cmd.mjs — whether a declared gate cmd IS this checker, and when it is not —
2
+ // WHICH of the three claims it makes instead. The shape, admissibility and realpath screens live in
3
+ // checker-claim.mjs (the ONE home, twinned into the standalone migration); this module owns only
4
+ // the binding to THIS tool. Mirrors the SHAPE of the review-dependent matcher
5
+ // (gates-declaration.mjs) without joining either of its arrays: this gate is neither a final core
6
+ // check nor review-dependent.
4
7
  //
5
8
  // Dependency-free, Node >= 22. No side effects on import.
6
9
 
7
- import { realpathSync } from 'node:fs';
8
- import { isAbsolute, join } from 'node:path';
9
10
  import { fileURLToPath } from 'node:url';
11
+ import { CHECKER_CLAIM, checkerClaimTool, classifyCheckerClaim, dqUnsafePath } from './checker-claim.mjs';
10
12
 
11
- // STRICT full command — `node` + ONE (quoted or bare) path token + the exact basename + ` --check` +
12
- // END — and the token must realpath-resolve to THIS kit's own checker, so an id squatter never
13
- // matches.
14
- //
15
- // Separators are PLAIN SPACES, not \s: a newline between the tokens is not a command a runner would
16
- // execute as written. The token is screened by the rules of the quoting it actually carries, because
17
- // the two halves are interpreted differently and a single screen would be wrong for one of them:
18
- // • QUOTED — double quotes survive most bytes, so only what breaks OUT of them is refused.
19
- // • BARE — anything the shell may split, expand or glob makes the executed command different
20
- // from the string, so a bare token is admitted only from a known-safe alphabet.
21
- // Either way the point is the same: a path that resolves literally here while the shell would read
22
- // it differently must never be called canonical, or the matcher certifies a command that never runs.
23
- export const dqUnsafePath = (text) => [...text].some((ch) => {
24
- const code = ch.codePointAt(0);
25
- return ch === '"' || ch === '$' || code === 96 || code === 92 || code === 13 || code === 10;
26
- });
27
-
28
- // Stated as the bytes the shell ACTS on, not as an alphabet of blessed ones: an allow-list refuses
29
- // perfectly ordinary paths (`@`, `+`, `,`, `%`, `=`, anything non-ASCII) that the shell passes
30
- // through verbatim, and refusing a command that really is canonical is its own defect. Whitespace
31
- // and ASCII control bytes are refused too — a bare token cannot contain them and still be one token.
32
- const SHELL_ACTIVE_BARE = new Set([...'"\'\\$|&;<>(){}[]*?!#~^`']);
33
- const bareTokenSafe = (text) => text.length > 0 && ![...text].some((ch) => {
34
- const code = ch.codePointAt(0);
35
- return code <= 0x20 || code === 0x7f || SHELL_ACTIVE_BARE.has(ch);
36
- });
13
+ export { dqUnsafePath };
37
14
 
38
- const CHECK_CMD_RE = /^node +(?:"((?:[^"]*[/\\])?source-size-check\.mjs)"|((?:[^\s"]*[/\\])?source-size-check\.mjs)) +--check$/;
39
15
  export const SOURCE_SIZE_GATE_ID = 'source-size';
40
16
  export const SOURCE_SIZE_TOOL_PATH = fileURLToPath(new URL('./source-size-check.mjs', import.meta.url));
41
17
 
42
- export const matchesSourceSizeGate = (cmd, projectDir) => {
43
- if (typeof cmd !== 'string') return false;
44
- const match = CHECK_CMD_RE.exec(cmd.trim());
45
- if (!match) return false;
46
- const token = match[1] ?? match[2];
47
- const admissible = match[1] !== undefined ? !dqUnsafePath(token) : bareTokenSafe(token);
48
- if (!admissible) return false;
49
- const abs = isAbsolute(token) ? token : join(projectDir, token);
50
- try {
51
- return realpathSync(abs) === realpathSync(SOURCE_SIZE_TOOL_PATH);
52
- } catch {
53
- return false; // unresolvable → never canonical (fail closed)
54
- }
55
- };
18
+ const SOURCE_SIZE_TOOL = checkerClaimTool('source-size-check.mjs', SOURCE_SIZE_TOOL_PATH);
19
+
20
+ // The three-outcome read: `canonical` (this copy), `tool-elsewhere` (the same invocation shape
21
+ // resolving to another real copy of the checker — a vendored deployment, not an absence), or
22
+ // `not-the-tool`.
23
+ export const classifySourceSizeGate = (cmd, projectDir) => classifyCheckerClaim(SOURCE_SIZE_TOOL, cmd, projectDir);
24
+
25
+ // The boolean surface every existing consumer already asks through — "is this gate MY checker?" —
26
+ // kept exactly as narrow as it was: only the canonical claim answers yes.
27
+ export const matchesSourceSizeGate = (cmd, projectDir) => classifySourceSizeGate(cmd, projectDir) === CHECKER_CLAIM.CANONICAL;
@@ -0,0 +1,102 @@
1
+ // tracked-tree-census.mjs — how much of a project's TRACKED tree the changed-line coverage domain
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`.
4
+ //
5
+ // Why this exists: the coverage checker's domain is `.mjs/.cjs/.js` by design, and on a TS project
6
+ // that domain is a rounding error of the tree. Certifying it and calling the flow optimal is the
7
+ // false green one layer up. The census is the FACT that turns "certified" into "certified over the
8
+ // assessable minority" — it never changes what a run may certify.
9
+ //
10
+ // Read-only: never writes, never commits. Dependency-free, Node >= 22. No side effects on import.
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { classifyChangedPath } from './changed-surface.mjs';
14
+
15
+ const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a large tracked tree; never truncate
16
+
17
+ export const CENSUS_VERDICT = Object.freeze({ NARROW: 'domain-narrow', WITHIN_DOMAIN: 'within-domain' });
18
+
19
+ // The tracked tree is listed with -z and split on NUL. A newline-split of the plain form would be
20
+ // wrong twice over: git C-QUOTES a path carrying quotes/control/non-ASCII bytes (so the classifier
21
+ // would read `"src/\303\251.ts"`, extension and all, as some other path), and a path containing a
22
+ // real newline would split into two phantom entries. -z emits raw bytes and never quotes.
23
+ //
24
+ // DE-DUPLICATED, because `ls-files` lists per INDEX ENTRY, not per file: during an unresolved merge
25
+ // one conflicted path appears once per stage (probed: three times for a content conflict). Counting
26
+ // those would inflate one population and could flip a tie into the narrow verdict on nothing but a
27
+ // merge in progress.
28
+ //
29
+ // The split and the de-duplication happen on RAW BYTES, before any decoding. A filename is a byte
30
+ // string on this platform and need not be valid UTF-8; decoding first turns every invalid sequence
31
+ // into the same replacement character, so two genuinely different paths would collapse into one and
32
+ // the de-duplication — added to fix an over-count — would become an UNDER-count. Decoding happens
33
+ // once per surviving entry, for the classifier only.
34
+ const NUL = 0;
35
+ const splitOnNul = (buffer) => {
36
+ const parts = [];
37
+ let start = 0;
38
+ for (let at = buffer.indexOf(NUL, start); at !== -1; at = buffer.indexOf(NUL, start)) {
39
+ if (at > start) parts.push(buffer.subarray(start, at));
40
+ start = at + 1;
41
+ }
42
+ if (start < buffer.length) parts.push(buffer.subarray(start));
43
+ return parts;
44
+ };
45
+
46
+ const listTrackedPaths = (root, spawn) => {
47
+ const result = spawn('git', ['ls-files', '-z'], { cwd: root, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
48
+ if (result.error || result.status !== 0) {
49
+ const reason = result.error ? (result.error.code ?? result.error.message) : `git exited ${result.status}`;
50
+ throw Object.assign(new Error(`tracked-tree census unavailable: ${reason}`), { code: 'CENSUS_UNAVAILABLE' });
51
+ }
52
+ // A spawn seam may hand back a string (an injected fixture); anything else is the real Buffer.
53
+ const raw = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(String(result.stdout ?? ''), 'utf8');
54
+ const seenBytes = new Set();
55
+ const paths = [];
56
+ for (const part of splitOnNul(raw)) {
57
+ const identity = part.toString('latin1'); // byte-exact key: one char per byte, never lossy
58
+ if (seenBytes.has(identity)) continue;
59
+ seenBytes.add(identity);
60
+ paths.push(part.toString('utf8'));
61
+ }
62
+ return paths;
63
+ };
64
+
65
+ // takeCensus(root) → { counts, unsupportedExtensions, verdict, total }.
66
+ //
67
+ // The verdict fires on STRICT DOMINANCE only — the unsupported population must strictly outnumber
68
+ // the assessable one. A tie, or a lone `.d.ts` shim beside a real JS tree, is not a narrow domain
69
+ // and must not raise an item the project cannot act on. `.d.ts` counts as unsupported like any
70
+ // other `.ts` (stated: the classification is by extension, and this leaf adds no exceptions to it).
71
+ // Anything outside both sets — `.py`, `.go`, a README — is `out-of-domain` and counted, never
72
+ // judged: detecting whole out-of-domain-language projects is deliberately not this census's job.
73
+ // An UNAVAILABLE census throws (a non-git tree, a broken git) — the caller's stated-skip lane. It
74
+ // never returns a verdict it could not compute; a silent "within-domain" would be the same false
75
+ // green one layer down.
76
+ export const takeCensus = (root, { spawn = spawnSync } = {}) => {
77
+ const counts = { assessable: 0, unsupported: 0, 'out-of-domain': 0, 'excluded-test': 0 };
78
+ const unsupportedExtensions = new Set();
79
+ for (const path of listTrackedPaths(root, spawn)) {
80
+ const kind = classifyChangedPath(path);
81
+ counts[kind] += 1;
82
+ if (kind === 'unsupported') {
83
+ const base = path.split('/').pop();
84
+ unsupportedExtensions.add(base.slice(base.lastIndexOf('.')));
85
+ }
86
+ }
87
+ const verdict = counts.unsupported > counts.assessable ? CENSUS_VERDICT.NARROW : CENSUS_VERDICT.WITHIN_DOMAIN;
88
+ return {
89
+ counts,
90
+ unsupportedExtensions: [...unsupportedExtensions].sort(),
91
+ verdict,
92
+ total: Object.values(counts).reduce((sum, n) => sum + n, 0),
93
+ };
94
+ };
95
+
96
+ // censusFact(census) → the canonical string an acknowledgment binds. It carries the VERDICT and the
97
+ // sorted set of unsupported extensions present — never the per-file counts. A count-bound fact
98
+ // would re-fire the moment any file is added, turning a durable acknowledgment into a nag; the
99
+ // FACT the maintainer acknowledged ("this tree is dominated by .ts/.tsx, and certification covers
100
+ // the JS minority") is exactly what stays stable while the tree grows, and exactly what changes
101
+ // when a new unsupported language arrives or the verdict flips back.
102
+ export const censusFact = (census) => `${census.verdict}:${census.unsupportedExtensions.join(',')}`;
@@ -0,0 +1,92 @@
1
+ // upgrade-runlist.mjs — the ORDERED registry of the upgrade step-3 operations: the ONE home for
2
+ // their identity (stable id · exact command · consent gate · relayed outcome vocabulary).
3
+ //
4
+ // references/modes/upgrade.md step 3 opens with a checklist rendered from these entries — the
5
+ // structure test (test/upgrade-runlist.test.mjs) binds checklist rows ↔ entries: same backticked
6
+ // ids, same order, each row carrying its registry command and naming its outcomes — and the future
7
+ // reconcile driver reads its item tokens from here (queue row UPGRADE-RECONCILE-DRIVER).
8
+ //
9
+ // A PURE LEAF with zero imports (the ensure-vocabulary.mjs pattern): reading operation identity
10
+ // must never drag an operation's implementation — and through it the writers — into a read-only
11
+ // consumer's import graph. The `configs` outcomes are a literal copy of RELAYED_ENSURE_TOKENS
12
+ // (ensure-vocabulary.mjs stays the owner); the structure test asserts the copy never drifts.
13
+ //
14
+ // `consent` is null for an operation the agent runs outright, else the ONE consent gate the mode
15
+ // doc teaches for it. `outcomes` is the vocabulary the doc names for relaying that operation's
16
+ // result — for `configs` the closed ensure tokens, elsewhere the doc's own outcome words.
17
+
18
+ const entry = (id, command, consent, outcomes) =>
19
+ Object.freeze({ id, command, consent, outcomes: Object.freeze(outcomes) });
20
+
21
+ export const UPGRADE_RUNLIST = Object.freeze([
22
+ entry(
23
+ 'pointers',
24
+ 'node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md',
25
+ null,
26
+ ['added', 'already present', 'skipped', 'STOP'],
27
+ ),
28
+ entry(
29
+ 'footprint',
30
+ 'node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run',
31
+ 'ambiguous → ask the user which it is; hidden → the conditional re-run without --dry-run (surfaced paths ask per bootstrap step 9)',
32
+ ['visible', 'ambiguous', 'hidden'],
33
+ ),
34
+ entry(
35
+ 'configs',
36
+ 'node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>',
37
+ null,
38
+ [
39
+ 'seeded',
40
+ 'note-refreshed',
41
+ 'already-current',
42
+ 'customized-preserved',
43
+ 'malformed-preserved',
44
+ 'already-present',
45
+ 'skipped-no-node',
46
+ 'old-adr-layout-migration-instructed',
47
+ 'failed',
48
+ ],
49
+ ),
50
+ entry(
51
+ 'gates-migration',
52
+ 'node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>',
53
+ 'preview first — apply only on an explicit yes, re-run with --apply',
54
+ ['preview', 'INERT', 'CUSTOMIZED'],
55
+ ),
56
+ entry(
57
+ 'bridges',
58
+ 'node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed',
59
+ null,
60
+ [
61
+ 'refreshed',
62
+ 'already current',
63
+ 'skipped',
64
+ 'not placed',
65
+ 'newer than the bundle',
66
+ 'unsupported host',
67
+ 'skipped-readonly',
68
+ 'could not refresh',
69
+ ],
70
+ ),
71
+ entry(
72
+ 'lens',
73
+ 'node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md',
74
+ null,
75
+ [
76
+ 'refreshed',
77
+ 'already current',
78
+ 'custom edit preserved',
79
+ 'file absent',
80
+ 'engine too old',
81
+ 'over the line cap — refused',
82
+ 'section absent — noted',
83
+ 'STOP',
84
+ ],
85
+ ),
86
+ entry(
87
+ 'bridge-settings',
88
+ 'node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile',
89
+ null,
90
+ ['ok', 'absent', 'flagged', 'duplicates', 'unusable'],
91
+ ),
92
+ ]);