@event4u/agent-config 17.2.1 → 17.4.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 (225) 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 +167 -193
  6. package/CREDITS.md +2 -2
  7. package/README.md +1 -1
  8. package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  11. package/dist/cli/commands/recordTriggerEval.js +1 -1
  12. package/dist/cli/registry.js +2 -2
  13. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
  14. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  15. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  16. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  17. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
  18. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
  19. package/dist/cli-delegate/cmd_conformance.js +190 -192
  20. package/dist/cli-delegate/cmd_doctor.js +5 -5
  21. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  22. package/dist/cli-delegate/cmd_explain.js +1 -1
  23. package/dist/cli-delegate/cmd_export.js +2 -2
  24. package/dist/cli-delegate/cmd_handoff.js +2 -2
  25. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  26. package/dist/cli-delegate/cmd_prune.js +2 -2
  27. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  28. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  29. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  30. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  31. package/dist/cli-delegate/cmd_sync.js +2 -2
  32. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  33. package/dist/cli-delegate/cmd_update.js +24 -13
  34. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  35. package/dist/cli-delegate/cmd_validate.js +2 -2
  36. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  37. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  38. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  39. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  40. package/dist/discovery/deprecation-report.md +1 -1
  41. package/dist/discovery/discovery-manifest.json +3 -3
  42. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  43. package/dist/discovery/discovery-manifest.summary.md +1 -1
  44. package/dist/discovery/dormancy-report.md +1 -1
  45. package/dist/discovery/orphan-report.md +1 -1
  46. package/dist/discovery/packs.json +2 -2
  47. package/dist/discovery/trust-report.md +1 -1
  48. package/dist/discovery/workspaces.json +2 -2
  49. package/dist/hooks/code_graph_refresh.chunk.cjs +443 -0
  50. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  51. package/dist/hooks/dispatch.js +1001 -938
  52. package/dist/hooks/dispatch.sha256 +1 -1
  53. package/dist/hooks/egress_engine.chunk.cjs +57 -7
  54. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  55. package/dist/hooks/update_notice.chunk.cjs +914 -0
  56. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  57. package/dist/install/install.mjs +6974 -6037
  58. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  59. package/dist/mcp/registry-manifest.json +3 -2
  60. package/dist/mcp/server.json +13 -4
  61. package/dist/mcp/server.mjs +71202 -63282
  62. package/dist/scripts/_lib/egress_rules.js +8 -2
  63. package/dist/scripts/condense.js +18 -15
  64. package/dist/server/routes/discovery.js +1 -1
  65. package/dist/server/routes/install.js +1 -1
  66. package/dist/server/routes/ping.js +1 -1
  67. package/dist/server/routes/schema.js +4 -4
  68. package/dist/server/routes/settings.js +2 -6
  69. package/dist/server/routes/wizard.js +6 -6
  70. package/dist/server/schemas/settings.js +42 -42
  71. package/dist/shared/settingsSurface.js +1 -1
  72. package/dist/shared/userMd/schema.js +1 -1
  73. package/dist/shared/zodJsonSchema.js +47 -0
  74. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  75. package/dist/ui/index.html +1 -1
  76. package/package.json +6 -3
  77. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  78. package/src/config/ci-local-parity.yml +11 -0
  79. package/src/config/continuity-surface.json +9 -2
  80. package/src/config/evaluator-budgets.json +9 -8
  81. package/src/config/gate-violation-baselines.json +3 -3
  82. package/src/config/gitignore-block.txt +6 -5
  83. package/src/config/hook-bundle-budget.json +46 -6
  84. package/src/config/pack-size-budget.json +12 -1
  85. package/src/config/packed-binary-manifest.json +18 -0
  86. package/src/config/sigstore/npm_keys.json +50 -0
  87. package/src/config/sigstore/trust-material.json +9 -0
  88. package/src/config/sigstore/trusted_root.json +126 -0
  89. package/src/config/skill-body-budget.json +7 -1
  90. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  91. package/src/scripts/_cli/cmd_update.ts +48 -20
  92. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  93. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  94. package/src/scripts/_dispatch.bash +4 -3
  95. package/src/scripts/_lib/confusables.ts +0 -19
  96. package/src/scripts/_lib/egress_normalize.ts +53 -23
  97. package/src/scripts/_lib/egress_rules.ts +8 -2
  98. package/src/scripts/_lib/egress_text.ts +5 -3
  99. package/src/scripts/_lib/egress_token.ts +7 -5
  100. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  101. package/src/scripts/_lib/git_common_dir.ts +10 -0
  102. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  103. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  104. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  105. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  106. package/src/scripts/_lib/lineage_map.ts +55 -0
  107. package/src/scripts/_lib/lineage_query.ts +140 -0
  108. package/src/scripts/_lib/lineage_seed.ts +116 -0
  109. package/src/scripts/_lib/lineage_watch.ts +249 -0
  110. package/src/scripts/_lib/link_crypto.ts +8 -4
  111. package/src/scripts/_lib/listing_manifests.ts +85 -0
  112. package/src/scripts/_lib/neighbour_census.ts +1 -0
  113. package/src/scripts/_lib/production_reach.ts +3 -3
  114. package/src/scripts/_lib/project_registry.ts +227 -0
  115. package/src/scripts/_lib/provenance_verify.ts +14 -242
  116. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  117. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  118. package/src/scripts/_lib/source_id.ts +48 -0
  119. package/src/scripts/_lib/source_shape.ts +4 -0
  120. package/src/scripts/_lib/unattended_gates.ts +6 -1
  121. package/src/scripts/adoption_status.ts +231 -27
  122. package/src/scripts/advisories.ts +26 -10
  123. package/src/scripts/ai_council/council_egress.ts +5 -6
  124. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  125. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  126. package/src/scripts/check_dependency_floors.ts +14 -0
  127. package/src/scripts/check_egress_mutations.ts +1 -0
  128. package/src/scripts/check_release_manifest_content.ts +17 -2
  129. package/src/scripts/check_release_pr_shape.ts +5 -0
  130. package/src/scripts/code_graph/build.ts +145 -45
  131. package/src/scripts/code_graph/cli.ts +382 -35
  132. package/src/scripts/code_graph/conventions_default.ts +247 -0
  133. package/src/scripts/code_graph/detect.ts +5 -5
  134. package/src/scripts/code_graph/estate_cli.ts +135 -0
  135. package/src/scripts/code_graph/estate_index.ts +394 -0
  136. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  137. package/src/scripts/code_graph/estate_map.ts +141 -0
  138. package/src/scripts/code_graph/extract.ts +8 -2
  139. package/src/scripts/code_graph/extract_more.ts +241 -2
  140. package/src/scripts/code_graph/graph_budget.ts +158 -0
  141. package/src/scripts/code_graph/graph_home.ts +80 -0
  142. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  143. package/src/scripts/code_graph/loader.ts +10 -2
  144. package/src/scripts/code_graph/map.ts +195 -53
  145. package/src/scripts/code_graph/map_model.ts +113 -20
  146. package/src/scripts/code_graph/map_verify.ts +80 -5
  147. package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
  148. package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
  149. package/src/scripts/code_graph/passes.ts +173 -0
  150. package/src/scripts/code_graph/project_ref.ts +25 -0
  151. package/src/scripts/code_graph/projects_cli.ts +254 -0
  152. package/src/scripts/code_graph/providers/angular.ts +55 -0
  153. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  154. package/src/scripts/code_graph/providers/catalog.ts +299 -0
  155. package/src/scripts/code_graph/providers/django.ts +206 -0
  156. package/src/scripts/code_graph/providers/express.ts +25 -0
  157. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  158. package/src/scripts/code_graph/providers/flask.ts +99 -0
  159. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  160. package/src/scripts/code_graph/providers/index.ts +103 -0
  161. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  162. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  163. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  164. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  165. package/src/scripts/code_graph/providers/rails.ts +204 -0
  166. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  167. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  168. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  169. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  170. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  171. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  172. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  173. package/src/scripts/code_graph/providers/spring.ts +175 -0
  174. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  175. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  176. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  177. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  178. package/src/scripts/code_graph/providers/types.ts +56 -0
  179. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  180. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  181. package/src/scripts/code_graph/query.ts +9 -9
  182. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  183. package/src/scripts/code_graph/refresh_lock.ts +197 -0
  184. package/src/scripts/code_graph/repo_identity.ts +169 -0
  185. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  186. package/src/scripts/code_graph/scip_reader.ts +2 -2
  187. package/src/scripts/code_graph/sqlite_store.ts +31 -28
  188. package/src/scripts/code_graph/types.ts +79 -41
  189. package/src/scripts/code_graph/validate.ts +6 -8
  190. package/src/scripts/condense.ts +15 -15
  191. package/src/scripts/generate_settings_reference.ts +2 -6
  192. package/src/scripts/hook_chunks.mjs +11 -0
  193. package/src/scripts/hook_manifest.json +1 -1
  194. package/src/scripts/hook_manifest.yaml +43 -23
  195. package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
  196. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  197. package/src/scripts/hooks/host_semantics.ts +72 -0
  198. package/src/scripts/hooks/lazy_concern.ts +8 -0
  199. package/src/scripts/hooks/user_notice.ts +105 -0
  200. package/src/scripts/install-hooks.sh +54 -33
  201. package/src/scripts/install.ts +2 -6
  202. package/src/scripts/lint_harvest_provenance.ts +137 -11
  203. package/src/scripts/lint_hook_manifest.ts +2 -0
  204. package/src/scripts/lint_marketplace.ts +33 -0
  205. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  206. package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
  207. package/src/scripts/mcp_server/graph_tools.ts +165 -41
  208. package/src/scripts/network_sources.ts +24 -6
  209. package/src/scripts/release.ts +2 -5
  210. package/src/scripts/release_drill.ts +4 -1
  211. package/src/scripts/release_env.ts +17 -0
  212. package/src/scripts/release_mix_taxonomy.json +3 -0
  213. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  214. package/src/scripts/update_notice_hook.ts +75 -7
  215. package/src/server/schemas/settings.ts +42 -42
  216. package/src/shared/settingsSurface.ts +1 -1
  217. package/src/shared/userMd/schema.ts +1 -1
  218. package/src/shared/zodJsonSchema.ts +41 -0
  219. package/src/vendor/grammars/README.md +8 -6
  220. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  221. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  222. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  223. package/dist/ui/assets/index-w70Ado09.js +0 -23
  224. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  225. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -53,12 +53,18 @@ export const ASSIGNMENT_KEYWORDS = {
53
53
  token: 'token',
54
54
  bearer: 'bearer',
55
55
  };
