@event4u/agent-config 14.6.0 → 14.7.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 +260 -7
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/roadmap/process-full.md +116 -30
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +135 -104
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +3 -0
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +45 -2
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
- package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-JZ4NR2TW.js} +338 -0
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_conformance.js +459 -709
- package/dist/cli-delegate/cmd_doctor.js +6 -6
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +7 -6
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +7 -6
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +7 -6
- package/dist/cli-delegate/cmd_uninstall.js +9 -7
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +7 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +68 -25
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2901 -2053
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +276 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1744 -1191
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/condense.js +13 -164
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/estate-count-budget.json +339 -3
- package/src/config/gate-violation-baselines.json +41 -41
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +19 -6
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +325 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +6 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +38 -8
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/check_references.ts +60 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_single_delivery.ts +59 -15
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +20 -170
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +782 -20
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +6 -0
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +7 -0
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Class-B path (road-to-org-telemetry Phase 5, step 5.2).
|
|
3
|
+
*
|
|
4
|
+
* Class A is the automatic half: structural fields, no content, shipped under
|
|
5
|
+
* recorded org consent. Class B is an abstracted CASE — what was expected
|
|
6
|
+
* versus what happened, and which artefacts were involved — and it ships only
|
|
7
|
+
* on explicit per-case approval of the exact text that would leave.
|
|
8
|
+
*
|
|
9
|
+
* THE GATE IS THE HUMAN READING THE OUTBOUND TEXT, NOT A FILTER. Rank 1 of the
|
|
10
|
+
* roadmap risk register is "an abstraction miss ships a path or an identifier
|
|
11
|
+
* out of a consumer project", and its stated mitigation is per-case review of
|
|
12
|
+
* the exact outbound text rather than a filter heuristic. So this module's job
|
|
13
|
+
* is to make the exact bytes visible and then refuse to release them unless the
|
|
14
|
+
* approval names those same bytes. `approve` takes a digest of the rendered
|
|
15
|
+
* text; a text that changed after the human read it no longer matches, and the
|
|
16
|
+
* release refuses rather than shipping something nobody approved.
|
|
17
|
+
*
|
|
18
|
+
* SILENT SHIPPING IS PERMANENTLY OUT OF SCOPE, and the step says so in those
|
|
19
|
+
* words. There is deliberately no "auto-approve", no allow-list of
|
|
20
|
+
* self-evidently safe classes, and no timeout that approves by default.
|
|
21
|
+
*
|
|
22
|
+
* TWO EXISTING PRIMITIVES ARE REUSED RATHER THAN RE-INVENTED:
|
|
23
|
+
* - `renderReport` is "the existing symptom format" the step names.
|
|
24
|
+
* - `egressBlockedReason` is the audited privacy floor — the same one the
|
|
25
|
+
* local corpus write and the upstream release already pass through. It
|
|
26
|
+
* REFUSES rather than rewriting, which is the property that makes it a gate;
|
|
27
|
+
* a module that silently scrubbed and shipped would be a soft gate wearing
|
|
28
|
+
* a hard gate's name.
|
|
29
|
+
*
|
|
30
|
+
* WHAT THIS MODULE DOES NOT DO: send anything. Transport and sink-side storage
|
|
31
|
+
* are Phase 5 step 5.3, transferred with `sink-choice` — no sink exists for an
|
|
32
|
+
* approved case to travel to. `ClassBRecord` is therefore built, gated and
|
|
33
|
+
* held; the field that carries the text is `case_text` and it is a typed
|
|
34
|
+
* member of a typed record, never a string interpolated into a prompt.
|
|
35
|
+
*/
|
|
36
|
+
import * as crypto from 'node:crypto';
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
type DefectRecord,
|
|
40
|
+
egressBlockedReason,
|
|
41
|
+
renderReport,
|
|
42
|
+
} from './self_repair.js';
|
|
43
|
+
|
|
44
|
+
/** Schema version for the Class-B envelope, independent of Class A. */
|
|
45
|
+
export const CLASS_B_SCHEMA_VERSION = 1;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One approved case.
|
|
49
|
+
*
|
|
50
|
+
* `case_text` is the ONLY free-form member and it exists on purpose: Class B
|
|
51
|
+
* is defined as an abstracted case, so a Class-B record without text would be
|
|
52
|
+
* a Class-A record. The controls are that it is (a) a named, typed field
|
|
53
|
+
* rather than an open `payload`, (b) present only after `approve` matched a
|
|
54
|
+
* digest a human read, and (c) documented as data end to end — see
|
|
55
|
+
* `assertNeverInterpolated`.
|
|
56
|
+
*/
|
|
57
|
+
export interface ClassBRecord {
|
|
58
|
+
schema_version: number;
|
|
59
|
+
record_class: 'case';
|
|
60
|
+
/** Detector / intake class, from the pinned vocabulary. */
|
|
61
|
+
defect_class: string;
|
|
62
|
+
/** Fingerprint of the local record this case abstracts. */
|
|
63
|
+
fingerprint: string;
|
|
64
|
+
/** SHA-256 of `case_text`, so a consumer can verify what was approved. */
|
|
65
|
+
case_digest: string;
|
|
66
|
+
/** The abstracted case, exactly as the human approved it. */
|
|
67
|
+
case_text: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ClassBRefusal =
|
|
71
|
+
| { ok: false; reason: string; kind: 'privacy-floor' | 'digest-mismatch' | 'empty' };
|
|
72
|
+
|
|
73
|
+
export type ClassBResult = { ok: true; record: ClassBRecord } | ClassBRefusal;
|
|
74
|
+
|
|
75
|
+
/** SHA-256 hex of the exact bytes a human is asked to approve. */
|
|
76
|
+
export function caseDigest(text: string): string {
|
|
77
|
+
return crypto.createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the exact outbound text for one record, or refuse.
|
|
82
|
+
*
|
|
83
|
+
* Refusal comes from the audited privacy floor and is returned as a reason
|
|
84
|
+
* string for the caller to show. It is NOT downgraded to a scrub-and-continue:
|
|
85
|
+
* a record whose evidence trips the floor stays local, which is the same
|
|
86
|
+
* verdict the upstream release path already reaches.
|
|
87
|
+
*/
|
|
88
|
+
export function renderCase(
|
|
89
|
+
record: DefectRecord,
|
|
90
|
+
repoRoot?: string | null,
|
|
91
|
+
): { ok: true; text: string; digest: string } | ClassBRefusal {
|
|
92
|
+
const blocked = egressBlockedReason(record, repoRoot ?? null);
|
|
93
|
+
if (blocked !== null) {
|
|
94
|
+
return { ok: false, reason: blocked, kind: 'privacy-floor' };
|
|
95
|
+
}
|
|
96
|
+
// `issue` is the route whose rendering carries no patch and no repository
|
|
97
|
+
// path — the narrowest of the existing shapes, and the right one for a
|
|
98
|
+
// case that travels to a telemetry sink rather than to a code host.
|
|
99
|
+
const text = renderReport(record, 'issue');
|
|
100
|
+
if (text.trim() === '') {
|
|
101
|
+
return { ok: false, reason: 'nothing to send', kind: 'empty' };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, text, digest: caseDigest(text) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Approve one rendered case for release.
|
|
108
|
+
*
|
|
109
|
+
* `approved_digest` is what the human saw. If the text has changed since —
|
|
110
|
+
* a new occurrence folded in, a re-render, anything — the digests differ and
|
|
111
|
+
* this refuses. That is the difference between approving a case and approving
|
|
112
|
+
* a category.
|
|
113
|
+
*/
|
|
114
|
+
export function approve(
|
|
115
|
+
record: DefectRecord,
|
|
116
|
+
text: string,
|
|
117
|
+
approved_digest: string,
|
|
118
|
+
): ClassBResult {
|
|
119
|
+
const actual = caseDigest(text);
|
|
120
|
+
if (actual !== approved_digest) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
kind: 'digest-mismatch',
|
|
124
|
+
reason:
|
|
125
|
+
'the approved text is not the text that would be sent — '
|
|
126
|
+
+ 're-render, read it again, and approve the new digest',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
record: {
|
|
132
|
+
schema_version: CLASS_B_SCHEMA_VERSION,
|
|
133
|
+
record_class: 'case',
|
|
134
|
+
defect_class: record.defect_class,
|
|
135
|
+
fingerprint: record.fingerprint,
|
|
136
|
+
case_digest: actual,
|
|
137
|
+
case_text: text,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Serialise an approved case as ONE JSON object.
|
|
144
|
+
*
|
|
145
|
+
* This is the whole of "quoted, typed data" (step 5.3's repository-side half):
|
|
146
|
+
* the text is a JSON string value, so a downstream reader parses it into a
|
|
147
|
+
* field. Nothing here concatenates it into a sentence, a prompt, or a shell
|
|
148
|
+
* argument, and rank 2 of the risk register — a crafted complaint steering a
|
|
149
|
+
* generated change — is what that discipline is for.
|
|
150
|
+
*/
|
|
151
|
+
export function serialiseCase(record: ClassBRecord): string {
|
|
152
|
+
return JSON.stringify(record);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Guard for a downstream consumer: refuse a prompt-shaped use of case text.
|
|
157
|
+
*
|
|
158
|
+
* Deliberately a narrow, honest tool. It cannot stop a caller that never asks;
|
|
159
|
+
* what it does is give the generation step in Phase 6 something to call, so
|
|
160
|
+
* "reads taxonomy fields only, never Class-B free text" is a check rather than
|
|
161
|
+
* a convention. The intended use is at the boundary where a record becomes
|
|
162
|
+
* model input.
|
|
163
|
+
*/
|
|
164
|
+
export function assertNeverInterpolated(candidate: string, record: ClassBRecord): void {
|
|
165
|
+
if (record.case_text !== '' && candidate.includes(record.case_text)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'Class-B case text must reach a model as quoted data, never as part '
|
|
168
|
+
+ 'of an instruction string — pass the record, not a concatenation',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-help rendering for the council CLI.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than in `council_cli.ts` for a measured reason: that file
|
|
5
|
+
* sits ~2,500 lines above the 1,500-line source ceiling, and
|
|
6
|
+
* `check_source_size_budget` is a shrink-only ratchet — every line added there
|
|
7
|
+
* is a new violation, so a 45-line help renderer reds the build. Extraction is
|
|
8
|
+
* the documented response; raising the baseline is not.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT FIXES. `council run --help` printed `usage: agent-config council run
|
|
11
|
+
* [-h] ...` and no flag list at all. Measured 2026-08-20: a first council run
|
|
12
|
+
* cost six calls, two of them spent discovering `--confirm` and `--output` by
|
|
13
|
+
* grepping the CLI source. The information was available at runtime all along —
|
|
14
|
+
* the parser's own spec table carries it — it simply was not printed.
|
|
15
|
+
*
|
|
16
|
+
* Rendered from those specs rather than hand-listed on purpose: a hand-written
|
|
17
|
+
* flag list is a second source of truth that drifts the first time a flag is
|
|
18
|
+
* added.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The shape `council_cli`'s spec table exposes, narrowed to what help needs. */
|
|
22
|
+
export interface HelpSpec {
|
|
23
|
+
readonly positionals: readonly string[];
|
|
24
|
+
readonly opts: readonly { readonly flag: string; readonly takesValue: boolean; readonly choices?: readonly string[] | null }[];
|
|
25
|
+
readonly requiredOpts: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `usageLine` first (its exact text is asserted elsewhere and must not move),
|
|
30
|
+
* then positionals, then every flag with its value shape and a `[required]`
|
|
31
|
+
* marker where the parser demands one.
|
|
32
|
+
*/
|
|
33
|
+
export function renderSubHelp(_cmd: string, usageLine: string, spec: HelpSpec): string {
|
|
34
|
+
const lines: string[] = [usageLine.trimEnd()];
|
|
35
|
+
if (spec.positionals.length > 0) {
|
|
36
|
+
lines.push('', 'positional arguments:');
|
|
37
|
+
for (const p of spec.positionals) {
|
|
38
|
+
lines.push(` ${p}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (spec.opts.length > 0) {
|
|
42
|
+
lines.push('', 'options:');
|
|
43
|
+
const required = new Set(spec.requiredOpts);
|
|
44
|
+
for (const o of spec.opts) {
|
|
45
|
+
const value = o.takesValue
|
|
46
|
+
? o.choices && o.choices.length > 0
|
|
47
|
+
? ` {${o.choices.join(',')}}`
|
|
48
|
+
: ' VALUE'
|
|
49
|
+
: '';
|
|
50
|
+
lines.push(` ${o.flag}${value}${required.has(o.flag) ? ' [required]' : ''}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push('');
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
@@ -29,7 +29,14 @@
|
|
|
29
29
|
* - post_tool_use → inspect tool + command, record verifications
|
|
30
30
|
* - stop → record stop fired (claim-done window)
|
|
31
31
|
*
|
|
32
|
-
* Output: `agents/state/verify-before-complete
|
|
32
|
+
* Output: `agents/state/verify-before-complete/<sha256(session_id)>.json`
|
|
33
|
+
* — ONE FILE PER SESSION. It was one file per project root until 2026-08-20,
|
|
34
|
+
* which under this repo's worktree workflow (`CLAUDE_PROJECT_DIR` resolves to
|
|
35
|
+
* the PARENT checkout) meant one file across every concurrent run: a
|
|
36
|
+
* neighbour's CI witness and verification counters became this run's, and the
|
|
37
|
+
* in-file session-boundary reset in `_update` turned into the damage rather
|
|
38
|
+
* than the defense, because two live runs then clear each other in a loop.
|
|
39
|
+
* Consumers address it via `statePathFor`, never a path literal.
|
|
33
40
|
* {
|
|
34
41
|
* "schema_version": 1,
|
|
35
42
|
* "session_id": "<str>",
|
|
@@ -49,14 +56,79 @@ import fs from "node:fs";
|
|
|
49
56
|
import path from "node:path";
|
|
50
57
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
51
58
|
|
|
52
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
has_stable_session_id,
|
|
61
|
+
prune_legacy_state_file,
|
|
62
|
+
prune_stale_session_states,
|
|
63
|
+
session_state_file,
|
|
64
|
+
update_json_under_lock,
|
|
65
|
+
} from "./hooks/state_io.js";
|
|
53
66
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
54
67
|
|
|
55
68
|
// NOTE: the Python docstring says `agents/runtime/state/`, but the code
|
|
56
69
|
// constant is `agents/state/`. Replicated verbatim — latent docstring/code
|
|
57
70
|
// divergence in the retired Python implementation (ADR-200 § replicate latent bugs).
|
|
71
|
+
//
|
|
72
|
+
// PRE-SPLIT PATH. Nothing reads it after the per-session split below;
|
|
73
|
+
// `prune_legacy_state_file` removes it once this version owns the tree. Kept
|
|
74
|
+
// exported because `prune_legacy_state_file` needs to name the path it deletes,
|
|
75
|
+
// and because a reader that still resolves it should get a compile-visible
|
|
76
|
+
// symbol rather than a silently stale path literal. It is NOT kept for an
|
|
77
|
+
// "older bundle still writing during an upgrade" — that phrasing was copied
|
|
78
|
+
// here unchecked and is corrected at `state_io.prune_legacy_state_file`, which
|
|
79
|
+
// carries the measured deployment shape and the one narrow window that remains.
|
|
58
80
|
export const STATE_FILE = path.join("agents", "state", "verify-before-complete.json");
|
|
59
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Per-SESSION state, one file each.
|
|
84
|
+
*
|
|
85
|
+
* WHY, in one sentence: the single file above is shared by every concurrent
|
|
86
|
+
* session under one project root — which in this repo's worktree workflow is
|
|
87
|
+
* the PARENT checkout — so a neighbouring run's CI witness and verification
|
|
88
|
+
* counters became this run's.
|
|
89
|
+
*
|
|
90
|
+
* The in-file session-boundary reset in `_update` is NOT the fix and is the
|
|
91
|
+
* reason this was easy to miss. It is written for SEQUENTIAL sessions: notice a
|
|
92
|
+
* foreign `session_id`, clear the session-scoped counters, carry on. Under
|
|
93
|
+
* CONCURRENT sessions that same code is the damage — each run reads the other's
|
|
94
|
+
* id, resets, and writes, so two sessions erase each other's verification
|
|
95
|
+
* evidence in a loop. The direction of the loss is toward FORGETTING a
|
|
96
|
+
* verification that did happen, which then reads as "not verified this turn".
|
|
97
|
+
*
|
|
98
|
+
* The reset stays, but NOT for the reasons this comment first gave. It said the
|
|
99
|
+
* reset "still covers the id-less bucket and a legacy file", and a cross-model
|
|
100
|
+
* review (2026-08-20, both seats) showed neither is reachable: an id-less
|
|
101
|
+
* envelope returns from `run()` before `_update` is ever called, and the legacy
|
|
102
|
+
* file is never LOADED any more — only deleted. Writing an unreachable
|
|
103
|
+
* justification next to retained code is how dead logic survives review, so the
|
|
104
|
+
* real one is stated instead.
|
|
105
|
+
*
|
|
106
|
+
* Its one reachable case is a file at THIS session's digest path carrying
|
|
107
|
+
* somebody else's `session_id` — a copy, a restore, a hand-edit, or a buggy
|
|
108
|
+
* writer. That is integrity recovery on the producer side, and it pairs with the
|
|
109
|
+
* consumer side refusing the same file outright (`owns_session_state`). Both
|
|
110
|
+
* halves are needed: the producer cannot refuse to run, and the consumer cannot
|
|
111
|
+
* repair. `before_complete_session_isolation.test.ts` reaches it directly.
|
|
112
|
+
*
|
|
113
|
+
* Rationale, the digest-not-sanitiser property, and the claim-then-revalidate
|
|
114
|
+
* prune all live once in `hooks/state_io.ts` § Per-session concern state — this
|
|
115
|
+
* is the second concern to need them, which is what made sharing them right.
|
|
116
|
+
*/
|
|
117
|
+
export const STATE_DIR = path.join("agents", "state", "verify-before-complete");
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Days after which an untouched session's state is pruned.
|
|
121
|
+
*
|
|
122
|
+
* Matches the language hook and the council session-artefact window — a
|
|
123
|
+
* convention match, not a measurement, and stated as such.
|
|
124
|
+
*/
|
|
125
|
+
export const STATE_RETENTION_DAYS = 7;
|
|
126
|
+
|
|
127
|
+
/** Path of one session's state file. */
|
|
128
|
+
export function statePathFor(session_id: string): string {
|
|
129
|
+
return session_state_file(STATE_DIR, session_id);
|
|
130
|
+
}
|
|
131
|
+
|
|
60
132
|
// Tool names across platforms whose `command` / `tool_input.command` field
|
|
61
133
|
// carries a shell command we can inspect. Edit tools are deliberately
|
|
62
134
|
// excluded — they cannot run verification.
|
|
@@ -125,26 +197,6 @@ function _empty_state(): StateDict {
|
|
|
125
197
|
};
|
|
126
198
|
}
|
|
127
199
|
|
|
128
|
-
function _load_state(target: string): StateDict {
|
|
129
|
-
let isFile = false;
|
|
130
|
-
try {
|
|
131
|
-
isFile = fs.statSync(target).isFile();
|
|
132
|
-
} catch {
|
|
133
|
-
isFile = false;
|
|
134
|
-
}
|
|
135
|
-
if (!isFile) {
|
|
136
|
-
return _empty_state();
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const decoded = JSON.parse(fs.readFileSync(target, "utf-8")) as unknown;
|
|
140
|
-
if (typeof decoded === "object" && decoded !== null && !Array.isArray(decoded)) {
|
|
141
|
-
return { ..._empty_state(), ...(decoded as StateDict) };
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// fall through
|
|
145
|
-
}
|
|
146
|
-
return _empty_state();
|
|
147
|
-
}
|
|
148
200
|
|
|
149
201
|
/** Return [tool_name, command_text] from a tool-event payload. */
|
|
150
202
|
function _extract_command(payload: StateDict): [string | null, string | null] {
|
|
@@ -424,19 +476,92 @@ export function run(
|
|
|
424
476
|
}
|
|
425
477
|
|
|
426
478
|
const event = (envelope["event"] || "") as string;
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
479
|
+
const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
|
|
480
|
+
|
|
481
|
+
// No stable identity → persist NOTHING. Sanitising an empty id into a shared
|
|
482
|
+
// literal is the original cross-session defect restored in the one case with
|
|
483
|
+
// no guard left (see `state_io` § has_stable_session_id).
|
|
484
|
+
//
|
|
485
|
+
// What that costs, and it is worse than the word this comment first used: on a
|
|
486
|
+
// host that sends no `session_id` this concern records no evidence at all, so
|
|
487
|
+
// `readCiSettled` returns "nothing observed" and the turn-end gate's completion
|
|
488
|
+
// detector never fires. The first version called that "the SAFE direction —
|
|
489
|
+
// under-refusing". A cross-model review (2026-08-20, both seats) rejected the
|
|
490
|
+
// word: for a BLOCKING gate, not refusing is fail-OPEN. A premature completion
|
|
491
|
+
// claim over unsettled CI passes unchallenged. This is DEGRADED ENFORCEMENT, and
|
|
492
|
+
// naming it "safe" hides the loss behind a reassuring adjective.
|
|
493
|
+
//
|
|
494
|
+
// It is still the right call among the options available, which is a different
|
|
495
|
+
// claim and the only one the evidence supports: the alternative is one shared
|
|
496
|
+
// file whose contents belong to whichever concurrent run wrote last, i.e. a
|
|
497
|
+
// gate that refuses or clears on somebody else's evidence. A gate that goes
|
|
498
|
+
// quiet is recoverable; a gate that acts on a foreign witness is not.
|
|
499
|
+
// Every host this suite binds `post_tool_use` on sends a `session_id`
|
|
500
|
+
// (`hook_manifest.yaml` platforms × `native_event_aliases`), so the degraded
|
|
501
|
+
// path is a fallback rather than a supported mode — but it is not verified per
|
|
502
|
+
// host, and this comment does not claim it is.
|
|
503
|
+
if (!has_stable_session_id(session_id)) {
|
|
504
|
+
if (verbose) {
|
|
505
|
+
process.stderr.write(
|
|
506
|
+
"verify-before-complete-hook: no session_id — running stateless, nothing recorded\n",
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
430
511
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
512
|
+
const target = path.join(consumer_root, statePathFor(session_id));
|
|
513
|
+
|
|
514
|
+
// LOAD → UPDATE → PUBLISH under ONE lock, not three separate steps.
|
|
515
|
+
//
|
|
516
|
+
// This was `_load_state` / `_update` / `atomic_write_json`, which makes the
|
|
517
|
+
// publish atomic and leaves the transaction racy. A cross-model review
|
|
518
|
+
// (2026-08-20, both seats) named the interleaving, and this host runs tool
|
|
519
|
+
// calls in parallel, so it is reachable rather than theoretical: two
|
|
520
|
+
// `post_tool_use` invocations for the SAME session both load the counter at
|
|
521
|
+
// N, both compute N+1, both publish N+1, and one verification is lost. The
|
|
522
|
+
// per-session split does nothing about it — the two failures are independent,
|
|
523
|
+
// and closing the cross-session one made this one easier to mistake for
|
|
524
|
+
// solved.
|
|
525
|
+
//
|
|
526
|
+
// `state` is captured for the verbose line below; the value that lands is the
|
|
527
|
+
// one computed inside the lock, from state read inside the lock.
|
|
528
|
+
let state: StateDict = {};
|
|
529
|
+
// Three-state result (state_io § Three states, not two). This mutator NEVER
|
|
530
|
+
// returns null, so `skipped` is unreachable here — but it is handled with
|
|
531
|
+
// `written` rather than lumped with `failed`, because the two have opposite
|
|
532
|
+
// meanings for this caller: a decline means the state needed no change, a
|
|
533
|
+
// failure means the state on disk is not what this run computed. Writing
|
|
534
|
+
// `!== "written"` would have made a future mutator that learns to decline
|
|
535
|
+
// silently stop recording, which is the failure this API change exists to
|
|
536
|
+
// make impossible to write by accident.
|
|
537
|
+
const outcome = update_json_under_lock<StateDict>(target, (loaded) => {
|
|
538
|
+
// `_empty_state()` UNDER the loaded value, which is what the `_load_state`
|
|
539
|
+
// this replaced did (`{ ..._empty_state(), ...decoded }`). Dropping it would
|
|
540
|
+
// have been a silent regression that the suite could not see: `_asInt`
|
|
541
|
+
// treats a missing counter as 0 and the `=== true` guards treat a missing
|
|
542
|
+
// flag as false, so every assertion would still pass while `schema_version`
|
|
543
|
+
// and an explicit `ci_last: null` vanished from a freshly created file.
|
|
544
|
+
state = _update({ ..._empty_state(), ...loaded } as StateDict, event, envelope);
|
|
545
|
+
return state;
|
|
546
|
+
});
|
|
547
|
+
if (outcome === "failed") {
|
|
434
548
|
if (verbose) {
|
|
435
549
|
process.stderr.write("verify-before-complete-hook: state write failed\n");
|
|
436
550
|
}
|
|
437
551
|
return 0;
|
|
438
552
|
}
|
|
439
553
|
|
|
554
|
+
// Housekeeping on the once-per-turn events only — never on `post_tool_use`,
|
|
555
|
+
// which fires many times per turn, and never before the write above.
|
|
556
|
+
if (event === "session_start" || event === "user_prompt_submit") {
|
|
557
|
+
prune_legacy_state_file(path.join(consumer_root, STATE_FILE));
|
|
558
|
+
prune_stale_session_states(
|
|
559
|
+
path.join(consumer_root, STATE_DIR),
|
|
560
|
+
Date.now(),
|
|
561
|
+
STATE_RETENTION_DAYS,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
440
565
|
if (verbose) {
|
|
441
566
|
process.stderr.write(
|
|
442
567
|
`verify-before-complete-hook: event=${event} ` +
|
|
@@ -35,6 +35,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
35
35
|
|
|
36
36
|
import { parse as parseYaml } from 'yaml';
|
|
37
37
|
|
|
38
|
+
import { PinnedRepoError, pristine_tree_for } from './_lib/bench_ab_pinned_repo.js';
|
|
38
39
|
import { medianComplexityPerChangedFunction } from './_lib/bench_ab_complexity.js';
|
|
39
40
|
import { changed_files, diff_line_counts } from './_lib/bench_ab_scoring_v2.js';
|
|
40
41
|
|
|
@@ -63,15 +64,15 @@ function _dictOr(v: unknown): Dict {
|
|
|
63
64
|
* trial to `skipped_reason: 'task id not in the corpus'`, which is a reportable
|
|
64
65
|
* state, rather than aborting a re-score that may still cover most of the report.
|
|
65
66
|
*/
|
|
66
|
-
function loadCorpusFixtures(corpusPath: string | null): Map<string,
|
|
67
|
-
const byId = new Map<string,
|
|
67
|
+
function loadCorpusFixtures(corpusPath: string | null): Map<string, Dict> {
|
|
68
|
+
const byId = new Map<string, Dict>();
|
|
68
69
|
if (!corpusPath || !fs.existsSync(corpusPath)) return byId;
|
|
69
70
|
try {
|
|
70
71
|
const corpus = _dictOr(parseYaml(fs.readFileSync(corpusPath, 'utf8'), { version: '1.1' }));
|
|
71
72
|
const tasks = Array.isArray(corpus['tasks']) ? (corpus['tasks'] as Dict[]) : [];
|
|
72
73
|
for (const t of tasks) {
|
|
73
|
-
if (t['id'] !== undefined && t['fixture'] !== undefined) {
|
|
74
|
-
byId.set(String(t['id']),
|
|
74
|
+
if (t['id'] !== undefined && (t['fixture'] !== undefined || t['repo'] !== undefined)) {
|
|
75
|
+
byId.set(String(t['id']), t);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
} catch {
|
|
@@ -109,7 +110,7 @@ export async function rescoreReport(
|
|
|
109
110
|
|
|
110
111
|
for (const rec of records) {
|
|
111
112
|
const taskId = String(rec['id'] ?? '<unknown>');
|
|
112
|
-
const
|
|
113
|
+
const corpusTask = byId.get(taskId) ?? null;
|
|
113
114
|
const arms = _dictOr(rec['arms']);
|
|
114
115
|
for (const [arm, runsRaw] of Object.entries(arms)) {
|
|
115
116
|
const runs = Array.isArray(runsRaw) ? (runsRaw as Dict[]) : [];
|
|
@@ -134,12 +135,31 @@ export async function rescoreReport(
|
|
|
134
135
|
out.push(row);
|
|
135
136
|
continue;
|
|
136
137
|
}
|
|
137
|
-
if (
|
|
138
|
+
if (corpusTask === null) {
|
|
138
139
|
row.skipped_reason = 'task id not in the corpus';
|
|
139
140
|
out.push(row);
|
|
140
141
|
continue;
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
+
// Delta #9: a pinned task's pristine tree is a local cache entry, and
|
|
144
|
+
// this re-scorer is `offlineOnly` on purpose — a cold cache reports
|
|
145
|
+
// "not materialised" rather than fetching a repository during what is
|
|
146
|
+
// meant to be an offline re-score of a finished sweep. A malformed pin
|
|
147
|
+
// is reported per trial, never thrown: one bad corpus entry must not
|
|
148
|
+
// abort a re-score that still covers every other trial.
|
|
149
|
+
let fixture: string | null;
|
|
150
|
+
try {
|
|
151
|
+
fixture = pristine_tree_for(corpusTask, { fixturesRoot, offlineOnly: true });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
row.skipped_reason =
|
|
154
|
+
err instanceof PinnedRepoError ? `invalid pin: ${err.message}` : 'invalid corpus task';
|
|
155
|
+
out.push(row);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (fixture === null) {
|
|
159
|
+
row.skipped_reason = 'pinned repo not materialised locally';
|
|
160
|
+
out.push(row);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
143
163
|
if (!fs.existsSync(fixture)) {
|
|
144
164
|
row.skipped_reason = 'fixture missing on disk';
|
|
145
165
|
out.push(row);
|
|
@@ -473,6 +473,57 @@ function _extract_personas_frontmatter(text: string): Array<[number, string]> {
|
|
|
473
473
|
return results;
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Paths this checker must not call broken because nothing is allowed to fix them.
|
|
478
|
+
*
|
|
479
|
+
* `archive_completed_roadmaps` treats `agents/evidence/**` and every `*.patch` as
|
|
480
|
+
* a FROZEN RECORD and deliberately does not rewrite path strings there — a review
|
|
481
|
+
* artefact is a record of what a reviewer was shown, and editing it retroactively
|
|
482
|
+
* makes it claim something that did not happen (measured 2026-08-11: one archival
|
|
483
|
+
* rewrote four frozen artefacts including a `diff.patch` header, and reported it
|
|
484
|
+
* as "4 ref(s) migrated"). So when a roadmap is archived, a record naming its old
|
|
485
|
+
* path keeps naming it — correctly.
|
|
486
|
+
*
|
|
487
|
+
* Without this carve-out the two gates contradict each other: the sweep forbids
|
|
488
|
+
* the rewrite and this checker reds the un-rewritten path, so EVERY archival of a
|
|
489
|
+
* roadmap a review artefact mentions turns CI red with no compliant fix available.
|
|
490
|
+
* Measured on the change that made archival automatic: 2 such references.
|
|
491
|
+
*
|
|
492
|
+
* Deliberately narrow — three conditions, all required. The source is a frozen
|
|
493
|
+
* record; the reference is a top-level roadmap path; and the file is still
|
|
494
|
+
* findable under a terminal disposition. A frozen record pointing at a roadmap
|
|
495
|
+
* that exists NOWHERE is still broken and still reds, which is the part a blanket
|
|
496
|
+
* `agents/evidence/reviews` exclusion would have lost: that directory holds live
|
|
497
|
+
* `*.findings.md` work alongside archived records, so excluding it wholesale would
|
|
498
|
+
* stop checking documents that are still being acted on.
|
|
499
|
+
*
|
|
500
|
+
* The frozen-record predicate is duplicated from `_is_frozen_record` in
|
|
501
|
+
* `src/agent-src/scripts/archive_completed_roadmaps.ts` rather than imported: that
|
|
502
|
+
* module is projected into consumer installs and pulls the dashboard generator in
|
|
503
|
+
* with it, so a package-internal gate importing it would drag the projection layer
|
|
504
|
+
* into CI's gate path. `tests/scripts/check_references.test.ts` holds the two
|
|
505
|
+
* copies against one table so the duplication cannot drift silently.
|
|
506
|
+
*/
|
|
507
|
+
const _FROZEN_RECORD_PREFIXES = ['agents/evidence/'];
|
|
508
|
+
const _TERMINAL_ROADMAP_DIRS = ['archive', 'skipped', 'later'];
|
|
509
|
+
|
|
510
|
+
export function _is_frozen_record_source(rel: string): boolean {
|
|
511
|
+
const norm = rel.split(path.sep).join('/');
|
|
512
|
+
return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith('.patch');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Does `ref` name a top-level roadmap that now lives under a terminal disposition? */
|
|
516
|
+
export function _resolves_under_terminal_disposition(ref: string, root: string): boolean {
|
|
517
|
+
const norm = ref.split(path.sep).join('/');
|
|
518
|
+
const m = /^agents\/roadmaps\/([^/]+\.md)$/.exec(norm);
|
|
519
|
+
if (m === null) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
return _TERMINAL_ROADMAP_DIRS.some((d) =>
|
|
523
|
+
_exists(path.join(root, 'agents', 'roadmaps', d, m[1] as string)),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
476
527
|
function _find_suggestion(p: string, root: string): string {
|
|
477
528
|
const name = path.basename(p);
|
|
478
529
|
// DEAD-ROOT REPAIR (road-to-renewal-foundation Phase 1): the middle entry
|
|
@@ -664,6 +715,15 @@ function check_file(
|
|
|
664
715
|
// check above already swallows refs into `agents/runtime/`,
|
|
665
716
|
// so no extra carve-out is needed.
|
|
666
717
|
}
|
|
718
|
+
if (
|
|
719
|
+
!resolved &&
|
|
720
|
+
_is_frozen_record_source(_relPosix(root, filepath)) &&
|
|
721
|
+
_resolves_under_terminal_disposition(rawRef, root)
|
|
722
|
+
) {
|
|
723
|
+
// A record naming the pre-archival path of a roadmap that is
|
|
724
|
+
// still findable. Nothing may rewrite it — see above.
|
|
725
|
+
resolved = true;
|
|
726
|
+
}
|
|
667
727
|
if (!resolved) {
|
|
668
728
|
broken.push(
|
|
669
729
|
makeBrokenRef(
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* matching git tag, and npm's `latest` therefore lags behind main.
|
|
14
14
|
*
|
|
15
15
|
* Two independent invariants:
|
|
16
|
-
* 1. Tag invariant
|
|
17
|
-
*
|
|
16
|
+
* 1. Tag invariant: the version in `package.json` MUST have a matching git
|
|
17
|
+
* tag ON THE REMOTE. A local-only tag published nothing — `publish-npm.yml`
|
|
18
|
+
* triggers on `push: tags:` — so the probe asks `origin` (one `ls-remote`).
|
|
18
19
|
* 2. npm invariant (`--check-npm`, network): `npm view <pkg>
|
|
19
20
|
* dist-tags.latest` MUST equal the `package.json` version.
|
|
20
21
|
*
|
|
@@ -51,13 +52,32 @@ function _package_name(): string {
|
|
|
51
52
|
return String(data['name']);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* True when the tag is PUBLISHED — present on `origin`, never merely local.
|
|
57
|
+
*
|
|
58
|
+
* This probe used to answer local-OR-remote, and that made the gate reply
|
|
59
|
+
* "tagged, therefore published" to the exact state it exists to detect. A tag
|
|
60
|
+
* created by `release.ts` step 8 whose push then failed sits in the local
|
|
61
|
+
* repository while nothing has shipped: `publish-npm.yml` triggers on
|
|
62
|
+
* `push: tags:`, so a tag that never reached the remote published nothing.
|
|
63
|
+
* Measured 2026-08-20 on 14.6.0 — main carried `package.json` 14.6.0, the tag
|
|
64
|
+
* existed only in the maintainer's checkout, and npm still served 14.5.0.
|
|
65
|
+
*
|
|
66
|
+
* In CI the old form happened to be harmless: `actions/checkout` brings no
|
|
67
|
+
* unpushed local tag, so the remote arm decided. The hole was LOCAL — a
|
|
68
|
+
* maintainer running `task check-release-published` (or `task preflight`) on
|
|
69
|
+
* the machine that minted the tag got a green for an unpublished release, on
|
|
70
|
+
* the one machine where the stuck state is visible. The sibling defect in
|
|
71
|
+
* `release.ts::_detect_in_flight_target` is the same construct.
|
|
72
|
+
*
|
|
73
|
+
* @param git Seam for the probe. Production passes nothing and gets the real
|
|
74
|
+
* `_git`; tests inject a stub, because the alternative — reaching the branches
|
|
75
|
+
* only by creating and deleting tags in the checkout — is why neither arm of
|
|
76
|
+
* this function had a test while both were wrong.
|
|
77
|
+
*/
|
|
78
|
+
function _tag_exists(tag: string, git: (...args: string[]) => [number, string] = _git): boolean {
|
|
79
|
+
const [rc] = git('ls-remote', '--exit-code', '--tags', 'origin', tag);
|
|
80
|
+
return rc === 0;
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
function _on_main(): boolean {
|
|
@@ -179,8 +199,9 @@ function main(argv: readonly string[] = [], hooks: Hooks = _hooks): number {
|
|
|
179
199
|
if (!hooks._tag_exists(version)) {
|
|
180
200
|
problems.push(
|
|
181
201
|
`package.json is ${version} but no git tag ${version} exists ` +
|
|
182
|
-
`
|
|
183
|
-
`
|
|
202
|
+
`on origin — the release was bumped/merged but never tagged, ` +
|
|
203
|
+
`or the tag push never landed. Complete it: tag the ` +
|
|
204
|
+
`release-merge commit and push ` +
|
|
184
205
|
`(triggers publish-npm.yml), e.g. \`git tag ${version} && git ` +
|
|
185
206
|
`push origin ${version}\`.`,
|
|
186
207
|
);
|