@event4u/agent-config 14.18.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 (146) 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 +169 -130
  5. package/README.md +71 -149
  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/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  17. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  21. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  22. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  23. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  24. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  25. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  26. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  27. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  28. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  29. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  30. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  31. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  32. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  33. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  34. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  35. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  36. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  37. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  38. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  39. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  40. package/dist/cli/commands/recordTriggerEval.js +2 -2
  41. package/dist/cli/main.js +1 -1
  42. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  43. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  44. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  45. package/dist/cli-delegate/cmd_conformance.js +420 -16
  46. package/dist/cli-delegate/cmd_doctor.js +1 -1
  47. package/dist/cli-delegate/cmd_export.js +1 -1
  48. package/dist/cli-delegate/cmd_handoff.js +1 -1
  49. package/dist/cli-delegate/cmd_prune.js +1 -1
  50. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  51. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  52. package/dist/cli-delegate/cmd_sync.js +1 -1
  53. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  54. package/dist/cli-delegate/cmd_validate.js +1 -1
  55. package/dist/discovery/deprecation-report.md +1 -1
  56. package/dist/discovery/discovery-manifest.json +28 -27
  57. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  58. package/dist/discovery/discovery-manifest.summary.md +1 -1
  59. package/dist/discovery/dormancy-report.md +1 -1
  60. package/dist/discovery/orphan-report.md +1 -1
  61. package/dist/discovery/packs.json +2 -2
  62. package/dist/discovery/trust-report.md +1 -1
  63. package/dist/discovery/workspaces.json +2 -2
  64. package/dist/hooks/dispatch.js +2132 -1462
  65. package/dist/install/install.mjs +588 -527
  66. package/dist/mcp/registry-manifest.json +1 -1
  67. package/dist/mcp/server.json +2 -2
  68. package/dist/mcp/server.mjs +1619 -1045
  69. package/dist/scripts/_lib/model_tier.js +1 -1
  70. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  71. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  72. package/docs/guidelines/design-antipatterns.md +87 -64
  73. package/package.json +1 -1
  74. package/src/agent-src/README.md +1 -1
  75. package/src/config/ci-local-parity.yml +6 -0
  76. package/src/config/gate-coverage.yml +170 -0
  77. package/src/config/gate-violation-baselines.json +3 -3
  78. package/src/config/release-gate-locality.yml +12 -0
  79. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  80. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  81. package/src/scripts/_cli/cmd_update.ts +1 -1
  82. package/src/scripts/_cli/cmd_versions.ts +1 -1
  83. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  84. package/src/scripts/_lib/consequence_objects.ts +152 -0
  85. package/src/scripts/_lib/exec_evidence.ts +7 -0
  86. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  87. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  88. package/src/scripts/_lib/model_tier.ts +1 -1
  89. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  90. package/src/scripts/_lib/release_highlights.ts +93 -0
  91. package/src/scripts/_lib/release_material.ts +64 -1
  92. package/src/scripts/_lib/runtime_journal.ts +26 -3
  93. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  94. package/src/scripts/_lib/session_register.ts +11 -6
  95. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  96. package/src/scripts/_lib/surface.ts +86 -0
  97. package/src/scripts/_lib/ui_authority.ts +116 -0
  98. package/src/scripts/adr/regenerate_index.ts +54 -1
  99. package/src/scripts/adr_cite_check.ts +71 -15
  100. package/src/scripts/ai_council/clients.ts +1 -1
  101. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  102. package/src/scripts/build_proof.ts +15 -2
  103. package/src/scripts/check_beta_review_markers.ts +141 -17
  104. package/src/scripts/check_enforcement_coverage.ts +1 -1
  105. package/src/scripts/check_enforcement_denominator.ts +121 -11
  106. package/src/scripts/check_finding_dispositions.ts +66 -9
  107. package/src/scripts/check_gate_coverage.ts +96 -0
  108. package/src/scripts/check_release_highlights.ts +74 -1
  109. package/src/scripts/check_skill_activation_claim.ts +179 -0
  110. package/src/scripts/check_trigger_evals.ts +1 -1
  111. package/src/scripts/conformance_scan.ts +44 -3
  112. package/src/scripts/explain_run.ts +1 -1
  113. package/src/scripts/generate_capability_matrix.ts +1 -1
  114. package/src/scripts/git_authorization_hook.ts +75 -1
  115. package/src/scripts/hook_manifest.json +1 -1
  116. package/src/scripts/hook_manifest.yaml +14 -11
  117. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  118. package/src/scripts/hooks/category_a.ts +291 -0
  119. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  120. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  121. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  122. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  123. package/src/scripts/hooks/host_lowering.ts +215 -0
  124. package/src/scripts/hooks/host_lowering.yaml +185 -0
  125. package/src/scripts/hooks/host_semantics.ts +105 -37
  126. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  127. package/src/scripts/hooks/permission_gate.ts +117 -0
  128. package/src/scripts/hooks_status.ts +12 -10
  129. package/src/scripts/install.ts +53 -82
  130. package/src/scripts/install_anthropic_key.sh +2 -2
  131. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  132. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  133. package/src/scripts/lint_hook_manifest.ts +87 -3
  134. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  135. package/src/scripts/lint_readme_size.ts +158 -12
  136. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  137. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  138. package/src/scripts/measure_sibling_naming.ts +150 -0
  139. package/src/scripts/redact_hook_capture.ts +1 -1
  140. package/src/scripts/report_skill_activation.ts +319 -7
  141. package/src/scripts/schemas/review-findings.schema.json +5 -0
  142. package/src/scripts/schemas/skill.schema.json +2 -2
  143. package/src/scripts/session_canary_hook.ts +1 -1
  144. package/src/scripts/session_register_hook.ts +4 -3
  145. package/src/scripts/trigger_eval_grandfather.json +5 -1
  146. package/src/scripts/validate_agent_settings.ts +1 -1