56
- /** Source of a keyword-assignment shape: keyword group, operator, then the consumer's value. */
56
+ /**
57
+ * Source of a keyword-assignment shape: keyword group, operator, then the consumer's value.
58
+ * Not an assignment: `===` (strict equality), `==` followed by whitespace (a spaced
59
+ * comparison) and `=>` (an arrow) — read as one, `token === 'x'` yielded the value `==` and
60
+ * the council bundle refused every later `==`. A shell `name==value` stays an assignment,
61
+ * and its second `=` belongs to the operator, so the value read is `value`, never a lone `=`.
62
+ */
57
63
  export function assignmentSource(shape) {
58
64
  const alt = shape.keywords.map((k) => ASSIGNMENT_KEYWORDS[k]).join('|');
59
65
  const group = shape.capture === true ? `(${alt})` : `(?:${alt})`;
60
66
  const gap = shape.gap ?? '\\s*';
61
- const head = `${shape.before ?? ''}${group}${shape.after ?? ''}${gap}[:=]`;
67
+ const head = `${shape.before ?? ''}${group}${shape.after ?? ''}${gap}(?::|=(?!==|=\\s|>)=?)`;
62
68
  return shape.value === undefined ? head : `${head}${gap}${shape.value}`;
63
69
  }
64
70
  export function assignmentPattern(shape, flags) {
@@ -1930,19 +1930,19 @@ export const PLUGIN_POINTER_SLUG = 'install-agent-config';
1930
1930
  export const PLUGIN_POINTER_SKILL_BODY = `---
1931
1931
  name: install-agent-config
1932
1932
  description: >-
1933
- Install or upgrade the event4u agent-config suite. This marketplace plugin
1934
- is a bootstrap shim — it ships hooks plus this pointer only; ALL skills,
1935
- rules, and commands are installed via \`npx -y @event4u/agent-config init\`.
1936
- Use when the user asks to install, set up, or upgrade agent-config, or
1933
+ Governance rules, skills and commands for AI coding agents in Claude Code, Cursor, GitHub Copilot and Gemini CLI —
1934
+ install or upgrade the full event4u agent-config suite. No plugin that ships this pointer is the full suite (the Claude
1935
+ Code one adds hooks, the Copilot one a read-only MCP server); ALL content installs via \`npx -y @event4u/agent-config init\`.
1936
+ Use when the user asks to install, set up, or upgrade agent-config, wants coding-agent rules or skills, or
1937
1937
  wonders why this plugin contains no other skills.
1938
1938
  ---
1939
1939
 
1940
1940
  # Install agent-config
1941
1941
 
1942
- This plugin is a **bootstrap shim** kept so the marketplace listing stays
1943
- discoverable. Its hooks run only alongside a global npm install of the package (never a project-local one) —
1944
- on a plugin-only install no hook runs, so the \`fail_closed\` guards (\`block-no-verify\`, \`block-kernel-rule-writes\`,
1945
- \`block-plumbing-writes\`) are **off**; the session start says so once. Content ships as a file projection.
1942
+ The plugin that delivered this skill is a **discovery entry**, not the suite. The Claude Code plugin adds hooks that
1943
+ run only alongside a global npm install of the package (never a project-local one) — on a plugin-only install no hook
1944
+ runs, so the \`fail_closed\` guards (\`block-no-verify\`, \`block-kernel-rule-writes\`, \`block-plumbing-writes\`) are
1945
+ **off**; the session start says so once. The Copilot plugin adds a read-only MCP server and no hooks.
1946
1946
 
1947
1947
  ## Install (canonical, one command)
1948
1948
 
@@ -1952,7 +1952,7 @@ npx -y @event4u/agent-config init
1952
1952
 
1953
1953
  The installer writes the full content projection, registers the dispatcher
1954
1954
  hooks as a managed block in \`~/.claude/settings.json\` (byte-identical to
1955
- this plugin's hooks, so nothing double-fires), and keeps everything
1955
+ the Claude Code plugin's hooks, so nothing double-fires), and keeps everything
1956
1956
  upgradeable via \`agent-config upgrade\`.
1957
1957
 
1958
1958
  ## Already installed both?
@@ -1969,11 +1969,14 @@ export function generate_plugin_command_skills() {
1969
1969
  const pointer_dir = path.join(MODULE_STATE.PLUGIN_SKILLS_DIR, PLUGIN_POINTER_SLUG);
1970
1970
  _mkdirp(pointer_dir);
1971
1971
  const pointer_file = path.join(pointer_dir, 'SKILL.md');
1972
- if (_isSymlink(pointer_file)) {
1973
- fs.unlinkSync(pointer_file);
1974
- }
1975
- if (!_isFile(pointer_file) || fs.readFileSync(pointer_file, 'utf-8') !== PLUGIN_POINTER_SKILL_BODY) {
1976
- fs.writeFileSync(pointer_file, PLUGIN_POINTER_SKILL_BODY);
1972
+ for (const file of [pointer_file, path.join(MODULE_STATE.PROJECT_ROOT, '.github', 'plugin', 'skills', PLUGIN_POINTER_SLUG, 'SKILL.md')]) {
1973
+ if (_isSymlink(file)) {
1974
+ fs.unlinkSync(file);
1975
+ }
1976
+ if (!_isFile(file) || fs.readFileSync(file, 'utf-8') !== PLUGIN_POINTER_SKILL_BODY) {
1977
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1978
+ fs.writeFileSync(file, PLUGIN_POINTER_SKILL_BODY);
1979
+ }
1977
1980
  }
1978
1981
  // 2. Prune every other skill dir so the tree converges to shim shape
1979
1982
  // from any prior state. Removable = every entry is a symlink (the
@@ -2001,7 +2004,7 @@ export function generate_plugin_command_skills() {
2001
2004
  removed_dirs += 1;
2002
2005
  }
2003
2006
  }
2004
- let msg = ' ✅ Plugin skills: bootstrap shim (1 pointer skill) in .claude-plugin/skills/';
2007
+ let msg = ' ✅ Plugin skills: bootstrap shim (1 pointer skill) in .claude-plugin/skills/ and .github/plugin/skills/';
2005
2008
  if (removed_dirs) {
2006
2009
  msg += ` (${removed_dirs} content entries pruned)`;
2007
2010
  }
@@ -12,7 +12,7 @@
12
12
  * (PR working tree, dev install) rather than a runtime bug. See ADR-013
13
13
  * and the R3 roadmap for the broader contract.
14
14
  */
15
- import { z } from 'zod';
15
+ import * as z from 'zod';
16
16
  import { loadManifest, ManifestNotFoundError, ManifestParseError, } from '../../cli/discovery/loadManifest.js';
17
17
  export const DiscoverySliceSchema = z.enum(['workspaces', 'packs', 'artefacts', 'unassigned']);
18
18
  function pickSlice(manifest, slice) {
@@ -12,7 +12,7 @@
12
12
  * road-to-single-install-source-of-truth § Phase 3 — this module now only
13
13
  * serves the read-only detect / plan-preview / recovery / legacy-v3 routes.
14
14
  */
15
- import { z } from 'zod';
15
+ import * as z from 'zod';
16
16
  import { computeConflicts } from '../../install/conflict.js';
17
17
  import { cpSync, existsSync, mkdirSync } from 'node:fs';
18
18
  import { detectLegacyV3, detectPackageRoot, detectProjectShape, detectScope, detectToolPresence, } from '../../install/detect.js';
@@ -6,7 +6,7 @@
6
6
  * are connected to the right process. The zod schema is exported so
7
7
  * tests assert against the same source of truth as the handler.
8
8
  */
9
- import { z } from 'zod';
9
+ import * as z from 'zod';
10
10
  import { readFileSync } from 'node:fs';
11
11
  import { PACKAGE_JSON } from '../../cli/paths.js';
12
12
  import { CAPABILITIES } from '../../shared/capabilities.js';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The SPA bundle does not ship Zod (council security-engineer mandate:
5
5
  * client and server cannot drift). Instead it fetches JSON-Schema once at
6
- * mount and feeds it to the form renderer. `zod-to-json-schema` does the
6
+ * mount and feeds it to the form renderer. `toJsonSchema7` does the
7
7
  * conversion in-process; the response is cached for the lifetime of the
8
8
  * server process because the schemas are compile-time constants.
9
9
  *
@@ -14,7 +14,7 @@
14
14
  * generatedAt: ISO-8601 string,
15
15
  * }
16
16
  */
17
- import { zodToJsonSchema } from 'zod-to-json-schema';
17
+ import { toJsonSchema7 } from '../../shared/zodJsonSchema.js';
18
18
  import { settingsSchema } from '../schemas/settings.js';
19
19
  import { userIdentitySchema } from '../../shared/userMd/schema.js';
20
20
  let cached = null;
@@ -22,8 +22,8 @@ function build() {
22
22
  if (cached !== null)
23
23
  return cached;
24
24
  cached = {
25
- settings: zodToJsonSchema(settingsSchema, { name: 'Settings', target: 'jsonSchema7' }),
26
- userIdentity: zodToJsonSchema(userIdentitySchema, { name: 'UserIdentity', target: 'jsonSchema7' }),
25
+ settings: toJsonSchema7(settingsSchema, 'Settings'),
26
+ userIdentity: toJsonSchema7(userIdentitySchema, 'UserIdentity'),
27
27
  generatedAt: new Date().toISOString(),
28
28
  };
29
29
  return cached;
@@ -1,7 +1,7 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
- import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { toJsonSchema7 } from '../../shared/zodJsonSchema.js';
5
5
  import { settingsSchema } from '../schemas/settings.js';
6
6
  import { parseYaml, mergeIntoTemplate, diffValues, deepMerge, renderSparseSettings, substituteTemplatePlaceholders, TEMPLATE_PLACEHOLDER_DEFAULTS } from '../io/yamlIO.js';
7
7
  import { sparseSettingsValues } from './wizard.js';
@@ -102,11 +102,7 @@ const SHARED_WRITE_MESSAGE = 'This write lands through an agent-switch shared sy
102
102
  // added there must get its default here, or `settingsSchema.safeParse` on
103
103
  // the merged defaults will reject the first save.
104
104
  // Computed once — Zod → JSON Schema conversion is pure and the schema is static.
105
- const SETTINGS_JSON_SCHEMA = zodToJsonSchema(settingsSchema, {
106
- name: 'AgentSettings',
107
- $refStrategy: 'none',
108
- target: 'jsonSchema7',
109
- });
105
+ const SETTINGS_JSON_SCHEMA = toJsonSchema7(settingsSchema, 'AgentSettings');
110
106
  const SETTINGS_RELATIVE = join('settings', '.agent-settings.yml');
111
107
  /** Pre-typed-subdir flat-root location. Read as fallback for migration. */
112
108
  const LEGACY_FLAT_RELATIVE = '.agent-settings.yml';
@@ -4,7 +4,7 @@ import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { randomBytes } from 'node:crypto';
7
- import { z } from 'zod';
7
+ import * as z from 'zod';
8
8
  import { settingsSchema } from '../schemas/settings.js';
9
9
  import { userIdentitySchema } from '../../shared/userMd/schema.js';
10
10
  import { composeUserIdentity } from '../../shared/userMd/utils.js';
@@ -101,7 +101,7 @@ async function readInstalledPacks(writeRoot, legacyReadRoot, userGlobalReadRoot)
101
101
  const wizardStateSchema = z.object({
102
102
  step: z.number().int().min(0),
103
103
  totalSteps: z.number().int().min(1).optional(),
104
- partial: z.record(z.unknown()).default({}),
104
+ partial: z.record(z.string(), z.unknown()).default({}),
105
105
  startedAt: z.string().nullable().default(null),
106
106
  });
107
107
  // road-to-global-only-install § Phase 1.5 — WizardApplyPayload shape.
@@ -112,14 +112,14 @@ const wizardStateSchema = z.object({
112
112
  const installerV1PayloadSchema = z.object({
113
113
  schema_version: z.literal('installer-v1'),
114
114
  ai_tools: z.array(z.string().min(1)).min(1),
115
- configs: z.record(z.record(z.unknown())).default({}),
115
+ configs: z.record(z.string(), z.record(z.string(), z.unknown())).default({}),
116
116
  dry_run: z.boolean().optional(),
117
117
  });
118
118
  const wizardV2PayloadSchema = z.object({
119
119
  schema_version: z.literal('wizard-v2'),
120
120
  tools: z.array(z.string().min(1)).min(1),
121
121
  packs: z.array(z.string().min(1)).default([]),
122
- settings: z.record(z.unknown()).default({}),
122
+ settings: z.record(z.string(), z.unknown()).default({}),
123
123
  scope_to_project_only: z.boolean().optional(),
124
124
  dry_run: z.boolean().optional(),
125
125
  });
@@ -501,12 +501,12 @@ const aiCouncilPayloadSchema = z.object({
501
501
  defaultMode: z.enum(['manual', 'api', 'cli', 'auto']).optional(),
502
502
  minRounds: z.number().int().min(1).optional(),
503
503
  maxTotalUsd: z.number().min(0).optional(),
504
- members: z.record(z.object({
504
+ members: z.record(z.string(), z.object({
505
505
  enabled: z.boolean().optional(),
506
506
  participateLowImpact: z.boolean().optional(),
507
507
  })).optional(),
508
508
  // keys ∈ {trivial, low_impact, medium_impact} — others ignored.
509
- decision: z.record(z.enum(['agent', 'council', 'user'])).optional(),
509
+ decision: z.record(z.string(), z.enum(['agent', 'council', 'user'])).optional(),
510
510
  /**
511
511
  * Declared so the operator can flip the one BILLING-CLASS decision in the
512
512
  * fallback design without hand-editing — NOT to stop the GUI stripping it:
@@ -10,7 +10,7 @@
10
10
  * Render strategy: depth ≤ 2. The form renderer (`SchemaForm`, Phase 2)
11
11
  * recurses one level into nested objects; deeper trees fail render-time.
12
12
  */
13
- import { z } from 'zod';
13
+ import * as z from 'zod';
14
14
  const ruleLoadingTier = z.enum(['minimal', 'balanced', 'full', 'custom']);
15
15
  const disciplineProfile = z.enum(['auto', 'off', 'essential', 'full']);
16
16
  const enforcementMode = z.enum(['advisory', 'hard-stop']);
@@ -35,11 +35,11 @@ export const settingsSchema = z.object({
35
35
  agent_config_version: z.string().default('').describe('Pin the package to an exact semver (e.g. "1.4.2") so all teammates load the same skill / rule set. Leave empty to track whatever is installed locally — useful for the maintainers of this package, risky for production projects.'),
36
36
  profile: z.object({
37
37
  id: profileId.default('developer').describe('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>`.'),
38
- }).default({ id: 'developer' }),
38
+ }).prefault({ id: 'developer' }),
39
39
  projection: z.object({
40
40
  mode: projectionMode.default('legacy-all').describe('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.'),
41
41
  rule_packs: z.union([z.literal('auto'), z.array(z.string())]).default([]).describe('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.'),
42
- }).default({ mode: 'legacy-all' }),
42
+ }).prefault({ mode: 'legacy-all' }),
43
43
  rule_loading_tier: ruleLoadingTier.default('balanced').describe('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).'),
