@cleocode/core 2026.10.18 → 2026.10.19

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 (259) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/check/pr-gate.js +1 -1
  3. package/dist/check/pr-gate.js.map +1 -1
  4. package/dist/cloud/nexus-vault.d.ts +4 -3
  5. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  6. package/dist/cloud/nexus-vault.js +62 -22
  7. package/dist/cloud/nexus-vault.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +11 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/docs/conduit-history-backfill.d.ts +210 -0
  12. package/dist/docs/conduit-history-backfill.d.ts.map +1 -0
  13. package/dist/docs/conduit-history-backfill.js +451 -0
  14. package/dist/docs/conduit-history-backfill.js.map +1 -0
  15. package/dist/docs/docs-ops.d.ts +33 -0
  16. package/dist/docs/docs-ops.d.ts.map +1 -1
  17. package/dist/docs/docs-ops.js +72 -0
  18. package/dist/docs/docs-ops.js.map +1 -1
  19. package/dist/docs/docs-update.d.ts.map +1 -1
  20. package/dist/docs/docs-update.js +71 -0
  21. package/dist/docs/docs-update.js.map +1 -1
  22. package/dist/docs/doctor.d.ts +18 -4
  23. package/dist/docs/doctor.d.ts.map +1 -1
  24. package/dist/docs/doctor.js +124 -3
  25. package/dist/docs/doctor.js.map +1 -1
  26. package/dist/docs/section-merge.d.ts +70 -9
  27. package/dist/docs/section-merge.d.ts.map +1 -1
  28. package/dist/docs/section-merge.js +167 -10
  29. package/dist/docs/section-merge.js.map +1 -1
  30. package/dist/docs/section-write-path.d.ts.map +1 -1
  31. package/dist/docs/section-write-path.js +23 -8
  32. package/dist/docs/section-write-path.js.map +1 -1
  33. package/dist/gateway-client/generated/types.gen.d.ts +4 -0
  34. package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
  35. package/dist/go/driver.d.ts +29 -145
  36. package/dist/go/driver.d.ts.map +1 -1
  37. package/dist/go/driver.js +75 -168
  38. package/dist/go/driver.js.map +1 -1
  39. package/dist/go/index.d.ts +1 -1
  40. package/dist/go/index.d.ts.map +1 -1
  41. package/dist/go/index.js +1 -1
  42. package/dist/go/index.js.map +1 -1
  43. package/dist/goal/close.d.ts +1 -1
  44. package/dist/goal/close.js +2 -2
  45. package/dist/goal/close.js.map +1 -1
  46. package/dist/goal/draft.d.ts +10 -3
  47. package/dist/goal/draft.d.ts.map +1 -1
  48. package/dist/goal/draft.js +14 -9
  49. package/dist/goal/draft.js.map +1 -1
  50. package/dist/goal/index.d.ts +9 -12
  51. package/dist/goal/index.d.ts.map +1 -1
  52. package/dist/goal/index.js +9 -12
  53. package/dist/goal/index.js.map +1 -1
  54. package/dist/goal/runtime.d.ts +126 -0
  55. package/dist/goal/runtime.d.ts.map +1 -0
  56. package/dist/goal/runtime.js +289 -0
  57. package/dist/goal/runtime.js.map +1 -0
  58. package/dist/goal/status.d.ts +39 -0
  59. package/dist/goal/status.d.ts.map +1 -0
  60. package/dist/goal/status.js +101 -0
  61. package/dist/goal/status.js.map +1 -0
  62. package/dist/hooks/project-runner.d.ts +8 -3
  63. package/dist/hooks/project-runner.d.ts.map +1 -1
  64. package/dist/hooks/project-runner.js +104 -14
  65. package/dist/hooks/project-runner.js.map +1 -1
  66. package/dist/hooks/project-state.js +1 -1
  67. package/dist/hooks/project-state.js.map +1 -1
  68. package/dist/internal.d.ts +7 -3
  69. package/dist/internal.d.ts.map +1 -1
  70. package/dist/internal.js +5 -3
  71. package/dist/internal.js.map +1 -1
  72. package/dist/lifecycle/engine-ops.d.ts.map +1 -1
  73. package/dist/lifecycle/engine-ops.js +9 -6
  74. package/dist/lifecycle/engine-ops.js.map +1 -1
  75. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  76. package/dist/memory/retrieval/observe.js +4 -8
  77. package/dist/memory/retrieval/observe.js.map +1 -1
  78. package/dist/paths.d.ts.map +1 -1
  79. package/dist/paths.js +4 -1
  80. package/dist/paths.js.map +1 -1
  81. package/dist/playbooks/cantbook-profile.d.ts +3 -3
  82. package/dist/playbooks/cantbook-profile.js +3 -3
  83. package/dist/playbooks/pi-runner-wiring.d.ts +3 -3
  84. package/dist/playbooks/pi-runner-wiring.js +3 -3
  85. package/dist/release/ci-evidence.d.ts +28 -0
  86. package/dist/release/ci-evidence.d.ts.map +1 -1
  87. package/dist/release/ci-evidence.js +76 -20
  88. package/dist/release/ci-evidence.js.map +1 -1
  89. package/dist/release/open.d.ts +11 -0
  90. package/dist/release/open.d.ts.map +1 -1
  91. package/dist/release/open.js +49 -10
  92. package/dist/release/open.js.map +1 -1
  93. package/dist/release/preflight-skip.d.ts +50 -0
  94. package/dist/release/preflight-skip.d.ts.map +1 -1
  95. package/dist/release/preflight-skip.js +151 -34
  96. package/dist/release/preflight-skip.js.map +1 -1
  97. package/dist/release/reconcile.d.ts.map +1 -1
  98. package/dist/release/reconcile.js +6 -3
  99. package/dist/release/reconcile.js.map +1 -1
  100. package/dist/scaffold/ensure-config.d.ts +1 -1
  101. package/dist/scaffold/ensure-config.d.ts.map +1 -1
  102. package/dist/scaffold/ensure-config.js +2 -0
  103. package/dist/scaffold/ensure-config.js.map +1 -1
  104. package/dist/security/index.d.ts +1 -1
  105. package/dist/security/index.d.ts.map +1 -1
  106. package/dist/security/index.js +1 -1
  107. package/dist/security/index.js.map +1 -1
  108. package/dist/security/override-cap-setting.d.ts +180 -0
  109. package/dist/security/override-cap-setting.d.ts.map +1 -0
  110. package/dist/security/override-cap-setting.js +657 -0
  111. package/dist/security/override-cap-setting.js.map +1 -0
  112. package/dist/security/override-cap.d.ts +20 -29
  113. package/dist/security/override-cap.d.ts.map +1 -1
  114. package/dist/security/override-cap.js +47 -71
  115. package/dist/security/override-cap.js.map +1 -1
  116. package/dist/security/owner-override-auth.d.ts +58 -0
  117. package/dist/security/owner-override-auth.d.ts.map +1 -1
  118. package/dist/security/owner-override-auth.js +82 -1
  119. package/dist/security/owner-override-auth.js.map +1 -1
  120. package/dist/security/owner-override-authority.d.ts +67 -0
  121. package/dist/security/owner-override-authority.d.ts.map +1 -0
  122. package/dist/security/owner-override-authority.js +100 -0
  123. package/dist/security/owner-override-authority.js.map +1 -0
  124. package/dist/sessions/handoff.d.ts.map +1 -1
  125. package/dist/sessions/handoff.js +4 -5
  126. package/dist/sessions/handoff.js.map +1 -1
  127. package/dist/sessions/index.d.ts.map +1 -1
  128. package/dist/sessions/index.js +22 -25
  129. package/dist/sessions/index.js.map +1 -1
  130. package/dist/store/conduit-doc-history.d.ts +39 -7
  131. package/dist/store/conduit-doc-history.d.ts.map +1 -1
  132. package/dist/store/conduit-doc-history.js +62 -12
  133. package/dist/store/conduit-doc-history.js.map +1 -1
  134. package/dist/store/data-safety-central.d.ts.map +1 -1
  135. package/dist/store/data-safety-central.js +3 -2
  136. package/dist/store/data-safety-central.js.map +1 -1
  137. package/dist/store/json.d.ts.map +1 -1
  138. package/dist/store/json.js +7 -7
  139. package/dist/store/json.js.map +1 -1
  140. package/dist/store/portable-bundle-scan.d.ts +23 -1
  141. package/dist/store/portable-bundle-scan.d.ts.map +1 -1
  142. package/dist/store/portable-bundle-scan.js +85 -3
  143. package/dist/store/portable-bundle-scan.js.map +1 -1
  144. package/dist/store/portable-bundle.d.ts.map +1 -1
  145. package/dist/store/portable-bundle.js +6 -1
  146. package/dist/store/portable-bundle.js.map +1 -1
  147. package/dist/store/row-identity-registry.d.ts +5 -3
  148. package/dist/store/row-identity-registry.d.ts.map +1 -1
  149. package/dist/store/row-identity-registry.js +54 -38
  150. package/dist/store/row-identity-registry.js.map +1 -1
  151. package/dist/store/schema/cleo-project/conduit.d.ts.map +1 -1
  152. package/dist/store/schema/cleo-project/conduit.js +4 -3
  153. package/dist/store/schema/cleo-project/conduit.js.map +1 -1
  154. package/dist/store/schema/cleo-project/runtime.d.ts +32 -0
  155. package/dist/store/schema/cleo-project/runtime.d.ts.map +1 -1
  156. package/dist/store/schema/cleo-project/runtime.js +7 -3
  157. package/dist/store/schema/cleo-project/runtime.js.map +1 -1
  158. package/dist/store/session-store.d.ts +45 -0
  159. package/dist/store/session-store.d.ts.map +1 -1
  160. package/dist/store/session-store.js +96 -1
  161. package/dist/store/session-store.js.map +1 -1
  162. package/dist/store/sqlite.d.ts +2 -0
  163. package/dist/store/sqlite.d.ts.map +1 -1
  164. package/dist/store/sqlite.js +12 -0
  165. package/dist/store/sqlite.js.map +1 -1
  166. package/dist/store/sync/merge/rules.d.ts.map +1 -1
  167. package/dist/store/sync/merge/rules.js +0 -16
  168. package/dist/store/sync/merge/rules.js.map +1 -1
  169. package/dist/store/sync/sealer.d.ts.map +1 -1
  170. package/dist/store/sync/sealer.js +27 -1
  171. package/dist/store/sync/sealer.js.map +1 -1
  172. package/dist/store/sync/sequencing.d.ts +3 -1
  173. package/dist/store/sync/sequencing.d.ts.map +1 -1
  174. package/dist/store/sync/sequencing.js +5 -3
  175. package/dist/store/sync/sequencing.js.map +1 -1
  176. package/dist/store/sync/timestamp-canon.d.ts.map +1 -1
  177. package/dist/store/sync/timestamp-canon.js +4 -0
  178. package/dist/store/sync/timestamp-canon.js.map +1 -1
  179. package/dist/store/table-classification.d.ts.map +1 -1
  180. package/dist/store/table-classification.js +12 -8
  181. package/dist/store/table-classification.js.map +1 -1
  182. package/dist/store/twin-collapse.d.ts +21 -0
  183. package/dist/store/twin-collapse.d.ts.map +1 -1
  184. package/dist/store/twin-collapse.js +235 -18
  185. package/dist/store/twin-collapse.js.map +1 -1
  186. package/dist/system/health.d.ts +17 -0
  187. package/dist/system/health.d.ts.map +1 -1
  188. package/dist/system/health.js +96 -0
  189. package/dist/system/health.js.map +1 -1
  190. package/dist/tasks/affected-packages.d.ts +67 -2
  191. package/dist/tasks/affected-packages.d.ts.map +1 -1
  192. package/dist/tasks/affected-packages.js +149 -21
  193. package/dist/tasks/affected-packages.js.map +1 -1
  194. package/dist/tasks/affected-scope.d.ts.map +1 -1
  195. package/dist/tasks/affected-scope.js +91 -5
  196. package/dist/tasks/affected-scope.js.map +1 -1
  197. package/dist/tasks/changeset-plan.d.ts +33 -0
  198. package/dist/tasks/changeset-plan.d.ts.map +1 -0
  199. package/dist/tasks/changeset-plan.js +152 -0
  200. package/dist/tasks/changeset-plan.js.map +1 -0
  201. package/dist/tasks/complete.d.ts.map +1 -1
  202. package/dist/tasks/complete.js +9 -1
  203. package/dist/tasks/complete.js.map +1 -1
  204. package/dist/tasks/done-plan.d.ts.map +1 -1
  205. package/dist/tasks/done-plan.js +28 -14
  206. package/dist/tasks/done-plan.js.map +1 -1
  207. package/dist/tasks/done-record.d.ts.map +1 -1
  208. package/dist/tasks/done-record.js +5 -1
  209. package/dist/tasks/done-record.js.map +1 -1
  210. package/dist/tasks/evidence.d.ts.map +1 -1
  211. package/dist/tasks/evidence.js +149 -18
  212. package/dist/tasks/evidence.js.map +1 -1
  213. package/dist/tasks/gate-runner.d.ts.map +1 -1
  214. package/dist/tasks/gate-runner.js +76 -4
  215. package/dist/tasks/gate-runner.js.map +1 -1
  216. package/dist/tasks/qa-run-binding.d.ts +18 -0
  217. package/dist/tasks/qa-run-binding.d.ts.map +1 -1
  218. package/dist/tasks/qa-run-binding.js +41 -1
  219. package/dist/tasks/qa-run-binding.js.map +1 -1
  220. package/dist/tasks/test-run-binding.d.ts +62 -0
  221. package/dist/tasks/test-run-binding.d.ts.map +1 -1
  222. package/dist/tasks/test-run-binding.js +126 -21
  223. package/dist/tasks/test-run-binding.js.map +1 -1
  224. package/dist/tasks/tool-resolver.d.ts +7 -0
  225. package/dist/tasks/tool-resolver.d.ts.map +1 -1
  226. package/dist/tasks/tool-resolver.js +3 -1
  227. package/dist/tasks/tool-resolver.js.map +1 -1
  228. package/dist/validation/engine-ops.d.ts.map +1 -1
  229. package/dist/validation/engine-ops.js +24 -29
  230. package/dist/validation/engine-ops.js.map +1 -1
  231. package/migrations/drizzle-cleo-project/20261016120000_t12535-playbook-row-uids/migration.sql +24 -0
  232. package/package.json +13 -13
  233. package/schemas/project-context.schema.json +5 -0
  234. package/templates/CLEO-INJECTION.md +1 -1
  235. package/templates/CLEO-REFERENCE.md +42 -1
  236. package/templates/cleo-gitignore +3 -0
  237. package/templates/git-hooks/legacy-hashes.json +1 -0
  238. package/templates/git-hooks/pre-push +35 -7
  239. package/templates/workflows/release-prepare.yml.tmpl +82 -4
  240. package/dist/goal/advance-with-persist.d.ts +0 -80
  241. package/dist/goal/advance-with-persist.d.ts.map +0 -1
  242. package/dist/goal/advance-with-persist.js +0 -105
  243. package/dist/goal/advance-with-persist.js.map +0 -1
  244. package/dist/goal/arm.d.ts +0 -61
  245. package/dist/goal/arm.d.ts.map +0 -1
  246. package/dist/goal/arm.js +0 -65
  247. package/dist/goal/arm.js.map +0 -1
  248. package/dist/goal/continuation.d.ts +0 -66
  249. package/dist/goal/continuation.d.ts.map +0 -1
  250. package/dist/goal/continuation.js +0 -111
  251. package/dist/goal/continuation.js.map +0 -1
  252. package/dist/goal/judge.d.ts +0 -113
  253. package/dist/goal/judge.d.ts.map +0 -1
  254. package/dist/goal/judge.js +0 -193
  255. package/dist/goal/judge.js.map +0 -1
  256. package/dist/goal/loop.d.ts +0 -75
  257. package/dist/goal/loop.d.ts.map +0 -1
  258. package/dist/goal/loop.js +0 -132
  259. package/dist/goal/loop.js.map +0 -1
