@event4u/agent-config 9.0.0 → 9.1.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 +67 -20
- package/MIGRATION.md +14 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/explain-run.md +88 -0
- package/dist/agent-src/commands/pr/create/description-only.md +138 -8
- package/dist/agent-src/commands/pr/create.md +16 -5
- package/dist/agent-src/skills/llm-provider-knowledge/SKILL.md +146 -0
- package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +20 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +11 -0
- package/dist/agent-src/skills/token-optimizer/SKILL.md +10 -4
- package/dist/agent-src/templates/agent-settings.md +28 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +68 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +8 -6
- package/dist/discovery/trust-report.md +8 -8
- package/dist/discovery/workspaces.json +6 -4
- package/dist/install/install.mjs +38 -31
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/scripts/_lib/agent_settings.js +4 -1
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +5 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +2 -2
- package/docs/MIGRATION.md +20 -0
- package/docs/architecture.md +3 -3
- package/docs/command-flows.md +1 -1
- package/docs/contracts/agents-layout.md +1 -1
- package/docs/contracts/consumer-bridge.md +55 -63
- package/docs/contracts/install-layout.md +1 -1
- package/docs/contracts/migrate-command.md +1 -1
- package/docs/customization.md +22 -1
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +34 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
- package/docs/installation.md +2 -1
- package/docs/maintainers/dev-mode.md +3 -2
- package/docs/proof.md +5 -5
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
- package/src/config/agent-settings.template.yml +39 -0
- package/src/config/gitignore-block.txt +16 -6
- package/src/scripts/_cli/cmd_conformance.ts +2 -2
- package/src/scripts/_cli/cmd_doctor.ts +162 -13
- package/src/scripts/_cli/cmd_refresh.ts +35 -118
- package/src/scripts/_lib/agent_settings.ts +4 -1
- package/src/scripts/ai_council/clients.ts +68 -6
- package/src/scripts/ai_council/pricing.ts +62 -0
- package/src/scripts/council_cli.ts +19 -3
- package/src/scripts/install.ts +44 -45
- package/src/scripts/lint_agents_layout.ts +10 -4
- package/src/scripts/lint_breaking_changes_index.ts +303 -0
- package/src/scripts/mcp_server/tools.ts +1 -3
- package/src/scripts/schemas/agent-settings.schema.json +17 -0
- package/src/scripts/self_review_gate.test.ts +11 -0
- package/src/scripts/self_review_gate.ts +7 -1
- package/src/scripts/skill_eval_coverage.ts +4 -1
- package/src/server/schemas/settings.ts +15 -0
|
@@ -42,7 +42,11 @@ const ALLOWED_FLAT_FILES: ReadonlySet<string> = new Set([
|
|
|
42
42
|
// separate violation caught by check_tracked_but_ignored.ts.
|
|
43
43
|
'.ai-video.xml', // operator AI-video config; gitignored per .gitignore § AI Video
|
|
44
44
|
'installed-tools.lock', // installer per-user inventory; gitignored per ADR-020
|
|
45
|
-
|
|
45
|
+
// Retired marker (ADR-020 amendment 2026-07-13). No longer written; the
|
|
46
|
+
// global root resolves from `~/.event4u/agent-config`. Kept tolerated so a
|
|
47
|
+
// legacy leftover in a not-yet-reinstalled consumer is not flagged as a
|
|
48
|
+
// violation — the next install deletes it.
|
|
49
|
+
'.event4u-bridge.yml',
|
|
46
50
|
]);
|
|
47
51
|
|
|
48
52
|
// All directories allowed at the agents/ root in the SOURCE REPO.
|
|
@@ -75,6 +79,8 @@ const ALLOWED_SOURCE_DIRS: ReadonlySet<string> = new Set([
|
|
|
75
79
|
const CONSUMER_EXPECTED_ENTRIES: ReadonlySet<string> = new Set([
|
|
76
80
|
// Required consumer entries
|
|
77
81
|
'overrides',
|
|
82
|
+
// Retired marker (ADR-020 amendment 2026-07-13) — no longer written, but
|
|
83
|
+
// kept in the tolerated set so a legacy leftover does not warn.
|
|
78
84
|
'.event4u-bridge.yml',
|
|
79
85
|
'.gitkeep',
|
|
80
86
|
// Optional but legitimate consumer entries (will not trigger warnings)
|
|
@@ -93,9 +99,9 @@ const CONSUMER_EXPECTED_ENTRIES: ReadonlySet<string> = new Set([
|
|
|
93
99
|
const MIGRATE_HINT =
|
|
94
100
|
'Run `npx @event4u/agent-config migrate` to sweep legacy project-scope ' +
|
|
95
101
|
'artefacts in one pass. The unified `migrate` command (see ' +
|
|
96
|
-
'`docs/contracts/migrate-command.md`) leaves `agents/overrides/`
|
|
97
|
-
'
|
|
98
|
-
'
|
|
102
|
+
'`docs/contracts/migrate-command.md`) leaves `agents/overrides/` as the ' +
|
|
103
|
+
'only consumer-side agent surface; the wizard recreates fresh config on ' +
|
|
104
|
+
'`agent-config setup`.';
|
|
99
105
|
|
|
100
106
|
function _isDir(p: string): boolean {
|
|
101
107
|
try {
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* BREAKING_CHANGES.md ↔ CHANGELOG sync linter (road-to-feedback-9.0 P1.4).
|
|
4
|
+
*
|
|
5
|
+
* `BREAKING_CHANGES.md` is the discoverable index of consumer-facing breaking
|
|
6
|
+
* changes, one per major (`X.0.0`). When a diff ships a NEW breaking entry in a
|
|
7
|
+
* RELEASED CHANGELOG section it must also touch the index in the same change,
|
|
8
|
+
* or the index silently rots behind the changelog.
|
|
9
|
+
*
|
|
10
|
+
* TRIGGER: a git diff (working tree vs `origin/main` — staged + unstaged) that
|
|
11
|
+
* ADDS a `### BREAKING` / `### BREAKING CHANGES` subheading under a RELEASED
|
|
12
|
+
* major section (`## [N.0.0]`) in `CHANGELOG.md`, where the same major had no
|
|
13
|
+
* such subheading at the base ref (a wholly-new released section counts). A
|
|
14
|
+
* breaking note under `## [Unreleased]` is WIP and never trips the gate.
|
|
15
|
+
*
|
|
16
|
+
* REQUIREMENT: when triggered, the same diff must also modify
|
|
17
|
+
* `BREAKING_CHANGES.md`.
|
|
18
|
+
*
|
|
19
|
+
* Escape hatch (mirrors `check_structural_breaking.ts`'s `ci-override` trailer):
|
|
20
|
+
* an own-line trailer `ci-override: breaking-index-override` (the bare token
|
|
21
|
+
* `breaking-index-override` on its own line is also accepted) in any commit
|
|
22
|
+
* body in the range, or the env var `BREAKING_INDEX_OVERRIDE=1`, clears the
|
|
23
|
+
* gate for an intentional index-deferred change.
|
|
24
|
+
*
|
|
25
|
+
* CLI: exit 0 clean/cleared, 1 violation (or selftest failure), 2 usage error.
|
|
26
|
+
* `--selftest` proves the rule on inline string fixtures (no git needed).
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
34
|
+
// src/scripts/lint_breaking_changes_index.ts → two levels up is the repo root.
|
|
35
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
36
|
+
const BASE_REF = 'origin/main';
|
|
37
|
+
const CHANGELOG_REL = 'CHANGELOG.md';
|
|
38
|
+
const INDEX_REL = 'BREAKING_CHANGES.md';
|
|
39
|
+
|
|
40
|
+
// A RELEASED major section heading: `## [N.0.0]…` (minor + patch both 0).
|
|
41
|
+
// `## [Unreleased]` never matches, so WIP breaking notes are out of scope.
|
|
42
|
+
const RELEASED_MAJOR_HEADING_RE = /^## \[(?<version>\d+\.0\.0)\]/;
|
|
43
|
+
// Any other level-1/2 heading closes the current section.
|
|
44
|
+
const SECTION_BOUNDARY_RE = /^##? /;
|
|
45
|
+
// A breaking subheading as emitted by release-please: `### BREAKING` or
|
|
46
|
+
// `### BREAKING CHANGES` (all-caps). Mixed-case `### Breaking changes` under
|
|
47
|
+
// Unreleased is intentionally NOT matched.
|
|
48
|
+
const BREAKING_SUBHEADING_RE = /^### BREAKING\b/;
|
|
49
|
+
// Override trailer — same shape as check_structural_breaking's `ci-override:`
|
|
50
|
+
// mechanism (own line, not an inline mention); the bare token is also accepted.
|
|
51
|
+
const OVERRIDE_RE = /^(?:ci-override:\s*)?breaking-index-override\s*$/m;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return the set of RELEASED major versions whose CHANGELOG section contains a
|
|
55
|
+
* `### BREAKING` subheading.
|
|
56
|
+
*/
|
|
57
|
+
export function released_breaking_majors(lines: readonly string[]): Set<string> {
|
|
58
|
+
const found = new Set<string>();
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const m = RELEASED_MAJOR_HEADING_RE.exec(lines[i]!);
|
|
61
|
+
if (!m) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const version = m.groups!['version']!;
|
|
65
|
+
// Section body: heading line down to (exclusive) the next boundary.
|
|
66
|
+
let end = lines.length;
|
|
67
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
68
|
+
if (SECTION_BOUNDARY_RE.test(lines[j]!)) {
|
|
69
|
+
end = j;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (lines.slice(i + 1, end).some((ln) => BREAKING_SUBHEADING_RE.test(ln))) {
|
|
74
|
+
found.add(version);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Core check. Returns the sorted list of RELEASED major versions that GAINED a
|
|
82
|
+
* `### BREAKING` subheading between `oldLines` and `newLines` — the versions
|
|
83
|
+
* that should be reflected in `BREAKING_CHANGES.md`.
|
|
84
|
+
*/
|
|
85
|
+
export function added_released_breaking_majors(
|
|
86
|
+
oldLines: readonly string[],
|
|
87
|
+
newLines: readonly string[],
|
|
88
|
+
): string[] {
|
|
89
|
+
const before = released_breaking_majors(oldLines);
|
|
90
|
+
const after = released_breaking_majors(newLines);
|
|
91
|
+
const added = [...after].filter((v) => !before.has(v));
|
|
92
|
+
added.sort();
|
|
93
|
+
return added;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CheckResult {
|
|
97
|
+
readonly addedMajors: readonly string[];
|
|
98
|
+
readonly indexChanged: boolean;
|
|
99
|
+
readonly overridden: boolean;
|
|
100
|
+
readonly violation: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Pure decision: violation iff a released BREAKING was added, the index was not touched, and no override cleared it. */
|
|
104
|
+
export function evaluate(
|
|
105
|
+
oldLines: readonly string[],
|
|
106
|
+
newLines: readonly string[],
|
|
107
|
+
indexChanged: boolean,
|
|
108
|
+
overridden: boolean,
|
|
109
|
+
): CheckResult {
|
|
110
|
+
const addedMajors = added_released_breaking_majors(oldLines, newLines);
|
|
111
|
+
const violation = addedMajors.length > 0 && !indexChanged && !overridden;
|
|
112
|
+
return { addedMajors, indexChanged, overridden, violation };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── git helpers (CLI only) ──────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function _git(...args: string[]): string {
|
|
118
|
+
const proc = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf-8' });
|
|
119
|
+
return (proc.stdout as string | null) ?? '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _base(): string {
|
|
123
|
+
const mb = _git('merge-base', BASE_REF, 'HEAD').trim();
|
|
124
|
+
return mb || BASE_REF;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _splitlines(s: string): string[] {
|
|
128
|
+
return s.split(/\r\n|\r|\n/);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** File contents at a ref, or '' when the path did not exist there. */
|
|
132
|
+
function _blob_at(ref: string, rel: string): string {
|
|
133
|
+
return _git('show', `${ref}:${rel}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── selftest fixtures ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function _changelog(sections: string): string[] {
|
|
139
|
+
return ('# Changelog\n\n## [Unreleased]\n\n' + sections).split('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const _RELEASED_WITH_BREAKING =
|
|
143
|
+
'## [9.0.0](https://example/compare/8.13.0...9.0.0) (2026-07-13)\n\n' +
|
|
144
|
+
'### BREAKING CHANGES\n\n* installer path changed ([abc1234](https://example))\n\n';
|
|
145
|
+
const _RELEASED_NO_BREAKING =
|
|
146
|
+
'## [9.0.0](https://example/compare/8.13.0...9.0.0) (2026-07-13)\n\n' +
|
|
147
|
+
'### Features\n\n* a feature ([abc1234](https://example))\n\n';
|
|
148
|
+
|
|
149
|
+
function run_selftest(): number {
|
|
150
|
+
let failed = 0;
|
|
151
|
+
const assert = (label: string, ok: boolean): void => {
|
|
152
|
+
process.stdout.write(`${ok ? '✅' : '❌'} selftest: ${label}\n`);
|
|
153
|
+
if (!ok) failed += 1;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const oldNoBreaking = _changelog(_RELEASED_NO_BREAKING);
|
|
157
|
+
const newWithBreaking = _changelog(_RELEASED_WITH_BREAKING);
|
|
158
|
+
|
|
159
|
+
// 1. Released BREAKING added, index NOT changed, no override → VIOLATION.
|
|
160
|
+
const red = evaluate(oldNoBreaking, newWithBreaking, false, false);
|
|
161
|
+
assert(
|
|
162
|
+
'released BREAKING added without index change FAILS',
|
|
163
|
+
red.violation && red.addedMajors.length === 1 && red.addedMajors[0] === '9.0.0',
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// 2. Same, but the index WAS changed → passes.
|
|
167
|
+
const greenIdx = evaluate(oldNoBreaking, newWithBreaking, true, false);
|
|
168
|
+
assert('index change clears the gate', !greenIdx.violation && greenIdx.addedMajors.length === 1);
|
|
169
|
+
|
|
170
|
+
// 3. Same, index untouched, but override present → passes.
|
|
171
|
+
const greenOverride = evaluate(oldNoBreaking, newWithBreaking, false, true);
|
|
172
|
+
assert('override clears the gate', !greenOverride.violation);
|
|
173
|
+
|
|
174
|
+
// 4. BREAKING only under [Unreleased] (WIP) → never trips.
|
|
175
|
+
const oldEmpty = _changelog('');
|
|
176
|
+
const newUnreleasedBreaking = (
|
|
177
|
+
'# Changelog\n\n## [Unreleased]\n\n### BREAKING CHANGES\n\n* wip break ([abc1234](https://example))\n\n'
|
|
178
|
+
).split('\n');
|
|
179
|
+
const wip = evaluate(oldEmpty, newUnreleasedBreaking, false, false);
|
|
180
|
+
assert('Unreleased BREAKING does not trip the gate', !wip.violation && wip.addedMajors.length === 0);
|
|
181
|
+
|
|
182
|
+
// 5. A released BREAKING already present at base (no new addition) → clean.
|
|
183
|
+
const noop = evaluate(newWithBreaking, newWithBreaking, false, false);
|
|
184
|
+
assert('pre-existing released BREAKING is not re-flagged', !noop.violation);
|
|
185
|
+
|
|
186
|
+
if (failed > 0) {
|
|
187
|
+
process.stderr.write(`❌ selftest: ${failed} fixture(s) failed.\n`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write('✅ selftest: all fixtures behave as specified.\n');
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
interface ParsedArgs {
|
|
197
|
+
selftest: boolean;
|
|
198
|
+
quiet: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _argparse_error(message: string): never {
|
|
202
|
+
process.stderr.write(`lint_breaking_changes_index: error: ${message}\n`);
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parse_args(argv: readonly string[]): ParsedArgs {
|
|
207
|
+
const args: ParsedArgs = { selftest: false, quiet: false };
|
|
208
|
+
for (const arg of argv) {
|
|
209
|
+
if (arg === '--selftest') {
|
|
210
|
+
args.selftest = true;
|
|
211
|
+
} else if (arg === '--quiet') {
|
|
212
|
+
args.quiet = true;
|
|
213
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
214
|
+
process.stdout.write('usage: lint_breaking_changes_index [-h] [--selftest] [--quiet]\n');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
} else {
|
|
217
|
+
_argparse_error(`unrecognized arguments: ${arg}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return args;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function main(argv?: readonly string[]): number {
|
|
224
|
+
const args = parse_args(argv ?? process.argv.slice(2));
|
|
225
|
+
if (args.selftest) {
|
|
226
|
+
return run_selftest();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const base = _base();
|
|
230
|
+
if (!base) {
|
|
231
|
+
return 0; // no trunk to diff against (e.g. shallow CI) → no-op
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const oldLines = _splitlines(_blob_at(base, CHANGELOG_REL));
|
|
235
|
+
const changelogPath = path.join(REPO_ROOT, CHANGELOG_REL);
|
|
236
|
+
const newLines = fs.existsSync(changelogPath)
|
|
237
|
+
? _splitlines(fs.readFileSync(changelogPath, 'utf-8'))
|
|
238
|
+
: [];
|
|
239
|
+
|
|
240
|
+
// Was BREAKING_CHANGES.md touched (staged or unstaged) vs the base?
|
|
241
|
+
const indexChanged = _git('diff', '--name-only', base, '--', INDEX_REL).trim() !== '';
|
|
242
|
+
|
|
243
|
+
const log = _git('log', '--format=%B', `${base}..HEAD`);
|
|
244
|
+
const overridden = OVERRIDE_RE.test(log) || process.env['BREAKING_INDEX_OVERRIDE'] === '1';
|
|
245
|
+
|
|
246
|
+
const result = evaluate(oldLines, newLines, indexChanged, overridden);
|
|
247
|
+
|
|
248
|
+
if (!result.violation) {
|
|
249
|
+
if (!args.quiet) {
|
|
250
|
+
if (result.addedMajors.length > 0) {
|
|
251
|
+
const why = result.overridden ? 'override' : `${INDEX_REL} updated`;
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
`✅ lint-breaking-changes-index: ${result.addedMajors.length} released ` +
|
|
254
|
+
`BREAKING addition(s), cleared by ${why}\n`,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
process.stdout.write(
|
|
258
|
+
'✅ lint-breaking-changes-index: no new released BREAKING entries vs trunk\n',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
process.stderr.write(
|
|
266
|
+
`❌ lint-breaking-changes-index: released BREAKING entr(y/ies) added to ${CHANGELOG_REL} ` +
|
|
267
|
+
`without a matching ${INDEX_REL} change:\n`,
|
|
268
|
+
);
|
|
269
|
+
for (const v of result.addedMajors) {
|
|
270
|
+
process.stderr.write(` - ## [${v}] gained a ### BREAKING section\n`);
|
|
271
|
+
}
|
|
272
|
+
process.stderr.write(
|
|
273
|
+
`\nWhen a released major ships a breaking change, ${INDEX_REL} — the discoverable\n` +
|
|
274
|
+
`index (one entry per major) — must be updated in the same change. Resolve by:\n` +
|
|
275
|
+
` • adding the matching entry to ${INDEX_REL}, or\n` +
|
|
276
|
+
' • adding an own-line `ci-override: breaking-index-override` trailer to a commit\n' +
|
|
277
|
+
' body (or setting BREAKING_INDEX_OVERRIDE=1) for an intentionally deferred index.\n',
|
|
278
|
+
);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function _isCliEntry(): boolean {
|
|
283
|
+
if (process.argv[1] === undefined) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
287
|
+
if (import.meta.url === argvUrl) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
292
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
293
|
+
return here === argv;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
300
|
+
process.exit(main());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { main, run_selftest, RELEASED_MAJOR_HEADING_RE, BREAKING_SUBHEADING_RE, OVERRIDE_RE };
|
|
@@ -775,9 +775,7 @@ async function _runDoctorLeg(consumerRoot: string): Promise<{
|
|
|
775
775
|
let tagDrift: Record<string, unknown>[] = [];
|
|
776
776
|
let checks: Record<string, unknown>[];
|
|
777
777
|
if (manifest === null) {
|
|
778
|
-
const bridgePresent =
|
|
779
|
-
path.join(projectRoot, doctor.BRIDGE_MARKER_RELATIVE),
|
|
780
|
-
);
|
|
778
|
+
const bridgePresent = doctor._is_global_only_consumer(projectRoot);
|
|
781
779
|
checks = doctor._run_checks_no_manifest(projectRoot, bridgePresent, null);
|
|
782
780
|
} else {
|
|
783
781
|
const [records, known] = doctor._collect_manifest_entries(projectRoot, manifest);
|
|
@@ -106,6 +106,23 @@
|
|
|
106
106
|
"description": "Controls whether skills marked token_budget_class: rich may load in full (exempt from telegraph-speak and thin-projector trimming). on = allowed (default); off = fall back to standard condensed behavior; ask = surface an estimated token delta (tokens, not dollars) and ask the user once per session before loading."
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
},
|
|
110
|
+
"commands": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"additionalProperties": true,
|
|
113
|
+
"properties": {
|
|
114
|
+
"create_pr": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": true,
|
|
117
|
+
"properties": {
|
|
118
|
+
"detail_level": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"enum": ["min", "med", "max"],
|
|
121
|
+
"description": "Verbosity tier for the generated PR description body, consumed by /create-pr. min (default) = title + 2-3 sentence what/why/impact + linked ticket; med = min + grouped changes + tests note; max = med + how-to-test + edge cases + reviewer guidance. Critical info (breaking changes, migrations, security, rollback) is always included at every tier."
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
109
126
|
}
|
|
110
127
|
}
|
|
111
128
|
}
|
|
@@ -62,4 +62,15 @@ describe('renderReview', () => {
|
|
|
62
62
|
expect(renderReview(blocking, false)).toContain('WOULD block');
|
|
63
63
|
expect(renderReview(blocking, true)).toContain('merge-blocking');
|
|
64
64
|
});
|
|
65
|
+
it('labels each row (Blocking)/(Advisory) so a critical-but-non-blocking row never reads as inconsistent with the count', () => {
|
|
66
|
+
const findings = [f('critical', 'correctness'), f('critical', 'security')];
|
|
67
|
+
const out = renderReview(findings, false);
|
|
68
|
+
// critical × correctness is NOT blocking → labelled Advisory
|
|
69
|
+
expect(out).toContain('| critical (Advisory) | correctness |');
|
|
70
|
+
// critical × security IS blocking → labelled Blocking
|
|
71
|
+
expect(out).toContain('| critical (Blocking) | security |');
|
|
72
|
+
// verdict count matches the number of (Blocking) rows (exactly 1)
|
|
73
|
+
expect(out).toContain('1 finding(s) WOULD block');
|
|
74
|
+
expect((out.match(/\(Blocking\)/g) ?? []).length).toBe(1);
|
|
75
|
+
});
|
|
65
76
|
});
|
|
@@ -183,8 +183,14 @@ export function renderReview(findings: Finding[], enforce: boolean): string {
|
|
|
183
183
|
return `${banner}\n\n✅ Self-review gate: no findings.`;
|
|
184
184
|
}
|
|
185
185
|
const blocking = findings.filter(classifyBlocking);
|
|
186
|
+
// Each row carries an explicit (Blocking)/(Advisory) marker so a
|
|
187
|
+
// critical-but-non-blocking finding (e.g. critical × correctness) can never
|
|
188
|
+
// read as inconsistent with the narrow `blocking.length` verdict count.
|
|
186
189
|
const rows = findings
|
|
187
|
-
.map((f) =>
|
|
190
|
+
.map((f) => {
|
|
191
|
+
const marker = classifyBlocking(f) ? 'Blocking' : 'Advisory';
|
|
192
|
+
return `| ${f.severity} (${marker}) | ${f.kind} | ${f.file ?? '—'} | ${f.title} |`;
|
|
193
|
+
})
|
|
188
194
|
.join('\n');
|
|
189
195
|
const verdictLine = blocking.length
|
|
190
196
|
? enforce
|
|
@@ -219,7 +219,10 @@ export function writeFloor(opts: CoverageOptions = {}): CoverageReport {
|
|
|
219
219
|
_comment:
|
|
220
220
|
'Behavioural-eval coverage ratchet floor (road-to-skill-eval-coverage Phase 3). ' +
|
|
221
221
|
'skill_eval_coverage --check fails if coverage drops below these counts. ' +
|
|
222
|
-
'Regenerate with `--write-floor` ONLY after authoring evals — the floor only rises.'
|
|
222
|
+
'Regenerate with `--write-floor` ONLY after authoring evals — the floor only rises. ' +
|
|
223
|
+
'LOWERING POLICY: a legitimate drop (skills merged, redundant eval removed) is allowed ' +
|
|
224
|
+
'ONLY by editing a count downward WITH a logged reason in coverage-floor.README.md — ' +
|
|
225
|
+
'never by deleting the floor file or a tier key. See coverage-floor.README.md.',
|
|
223
226
|
...floor,
|
|
224
227
|
},
|
|
225
228
|
null,
|
|
@@ -343,6 +343,21 @@ export const settingsSchema = z.object({
|
|
|
343
343
|
preview_description: z.boolean().default(false).describe(
|
|
344
344
|
'When /create-pr runs, show the generated title and body and wait for confirmation before opening the PR. Off by default (zero-friction PR creation); turn on if you want a last-look gate.',
|
|
345
345
|
),
|
|
346
|
+
detail_level: z.enum(['min', 'med', 'max']).default('min').describe(
|
|
347
|
+
'Verbosity tier for the generated PR description body. min (default) = title + 2-3 sentence what/why/impact + linked ticket (token-frugal); med = min + grouped changes + tests note; max = med + how-to-test + edge cases + reviewer guidance. Critical info (breaking changes, migrations, security, rollback) is ALWAYS included at every tier — the tier governs explanatory depth, never whether a critical callout appears.',
|
|
348
|
+
),
|
|
349
|
+
api_examples: z.boolean().default(true).describe(
|
|
350
|
+
'JSON request/response examples for API-endpoint changes. true (default) = include a fenced example ONLY when grounded in a real source (response DTO/resource, OpenAPI/schema, test fixture, or an actual probe); no grounded source → a one-line pointer, never an invented example. false = never add API examples.',
|
|
351
|
+
),
|
|
352
|
+
screenshots: z.boolean().default(false).describe(
|
|
353
|
+
'Screenshots for frontend changes. false (default) = never attempt. true = attempt when the host has browser/preview tooling and the diff touches a frontend surface; capability-gated (emits a one-line note and leaves the placeholder when tooling is absent, never fails or blocks the PR). Before/after + changed-region highlighting is best-effort.',
|
|
354
|
+
),
|
|
355
|
+
ui_paths: z.array(z.string()).default([]).describe(
|
|
356
|
+
'Optional glob list that makes frontend detection explicit instead of heuristic (e.g. ["resources/views/**", "src/pages/**"]). Empty (default) = a light path/extension heuristic that fails open (no false enrichment when the surface is ambiguous).',
|
|
357
|
+
),
|
|
358
|
+
api_paths: z.array(z.string()).default([]).describe(
|
|
359
|
+
'Optional glob list that makes API-endpoint detection explicit instead of heuristic (e.g. ["app/Http/Controllers/Api/**", "src/pages/api/**"]). Empty (default) = a light path/extension heuristic that fails open.',
|
|
360
|
+
),
|
|
346
361
|
}),
|
|
347
362
|
}),
|
|
348
363
|
memory: z.object({
|