44
44
  discipline_profile: disciplineProfile.optional().describe('The ONE runtime knob for the discipline-rule tier (successor of rule_loading_tier; council 2026-07-07). off = kernel only (~1x tokens). essential = kernel + the measured lift-carrying rules (~3.3x, keeps the weak-host discipline lift). full = everything (~11.7x, EXPERIMENTAL — residual lift over essential not established). auto = resolve per session against the evidence-gated NULL-lift disable-list in src/config/host-capabilities.yml (measured-null host → off, otherwise → essential). Optional and opt-in until the P1/P2 evidence gates pass (agents/roadmaps/road-to-discipline-profile-tiering.md); absent = legacy rule_loading_tier applies.'),
45
45
  cost: z.object({
@@ -51,13 +51,13 @@ export const settingsSchema = z.object({
51
51
  cheap: z.number().min(0).nullable().default(null).describe('USD ceiling for the cheap model tier. Budget-aware delegation was ARCHIVED 2026-08-16 (docs/contracts/budget-routing.md), so no code routes on this today; the cap is still summed and reported by `budget.mjs tier`. null = no separate tier cap; global ceilings still apply.'),
52
52
  medium: z.number().min(0).nullable().default(null).describe('USD ceiling for the medium model tier. null = no separate tier cap.'),
53
53
  strong: z.number().min(0).nullable().default(null).describe('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.'),
54
- }).default({ cheap: null, medium: null, strong: null }),
54
+ }).prefault({ cheap: null, medium: null, strong: null }),
55
55
  }),
