@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.
- package/CHANGELOG.md +122 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +7 -1
- package/references/modes/doc-parity.md +1 -1
- package/references/modes/gates.md +20 -4
- package/references/modes/procedures.md +2 -0
- package/references/modes/recommendations.md +4 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/setup.md +18 -2
- package/references/modes/upgrade.md +38 -18
- package/references/modes/velocity.md +1 -0
- package/references/scripts/migrate-gates-branches.test.mjs +146 -1
- package/references/scripts/migrate-gates.mjs +295 -60
- package/references/scripts/migrate-gates.test.mjs +206 -14
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/gates.json +1 -1
- package/tools/ack-write.mjs +20 -11
- package/tools/atomic-write.mjs +71 -18
- package/tools/checker-claim.mjs +100 -0
- package/tools/coverage-producer.mjs +43 -6
- package/tools/direct-run.mjs +76 -0
- package/tools/doc-parity.mjs +34 -3
- package/tools/engine-source.mjs +12 -8
- package/tools/ensure-configs.mjs +141 -0
- package/tools/ensure-ops.mjs +284 -0
- package/tools/ensure-vocabulary.mjs +71 -0
- package/tools/flow-check-cores.mjs +253 -0
- package/tools/flow-check-git-lane.mjs +56 -0
- package/tools/flow-check-rungs.mjs +330 -0
- package/tools/flow-check.mjs +23 -611
- package/tools/gates-declaration.mjs +36 -11
- package/tools/gates-init.mjs +140 -25
- package/tools/hide-footprint.mjs +21 -3
- package/tools/lens-region.mjs +74 -23
- package/tools/orchestration-config.mjs +5 -3
- package/tools/orchestration-write.mjs +7 -0
- package/tools/procedures.mjs +64 -5
- package/tools/recommendations.mjs +384 -34
- package/tools/refresh-parity.mjs +263 -0
- package/tools/run-gates.mjs +8 -5
- package/tools/setup-backends.mjs +88 -77
- package/tools/source-size-check.mjs +310 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +59 -0
- package/tools/source-size-gate-cmd.mjs +27 -0
- package/tools/source-size-judge.mjs +114 -0
- package/tools/source-size-refusal.mjs +70 -0
- package/tools/source-size-report.mjs +254 -0
- package/tools/source-size-scope.mjs +145 -0
- package/tools/tracked-tree-census.mjs +102 -0
- package/tools/upgrade-runlist.mjs +92 -0
- 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),
|
|
367
|
-
|
|
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
|
};
|