@event4u/agent-config 17.2.1 → 17.3.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 (152) 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/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +152 -0
  6. package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  9. package/dist/cli/commands/recordTriggerEval.js +1 -1
  10. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
  11. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  12. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
  13. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
  14. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  15. package/dist/cli-delegate/cmd_conformance.js +20 -14
  16. package/dist/cli-delegate/cmd_doctor.js +3 -3
  17. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  18. package/dist/cli-delegate/cmd_explain.js +1 -1
  19. package/dist/cli-delegate/cmd_export.js +2 -2
  20. package/dist/cli-delegate/cmd_prune.js +2 -2
  21. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  22. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  23. package/dist/cli-delegate/cmd_sync.js +2 -2
  24. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  25. package/dist/cli-delegate/cmd_validate.js +2 -2
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +3 -3
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +1 -1
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +2 -2
  33. package/dist/discovery/trust-report.md +1 -1
  34. package/dist/discovery/workspaces.json +2 -2
  35. package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
  36. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  37. package/dist/hooks/dispatch.js +21 -11
  38. package/dist/hooks/dispatch.sha256 +1 -1
  39. package/dist/hooks/egress_engine.chunk.cjs +1 -1
  40. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  41. package/dist/install/install.mjs +6974 -6037
  42. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  43. package/dist/mcp/registry-manifest.json +3 -2
  44. package/dist/mcp/server.json +13 -4
  45. package/dist/mcp/server.mjs +5561 -2463
  46. package/dist/scripts/_lib/egress_rules.js +8 -2
  47. package/dist/scripts/condense.js +9 -9
  48. package/dist/server/routes/discovery.js +1 -1
  49. package/dist/server/routes/install.js +1 -1
  50. package/dist/server/routes/ping.js +1 -1
  51. package/dist/server/routes/schema.js +4 -4
  52. package/dist/server/routes/settings.js +2 -6
  53. package/dist/server/routes/wizard.js +6 -6
  54. package/dist/server/schemas/settings.js +42 -42
  55. package/dist/shared/settingsSurface.js +1 -1
  56. package/dist/shared/userMd/schema.js +1 -1
  57. package/dist/shared/zodJsonSchema.js +47 -0
  58. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  59. package/dist/ui/index.html +1 -1
  60. package/package.json +3 -3
  61. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  62. package/src/config/ci-local-parity.yml +11 -0
  63. package/src/config/continuity-surface.json +9 -2
  64. package/src/config/gate-violation-baselines.json +3 -3
  65. package/src/config/gitignore-block.txt +6 -5
  66. package/src/config/hook-bundle-budget.json +16 -1
  67. package/src/config/pack-size-budget.json +12 -1
  68. package/src/config/packed-binary-manifest.json +9 -0
  69. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  70. package/src/scripts/_dispatch.bash +1 -1
  71. package/src/scripts/_lib/egress_rules.ts +8 -2
  72. package/src/scripts/_lib/listing_manifests.ts +85 -0
  73. package/src/scripts/_lib/neighbour_census.ts +1 -0
  74. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  75. package/src/scripts/adoption_status.ts +231 -27
  76. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  77. package/src/scripts/check_dependency_floors.ts +7 -0
  78. package/src/scripts/check_release_manifest_content.ts +17 -2
  79. package/src/scripts/check_release_pr_shape.ts +5 -0
  80. package/src/scripts/code_graph/build.ts +80 -44
  81. package/src/scripts/code_graph/cli.ts +260 -16
  82. package/src/scripts/code_graph/conventions_default.ts +228 -0
  83. package/src/scripts/code_graph/extract.ts +5 -2
  84. package/src/scripts/code_graph/extract_more.ts +105 -1
  85. package/src/scripts/code_graph/loader.ts +10 -2
  86. package/src/scripts/code_graph/map.ts +174 -41
  87. package/src/scripts/code_graph/map_model.ts +53 -15
  88. package/src/scripts/code_graph/map_verify.ts +12 -0
  89. package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
  90. package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
  91. package/src/scripts/code_graph/passes.ts +173 -0
  92. package/src/scripts/code_graph/providers/angular.ts +55 -0
  93. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  94. package/src/scripts/code_graph/providers/catalog.ts +290 -0
  95. package/src/scripts/code_graph/providers/django.ts +163 -0
  96. package/src/scripts/code_graph/providers/express.ts +25 -0
  97. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  98. package/src/scripts/code_graph/providers/flask.ts +99 -0
  99. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  100. package/src/scripts/code_graph/providers/index.ts +101 -0
  101. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  102. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  103. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  104. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  105. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  106. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  107. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  108. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  109. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  110. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  111. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  112. package/src/scripts/code_graph/providers/spring.ts +138 -0
  113. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  114. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  115. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  116. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  117. package/src/scripts/code_graph/providers/types.ts +56 -0
  118. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  119. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  120. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  121. package/src/scripts/code_graph/refresh_lock.ts +193 -0
  122. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  123. package/src/scripts/code_graph/scip_reader.ts +2 -2
  124. package/src/scripts/code_graph/sqlite_store.ts +10 -4
  125. package/src/scripts/code_graph/types.ts +75 -41
  126. package/src/scripts/code_graph/validate.ts +6 -8
  127. package/src/scripts/condense.ts +9 -9
  128. package/src/scripts/generate_settings_reference.ts +2 -6
  129. package/src/scripts/hook_chunks.mjs +4 -0
  130. package/src/scripts/hook_manifest.json +1 -1
  131. package/src/scripts/hook_manifest.yaml +24 -7
  132. package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
  133. package/src/scripts/hooks/lazy_concern.ts +4 -0
  134. package/src/scripts/install-hooks.sh +54 -33
  135. package/src/scripts/install.ts +2 -6
  136. package/src/scripts/lint_hook_manifest.ts +2 -0
  137. package/src/scripts/lint_marketplace.ts +33 -0
  138. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  139. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
  140. package/src/scripts/mcp_server/graph_tools.ts +5 -1
  141. package/src/scripts/release.ts +2 -5
  142. package/src/scripts/release_drill.ts +4 -1
  143. package/src/scripts/release_env.ts +17 -0
  144. package/src/scripts/release_mix_taxonomy.json +3 -0
  145. package/src/server/schemas/settings.ts +42 -42
  146. package/src/shared/settingsSurface.ts +1 -1
  147. package/src/shared/userMd/schema.ts +1 -1
  148. package/src/shared/zodJsonSchema.ts +41 -0
  149. package/src/vendor/grammars/README.md +6 -5
  150. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  151. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  152. package/dist/ui/assets/index-w70Ado09.js +0 -23