56
56
  enforcement: enforcementMode.default('advisory').describe('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.'),
57
57
  }),
58
58
  model: z.object({
59
59
  auto_switch: modelAutoSwitch.default('suggest').describe('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.'),
60
- }).default({ auto_switch: 'suggest' }),
60
+ }).prefault({ auto_switch: 'suggest' }),
61
61
  personal: z.object({
62
62
  ide: z.string().default('').describe('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.'),
63
63
  open_edited_files: z.boolean().default(false).describe('After the agent edits a file, run `<ide> <path>` to surface it in your editor immediately. Off by default to avoid window-stealing during long agent runs.'),
@@ -76,7 +76,7 @@ export const settingsSchema = z.object({
76
76
  commit_format: gitCommitFormat.default('ticket-scope').describe('Where a ticket id goes in a commit subject. ticket-scope (default) = the ticket is the Conventional-Commits scope, `feat(DEV-1234): add export filter` — the behaviour every install had before this key existed. ticket-conventional = the ticket leads the subject and the scope names the system area, `DEV-1234 feat(exporter): add export filter`; a ticket is then never a scope. Without a ticket both shapes are plain Conventional Commits. A commit-linting config in the repository (commitlint, a commit-msg hook) outranks this key.'),
77
77
  branch_pattern: z.string().default('{type}/{slug}').describe('Shape of a branch name the agent creates. Placeholders: {type} (feat, fix, …), {ticket} (e.g. DEV-1234), {slug} (short kebab-case description). Default {type}/{slug}. {slug} is required, and characters outside the placeholders are limited to A-Z a-z 0-9 . _ / - so a rendered name is safe in a command; agent-config git:convention show reports anything else as invalid. A team that names branches after the ticket sets {ticket}-{slug}. A placeholder with no value is dropped together with the separator that follows it, or the one before it when it is last, so no name starts or ends with a separator ({type}/{ticket}-{slug} without a ticket gives feat/slug). Reading a ticket back out of an existing branch never depends on this pattern.'),
78
78
  update_strategy: gitUpdateStrategy.default('merge').describe('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.'),
79
- }).default({}),
79
+ }).prefault({}),
80
80
  github: z.object({
81
81
  pr_reply_method: replyMethod.default('create_review_comment').describe('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.'),
82
82
  }),
