@sabaiway/agent-workflow-kit 1.41.0 → 1.43.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 +62 -0
- package/README.md +4 -3
- package/SKILL.md +9 -5
- package/capability.json +1 -1
- package/migrations/README.md +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +4 -3
- package/references/modes/gates.md +1 -1
- package/references/modes/hook.md +1 -1
- package/references/modes/migrate-adr-store.md +17 -0
- package/references/modes/status.md +1 -1
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +1 -1
- package/references/scripts/archive-decisions.mjs +705 -320
- package/references/scripts/archive-decisions.test.mjs +652 -312
- package/references/scripts/check-docs-size.mjs +35 -5
- package/references/scripts/check-docs-size.test.mjs +55 -0
- package/references/templates/adr/log.md +25 -0
- package/references/templates/adr-record.md +72 -0
- package/references/templates/decisions.md +7 -18
- package/tools/atomic-write.mjs +13 -0
- package/tools/cheap-agents.mjs +1 -1
- package/tools/commands.mjs +8 -1
- package/tools/family-registry.mjs +26 -3
- package/tools/migrate-adr-store.mjs +271 -0
- package/tools/renderers.mjs +4 -0
- package/tools/seed-gates.mjs +160 -55
- package/tools/velocity-profile.mjs +1 -1
- package/tools/view-model.mjs +1 -0
package/tools/renderers.mjs
CHANGED
|
@@ -77,6 +77,10 @@ const renderProject = (vm, { color }) => {
|
|
|
77
77
|
}
|
|
78
78
|
for (const s of p.deployStamps) lines.push(` ${pad(s.display, STAMP_COL)}${s.version ?? '—'}`);
|
|
79
79
|
lines.push(` ${pad('docs/ai present', STAMP_COL)}${p.docsAi ? 'yes' : 'no'}`);
|
|
80
|
+
// Only the actionable 'old' layout renders a line — a migrated/none store needs no note (AD-051).
|
|
81
|
+
if (p.adrLayout === 'old') {
|
|
82
|
+
lines.push(` ${pad('ADR store', STAMP_COL)}old layout — run /agent-workflow-kit migrate-adr-store`);
|
|
83
|
+
}
|
|
80
84
|
if (p.visibility) {
|
|
81
85
|
const v = p.visibility.error ? `error: ${p.visibility.error}` : p.visibility.phrase;
|
|
82
86
|
lines.push(` ${pad('visibility', STAMP_COL)}${v}`);
|
package/tools/seed-gates.mjs
CHANGED
|
@@ -6,14 +6,28 @@
|
|
|
6
6
|
// EMPTY (AD-021/AD-038): a populated declaration is per-entry maintainer consent recorded through
|
|
7
7
|
// this preview, never auto-seeding.
|
|
8
8
|
//
|
|
9
|
-
// What it offers (the derivation invariants, test-pinned):
|
|
9
|
+
// What it offers (the derivation invariants, test-pinned) — CLOSED-WORLD since AD-052: an entry
|
|
10
|
+
// is offered only when every axis is proven safe by MEMBERSHIP in a finite, test-guarded set,
|
|
11
|
+
// never by absence from a blocklist (Issue-011 killed the blocklist model — one more gap per
|
|
12
|
+
// review round, unprovable):
|
|
10
13
|
// • sources: discoverGateCandidates over package.json scripts (velocity-profile.mjs stays
|
|
11
14
|
// read-only; THIS module owns the offer mapping) + the conditional review-state candidate;
|
|
12
|
-
// • warn-flagged candidates (release/publish/
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// never
|
|
16
|
-
// •
|
|
15
|
+
// • NAME screens shape WHICH entries are offered: warn-flagged candidates (release/publish/
|
|
16
|
+
// deploy/push/version/commit/tag, pre*/post*) never enter; only terminating verification
|
|
17
|
+
// class NAMES (test / lint / type-check / build) pass, never dev/start/watch/serve/preview,
|
|
18
|
+
// never a mutating-variant name; a shell-unsafe name is screened first;
|
|
19
|
+
// • the BODY must be a string member of the literal BODY_ALLOWLIST after a pinned ASCII-only
|
|
20
|
+
// normalization — anything else is screened out with a LOUD note (never silently absent);
|
|
21
|
+
// • the cmd is the per-PM HOOK-FREE form `COREPACK_ENABLE_NETWORK=0 <pm> exec -- <allowlisted-body>`
|
|
22
|
+
// (packageManager field, else lockfile probe, else npm): `exec` runs a command, not a named
|
|
23
|
+
// script, so the pre/post lifecycle-hook class dies structurally on npm/pnpm/yarn alike, and
|
|
24
|
+
// the Corepack env prefix blocks a hostile packageManager pin from fetching the PM binary; a
|
|
25
|
+
// family with no verified fail-closed exec contract is WITHHELD with a loud note. NEVER
|
|
26
|
+
// `<pm> run <name>` — that re-exposes hooks and lets a later package.json edit change a
|
|
27
|
+
// byte-exact-approved gate;
|
|
28
|
+
// • the safety claim is scoped to the OFFER DERIVATION, not a runtime sandbox: a gate still
|
|
29
|
+
// executes project-controlled tooling (a node_modules/.bin PATH shim intercepts under every
|
|
30
|
+
// form) — the documented residual, disclosed in the preview, bounded by the two consents;
|
|
17
31
|
// • ids derive kebab-case from script names (build:prod → build-prod) and every offered entry
|
|
18
32
|
// passes the runner's validateDeclaration (the seeder imports the validator — NEVER the
|
|
19
33
|
// reverse: run-gates.mjs stays a runner that writes nothing);
|
|
@@ -62,33 +76,35 @@ const usageFail = (message) =>
|
|
|
62
76
|
export const TRUST_CHAIN_DISCLOSURE =
|
|
63
77
|
'Disclosure: once the optional approval hook is wired (/agent-workflow-kit hook), it ' +
|
|
64
78
|
'auto-approves byte-exact declared gate commands from the project root — seeding (this consent) ' +
|
|
65
|
-
'and hook wiring (its own consent) are two separate yeses.'
|
|
79
|
+
'and hook wiring (its own consent) are two separate yeses. A script gate runs the project\'s ' +
|
|
80
|
+
'own tooling, which executes project-controlled code (the seeder does not sandbox it; the two ' +
|
|
81
|
+
'consents bound it).';
|
|
66
82
|
|
|
67
83
|
const USAGE = `usage: seed-gates [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
|
|
68
84
|
|
|
69
85
|
Consent-gated seeder for the project's own docs/ai/gates.json. Default is --dry-run: prints the
|
|
70
86
|
derived { id, title, cmd } entries and writes NOTHING. --apply APPENDS exactly the consented
|
|
71
87
|
entries (--only <id> selects a subset; append-only — existing entries are never modified or
|
|
72
|
-
removed, an id collision is refused).
|
|
73
|
-
(test / lint / type-check / build
|
|
74
|
-
|
|
88
|
+
removed, an id collision is refused). The offer is CLOSED-WORLD: only a terminating-class script
|
|
89
|
+
name (test / lint / type-check / build — never release/publish/deploy, never watch/serve, never a
|
|
90
|
+
write-mode variant) whose BODY is a member of the literal runner allowlist is offered, as the
|
|
91
|
+
hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for the detected package manager. A gate-class script whose
|
|
92
|
+
body is NOT in the allowlist is screened out with a note naming it (a command you trust can still
|
|
93
|
+
be declared by hand); non-gate-class names are excluded silently, as always.
|
|
94
|
+
${TRUST_CHAIN_DISCLOSURE}`;
|
|
75
95
|
|
|
76
|
-
// ── candidate classification (the LOCKED derivation invariants)
|
|
77
|
-
//
|
|
96
|
+
// ── candidate classification: the NAME screens (the LOCKED derivation invariants) ──────
|
|
97
|
+
// NAME screens shape WHICH safe entries are offered; the danger axis (the BODY) is closed-world
|
|
98
|
+
// below (AD-052). The derived cmd is bash-interpolated by the gate runner and can become
|
|
78
99
|
// hook-auto-approvable, so only shell-SAFE script names ever enter the offer: a name carrying
|
|
79
100
|
// whitespace or any shell metacharacter (`test:ci && echo pwn`) is screened out entirely.
|
|
80
101
|
const SAFE_SCRIPT_NAME_PATTERN = /^[A-Za-z0-9:_.-]+$/;
|
|
81
102
|
const TERMINATING_CLASS_PATTERN = /^(test|lint|type-?check|types|tsc|build)([:._-]|$)/i;
|
|
82
|
-
// Non-terminating screening is TOKEN-set based, position-independent (never an anchored
|
|
83
|
-
// the anchored form missed `build:preview`
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// can still be declared by hand; a wrongly-included one would become hook-auto-approvable.
|
|
103
|
+
// Non-terminating NAME screening is TOKEN-set based, position-independent (never an anchored
|
|
104
|
+
// regex — the anchored form missed `build:preview`): a non-terminating token in ANY name segment
|
|
105
|
+
// disqualifies; `watch` disqualifies as a substring anywhere (watchAll). Conservative on purpose —
|
|
106
|
+
// a screened-out script can still be declared by hand.
|
|
87
107
|
const NON_TERMINATING_TOKENS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
|
|
88
|
-
// A terminating-looking NAME can still hide release work in its BODY (`"test": "npm publish"`):
|
|
89
|
-
// the same token classes the warn-name screen rejects are rejected as bare body words too — an
|
|
90
|
-
// offered cmd is hook-auto-approvable, so a dangerous body must never ride a clean name.
|
|
91
|
-
const DANGEROUS_BODY_TOKENS = new Set(['release', 'publish', 'deploy', 'push', 'version', 'commit', 'tag']);
|
|
92
108
|
const WATCH_ANYWHERE_PATTERN = /watch/i;
|
|
93
109
|
const wordOf = (raw) => raw.toLowerCase().replace(/^-+/, '').split('=')[0];
|
|
94
110
|
const hasTokenIn = (text, splitter, tokens) =>
|
|
@@ -97,16 +113,82 @@ const hasTokenIn = (text, splitter, tokens) =>
|
|
|
97
113
|
.some((part) => tokens.has(wordOf(part)));
|
|
98
114
|
const isNonTerminatingName = (name) =>
|
|
99
115
|
hasTokenIn(name, /[:._-]/, NON_TERMINATING_TOKENS) || WATCH_ANYWHERE_PATTERN.test(name);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// A MUTATING VARIANT of a terminating class never enters the offer — a hook-auto-approvable gate
|
|
104
|
-
// must never be a writer. Screened on BOTH axes: the script NAME (lint:fix, test:update,
|
|
105
|
-
// build:write, test:snapshot) and the script BODY's write-mode flags (eslint --fix,
|
|
106
|
-
// prettier --write / -w, jest -u / --updateSnapshot, tsc -w). Conservative by design: an excluded
|
|
107
|
-
// candidate can still be declared by hand — a wrongly-included one would be silently auto-approved.
|
|
116
|
+
// A MUTATING VARIANT NAME of a terminating class never enters the offer (lint:fix, test:update,
|
|
117
|
+
// build:write, test:snapshot) — a hook-auto-approvable gate must never be a writer. Write-mode
|
|
118
|
+
// BODIES need no screen: the closed-world allowlist simply has no write-mode member.
|
|
108
119
|
const MUTATING_VARIANT_NAME_PATTERN = /(^|[:._-])(fix|write|update|snapshot)([:._-]|$)/i;
|
|
109
|
-
|
|
120
|
+
|
|
121
|
+
// ── the CLOSED-WORLD body contract (AD-052 — the Issue-011 structural fix) ─────────────
|
|
122
|
+
// The body axis is ALLOWLIST MEMBERSHIP, never blocklist screening. The failure direction flips
|
|
123
|
+
// by construction: worst case = a legit command not offered (mild, add-by-hand) — never a
|
|
124
|
+
// dangerous one offered. Extension contract (edit-safety): adding an entry = adding the literal
|
|
125
|
+
// here + classifying its stem in the partition below + its own test case; the self-safety test
|
|
126
|
+
// pins every entry against shell metacharacters, write-mode flags, and unrecognized runner stems.
|
|
127
|
+
// Deliberately EXCLUDED: `tsc -p .` without --noEmit — it EMITS compiled .js into the tree by
|
|
128
|
+
// default, a write side-effect a verification gate must not have.
|
|
129
|
+
export const BODY_ALLOWLIST = Object.freeze([
|
|
130
|
+
'node --test',
|
|
131
|
+
'vitest run',
|
|
132
|
+
'jest',
|
|
133
|
+
'jest --ci',
|
|
134
|
+
'eslint .',
|
|
135
|
+
'prettier --check .',
|
|
136
|
+
'tsc --noEmit',
|
|
137
|
+
'tsc -p . --noEmit',
|
|
138
|
+
'vite build',
|
|
139
|
+
]);
|
|
140
|
+
// The FIXED executable-source partition of the allowlist stems (test-pinned, never a runtime
|
|
141
|
+
// probe): a host-runtime stem (`node`) is always on PATH and never a node_modules package — no
|
|
142
|
+
// local-bin resolution applies to it; a package-runner stem resolves from the project's own
|
|
143
|
+
// node_modules (or Berry .pnp.cjs) via `<pm> exec`, under the per-PM no-network-fetch rule below.
|
|
144
|
+
export const HOST_RUNTIME_STEMS = Object.freeze(['node']);
|
|
145
|
+
export const PACKAGE_RUNNER_STEMS = Object.freeze(['vitest', 'jest', 'eslint', 'prettier', 'tsc', 'vite']);
|
|
146
|
+
|
|
147
|
+
// Decision-3 order, test-pinned: (1) string-typed, else not offered; (2) reject ANY char outside
|
|
148
|
+
// printable ASCII + space/tab BEFORE trimming — String.trim() strips \n/NBSP/BOM at the EDGES and
|
|
149
|
+
// would mask a leading/trailing forbidden char; (3) trim + collapse ASCII space/tab runs to one
|
|
150
|
+
// space; (4) literal membership. No case folding, no arg reordering, no separate env/path reject
|
|
151
|
+
// axis — `FOO=bar node --test` and `./scripts/x.sh` are already rejected by NON-MEMBERSHIP.
|
|
152
|
+
const PRINTABLE_ASCII_BODY_PATTERN = /^[\x20-\x7E\t]*$/;
|
|
153
|
+
const allowlistedBodyOf = (body) => {
|
|
154
|
+
if (typeof body !== 'string') return null;
|
|
155
|
+
if (!PRINTABLE_ASCII_BODY_PATTERN.test(body)) return null;
|
|
156
|
+
const normalized = body.replace(/^[ \t]+|[ \t]+$/g, '').replace(/[ \t]+/g, ' ');
|
|
157
|
+
return BODY_ALLOWLIST.includes(normalized) ? normalized : null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// The per-PM HOOK-FREE exec form (Decision 1, host-proven npm 12 / pnpm 10 / yarn 1 + berry 4):
|
|
161
|
+
// `exec` runs a COMMAND, not a named script — no pre/post lifecycle exists to fire, uniformly.
|
|
162
|
+
// The `--` keeps a body flag (`jest --ci`) from being absorbed by the PM. Per-PM hardening — the
|
|
163
|
+
// pinned invariant is NO NETWORK FETCH of an absent runner, not "no user-machine fallback":
|
|
164
|
+
// npm — `--script-shell /bin/sh` beats a hostile per-project `.npmrc script-shell` (proven the
|
|
165
|
+
// flag wins) and `--offline` refuses an absent runner as a cache miss, never a registry
|
|
166
|
+
// fetch; a hit on a previously-installed real package (the npm cache OR the host's
|
|
167
|
+
// global tree — both user machine state, not project content) is the documented runtime
|
|
168
|
+
// residual (Decision 2 ii / 6), not a separate guard.
|
|
169
|
+
// pnpm — no network fetch; an absent runner fails closed when no project-local or
|
|
170
|
+
// PATH-resolvable runner exists (a user-installed one executing is the same
|
|
171
|
+
// user-machine-state residual). Reads no npm `.npmrc script-shell`.
|
|
172
|
+
// yarn — same contract, uniform for classic + berry (berry resolves via .pnp.cjs; proven).
|
|
173
|
+
// The `COREPACK_ENABLE_NETWORK=0` prefix closes ONE more attacker-reachable fetch axis: when the
|
|
174
|
+
// PM binary is a Corepack shim, a hostile `packageManager: "<pm>@<uncached-version>"` field makes
|
|
175
|
+
// Corepack DOWNLOAD that PM release from the registry BEFORE exec even resolves the runner. The env
|
|
176
|
+
// var disables that provisioning (proven: it fails closed "Network access disabled" on an uncached
|
|
177
|
+
// pin, and is inert when the PM is a real install or already provisioned) — so the no-network
|
|
178
|
+
// contract holds for the PM-provision step too, not just the runner. Applies to all three (Corepack
|
|
179
|
+
// can shim npm/pnpm/yarn alike). An UNKNOWN family has no verified fail-closed exec contract → cmd
|
|
180
|
+
// null (withheld, loud note) — the mild worst case, never a hole. detectPackageManager can only
|
|
181
|
+
// yield npm/pnpm/yarn today (bun collapses to npm — characterized), so this is the builder floor.
|
|
182
|
+
const COREPACK_NO_NETWORK = 'COREPACK_ENABLE_NETWORK=0';
|
|
183
|
+
export const execCmdFor = (pm, body) => {
|
|
184
|
+
if (pm === 'npm') return { cmd: `${COREPACK_NO_NETWORK} npm exec --offline --script-shell /bin/sh -- ${body}`, note: null };
|
|
185
|
+
if (pm === 'pnpm') return { cmd: `${COREPACK_NO_NETWORK} pnpm exec -- ${body}`, note: null };
|
|
186
|
+
if (pm === 'yarn') return { cmd: `${COREPACK_NO_NETWORK} yarn exec -- ${body}`, note: null };
|
|
187
|
+
return {
|
|
188
|
+
cmd: null,
|
|
189
|
+
note: `no fail-closed exec form is verified for this package manager (${pm}) — add the gate by hand`,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
110
192
|
|
|
111
193
|
export const kebabIdOf = (name) =>
|
|
112
194
|
String(name)
|
|
@@ -133,8 +215,11 @@ export const detectPackageManager = (cwd, deps = {}) => {
|
|
|
133
215
|
return 'npm';
|
|
134
216
|
};
|
|
135
217
|
|
|
136
|
-
// The script-derived offer entries
|
|
137
|
-
|
|
218
|
+
// The script-derived offer: entries + loud honesty notes (a gate-class script screened out on its
|
|
219
|
+
// body, or a whole family withheld, is COUNTED and NAMED — never silently absent). Order =
|
|
220
|
+
// package.json scripts order (the offer the user reads). `deps.packageManager` injects a family
|
|
221
|
+
// token past the detector for the builder-boundary fail-closed proof (T4b).
|
|
222
|
+
const deriveScripts = (cwd, deps = {}) => {
|
|
138
223
|
const read = deps.readFile ?? readFileSync;
|
|
139
224
|
const pkg = (() => {
|
|
140
225
|
try {
|
|
@@ -143,29 +228,48 @@ export const deriveScriptEntries = (cwd, deps = {}) => {
|
|
|
143
228
|
return null; // no/unreadable package.json → no script candidates (an honest empty offer)
|
|
144
229
|
}
|
|
145
230
|
})();
|
|
146
|
-
const pm = detectPackageManager(cwd, deps);
|
|
147
|
-
const
|
|
148
|
-
const bodyOf = (name) => String(pkg?.scripts?.[name] ?? '');
|
|
149
|
-
return discoverGateCandidates(pkg ?? {})
|
|
231
|
+
const pm = deps.packageManager ?? detectPackageManager(cwd, deps);
|
|
232
|
+
const named = discoverGateCandidates(pkg ?? {})
|
|
150
233
|
.filter((c) => !c.warn) // warn-flagged NEVER enter the offer
|
|
151
234
|
.filter((c) => SAFE_SCRIPT_NAME_PATTERN.test(c.scriptName)) // shell-safe names only, FIRST
|
|
152
235
|
.filter((c) => TERMINATING_CLASS_PATTERN.test(c.scriptName))
|
|
153
236
|
.filter((c) => !isNonTerminatingName(c.scriptName))
|
|
154
|
-
.filter((c) => !MUTATING_VARIANT_NAME_PATTERN.test(c.scriptName)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
.filter((c) => !MUTATING_VARIANT_NAME_PATTERN.test(c.scriptName));
|
|
238
|
+
const seen = new Set(); // first occurrence of a derived id wins, whatever its outcome (conservative)
|
|
239
|
+
const entries = [];
|
|
240
|
+
const screenedIds = [];
|
|
241
|
+
const withheld = [];
|
|
242
|
+
for (const c of named) {
|
|
243
|
+
const id = kebabIdOf(c.scriptName);
|
|
244
|
+
if (!id || seen.has(id)) continue; // an empty or duplicate derived id never enters the offer
|
|
245
|
+
seen.add(id);
|
|
246
|
+
const body = allowlistedBodyOf(pkg?.scripts?.[c.scriptName]);
|
|
247
|
+
if (body === null) {
|
|
248
|
+
screenedIds.push(id);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const { cmd, note } = execCmdFor(pm, body);
|
|
252
|
+
if (cmd === null) {
|
|
253
|
+
withheld.push({ id, note });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
entries.push({ id, title: `Project script ${c.scriptName}: ${body}`, cmd });
|
|
257
|
+
}
|
|
258
|
+
const notes = [];
|
|
259
|
+
if (screenedIds.length) {
|
|
260
|
+
notes.push(
|
|
261
|
+
`${screenedIds.length} gate-class script(s) screened out — body not in the closed-world ` +
|
|
262
|
+
`allowlist: ${screenedIds.join(', ')} — a command you trust can still be declared by hand in ${GATES_REL}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (withheld.length) {
|
|
266
|
+
notes.push(`${withheld.length} script gate(s) withheld: ${withheld.map((w) => w.id).join(', ')} — ${withheld[0].note}`);
|
|
267
|
+
}
|
|
268
|
+
return { entries, notes };
|
|
167
269
|
};
|
|
168
270
|
|
|
271
|
+
export const deriveScriptEntries = (cwd, deps = {}) => deriveScripts(cwd, deps).entries;
|
|
272
|
+
|
|
169
273
|
// The conditional review-state candidate — keyed on the SLOT the checker enforces
|
|
170
274
|
// (plan-execution.review, tools/review-state.mjs), read via the shared config reader. Offered only
|
|
171
275
|
// when the config DECLARES reviewed/council there; solo configs and a council-on-plan-authoring-only
|
|
@@ -256,14 +360,13 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
|
|
|
256
360
|
// plus loud notes. Both review candidates key on the same slot (plan-execution.review reviewed/council)
|
|
257
361
|
// but gate distinct axes (receipt presence vs review-round convergence) — offered together.
|
|
258
362
|
export const buildOffer = (cwd, deps = {}) => {
|
|
259
|
-
const
|
|
363
|
+
const scripts = deriveScripts(cwd, deps);
|
|
260
364
|
const rs = reviewStateCandidate(cwd, deps);
|
|
261
365
|
const rl = reviewLedgerCandidate(cwd, deps);
|
|
262
366
|
const candidates = [rs.candidate, rl.candidate].filter(Boolean);
|
|
263
|
-
const notes = [rs.note, rl.note].filter(Boolean);
|
|
264
367
|
return {
|
|
265
|
-
entries: [...entries, ...candidates],
|
|
266
|
-
notes,
|
|
368
|
+
entries: [...scripts.entries, ...candidates],
|
|
369
|
+
notes: [...scripts.notes, rs.note, rl.note].filter(Boolean),
|
|
267
370
|
};
|
|
268
371
|
};
|
|
269
372
|
|
|
@@ -361,7 +464,7 @@ export const applySeed = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
361
464
|
}
|
|
362
465
|
const offer = buildOffer(cwd, deps);
|
|
363
466
|
assertOnlyIdsOffered(offer, onlyIds); // BEFORE the empty-offer return — a typo is never masked
|
|
364
|
-
if (!offer.entries.length) return { outcome: 'nothing' };
|
|
467
|
+
if (!offer.entries.length) return { outcome: 'nothing', notes: offer.notes };
|
|
365
468
|
const selected = onlyIds.length ? offer.entries.filter((e) => onlyIds.includes(e.id)) : offer.entries;
|
|
366
469
|
|
|
367
470
|
const existing = loadExistingDeclaration(cwd, deps);
|
|
@@ -382,7 +485,7 @@ export const applySeed = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
382
485
|
validateDeclaration(merged); // every written declaration passes the runner's validator, always
|
|
383
486
|
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
384
487
|
const { writtenPath } = writeDocsAiFileAtomic(cwd, GATES_REL, body, deps, { stop, noun: 'a gate declaration' });
|
|
385
|
-
return { outcome: 'written', writtenPath, appended: selected.map((e) => e.id) };
|
|
488
|
+
return { outcome: 'written', writtenPath, appended: selected.map((e) => e.id), notes: offer.notes };
|
|
386
489
|
};
|
|
387
490
|
|
|
388
491
|
// ── CLI ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -439,9 +542,11 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
439
542
|
const result = applySeed({ cwd, onlyIds: args.only }, deps);
|
|
440
543
|
if (result.outcome === 'nothing') {
|
|
441
544
|
log('[agent-workflow-kit] nothing to offer — no seedable terminating verification scripts were found; wrote nothing.');
|
|
545
|
+
for (const note of result.notes) log(` note: ${note}`); // the user learns WHY (same note as the preview)
|
|
442
546
|
return EXIT_OK;
|
|
443
547
|
}
|
|
444
548
|
log(`[agent-workflow-kit] appended ${result.appended.length} consented gate(s) to ${GATES_REL}: ${result.appended.join(', ')}`);
|
|
549
|
+
for (const note of result.notes) log(` note: ${note}`); // a mixed offer never silently omits what was screened
|
|
445
550
|
log(`[agent-workflow-kit] ${TRUST_CHAIN_DISCLOSURE}`);
|
|
446
551
|
return EXIT_OK;
|
|
447
552
|
} catch (err) {
|
|
@@ -15,7 +15,7 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, COMMAND_REDLINES } from '.
|
|
|
15
15
|
|
|
16
16
|
// Deployment-lineage head this velocity build targets; bump together with agent-workflow-memory
|
|
17
17
|
// LINEAGE_HEAD when the deployed docs/ai structure changes.
|
|
18
|
-
export const EXPECTED_WORKFLOW_VERSION = '
|
|
18
|
+
export const EXPECTED_WORKFLOW_VERSION = '2.0.0';
|
|
19
19
|
export const SETTINGS_FILE = '.claude/settings.json';
|
|
20
20
|
export const SETTINGS_LOCAL_FILE = '.claude/settings.local.json';
|
|
21
21
|
export const CLAUDE_DIR = '.claude';
|
package/tools/view-model.mjs
CHANGED
|
@@ -111,6 +111,7 @@ const projectVm = (p) =>
|
|
|
111
111
|
dir: p.dir,
|
|
112
112
|
deployed: p.deployed,
|
|
113
113
|
docsAi: p.docsAi,
|
|
114
|
+
adrLayout: p.adrLayout ?? null,
|
|
114
115
|
deployStamps: (p.deployStamps ?? []).map((st) => ({ display: st.display, version: st.version ?? null })),
|
|
115
116
|
visibility: visibilityVm(p.visibility),
|
|
116
117
|
settings: settingsVm(p.settings),
|