@sabaiway/agent-workflow-kit 14.29.0 → 14.30.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 +71 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +2 -2
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +1 -1
- package/references/scripts/check-docs-size-cli.test.mjs +45 -0
- package/references/scripts/check-docs-size-ensure.test.mjs +28 -0
- package/references/scripts/check-docs-size.mjs +6 -4
- package/tools/ensure-configs.mjs +55 -22
- package/tools/ensure-ops.mjs +99 -8
- package/tools/ensure-specs.mjs +55 -23
- package/tools/ensure-vocabulary.mjs +2 -1
- package/tools/hide-footprint.mjs +73 -21
- package/tools/recommendations.mjs +3 -3
- package/tools/script-priors.mjs +3 -2
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/velocity-profile.mjs +3 -3
package/tools/hide-footprint.mjs
CHANGED
|
@@ -35,6 +35,11 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export const START_MARKER = '# >>> agent-workflow-kit hidden mode (managed; do not edit between markers) >>>';
|
|
37
37
|
export const END_MARKER = '# <<< agent-workflow-kit hidden mode <<<';
|
|
38
|
+
// Any other line opening or closing with these prefixes (a damaged hidden-mode marker aside: it
|
|
39
|
+
// refuses) marks ANOTHER kit lane's block (the sandbox-masks lane's): this writer never absorbs,
|
|
40
|
+
// moves or re-encodes a byte of it, a byte that is not UTF-8 aside (the file is read as UTF-8).
|
|
41
|
+
const FOREIGN_OPEN = '# >>> agent-workflow';
|
|
42
|
+
const FOREIGN_CLOSE = '# <<< agent-workflow';
|
|
38
43
|
|
|
39
44
|
// Recognized legacy forms we ABSORB (local) or MIGRATE (global). Matched by prefix so a minor wording
|
|
40
45
|
// drift can't strand a line (D7/D8).
|
|
@@ -101,11 +106,18 @@ const detectEol = (content) => {
|
|
|
101
106
|
};
|
|
102
107
|
// Split into logical lines; `lines.join(eol)` reproduces the structure (CRLF normalized to `eol`).
|
|
103
108
|
const splitLines = (content) => (content === '' ? [] : content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'));
|
|
104
|
-
//
|
|
109
|
+
// Each logical line's own ending ('' on an unterminated last line), index-aligned with splitLines.
|
|
110
|
+
const lineEnds = (content) => {
|
|
111
|
+
if (content === '') return [];
|
|
112
|
+
const parts = content.split(/(\r\n|\n|\r)/);
|
|
113
|
+
return parts.filter((_, i) => i % 2 === 0).map((_, i) => parts[2 * i + 1] ?? '');
|
|
114
|
+
};
|
|
115
|
+
// Re-join, dropping trailing blank lines (canonical, idempotent): a plain line takes `eol`; a line
|
|
116
|
+
// given as { text, end } (a foreign block's) keeps its own ending, an absent one on the last line included.
|
|
105
117
|
const joinLines = (lines, eol) => {
|
|
106
118
|
const out = [...lines];
|
|
107
119
|
while (out.length && out[out.length - 1] === '') out.pop();
|
|
108
|
-
return out.
|
|
120
|
+
return out.map((l, i) => (typeof l === 'string' ? `${l}${eol}` : `${l.text}${l.end || (i < out.length - 1 ? eol : '')}`)).join('');
|
|
109
121
|
};
|
|
110
122
|
|
|
111
123
|
// A raw exclude line → an anchored pattern (leading "/"), or null when blank/comment.
|
|
@@ -261,15 +273,44 @@ const assembleCandidates = (deps, dir, fsx, forcedPreserve) => {
|
|
|
261
273
|
|
|
262
274
|
export const buildBlock = (patterns) => [...new Set(patterns)].sort();
|
|
263
275
|
|
|
264
|
-
//
|
|
265
|
-
// { state:'
|
|
276
|
+
// Scan every kit marker line (judged trimmed, as the masks lane reads its own): our fence and each
|
|
277
|
+
// foreign block. { state:'none'|'ok', startIdx, endIdx, foreign:Set<lineIdx>, blockStart:Map<closer
|
|
278
|
+
// idx, opener idx> } | { state:'malformed', reason }. A closer `# <<< <lane> <<<` closes only an
|
|
279
|
+
// opener whose lane (its name up to ' — ' or ' >>>') is <lane>; malformed → caller STOPs, file unchanged.
|
|
280
|
+
const DAMAGED_OWN = ['# >>> agent-workflow-kit hidden mode', '# <<< agent-workflow-kit hidden mode'];
|
|
266
281
|
const findFence = (lines) => {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
const bad = (reason) => ({ state: 'malformed', reason });
|
|
283
|
+
const foreign = new Set();
|
|
284
|
+
const blockStart = new Map();
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
let open = null;
|
|
287
|
+
let own = null;
|
|
288
|
+
for (const [i, raw] of lines.entries()) {
|
|
289
|
+
const t = raw.trim();
|
|
290
|
+
const opens = t.startsWith(FOREIGN_OPEN);
|
|
291
|
+
if (!opens && !t.startsWith(FOREIGN_CLOSE)) continue;
|
|
292
|
+
const mine = t === START_MARKER || t === END_MARKER;
|
|
293
|
+
if (!mine && DAMAGED_OWN.some((p) => t.startsWith(p))) return bad(`line ${i + 1} is a damaged hidden-mode marker`);
|
|
294
|
+
if (opens) {
|
|
295
|
+
if (open) return bad(`line ${i + 1} opens a block inside the one opened at line ${open.i + 1}`);
|
|
296
|
+
open = { i, mine, name: t.slice(6) };
|
|
297
|
+
} else {
|
|
298
|
+
if (!open) return bad(`line ${i + 1} closes a block that was never opened`);
|
|
299
|
+
const lane = t.endsWith(' <<<') ? t.slice(6, -4) : null;
|
|
300
|
+
const closes = mine ? open.mine : !open.mine && lane !== null && open.name.split(/ — | >>>/)[0] === lane;
|
|
301
|
+
if (!closes) return bad(`line ${i + 1} closes another block than the one opened at line ${open.i + 1}`);
|
|
302
|
+
}
|
|
303
|
+
if (seen.has(t)) return bad(`line ${i + 1} repeats the marker of an earlier block`);
|
|
304
|
+
seen.add(t);
|
|
305
|
+
if (!opens) {
|
|
306
|
+
if (mine) own = { startIdx: open.i, endIdx: i };
|
|
307
|
+
else for (let j = open.i; j <= i; j += 1) foreign.add(j);
|
|
308
|
+
if (!mine) blockStart.set(i, open.i);
|
|
309
|
+
open = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (open) return bad(`the block opened at line ${open.i + 1} is never closed`);
|
|
313
|
+
return { state: own ? 'ok' : 'none', ...own, foreign, blockStart };
|
|
273
314
|
};
|
|
274
315
|
|
|
275
316
|
// ── absorb (local pre-existing recognized lines → folded into the fence) ──────────
|
|
@@ -280,21 +321,24 @@ const findFence = (lines) => {
|
|
|
280
321
|
// block. Low-risk rules fold regardless of presence (D8 preserve). A HIGH-RISK rule folds as consent
|
|
281
322
|
// only while its file is still PRESENT; if its file is absent it is left exactly as-is (a deliberate
|
|
282
323
|
// pre-emptive user hide is never silently removed, and there is no present-file report mismatch).
|
|
324
|
+
// A foreign block's line arrives as { text, end } and is kept whole; `removed` lists, in file order,
|
|
325
|
+
// every line taken out (the report names each one).
|
|
283
326
|
const absorbOutside = (outsideLines, isPresent) => {
|
|
284
327
|
const kept = [];
|
|
285
328
|
const absorbed = [];
|
|
329
|
+
const removed = [];
|
|
286
330
|
for (const raw of outsideLines) {
|
|
331
|
+
if (typeof raw !== 'string') { kept.push(raw); continue; }
|
|
287
332
|
const t = raw.trim();
|
|
288
|
-
if (isAd013Comment(t) || isLegacyGlobalHeader(t)) continue; // orphan header for an absorbed rule
|
|
289
333
|
const pat = lineToPattern(raw);
|
|
290
334
|
const canon = pat ? recognizeHideRule(pat) : null;
|
|
291
|
-
if (
|
|
335
|
+
if (isAd013Comment(t) || isLegacyGlobalHeader(t)) removed.push(raw); // orphan header for an absorbed rule
|
|
336
|
+
else if (canon && (LOWRISK_SET.has(canon) || isPresent(canon))) {
|
|
292
337
|
absorbed.push(canon);
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
kept.push(raw); // unrecognized, or a high-risk recognized rule whose file is absent → leave it
|
|
338
|
+
removed.push(raw);
|
|
339
|
+
} else kept.push(raw); // unrecognized, or a high-risk recognized rule whose file is absent → leave it
|
|
296
340
|
}
|
|
297
|
-
return { kept, absorbed };
|
|
341
|
+
return { kept, absorbed, removed };
|
|
298
342
|
};
|
|
299
343
|
|
|
300
344
|
// ── verify (D5 — over the WROTE set; "hidden" ≡ untracked ∧ ignored-by-OUR-source) ─
|
|
@@ -396,9 +440,13 @@ export const hideFootprint = (opts = {}, deps = {}) => {
|
|
|
396
440
|
}
|
|
397
441
|
})();
|
|
398
442
|
const eol = detectEol(content);
|
|
399
|
-
const
|
|
400
|
-
const
|
|
443
|
+
const ends = lineEnds(content);
|
|
444
|
+
const texts = splitLines(content);
|
|
445
|
+
const fence = findFence(texts);
|
|
401
446
|
if (fence.state === 'malformed') throw stop(`refusing to edit a malformed managed block in ${excludeFile}: ${fence.reason} (file left unchanged)`);
|
|
447
|
+
const lines = texts.map((l, i) => (fence.foreign.has(i) ? { text: l, end: ends[i] } : l));
|
|
448
|
+
// A created fence goes before a foreign block that ends the file unterminated, so no byte of it moves.
|
|
449
|
+
const tailBlock = ends.at(-1) === '' ? lines[fence.blockStart.get(texts.length - 1)] : undefined;
|
|
402
450
|
|
|
403
451
|
const before = fence.state === 'ok' ? lines.slice(0, fence.startIdx) : lines;
|
|
404
452
|
const fenceBodyLines = fence.state === 'ok' ? lines.slice(fence.startIdx + 1, fence.endIdx) : [];
|
|
@@ -416,8 +464,8 @@ export const hideFootprint = (opts = {}, deps = {}) => {
|
|
|
416
464
|
|
|
417
465
|
// ── absorb pre-existing recognized lines, assemble + classify candidates ───────
|
|
418
466
|
const isPresentCanon = (canon) => presentOnDisk(canon, fsx, dir);
|
|
419
|
-
const { kept: cleanBefore, absorbed: absorbedBefore } = absorbOutside(before, isPresentCanon);
|
|
420
|
-
const { kept: cleanAfter, absorbed: absorbedAfter } = absorbOutside(after, isPresentCanon);
|
|
467
|
+
const { kept: cleanBefore, absorbed: absorbedBefore, removed: removedBefore } = absorbOutside(before, isPresentCanon);
|
|
468
|
+
const { kept: cleanAfter, absorbed: absorbedAfter, removed: removedAfter } = absorbOutside(after, isPresentCanon);
|
|
421
469
|
const fenceBodyPatterns = fenceBodyLines.map((l) => recognizeHideRule(lineToPattern(l) ?? '')).filter(Boolean);
|
|
422
470
|
// Every pre-existing recognized hide rule (current fence body + absorbed loose lines, glob children
|
|
423
471
|
// included) is prior consent. LOW-RISK rules are force-preserved as candidates regardless of presence
|
|
@@ -455,8 +503,9 @@ export const hideFootprint = (opts = {}, deps = {}) => {
|
|
|
455
503
|
|
|
456
504
|
// ── build the new file (splice the fence; preserve outside lines) ──────────────
|
|
457
505
|
const fenceLines = writtenPatterns.length ? [START_MARKER, ...writtenPatterns, END_MARKER] : [];
|
|
506
|
+
const at = fence.state === 'none' && tailBlock ? cleanBefore.indexOf(tailBlock) : cleanBefore.length;
|
|
458
507
|
const newLines = writtenPatterns.length
|
|
459
|
-
? [...cleanBefore, ...fenceLines, ...cleanAfter]
|
|
508
|
+
? [...cleanBefore.slice(0, at), ...fenceLines, ...cleanBefore.slice(at), ...cleanAfter]
|
|
460
509
|
: [...cleanBefore, ...cleanAfter];
|
|
461
510
|
const newContent = joinLines(newLines, eol);
|
|
462
511
|
const changed = newContent !== content;
|
|
@@ -482,6 +531,7 @@ export const hideFootprint = (opts = {}, deps = {}) => {
|
|
|
482
531
|
wrote: writtenPatterns,
|
|
483
532
|
added,
|
|
484
533
|
removed,
|
|
534
|
+
removedOutside: [...removedBefore, ...removedAfter],
|
|
485
535
|
asks: asks.filter((a) => !includedAsks.some((i) => i.pattern === a.pattern)).map((a) => ({ path: a.pattern, reason: a.reason, owner: a.owner })),
|
|
486
536
|
needsUntrack: needsUntrack.map((a) => {
|
|
487
537
|
const target = patternToProbe(a.pattern).replace(/\/$/, '');
|
|
@@ -558,6 +608,8 @@ export const formatReport = (r, dryRun) => {
|
|
|
558
608
|
if (r.removed.length) lines.push(` • −${r.removed.length} removed: ${r.removed.join(', ')}`);
|
|
559
609
|
if (!r.added.length && !r.removed.length) lines.push(' • +0/−0 — the hidden set is unchanged');
|
|
560
610
|
}
|
|
611
|
+
// Each line taken out outside the fence, verbatim, on the whole-line machine channel (exempt by grammar).
|
|
612
|
+
for (const l of r.removedOutside ?? []) lines.push(` [hide-footprint] ${dryRun ? 'would-remove' : 'removed'}=${l}`);
|
|
561
613
|
for (const a of r.asks) lines.push(` • ASK ${a.path} — ${a.reason}`);
|
|
562
614
|
for (const n of r.needsUntrack) lines.push(` • tracked, NOT hidden: ${n.path} — run \`${n.command}\` to un-track (kept on disk)`);
|
|
563
615
|
if (r.dropped.length) lines.push(` • skipped ${r.dropped.length} already-ignored (tracked .gitignore)`);
|
|
@@ -244,8 +244,8 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
244
244
|
// WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
|
|
245
245
|
export const WHATS = Object.freeze({
|
|
246
246
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
247
|
-
'velocity-core.rule': '
|
|
248
|
-
'velocity-core.both': 'routine read-only commands still prompt, and
|
|
247
|
+
'velocity-core.rule': 'auto mode refuses a harness apply, so work cannot stay in auto mode — the apply ask rule is not seeded',
|
|
248
|
+
'velocity-core.both': 'routine read-only commands still prompt, and auto mode refuses a harness apply — {n} entr(ies) not seeded',
|
|
249
249
|
'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
|
|
250
250
|
'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
|
|
251
251
|
'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
|
|
@@ -319,7 +319,7 @@ const templateBudget = (template) => ITEM_LINE_CAP - template.replace(/\{[A-Za-z
|
|
|
319
319
|
|
|
320
320
|
// ── the frozen benefit registry (fact-true; pinned by tests) ────────────────────────────────────
|
|
321
321
|
export const BENEFITS = Object.freeze({
|
|
322
|
-
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away;
|
|
322
|
+
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away; all work stays in auto mode',
|
|
323
323
|
'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
|
|
324
324
|
'bridge-tier':
|
|
325
325
|
'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
|
package/tools/script-priors.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// script-priors.mjs — the APPEND-ONLY catalog of every enforcement-script body this family has shipped
|
|
2
2
|
// into a project's `scripts/`, as sha256 digests, and the ONE classifier the spec-layer ensure asks
|
|
3
3
|
// before it refreshes a deployed script: `current` (byte-equal to the bundled copy) · `prior` (a body
|
|
4
|
-
// a release shipped, never edited since) · `custom` (anything else — preserved verbatim,
|
|
5
|
-
//
|
|
4
|
+
// a release shipped, never edited since) · `custom` (anything else — preserved verbatim, replaced only by
|
|
5
|
+
// the replace run the user's yes starts). The lens-region idiom (AD-041) over whole script bodies.
|
|
6
6
|
//
|
|
7
7
|
// Append discipline: when a release changes one of PRIOR_FILES, append the OUTGOING body's row here
|
|
8
8
|
// in the same release and copy its bytes to test/fixtures/script-priors/<firstShipped>/<file>.txt —
|
|
@@ -36,6 +36,7 @@ export const SCRIPT_PRIORS = Object.freeze([
|
|
|
36
36
|
prior('check-docs-size.mjs', '4.6.0', '6.0.0', '22d020c3668cdbfb4cbc1f67a8a85b2baa2d0c4956808a50626d37409e81ab38'),
|
|
37
37
|
prior('spec-schema.mjs', '5.0.0', '7.2.1', '6a7d1e836c158baab5a412a1ad76d34891934dc28c88486c264a4b5d46f5892a'),
|
|
38
38
|
prior('spec-schema.test.mjs', '5.0.0', '7.2.1', '698564d7283281be1826019d214df475bb876687915de086818d8f5ece0e0336'),
|
|
39
|
+
prior('check-docs-size.mjs', '7.0.0', '8.1.5', '0cf90ae2cbfc809847574a8c71a6b1322c783359740b08e0ff179f0d9d04dffc'),
|
|
39
40
|
]);
|
|
40
41
|
|
|
41
42
|
export const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
@@ -34,7 +34,7 @@ export const UPGRADE_RUNLIST = Object.freeze([
|
|
|
34
34
|
entry(
|
|
35
35
|
'configs',
|
|
36
36
|
'node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>',
|
|
37
|
-
|
|
37
|
+
'a specs replace line → ONE yes naming every file the custom lines name and saying their edits are overwritten, asked even on a non-zero exit; on the yes run the printed line, then STOP if that run fails or an op it does not re-run failed; on a no STOP on any failed op. The printed line: node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --replace-custom <digest> --cwd <project>',
|
|
38
38
|
[
|
|
39
39
|
'seeded',
|
|
40
40
|
'note-refreshed',
|
|
@@ -39,7 +39,7 @@ export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
|
|
|
39
39
|
export const SAFE_DEFAULT_MODES = Object.freeze(['default', 'acceptEdits', 'plan']);
|
|
40
40
|
export const UNSAFE_BYPASS_MODE = 'bypassPermissions';
|
|
41
41
|
export const ACCEPT_EDITS_MODE = 'acceptEdits';
|
|
42
|
-
//
|
|
42
|
+
// Lets all work stay in auto mode: a harness apply meets the harness dialog, not a refusal; an ask rule allows nothing.
|
|
43
43
|
export const APPLY_ASK_RULE = 'Bash(node *apply-danger-check.mjs*--apply*)';
|
|
44
44
|
|
|
45
45
|
// The audited read-only core. Every entry is a Claude Code Bash allow pattern whose command runs NO
|
|
@@ -337,8 +337,8 @@ absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned by
|
|
|
337
337
|
(project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
|
|
338
338
|
byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
|
|
339
339
|
dry-run preview byte-strings. Never touches settings.local.json.
|
|
340
|
-
Every apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask
|
|
341
|
-
|
|
340
|
+
Every apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask once, so all work
|
|
341
|
+
can stay in auto mode: without it auto mode refuses a harness apply and you must leave auto mode for it; with it the harness shows its usual dialog. It allows nothing.
|
|
342
342
|
--bridge-tier (own consent) derives each entry for a used bridge from the effective project recipes:
|
|
343
343
|
review wrappers' CODE mode (codex-review code, agy-review code), the quoted grounding pre-step rule,
|
|
344
344
|
and <wrapper> * exclusions in sandbox.excludedCommands. An execute slot delegating to codex adds
|