@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.
@@ -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
- for (const { canon, dst, name } of refreshOrder(refresh)) {
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). Never a silent
119
- // fallback every rejection is a loud `path: reason` (exit 1). Returns the config on success.
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()})`);