@sabaiway/agent-workflow-kit 1.18.0 → 1.20.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 +41 -0
- package/README.md +11 -6
- package/SKILL.md +8 -2
- package/bin/install.mjs +128 -33
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/templates/agent_rules.md +5 -0
- package/tools/family-members.mjs +67 -0
- package/tools/family-registry.mjs +130 -160
- package/tools/labels.mjs +81 -0
- package/tools/presentation.mjs +55 -0
- package/tools/renderers.mjs +119 -0
- package/tools/surface.mjs +75 -0
- package/tools/view-model.mjs +89 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 1.20.0 — Fallback-template lens + the cross-package lens-mirror guard (kit)
|
|
8
|
+
|
|
9
|
+
A **feature** release. The kit's fallback `agent_rules.md` template gains the **§2.5 Right-altitude &
|
|
10
|
+
code-grounded** lens (byte-identical to memory's §2.6 block), and a new cross-package
|
|
11
|
+
`test/lens-mirror.test.mjs` ties the shared lens vocabulary across the engine canon (`planning.md` +
|
|
12
|
+
`procedures.md`) AND both `agent_rules.md` templates: each distinctive token must survive in every file's
|
|
13
|
+
lens **region**, and the two template blocks must stay byte-identical apart from the heading number. The
|
|
14
|
+
deployment-lineage head stays **`1.3.0`** (no `docs/ai` structural change, no migration); the kit
|
|
15
|
+
**package** version is a separate axis.
|
|
16
|
+
|
|
17
|
+
- **Non-vacuous guard.** Deleting the lens block (or a distinctive token) from any of the four files
|
|
18
|
+
fails the guard — proven by an injected-divergence dry-run.
|
|
19
|
+
|
|
20
|
+
## 1.19.0 — One-command freshness: `init` refreshes memory too, and a capability-adaptive `status` (kit)
|
|
21
|
+
|
|
22
|
+
A **feature** release that closes the returning-user gap and modernizes the status surface. The
|
|
23
|
+
deployment-lineage head stays **`1.3.0`** (no `docs/ai` structural change, no migration); the kit
|
|
24
|
+
**package** version is a separate axis.
|
|
25
|
+
|
|
26
|
+
- **`init` now leaves no stale core member.** After installing/refreshing the kit, `npx
|
|
27
|
+
@sabaiway/agent-workflow-kit@latest init` also refreshes the **memory substrate** and the
|
|
28
|
+
**methodology engine** over npm — so a returning user is no longer left with silently stale memory.
|
|
29
|
+
The memory refresh is **best-effort: a miss is a loud DEGRADED success** — a warning with the exact
|
|
30
|
+
recovery command (and the on-disk version) plus **exit 0**, never a silent skip and never the engine's
|
|
31
|
+
hard STOP. New **`--no-memory`** flag skips it for air-gapped/scripted installs. The cascade
|
|
32
|
+
membership is derived from the one family registry and drift-guarded; bridges are still placed by
|
|
33
|
+
`setup`, never by `init`.
|
|
34
|
+
- **Capability-adaptive `status` output.** The direct-CLI status view (`node tools/family-registry.mjs`)
|
|
35
|
+
is rebuilt as a `surface → view-model → renderers` pipeline: it auto-detects the terminal (plain vs
|
|
36
|
+
ANSI, color via `NO_COLOR`/`FORCE_COLOR`, width with a 40-col floor, ASCII-glyph fallback) and renders
|
|
37
|
+
all four blocks (members · bridges · project deploy/visibility · settings). `--format=<auto|plain|ansi|json>`
|
|
38
|
+
(with `--json` as sugar) selects the surface; unknown flags and a missing `--dir` value now **reject
|
|
39
|
+
loudly** instead of being silently ignored.
|
|
40
|
+
- **Additive `--json` freshness signal.** Each `installed[]` entry gains a structural `refresh`
|
|
41
|
+
`{ behind, recommend }` object (derived from the registry, never parsed from a caveat). The
|
|
42
|
+
agent-mediated `/agent-workflow-kit status` reads it to show a localized "needs refresh" label + the
|
|
43
|
+
exact command **once**; every existing envelope field is unchanged.
|
|
44
|
+
- **Docs.** Install help + READMEs document the memory/engine refresh, `--no-memory`, the degraded-success
|
|
45
|
+
recovery, and that bridges are not installed by `init`. Tarball **75 → 81** (the pure member-table leaf
|
|
46
|
+
+ five status-presenter modules).
|
|
47
|
+
|
|
7
48
|
## 1.18.0 — Agent-writable orchestration config (`set-recipe`), version-aware setup, durable session contracts (kit)
|
|
8
49
|
|
|
9
50
|
A **feature** release. The per-project recipe config (`docs/ai/orchestration.json`) is no longer
|
package/README.md
CHANGED
|
@@ -194,20 +194,25 @@ other agent). The manual path works identically but **isn't reflected in install
|
|
|
194
194
|
<details>
|
|
195
195
|
<summary><b>What <code>init</code> touches — and how to undo it</b></summary>
|
|
196
196
|
|
|
197
|
-
`init` is **additive — it never deletes your settings.** It writes
|
|
197
|
+
`init` is **additive — it never deletes your settings.** It writes its own namespaced slots, then
|
|
198
|
+
refreshes the other npm core members so a returning `init` leaves **no stale core member**:
|
|
198
199
|
|
|
199
200
|
| Path | What |
|
|
200
201
|
|------|------|
|
|
201
202
|
| `~/.claude/skills/agent-workflow-kit/` | the kit itself (refreshed on every `init`) |
|
|
203
|
+
| `~/.claude/skills/agent-workflow-memory/` | the **memory substrate**, refreshed via `npx @sabaiway/agent-workflow-memory@latest init` — **best-effort:** a failure is a **loud degraded success** (warning + the exact recovery command + exit 0), never silent; skip with `--no-memory` |
|
|
204
|
+
| `~/.claude/skills/agent-workflow-engine/` | the **methodology engine** the kit reads live, refreshed via `npx @sabaiway/agent-workflow-engine@latest init` — **required** (the live read STOPs without it); skip with `--no-engine` |
|
|
202
205
|
| `~/.codex/skills/agent-workflow-kit` | a symlink — only if you have Codex |
|
|
203
206
|
| `…/global_workflows/agent-workflow-kit.md` | a managed file — only if you have Devin Desktop |
|
|
204
207
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
The **execution-backend bridges** (`codex` / `agy`) are **not** installed by `init` — set one up on
|
|
209
|
+
demand with `/agent-workflow-kit setup`. Your other Codex skills and Devin Desktop workflows are
|
|
210
|
+
never touched. If one of those exact slots already holds a file the kit didn't write, it is **left
|
|
211
|
+
alone** and you're told — re-run with `--force` to replace it (the original is first copied to
|
|
212
|
+
`*.bak.<timestamp>` and the restore command is printed).
|
|
209
213
|
|
|
210
|
-
**Uninstall:** delete the slots above (the kit
|
|
214
|
+
**Uninstall:** delete the slots above (the kit / memory / engine folders, the symlink, the workflow
|
|
215
|
+
file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
211
216
|
</details>
|
|
212
217
|
|
|
213
218
|
---
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '1.
|
|
6
|
+
version: '1.20.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -28,6 +28,10 @@ when it is present and healthy, and otherwise uses its **own bundled copy** (`re
|
|
|
28
28
|
memory substrate**. (The methodology slot is a separate axis: its fragment is read **live from the
|
|
29
29
|
installed `agent-workflow-engine`**, which `npx @sabaiway/agent-workflow-kit@latest init` installs — a
|
|
30
30
|
runtime dependency placed by `init` and read live; see *Methodology slot reconciliation* below.)
|
|
31
|
+
`init` also **refreshes the installed memory substrate** (best-effort — a miss is a loud DEGRADED
|
|
32
|
+
success: a warning with the exact recovery command + exit 0, never silent, never the engine's hard
|
|
33
|
+
STOP; `--no-memory` skips it), so a returning `init` leaves **no stale core member**. The
|
|
34
|
+
execution-backend bridges are still **not** installed by `init` (placed on demand by `setup`).
|
|
31
35
|
|
|
32
36
|
**Detection (kit-owned, decided BEFORE any project write).** Run the kit's **own shipped**
|
|
33
37
|
validator — `node ${CLAUDE_SKILL_DIR}/tools/manifest/validate.mjs <memory-skill-dir>` — never a
|
|
@@ -336,7 +340,9 @@ Read-only. The **single answer to "versions + deployment + settings + bridges"**
|
|
|
336
340
|
|
|
337
341
|
Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]` and render it **compact**, in the user's conversational language — **never paste the JSON or any internal field name** (no-leak rule). Map the **`installed[].state`** token via the value→plain-language map under *The version block + welcome mat* (the `visibility` and wrapper states have their own phrasings, below). Present, each area on its own line(s), routing detail to its domain mode:
|
|
338
342
|
|
|
339
|
-
1. **Versions —
|
|
343
|
+
1. **Versions — a status-only render from `installed[]` + `deploymentHead`** (this is **NOT** the shared notes-based version block — see the separation note below): the deployment-structure head, then each member by its `display` showing its `version` (or, when there is no version, the plain phrase for its `state`, mapped above), plus the two-axes disambiguation. **Freshness comes from `installed[].refresh`, not from `notes`:** for each member whose **`refresh.behind`** is `true`, show a **localized "needs refresh"** label and the **verbatim `refresh.recommend`** command **exactly once** (the command/package name stays source-language; **do not also paste the English `notes` caveats** — `refresh.recommend` is the single source of the recovery step, so the command is never duplicated on this surface). Lead with a one-line **headline count** derived from `installed[].state` + `refresh.behind` (e.g. *"5 members installed · 1 needs a refresh"*).
|
|
344
|
+
|
|
345
|
+
> **Status reads `refresh`; the shared version block + the bootstrap/upgrade footers stay `notes`-based (unchanged this release).** *Mode: status* has its OWN status-only render (above), keyed on `installed[].refresh.behind` / `refresh.recommend`. The shared **version block** (under *The version block + welcome mat*) and the bootstrap (step 11) + every upgrade (steps 4 / 8) report footer still consume `installed[].notes` verbatim — that wiring is deliberately **untouched** here (their migration onto `refresh` is deferred). Do not rewrite those footers onto `refresh`.
|
|
340
346
|
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly.
|
|
341
347
|
3. **Settings (`--dir`, one line each)** (from `project.settings`):
|
|
342
348
|
- **recipes** — the effective recipe per slot (detail → `/agent-workflow-kit procedures` / `recipes`); a `recipes.detectError` → say the backends couldn't be checked, so recipes floored at solo.
|
package/bin/install.mjs
CHANGED
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
// compares the version already on disk (the installed SKILL.md) against this runner's own version,
|
|
19
19
|
// never the registry — which is why `@latest` (above) is the documented form: a bare `npx … init`
|
|
20
20
|
// can reuse an OLDER cached build of this installer, so a returning user must bypass the cache to
|
|
21
|
-
// actually upgrade (see decisions.md AD-012).
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// actually upgrade (see decisions.md AD-012). Two steps DO contact a server: after the kit copy,
|
|
22
|
+
// `init` cascades the other npm-installable CORE members so a returning run leaves NO stale core
|
|
23
|
+
// member — the memory substrate (`npx @sabaiway/agent-workflow-memory@latest init`, best-effort: a
|
|
24
|
+
// failure is a loud DEGRADED success, never silent — skippable with `--no-memory`) and the
|
|
25
|
+
// methodology engine the kit reads live (`npx @sabaiway/agent-workflow-engine@latest init`, fatal —
|
|
26
|
+
// the live read STOPs without it — skippable with `--no-engine`). The bridges are NOT installed by
|
|
27
|
+
// `init` (placed by `/agent-workflow-kit setup`). No tracking either way.
|
|
24
28
|
//
|
|
25
29
|
// Dependency-free, Node >= 18.
|
|
26
30
|
|
|
@@ -31,6 +35,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
31
35
|
import { homedir } from 'node:os';
|
|
32
36
|
import { spawnSync } from 'node:child_process';
|
|
33
37
|
import { copyTreeRefresh } from '../tools/fs-safe.mjs';
|
|
38
|
+
// The ONE registry of family members (npm packages, kinds). The init-refresh cascade derives its
|
|
39
|
+
// membership from this table — no second source of "who gets refreshed" — so it can't drift from the
|
|
40
|
+
// manifests (a drift-guard test pins the derivation). Imported from the DATA LEAF (family-members.mjs),
|
|
41
|
+
// NOT family-registry.mjs, so the npx cold-start path stays lean (no status/presenter graph pulled in).
|
|
42
|
+
import { FAMILY_MEMBERS } from '../tools/family-members.mjs';
|
|
34
43
|
|
|
35
44
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
45
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
@@ -146,6 +155,7 @@ const parseArgs = (argv) => {
|
|
|
146
155
|
help: argv.includes('--help') || argv.includes('-h'),
|
|
147
156
|
version: argv.includes('--version') || argv.includes('-v'),
|
|
148
157
|
noLaunchers: argv.includes('--no-launchers'),
|
|
158
|
+
noMemory: argv.includes('--no-memory'),
|
|
149
159
|
noEngine: argv.includes('--no-engine'),
|
|
150
160
|
force: argv.includes('--force'),
|
|
151
161
|
allowDowngrade: argv.includes('--allow-downgrade'),
|
|
@@ -153,47 +163,72 @@ const parseArgs = (argv) => {
|
|
|
153
163
|
};
|
|
154
164
|
};
|
|
155
165
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// with the kit's own npx install context
|
|
160
|
-
// re-create the mirror Plan 3D deletes).
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
// Init-refresh cascade (Plan 3D / AD-016 for the engine; the memory cascade closes the
|
|
167
|
+
// returning-user "no stale core member" journey). After the kit copy, init refreshes the OTHER
|
|
168
|
+
// npm-installable CORE members — the memory substrate + the methodology engine — each over npm,
|
|
169
|
+
// consistent with the kit's own npx install context (NO engine/memory canon is bundled into the kit;
|
|
170
|
+
// that would re-create the mirror Plan 3D deletes). The npm package names are read from the ONE
|
|
171
|
+
// registry (FAMILY_MEMBERS) so there is no second source of truth. --no-memory / --no-engine skip
|
|
172
|
+
// each for air-gapped/scripted installs.
|
|
173
|
+
const npmFor = (name) => {
|
|
174
|
+
const member = FAMILY_MEMBERS.find((m) => m.name === name);
|
|
175
|
+
if (!member?.npm) throw new Error(`[agent-workflow-kit] no npm package registered for ${name}`);
|
|
176
|
+
return member.npm;
|
|
177
|
+
};
|
|
178
|
+
export const ENGINE_PACKAGE = npmFor('agent-workflow-engine');
|
|
179
|
+
export const MEMORY_PACKAGE = npmFor('agent-workflow-memory');
|
|
180
|
+
|
|
181
|
+
// The npm-installable core members the cascade refreshes, DERIVED from FAMILY_MEMBERS (every npm
|
|
182
|
+
// member that is neither the kit — the composition root this very runner installs — nor an
|
|
183
|
+
// execution-backend, which `setup` places, not init). Order + policy are part of the contract:
|
|
184
|
+
// NON-FATAL first (memory — a miss is a degraded success), then FATAL (engine — the live methodology
|
|
185
|
+
// read STOPs without it), so a hard engine failure never pre-empts the best-effort memory refresh.
|
|
186
|
+
// A drift-guard test pins this derivation, so adding a future npm core member can't silently bypass
|
|
187
|
+
// the cascade. Per-member messaging stays explicit in main() (the degraded-vs-STOP wording differs).
|
|
188
|
+
const FATAL_KINDS = new Set(['methodology-engine']);
|
|
189
|
+
export const cascadePlan = () =>
|
|
190
|
+
FAMILY_MEMBERS
|
|
191
|
+
.filter((m) => m.npm && m.kind !== 'execution-backend' && m.name !== 'agent-workflow-kit')
|
|
192
|
+
.map((m) => ({ name: m.name, npm: m.npm, fatal: FATAL_KINDS.has(m.kind) }))
|
|
193
|
+
.sort((a, b) => Number(a.fatal) - Number(b.fatal)); // non-fatal (memory) before fatal (engine)
|
|
194
|
+
|
|
195
|
+
// The exact command + argv to install a member over npx. Windows resolution: spawn `npx.cmd` on
|
|
196
|
+
// win32, `npx` elsewhere, WITHOUT shell:true (no shell-parse overhead/inconsistency; the repo has no
|
|
197
|
+
// npx-spawn precedent to inherit a shell from). `@latest` exactly — never a pinned version, so the
|
|
198
|
+
// cascade always fetches the newest published member. Pure → unit-tested in-process, no network.
|
|
199
|
+
export const memberInstallArgv = (npmPackage, platform) => ({
|
|
167
200
|
command: platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
168
|
-
args: [`${
|
|
201
|
+
args: [`${npmPackage}@latest`, 'init'],
|
|
169
202
|
options: { stdio: 'inherit' }, // note: no `shell: true`
|
|
170
203
|
});
|
|
204
|
+
export const engineInstallArgv = (platform) => memberInstallArgv(ENGINE_PACKAGE, platform);
|
|
205
|
+
export const memoryInstallArgv = (platform) => memberInstallArgv(MEMORY_PACKAGE, platform);
|
|
171
206
|
|
|
172
207
|
// The default runner — the only place that actually spawns. Injected in tests so the suite never
|
|
173
208
|
// hits the network.
|
|
174
|
-
const
|
|
209
|
+
const spawnMember = ({ command, args, options }) => spawnSync(command, args, options);
|
|
175
210
|
|
|
176
211
|
// Synchronous backoff before the single retry. The common first-attempt failure is a TRANSIENT
|
|
177
212
|
// npm/network blip (rate-limit, registry hiccup, momentary DNS) — an immediate retry tends to hit the
|
|
178
213
|
// same blip, so wait briefly first. Atomics.wait is a dependency-free sync sleep (the install flow is
|
|
179
214
|
// already synchronous here). Injected in tests as a 0ms no-op so the suite never actually sleeps.
|
|
180
|
-
const
|
|
215
|
+
const RETRY_DELAY_MS = 1500;
|
|
181
216
|
const sleepSync = (ms) => {
|
|
182
217
|
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
183
218
|
};
|
|
184
219
|
|
|
185
220
|
// D1 failure policy: attempt → wait → retry-once → fail. Retry exactly once before giving up. Pure
|
|
186
221
|
// aside from the injected runner/sleep; returns { ok } so the caller prints the loud manual-recovery
|
|
187
|
-
// message
|
|
188
|
-
// a silent skip).
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const ranOk = (
|
|
222
|
+
// message (the engine's nonzero exit, the memory's degraded warning) on a hard failure (the kit is
|
|
223
|
+
// already on disk — recovery is one step; never a silent skip). `label` names the member in the
|
|
224
|
+
// per-attempt warning (e.g. "methodology engine", "memory substrate").
|
|
225
|
+
const installMember = (label, descriptor, runner, { sleep = sleepSync, retryDelayMs = RETRY_DELAY_MS } = {}) => {
|
|
226
|
+
const ranOk = (attempt) => {
|
|
192
227
|
const res = runner(descriptor);
|
|
193
228
|
const ok = (res?.status ?? 1) === 0 && !res?.error;
|
|
194
229
|
if (!ok) {
|
|
195
230
|
const why = res?.error ? `: ${res.error.message}` : ` (exit ${res?.status ?? 'unknown'})`;
|
|
196
|
-
console.warn(`[agent-workflow-kit]
|
|
231
|
+
console.warn(`[agent-workflow-kit] ${label} install ${attempt} failed${why}.`);
|
|
197
232
|
}
|
|
198
233
|
return ok;
|
|
199
234
|
};
|
|
@@ -203,17 +238,47 @@ export const installEngine = (platform, runner, { sleep = sleepSync, retryDelayM
|
|
|
203
238
|
return { ok: false };
|
|
204
239
|
};
|
|
205
240
|
|
|
241
|
+
export const installEngine = (platform, runner, opts = {}) =>
|
|
242
|
+
installMember('methodology engine', engineInstallArgv(platform), runner, opts);
|
|
243
|
+
export const installMemory = (platform, runner, opts = {}) =>
|
|
244
|
+
installMember('memory substrate', memoryInstallArgv(platform), runner, opts);
|
|
245
|
+
|
|
206
246
|
const resolveTarget = (dirArg) => {
|
|
207
247
|
if (dirArg) return resolve(dirArg);
|
|
208
248
|
if (process.env.AGENT_WORKFLOW_KIT_DIR) return resolve(process.env.AGENT_WORKFLOW_KIT_DIR);
|
|
209
249
|
return resolve(homedir(), '.claude/skills/agent-workflow-kit');
|
|
210
250
|
};
|
|
211
251
|
|
|
252
|
+
// Where the memory substrate lives — its env override else its home-relative default, both read from
|
|
253
|
+
// the ONE registry (FAMILY_MEMBERS), expanding a leading `~/`. Used only by the DEGRADED warning to
|
|
254
|
+
// report the on-disk memory version after a refresh miss (init never WRITES here — that is memory's
|
|
255
|
+
// own installer's job). getenv/home injectable for hermetic tests.
|
|
256
|
+
const resolveMemoryTarget = (getenv = process.env, home = homedir()) => {
|
|
257
|
+
const member = FAMILY_MEMBERS.find((m) => m.name === 'agent-workflow-memory');
|
|
258
|
+
const envDir = getenv[member.installed.env];
|
|
259
|
+
if (envDir) return resolve(envDir);
|
|
260
|
+
const dflt = member.installed.default;
|
|
261
|
+
return dflt.startsWith('~/') ? resolve(home, dflt.slice(2)) : resolve(dflt);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Crash-proof on-disk version read for the degraded memory warning: readInstalledVersion returns null
|
|
265
|
+
// for an ABSENT SKILL.md and THROWS for a present-but-UNREADABLE one — but the warning must NEVER
|
|
266
|
+
// become fatal, so any throw is swallowed to null here. Both branches (absent → null directly,
|
|
267
|
+
// unreadable → caught → null) keep the warning best-effort and exit 0 (degraded success, no silent
|
|
268
|
+
// skip). Returns the semver string or null.
|
|
269
|
+
export const readMemberVersionSafe = async (dir) => {
|
|
270
|
+
try {
|
|
271
|
+
return await readInstalledVersion(dir);
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
212
277
|
const printHelp = (version) => {
|
|
213
278
|
console.log(`agent-workflow-kit ${version}
|
|
214
279
|
|
|
215
280
|
Usage:
|
|
216
|
-
npx @sabaiway/agent-workflow-kit@latest init [--dir <path>] [--no-launchers] [--no-engine] [--force] [--allow-downgrade]
|
|
281
|
+
npx @sabaiway/agent-workflow-kit@latest init [--dir <path>] [--no-launchers] [--no-memory] [--no-engine] [--force] [--allow-downgrade]
|
|
217
282
|
npx @sabaiway/agent-workflow-kit@latest --version
|
|
218
283
|
npx @sabaiway/agent-workflow-kit@latest --help
|
|
219
284
|
|
|
@@ -222,11 +287,15 @@ Use the @latest form: a bare \`npx … init\` (no @latest) can reuse an OLDER ca
|
|
|
222
287
|
|
|
223
288
|
Installs/refreshes the kit at ~/.claude/skills/agent-workflow-kit
|
|
224
289
|
(override with --dir <path> or AGENT_WORKFLOW_KIT_DIR), then wires any
|
|
225
|
-
Codex / Devin Desktop you have, then
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
290
|
+
Codex / Devin Desktop you have, then refreshes the other npm core members so no stale
|
|
291
|
+
one is left behind: the memory substrate (npx ${MEMORY_PACKAGE}@latest init)
|
|
292
|
+
and the methodology engine the kit reads live (npx ${ENGINE_PACKAGE}@latest init).
|
|
293
|
+
--no-launchers skips the launcher wiring; --no-memory skips the memory refresh (if it is
|
|
294
|
+
stale, refresh it yourself + restart — a miss is otherwise a non-fatal degraded success,
|
|
295
|
+
never the engine's hard STOP); --no-engine skips the engine install (the live methodology
|
|
296
|
+
read then STOPs until you install it by hand); --force replaces a pre-existing non-kit
|
|
297
|
+
launcher file (backed up first). The bridges are NOT installed by init (placed by
|
|
298
|
+
/agent-workflow-kit setup). init is additive — it never deletes your settings. If the
|
|
230
299
|
installed kit is newer than the version you ran, init refuses (no network — it compares
|
|
231
300
|
the version on disk) and points you at @latest; --allow-downgrade overrides that
|
|
232
301
|
refusal (distinct from --force, which is launcher-only).
|
|
@@ -335,9 +404,35 @@ const main = async () => {
|
|
|
335
404
|
}
|
|
336
405
|
}
|
|
337
406
|
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
407
|
+
// Memory substrate refresh — AFTER the kit + launchers, BEFORE the (fatal) engine and the success
|
|
408
|
+
// block, so a returning `init` leaves no stale memory. Unlike the engine, a memory miss is a
|
|
409
|
+
// DEGRADED success: warn with the exact recovery command + the on-disk version (read crash-proof)
|
|
410
|
+
// and keep exit 0 — never silently skip (Hard Constraint), never the engine's hard STOP.
|
|
411
|
+
const memoryCmd = `npx ${MEMORY_PACKAGE}@latest init`;
|
|
412
|
+
if (args.noMemory) {
|
|
413
|
+
console.log(
|
|
414
|
+
`[agent-workflow-kit] --no-memory: skipped refreshing the memory substrate. If it is stale, ` +
|
|
415
|
+
`refresh it yourself and restart the session:\n ${memoryCmd}`,
|
|
416
|
+
);
|
|
417
|
+
} else {
|
|
418
|
+
console.log(`[agent-workflow-kit] refreshing the memory substrate: ${memoryCmd}`);
|
|
419
|
+
const memory = installMemory(process.platform, spawnMember);
|
|
420
|
+
if (memory.ok) {
|
|
421
|
+
console.log('[agent-workflow-kit] memory substrate refreshed.');
|
|
422
|
+
} else {
|
|
423
|
+
const onDisk = await readMemberVersionSafe(resolveMemoryTarget());
|
|
424
|
+
const where = onDisk ? `on disk: v${onDisk}` : 'not found on disk';
|
|
425
|
+
console.warn(
|
|
426
|
+
`[agent-workflow-kit] could not refresh the memory substrate (${where}) — continuing; the ` +
|
|
427
|
+
`kit itself IS installed and works. To get the latest memory, run it yourself and restart ` +
|
|
428
|
+
`the session:\n ${memoryCmd}`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Mandatory engine install — AFTER the kit + launchers + memory but BEFORE the success block, so a
|
|
434
|
+
// failure never first claims everything succeeded. The kit reads the methodology fragment live from
|
|
435
|
+
// the installed engine; this places it (over npm, no canon bundled into the kit). --no-engine opts out.
|
|
341
436
|
const engineCmd = `npx ${ENGINE_PACKAGE}@latest init`;
|
|
342
437
|
if (args.noEngine) {
|
|
343
438
|
console.log(
|
|
@@ -347,7 +442,7 @@ const main = async () => {
|
|
|
347
442
|
);
|
|
348
443
|
} else {
|
|
349
444
|
console.log(`[agent-workflow-kit] installing the methodology engine the kit reads live: ${engineCmd}`);
|
|
350
|
-
const engine = installEngine(process.platform,
|
|
445
|
+
const engine = installEngine(process.platform, spawnMember);
|
|
351
446
|
if (!engine.ok) {
|
|
352
447
|
// D1: two attempts failed → loud error + concrete recommendations + nonzero exit. The kit IS on
|
|
353
448
|
// disk, so recovery is one step. Never a silent skip (Hard Constraint: no silent failures).
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -61,6 +61,11 @@ Before proposing changes or committing, review against:
|
|
|
61
61
|
### 2.4. Quality Gates
|
|
62
62
|
- Always run type-checker, linter, and all tests before committing.
|
|
63
63
|
|
|
64
|
+
### 2.5. Right-altitude & code-grounded (planning + review)
|
|
65
|
+
- **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
|
|
66
|
+
- **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
|
|
67
|
+
- **Convergence heuristic.** When a review round keeps finding mechanism issues on a stable architecture, stop refining prose — raise the altitude or hand it to Execute. (Full canon: the project's planning methodology / workflow-methodology canon.)
|
|
68
|
+
|
|
64
69
|
---
|
|
65
70
|
|
|
66
71
|
## 3. Token & Session Optimization
|
|
@@ -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
|
|
@@ -41,73 +41,52 @@ import {
|
|
|
41
41
|
readSettingsFile,
|
|
42
42
|
resolveEffectiveMode,
|
|
43
43
|
} from './velocity-profile.mjs';
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
};
|
|
55
81
|
|
|
56
82
|
// ── the unified registry ───────────────────────────────────────────────────────
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export const FAMILY_MEMBERS = [
|
|
65
|
-
{
|
|
66
|
-
name: 'agent-workflow-kit',
|
|
67
|
-
kind: 'composition-root',
|
|
68
|
-
installed: { env: 'AGENT_WORKFLOW_KIT_DIR', default: '~/.claude/skills/agent-workflow-kit', file: 'SKILL.md' },
|
|
69
|
-
deployed: { file: 'docs/ai/.workflow-version' },
|
|
70
|
-
npm: '@sabaiway/agent-workflow-kit',
|
|
71
|
-
wrapperCmds: [],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: 'agent-workflow-memory',
|
|
75
|
-
kind: 'memory-substrate',
|
|
76
|
-
installed: { env: 'AGENT_WORKFLOW_MEMORY_DIR', default: '~/.claude/skills/agent-workflow-memory', file: 'SKILL.md' },
|
|
77
|
-
deployed: { file: 'docs/ai/.memory-version' },
|
|
78
|
-
npm: '@sabaiway/agent-workflow-memory',
|
|
79
|
-
wrapperCmds: [],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'agent-workflow-engine',
|
|
83
|
-
kind: 'methodology-engine',
|
|
84
|
-
installed: { env: 'AGENT_WORKFLOW_ENGINE_DIR', default: '~/.claude/skills/agent-workflow-engine', file: 'SKILL.md' },
|
|
85
|
-
deployed: null,
|
|
86
|
-
npm: '@sabaiway/agent-workflow-engine',
|
|
87
|
-
wrapperCmds: [],
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: 'codex-cli-bridge',
|
|
91
|
-
kind: 'execution-backend',
|
|
92
|
-
installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
|
|
93
|
-
deployed: null,
|
|
94
|
-
npm: null,
|
|
95
|
-
wrapperCmds: ['codex-exec', 'codex-review'],
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: 'antigravity-cli-bridge',
|
|
99
|
-
kind: 'execution-backend',
|
|
100
|
-
installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
|
|
101
|
-
deployed: null,
|
|
102
|
-
npm: null,
|
|
103
|
-
wrapperCmds: ['agy-run'],
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
// A GLOBAL skill (lives under ~/.claude/skills) may be shared by other projects on the host — the
|
|
108
|
-
// uninstaller warns before removing one (there is no cross-project dependency tracking). All current
|
|
109
|
-
// members are global skills; the field is explicit so the warning is data-driven, not hardcoded.
|
|
110
|
-
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';
|
|
111
90
|
|
|
112
91
|
// ── pure probes ──────────────────────────────────────────────────────────────────
|
|
113
92
|
// Wrapped marker probe → 'present' (a regular file) | 'absent' (ENOENT / not a regular file) |
|
|
@@ -268,62 +247,10 @@ export const surveyProject = (projectDir, deps = {}) => {
|
|
|
268
247
|
};
|
|
269
248
|
|
|
270
249
|
// ── report ───────────────────────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
const formatSettings = (s) => {
|
|
276
|
-
const out = ['', 'settings'];
|
|
277
|
-
if (s.recipes?.error) out.push(` ${pad('recipes', 14)}error: ${s.recipes.error}`);
|
|
278
|
-
else {
|
|
279
|
-
const parts = [];
|
|
280
|
-
for (const [act, slots] of Object.entries(s.recipes?.activities ?? {})) {
|
|
281
|
-
for (const [slot, r] of Object.entries(slots)) parts.push(`${act}.${slot}=${r.recipe}`);
|
|
282
|
-
}
|
|
283
|
-
out.push(` ${pad('recipes', 14)}${parts.join(' · ') || '—'}`);
|
|
284
|
-
if (s.recipes?.detectError) out.push(` ${pad('', 14)}↳ couldn't check backends (${s.recipes.detectError}); recipes floored at solo`);
|
|
285
|
-
}
|
|
286
|
-
if (s.attribution?.error) out.push(` ${pad('attribution', 14)}error: ${s.attribution.error}`);
|
|
287
|
-
else out.push(` ${pad('attribution', 14)}includeCoAuthoredBy effective=${String(s.attribution?.effective)}`);
|
|
288
|
-
if (s.velocity?.error) out.push(` ${pad('velocity', 14)}error: ${s.velocity.error}`);
|
|
289
|
-
else out.push(` ${pad('velocity', 14)}defaultMode=${String(s.velocity?.defaultMode)} · allow project/local=${s.velocity?.allowEntries?.project}/${s.velocity?.allowEntries?.local}`);
|
|
290
|
-
return out;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
export const formatStatus = (family, project = null, extras = {}) => {
|
|
294
|
-
const lines = ['agent-workflow family — installed skills (skill axis)', ''];
|
|
295
|
-
for (const m of family) {
|
|
296
|
-
const ver = m.version ? `v${m.version}` : '—';
|
|
297
|
-
lines.push(` ${pad(m.name, 26)}[${pad(m.manifestState, 16)}] ${pad(ver, 10)} ${m.kind}`);
|
|
298
|
-
for (const c of m.caveats ?? []) lines.push(` ↳ ${c}`);
|
|
299
|
-
}
|
|
300
|
-
if (extras.bridges) {
|
|
301
|
-
const WRAP_MARK = { present: '✓', missing: '✗', unknown: '?' };
|
|
302
|
-
lines.push('', 'execution backends (host)', '');
|
|
303
|
-
for (const b of extras.bridges) {
|
|
304
|
-
const w = b.wrappers.map((x) => `${x.cmd} ${WRAP_MARK[x.state] ?? '?'}`).join(', ') || '—';
|
|
305
|
-
lines.push(` ${pad(b.display, 20)}${pad(b.readiness, 18)}wrappers: ${w}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (project) {
|
|
309
|
-
lines.push('', `project deployment (${project.dir})`, '');
|
|
310
|
-
if (!project.deployed) {
|
|
311
|
-
lines.push(' no agent-workflow deployment detected here (no docs/ai, no version stamp).');
|
|
312
|
-
} else {
|
|
313
|
-
for (const s of project.stamps) {
|
|
314
|
-
lines.push(` ${pad(s.file, 26)}${s.version ?? '—'}`);
|
|
315
|
-
}
|
|
316
|
-
lines.push(` ${pad('docs/ai present', 26)}${project.docsAiPresent ? 'yes' : 'no'}`);
|
|
317
|
-
if (extras.visibility) {
|
|
318
|
-
lines.push(` ${pad('visibility', 26)}${extras.visibility.error ? `error: ${extras.visibility.error}` : extras.visibility.state}`);
|
|
319
|
-
} else {
|
|
320
|
-
lines.push(` ${pad('hidden-mode fence', 26)}${project.hiddenFence ? 'present' : 'absent'}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (extras.settings) lines.push(...formatSettings(extras.settings));
|
|
324
|
-
}
|
|
325
|
-
return lines.join('\n');
|
|
326
|
-
};
|
|
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.
|
|
327
254
|
|
|
328
255
|
// ── the no-leak --json envelope ──────────────────────────────────────────────────
|
|
329
256
|
// A machine-readable view with USER-SAFE field names only — NEVER the internal manifestState /
|
|
@@ -331,28 +258,8 @@ export const formatStatus = (family, project = null, extras = {}) => {
|
|
|
331
258
|
// consumes THIS, never the human table verbatim. An envelope-shape test pins its shape so later phases
|
|
332
259
|
// (the settings/visibility block) can't silently break the Phase-2 version consumer.
|
|
333
260
|
|
|
334
|
-
// internal
|
|
335
|
-
//
|
|
336
|
-
const STATE_PUBLIC = {
|
|
337
|
-
[OK]: 'installed',
|
|
338
|
-
[NOT_INSTALLED]: 'absent',
|
|
339
|
-
[FOREIGN]: 'other-tool',
|
|
340
|
-
[STUB]: 'placeholder',
|
|
341
|
-
[INVALID_MANIFEST]: 'invalid',
|
|
342
|
-
[UNSUPPORTED_SCHEMA]: 'unsupported',
|
|
343
|
-
[UNKNOWN]: 'uncheckable',
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
// Short, user-facing labels for the version block (kit · memory · engine · codex-bridge · …), so the
|
|
347
|
-
// render is deterministic and the agent never invents a label.
|
|
348
|
-
export const DISPLAY_NAMES = {
|
|
349
|
-
'agent-workflow-kit': 'kit',
|
|
350
|
-
'agent-workflow-memory': 'memory',
|
|
351
|
-
'agent-workflow-engine': 'engine',
|
|
352
|
-
'codex-cli-bridge': 'codex-bridge',
|
|
353
|
-
'antigravity-cli-bridge': 'antigravity-bridge',
|
|
354
|
-
};
|
|
355
|
-
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.
|
|
356
263
|
|
|
357
264
|
// ── the settings survey (Phase 3) — read-only, honest, localized-on-error ──────────
|
|
358
265
|
// Each sub-survey returns a small user-safe object OR a single `{ error }` field (a localized message,
|
|
@@ -380,8 +287,8 @@ const detectSafe = (deps) => {
|
|
|
380
287
|
};
|
|
381
288
|
|
|
382
289
|
// visibility: the THREE honest states from inferVisibility (NOT the single hiddenFence bit) → user-safe
|
|
383
|
-
// words. Never the internal "hidden fence" / marker terms. A
|
|
384
|
-
|
|
290
|
+
// words (VISIBILITY_PUBLIC, from labels.mjs). Never the internal "hidden fence" / marker terms. A
|
|
291
|
+
// git/probe error → a localized error field.
|
|
385
292
|
export const surveyVisibility = (dir, deps = {}) => {
|
|
386
293
|
try {
|
|
387
294
|
const vis = inferVisibility(deps, resolve(dir));
|
|
@@ -469,6 +376,20 @@ export const surveyBridges = (deps = {}) => {
|
|
|
469
376
|
});
|
|
470
377
|
};
|
|
471
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
|
+
|
|
472
393
|
export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
473
394
|
const installed = family.map((m) => {
|
|
474
395
|
const entry = {
|
|
@@ -478,6 +399,7 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
478
399
|
state: STATE_PUBLIC[m.manifestState] ?? STATE_PUBLIC[UNKNOWN],
|
|
479
400
|
};
|
|
480
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)
|
|
481
403
|
return entry;
|
|
482
404
|
});
|
|
483
405
|
const envelope = { deploymentHead: EXPECTED_WORKFLOW_VERSION, installed };
|
|
@@ -498,27 +420,72 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
498
420
|
};
|
|
499
421
|
|
|
500
422
|
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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 };
|
|
508
453
|
};
|
|
509
454
|
|
|
510
|
-
const
|
|
511
|
-
const args = parseArgs(argv);
|
|
512
|
-
if (args.help) {
|
|
513
|
-
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.
|
|
514
456
|
|
|
515
457
|
Usage:
|
|
516
|
-
node family-registry.mjs [--dir <project>] [--json]
|
|
517
|
-
# 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).
|
|
518
463
|
|
|
519
|
-
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);
|
|
520
469
|
return;
|
|
521
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
|
+
|
|
522
489
|
const family = surveyFamily();
|
|
523
490
|
const bridges = surveyBridges();
|
|
524
491
|
const project = args.dir ? surveyProject(args.dir) : null;
|
|
@@ -527,7 +494,10 @@ Detection only — never writes, never commits, never runs a subscription CLI.`)
|
|
|
527
494
|
extras.visibility = surveyVisibility(args.dir);
|
|
528
495
|
extras.settings = surveySettings(args.dir);
|
|
529
496
|
}
|
|
530
|
-
|
|
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));
|
|
531
501
|
};
|
|
532
502
|
|
|
533
503
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
package/tools/labels.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// labels.mjs — the frozen vocabulary LEAF of the status surface (Plan: One-init-freshness §4.2 B1).
|
|
2
|
+
//
|
|
3
|
+
// This module owns the internal↔public token vocabulary that used to live inline in
|
|
4
|
+
// family-registry.mjs: the manifestState constants, the internal→public state map, the visibility
|
|
5
|
+
// map, the short display names, and the no-leak forbidden-term set. It is a LEAF — it imports
|
|
6
|
+
// nothing from the family (only the language is here), so the import graph stays acyclic: nobody
|
|
7
|
+
// imports family-registry for vocabulary, and family-registry imports + re-exports the public subset
|
|
8
|
+
// it exported before (the 7 state constants + DISPLAY_NAMES), so every existing importer stays green.
|
|
9
|
+
//
|
|
10
|
+
// Pure data, no side effects, Node >= 18. Every export is frozen — this is a contract, not a mutable.
|
|
11
|
+
|
|
12
|
+
// ── manifestState values (internal; the detect-backends precedence, generalized to any member) ──────
|
|
13
|
+
// These are INTERNAL — they must never reach a user surface verbatim (mapped through STATE_PUBLIC
|
|
14
|
+
// first). The two surfaces are pinned separately; the no-leak guard (INTERNAL_RENDER_FORBIDDEN) keeps
|
|
15
|
+
// the internal literals out of the serialized envelope.
|
|
16
|
+
export const NOT_INSTALLED = 'not-installed';
|
|
17
|
+
export const UNSUPPORTED_SCHEMA = 'unsupported-schema';
|
|
18
|
+
export const INVALID_MANIFEST = 'invalid-manifest';
|
|
19
|
+
export const STUB = 'stub';
|
|
20
|
+
export const FOREIGN = 'foreign';
|
|
21
|
+
export const OK = 'ok';
|
|
22
|
+
// The marker could not be probed (a non-ENOENT fs error — EACCES/EIO). Surfaced explicitly instead of
|
|
23
|
+
// being masked as not-installed (no silent failure). NOTE: 'unknown' is ALSO a PUBLIC value — it is a
|
|
24
|
+
// bridge wrappers[].state ("couldn't check"), so it is deliberately EXCLUDED from the no-leak set.
|
|
25
|
+
export const UNKNOWN = 'unknown';
|
|
26
|
+
|
|
27
|
+
// internal manifestState → a STABLE, user-safe token. SKILL.md owns the value→plain-language phrasing;
|
|
28
|
+
// presentation.mjs owns the direct-CLI English phrasing. Deliberately NOT the internal literals
|
|
29
|
+
// (foreign/stub/…): those must never leak past this boundary.
|
|
30
|
+
export const STATE_PUBLIC = Object.freeze({
|
|
31
|
+
[OK]: 'installed',
|
|
32
|
+
[NOT_INSTALLED]: 'absent',
|
|
33
|
+
[FOREIGN]: 'other-tool',
|
|
34
|
+
[STUB]: 'placeholder',
|
|
35
|
+
[INVALID_MANIFEST]: 'invalid',
|
|
36
|
+
[UNSUPPORTED_SCHEMA]: 'unsupported',
|
|
37
|
+
[UNKNOWN]: 'uncheckable',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// visibility: the THREE honest states from inferVisibility → user-safe words. Never the internal
|
|
41
|
+
// "hidden fence" / marker terms. ('hidden' here is the PUBLIC visibility word, not the internal fence.)
|
|
42
|
+
export const VISIBILITY_PUBLIC = Object.freeze({ visible: 'visible', hidden: 'hidden', ambiguous: 'unclear' });
|
|
43
|
+
|
|
44
|
+
// Short, user-facing labels for the version block (kit · memory · engine · codex-bridge · …), so the
|
|
45
|
+
// render is deterministic and the agent never invents a label.
|
|
46
|
+
export const DISPLAY_NAMES = Object.freeze({
|
|
47
|
+
'agent-workflow-kit': 'kit',
|
|
48
|
+
'agent-workflow-memory': 'memory',
|
|
49
|
+
'agent-workflow-engine': 'engine',
|
|
50
|
+
'codex-cli-bridge': 'codex-bridge',
|
|
51
|
+
'antigravity-cli-bridge': 'antigravity-bridge',
|
|
52
|
+
});
|
|
53
|
+
export const displayOf = (name) => DISPLAY_NAMES[name] ?? name;
|
|
54
|
+
|
|
55
|
+
// ── the no-leak forbidden set (Plan §4.3 INV-4) ────────────────────────────────────────────────────
|
|
56
|
+
// The terms that must NEVER appear in the serialized buildEnvelope output (the agent reads the JSON
|
|
57
|
+
// directly, so the no-leak boundary is the envelope, not just the rendered text). The INV-4 test
|
|
58
|
+
// iterates this set over JSON.stringify(envelope). Deliberately EXCLUDES:
|
|
59
|
+
// • 'unknown' — also a PUBLIC bridge wrappers[].state value (a `state:"unknown"` fixture must pass);
|
|
60
|
+
// • 'ok' — too generic to forbid as a substring, and not sensitive (its public form is 'installed').
|
|
61
|
+
// Sourced here as the ONE definition so the guard can't drift from a hand-copied list in the test.
|
|
62
|
+
export const INTERNAL_RENDER_FORBIDDEN = Object.freeze([
|
|
63
|
+
// internal manifestState literals that must never reach a surface
|
|
64
|
+
NOT_INSTALLED,
|
|
65
|
+
UNSUPPORTED_SCHEMA,
|
|
66
|
+
INVALID_MANIFEST,
|
|
67
|
+
STUB,
|
|
68
|
+
FOREIGN,
|
|
69
|
+
// internal field names (the structural keys of the surveys, never the envelope)
|
|
70
|
+
'manifestState',
|
|
71
|
+
'hiddenFence',
|
|
72
|
+
// the hidden-mode marker / fence jargon (the SKILL communication firewall) — never the public
|
|
73
|
+
// visibility words ('hidden' / 'visible' / 'unclear'), only the internal compound terms + operations
|
|
74
|
+
'hidden fence',
|
|
75
|
+
'hidden-mode fence',
|
|
76
|
+
'reconcile',
|
|
77
|
+
'ensureSlot',
|
|
78
|
+
// raw deployment-stamp FILENAMES (the envelope exposes member/display/version, never the file)
|
|
79
|
+
'.workflow-version',
|
|
80
|
+
'.memory-version',
|
|
81
|
+
]);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// presentation.mjs — the frozen ENGLISH/neutral vocabulary the direct-CLI renderers use to draw the
|
|
2
|
+
// four status blocks (members · bridges · project deploy/visibility · settings). Plan §4.2.
|
|
3
|
+
//
|
|
4
|
+
// This is the DIRECT-CLI surface only (`node tools/family-registry.mjs` in a terminal). The
|
|
5
|
+
// agent-mediated surface (`/agent-workflow-kit status`) consumes the `--json` envelope and localizes
|
|
6
|
+
// in the user's language per SKILL.md §4.4 — it never reads this file. Keeping the vocabulary here (a
|
|
7
|
+
// frozen leaf) means the renderers carry no inline strings and the phrasing is pinned by tests.
|
|
8
|
+
//
|
|
9
|
+
// Pure data, no side effects, Node >= 18. Every export frozen.
|
|
10
|
+
|
|
11
|
+
// public state token (STATE_PUBLIC value) → the direct-CLI English phrase. `installed` is null because
|
|
12
|
+
// an installed member shows its VERSION, not a phrase (the renderer special-cases it). Mirrors the
|
|
13
|
+
// SKILL.md value→plain-language map so the two surfaces stay semantically aligned.
|
|
14
|
+
export const STATE_PHRASING = Object.freeze({
|
|
15
|
+
installed: null, // → show the version instead
|
|
16
|
+
absent: 'not installed',
|
|
17
|
+
'other-tool': 'a different tool occupies that skill slot',
|
|
18
|
+
placeholder: 'a placeholder, not a working install',
|
|
19
|
+
invalid: "installed but its manifest didn't validate",
|
|
20
|
+
unsupported: 'installed but its manifest schema is too new for this kit',
|
|
21
|
+
uncheckable: "couldn't be checked (a permission error)",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// public visibility (VISIBILITY_PUBLIC value) → phrase. Never the internal "fence"/marker terms.
|
|
25
|
+
export const VISIBILITY_PHRASING = Object.freeze({
|
|
26
|
+
visible: 'visible (tracked)',
|
|
27
|
+
hidden: 'hidden (git-ignored, local-only)',
|
|
28
|
+
unclear: 'unclear (uncommitted or partially set up)',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Block titles. `project` is a function of the resolved dir.
|
|
32
|
+
export const BLOCK_TITLES = Object.freeze({
|
|
33
|
+
members: 'agent-workflow family — installed members (skill axis)',
|
|
34
|
+
bridges: 'execution backends (host)',
|
|
35
|
+
project: (dir) => `project deployment (${dir})`,
|
|
36
|
+
settings: 'settings',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Per-area settings row labels (the left column of the settings block).
|
|
40
|
+
export const SETTINGS_LABELS = Object.freeze({
|
|
41
|
+
recipes: 'recipes',
|
|
42
|
+
attribution: 'attribution',
|
|
43
|
+
velocity: 'velocity',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Glyph sets — Unicode for a capable terminal, an ASCII fallback for a narrow / Windows-legacy one.
|
|
47
|
+
// The renderer picks one set by the resolved surface's `ascii` flag (surface.mjs).
|
|
48
|
+
export const GLYPHS = Object.freeze({
|
|
49
|
+
unicode: Object.freeze({ note: '↳', present: '✓', missing: '✗', unknown: '?', bullet: '•' }),
|
|
50
|
+
ascii: Object.freeze({ note: '->', present: '+', missing: 'x', unknown: '?', bullet: '*' }),
|
|
51
|
+
});
|
|
52
|
+
export const glyphsFor = (ascii) => (ascii ? GLYPHS.ascii : GLYPHS.unicode);
|
|
53
|
+
|
|
54
|
+
// The "no deployment here" line for an undeployed project.
|
|
55
|
+
export const NO_DEPLOYMENT = 'no agent-workflow deployment detected here (no docs/ai, no version stamp).';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// renderers.mjs — the direct-CLI presenters (plain + ansi) for the four status blocks. Plan §4.2/§4.5.
|
|
2
|
+
//
|
|
3
|
+
// Input: the ViewModel (view-model.mjs, built from the no-leak envelope) + a resolved surface
|
|
4
|
+
// (surface.mjs: { mode, width, color, ascii }). Output: a single string. `mode` selects plain vs ansi;
|
|
5
|
+
// the JSON mode is handled by the caller (it prints the envelope, not a render). The renderers do
|
|
6
|
+
// LAYOUT + GLYPHS only — phrasing + headline counts already live in the ViewModel.
|
|
7
|
+
//
|
|
8
|
+
// Pure, no side effects, Node >= 18.
|
|
9
|
+
|
|
10
|
+
import { BLOCK_TITLES, SETTINGS_LABELS, glyphsFor, NO_DEPLOYMENT } from './presentation.mjs';
|
|
11
|
+
|
|
12
|
+
const MEMBER_COL = 20;
|
|
13
|
+
const VERSION_COL = 12;
|
|
14
|
+
const READINESS_COL = 14;
|
|
15
|
+
const STAMP_COL = 26;
|
|
16
|
+
const SETTINGS_COL = 14;
|
|
17
|
+
|
|
18
|
+
const SGR = Object.freeze({ bold: '\x1b[1m', reset: '\x1b[0m' });
|
|
19
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
20
|
+
export const visibleLength = (s) => s.replace(ANSI_RE, '').length;
|
|
21
|
+
|
|
22
|
+
const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
|
|
23
|
+
// A styled span only when color is on — so color:false emits ZERO SGR (plain output, byte-clean).
|
|
24
|
+
const heading = (text, color) => (color ? `${SGR.bold}${text}${SGR.reset}` : text);
|
|
25
|
+
|
|
26
|
+
const renderMembers = (vm, { color, glyph }) => {
|
|
27
|
+
const lines = [heading(BLOCK_TITLES.members, color), ''];
|
|
28
|
+
for (const m of vm.members) {
|
|
29
|
+
const ver = m.version ? `v${m.version}` : '—';
|
|
30
|
+
const tail = m.statePhrase == null ? '' : m.statePhrase; // installed → the version IS the info
|
|
31
|
+
lines.push(` ${pad(m.display, MEMBER_COL)}${pad(ver, VERSION_COL)}${tail}`.trimEnd());
|
|
32
|
+
for (const note of m.notes) lines.push(` ${glyph.note} ${note}`); // verbatim caveats (INV-3)
|
|
33
|
+
}
|
|
34
|
+
// refresh.behind drives a HEADLINE COUNT only — the recovery command stays in the verbatim notes
|
|
35
|
+
// above (the direct CLI never dedupes, never re-prints it). Omitted when nothing is behind.
|
|
36
|
+
if (vm.headline.behind > 0) {
|
|
37
|
+
lines.push(` ${vm.headline.behind} member(s) need a refresh (see the ${glyph.note} notes above).`);
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const renderBridges = (vm, { glyph, color }) => {
|
|
43
|
+
if (!vm.bridges) return [];
|
|
44
|
+
const lines = ['', heading(BLOCK_TITLES.bridges, color), ''];
|
|
45
|
+
for (const b of vm.bridges) {
|
|
46
|
+
const wrappers = b.wrappers.map((w) => `${w.cmd} ${glyph[w.state] ?? glyph.unknown}`).join(', ') || '—';
|
|
47
|
+
lines.push(` ${pad(b.display, MEMBER_COL)}${pad(b.readiness, READINESS_COL)}wrappers: ${wrappers}`);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const renderProject = (vm, { color }) => {
|
|
53
|
+
const p = vm.project;
|
|
54
|
+
if (!p) return [];
|
|
55
|
+
const lines = ['', heading(BLOCK_TITLES.project(p.dir), color), ''];
|
|
56
|
+
if (!p.deployed) {
|
|
57
|
+
lines.push(` ${NO_DEPLOYMENT}`);
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
for (const s of p.deployStamps) lines.push(` ${pad(s.display, STAMP_COL)}${s.version ?? '—'}`);
|
|
61
|
+
lines.push(` ${pad('docs/ai present', STAMP_COL)}${p.docsAi ? 'yes' : 'no'}`);
|
|
62
|
+
if (p.visibility) {
|
|
63
|
+
const v = p.visibility.error ? `error: ${p.visibility.error}` : p.visibility.phrase;
|
|
64
|
+
lines.push(` ${pad('visibility', STAMP_COL)}${v}`);
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const renderSettings = (vm, { color, glyph }) => {
|
|
70
|
+
const s = vm.project?.settings;
|
|
71
|
+
if (!s) return [];
|
|
72
|
+
const lines = ['', heading(BLOCK_TITLES.settings, color)];
|
|
73
|
+
// recipes — the effective recipe per slot, or a loud error; a detector floor adds a sub-line.
|
|
74
|
+
if (s.recipes?.error) lines.push(` ${pad(SETTINGS_LABELS.recipes, SETTINGS_COL)}error: ${s.recipes.error}`);
|
|
75
|
+
else if (s.recipes) {
|
|
76
|
+
const joined = s.recipes.pairs.map((p) => `${p.key}=${p.recipe}`).join(' · ') || '—';
|
|
77
|
+
lines.push(` ${pad(SETTINGS_LABELS.recipes, SETTINGS_COL)}${joined}`);
|
|
78
|
+
if (s.recipes.detectError) {
|
|
79
|
+
lines.push(` ${pad('', SETTINGS_COL)}${glyph.note} couldn't check backends (${s.recipes.detectError}); recipes floored at solo`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// attribution — effective includeCoAuthoredBy; a real local override is called out.
|
|
83
|
+
if (s.attribution?.error) lines.push(` ${pad(SETTINGS_LABELS.attribution, SETTINGS_COL)}error: ${s.attribution.error}`);
|
|
84
|
+
else if (s.attribution) {
|
|
85
|
+
const override = s.attribution.override ? ' (local override)' : '';
|
|
86
|
+
lines.push(` ${pad(SETTINGS_LABELS.attribution, SETTINGS_COL)}includeCoAuthoredBy effective=${String(s.attribution.effective)}${override}`);
|
|
87
|
+
}
|
|
88
|
+
// velocity — effective defaultMode + per-source allow counts.
|
|
89
|
+
if (s.velocity?.error) lines.push(` ${pad(SETTINGS_LABELS.velocity, SETTINGS_COL)}error: ${s.velocity.error}`);
|
|
90
|
+
else if (s.velocity) {
|
|
91
|
+
lines.push(` ${pad(SETTINGS_LABELS.velocity, SETTINGS_COL)}defaultMode=${String(s.velocity.defaultMode)} · allow project/local=${s.velocity.allow.project}/${s.velocity.allow.local}`);
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Pad each NON-empty line UP to the surface width (by VISIBLE length, so SGR codes don't count); empty
|
|
97
|
+
// separator lines stay empty. `width` is a MIN target for a consistent block, NOT a hard max: a long
|
|
98
|
+
// content line — a verbatim caveat `↳`, a long recipe row — is left INTACT and is NEVER truncated,
|
|
99
|
+
// because truncating could hide part of a recovery command (a no-silent-failure violation). The
|
|
100
|
+
// terminal soft-wraps an over-width line; data integrity wins over a perfectly rectangular block.
|
|
101
|
+
const padLineTo = (line, width) => {
|
|
102
|
+
if (line === '') return '';
|
|
103
|
+
const vis = visibleLength(line);
|
|
104
|
+
return vis < width ? line + ' '.repeat(width - vis) : line;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const render = (vm, surface = {}) => {
|
|
108
|
+
const glyph = glyphsFor(Boolean(surface.ascii));
|
|
109
|
+
const color = surface.mode === 'ansi' && Boolean(surface.color); // color applies in ansi mode only
|
|
110
|
+
const ctx = { color, glyph };
|
|
111
|
+
const lines = [
|
|
112
|
+
...renderMembers(vm, ctx),
|
|
113
|
+
...renderBridges(vm, ctx),
|
|
114
|
+
...renderProject(vm, ctx),
|
|
115
|
+
...renderSettings(vm, ctx),
|
|
116
|
+
];
|
|
117
|
+
if (surface.mode === 'ansi') return lines.map((l) => padLineTo(l, surface.width ?? 80)).join('\n');
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// surface.mjs — capability detection for the kit's OWN stdout (the direct CLI). Plan §4.5.
|
|
2
|
+
//
|
|
3
|
+
// Resolves how `node tools/family-registry.mjs` should render: the requested format, the effective
|
|
4
|
+
// render mode (json | plain | ansi), whether to emit color, the terminal width, and whether to fall
|
|
5
|
+
// back to ASCII glyphs. This governs ONLY the kit's direct-CLI output — the agent-mediated
|
|
6
|
+
// `/agent-workflow-kit status` surface always consumes `--json` and localizes itself.
|
|
7
|
+
//
|
|
8
|
+
// Pure + fully injectable (argv / env / isTTY / columns / platform are inputs), no side effects on
|
|
9
|
+
// import, Node >= 18. Tested in isolation against the §4.5 table.
|
|
10
|
+
|
|
11
|
+
export const FORMATS = Object.freeze(['auto', 'plain', 'ansi', 'json']);
|
|
12
|
+
export const FORMAT_ENV = 'AGENT_WORKFLOW_FORMAT';
|
|
13
|
+
export const MIN_WIDTH = 40; // below this, force plain ASCII (a box can't lay out under ~40 cols)
|
|
14
|
+
export const DEFAULT_WIDTH = 80;
|
|
15
|
+
const FORMAT_FLAG = '--format=';
|
|
16
|
+
|
|
17
|
+
// Resolve the requested format. A FLAG (--json or --format=X) beats the AGENT_WORKFLOW_FORMAT env;
|
|
18
|
+
// among flags the LAST on argv wins (deterministic, standard last-wins). `--json` is exact sugar for
|
|
19
|
+
// `--format=json`. A bare `--format` (no value), an empty value, or an unknown value → a LOUD reject
|
|
20
|
+
// (never a silent fallback — Hard Constraint). Absent everywhere → 'auto'.
|
|
21
|
+
export const resolveFormat = (argv = [], env = {}) => {
|
|
22
|
+
let fromFlag = null;
|
|
23
|
+
for (const a of argv) {
|
|
24
|
+
if (a === '--json') fromFlag = 'json';
|
|
25
|
+
else if (a === '--format') throw new Error(`[agent-workflow-kit] --format needs a value: --format=<${FORMATS.join('|')}>`);
|
|
26
|
+
else if (a.startsWith(FORMAT_FLAG)) fromFlag = a.slice(FORMAT_FLAG.length);
|
|
27
|
+
}
|
|
28
|
+
const requested = fromFlag ?? env[FORMAT_ENV] ?? 'auto';
|
|
29
|
+
if (!FORMATS.includes(requested)) {
|
|
30
|
+
throw new Error(`[agent-workflow-kit] invalid format "${requested}" — expected one of ${FORMATS.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return requested;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Terminal width: stdout.columns (>0) wins, else $COLUMNS (>0), else the 80-col default. A garbage /
|
|
36
|
+
// zero / undefined value falls through to the next source — never NaN.
|
|
37
|
+
export const resolveWidth = ({ columns, env = {} } = {}) => {
|
|
38
|
+
const fromStdout = Number(columns);
|
|
39
|
+
if (Number.isFinite(fromStdout) && fromStdout > 0) return fromStdout;
|
|
40
|
+
const fromEnv = Number(env.COLUMNS);
|
|
41
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
42
|
+
return DEFAULT_WIDTH;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Color is ORTHOGONAL to the render mode (the ansi renderer applies it; plain ignores it). Precedence:
|
|
46
|
+
// CLICOLOR_FORCE / FORCE_COLOR present → on (unless explicitly '0'/'false'); else NO_COLOR present
|
|
47
|
+
// (incl. empty value) → off; else follow isTTY. FORCE beats NO_COLOR.
|
|
48
|
+
export const resolveColor = ({ env = {}, isTTY = false } = {}) => {
|
|
49
|
+
if ('CLICOLOR_FORCE' in env || 'FORCE_COLOR' in env) {
|
|
50
|
+
const v = 'CLICOLOR_FORCE' in env ? env.CLICOLOR_FORCE : env.FORCE_COLOR;
|
|
51
|
+
return !(v === '0' || String(v).toLowerCase() === 'false');
|
|
52
|
+
}
|
|
53
|
+
if ('NO_COLOR' in env) return false; // any value, incl. empty, disables (the NO_COLOR spec)
|
|
54
|
+
return Boolean(isTTY);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isUtf8Env = (env = {}) => /utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || '');
|
|
58
|
+
|
|
59
|
+
// The full resolved surface. mode: 'json' (machine envelope) | 'plain' | 'ansi'. For a non-json
|
|
60
|
+
// format: an explicit plain/ansi is honored; 'auto' detects a tier (plain when NOT a TTY, or TERM=dumb,
|
|
61
|
+
// or any CI env; else ansi). A width below MIN_WIDTH FORCES plain. ascii glyphs when narrow, or on a
|
|
62
|
+
// Windows TTY without a UTF-8 locale (shallow — deeper Windows/UTF-8 depth is deferred, Plan §9).
|
|
63
|
+
export const detectSurface = ({ argv = [], env = {}, isTTY = false, columns, platform = 'linux' } = {}) => {
|
|
64
|
+
const format = resolveFormat(argv, env);
|
|
65
|
+
const width = resolveWidth({ columns, env });
|
|
66
|
+
if (format === 'json') return { format, mode: 'json', width, color: false, ascii: false };
|
|
67
|
+
|
|
68
|
+
const autoTier = !isTTY || env.TERM === 'dumb' || 'CI' in env ? 'plain' : 'ansi';
|
|
69
|
+
const requestedMode = format === 'auto' ? autoTier : format; // 'plain' | 'ansi'
|
|
70
|
+
const narrow = width < MIN_WIDTH;
|
|
71
|
+
const mode = narrow ? 'plain' : requestedMode; // the width floor wins over an ansi request
|
|
72
|
+
const ascii = narrow || (platform === 'win32' && !isUtf8Env(env));
|
|
73
|
+
const color = resolveColor({ env, isTTY }); // orthogonal — the ansi renderer gates on mode + color
|
|
74
|
+
return { format, mode, width, color, ascii };
|
|
75
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// view-model.mjs — transform the no-leak `--json` envelope (buildEnvelope output) into a render-ready
|
|
2
|
+
// ViewModel for the direct-CLI renderers. Plan §4.2 / §4.5: surface → VIEW-MODEL → renderers.
|
|
3
|
+
//
|
|
4
|
+
// One data source: the renderers never touch the raw surveys, only this VM, which is built from the
|
|
5
|
+
// ENVELOPE — so the renderers inherit the envelope's no-leak guarantee (internal manifestState / stamp
|
|
6
|
+
// filenames never reach them). This module resolves public tokens → English phrases (presentation.mjs)
|
|
7
|
+
// and computes the headline counts; the renderers do layout + glyphs only.
|
|
8
|
+
//
|
|
9
|
+
// Pure, no side effects, Node >= 18.
|
|
10
|
+
|
|
11
|
+
import { STATE_PHRASING, VISIBILITY_PHRASING } from './presentation.mjs';
|
|
12
|
+
|
|
13
|
+
const memberVm = (m) => {
|
|
14
|
+
const refresh = m.refresh ?? { behind: false, recommend: null };
|
|
15
|
+
return {
|
|
16
|
+
display: m.display,
|
|
17
|
+
version: m.version ?? null,
|
|
18
|
+
state: m.state,
|
|
19
|
+
// 'installed' → null (the renderer shows the version instead); every other public token → a phrase.
|
|
20
|
+
statePhrase: m.state in STATE_PHRASING ? STATE_PHRASING[m.state] : m.state,
|
|
21
|
+
notes: m.notes ?? [], // the verbatim caveats — printed as ↳ sub-lines (INV-3: no dedupe)
|
|
22
|
+
behind: Boolean(refresh.behind), // used ONLY for the headline count on the direct CLI (INV-3)
|
|
23
|
+
recommend: refresh.recommend ?? null,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const bridgeVm = (b) => ({
|
|
28
|
+
display: b.display,
|
|
29
|
+
readiness: b.readiness,
|
|
30
|
+
// preserve the three-state wrapper status (present | missing | unknown) — the renderer maps to a glyph.
|
|
31
|
+
wrappers: (b.wrappers ?? []).map((w) => ({ cmd: w.cmd, state: w.state })),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const visibilityVm = (v) => {
|
|
35
|
+
if (!v) return null;
|
|
36
|
+
if (v.error) return { error: v.error };
|
|
37
|
+
return { phrase: v.state in VISIBILITY_PHRASING ? VISIBILITY_PHRASING[v.state] : v.state };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const recipesVm = (r) => {
|
|
41
|
+
if (!r) return null;
|
|
42
|
+
if (r.error) return { error: r.error };
|
|
43
|
+
const pairs = [];
|
|
44
|
+
for (const [activity, slots] of Object.entries(r.activities ?? {})) {
|
|
45
|
+
for (const [slot, v] of Object.entries(slots)) pairs.push({ key: `${activity}.${slot}`, recipe: v.recipe });
|
|
46
|
+
}
|
|
47
|
+
return { pairs, detectError: r.detectError ?? null };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const attributionVm = (a) => {
|
|
51
|
+
if (!a) return null;
|
|
52
|
+
if (a.error) return { error: a.error };
|
|
53
|
+
// A local OVERRIDE only when local actually set the key (non-null) AND it differs from project — a
|
|
54
|
+
// null local means the key is absent there, so the project value stands (that is not an override).
|
|
55
|
+
const override = a.local != null && a.local !== a.project;
|
|
56
|
+
return { effective: a.effective ?? null, override };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const velocityVm = (v) => {
|
|
60
|
+
if (!v) return null;
|
|
61
|
+
if (v.error) return { error: v.error };
|
|
62
|
+
return { defaultMode: v.defaultMode ?? null, allow: { project: v.allowEntries?.project ?? 0, local: v.allowEntries?.local ?? 0 } };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const settingsVm = (s) =>
|
|
66
|
+
s ? { recipes: recipesVm(s.recipes), attribution: attributionVm(s.attribution), velocity: velocityVm(s.velocity) } : null;
|
|
67
|
+
|
|
68
|
+
const projectVm = (p) =>
|
|
69
|
+
p
|
|
70
|
+
? {
|
|
71
|
+
dir: p.dir,
|
|
72
|
+
deployed: p.deployed,
|
|
73
|
+
docsAi: p.docsAi,
|
|
74
|
+
deployStamps: (p.deployStamps ?? []).map((st) => ({ display: st.display, version: st.version ?? null })),
|
|
75
|
+
visibility: visibilityVm(p.visibility),
|
|
76
|
+
settings: settingsVm(p.settings),
|
|
77
|
+
}
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
export const toViewModel = (envelope = {}) => {
|
|
81
|
+
const members = (envelope.installed ?? []).map(memberVm);
|
|
82
|
+
return {
|
|
83
|
+
deploymentHead: envelope.deploymentHead ?? null,
|
|
84
|
+
members,
|
|
85
|
+
headline: { total: members.length, behind: members.filter((m) => m.behind).length },
|
|
86
|
+
bridges: envelope.bridges ? envelope.bridges.map(bridgeVm) : null,
|
|
87
|
+
project: projectVm(envelope.project),
|
|
88
|
+
};
|
|
89
|
+
};
|