@@ -0,0 +1,657 @@
1
+ /**
2
+ * The per-project owner override cap setting (T13787, owner decision
3
+ * 2026-10-16): `evidence.overrideCapPerSession` replaces the fixed cap of 10
4
+ * per session, and ONLY the owner may change it.
5
+ *
6
+ * ## Threat model
7
+ *
8
+ * An agent running as the same OS user can rewrite any local file, so no
9
+ * local check is tamper-proof. The goal is narrower and achievable: an agent
10
+ * cannot raise the cap through the supported path, and cannot raise it
11
+ * silently. Tampering is detectable, not preventable.
12
+ *
13
+ * ## Enrollment (explicit, owner-only, anchored twice)
14
+ *
15
+ * `cleo config owner-enroll` (never a side effect of a set) derives an Ed25519
16
+ * key from the owner-auth password (scrypt over a fresh per-project salt) and
17
+ * records its public key in two places a single file edit cannot reset:
18
+ * - `.cleo/owner-settings.json`, which is tracked in git like
19
+ * `.cleo/project.json` (ADR-094), so a delete or forge shows in
20
+ * `git status` and PR diffs;
21
+ * - the global store (`<cleoHome>/owner-enrollments.json`), keyed by project
22
+ * id, with when, which session and which terminal enrolled it.
23
+ * Re-enrolling over an existing record needs `--reset`, which is audited.
24
+ * `cleo doctor` reports the enrollment and any mismatch.
25
+ *
26
+ * ## Who may enroll or set
27
+ *
28
+ * T1118 L4 as built: not a restricted agent role, stdin and stderr a TTY, and
29
+ * the password typed now matching the bound session's owner-auth verifier
30
+ * (`cleo session start --scope global --name owner --owner-auth`). A pty
31
+ * (`script`) passes the TTY check, so an agent CAN enroll in a project that has
32
+ * no enrollment yet — which is why enrollment is explicit, recorded with its
33
+ * terminal, and shown by `cleo doctor` and git.
34
+ *
35
+ * ## Reading
36
+ *
37
+ * The cap is the sealed value only when the project file and the global record
38
+ * agree on the key and the seal verifies. A missing or unreadable file with a
39
+ * global record, a file with no global record, a key mismatch or a bad seal
40
+ * each fall back to {@link DEFAULT_OVERRIDE_CAP_PER_SESSION}, with a warning and
41
+ * an audit line in `.cleo/audit/force-bypass.jsonl`. Every set and every
42
+ * refused attempt is audited there with the old and new value. Who may use
43
+ * `CLEO_OWNER_OVERRIDE=1` at all is decided by `owner-override-authority.ts`
44
+ * (T13790).
45
+ *
46
+ * @task T13787
47
+ */
48
+ import { execFileSync } from 'node:child_process';
49
+ import { createHash, randomBytes, scryptSync } from 'node:crypto';
50
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
51
+ import { join } from 'node:path';
52
+ import { BRANCH_LOCK_ERROR_CODES } from '@cleocode/contracts/branch-lock.js';
53
+ import { readPortableProjectId } from '@cleocode/paths';
54
+ import { getCleoHome } from '../paths.js';
55
+ import { atomicWriteJson } from '../store/atomic.js';
56
+ import { DEFAULT_OVERRIDE_CAP_PER_SESSION, MAX_OVERRIDE_CAP_PER_SESSION, OVERRIDE_CAP_OWNER_COMMAND, OVERRIDE_CAP_SETTING, } from './override-cap.js';
57
+ import { appendOwnerOverrideAudit, isTtyPresent, OWNER_SCRYPT_PARAMS, OWNER_SESSION_MARK, validateOwnerOverride, verifyOwnerAuthPassword, } from './owner-override-auth.js';
58
+ /** The explicit owner enrollment command, named in every refusal. */
59
+ export const OWNER_ENROLL_COMMAND = 'cleo config owner-enroll';
60
+ /** How the owner runs {@link OVERRIDE_CAP_OWNER_COMMAND}. */
61
+ const OWNER_HOW = 'run it yourself in an interactive terminal, in a session started with `cleo session start --scope global --name owner --owner-auth`';
62
+ /** An enrollment younger than this is reported as a doctor WARN. */
63
+ const RECENT_ENROLLMENT_MS = 7 * 24 * 60 * 60 * 1000;
64
+ /** Parent commands that wrap a fake terminal around a process. */
65
+ const PTY_WRAPPERS = /(^|\/)(script|expect|unbuffer)$/;
66
+ /** Git's view of the project file, as `cleo doctor project-identity` checks identity files. */
67
+ function trackingWarnings(projectRoot) {
68
+ const rel = '.cleo/owner-settings.json';
69
+ const git = (args) => {
70
+ try {
71
+ const out = execFileSync('git', args, {
72
+ cwd: projectRoot,
73
+ encoding: 'utf-8',
74
+ stdio: ['ignore', 'pipe', 'ignore'],
75
+ });
76
+ return { ok: true, out };
77
+ }
78
+ catch {
79
+ return { ok: false, out: '' };
80
+ }
81
+ };
82
+ if (!git(['rev-parse', '--is-inside-work-tree']).ok)
83
+ return [];
84
+ if (git(['check-ignore', '-q', '--', rel]).ok)
85
+ return [
86
+ `${rel} is git-ignored, so a delete or forge would not show in git; add \`!owner-settings.json\` to .cleo/.gitignore (cleo upgrade) and commit it`,
87
+ ];
88
+ if (!git(['ls-files', '--error-unmatch', '--', rel]).ok)
89
+ return [
90
+ `${rel} is not tracked in git; commit it so a delete or forge shows in git status and PR diffs`,
91
+ ];
92
+ if (git(['status', '--porcelain', '--', rel]).out.trim() !== '')
93
+ return [
94
+ `${rel} has uncommitted changes; review and commit them (or restore it from git if you did not make them)`,
95
+ ];
96
+ return [];
97
+ }
98
+ /**
99
+ * What git history says about an earlier enrollment (T13829, review-2253):
100
+ * `present` when `.cleo/owner-settings.json` appears on any ref or in the
101
+ * reflog (a commit that deleted it, or a deleted ref, cannot hide it);
102
+ * `unavailable` when history cannot prove absence (a shallow clone, or git
103
+ * erroring inside a work tree); `none` only when it is provably absent or the
104
+ * project is not a git work tree at all.
105
+ */
106
+ function settingsHistory(projectRoot) {
107
+ const git = (args) => {
108
+ try {
109
+ return execFileSync('git', args, {
110
+ cwd: projectRoot,
111
+ encoding: 'utf-8',
112
+ stdio: ['ignore', 'pipe', 'ignore'],
113
+ }).trim();
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ };
119
+ if (git(['rev-parse', '--is-inside-work-tree']) !== 'true')
120
+ return 'none';
121
+ const shallow = git(['rev-parse', '--is-shallow-repository']);
122
+ if (shallow !== 'false')
123
+ return 'unavailable';
124
+ const log = git([
125
+ 'log',
126
+ '--all',
127
+ '--reflog',
128
+ '--format=%H',
129
+ '-n',
130
+ '1',
131
+ '--',
132
+ '.cleo/owner-settings.json',
133
+ ]);
134
+ if (log === null)
135
+ return 'unavailable';
136
+ return log === '' ? 'none' : 'present';
137
+ }
138
+ function settingsPath(projectRoot) {
139
+ return join(projectRoot, '.cleo', 'owner-settings.json');
140
+ }
141
+ function globalPath() {
142
+ return join(getCleoHome(), 'owner-enrollments.json');
143
+ }
144
+ /** The global record key: the portable project id, else the real path. */
145
+ function projectKey(projectRoot) {
146
+ const id = readPortableProjectId(projectRoot);
147
+ if (id.status === 'valid')
148
+ return id.projectId;
149
+ try {
150
+ return `path:${realpathSync(projectRoot)}`;
151
+ }
152
+ catch {
153
+ return `path:${projectRoot}`;
154
+ }
155
+ }
156
+ function fingerprintOf(pubkey) {
157
+ return createHash('sha256').update(pubkey).digest('hex').slice(0, 16);
158
+ }
159
+ /** The project file: absent, readable, or unreadable (garbage, no key). */
160
+ function readSettings(projectRoot) {
161
+ if (!existsSync(settingsPath(projectRoot)))
162
+ return { state: 'absent' };
163
+ try {
164
+ const parsed = JSON.parse(readFileSync(settingsPath(projectRoot), 'utf-8'));
165
+ if (typeof parsed.ownerPubkey !== 'string' || typeof parsed.salt !== 'string')
166
+ return { state: 'bad', why: 'it has no enrolled owner key' };
167
+ return {
168
+ state: 'read',
169
+ file: {
170
+ schemaVersion: 1,
171
+ ownerPubkey: parsed.ownerPubkey,
172
+ salt: parsed.salt,
173
+ settings: parsed.settings ?? {},
174
+ enrolledAt: parsed.enrolledAt ?? '',
175
+ sessionId: parsed.sessionId ?? null,
176
+ tty: parsed.tty ?? null,
177
+ parent: parsed.parent ?? null,
178
+ },
179
+ };
180
+ }
181
+ catch (err) {
182
+ return { state: 'bad', why: err instanceof Error ? err.message : String(err) };
183
+ }
184
+ }
185
+ function readGlobalAll() {
186
+ try {
187
+ const parsed = JSON.parse(readFileSync(globalPath(), 'utf-8'));
188
+ return parsed.projects ?? {};
189
+ }
190
+ catch {
191
+ return {};
192
+ }
193
+ }
194
+ function readGlobal(projectRoot) {
195
+ return readGlobalAll()[projectKey(projectRoot)] ?? null;
196
+ }
197
+ /** The exact bytes a seal signs. */
198
+ function sealPayload(key, value, setAt, salt, ownerPubkey) {
199
+ return JSON.stringify({ key, value, setAt, salt, ownerPubkey });
200
+ }
201
+ /** The owner's signing key: Ed25519 from scrypt(password, salt). */
202
+ async function ownerKey(password, salt) {
203
+ const { AgentIdentity } = await import('../identity/cleo-identity.js');
204
+ return AgentIdentity.fromSeed(new Uint8Array(scryptSync(password, Buffer.from(salt, 'hex'), 32, OWNER_SCRYPT_PARAMS)));
205
+ }
206
+ /** Best-effort terminal provenance of this process, for the enrollment record. */
207
+ function provenance(sessionId) {
208
+ const ps = (args) => {
209
+ try {
210
+ return (execFileSync('ps', args, {
211
+ encoding: 'utf-8',
212
+ stdio: ['ignore', 'pipe', 'ignore'],
213
+ }).trim() || null);
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ };
219
+ return {
220
+ enrolledAt: new Date().toISOString(),
221
+ sessionId,
222
+ tty: ps(['-o', 'tty=', '-p', String(process.pid)]),
223
+ parent: ps(['-o', 'comm=', '-p', String(process.ppid)]),
224
+ };
225
+ }
226
+ function audit(projectRoot, action, fields) {
227
+ appendOwnerOverrideAudit(projectRoot, {
228
+ timestamp: new Date().toISOString(),
229
+ taskId: '*',
230
+ gate: OVERRIDE_CAP_SETTING,
231
+ action,
232
+ agent_id: process.env['CLEO_AGENT_ID'] ?? 'unknown',
233
+ session_id: fields.sessionId,
234
+ token_validated: fields.ok === true,
235
+ tty_present: isTtyPresent(),
236
+ override_count: 0,
237
+ reason: fields.reason,
238
+ pid: process.pid,
239
+ setting: OVERRIDE_CAP_SETTING,
240
+ ...(fields.old !== undefined ? { old_value: fields.old } : {}),
241
+ ...(fields.next !== undefined ? { new_value: fields.next } : {}),
242
+ });
243
+ }
244
+ /**
245
+ * The project's owner state, judged without auditing: the sealed cap, or why
246
+ * stored state is ignored.
247
+ */
248
+ async function judgeOwnerState(projectRoot) {
249
+ const read = readSettings(projectRoot);
250
+ const global = readGlobal(projectRoot);
251
+ if (read.state === 'absent')
252
+ return global
253
+ ? {
254
+ kind: 'tampered',
255
+ why: `.cleo/owner-settings.json is missing, but the global store records owner key ${global.fingerprint} for this project (deleted? restore it from git, or the owner re-enrolls with \`${OWNER_ENROLL_COMMAND} --reset\`)`,
256
+ }
257
+ : { kind: 'none' };
258
+ if (read.state === 'bad')
259
+ return {
260
+ kind: 'tampered',
261
+ why: `.cleo/owner-settings.json is unreadable (${read.why}); restore it from git, or the owner re-enrolls with \`${OWNER_ENROLL_COMMAND} --reset\``,
262
+ };
263
+ const file = read.file;
264
+ const fp = fingerprintOf(file.ownerPubkey);
265
+ if (!global)
266
+ return {
267
+ kind: 'tampered',
268
+ why: `.cleo/owner-settings.json names owner key ${fp}, which the global store never enrolled for this project (forged or copied in?)`,
269
+ };
270
+ if (global.ownerPubkey !== file.ownerPubkey)
271
+ return {
272
+ kind: 'tampered',
273
+ why: `.cleo/owner-settings.json names owner key ${fp}, but the global store enrolled ${global.fingerprint} for this project`,
274
+ };
275
+ const entry = file.settings[OVERRIDE_CAP_SETTING];
276
+ if (!entry)
277
+ return { kind: 'ok', file, global, cap: null };
278
+ const { verifyAuditLine } = await import('../identity/cleo-identity.js');
279
+ const valid = Number.isInteger(entry.value) &&
280
+ entry.value >= 1 &&
281
+ entry.value <= MAX_OVERRIDE_CAP_PER_SESSION &&
282
+ typeof entry.sig === 'string' &&
283
+ (await verifyAuditLine(sealPayload(OVERRIDE_CAP_SETTING, entry.value, entry.setAt, file.salt, file.ownerPubkey), entry.sig, file.ownerPubkey));
284
+ if (!valid)
285
+ return {
286
+ kind: 'tampered',
287
+ why: `${OVERRIDE_CAP_SETTING} in .cleo/owner-settings.json is not sealed by the enrolled owner key (edited by hand?)`,
288
+ };
289
+ return { kind: 'ok', file, global, cap: entry.value };
290
+ }
291
+ /**
292
+ * The per-session override cap in force for a project: the owner's sealed
293
+ * `evidence.overrideCapPerSession`, or {@link DEFAULT_OVERRIDE_CAP_PER_SESSION}.
294
+ * Stored state that is missing, unanchored, mismatched or unsealed is ignored,
295
+ * warned about and audited (see the module doc).
296
+ *
297
+ * @param projectRoot - Project root.
298
+ * @returns The cap, its source, and a warning when stored state was ignored.
299
+ * @task T13787
300
+ */
301
+ export async function resolveOverrideCap(projectRoot) {
302
+ const state = await judgeOwnerState(projectRoot);
303
+ // T13829: an enrollment that replaced another without proof of the previous
304
+ // owner sets no cap until the owner confirms it.
305
+ if (state.kind === 'ok' && state.global.resetFrom) {
306
+ const warning = `owner key ${state.global.fingerprint} replaced an earlier enrollment without proof of the previous owner (${state.global.resetFrom}); ${OVERRIDE_CAP_SETTING} falls back to ${DEFAULT_OVERRIDE_CAP_PER_SESSION} until \`${OWNER_ENROLL_COMMAND} --confirm\`.`;
307
+ // Audited once per unconfirmed state, not on every read.
308
+ const stateKey = `${state.global.fingerprint}:${state.global.resetFrom}:${state.global.enrolledAt}`;
309
+ if (state.global.unconfirmedAudited !== stateKey) {
310
+ audit(projectRoot, 'owner-settings-unconfirmed', { sessionId: null, reason: warning });
311
+ await atomicWriteJson(globalPath(), {
312
+ schemaVersion: 1,
313
+ projects: {
314
+ ...readGlobalAll(),
315
+ [projectKey(projectRoot)]: { ...state.global, unconfirmedAudited: stateKey },
316
+ },
317
+ });
318
+ }
319
+ return { cap: DEFAULT_OVERRIDE_CAP_PER_SESSION, source: 'default', warning };
320
+ }
321
+ if (state.kind === 'ok' && state.cap !== null)
322
+ return { cap: state.cap, source: 'owner-setting' };
323
+ if (state.kind !== 'tampered')
324
+ return { cap: DEFAULT_OVERRIDE_CAP_PER_SESSION, source: 'default' };
325
+ const warning = `${state.why}; ${OVERRIDE_CAP_SETTING} falls back to ${DEFAULT_OVERRIDE_CAP_PER_SESSION}.`;
326
+ audit(projectRoot, 'owner-settings-invalid', { sessionId: null, reason: warning });
327
+ return { cap: DEFAULT_OVERRIDE_CAP_PER_SESSION, source: 'default', warning };
328
+ }
329
+ /**
330
+ * The project's owner enrollment for `cleo doctor`: none, enrolled (with
331
+ * fingerprint, when, which session and terminal), or tampered (why).
332
+ *
333
+ * @param projectRoot - Project root.
334
+ * @returns The status, with the cap in force.
335
+ * @task T13787
336
+ */
337
+ export async function ownerEnrollmentStatus(projectRoot) {
338
+ const state = await judgeOwnerState(projectRoot);
339
+ if (state.kind === 'none')
340
+ return {
341
+ status: 'none',
342
+ message: `No owner is enrolled for this project; ${OVERRIDE_CAP_SETTING} is ${DEFAULT_OVERRIDE_CAP_PER_SESSION}.`,
343
+ cap: DEFAULT_OVERRIDE_CAP_PER_SESSION,
344
+ warnings: [],
345
+ };
346
+ if (state.kind === 'tampered')
347
+ return {
348
+ status: 'tampered',
349
+ message: `Owner settings ignored: ${state.why}. ${OVERRIDE_CAP_SETTING} is ${DEFAULT_OVERRIDE_CAP_PER_SESSION}.`,
350
+ cap: DEFAULT_OVERRIDE_CAP_PER_SESSION,
351
+ warnings: [],
352
+ };
353
+ const g = state.global;
354
+ const cap = state.cap ?? DEFAULT_OVERRIDE_CAP_PER_SESSION;
355
+ if (g.resetFrom)
356
+ return {
357
+ status: 'unconfirmed',
358
+ message: `Owner key ${g.fingerprint} replaced an earlier enrollment WITHOUT proof of the previous owner ` +
359
+ `(${g.resetFrom}) at ${g.enrolledAt}, by session ${g.sessionId ?? 'unknown'} on tty ${g.tty ?? 'unknown'} ` +
360
+ `(parent ${g.parent ?? 'unknown'}); ${OVERRIDE_CAP_SETTING} stays ${DEFAULT_OVERRIDE_CAP_PER_SESSION} until confirmed. If you did this, confirm it with ` +
361
+ `\`${OWNER_ENROLL_COMMAND} --confirm\`; if not, restore .cleo/owner-settings.json from git and re-enroll. ` +
362
+ 'An agent that did the reset could also confirm it: this is visibility, not prevention.',
363
+ fingerprint: g.fingerprint,
364
+ enrolledAt: g.enrolledAt,
365
+ sessionId: g.sessionId,
366
+ tty: g.tty,
367
+ parent: g.parent,
368
+ cap: DEFAULT_OVERRIDE_CAP_PER_SESSION,
369
+ warnings: [],
370
+ };
371
+ const enrolledMs = Date.parse(g.enrolledAt);
372
+ const warnings = [
373
+ ...(Number.isFinite(enrolledMs) && Date.now() - enrolledMs < RECENT_ENROLLMENT_MS
374
+ ? [
375
+ `owner key ${g.fingerprint} was enrolled or reset recently (${g.enrolledAt}, session ${g.sessionId ?? 'unknown'}, tty ${g.tty ?? 'unknown'}, parent ${g.parent ?? 'unknown'}); confirm you did it`,
376
+ ]
377
+ : []),
378
+ ...(g.parent && PTY_WRAPPERS.test(g.parent)
379
+ ? [`the enrolling process ran under ${g.parent}, which fakes a terminal; confirm you did it`]
380
+ : []),
381
+ ...trackingWarnings(projectRoot),
382
+ ];
383
+ return {
384
+ warnings,
385
+ status: 'enrolled',
386
+ message: `Owner key ${g.fingerprint} enrolled ${g.enrolledAt} by session ${g.sessionId ?? 'unknown'} ` +
387
+ `on tty ${g.tty ?? 'unknown'} (parent ${g.parent ?? 'unknown'}); ${OVERRIDE_CAP_SETTING} = ${cap}. ` +
388
+ 'If you did not enroll it, run `cleo config owner-enroll --reset` yourself.',
389
+ fingerprint: g.fingerprint,
390
+ enrolledAt: g.enrolledAt,
391
+ sessionId: g.sessionId,
392
+ tty: g.tty,
393
+ parent: g.parent,
394
+ cap,
395
+ };
396
+ }
397
+ /** T1118 L4: role fence, TTY, and the password against the session's verifier. */
398
+ function ownerCheck(caller) {
399
+ const token = caller.session?.token ?? null;
400
+ if (token === null)
401
+ return {
402
+ ok: false,
403
+ code: BRANCH_LOCK_ERROR_CODES.E_OVERRIDE_TOKEN_INVALID,
404
+ message: 'The bound session has no owner-auth token.',
405
+ };
406
+ // Review-2253 round 2: only a scrypt verifier vouches for the owner; a
407
+ // legacy HMAC token is a cheap offline oracle and is never accepted here.
408
+ const verifier = token.startsWith(`${OWNER_SESSION_MARK}|`)
409
+ ? token.slice(OWNER_SESSION_MARK.length + 1)
410
+ : token;
411
+ if (!verifier.startsWith('scrypt$v1$'))
412
+ return {
413
+ ok: false,
414
+ code: BRANCH_LOCK_ERROR_CODES.E_OVERRIDE_TOKEN_INVALID,
415
+ message: 'The bound session has no scrypt owner-auth verifier (it predates T13787 or was started with --owner alone); start a new --owner-auth session.',
416
+ };
417
+ const auth = validateOwnerOverride({
418
+ sessionId: caller.session?.id ?? 'global',
419
+ skipTokenCheck: true,
420
+ });
421
+ if (!auth.allowed)
422
+ return {
423
+ ok: false,
424
+ code: auth.errorCode ?? 'E_OVERRIDE_REFUSED',
425
+ message: auth.errorMessage ?? 'Refused.',
426
+ };
427
+ if (!verifyOwnerAuthPassword(caller.password, token, caller.session?.name ?? 'session'))
428
+ return {
429
+ ok: false,
430
+ code: BRANCH_LOCK_ERROR_CODES.E_OVERRIDE_TOKEN_INVALID,
431
+ message: "The password does not match the bound session's owner-auth token.",
432
+ };
433
+ return { ok: true };
434
+ }
435
+ /**
436
+ * Refuse, and audit, a write of `evidence.overrideCapPerSession` that did not
437
+ * come through {@link setOwnerOverrideCap} — a plain config write (T13787).
438
+ *
439
+ * @param projectRoot - Project root.
440
+ * @param value - The value the write asked for.
441
+ * @returns The refusal message, naming the owner command.
442
+ * @task T13787
443
+ */
444
+ export async function refuseOverrideCapWrite(projectRoot, value) {
445
+ const previous = (await resolveOverrideCap(projectRoot)).cap;
446
+ const message = `${OVERRIDE_CAP_SETTING} is owner-only and is never written to config.json. Only the owner can change it, with \`${OVERRIDE_CAP_OWNER_COMMAND}\`: ${OWNER_HOW}.`;
447
+ audit(projectRoot, 'override-cap-set-refused', {
448
+ sessionId: null,
449
+ old: previous,
450
+ ...(typeof value === 'number' ? { next: value } : {}),
451
+ reason: 'plain config write refused',
452
+ });
453
+ return message;
454
+ }
455
+ /**
456
+ * Enroll the owner key for this project — explicitly, never as a side effect
457
+ * of a set (T13787 review). Refused, and audited, unless the caller passes
458
+ * T1118 L4; refused over an existing enrollment (project file or global
459
+ * record) unless `reset`, which is audited loudly and drops the old seals.
460
+ *
461
+ * @param projectRoot - Project root.
462
+ * @param caller - The password typed at the TTY and the bound session.
463
+ * @param opts - `reset`: replace an existing enrollment.
464
+ * @returns The new key's fingerprint, or why enrollment was refused.
465
+ * @task T13787
466
+ */
467
+ export async function enrollOwner(projectRoot, caller, opts = {}) {
468
+ const sessionId = caller.session?.id ?? null;
469
+ const refuse = (code, message) => {
470
+ audit(projectRoot, 'owner-enroll-refused', { sessionId, reason: message });
471
+ return { ok: false, code, message: `${message} Only the owner can enroll: ${OWNER_HOW}.` };
472
+ };
473
+ const check = ownerCheck(caller);
474
+ if (!check.ok)
475
+ return refuse(check.code, check.message);
476
+ const existing = readGlobal(projectRoot);
477
+ const fileThere = readSettings(projectRoot).state !== 'absent';
478
+ if ((existing || fileThere) && opts.reset !== true)
479
+ return refuse(
480
+ // @sync-invariant none:local-only owner enrollment state is device-local and git-tracked; refused before any write
481
+ 'E_OWNER_ALREADY_ENROLLED', `An owner is already enrolled for this project${existing ? ` (key ${existing.fingerprint})` : ''}; re-enrolling needs \`${OWNER_ENROLL_COMMAND} --reset\`.`);
482
+ // Review-2253 rounds 2-3: while the global record is intact, a reset must
483
+ // prove the old owner's password against it — even with the project file
484
+ // deleted. Only with both anchors gone (the global one outside the repo) is
485
+ // a reset password-free, and then it is recorded as unconfirmed.
486
+ let resetFrom;
487
+ if (opts.reset === true) {
488
+ if (existing) {
489
+ const read = readSettings(projectRoot);
490
+ const oldSalt = existing.salt ??
491
+ (read.state === 'read' && read.file.ownerPubkey === existing.ownerPubkey
492
+ ? read.file.salt
493
+ : undefined);
494
+ const proven = oldSalt !== undefined &&
495
+ (await ownerKey(caller.password, oldSalt)).pubkeyHex === existing.ownerPubkey;
496
+ if (!proven)
497
+ return refuse(
498
+ // @sync-invariant none:local-only owner enrollment state is device-local and git-tracked; refused before any write
499
+ 'E_OWNER_RESET_NOT_OWNER', `Resetting needs the current owner's password (key ${existing.fingerprint}, recorded in the global owner-enrollments.json). If it is lost, remove BOTH anchors deliberately (.cleo/owner-settings.json and this project's record in the global owner-enrollments.json, outside the repo) and enroll again.`);
500
+ }
501
+ else {
502
+ resetFrom = fileThere ? 'tampered' : 'fresh-after-removal';
503
+ }
504
+ }
505
+ else if (!existing && !fileThere) {
506
+ // Both anchors gone: git history decides whether a plain enroll replaces an
507
+ // owner it never proved (present), or cannot tell (unavailable).
508
+ const history = settingsHistory(projectRoot);
509
+ if (history === 'present')
510
+ resetFrom = 'fresh-after-removal';
511
+ else if (history === 'unavailable')
512
+ resetFrom = 'history-unavailable';
513
+ }
514
+ const salt = randomBytes(16).toString('hex');
515
+ const key = await ownerKey(caller.password, salt);
516
+ const prov = provenance(sessionId);
517
+ const fingerprint = fingerprintOf(key.pubkeyHex);
518
+ const file = {
519
+ schemaVersion: 1,
520
+ ownerPubkey: key.pubkeyHex,
521
+ salt,
522
+ settings: {},
523
+ ...prov,
524
+ };
525
+ await atomicWriteJson(settingsPath(projectRoot), file);
526
+ await atomicWriteJson(globalPath(), {
527
+ schemaVersion: 1,
528
+ projects: {
529
+ ...readGlobalAll(),
530
+ [projectKey(projectRoot)]: {
531
+ ownerPubkey: key.pubkeyHex,
532
+ fingerprint,
533
+ projectRoot,
534
+ salt,
535
+ ...prov,
536
+ ...(resetFrom ? { resetFrom } : {}),
537
+ },
538
+ },
539
+ });
540
+ audit(projectRoot, opts.reset === true ? 'owner-enroll-reset' : 'owner-enroll', {
541
+ sessionId,
542
+ ok: true,
543
+ reason: opts.reset === true
544
+ ? `owner key RESET to ${fingerprint}${existing ? ` (was ${existing.fingerprint}, old password proven)` : ` without proof of a previous owner (${resetFrom})`}; earlier seals dropped (tty ${prov.tty ?? 'unknown'}, parent ${prov.parent ?? 'unknown'})`
545
+ : `owner key ${fingerprint} enrolled (tty ${prov.tty ?? 'unknown'}, parent ${prov.parent ?? 'unknown'})`,
546
+ });
547
+ return { ok: true, fingerprint, reset: opts.reset === true };
548
+ }
549
+ /**
550
+ * Confirm an enrollment that replaced another without proof of the previous
551
+ * owner (review-2253 round 3): the owner proves the CURRENT key's password and
552
+ * the `resetFrom` mark that makes `cleo doctor` FAIL is cleared, audited. An
553
+ * agent that did the reset could confirm it too — visibility, not prevention.
554
+ *
555
+ * @param projectRoot - Project root.
556
+ * @param caller - The password typed at the TTY and the bound session.
557
+ * @returns The confirmed key's fingerprint, or why it was refused.
558
+ * @task T13787
559
+ */
560
+ export async function confirmOwnerEnrollment(projectRoot, caller) {
561
+ const sessionId = caller.session?.id ?? null;
562
+ const refuse = (code, message) => {
563
+ audit(projectRoot, 'owner-enroll-confirm-refused', { sessionId, reason: message });
564
+ return { ok: false, code, message: `${message} Only the owner can confirm: ${OWNER_HOW}.` };
565
+ };
566
+ const check = ownerCheck(caller);
567
+ if (!check.ok)
568
+ return refuse(check.code, check.message);
569
+ const state = await judgeOwnerState(projectRoot);
570
+ if (state.kind !== 'ok')
571
+ return refuse(
572
+ // @sync-invariant none:local-only owner enrollment state is device-local and git-tracked; refused before any write
573
+ 'E_OWNER_SETTINGS_TAMPERED', state.kind === 'tampered' ? `${state.why}.` : 'No owner is enrolled for this project.');
574
+ if ((await ownerKey(caller.password, state.file.salt)).pubkeyHex !== state.file.ownerPubkey)
575
+ return refuse(BRANCH_LOCK_ERROR_CODES.E_OVERRIDE_TOKEN_INVALID, 'The password does not match the owner key enrolled for this project.');
576
+ const { resetFrom: _cleared, ...rest } = state.global;
577
+ await atomicWriteJson(globalPath(), {
578
+ schemaVersion: 1,
579
+ projects: {
580
+ ...readGlobalAll(),
581
+ [projectKey(projectRoot)]: { ...rest, confirmedAt: new Date().toISOString() },
582
+ },
583
+ });
584
+ audit(projectRoot, 'owner-enroll-confirm', {
585
+ sessionId,
586
+ ok: true,
587
+ reason: `owner confirmed key ${state.global.fingerprint}${state.global.resetFrom ? ` (was reset from ${state.global.resetFrom})` : ''}`,
588
+ });
589
+ return { ok: true, fingerprint: state.global.fingerprint, reset: false };
590
+ }
591
+ /**
592
+ * Set `evidence.overrideCapPerSession` through the owner path: refused, and
593
+ * audited, unless the caller passes T1118 L4, the project is enrolled with its
594
+ * file and global record agreeing, and `password` derives the enrolled key.
595
+ * Never enrolls. Values above {@link MAX_OVERRIDE_CAP_PER_SESSION} are clamped.
596
+ *
597
+ * @param projectRoot - Project root.
598
+ * @param input - The requested value, the password typed at the TTY, and the
599
+ * bound session with its owner-auth verifier (null when it has none).
600
+ * @returns The new and previous cap, or why the set was refused.
601
+ * @task T13787
602
+ */
603
+ export async function setOwnerOverrideCap(projectRoot, input) {
604
+ const previous = (await resolveOverrideCap(projectRoot)).cap;
605
+ const sessionId = input.session?.id ?? null;
606
+ const refuse = (code, message) => {
607
+ audit(projectRoot, 'override-cap-set-refused', {
608
+ sessionId,
609
+ old: previous,
610
+ next: input.value,
611
+ reason: message,
612
+ });
613
+ return {
614
+ ok: false,
615
+ code,
616
+ message: `${message} Only the owner can change ${OVERRIDE_CAP_SETTING}: ${OWNER_HOW}.`,
617
+ };
618
+ };
619
+ if (!Number.isInteger(input.value) || input.value < 1)
620
+ // @sync-invariant none:input-shape the requested cap is not a positive integer
621
+ return refuse('E_VALIDATION', `${OVERRIDE_CAP_SETTING} must be a positive integer.`);
622
+ const check = ownerCheck(input);
623
+ if (!check.ok)
624
+ return refuse(check.code, check.message);
625
+ const read = readSettings(projectRoot);
626
+ const global = readGlobal(projectRoot);
627
+ if (read.state === 'absent' && !global)
628
+ return refuse(
629
+ // @sync-invariant none:local-only owner enrollment state is device-local and git-tracked; refused before any write
630
+ 'E_OWNER_NOT_ENROLLED', `No owner is enrolled for this project; the owner enrolls first with \`${OWNER_ENROLL_COMMAND}\`.`);
631
+ const state = await judgeOwnerState(projectRoot);
632
+ if (state.kind !== 'ok')
633
+ return refuse(
634
+ // @sync-invariant none:local-only owner enrollment state is device-local and git-tracked; refused before any write
635
+ 'E_OWNER_SETTINGS_TAMPERED', `${state.kind === 'tampered' ? state.why : 'owner settings are not usable'}.`);
636
+ const file = state.file;
637
+ const key = await ownerKey(input.password, file.salt);
638
+ if (key.pubkeyHex !== file.ownerPubkey)
639
+ return refuse(BRANCH_LOCK_ERROR_CODES.E_OVERRIDE_TOKEN_INVALID, 'The password does not match the owner key enrolled for this project.');
640
+ const value = Math.min(input.value, MAX_OVERRIDE_CAP_PER_SESSION);
641
+ const setAt = new Date().toISOString();
642
+ const { signAuditLine } = await import('../identity/cleo-identity.js');
643
+ const { sig } = await signAuditLine(key, sealPayload(OVERRIDE_CAP_SETTING, value, setAt, file.salt, file.ownerPubkey));
644
+ await atomicWriteJson(settingsPath(projectRoot), {
645
+ ...file,
646
+ settings: { ...file.settings, [OVERRIDE_CAP_SETTING]: { value, setAt, sig } },
647
+ });
648
+ audit(projectRoot, 'override-cap-set', {
649
+ sessionId,
650
+ old: previous,
651
+ next: value,
652
+ ok: true,
653
+ reason: 'owner set',
654
+ });
655
+ return { ok: true, value, previous, clamped: value !== input.value };
656
+ }
657
+ //# sourceMappingURL=override-cap-setting.js.map