@@ -106,8 +106,8 @@ export const settingsSchema = z.object({
106
106
  request_size_caps: z.object({
107
107
  bash_max_output_length: z.number().int().positive().max(150_000).nullable().default(null).describe('Character cap on Bash tool output the host places inline into a model request. null (default) = this package writes nothing and the host default of 30000 applies. A number is written into the host env block. Values above 150000 are capped by the host itself, so the schema refuses them here rather than writing a value the host silently changes. Bounds request SIZE, never request COUNT: lowering it suppresses no telemetry, no update check and no download.'),
108
108
  max_mcp_output_tokens: z.number().int().positive().nullable().default(null).describe('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.'),
109
- }).default({}).describe('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.'),
110
- }).default({}).describe('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.'),
109
+ }).prefault({}).describe('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.'),
110
+ }).prefault({}).describe('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.'),
111
111
  augment: z.object({
112
112
  rules_use_symlinks: z.boolean().default(false).describe('When true, .augment/rules/*.md are symlinks into dist/agent-src/rules/ — edits flow back to source on save. When false (default), they are copies — safer on Windows and shared volumes, but rule edits in .augment/ are lost on the next `task sync`.'),
113
113
  }),
@@ -138,24 +138,24 @@ export const settingsSchema = z.object({
138
138
  // outcome than an uninformative table cell.
139
139
  max_cost_per_run_usd: z.number().min(0).nullable().default(null).describe('Per-run USD ceiling for a CLASS-1 roadmap blocker executed via `agent-config gates --execute`. A class-1 entry whose **Budget:** field states a larger figure renders its consent line instead of running. null (default) = no per-run ceiling, per ADR-279: a spend bound applies only where one was set, and the absence of a bound is not a refusal. Bounds the size of an authorised spend; never supplies the authorisation — `--confirm` is still required on every class-1 run, set or unset.'),
140
140
  max_cost_per_rolling_7d_usd: z.number().min(0).nullable().default(null).describe('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.'),
141
- }).default({ max_cost_per_run_usd: null, max_cost_per_rolling_7d_usd: null }),
141
+ }).prefault({ max_cost_per_run_usd: null, max_cost_per_rolling_7d_usd: null }),
142
142
  }),
