@event4u/agent-config 9.7.0 → 9.8.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +5 -5
  4. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  5. package/dist/agent-src/commands/commit.md +17 -0
  6. package/dist/agent-src/commands/pr/create.md +4 -0
  7. package/dist/agent-src/contexts/override-system.md +75 -5
  8. package/dist/agent-src/rules/context-hygiene.md +2 -0
  9. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  10. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  11. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  12. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  14. package/dist/agent-src/rules/language-and-tone.md +2 -0
  15. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  16. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  17. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  18. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  19. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  20. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  21. package/dist/agent-src/rules/output-discipline.md +2 -0
  22. package/dist/agent-src/rules/persona-governance.md +2 -0
  23. package/dist/agent-src/rules/preservation-guard.md +3 -0
  24. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  25. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  26. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  27. package/dist/agent-src/rules/skill-quality.md +2 -0
  28. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  29. package/dist/agent-src/rules/source-of-truth.md +2 -0
  30. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  31. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  32. package/dist/agent-src/rules/tool-safety.md +1 -0
  33. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  34. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  35. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  36. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  37. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  38. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  39. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  40. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  41. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  42. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  43. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  44. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  45. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  46. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  52. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  53. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  54. package/dist/cli/configRoot.js +112 -0
  55. package/dist/cli/configRoot.js.map +1 -0
  56. package/dist/cli/configRoot.test.js +97 -0
  57. package/dist/cli/configRoot.test.js.map +1 -0
  58. package/dist/cli/help.js +4 -1
  59. package/dist/cli/help.js.map +1 -1
  60. package/dist/cli/main.js +19 -2
  61. package/dist/cli/main.js.map +1 -1
  62. package/dist/cli/registry.js +1 -0
  63. package/dist/cli/registry.js.map +1 -1
  64. package/dist/discovery/deprecation-report.md +1 -1
  65. package/dist/discovery/discovery-manifest.json +161 -44
  66. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  67. package/dist/discovery/discovery-manifest.summary.md +4 -4
  68. package/dist/discovery/orphan-report.md +1 -1
  69. package/dist/discovery/packs.json +13 -8
  70. package/dist/discovery/trust-report.md +11 -11
  71. package/dist/discovery/workspaces.json +11 -6
  72. package/dist/install/install.mjs +16 -0
  73. package/dist/mcp/registry-manifest.json +3 -3
  74. package/dist/router.json +1 -1
  75. package/dist/server/app.js +10 -0
  76. package/dist/server/app.js.map +1 -1
  77. package/dist/server/routes/ping.js +18 -0
  78. package/dist/server/routes/ping.js.map +1 -1
  79. package/dist/server/schemas/settings.js +8 -0
  80. package/dist/server/schemas/settings.js.map +1 -1
  81. package/dist/server/serverInfo.js +6 -2
  82. package/dist/server/serverInfo.js.map +1 -1
  83. package/dist/server/token.js +11 -4
  84. package/dist/server/token.js.map +1 -1
  85. package/dist/server/writeRoot.js +15 -1
  86. package/dist/server/writeRoot.js.map +1 -1
  87. package/dist/shared/capabilities.js +33 -0
  88. package/dist/shared/capabilities.js.map +1 -0
  89. package/dist/shared/capabilities.test.js +39 -0
  90. package/dist/shared/capabilities.test.js.map +1 -0
  91. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  92. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  93. package/dist/ui/index.html +14 -5
  94. package/docs/CLAIMS.md +62 -37
  95. package/docs/architecture.md +2 -2
  96. package/docs/benchmark.md +137 -0
  97. package/docs/catalog.md +8 -3
  98. package/docs/comparison.yaml +44 -2
  99. package/docs/contracts/local-server-ports.md +53 -0
  100. package/docs/contracts/no-runtime-boundary.md +1 -1
  101. package/docs/contracts/subagent-boundary.md +15 -0
  102. package/docs/customization.md +14 -3
  103. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  104. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  105. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  106. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  107. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  108. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  109. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  110. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  111. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  112. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  113. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  114. package/docs/decisions/INDEX.md +21 -0
  115. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  116. package/docs/distribution/awesome-list-submission.md +1 -1
  117. package/docs/distribution/registries.md +36 -27
  118. package/docs/enforcement-by-host.md +7 -0
  119. package/docs/featured-skills.md +2 -2
  120. package/docs/getting-started-by-role.md +1 -1
  121. package/docs/getting-started.md +1 -1
  122. package/docs/governance-advantage.md +1 -1
  123. package/docs/guides/gated-platform-reads.md +221 -0
  124. package/docs/positioning-evidence.md +1 -1
  125. package/docs/proof.md +92 -16
  126. package/docs/threat-model.md +1 -0
  127. package/package.json +5 -2
  128. package/src/config/agent-settings.template.yml +43 -0
  129. package/src/config/reach-channels.yml +217 -0
  130. package/src/config/reach-prescriptions-intake.yml +200 -0
  131. package/src/scripts/_dispatch.bash +15 -0
  132. package/src/scripts/_lib/exec_evidence.ts +251 -0
  133. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  134. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  135. package/src/scripts/_lib/kernel_rules.ts +46 -0
  136. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  137. package/src/scripts/_lib/risk_paths.ts +160 -0
  138. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  139. package/src/scripts/_lib/secret_detector.ts +319 -0
  140. package/src/scripts/_lib/tool_probe.ts +359 -0
  141. package/src/scripts/adr/regenerate_index.ts +81 -6
  142. package/src/scripts/audit_adr_coverage.ts +33 -0
  143. package/src/scripts/build_proof.ts +162 -0
  144. package/src/scripts/check_adr_frontmatter.ts +195 -0
  145. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  146. package/src/scripts/check_backstop_debt.ts +200 -0
  147. package/src/scripts/check_claims.ts +188 -8
  148. package/src/scripts/check_comparison.ts +12 -0
  149. package/src/scripts/check_enforcement_coverage.ts +592 -0
  150. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  151. package/src/scripts/check_reach_channels.ts +407 -0
  152. package/src/scripts/check_reach_staleness.ts +364 -0
  153. package/src/scripts/check_secret_leak.test.ts +47 -0
  154. package/src/scripts/check_secret_leak.ts +290 -0
  155. package/src/scripts/code_graph/build.ts +0 -0
  156. package/src/scripts/code_graph/cli.ts +207 -0
  157. package/src/scripts/code_graph/detect.ts +134 -0
  158. package/src/scripts/code_graph/extract.ts +386 -0
  159. package/src/scripts/code_graph/loader.ts +123 -0
  160. package/src/scripts/code_graph/query.ts +176 -0
  161. package/src/scripts/code_graph/sanitize.ts +30 -0
  162. package/src/scripts/code_graph/types.ts +83 -0
  163. package/src/scripts/code_graph/validate.ts +63 -0
  164. package/src/scripts/external_sources_denylist.json +3 -1
  165. package/src/scripts/generate_subagent_floor.ts +194 -0
  166. package/src/scripts/hook_manifest.yaml +13 -3
  167. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  168. package/src/scripts/iron_law_sha.ts +3 -12
  169. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  170. package/src/scripts/measure_rule_budget.ts +3 -12
  171. package/src/scripts/postinstall_gui.ts +129 -0
  172. package/src/scripts/reach_doctor.ts +1651 -0
  173. package/src/scripts/release.ts +24 -3
  174. package/src/scripts/risk_escalation_shadow.ts +160 -0
  175. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  176. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  177. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  178. package/src/scripts/schemas/rule.schema.json +10 -0
  179. package/src/scripts/skill_linter.ts +55 -0
  180. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  181. package/src/server/schemas/settings.ts +16 -0
  182. package/src/shared/capabilities.test.ts +43 -0
  183. package/src/shared/capabilities.ts +75 -0
  184. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  185. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Override audit — kernel-rule overrides, reported rather than policed.
