@sabaiway/agent-workflow-kit 1.15.2 → 1.17.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 +72 -0
- package/README.md +2 -1
- package/SKILL.md +91 -15
- package/bridges/codex-cli-bridge/SKILL.md +81 -28
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +337 -44
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +593 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +279 -19
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +542 -0
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/references/driving-codex.md +67 -40
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +148 -43
- package/bridges/codex-cli-bridge/setup/README.md +6 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/tools/commands.mjs +206 -0
- package/tools/delegation.mjs +8 -0
- package/tools/family-registry.mjs +263 -13
- package/tools/procedures.mjs +3 -1
package/tools/delegation.mjs
CHANGED
|
@@ -20,8 +20,16 @@ export const EXPECTED_MEMORY_NAME = 'agent-workflow-memory';
|
|
|
20
20
|
// The assets a memory candidate must carry, AND their required type. A partial install (manifest +
|
|
21
21
|
// SKILL.md only) is missing these → invalid → fallback. Checking the type (not just existence)
|
|
22
22
|
// rejects a wrong-shaped install (e.g. a file where a dir is expected) BEFORE any project write.
|
|
23
|
+
//
|
|
24
|
+
// `references/templates/orchestration.json` (Step 2.4) is a SURGICAL gate: a memory too old to ship
|
|
25
|
+
// the orchestration-config template (pre-1.2.0, e.g. v1.0.0) can't seed `docs/ai/orchestration.json`,
|
|
26
|
+
// so it must NOT be delegate-classified — the kit then falls back to its OWN bundled substrate, which
|
|
27
|
+
// DOES seed orchestration.json (Mode: upgrade step 3). This closes the stale-memory trap that the
|
|
28
|
+
// read-only family-registry note (MEMORY_ORCH_TEMPLATE_REL) only INFORMS about — the gate ACTS. The
|
|
29
|
+
// two key on the same asset; a cross-tool parity test pins them in lockstep.
|
|
23
30
|
export const REQUIRED_MEMORY_ASSETS = [
|
|
24
31
|
{ path: 'references/templates', type: 'dir' },
|
|
32
|
+
{ path: 'references/templates/orchestration.json', type: 'file' },
|
|
25
33
|
{ path: 'references/contracts.md', type: 'file' },
|
|
26
34
|
{ path: 'references/scripts', type: 'dir' },
|
|
27
35
|
{ path: 'scripts/stamp-takeover.mjs', type: 'file' },
|
|
@@ -16,14 +16,29 @@
|
|
|
16
16
|
// Pure, dependency-injectable (fs/env/home/validator are deps), dependency-free, Node >= 18. No
|
|
17
17
|
// side effects on import (the isDirectRun idiom) — tests import the helpers with nothing run.
|
|
18
18
|
|
|
19
|
-
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { existsSync, statSync, readFileSync, lstatSync } from 'node:fs';
|
|
20
20
|
import { join, resolve } from 'node:path';
|
|
21
21
|
import { pathToFileURL } from 'node:url';
|
|
22
22
|
import os from 'node:os';
|
|
23
|
-
import { resolveDir } from './detect-backends.mjs';
|
|
23
|
+
import { resolveDir, detectBackends, findOnPath } from './detect-backends.mjs';
|
|
24
24
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
25
|
-
import { START_MARKER, excludePath } from './hide-footprint.mjs';
|
|
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
|
+
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
28
|
+
import { loadConfig } from './procedures.mjs';
|
|
29
|
+
// The deployment-lineage head + the shared settings readers are reused from velocity-profile so the
|
|
30
|
+
// `--json` envelope/settings-survey has ONE implementation each, never a drifting copy:
|
|
31
|
+
// EXPECTED_WORKFLOW_VERSION — the head literal (drift-guarded vs memory LINEAGE_HEAD)
|
|
32
|
+
// readSettingsFile / resolveEffectiveMode — the loud `.claude/settings.*` reader + mode precedence
|
|
33
|
+
// (No import cycle: velocity-profile / recipes / procedures import only node builtins + siblings, none
|
|
34
|
+
// import family-registry, and none has a side effect on import — every CLI is isDirectRun-guarded.)
|
|
35
|
+
import {
|
|
36
|
+
EXPECTED_WORKFLOW_VERSION,
|
|
37
|
+
SETTINGS_FILE,
|
|
38
|
+
SETTINGS_LOCAL_FILE,
|
|
39
|
+
readSettingsFile,
|
|
40
|
+
resolveEffectiveMode,
|
|
41
|
+
} from './velocity-profile.mjs';
|
|
27
42
|
|
|
28
43
|
// ── manifestState values (the detect-backends precedence, generalized to any member kind) ──────────
|
|
29
44
|
export const NOT_INSTALLED = 'not-installed';
|
|
@@ -93,14 +108,14 @@ export const FAMILY_MEMBERS = [
|
|
|
93
108
|
export const isGlobalSkill = (member) => member.kind !== undefined; // every member is a global skill today
|
|
94
109
|
|
|
95
110
|
// ── pure probes ──────────────────────────────────────────────────────────────────
|
|
96
|
-
// Wrapped marker probe → 'present' (a regular file) | 'absent' (ENOENT / not a file) |
|
|
97
|
-
// non-ENOENT fs error, e.g. EACCES).
|
|
98
|
-
//
|
|
111
|
+
// Wrapped marker probe → 'present' (a regular file) | 'absent' (ENOENT / not a regular file) |
|
|
112
|
+
// 'unknown' (a non-ENOENT fs error, e.g. EACCES). STAT-FIRST on purpose: `existsSync()` SWALLOWS an
|
|
113
|
+
// EACCES into a bare `false`, which would mask a permission error as 'absent' (a silent failure);
|
|
114
|
+
// `statSync` THROWS the EACCES so it surfaces as 'unknown'. 'unknown' is never collapsed to 'absent' —
|
|
115
|
+
// classifyMember reports it (uninstall then leaves the dir alone) and the memory caveat skips it.
|
|
99
116
|
const probeMarker = (path, deps = {}) => {
|
|
100
|
-
const exists = deps.exists ?? existsSync;
|
|
101
117
|
const stat = deps.stat ?? statSync;
|
|
102
118
|
try {
|
|
103
|
-
if (!exists(path)) return 'absent';
|
|
104
119
|
return stat(path).isFile() ? 'present' : 'absent';
|
|
105
120
|
} catch (err) {
|
|
106
121
|
return err && err.code === 'ENOENT' ? 'absent' : 'unknown';
|
|
@@ -158,6 +173,17 @@ const ENGINE_FRAGMENT_CAVEATS = [
|
|
|
158
173
|
{ rel: PROCEDURES_FRAGMENT_REL, caveat: 'engine present but does not ship the activity-procedures canon (too old / incomplete) — run `npx @sabaiway/agent-workflow-engine@latest init`' },
|
|
159
174
|
];
|
|
160
175
|
|
|
176
|
+
// The orchestration-config TEMPLATE a current memory ships (added in memory 1.2.0; absent in older
|
|
177
|
+
// installs such as v1.0.0). It is the SAME asset Step 2.4 adds to delegation.mjs's
|
|
178
|
+
// REQUIRED_MEMORY_ASSETS: the read-only note here INFORMS, the delegation gate ACTS. Step 2.4 also
|
|
179
|
+
// adds a parity drift-guard tying this path to that required-asset set, so the note and the gate can
|
|
180
|
+
// never key on different files (until then they are kept in lockstep by review).
|
|
181
|
+
export const MEMORY_ORCH_TEMPLATE_REL = 'references/templates/orchestration.json';
|
|
182
|
+
// Worded as an honest OBSERVATION, not a diagnosis (absence can mean old OR incomplete), and it makes
|
|
183
|
+
// NO claim about an orchestration.json seeding outcome (that depends on delegate-vs-fallback).
|
|
184
|
+
const MEMORY_BEHIND_NOTE =
|
|
185
|
+
"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.";
|
|
186
|
+
|
|
161
187
|
export const surveyFamily = (deps = {}) =>
|
|
162
188
|
FAMILY_MEMBERS.map((member) => {
|
|
163
189
|
const row = classifyMember(member, deps);
|
|
@@ -173,6 +199,14 @@ export const surveyFamily = (deps = {}) =>
|
|
|
173
199
|
const caveats = ENGINE_FRAGMENT_CAVEATS.filter((f) => !fragmentUsable(f.rel)).map((f) => f.caveat);
|
|
174
200
|
if (caveats.length) row.caveats = caveats;
|
|
175
201
|
}
|
|
202
|
+
// Memory offline caveat (Step 2.2): a distinct probe — the orchestration TEMPLATE file's existence.
|
|
203
|
+
// Only attach when it is provably ABSENT (a non-ENOENT probe error → 'unknown' → skip, never a
|
|
204
|
+
// false "missing" claim). Mirrors the engine-caveat SHAPE; keyed on the Step-2.4 required asset.
|
|
205
|
+
if (row.kind === 'memory-substrate' && row.manifestState === OK && row.skillDir) {
|
|
206
|
+
if (probeMarker(join(row.skillDir, MEMORY_ORCH_TEMPLATE_REL), deps) === 'absent') {
|
|
207
|
+
row.caveats = [...(row.caveats ?? []), MEMORY_BEHIND_NOTE];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
176
210
|
return row;
|
|
177
211
|
});
|
|
178
212
|
|
|
@@ -234,13 +268,41 @@ export const surveyProject = (projectDir, deps = {}) => {
|
|
|
234
268
|
// ── report ───────────────────────────────────────────────────────────────────────
|
|
235
269
|
const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
|
|
236
270
|
|
|
237
|
-
|
|
271
|
+
// The human (non-JSON) settings render. A dev view — the agent consumes `--json` + renders in plain
|
|
272
|
+
// language (Mode: status). Each area shows its `error` field loudly when one fired.
|
|
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 = {}) => {
|
|
238
292
|
const lines = ['agent-workflow family — installed skills (skill axis)', ''];
|
|
239
293
|
for (const m of family) {
|
|
240
294
|
const ver = m.version ? `v${m.version}` : '—';
|
|
241
295
|
lines.push(` ${pad(m.name, 26)}[${pad(m.manifestState, 16)}] ${pad(ver, 10)} ${m.kind}`);
|
|
242
296
|
for (const c of m.caveats ?? []) lines.push(` ↳ ${c}`);
|
|
243
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
|
+
}
|
|
244
306
|
if (project) {
|
|
245
307
|
lines.push('', `project deployment (${project.dir})`, '');
|
|
246
308
|
if (!project.deployed) {
|
|
@@ -250,16 +312,197 @@ export const formatStatus = (family, project = null) => {
|
|
|
250
312
|
lines.push(` ${pad(s.file, 26)}${s.version ?? '—'}`);
|
|
251
313
|
}
|
|
252
314
|
lines.push(` ${pad('docs/ai present', 26)}${project.docsAiPresent ? 'yes' : 'no'}`);
|
|
253
|
-
|
|
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
|
+
}
|
|
254
320
|
}
|
|
321
|
+
if (extras.settings) lines.push(...formatSettings(extras.settings));
|
|
255
322
|
}
|
|
256
323
|
return lines.join('\n');
|
|
257
324
|
};
|
|
258
325
|
|
|
326
|
+
// ── the no-leak --json envelope ──────────────────────────────────────────────────
|
|
327
|
+
// A machine-readable view with USER-SAFE field names only — NEVER the internal manifestState /
|
|
328
|
+
// hiddenFence terms or the raw stamp FILENAMES. The render (SKILL.md version block + Mode: status)
|
|
329
|
+
// consumes THIS, never the human table verbatim. An envelope-shape test pins its shape so later phases
|
|
330
|
+
// (the settings/visibility block) can't silently break the Phase-2 version consumer.
|
|
331
|
+
|
|
332
|
+
// internal manifestState → a STABLE, user-safe token (SKILL.md owns the value→plain-language phrasing).
|
|
333
|
+
// Deliberately NOT the internal literals (foreign/stub/…): those must never leak past this boundary.
|
|
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;
|
|
354
|
+
|
|
355
|
+
// ── the settings survey (Phase 3) — read-only, honest, localized-on-error ──────────
|
|
356
|
+
// Each sub-survey returns a small user-safe object OR a single `{ error }` field (a localized message,
|
|
357
|
+
// never a crash): a malformed/unreadable file in ONE area must not break the rest of `status`. The
|
|
358
|
+
// readers are REUSED (one implementation each): loadConfig (procedures.mjs), readSettingsFile /
|
|
359
|
+
// resolveEffectiveMode (velocity-profile.mjs), resolveActivityRecipe (recipes.mjs), inferVisibility
|
|
360
|
+
// (hide-footprint.mjs). Engine-free throughout (the effective-recipe view never reads the engine).
|
|
361
|
+
|
|
362
|
+
// A localized, user-safe error string. The reused readers already build cwd-relative `path: reason`
|
|
363
|
+
// messages (loadConfig / velocity), so this just normalizes to a string — never a raw stack/abs path.
|
|
364
|
+
const localizeError = (err) => (err && err.message ? String(err.message) : String(err));
|
|
365
|
+
|
|
366
|
+
const hasOwn = (o, k) => o != null && Object.prototype.hasOwnProperty.call(o, k);
|
|
367
|
+
|
|
368
|
+
// The backend detector is a SECONDARY input to the survey — a corrupt bridge must never break the
|
|
369
|
+
// read-only view, but the failure must NOT be silent (Hard Constraint). Run it defensively: a throw →
|
|
370
|
+
// { detection: [], error: <localized> } so callers floor gracefully (recipes → solo, bridges →
|
|
371
|
+
// 'unknown') AND can surface the concrete reason. Returns a value so callers stay `const`.
|
|
372
|
+
const detectSafe = (deps) => {
|
|
373
|
+
try {
|
|
374
|
+
return { detection: (deps.detect ?? detectBackends)(deps), error: null };
|
|
375
|
+
} catch (err) {
|
|
376
|
+
return { detection: [], error: localizeError(err) };
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// visibility: the THREE honest states from inferVisibility (NOT the single hiddenFence bit) → user-safe
|
|
381
|
+
// words. Never the internal "hidden fence" / marker terms. A git/probe error → a localized error field.
|
|
382
|
+
const VISIBILITY_PUBLIC = { visible: 'visible', hidden: 'hidden', ambiguous: 'unclear' };
|
|
383
|
+
export const surveyVisibility = (dir, deps = {}) => {
|
|
384
|
+
try {
|
|
385
|
+
const vis = inferVisibility(deps, resolve(dir));
|
|
386
|
+
return { state: VISIBILITY_PUBLIC[vis.visibility] ?? 'unclear' };
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return { error: localizeError(err) };
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// orchestration recipes: the EFFECTIVE recipe per slot (config · default · effective), engine-free —
|
|
393
|
+
// shared loadConfig + resolveActivityRecipe + the read-only backend detector. A malformed config → a
|
|
394
|
+
// localized error field; a detection failure floors at solo (a corrupt bridge must not break the view).
|
|
395
|
+
export const surveyRecipes = (dir, deps = {}) => {
|
|
396
|
+
// A detector failure floors recipes at solo (mirrors procedures) but is surfaced as `detectError`, so
|
|
397
|
+
// the render says "couldn't check backends" instead of letting a real solo-default look identical.
|
|
398
|
+
const { detection, error: detectError } = detectSafe(deps);
|
|
399
|
+
try {
|
|
400
|
+
const { config, source } = loadConfig(resolve(dir), deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
401
|
+
const activities = {};
|
|
402
|
+
for (const [activity, def] of Object.entries(ACTIVITIES)) {
|
|
403
|
+
activities[activity] = {};
|
|
404
|
+
for (const slot of Object.keys(def.slots)) {
|
|
405
|
+
const r = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity, slot });
|
|
406
|
+
activities[activity][slot] = { recipe: r.recipe, source: r.source, degradedFrom: r.degradedFrom ?? null };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { configSource: source, activities, ...(detectError ? { detectError } : {}) };
|
|
410
|
+
} catch (err) {
|
|
411
|
+
return { error: localizeError(err) };
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// attribution: includeCoAuthoredBy across .claude/settings.json + settings.local.json as
|
|
416
|
+
// project · local override · effective (local WINS only when it actually sets the key — presence-based,
|
|
417
|
+
// mirroring resolveEffectiveMode). Reuses the loud readSettingsFile; the precedence read is new code.
|
|
418
|
+
export const surveyAttribution = (dir, deps = {}) => {
|
|
419
|
+
try {
|
|
420
|
+
const d = resolve(dir);
|
|
421
|
+
const project = readSettingsFile(join(d, SETTINGS_FILE), { ...deps, cwd: d });
|
|
422
|
+
const local = readSettingsFile(join(d, SETTINGS_LOCAL_FILE), { ...deps, cwd: d });
|
|
423
|
+
const projVal = project.present && hasOwn(project.data, 'includeCoAuthoredBy') ? project.data.includeCoAuthoredBy : null;
|
|
424
|
+
const localHasKey = local.present && hasOwn(local.data, 'includeCoAuthoredBy');
|
|
425
|
+
const localVal = localHasKey ? local.data.includeCoAuthoredBy : null;
|
|
426
|
+
return { project: projVal, local: localVal, effective: localHasKey ? localVal : projVal };
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return { error: localizeError(err) };
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// velocity: the effective permissions.defaultMode (local > project, via resolveEffectiveMode) + the
|
|
433
|
+
// per-source count of allowlist entries — a read-only view of what the velocity profile may have seeded.
|
|
434
|
+
export const surveyVelocity = (dir, deps = {}) => {
|
|
435
|
+
try {
|
|
436
|
+
const d = resolve(dir);
|
|
437
|
+
const project = readSettingsFile(join(d, SETTINGS_FILE), { ...deps, cwd: d });
|
|
438
|
+
const local = readSettingsFile(join(d, SETTINGS_LOCAL_FILE), { ...deps, cwd: d });
|
|
439
|
+
const { effectiveMode } = resolveEffectiveMode(project.data, local.data);
|
|
440
|
+
const allowOf = (s) => (s.present && Array.isArray(s.data?.permissions?.allow) ? s.data.permissions.allow.length : 0);
|
|
441
|
+
return { defaultMode: effectiveMode ?? null, allowEntries: { project: allowOf(project), local: allowOf(local) } };
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return { error: localizeError(err) };
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// the project-scoped settings survey (needs a project dir). Each area is independently localized-on-error.
|
|
448
|
+
export const surveySettings = (dir, deps = {}) => ({
|
|
449
|
+
recipes: surveyRecipes(dir, deps),
|
|
450
|
+
attribution: surveyAttribution(dir, deps),
|
|
451
|
+
velocity: surveyVelocity(dir, deps),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// bridges: HOST-scoped (no project needed). Wrapper command NAMES come from FAMILY_MEMBERS[].wrapperCmds
|
|
455
|
+
// (static, always present), their PATH-presence is probed DIRECTLY via findOnPath over those names (NOT
|
|
456
|
+
// detect-backends' wrappers[], which is [] when the bridge isn't ok — the onboarding case), and the
|
|
457
|
+
// readiness summary comes from the detector. NO default-model claim (a negative drift-guard asserts it).
|
|
458
|
+
export const surveyBridges = (deps = {}) => {
|
|
459
|
+
const { detection } = detectSafe(deps); // a detector failure → every readiness reads 'unknown' (honest)
|
|
460
|
+
const probe = deps.findOnPath ?? findOnPath;
|
|
461
|
+
return FAMILY_MEMBERS.filter((m) => m.kind === 'execution-backend').map((m) => {
|
|
462
|
+
const det = detection.find((d) => d.name === m.name);
|
|
463
|
+
// Preserve findOnPath's THREE-state result (present | missing | unknown): an `unknown` (EACCES,
|
|
464
|
+
// "cannot confirm") must stay distinct from a real `missing` — never flattened to a false boolean.
|
|
465
|
+
const wrappers = m.wrapperCmds.map((cmd) => ({ cmd, state: probe(cmd, deps).state }));
|
|
466
|
+
return { member: m.name, display: displayOf(m.name), readiness: det?.readiness ?? 'unknown', wrappers };
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
471
|
+
const installed = family.map((m) => {
|
|
472
|
+
const entry = {
|
|
473
|
+
member: m.name,
|
|
474
|
+
display: displayOf(m.name),
|
|
475
|
+
version: m.version ?? null,
|
|
476
|
+
state: STATE_PUBLIC[m.manifestState] ?? STATE_PUBLIC[UNKNOWN],
|
|
477
|
+
};
|
|
478
|
+
if (m.caveats?.length) entry.notes = m.caveats; // plain-language observations (Steps 2.2 / engine)
|
|
479
|
+
return entry;
|
|
480
|
+
});
|
|
481
|
+
const envelope = { deploymentHead: EXPECTED_WORKFLOW_VERSION, installed };
|
|
482
|
+
if (extras.bridges) envelope.bridges = extras.bridges; // HOST-scoped (no project needed)
|
|
483
|
+
if (project) {
|
|
484
|
+
envelope.project = {
|
|
485
|
+
dir: project.dir,
|
|
486
|
+
deployed: project.deployed,
|
|
487
|
+
docsAi: project.docsAiPresent,
|
|
488
|
+
// member + display + version only — never the internal stamp FILENAME (s.file).
|
|
489
|
+
deployStamps: project.stamps.map((s) => ({ member: s.name, display: displayOf(s.name), version: s.version ?? null })),
|
|
490
|
+
};
|
|
491
|
+
// project-scoped Phase-3 additions (visibility from inferVisibility, not the hiddenFence bit).
|
|
492
|
+
if (extras.visibility) envelope.project.visibility = extras.visibility;
|
|
493
|
+
if (extras.settings) envelope.project.settings = extras.settings;
|
|
494
|
+
}
|
|
495
|
+
return envelope;
|
|
496
|
+
};
|
|
497
|
+
|
|
259
498
|
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
260
499
|
const parseArgs = (argv) => {
|
|
261
500
|
const dirFlag = argv.indexOf('--dir');
|
|
262
|
-
return {
|
|
501
|
+
return {
|
|
502
|
+
help: argv.includes('--help') || argv.includes('-h'),
|
|
503
|
+
json: argv.includes('--json'),
|
|
504
|
+
dir: dirFlag >= 0 ? argv[dirFlag + 1] : undefined,
|
|
505
|
+
};
|
|
263
506
|
};
|
|
264
507
|
|
|
265
508
|
const main = (argv) => {
|
|
@@ -268,14 +511,21 @@ const main = (argv) => {
|
|
|
268
511
|
console.log(`family-registry — read-only view of the agent-workflow family.
|
|
269
512
|
|
|
270
513
|
Usage:
|
|
271
|
-
node family-registry.mjs [--dir <project>]
|
|
514
|
+
node family-registry.mjs [--dir <project>] [--json]
|
|
515
|
+
# skill axis always; deploy axis when --dir is given; --json = the no-leak machine envelope
|
|
272
516
|
|
|
273
517
|
Detection only — never writes, never commits, never runs a subscription CLI.`);
|
|
274
518
|
return;
|
|
275
519
|
}
|
|
276
520
|
const family = surveyFamily();
|
|
521
|
+
const bridges = surveyBridges();
|
|
277
522
|
const project = args.dir ? surveyProject(args.dir) : null;
|
|
278
|
-
|
|
523
|
+
const extras = { bridges };
|
|
524
|
+
if (args.dir) {
|
|
525
|
+
extras.visibility = surveyVisibility(args.dir);
|
|
526
|
+
extras.settings = surveySettings(args.dir);
|
|
527
|
+
}
|
|
528
|
+
console.log(args.json ? JSON.stringify(buildEnvelope(family, project, extras), null, 2) : formatStatus(family, project, extras));
|
|
279
529
|
};
|
|
280
530
|
|
|
281
531
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
package/tools/procedures.mjs
CHANGED
|
@@ -141,7 +141,9 @@ const validateConfig = (config) => {
|
|
|
141
141
|
// Load + validate the config from <cwd>/docs/ai/orchestration.json. Absent FILE → computed defaults
|
|
142
142
|
// (NOT an error): { config: null, source: 'none' }. Malformed JSON / schema-invalid / unreadable →
|
|
143
143
|
// loud `path: reason` (exit 1). The resolver receives the parsed+validated object (§2.2 IO/resolver split).
|
|
144
|
-
|
|
144
|
+
// Exported so the read-only status settings-survey (family-registry.mjs) reuses ONE config reader —
|
|
145
|
+
// same strict-JSON + loud-on-malformed contract, no second drifting implementation (Plan 3.1).
|
|
146
|
+
export const loadConfig = (cwd, readFile = readFileSync, lstat = lstatSync) => {
|
|
145
147
|
const full = join(cwd, CONFIG_REL);
|
|
146
148
|
// Distinguish a TRULY-absent config (no entry at all → computed defaults) from a present-but-
|
|
147
149
|
// unreadable one (a directory, a DANGLING SYMLINK, a permission error → loud exit 1). lstat does NOT
|