143
143
  planning: z.object({
144
144
  closure_pass: z.boolean().default(true).describe('Gate C — plan-closure pass. true (default) = a plan-artifact ask (/roadmap:create, roadmap-writing, /feature:plan, /feature:roadmap, /roadmap:materialize, /implement-ticket, /jira-ticket, /analyze:inbox, /analyze:roadmap-repos) ends in a closure pass (/challenge-me closure): every foreseeable decision is closed at the lowest rung that owns it and written into the roadmap\'s "## Decisions" table, so a long run never meets a question planning could have closed. false = inert, plan asks author directly. An explicit user bypass always wins for that turn and is counted as a bypass rather than as an absent closure.'),
145
145
  risk_review: z.boolean().default(true).describe('Gate R1 — plan-risk review. true (default) = every ready (non-draft) plan must carry a schema-valid "## Risk Register" section (ranked risks, mitigation + anchor per row, freshness marker, exact honest-null grammar), enforced by lint_plan_risk_register at pre-push + CI. false = escape hatch, the validator skips.'),
146
146
  completion_review: z.boolean().default(true).describe('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.'),
147
147
  })
148
- // `.default({})` is load-bearing, not cosmetic: without it the SECTION is
148
+ // `.prefault({})` is load-bearing, not cosmetic: without it the SECTION is
149
149
  // required, so any settings payload that omits `planning` fails with
150
150
  // `ZodError: planning Required` — which contradicts this gate family's own
151
151
  // contract ("missing key = true" for all three) and reds every server
152
152
  // test whose fixture predates the section. With the default, an absent
153
153
  // section materialises as the three `true` leaves the contract promises.
154
- .default({}),
154
+ .prefault({}),
155
155
  execution: z.object({
156
156
  fix_loop_max: z.number().int().min(1).default(10).describe('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.'),
157
157
  escalation: z.array(z.string()).default(['independent', 'council', 'team', 'owner_owned_check']).describe('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.'),
158
- }).default({}),
158
+ }).prefault({}),
159
159
  quality: z.object({
160
160
  local_auto_run: z.boolean().default(false).describe('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.'),
161
161
  local_auto_run_in_mission: z.boolean().default(true).describe('Run quality tools autonomously inside a mission — an autonomous roadmap run with a claimed contract. On by default: nobody is waiting, the next action belongs to the agent itself, and a red found twenty steps later costs more than the run that would have caught it at step one. Inert outside a mission, and a mission never lowers local_auto_run: true.'),
@@ -170,37 +170,37 @@ export const settingsSchema = z.object({
170
170
  color: z.number().nullable().default(null).describe('Colour distance below which a value may be reconciled, in OKLab ΔEOK. Ships null, and null means "preserve and report" rather than "reconcile freely". No number is set because none is supported: zero of seven design-to-code benchmarks in the evidence set score token conformance, and a number in a config file — even flagged unmeasured — shapes behavior and acquires the authority of a default. Not an RGB channel delta: RGB distance is not perceptually uniform.'),
171
171
  length: z.number().nullable().default(null).describe('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.'),
172
172
  })
173
- .default({ color: null, length: null }),
173
+ .prefault({ color: null, length: null }),
174
174
  })
175
- .default({ enabled: false, tolerance: { color: null, length: null } }),
176
- }).default({
175
+ .prefault({ enabled: false, tolerance: { color: null, length: null } }),
176
+ }).prefault({
177
177
  fidelity_mode: 'strict',
178
178
  approximation: { enabled: false, tolerance: { color: null, length: null } },
179
179
  }),
180
180
  code_graph: z.object({
181
181
  consumer_index_paths: z.array(z.string()).default([]).describe("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."),
182
- }).default({}),
182
+ }).prefault({}),
183
183
  screenshots: z.object({
184
184
  identity_allowlist: z.array(z.string()).default([]).describe("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."),
185
185
  forbid_terminal_capture: z.boolean().default(true).describe('Consumed by the doc-screenshot-hygiene rule. true (default) = terminal/CLI/IDE screenshots are forbidden (highest leak vector: absolute local paths, env tokens); use text code blocks with text redaction instead. false = allowed, still subject to the data-bearing human gate.'),
186
186
  data_bearing_gate: z.enum(['on', 'off']).default('on').describe('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).'),
187
- }).default({ identity_allowlist: [], forbid_terminal_capture: true, data_bearing_gate: 'on' }),
187
+ }).prefault({ identity_allowlist: [], forbid_terminal_capture: true, data_bearing_gate: 'on' }),
188
188
  verbosity: z.object({
189
189
  offer_council_in_delivery: z.boolean().default(false).describe('Offer "run AI Council on this?" inside delivery commands (/feature-plan, /review-changes, /roadmap-create). Council commands themselves are unaffected.'),
190
- }).default({ offer_council_in_delivery: false }),
190
+ }).prefault({ offer_council_in_delivery: false }),
191
191
  code_style: z.object({
192
192
  docblocks: z.enum(['minimal', 'full']).default('minimal').describe('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.'),
193
- }).default({ docblocks: 'minimal' }),
193
+ }).prefault({ docblocks: 'minimal' }),
194
194
  reasoning: z.object({
195
195
  enabled: z.boolean().default(true).describe('Master switch for the Reasoning Discipline Protocol (RDP). false = the whole layer is inert (zero overhead).'),
196
- }).default({}),
196
+ }).prefault({}),
197
197
  subagents: z.object({
198
198
  quota_arbitrage: z.boolean().default(true).describe('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.'),
199
199
  model_map: z.object({
200
200
  lite: z.string().default('').describe('Model alias for lite-tier sub-tasks. Empty = the tier runtime default.'),
201
201
  medium: z.string().default('').describe('Model alias for medium-tier sub-tasks. Empty = the tier runtime default.'),
202
202
  high: z.string().default('').describe('Model alias for high-tier sub-tasks. Empty = the tier runtime default.'),
203
- }).default({}).describe('Per-tier model map for downshift routing. Each empty value uses the tier runtime default (no vendor model baked in).'),
203
+ }).prefault({}).describe('Per-tier model map for downshift routing. Each empty value uses the tier runtime default (no vendor model baked in).'),
204
204
  implementer_model: z.string().default('').describe('Override the model the orchestrator dispatches to subagents that write code (e.g. claude-sonnet-4, gpt-5). Empty (default) = inherit the session\'s primary model — cheapest and usually right.'),
