@event4u/agent-config 9.16.0 → 9.17.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/AGENTS.md +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/architecture.md +2 -2
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +1 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3194 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +31 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +602 -507
- package/dist/install/install.mjs +23 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +13 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +22 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +30 -0
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +9 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +325 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +23 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* - All headings from source present in condensed
|
|
15
15
|
* - All code blocks preserved exactly
|
|
16
16
|
* - YAML frontmatter identical
|
|
17
|
-
* - Word count reduction
|
|
17
|
+
* - Word count: excessive-reduction alarm only (> 60% lost warns; expected
|
|
18
|
+
* reduction is 0% post-ADR-201 verbatim copy — see the note near the check)
|
|
18
19
|
* - Iron Law sections (## Iron Law / ### Iron Law / ## The Iron Law /
|
|
19
20
|
* Iron Laws / numbered) preserved per `preservation-guard`: heading
|
|
20
21
|
* verbatim at original level, structural-unit survival.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding-disposition gate (release-truth Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* The recorded failure: the 9.14.0 release-PR self-review reported a critical
|
|
5
|
+
* symlink-traversal finding; the PR merged, the release head said "Security
|
|
6
|
+
* and correctness: none", and whether the finding was fixed or adjudicated
|
|
7
|
+
* false-positive was not reconstructable from the record.
|
|
8
|
+
*
|
|
9
|
+
* The record is a COMMITTED ledger — `agents/evidence/release-findings/
|
|
10
|
+
* <version>.json` — never the PR comment (council 2026-08-03: a comment is
|
|
11
|
+
* mutable and unaudited; it is transport, not a record). Every blocking/high
|
|
12
|
+
* finding in the ledger must carry a complete disposition
|
|
13
|
+
* `{status: fixed|false_positive|accepted_risk, commit (when fixed),
|
|
14
|
+
* rationale, verified_by}`; the release workflow is red while one does not.
|
|
15
|
+
*
|
|
16
|
+
* Modes:
|
|
17
|
+
* --release X.Y.Z [--dir <ledger dir>] validate the committed ledger
|
|
18
|
+
* --pr <n> additionally treat the gate's PR
|
|
19
|
+
* comment (machine block) as a TRIGGER: every blocking finding the
|
|
20
|
+
* comment reports must exist in the ledger — an un-ingested blocking
|
|
21
|
+
* finding is red. Deleting the comment can evade the trigger, but the
|
|
22
|
+
* committed ledger stays the durable record either way.
|
|
23
|
+
* --ingest <findings.json> --release X.Y.Z merge findings (from
|
|
24
|
+
* self_review_gate --findings-out) into the ledger with an empty
|
|
25
|
+
* disposition, so validation is red until a human fills it.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 complete · 1 missing/incomplete dispositions · 2 usage or
|
|
28
|
+
* schema error. Deliberately NOT fail-open: this script never calls a model;
|
|
29
|
+
* a broken ledger file is a hard error, not a neutral skip.
|
|
30
|
+
*/
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import process from 'node:process';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
38
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
39
|
+
|
|
40
|
+
export const LEDGER_DIR = 'agents/evidence/release-findings';
|
|
41
|
+
export const DISPOSITION_STATUSES = ['fixed', 'false_positive', 'accepted_risk'] as const;
|
|
42
|
+
export type DispositionStatus = (typeof DISPOSITION_STATUSES)[number];
|
|
43
|
+
|
|
44
|
+
export interface LedgerFinding {
|
|
45
|
+
finding_id: string;
|
|
46
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
47
|
+
kind: 'security' | 'claim' | 'correctness' | 'style';
|
|
48
|
+
title: string;
|
|
49
|
+
file?: string | null;
|
|
50
|
+
status?: string;
|
|
51
|
+
commit?: string;
|
|
52
|
+
rationale?: string;
|
|
53
|
+
verified_by?: string;
|
|
54
|
+
date?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Ledger {
|
|
58
|
+
schema_version: number;
|
|
59
|
+
release: string;
|
|
60
|
+
findings: LedgerFinding[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mirrors self_review_gate.classifyBlocking — security/claim × critical/high. */
|
|
64
|
+
export function isBlocking(f: Pick<LedgerFinding, 'kind' | 'severity'>): boolean {
|
|
65
|
+
return (
|
|
66
|
+
(f.kind === 'security' || f.kind === 'claim') &&
|
|
67
|
+
(f.severity === 'critical' || f.severity === 'high')
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pure core: every blocking finding must carry a complete disposition.
|
|
73
|
+
* Returns one problem string per violation; empty array = gate green.
|
|
74
|
+
* Empty rationale / verified_by is a hygiene violation (waiver precedent:
|
|
75
|
+
* "empty string = hygiene violation"); status `fixed` requires a commit.
|
|
76
|
+
*/
|
|
77
|
+
export function missing_dispositions(findings: readonly LedgerFinding[]): string[] {
|
|
78
|
+
const problems: string[] = [];
|
|
79
|
+
for (const f of findings) {
|
|
80
|
+
if (!isBlocking(f)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const label = `${f.finding_id} (${f.severity} ${f.kind}: ${f.title})`;
|
|
84
|
+
const status = (f.status ?? '').trim();
|
|
85
|
+
if (!status) {
|
|
86
|
+
problems.push(`${label}: no disposition status`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!(DISPOSITION_STATUSES as readonly string[]).includes(status)) {
|
|
90
|
+
problems.push(`${label}: unknown status ${JSON.stringify(status)}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!(f.rationale ?? '').trim()) {
|
|
94
|
+
problems.push(`${label}: empty rationale`);
|
|
95
|
+
}
|
|
96
|
+
if (!(f.verified_by ?? '').trim()) {
|
|
97
|
+
problems.push(`${label}: empty verified_by`);
|
|
98
|
+
}
|
|
99
|
+
if (status === 'fixed' && !(f.commit ?? '').trim()) {
|
|
100
|
+
problems.push(`${label}: status "fixed" without a commit`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return problems;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Findings the gate comment reported but the ledger never ingested. */
|
|
107
|
+
export function unrecorded_findings(
|
|
108
|
+
reported: ReadonlyArray<Pick<LedgerFinding, 'finding_id' | 'severity' | 'kind' | 'title'>>,
|
|
109
|
+
ledger: readonly LedgerFinding[],
|
|
110
|
+
): string[] {
|
|
111
|
+
const known = new Set(ledger.map((f) => f.finding_id));
|
|
112
|
+
return reported
|
|
113
|
+
.filter((f) => isBlocking(f) && !known.has(f.finding_id))
|
|
114
|
+
.map((f) => `${f.finding_id} (${f.severity} ${f.kind}: ${f.title}) reported by the self-review but not in the ledger`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parse_ledger(raw: string, source: string): Ledger {
|
|
118
|
+
let data: unknown;
|
|
119
|
+
try {
|
|
120
|
+
data = JSON.parse(raw);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new Error(`${source}: invalid JSON (${e instanceof Error ? e.message : String(e)})`);
|
|
123
|
+
}
|
|
124
|
+
const obj = data as Partial<Ledger>;
|
|
125
|
+
if (obj.schema_version !== 1 || typeof obj.release !== 'string' || !Array.isArray(obj.findings)) {
|
|
126
|
+
throw new Error(`${source}: expected {schema_version: 1, release, findings[]}`);
|
|
127
|
+
}
|
|
128
|
+
for (const f of obj.findings) {
|
|
129
|
+
if (!f || typeof f.finding_id !== 'string' || !f.finding_id.trim()) {
|
|
130
|
+
throw new Error(`${source}: finding without a finding_id`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return obj as Ledger;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const _MACHINE_BLOCK_RE = /<!-- release-findings-json: (\[.*?\]) -->/su;
|
|
137
|
+
|
|
138
|
+
/** Extract reported findings from the gate's PR comments (latest block wins). */
|
|
139
|
+
export function parse_comment_findings(commentBodies: readonly string[]): LedgerFinding[] {
|
|
140
|
+
for (let i = commentBodies.length - 1; i >= 0; i--) {
|
|
141
|
+
const m = _MACHINE_BLOCK_RE.exec(commentBodies[i]!);
|
|
142
|
+
if (m) {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(m[1]!) as LedgerFinding[];
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _ledger_path(dir: string, release: string): string {
|
|
154
|
+
return path.join(dir, `${release}.json`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _gh_comment_bodies(pr: string): string[] {
|
|
158
|
+
const r = spawnSync(
|
|
159
|
+
'gh',
|
|
160
|
+
['pr', 'view', pr, '--json', 'comments', '-q', '[.comments[].body] | @json'],
|
|
161
|
+
{ encoding: 'utf-8', cwd: REPO_ROOT, maxBuffer: 64 * 1024 * 1024 },
|
|
162
|
+
);
|
|
163
|
+
if (r.status !== 0) {
|
|
164
|
+
process.stderr.write(`gh pr view ${pr} failed: ${r.stderr}\n`);
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(JSON.parse(r.stdout.trim())) as string[];
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function main(argv: readonly string[]): number {
|
|
175
|
+
let release: string | null = null;
|
|
176
|
+
let dir = path.join(REPO_ROOT, LEDGER_DIR);
|
|
177
|
+
let pr: string | null = null;
|
|
178
|
+
let ingest: string | null = null;
|
|
179
|
+
for (let i = 0; i < argv.length; i++) {
|
|
180
|
+
const a = argv[i];
|
|
181
|
+
if (a === '--release') release = argv[++i] ?? null;
|
|
182
|
+
else if (a === '--dir') dir = argv[++i] ?? dir;
|
|
183
|
+
else if (a === '--pr') pr = argv[++i] ?? null;
|
|
184
|
+
else if (a === '--ingest') ingest = argv[++i] ?? null;
|
|
185
|
+
else {
|
|
186
|
+
process.stderr.write(`unknown argument: ${a}\n`);
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!release) {
|
|
191
|
+
const pkg = JSON.parse(
|
|
192
|
+
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'),
|
|
193
|
+
) as Record<string, unknown>;
|
|
194
|
+
release = String(pkg['version']);
|
|
195
|
+
}
|
|
196
|
+
const ledgerPath = _ledger_path(dir, release);
|
|
197
|
+
|
|
198
|
+
if (ingest) {
|
|
199
|
+
const incoming = parse_ledger(
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
schema_version: 1,
|
|
202
|
+
release,
|
|
203
|
+
findings: (JSON.parse(fs.readFileSync(ingest, 'utf-8')) as { findings: LedgerFinding[] }).findings,
|
|
204
|
+
}),
|
|
205
|
+
ingest,
|
|
206
|
+
);
|
|
207
|
+
let ledger: Ledger = { schema_version: 1, release, findings: [] };
|
|
208
|
+
if (fs.existsSync(ledgerPath)) {
|
|
209
|
+
ledger = parse_ledger(fs.readFileSync(ledgerPath, 'utf-8'), ledgerPath);
|
|
210
|
+
}
|
|
211
|
+
const known = new Set(ledger.findings.map((f) => f.finding_id));
|
|
212
|
+
let added = 0;
|
|
213
|
+
for (const f of incoming.findings) {
|
|
214
|
+
if (!known.has(f.finding_id)) {
|
|
215
|
+
ledger.findings.push(f);
|
|
216
|
+
added++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
220
|
+
fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n');
|
|
221
|
+
process.stdout.write(
|
|
222
|
+
`📥 ingested ${added} new finding(s) into ${path.relative(REPO_ROOT, ledgerPath)} — ` +
|
|
223
|
+
'fill dispositions before the release validation goes green\n',
|
|
224
|
+
);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let ledger: Ledger = { schema_version: 1, release, findings: [] };
|
|
229
|
+
if (fs.existsSync(ledgerPath)) {
|
|
230
|
+
try {
|
|
231
|
+
ledger = parse_ledger(fs.readFileSync(ledgerPath, 'utf-8'), ledgerPath);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
process.stderr.write(`❌ ${e instanceof Error ? e.message : String(e)}\n`);
|
|
234
|
+
return 2;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const problems = missing_dispositions(ledger.findings);
|
|
239
|
+
if (pr) {
|
|
240
|
+
const reported = parse_comment_findings(_gh_comment_bodies(pr));
|
|
241
|
+
problems.push(...unrecorded_findings(reported, ledger.findings));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (problems.length === 0) {
|
|
245
|
+
const n = ledger.findings.length;
|
|
246
|
+
process.stdout.write(
|
|
247
|
+
n === 0
|
|
248
|
+
? `✅ no recorded findings for ${release} (ledger ${fs.existsSync(ledgerPath) ? 'empty' : 'absent'})\n`
|
|
249
|
+
: `✅ all ${n} recorded finding(s) for ${release} dispositioned (blocking ones completely)\n`,
|
|
250
|
+
);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
process.stderr.write(`❌ undispositioned blocking finding(s) for ${release}:\n`);
|
|
254
|
+
for (const p of problems) {
|
|
255
|
+
process.stderr.write(` - ${p}\n`);
|
|
256
|
+
}
|
|
257
|
+
process.stderr.write(
|
|
258
|
+
` Record dispositions in ${path.relative(REPO_ROOT, ledgerPath)} ` +
|
|
259
|
+
'({finding_id, status: fixed|false_positive|accepted_risk, commit, rationale, verified_by}).\n' +
|
|
260
|
+
' Ingest reported findings: ./scripts-run src/scripts/check_finding_dispositions ' +
|
|
261
|
+
`--ingest <findings.json> --release ${release}\n`,
|
|
262
|
+
);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const _isMain = (() => {
|
|
267
|
+
const entry = process.argv[1];
|
|
268
|
+
if (!entry) return false;
|
|
269
|
+
try {
|
|
270
|
+
return fs.realpathSync(entry) === fs.realpathSync(_HERE);
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
if (_isMain) {
|
|
277
|
+
process.exit(main(process.argv.slice(2)));
|
|
278
|
+
}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* 2. PER-SKILL share of the skills subtree — no single skill may silently
|
|
11
11
|
* reclaim space freed elsewhere. Named exceptions carry a reason.
|
|
12
12
|
*
|
|
13
|
-
* The UNPACKED size is deliberately NOT
|
|
14
|
-
* `evaluator-budgets.unpacked_size_mb
|
|
15
|
-
*
|
|
13
|
+
* The UNPACKED size is deliberately NOT gated here — and since 2026-08-04 not
|
|
14
|
+
* anywhere: the former `evaluator-budgets.unpacked_size_mb` key was removed by
|
|
15
|
+
* maintainer decision (see `removed_2026_08_04` in evaluator-budgets.json);
|
|
16
|
+
* the evaluator umbrella still measures it as evidence.
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
19
|
* ./scripts-run src/scripts/check_pack_size [--json]
|
|
@@ -58,6 +58,7 @@ const SKIP_DIRS = [
|
|
|
58
58
|
'agents/reports', // transient run reports — historical/scratch refs, not stable artefacts
|
|
59
59
|
'agents/roadmaps/skipped', // skipped roadmaps — abandoned plans w/ forward-refs that never shipped
|
|
60
60
|
'agents/runtime', // volatile / machine-generated artefacts (gitignored)
|
|
61
|
+
'agents/state', // Tier-1 hook runtime state (gitignored, agents-layout § state/) — targets are runtime-created, never in the tree
|
|
61
62
|
'agents/tmp', // user inbox (gitignored) — user-dropped notes/todos; agents only read + move to tmp.old/
|
|
62
63
|
'agents/tmp.old', // processed inbox archive (gitignored) — consumed tmp/ files; janitor TTL 30 days
|
|
63
64
|
'agents/.harvest-local', // deliberate gitignored evidence store (source-confidentiality) — refs to it can never resolve in CI
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Highlight plausibility gate (release-truth Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* The recorded failure: the curated release head said `_none_` twice while
|
|
5
|
+
* false — 9.13.0 shipped behaviour changes, a removed public trigger type, a
|
|
6
|
+
* security fix, and honest nulls; the head claimed none of it. Same at
|
|
7
|
+
* 9.14.0.
|
|
8
|
+
*
|
|
9
|
+
* This gate derives GENERATED evidence categories from the release span
|
|
10
|
+
* (security-tagged commits, behaviour/default changes from conventional
|
|
11
|
+
* commit types + rule/schema diffs, honest-null markers, removed public
|
|
12
|
+
* surface) and FAILS when a populated generated category meets a `_none_`
|
|
13
|
+
* curated field. It blocks the contradiction only — a human still writes the
|
|
14
|
+
* prose; nothing here auto-formulates highlights, and a filled field is
|
|
15
|
+
* never judged for quality.
|
|
16
|
+
*
|
|
17
|
+
* Derivation is deliberately conservative (documented per-label below):
|
|
18
|
+
* a false red makes every release annoying; a miss only returns the head to
|
|
19
|
+
* the pre-gate state. Adjudication of derived evidence stays human.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* check_release_highlights --version X.Y.Z [--from <ref>] [--to <ref>]
|
|
23
|
+
* [--changelog <path>]
|
|
24
|
+
*
|
|
25
|
+
* `--from` defaults to the latest reachable release tag before `--to`
|
|
26
|
+
* (default HEAD). Exit codes: 0 plausible · 1 contradiction · 2 usage error.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import process from 'node:process';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
import { extract_changelog_section } from './_lib/release_material.js';
|
|
35
|
+
|
|
36
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
37
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
38
|
+
|
|
39
|
+
// The five curated labels, mirrored from release.ts RELEASE_HEAD_SECTIONS
|
|
40
|
+
// (kept literal here so this checker has no import edge into the release
|
|
41
|
+
// pipeline it audits; the unit tests pin the two lists against each other).
|
|
42
|
+
export const HEAD_LABELS: readonly string[] = [
|
|
43
|
+
'Behaviour changes',
|
|
44
|
+
'Default changes + migration',
|
|
45
|
+
'Security and correctness',
|
|
46
|
+
'Honest nulls',
|
|
47
|
+
'Known limitations',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const HEAD_NONE = '_none_';
|
|
51
|
+
|
|
52
|
+
export interface SpanCommit {
|
|
53
|
+
sha: string;
|
|
54
|
+
subject: string;
|
|
55
|
+
body: string;
|
|
56
|
+
/** `A` / `M` / `D` / `R…` status per touched path. */
|
|
57
|
+
files: ReadonlyArray<{ status: string; path: string }>;
|
|
58
|
+
breaking: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Public artefact trees whose deletions count as removed public surface. */
|
|
62
|
+
const _PUBLIC_SURFACE_PREFIXES = [
|
|
63
|
+
'src/skills/',
|
|
64
|
+
'src/rules/',
|
|
65
|
+
'src/agent-src/commands/',
|
|
66
|
+
'src/domains/',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const _RULE_OR_SCHEMA_PREFIXES = ['src/rules/', 'src/scripts/schemas/'];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Derive evidence per curated label from the span. Rules per label:
|
|
73
|
+
*
|
|
74
|
+
* - Security and correctness: conventional scope matching /secur/i, or the
|
|
75
|
+
* whole word "security" in the subject.
|
|
76
|
+
* - Behaviour changes: breaking (`!` / BREAKING CHANGE) commits, diffs
|
|
77
|
+
* touching `src/rules/` or `src/scripts/schemas/`, and deletions under the
|
|
78
|
+
* public artefact trees (removed public surface).
|
|
79
|
+
* - Default changes + migration: subject carrying the whole word "default",
|
|
80
|
+
* "migration", or "migrate".
|
|
81
|
+
* - Honest nulls: subject or body carrying an "honest null" marker.
|
|
82
|
+
* - Known limitations: never derived — pure prose, not gate-checkable.
|
|
83
|
+
*/
|
|
84
|
+
export function derive_categories(commits: readonly SpanCommit[]): Record<string, string[]> {
|
|
85
|
+
const out: Record<string, string[]> = {};
|
|
86
|
+
for (const label of HEAD_LABELS) {
|
|
87
|
+
out[label] = [];
|
|
88
|
+
}
|
|
89
|
+
const evidence = (c: SpanCommit): string => `${c.sha.slice(0, 7)} ${c.subject}`;
|
|
90
|
+
for (const c of commits) {
|
|
91
|
+
const scopeMatch = /^\w+\(([^)]*)\)!?:/u.exec(c.subject);
|
|
92
|
+
const scope = scopeMatch ? scopeMatch[1]! : '';
|
|
93
|
+
if (/secur/iu.test(scope) || /\bsecurity\b/iu.test(c.subject)) {
|
|
94
|
+
out['Security and correctness']!.push(evidence(c));
|
|
95
|
+
}
|
|
96
|
+
const removedPublic = c.files.filter(
|
|
97
|
+
(f) => f.status.startsWith('D') && _PUBLIC_SURFACE_PREFIXES.some((p) => f.path.startsWith(p)),
|
|
98
|
+
);
|
|
99
|
+
const ruleOrSchema = c.files.some((f) =>
|
|
100
|
+
_RULE_OR_SCHEMA_PREFIXES.some((p) => f.path.startsWith(p)),
|
|
101
|
+
);
|
|
102
|
+
if (c.breaking || ruleOrSchema || removedPublic.length > 0) {
|
|
103
|
+
const detail =
|
|
104
|
+
removedPublic.length > 0
|
|
105
|
+
? `${evidence(c)} (removes ${removedPublic.map((f) => f.path).join(', ')})`
|
|
106
|
+
: evidence(c);
|
|
107
|
+
out['Behaviour changes']!.push(detail);
|
|
108
|
+
}
|
|
109
|
+
if (/\b(default|migration|migrate)\b/iu.test(c.subject)) {
|
|
110
|
+
out['Default changes + migration']!.push(evidence(c));
|
|
111
|
+
}
|
|
112
|
+
if (/honest[ -]null/iu.test(`${c.subject}\n${c.body}`)) {
|
|
113
|
+
out['Honest nulls']!.push(evidence(c));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse the curated head from a changelog section body. Returns null when
|
|
121
|
+
* the section carries no `### Release highlights` head (pre-9.9.0 entries).
|
|
122
|
+
*/
|
|
123
|
+
export function parse_curated_head(sectionBody: string): Record<string, string> | null {
|
|
124
|
+
if (!sectionBody.includes('### Release highlights')) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const out: Record<string, string> = {};
|
|
128
|
+
for (const label of HEAD_LABELS) {
|
|
129
|
+
const re = new RegExp(
|
|
130
|
+
`^-\\s+\\*\\*${label.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}:\\*\\*\\s*(.*)$`,
|
|
131
|
+
'm',
|
|
132
|
+
);
|
|
133
|
+
const m = re.exec(sectionBody);
|
|
134
|
+
if (m) {
|
|
135
|
+
out[label] = m[1]!.trim();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Contradiction {
|
|
142
|
+
label: string;
|
|
143
|
+
evidence: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A populated generated category meeting a `_none_` curated field is a
|
|
148
|
+
* contradiction. Filled fields and underived labels never fail.
|
|
149
|
+
*/
|
|
150
|
+
export function highlight_contradictions(
|
|
151
|
+
curated: Readonly<Record<string, string>>,
|
|
152
|
+
derived: Readonly<Record<string, readonly string[]>>,
|
|
153
|
+
): Contradiction[] {
|
|
154
|
+
const out: Contradiction[] = [];
|
|
155
|
+
for (const label of HEAD_LABELS) {
|
|
156
|
+
const curatedValue = (curated[label] ?? '').trim();
|
|
157
|
+
const ev = derived[label] ?? [];
|
|
158
|
+
if (curatedValue === HEAD_NONE && ev.length > 0) {
|
|
159
|
+
out.push({ label, evidence: [...ev] });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── git span collection (CLI only — the core above is pure) ────────────────
|
|
166
|
+
|
|
167
|
+
const _RECORD = '\u001e';
|
|
168
|
+
const _FIELD = '\u001f';
|
|
169
|
+
|
|
170
|
+
export function _parse_git_log(raw: string): SpanCommit[] {
|
|
171
|
+
const commits: SpanCommit[] = [];
|
|
172
|
+
for (const record of raw.split(_RECORD)) {
|
|
173
|
+
if (!record.trim()) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const [head, ...restLines] = record.split('\n');
|
|
177
|
+
const parts = (head ?? '').split(_FIELD);
|
|
178
|
+
if (parts.length < 3) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const sha = parts[0]!.trim();
|
|
182
|
+
const subject = parts[1]!;
|
|
183
|
+
const bodyParts: string[] = [parts.slice(2).join(_FIELD)];
|
|
184
|
+
const files: Array<{ status: string; path: string }> = [];
|
|
185
|
+
for (const line of restLines) {
|
|
186
|
+
const m = /^([A-Z]\d*)\t(.+)$/u.exec(line);
|
|
187
|
+
if (m) {
|
|
188
|
+
// Renames carry two paths; the last tab field is the new path,
|
|
189
|
+
// the first the old — record both so a rename out of a public
|
|
190
|
+
// tree still counts its origin path.
|
|
191
|
+
const segs = m[2]!.split('\t');
|
|
192
|
+
for (const p of segs) {
|
|
193
|
+
files.push({ status: m[1]!, path: p });
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// %b is multi-line — everything that is not a name-status
|
|
197
|
+
// line is body continuation (honest-null markers live there).
|
|
198
|
+
bodyParts.push(line);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const body = bodyParts.join('\n');
|
|
202
|
+
commits.push({
|
|
203
|
+
sha,
|
|
204
|
+
subject,
|
|
205
|
+
body,
|
|
206
|
+
files,
|
|
207
|
+
breaking: /^\w+(\([^)]*\))?!:/u.test(subject) || /BREAKING CHANGE/u.test(body),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return commits;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _git(args: string[]): string {
|
|
214
|
+
const r = spawnSync('git', args, { encoding: 'utf-8', cwd: REPO_ROOT, maxBuffer: 64 * 1024 * 1024 });
|
|
215
|
+
if (r.status !== 0) {
|
|
216
|
+
process.stderr.write(`git ${args.join(' ')} failed: ${r.stderr}\n`);
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
219
|
+
return r.stdout;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _collect_span(from: string, to: string): SpanCommit[] {
|
|
223
|
+
const raw = _git([
|
|
224
|
+
'log',
|
|
225
|
+
`${from}..${to}`,
|
|
226
|
+
'--no-merges',
|
|
227
|
+
'--name-status',
|
|
228
|
+
'--format=%x1e%H%x1f%s%x1f%b',
|
|
229
|
+
]);
|
|
230
|
+
return _parse_git_log(raw);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function main(argv: readonly string[]): number {
|
|
234
|
+
let version: string | null = null;
|
|
235
|
+
let from: string | null = null;
|
|
236
|
+
let to = 'HEAD';
|
|
237
|
+
let changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
|
|
238
|
+
for (let i = 0; i < argv.length; i++) {
|
|
239
|
+
const a = argv[i];
|
|
240
|
+
if (a === '--version') version = argv[++i] ?? null;
|
|
241
|
+
else if (a === '--from') from = argv[++i] ?? null;
|
|
242
|
+
else if (a === '--to') to = argv[++i] ?? to;
|
|
243
|
+
else if (a === '--changelog') changelogPath = argv[++i] ?? changelogPath;
|
|
244
|
+
else {
|
|
245
|
+
process.stderr.write(`unknown argument: ${a}\n`);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!version) {
|
|
250
|
+
process.stderr.write('--version X.Y.Z is required\n');
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
if (!from) {
|
|
254
|
+
from = _git(['describe', '--tags', '--abbrev=0', '--match', '[0-9]*.[0-9]*.[0-9]*', `${to}^`]).trim();
|
|
255
|
+
}
|
|
256
|
+
const section = extract_changelog_section(fs.readFileSync(changelogPath, 'utf-8'), version);
|
|
257
|
+
if (!section) {
|
|
258
|
+
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
259
|
+
return 2;
|
|
260
|
+
}
|
|
261
|
+
const curated = parse_curated_head(section.body);
|
|
262
|
+
if (!curated) {
|
|
263
|
+
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
const derived = derive_categories(_collect_span(from, to));
|
|
267
|
+
const contradictions = highlight_contradictions(curated, derived);
|
|
268
|
+
if (contradictions.length === 0) {
|
|
269
|
+
process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
`❌ curated head contradicts the release span for ${version} (${from}..${to}):\n`,
|
|
274
|
+
);
|
|
275
|
+
for (const c of contradictions) {
|
|
276
|
+
process.stderr.write(` - **${c.label}:** is \`_none_\` but the span carries:\n`);
|
|
277
|
+
for (const e of c.evidence) {
|
|
278
|
+
process.stderr.write(` ${e}\n`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
process.stderr.write(
|
|
282
|
+
' Fill the curated head in CHANGELOG.md (a human writes the prose) or adjudicate ' +
|
|
283
|
+
'the evidence in the PR — the gate only blocks the `_none_` contradiction.\n',
|
|
284
|
+
);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const _isMain = (() => {
|
|
289
|
+
const entry = process.argv[1];
|
|
290
|
+
if (!entry) return false;
|
|
291
|
+
try {
|
|
292
|
+
return fs.realpathSync(entry) === fs.realpathSync(_HERE);
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
|
|
298
|
+
if (_isMain) {
|
|
299
|
+
process.exit(main(process.argv.slice(2)));
|
|
300
|
+
}
|
|
@@ -34,6 +34,10 @@ const ALLOWLIST_GLOBS = [
|
|
|
34
34
|
'src/domains/*/pack.yaml',
|
|
35
35
|
'src/domains/*/README.md',
|
|
36
36
|
'docs/archive/CHANGELOG-pre-*.md',
|
|
37
|
+
// Finding-disposition ledger (release-truth Phase 3) — a release PR must
|
|
38
|
+
// be able to carry the dispositions of its own self-review findings; the
|
|
39
|
+
// finding-dispositions gate is red until blocking ones are recorded here.
|
|
40
|
+
'agents/evidence/release-findings/*.json',
|
|
37
41
|
// Project-settings template pin — bumped by release.ts set_template_pin and
|
|
38
42
|
// its regenerated dist twin (kept in lockstep with package.json.version).
|
|
39
43
|
'src/agent-src/templates/agents/agent-project-settings.example.yml',
|