@sabaiway/agent-workflow-kit 4.5.0 → 5.1.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 +49 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/procedures.md +2 -2
- package/references/modes/upgrade.md +2 -2
- package/references/scripts/archive-changelog.mjs +300 -192
- package/references/scripts/archive-changelog.test.mjs +341 -0
- package/references/scripts/archive-conservation.test.mjs +466 -0
- package/references/scripts/archive-decisions.mjs +34 -17
- package/references/scripts/archive-decisions.test.mjs +93 -0
- package/references/scripts/archive-issues.mjs +344 -108
- package/references/scripts/archive-issues.test.mjs +762 -32
- package/references/scripts/archiver-structure.test.mjs +39 -0
- package/references/scripts/markdown-blocks.mjs +143 -0
- package/references/scripts/markdown-blocks.test.mjs +310 -0
- package/references/templates/changelog.md +3 -1
- package/references/templates/known_issues.md +13 -5
- package/tools/doc-parity.mjs +14 -2
- package/tools/known-footprint.mjs +5 -1
- package/tools/migrate-adr-store.mjs +32 -5
- package/tools/orchestration-config.mjs +26 -2
|
@@ -119,6 +119,26 @@ export const planScriptRefresh = (cwd, deps = {}) => {
|
|
|
119
119
|
return out;
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
+
// COMPANION seeds: modules the refreshed archivers IMPORT. The refresh above is deliberately
|
|
123
|
+
// directional (never ADDS a basename the consumer lacks), but refreshing an OLD deployment's
|
|
124
|
+
// archivers to this kit's canon without their runtime dependency would leave every refreshed
|
|
125
|
+
// script crashing on a missing `./markdown-blocks.mjs` import until a separate upgrade run — so
|
|
126
|
+
// the dependency rides the SAME apply, atomically, written before its importers.
|
|
127
|
+
const COMPANION_SEEDS = ['markdown-blocks.mjs', 'markdown-blocks.test.mjs'];
|
|
128
|
+
export const planCompanionSeeds = (cwd, refresh, deps = {}) => {
|
|
129
|
+
if (refresh.length === 0) return [];
|
|
130
|
+
const exists = deps.exists ?? existsSync;
|
|
131
|
+
const kitScripts = deps.kitScripts ?? KIT_SCRIPTS;
|
|
132
|
+
const consumerScripts = join(cwd, CONSUMER_SCRIPTS_REL);
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const name of COMPANION_SEEDS) {
|
|
135
|
+
const canon = join(kitScripts, name);
|
|
136
|
+
const dst = join(consumerScripts, name);
|
|
137
|
+
if (exists(canon) && !exists(dst)) out.push({ name, canon, dst });
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
};
|
|
141
|
+
|
|
122
142
|
const gitDirOf = (cwd, spawn) => {
|
|
123
143
|
const r = spawn('git', ['rev-parse', '--absolute-git-dir'], { cwd, encoding: 'utf8' });
|
|
124
144
|
return r && r.status === 0 && r.stdout ? r.stdout.trim() : null;
|
|
@@ -206,10 +226,15 @@ const applyScriptRefresh = (cwd, refresh, deps = {}) => {
|
|
|
206
226
|
const read = deps.read ?? readFileSync;
|
|
207
227
|
const chmod = deps.chmod ?? chmodSync;
|
|
208
228
|
const stat = deps.stat ?? statSync;
|
|
209
|
-
|
|
229
|
+
// Companion modules FIRST (a dependency must land before its importers), refresh order after —
|
|
230
|
+
// the discriminator still last, so an interrupted apply always re-plans in full. Returns the
|
|
231
|
+
// seeded names (computed pre-write; recomputing after would see them present and report none).
|
|
232
|
+
const seeds = planCompanionSeeds(cwd, refresh, deps);
|
|
233
|
+
for (const { canon, dst, name } of [...seeds, ...refreshOrder(refresh)]) {
|
|
210
234
|
writeContainedFileAtomic(cwd, dst, read(canon, 'utf8'), deps, { stop, label: `${CONSUMER_SCRIPTS_REL}/${name}` });
|
|
211
235
|
chmod(dst, stat(canon).mode & 0o777); // the exec bit is the git-tracked axis the mirror guard pins
|
|
212
236
|
}
|
|
237
|
+
return seeds.map((s) => s.name);
|
|
213
238
|
};
|
|
214
239
|
|
|
215
240
|
// ── the no-monolith crossing ─────────────────────────────────────────────────────
|
|
@@ -307,6 +332,7 @@ const crossWithoutMonolith = (cwd, args, stamp, { log, error, runMigrate, deps }
|
|
|
307
332
|
log(` ${ADR_DIR_REL}/: ${hasStore ? 'present, but the crossing has not been completed' : 'absent'}`);
|
|
308
333
|
log(` snapshot → ${preview.dir ? `${preview.dir} (${preview.viaGitDir ? 'git dir' : 'out-of-tree fallback'})` : 'NONE — no out-of-tree location; run inside a git repo (apply would refuse otherwise)'}`);
|
|
309
334
|
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
335
|
+
for (const s of planCompanionSeeds(cwd, refresh, deps)) log(` seed companion module ${CONSUMER_SCRIPTS_REL}/${s.name} (imported by the refreshed archivers; absent at the consumer)`);
|
|
310
336
|
log(` then seed the store: create ${ADR_DIR_REL}/, write ${NAV_REL} and regenerate docs/ai/index.md`);
|
|
311
337
|
const code = preflight((m) => error(` ${m}`));
|
|
312
338
|
if (code !== EXIT_OK) {
|
|
@@ -328,7 +354,7 @@ const crossWithoutMonolith = (cwd, args, stamp, { log, error, runMigrate, deps }
|
|
|
328
354
|
const snapshot = writeSnapshot(cwd, refresh, stamp, deps);
|
|
329
355
|
// The FULL refresh is re-planned and re-applied on every entry, so an interrupted one always
|
|
330
356
|
// completes; the discriminator script is written last (see refreshOrder).
|
|
331
|
-
applyScriptRefresh(cwd, refresh, deps);
|
|
357
|
+
const seededNames = applyScriptRefresh(cwd, refresh, deps);
|
|
332
358
|
|
|
333
359
|
// Capture the index-regeneration verdict instead of matching log prose: the rotator logs a failed
|
|
334
360
|
// regeneration and still returns 0, so "the gates are green" would not mean the index is fresh.
|
|
@@ -359,7 +385,7 @@ const crossWithoutMonolith = (cwd, args, stamp, { log, error, runMigrate, deps }
|
|
|
359
385
|
// interrupted crossing whose scripts were already current, which no "old-scheme" claim covers.
|
|
360
386
|
log('[migrate-adr-store] crossing complete — the one-file-per-ADR store is in place (no legacy monolith was present):');
|
|
361
387
|
log(` snapshot: ${snapshot.dir} (${snapshot.viaGitDir ? 'git dir' : 'out-of-tree fallback'}, ${snapshot.fileCount} file(s))`);
|
|
362
|
-
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version`);
|
|
388
|
+
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version${seededNames.length ? ` + seeded ${seededNames.join(', ')}` : ''}`);
|
|
363
389
|
log(` seeded ${ADR_DIR_REL}/ with ${NAV_REL} and regenerated docs/ai/index.md`);
|
|
364
390
|
log(' next: run the normal upgrade (it re-stamps the deployment lineage to the current head),');
|
|
365
391
|
log(' then review the migrated docs/ai/ tree and the re-stamp together and commit them yourself — this command never commits.');
|
|
@@ -394,6 +420,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
394
420
|
log(` old layout: ${monoliths.join(', ')} (will be exploded into ${ADR_DIR_REL}/ then retired)`);
|
|
395
421
|
log(` snapshot → ${preview.dir ? `${preview.dir} (${preview.viaGitDir ? 'git dir' : 'out-of-tree fallback'})` : 'NONE — no out-of-tree location; run inside a git repo (apply would refuse otherwise)'}`);
|
|
396
422
|
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
423
|
+
for (const s of planCompanionSeeds(cwd, refresh, deps)) log(` seed companion module ${CONSUMER_SCRIPTS_REL}/${s.name} (imported by the refreshed archivers; absent at the consumer)`);
|
|
397
424
|
log(' then the conservation-checked rotation:');
|
|
398
425
|
// Surface the rotation's own exit code: a failed dry-run must NOT print the
|
|
399
426
|
// "run with --apply" go-ahead nor exit 0 — it would send the user to --apply on an unsafe tree.
|
|
@@ -419,14 +446,14 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
419
446
|
}
|
|
420
447
|
|
|
421
448
|
const snapshot = writeSnapshot(cwd, refresh, stamp, deps);
|
|
422
|
-
applyScriptRefresh(cwd, refresh, deps);
|
|
449
|
+
const seededNames = applyScriptRefresh(cwd, refresh, deps);
|
|
423
450
|
const code = runMigrate(['--migrate', '--apply'], { root: cwd, log, logError: error });
|
|
424
451
|
if (code !== EXIT_OK) {
|
|
425
452
|
throw stop(`the rotation failed (exit ${code}) — the pre-migration snapshot is at ${snapshot.dir}; resolve the reported problem and re-run (the migration is idempotent).`);
|
|
426
453
|
}
|
|
427
454
|
log('[migrate-adr-store] migrated the 3-tier ADR cascade → one-file-per-ADR store:');
|
|
428
455
|
log(` snapshot: ${snapshot.dir} (${snapshot.viaGitDir ? 'git dir' : 'out-of-tree fallback'}, ${snapshot.fileCount} file(s))`);
|
|
429
|
-
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version`);
|
|
456
|
+
log(` refreshed ${refresh.length} enforcement script(s) to this kit's version${seededNames.length ? ` + seeded ${seededNames.join(', ')}` : ''}`);
|
|
430
457
|
log(' next: run the normal upgrade (it re-stamps the deployment lineage to the current head),');
|
|
431
458
|
log(' then review the migrated docs/ai/ tree and the re-stamp together and commit them yourself — this command never commits.');
|
|
432
459
|
return EXIT_OK;
|
|
@@ -113,10 +113,24 @@ export const parseOp = (kind, token) => {
|
|
|
113
113
|
|
|
114
114
|
// ── config validation (config errors → exit 1) ──────────────────────────────────────
|
|
115
115
|
|
|
116
|
+
// The accepted `flow` schema version — the SINGLE source both the acceptance check and the refusal
|
|
117
|
+
// message use; future flow-aware releases IMPORT this constant, never re-type it. The wire value is
|
|
118
|
+
// pinned NUMERIC (the string form is a named refusal case).
|
|
119
|
+
export const FLOW_SCHEMA_VERSION = 1;
|
|
120
|
+
|
|
121
|
+
// The honest lagging-kit contract sentence (tolerate-first ordering): what a kit WITHOUT the flow
|
|
122
|
+
// branch does when it meets a `flow` block, and that this release enforces nothing against such a
|
|
123
|
+
// reader. doc-parity binds it VERBATIM into the procedures mode doc's exit-1 contract line, so the
|
|
124
|
+
// doc can never soften the admission while the gap is real (enforcement arms with `set-flow`).
|
|
125
|
+
export const FLOW_LAGGING_KIT_CONTRACT =
|
|
126
|
+
'a kit predating the `"flow"` key that reads a config carrying one fails this config load loudly (exit `1`, reddening its full gate matrix); this kit release enforces NO version floor against such a pre-flow reader — tolerate-first ordering is the only mitigation until a flow-aware release arms enforcement on the `set-flow` path';
|
|
127
|
+
|
|
116
128
|
// Validate a parsed orchestration.json object against the schema. Strict: an unknown top-level
|
|
117
129
|
// activity, an unknown slot for an activity, or a recipe invalid-for-slot is an error. All slots are
|
|
118
|
-
// optional. An optional "_README" string key is allowed + ignored (self-documentation).
|
|
119
|
-
//
|
|
130
|
+
// optional. An optional "_README" string key is allowed + ignored (self-documentation). A versioned
|
|
131
|
+
// "flow" object key is TOLERATED when its `schema` strict-equals FLOW_SCHEMA_VERSION — every other
|
|
132
|
+
// byte of the block is deliberately uninterpreted here (nothing in this kit reads or writes it yet).
|
|
133
|
+
// Never a silent fallback — every rejection is a loud `path: reason` (exit 1). Returns the config on success.
|
|
120
134
|
export const validateConfig = (config) => {
|
|
121
135
|
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
122
136
|
throw fail(1, `${CONFIG_REL}: must be a JSON object of activity → { slot: recipe }`);
|
|
@@ -126,6 +140,16 @@ export const validateConfig = (config) => {
|
|
|
126
140
|
if (typeof val !== 'string') throw fail(1, `${CONFIG_REL}: "_README" must be a string`);
|
|
127
141
|
continue;
|
|
128
142
|
}
|
|
143
|
+
if (key === 'flow') {
|
|
144
|
+
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
|
|
145
|
+
throw fail(1, `${CONFIG_REL}: "flow" must be a JSON object carrying { "schema": ${FLOW_SCHEMA_VERSION} }`);
|
|
146
|
+
}
|
|
147
|
+
if (val.schema !== FLOW_SCHEMA_VERSION) {
|
|
148
|
+
const got = 'schema' in val ? JSON.stringify(val.schema) : 'absent';
|
|
149
|
+
throw fail(1, `${CONFIG_REL}: "flow".schema must be the number ${FLOW_SCHEMA_VERSION} (got ${got})`);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
129
153
|
const activityDef = ACTIVITIES[key];
|
|
130
154
|
if (!activityDef) {
|
|
131
155
|
throw fail(1, `${CONFIG_REL}: unknown activity "${key}" (known: ${KNOWN_ACTIVITIES()})`);
|