@event4u/agent-config 12.0.0 → 13.0.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 +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +44 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/MIGRATION.md +20 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-trial workspace lifecycle — the clone, and the transcript beside it.
|
|
3
|
+
*
|
|
4
|
+
* The two belong together: `reset_fixture` re-creates the clone AND drops the
|
|
5
|
+
* sibling transcript, and both are the delta-#7 evidence an offline re-scorer
|
|
6
|
+
* reads. They moved out of `bench_ab_v2_run.ts` because the source-size ratchet
|
|
7
|
+
* is shrink-only and extraction is its intended answer — raising the baseline
|
|
8
|
+
* is a defect, and trimming the prose out of load-bearing comments is worse.
|
|
9
|
+
*
|
|
10
|
+
* ## Transcript preservation — delta #7's sibling, for T5.
|
|
11
|
+
*
|
|
12
|
+
* T5 scores what a run did BEFORE it wrote, which leaves no residue in the
|
|
13
|
+
* final tree: the transcript is the only evidence, and the report discards it.
|
|
14
|
+
* These two functions are what make a completed sweep re-scorable for search
|
|
15
|
+
* adherence instead of needing a re-run.
|
|
16
|
+
*
|
|
17
|
+
* BESIDE the clone, never inside it. `changed_files` diffs the clone against
|
|
18
|
+
* the pristine fixture, so a transcript written into the workspace would appear
|
|
19
|
+
* as a file the run created: T5's evidence would corrupt T1's, and the size
|
|
20
|
+
* endpoint would gain one added file per trial for a reason no reader could see.
|
|
21
|
+
*
|
|
22
|
+
* A file rather than a field on the trial record because transcripts are the
|
|
23
|
+
* largest thing a sweep produces, and inlining them would multiply a pinned
|
|
24
|
+
* report's size for data only the T5 re-scorer ever reads. Truncating instead
|
|
25
|
+
* was the other option and is worse: a rubric scored on a clipped transcript
|
|
26
|
+
* measures the clip.
|
|
27
|
+
*
|
|
28
|
+
* KNOWN COVERAGE GAP, stated rather than silently carried: the
|
|
29
|
+
* `package-recursive` arm records neither `workspace` nor `transcript_path`, so
|
|
30
|
+
* T1/T2/T4 and now T5 all skip it. The workspace half predates this module and
|
|
31
|
+
* closing it would alter what the existing endpoints cover, which is a
|
|
32
|
+
* different decision from adding one — so it is named here and in the T5
|
|
33
|
+
* re-scorer's "no transcript recorded" reason instead of being half-fixed.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'node:fs';
|
|
36
|
+
import * as os from 'node:os';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
41
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
42
|
+
const FIXTURES_ROOT = path.join(REPO_ROOT, 'internal', 'bench', 'ab');
|
|
43
|
+
const WORK_ROOT = path.join(os.tmpdir(), 'agent-config-bench-v2-clones');
|
|
44
|
+
|
|
45
|
+
type Dict = Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
/** Where this trial's transcript is preserved — a sibling of the clone. */
|
|
48
|
+
export function transcript_path_for(clone: string): string {
|
|
49
|
+
return `${clone}.transcript.txt`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write the transcript beside the clone; return the path, or `null` when there
|
|
54
|
+
* was nothing to preserve or the write failed.
|
|
55
|
+
*
|
|
56
|
+
* A null propagates onto the trial as `transcript_path: null`, which the T5
|
|
57
|
+
* re-scorer reads as "not measured on this trial" — never as a zero score. A
|
|
58
|
+
* failed write must not be able to look like a run that searched for nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function preserve_transcript(clone: string, transcript: string): string | null {
|
|
61
|
+
if (!transcript) return null;
|
|
62
|
+
const dest = transcript_path_for(clone);
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(dest, transcript, { encoding: 'utf-8' });
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return dest;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Workspace directory for one trial — delta #7 of the S0.3 spike.
|
|
74
|
+
*
|
|
75
|
+
* Keyed by `task|arm|seed`, not by task alone. The old task-only key meant every
|
|
76
|
+
* arm and every seed of a task reused ONE directory that the next trial deleted,
|
|
77
|
+
* so at the end of a sweep exactly one workspace survived per task — the last one
|
|
78
|
+
* written, with no record of which arm or seed it belonged to.
|
|
79
|
+
*
|
|
80
|
+
* That is not a tidiness problem. Phase 3's anti-golfing gate is specified as
|
|
81
|
+
* retro-fittable onto already-completed runs by offline re-scoring, and the
|
|
82
|
+
* roadmap calls that gate cheap *because* the workspaces are preserved. Under the
|
|
83
|
+
* old key there was nothing to re-score, so the claim was false. A distinct
|
|
84
|
+
* directory per trial is what makes it true.
|
|
85
|
+
*
|
|
86
|
+
* `arm` is sanitised because arm names are used verbatim as a path segment and
|
|
87
|
+
* one of them would otherwise be free to escape the root.
|
|
88
|
+
*/
|
|
89
|
+
export function workspace_dir(task_id: string, arm: string, seed: number): string {
|
|
90
|
+
const safe_arm = arm.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
91
|
+
return path.join(WORK_ROOT, `${task_id}__${safe_arm}__seed${seed}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Copy the task's pristine fixture into this trial's own working clone.
|
|
96
|
+
*
|
|
97
|
+
* The clone is re-created from the fixture on every call, so a resumed or
|
|
98
|
+
* repeated trial still starts pristine — the per-trial key changes *which*
|
|
99
|
+
* directory that is, never whether it is clean.
|
|
100
|
+
*/
|
|
101
|
+
export function reset_fixture(task: Dict, arm: string, seed: number): [string, string] {
|
|
102
|
+
const fixture = path.join(FIXTURES_ROOT, String(task['fixture']));
|
|
103
|
+
const dest = workspace_dir(String(task['id']), arm, seed);
|
|
104
|
+
if (fs.existsSync(dest)) {
|
|
105
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
fs.rmSync(transcript_path_for(dest), { force: true });
|
|
108
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
109
|
+
fs.cpSync(fixture, dest, { recursive: true });
|
|
110
|
+
return [dest, fixture];
|
|
111
|
+
}
|
|
@@ -126,3 +126,176 @@ export function git_common_dir(project_root: string): string | null {
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Workspace identity
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One answer to one identity question, and never a silent default.
|
|
136
|
+
*
|
|
137
|
+
* The census that motivated this type (`agents/evidence/analysis/
|
|
138
|
+
* workspace-identity-census.md`) found three of eight repo-root sites falling
|
|
139
|
+
* back to a value that is *plausible but wrong* when git cannot answer —
|
|
140
|
+
* `process.cwd()`, a path derived from the script's own location, an
|
|
141
|
+
* exception. A caller cannot distinguish those from a real answer, which is
|
|
142
|
+
* how a gate ends up scanning the wrong tree and exiting green.
|
|
143
|
+
*/
|
|
144
|
+
export type IdentityField =
|
|
145
|
+
| { readonly resolved: true; readonly value: string; readonly provenance: string }
|
|
146
|
+
| { readonly resolved: false; readonly reason: string };
|
|
147
|
+
|
|
148
|
+
/** The five workspace-identity questions, each independently resolvable. */
|
|
149
|
+
export interface WorkspaceIdentity {
|
|
150
|
+
/**
|
|
151
|
+
* Top level of the working tree the caller is in — the drop-in for
|
|
152
|
+
* `git rev-parse --show-toplevel`. Inside a linked worktree this is the
|
|
153
|
+
* worktree's own root, exactly as git reports it.
|
|
154
|
+
*/
|
|
155
|
+
readonly repoRoot: IdentityField;
|
|
156
|
+
/**
|
|
157
|
+
* The main (non-linked) checkout. Derived from the common git dir, so it
|
|
158
|
+
* is the same value from every worktree of the repo. This is the field the
|
|
159
|
+
* two shipped misclassification defects needed and did not have.
|
|
160
|
+
*/
|
|
161
|
+
readonly mainWorktree: IdentityField;
|
|
162
|
+
/**
|
|
163
|
+
* The checkout the caller is in. Its `value` equals `repoRoot` by git's own
|
|
164
|
+
* definition; the two fields exist separately because the *provenance*
|
|
165
|
+
* differs and that is the part callers need — `main-checkout` versus
|
|
166
|
+
* `linked-worktree` is precisely the question `isStandardLocation` got
|
|
167
|
+
* wrong twice.
|
|
168
|
+
*/
|
|
169
|
+
readonly currentWorktree: IdentityField;
|
|
170
|
+
/** Branch of the current checkout; unresolved on a detached HEAD. */
|
|
171
|
+
readonly branch: IdentityField;
|
|
172
|
+
/**
|
|
173
|
+
* The remote's default branch, as a `refs/remotes/<remote>/<name>` ref —
|
|
174
|
+
* the offline notion of "what a PR from here would target".
|
|
175
|
+
*
|
|
176
|
+
* Deliberately NOT the `gh`-derived base of an actual open PR: that needs
|
|
177
|
+
* network and auth, and `check_branch_freshness.ts` already owns it. This
|
|
178
|
+
* field answers the question a gate can answer without leaving the disk,
|
|
179
|
+
* and reports `unresolved` rather than guessing `main` when the repo has
|
|
180
|
+
* no recorded default.
|
|
181
|
+
*/
|
|
182
|
+
readonly prBase: IdentityField;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolved(value: string, provenance: string): IdentityField {
|
|
186
|
+
return { resolved: true, value, provenance };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function unresolved(reason: string): IdentityField {
|
|
190
|
+
return { resolved: false, reason };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Walk up from `start` to the first directory holding a `.git` entry.
|
|
195
|
+
*
|
|
196
|
+
* File-based on purpose, like everything else here: `rev-parse --show-toplevel`
|
|
197
|
+
* would be redirected by an inherited `GIT_DIR`, and the symptom is a wrong
|
|
198
|
+
* path rather than an error.
|
|
199
|
+
*/
|
|
200
|
+
function discover_root(start: string): string | null {
|
|
201
|
+
let dir = realpath_or_self(path.resolve(start));
|
|
202
|
+
for (;;) {
|
|
203
|
+
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
204
|
+
const parent = path.dirname(dir);
|
|
205
|
+
if (parent === dir) return null;
|
|
206
|
+
dir = parent;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Is `project_root`'s `.git` a linked-worktree pointer file rather than a
|
|
212
|
+
* directory? Returns `null` when there is no `.git` entry at all.
|
|
213
|
+
*/
|
|
214
|
+
function is_linked_worktree(project_root: string): boolean | null {
|
|
215
|
+
try {
|
|
216
|
+
return !fs.statSync(path.join(project_root, '.git')).isDirectory();
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The remote default branch ref, read from disk. `null` when unrecorded. */
|
|
223
|
+
function remote_head_ref(common_dir: string, remote: string): string | null {
|
|
224
|
+
// A symbolic ref is stored loose; `git pack-refs` never packs one, so
|
|
225
|
+
// packed-refs is not a fallback for this specific ref — a repo that has
|
|
226
|
+
// never run `git remote set-head` simply has no answer on disk.
|
|
227
|
+
try {
|
|
228
|
+
const raw = fs.readFileSync(path.join(common_dir, 'refs', 'remotes', remote, 'HEAD'), 'utf-8');
|
|
229
|
+
const m = raw.match(/^ref:\s*(refs\/remotes\/.+?)\s*$/m);
|
|
230
|
+
return m === null ? null : m[1]!.trim();
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve all five workspace-identity questions from `start` (default: the
|
|
238
|
+
* process cwd), reading files only.
|
|
239
|
+
*
|
|
240
|
+
* Every field is either a value with its provenance or an explicit reason it
|
|
241
|
+
* could not be resolved. Nothing here throws and nothing guesses.
|
|
242
|
+
*/
|
|
243
|
+
export function workspaceIdentity(
|
|
244
|
+
start: string = process.cwd(),
|
|
245
|
+
remote = 'origin',
|
|
246
|
+
): WorkspaceIdentity {
|
|
247
|
+
const root = discover_root(start);
|
|
248
|
+
if (root === null) {
|
|
249
|
+
const why = `no .git entry in ${path.resolve(start)} or any ancestor`;
|
|
250
|
+
return {
|
|
251
|
+
repoRoot: unresolved(why),
|
|
252
|
+
mainWorktree: unresolved(why),
|
|
253
|
+
currentWorktree: unresolved(why),
|
|
254
|
+
branch: unresolved(why),
|
|
255
|
+
prBase: unresolved(why),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const linked = is_linked_worktree(root);
|
|
260
|
+
const repoRoot = resolved(root, 'upward .git walk from cwd');
|
|
261
|
+
const currentWorktree = resolved(
|
|
262
|
+
root,
|
|
263
|
+
linked === true
|
|
264
|
+
? 'linked-worktree (.git is a gitdir pointer file)'
|
|
265
|
+
: 'main-checkout (.git is a directory)',
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const common = git_common_dir(root);
|
|
269
|
+
let mainWorktree: IdentityField;
|
|
270
|
+
if (common === null) {
|
|
271
|
+
mainWorktree = unresolved('no common git dir resolvable from the repo root');
|
|
272
|
+
} else if (path.basename(common) === '.git') {
|
|
273
|
+
mainWorktree = resolved(path.dirname(common), 'dirname of the common git dir');
|
|
274
|
+
} else {
|
|
275
|
+
// Bare repo, `core.worktree`, or a separate-gitdir layout: the common
|
|
276
|
+
// dir is not `<main>/.git`, so `dirname` would name a directory that is
|
|
277
|
+
// not a checkout. Refusing is the point of the type.
|
|
278
|
+
mainWorktree = unresolved(
|
|
279
|
+
`common git dir ${common} is not named .git — bare or separate-gitdir layout has no main worktree to derive`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const br = current_branch(root);
|
|
284
|
+
const branch = br === null
|
|
285
|
+
? unresolved('detached HEAD, or HEAD unreadable in the per-worktree git dir')
|
|
286
|
+
: resolved(br, 'ref line of <git-dir>/HEAD');
|
|
287
|
+
|
|
288
|
+
let prBase: IdentityField;
|
|
289
|
+
if (common === null) {
|
|
290
|
+
prBase = unresolved('no common git dir resolvable from the repo root');
|
|
291
|
+
} else {
|
|
292
|
+
const ref = remote_head_ref(common, remote);
|
|
293
|
+
prBase = ref === null
|
|
294
|
+
? unresolved(
|
|
295
|
+
`no refs/remotes/${remote}/HEAD on disk — run \`git remote set-head ${remote} -a\`; not guessed`,
|
|
296
|
+
)
|
|
297
|
+
: resolved(ref, `symbolic ref refs/remotes/${remote}/HEAD`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { repoRoot, mainWorktree, currentWorktree, branch, prBase };
|
|
301
|
+
}
|
|
@@ -25,7 +25,16 @@ import fs from "node:fs";
|
|
|
25
25
|
|
|
26
26
|
// Tier → Claude model. The ONLY per-vendor mapping the package maintains
|
|
27
27
|
// (ADR-035 § 3); other agents resolve the tier band to their own line-up.
|
|
28
|
+
//
|
|
29
|
+
// `frontier` was added by ADR-232, which amends ADR-035 § Decision 1 on that
|
|
30
|
+
// record's own reopen condition. The asymmetry that triggered it is worth
|
|
31
|
+
// keeping visible here: `MODEL_FAMILIES` in the orchestration-record hook has
|
|
32
|
+
// enumerated four families since it shipped, while this map named three — so a
|
|
33
|
+
// dispatch on the fourth band was recordable but not declarable. Adding the row
|
|
34
|
+
// here rather than a second table elsewhere is what keeps ADR-035 § 3 intact:
|
|
35
|
+
// one mapping, one owner, no two-clocks drift.
|
|
28
36
|
export const TIER_TO_CLAUDE_MODEL: Record<string, string> = {
|
|
37
|
+
frontier: "fable",
|
|
29
38
|
high: "opus",
|
|
30
39
|
medium: "sonnet",
|
|
31
40
|
lite: "haiku",
|
|
@@ -76,6 +76,91 @@ const _PUBLIC_SURFACE_PREFIXES = [
|
|
|
76
76
|
|
|
77
77
|
const _RULE_OR_SCHEMA_PREFIXES = ['src/rules/', 'src/scripts/schemas/'];
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Paths whose repair is a correctness fix to shipped *behaviour* rather than a
|
|
81
|
+
* record of one. The distinction is load-bearing: 43 % of `fix(...)` commits
|
|
82
|
+
* over the six spans measured in
|
|
83
|
+
* `agents/evidence/analysis/release-head-derivation-recall.md` touch no
|
|
84
|
+
* executable file at all — `52d7fe1 fix(worktrees): the inventory
|
|
85
|
+
* misclassifies from inside a worktree, totally` changes two markdown files and
|
|
86
|
+
* repairs nothing. Keying on the touched surface is what separates a bug fix
|
|
87
|
+
* from a bug report, and it moves hand-judged precision from 54 % to 96 %.
|
|
88
|
+
*/
|
|
89
|
+
const _EXECUTABLE_SUFFIX = /\.(?:ts|tsx|js|mjs|cjs|sh|py)$/u;
|
|
90
|
+
const _EXECUTABLE_PREFIXES = ['.github/workflows/', '.github/actions/'];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Shebang entry points with no extension, which a suffix test cannot see. An
|
|
94
|
+
* exact set rather than a heuristic: the alternative is reading file contents,
|
|
95
|
+
* which a span commit does not carry.
|
|
96
|
+
*
|
|
97
|
+
* The recall limit that remains, named rather than left for a reader to
|
|
98
|
+
* discover: a NEW extensionless entry point is invisible until it is added
|
|
99
|
+
* here. Precision was measured over six spans; this predicate's recall was not,
|
|
100
|
+
* and adding to this set is the maintenance that follows a new entry point.
|
|
101
|
+
*/
|
|
102
|
+
const _EXECUTABLE_EXACT: ReadonlySet<string> = new Set([
|
|
103
|
+
'scripts-run',
|
|
104
|
+
'src/scripts/agent-config',
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
function _touches_executable(files: SpanCommit['files']): boolean {
|
|
108
|
+
return files.some(
|
|
109
|
+
(f) =>
|
|
110
|
+
_EXECUTABLE_SUFFIX.test(f.path) ||
|
|
111
|
+
_EXECUTABLE_EXACT.has(f.path) ||
|
|
112
|
+
_EXECUTABLE_PREFIXES.some((p) => f.path.startsWith(p)),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Conventional-commit type, lower-cased, or `''` when the subject carries no
|
|
118
|
+
* conventional prefix.
|
|
119
|
+
*
|
|
120
|
+
* Two things this deliberately does, both of which an earlier version got
|
|
121
|
+
* wrong. It requires the conventional `:` separator, so `fix the flaky runner`
|
|
122
|
+
* is not classified as type `fix` — otherwise this function disagrees with the
|
|
123
|
+
* scope regex three lines below it and with every prose description of the
|
|
124
|
+
* rule. And it recognises `Revert "<subject>"`, the form `git revert` writes by
|
|
125
|
+
* default, which is not conventional at all: without it the documented revert
|
|
126
|
+
* half only ever applied to a hand-written `revert(scope):`, and a revert of
|
|
127
|
+
* shipped behaviour is the commit class most likely to be a correctness repair.
|
|
128
|
+
*/
|
|
129
|
+
function _commit_type(subject: string): string {
|
|
130
|
+
if (/^Revert\s+"/u.test(subject)) {
|
|
131
|
+
return 'revert';
|
|
132
|
+
}
|
|
133
|
+
const m = /^(\w+)(?:\([^)]*\))?!?:/u.exec(subject);
|
|
134
|
+
return m ? m[1]!.toLowerCase() : '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recorded-null forms beyond the literal marker, taken from real subjects and
|
|
139
|
+
* bodies over the six measured spans rather than invented: a waived-rather-than-
|
|
140
|
+
* met condition, a published or recorded null, an archival on a roadmap's own
|
|
141
|
+
* falsifier. All six commits these add across the six spans are true positives.
|
|
142
|
+
*/
|
|
143
|
+
const _NULL_FORMS: readonly RegExp[] = [
|
|
144
|
+
/honest[ -]null/iu,
|
|
145
|
+
/\bwaived,? (?:rather than|not) met\b/iu,
|
|
146
|
+
/\bsoak was waived\b/iu,
|
|
147
|
+
// The trailing lookahead keeps a recorded *result* apart from a field that
|
|
148
|
+
// merely holds null. This pattern is applied to the body as well as the
|
|
149
|
+
// subject, and a body is where "records null token_delta" appears — a
|
|
150
|
+
// telemetry field, not a null result. Excluding an identifier-shaped token
|
|
151
|
+
// after `null` costs nothing real: "records the null result" and "closed as
|
|
152
|
+
// a published null" both still derive. The lookahead is deliberately
|
|
153
|
+
// SAME-LINE (`[ \t]`, never `\s`): the first version used `\s+` and dropped
|
|
154
|
+
// a true positive, because 92f9b9a ends its subject on "closed as a
|
|
155
|
+
// published null" and its body then opens with `count_open reached 0` — an
|
|
156
|
+
// identifier on the NEXT line, which `\s+` happily crossed. The residual
|
|
157
|
+
// limit, named rather than left to be discovered: a field name with no
|
|
158
|
+
// underscore ("records null delta") is still caught.
|
|
159
|
+
/\b(?:publish(?:es|ed|ing)?|record(?:s|ed|ing)?|report(?:s|ed|ing)?) (?:the |an |a |its )?(?:honest )?null\b(?![ \t]+`?[a-z][a-z0-9]*_[a-z0-9_]*)/iu,
|
|
160
|
+
/\bnull(?: result)? (?:stands|is the answer)\b/iu,
|
|
161
|
+
/\bon (?:its|the) own falsifier\b/iu,
|
|
162
|
+
];
|
|
163
|
+
|
|
79
164
|
/**
|
|
80
165
|
* Why a label fired, in generator-facing prose. Keyed by label; a label absent
|
|
81
166
|
* here is never derived (`Known limitations` is pure prose, not checkable).
|
|
@@ -83,25 +168,37 @@ const _RULE_OR_SCHEMA_PREFIXES = ['src/rules/', 'src/scripts/schemas/'];
|
|
|
83
168
|
const _DERIVED_REASON: Readonly<Record<string, string>> = {
|
|
84
169
|
'Behaviour changes': 'rule/schema diffs, breaking commits or removed public surface',
|
|
85
170
|
'Default changes + migration': 'commits naming a default, migration or migrate',
|
|
86
|
-
'Security and correctness': 'security-scoped commits',
|
|
87
|
-
'Honest nulls': 'commits
|
|
171
|
+
'Security and correctness': 'security-scoped commits or fixes to executable surface',
|
|
172
|
+
'Honest nulls': 'commits recording a null, waived or falsified result',
|
|
88
173
|
};
|
|
89
174
|
|
|
90
175
|
/**
|
|
91
176
|
* Derive evidence per curated label from the span. Rules per label:
|
|
92
177
|
*
|
|
93
178
|
* - Security and correctness: conventional scope matching /secur/i, or the
|
|
94
|
-
* whole word "security" in the subject
|
|
179
|
+
* whole word "security" in the subject — plus the *correctness* half the
|
|
180
|
+
* label names: a `fix` or `revert` commit touching executable surface.
|
|
95
181
|
* - Behaviour changes: breaking (`!` / BREAKING CHANGE) commits, diffs
|
|
96
182
|
* touching `src/rules/` or `src/scripts/schemas/`, and deletions under the
|
|
97
183
|
* public artefact trees (removed public surface).
|
|
98
184
|
* - Default changes + migration: subject carrying the whole word "default",
|
|
99
185
|
* "migration", or "migrate".
|
|
100
|
-
* - Honest nulls: subject or body carrying
|
|
186
|
+
* - Honest nulls: subject or body carrying one of the recorded-null forms —
|
|
187
|
+
* the literal marker, a waived-rather-than-met condition, a published or
|
|
188
|
+
* recorded null, an archival on a roadmap's own falsifier.
|
|
101
189
|
* - Known limitations: never derived — pure prose, not gate-checkable.
|
|
102
190
|
*
|
|
103
191
|
* Derivation is deliberately conservative: a false red makes every release
|
|
104
|
-
* annoying, a miss only returns the head to the pre-gate state.
|
|
192
|
+
* annoying, a miss only returns the head to the pre-gate state. Conservative
|
|
193
|
+
* is not the same as silent, and two labels had crossed that line: measured
|
|
194
|
+
* over the six spans in
|
|
195
|
+
* `agents/evidence/analysis/release-head-derivation-recall.md`, `Security and
|
|
196
|
+
* correctness` fired **1 of 45** hand-confirmed in-category commits and
|
|
197
|
+
* `Honest nulls` **3 of 9**, so the
|
|
198
|
+
* curated `_none_` shipped uncontested where the span carried the evidence.
|
|
199
|
+
* The widening is aimed at those two and measured before it landed; the naive
|
|
200
|
+
* "any `fix(` counts" alternative was rejected on data (54 % precision against
|
|
201
|
+
* 96 %), not on taste.
|
|
105
202
|
*/
|
|
106
203
|
export function derive_category_hits(
|
|
107
204
|
commits: readonly SpanCommit[],
|
|
@@ -113,7 +210,10 @@ export function derive_category_hits(
|
|
|
113
210
|
for (const c of commits) {
|
|
114
211
|
const scopeMatch = /^\w+\(([^)]*)\)!?:/u.exec(c.subject);
|
|
115
212
|
const scope = scopeMatch ? scopeMatch[1]! : '';
|
|
116
|
-
|
|
213
|
+
const type = _commit_type(c.subject);
|
|
214
|
+
const correctnessFix =
|
|
215
|
+
(type === 'fix' || type === 'revert') && _touches_executable(c.files);
|
|
216
|
+
if (/secur/iu.test(scope) || /\bsecurity\b/iu.test(c.subject) || correctnessFix) {
|
|
117
217
|
out['Security and correctness']!.push({ sha: c.sha, text: c.subject });
|
|
118
218
|
}
|
|
119
219
|
const removedPublic = c.files.filter(
|
|
@@ -134,7 +234,8 @@ export function derive_category_hits(
|
|
|
134
234
|
if (/\b(default|migration|migrate)\b/iu.test(c.subject)) {
|
|
135
235
|
out['Default changes + migration']!.push({ sha: c.sha, text: c.subject });
|
|
136
236
|
}
|
|
137
|
-
|
|
237
|
+
const subjectAndBody = `${c.subject}\n${c.body}`;
|
|
238
|
+
if (_NULL_FORMS.some((re) => re.test(subjectAndBody))) {
|
|
138
239
|
out['Honest nulls']!.push({ sha: c.sha, text: c.subject });
|
|
139
240
|
}
|
|
140
241
|
}
|