@event4u/agent-config 14.17.0 → 14.19.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 (184) 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 +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -288,3 +288,132 @@ function _first_diff(a: string, b: string): string {
288
288
  }
289
289
  return 'surfaces differ only in normalization-invisible content';
290
290
  }
291
+
292
+ /**
293
+ * The marker the governance-versus-product response line carries.
294
+ *
295
+ * Relocated here on 2026-09-05 from `check_release_highlights.ts`, where it was
296
+ * defined next to the refusal that reads it. That placement is why 14.17.0
297
+ * failed: the gate owned the marker, so the **generator** could not write the
298
+ * line without importing a gate, and therefore did not write it at all. The
299
+ * obligation was then discoverable only after `gh pr create` had already spent
300
+ * a branch, a PR and a CI run — the exact cost `guard_release_branch_push` was
301
+ * added to avoid for the sibling obligation two releases earlier.
302
+ *
303
+ * This module has no imports on purpose (see `CURATED_HEAD_INSTRUCTION`), which
304
+ * is what lets the writer, the local push guard and the CI gate all reach one
305
+ * definition instead of three copies.
306
+ */
307
+ export const MIX_RESPONSE_MARKER = '**Governance mix:**';
308
+
309
+ /**
310
+ * The sentinel that marks an EMITTED-BUT-UNANSWERED response.
311
+ *
312
+ * A generator that emits the measured level alone would satisfy the gate's
313
+ * length floor (`MIX_RESPONSE_MIN_CHARS`, 40) with a number the machine
314
+ * produced, turning a written-answer obligation into a formality the tool
315
+ * discharges for itself. That is a smuggled auto-approval, not a fix.
316
+ *
317
+ * So the writer emits the level AND this sentinel, and every gate refuses the
318
+ * sentinel — the same discipline `CURATED_HEAD_INSTRUCTION` and
319
+ * `DERIVED_MARKER` already follow. The net effect is only that the obligation
320
+ * moves from CI to the moment the section is written, where the fix is an edit
321
+ * to a file already open in the working tree.
322
+ */
323
+ export const MIX_RESPONSE_PLACEHOLDER = '<the consumer work>';
324
+
325
+ /**
326
+ * Every placeholder token the writer emits, so none of them can ship.
327
+ *
328
+ * The block carries two, and refusing only the first would leave
329
+ * `<roadmap or issue>` publishable — `CHANGELOG.md` is in `package.json`
330
+ * `files`, so that is the same "the generator's own scaffolding reached npm"
331
+ * failure `CURATED_HEAD_INSTRUCTION` exists for, one token to the right.
332
+ *
333
+ * A named list rather than a shape match over angle brackets, for the reason
334
+ * that constant already states: a pattern such as `/<[a-z ]+>/` rejects
335
+ * unrelated legitimate prose and misses a reworded placeholder. Adding a token
336
+ * to `render_mix_response` means adding it here.
337
+ */
338
+ export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
339
+ MIX_RESPONSE_PLACEHOLDER,
340
+ '<roadmap or issue>',
341
+ ];
342
+
343
+ /**
344
+ * Render the response block the writer emits when the obligation triggers.
345
+ *
346
+ * Two blockquote lines, immediately under the curated head and outside it, per
347
+ * `docs/contracts/CHANGELOG-conventions.md` § Governance-versus-product
348
+ * response — the head's ten-line cap is for product lines, and a sixth label
349
+ * would make every historical section retroactively incomplete.
350
+ */
351
+ export function render_mix_response(level: string): string[] {
352
+ return [
353
+ `> ${MIX_RESPONSE_MARKER} ${level}.`,
354
+ `> ${PROMISE_PHRASE} ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
355
+ ];
356
+ }
357
+
358
+ /**
359
+ * The words that turn a governance response into a PROMISE about a later cycle.
360
+ *
361
+ * Named rather than inlined because two sides now read it: the writer emits it,
362
+ * and the next release reads the previous section for it to decide whether a
363
+ * read-back is owed. A promise nothing reads back costs nothing to restate, so
364
+ * it can be restated for as many cycles as there are cycles — the defect this
365
+ * constant exists to make addressable.
366
+ */
367
+ export const PROMISE_PHRASE = 'Next cycle ships';
368
+
369
+ /**
370
+ * The marker under which a release head answers the PREVIOUS head's promise.
371
+ *
372
+ * A named sentinel, on the same discipline as `MIX_RESPONSE_MARKER` and
373
+ * `DERIVED_MARKER`: a shape match over prose would accept a sentence that
374
+ * happens to mention last cycle and reject a reworded answer.
375
+ */
376
+ export const PROMISE_READBACK_MARKER = '**Previous cycle:**';
377
+
378
+ /**
379
+ * The three outcomes a read-back may report, and the reason there are exactly
380
+ * three.
381
+ *
382
+ * `shipped` and `did not ship` are the honest halves of the fact. `withdrawn`
383
+ * exists because a promise the project decided not to keep is a legitimate
384
+ * third state — but only WITH a reason, which is why the answer also has to
385
+ * clear a written-length floor rather than being one of these three words on
386
+ * its own. Without the third state the mechanism would push an author toward
387
+ * restating a dead promise as still-outstanding, which is the failure it is
388
+ * built to stop.
389
+ */
390
+ export const PROMISE_OUTCOMES: readonly string[] = ['shipped', 'did not ship', 'withdrawn'];
391
+
392
+ /** Every `## X.Y.Z` release version in the file, in document order (newest first). */
393
+ export function changelog_versions(text: string): string[] {
394
+ const out: string[] = [];
395
+ for (const m of text.matchAll(/^##\s+\[?(\d+\.\d+\.\d+)\b/gmu)) {
396
+ out.push(m[1] as string);
397
+ }
398
+ return out;
399
+ }
400
+
401
+ /**
402
+ * The release published immediately before `version`, or null when there is none.
403
+ *
404
+ * Read from the CHANGELOG rather than from git tags on purpose: the promise
405
+ * being read back is a property of the SECTION, and a tag can exist for a
406
+ * version whose section was archived into `docs/archive/` under an era split —
407
+ * in which case there is no previous promise reachable in this file and the
408
+ * honest answer is that none is owed.
409
+ *
410
+ * `version` itself need not be present: a section still under authoring is
411
+ * named `[Unreleased]`, so the caller passes the version it is releasing and
412
+ * gets the newest RELEASED version that is not it.
413
+ */
414
+ export function previous_changelog_version(text: string, version: string): string | null {
415
+ const versions = changelog_versions(text);
416
+ const idx = versions.indexOf(version);
417
+ if (idx !== -1) return versions[idx + 1] ?? null;
418
+ return versions[0] ?? null;
419
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The repeated-run evidence mode and the `flaky` outcome.
3
+ *
4
+ * `docs/contracts/evidence-artifact-types.md` § The second axis is the
5
+ * contract; this is the executable half, so "a run that passes 4 of 5 times is
6
+ * recorded as flaky rather than as passing" is a thing a test can assert rather
7
+ * than a sentence in a document.
8
+ *
9
+ * Two rules carry the whole design, both from the AI council of 2026-09-04
10
+ * (2/2 convergent):
11
+ *
12
+ * 1. **`n >= 2`.** `repeated:1` is `single-run` wearing extra syntax, and
13
+ * admitting it would let one run be presented as a repeat.
14
+ * 2. **Unanimity, no threshold.** All pass → pass. All fail → fail. Anything
15
+ * else → flaky. A 4-of-5 pass is precisely the case this exists to stop
16
+ * being called a pass, so a majority rule would defeat the purpose.
17
+ */
18
+
19
+ export type ExecutionMode = { kind: 'single-run' } | { kind: 'repeated'; n: number };
20
+
21
+ export type RepeatedOutcome = 'pass' | 'fail' | 'flaky';
22
+
23
+ /** The marker, when an artifact declares one. `single-run` is never written. */
24
+ export const EVIDENCE_MODE_RE = /<!--\s*evidence-mode:\s*(single-run|repeated:(\d+))\s*-->/;
25
+
26
+ /**
27
+ * Read the mode an artifact declares.
28
+ *
29
+ * Absent → `single-run`, which is the default that keeps this axis from being
30
+ * a migration. A malformed `repeated:<n>` with `n < 2` is rejected rather than
31
+ * rounded to `single-run`: a declaration that does not parse must not read to
32
+ * its author as accepted.
33
+ */
34
+ export function parseExecutionMode(text: string): ExecutionMode | { kind: 'invalid'; reason: string } {
35
+ const m = EVIDENCE_MODE_RE.exec(text);
36
+ if (m === null) return { kind: 'single-run' };
37
+ if (m[1] === 'single-run') return { kind: 'single-run' };
38
+ const n = Number(m[2]);
39
+ if (!Number.isInteger(n) || n < 2) {
40
+ return {
41
+ kind: 'invalid',
42
+ reason: `repeated:${String(n)} declares fewer than 2 runs; repeated:1 is single-run with extra syntax`,
43
+ };
44
+ }
45
+ return { kind: 'repeated', n };
46
+ }
47
+
48
+ /**
49
+ * Aggregate `n` retained per-run verdicts.
50
+ *
51
+ * Takes the RUNS, never a summary count — an artifact that kept only
52
+ * "3 of 5 passed" has recorded a number, not evidence, and this signature is
53
+ * where that requirement is enforced rather than requested.
54
+ */
55
+ export function aggregateRepeatedRuns(runs: readonly boolean[]): RepeatedOutcome {
56
+ if (runs.length < 2) {
57
+ throw new Error(
58
+ `aggregateRepeatedRuns needs at least 2 retained runs, got ${String(runs.length)}; ` +
59
+ 'a single run is `single-run`, not a repeat',
60
+ );
61
+ }
62
+ if (runs.every((r) => r)) return 'pass';
63
+ if (runs.every((r) => !r)) return 'fail';
64
+ return 'flaky';
65
+ }
66
+
67
+ /** One line for the artifact, so `flaky` is reported as itself. */
68
+ export function describeRepeatedRun(runs: readonly boolean[]): string {
69
+ const outcome = aggregateRepeatedRuns(runs);
70
+ const passed = runs.filter((r) => r).length;
71
+ const line = `evidence-mode: repeated:${String(runs.length)} · outcome: ${outcome} · ${String(passed)}/${String(runs.length)} passed`;
72
+ if (outcome !== 'flaky') return line;
73
+ return (
74
+ `${line} — the runs did not agree, so this is neither a pass nor a fail. ` +
75
+ 'Recording it as either would be the silent green or the silent red this outcome exists to prevent.'
76
+ );
77
+ }
@@ -174,6 +174,8 @@ import type { DatabaseSync } from 'node:sqlite';
174
174
  import { current_branch, git_common_dir, git_dir } from './git_common_dir.js';
175
175
  import type { TerminalState } from './outcome_envelope.js';
176
176
  import { readRunTerminalState, RUN_TERMINAL_STATES } from './outcome_vocabularies.js';
177
+ import { addColumnIfMissing } from './sqlite_schema.js';
178
+ import { readSurface, type Surface } from './surface.js';
177
179
  import {
178
180
  isSqliteAvailableSync,
179
181
  loadSqliteSync,
@@ -408,6 +410,14 @@ export interface JournalEvent {
408
410
  at: string;
409
411
  /** Bounded identifier: the hook or command name. Never free text. */
410
412
  capability: string;
413
+ /**
414
+ * IDE / CLI / cloud dimension of the dispatch. Closed vocabulary, and
415
+ * almost always `unknown`: no host observed here sends a distinguishing
416
+ * marker and the detector refuses to infer one. Recorded anyway because
417
+ * "did this come from the surface the lattice assumed" has no answer at
418
+ * all while the field is absent.
419
+ */
420
+ surface: Surface;
411
421
  /** Imported from `outcome_envelope.ts`; null until an episode terminates. */
412
422
  terminal_state: TerminalState | null;
413
423
  /** Repo-relative locator for the return. Never an absolute path. */
@@ -447,6 +457,7 @@ export const JOURNAL_RECORD_KEYS = Object.freeze([
447
457
  'worktree_id',
448
458
  'at',
449
459
  'capability',
460
+ 'surface',
450
461
  'terminal_state',
451
462
  'return_ref',
452
463
  'verification_ref',
@@ -759,6 +770,7 @@ function createSchema(db: DatabaseSync): void {
759
770
  worktree_id TEXT NOT NULL,
760
771
  at TEXT NOT NULL,
761
772
  capability TEXT NOT NULL,
773
+ surface TEXT NOT NULL DEFAULT 'unknown',
762
774
  terminal_state TEXT,
763
775
  return_ref TEXT,
764
776
  verification_ref TEXT,
@@ -767,6 +779,9 @@ function createSchema(db: DatabaseSync): void {
767
779
  amends_seq INTEGER
768
780
  )`,
769
781
  );
782
+ // A pre-existing row takes the default, which is the honest value: that
783
+ // dispatch was never asked what surface it came from.
784
+ addColumnIfMissing(db, 'journal_event', 'surface', "TEXT NOT NULL DEFAULT 'unknown'");
770
785
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_episode ON journal_event(repository_id, episode_id, seq)');
771
786
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_session ON journal_event(repository_id, session_id, seq)');
772
787
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_task ON journal_event(repository_id, task_id)');
@@ -984,6 +999,8 @@ export interface RecordEventInput {
984
999
  prompt_id?: string | null;
985
1000
  episode_id?: string | null;
986
1001
  terminal_state?: TerminalState | null;
1002
+ /** Closed vocabulary; anything else (including absent) records `unknown`. */
1003
+ surface?: Surface | null;
987
1004
  return_ref?: string | null;
988
1005
  verification_ref?: string | null;
989
1006
  consumption?: Consumption | null;
@@ -1057,6 +1074,10 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1057
1074
  worktree_id,
1058
1075
  at,
1059
1076
  capability: input.capability,
1077
+ // Tolerant by design: a surface this build cannot place is `unknown`,
1078
+ // never a throw. The field answers "which surface" and must not be able
1079
+ // to fail a write that would otherwise have succeeded.
1080
+ surface: readSurface(input.surface),
1060
1081
  terminal_state: input.terminal_state ?? null,
1061
1082
  return_ref: requireRef('return_ref', input.return_ref),
1062
1083
  verification_ref: requireRef('verification_ref', input.verification_ref),
@@ -1070,9 +1091,9 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1070
1091
  `INSERT INTO journal_event
1071
1092
  (event, episode_id, session_id, task_id, prompt_id, boundary_status,
1072
1093
  boundary_rule_version, repository_id, worktree_id, at, capability,
1073
- terminal_state, return_ref, verification_ref, consumption, retain_until,
1074
- amends_seq)
1075
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1094
+ surface, terminal_state, return_ref, verification_ref, consumption,
1095
+ retain_until, amends_seq)
1096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1076
1097
  )
1077
1098
  .run(
1078
1099
  row.event,
@@ -1086,6 +1107,7 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1086
1107
  row.worktree_id,
1087
1108
  row.at,
1088
1109
  row.capability,
1110
+ row.surface,
1089
1111
  row.terminal_state,
1090
1112
  row.return_ref,
1091
1113
  row.verification_ref,
@@ -1111,6 +1133,7 @@ function toEvent(r: Record<string, unknown>): JournalEvent {
1111
1133
  worktree_id: String(r['worktree_id']),
1112
1134
  at: String(r['at']),
1113
1135
  capability: String(r['capability']),
1136
+ surface: readSurface(r['surface']),
1114
1137
  // Tolerant, not a cast: a value this build cannot place reads as null.
1115
1138
  terminal_state: readRunTerminalState(r['terminal_state']),
1116
1139
  return_ref: (r['return_ref'] as string | null) ?? null,
@@ -28,6 +28,10 @@ import process from 'node:process';
28
28
  import { parse as parseYaml } from 'yaml';
29
29
 
30
30
  import { inheritedGitOverrides } from './repo_root.js';
31
+ import {
32
+ checkHostPermissionSettings,
33
+ defaultHostPermissionDeps,
34
+ } from './host_permission_checks.js';
31
35
 
32
36
  /** The verdict shape `cmd_doctor` renders. Mirrors its internal `Dict` rows. */
33
37
  export interface WiringCheck {
@@ -291,7 +295,7 @@ export function checkInheritedGitEnv(env: NodeJS.ProcessEnv = process.env): Wiri
291
295
  * two spreads.
292
296
  */
293
297
  /** The four ids, in render order. Spread into `cmd_doctor`'s two id registries. */
294
- export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch'] as const;
298
+ export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch', 'host-permission-settings'] as const;
295
299
 
296
300
 
297
301
  /**
@@ -333,6 +337,7 @@ export function checkHooksDisabled(env: NodeJS.ProcessEnv = process.env): Wiring
333
337
 
334
338
  export function wiringRunners(opts: {
335
339
  packageRoot: string;
340
+ projectRoot: string;
336
341
  iterOverrides: () => Iterable<[string, unknown, string]>;
337
342
  }): Record<string, () => WiringCheck> {
338
343
  return {
@@ -344,6 +349,12 @@ export function wiringRunners(opts: {
344
349
  'hook-resolution': () => checkHookResolution(opts.packageRoot).check,
345
350
  'inherited-git-env': () => checkInheritedGitEnv(),
346
351
  'hooks-kill-switch': () => checkHooksDisabled(),
352
+ // The host's own permission settings, which decide how often a call is
353
+ // confirmed. It lives with this family rather than its own because
354
+ // `checkHookWiring` below already reads the same `~/.claude/settings.json`
355
+ // for the same host — the question is the same surface, one key over.
356
+ 'host-permission-settings': () =>
357
+ checkHostPermissionSettings(defaultHostPermissionDeps(opts.projectRoot)),
347
358
  };
348
359
  }
349
360
 
@@ -175,14 +175,19 @@ export function stop_means_session_end(platform: string | null | undefined): boo
175
175
  * Deliberately **not** derived from the frequency lattice. That instrument
176
176
  * models *slot presence*, and two hosts have a slot that never fires:
177
177
  *
178
- * - `cursor` — `user_prompt_submit` and `stop` are **IDE-only**; the CLI fires
179
- * only shell-execution hooks (`hook_manifest.yaml:365-366`,
180
- * `chat-history-platform-hooks.md:214`). The lattice has no IDE/CLI dimension,
181
- * so it reports cursor covered.
178
+ * - `cursor` — excluded because reachability there is UNESTABLISHED, not
179
+ * because it is known absent. The earlier wording asserted the CLI fires only
180
+ * shell-execution hooks, sourced to a 2026-01 reading that carried no expiry
181
+ * and could not be re-verified in either direction. The safe direction is the
182
+ * one taken: an unverified slot must not read as covered. The lattice has no
183
+ * IDE/CLI dimension of its own, so it would report cursor covered; the
184
+ * envelope now carries a `surface` field (`_lib/surface.ts`) that records
185
+ * which surface a dispatch came from, and it answers `unknown` on cursor
186
+ * because no marker distinguishes the two.
182
187
  * - `cowork` — slots are structurally wired but lifecycle events do not fire
183
188
  * (`hook_manifest.yaml:346-351`).
184
- * - `copilot` — no hook surface at all (`hook_manifest.yaml:428-429`), excluded
185
- * from the join by declaration.
189
+ * - `copilot` — nothing bound by this package (`fallback_only: true` in the
190
+ * manifest), excluded from the join by declaration.
186
191
  *
187
192
  * A session on an excluded host still *registers* if a `session_start` reaches
188
193
  * it, and simply stops being visible after its TTL. It is never reported as
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Schema helpers for the `node:sqlite` stores in this tree.
3
+ */
4
+ import type { DatabaseSync } from 'node:sqlite';
5
+
6
+ /**
7
+ * Add a column to an existing table when it is absent.
8
+ *
9
+ * `CREATE TABLE IF NOT EXISTS` is a no-op on a database that predates a
10
+ * column, so a store created by an earlier build needs the explicit add or the
11
+ * field is silently missing from every read.
12
+ *
13
+ * `PRAGMA table_info` rather than a catch around `ALTER TABLE`: swallowing the
14
+ * error would also swallow a genuine schema failure, and a store that silently
15
+ * fails to migrate reads exactly like one that never needed to.
16
+ */
17
+ export function addColumnIfMissing(db: DatabaseSync, table: string, column: string, decl: string): void {
18
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
19
+ if (cols.length === 0) return;
20
+ if (cols.some((c) => c.name === column)) return;
21
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
22
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * surface — the IDE / CLI / cloud dimension of a dispatch, as an ENVELOPE
3
+ * field rather than a platform key.
4
+ *
5
+ * WHY A FIELD AND NOT A PLATFORM. `_lib/session_register.ts` records that the
6
+ * capability lattice has no IDE/CLI dimension, which is why it reports cursor
7
+ * covered whether or not its per-turn slots fire there. Splitting `cursor`
8
+ * into `cursor-ide` / `cursor-cli` would change `--platform`, the eight
9
+ * platform identifiers, and every manifest row keyed on them. A field changes
10
+ * none of those and is still the thing `host_lowering.yaml`'s `(host, surface,
11
+ * slot)` key needs.
12
+ *
13
+ * WHAT THIS CAN AND CANNOT DECIDE — the honest half.
14
+ *
15
+ * `unknown` is the default and, today, very nearly the only answer. Nothing in
16
+ * this tree distinguishes an IDE session from a CLI one on any host: no host
17
+ * payload observed here carries such a marker, and inferring it from a TTY or
18
+ * a terminal name would be a guess wearing a function's authority — the exact
19
+ * move the surrounding roadmap exists to remove. So there is no `ide` or `cli`
20
+ * branch. When a host is observed to send a distinguishing marker, it is added
21
+ * to {@link CLOUD_PAYLOAD_MARKERS}'s sibling with a dated citation, and the
22
+ * branch arrives with the evidence rather than ahead of it.
23
+ *
24
+ * The `cloud` branch is a DECLARED VOCABULARY, not an observation. This package
25
+ * will honour these payload keys if a host sends one; none has been observed
26
+ * sending one. That distinction is the point: the mechanism is real and tested,
27
+ * the host claim is absent and says so.
28
+ */
29
+
30
+ /** The closed surface vocabulary. `unknown` is the safe default. */
31
+ export const SURFACES = ['ide', 'cli', 'cloud', 'unknown'] as const;
32
+ export type Surface = (typeof SURFACES)[number];
33
+
34
+ /**
35
+ * Payload keys whose truthy presence declares a background / cloud agent.
36
+ *
37
+ * Declared, not observed — see the module header. Kept as a list so a host that
38
+ * starts sending one needs no code change, only a citation in this comment.
39
+ */
40
+ export const CLOUD_PAYLOAD_MARKERS: readonly string[] = [
41
+ 'background_agent',
42
+ 'is_background',
43
+ 'isBackgroundAgent',
44
+ ];
45
+
46
+ /** Explicit override, for a caller that already knows the surface. */
47
+ export const SURFACE_ENV_VAR = 'AGENT_CONFIG_SURFACE';
48
+
49
+ function _isSurface(v: unknown): v is Surface {
50
+ return typeof v === 'string' && (SURFACES as readonly string[]).includes(v);
51
+ }
52
+
53
+ /**
54
+ * Resolve the surface for one dispatch.
55
+ *
56
+ * Precedence: an explicit environment override, then a declared payload
57
+ * marker, then `unknown`. An unrecognised override value is ignored rather
58
+ * than trusted — a typo must not become a capability claim.
59
+ */
60
+ export function detectSurface(
61
+ payload: unknown,
62
+ env: Record<string, string | undefined> = process.env,
63
+ ): Surface {
64
+ const override = env[SURFACE_ENV_VAR];
65
+ if (_isSurface(override)) return override;
66
+
67
+ if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) {
68
+ const obj = payload as Record<string, unknown>;
69
+ for (const key of CLOUD_PAYLOAD_MARKERS) {
70
+ if (obj[key]) return 'cloud';
71
+ }
72
+ }
73
+ return 'unknown';
74
+ }
75
+
76
+ /**
77
+ * Read a persisted or caller-supplied surface value.
78
+ *
79
+ * Tolerant, not a cast: an unrecognised value — a row written by a newer build,
80
+ * a typo, an absent field — reads as `unknown`. Same posture as
81
+ * `readRunTerminalState`, and for the same reason: a value this build cannot
82
+ * place must not fail a write or a read.
83
+ */
84
+ export function readSurface(v: unknown): Surface {
85
+ return _isSurface(v) ? v : 'unknown';
86
+ }