@event4u/agent-config 9.21.0 → 9.23.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.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +69 -221
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/optimize/deep.md +13 -0
  5. package/dist/agent-src/commands/worktree/cleanup.md +17 -0
  6. package/dist/agent-src/commands/worktree/status.md +6 -0
  7. package/dist/agent-src/contexts/execution/cheap-question-mechanics.md +12 -2
  8. package/dist/agent-src/contexts/execution/mandated-lines.md +180 -0
  9. package/dist/agent-src/rules/context-hygiene.md +17 -0
  10. package/dist/agent-src/rules/downstream-changes.md +26 -0
  11. package/dist/agent-src/rules/think-before-action.md +25 -0
  12. package/dist/agent-src/rules/token-budget-discipline.md +46 -3
  13. package/dist/agent-src/rules/token-efficiency.md +13 -0
  14. package/dist/agent-src/rules/ui-audit-gate.md +33 -2
  15. package/dist/agent-src/skills/adversarial-review/SKILL.md +18 -4
  16. package/dist/agent-src/skills/decision-review/SKILL.md +49 -0
  17. package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +5 -1
  18. package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +16 -0
  19. package/dist/agent-src/skills/judge-synthesis/SKILL.md +14 -1
  20. package/dist/agent-src/skills/rule-writing/SKILL.md +33 -0
  21. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +31 -0
  22. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  23. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
  24. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  25. package/dist/cli/registry.js +1 -0
  26. package/dist/cli/registry.js.map +1 -1
  27. package/dist/cli-delegate/chunk-DVRS2KVR.js +4602 -0
  28. package/dist/cli-delegate/{chunk-ELMFOQPW.js → chunk-SJQMJI7M.js} +8 -4597
  29. package/dist/cli-delegate/cmd_export.js +2 -1
  30. package/dist/cli-delegate/cmd_prune.js +2 -1
  31. package/dist/cli-delegate/cmd_settings_migrate.js +2 -1
  32. package/dist/cli-delegate/cmd_settings_set.js +3370 -0
  33. package/dist/cli-delegate/cmd_sync.js +2 -1
  34. package/dist/cli-delegate/cmd_uninstall.js +2 -1
  35. package/dist/cli-delegate/cmd_validate.js +2 -1
  36. package/dist/discovery/deprecation-report.md +1 -1
  37. package/dist/discovery/discovery-manifest.json +20 -20
  38. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  39. package/dist/discovery/discovery-manifest.summary.md +1 -1
  40. package/dist/discovery/orphan-report.md +1 -1
  41. package/dist/discovery/packs.json +2 -2
  42. package/dist/discovery/trust-report.md +1 -1
  43. package/dist/discovery/workspaces.json +2 -2
  44. package/dist/hooks/dispatch.js +79 -1
  45. package/dist/mcp/registry-manifest.json +2 -2
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/scripts/condense.js +47 -2
  48. package/dist/scripts/condense.js.map +1 -1
  49. package/dist/server/routes/settings.js +82 -0
  50. package/dist/server/routes/settings.js.map +1 -1
  51. package/dist/shared/settingsClasses.js +191 -0
  52. package/dist/shared/settingsClasses.js.map +1 -0
  53. package/docs/contracts/settings-classes.md +342 -0
  54. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +23 -2
  55. package/docs/guidelines/agent-infra/false-green.md +187 -0
  56. package/docs/guidelines/agent-infra/gate-authoring.md +177 -0
  57. package/package.json +3 -1
  58. package/src/config/ci-local-parity.yml +12 -0
  59. package/src/config/evaluator-budgets.json +4 -4
  60. package/src/config/gate-coverage.yml +54 -12
  61. package/src/config/gate-violation-baselines.json +6 -0
  62. package/src/config/pack-size-budget.json +4 -3
  63. package/src/scripts/_cli/cmd_settings_set.ts +509 -0
  64. package/src/scripts/_dispatch.bash +16 -0
  65. package/src/scripts/_lib/bench_ab_activation.ts +368 -0
  66. package/src/scripts/_lib/gate_ledger.ts +295 -0
  67. package/src/scripts/_lib/gate_result.ts +101 -0
  68. package/src/scripts/_lib/gate_self_test.ts +129 -0
  69. package/src/scripts/_lib/measured_render.ts +97 -0
  70. package/src/scripts/_lib/ratchet_base_ref.ts +373 -0
  71. package/src/scripts/bench_ab_task_runner.ts +18 -0
  72. package/src/scripts/bench_ab_v2_run.ts +147 -6
  73. package/src/scripts/bench_ab_v2_stats.ts +74 -2
  74. package/src/scripts/check_gate_completeness.ts +244 -0
  75. package/src/scripts/check_gate_coverage.ts +35 -6
  76. package/src/scripts/check_references.ts +125 -15
  77. package/src/scripts/check_review_dispositions.ts +14 -1
  78. package/src/scripts/check_suppression_hygiene.ts +503 -0
  79. package/src/scripts/condense.ts +47 -2
  80. package/src/scripts/hook_manifest.yaml +23 -0
  81. package/src/scripts/hooks/dispatch_hook.ts +65 -1
  82. package/src/scripts/hooks/host_semantics.ts +140 -0
  83. package/src/scripts/lint_framework_leakage.ts +147 -15
  84. package/src/scripts/lint_mandated_lines.ts +339 -0
  85. package/src/scripts/lint_roadmap_complexity.ts +102 -11
  86. package/src/scripts/lint_roadmap_family_cap.ts +191 -0
  87. package/src/scripts/lint_settings_classes.ts +397 -0
  88. package/src/scripts/lint_token_budget_discipline.ts +109 -1
  89. package/src/scripts/report_imperative_density.ts +229 -0
  90. package/src/scripts/verify_physical_move.ts +10 -1
  91. package/src/scripts/worktree_cleanup_check.ts +319 -4
  92. package/src/server/io/yamlIO.ts +173 -0
  93. package/src/shared/settingsClasses.ts +235 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Estate-level result handling — telling "found violations" from "broke".