4
+ *
5
+ * The obvious design for this file was a build-failing lint on any kernel-named
6
+ * file under `agents/overrides/rules/`. The council talked us out of it, and the
7
+ * argument holds: the override layer is resolved by the *agent reading the
8
+ * instructions*, not by a loader, so a consumer who wants a kernel rule relaxed
9
+ * has other routes — a persona file, host-level agent config, a line in the
10
+ * prompt. Failing the build on route one does not shrink that surface; it moves
11
+ * the override to a route with no visibility at all, while the red X reads as
12
+ * coverage. That is the exact "claims enforcement it does not have" defect this
13
+ * whole roadmap exists to remove, pointed at ourselves.
14
+ *
15
+ * So the default output is a REPORT. A consumer reading "0 kernel overrides" or
16
+ * "2 kernel overrides, both registered" learns something true.
17
+ *
18
+ * Two things do hard-fail, because on our own authoring surface the coverage
19
+ * claim IS true:
20
+ * 1. a `Mode: replace` override on a kernel / safety-floor rule — the class is
21
+ * non-replaceable, and this is mechanically decidable from the header;
22
+ * 2. an `extend` on one with no entry in `agents/overrides/kernel-exceptions.yml`
23
+ * — tightening is allowed, going unrecorded is not.
24
+ *
25
+ * Honest limit, stated in-band: an `extend` block whose prose says "ignore
26
+ * everything above" passes this check. No linter reads intent. The registry
27
+ * entry is where a human records it, and the report is what makes it reviewable.
28
+ *
29
+ * Usage:
30
+ * ./scripts-run src/scripts/lint_override_kernel_guard # report
31
+ * ./scripts-run src/scripts/lint_override_kernel_guard --json
32
+ * ./scripts-run src/scripts/lint_override_kernel_guard --strict # + hard-fails
33
+ *
34
+ * Exit codes: 0 clean · 1 violation (--strict) · 2 usage/env error.
35
+ */
36
+
37
+ import * as fs from 'node:fs';
38
+ import * as path from 'node:path';
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
+
41
+ import { KERNEL_RULE_IDS, is_kernel_rule } from './_lib/kernel_rules.js';
42
+
43
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
44
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
45
+ const OVERRIDES_RULES = path.join(REPO_ROOT, 'agents', 'overrides', 'rules');
46
+ const REGISTRY = path.join(REPO_ROOT, 'agents', 'overrides', 'kernel-exceptions.yml');
47
+ const RULES_DIR = path.join(REPO_ROOT, 'src', 'rules');
48
+
49
+ export interface OverrideRow {
50
+ rule: string;
51
+ file: string;
52
+ mode: 'extend' | 'replace' | 'unknown';
53
+ kernel: boolean;
54
+ safety_floor: boolean;
55
+ registered: boolean;
56
+ cited: boolean;
57
+ violations: string[];
58
+ }
59
+
60
+ /** `**Mode:** \`extend\`` — the header form the override contract specifies. */
61
+ export function parse_mode(text: string): 'extend' | 'replace' | 'unknown' {
62
+ const m = /^\s*\*\*Mode:\*\*\s*`?(extend|replace)`?\s*$/im.exec(text);
63
+ return m ? (m[1] as 'extend' | 'replace') : 'unknown';
64
+ }
65
+
66
+ /** `> Overrides: <rule> §<section> — <reason>` — the citation obligation. */
67
+ export function has_citation(text: string): boolean {
68
+ return /^>\s*Overrides:\s*\S+.*—.+$/im.test(text);
69
+ }
70
+
71
+ /** Rule ids carrying `tier: safety-floor`, read from the rule sources. */
72
+ export function safety_floor_ids(rules_dir: string): Set<string> {
73
+ const out = new Set<string>();
74
+ if (!fs.existsSync(rules_dir)) return out;
75
+ for (const name of fs.readdirSync(rules_dir)) {
76
+ if (!name.endsWith('.md')) continue;
77
+ const head = fs.readFileSync(path.join(rules_dir, name), 'utf-8').slice(0, 2000);
78
+ if (/^tier:\s*"?safety-floor"?\s*$/im.test(head)) out.add(name.replace(/\.md$/, ''));
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /**
84
+ * Rule ids with a registered exception.
85
+ *
86
+ * Narrow reader, not a YAML dependency: the registry is a fixed two-level shape
87
+ * and the only fact needed is which rules appear under `exceptions:`. Anything
88
+ * more permissive would let a malformed registry read as coverage.
89
+ */
90
+ export function registered_rules(text: string): Set<string> {
91
+ const out = new Set<string>();
92
+ let in_exceptions = false;
93
+ for (const raw of text.split('\n')) {
94
+ const line = raw.replace(/\s+$/, '');
95
+ if (/^exceptions:\s*$/.test(line)) { in_exceptions = true; continue; }
96
+ if (!in_exceptions) continue;
97
+ if (/^\S/.test(line)) break;
98
+ const rule_name = /^\s*-?\s*rule:\s*"?([a-z0-9-]+)"?\s*$/i.exec(line)?.[1];
99
+ if (rule_name !== undefined) out.add(rule_name);
100
+ }
101
+ return out;
102
+ }
103
+
104
+ export function audit(): OverrideRow[] {
105
+ if (!fs.existsSync(OVERRIDES_RULES)) return [];
106
+ const floor = safety_floor_ids(RULES_DIR);
107
+ const registered = fs.existsSync(REGISTRY)
108
+ ? registered_rules(fs.readFileSync(REGISTRY, 'utf-8'))
109
+ : new Set<string>();
110
+
111
+ const rows: OverrideRow[] = [];
112
+ for (const name of fs.readdirSync(OVERRIDES_RULES).sort()) {
113
+ if (!name.endsWith('.md')) continue;
114
+ const rule = name.replace(/\.md$/, '');
115
+ const kernel = is_kernel_rule(rule);
116
+ const is_floor = floor.has(rule);
117
+ if (!kernel && !is_floor) continue; // ordinary override — not this gate's business
118
+
119
+ const text = fs.readFileSync(path.join(OVERRIDES_RULES, name), 'utf-8');
120
+ const mode = parse_mode(text);
121
+ const cited = has_citation(text);
122
+ const is_registered = registered.has(rule);
123
+
124
+ const violations: string[] = [];
125
+ if (mode === 'replace') {
126
+ violations.push(
127
+ `\`replace\` on a ${kernel ? 'kernel' : 'safety-floor'} rule — this class may be tightened, never replaced`,
128
+ );
129
+ }
130
+ if (mode === 'unknown') {
131
+ violations.push(
132
+ 'no readable `**Mode:**` header — an override on this class must declare its mode',
133
+ );
134
+ }
135
+ if (mode === 'extend' && !is_registered) {
136
+ violations.push(
137
+ `\`extend\` on a ${kernel ? 'kernel' : 'safety-floor'} rule with no entry in agents/overrides/kernel-exceptions.yml`,
138
+ );
139
+ }
140
+ if (!cited) {
141
+ violations.push('missing `> Overrides: <rule> §<section> — <reason>` citation');
142
+ }
143
+
144
+ rows.push({
145
+ rule,
146
+ file: path.relative(REPO_ROOT, path.join(OVERRIDES_RULES, name)),
147
+ mode,
148
+ kernel,
149
+ safety_floor: is_floor,
150
+ registered: is_registered,
151
+ cited,
152
+ violations,
153
+ });
154
+ }
155
+ return rows;
156
+ }
157
+
158
+ function main(argv: string[]): number {
159
+ const as_json = argv.includes('--json');
160
+ const strict = argv.includes('--strict');
161
+ const rows = audit();
162
+
163
+ if (as_json) {
164
+ process.stdout.write(JSON.stringify({ kernel_overrides: rows }, null, 2) + '\n');
165
+ return 0;
166
+ }
167
+
168
+ const bad = rows.filter((r) => r.violations.length > 0);
169
+
170
+ if (rows.length === 0) {
171
+ process.stdout.write(
172
+ `✅ override audit: 0 kernel / safety-floor overrides present ` +
173
+ `(${KERNEL_RULE_IDS.length} kernel rules, none overridden)\n`,
174
+ );
175
+ return 0;
176
+ }
177
+
178
+ const lines: string[] = [];
179
+ lines.push(
180
+ `override audit: ${rows.length} kernel / safety-floor override(s) · ` +
181
+ `${rows.filter((r) => r.registered).length} registered · ${bad.length} with findings`,
182
+ );
183
+ for (const r of rows) {
184
+ const tag = r.kernel ? 'kernel' : 'safety-floor';
185
+ lines.push(` · ${r.rule} [${tag}] mode=${r.mode} registered=${r.registered ? 'yes' : 'NO'}`);
186
+ for (const v of r.violations) lines.push(` ❌ ${v}`);
187
+ }
188
+ lines.push('');
189
+ lines.push(
190
+ ' Limit: this audits files under agents/overrides/. A rule can also be ' +
191
+ 'relaxed through a persona, host config, or a direct instruction — ' +
192
+ 'those routes are not visible here and are not claimed to be covered.',
193
+ );
194
+ process.stdout.write(lines.join('\n') + '\n');
195
+
196
+ if (strict && bad.length > 0) {
197
+ process.stderr.write(
198
+ `❌ lint_override_kernel_guard: ${bad.length} override(s) violate the non-overridable class\n`,
199
+ );
200
+ return 1;
201
+ }
202
+ return 0;
203
+ }
204
+
205
+ // Main-guard (realpath-compared, mirrors the repo convention).
206
+ if (process.argv[1] !== undefined) {
207
+ try {
208
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
209
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
210
+ if (here === argv1) {
211
+ process.exit(main(process.argv.slice(2)));
212
+ }
213
+ } catch {
214
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
215
+ if (import.meta.url === argvUrl) {
216
+ process.exit(main(process.argv.slice(2)));
217
+ }
218
+ }
219
+ }
@@ -29,6 +29,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
29
29
 
30
30
  import { artefact_roots } from './_lib/agent_src.js';
31
31
  import * as token_count from './_lib/token_count.js';
32
+ import { KERNEL_RULE_ID_SET } from './_lib/kernel_rules.js';
32
33
 
33
34
  const _HERE = fileURLToPath(import.meta.url);
34
35
  // src/scripts/measure_rule_budget.ts → parents[2] is the repo root (mirrors
@@ -47,18 +48,8 @@ const PER_RULE_HARD = 2_500;
47
48
  const PER_RULE_TARGET = 2_000;
48
49
  const PER_RULE_OVERRIDE_CEILING = 4_000; // Iron-Law-override ADR ceiling.
49
50
 
50
- // Locked kernel set — docs/contracts/kernel-membership.md § 4.
51
- const KERNEL_RULES: ReadonlySet<string> = new Set([
52
- 'agent-authority',
53
- 'ask-when-uncertain',
54
- 'commit-policy',
55
- 'direct-answers',
56
- 'language-and-tone',
57
- 'no-cheap-questions',
58
- 'non-destructive-by-default',
59
- 'scope-control',
60
- 'verify-before-complete',
61
- ]);
51
+ // Locked kernel set — single-sourced in _lib/kernel_rules.ts (contract: docs/contracts/kernel-membership.md § 4).
52
+ const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_ID_SET;
62
53
 
63
54
  export interface RuleMeasure {
64
55
  id: string;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * npm `postinstall` entry — on a GLOBAL (re)install of this package, terminate a
3
+ * running local GUI server (if one is live) and start a fresh one, so the GUI a
4
+ * user has open picks up the just-installed version.
5
+ *
6
+ * Requested behaviour (2026-07-22): on `npm i -g @event4u/agent-config@latest`
7
+ * in an interactive desktop context, restart the GUI — killing any running
8
+ * instance first; and if none was running, still start it. In CI / headless /
9
+ * non-global / opted-out contexts, do nothing.
10
+ *
11
+ * Safety contract (a postinstall must never break `npm install`):
12
+ * - Every path exits 0. Any error is swallowed. The package.json hook also
13
+ * appends `|| true` and silences stderr, so a not-yet-built dist during the
14
+ * repo's own `npm ci` (dist/scripts/postinstall_gui.js absent) is a no-op.
15
+ * - The GUI is spawned DETACHED + unref'd so npm does not hang on it.
16
+ * - Gated to GLOBAL installs only (`npm_config_global`), so installing this
17
+ * package as a transitive dependency never launches a browser.
18
+ * - TTY is deliberately NOT required: npm does not give lifecycle scripts a
19
+ * TTY, so requiring one would suppress the GUI on every real terminal
20
+ * install. The reliable non-interactive signals are CI / headless / opt-out.
21
+ */
22
+
23
+ import { spawn } from 'node:child_process';
24
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { dirname, resolve } from 'node:path';
28
+
29
+ // NB: this file ships as SOURCE under `src/scripts/` (package.json `files`), so
30
+ // it must import ONLY node builtins — a cross-dir import into src/cli or
31
+ // src/server (not in the `files` whitelist) would fail the prepack-check and
32
+ // crash a source run with ERR_MODULE_NOT_FOUND. The two helpers below are
33
+ // therefore inlined from their canonical homes (kept trivially in sync):
34
+ // isHeadless() — mirror of src/cli/commands/uiServe.ts
35
+ // serverInfo — mirror of src/server/serverInfo.ts (path + pid read + clear)
36
+
37
+ /** Mirror of `src/cli/commands/uiServe.ts` isHeadless(). */
38
+ function isHeadless(): boolean {
39
+ if (process.env['SSH_CONNECTION']) return true;
40
+ if (process.platform === 'linux' && !process.env['DISPLAY']) return true;
41
+ return false;
42
+ }
43
+
44
+ /** Mirror of `src/server/serverInfo.ts` — the on-disk running-server record. */
45
+ function serverInfoPath(): string {
46
+ return resolve(homedir(), '.event4u', 'agent-config', 'local-server.json');
47
+ }
48
+ function readServerPid(): number | null {
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(serverInfoPath(), 'utf8')) as { pid?: unknown };
51
+ return typeof parsed.pid === 'number' ? parsed.pid : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function clearServerInfo(): void {
57
+ try {
58
+ rmSync(serverInfoPath(), { force: true });
59
+ } catch {
60
+ /* best-effort */
61
+ }
62
+ }
63
+
64
+ export interface LaunchEnv {
65
+ npm_config_global?: string | undefined;
66
+ CI?: string | undefined;
67
+ AGENT_CONFIG_NO_UI?: string | undefined;
68
+ }
69
+
70
+ /**
71
+ * Pure gate: should the postinstall (re)start the GUI? True only for a global
72
+ * install on a non-CI, non-opted-out, non-headless host. `headless` is passed in
73
+ * (from `isHeadless()`) to keep this unit-testable without env/platform mocking.
74
+ */
75
+ export function shouldPostinstallLaunchGui(env: LaunchEnv, headless: boolean): boolean {
76
+ const truthy = (v: string | undefined): boolean => v !== undefined && v.trim() !== '' && v.trim() !== '0';
77
+ if (!truthy(env.npm_config_global)) return false; // only global installs
78
+ if (truthy(env.CI)) return false; // never in CI
79
+ if (truthy(env.AGENT_CONFIG_NO_UI)) return false; // explicit opt-out
80
+ if (headless) return false; // SSH / no-DISPLAY
81
+ return true;
82
+ }
83
+
84
+ /** Terminate a previously-recorded live server instance, if any. Best-effort. */
85
+ function terminatePreviousInstance(): void {
86
+ const pid = readServerPid();
87
+ if (pid === null) return;
88
+ try {
89
+ process.kill(pid, 0); // liveness probe — throws if the process is gone
90
+ } catch {
91
+ clearServerInfo(); // stale record — nothing to kill
92
+ return;
93
+ }
94
+ try {
95
+ process.kill(pid, 'SIGTERM');
96
+ } catch {
97
+ // already exiting / not ours — leave the record for the new instance to overwrite
98
+ return;
99
+ }
100
+ clearServerInfo();
101
+ }
102
+
103
+ /** Spawn the config GUI detached so npm returns immediately. */
104
+ function launchGuiDetached(): void {
105
+ const here = dirname(fileURLToPath(import.meta.url)); // dist/scripts
106
+ const bin = resolve(here, '..', 'cli', 'agent-config.js'); // dist/cli/agent-config.js
107
+ if (!existsSync(bin)) return; // built artefact missing (dev npm ci before build) — no-op
108
+ const child = spawn(process.execPath, [bin, 'config'], {
109
+ detached: true,
110
+ stdio: 'ignore',
111
+ windowsHide: true,
112
+ });
113
+ child.unref();
114
+ }
115
+
116
+ export function main(env: LaunchEnv = process.env): number {
117
+ try {
118
+ if (!shouldPostinstallLaunchGui(env, isHeadless())) return 0;
119
+ terminatePreviousInstance();
120
+ launchGuiDetached();
121
+ } catch {
122
+ // A postinstall must never fail the install.
123
+ }
124
+ return 0;
125
+ }
126
+
127
+ if (import.meta.url === `file://${process.argv[1]}`) {
128
+ process.exit(main());
129
+ }