@sabaiway/agent-workflow-kit 5.5.0 → 5.7.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 (55) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +1 -1
  3. package/SKILL.md +1 -1
  4. package/capability.json +1 -1
  5. package/package.json +1 -1
  6. package/references/hooks/gate-approve.mjs +7 -1
  7. package/references/modes/doc-parity.md +1 -1
  8. package/references/modes/gates.md +20 -4
  9. package/references/modes/procedures.md +2 -0
  10. package/references/modes/recommendations.md +4 -1
  11. package/references/modes/review-state.md +1 -1
  12. package/references/modes/setup.md +18 -2
  13. package/references/modes/upgrade.md +38 -18
  14. package/references/modes/velocity.md +1 -0
  15. package/references/scripts/migrate-gates-branches.test.mjs +146 -1
  16. package/references/scripts/migrate-gates.mjs +295 -60
  17. package/references/scripts/migrate-gates.test.mjs +206 -14
  18. package/references/shared/deploy-tail.md +1 -1
  19. package/references/templates/gates.json +1 -1
  20. package/tools/ack-write.mjs +20 -11
  21. package/tools/atomic-write.mjs +71 -18
  22. package/tools/checker-claim.mjs +100 -0
  23. package/tools/coverage-producer.mjs +43 -6
  24. package/tools/direct-run.mjs +76 -0
  25. package/tools/doc-parity.mjs +34 -3
  26. package/tools/engine-source.mjs +12 -8
  27. package/tools/ensure-configs.mjs +141 -0
  28. package/tools/ensure-ops.mjs +284 -0
  29. package/tools/ensure-vocabulary.mjs +71 -0
  30. package/tools/flow-check-cores.mjs +253 -0
  31. package/tools/flow-check-git-lane.mjs +56 -0
  32. package/tools/flow-check-rungs.mjs +330 -0
  33. package/tools/flow-check.mjs +23 -611
  34. package/tools/gates-declaration.mjs +36 -11
  35. package/tools/gates-init.mjs +140 -25
  36. package/tools/hide-footprint.mjs +21 -3
  37. package/tools/lens-region.mjs +74 -23
  38. package/tools/orchestration-config.mjs +5 -3
  39. package/tools/orchestration-write.mjs +7 -0
  40. package/tools/procedures.mjs +64 -5
  41. package/tools/recommendations.mjs +384 -34
  42. package/tools/refresh-parity.mjs +263 -0
  43. package/tools/run-gates.mjs +8 -5
  44. package/tools/setup-backends.mjs +88 -77
  45. package/tools/source-size-check.mjs +310 -0
  46. package/tools/source-size-config.mjs +244 -0
  47. package/tools/source-size-core.mjs +59 -0
  48. package/tools/source-size-gate-cmd.mjs +27 -0
  49. package/tools/source-size-judge.mjs +114 -0
  50. package/tools/source-size-refusal.mjs +70 -0
  51. package/tools/source-size-report.mjs +254 -0
  52. package/tools/source-size-scope.mjs +145 -0
  53. package/tools/tracked-tree-census.mjs +102 -0
  54. package/tools/upgrade-runlist.mjs +92 -0
  55. package/tools/velocity-profile.mjs +24 -3
@@ -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
+ ]);
@@ -168,6 +168,19 @@ export const KIT_WRITER_PREVIEW_TOOLS = Object.freeze([
168
168
  'tools/cheap-agents.mjs',
169
169
  'tools/gate-hook.mjs',
170
170
  ]);
171
+ // The source-size checker: a WRITER tool (--write-baseline, --adopt) whose ONE read-only mode is
172
+ // seeded, and only in that exact byte-form. It joins neither list above on purpose — a wildcard
173
+ // would cover its writers, and its arg-free invocation is a usage error rather than a dry-run, so
174
+ // the writer-preview class does not describe it either.
175
+ //
176
+ // What this rule covers, stated exactly because the near-miss is easy to assume: the AGENT's own
177
+ // direct `node <abs> --check`. It is NOT the byte-string a declared gate carries — gates-init emits
178
+ // the path DOUBLE-QUOTED (a kit path with a space must survive), while a seedable allow rule may
179
+ // carry no quotes at all, so the two spellings cannot be one string. The DECLARED gate's
180
+ // promptlessness is the gate-approval hook's job, byte-exact against gates.json; this rule exists
181
+ // for the invocation an agent types itself, which no declaration covers.
182
+ export const KIT_SOURCE_SIZE_TOOL = 'tools/source-size-check.mjs';
183
+ const SOURCE_SIZE_CHECK_FLAG = '--check';
171
184
  const KIT_WILDCARD_TOOLS = Object.freeze(KIT_READONLY_TOOLS.filter((rel) => rel !== KIT_RUN_GATES_TOOL));