205
205
  judge_model: z.string().default('').describe('Override the model used for review / judge subagents that critique implementer output. Empty (default) = one tier above the implementer model — picks up nuance the implementer missed.'),
206
206
  model_ceiling: z.string().default('').describe('Session-wide model CEILING for subagents (spend cap). Empty (default) = no ceiling. When set, suite-owned CLI spawn wrappers export CLAUDE_CODE_SUBAGENT_MODEL to the sessions they launch. Class C: a human sets it; the agent never writes or infers one.'),
@@ -213,7 +213,7 @@ export const settingsSchema = z.object({
213
213
  // `worktrees.cleanup` governs REMOVAL, a separate question (ADR-295).
214
214
  worktrees: z.object({
215
215
  cleanup: z.enum(['off', 'notify', 'auto']).default('notify').describe('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).'),
216
- }).default({ cleanup: 'notify' }),
216
+ }).prefault({ cleanup: 'notify' }),
217
217
  ai_team: z.object({
218
218
  model: z.string().default('auto').describe("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>`."),
219
219
  allow_delegate: z.boolean().default(false).describe('Second opt-in for the only wrapper that delegates write access (/team:delegate). false (default) = delegate refuses even when /team itself is available. Availability (codex CLI installed + authenticated) is necessary but not sufficient — this key must ALSO be true before delegation is reachable.'),
@@ -221,12 +221,12 @@ export const settingsSchema = z.object({
221
221
  review_gate: z.object({
222
222
  managed: z.boolean().default(false).describe("Managed governance of the codex plugin's Stop-hook Review Gate (road-to-team-mode Phase 4). false (default) = byte-identical pre-Phase-4 behavior: no counting, no circuit breaker. true = count consecutive BLOCK verdicts per session and trip the circuit breaker at max_consecutive_blocks."),
223
223
  max_consecutive_blocks: z.number().int().min(1).default(3).describe('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.'),
224
- }).default({ managed: false, max_consecutive_blocks: 3 }),
225
- }).default({ model: 'auto', allow_delegate: false, max_calls_per_day: 50, review_gate: { managed: false, max_consecutive_blocks: 3 } }),
224
+ }).prefault({ managed: false, max_consecutive_blocks: 3 }),
225
+ }).prefault({ model: 'auto', allow_delegate: false, max_calls_per_day: 50, review_gate: { managed: false, max_consecutive_blocks: 3 } }),
226
226
  emergency: z.object({
227
227
  orchestration_halt: z.boolean().default(false).describe('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.'),
228
228
  orchestration_halt_justification: z.string().default('').describe('Required non-empty before orchestration_halt may return to false. Ignored while arming the halt.'),
229
- }).default({ orchestration_halt: false, orchestration_halt_justification: '' }),
229
+ }).prefault({ orchestration_halt: false, orchestration_halt_justification: '' }),
230
230
  onboarding: z.object({
231
231
  onboarded: z.boolean().default(false).describe('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.'),
232
232
  }),
@@ -246,7 +246,7 @@ export const settingsSchema = z.object({
246
246
  }),
247
247
  retro: z.object({
248
248
  enabled: z.boolean().default(true).describe('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.'),
249
- }).default({}),
249
+ }).prefault({}),
250
250
  memory: z.object({
251
251
  redact_patterns: z.array(z.string()).default([]).describe('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.'),
252
252
  session_index: z.enum(['on', 'off']).default('off').describe('Opt-in compact memory index at session start (road-to-memory-retrieval-economy P5). on = inject a compact id + title + ~tokens index of curated entries (hard cap 30 rows, bodies never included) through the hot-context hook; the agent fetches full entries via memory_get on demand. off (default) = no injection — the ship-criterion (measured hit-rate gain) is unproven, so off unless proven.'),
@@ -256,44 +256,44 @@ export const settingsSchema = z.object({
256
256
  global_sharing: z.object({
257
257
  enabled: z.boolean().default(true).describe('Master switch for the file-first global knowledge-card store (ADR-100; default-ON per ADR-119, the validated bounded-downside flip superseding ADR-103 — write-time redaction incl. hidden-unicode hardening, narrowest tier default, pre-registered demotion trigger). User-global setting — keep in ~/.event4u/agent-config/agent-settings.yml. false fully no-ops the layer (single-key revert); project-local cards (v1) are unaffected.'),
258
258
  allowed_tiers: z.array(z.string()).default(['public']).describe('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.'),
259
- }).default({}),
260
- }).default({}),
259
+ }).prefault({}),
260
+ }).prefault({}),
261
261
  egress: z.object({
262
262
  placeholders: z.object({
263
263
  mode: z.enum(['off', 'observe', 'protect', 'strict']).default('off').describe('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.'),
264
- }).default({}),
265
- }).default({}),
264
+ }).prefault({}),
265
+ }).prefault({}),
266
266
  hooks: z.object({
267
267
  concern_budget: z.object({
268
268
  tier1_concerns: z.array(z.string()).default([]).describe('Concern IDs (one per line) that are allowed to block the run on failure rather than warn. Reserved for high-confidence guards — leave empty unless you maintain custom hooks.'),
269
269
  }),
270
270
  verify_before_complete: z.object({
271
271
  touched_file_quality: z.enum(['off', 'shadow', 'warn']).default('off').describe("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."),
272
- }).default({}),
272
+ }).prefault({}),
273
273
  injection_scan: z.object({
274
274
  enabled: z.boolean().default(false).describe('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.'),
275
- }).default({}),
275
+ }).prefault({}),
276
276
  runtime_journal: z.object({
277
277
  enabled: z.boolean().default(false).describe('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.'),
278
- }).default({}),
278
+ }).prefault({}),
279
279
  rtk_wrap: z.object({
280
280
  enabled: z.boolean().default(false).describe('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.'),
281
- }).default({}),
281
+ }).prefault({}),
282
282
  design_slop: z.object({
283
283
  enabled: z.boolean().default(false).describe('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.'),
284
- }).default({}),
284
+ }).prefault({}),
285
285
  design_pass: z.object({
286
286
  enabled: z.boolean().default(false).describe('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.'),
287
- }).default({}),
287
+ }).prefault({}),
288
288
  ui_route_nudge: z.object({
289
289
  enabled: z.boolean().default(false).describe('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.'),
290
- }).default({}),
290
+ }).prefault({}),
291
291
  code_graph: z.object({
292
292
  enabled: z.boolean().default(false).describe('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.'),
293
- }).default({}),
293
+ }).prefault({}),
294
294
  suggestion_capture: z.object({
295
295
  enabled: z.boolean().default(false).describe('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.'),
296
- }).default({}),
296
+ }).prefault({}),
297
297
  // `turn_end_gate` is deliberately ABSENT. The stop-slot turn-end gate is