@@ -17,6 +17,8 @@
17
17
  * - metadata.version must match package.json (single source of truth)
18
18
  * - every version field in BOTH .augment-plugin/ manifests matches
19
19
  * package.json too — see check_augment_manifests()
20
+ * - the Copilot plugin and Gemini extension match it in `version` and in the
21
+ * pinned `npx` argument — see check_listing_manifests()
20
22
  * - the union of plugins[].skills[] must be EXACTLY the pointer skill
21
23
  * (./.claude-plugin/skills/install-agent-config) — a repopulated
22
24
  * content-skill list FAILS by design
@@ -29,6 +31,7 @@ import fs from "node:fs";
29
31
  import path from "node:path";
30
32
  import { fileURLToPath, pathToFileURL } from "node:url";
31
33
 
34
+ import { LISTING_MANIFESTS, listingVersionFindings } from "./_lib/listing_manifests.js";
32
35
  import { assertWatchlistResolves, DeadScopeError } from "./_lib/scan_scope.js";
33
36
 
34
37
  const ROOT = ".";
@@ -149,6 +152,34 @@ export function check_augment_manifests(
149
152
  }
150
153
  }
151
154
 
155
+ /**
156
+ * The Copilot discovery plugin and the Gemini extension carry the package
157
+ * version AND pin it inside their `npx` arguments; a crawler installs from the
158
+ * default branch, so either copy drifting ships a stale server to every new
159
+ * install. Same sync rule as the Augment twins, plus the pin.
160
+ */
161
+ export function check_listing_manifests(
162
+ pkgVersion: string,
163
+ errors: string[],
164
+ base: string = ROOT,
165
+ ): void {
166
+ for (const rel of LISTING_MANIFESTS) {
167
+ const file = path.join(base, rel);
168
+ if (!_exists(file)) {
169
+ errors.push(`${file} not found (a listing manifest the release version-syncs)`);
170
+ continue;
171
+ }
172
+ let data: unknown;
173
+ try {
174
+ data = JSON.parse(fs.readFileSync(file, "utf-8"));
175
+ } catch (e) {
176
+ errors.push(`${file} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
177
+ continue;
178
+ }
179
+ errors.push(...listingVersionFindings(file, data, pkgVersion));
180
+ }
181
+ }
182
+
152
183
  /** Resolve a key path, expanding `*` over array indices. Returns [label, value]. */
153
184
  function _resolveVersionField(
154
185
  data: JsonValue,
@@ -185,6 +216,7 @@ export function main(): number {
185
216
  ".claude-plugin/marketplace.json",
186
217
  ".augment-plugin/plugin.json",
187
218
  ".augment-plugin/marketplace.json",
219
+ ...LISTING_MANIFESTS,
188
220
  "package.json",
189
221
  `${POINTER_SKILL_ENTRY.replace(/^\.\//, "")}/SKILL.md`,
190
222
  ],
@@ -248,6 +280,7 @@ export function main(): number {
248
280
  }
249
281
  if (typeof pkgVersion === "string") {
250
282
  check_augment_manifests(pkgVersion, errors);
283
+ check_listing_manifests(pkgVersion, errors);
251
284
  }
252
285
  }
253
286
  } else {
@@ -27,6 +27,7 @@ import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
30
+ import { REGISTRY_DESCRIPTION_MAX, SERVER_SCHEMA_URL } from './build_mcp_registry_manifest.js';
30
31
 
31
32
  const _HERE = path.resolve(fileURLToPath(import.meta.url));
32
33
  // ROOT = Path(__file__).resolve().parents[2]
@@ -286,6 +287,37 @@ function _validateSchema(data: Json, schema: JsonObject): SchemaError | null {
286
287
  return errors.length ? errors[0]! : null;
287
288
  }
288
289
 
290
+ /**
291
+ * What the live registry rejects or mis-installs, checked before a release
292
+ * publishes it: the schema pin, the 100-character description cap, and the
293
+ * `mcp-server` argument without which a client starts the bare CLI.
294
+ */
295
+ export function checkRegistryPayload(server: JsonObject): string | null {
296
+ if (server['$schema'] !== SERVER_SCHEMA_URL) {
297
+ return `$schema must be ${SERVER_SCHEMA_URL} (got ${String(server['$schema'])})`;
298
+ }
299
+ const description = server['description'];
300
+ if (typeof description !== 'string' || description.length === 0) {
301
+ return 'description is missing — set package.json agentConfig.registryDescription';
302
+ }
303
+ if (description.length > REGISTRY_DESCRIPTION_MAX) {
304
+ return `description is ${description.length} characters; the registry accepts at most ${REGISTRY_DESCRIPTION_MAX}`;
305
+ }
306
+ const packages = server['packages'];
307
+ const first = Array.isArray(packages) ? (packages[0] as JsonObject | undefined) : undefined;
308
+ const args = first?.['packageArguments'];
309
+ const startsServer =
310
+ Array.isArray(args) &&
311
+ args.some((a) => {
312
+ const arg = a as JsonObject;
313
+ return arg['type'] === 'positional' && arg['value'] === 'mcp-server';
314
+ });
315
+ if (!startsServer) {
316
+ return 'the npm package entry has no positional `mcp-server` argument, so a client would start the CLI';
317
+ }
318
+ return null;
319
+ }
320
+
289
321
  function main(argv: readonly string[]): number {
290
322
  let args: Args;
291
323
  try {
@@ -369,6 +401,10 @@ function main(argv: readonly string[]): number {
369
401
  `(${String(pkg_for_gate['mcpName'])})`,
370
402
  );
371
403
  }
404
+ const registryProblem = checkRegistryPayload(server_payload);
405
+ if (registryProblem !== null) {
406
+ return _fail(`server.json: ${registryProblem}`);
407
+ }
372
408
 
373
409
  if (!args.quiet) {
374
410
  const registries = (manifest as JsonObject)['registries'];
@@ -372,6 +372,7 @@
372
372
  "migrates",
373
373
  "persists",
374
374
  "relates",
375
+ "injects",
375
376
  "documents"
376
377
  ],
377
378
  "description": "Restrict to one relation. Omit for all of them."
@@ -62,7 +62,7 @@ import {
62
62
  testsFor,
63
63
  untested,
64
64
  } from '../code_graph/verbs.js';
65
- import { areaReport, readMap } from '../code_graph/map.js';
65
+ import { areaReport, graphGeneration, readMap } from '../code_graph/map.js';
66
66
  import type { BuiltinTool } from './tools.js';
67
67
 
68
68
  /** The `no graph here` answer, in one place so all five agree on its shape. */
@@ -134,6 +134,7 @@ const RELATIONS = [
134
134
  'migrates',
135
135
  'persists',
136
136
  'relates',
137
+ 'injects',
137
138
  'documents',
138
139
  ] as const;
139
140
 
@@ -399,11 +400,14 @@ export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
399
400
  map: 'present',
400
401
  drawn_at_commit: map.meta.commit,
401
402
  graph_state_when_drawn: map.meta.state,
403
+ // Same generation → the map shows exactly the graph the queries read.
404
+ map_matches_graph: map.meta.graph_generation !== undefined && map.meta.graph_generation === graphGeneration(path.join(root, NATIVE_CACHE_REL)),
402
405
  files: m.files.length,
403
406
  areas: m.areas.length,
404
407
  trusted_links: m.analyses.trustedEdges,
405
408
  guess_share: m.analyses.guessShare,
406
409
  request_paths: m.analyses.requestPathsTotal,
410
+ providers: (m.providers ?? []).map((p) => `${p.id} (${p.status})`),
407
411
  largest_areas: m.areas.slice(0, 5).map((a) => `${a.id} ${a.name} (${a.files.length})`),
408
412
  map_md: 'agents/map/MAP.md',
409
413
  index_html: 'agents/map/index.html',
@@ -152,8 +152,6 @@ const _HERE = fileURLToPath(import.meta.url);
152
152
 
153
153
  import {
154
154
  ArgparseExit,
155
- AUGMENT_MARKETPLACE_JSON,
156
- AUGMENT_PLUGIN_JSON,
157
155
  CHANGELOG,
158
156
  _set_changelog_reader,
159
157
  read_changelog_text,
@@ -169,7 +167,7 @@ import {
169
167
  REPO_ROOT,
170
168
  REPO_SLUG,
171
169
  SystemExitError,
172
- set_augment_manifest_version,
170
+ set_plugin_manifest_versions,
173
171
  set_marketplace_version,
174
172
  set_template_pin,
175
173
  _rstrip,
@@ -939,8 +937,7 @@ function execute(
939
937
  set_package_version(PACKAGE_JSON, plan.target, plan.commits);
940
938
  set_lockfile_version(PACKAGE_LOCK_JSON, plan.target);
941
939
  set_marketplace_version(MARKETPLACE_JSON, plan.target);
942
- set_augment_manifest_version(AUGMENT_PLUGIN_JSON, plan.target);
943
- set_augment_manifest_version(AUGMENT_MARKETPLACE_JSON, plan.target);
940
+ set_plugin_manifest_versions(plan.target);
944
941
  set_template_pin(PROJECT_TEMPLATE, plan.target);
945
942
  writeReleasedSurface(plan.target);
946
943
  prepend_changelog(CHANGELOG, plan.changelog_entry);
@@ -44,6 +44,7 @@ import { releaseHoldPreflight } from './_lib/release_holds.js';
44
44
  import { SETTINGS_SURFACE_FIXTURE } from './_lib/release_settings_surface.js';
45
45
  import { die } from './release_publication.js';
46
46
  import { DERIVED_MARKER } from './_lib/release_highlights.js';
47
+ import { listingManifestPaths } from './_lib/listing_manifests.js';
47
48
  import {
48
49
  AUGMENT_MARKETPLACE_JSON,
49
50
  AUGMENT_PLUGIN_JSON,
@@ -52,13 +53,14 @@ import {
52
53
  PACKAGE_JSON,
53
54
  PACKAGE_LOCK_JSON,
54
55
  PROJECT_TEMPLATE,
56
+ REPO_ROOT as ENV_REPO_ROOT,
55
57
  } from './release_env.js';
56
58
 
57
59
  /**
58
60
  * The files step 2 mutates on disk, diverted into memory for every scenario.
59
61
  *
60
62
  * The FakeWorld intercepts COMMANDS, not `fs` writes, and step 2's bump is
61
- * seven direct writes. Every scenario before 2026-09-03 passed `--resume` with
63
+ * ten direct writes. Every scenario before 2026-09-03 passed `--resume` with
62
64
  * `package.json` already at the target, so step 2 short-circuited and nothing
63
65
  * was ever written — the drill looked side-effect-free because no scenario had
64
66
  * reached the writing arm. The first `resume: false` scenario appended twelve
@@ -83,6 +85,7 @@ const _MUTATED_BY_STEP_2: readonly string[] = [
83
85
  MARKETPLACE_JSON,
84
86
  AUGMENT_PLUGIN_JSON,
85
87
  AUGMENT_MARKETPLACE_JSON,
88
+ ...listingManifestPaths(ENV_REPO_ROOT),
86
89
  PROJECT_TEMPLATE,
87
90
  SETTINGS_SURFACE_FIXTURE,
88
91
  ];
@@ -24,6 +24,8 @@ import * as fs from 'node:fs';
24
24
  import * as path from 'node:path';
25
25
  import { fileURLToPath } from 'node:url';
26
26
 
27
+ import { listingManifestPaths, setListingManifestVersion } from './_lib/listing_manifests.js';
28
+
27
29
  const _HERE = fileURLToPath(import.meta.url);
28
30
 
29
31
  // ---------------------------------------------------------------------------
@@ -293,6 +295,20 @@ function set_augment_manifest_version(p: string, version: string): void {
293
295
  fs.writeFileSync(p, jsonDumpsIndent(data, 2) + '\n', 'utf-8');
294
296
  }
295
297
 
298
+ /**
299
+ * Every plugin and extension manifest the release keeps at the package
300
+ * version: the two `.augment-plugin/` twins and the two listing manifests that
301
+ * also pin the version inside their `npx` arguments. One entry point so
302
+ * `release.ts`, which sits past the source-size cap, spends one line on all four.
303
+ */
304
+ function set_plugin_manifest_versions(version: string, root: string = REPO_ROOT): void {
305
+ set_augment_manifest_version(path.join(root, '.augment-plugin', 'plugin.json'), version);
306
+ set_augment_manifest_version(path.join(root, '.augment-plugin', 'marketplace.json'), version);
307
+ for (const p of listingManifestPaths(root)) {
308
+ setListingManifestVersion(p, version, (v) => jsonDumpsIndent(v, 2));
309
+ }
310
+ }
311
+
296
312
  /** Update `metadata.version`; preserve 2-space indentation + UTF-8. */
297
313
  function set_marketplace_version(p: string, version: string): void {
298
314
  const data = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
@@ -374,6 +390,7 @@ export {
374
390
  commaGroup,
375
391
  jsonDumpsIndent,
376
392
  set_augment_manifest_version,
393
+ set_plugin_manifest_versions,
377
394
  set_marketplace_version,
378
395
  set_template_pin,
379
396
  pyLen,
@@ -117,6 +117,9 @@
117
117
  { "prefix": "LICENSE", "category": "consumer" },
118
118
  { "prefix": "NOTICE", "category": "consumer" },
119
119
  { "prefix": "llms.txt", "category": "consumer" },
120
+ { "prefix": "gemini-extension.json", "category": "consumer" },
121
+ { "prefix": "gemini-extension-context.md", "category": "consumer" },
122
+ { "prefix": "assets/", "category": "consumer" },
120
123
 
121
124
  { "prefix": "AGENTS.md", "category": "governance" },
122
125
  { "prefix": "CLAUDE.md", "category": "governance" },
@@ -11,7 +11,7 @@
11
11
  * recurses one level into nested objects; deeper trees fail render-time.
12
12
  */
13
13
 
14
- import { z } from 'zod';
14
+ import * as z from 'zod';
15
15
 
16
16
  const ruleLoadingTier = z.enum(['minimal', 'balanced', 'full', 'custom']);
17
17
  const disciplineProfile = z.enum(['auto', 'off', 'essential', 'full']);
@@ -42,7 +42,7 @@ export const settingsSchema = z.object({
42
42
  id: profileId.default('developer').describe(
43
43
  'Which experience you run — the audience identity that selects your default skill / command surface, README entry-path, and persona pre-selection (ADR-010, docs/contracts/profile-system.md). Six seed profiles: developer · content_creator · founder · agency · finance · ops. This is the first wizard question. In 6.0.0-A it records the choice only; pack-scoped surfacing (projection-time filtering, ADR-040) activates in 6.0.0-B behind a staged, opt-in rollout. Switch later with `agent-config use --profile=<id>`.',
44
44
  ),
45
- }).default({ id: 'developer' }),
45
+ }).prefault({ id: 'developer' }),
46
46
  projection: z.object({
47
47
  mode: projectionMode.default('legacy-all').describe(
48
48
  'Whether the per-tool projector writes EVERY artefact into the host-tool trees (.claude/ .cursor/ .windsurf/) or only the active profile + packs\' artefacts (ADR-040, docs/contracts/capability-packs.md). legacy-all = (default, non-breaking) project the full surface exactly as 5.x did. scoped = project only the active profile\'s packs unioned with the runtime.active_packs overlay, expanded over the requires graph — opt in with `agent-config use --profile=<id>`. A failed scoped projection restores the full tree.',
@@ -50,7 +50,7 @@ export const settingsSchema = z.object({
50
50
  rule_packs: z.union([z.literal('auto'), z.array(z.string())]).default([]).describe(
51
51
  'Scoping axis for the RULE layer beyond the derived workspace set, per pack ids (src/config/discovery/packs.yml). When set, a non-kernel rule also needs a packs frontmatter intersection to ship — e.g. deselecting frontend-design drops ui-audit-gate + design-fidelity. Opt-in, and an empty list is inactive; the workspace axis beside it is derived rather than configured (ADR-293 step 4.6). The literal "auto" derives the id list from the active-pack set (the same set the skill/command prune uses), so a domain safety floor stops shipping into installs that do not have the pack it guards; an explicit list stays supported and wins over the derivation.',
52
52
  ),
53
- }).default({ mode: 'legacy-all' }),
53
+ }).prefault({ mode: 'legacy-all' }),
54
54
  rule_loading_tier: ruleLoadingTier.default('balanced').describe(
55
55
  'Master switch for which rule tiers load and how cautiously the agent spends tokens. minimal = only the 9 kernel rules (cheapest, fewest guardrails). balanced = kernel + tier-1 (recommended default). full = kernel + tier-1 + tier-2 (most guardrails, highest token cost). custom = roll your own in agents/overrides/. LEGACY: superseded by discipline_profile — when that key is set it wins (mapping: minimal→off, balanced→essential, full→full).',
56
56
  ),
@@ -78,7 +78,7 @@ export const settingsSchema = z.object({
78
78
  strong: z.number().min(0).nullable().default(null).describe(
79
79
  'USD ceiling for the strong model tier. null = no separate tier cap. The never-block-to-save-money relation this used to describe went with the archived budget-routing layer (docs/contracts/budget-routing.md); nothing routes between tiers today.',
80
80
  ),
81
- }).default({ cheap: null, medium: null, strong: null }),
81
+ }).prefault({ cheap: null, medium: null, strong: null }),
82
82
  }),
83
83
  enforcement: enforcementMode.default('advisory').describe(
84
84
  'What happens when a budget hits 100%. advisory = show a banner, keep working (default — never blocks an active task). hard-stop = refuse further model calls until the budget resets or you raise the ceiling.',
@@ -88,7 +88,7 @@ export const settingsSchema = z.object({
88
88
  auto_switch: modelAutoSwitch.default('suggest').describe(
89
89
  'Per-skill model auto-switch (ADR-035). Skills declare a vendor-neutral model_tier (lite/medium/high); the generator maps it to a native Claude model (high→opus, medium→sonnet, lite→haiku). suggest (default) = never emit a native Claude model: key; the model-recommendation rule names the tier as a one-question suggestion on every surface — your explicit /model choice is never silently overridden. auto = render a native Claude model: into lite/medium/high-tier skills so Claude Code switches automatically for that turn (reverts next prompt), and suggest on surfaces without a native override. off = inert, no native key and no suggestion.',
90
90
  ),
91
- }).default({ auto_switch: 'suggest' }),
91
+ }).prefault({ auto_switch: 'suggest' }),
92
92
  personal: z.object({
93
93
  ide: z.string().default('').describe(
94
94
  'CLI binary your IDE registers (code, code-insiders, phpstorm, cursor, windsurf, idea, subl, …). Used by the file-editor skill to open edited files. Leave empty to disable IDE integration.',
@@ -133,7 +133,7 @@ export const settingsSchema = z.object({
133
133
  update_strategy: gitUpdateStrategy.default('merge').describe(
134
134
  'How a feature branch is brought up to date with its base. merge (default) = merge the base into the branch, as before this key existed. rebase = rebase the branch onto origin/<base> and publish with --force-with-lease, never a merge of the base into the branch. Rebase rewrites history, so the agent still asks for it unless the git-history-discipline rule already authorises the rewrite (the user asked this turn, an unrevoked standing instruction, or a pull --rebase the user started); this key decides which operation is proposed, never whether one is authorised.',
135
135
  ),
136
- }).default({}),
136
+ }).prefault({}),
137
137
  github: z.object({
138
138
  pr_reply_method: replyMethod.default('create_review_comment').describe(
139
139
  'How the agent replies to PR review comments. create_review_comment = post a new review comment (works on every GitHub plan). replies_endpoint = thread the reply under the original comment (needs the newer REST endpoint). auto = detect at runtime, prefer threaded replies when available.',
@@ -175,10 +175,10 @@ export const settingsSchema = z.object({
175
175
  max_mcp_output_tokens: z.number().int().positive().nullable().default(null).describe(
176
176
  'Token cap on a single MCP tool result the host places into a model request. null (default) = nothing written, host default 25000 applies. Same size-not-count caveat: it does not govern MCP connections, timeouts, or whether a tool runs.',
177
177
  ),
178
- }).default({}).describe(
178
+ }).prefault({}).describe(
179
179
  'Request-size caps this package may write into the host env block. Both null by default. No traffic or auto-updater variable is expressible here, by construction — see the block comment.',
180
180
  ),
181
- }).default({}).describe(
181
+ }).prefault({}).describe(
182
182
  'What this package may write into the host environment. ABSENT resolves to the all-null shape, which is the same thing as off — a settings file predating this section must not fail to parse, and "no section" and "both caps null" both mean write nothing.',
183
183
  ),
184
184
  augment: z.object({
@@ -235,7 +235,7 @@ export const settingsSchema = z.object({
235
235
  max_cost_per_rolling_7d_usd: z.number().min(0).nullable().default(null).describe(
236
236
  'Rolling 7-day USD ceiling for class-1 gate execution, summed from the append-only receipt ledger at agents/runtime/state/gate-budget-ledger.jsonl. A run whose estimate would cross it renders instead of running. null (default) = no rolling ceiling, per ADR-279. A per-run cap alone bounds one mistake, not a week of them, which is why option (a) of b-gate-budget-preauth carries two numbers — but each cap now bounds on its own, so setting one and leaving the other null is supported.',
237
237
  ),
238
- }).default({ max_cost_per_run_usd: null, max_cost_per_rolling_7d_usd: null }),
238
+ }).prefault({ max_cost_per_run_usd: null, max_cost_per_rolling_7d_usd: null }),
239
239
  }),
240
240
  planning: z.object({
241
241
  closure_pass: z.boolean().default(true).describe(
@@ -248,13 +248,13 @@ export const settingsSchema = z.object({
248
248
  'Gate R2 — completion review at 100% roadmap completion / pre-PR. true (default) = a findings-before-fixes review by a fresh reviewer context must exist for the current diff hash (or an exact honest-null / skip declaration) before fix commits and PR creation, enforced by check_completion_review at pre-push + CI (CI authoritative; a crashed validator warns and allows). false = escape hatch, the validator skips.',
249
249
  ),
250
250
  })
251
- // `.default({})` is load-bearing, not cosmetic: without it the SECTION is
251
+ // `.prefault({})` is load-bearing, not cosmetic: without it the SECTION is
252
252
  // required, so any settings payload that omits `planning` fails with
253
253
  // `ZodError: planning Required` — which contradicts this gate family's own
254
254
  // contract ("missing key = true" for all three) and reds every server
255
255
  // test whose fixture predates the section. With the default, an absent
256
256
  // section materialises as the three `true` leaves the contract promises.
257
- .default({}),
257
+ .prefault({}),
258
258
  execution: z.object({
259
259
  fix_loop_max: z.number().int().min(1).default(10).describe(
260
260
  'Consecutive failed fix attempts against one validation target before the escalation ladder reaches its terminal rung. Reaching the bound triggers a strategy change, never a question. Overridable globally, per project and per prompt.',
@@ -262,7 +262,7 @@ export const settingsSchema = z.object({
262
262
  escalation: z.array(z.string()).default(['independent', 'council', 'team', 'owner_owned_check']).describe(
263
263
  'The escalation rungs, in order, walked when the fix-loop bound is reached: independent (another session or a provider-diverse reviewer), council, team, then owner_owned_check — which asks whether the residue is owner-owned, not whether to ask the owner now.',
264
264
  ),
265
- }).default({}),
265
+ }).prefault({}),
266
266
  quality: z.object({
267
267
  local_auto_run: z.boolean().default(false).describe(
268
268
  'Run quality tools (linters, type-checks, formatters) and the local test suite autonomously after edits, IN CHAT. Off by default — in a chat turn a human is present and remote CI is the authoritative gate, so a local full-pipeline run duplicates it at wall-clock cost. The agent only runs a quality tool on an explicit ask, a concrete CI failure, or the new-gate carve-out. A mission is governed by local_auto_run_in_mission instead.',
@@ -289,10 +289,10 @@ export const settingsSchema = z.object({
289
289
  'Length distance below which a value may be reconciled, in absolute CSS pixels. Ships null, same reasoning as color. GitHub Primer\'s ±1px spacing widening is recorded as an externally observed candidate in src/scripts/_lib/design_tolerance.ts and is deliberately not the shipped value; max(1px, 2%) was rejected for growing more permissive at large dimensions with no evidence that this is wanted.',
290
290
  ),
291
291
  })
292
- .default({ color: null, length: null }),
292
+ .prefault({ color: null, length: null }),
293
293
  })
294
- .default({ enabled: false, tolerance: { color: null, length: null } }),
295
- }).default({
294
+ .prefault({ enabled: false, tolerance: { color: null, length: null } }),
295
+ }).prefault({
296
296
  fidelity_mode: 'strict',
297
297
  approximation: { enabled: false, tolerance: { color: null, length: null } },
298
298
  }),
@@ -300,7 +300,7 @@ export const settingsSchema = z.object({
300
300
  consumer_index_paths: z.array(z.string()).default([]).describe(
301
301
  "Extra project-relative paths where THIS project keeps a code-graph index another tool wrote. The built-in list is deliberately vendor-neutral (graph.json, code-graph.json, .code-graph/graph.json) and names no tool, so a tool that writes its index elsewhere is named here rather than waited for. A graph found this way LOADS and answers query and explain with every edge tagged read-from-elsewhere; the gate verbs (impact, untested, dead) refuse a graph made only of such edges and say so, because an empty answer from a gate must not read as 'nothing found' when it means 'nothing trusted'. Absolute paths and paths escaping the project root are ignored.",
302
302
  ),
303
- }).default({}),
303
+ }).prefault({}),
304
304
  screenshots: z.object({
305
305
  identity_allowlist: z.array(z.string()).default([]).describe(
306
306
  "Consumed by the doc-screenshot-hygiene rule and screenshot-hygiene skill. Public identity tokens SAFE to show unredacted in a documentation screenshot — the maintainer's own public handles plus well-known fake-data tokens. Not a general fake-data dictionary and not identity-resolution: everything not listed is treated as sensitive by default, and a public handle co-located with a real name does not whitelist the real name. Default [] = nothing auto-allowed.",
@@ -311,22 +311,22 @@ export const settingsSchema = z.object({
311
311
  data_bearing_gate: z.enum(['on', 'off']).default('on').describe(
312
312
  'Consumed by the doc-screenshot-hygiene rule. on (default) = a data-bearing screenshot embed is gated behind this-turn human confirmation; uncertain/unresolved regions redact-or-refuse, never ship-and-hope; illustrative/no-data screenshots may embed with a stated justification. off = no data-bearing gate (the anonymization taxonomy still applies).',
313
313
  ),
314
- }).default({ identity_allowlist: [], forbid_terminal_capture: true, data_bearing_gate: 'on' }),
314
+ }).prefault({ identity_allowlist: [], forbid_terminal_capture: true, data_bearing_gate: 'on' }),
315
315
  verbosity: z.object({
316
316
  offer_council_in_delivery: z.boolean().default(false).describe(
317
317
  'Offer "run AI Council on this?" inside delivery commands (/feature-plan, /review-changes, /roadmap-create). Council commands themselves are unaffected.',
318
318
  ),
319
- }).default({ offer_council_in_delivery: false }),
319
+ }).prefault({ offer_council_in_delivery: false }),
320
320
  code_style: z.object({
321
321
  docblocks: z.enum(['minimal', 'full']).default('minimal').describe(
322
322
  'Consumed by the code-comment-discipline rule. minimal (default) = no signature-mirroring docblocks; docblocks only for machine-relevant precision (generics, array shapes) or genuine why-context. full = the exported public surface of a library package may carry one-line summary docblocks; the redundancy ban still holds.',
323
323
  ),
324
- }).default({ docblocks: 'minimal' }),
324
+ }).prefault({ docblocks: 'minimal' }),
325
325
  reasoning: z.object({
326
326
  enabled: z.boolean().default(true).describe(
327
327
  'Master switch for the Reasoning Discipline Protocol (RDP). false = the whole layer is inert (zero overhead).',
328
328
  ),
329
- }).default({}),
329
+ }).prefault({}),
330
330
  subagents: z.object({
331
331
  quota_arbitrage: z.boolean().default(true).describe(
332
332
  'Prefer a separate quota-pool model for delegable sub-tasks where the host manifest reports one. Optional bonus only — identical behaviour (minus the quota win) where unsupported. Never load-bearing.',
@@ -335,7 +335,7 @@ export const settingsSchema = z.object({
335
335
  lite: z.string().default('').describe('Model alias for lite-tier sub-tasks. Empty = the tier runtime default.'),
336
336
  medium: z.string().default('').describe('Model alias for medium-tier sub-tasks. Empty = the tier runtime default.'),
337
337
  high: z.string().default('').describe('Model alias for high-tier sub-tasks. Empty = the tier runtime default.'),
338
- }).default({}).describe(
338
+ }).prefault({}).describe(
339
339
  'Per-tier model map for downshift routing. Each empty value uses the tier runtime default (no vendor model baked in).',
340
340
  ),
341
341
  implementer_model: z.string().default('').describe(
@@ -362,7 +362,7 @@ export const settingsSchema = z.object({
362
362
  cleanup: z.enum(['off', 'notify', 'auto']).default('notify').describe(
363
363
  'What the agent may do about a worktree whose work is provably on the trunk (merged by merge commit, rebase or squash), that is clean, holds no ignored file a rebuild would not restore, and has no live session. notify (default, and what an absent key resolves to) says once a day how many exist and removes nothing; auto removes them right after /pr:merge and once a day on the first session start, without force, keeping local branches; off says nothing. The daily pass reads merged pull requests through your gh login (branch name and head commit only).',
364
364
  ),
365
- }).default({ cleanup: 'notify' }),
365
+ }).prefault({ cleanup: 'notify' }),
366
366
  ai_team: z.object({
367
367
  model: z.string().default('auto').describe(
368
368
  "Model handed to the codex CLI. 'auto' (default) = pass no --model flag so the CLI's own default applies — tracks the subscription's current strongest model instead of pinning a stale ID. Any other value passes through verbatim as `--model <value>`.",
@@ -380,8 +380,8 @@ export const settingsSchema = z.object({
380
380
  max_consecutive_blocks: z.number().int().min(1).default(3).describe(
381
381
  'Circuit-breaker bound: after this many CONSECUTIVE BLOCK verdicts in one session, a visible notice is injected exactly once and the managed layer stops re-blocking — the user decides, never an infinite Claude↔Codex loop. An ALLOW verdict resets the counter. Positive integer.',
382
382
  ),
383
- }).default({ managed: false, max_consecutive_blocks: 3 }),
384
- }).default({ model: 'auto', allow_delegate: false, max_calls_per_day: 50, review_gate: { managed: false, max_consecutive_blocks: 3 } }),
383
+ }).prefault({ managed: false, max_consecutive_blocks: 3 }),
384
+ }).prefault({ model: 'auto', allow_delegate: false, max_calls_per_day: 50, review_gate: { managed: false, max_consecutive_blocks: 3 } }),
385
385
  emergency: z.object({
386
386
  orchestration_halt: z.boolean().default(false).describe(
387
387
  'The one audited incident switch over the always-on orchestration stack (subagents, council, team). NOT an activation gate: false (default) = the stack runs normally. true = halted; arming requires no ceremony. Disarming (returning to false) requires orchestration_halt_justification to be a non-empty string. Both transitions emit one telemetry line.',
@@ -389,7 +389,7 @@ export const settingsSchema = z.object({
389
389
  orchestration_halt_justification: z.string().default('').describe(
390
390
  'Required non-empty before orchestration_halt may return to false. Ignored while arming the halt.',
391
391
  ),
392
- }).default({ orchestration_halt: false, orchestration_halt_justification: '' }),
392
+ }).prefault({ orchestration_halt: false, orchestration_halt_justification: '' }),
393
393
  onboarding: z.object({
394
394
  onboarded: z.boolean().default(false).describe(
395
395
  'Set to true once the developer has completed `agent-config setup`. The onboarding-gate rule blocks the first turn of every chat until this is true. Toggle back to false to re-trigger the wizard.',
@@ -425,7 +425,7 @@ export const settingsSchema = z.object({
425
425
  enabled: z.boolean().default(true).describe(
426
426
  'Automatic retro after merged work (ADR-292, docs/contracts/retro-v1.md). true (default, owner directive 2026-10-12) = a merged PR and the first session of a new local day enqueue finished work; a deterministic triage picks what is worth one learning, and nothing behavior-changing is applied without a person. The one network read is the daily probe asking GitHub, through your own gh login, whether a pushed PR merged (repository and PR number only, three-second budget). false = no automatic path runs — no merge enqueue, no forge-merge marker, no daily probe and no network read; an explicit /retro still runs on request. Class C: an agent cannot set it; it cascades from the user-global file.',
427
427
  ),
428
- }).default({}),
428
+ }).prefault({}),
429
429
  memory: z.object({
430
430
  redact_patterns: z.array(z.string()).default([]).describe(
431
431
  'Regex patterns (one per line) that scrub matches from chat-history transcripts and memory before they hit disk. Use for secrets, customer names, internal URLs. Patterns are anchored and case-insensitive.',
@@ -445,15 +445,15 @@ export const settingsSchema = z.object({
445
445
  allowed_tiers: z.array(z.string()).default(['public']).describe(
446
446
  'Origin tiers auto-eligible to cross a project boundary. proprietary is manual-only regardless (the gate hard-codes it), so an in-house schema never auto-shares.',
447
447
  ),
448
- }).default({}),
449
- }).default({}),
448
+ }).prefault({}),
449
+ }).prefault({}),
450
450
  egress: z.object({
451
451
  placeholders: z.object({
452
452
  mode: z.enum(['off', 'observe', 'protect', 'strict']).default('off').describe(
453
453
  'Dispatcher egress stage (docs/contracts/egress-placeholders-v1.md). off (default) = nothing. observe = detect and count listed values in prompts and tool bodies, types and counts only, and change nothing a hook concern sees. protect = also give every recording and advisory concern a copy with each value replaced by a placeholder token, so local stores keep tokens; guards still see the original. strict = as protect until the host rewrite exists. No mode yet changes what reaches the model; the rule table holds credential rows and the validated identifier rows. Project layers only.',
454
454
  ),
455
- }).default({}),
456
- }).default({}),
455
+ }).prefault({}),
456
+ }).prefault({}),
457
457
  hooks: z.object({
458
458
  concern_budget: z.object({
459
459
  tier1_concerns: z.array(z.string()).default([]).describe(
@@ -464,47 +464,47 @@ export const settingsSchema = z.object({
464
464
  touched_file_quality: z.enum(['off', 'shadow', 'warn']).default('off').describe(
465
465
  "Stop-slot pass that runs the project's OWN quality commands — the ones resolve_toolchain() already reports — over the files this turn edited. Default off. `shadow` records a per-run result and emits nothing; `warn` adds ONE advisory line of at most 200 bytes. Never blocks and never counts as verification: a command recorded here never reaches _lib/verification_command.ts, so a clean eslint run cannot let a turn claim it verified anything. A writing command runs only in its check form (pint --test) or not at all, and a command with no per-file form is skipped rather than run project-wide at every stop.",
466
466
  ),
467
- }).default({}),
467
+ }).prefault({}),
468
468
  injection_scan: z.object({
469
469
  enabled: z.boolean().default(false).describe(
470
470
  'PostToolUse prompt-injection scanner (road-to-security-pillar.md P3.2). Default off. When on, scans tool output (file reads, web fetches, MCP responses) for injection signatures and WARNS in context (never blocks). Runtime backstop on top of the always-on untrusted-input-defense rule; detection is probabilistic.',
471
471
  ),
472
- }).default({}),
472
+ }).prefault({}),
473
473
  runtime_journal: z.object({
474
474
  enabled: z.boolean().default(false).describe(
475
475
  'Runtime event journal (road-to-runtime-event-journal Phase 1). Default off. When on, one durable, episode-keyed record per dispatched hook event is appended to <git-common-dir>/agent-journal/journal.sqlite — a Class-A write that opens, writes and exits. It emits nothing to the model, never warns and never blocks: a locked database, a missing node:sqlite, a malformed envelope or a full disk all degrade to silence. Off rather than on because it CREATES a storage surface (a SQLite database under your .git/) rather than appending to a directory that already exists. The record type has no field able to hold free-form content — ids, an event name, a bounded capability identifier, repo-relative locators and two 12-hex digests; no prompt, no file body, no absolute path. Retention: a 30-day TTL anchored on episode close, plus explicit, time-bounded, human-only holds that themselves expire.',
476
476
  ),
477
- }).default({}),
477
+ }).prefault({}),
478
478
  rtk_wrap: z.object({
479
479
  enabled: z.boolean().default(false).describe(
480
480
  'PreToolUse RTK-wrap nudge (token-saving Phase 3). Default off. When on AND the binary on PATH is verified as Rust Token Killer (a live two-stage identity probe — not a self-reported flag, and never a colliding same-name binary), warns (never blocks) "re-run wrapped with rtk" before a single verbose CLI command (git/npm/cargo/docker/…) — upstream reports 60–90% output-token savings (their estimate). Skips completeness-critical / piped / compound commands and git diff. No-op when rtk is absent, unverified, or a different tool. Its own activation charge is one advisory line under the 1,024 B default concern cap in src/config/hook-token-budget.json; the per-fire distribution is UNMEASURED — stated rather than estimated, because the saving above is a third-party output-token claim and not a standing-payload reduction this package measured.',
481
481
  ),
482
- }).default({}),
482
+ }).prefault({}),
483
483
  design_slop: z.object({
484
484
  enabled: z.boolean().default(false).describe(
485
485
  'PreToolUse anti-slop nudge (road-to-anti-slop-detector Phase 3). Default off. When on, runs the lint_design_slop registry against about-to-be-written UI content and WARNS (never blocks) on P0/P1 aesthetic tells (side-stripe, gradient-text, magic z-index, …). Flags are rebuttable via DESIGN.md / design-slop-disable. Anti-loop: a file::rule signature surfaced 3x goes silent. Host-limited convenience layer; the universal gate is the lint_design_slop linter/CI.',
486
486
  ),
487
- }).default({}),
487
+ }).prefault({}),
488
488
  design_pass: z.object({
489
489
  enabled: z.boolean().default(false).describe(
490
490
  'PostToolUse + stop design pass (road-to-frontend-power E1.1/E1.2/E1.3). Default off. One concern on two slots: on post_tool_use a write to a UI surface delivers the design findings as context and never blocks; on stop the same pass runs over every UI file touched this session, deduped against what the post pass surfaced, and a P0 objective floor (contrast, font size, heading skip, focus) blocks with a continuation. P1-P3 never block. post_tool_use rather than pre_tool_use for two measured reasons: pre_tool_use is declared by three hosts and honoured by one while post_tool_use is declared by six, and _lib/ui_surface.ts is a path predicate, so a pre-write gate cannot fire on the first write of a new surface. A pass that could not fully run reports verification: degraded with a reason rather than passing silently. Does not replace design_slop: two design keys is the honest state until the tiering experiment has a number.',
491
491
  ),
492
- }).default({}),
492
+ }).prefault({}),
493
493
  ui_route_nudge: z.object({
494
494
  enabled: z.boolean().default(false).describe(
495
495
  'PreToolUse UI-route nudge (road-to-frontend-skill-application Phase 4). Default off. When on, a Write/Edit to a UI surface with no design consultation latched this session WARNS (never blocks) naming the route — run existing-ui-audit, then the fe-design loop. A read or search touching fe-design / existing-ui-audit / design-review / design-intelligence latches consultation and silences it for the session. Anti-loop: at most 2 nudges per session. It does not read the rules: the UI-surface decision comes from _lib/ui_surface.ts and no code parses rule frontmatter, so this runs parallel to the two UI rules rather than consuming their triggers, and a test keeps the sets from drifting. It is a nudge, so their enforced_by: none stays accurate.',
496
496
  ),
497
- }).default({}),
497
+ }).prefault({}),
498
498
  code_graph: z.object({
499
499
  enabled: z.boolean().default(false).describe(
500
500
  'PreToolUse code-graph nudge (ADR-124 Phase 4). Default off. When on AND a native code-graph cache or a consumer-shipped graph.json/SCIP index is present, warns once per session (never blocks) as the agent is about to Grep/Glob or Read a source file — query the graph first for who-calls/where-used/impact questions (or rebuild if stale, build if absent). Source G’s strict block-first-read mode is deliberately un-ported.',
501
501
  ),
502
- }).default({}),
502
+ }).prefault({}),
503
503
  suggestion_capture: z.object({
504
504
  enabled: z.boolean().default(false).describe(
505
505
  'Suggestion-block capture (road-to-suggestion-block-capture Phase 2). Default off. Two slots: `stop` reads `last_assistant_message` — a payload field, so no transcript file is read — and latches that the assistant turn carried a numbered-options block; `user_prompt_submit` CONSUMES that latch and classifies the answering turn as option_n / as_is / other / stale_block. Consume-once is the correctness guard: the latch is deleted on read, so a bare "1" three turns later meets no latch and classifies `other`, and a latch past its TTL or unparseable is `stale_block` rather than a guess. Writes COUNTS ONLY to agents/runtime/state/audit/suggestion-capture.jsonl — the record type has no field able to hold a prompt, an option label or a command name, and a test asserts the written key set against src/config/suggestion-capture.json. Off by default because this is an INSTRUMENT rather than a feature: it exists so a capture rate can be measured on a maintainer workspace over a fixed soak window, and there is nothing in it for a consumer to gain.',
506
506
  ),
507
- }).default({}),
507
+ }).prefault({}),
508
508
  // `turn_end_gate` is deliberately ABSENT. The stop-slot turn-end gate is
