@sabaiway/agent-workflow-kit 1.17.0 → 1.19.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 +55 -0
- package/README.md +13 -7
- package/SKILL.md +44 -8
- package/bin/install.mjs +128 -33
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/templates/orchestration.json +8 -3
- package/tools/commands.mjs +7 -0
- package/tools/family-members.mjs +67 -0
- package/tools/family-registry.mjs +133 -161
- package/tools/inject-methodology.mjs +90 -16
- package/tools/labels.mjs +81 -0
- package/tools/orchestration-config.mjs +297 -0
- package/tools/orchestration-write.mjs +97 -0
- package/tools/presentation.mjs +55 -0
- package/tools/procedures.mjs +17 -102
- package/tools/renderers.mjs +119 -0
- package/tools/set-recipe.mjs +217 -0
- package/tools/setup-backends.mjs +71 -5
- package/tools/surface.mjs +75 -0
- package/tools/view-model.mjs +89 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// family-members.mjs — the pure DATA LEAF: the one authoritative table of agent-workflow family members.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from family-registry.mjs so the two consumers that only need the static table — the npx
|
|
4
|
+
// installer (bin/install.mjs, which derives its init-refresh cascade from it) and family-registry
|
|
5
|
+
// itself — can import the DATA without dragging in the whole status/presenter graph
|
|
6
|
+
// (detect-backends, the manifest validator, hide-footprint, engine-source, recipes, the renderers …).
|
|
7
|
+
// install.mjs runs on the npx cold-start hot path, so the leaner import matters; single-source-of-truth
|
|
8
|
+
// + the drift-guard (family-registry.test.mjs pins FAMILY_MEMBERS to the 5 in-repo capability.json)
|
|
9
|
+
// are preserved — the table just lives in a dependency-free leaf now.
|
|
10
|
+
//
|
|
11
|
+
// Pure data, no imports, no side effects, Node >= 18.
|
|
12
|
+
|
|
13
|
+
// ── the unified registry ───────────────────────────────────────────────────────
|
|
14
|
+
// One entry per family member. `installed` is the detect.installed spec (env + home-relative default
|
|
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, which are placed by `setup`, not npm);
|
|
17
|
+
// `wrapperCmds` is the deduped roles[].cmd set the `setup` linker creates on PATH (bridges only).
|
|
18
|
+
// Kept in lockstep with the 5 in-repo capability.json by the drift-guard test. The two release skills
|
|
19
|
+
// (release-engineering / release-marketing) are deliberately NOT here — they are not family members
|
|
20
|
+
// (AD-013): no capability.json, not in the kit tarball, not in the role vocabulary.
|
|
21
|
+
export const FAMILY_MEMBERS = [
|
|
22
|
+
{
|
|
23
|
+
name: 'agent-workflow-kit',
|
|
24
|
+
kind: 'composition-root',
|
|
25
|
+
installed: { env: 'AGENT_WORKFLOW_KIT_DIR', default: '~/.claude/skills/agent-workflow-kit', file: 'SKILL.md' },
|
|
26
|
+
deployed: { file: 'docs/ai/.workflow-version' },
|
|
27
|
+
npm: '@sabaiway/agent-workflow-kit',
|
|
28
|
+
wrapperCmds: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'agent-workflow-memory',
|
|
32
|
+
kind: 'memory-substrate',
|
|
33
|
+
installed: { env: 'AGENT_WORKFLOW_MEMORY_DIR', default: '~/.claude/skills/agent-workflow-memory', file: 'SKILL.md' },
|
|
34
|
+
deployed: { file: 'docs/ai/.memory-version' },
|
|
35
|
+
npm: '@sabaiway/agent-workflow-memory',
|
|
36
|
+
wrapperCmds: [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'agent-workflow-engine',
|
|
40
|
+
kind: 'methodology-engine',
|
|
41
|
+
installed: { env: 'AGENT_WORKFLOW_ENGINE_DIR', default: '~/.claude/skills/agent-workflow-engine', file: 'SKILL.md' },
|
|
42
|
+
deployed: null,
|
|
43
|
+
npm: '@sabaiway/agent-workflow-engine',
|
|
44
|
+
wrapperCmds: [],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'codex-cli-bridge',
|
|
48
|
+
kind: 'execution-backend',
|
|
49
|
+
installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
|
|
50
|
+
deployed: null,
|
|
51
|
+
npm: null,
|
|
52
|
+
wrapperCmds: ['codex-exec', 'codex-review'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'antigravity-cli-bridge',
|
|
56
|
+
kind: 'execution-backend',
|
|
57
|
+
installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
|
|
58
|
+
deployed: null,
|
|
59
|
+
npm: null,
|
|
60
|
+
wrapperCmds: ['agy-run'],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// A GLOBAL skill (lives under ~/.claude/skills) may be shared by other projects on the host — the
|
|
65
|
+
// uninstaller warns before removing one (there is no cross-project dependency tracking). All current
|
|
66
|
+
// members are global skills; the field is explicit so the warning is data-driven, not hardcoded.
|
|
67
|
+
export const isGlobalSkill = (member) => member.kind !== undefined; // every member is a global skill today
|
|
@@ -25,7 +25,9 @@ import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from
|
|
|
25
25
|
import { START_MARKER, excludePath, inferVisibility } from './hide-footprint.mjs';
|
|
26
26
|
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
27
27
|
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
28
|
-
|
|
28
|
+
// The config reader lives in orchestration-config.mjs (the single config contract). The read-only status
|
|
29
|
+
// settings-survey reuses THIS reader (one strict-JSON + loud-on-malformed contract), not a second copy.
|
|
30
|
+
import { loadConfig } from './orchestration-config.mjs';
|
|
29
31
|
// The deployment-lineage head + the shared settings readers are reused from velocity-profile so the
|
|
30
32
|
// `--json` envelope/settings-survey has ONE implementation each, never a drifting copy:
|
|
31
33
|
// EXPECTED_WORKFLOW_VERSION — the head literal (drift-guarded vs memory LINEAGE_HEAD)
|
|
@@ -39,73 +41,52 @@ import {
|
|
|
39
41
|
readSettingsFile,
|
|
40
42
|
resolveEffectiveMode,
|
|
41
43
|
} from './velocity-profile.mjs';
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
// The status vocabulary (manifestState constants, internal→public maps, display names, the no-leak
|
|
45
|
+
// forbidden set) lives in the frozen labels.mjs LEAF (Plan §4.2 B1) so the import graph is acyclic —
|
|
46
|
+
// nothing imports family-registry for vocabulary. Imported here for internal use; the public subset is
|
|
47
|
+
// re-exported below.
|
|
48
|
+
import {
|
|
49
|
+
NOT_INSTALLED,
|
|
50
|
+
UNSUPPORTED_SCHEMA,
|
|
51
|
+
INVALID_MANIFEST,
|
|
52
|
+
STUB,
|
|
53
|
+
FOREIGN,
|
|
54
|
+
OK,
|
|
55
|
+
UNKNOWN,
|
|
56
|
+
STATE_PUBLIC,
|
|
57
|
+
VISIBILITY_PUBLIC,
|
|
58
|
+
DISPLAY_NAMES,
|
|
59
|
+
displayOf,
|
|
60
|
+
} from './labels.mjs';
|
|
61
|
+
// The capability-adaptive direct-CLI presenter (Plan §4.2/§4.5): the surface detector, the
|
|
62
|
+
// envelope→ViewModel transform, and the plain/ansi renderers. main() composes them; the agent-mediated
|
|
63
|
+
// `status` surface ignores them (it consumes --json). These are leaves — no import cycle.
|
|
64
|
+
import { detectSurface } from './surface.mjs';
|
|
65
|
+
import { toViewModel } from './view-model.mjs';
|
|
66
|
+
import { render } from './renderers.mjs';
|
|
67
|
+
|
|
68
|
+
// ── manifestState values — re-export the EXACT public subset family-registry exported before B1 ─────
|
|
69
|
+
// (the 7 state constants + DISPLAY_NAMES) so every existing importer (uninstall.mjs, the test suites)
|
|
70
|
+
// stays green. STATE_PUBLIC / VISIBILITY_PUBLIC / displayOf were private here and are NOT re-exported.
|
|
71
|
+
export {
|
|
72
|
+
NOT_INSTALLED,
|
|
73
|
+
UNSUPPORTED_SCHEMA,
|
|
74
|
+
INVALID_MANIFEST,
|
|
75
|
+
STUB,
|
|
76
|
+
FOREIGN,
|
|
77
|
+
OK,
|
|
78
|
+
UNKNOWN,
|
|
79
|
+
DISPLAY_NAMES,
|
|
80
|
+
};
|
|
53
81
|
|
|
54
82
|
// ── the unified registry ───────────────────────────────────────────────────────
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export const FAMILY_MEMBERS = [
|
|
63
|
-
{
|
|
64
|
-
name: 'agent-workflow-kit',
|
|
65
|
-
kind: 'composition-root',
|
|
66
|
-
installed: { env: 'AGENT_WORKFLOW_KIT_DIR', default: '~/.claude/skills/agent-workflow-kit', file: 'SKILL.md' },
|
|
67
|
-
deployed: { file: 'docs/ai/.workflow-version' },
|
|
68
|
-
npm: '@sabaiway/agent-workflow-kit',
|
|
69
|
-
wrapperCmds: [],
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'agent-workflow-memory',
|
|
73
|
-
kind: 'memory-substrate',
|
|
74
|
-
installed: { env: 'AGENT_WORKFLOW_MEMORY_DIR', default: '~/.claude/skills/agent-workflow-memory', file: 'SKILL.md' },
|
|
75
|
-
deployed: { file: 'docs/ai/.memory-version' },
|
|
76
|
-
npm: '@sabaiway/agent-workflow-memory',
|
|
77
|
-
wrapperCmds: [],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'agent-workflow-engine',
|
|
81
|
-
kind: 'methodology-engine',
|
|
82
|
-
installed: { env: 'AGENT_WORKFLOW_ENGINE_DIR', default: '~/.claude/skills/agent-workflow-engine', file: 'SKILL.md' },
|
|
83
|
-
deployed: null,
|
|
84
|
-
npm: '@sabaiway/agent-workflow-engine',
|
|
85
|
-
wrapperCmds: [],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'codex-cli-bridge',
|
|
89
|
-
kind: 'execution-backend',
|
|
90
|
-
installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
|
|
91
|
-
deployed: null,
|
|
92
|
-
npm: null,
|
|
93
|
-
wrapperCmds: ['codex-exec', 'codex-review'],
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'antigravity-cli-bridge',
|
|
97
|
-
kind: 'execution-backend',
|
|
98
|
-
installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
|
|
99
|
-
deployed: null,
|
|
100
|
-
npm: null,
|
|
101
|
-
wrapperCmds: ['agy-run'],
|
|
102
|
-
},
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
// A GLOBAL skill (lives under ~/.claude/skills) may be shared by other projects on the host — the
|
|
106
|
-
// uninstaller warns before removing one (there is no cross-project dependency tracking). All current
|
|
107
|
-
// members are global skills; the field is explicit so the warning is data-driven, not hardcoded.
|
|
108
|
-
export const isGlobalSkill = (member) => member.kind !== undefined; // every member is a global skill today
|
|
83
|
+
// FAMILY_MEMBERS (+ isGlobalSkill) is the one authoritative member table. It moved to the
|
|
84
|
+
// dependency-free DATA LEAF family-members.mjs so the npx installer can derive its init-refresh cascade
|
|
85
|
+
// from the table WITHOUT importing this whole status/presenter graph (a leaner npx cold-start path).
|
|
86
|
+
// Re-exported here so every existing importer (uninstall.mjs, the test suites) stays green, and the
|
|
87
|
+
// drift-guard (family-registry.test.mjs) still pins the table to the 5 in-repo capability.json.
|
|
88
|
+
export { FAMILY_MEMBERS, isGlobalSkill } from './family-members.mjs';
|
|
89
|
+
import { FAMILY_MEMBERS } from './family-members.mjs';
|
|
109
90
|
|
|
110
91
|
// ── pure probes ──────────────────────────────────────────────────────────────────
|
|
111
92
|
// Wrapped marker probe → 'present' (a regular file) | 'absent' (ENOENT / not a regular file) |
|
|
@@ -266,62 +247,10 @@ export const surveyProject = (projectDir, deps = {}) => {
|
|
|
266
247
|
};
|
|
267
248
|
|
|
268
249
|
// ── report ───────────────────────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
const formatSettings = (s) => {
|
|
274
|
-
const out = ['', 'settings'];
|
|
275
|
-
if (s.recipes?.error) out.push(` ${pad('recipes', 14)}error: ${s.recipes.error}`);
|
|
276
|
-
else {
|
|
277
|
-
const parts = [];
|
|
278
|
-
for (const [act, slots] of Object.entries(s.recipes?.activities ?? {})) {
|
|
279
|
-
for (const [slot, r] of Object.entries(slots)) parts.push(`${act}.${slot}=${r.recipe}`);
|
|
280
|
-
}
|
|
281
|
-
out.push(` ${pad('recipes', 14)}${parts.join(' · ') || '—'}`);
|
|
282
|
-
if (s.recipes?.detectError) out.push(` ${pad('', 14)}↳ couldn't check backends (${s.recipes.detectError}); recipes floored at solo`);
|
|
283
|
-
}
|
|
284
|
-
if (s.attribution?.error) out.push(` ${pad('attribution', 14)}error: ${s.attribution.error}`);
|
|
285
|
-
else out.push(` ${pad('attribution', 14)}includeCoAuthoredBy effective=${String(s.attribution?.effective)}`);
|
|
286
|
-
if (s.velocity?.error) out.push(` ${pad('velocity', 14)}error: ${s.velocity.error}`);
|
|
287
|
-
else out.push(` ${pad('velocity', 14)}defaultMode=${String(s.velocity?.defaultMode)} · allow project/local=${s.velocity?.allowEntries?.project}/${s.velocity?.allowEntries?.local}`);
|
|
288
|
-
return out;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
export const formatStatus = (family, project = null, extras = {}) => {
|
|
292
|
-
const lines = ['agent-workflow family — installed skills (skill axis)', ''];
|
|
293
|
-
for (const m of family) {
|
|
294
|
-
const ver = m.version ? `v${m.version}` : '—';
|
|
295
|
-
lines.push(` ${pad(m.name, 26)}[${pad(m.manifestState, 16)}] ${pad(ver, 10)} ${m.kind}`);
|
|
296
|
-
for (const c of m.caveats ?? []) lines.push(` ↳ ${c}`);
|
|
297
|
-
}
|
|
298
|
-
if (extras.bridges) {
|
|
299
|
-
const WRAP_MARK = { present: '✓', missing: '✗', unknown: '?' };
|
|
300
|
-
lines.push('', 'execution backends (host)', '');
|
|
301
|
-
for (const b of extras.bridges) {
|
|
302
|
-
const w = b.wrappers.map((x) => `${x.cmd} ${WRAP_MARK[x.state] ?? '?'}`).join(', ') || '—';
|
|
303
|
-
lines.push(` ${pad(b.display, 20)}${pad(b.readiness, 18)}wrappers: ${w}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (project) {
|
|
307
|
-
lines.push('', `project deployment (${project.dir})`, '');
|
|
308
|
-
if (!project.deployed) {
|
|
309
|
-
lines.push(' no agent-workflow deployment detected here (no docs/ai, no version stamp).');
|
|
310
|
-
} else {
|
|
311
|
-
for (const s of project.stamps) {
|
|
312
|
-
lines.push(` ${pad(s.file, 26)}${s.version ?? '—'}`);
|
|
313
|
-
}
|
|
314
|
-
lines.push(` ${pad('docs/ai present', 26)}${project.docsAiPresent ? 'yes' : 'no'}`);
|
|
315
|
-
if (extras.visibility) {
|
|
316
|
-
lines.push(` ${pad('visibility', 26)}${extras.visibility.error ? `error: ${extras.visibility.error}` : extras.visibility.state}`);
|
|
317
|
-
} else {
|
|
318
|
-
lines.push(` ${pad('hidden-mode fence', 26)}${project.hiddenFence ? 'present' : 'absent'}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (extras.settings) lines.push(...formatSettings(extras.settings));
|
|
322
|
-
}
|
|
323
|
-
return lines.join('\n');
|
|
324
|
-
};
|
|
250
|
+
// The direct-CLI human render (formatStatus + formatSettings) was REPLACED by the capability-adaptive
|
|
251
|
+
// presenter pipeline (surface → view-model → renderers, Plan §4.2/§4.5): main() builds the no-leak
|
|
252
|
+
// envelope once, then renders it via toViewModel + render (plain/ansi) or prints it as JSON. ONE data
|
|
253
|
+
// source for both surfaces — the agent-mediated `status` consumes `--json`, the direct CLI renders it.
|
|
325
254
|
|
|
326
255
|
// ── the no-leak --json envelope ──────────────────────────────────────────────────
|
|
327
256
|
// A machine-readable view with USER-SAFE field names only — NEVER the internal manifestState /
|
|
@@ -329,28 +258,8 @@ export const formatStatus = (family, project = null, extras = {}) => {
|
|
|
329
258
|
// consumes THIS, never the human table verbatim. An envelope-shape test pins its shape so later phases
|
|
330
259
|
// (the settings/visibility block) can't silently break the Phase-2 version consumer.
|
|
331
260
|
|
|
332
|
-
// internal
|
|
333
|
-
//
|
|
334
|
-
const STATE_PUBLIC = {
|
|
335
|
-
[OK]: 'installed',
|
|
336
|
-
[NOT_INSTALLED]: 'absent',
|
|
337
|
-
[FOREIGN]: 'other-tool',
|
|
338
|
-
[STUB]: 'placeholder',
|
|
339
|
-
[INVALID_MANIFEST]: 'invalid',
|
|
340
|
-
[UNSUPPORTED_SCHEMA]: 'unsupported',
|
|
341
|
-
[UNKNOWN]: 'uncheckable',
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
// Short, user-facing labels for the version block (kit · memory · engine · codex-bridge · …), so the
|
|
345
|
-
// render is deterministic and the agent never invents a label.
|
|
346
|
-
export const DISPLAY_NAMES = {
|
|
347
|
-
'agent-workflow-kit': 'kit',
|
|
348
|
-
'agent-workflow-memory': 'memory',
|
|
349
|
-
'agent-workflow-engine': 'engine',
|
|
350
|
-
'codex-cli-bridge': 'codex-bridge',
|
|
351
|
-
'antigravity-cli-bridge': 'antigravity-bridge',
|
|
352
|
-
};
|
|
353
|
-
const displayOf = (name) => DISPLAY_NAMES[name] ?? name;
|
|
261
|
+
// STATE_PUBLIC (internal→public token map) + DISPLAY_NAMES + displayOf now live in labels.mjs (B1) —
|
|
262
|
+
// imported at the top of this file. They are used below exactly as before.
|
|
354
263
|
|
|
355
264
|
// ── the settings survey (Phase 3) — read-only, honest, localized-on-error ──────────
|
|
356
265
|
// Each sub-survey returns a small user-safe object OR a single `{ error }` field (a localized message,
|
|
@@ -378,8 +287,8 @@ const detectSafe = (deps) => {
|
|
|
378
287
|
};
|
|
379
288
|
|
|
380
289
|
// visibility: the THREE honest states from inferVisibility (NOT the single hiddenFence bit) → user-safe
|
|
381
|
-
// words. Never the internal "hidden fence" / marker terms. A
|
|
382
|
-
|
|
290
|
+
// words (VISIBILITY_PUBLIC, from labels.mjs). Never the internal "hidden fence" / marker terms. A
|
|
291
|
+
// git/probe error → a localized error field.
|
|
383
292
|
export const surveyVisibility = (dir, deps = {}) => {
|
|
384
293
|
try {
|
|
385
294
|
const vis = inferVisibility(deps, resolve(dir));
|
|
@@ -467,6 +376,20 @@ export const surveyBridges = (deps = {}) => {
|
|
|
467
376
|
});
|
|
468
377
|
};
|
|
469
378
|
|
|
379
|
+
// INV-2 (structural refresh — additive, derived, never parsed from a caveat string). A member is
|
|
380
|
+
// `behind` when it is a refreshable CORE member (memory / engine) carrying a refresh-recommending
|
|
381
|
+
// caveat — surveyFamily attaches caveats ONLY to those two kinds, and today every such caveat is
|
|
382
|
+
// refresh-recommending. `recommend` is composed from the member's npm package in the ONE registry
|
|
383
|
+
// (FAMILY_MEMBERS[].npm), never extracted from the caveat text. The kit (composition-root) + the two
|
|
384
|
+
// bridges (execution-backend) never carry such a caveat, so they derive { behind:false, recommend:null }
|
|
385
|
+
// — gated by the CAVEAT, not by npm-nullness (the kit's npm is non-null; only the bridges are null).
|
|
386
|
+
const REFRESHABLE_KINDS = new Set(['memory-substrate', 'methodology-engine']);
|
|
387
|
+
const npmOf = (name) => FAMILY_MEMBERS.find((m) => m.name === name)?.npm ?? null;
|
|
388
|
+
const refreshOf = (m) => {
|
|
389
|
+
const behind = REFRESHABLE_KINDS.has(m.kind) && Boolean(m.caveats?.length);
|
|
390
|
+
return { behind, recommend: behind ? `npx ${npmOf(m.name)}@latest init` : null };
|
|
391
|
+
};
|
|
392
|
+
|
|
470
393
|
export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
471
394
|
const installed = family.map((m) => {
|
|
472
395
|
const entry = {
|
|
@@ -476,6 +399,7 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
476
399
|
state: STATE_PUBLIC[m.manifestState] ?? STATE_PUBLIC[UNKNOWN],
|
|
477
400
|
};
|
|
478
401
|
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)
|
|
479
403
|
return entry;
|
|
480
404
|
});
|
|
481
405
|
const envelope = { deploymentHead: EXPECTED_WORKFLOW_VERSION, installed };
|
|
@@ -496,27 +420,72 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
496
420
|
};
|
|
497
421
|
|
|
498
422
|
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
423
|
+
// Parse contract (Plan §4.5, grounded): the old parseArgs SILENTLY ignored unknown args + a missing
|
|
424
|
+
// --dir value. With the explicit format surface added, the parse now rejects LOUDLY (no silent
|
|
425
|
+
// failure): an unknown flag, a `--dir` with no value, and an invalid `--format` (validated by
|
|
426
|
+
// resolveFormat) all throw. `--json` vs `--format=*` precedence is deterministic (last-wins, in
|
|
427
|
+
// surface.mjs). Returns only { dir } — help is handled first, the format/mode comes from the surface.
|
|
428
|
+
const HELP_FLAGS = new Set(['--help', '-h']);
|
|
429
|
+
// --dir and --format carry/consume a value, so they are handled explicitly below — only the valueless
|
|
430
|
+
// flags live in KNOWN_FLAGS.
|
|
431
|
+
const KNOWN_FLAGS = new Set(['--help', '-h', '--json']);
|
|
432
|
+
// Single left-to-right pass (functional reduce, no `let`): EVERY `--dir` must carry a value — a
|
|
433
|
+
// trailing or REPEATED `--dir` with no value rejects loudly, never silently (a first-occurrence-only
|
|
434
|
+
// check let `--dir /p --dir` slip through as a "known flag"). The token right after a `--dir` is its
|
|
435
|
+
// value (skipped), so a path is never mistaken for an unknown flag; the last `--dir` wins.
|
|
436
|
+
export const parseArgs = (argv) => {
|
|
437
|
+
const { dir } = argv.reduce(
|
|
438
|
+
(state, a, i) => {
|
|
439
|
+
if (state.skip) return { dir: state.dir, skip: false }; // this token was the preceding --dir value
|
|
440
|
+
if (a === '--dir') {
|
|
441
|
+
const value = argv[i + 1];
|
|
442
|
+
if (value === undefined || value.startsWith('-')) {
|
|
443
|
+
throw new Error('[agent-workflow-kit] --dir needs a value: --dir <project>');
|
|
444
|
+
}
|
|
445
|
+
return { dir: value, skip: true }; // consume the value; last --dir wins
|
|
446
|
+
}
|
|
447
|
+
if (KNOWN_FLAGS.has(a) || a === '--format' || a.startsWith('--format=')) return state;
|
|
448
|
+
throw new Error(`[agent-workflow-kit] unknown argument: ${a}`);
|
|
449
|
+
},
|
|
450
|
+
{ dir: undefined, skip: false },
|
|
451
|
+
);
|
|
452
|
+
return { dir };
|
|
506
453
|
};
|
|
507
454
|
|
|
508
|
-
const
|
|
509
|
-
const args = parseArgs(argv);
|
|
510
|
-
if (args.help) {
|
|
511
|
-
console.log(`family-registry — read-only view of the agent-workflow family.
|
|
455
|
+
const HELP = `family-registry — read-only view of the agent-workflow family.
|
|
512
456
|
|
|
513
457
|
Usage:
|
|
514
|
-
node family-registry.mjs [--dir <project>] [--json]
|
|
515
|
-
# skill axis always; deploy axis when --dir is given
|
|
458
|
+
node family-registry.mjs [--dir <project>] [--format=<auto|plain|ansi|json>] [--json]
|
|
459
|
+
# skill axis always; deploy axis when --dir is given.
|
|
460
|
+
# --format: auto (default — ansi on a TTY, plain otherwise) | plain | ansi | json.
|
|
461
|
+
# --json is sugar for --format=json (the no-leak machine envelope). AGENT_WORKFLOW_FORMAT
|
|
462
|
+
# sets the default; a flag beats it. Width/color follow the terminal (NO_COLOR / FORCE_COLOR).
|
|
516
463
|
|
|
517
|
-
Detection only — never writes, never commits, never runs a subscription CLI
|
|
464
|
+
Detection only — never writes, never commits, never runs a subscription CLI.`;
|
|
465
|
+
|
|
466
|
+
const main = (argv) => {
|
|
467
|
+
if (argv.some((a) => HELP_FLAGS.has(a))) {
|
|
468
|
+
console.log(HELP);
|
|
518
469
|
return;
|
|
519
470
|
}
|
|
471
|
+
// Validate args + resolve the output surface BEFORE any survey work; a bad flag/format → loud exit 1.
|
|
472
|
+
const args = (() => {
|
|
473
|
+
try {
|
|
474
|
+
const parsed = parseArgs(argv);
|
|
475
|
+
const surface = detectSurface({
|
|
476
|
+
argv,
|
|
477
|
+
env: process.env,
|
|
478
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
479
|
+
columns: process.stdout.columns,
|
|
480
|
+
platform: process.platform,
|
|
481
|
+
});
|
|
482
|
+
return { ...parsed, surface };
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.error(err?.message ?? String(err));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
|
|
520
489
|
const family = surveyFamily();
|
|
521
490
|
const bridges = surveyBridges();
|
|
522
491
|
const project = args.dir ? surveyProject(args.dir) : null;
|
|
@@ -525,7 +494,10 @@ Detection only — never writes, never commits, never runs a subscription CLI.`)
|
|
|
525
494
|
extras.visibility = surveyVisibility(args.dir);
|
|
526
495
|
extras.settings = surveySettings(args.dir);
|
|
527
496
|
}
|
|
528
|
-
|
|
497
|
+
const envelope = buildEnvelope(family, project, extras);
|
|
498
|
+
// Two surfaces, one data source: JSON prints the envelope verbatim; plain/ansi render it through the
|
|
499
|
+
// capability-adaptive presenter pipeline (view-model → renderers). No-leak inherited from the envelope.
|
|
500
|
+
console.log(args.surface.mode === 'json' ? JSON.stringify(envelope, null, 2) : render(toViewModel(envelope), args.surface));
|
|
529
501
|
};
|
|
530
502
|
|
|
531
503
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
// STOP loudly (never a silent fallback) when the engine is needed but absent/invalid.
|
|
19
19
|
//
|
|
20
20
|
// Pure string functions (testable with byte-preservation fixtures); dependency-free, Node >= 18.
|
|
21
|
+
//
|
|
22
|
+
// Canonical-refresh (AD-025): a slot filled BEFORE a clause existed is normally preserved verbatim
|
|
23
|
+
// (reconcile only fills an EMPTY slot). To push a NEW canonical clause to the EXISTING filled base
|
|
24
|
+
// without clobbering a user's customization, reconcileMarkerSlot also REFRESHES a filled slot — but
|
|
25
|
+
// ONLY when its content normalize-matches a KNOWN PRIOR canonical fragment (drift-guarded, append-only
|
|
26
|
+
// per descriptor). A customized slot never matches → preserved verbatim + a read-only upgrade advisory.
|
|
27
|
+
|
|
28
|
+
import { normalizeCanonical } from './orchestration-config.mjs';
|
|
21
29
|
|
|
22
30
|
export const START_MARKER = '<!-- workflow:methodology:start -->';
|
|
23
31
|
export const END_MARKER = '<!-- workflow:methodology:end -->';
|
|
@@ -57,11 +65,29 @@ export const ORCH_ANCHOR = new RegExp(`^.*${escapeRegExp(END_MARKER)}.*$`, 'm');
|
|
|
57
65
|
export const EMPTY_SLOT = `${START_MARKER}\n${END_MARKER}`;
|
|
58
66
|
export const ORCH_EMPTY_SLOT = `${ORCH_START_MARKER}\n${ORCH_END_MARKER}`;
|
|
59
67
|
|
|
68
|
+
// ── known-prior canonical fragments (drift-guarded, APPEND-ONLY per slot) ───────
|
|
69
|
+
// The EXACT content a PREVIOUS release's engine fragment shipped (what a filled slot would carry today).
|
|
70
|
+
// reconcileMarkerSlot refreshes a filled slot to the current engine fragment IFF its content
|
|
71
|
+
// normalize-matches one of these — so the install base gains a new clause without clobbering a
|
|
72
|
+
// customization (which never matches). Any release that CHANGES an engine fragment must FIRST append the
|
|
73
|
+
// OUTGOING content here, so the immediately-previous deployments still match (a drift-guard test pins
|
|
74
|
+
// current-minus-one → new). NEVER edit an existing entry — only append.
|
|
75
|
+
export const KNOWN_PRIOR_METHODOLOGY_SLOT = [
|
|
76
|
+
// v1.3.0 — pre-communication-contract methodology pointer (procedures route, no §1.9 clause).
|
|
77
|
+
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. Full vocabulary, lifecycle, and the plan-then-execute split live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe.',
|
|
78
|
+
];
|
|
79
|
+
export const KNOWN_PRIOR_ORCH_SLOT = [
|
|
80
|
+
// v1.3.0 — pre-read-at-start orchestration pointer (recipes vocabulary, no orchestration.json clause).
|
|
81
|
+
'> **Orchestration recipes** — compose plan → execute → review with a named recipe: **Solo** (no backend), **Reviewed** (one backend reviews), **Council** (both review, you synthesize), **Delegated** (a backend executes a bounded sub-task); the orchestrator always commits, a backend is never autonomous. Pick + plan one for this environment with `/agent-workflow-kit recipes` (read-only); the deployed how/why lives in your `docs/ai/` workflow docs.',
|
|
82
|
+
];
|
|
83
|
+
|
|
60
84
|
// A slot descriptor bundles everything the generic engine needs to operate on ONE marker pair.
|
|
61
85
|
// `leadingBlank` controls whether the inserted empty pair gets a blank separator line above it — the
|
|
62
86
|
// methodology insert keeps it (readability), the orchestration insert omits it to save a cap line
|
|
63
|
-
// (the pair already sits right under the methodology block).
|
|
64
|
-
|
|
87
|
+
// (the pair already sits right under the methodology block). `knownPriorCanonicals` drives the refresh;
|
|
88
|
+
// `upgradeSignature` + `upgradeAdvice` drive the read-only advisory for a CUSTOMIZED (non-matching) slot
|
|
89
|
+
// that predates the current clause.
|
|
90
|
+
export const METHODOLOGY_DESCRIPTOR = {
|
|
65
91
|
startMarker: START_MARKER,
|
|
66
92
|
endMarker: END_MARKER,
|
|
67
93
|
anchor: METHODOLOGY_ANCHOR,
|
|
@@ -69,6 +95,10 @@ const METHODOLOGY_DESCRIPTOR = {
|
|
|
69
95
|
leadingBlank: true,
|
|
70
96
|
markerName: 'methodology',
|
|
71
97
|
anchorLabel: 'methodology anchor (the "Read it before any code change." Session-Protocols line)',
|
|
98
|
+
knownPriorCanonicals: KNOWN_PRIOR_METHODOLOGY_SLOT,
|
|
99
|
+
upgradeSignature: 'Communication',
|
|
100
|
+
upgradeAdvice:
|
|
101
|
+
'the workflow-methodology pointer predates the communication-contract clause (deliver the artifact inline, lead with the result) — refresh it to the current canon, or add the clause by hand; the contract still applies.',
|
|
72
102
|
};
|
|
73
103
|
export const ORCHESTRATION_DESCRIPTOR = {
|
|
74
104
|
startMarker: ORCH_START_MARKER,
|
|
@@ -78,6 +108,10 @@ export const ORCHESTRATION_DESCRIPTOR = {
|
|
|
78
108
|
leadingBlank: false,
|
|
79
109
|
markerName: 'orchestration',
|
|
80
110
|
anchorLabel: 'orchestration anchor (the methodology end-marker line)',
|
|
111
|
+
knownPriorCanonicals: KNOWN_PRIOR_ORCH_SLOT,
|
|
112
|
+
upgradeSignature: '/agent-workflow-kit set-recipe',
|
|
113
|
+
upgradeAdvice:
|
|
114
|
+
'the orchestration-recipes pointer predates the read-at-start clause — add "read `docs/ai/orchestration.json` at session start; set it with /agent-workflow-kit set-recipe", or set your preference now with /agent-workflow-kit set-recipe.',
|
|
81
115
|
};
|
|
82
116
|
|
|
83
117
|
// ── generic marker-slot engine (descriptor-parameterized) ──────────────────────
|
|
@@ -160,16 +194,27 @@ export const ensureMarkerSlot = (text, descriptor) => {
|
|
|
160
194
|
// Bootstrap/upgrade reconciliation policy (pure): ensure the slot exists, then fill it ONLY IF it
|
|
161
195
|
// is empty (a filled/customized slot is preserved verbatim), enforcing the line cap — all as one step
|
|
162
196
|
// the CLI commits with a single atomic write. On ANY error the INPUT bytes are returned unchanged.
|
|
163
|
-
// reconciled-inserted
|
|
164
|
-
// reconciled-filled
|
|
165
|
-
//
|
|
166
|
-
//
|
|
197
|
+
// reconciled-inserted — slot was absent, inserted at the anchor, then filled.
|
|
198
|
+
// reconciled-filled — slot existed but was empty, now filled.
|
|
199
|
+
// reconciled-refreshed — slot was filled with a KNOWN PRIOR canonical → replaced with `fragment`.
|
|
200
|
+
// present-filled — slot already carried CUSTOM content → preserved verbatim.
|
|
201
|
+
// error — malformed slot, 0/>1 anchors, or cap exceeded → input unchanged.
|
|
167
202
|
export const reconcileMarkerSlot = (text, descriptor, fragment, { maxLines } = {}) => {
|
|
168
203
|
const ensured = ensureMarkerSlot(text, descriptor);
|
|
169
204
|
if (ensured.status === 'error') return { status: 'error', text, error: ensured.error };
|
|
170
205
|
const current = extractMarkerSlot(ensured.text, descriptor);
|
|
171
206
|
const isEmpty = current == null || current.trim() === '';
|
|
172
207
|
if (!isEmpty) {
|
|
208
|
+
// Canonical-refresh: a slot whose content normalize-matches a known prior canonical is STALE — replace
|
|
209
|
+
// it with the current `fragment`. A customized slot never matches → preserved verbatim. (`fragment`
|
|
210
|
+
// is empty for a customized slot — main() only sources it when markerSlotNeedsFill is true.)
|
|
211
|
+
const priors = descriptor.knownPriorCanonicals ?? [];
|
|
212
|
+
const matchesPrior = fragment && fragment.trim() !== '' && priors.some((p) => normalizeCanonical(p) === normalizeCanonical(current));
|
|
213
|
+
if (matchesPrior) {
|
|
214
|
+
const injected = injectIntoSlot(ensured.text, descriptor, fragment, { maxLines });
|
|
215
|
+
if (injected.status !== 'injected') return { status: 'error', text, error: injected.error };
|
|
216
|
+
return { status: 'reconciled-refreshed', text: injected.text };
|
|
217
|
+
}
|
|
173
218
|
if (maxLines != null && lineCount(ensured.text) > maxLines) {
|
|
174
219
|
return { status: 'error', text, error: `AGENTS.md is ${lineCount(ensured.text)} lines (cap ${maxLines}) — trim the file (a customized ${descriptor.markerName} slot must still fit the cap)` };
|
|
175
220
|
}
|
|
@@ -189,7 +234,11 @@ export const markerSlotNeedsFill = (text, descriptor) => {
|
|
|
189
234
|
const ensured = ensureMarkerSlot(text, descriptor);
|
|
190
235
|
if (ensured.status === 'error') return false;
|
|
191
236
|
const current = extractMarkerSlot(ensured.text, descriptor);
|
|
192
|
-
|
|
237
|
+
if (current == null || current.trim() === '') return true; // empty → fill
|
|
238
|
+
// filled-but-STALE (content matches a known prior canonical) → needs a refresh, so main() must
|
|
239
|
+
// re-source the fragment. A customized slot doesn't match → no source needed (preserved verbatim).
|
|
240
|
+
const priors = descriptor.knownPriorCanonicals ?? [];
|
|
241
|
+
return priors.some((p) => normalizeCanonical(p) === normalizeCanonical(current));
|
|
193
242
|
};
|
|
194
243
|
|
|
195
244
|
// ── methodology-slot exports (delegate to the generic engine, byte-for-byte) ────
|
|
@@ -217,6 +266,19 @@ export const methodologyProceduresHint = (text) => {
|
|
|
217
266
|
return `the methodology pointer has no procedures route — add "${PROCEDURES_POINTER} <activity>" for auto-discovery; the activity procedures are reachable now via ${PROCEDURES_POINTER}.`;
|
|
218
267
|
};
|
|
219
268
|
|
|
269
|
+
// Generic read-only upgrade advisory (AD-025; the §1.6a/§1.9 reach to CUSTOMIZED filled slots): a slot
|
|
270
|
+
// that is present + FILLED but lacks the descriptor's current-canon `upgradeSignature` (and so could not
|
|
271
|
+
// be canonical-refreshed — its content is customized) gets a one-line note the upgrade flow surfaces.
|
|
272
|
+
// Returns null for an absent / empty / malformed slot, or one that already carries the signature. Pure;
|
|
273
|
+
// never edits the file (a customization is never rewritten — only the user decides).
|
|
274
|
+
export const markerSlotUpgradeHint = (text, descriptor) => {
|
|
275
|
+
if (!descriptor.upgradeSignature) return null;
|
|
276
|
+
const content = extractMarkerSlot(text, descriptor);
|
|
277
|
+
if (content == null || content.trim() === '') return null; // only a FILLED slot
|
|
278
|
+
if (content.includes(descriptor.upgradeSignature)) return null; // already carries the current clause
|
|
279
|
+
return descriptor.upgradeAdvice;
|
|
280
|
+
};
|
|
281
|
+
|
|
220
282
|
// A cap-refusal is a SOFT, reported skip (distinct from a malformed/anchor STOP) — keyed off the
|
|
221
283
|
// stable "(cap N)" substring both cap messages carry, so the dual-slot reconcile can skip the
|
|
222
284
|
// orchestration pointer (loud) while keeping the methodology fill, instead of aborting both.
|
|
@@ -317,18 +379,23 @@ const main = async (argv) => {
|
|
|
317
379
|
console.error(`[inject-methodology] reconcile refused — ${methResult.error}`);
|
|
318
380
|
process.exit(1);
|
|
319
381
|
}
|
|
320
|
-
const afterMeth = methResult.text; // === text when the methodology slot was already filled
|
|
382
|
+
const afterMeth = methResult.text; // === text when the methodology slot was already filled (custom)
|
|
321
383
|
const describeMeth = {
|
|
322
384
|
'reconciled-inserted': 'inserted the workflow-methodology pointer at the Session-Protocols anchor and filled it',
|
|
323
385
|
'reconciled-filled': 'filled the empty workflow-methodology pointer',
|
|
386
|
+
'reconciled-refreshed': 'refreshed the workflow-methodology pointer to the current canon',
|
|
324
387
|
'present-filled': 'workflow-methodology pointer already present',
|
|
325
388
|
}[methResult.status];
|
|
326
|
-
// Read-only upgrade
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
if (
|
|
389
|
+
// Read-only upgrade advisories for a CUSTOMIZED methodology pointer that reconcile preserved verbatim
|
|
390
|
+
// (a refreshed/filled/inserted slot already carries the current canon, so it gets none): the AD-019
|
|
391
|
+
// procedures route AND the §1.9 communication-contract clause. No mutation — reported notes only.
|
|
392
|
+
const notes = [];
|
|
393
|
+
if (methResult.status === 'present-filled') {
|
|
394
|
+
const p = methodologyProceduresHint(afterMeth); if (p) notes.push(p);
|
|
395
|
+
const u = markerSlotUpgradeHint(afterMeth, METHODOLOGY_DESCRIPTOR); if (u) notes.push(u);
|
|
396
|
+
}
|
|
397
|
+
const reportNotes = () => {
|
|
398
|
+
for (const n of notes) console.log(`[inject-methodology] note: ${n}`);
|
|
332
399
|
};
|
|
333
400
|
|
|
334
401
|
// ── Explicit [fragment.md] binds methodology ONLY → skip the orchestration reconcile ──
|
|
@@ -374,8 +441,15 @@ const main = async (argv) => {
|
|
|
374
441
|
describeOrch = {
|
|
375
442
|
'reconciled-inserted': 'inserted the orchestration-recipes pointer below it and filled it',
|
|
376
443
|
'reconciled-filled': 'filled the empty orchestration-recipes pointer',
|
|
444
|
+
'reconciled-refreshed': 'refreshed the orchestration-recipes pointer to the current canon',
|
|
377
445
|
'present-filled': 'orchestration-recipes pointer already present',
|
|
378
446
|
}[orchResult.status];
|
|
447
|
+
// §1.6a advisory: a CUSTOMIZED orchestration pointer preserved verbatim, lacking the read-at-start
|
|
448
|
+
// clause, gets a read-only nudge (a refreshed/filled slot already carries it).
|
|
449
|
+
if (orchResult.status === 'present-filled') {
|
|
450
|
+
const u = markerSlotUpgradeHint(finalText, ORCHESTRATION_DESCRIPTOR);
|
|
451
|
+
if (u) notes.push(u);
|
|
452
|
+
}
|
|
379
453
|
}
|
|
380
454
|
}
|
|
381
455
|
|
|
@@ -384,12 +458,12 @@ const main = async (argv) => {
|
|
|
384
458
|
// Byte-unchanged. Still report a cap-skip (it is not "nothing to do" — a pointer was withheld).
|
|
385
459
|
if (orchSkipped) console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}.`);
|
|
386
460
|
else console.log('[inject-methodology] reconcile: both pointers already present and filled — nothing to do (zero-diff).');
|
|
387
|
-
|
|
461
|
+
reportNotes();
|
|
388
462
|
return;
|
|
389
463
|
}
|
|
390
464
|
await writeAtomic(finalText);
|
|
391
465
|
console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}.`);
|
|
392
|
-
|
|
466
|
+
reportNotes();
|
|
393
467
|
return;
|
|
394
468
|
}
|
|
395
469
|
|