@sabaiway/agent-workflow-kit 5.6.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 +60 -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 +16 -3
- package/references/modes/recommendations.md +3 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/setup.md +18 -2
- package/references/modes/upgrade.md +38 -18
- 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/gates-declaration.mjs +23 -10
- package/tools/gates-init.mjs +6 -3
- 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/recommendations.mjs +315 -66
- 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 +6 -16
- package/tools/source-size-core.mjs +7 -1
- package/tools/source-size-gate-cmd.mjs +18 -46
- package/tools/tracked-tree-census.mjs +102 -0
- 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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
]);
|