509
509
  // always armed (2026-08-12) and has no settings surface: whether it
510
510
  // fires is decided by each detector's own trigger conditions, not by a
@@ -544,12 +544,12 @@ export const settingsSchema = z.object({
544
544
  kev: z.enum(['off', 'on']).default('on').describe(
545
545
  'Whether a refresh reads the CISA known-exploited catalog. on (default) downloads the whole catalog and matches it locally, so nothing about the project is sent, and a finding in it is raised one tier in a runtime scope; off leaves tiers to the other sources. Runs only where egress is query.',
546
546
  ),
547
- }).default({ mode: 'notify', egress: 'query', kev: 'on' }),
547
+ }).prefault({ mode: 'notify', egress: 'query', kev: 'on' }),
548
548
  update: z.object({
549
549
  mode: z.enum(['off', 'notify', 'ask', 'auto']).default('notify').describe(
550
550
  'What the agent may do about a newer release. notify tells you and installs nothing (the default, and what an absent key resolves to); ask behaves as notify until the ring-0 exit ships a prompt; auto installs a staged release at a session start when every gate passes and no other session is live; off says nothing at all. Read from the user-global layer only — a repository cannot set how your global install updates itself.',
551
551
  ),
552
- }).default({ mode: 'notify' }),
552
+ }).prefault({ mode: 'notify' }),
553
553
  legal_review_prep: z.object({
554
554
  acknowledged: z.boolean().default(false).describe(
555
555
  'I understand the legal-review-prep pack provides templates and general information ONLY — it is NOT legal advice, creates no attorney-client relationship, and never replaces a licensed lawyer. Individual cases require an attorney. The pack stays inactive until this is checked.',
@@ -557,7 +557,7 @@ export const settingsSchema = z.object({
557
557
  require_council: z.boolean().default(true).describe(
558
558
  'Gate legal work-product behind a multi-model AI-council / deep-research pass (defense-in-depth: documented multi-stage review + audit trail; fail-closed when no council is configured). Leave on for the safest posture. Turning it off lets single-model legal output through — not recommended for a high-risk pack.',
559
559
  ),
560
- }).default({ acknowledged: false, require_council: true }),
560
+ }).prefault({ acknowledged: false, require_council: true }),
561
561
  });
562
562
 
563
563
  export type Settings = z.infer<typeof settingsSchema>;
@@ -82,7 +82,7 @@ function unwrapRef(root: JsonSchemaNode): JsonSchemaNode {
82
82
  }
83
83
 
84
84
  /**
85
- * Flatten a `zodToJsonSchema` output into dotted leaf entries. Objects with
85
+ * Flatten a `toJsonSchema7` output into dotted leaf entries. Objects with
86
86
  * `properties` recurse; everything else (scalars, enums, arrays, free-form
87
87
  * maps) is a leaf. Deterministic key order (sorted) so snapshots diff
88
88
  * cleanly in git and across runs.
@@ -11,7 +11,7 @@
11
11
  * the same Zod schema. ESLint forbids Node-only imports in this tree.
12
12
  */
13
13
 
14
- import { z } from 'zod';
14
+ import * as z from 'zod';
15
15
 
16
16
  /** Hard cap on the optional `notes` block. Keeps the file loadable into every reply. */
17
17
  export const USER_IDENTITY_NOTES_MAX_CHARS = 8_000;