3
+ *
4
+ * A gate exits non-zero for two structurally different reasons, and the exit
5
+ * code alone cannot separate them:
6
+ *
7
+ * - **verdict** — it ran, inspected its corpus, and found violations. The
8
+ * estate is being measured correctly; something in the tree is wrong.
9
+ * - **estate-invalidating** — it could not measure at all: the scan root is
10
+ * dead, planned targets went unaccounted, the ratchet's base ref is
11
+ * unreachable, the ledger was mis-used. Nothing was certified, and a report
12
+ * built from this run is not a report.
13
+ *
14
+ * Conflating them costs in both directions. Treated as a verdict, an
15
+ * invalidated run sends a contributor hunting for a violation that does not
16
+ * exist. Treated as a crash, a real violation gets triaged as flakiness. This
17
+ * repository has recorded three traps of the second kind — estate invalidation
18
+ * misreported as a per-gate red.
19
+ *
20
+ * The classification reads the failure's own error name, which the structural
21
+ * errors already carry, rather than pattern-matching prose. A gate that throws
22
+ * something else is `crashed`: unknown, non-green, and reported as itself.
23
+ *
24
+ * **Gaming risk.** Nothing here can be gamed by a passing gate — the module
25
+ * only ever downgrades confidence. The real risk is the opposite: a future
26
+ * error class that IS estate-invalidating but is not listed below reads as a
27
+ * plain crash. Mitigated by keying on the shared `_lib` error names and by the
28
+ * fixture that pins each one; the residual is that a new structural error must
29
+ * be added here deliberately.
30
+ */
31
+
32
+ /**
33
+ * Error names that mean the measurement is void, not that the tree is dirty.
34
+ *
35
+ * Sourced from the shared gate primitives: `scan_scope`, `gate_ledger`, and
36
+ * `ratchet_base_ref`.
37
+ */
38
+ export const ESTATE_INVALIDATING_ERRORS: readonly string[] = [
39
+ 'DeadScopeError',
40
+ 'UnaccountedTargetsError',
41
+ 'LedgerUsageError',
42
+ 'BaseRefUnavailableError',
43
+ 'ProbeOverflowError',
44
+ ];
45
+
46
+ export type GateOutcome = 'clean' | 'violations' | 'crashed' | 'estate_invalid';
47
+
48
+ export interface GateRunSignal {
49
+ /** Process exit code, or `null` when the gate never produced one. */
50
+ exitCode: number | null;
51
+ /** Combined stdout + stderr, used only to spot a named structural error. */
52
+ output: string;
53
+ /** True when the runner itself could not execute the gate. */
54
+ spawnFailed?: boolean;
55
+ }
56
+
57
+ /** True when `output` names one of the structural error classes. */
58
+ export function namesEstateInvalidatingError(output: string): boolean {
59
+ return ESTATE_INVALIDATING_ERRORS.some((name) => output.includes(name));
60
+ }
61
+
62
+ /**
63
+ * Classify one gate run.
64
+ *
65
+ * Fail-closed on a null exit code: a gate that produced no verdict was not run,
66
+ * and a gate that was not run is not a passing gate.
67
+ */
68
+ export function classifyGateRun(signal: GateRunSignal): GateOutcome {
69
+ if (signal.spawnFailed === true || signal.exitCode === null) {
70
+ return 'crashed';
71
+ }
72
+ if (namesEstateInvalidatingError(signal.output)) {
73
+ return 'estate_invalid';
74
+ }
75
+ return signal.exitCode === 0 ? 'clean' : 'violations';
76
+ }
77
+
78
+ /** True when this outcome must never contribute to a green aggregate. */
79
+ export function blocksGreenAggregate(outcome: GateOutcome): boolean {
80
+ return outcome !== 'clean';
81
+ }
82
+
83
+ /**
84
+ * One-line explanation, so the estate report says WHICH kind of failure it is.
85
+ */
86
+ export function describeOutcome(id: string, outcome: GateOutcome): string {
87
+ switch (outcome) {
88
+ case 'clean':
89
+ return `${id}: clean`;
90
+ case 'violations':
91
+ return `${id}: found violations — the measurement is sound, the tree is not`;
92
+ case 'estate_invalid':
93
+ return (
94
+ `${id}: ESTATE INVALID — it could not measure (dead scope, unaccounted targets, ` +
95
+ 'unreachable base ref, or ledger misuse). Nothing was certified by this run, so ' +
96
+ 'no report built on it is a report'
97
+ );
98
+ case 'crashed':
99
+ return `${id}: crashed with no verdict — not run, therefore not passing`;
100
+ }
101
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Self-test harness — a gate proving, on demand, that its rejections fire.
3
+ *
4
+ * WHY. Every gate in this tree has fixtures in `tests/`, and those fixtures run
5
+ * in CI on the test job. That leaves one gap: nothing proves the gate binary a
6
+ * contributor actually invokes still rejects anything. A gate whose detection
7
+ * silently stopped matching — a regex that no longer fires, a scan root that
8
+ * moved, a suppression that widened — passes its unit tests against imported
9
+ * functions while the CLI it ships reports clean forever.
10
+ *
11
+ * `--self-test` closes that: the gate builds known-bad fixtures in a temporary
12
+ * directory and invokes ITSELF against them, asserting each rejection fires.
13
+ *
14
+ * **The floor is the load-bearing part.** A self-test is itself a checker, so a
15
+ * truncated case list must fail rather than print success — otherwise deleting
16
+ * cases is the cheapest way to a green self-test. {@link runSelfTest} refuses
17
+ * to pass below its declared minimum, and the minimum is a constant in the
18
+ * gate's own source where a reviewer sees it change.
19
+ *
20
+ * **Gaming risk.** The obvious degenerate pass is a case whose `expect` is
21
+ * `accept` and whose fixture is empty — it asserts nothing and still counts
22
+ * toward the floor. Mitigated by requiring at least one `reject` case and by
23
+ * counting reject cases separately in the floor check; the residual is that no
24
+ * harness can judge whether a fixture is *representative*, which stays a human
25
+ * read at review time.
26
+ */
27
+ import { spawnSync } from 'node:child_process';
28
+ import * as path from 'node:path';
29
+ import process from 'node:process';
30
+
31
+ /**
32
+ * Invoke a gate's real CLI against a fixture.
33
+ *
34
+ * Deliberately shells out rather than calling `main()` in-process: the thing
35
+ * under test is the binary a contributor runs, including its argv parsing and
36
+ * its entry guard. An in-process call would skip exactly the layers that have
37
+ * silently no-opped in this repository before.
38
+ */
39
+ export function runGateCli(
40
+ repoRoot: string,
41
+ scriptRelPath: string,
42
+ args: readonly string[],
43
+ cwd: string,
44
+ ): number {
45
+ const tsx = path.join(
46
+ repoRoot,
47
+ 'node_modules',
48
+ '.bin',
49
+ process.platform === 'win32' ? 'tsx.cmd' : 'tsx',
50
+ );
51
+ const res = spawnSync(tsx, [path.join(repoRoot, scriptRelPath), ...args], {
52
+ cwd,
53
+ encoding: 'utf-8',
54
+ maxBuffer: 64 * 1024 * 1024,
55
+ // The child must not recurse into its own self-test.
56
+ env: { ...process.env, GATE_SELF_TEST_CHILD: '1' },
57
+ });
58
+ return res.status ?? 1;
59
+ }
60
+
61
+ export interface SelfTestCase {
62
+ /** Short description, printed per case. */
63
+ name: string;
64
+ /** `reject` — the gate must fail on this fixture. `accept` — it must pass. */
65
+ expect: 'reject' | 'accept';
66
+ /** Runs the fixture and returns the gate's exit code. */
67
+ run: () => number;
68
+ }
69
+
70
+ export interface SelfTestOptions {
71
+ gate: string;
72
+ cases: readonly SelfTestCase[];
73
+ /** Minimum total cases. Below this, the run FAILS regardless of results. */
74
+ minCases: number;
75
+ /** Minimum `reject` cases. A suite that only proves passes proves nothing. */
76
+ minRejectCases?: number;
77
+ write?: (chunk: string) => unknown;
78
+ }
79
+
80
+ /**
81
+ * Run a gate's self-test suite.
82
+ *
83
+ * @returns process exit code — 0 when every case behaved and the floor held.
84
+ */
85
+ export function runSelfTest(opts: SelfTestOptions): number {
86
+ const write = opts.write ?? process.stdout.write.bind(process.stdout);
87
+ const minReject = opts.minRejectCases ?? 1;
88
+ const rejectCases = opts.cases.filter((c) => c.expect === 'reject').length;
89
+
90
+ if (opts.cases.length < opts.minCases || rejectCases < minReject) {
91
+ write(
92
+ `❌ ${opts.gate} --self-test: ${String(opts.cases.length)} case(s) ` +
93
+ `(${String(rejectCases)} rejecting) is below the declared floor of ` +
94
+ `${String(opts.minCases)} (${String(minReject)} rejecting). A truncated self-test ` +
95
+ 'must fail rather than print success — deleting cases is otherwise the cheapest ' +
96
+ 'route to a green one.\n',
97
+ );
98
+ return 1;
99
+ }
100
+
101
+ let failed = 0;
102
+ for (const testCase of opts.cases) {
103
+ let exit: number;
104
+ try {
105
+ exit = testCase.run();
106
+ } catch (e) {
107
+ write(`❌ ${opts.gate} --self-test: ${testCase.name} — threw: ${String(e)}\n`);
108
+ failed += 1;
109
+ continue;
110
+ }
111
+ const behaved = testCase.expect === 'reject' ? exit !== 0 : exit === 0;
112
+ if (behaved) {
113
+ write(`✅ ${testCase.name} (expected ${testCase.expect}, exit ${String(exit)})\n`);
114
+ } else {
115
+ write(
116
+ `❌ ${testCase.name}: expected the gate to ${testCase.expect} this fixture, ` +
117
+ `got exit ${String(exit)}. The gate's detection has stopped firing on a case ` +
118
+ 'it is supposed to catch.\n',
119
+ );
120
+ failed += 1;
121
+ }
122
+ }
123
+
124
+ write(
125
+ `\n${opts.gate} --self-test: ${String(opts.cases.length - failed)}/${String(opts.cases.length)} ` +
126
+ `case(s) behaved (${String(rejectCases)} rejecting, floor ${String(opts.minCases)})\n`,
127
+ );
128
+ return failed > 0 ? 1 : 0;
129
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Render-only-measured — a category with no measurement is never a number.
3
+ *
4
+ * WHY. The recorded failure: the always-budget report printed a share for a
5
+ * dimension it counted **zero** artefacts in. `0 / 0` became `0.0%`, and `0.0%`
6
+ * reads as a measurement that came back low — the most confident possible way
7
+ * to say nothing. Nobody looking at the page could tell the difference between
8
+ * "we measured this and it is at zero" and "we never measured this at all".
9
+ *
10
+ * The invariant is one line: **an unmeasured category renders as absent or
11
+ * explicitly "not measured", never as a zero and never as a computed share.**
12
+ * A zero is a claim; absence of measurement is not.
13
+ *
14
+ * The second half is the gap list. A coverage number published alone invites
15
+ * the reader to assume the remainder is small and boring. Naming the
16
+ * un-measured artefacts beside the covered count costs one line and removes the
17
+ * assumption — see {@link renderCoverage}.
18
+ *
19
+ * **Gaming risk.** A generator can still bypass this module and interpolate its
20
+ * own `${(n/d*100).toFixed(1)}%`. Nothing here can stop that; the mitigation is
21
+ * the paired test assertion (a generator handed an unmeasured dimension must
22
+ * emit no percentage) plus review. The residual is a generator that neither
23
+ * imports this nor carries that test, which is why the invariant is written
24
+ * into the authoring guideline rather than only into this file.
25
+ */
26
+
27
+ /** How an unmeasured category is spelled. Exported so tests can pin it. */
28
+ export const NOT_MEASURED = 'not measured';
29
+
30
+ export interface ShareOptions {
31
+ /** Decimal places for a real share. Default 1. */
32
+ precision?: number;
33
+ /**
34
+ * Set false when the dimension was not measured at all, even though a
35
+ * denominator happens to be available. A denominator alone does not prove
36
+ * the measurement ran.
37
+ */
38
+ measured?: boolean;
39
+ }
40
+
41
+ /**
42
+ * A percentage, or {@link NOT_MEASURED} when there is nothing to divide.
43
+ *
44
+ * Never returns `0.0%` for an empty denominator — that is the whole point.
45
+ */
46
+ export function renderShare(numerator: number, denominator: number, opts: ShareOptions = {}): string {
47
+ if (opts.measured === false || denominator <= 0 || !Number.isFinite(denominator)) {
48
+ return NOT_MEASURED;
49
+ }
50
+ if (!Number.isFinite(numerator)) {
51
+ return NOT_MEASURED;
52
+ }
53
+ return `${((numerator / denominator) * 100).toFixed(opts.precision ?? 1)}%`;
54
+ }
55
+
56
+ /** A count, or {@link NOT_MEASURED} when the category was never measured. */
57
+ export function renderCount(value: number | null | undefined, measured = true): string {
58
+ if (!measured || value === null || value === undefined || !Number.isFinite(value)) {
59
+ return NOT_MEASURED;
60
+ }
61
+ return String(value);
62
+ }
63
+
64
+ export interface CoverageLine {
65
+ label: string;
66
+ covered: number;
67
+ total: number;
68
+ /** The un-covered artefacts, named. Empty array means genuinely none. */
69
+ gaps: readonly string[];
70
+ /** Cap on how many gap names are printed inline. Default 8. */
71
+ maxGaps?: number;
72
+ }
73
+
74
+ /**
75
+ * A coverage number WITH its gap list.
76
+ *
77
+ * The gap list is not optional formatting. A coverage number published alone
78
+ * lets the reader assume the remainder is small and boring; naming it removes
79
+ * the assumption at the cost of one line. When the count is capped for length,
80
+ * the true remainder is still stated — a silently truncated gap list reads as
81
+ * "covered everything" exactly like the number it was meant to qualify.
82
+ */
83
+ export function renderCoverage(line: CoverageLine): string {
84
+ const share = renderShare(line.covered, line.total);
85
+ const head = `${line.label}: ${String(line.covered)}/${String(line.total)} (${share})`;
86
+ if (line.total <= 0) {
87
+ return `${line.label}: ${NOT_MEASURED} — the population is empty, so there is no coverage to report`;
88
+ }
89
+ if (line.gaps.length === 0) {
90
+ return `${head} — no gaps`;
91
+ }
92
+ const cap = line.maxGaps ?? 8;
93
+ const shown = line.gaps.slice(0, cap);
94
+ const more = line.gaps.length - shown.length;
95
+ const tail = more > 0 ? `, … and ${String(more)} more` : '';
96
+ return `${head} — not covered: ${shown.join(', ')}${tail}`;
97
+ }