@event4u/agent-config 9.0.0 → 9.2.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 +22 -204
- 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/implement-ticket.md +2 -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/commands/review/changes.md +22 -0
- package/dist/agent-src/contexts/execution/subagent-modes-detail.md +41 -6
- package/dist/agent-src/contexts/execution/verify-budget.md +15 -0
- package/dist/agent-src/rules/cross-source-consistency.md +92 -0
- package/dist/agent-src/rules/delegation-policy.md +1 -1
- package/dist/agent-src/skills/estimate-ticket/SKILL.md +2 -0
- package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
- 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/po-discovery/SKILL.md +2 -0
- package/dist/agent-src/skills/refine-ticket/SKILL.md +7 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +14 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +26 -5
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +18 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +14 -2
- package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +79 -0
- package/dist/agent-src/skills/subagent-orchestration/schemas/adversarial-findings.json +56 -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 +105 -28
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +16 -12
- package/dist/discovery/trust-report.md +8 -8
- package/dist/discovery/workspaces.json +12 -8
- package/dist/install/install.mjs +47 -31
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- 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 +10 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +10 -3
- package/docs/MIGRATION.md +20 -0
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-9.2.0.md +276 -0
- package/docs/benchmark.md +24 -0
- 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/decisions/ADR-122-adversarial-verification-council.md +136 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/design/adversarial-council-eval.md +106 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
- package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +109 -0
- package/docs/installation.md +2 -1
- package/docs/maintainers/dev-mode.md +3 -2
- package/docs/proof.md +7 -7
- 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 +81 -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/adversarial_council_gate.test.ts +112 -0
- package/src/scripts/_lib/adversarial_council_gate.ts +98 -0
- package/src/scripts/_lib/adversarial_reconcile.test.ts +197 -0
- package/src/scripts/_lib/adversarial_reconcile.ts +187 -0
- package/src/scripts/_lib/agent_settings.ts +4 -1
- package/src/scripts/_lib/verify_budget.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 +28 -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 +24 -0
|
@@ -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);
|
|
@@ -84,6 +84,17 @@
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"consistency": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"additionalProperties": true,
|
|
90
|
+
"properties": {
|
|
91
|
+
"cross_source": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["on", "auto", "off"],
|
|
94
|
+
"description": "Consumed by the cross-source-consistency rule. When the agent works from multiple sources (ticket text, an attached image/mockup, the spec, the codebase) it checks them against each other and asks before proceeding on a discrepancy. on (default) = surface every real cross-source contradiction / silent-scope-expansion as one question; auto = surface only high-confidence contradictions, state low-confidence as an assumption; off = no cross-source checking."
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
87
98
|
"code_style": {
|
|
88
99
|
"type": "object",
|
|
89
100
|
"additionalProperties": true,
|
|
@@ -106,6 +117,23 @@
|
|
|
106
117
|
"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
118
|
}
|
|
108
119
|
}
|
|
120
|
+
},
|
|
121
|
+
"commands": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"additionalProperties": true,
|
|
124
|
+
"properties": {
|
|
125
|
+
"create_pr": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": true,
|
|
128
|
+
"properties": {
|
|
129
|
+
"detail_level": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"enum": ["min", "med", "max"],
|
|
132
|
+
"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."
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
139
|
}
|
|
@@ -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,
|
|
@@ -26,6 +26,7 @@ const qualityCadence = z.enum(['end_of_roadmap', 'per_phase', 'per_step']);
|
|
|
26
26
|
const regenCadence = z.enum(['per_step', 'every_5_steps', 'phase_boundary']);
|
|
27
27
|
const worktreeMode = z.enum(['off', 'on', 'ask']);
|
|
28
28
|
const fidelityMode = z.enum(['strict', 'structural', 'hard-floor']);
|
|
29
|
+
const crossSourceMode = z.enum(['on', 'auto', 'off']);
|
|
29
30
|
const richSkillsMode = z.enum(['on', 'ask', 'off']);
|
|
30
31
|
const replyMethod = z.enum(['replies_endpoint', 'create_review_comment', 'auto']);
|
|
31
32
|
const confidenceBand = z.enum(['off', 'low', 'medium', 'high']);
|
|
@@ -203,6 +204,11 @@ export const settingsSchema = z.object({
|
|
|
203
204
|
'How strictly the agent must follow a user-provided prototype / mockup / design system (consumed by the design-fidelity rule). strict = build 1:1, every visible deviation needs confirmation; structural = structure locked, silent gaps fillable with a stated assumption; hard-floor = any deviation is never autonomous.',
|
|
204
205
|
),
|
|
205
206
|
}).default({ fidelity_mode: 'strict' }),
|
|
207
|
+
consistency: z.object({
|
|
208
|
+
cross_source: crossSourceMode.default('on').describe(
|
|
209
|
+
'Consumed by the cross-source-consistency rule. When the agent works from multiple sources (ticket text, an attached image/mockup, the spec, the codebase) it checks them against each other and asks before proceeding on a discrepancy — instead of silently guessing. on (default) = surface every real cross-source contradiction / silent-scope-expansion as one question; auto = surface only high-confidence contradictions, state low-confidence as an assumption; off = no cross-source checking.',
|
|
210
|
+
),
|
|
211
|
+
}).default({ cross_source: 'on' }),
|
|
206
212
|
tokens: z.object({
|
|
207
213
|
rich_skills: richSkillsMode.default('on').describe(
|
|
208
214
|
'Whether skills marked token_budget_class: rich may load in full (exempt from telegraph-speak + thin-projector trimming), consumed by the token-budget-discipline rule. on = allowed (default); off = fall back to standard condensed behavior; ask = surface an estimated token delta (tokens, not dollars) and ask once per session before loading.',
|
|
@@ -282,6 +288,9 @@ export const settingsSchema = z.object({
|
|
|
282
288
|
max_parallel: z.number().int().min(1).default(3).describe(
|
|
283
289
|
'Hard cap on subagents running in parallel during /do-in-parallel, /do-competitively, and /judge runs. Raise for faster fan-out, lower if you hit rate limits or want lower token spend.',
|
|
284
290
|
),
|
|
291
|
+
adversarial_council: z.enum(['off', 'ask', 'on']).default('off').describe(
|
|
292
|
+
'Opt-in adversarial-verification-council mode (subagent-orchestration Mode 9, ADR-122). off (default) = never runs; ask = offer it on an explicit high-risk change; on = auto-run on high-risk changes. Advisory only — a panel of distinct-model skeptics red-teams a real change for defect FINDING coverage and NEVER auto-gates it (Hard Floor). Stays default-off until the adversarial-council-finding-coverage claim is backed.',
|
|
293
|
+
),
|
|
285
294
|
}),
|
|
286
295
|
worktrees: z.object({
|
|
287
296
|
mode: worktreeMode.default('ask').describe(
|
|
@@ -343,6 +352,21 @@ export const settingsSchema = z.object({
|
|
|
343
352
|
preview_description: z.boolean().default(false).describe(
|
|
344
353
|
'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
354
|
),
|
|
355
|
+
detail_level: z.enum(['min', 'med', 'max']).default('min').describe(
|
|
356
|
+
'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.',
|
|
357
|
+
),
|
|
358
|
+
api_examples: z.boolean().default(true).describe(
|
|
359
|
+
'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.',
|
|
360
|
+
),
|
|
361
|
+
screenshots: z.boolean().default(false).describe(
|
|
362
|
+
'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.',
|
|
363
|
+
),
|
|
364
|
+
ui_paths: z.array(z.string()).default([]).describe(
|
|
365
|
+
'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).',
|
|
366
|
+
),
|
|
367
|
+
api_paths: z.array(z.string()).default([]).describe(
|
|
368
|
+
'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.',
|
|
369
|
+
),
|
|
346
370
|
}),
|
|
347
371
|
}),
|
|
348
372
|
memory: z.object({
|