298
298
  // always armed (2026-08-12) and has no settings surface: whether it
299
299
  // fires is decided by each detector's own trigger conditions, not by a
@@ -315,13 +315,13 @@ export const settingsSchema = z.object({
315
315
  mode: z.enum(['off', 'notify']).default('notify').describe('Whether the first session start of a day prints the advisory findings that need action today. notify (default) reads the local cache only — a session start never touches the network; off prints nothing, and agent-config audit:deps show still works.'),
316
316
  egress: z.enum(['none', 'query']).default('query').describe('What leaves the machine when agent-config audit:deps refresh runs. query (default) sends names and versions of public packages to api.osv.dev and reads endoflife.date and the PHP and Node release feeds; a private package name is never sent, and a native npm or composer audit, which sends the lockfile package list to the registry that tool is configured for, runs only for a lockfile with no private package. none sends nothing, and findings read unknown rather than clear.'),
317
317
  kev: z.enum(['off', 'on']).default('on').describe('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.'),
318
- }).default({ mode: 'notify', egress: 'query', kev: 'on' }),
318
+ }).prefault({ mode: 'notify', egress: 'query', kev: 'on' }),
319
319
  update: z.object({
320
320
  mode: z.enum(['off', 'notify', 'ask', 'auto']).default('notify').describe('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.'),
321
- }).default({ mode: 'notify' }),
321
+ }).prefault({ mode: 'notify' }),
322
322
  legal_review_prep: z.object({
323
323
  acknowledged: z.boolean().default(false).describe('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.'),
324
324
  require_council: z.boolean().default(true).describe('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.'),
325
- }).default({ acknowledged: false, require_council: true }),
325
+ }).prefault({ acknowledged: false, require_council: true }),
326
326
  });
327
327
  //# sourceMappingURL=settings.js.map
@@ -23,7 +23,7 @@ function unwrapRef(root) {
23
23
  return root;
24
24
  }
25
25
  /**
26
- * Flatten a `zodToJsonSchema` output into dotted leaf entries. Objects with
26
+ * Flatten a `toJsonSchema7` output into dotted leaf entries. Objects with
27
27
  * `properties` recurse; everything else (scalars, enums, arrays, free-form
28
28
  * maps) is a leaf. Deterministic key order (sorted) so snapshots diff
29
29
  * cleanly in git and across runs.
@@ -10,7 +10,7 @@
10
10
  * wizard finish) and the Vite-bundled UI (form validation) consume
11
11
  * the same Zod schema. ESLint forbids Node-only imports in this tree.
12
12
  */
13
- import { z } from 'zod';
13
+ import * as z from 'zod';
14
14
  /** Hard cap on the optional `notes` block. Keeps the file loadable into every reply. */
15
15
  export const USER_IDENTITY_NOTES_MAX_CHARS = 8_000;
16
16
  /** Hard cap on the `voice_sample` block — one to three sentences in practice. */
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A Zod schema as a draft-07 JSON Schema, in the envelope the settings surface,
3
+ * the generated settings reference and the UI form renderer read:
4
+ * `{ $ref: '#/definitions/<name>', definitions: { <name>: … }, $schema }`.
5
+ *
6
+ * Zod 4 converts natively (`z.toJSONSchema`); this replaces `zod-to-json-schema`,
7
+ * which only understands Zod 3. `io: 'input'` keeps a defaulted field optional,
8
+ * as the old converter did, because a settings file may omit it.
9
+ */
10
+ import * as z from 'zod';
11
+ /**
12
+ * The places the native output differs from what consumers were built on: an
13
+ * object without `additionalProperties` (the old converter closed every
14
+ * object), an `enum` without a `type` (it named the primitive type), and an
15
+ * integer carrying the safe-integer bounds Zod 4 adds to `.int()`, which says
16
+ * nothing a reader of a settings schema can use.
17
+ */
18
+ function normalise(node) {
19
+ if (Array.isArray(node)) {
20
+ for (const n of node)
21
+ normalise(n);
22
+ return;
23
+ }
24
+ if (typeof node !== 'object' || node === null)
25
+ return;
26
+ const o = node;
27
+ if (o['properties'] !== undefined && o['additionalProperties'] === undefined)
28
+ o['additionalProperties'] = false;
29
+ if (Array.isArray(o['enum']) && o['type'] === undefined) {
30
+ const kinds = new Set(o['enum'].map((v) => typeof v));
31
+ if (kinds.size === 1 && (kinds.has('string') || kinds.has('number') || kinds.has('boolean')))
32
+ o['type'] = [...kinds][0];
33
+ }
34
+ if (o['maximum'] === Number.MAX_SAFE_INTEGER)
35
+ delete o['maximum'];
36
+ if (o['minimum'] === Number.MIN_SAFE_INTEGER)
37
+ delete o['minimum'];
38
+ for (const v of Object.values(o))
39
+ normalise(v);
40
+ }
41
+ export function toJsonSchema7(schema, name) {
42
+ const body = z.toJSONSchema(schema, { target: 'draft-7', io: 'input', unrepresentable: 'any' });
43
+ normalise(body);
44
+ const { $schema: _drop, ...definition } = body;
45
+ return { $ref: `#/definitions/${name}`, definitions: { [name]: definition }, $schema: 'http://json-schema.org/draft-07/schema#' };
46
+ }
47
+ //# sourceMappingURL=zodJsonSchema.js.map