@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.
Files changed (66) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +33 -0
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/pr/create.md +16 -1
  7. package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
  8. package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
  9. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/cli/registry.js +1 -0
  12. package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
  13. package/dist/cli-delegate/cmd_conformance.js +1 -1
  14. package/dist/cli-delegate/cmd_doctor.js +1 -1
  15. package/dist/discovery/deprecation-report.md +1 -1
  16. package/dist/discovery/discovery-manifest.json +5 -5
  17. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  18. package/dist/discovery/discovery-manifest.summary.md +1 -1
  19. package/dist/discovery/dormancy-report.md +1 -1
  20. package/dist/discovery/orphan-report.md +1 -1
  21. package/dist/discovery/packs.json +2 -2
  22. package/dist/discovery/trust-report.md +1 -1
  23. package/dist/discovery/workspaces.json +2 -2
  24. package/dist/hooks/dispatch.js +528 -371
  25. package/dist/mcp/registry-manifest.json +2 -2
  26. package/dist/mcp/server.json +2 -2
  27. package/dist/mcp/server.mjs +185 -98
  28. package/package.json +1 -1
  29. package/src/config/assurance-capability-registry.json +1 -1
  30. package/src/config/evaluator-budgets.json +4 -4
  31. package/src/config/gate-coverage.yml +62 -2
  32. package/src/config/gate-reachability-exemptions.json +1 -0
  33. package/src/config/gate-violation-baselines.json +4 -3
  34. package/src/config/web-launch-readiness.json +4 -2
  35. package/src/scripts/_cli/_doctor_emit.ts +71 -0
  36. package/src/scripts/_cli/_doctor_strict.ts +163 -0
  37. package/src/scripts/_cli/cmd_doctor.ts +16 -37
  38. package/src/scripts/_dispatch.bash +11 -0
  39. package/src/scripts/_lib/branch_convergence.ts +186 -0
  40. package/src/scripts/_lib/context_observation.ts +132 -0
  41. package/src/scripts/_lib/continuation_ladder.ts +188 -0
  42. package/src/scripts/_lib/experiment_binding.ts +95 -0
  43. package/src/scripts/_lib/ignored_blocker.ts +8 -1
  44. package/src/scripts/_lib/outcome_envelope.ts +6 -1
  45. package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
  46. package/src/scripts/_lib/release_highlights.ts +12 -3
  47. package/src/scripts/_lib/repeated_failure.ts +19 -2
  48. package/src/scripts/_lib/runtime_journal.ts +11 -4
  49. package/src/scripts/check_agent_artifact_location.ts +337 -0
  50. package/src/scripts/check_artefact_count_messaging.ts +74 -8
  51. package/src/scripts/check_enforcement_coverage.ts +40 -1
  52. package/src/scripts/check_release_highlights.ts +17 -1
  53. package/src/scripts/check_web_launch_readiness.ts +310 -20
  54. package/src/scripts/evolution_lab.ts +53 -1
  55. package/src/scripts/generate_capability_matrix.ts +16 -9
  56. package/src/scripts/grade_target_readiness.ts +221 -2
  57. package/src/scripts/hook_effect_doctor.ts +47 -4
  58. package/src/scripts/hooks/run_continuation_hook.ts +78 -88
  59. package/src/scripts/hooks/session_eol_hook.ts +12 -1
  60. package/src/scripts/install-hooks.sh +18 -0
  61. package/src/scripts/lint_code_comments.ts +106 -1
  62. package/src/scripts/release_drill.ts +57 -1
  63. package/src/scripts/release_publication.ts +58 -0
  64. package/src/scripts/roadmap_context.ts +15 -0
  65. package/src/scripts/skill_scout.ts +633 -0
  66. 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
- findings.push(...scanText(rel, text));
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: { changelog_file: markedChangelogFixture(current_version()) },
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).