@event4u/agent-config 14.14.0 → 14.14.1
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +1 -1
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +528 -371
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +185 -98
- package/package.json +1 -1
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +62 -2
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +4 -3
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/release_highlights.ts +12 -3
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_release_highlights.ts +17 -1
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/evolution_lab.ts +53 -1
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/lint_code_comments.ts +106 -1
- package/src/scripts/release_drill.ts +57 -1
- package/src/scripts/release_publication.ts +58 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -353,6 +353,107 @@ function changedFiles(root: string, base: string): string[] {
|
|
|
353
353
|
return [...new Set([...tracked, ...untracked, ...dirty])].filter((f) => f.trim().length > 0);
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Line numbers this diff ADDED, per file. `'all'` for an untracked file, whose
|
|
358
|
+
* every line is new.
|
|
359
|
+
*
|
|
360
|
+
* The gate is described in `.github/workflows/rule-backstops.yml` as
|
|
361
|
+
* *"diff-scoped, so a pre-existing tree does not turn red"*, and selecting the
|
|
362
|
+
* FILE set from the diff while scanning each file WHOLE only delivers that for
|
|
363
|
+
* files nobody touches. Measured 2026-09-02 on PR #1813, which added no
|
|
364
|
+
* flagged comment at all and was refused on eleven legacy lines — four
|
|
365
|
+
* provenance paths and seven box rules — in four files it edited for unrelated
|
|
366
|
+
* reasons. A gate that reds on debt in any file you open makes every edit to an
|
|
367
|
+
* old file carry an unbounded comment sweep, and the escape it offers per line
|
|
368
|
+
* turns into exactly the allowlist growth `autonomous-execution` forbids.
|
|
369
|
+
*
|
|
370
|
+
* Teeth are unchanged where they belong: a comment the diff writes is scanned
|
|
371
|
+
* on the line it writes. The explicit-path mode (`--files`) stays whole-file,
|
|
372
|
+
* because a caller naming a path is asking for that file, not for a diff.
|
|
373
|
+
*
|
|
374
|
+
* Parsed from `--unified=0` hunk headers rather than from a patch body: the
|
|
375
|
+
* header carries the post-image start and count directly, so no line of added
|
|
376
|
+
* content has to be counted by hand.
|
|
377
|
+
*/
|
|
378
|
+
export function addedLineMap(root: string, base: string): Map<string, Set<number> | 'all'> {
|
|
379
|
+
const out = new Map<string, Set<number> | 'all'>();
|
|
380
|
+
const u = spawnSync('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
381
|
+
cwd: root, encoding: 'utf-8',
|
|
382
|
+
});
|
|
383
|
+
if (u.status === 0) {
|
|
384
|
+
for (const f of u.stdout.split('\n')) {
|
|
385
|
+
if (f.trim()) out.set(f.trim(), 'all');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const argv of [
|
|
389
|
+
['diff', '--unified=0', '--diff-filter=ACMR', `${base}...HEAD`],
|
|
390
|
+
['diff', '--unified=0', '--diff-filter=ACMR'],
|
|
391
|
+
]) {
|
|
392
|
+
const r = spawnSync('git', argv, { cwd: root, encoding: 'utf-8' });
|
|
393
|
+
if (r.status !== 0) continue;
|
|
394
|
+
mergeAddedLines(out, parseAddedLines(r.stdout));
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Added line numbers per file, from a `git diff --unified=0` patch.
|
|
401
|
+
*
|
|
402
|
+
* Split out from the git call so the parse is testable without a repository
|
|
403
|
+
* whose diff the test would have to manufacture. A `@@` header with no count
|
|
404
|
+
* means one line — `@@ -4 +4 @@` — and a `+++ /dev/null` header is a deletion,
|
|
405
|
+
* which adds nothing.
|
|
406
|
+
*/
|
|
407
|
+
export function parseAddedLines(diffText: string): Map<string, Set<number>> {
|
|
408
|
+
const out = new Map<string, Set<number>>();
|
|
409
|
+
let file: string | null = null;
|
|
410
|
+
for (const line of diffText.split('\n')) {
|
|
411
|
+
if (line.startsWith('+++ ')) {
|
|
412
|
+
const p = line.slice(4).trim();
|
|
413
|
+
file = p === '/dev/null' ? null : p.replace(/^b\//, '');
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!file || !line.startsWith('@@')) continue;
|
|
417
|
+
const m = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
418
|
+
if (!m) continue;
|
|
419
|
+
const start = Number.parseInt(m[1] as string, 10);
|
|
420
|
+
const count = m[2] === undefined ? 1 : Number.parseInt(m[2], 10);
|
|
421
|
+
const set = out.get(file) ?? new Set<number>();
|
|
422
|
+
for (let n = start; n < start + count; n += 1) set.add(n);
|
|
423
|
+
out.set(file, set);
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Fold one parsed patch into the accumulator; `'all'` always wins. */
|
|
429
|
+
function mergeAddedLines(
|
|
430
|
+
into: Map<string, Set<number> | 'all'>,
|
|
431
|
+
from: Map<string, Set<number>>,
|
|
432
|
+
): void {
|
|
433
|
+
for (const [file, lines] of from) {
|
|
434
|
+
const prev = into.get(file);
|
|
435
|
+
if (prev === 'all') continue;
|
|
436
|
+
const set = prev ?? new Set<number>();
|
|
437
|
+
for (const n of lines) set.add(n);
|
|
438
|
+
into.set(file, set);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Findings the diff is answerable for: those sitting on a line it added.
|
|
444
|
+
*
|
|
445
|
+
* `'all'` (an untracked file) keeps everything, `undefined` (a file the diff
|
|
446
|
+
* did not touch) keeps nothing — the two ends of the same rule.
|
|
447
|
+
*/
|
|
448
|
+
export function findingsOnAddedLines(
|
|
449
|
+
findings: readonly CommentFinding[],
|
|
450
|
+
lines: Set<number> | 'all' | undefined,
|
|
451
|
+
): CommentFinding[] {
|
|
452
|
+
if (lines === undefined) return [];
|
|
453
|
+
if (lines === 'all') return [...findings];
|
|
454
|
+
return findings.filter((f) => lines.has(f.line));
|
|
455
|
+
}
|
|
456
|
+
|
|
356
457
|
/** One fixture: a file body and the classes the gate must report on it. */
|
|
357
458
|
export interface CommentCase {
|
|
358
459
|
/** Printed by both consumers, so it reads as the assertion it makes. */
|
|
@@ -575,12 +676,16 @@ export function main(argv: string[]): number {
|
|
|
575
676
|
if (!base && explicit.length === 0) { process.stderr.write('--base needs a ref\n'); return 1; }
|
|
576
677
|
|
|
577
678
|
const candidates = (explicit.length > 0 ? explicit : changedFiles(root, base)).filter(isScannable);
|
|
679
|
+
// Whole file when a caller named the path; only the lines the diff added
|
|
680
|
+
// when the scope IS the diff — see `addedLineMap` for the measured reason.
|
|
681
|
+
const added = explicit.length > 0 ? null : addedLineMap(root, base);
|
|
578
682
|
const findings: CommentFinding[] = [];
|
|
579
683
|
for (const rel of candidates) {
|
|
580
684
|
const abs = path.isAbsolute(rel) ? rel : path.join(root, rel);
|
|
581
685
|
let text: string;
|
|
582
686
|
try { text = fs.readFileSync(abs, 'utf-8'); } catch { continue; }
|
|
583
|
-
|
|
687
|
+
const all = scanText(rel, text);
|
|
688
|
+
findings.push(...(added ? findingsOnAddedLines(all, added.get(rel)) : all));
|
|
584
689
|
}
|
|
585
690
|
|
|
586
691
|
try {
|
|
@@ -122,6 +122,20 @@ interface WorldConfig {
|
|
|
122
122
|
* its own, because a marker otherwise stops the run at step 8.
|
|
123
123
|
*/
|
|
124
124
|
tag_already_remote?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* The release PR is ALREADY merged when the run starts — the real
|
|
127
|
+
* resume-after-merge state, in which steps 1 through 7 skip and step 8 is
|
|
128
|
+
* the first step that does work.
|
|
129
|
+
*
|
|
130
|
+
* Added 2026-09-02 with `guard_release_branch_push`. Before that guard, a
|
|
131
|
+
* marker scenario could start from scratch and still reach step 8, because
|
|
132
|
+
* nothing between the changelog write and the tag looked at the section.
|
|
133
|
+
* The push guard now stops such a run at step 4 — correctly, and that is
|
|
134
|
+
* the point of it — which makes this knob the only way left to exercise
|
|
135
|
+
* the tag and Release guards on their own. It is not a bypass: the state
|
|
136
|
+
* it fakes is one every resumed release actually passes through.
|
|
137
|
+
*/
|
|
138
|
+
pr_already_merged?: boolean;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
/**
|
|
@@ -206,6 +220,7 @@ class FakeWorld {
|
|
|
206
220
|
this.merge_races_once = cfg.merge_races_once ?? false;
|
|
207
221
|
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
208
222
|
this.checks_fail = cfg.checks_fail ?? false;
|
|
223
|
+
this.pr_merged = cfg.pr_already_merged ?? false;
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
exec(args: readonly string[]): ExecResult {
|
|
@@ -375,10 +390,49 @@ function _count(world: FakeWorld, needle: string): number {
|
|
|
375
390
|
}
|
|
376
391
|
|
|
377
392
|
const SCENARIOS: Record<string, Scenario> = {
|
|
393
|
+
'marker-refuses-before-branch-push': {
|
|
394
|
+
summary:
|
|
395
|
+
'step 4: a draft marker refuses the branch push — the first remote state — so no PR is ever opened',
|
|
396
|
+
config: { changelog_file: markedChangelogFixture(current_version()) },
|
|
397
|
+
expect_success: false,
|
|
398
|
+
verify: (w, err) => {
|
|
399
|
+
const f: string[] = [];
|
|
400
|
+
_expect(
|
|
401
|
+
(err ?? '').includes(`refusing to push ${w.branch}`),
|
|
402
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
403
|
+
f,
|
|
404
|
+
);
|
|
405
|
+
// The load-bearing assertions are about what did NOT happen. Each
|
|
406
|
+
// one is a remote side effect the 14.14.0 run actually produced
|
|
407
|
+
// before this guard existed: a pushed branch, an open PR, and a
|
|
408
|
+
// check watch on a check that could not pass.
|
|
409
|
+
_expect(
|
|
410
|
+
!w.calls.includes(`git push -u origin ${w.branch}`),
|
|
411
|
+
'the branch was pushed after the refusal',
|
|
412
|
+
f,
|
|
413
|
+
);
|
|
414
|
+
_expect(
|
|
415
|
+
!w.calls.some((c) => c.startsWith('gh pr create')),
|
|
416
|
+
'a pull request was opened after the refusal',
|
|
417
|
+
f,
|
|
418
|
+
);
|
|
419
|
+
_expect(
|
|
420
|
+
!w.calls.some((c) => c.includes('--watch')),
|
|
421
|
+
'CI was waited on after the refusal',
|
|
422
|
+
f,
|
|
423
|
+
);
|
|
424
|
+
_expect(!w.tag_local && !w.tag_remote, 'a tag exists after the refusal', f);
|
|
425
|
+
_expect(!w.release_created, 'a GitHub Release was created after the refusal', f);
|
|
426
|
+
return f;
|
|
427
|
+
},
|
|
428
|
+
},
|
|
378
429
|
'marker-refuses-before-tag': {
|
|
379
430
|
summary:
|
|
380
431
|
'step 8: a draft marker in the merged section refuses BEFORE the annotated tag is created',
|
|
381
|
-
config: {
|
|
432
|
+
config: {
|
|
433
|
+
pr_already_merged: true,
|
|
434
|
+
changelog_file: markedChangelogFixture(current_version()),
|
|
435
|
+
},
|
|
382
436
|
expect_success: false,
|
|
383
437
|
verify: (w, err) => {
|
|
384
438
|
const f: string[] = [];
|
|
@@ -403,6 +457,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
403
457
|
summary:
|
|
404
458
|
'step 8 (§ 3.2): a tag created but never pushed is NOT pushed when its section carries the marker',
|
|
405
459
|
config: {
|
|
460
|
+
pr_already_merged: true,
|
|
406
461
|
tag_created_unpushed: true,
|
|
407
462
|
changelog_file: markedChangelogFixture(current_version()),
|
|
408
463
|
},
|
|
@@ -431,6 +486,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
431
486
|
summary:
|
|
432
487
|
'step 9: with the tag already pushed, a marker in the TAGGED section refuses before the Release is created',
|
|
433
488
|
config: {
|
|
489
|
+
pr_already_merged: true,
|
|
434
490
|
tag_already_remote: true,
|
|
435
491
|
changelog: markedChangelogFixture(current_version()),
|
|
436
492
|
changelog_file: markedChangelogFixture(current_version()),
|
|
@@ -422,7 +422,65 @@ export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
|
422
422
|
return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Refuse the FIRST remote state of a release whose section is not publishable.
|
|
427
|
+
*
|
|
428
|
+
* The fourth guard site, and the one the other three structurally cannot cover.
|
|
429
|
+
* They sit at the irreversible transitions — annotated tag, resumed tag push,
|
|
430
|
+
* GitHub Release body — and every one of them is DOWNSTREAM of `gh pr create`
|
|
431
|
+
* and of `gh pr checks --watch`. `check_release_highlights` refuses the same
|
|
432
|
+
* section from the other side, on the release PR, so a run whose head is still
|
|
433
|
+
* the generator's draft pushed a branch, opened a PR, burned CI, waited on a
|
|
434
|
+
* check that could not pass, and died there — leaving a half-open release to
|
|
435
|
+
* clean up by hand.
|
|
436
|
+
*
|
|
437
|
+
* That is not a hypothetical: it is PR #1812 (14.14.0, 2026-09-02), and the
|
|
438
|
+
* 2026-09-01 flip's own risk register pre-registered it as Risk 1 — *"the flip
|
|
439
|
+
* reds the next release PR on its first run"*. Its stated mitigation was that
|
|
440
|
+
* the releaser edits prose before merge; nothing in the pipeline asked, so
|
|
441
|
+
* nothing did.
|
|
442
|
+
*
|
|
443
|
+
* The branch push is the first thing in the pipeline that leaves the machine,
|
|
444
|
+
* so a refusal here costs one local edit and no remote state at all: no branch
|
|
445
|
+
* on the remote, no pull request, no CI minutes, no tag. It sits at the push
|
|
446
|
+
* CALL SITE rather than in a formatter for the reason the publication guards
|
|
447
|
+
* already state — a formatter has no notion of whether it is publishing, and
|
|
448
|
+
* the call site is the one that can still stop. Deriving the version from
|
|
449
|
+
* `release/X.Y.Z` mirrors what `release-validation.yml` already does with
|
|
450
|
+
* `head_ref`.
|
|
451
|
+
*
|
|
452
|
+
* Two states pass unguarded, deliberately: a branch that is not
|
|
453
|
+
* `release/X.Y.Z` (nothing about a release to read), and a version with no
|
|
454
|
+
* changelog section (the missing-section refusal belongs to the publication
|
|
455
|
+
* sites, which need a section to publish; refusing here would break every
|
|
456
|
+
* non-release push through this helper).
|
|
457
|
+
*/
|
|
458
|
+
export function guard_release_branch_push(branch: string): void {
|
|
459
|
+
const prefix = 'release/';
|
|
460
|
+
if (!branch.startsWith(prefix)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const version = branch.slice(prefix.length);
|
|
464
|
+
const section = extract_changelog_section(read_changelog_text(), version);
|
|
465
|
+
if (!section) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const blockers = publication_blockers(section.body, version, `\`${branch}\``);
|
|
469
|
+
if (blockers.length === 0) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
die(
|
|
473
|
+
`refusing to push ${branch} — the ${version} changelog section is not publishable ` +
|
|
474
|
+
`(${String(blockers.length)} blocker(s)):\n` +
|
|
475
|
+
blockers.map((b) => ` - ${b}`).join('\n') +
|
|
476
|
+
`\n Nothing has left this machine: no branch on ${REMOTE}, no pull request, no tag.\n` +
|
|
477
|
+
' Curate the `### Release highlights` head in CHANGELOG.md under ' +
|
|
478
|
+
`\`## [${version}]\`, then re-run \`task release -- --resume\`.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
425
482
|
function push_release_branch(branch: string): void {
|
|
483
|
+
guard_release_branch_push(branch);
|
|
426
484
|
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
|
|
427
485
|
if (first.returncode === 0) {
|
|
428
486
|
process.stdout.write(first.stdout);
|
|
@@ -49,6 +49,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
49
49
|
import { gitEnv } from './_lib/git_env.js';
|
|
50
50
|
import { read_live_records, register_dir } from './_lib/session_register.js';
|
|
51
51
|
import { other_worktree_branches_detailed } from './sessions_cli.js';
|
|
52
|
+
import { recordContextObservation } from './_lib/context_observation.js';
|
|
52
53
|
|
|
53
54
|
/** One open pull request, with the file set it changes. */
|
|
54
55
|
export interface PullRequestInfo {
|
|
@@ -746,6 +747,20 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
746
747
|
roadmap: at('--roadmap'),
|
|
747
748
|
...(ownedFile !== null ? { ownedPaths: readOwnedPathsFile(ownedFile) } : {}),
|
|
748
749
|
});
|
|
750
|
+
// `road-to-wired-instruments` 2.1 — this probe is the only thing in the tree
|
|
751
|
+
// that can OBSERVE the world (it costs a `gh` call), so it records what it
|
|
752
|
+
// saw where a Stop-path concern can read it without spawning anything. The
|
|
753
|
+
// continuation ladder's premise rung is the consumer; the resume checkpoint
|
|
754
|
+
// is the other.
|
|
755
|
+
//
|
|
756
|
+
// Only a LIVE reading is recorded. Offline, `contextFingerprint` still
|
|
757
|
+
// returns a digest — over a null base and an empty PR list — and that digest
|
|
758
|
+
// is a statement about the network, not about the repository. Recording it
|
|
759
|
+
// would make every offline probe look like a moved premise, which is a false
|
|
760
|
+
// halt manufactured out of a dropped connection.
|
|
761
|
+
if (ctx.network === 'live') {
|
|
762
|
+
recordContextObservation(root, ctx.roadmap ?? '', ctx.fingerprint);
|
|
763
|
+
}
|
|
749
764
|
if (argv.includes('--fingerprint')) {
|
|
750
765
|
// Just the digest, for the loop's phase-boundary comparison and for the
|
|
751
766
|
// resume checkpoint's `context_fingerprint` (§ 5.6 — same value).
|