@@ -351,6 +351,69 @@ export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
351
351
  export function render_mix_response(level: string): string[] {
352
352
  return [
353
353
  `> ${MIX_RESPONSE_MARKER} ${level}.`,
354
- `> Next cycle ships ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
354
+ `> ${PROMISE_PHRASE} ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
355
355
  ];
356
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
+ }
@@ -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
+ }
@@ -341,3 +341,119 @@ export function operationConflicts(authority: UiAuthority, op: Operation): Confl
341
341
  }
342
342
  return out;
343
343
  }
344
+
345
+ /* -------------------------------------------------------------------------- */
346
+ /* Antipattern signal classes against the precedence list */
347
+ /* -------------------------------------------------------------------------- */
348
+
349
+ /**
350
+ * The five classes a `design-antipatterns.md` catalog row may claim.
351
+ *
352
+ * Kept here, beside the precedence list, rather than in the catalog gate: the
353
+ * question "may this signal overrule what the user supplied" is a precedence
354
+ * question, and precedence lives in exactly one module by construction.
355
+ */
356
+ export type SignalClass =
357
+ | 'floor'
358
+ | 'invariant'
359
+ | 'craft-presumption'
360
+ | 'style-preference'
361
+ | 'reference-constraint';
362
+
363
+ /** The two classes that are opinions rather than thresholds. */
364
+ export const SOFT_CLASSES = ['craft-presumption', 'style-preference'] as const;
365
+
366
+ export interface SignalVerdict {
367
+ /**
368
+ * `blocks` — refuse the surface. `flags` — surface it as a rebuttable
369
+ * presumption. `yields` — say nothing: something with more authority has
370
+ * already decided this.
371
+ */
372
+ outcome: 'blocks' | 'flags' | 'yields';
373
+ /** The precedence level that outranked it, numbered as in this file's header. */
374
+ outranked_by: { level: number; source: ProvenanceSource; detail: string } | null;
375
+ reason: string;
376
+ }
377
+
378
+ /**
379
+ * Where a soft signal sits against the precedence list.
380
+ *
381
+ * A craft presumption is a good default and nothing more. When the run has a
382
+ * supplied reference (4), a coherent incumbent (5), or a project `DESIGN.md`
383
+ * register (6), that artifact has already decided the question the presumption
384
+ * would re-open, and re-opening it is the redesign-by-taste failure
385
+ * `design-fidelity` exists to stop.
386
+ *
387
+ * A `floor` or an `invariant` is NOT a taste call — an accessibility threshold
388
+ * or a structural correctness rule — so nothing on the list releases it, and a
389
+ * reference artifact that violates one is a finding about the reference.
390
+ *
391
+ * LEVELS 1–3 ARE DELIBERATELY NOT MECHANISED HERE. Explicit user authority, a
392
+ * registered hard constraint, and the surface brief all outrank a presumption a
393
+ * fortiori, but `Signals` carries them only as whole-surface fields
394
+ * (`surface_mode`, `register`, `change_intent`) with no per-pattern statement.
395
+ * Reading a brief's mere presence as blanket permission for every antipattern
396
+ * would silence the catalog on every briefed surface, which is the opposite of
397
+ * what those levels mean. A per-pattern override is `DESIGN.md`'s declared
398
+ * suppression, which is level 6 and IS handled.
399
+ */
400
+ export function resolveSignal(signalClass: SignalClass, signals: Signals = {}): SignalVerdict {
401
+ if (signalClass === 'floor' || signalClass === 'invariant') {
402
+ return {
403
+ outcome: 'blocks',
404
+ outranked_by: null,
405
+ reason:
406
+ `a ${signalClass} is a threshold, not a preference — no supplied artifact releases it, `
407
+ + 'and an artifact that violates one is a finding about the artifact',
408
+ };
409
+ }
410
+
411
+ if (signalClass === 'reference-constraint') {
412
+ const present = signals.reference !== undefined || signals.design_md?.present === true;
413
+ return present
414
+ ? {
415
+ outcome: 'blocks',
416
+ outranked_by: null,
417
+ reason: 'the reference or DESIGN.md this constraint is bound to is present in this run, so it carries that artifact\'s own authority',
418
+ }
419
+ : {
420
+ outcome: 'flags',
421
+ outranked_by: null,
422
+ reason: 'the reference this constraint is bound to is not present in this run, so it has no artifact behind it',
423
+ };
424
+ }
425
+
426
+ if (signals.reference !== undefined) {
427
+ return {
428
+ outcome: 'yields',
429
+ outranked_by: { level: 4, source: 'reference-artifact', detail: signals.reference.path ?? signals.reference.maturity },
430
+ reason: `a ${signalClass} never outranks a supplied reference artifact`,
431
+ };
432
+ }
433
+ if (signals.incumbent?.coherent === true) {
434
+ return {
435
+ outcome: 'yields',
436
+ outranked_by: { level: 5, source: 'incumbent-scan', detail: 'coherent incumbent' },
437
+ reason: `a ${signalClass} never outranks a coherent incumbent`,
438
+ };
439
+ }
440
+ if (signals.design_md?.present === true) {
441
+ return {
442
+ outcome: 'yields',
443
+ outranked_by: { level: 6, source: 'design-md', detail: signals.design_md.register ?? 'register unstated' },
444
+ reason: `a ${signalClass} never outranks a project DESIGN.md register`,
445
+ };
446
+ }
447
+ if (signals.product_md?.present === true) {
448
+ return {
449
+ outcome: 'yields',
450
+ outranked_by: { level: 6, source: 'product-md', detail: signals.product_md.register ?? 'register unstated' },
451
+ reason: `a ${signalClass} never outranks a project PRODUCT.md register`,
452
+ };
453
+ }
454
+ return {
455
+ outcome: 'flags',
456
+ outranked_by: null,
457
+ reason: `a ${signalClass} with nothing above it on the precedence list is a rebuttable presumption`,
458
+ };
459
+ }
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import process from 'node:process';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
- import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
19
+ import { dated_trigger_day, readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
20
20
  import { generatedByItalic } from '../_lib/generated_by.js';
21
21
 
22
22
  // ^ADR-(\d{3})-([a-z0-9-]+)\.md$
@@ -262,6 +262,41 @@ export function scanOutside(repoRoot: string, roots: readonly string[] = OUTSIDE
262
262
  return out;
263
263
  }
264
264
 
265
+ /**
266
+ * The rows whose `review_trigger` opens with a calendar date.
267
+ *
268
+ * WHY THIS SECTION CARRIES THE DATE AND NOT THE FIRED/NOT-FIRED STATE
269
+ * ------------------------------------------------------------------
270
+ * `regenerate_index --check` byte-compares this file in CI
271
+ * (`.github/workflows/rule-backstops.yml`), so anything rendered here has to be
272
+ * a function of the tree alone. A `fired` / `not-fired` column would be a
273
+ * function of the tree AND the day, which means the index would go stale on a
274
+ * calendar boundary nobody committed and red every open pull request until
275
+ * someone regenerated it. The date is the decidable half; the reader compares
276
+ * it to today, and `adr_cite_check` prints the comparison for a machine.
277
+ */
278
+ export interface DatedTriggerRow {
279
+ label: string;
280
+ path: string;
281
+ day: string;
282
+ decision: string;
283
+ }
284
+
285
+ export function datedTriggerRows(rows: readonly Row[]): DatedTriggerRow[] {
286
+ const out: DatedTriggerRow[] = [];
287
+ for (const r of rows) {
288
+ const day = dated_trigger_day(r.review_trigger ?? '');
289
+ if (day === null) continue;
290
+ out.push({
291
+ label: 'num' in r ? `ADR-${r.num as string}` : (r.path as string).slice(0, -3),
292
+ path: r.path as string,
293
+ day,
294
+ decision: r.decision ?? r.slug ?? '—',
295
+ });
296
+ }
297
+ return out.sort((a, b) => a.day.localeCompare(b.day) || a.label.localeCompare(b.label));
298
+ }
299
+
265
300
  /** Mirror `render(num, leg)`. */
266
301
  export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): string {
267
302
  const out = [
@@ -277,6 +312,24 @@ export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): stri
277
312
  if (leg.length > 0) {
278
313
  out.push('', '## Unnumbered (legacy)', '', HEAD, ...leg.map(row));
279
314
  }
315
+ const dated = datedTriggerRows([...num, ...leg]);
316
+ if (dated.length > 0) {
317
+ out.push(
318
+ '',
319
+ '## Dated review triggers',
320
+ '',
321
+ 'A `review_trigger` that opens with a calendar date is the one reopen condition',
322
+ 'this repository can decide without interpreting anything, so it is listed where a',
323
+ 'reader meets it rather than only in a command they would have to know to run.',
324
+ 'Compare the expiry to today; `./scripts-run src/scripts/adr_cite_check <ADR-NNN>`',
325
+ 'prints `fired` or `not-fired` for the same date. Every other trigger is semantic',
326
+ 'and reports `indeterminate` — a trigger that merely mentions a date is not listed.',
327
+ '',
328
+ '| Expiry | # | Decision |',
329
+ '|---|---|---|',
330
+ ...dated.map((d) => `| ${d.day} | [${d.label}](${d.path}) | ${d.decision} |`),
331
+ );
332
+ }
280
333
  if (outside.length > 0) {
281
334
  out.push(
282
335
  '',