@sabaiway/agent-workflow-kit 1.25.0 → 1.27.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 +75 -0
- package/README.md +13 -7
- package/SKILL.md +88 -28
- package/bin/install.mjs +74 -29
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/changelog-skeleton.md +23 -0
- package/references/agents/gate-triage.md +23 -0
- package/references/agents/mechanical-sweep.md +20 -0
- package/references/contracts.md +1 -0
- package/references/scripts/archive-decisions.mjs +424 -0
- package/references/scripts/archive-decisions.test.mjs +365 -0
- package/references/scripts/install-git-hooks.mjs +1 -0
- package/references/templates/agent_rules.md +1 -0
- package/references/templates/gates.json +4 -0
- package/tools/cheap-agents.mjs +239 -0
- package/tools/commands.mjs +29 -6
- package/tools/family-members.mjs +2 -1
- package/tools/family-registry.mjs +111 -13
- package/tools/known-footprint.mjs +3 -0
- package/tools/labels.mjs +13 -0
- package/tools/procedures.mjs +19 -0
- package/tools/recipes.mjs +56 -17
- package/tools/renderers.mjs +12 -1
- package/tools/run-gates.mjs +299 -0
- package/tools/semver-lite.mjs +23 -0
- package/tools/setup-backends.mjs +143 -6
- package/tools/view-model.mjs +11 -1
package/tools/commands.mjs
CHANGED
|
@@ -26,15 +26,19 @@ const BARE_INVOCATION = `/${SKILL_NAME}`;
|
|
|
26
26
|
const invocationOf = (token) => `${BARE_INVOCATION}${token ? ` ${token}` : ''}`;
|
|
27
27
|
|
|
28
28
|
// ── kinds ────────────────────────────────────────────────────────────────────────
|
|
29
|
-
// read-only
|
|
30
|
-
// writer
|
|
31
|
-
// guarded
|
|
29
|
+
// read-only — never writes, never commits, never runs a subscription CLI.
|
|
30
|
+
// writer — writes files (a project deployment or a settings/skill placement).
|
|
31
|
+
// guarded — a destructive teardown gated behind a mandatory dry-run-first + explicit consent.
|
|
32
|
+
// project-exec — the kit itself writes nothing, but the mode RUNS the project's own declared
|
|
33
|
+
// commands with the caller's privileges (the `gates` runner) — honest-tagged so a
|
|
34
|
+
// user never reads "read-only" on a surface that executes their gate matrix.
|
|
32
35
|
// `writer` and `guarded` are the "acts on the system" kinds; only an explicit known token may reach
|
|
33
36
|
// one (plus the bare bootstrap exception). Garbage routes to `help` (read-only) — see routeInvocation.
|
|
34
37
|
export const READ_ONLY = 'read-only';
|
|
35
38
|
export const WRITER = 'writer';
|
|
36
39
|
export const GUARDED = 'guarded';
|
|
37
|
-
const
|
|
40
|
+
export const PROJECT_EXEC = 'project-exec';
|
|
41
|
+
const KINDS = new Set([READ_ONLY, WRITER, GUARDED, PROJECT_EXEC]);
|
|
38
42
|
|
|
39
43
|
// ── groups (fixed render order) ────────────────────────────────────────────────────
|
|
40
44
|
export const GROUP_ORDER = Object.freeze(['Inspect', 'Configure', 'Orchestrate', 'Lifecycle']);
|
|
@@ -87,6 +91,13 @@ const CATALOG = [
|
|
|
87
91
|
kind: READ_ONLY,
|
|
88
92
|
oneLine: 'List every command, grouped, marking each as read-only or as one that makes changes.',
|
|
89
93
|
},
|
|
94
|
+
{
|
|
95
|
+
key: 'gates',
|
|
96
|
+
invocation: invocationOf('gates'),
|
|
97
|
+
group: 'Inspect',
|
|
98
|
+
kind: PROJECT_EXEC,
|
|
99
|
+
oneLine: 'Run the project’s own declared gate commands (docs/ai/gates.json) as one batch — a PASS/FAIL table, one summary line.',
|
|
100
|
+
},
|
|
90
101
|
{
|
|
91
102
|
key: 'setup',
|
|
92
103
|
invocation: invocationOf('setup'),
|
|
@@ -101,6 +112,13 @@ const CATALOG = [
|
|
|
101
112
|
kind: WRITER,
|
|
102
113
|
oneLine: 'Seed a read-only command allowlist so routine read-only commands stop prompting (Claude Code; opt-in; preview first).',
|
|
103
114
|
},
|
|
115
|
+
{
|
|
116
|
+
key: 'agents',
|
|
117
|
+
invocation: invocationOf('agents'),
|
|
118
|
+
group: 'Configure',
|
|
119
|
+
kind: WRITER,
|
|
120
|
+
oneLine: 'Place bundled cheap-model subagent definitions for mechanical work — sweeps, changelog skeletons, gate triage (Claude Code; opt-in; preview first).',
|
|
121
|
+
},
|
|
104
122
|
{
|
|
105
123
|
key: 'recipes',
|
|
106
124
|
invocation: invocationOf('recipes'),
|
|
@@ -165,7 +183,12 @@ export const routeInvocation = (token) => {
|
|
|
165
183
|
|
|
166
184
|
// ── render ──────────────────────────────────────────────────────────────────────────
|
|
167
185
|
|
|
168
|
-
const KIND_TAG = {
|
|
186
|
+
const KIND_TAG = {
|
|
187
|
+
[READ_ONLY]: '[read-only] ',
|
|
188
|
+
[WRITER]: '[writer] ',
|
|
189
|
+
[GUARDED]: '[guarded] ',
|
|
190
|
+
[PROJECT_EXEC]: '[runs project cmds]',
|
|
191
|
+
};
|
|
169
192
|
const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
|
|
170
193
|
|
|
171
194
|
// formatHelp() → the grouped, kind-tagged human index. Deterministic; groups in GROUP_ORDER, modes in
|
|
@@ -175,7 +198,7 @@ export const formatHelp = () => {
|
|
|
175
198
|
const lines = [
|
|
176
199
|
`${SKILL_NAME} — command index (this list is read-only)`,
|
|
177
200
|
'',
|
|
178
|
-
'Each command is tagged read-only · writer (makes changes) · guarded (destructive, previews first).',
|
|
201
|
+
'Each command is tagged read-only · writer (makes changes) · guarded (destructive, previews first) · runs project cmds (executes your own declared commands).',
|
|
179
202
|
];
|
|
180
203
|
for (const group of GROUP_ORDER) {
|
|
181
204
|
const inGroup = COMMANDS.filter((c) => c.group === group);
|
package/tools/family-members.mjs
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// ── the unified registry ───────────────────────────────────────────────────────
|
|
14
14
|
// One entry per family member. `installed` is the detect.installed spec (env + home-relative default
|
|
15
15
|
// + marker file); `deployed` is the project-relative stamp a deploy writes (kit + memory only);
|
|
16
|
-
// `npm` is the install package (null for the bridges
|
|
16
|
+
// `npm` is the install package (null for the bridges — placed by `setup`, and once placed refreshed
|
|
17
|
+
// by `init`/`upgrade` from the kit's bundled copies, never npm);
|
|
17
18
|
// `wrapperCmds` is the deduped roles[].cmd set the `setup` linker creates on PATH (bridges only).
|
|
18
19
|
// Kept in lockstep with the 5 in-repo capability.json by the drift-guard test. The two release skills
|
|
19
20
|
// (release-engineering / release-marketing) are deliberately NOT here — they are not family members
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
// side effects on import (the isDirectRun idiom) — tests import the helpers with nothing run.
|
|
18
18
|
|
|
19
19
|
import { existsSync, statSync, readFileSync, lstatSync } from 'node:fs';
|
|
20
|
-
import { join, resolve } from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
20
|
+
import { join, resolve, dirname } from 'node:path';
|
|
21
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
22
22
|
import os from 'node:os';
|
|
23
23
|
import { resolveDir, detectBackends, findOnPath } from './detect-backends.mjs';
|
|
24
|
+
// The ONE dependency-free semver (shared with bin/install.mjs) — the bridge freshness probe compares
|
|
25
|
+
// the placed version against the kit-bundled mirror; null-on-unparseable maps to 'unknown' (INV-B).
|
|
26
|
+
import { parseSemver, compareSemver } from './semver-lite.mjs';
|
|
24
27
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
25
28
|
import { START_MARKER, excludePath, inferVisibility } from './hide-footprint.mjs';
|
|
26
29
|
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
@@ -57,6 +60,10 @@ import {
|
|
|
57
60
|
VISIBILITY_PUBLIC,
|
|
58
61
|
DISPLAY_NAMES,
|
|
59
62
|
displayOf,
|
|
63
|
+
FRESH_CURRENT,
|
|
64
|
+
FRESH_BEHIND,
|
|
65
|
+
FRESH_UNKNOWN,
|
|
66
|
+
FRESH_NOT_CHECKED,
|
|
60
67
|
} from './labels.mjs';
|
|
61
68
|
// The capability-adaptive direct-CLI presenter (Plan §4.2/§4.5): the surface detector, the
|
|
62
69
|
// envelope→ViewModel transform, and the plain/ansi renderers. main() composes them; the agent-mediated
|
|
@@ -135,7 +142,18 @@ export const classifyMember = (member, deps = {}) => {
|
|
|
135
142
|
const report = markerPresent ? validate(skillDir) : { result: NOT_INSTALLED };
|
|
136
143
|
const manifestState = classifyState(markerPresent, report, member);
|
|
137
144
|
const installed = manifestState !== NOT_INSTALLED;
|
|
138
|
-
|
|
145
|
+
// Crash-safe version read: readAuthoritativeVersion THROWS on a present-but-unreadable SKILL.md
|
|
146
|
+
// (stat needs no read permission, readFileSync does — a TOCTOU/EACCES window after validate).
|
|
147
|
+
// The read-only survey must never crash on it: degrade to null, which the bridge freshness probe
|
|
148
|
+
// then reports as 'unknown' (INV-B), never as a throw and never as a silent claim.
|
|
149
|
+
const version = (() => {
|
|
150
|
+
if (manifestState !== OK) return null;
|
|
151
|
+
try {
|
|
152
|
+
return readVersion(skillDir).version ?? null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
139
157
|
|
|
140
158
|
return { name: member.name, kind: member.kind, installed, skillDir: installed ? skillDir : null, manifestState, version };
|
|
141
159
|
};
|
|
@@ -165,6 +183,51 @@ export const MEMORY_ORCH_TEMPLATE_REL = 'references/templates/orchestration.json
|
|
|
165
183
|
const MEMORY_BEHIND_NOTE =
|
|
166
184
|
"the memory installed here doesn't include the current orchestration template — refresh it with `npx @sabaiway/agent-workflow-memory@latest init`, then restart the session.";
|
|
167
185
|
|
|
186
|
+
// ── the bridge freshness probe (deterministic-first — INV-A / INV-B) ─────────────
|
|
187
|
+
// The bridges are not npm packages: their ONLY delivery channel is the copy bundled inside this kit
|
|
188
|
+
// (`bridges/<name>/`, placed by `/agent-workflow-kit setup`). A placed bridge therefore has exactly
|
|
189
|
+
// one authoritative freshness comparison — its installed version (readAuthoritativeVersion, already
|
|
190
|
+
// on the row) vs the kit-bundled mirror's capability.json. BOTH are local files, so the "never
|
|
191
|
+
// checks npm" invariant holds. The probe sets the internal row field `freshness`
|
|
192
|
+
// ('behind' | 'current' | 'unknown'); refreshOf derives the public refresh block STRUCTURALLY from
|
|
193
|
+
// that field (INV-2 — never parsed from caveat text). INV-B: an unreadable/unparseable version on
|
|
194
|
+
// EITHER side degrades to 'unknown' + a plain-language note — never a false claim in either direction.
|
|
195
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
196
|
+
// bridges/ ships beside tools/ in both the repo and the installed kit (the setup-backends.mjs
|
|
197
|
+
// resolution), so this resolves in both. Injectable via deps.bundleRoot for tests.
|
|
198
|
+
const BUNDLE_ROOT = resolve(__dirname, '..', 'bridges');
|
|
199
|
+
|
|
200
|
+
// Crash-safe on the bundled side: an absent / unreadable / malformed bundle manifest → null (INV-B
|
|
201
|
+
// 'unknown'), never a throw — the read-only survey must survive a broken kit install.
|
|
202
|
+
const readBundledBridgeVersion = (name, deps = {}) => {
|
|
203
|
+
const read = deps.readFile ?? readFileSync;
|
|
204
|
+
const root = deps.bundleRoot ?? BUNDLE_ROOT;
|
|
205
|
+
try {
|
|
206
|
+
const manifest = JSON.parse(String(read(join(root, name, 'capability.json'), 'utf8')));
|
|
207
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const bridgeBehindNote = (display, placed, bundled) =>
|
|
214
|
+
`the ${display} installed here (v${placed}) is older than the copy bundled with this kit (v${bundled}) — refresh it with \`/agent-workflow-kit setup\`.`;
|
|
215
|
+
// The recovery differs per failing SIDE: `setup` re-places FROM the bundle, so it can repair an
|
|
216
|
+
// unreadable INSTALLED copy but can never repair an unreadable BUNDLED copy — that needs a kit
|
|
217
|
+
// refresh first (the npx installer), then `setup`.
|
|
218
|
+
const bridgeUnknownNote = (display, side, recovery) =>
|
|
219
|
+
`couldn't compare the ${display} installed here with the copy bundled with this kit (${side}), so its freshness is unknown — ${recovery}.`;
|
|
220
|
+
const UNKNOWN_SIDES = Object.freeze({
|
|
221
|
+
placed: {
|
|
222
|
+
side: 'the installed copy has no readable version',
|
|
223
|
+
recovery: '`/agent-workflow-kit setup` re-places the bundled copy',
|
|
224
|
+
},
|
|
225
|
+
bundled: {
|
|
226
|
+
side: "the kit's bundled copy has no readable version",
|
|
227
|
+
recovery: 'refresh the kit first (`npx @sabaiway/agent-workflow-kit@latest init`), then `/agent-workflow-kit setup`',
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
168
231
|
export const surveyFamily = (deps = {}) =>
|
|
169
232
|
FAMILY_MEMBERS.map((member) => {
|
|
170
233
|
const row = classifyMember(member, deps);
|
|
@@ -184,8 +247,30 @@ export const surveyFamily = (deps = {}) =>
|
|
|
184
247
|
// Only attach when it is provably ABSENT (a non-ENOENT probe error → 'unknown' → skip, never a
|
|
185
248
|
// false "missing" claim). Mirrors the engine-caveat SHAPE; keyed on the Step-2.4 required asset.
|
|
186
249
|
if (row.kind === 'memory-substrate' && row.manifestState === OK && row.skillDir) {
|
|
187
|
-
|
|
250
|
+
const templateProbe = probeMarker(join(row.skillDir, MEMORY_ORCH_TEMPLATE_REL), deps);
|
|
251
|
+
if (templateProbe === 'absent') {
|
|
188
252
|
row.caveats = [...(row.caveats ?? []), MEMORY_BEHIND_NOTE];
|
|
253
|
+
} else if (templateProbe === 'unknown') {
|
|
254
|
+
// Could not verify → this row must not be counted "checked, current" by the verdict (INV-B).
|
|
255
|
+
row.freshness = FRESH_UNKNOWN;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Bridge freshness probe (INV-A / INV-B): only for a provably-OURS placed bridge (manifestState
|
|
259
|
+
// OK). Compares two LOCAL files; a non-OK / absent bridge is out of scope ('not-checked').
|
|
260
|
+
if (row.kind === 'execution-backend' && row.manifestState === OK && row.skillDir) {
|
|
261
|
+
const bundled = readBundledBridgeVersion(row.name, deps);
|
|
262
|
+
const cmp = compareSemver(row.version, bundled);
|
|
263
|
+
if (cmp === null) {
|
|
264
|
+
const { side, recovery } = parseSemver(row.version) === null ? UNKNOWN_SIDES.placed : UNKNOWN_SIDES.bundled;
|
|
265
|
+
row.freshness = FRESH_UNKNOWN;
|
|
266
|
+
row.caveats = [...(row.caveats ?? []), bridgeUnknownNote(displayOf(row.name), side, recovery)];
|
|
267
|
+
} else if (cmp < 0) {
|
|
268
|
+
row.freshness = FRESH_BEHIND;
|
|
269
|
+
row.caveats = [...(row.caveats ?? []), bridgeBehindNote(displayOf(row.name), row.version, bundled)];
|
|
270
|
+
} else {
|
|
271
|
+
// Equal OR newer-than-bundled → not behind. A newer placed bridge is a Phase-2 concern for
|
|
272
|
+
// the refresh DRIVER (INV-D never-downgrade skip); the read-only status axis never flags it.
|
|
273
|
+
row.freshness = FRESH_CURRENT;
|
|
189
274
|
}
|
|
190
275
|
}
|
|
191
276
|
return row;
|
|
@@ -376,18 +461,31 @@ export const surveyBridges = (deps = {}) => {
|
|
|
376
461
|
});
|
|
377
462
|
};
|
|
378
463
|
|
|
379
|
-
// INV-2 (structural refresh — additive, derived, never parsed from a caveat string)
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
464
|
+
// INV-2 (structural refresh — additive, derived, never parsed from a caveat string), per KIND:
|
|
465
|
+
// • memory / engine (REFRESHABLE_KINDS): `behind` ⟷ an OK row carrying a refresh-recommending
|
|
466
|
+
// caveat (surveyFamily attaches caveats to those kinds only then, and every such caveat is
|
|
467
|
+
// refresh-recommending); `recommend` is composed from FAMILY_MEMBERS[].npm — never the caveat text.
|
|
468
|
+
// • execution-backend: `behind` ⟷ the bridge freshness probe's row field (freshness === 'behind');
|
|
469
|
+
// `recommend` is composed PER-KIND — bridges have npm:null (placed by `setup`, never npx), so the
|
|
470
|
+
// runnable recovery is `/agent-workflow-kit setup`, never an npx composition.
|
|
471
|
+
// • composition-root (the kit): no freshness probe exists on this surface (the two-axes doctrine —
|
|
472
|
+
// kit freshness is the npx installer's axis) → never behind, 'not-checked'.
|
|
473
|
+
// `freshness` is the checked-vs-unknown signal the zero-behind verdict scopes itself with (INV-C):
|
|
474
|
+
// behind:false alone cannot distinguish "checked, current" from "could not be checked".
|
|
386
475
|
const REFRESHABLE_KINDS = new Set(['memory-substrate', 'methodology-engine']);
|
|
387
476
|
const npmOf = (name) => FAMILY_MEMBERS.find((m) => m.name === name)?.npm ?? null;
|
|
477
|
+
export const BRIDGE_REFRESH_RECOMMEND = '/agent-workflow-kit setup';
|
|
388
478
|
const refreshOf = (m) => {
|
|
479
|
+
if (m.kind === 'execution-backend') {
|
|
480
|
+
const freshness = m.freshness ?? FRESH_NOT_CHECKED;
|
|
481
|
+
const behind = freshness === FRESH_BEHIND;
|
|
482
|
+
return { behind, recommend: behind ? BRIDGE_REFRESH_RECOMMEND : null, freshness };
|
|
483
|
+
}
|
|
389
484
|
const behind = REFRESHABLE_KINDS.has(m.kind) && Boolean(m.caveats?.length);
|
|
390
|
-
|
|
485
|
+
const freshness = REFRESHABLE_KINDS.has(m.kind) && m.manifestState === OK
|
|
486
|
+
? m.freshness ?? (behind ? FRESH_BEHIND : FRESH_CURRENT)
|
|
487
|
+
: FRESH_NOT_CHECKED;
|
|
488
|
+
return { behind, recommend: behind ? `npx ${npmOf(m.name)}@latest init` : null, freshness };
|
|
391
489
|
};
|
|
392
490
|
|
|
393
491
|
export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
@@ -399,7 +497,7 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
399
497
|
state: STATE_PUBLIC[m.manifestState] ?? STATE_PUBLIC[UNKNOWN],
|
|
400
498
|
};
|
|
401
499
|
if (m.caveats?.length) entry.notes = m.caveats; // plain-language observations (Steps 2.2 / engine)
|
|
402
|
-
entry.refresh = refreshOf(m); // additive (INV-1): always present; { behind, recommend } (INV-2)
|
|
500
|
+
entry.refresh = refreshOf(m); // additive (INV-1): always present; { behind, recommend, freshness } (INV-2)
|
|
403
501
|
return entry;
|
|
404
502
|
});
|
|
405
503
|
const envelope = { deploymentHead: EXPECTED_WORKFLOW_VERSION, installed };
|
|
@@ -43,6 +43,8 @@ export const KIT_OWN_PATHS = [
|
|
|
43
43
|
'/scripts/_expect-shim.mjs',
|
|
44
44
|
'/scripts/archive-changelog.mjs',
|
|
45
45
|
'/scripts/archive-changelog.test.mjs',
|
|
46
|
+
'/scripts/archive-decisions.mjs',
|
|
47
|
+
'/scripts/archive-decisions.test.mjs',
|
|
46
48
|
'/scripts/archive-issues.mjs',
|
|
47
49
|
'/scripts/archive-issues.test.mjs',
|
|
48
50
|
'/scripts/check-docs-size.mjs',
|
|
@@ -59,6 +61,7 @@ export const KIT_OWN_PATHS = [
|
|
|
59
61
|
// filesystem (expandGlob) to concrete present files before any git probe; never fed to ls-files.
|
|
60
62
|
export const KNOWN_FOOTPRINT = [
|
|
61
63
|
{ pattern: '/.claude/skills/', owner: 'Claude Code', type: 'dir', falsePositiveRisk: false, note: 'local-dev skills; absorbs the AD-013 one-off' },
|
|
64
|
+
{ pattern: '/.claude/agents/', owner: 'Claude Code', type: 'dir', falsePositiveRisk: false, note: 'project subagent definitions (incl. the kit-placed cheap-lane vehicles)' },
|
|
62
65
|
{ pattern: '/.cursor/rules/', owner: 'Cursor', type: 'dir', falsePositiveRisk: false, note: 'project rule files' },
|
|
63
66
|
{ pattern: '/.cursorrules', owner: 'Cursor (legacy)', type: 'file', falsePositiveRisk: true, note: 'legacy single-file rules' },
|
|
64
67
|
{ pattern: '/.codeium/', owner: 'Codeium/Windsurf', type: 'dir', falsePositiveRisk: false, note: 'home-scoped launchers live under ~/, out of scope' },
|
package/tools/labels.mjs
CHANGED
|
@@ -41,6 +41,19 @@ export const STATE_PUBLIC = Object.freeze({
|
|
|
41
41
|
// "hidden fence" / marker terms. ('hidden' here is the PUBLIC visibility word, not the internal fence.)
|
|
42
42
|
export const VISIBILITY_PUBLIC = Object.freeze({ visible: 'visible', hidden: 'hidden', ambiguous: 'unclear' });
|
|
43
43
|
|
|
44
|
+
// ── refresh freshness tokens (PUBLIC — the envelope's installed[].refresh.freshness) ────────────────
|
|
45
|
+
// The checked-vs-unknown signal the zero-behind verdict scopes itself with (INV-C): `behind:false`
|
|
46
|
+
// alone cannot distinguish "checked, current" from "could not be checked".
|
|
47
|
+
// 'current' / 'behind' — a freshness probe RAN and concluded (these two are the "checked" scope);
|
|
48
|
+
// 'unknown' — a probe ran but could not conclude (INV-B: never collapsed to a claim
|
|
49
|
+
// in either direction);
|
|
50
|
+
// 'not-checked' — no freshness probe exists for this member/state (e.g. the kit itself:
|
|
51
|
+
// its freshness is the npx installer's axis, never checked here).
|
|
52
|
+
export const FRESH_CURRENT = 'current';
|
|
53
|
+
export const FRESH_BEHIND = 'behind';
|
|
54
|
+
export const FRESH_UNKNOWN = 'unknown';
|
|
55
|
+
export const FRESH_NOT_CHECKED = 'not-checked';
|
|
56
|
+
|
|
44
57
|
// Short, user-facing labels for the version block (kit · memory · engine · codex-bridge · …), so the
|
|
45
58
|
// render is deterministic and the agent never invents a label.
|
|
46
59
|
export const DISPLAY_NAMES = Object.freeze({
|
package/tools/procedures.mjs
CHANGED
|
@@ -188,6 +188,22 @@ const reviewLoopAdvice = (slots) =>
|
|
|
188
188
|
]
|
|
189
189
|
: [];
|
|
190
190
|
|
|
191
|
+
// The cost-lane advisory block (cost-tiered execution — orchestration.md §5 canon, paraphrased
|
|
192
|
+
// at the point of use like reviewLoopAdvice paraphrases §9/§4). Rendered UNCONDITIONALLY for
|
|
193
|
+
// every activity — the lanes route EVERY step, review-backed or not (unlike reviewLoopAdvice,
|
|
194
|
+
// which fires only when a review backend engages). It may name the kit's own GENERIC L0
|
|
195
|
+
// surfaces (the gate runner, the rotation checks, the cheap-agents vehicles) — point-of-use
|
|
196
|
+
// routing, still project-agnostic (never a project's publish mechanics). Its distinctive tokens
|
|
197
|
+
// are drift-guarded against the canon on both sides (procedures.test.mjs + the engine canon tests).
|
|
198
|
+
const costLanesAdvice = () => [
|
|
199
|
+
'Cost lanes (orchestration.md §5) — route every step to the cheapest adequate executor:',
|
|
200
|
+
' • L0 deterministic script — run the gate matrix as ONE batch: /agent-workflow-kit gates (the project-declared docs/ai/gates.json); rotation headroom: archive-changelog / archive-issues / archive-decisions --check. An exit code beats a model re-read.',
|
|
201
|
+
' • L1 cheap subagent (small model, low effort, read-only tools — /agent-workflow-kit agents places the vehicles) — mechanical sweeps, changelog fact-skeletons, gate-failure triage. Extraction/drafting ONLY; the orchestrator verifies the output and owns every conclusion.',
|
|
202
|
+
' • L2 subscription bridge (codex / agy) — reviews per the resolved recipe above, on frontier bridge models (quality-first).',
|
|
203
|
+
' • L3 frontier — judgment: plan/fold/synthesis, ADR/handover/changelog-entry wording, persuasive copy, go/no-go, real code.',
|
|
204
|
+
' • A step with no named guardrail does not move down a lane; red lines never move down (council review models · real code · memory/copy wording · the maintainer approval asks).',
|
|
205
|
+
];
|
|
206
|
+
|
|
191
207
|
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
192
208
|
// the closed flag set, the grounding note, the round-2/continue delta, and the guarded passthrough
|
|
193
209
|
// tiers — every descriptor printed VERBATIM from the registry mirror of the bridge manifest
|
|
@@ -224,6 +240,7 @@ const formatHuman = ({ activity, section, slots, warnings }) => {
|
|
|
224
240
|
}
|
|
225
241
|
const advice = reviewLoopAdvice(slots);
|
|
226
242
|
if (advice.length) lines.push('', ...advice);
|
|
243
|
+
lines.push('', ...costLanesAdvice());
|
|
227
244
|
if (warnings.length) {
|
|
228
245
|
lines.push('', 'warnings:');
|
|
229
246
|
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
@@ -240,6 +257,8 @@ const buildJson = ({ activity, section, slots, configSource, warnings }) => ({
|
|
|
240
257
|
slots.map((s) => [s.slot, { recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts }]),
|
|
241
258
|
),
|
|
242
259
|
reviewLoop: reviewLoopAdvice(slots),
|
|
260
|
+
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|
|
261
|
+
costLanes: costLanesAdvice(),
|
|
243
262
|
configSource,
|
|
244
263
|
warnings,
|
|
245
264
|
});
|
package/tools/recipes.mjs
CHANGED
|
@@ -283,21 +283,43 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
283
283
|
};
|
|
284
284
|
};
|
|
285
285
|
|
|
286
|
+
// ── the one-line backend status (deterministic-first: the tool speaks, the agent pastes) ───────────
|
|
287
|
+
|
|
288
|
+
// composeStatusLine(detection, recommendation) → the ENTIRE one-line backend-status summary the
|
|
289
|
+
// bootstrap/upgrade report footers print. Machine-composed so the agent pastes it verbatim and
|
|
290
|
+
// composes NOTHING factual (this closes the realistic-example contamination class: a session once
|
|
291
|
+
// echoed SKILL.md's canonical example while the detector said otherwise). Display names come from
|
|
292
|
+
// DISPLAY_ALIASES — the ONE alias table the recommendation clause already uses; ordering is the
|
|
293
|
+
// deterministic BACKEND_PRIORITY (codex before agy), independent of detection emission order.
|
|
294
|
+
// Always exactly one line: no part may carry a newline (pinned by tests).
|
|
295
|
+
export const composeStatusLine = (detection, recommendation) => {
|
|
296
|
+
const backends = [...detection]
|
|
297
|
+
.sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
|
|
298
|
+
.map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`)
|
|
299
|
+
.join(' · ');
|
|
300
|
+
return `backends: ${backends} — run /agent-workflow-kit backends · recipes: ${recommendation.clause} — see /agent-workflow-kit recipes`;
|
|
301
|
+
};
|
|
302
|
+
|
|
286
303
|
// ── report + CLI ─────────────────────────────────────────────────────────────────
|
|
287
304
|
|
|
288
|
-
// The structured report behind `--json` — the recipes, the recommendation,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
role,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
})
|
|
305
|
+
// The structured report behind `--json` — the recipes, the recommendation, a plan per recipe, and
|
|
306
|
+
// (additive) the pasteable one-line backend status composed from the same detection.
|
|
307
|
+
export const buildReport = (detection) => {
|
|
308
|
+
const recommendation = recommendRecipe(detection);
|
|
309
|
+
return {
|
|
310
|
+
recipes: RECIPES.map(({ id, title, role, minBackends, degradesTo, summary }) => ({
|
|
311
|
+
id,
|
|
312
|
+
title,
|
|
313
|
+
role,
|
|
314
|
+
minBackends,
|
|
315
|
+
degradesTo,
|
|
316
|
+
summary,
|
|
317
|
+
})),
|
|
318
|
+
recommendation,
|
|
319
|
+
plans: RECIPES.map((r) => planRecipe(r.id, detection)),
|
|
320
|
+
statusLine: composeStatusLine(detection, recommendation),
|
|
321
|
+
};
|
|
322
|
+
};
|
|
301
323
|
|
|
302
324
|
// formatRecipes(detection) → deterministic human advisor text: the four recipes, the recommendation,
|
|
303
325
|
// and the per-recipe plan for the current environment (degradation reasons + dispatch + notes).
|
|
@@ -320,20 +342,37 @@ export const formatRecipes = (detection) => {
|
|
|
320
342
|
return lines.join('\n');
|
|
321
343
|
};
|
|
322
344
|
|
|
345
|
+
// The full argv vocabulary — anything else rejects LOUDLY. The old parse silently routed unknown
|
|
346
|
+
// args into the multi-line human render; with `--status-line` (whose output is pasted as fact) a
|
|
347
|
+
// mistyped flag masquerading as a mode would be a silent failure, so the parse is strict now.
|
|
348
|
+
const KNOWN_ARGS = new Set(['--help', '-h', '--json', '--status-line']);
|
|
349
|
+
|
|
323
350
|
const main = (argv) => {
|
|
324
351
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
325
352
|
console.log(`recipes — read-only orchestration-recipe advisor for the agent-workflow family.
|
|
326
353
|
|
|
327
354
|
Usage:
|
|
328
|
-
node recipes.mjs [--json]
|
|
355
|
+
node recipes.mjs [--json | --status-line]
|
|
329
356
|
|
|
330
357
|
Lists the four recipes (Solo / Reviewed / Council / Delegated) and, from the read-only backend
|
|
331
|
-
detector, plans + recommends one for the current environment.
|
|
332
|
-
|
|
358
|
+
detector, plans + recommends one for the current environment. --status-line prints exactly ONE
|
|
359
|
+
line — the machine-composed backend-status summary the bootstrap/upgrade reports paste verbatim.
|
|
360
|
+
--json emits the structured report (incl. the same line as \`statusLine\`); the two are mutually
|
|
361
|
+
exclusive. Detection only — never writes, never commits, never runs a subscription CLI.`);
|
|
333
362
|
return;
|
|
334
363
|
}
|
|
364
|
+
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
365
|
+
if (unknown !== undefined) {
|
|
366
|
+
console.error(`[agent-workflow-kit] unknown argument: ${unknown}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
if (argv.includes('--json') && argv.includes('--status-line')) {
|
|
370
|
+
console.error('[agent-workflow-kit] --json and --status-line are mutually exclusive — pick one output');
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
335
373
|
const detection = detectBackends();
|
|
336
|
-
if (argv.includes('--
|
|
374
|
+
if (argv.includes('--status-line')) console.log(composeStatusLine(detection, recommendRecipe(detection)));
|
|
375
|
+
else if (argv.includes('--json')) console.log(JSON.stringify(buildReport(detection), null, 2));
|
|
337
376
|
else console.log(formatRecipes(detection));
|
|
338
377
|
};
|
|
339
378
|
|
package/tools/renderers.mjs
CHANGED
|
@@ -34,7 +34,18 @@ const renderMembers = (vm, { color, glyph }) => {
|
|
|
34
34
|
// refresh.behind drives a HEADLINE COUNT only — the recovery command stays in the verbatim notes
|
|
35
35
|
// above (the direct CLI never dedupes, never re-prints it). Omitted when nothing is behind.
|
|
36
36
|
if (vm.headline.behind > 0) {
|
|
37
|
-
|
|
37
|
+
// A mixed behind+unknown state must not swallow the unknown count (INV-B): append it here so an
|
|
38
|
+
// uncheckable member never disappears behind the refresh headline.
|
|
39
|
+
const unknownTail = vm.headline.unknown > 0 ? ` · ${vm.headline.unknown} could not be checked` : '';
|
|
40
|
+
lines.push(` ${vm.headline.behind} member(s) need a refresh (see the ${glyph.note} notes above)${unknownTail}.`);
|
|
41
|
+
} else if (vm.headline.unknown > 0) {
|
|
42
|
+
// INV-B × INV-C: an uncheckable member BLOCKS the all-current verdict — state the split, never a
|
|
43
|
+
// blanket freshness claim in either direction.
|
|
44
|
+
lines.push(` freshness: ${vm.headline.checked} member(s) checked and current · ${vm.headline.unknown} could not be checked.`);
|
|
45
|
+
} else if (vm.headline.checked > 0) {
|
|
46
|
+
// INV-C: the zero-behind verdict is composed by the TOOL, scoped to what was actually checked —
|
|
47
|
+
// the agent has no gap to fill with an "everything is fresh" gloss.
|
|
48
|
+
lines.push(` freshness: all ${vm.headline.checked} checked member(s) are current.`);
|
|
38
49
|
}
|
|
39
50
|
return lines;
|
|
40
51
|
};
|