@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
|
@@ -17,15 +17,27 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ## Subcommands
|
|
19
19
|
*
|
|
20
|
-
* - `list [--json]` — live sessions on this repo
|
|
21
|
-
*
|
|
20
|
+
* - `list [--json] [--branches]` — live sessions on this repo, plus the branches
|
|
21
|
+
* checked out in OTHER worktrees. Human table by default; `--json` emits the
|
|
22
|
+
* raw records for a scripted screen, and `--branches` adds the second axis to
|
|
23
|
+
* the JSON (the human output always shows it). Exits 0 with an empty result
|
|
22
24
|
* when there is no register — its absence is the normal pre-first-session
|
|
23
25
|
* state, never an error.
|
|
24
|
-
* - `claim <slug>` — record that THIS
|
|
25
|
-
*
|
|
26
|
+
* - `claim <slug>` — record that THIS SESSION has taken a roadmap. Writes the
|
|
27
|
+
* bridge file the next heartbeat lifts into the register, keyed on the host
|
|
28
|
+
* session id where one exists.
|
|
26
29
|
* - `claim --release` — clear the claim.
|
|
30
|
+
*
|
|
31
|
+
* ## Why two axes and not one
|
|
32
|
+
*
|
|
33
|
+
* The slug axis needs a claim to have been written, and writing it is
|
|
34
|
+
* model-carried. The branch axis needs nothing: a worktree checkout is on disk
|
|
35
|
+
* from the first minute. Measured twice, the duplication was caught by the branch
|
|
36
|
+
* axis (`git branch`'s `+` marker) and missed entirely by the register, so
|
|
37
|
+
* reporting only the register is reporting the half that can be silent.
|
|
27
38
|
*/
|
|
28
39
|
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
29
41
|
import * as fs from 'node:fs';
|
|
30
42
|
import * as path from 'node:path';
|
|
31
43
|
import process from 'node:process';
|
|
@@ -36,17 +48,22 @@ import {
|
|
|
36
48
|
iso_now,
|
|
37
49
|
read_live_records,
|
|
38
50
|
register_dir,
|
|
51
|
+
safe_stem,
|
|
39
52
|
ttl_is_measured,
|
|
40
53
|
ttl_seconds_for,
|
|
41
54
|
} from './_lib/session_register.js';
|
|
42
|
-
import {
|
|
55
|
+
import { claim_is_stale, roadmap_claim_rel } from './session_register_hook.js';
|
|
43
56
|
|
|
44
57
|
function usage(): number {
|
|
45
58
|
process.stderr.write(
|
|
46
59
|
[
|
|
47
60
|
'usage:',
|
|
48
|
-
' agent-config sessions:list [--json]
|
|
61
|
+
' agent-config sessions:list [--json] [--branches]',
|
|
62
|
+
' live sessions, plus unmerged branches',
|
|
63
|
+
' held by other worktrees (--branches adds',
|
|
64
|
+
' that axis to the JSON form)',
|
|
49
65
|
' agent-config sessions:claim <slug> claim a roadmap for this session',
|
|
66
|
+
' agent-config sessions:claim <slug> --force claim it even though a peer holds it',
|
|
50
67
|
' agent-config sessions:claim --release drop this session\'s roadmap claim',
|
|
51
68
|
'',
|
|
52
69
|
].join('\n'),
|
|
@@ -54,18 +71,133 @@ function usage(): number {
|
|
|
54
71
|
return 2;
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Branches checked out in ANOTHER worktree — the second axis, and the one that
|
|
76
|
+
* does not depend on a claim having been written.
|
|
77
|
+
*
|
|
78
|
+
* `git branch` marks these with a `+` prefix, and that prefix is what actually
|
|
79
|
+
* caught the duplication both times it happened, from a memory note rather than
|
|
80
|
+
* from any command. It is reported here because the register alone cannot see it:
|
|
81
|
+
* a session that never ran `sessions:claim` is invisible to the slug axis while
|
|
82
|
+
* its branch is already on disk, so a claim-only screen has a blind spot exactly
|
|
83
|
+
* where the first minutes of a duplicate live.
|
|
84
|
+
*
|
|
85
|
+
* `git worktree list --porcelain` is preferred over parsing `git branch` output:
|
|
86
|
+
* the `+` marker is presentation and the porcelain form is a contract.
|
|
87
|
+
*/
|
|
88
|
+
export function other_worktree_branches(
|
|
89
|
+
root: string,
|
|
90
|
+
spawn: typeof spawnSync = spawnSync,
|
|
91
|
+
): { branch: string; worktree: string }[] {
|
|
92
|
+
return other_worktree_branches_detailed(root, spawn).rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Same walk, plus whether the unmerged filter was actually applied.
|
|
97
|
+
*
|
|
98
|
+
* R2 finding 12: when `origin/main` is missing or un-fetched the filter silently
|
|
99
|
+
* drops and every foreign worktree is reported — while the human output still
|
|
100
|
+
* claimed "N unmerged branch(es)". In a tree with 30+ long-merged worktrees that
|
|
101
|
+
* is not a small overstatement, and the reader had no way to see the degradation.
|
|
102
|
+
*/
|
|
103
|
+
export function other_worktree_branches_detailed(
|
|
104
|
+
root: string,
|
|
105
|
+
spawn: typeof spawnSync = spawnSync,
|
|
106
|
+
): { rows: { branch: string; worktree: string }[]; filtered: boolean } {
|
|
107
|
+
let out = '';
|
|
108
|
+
try {
|
|
109
|
+
const r = spawn('git', ['worktree', 'list', '--porcelain'], {
|
|
110
|
+
cwd: root,
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
timeout: GIT_PROBE_TIMEOUT_MS,
|
|
113
|
+
});
|
|
114
|
+
if (r.status !== 0) {
|
|
115
|
+
return { rows: [], filtered: false };
|
|
116
|
+
}
|
|
117
|
+
out = String(r.stdout ?? '');
|
|
118
|
+
} catch {
|
|
119
|
+
return { rows: [], filtered: false };
|
|
120
|
+
}
|
|
121
|
+
// Unmerged branches only, in ONE extra git call. Without this filter the
|
|
122
|
+
// list is every worktree the repo ever had — 30+ here, all long merged — and
|
|
123
|
+
// a list that is mostly noise is a list nobody reads, which is the failure
|
|
124
|
+
// mode a false-positive-prone warning always ends in.
|
|
125
|
+
let unmerged: Set<string> | null = null;
|
|
126
|
+
try {
|
|
127
|
+
const r = spawn('git', ['branch', '--no-merged', 'origin/main', '--format=%(refname:short)'], {
|
|
128
|
+
cwd: root,
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: GIT_PROBE_TIMEOUT_MS,
|
|
131
|
+
});
|
|
132
|
+
if (r.status === 0) {
|
|
133
|
+
unmerged = new Set(
|
|
134
|
+
String(r.stdout ?? '')
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((s) => s.trim())
|
|
137
|
+
.filter((s) => s !== ''),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
unmerged = null; // no filter available — report everything rather than nothing
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rows: { branch: string; worktree: string }[] = [];
|
|
145
|
+
let wt: string | null = null;
|
|
146
|
+
for (const raw of out.split('\n')) {
|
|
147
|
+
const line = raw.trim();
|
|
148
|
+
if (line.startsWith('worktree ')) {
|
|
149
|
+
wt = line.slice('worktree '.length).trim();
|
|
150
|
+
} else if (line.startsWith('branch ') && wt !== null) {
|
|
151
|
+
const ref = line.slice('branch '.length).trim();
|
|
152
|
+
const branch = ref.replace(/^refs\/heads\//, '');
|
|
153
|
+
// The caller's own worktree is not a foreign checkout. Compared by
|
|
154
|
+
// realpath because /var vs /private/var on macOS would otherwise make
|
|
155
|
+
// every session report itself as a collision.
|
|
156
|
+
let same = false;
|
|
157
|
+
try {
|
|
158
|
+
same = fs.realpathSync(wt) === fs.realpathSync(root);
|
|
159
|
+
} catch {
|
|
160
|
+
same = wt === root;
|
|
161
|
+
}
|
|
162
|
+
if (!same && (unmerged === null || unmerged.has(branch))) {
|
|
163
|
+
rows.push({ branch, worktree: wt });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
rows.sort((a, b) => a.branch.localeCompare(b.branch) || a.worktree.localeCompare(b.worktree));
|
|
168
|
+
return { rows, filtered: unmerged !== null };
|
|
169
|
+
}
|
|
170
|
+
|
|
57
171
|
function cmd_list(argv: string[], root: string): number {
|
|
58
172
|
const as_json = argv.includes('--json');
|
|
59
173
|
const dir = register_dir(root);
|
|
60
174
|
const records = dir === null ? [] : read_live_records(dir, { prune: true });
|
|
175
|
+
// Two git subprocesses with a 10 s timeout each, so they are not paid on the
|
|
176
|
+
// scripted path that discards them (R2 finding 8).
|
|
177
|
+
const wants_branches = !as_json || argv.includes('--branches');
|
|
178
|
+
const walk = wants_branches
|
|
179
|
+
? other_worktree_branches_detailed(root)
|
|
180
|
+
: { rows: [], filtered: false };
|
|
181
|
+
const held = walk.rows;
|
|
182
|
+
const filtered = walk.filtered;
|
|
61
183
|
|
|
62
184
|
if (as_json) {
|
|
63
|
-
|
|
185
|
+
// The record array stays the top-level shape it has always been when
|
|
186
|
+
// `--branches` is absent: a scripted screen pins it, and silently wrapping
|
|
187
|
+
// it in an object would break every existing caller.
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
argv.includes('--branches')
|
|
190
|
+
? `${JSON.stringify({ sessions: records, other_worktree_branches: held, unmerged_filter_applied: filtered }, null, 2)}\n`
|
|
191
|
+
: `${JSON.stringify(records, null, 2)}\n`,
|
|
192
|
+
);
|
|
64
193
|
return 0;
|
|
65
194
|
}
|
|
66
195
|
|
|
196
|
+
// One branch, not two: `write_held_branches([])` returns immediately, so the
|
|
197
|
+
// empty-and-empty case needed no special case of its own (R2 finding 7).
|
|
67
198
|
if (records.length === 0) {
|
|
68
199
|
process.stdout.write('No live sessions registered on this repository.\n');
|
|
200
|
+
write_held_branches(held, filtered);
|
|
69
201
|
return 0;
|
|
70
202
|
}
|
|
71
203
|
|
|
@@ -75,18 +207,28 @@ function cmd_list(argv: string[], root: string): number {
|
|
|
75
207
|
const age_min = Math.max(0, Math.round((now - Date.parse(r.last_seen)) / 60000));
|
|
76
208
|
const ttl_h = Math.round(ttl_seconds_for(r.platform) / 3600);
|
|
77
209
|
const ttl_note = ttl_is_measured(r.platform) ? `${ttl_h}h` : `${ttl_h}h (unmeasured host)`;
|
|
210
|
+
// A slug naming no open roadmap is labelled rather than printed straight.
|
|
211
|
+
// Measured: four live records all carried one already-archived slug, and a
|
|
212
|
+
// screening session read that as "taken" about work that had shipped.
|
|
213
|
+
const roadmap =
|
|
214
|
+
r.roadmap_slug === null
|
|
215
|
+
? '(none claimed)'
|
|
216
|
+
: claim_is_stale(root, r.roadmap_slug)
|
|
217
|
+
? `${r.roadmap_slug} ← STALE, no such open roadmap; treat as no claim`
|
|
218
|
+
: r.roadmap_slug;
|
|
78
219
|
process.stdout.write(
|
|
79
220
|
[
|
|
80
221
|
` ${r.session_id}`,
|
|
81
222
|
` host: ${r.platform} · TTL ${ttl_note}`,
|
|
82
223
|
` branch: ${r.branch ?? '(detached)'}`,
|
|
83
|
-
` roadmap: ${
|
|
224
|
+
` roadmap: ${roadmap}`,
|
|
84
225
|
` worktree: ${r.worktree}`,
|
|
85
226
|
` seen: ${age_min} min ago`,
|
|
86
227
|
'',
|
|
87
228
|
].join('\n'),
|
|
88
229
|
);
|
|
89
230
|
}
|
|
231
|
+
write_held_branches(held, filtered);
|
|
90
232
|
process.stdout.write(
|
|
91
233
|
'Advisory only — this register is not a lock, and an idle session\n' +
|
|
92
234
|
'disappears from it after its TTL although its user may return.\n',
|
|
@@ -94,30 +236,253 @@ function cmd_list(argv: string[], root: string): number {
|
|
|
94
236
|
return 0;
|
|
95
237
|
}
|
|
96
238
|
|
|
239
|
+
/** snake_case like every other function here (R2 finding 15). */
|
|
240
|
+
function write_held_branches(
|
|
241
|
+
held: readonly { branch: string; worktree: string }[],
|
|
242
|
+
filtered: boolean,
|
|
243
|
+
): void {
|
|
244
|
+
if (held.length === 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
filtered
|
|
249
|
+
? `\n${held.length} unmerged branch(es) checked out in another worktree — a claim\n` +
|
|
250
|
+
'may not have been written yet, so check these before picking work.\n' +
|
|
251
|
+
'Limit: a branch created seconds ago carries no commits and is therefore\n' +
|
|
252
|
+
'MERGED into main by definition, so it does not appear here — that first\n' +
|
|
253
|
+
'minute is what the roadmap-slug axis above covers.\n'
|
|
254
|
+
: `\n${held.length} branch(es) checked out in another worktree. UNFILTERED —\n` +
|
|
255
|
+
'`git branch --no-merged origin/main` was unavailable (no origin/main, or\n' +
|
|
256
|
+
'never fetched), so long-merged worktrees are included and this list\n' +
|
|
257
|
+
'overstates what is live. `git fetch origin` and re-run for the filtered set.\n',
|
|
258
|
+
);
|
|
259
|
+
for (const h of held) {
|
|
260
|
+
process.stdout.write(` ${h.branch}\n ${h.worktree}\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This session's id, from the host environment, or `null`.
|
|
266
|
+
*
|
|
267
|
+
* The CLI has no envelope, so the environment is the only channel — and it is a
|
|
268
|
+
* real one: Claude Code exports `CLAUDE_CODE_SESSION_ID` into every shell it
|
|
269
|
+
* spawns (probed 2026-08-12). A host that exports nothing degrades to the legacy
|
|
270
|
+
* per-worktree claim rather than losing the claim, and `sessions:claim` says so
|
|
271
|
+
* on that path instead of implying a per-session guarantee it cannot give.
|
|
272
|
+
*/
|
|
273
|
+
export function env_session_id(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
274
|
+
// The package's own variable is checked FIRST — an override that loses to the
|
|
275
|
+
// host variable is not an override. Caught by a test that set it and still got
|
|
276
|
+
// the ambient Claude Code id back, which is also the shape a second host or a
|
|
277
|
+
// debugging run would hit.
|
|
278
|
+
for (const key of ['AGENT_CONFIG_SESSION_ID', 'CLAUDE_CODE_SESSION_ID']) {
|
|
279
|
+
const v = String(env[key] ?? '').trim();
|
|
280
|
+
if (v !== '') {
|
|
281
|
+
return v;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Ceiling for the two `git` probes here. Named rather than repeated: the value
|
|
289
|
+
* is a contract with the caller (a screen must not hang), and two literals drift.
|
|
290
|
+
*/
|
|
291
|
+
const GIT_PROBE_TIMEOUT_MS = 10_000;
|
|
292
|
+
|
|
293
|
+
/** Same filesystem path, symlink-tolerant. `/var` vs `/private/var` on macOS. */
|
|
294
|
+
function _same_path(a: string, b: string): boolean {
|
|
295
|
+
try {
|
|
296
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
297
|
+
} catch {
|
|
298
|
+
return a === b;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Foreign-worktree branches whose NAME looks like this roadmap.
|
|
304
|
+
*
|
|
305
|
+
* Separated from `claim_conflicts` because it is a **heuristic** and its output is
|
|
306
|
+
* treated differently: a name match warns, it never refuses. R2 finding 4 caught
|
|
307
|
+
* the contradiction — the docstring called it "a reason to look, not a verdict"
|
|
308
|
+
* while a hit produced exit 1 and demanded `--force`.
|
|
309
|
+
*
|
|
310
|
+
* Two guards on the tail, both from that finding. A slug ending in a date
|
|
311
|
+
* (`road-to-inbox-harvest-2026-08`) yielded the tail `2026-08`, which cleared the
|
|
312
|
+
* old `length >= 4` check and matched every branch carrying that string; a tail
|
|
313
|
+
* whose tokens are all digits is therefore rejected. And a `road-to-` prefix
|
|
314
|
+
* stripped from a two-token slug can leave a generic word, so the tail must be at
|
|
315
|
+
* least 8 characters — measured against the real case, `dispatch-safety`.
|
|
316
|
+
*/
|
|
317
|
+
export function branch_name_hits(
|
|
318
|
+
root: string,
|
|
319
|
+
slug: string,
|
|
320
|
+
): { branch: string; worktree: string }[] {
|
|
321
|
+
const tokens = slug.replace(/^road-to-/, '').split('-').filter(Boolean);
|
|
322
|
+
const tail = tokens.slice(-2).join('-');
|
|
323
|
+
if (tail.length < 8 || /^[\d-]+$/.test(tail)) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
// Token-boundary match, not `includes`. The adversarial gate named the exact
|
|
327
|
+
// false positive: `dispatch-safety` is a substring of `redispatch-safety-valve`,
|
|
328
|
+
// which is a different task entirely. A branch name is `-` / `/` / `_`
|
|
329
|
+
// delimited, so requiring a delimiter (or an end) on both sides is the whole
|
|
330
|
+
// fix and it keeps `feat/dispatch-safety-confirmation` matching.
|
|
331
|
+
const bounded = new RegExp(`(^|[/_-])${tail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([/_-]|$)`);
|
|
332
|
+
return other_worktree_branches(root).filter((b) => bounded.test(b.branch));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Who else is already on this slug — checked BEFORE the claim is written.
|
|
337
|
+
*
|
|
338
|
+
* This is the only point in the flow where the check can work, and finding that
|
|
339
|
+
* out was the load-bearing part of the repair. The register's context block is
|
|
340
|
+
* emitted on `session_start` **only**, and at session start this session's own
|
|
341
|
+
* slug is `null` by construction, because the roadmap is picked later. A
|
|
342
|
+
* roadmap-collision warning in that renderer can therefore never fire on the
|
|
343
|
+
* session doing the picking: the warning window sat entirely before the decision.
|
|
344
|
+
*
|
|
345
|
+
* `sessions:claim` is the decision. It runs in the foreground, its output reaches
|
|
346
|
+
* the model directly, and it needs no hook slot and no heartbeat delay.
|
|
347
|
+
*
|
|
348
|
+
* Two sources, because either can be silent alone: a live register record with
|
|
349
|
+
* the same slug, and an unmerged branch in another worktree whose NAME contains
|
|
350
|
+
* the slug's distinctive tail (a peer that never claimed). The name match is a
|
|
351
|
+
* heuristic and says so — it is a reason to look, not a verdict.
|
|
352
|
+
*/
|
|
353
|
+
export function claim_conflicts(
|
|
354
|
+
root: string,
|
|
355
|
+
slug: string,
|
|
356
|
+
): { kind: 'session' | 'branch'; detail: string }[] {
|
|
357
|
+
const out: { kind: 'session' | 'branch'; detail: string }[] = [];
|
|
358
|
+
const dir = register_dir(root);
|
|
359
|
+
const sid = env_session_id();
|
|
360
|
+
if (dir !== null) {
|
|
361
|
+
for (const r of read_live_records(dir, { prune: false })) {
|
|
362
|
+
// Self-exclusion by session id where we have one, and by WORKTREE where
|
|
363
|
+
// we do not (R2 finding 2): on a host that fills the hook envelope but
|
|
364
|
+
// exports no shell variable, `sid` is null, our own record would count
|
|
365
|
+
// as a peer, and re-claiming the same slug would exit 1 citing
|
|
366
|
+
// ourselves — non-idempotent on exactly the graceful-degradation path.
|
|
367
|
+
// Compared through `safe_stem`, because the library normalises ids and a
|
|
368
|
+
// raw comparison fails silently on any id the two spell differently
|
|
369
|
+
// (R2 finding 11).
|
|
370
|
+
if (sid !== null && safe_stem(r.session_id) === safe_stem(sid)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (sid === null && _same_path(r.worktree, root)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (r.roadmap_slug === slug && !claim_is_stale(root, r.roadmap_slug, r.worktree)) {
|
|
377
|
+
out.push({
|
|
378
|
+
kind: 'session',
|
|
379
|
+
detail: `${r.platform} session on branch \`${r.branch ?? 'detached'}\` in ${r.worktree}`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const b of branch_name_hits(root, slug)) {
|
|
385
|
+
out.push({ kind: 'branch', detail: `branch \`${b.branch}\` in ${b.worktree}` });
|
|
386
|
+
}
|
|
387
|
+
// Deduplicated by rendered detail. Several records can describe the same
|
|
388
|
+
// peer — observed live: three sessions in one checkout all carrying one slug,
|
|
389
|
+
// which is the per-worktree claim defect this change also fixes. Three
|
|
390
|
+
// identical lines read as three peers and inflate the finding.
|
|
391
|
+
const seen = new Set<string>();
|
|
392
|
+
return out.filter((c) => {
|
|
393
|
+
const key = `${c.kind}\t${c.detail}`;
|
|
394
|
+
if (seen.has(key)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
seen.add(key);
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
97
402
|
function cmd_claim(argv: string[], root: string): number {
|
|
98
|
-
const
|
|
403
|
+
const sid = env_session_id();
|
|
404
|
+
const target = path.join(root, roadmap_claim_rel(sid));
|
|
99
405
|
if (argv.includes('--release')) {
|
|
406
|
+
// Release only what this session could have written. An identified session
|
|
407
|
+
// never writes the legacy file, so unlinking it would delete a PEER's claim
|
|
408
|
+
// in a mixed-host checkout and leave that peer reporting nothing, with no
|
|
409
|
+
// notice to either side (R2 finding 10). An unidentified session shares that
|
|
410
|
+
// file by construction and releasing it is the only release available.
|
|
100
411
|
try {
|
|
101
412
|
fs.unlinkSync(target);
|
|
102
413
|
} catch {
|
|
103
414
|
/* nothing to release */
|
|
104
415
|
}
|
|
105
|
-
process.stdout.write(
|
|
416
|
+
process.stdout.write(
|
|
417
|
+
sid === null
|
|
418
|
+
? 'Roadmap claim released (shared worktree claim — this host exports no session id).\n'
|
|
419
|
+
: 'Roadmap claim released.\n',
|
|
420
|
+
);
|
|
106
421
|
return 0;
|
|
107
422
|
}
|
|
108
423
|
const slug = argv.find((a) => !a.startsWith('-'));
|
|
109
424
|
if (slug === undefined || slug.trim() === '') {
|
|
110
425
|
return usage();
|
|
111
426
|
}
|
|
427
|
+
|
|
428
|
+
const forced = argv.includes('--force');
|
|
429
|
+
const conflicts = forced ? [] : claim_conflicts(root, slug.trim());
|
|
430
|
+
|
|
431
|
+
// Split by evidence class, which R2 finding 4 is right to demand: a live peer
|
|
432
|
+
// record carrying this exact slug is a FACT and refuses the write; a branch
|
|
433
|
+
// whose name merely resembles the slug is a HEURISTIC and only warns. A
|
|
434
|
+
// heuristic that forces `--force` trains people to pass `--force`.
|
|
435
|
+
const hard = conflicts.filter((c) => c.kind === 'session');
|
|
436
|
+
const soft = conflicts.filter((c) => c.kind === 'branch');
|
|
437
|
+
|
|
438
|
+
if (hard.length > 0) {
|
|
439
|
+
process.stderr.write(
|
|
440
|
+
`sessions:claim: REFUSED — "${slug.trim()}" is already claimed by a live session.\n` +
|
|
441
|
+
hard.map((c) => ` · ${c.detail}\n`).join('') +
|
|
442
|
+
(soft.length > 0 ? soft.map((c) => ` · also, by name: ${c.detail}\n`).join('') : '') +
|
|
443
|
+
'\nA different branch name does not make it a different task. Pick another\n' +
|
|
444
|
+
'roadmap, split this one explicitly with the peer, or re-run with --force\n' +
|
|
445
|
+
'if you have decided to duplicate deliberately.\n' +
|
|
446
|
+
'Advisory, not a lock: two sessions claiming simultaneously still race.\n',
|
|
447
|
+
);
|
|
448
|
+
return 1;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// A slug naming no open roadmap is a typo or an archived roadmap, and writing
|
|
452
|
+
// it manufactures exactly the stale claim this change teaches every reader to
|
|
453
|
+
// discount (R2 finding 13). The cheapest guard against creating one belongs at
|
|
454
|
+
// the only write site.
|
|
455
|
+
if (!forced && claim_is_stale(root, slug.trim())) {
|
|
456
|
+
process.stderr.write(
|
|
457
|
+
`sessions:claim: REFUSED — no open roadmap named "${slug.trim()}".\n` +
|
|
458
|
+
' Expected agents/roadmaps/<slug>.md; archive/, later/ and skipped/ do not count.\n' +
|
|
459
|
+
' Check the spelling, or use --force if the file is genuinely arriving later.\n',
|
|
460
|
+
);
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
|
|
112
464
|
try {
|
|
113
|
-
write_atomic(
|
|
465
|
+
write_atomic(
|
|
466
|
+
target,
|
|
467
|
+
`${JSON.stringify({ slug: slug.trim(), written_at: iso_now(), session_id: sid }, null, 2)}\n`,
|
|
468
|
+
);
|
|
114
469
|
} catch (exc) {
|
|
115
470
|
process.stderr.write(`sessions:claim: could not write the claim — ${String(exc)}\n`);
|
|
116
471
|
return 1; // the claim is the whole point of this verb; a silent no-op would lie
|
|
117
472
|
}
|
|
118
473
|
process.stdout.write(
|
|
119
474
|
`Claimed "${slug.trim()}" for this session. It becomes visible to other\n` +
|
|
120
|
-
'sessions on the next turn, when the heartbeat lifts it into the register.\n'
|
|
475
|
+
'sessions on the next turn, when the heartbeat lifts it into the register.\n' +
|
|
476
|
+
(sid === null
|
|
477
|
+
? 'NOTE: this host exports no session id, so the claim is scoped to this\n' +
|
|
478
|
+
'WORKTREE — a second session in the same checkout will overwrite it.\n'
|
|
479
|
+
: '') +
|
|
480
|
+
// The heuristic axis reports here rather than refusing above.
|
|
481
|
+
(soft.length > 0
|
|
482
|
+
? `WARNING: ${soft.length} branch name(s) in other worktrees resemble this roadmap.\n` +
|
|
483
|
+
soft.map((c) => ` · ${c.detail}\n`).join('') +
|
|
484
|
+
' A name match is a reason to look, not proof — check before you write code.\n'
|
|
485
|
+
: ''),
|
|
121
486
|
);
|
|
122
487
|
return 0;
|
|
123
488
|
}
|
|
@@ -36,6 +36,7 @@ const modelAutoSwitch = z.enum(['auto', 'suggest', 'off']);
|
|
|
36
36
|
const leanProjectionMode = z.enum(['eager-all', 'thin']);
|
|
37
37
|
const projectionMode = z.enum(['legacy-all', 'scoped']);
|
|
38
38
|
const memoryCadence = z.enum(['auto', 'always', 'never']);
|
|
39
|
+
const projectAudience = z.enum(['self', 'internal', 'client', 'public']);
|
|
39
40
|
|
|
40
41
|
export const settingsSchema = z.object({
|
|
41
42
|
agent_config_version: z.string().default('').describe(
|
|
@@ -142,6 +143,9 @@ export const settingsSchema = z.object({
|
|
|
142
143
|
improvement_pr_branch_prefix: z.string().default('improve/agent-').describe(
|
|
143
144
|
'Branch-name prefix for improvement PRs the agent opens against project.upstream_repo (e.g. "improve/agent-add-react-skill"). Pick a prefix your repo conventions allow.',
|
|
144
145
|
),
|
|
146
|
+
audience: projectAudience.default('public').describe(
|
|
147
|
+
'Who this project is built for — read by the demand gate (§ 8-pre of docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md), whose L0-L4 ladder measures MARKET demand and is meaningless where no market is intended. self = a tool its maintainer builds for themselves; the gate is inert and work is classified L-self (build) instead of being deferred for lack of a user population nobody wants. internal = a team tool; only "what breaks without it?" survives. client = built for a named client, who is the requester rather than a market segment. public (default) = a product with an intended market; full three-question gate, behaviour unchanged from before this key existed.',
|
|
148
|
+
),
|
|
145
149
|
}),
|
|
146
150
|
github: z.object({
|
|
147
151
|
pr_reply_method: replyMethod.default('create_review_comment').describe(
|
|
@@ -515,6 +519,11 @@ export const settingsSchema = z.object({
|
|
|
515
519
|
'PreToolUse anti-slop nudge (road-to-anti-slop-detector Phase 3). Default off. When on, runs the lint_design_slop registry against about-to-be-written UI content and WARNS (never blocks) on P0/P1 aesthetic tells (side-stripe, gradient-text, magic z-index, …). Flags are rebuttable via DESIGN.md / design-slop-disable. Anti-loop: a file::rule signature surfaced 3x goes silent. Host-limited convenience layer; the universal gate is the lint_design_slop linter/CI.',
|
|
516
520
|
),
|
|
517
521
|
}).default({}),
|
|
522
|
+
ui_route_nudge: z.object({
|
|
523
|
+
enabled: z.boolean().default(false).describe(
|
|
524
|
+
'PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4). Default off. When on, a Write/Edit to a UI surface with no design consultation latched this session WARNS (never blocks) naming the route — run existing-ui-audit, then the fe-design loop. A read or search touching fe-design / existing-ui-audit / design-review / design-intelligence latches consultation and silences it for the session. Anti-loop: at most 2 nudges per session. It does not read the rules: the UI-surface decision comes from _lib/ui_surface.ts and no code parses rule frontmatter, so this runs parallel to the two UI rules rather than consuming their triggers, and a test keeps the sets from drifting. It is a nudge, so their enforced_by: none stays accurate.',
|
|
525
|
+
),
|
|
526
|
+
}).default({}),
|
|
518
527
|
code_graph: z.object({
|
|
519
528
|
enabled: z.boolean().default(false).describe(
|
|
520
529
|
'PreToolUse code-graph nudge (ADR-124 Phase 4). Default off. When on AND a native code-graph cache or a consumer-shipped graph.json/SCIP index is present, warns once per session (never blocks) as the agent is about to Grep/Glob or Read a source file — query the graph first for who-calls/where-used/impact questions (or rebuild if stale, build if absent). Source G’s strict block-first-read mode is deliberately un-ported.',
|
|
@@ -530,6 +539,9 @@ export const settingsSchema = z.object({
|
|
|
530
539
|
language: z.boolean().default(true).describe(
|
|
531
540
|
'Detector B of the turn-end gate: refuse a turn whose reply is not in the pinned language (19 measured occurrences). Fenced code, inline code, quoted tool output, tables, URLs and paths are stripped first, and the remainder is then scored LEAD-FIRST by the same classifier that sets the pin — so the verdict usually comes from the reply opening, not from all of it. Sharing that classifier is deliberate: it stops the pin and the detector disagreeing about a turn. Accepted opt-in spellings for the switches below include YAML true/false and yes/no/on/off. Inert while hooks.turn_end_gate.enabled is false.',
|
|
532
541
|
),
|
|
542
|
+
verification: z.boolean().default(true).describe(
|
|
543
|
+
'Detector C of the turn-end gate: refuse a turn that changed a file and then ran nothing that could have checked it — no test, type-check, lint or build call follows the LAST edit. Ordering is the point: a run before the final edit did not exercise it, which is the freshness clause verify-before-complete states. Read off the turn tool calls, not the prose, because a reply cannot say whether an edit was verified. Deliberately narrow on what counts as verification — a missed detection costs one unguarded turn, a false refusal teaches you to switch the gate off. No fire rate is quoted: none has been measured yet. Inert while hooks.turn_end_gate.enabled is false.',
|
|
544
|
+
),
|
|
533
545
|
}).default({}),
|
|
534
546
|
}),
|
|
535
547
|
decision_engine: z.object({
|
/package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md
RENAMED
|
File without changes
|
/package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md
RENAMED
|
File without changes
|
/package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md
RENAMED
|
File without changes
|
|
File without changes
|