172
185
  // The kit root this tool runs from (tools/..) — the tier's seed-time path anchor.
173
186
  const KIT_ROOT = fileURLToPath(new URL('..', import.meta.url));
@@ -363,8 +376,9 @@ Allowlist mode (default): seeds the fixed read-only Claude Code allowlist into .
363
376
  Default is --dry-run. --apply writes; --accept-edits only sets defaultMode when applying.
364
377
  --kit-tools additionally seeds the audited kit-tool tier: ${KIT_WILDCARD_TOOLS.length} read-only kit tools by resolved
365
378
  absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned byte-string
366
- (project-exec - it runs YOUR declared gates.json), and the writers' exact arg-free dry-run
367
- preview byte-strings. Never touches settings.local.json.
379
+ (project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
380
+ byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
381
+ dry-run preview byte-strings. Never touches settings.local.json.
368
382
  --bridge-tier (own consent) seeds the bridge REVIEW wrappers' CODE mode for PLACED bridges
369
383
  (codex-review code, agy-review code - never the execution/probe wrappers, never plan/diff modes)
370
384
  + the quoted grounding pre-step rule, and the wrapper names into sandbox.excludedCommands (they
@@ -663,7 +677,7 @@ const formatAllowlist = (result) => [
663
677
  // The tier's honest posture, printed on every --kit-tools run: run-gates is project-exec (never
664
678
  // "read-only"), previews stay dry-run-only, and the tier gets none of the hook's residual ask-net.
665
679
  const KIT_TIER_NOTICE =
666
- 'kit-tools tier: paths are resolved absolute at seed time (fail-safe - a moved skill or stale path simply prompts again); run-gates.mjs is seeded as ONE exact byte-string pinned to this project root and is project-exec - it runs YOUR declared gates.json commands, never "read-only"; writer previews are exact dry-run byte-strings - every --apply/--write/--yes still prompts; tier entries get NO PreToolUse-hook residual coverage EXCEPT repo-search.mjs and path-inventory.mjs, whose invocations the hook scans because they take caller-supplied argument bytes (settings-level posture only for the rest - see the velocity mode notes).';
680
+ 'kit-tools tier: paths are resolved absolute at seed time (fail-safe - a moved skill or stale path simply prompts again); run-gates.mjs is seeded as ONE exact byte-string pinned to this project root and is project-exec - it runs YOUR declared gates.json commands, never "read-only"; source-size-check.mjs is seeded as ONE exact --check byte-string - its --write-baseline/--adopt writers are NOT covered and still prompt; writer previews are exact dry-run byte-strings - every --apply/--write/--yes still prompts; tier entries get NO PreToolUse-hook residual coverage EXCEPT repo-search.mjs and path-inventory.mjs, whose invocations the hook scans because they take caller-supplied argument bytes (settings-level posture only for the rest - see the velocity mode notes).';
667
681
 
668
682
  const formatKitTier = (result) =>
669
683
  result.kitTools
@@ -763,6 +777,12 @@ export const screenAllowlistEntry = (pattern) => {
763
777
  if (tokens[0] !== KIT_TOOL_INVOKER) return false;
764
778
  // Writer preview: the arg-free dry-run byte-string of a preview-class writer.
765
779
  if (tokens.length === 2) return isKitToolPathToken(tokens[1], KIT_WRITER_PREVIEW_TOOLS);
780
+ // source-size: EXACTLY `node <abs source-size-check> --check` — its read-only mode and nothing
781
+ // else, and no --cwd, because this rule covers the invocation an AGENT types directly. It is not
782
+ // the declared gate's byte-string (the fill quotes that path); see the tier constant above.
783
+ if (tokens.length === 3) {
784
+ return isKitToolPathToken(tokens[1], [KIT_SOURCE_SIZE_TOOL]) && tokens[2] === SOURCE_SIZE_CHECK_FLAG;
785
+ }
766
786
  // run-gates: EXACTLY `node <abs run-gates> --cwd <abs root>` — any other form keeps prompting.
767
787
  return (
768
788
  tokens.length === 4 &&
@@ -803,6 +823,7 @@ export const deriveKitToolsAllowlist = ({ projectDir } = {}) => {
803
823
  return Object.freeze([
804
824
  ...KIT_WILDCARD_TOOLS.map((rel) => `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, rel)}:*)`),
805
825
  `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, KIT_RUN_GATES_TOOL)} ${RUN_GATES_CWD_FLAG} ${projectRoot})`,
826
+ `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, KIT_SOURCE_SIZE_TOOL)} ${SOURCE_SIZE_CHECK_FLAG})`,
806
827
  ...KIT_WRITER_PREVIEW_TOOLS.map((rel) => `Bash(${KIT_TOOL_INVOKER} ${join(KIT_ROOT, rel)})`),
807
828
  ]);
808
829
  };