@event4u/agent-config 9.34.0 → 9.36.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +64 -77
- package/CONTRIBUTING.md +23 -14
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/commands/roadmap/next.md +37 -5
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/dist/cli/registry.js +2 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
- package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-JP53LNVT.js} +12 -0
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-NR7HSN3P.js} +2 -2
- package/dist/cli-delegate/{chunk-REMEZSVP.js → chunk-SFGARMAB.js} +13 -3
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-U7CMAIOC.js} +7 -2
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +3 -3
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +8 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +3 -3
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +3 -3
- package/dist/cli-delegate/cmd_uninstall.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +789 -401
- package/dist/install/install.mjs +21 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +99 -7
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +5 -2
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/scripts/roadmap_gates.ts +119 -4
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
- package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
- package/src/config/agent-settings.template.yml +43 -2
- package/src/config/gate-coverage.yml +12 -0
- package/src/config/quorum-attendance-budget.json +10 -2
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_dispatch.bash +6 -2
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/session_register.ts +67 -1
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +60 -0
- package/src/scripts/ai_council/events_log.ts +103 -4
- package/src/scripts/ai_council/quorum.ts +83 -6
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/council_cli.ts +36 -1
- package/src/scripts/hook_manifest.yaml +18 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_framework_leakage.ts +195 -28
- package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/memory_lookup.ts +1 -2
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/session_register_hook.ts +238 -18
- package/src/scripts/sessions_cli.ts +377 -12
- package/src/server/schemas/settings.ts +12 -0
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staged-confirmation store — the I/O half, and the half that makes
|
|
3
|
+
* exactly-once real.
|
|
4
|
+
*
|
|
5
|
+
* `staged_confirmation.ts` decides what a confirmation attempt MEANS. It cannot
|
|
6
|
+
* make the transition happen only once: two processes that both read a
|
|
7
|
+
* `pending` record and both conclude `execute` is a check-then-act race, and no
|
|
8
|
+
* pure function closes it. So the claim is a **directory rename**, which the
|
|
9
|
+
* filesystem performs atomically — the first caller moves
|
|
10
|
+
* `pending/<token>.json` to `resolved/`, every later caller gets `ENOENT` and
|
|
11
|
+
* learns the stage is already taken.
|
|
12
|
+
*
|
|
13
|
+
* That is also why the state lives in the path and not only in a field: a
|
|
14
|
+
* read-modify-write of a `state:` key would reopen exactly the window the
|
|
15
|
+
* rename closes.
|
|
16
|
+
*
|
|
17
|
+
* Records live under `agents/runtime/staged-confirmations/`, which the repo
|
|
18
|
+
* `.gitignore` already covers via the `/agents/runtime/` catch-all — a staged
|
|
19
|
+
* action is machine-generated local state, never a tracked artefact. The store
|
|
20
|
+
* mirrors `_lib/self_repair_store.ts`: one file per identity, every read
|
|
21
|
+
* tolerant of a malformed file, and no throw on a missing directory.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
confirmOnce,
|
|
28
|
+
declineStage,
|
|
29
|
+
type ConfirmOutcome,
|
|
30
|
+
type StageState,
|
|
31
|
+
type StagedAction,
|
|
32
|
+
stageStatus,
|
|
33
|
+
} from './staged_confirmation.js';
|
|
34
|
+
|
|
35
|
+
export const STORE_REL = path.join('agents', 'runtime', 'staged-confirmations');
|
|
36
|
+
|
|
37
|
+
export function storeDir(root: string): string {
|
|
38
|
+
return path.join(root, STORE_REL);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pendingDir(root: string): string {
|
|
42
|
+
return path.join(storeDir(root), 'pending');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolvedDir(root: string): string {
|
|
46
|
+
return path.join(storeDir(root), 'resolved');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A token is a filename before it is an identifier, so every site that turns
|
|
51
|
+
* one into a path checks it first — the read verbs, the write verb, and the
|
|
52
|
+
* record guard itself.
|
|
53
|
+
*
|
|
54
|
+
* `deriveToken` returns a 16-char sha256 prefix, so a token this module mints
|
|
55
|
+
* is always safe. Everything else is a way in: `readPending`,
|
|
56
|
+
* `claimConfirmation` and `declineConfirmation` take a caller-supplied token;
|
|
57
|
+
* `putPending` joins `stage.token` from a caller-built record (verified: it
|
|
58
|
+
* wrote outside the store root); and `isStage` accepted any string, so a record
|
|
59
|
+
* written by something else carried its token into the prune path. All four are
|
|
60
|
+
* guarded, and `path.join(pendingDir(root), '../x.json')` resolving outside
|
|
61
|
+
* `pending/` is what makes that necessary.
|
|
62
|
+
*/
|
|
63
|
+
const _TOKEN_RE = /^[0-9a-f]{8,64}$/;
|
|
64
|
+
|
|
65
|
+
export function isSafeToken(token: string): boolean {
|
|
66
|
+
return _TOKEN_RE.test(token);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every field the module later reads, `staged_at` included.
|
|
71
|
+
*
|
|
72
|
+
* It checked four of five while still asserting `v is StagedAction`, so a
|
|
73
|
+
* record that was valid JSON but carried no `staged_at` passed the guard and
|
|
74
|
+
* reached `listPending`'s `staged_at.localeCompare(...)`: a TypeError thrown
|
|
75
|
+
* through `roadmap_gates.renderPending`, i.e. one malformed file on disk taking
|
|
76
|
+
* out a shipped gate. An unsound guard is worse than no guard — it makes the
|
|
77
|
+
* caller's `!== null` check read as a proof that it is not.
|
|
78
|
+
*/
|
|
79
|
+
function isStage(v: unknown): v is StagedAction {
|
|
80
|
+
if (typeof v !== 'object' || v === null) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const s = v as Partial<StagedAction>;
|
|
84
|
+
return (
|
|
85
|
+
typeof s.token === 'string' &&
|
|
86
|
+
isSafeToken(s.token) &&
|
|
87
|
+
typeof s.action === 'string' &&
|
|
88
|
+
typeof s.object === 'string' &&
|
|
89
|
+
typeof s.staged_at === 'string' &&
|
|
90
|
+
typeof s.expires_at === 'string'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStageFile(file: string): StagedAction | null {
|
|
95
|
+
try {
|
|
96
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
97
|
+
return isStage(parsed) ? parsed : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeStageFile(file: string, stage: StagedAction): void {
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
fs.writeFileSync(file, `${JSON.stringify(stage, null, 2)}\n`, 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Persist a freshly staged action. Returns the path written.
|
|
110
|
+
*
|
|
111
|
+
* Throws on an unsafe token rather than returning: a caller reaching here with
|
|
112
|
+
* one built the record itself and has a bug, and `writeStageFile` creates
|
|
113
|
+
* directories, so a silent refusal would look like a successful stage nothing
|
|
114
|
+
* can ever confirm.
|
|
115
|
+
*/
|
|
116
|
+
export function putPending(root: string, stage: StagedAction): string {
|
|
117
|
+
if (!isSafeToken(stage.token)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`staged-confirmation: refusing to write under an unsafe token '${stage.token}' — ` +
|
|
120
|
+
'a token is a filename and must match the deriveToken shape',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const file = path.join(pendingDir(root), `${stage.token}.json`);
|
|
124
|
+
writeStageFile(file, stage);
|
|
125
|
+
return file;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function readPending(root: string, token: string): StagedAction | null {
|
|
129
|
+
if (!isSafeToken(token)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return readStageFile(path.join(pendingDir(root), `${token}.json`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function readResolved(root: string, token: string): StagedAction | null {
|
|
136
|
+
if (!isSafeToken(token)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return readStageFile(path.join(resolvedDir(root), `${token}.json`));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every stage still in `pending/`, with its effective status at `now` so an
|
|
144
|
+
* expired one is never rendered as awaiting approval. Sorted by `staged_at`
|
|
145
|
+
* (oldest first, ties by token) so two runs on an unchanged store print
|
|
146
|
+
* byte-identical output.
|
|
147
|
+
*/
|
|
148
|
+
export function listPending(
|
|
149
|
+
root: string,
|
|
150
|
+
now: number,
|
|
151
|
+
): { stage: StagedAction; status: StageState | 'expired' }[] {
|
|
152
|
+
let names: string[];
|
|
153
|
+
try {
|
|
154
|
+
names = fs.readdirSync(pendingDir(root));
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const out: { stage: StagedAction; status: StageState | 'expired' }[] = [];
|
|
159
|
+
for (const n of names.sort()) {
|
|
160
|
+
if (!n.endsWith('.json')) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const stage = readStageFile(path.join(pendingDir(root), n));
|
|
164
|
+
if (stage !== null) {
|
|
165
|
+
out.push({ stage, status: stageStatus(stage, now) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
out.sort(
|
|
169
|
+
(a, b) =>
|
|
170
|
+
a.stage.staged_at.localeCompare(b.stage.staged_at) ||
|
|
171
|
+
a.stage.token.localeCompare(b.stage.token),
|
|
172
|
+
);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Atomically claim a pending stage and decide the outcome.
|
|
178
|
+
*
|
|
179
|
+
* Order matters and is the whole mechanism:
|
|
180
|
+
*
|
|
181
|
+
* 1. Read the record. An absent one is `token-mismatch` — from the caller's
|
|
182
|
+
* side an unknown token and a wrong token are the same fact, and inventing
|
|
183
|
+
* a third outcome for "was never staged" would only be actionable if the
|
|
184
|
+
* store were a public index, which it is not.
|
|
185
|
+
* 2. Ask `confirmOnce` what the outcome WOULD be. A mismatch, a decline, or
|
|
186
|
+
* an expiry never claims anything — a stale surface must not consume a
|
|
187
|
+
* stage a human might still legitimately confirm.
|
|
188
|
+
* 3. Only for `execute`: rename `pending/` → `resolved/`. That is the claim.
|
|
189
|
+
* `ENOENT` means a concurrent caller won the race, so this caller reports
|
|
190
|
+
* `already-confirmed` and does NOT act — the exactly-once guarantee.
|
|
191
|
+
* 4. Write the confirmed record into `resolved/`, overwriting the moved file
|
|
192
|
+
* with the same content plus `state`/`resolved_at`.
|
|
193
|
+
*/
|
|
194
|
+
export function claimConfirmation(
|
|
195
|
+
root: string,
|
|
196
|
+
token: string,
|
|
197
|
+
now: number,
|
|
198
|
+
): { outcome: ConfirmOutcome; stage: StagedAction | null } {
|
|
199
|
+
if (!isSafeToken(token)) {
|
|
200
|
+
// Nothing this module staged can have that shape, so the honest answer
|
|
201
|
+
// is the same one an unknown token gets — and refusing here is what
|
|
202
|
+
// keeps the rename below off an arbitrary path.
|
|
203
|
+
return { outcome: 'token-mismatch', stage: null };
|
|
204
|
+
}
|
|
205
|
+
const pendingFile = path.join(pendingDir(root), `${token}.json`);
|
|
206
|
+
const stage = readStageFile(pendingFile);
|
|
207
|
+
if (stage === null) {
|
|
208
|
+
const already = readResolved(root, token);
|
|
209
|
+
if (already !== null) {
|
|
210
|
+
// `pruneExpired` moves a stage here WITHOUT rewriting `state`, so the
|
|
211
|
+
// status is derived rather than read off the field — otherwise an
|
|
212
|
+
// expired stage swept out of `pending/` would report as confirmed,
|
|
213
|
+
// which is the one wrong answer available here.
|
|
214
|
+
const status = stageStatus(already, now);
|
|
215
|
+
const outcome: ConfirmOutcome =
|
|
216
|
+
status === 'expired' ? 'expired' : status === 'declined' ? 'declined' : 'already-confirmed';
|
|
217
|
+
return { outcome, stage: already };
|
|
218
|
+
}
|
|
219
|
+
return { outcome: 'token-mismatch', stage: null };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const decided = confirmOnce(stage, token, now);
|
|
223
|
+
if (decided.outcome !== 'execute') {
|
|
224
|
+
return decided;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const resolvedFile = path.join(resolvedDir(root), `${token}.json`);
|
|
228
|
+
fs.mkdirSync(resolvedDir(root), { recursive: true });
|
|
229
|
+
try {
|
|
230
|
+
fs.renameSync(pendingFile, resolvedFile);
|
|
231
|
+
} catch {
|
|
232
|
+
// Lost the race, or the pending file vanished between the read and the
|
|
233
|
+
// rename. Either way this caller has NOT claimed the stage and must not
|
|
234
|
+
// act on it.
|
|
235
|
+
return { outcome: 'already-confirmed', stage: readResolved(root, token) ?? stage };
|
|
236
|
+
}
|
|
237
|
+
writeStageFile(resolvedFile, decided.stage);
|
|
238
|
+
return decided;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Decline a pending stage. Same claim discipline as `claimConfirmation`. */
|
|
242
|
+
export function declineConfirmation(
|
|
243
|
+
root: string,
|
|
244
|
+
token: string,
|
|
245
|
+
now: number,
|
|
246
|
+
): { declined: boolean; stage: StagedAction | null } {
|
|
247
|
+
if (!isSafeToken(token)) {
|
|
248
|
+
return { declined: false, stage: null };
|
|
249
|
+
}
|
|
250
|
+
const pendingFile = path.join(pendingDir(root), `${token}.json`);
|
|
251
|
+
const stage = readStageFile(pendingFile);
|
|
252
|
+
if (stage === null) {
|
|
253
|
+
return { declined: false, stage: readResolved(root, token) };
|
|
254
|
+
}
|
|
255
|
+
const decided = declineStage(stage, token, now);
|
|
256
|
+
if (!decided.declined) {
|
|
257
|
+
return { declined: false, stage };
|
|
258
|
+
}
|
|
259
|
+
const resolvedFile = path.join(resolvedDir(root), `${token}.json`);
|
|
260
|
+
fs.mkdirSync(resolvedDir(root), { recursive: true });
|
|
261
|
+
try {
|
|
262
|
+
fs.renameSync(pendingFile, resolvedFile);
|
|
263
|
+
} catch {
|
|
264
|
+
return { declined: false, stage: readResolved(root, token) ?? stage };
|
|
265
|
+
}
|
|
266
|
+
writeStageFile(resolvedFile, decided.stage);
|
|
267
|
+
return { declined: true, stage: decided.stage };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Move expired stages out of `pending/` so the enumeration stays a list of
|
|
272
|
+
* live decisions. Housekeeping only — `stageStatus` already derives expiry, so
|
|
273
|
+
* skipping the sweep changes what is *listed*, never what may execute.
|
|
274
|
+
*/
|
|
275
|
+
export function pruneExpired(root: string, now: number): number {
|
|
276
|
+
let names: string[];
|
|
277
|
+
try {
|
|
278
|
+
names = fs.readdirSync(pendingDir(root));
|
|
279
|
+
} catch {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
let moved = 0;
|
|
283
|
+
for (const name of names.sort()) {
|
|
284
|
+
if (!name.endsWith('.json')) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Move the file that was ENUMERATED, never a path rebuilt from the
|
|
288
|
+
// record's `token` field. The two disagree whenever a filename and its
|
|
289
|
+
// token do — a record written by anything but `putPending` — and the
|
|
290
|
+
// rebuilt path then hits ENOENT, gets swallowed by the catch below, and
|
|
291
|
+
// the stage sits in `pending/` forever while `moved` under-reports it.
|
|
292
|
+
const from = path.join(pendingDir(root), name);
|
|
293
|
+
const stage = readStageFile(from);
|
|
294
|
+
if (stage === null || stageStatus(stage, now) !== 'expired') {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const to = path.join(resolvedDir(root), name);
|
|
298
|
+
fs.mkdirSync(resolvedDir(root), { recursive: true });
|
|
299
|
+
try {
|
|
300
|
+
fs.renameSync(from, to);
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
moved += 1;
|
|
305
|
+
}
|
|
306
|
+
return moved;
|
|
307
|
+
}
|
|
@@ -355,6 +355,24 @@ project:
|
|
|
355
355
|
# Branch prefix for improvement PRs
|
|
356
356
|
improvement_pr_branch_prefix: "improve/agent-"
|
|
357
357
|
|
|
358
|
+
# Who this project is built for. Read by the demand gate
|
|
359
|
+
# (docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md
|
|
360
|
+
# § 8-pre), whose L0-L4 ladder measures MARKET demand and is therefore
|
|
361
|
+
# meaningless where no market is intended.
|
|
362
|
+
# self -> a tool its maintainer builds for themselves. The demand gate
|
|
363
|
+
# is inert; work is classified L-self (build), never deferred
|
|
364
|
+
# for lack of a user population that is not wanted.
|
|
365
|
+
# internal -> a team / company internal tool. Only "what breaks without
|
|
366
|
+
# it?" survives; the requester is known.
|
|
367
|
+
# client -> built for a named client. The client is the requester, not a
|
|
368
|
+
# market segment.
|
|
369
|
+
# public -> a product with an intended market. Full three-question gate
|
|
370
|
+
# and the full ladder.
|
|
371
|
+
# Default `public` = the behaviour every install had before this key existed.
|
|
372
|
+
# Absent resolves to `public` for the same reason: this package has no
|
|
373
|
+
# defaults layer, so absent means nobody said.
|
|
374
|
+
audience: public
|
|
375
|
+
|
|
358
376
|
# --- GitHub integration ---
|
|
359
377
|
github:
|
|
360
378
|
# API method for replying to PR review comments
|
|
@@ -1207,9 +1225,17 @@ hooks:
|
|
|
1207
1225
|
# language — detector B: the user-visible prose is not in the pinned
|
|
1208
1226
|
# language, with code, quotes, paths and identifiers already
|
|
1209
1227
|
# excluded (19 measured).
|
|
1228
|
+
# verification — detector C: the turn changed a file and then ran nothing that
|
|
1229
|
+
# could have checked it — no test, type-check, lint or build call
|
|
1230
|
+
# follows the LAST edit. Ordering is the point: a run before the
|
|
1231
|
+
# final edit did not exercise it, which is the freshness clause
|
|
1232
|
+
# verify-before-complete states. Reads the turn's tool calls, not
|
|
1233
|
+
# the prose. Deliberately narrow on what counts as verification —
|
|
1234
|
+
# a missed detection costs one unguarded turn, a false refusal
|
|
1235
|
+
# teaches you to switch the gate off.
|
|
1210
1236
|
#
|
|
1211
|
-
#
|
|
1212
|
-
# one line, and
|
|
1237
|
+
# All three detectors default ON *within* the master switch, so opting in needs
|
|
1238
|
+
# one line, and any one can be silenced without editing the hook manifest.
|
|
1213
1239
|
# A refused turn continues in the same turn and is never refused twice —
|
|
1214
1240
|
# re-entrancy is `stop_hook_active` plus a marker keyed on the last user
|
|
1215
1241
|
# message, never on the reply.
|
|
@@ -1217,6 +1243,7 @@ hooks:
|
|
|
1217
1243
|
enabled: false
|
|
1218
1244
|
promissory: true
|
|
1219
1245
|
language: true
|
|
1246
|
+
verification: true
|
|
1220
1247
|
|
|
1221
1248
|
# PreToolUse RTK-wrap nudge (token-saving Phase 3). Default-OFF. When enabled
|
|
1222
1249
|
# AND `rtk` is on PATH (a live probe — not a self-reported flag), warns (exit
|
|
@@ -1239,6 +1266,20 @@ hooks:
|
|
|
1239
1266
|
design_slop:
|
|
1240
1267
|
enabled: false
|
|
1241
1268
|
|
|
1269
|
+
# PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4).
|
|
1270
|
+
# Default-OFF. When enabled, a Write/Edit to a UI surface with no design
|
|
1271
|
+
# consultation latched this session WARNS (exit 2, never blocks) naming the
|
|
1272
|
+
# route: run existing-ui-audit, then the fe-design loop. A read or search
|
|
1273
|
+
# touching fe-design / existing-ui-audit / design-review / design-intelligence
|
|
1274
|
+
# latches consultation and silences it for the rest of the session, so the
|
|
1275
|
+
# agent that did the right thing is never nagged. Anti-loop: at most 2 nudges
|
|
1276
|
+
# per session. It does NOT read the two UI rules: the UI-surface decision
|
|
1277
|
+
# comes from _lib/ui_surface.ts, so this runs parallel to them rather than
|
|
1278
|
+
# consuming their triggers. Host-limited (pre_tool_use fires on ~2-3 hosts) —
|
|
1279
|
+
# elsewhere the obligation stays model-carried, as the two UI rules declare.
|
|
1280
|
+
ui_route_nudge:
|
|
1281
|
+
enabled: false
|
|
1282
|
+
|
|
1242
1283
|
# PreToolUse code-graph nudge (ADR-124 Phase 4). Default-OFF. When enabled
|
|
1243
1284
|
# AND a native code-graph engine has built a cache (or the repo ships a
|
|
1244
1285
|
# graph.json / SCIP index), WARNS once per session (exit 2, never blocks) as
|
|
@@ -168,6 +168,18 @@ gates:
|
|
|
168
168
|
corpus: "src/rules/*.md frontmatters (111 at baseline)"
|
|
169
169
|
status: enforced
|
|
170
170
|
|
|
171
|
+
- id: lint_rule_skill_pack_reach
|
|
172
|
+
argv: ["--quiet"]
|
|
173
|
+
min_scanned: 90
|
|
174
|
+
corpus: "src/rules/*.md frontmatters joined against src/skills packs and the pack registry (116 rules at baseline)"
|
|
175
|
+
status: enforced
|
|
176
|
+
note: >
|
|
177
|
+
Advisory on findings, enforced on COVERAGE: the gate exits 0 while its
|
|
178
|
+
unreachable-route set is non-empty (12 at introduction), so only this
|
|
179
|
+
floor can catch it going blind. A collapse below 90 would mean it stopped
|
|
180
|
+
reading src/rules, which is exactly the shape that would make "0 findings"
|
|
181
|
+
read as clean.
|
|
182
|
+
|
|
171
183
|
- id: check_condensation
|
|
172
184
|
argv: []
|
|
173
185
|
min_scanned: 380
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Pre-registered council ATTENDANCE metrics (road-to-inbox-harvest-2026-08-b Phase 1.4). Registered here rather than in roadmap prose because a roadmap is archived when it closes and a metric outlives it — the sibling budget files (hook-token-budget.json, recycle-threshold-budget.json) already carry the owner / review_by / honest-gap discipline this reuses. The instrument is the quorum_result line appendQuorumEvent writes to agents/runtime/council/events.log (schema v2); nothing here is measured yet, and no threshold is committed before data exists.",
|
|
2
|
+
"_comment": "Pre-registered council ATTENDANCE metrics (road-to-inbox-harvest-2026-08-b Phase 1.4). Registered here rather than in roadmap prose because a roadmap is archived when it closes and a metric outlives it — the sibling budget files (hook-token-budget.json, recycle-threshold-budget.json) already carry the owner / review_by / honest-gap discipline this reuses. The instrument is the quorum_result line appendQuorumEvent writes to agents/runtime/council/events.log (schema v4 since the ADR-224 shadow floor added gate_class + floor_would_hold at v3 and min_present at v4; v2 lines carry none of the three and v3 lines carry no min_present, so a rate over them must exclude rather than assume them); nothing here is measured yet, and no threshold is committed before data exists.",
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"registered_at": "2026-08-10",
|
|
5
5
|
"owner": "maintainer",
|
|
6
6
|
"review_by": "2026-11-10",
|
|
7
7
|
"unit": "quorum_result event lines in agents/runtime/council/events.log",
|
|
8
|
-
"privacy": "a quorum_result line carries {phase, command, dispatch, verdict, threshold, configured_total, total, present, solo, absent[{member, reason}]} — counts, closed vocabularies, and member ids only. The type has no field able to hold free-form content, so the CLI's own `detail` string (provider error text, which can embed paths and prompt fragments) is dropped at the boundary rather than scrubbed.",
|
|
8
|
+
"privacy": "a quorum_result line carries {phase, command, dispatch, verdict, threshold, configured_total, total, present, solo, gate_class, min_present, floor_would_hold, absent[{member, reason}]} — counts, closed vocabularies, booleans, and member ids only. The type has no field able to hold free-form content, so the CLI's own `detail` string (provider error text, which can embed paths and prompt fragments) is dropped at the boundary rather than scrubbed. The two v3 additions are booleans and the v4 addition is a small integer read from config, none of which can widen that surface.",
|
|
9
9
|
"phase_split": "A pass emits `pre_run` (build_members — who could be constructed) and, on the `run` path, `post_run` (_postRunQuorum — who actually returned a usable response). They disagree whenever a member constructs and then fails mid-flight, so a rate computed over both double-counts. A `pre_run` line is NOT evidence a pass ran: `command: estimate` is a spend-free preview, and a `run` / `debate` line is written before the `--confirm` gate (which defaults to false and returns before any provider call) and before the necessity and size-fit gates. A `post_run` line is the only line that proves providers were called — which is why every rate below is post_run-scoped, and why `command` exists at all.",
|
|
10
10
|
"attendance_rate": {
|
|
11
11
|
"definition": "of the members that entered the pass, how many answered: present / total per post_run pass. Bounded by 1 by construction (evaluateQuorum clamps present into [0, total]).",
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
"instrument": "quorum_result lines, phase=post_run, command=run, field solo, split by dispatch + configured_total",
|
|
24
24
|
"threshold": "DECIDED 2026-08-11 against real data — see ADR-224 (docs/decisions/ADR-224-gate-scoped-solo-attendance-floor.md). The quorum-solo-floor blocker pre-registered three outcomes — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 %. Read at 2026-08-11T09:30Z with the definition above: 1 of 8 post_run/command=run passes carried a degraded solo conclusion = 12.5 %, so the under-5 % null did not fire and the gate-scoped floor was chosen (council 2/2). It was decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble. Still no numeric threshold is committed here, deliberately: at n=8 the 95 % interval around 1/8 spans roughly 0.3 %-53 %, so the rate established urgency and not certainty, and ADR-224's review trigger reopens the null at n=40. What this metric measures does NOT change; once the floor lands, how often it FIRES is a second and different question needing its own instrument."
|
|
25
25
|
},
|
|
26
|
+
"shadow_floor_fire_rate": {
|
|
27
|
+
"definition": "share of post_run passes where a min_present floor WOULD have held the pass, had one been enforced: `floor_would_hold: true` over post_run/command=run lines. Written by the wouldSoloFloorHold predicate in quorum.ts, never re-derived from present/threshold here, for the same reason solo_conclusion_rate defers to isSoloConcluded — one definition, so a change to the rule cannot leave this file stale. This is the instrument solo_conclusion_rate's threshold note said the FIRING question would need; it answers 'how often would the floor hold a gate', which is NOT the question solo_conclusion_rate answers ('how often did a pass conclude on one voice'). The two are NOT the same rate and diverge in both directions. solo: true / floor_would_hold: false — a council configured with one member (the ceiling clamps to 1, deliberately). solo: false / floor_would_hold: true — any pass concluding on 2+ voices under a configured quorum_min_present above 2. They also AGREE on the case that matters most: a construction-degraded roster (2 configured, 1 constructed, 1 answered) is solo: true and floor_would_hold: true, because the floor is judged against max(total, configured_total) — judging it against total alone made this pass, the very one ADR-224 was decided on, structurally unable to fire.",
|
|
28
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, field floor_would_hold, split by dispatch + configured_total AND BY min_present (schema v4+)",
|
|
29
|
+
"threshold": "none committed before data — and deliberately none at the review date either unless a gate-class caller exists by then. A fire-rate over passes that gate nothing measures a counterfactual, not a risk; it is registered now so ADR-224's review trigger (b) has an accumulating series rather than a series that starts on the day someone remembers to add one.",
|
|
30
|
+
"split_by_min_present_or_the_rate_is_uninterpretable": "The floor this rate counts against is `quorum_min_present`, which lives in the USER-GLOBAL .ai-council.yml (ADR-104) — per developer machine, gitignored, invisible in this repository. Schema v3 consumed that value and did not record it, so two operators produced byte-identical lines while measuring different counterfactuals, and a pooled rate over them was a mean of incomparable things. Schema v4 records it as `min_present`; group on it before reading, and treat v3 lines as a separate stratum whose floor is UNKNOWN rather than assuming the default. The sharpest case: `quorum_min_present: 1` is valid config and, per the predicate's own test, can never hold anything — so it drives this rate to a permanent zero. Without min_present on the line that zero is indistinguishable from 'the floor genuinely never would have fired', which is precisely the reading ADR-224's review trigger (b) would act on. No threshold was loosened by adding this field; nothing was gated on it before.",
|
|
31
|
+
"nothing_is_enforced": "No value of this metric holds, delays or fails a council pass. wouldSoloFloorHold is a counterfactual with no caller branching on it, and quorum_min_present configures only what the counterfactual is computed against. A reader must not cite this rate as evidence that the floor protected anything — it has never protected anything."
|
|
32
|
+
},
|
|
26
33
|
"absent_reason_distribution": {
|
|
27
34
|
"definition": "count per reason over absent[] entries — no_binary / no_auth / timeout / quota (AbsentReason) plus the two runtime fallbacks the CLI writes directly, `unavailable` (transport resolved to nothing) and `binary_missing` (a CLI client refused to construct). Distinguishes a council degraded by configuration from one degraded by provider behaviour, which have opposite fixes.",
|
|
28
35
|
"instrument": "quorum_result lines, absent[].reason, split by phase (a pre_run absence is configuration; a post_run absence is mid-flight). Excluding `command: estimate` is NOT sufficient: `run` and `debate` also reach construction and return at the `--confirm` gate, which defaults to false. The reliable filter is a pre_run line whose pass produced a matching post_run line; a pre_run line with no post_run partner reached construction and stopped there, whatever its command says.",
|
|
@@ -34,6 +41,7 @@
|
|
|
34
41
|
"`lens` and `invocation` are empty on every pre_run line — neither is known at member-construction time — so any per-lens breakdown is post_run-only.",
|
|
35
42
|
"`command: debate` carries the pre_run half ONLY. There is no _postRunQuorum on the debate path, and defining `usable response` for a multi-round debate is a separate question this registration does not answer. So debate contributes to absent_reason_distribution and to nothing else: attendance_rate and solo_conclusion_rate cover the `run` path, and a reader must not read them as covering every metered pass.",
|
|
36
43
|
"A pre_run line proves construction happened, never that a pass followed — the `--confirm` gate (default false), the necessity gate and the size-fit gate all abort afterwards and nothing on the line records it. Any count over pre_run lines is an upper bound; pair it with a post_run line before treating it as a pass.",
|
|
44
|
+
"`gate_class` is `false` on EVERY line written so far and will stay false until a caller declares otherwise — nothing in the tree branches on QuorumStatus to hold a gate, so there is no gate-class caller to mark. shadow_floor_fire_rate is therefore a rate over ALL post_run passes, not over gate-class ones, and the gate-class-scoped rate ADR-224 actually cares about is UNMEASURED, not zero. Do not read a `false` majority as evidence that gate-class passes are rare; it is evidence that the concept has no consumer yet.",
|
|
37
45
|
"The CLI emit wiring itself has no test. The two modules are covered directly, but nothing exercises build_members / cmd_run / cmd_debate end-to-end against a captured log — and `_emitQuorumEvent` forwards no AppendEventOptions, so there is no target to capture into. Two defects in this file's first version (a `dispatch` written from the flag rather than the outcome, and a rate mixing config entries with clients) were found by review rather than by a test, which is what that gap costs."
|
|
38
46
|
]
|
|
39
47
|
}
|
|
@@ -3591,10 +3591,42 @@ function main(argv: string[] | null = null): number {
|
|
|
3591
3591
|
|
|
3592
3592
|
// --- CLI entry ---
|
|
3593
3593
|
|
|
3594
|
+
/**
|
|
3595
|
+
* Defined by `build:cli-delegate` only — see `cmd_session_recycle.ts` for why a
|
|
3596
|
+
* single "am I inside a bundle" flag cannot answer "may I run".
|
|
3597
|
+
*/
|
|
3598
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
3599
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
3600
|
+
|
|
3594
3601
|
function _isCliEntry(): boolean {
|
|
3602
|
+
const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
3603
|
+
const cliDelegate =
|
|
3604
|
+
typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
|
|
3605
|
+
// Inlined into the installer / hook / MCP bundle: never auto-run. This
|
|
3606
|
+
// file previously carried no such guard while its comment described the
|
|
3607
|
+
// pairing, so the four siblings documented one mechanism and implemented
|
|
3608
|
+
// two.
|
|
3609
|
+
if (bundled && !cliDelegate) {
|
|
3610
|
+
return false;
|
|
3611
|
+
}
|
|
3595
3612
|
if (process.argv[1] === undefined) {
|
|
3596
3613
|
return false;
|
|
3597
3614
|
}
|
|
3615
|
+
if (cliDelegate) {
|
|
3616
|
+
// `--splitting` moves this module's body into a shared chunk, so the URL
|
|
3617
|
+
// comparison below weighs the CHUNK against `argv[1]` and never matches.
|
|
3618
|
+
// `agent-config doctor` shipped producing zero bytes and exit 0 for
|
|
3619
|
+
// exactly that reason — a diagnostic reporting success while saying
|
|
3620
|
+
// nothing. The invoked file name is the reliable signal inside this
|
|
3621
|
+
// bundle; the delegate smoke test keeps the literal honest.
|
|
3622
|
+
if (path.basename(process.argv[1], '.js') === 'cmd_doctor') {
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
// A miss falls THROUGH to the realpath comparison below rather than
|
|
3626
|
+
// returning false: a symlinked or renamed invocation is exactly the
|
|
3627
|
+
// case that fallback exists for, and swallowing it here would rebuild
|
|
3628
|
+
// the silent no-op this change removes.
|
|
3629
|
+
}
|
|
3598
3630
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
3599
3631
|
if (import.meta.url === argvUrl) {
|
|
3600
3632
|
return true;
|
|
@@ -932,14 +932,37 @@ export function main(argv: string[] | null = null, options: MainOptions = {}): n
|
|
|
932
932
|
// build's `--define`) disables the auto-run there; under tsx/node it is
|
|
933
933
|
// undefined, so `typeof` short-circuits and the standalone CLI guard fires
|
|
934
934
|
// normally.
|
|
935
|
+
// The sentinel above is true in EVERY esbuild target, `build:cli-delegate`
|
|
936
|
+
// included — and there this file is the entry, so refusing on it refused to run
|
|
937
|
+
// the bundle built to run it: `dist/cli-delegate/cmd_migrate.js` produced zero
|
|
938
|
+
// bytes and exit 0, and `agent-config migrate` reported success while doing
|
|
939
|
+
// nothing. `__AGENT_CONFIG_CLI_DELEGATE__` is defined by that one build only, so
|
|
940
|
+
// the pair distinguishes "inlined into the installer" (never run) from "this
|
|
941
|
+
// bundle's own target" (run, subject to the entry check below).
|
|
935
942
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
936
|
-
const
|
|
937
|
-
|
|
943
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
944
|
+
const _inForeignBundle =
|
|
945
|
+
typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' &&
|
|
946
|
+
__AGENT_CONFIG_BUNDLE__ &&
|
|
947
|
+
!(typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__);
|
|
938
948
|
const _HERE = fileURLToPath(import.meta.url);
|
|
939
949
|
function _isCliEntry(): boolean {
|
|
940
950
|
if (process.argv[1] === undefined) {
|
|
941
951
|
return false;
|
|
942
952
|
}
|
|
953
|
+
if (typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__) {
|
|
954
|
+
// `--splitting` moves this module's body into a shared chunk, so the URL
|
|
955
|
+
// comparison below weighs the CHUNK against `argv[1]` and never matches —
|
|
956
|
+
// `agent-config migrate` shipped producing zero bytes and exit 0. The
|
|
957
|
+
// invoked file name is the reliable signal inside this bundle.
|
|
958
|
+
if (path.basename(process.argv[1], '.js') === 'cmd_migrate') {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
// A miss falls THROUGH to the realpath comparison below rather than
|
|
962
|
+
// returning false: a symlinked or renamed invocation is exactly the
|
|
963
|
+
// case that fallback exists for, and swallowing it here would rebuild
|
|
964
|
+
// the silent no-op this change removes.
|
|
965
|
+
}
|
|
943
966
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
944
967
|
if (import.meta.url === argvUrl) {
|
|
945
968
|
return true;
|
|
@@ -958,7 +981,7 @@ function _isCliEntry(): boolean {
|
|
|
958
981
|
}
|
|
959
982
|
}
|
|
960
983
|
|
|
961
|
-
if (!
|
|
984
|
+
if (!_inForeignBundle && (_isCliEntry() || process.argv[1] === _HERE)) {
|
|
962
985
|
try {
|
|
963
986
|
process.exitCode = main(process.argv.slice(2));
|
|
964
987
|
} catch (exc) {
|
|
@@ -449,10 +449,39 @@ export function main(argv: string[] | null = null, options: MainOptions = {}): n
|
|
|
449
449
|
|
|
450
450
|
// --- CLI entry ---
|
|
451
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Defined by `build:cli-delegate` only — see `cmd_session_recycle.ts` for why the
|
|
454
|
+
* URL comparison below cannot decide this inside that bundle.
|
|
455
|
+
*/
|
|
456
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
457
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
458
|
+
|
|
452
459
|
function _isCliEntry(): boolean {
|
|
460
|
+
const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
461
|
+
const cliDelegate =
|
|
462
|
+
typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
|
|
463
|
+
// Inlined into the installer / hook / MCP bundle: never auto-run. This
|
|
464
|
+
// file previously carried no such guard while its comment described the
|
|
465
|
+
// pairing, so the four siblings documented one mechanism and implemented
|
|
466
|
+
// two.
|
|
467
|
+
if (bundled && !cliDelegate) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
453
470
|
if (process.argv[1] === undefined) {
|
|
454
471
|
return false;
|
|
455
472
|
}
|
|
473
|
+
if (cliDelegate) {
|
|
474
|
+
// Found by running every delegate bundle, not by reading: `--splitting`
|
|
475
|
+
// had moved this body into a shared chunk too, so `agent-config refresh`
|
|
476
|
+
// was a silent exit 0 like doctor, migrate and session:recycle.
|
|
477
|
+
if (path.basename(process.argv[1], '.js') === 'cmd_refresh') {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
// A miss falls THROUGH to the realpath comparison below rather than
|
|
481
|
+
// returning false: a symlinked or renamed invocation is exactly the
|
|
482
|
+
// case that fallback exists for, and swallowing it here would rebuild
|
|
483
|
+
// the silent no-op this change removes.
|
|
484
|
+
}
|
|
456
485
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
457
486
|
if (import.meta.url === argvUrl) {
|
|
458
487
|
return true;
|
|
@@ -252,11 +252,49 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
252
252
|
return result.code;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Bundle-safety: never auto-run when inlined into SOMEONE ELSE'S bundle.
|
|
257
|
+
*
|
|
258
|
+
* `__AGENT_CONFIG_BUNDLE__` alone cannot express that. All four esbuild targets
|
|
259
|
+
* define it, `build:cli-delegate` included — and that bundle's entry points ARE
|
|
260
|
+
* these files, so a guard refusing on the flag refuses to run the very bundle
|
|
261
|
+
* built to run it. That shipped: `dist/cli-delegate/cmd_session_recycle.js`
|
|
262
|
+
* produced zero bytes and exit 0 on every installed copy, so
|
|
263
|
+
* `agent-config session:recycle` reported success while writing no envelope.
|
|
264
|
+
* `cmd_doctor` and `cmd_migrate` carried the same guard and were dead the same
|
|
265
|
+
* way.
|
|
266
|
+
*
|
|
267
|
+
* `__AGENT_CONFIG_CLI_DELEGATE__` is defined by that one build only, so the pair
|
|
268
|
+
* says what a single flag could not: inlined elsewhere (install / hook / MCP
|
|
269
|
+
* bundle) → never run; this bundle's own target → run, and let the `argv[1]`
|
|
270
|
+
* comparison below decide whether THIS module is the entry or a shared chunk
|
|
271
|
+
* pulled in beside it.
|
|
272
|
+
*/
|
|
256
273
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
274
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
257
275
|
function _isCliEntry(): boolean {
|
|
258
|
-
|
|
276
|
+
const bundled = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
277
|
+
const cliDelegate =
|
|
278
|
+
typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__;
|
|
279
|
+
if (bundled && !cliDelegate) return false;
|
|
259
280
|
if (process.argv[1] === undefined) return false;
|
|
281
|
+
if (cliDelegate) {
|
|
282
|
+
// `--splitting` turns the entry file into a re-export shim and moves this
|
|
283
|
+
// module's body into a shared chunk, where `import.meta.url` is the
|
|
284
|
+
// CHUNK's url and can never equal `argv[1]`. The comparison below then
|
|
285
|
+
// silently never fires — which is why two sibling commands shipped dead
|
|
286
|
+
// while others worked by accident, purely by where esbuild happened to
|
|
287
|
+
// place their code. Inside this bundle the invoked file name is the only
|
|
288
|
+
// reliable signal; the smoke test over every delegate bundle is what
|
|
289
|
+
// keeps this literal honest if the file is ever renamed.
|
|
290
|
+
if (path.basename(process.argv[1], '.js') === 'cmd_session_recycle') {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
// A miss falls THROUGH to the realpath comparison below rather than
|
|
294
|
+
// returning false: a symlinked or renamed invocation is exactly the
|
|
295
|
+
// case that fallback exists for, and swallowing it here would rebuild
|
|
296
|
+
// the silent no-op this change removes.
|
|
297
|
+
}
|
|
260
298
|
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
261
299
|
try {
|
|
262
300
|
return (
|
|
@@ -194,7 +194,10 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
194
194
|
code-graph Deterministic code-graph engine (ADR-124, Class A).
|
|
195
195
|
Usage: code-graph build|validate|detect|query|explain|affected|path [options]
|
|
196
196
|
sessions:list List live agent sessions on this repository (shared
|
|
197
|
-
session register)
|
|
197
|
+
session register) PLUS the unmerged branches checked
|
|
198
|
+
out in other worktrees — the axis that needs no
|
|
199
|
+
claim. Flags: --json, --branches (adds the branch
|
|
200
|
+
axis to the JSON; the human form always shows it)
|
|
198
201
|
sessions:claim Claim a roadmap for this session so other sessions skip
|
|
199
202
|
it; --release clears the claim
|
|
200
203
|
session:recycle Validate + write the main-session recycle envelope (--verify validates only)
|
|
@@ -209,7 +212,8 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
209
212
|
first. Flags: --all (include maintainer/external),
|
|
210
213
|
--json, --reply (reply-close form: the one blocking
|
|
211
214
|
decision in full, the rest as a count; prints
|
|
212
|
-
nothing when none are yours)
|
|
215
|
+
nothing when none are yours), --pending (staged
|
|
216
|
+
requires_confirmation actions awaiting you)
|
|
213
217
|
capabilities:index Regenerate CAPABILITIES.yaml — the package coverage index
|
|
214
218
|
(capability area → coverage → backing skills/commands → gaps).
|
|
215
219
|
Pass --check to fail if stale (for CI). Reads src/ (package repo).
|