@su-record/vibe 4.2.34 → 4.2.36

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/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/dist/cli/common.js +4 -1
  5. package/dist/cli/common.js.map +1 -1
  6. package/dist/cli/heldout.js +10 -4
  7. package/dist/cli/heldout.js.map +1 -1
  8. package/dist/cli/human.js +8 -4
  9. package/dist/cli/human.js.map +1 -1
  10. package/dist/cli/internal.js +37 -24
  11. package/dist/cli/internal.js.map +1 -1
  12. package/dist/cli/ledger.js +67 -0
  13. package/dist/cli/ledger.js.map +1 -0
  14. package/dist/cli/map.js +5 -3
  15. package/dist/cli/map.js.map +1 -1
  16. package/dist/cli/memory.js +104 -74
  17. package/dist/cli/memory.js.map +1 -1
  18. package/dist/cli/risks.js +20 -3
  19. package/dist/cli/risks.js.map +1 -1
  20. package/dist/cli/session.js +4 -2
  21. package/dist/cli/session.js.map +1 -1
  22. package/dist/cli/setup.js +44 -13
  23. package/dist/cli/setup.js.map +1 -1
  24. package/dist/cli/verification.js +1 -1
  25. package/dist/cli/verification.js.map +1 -1
  26. package/dist/cli/work.js +75 -22
  27. package/dist/cli/work.js.map +1 -1
  28. package/dist/cli.js +18 -6
  29. package/dist/cli.js.map +1 -1
  30. package/dist/core/analyze.js +21 -7
  31. package/dist/core/analyze.js.map +1 -1
  32. package/dist/core/artifact-paths.js +14 -3
  33. package/dist/core/artifact-paths.js.map +1 -1
  34. package/dist/core/capabilities.js +69 -15
  35. package/dist/core/capabilities.js.map +1 -1
  36. package/dist/core/check-authorization.js +37 -0
  37. package/dist/core/check-authorization.js.map +1 -0
  38. package/dist/core/check-process.js +12 -4
  39. package/dist/core/check-process.js.map +1 -1
  40. package/dist/core/check-report.js +66 -0
  41. package/dist/core/check-report.js.map +1 -0
  42. package/dist/core/check-selection.js +4 -2
  43. package/dist/core/check-selection.js.map +1 -1
  44. package/dist/core/check.js +66 -130
  45. package/dist/core/check.js.map +1 -1
  46. package/dist/core/checks/changed.js +20 -6
  47. package/dist/core/checks/changed.js.map +1 -1
  48. package/dist/core/checks/eval.js +35 -10
  49. package/dist/core/checks/eval.js.map +1 -1
  50. package/dist/core/checks/file.js +26 -23
  51. package/dist/core/checks/file.js.map +1 -1
  52. package/dist/core/checks/fileRules.js +42 -16
  53. package/dist/core/checks/fileRules.js.map +1 -1
  54. package/dist/core/checks/http.js +35 -34
  55. package/dist/core/checks/http.js.map +1 -1
  56. package/dist/core/checks/packs.js +1 -1
  57. package/dist/core/checks/packs.js.map +1 -1
  58. package/dist/core/checks/review.js +75 -40
  59. package/dist/core/checks/review.js.map +1 -1
  60. package/dist/core/checks/run.js +5 -2
  61. package/dist/core/checks/run.js.map +1 -1
  62. package/dist/core/checks/schema.js +25 -0
  63. package/dist/core/checks/schema.js.map +1 -0
  64. package/dist/core/checks/source.js +11 -6
  65. package/dist/core/checks/source.js.map +1 -1
  66. package/dist/core/config.js +9 -2
  67. package/dist/core/config.js.map +1 -1
  68. package/dist/core/consent.js +24 -16
  69. package/dist/core/consent.js.map +1 -1
  70. package/dist/core/context-notes.js +15 -7
  71. package/dist/core/context-notes.js.map +1 -1
  72. package/dist/core/context.js +24 -12
  73. package/dist/core/context.js.map +1 -1
  74. package/dist/core/conventions.js +22 -9
  75. package/dist/core/conventions.js.map +1 -1
  76. package/dist/core/docs/cfb.js +81 -45
  77. package/dist/core/docs/cfb.js.map +1 -1
  78. package/dist/core/docs/html.js +14 -3
  79. package/dist/core/docs/html.js.map +1 -1
  80. package/dist/core/docs/hwp.js +44 -22
  81. package/dist/core/docs/hwp.js.map +1 -1
  82. package/dist/core/docs/office.js +20 -10
  83. package/dist/core/docs/office.js.map +1 -1
  84. package/dist/core/docs/pdf.js +10 -4
  85. package/dist/core/docs/pdf.js.map +1 -1
  86. package/dist/core/docs/read.js +32 -24
  87. package/dist/core/docs/read.js.map +1 -1
  88. package/dist/core/docs/zip.js +32 -15
  89. package/dist/core/docs/zip.js.map +1 -1
  90. package/dist/core/evidence.js +17 -7
  91. package/dist/core/evidence.js.map +1 -1
  92. package/dist/core/failure-message.js +35 -13
  93. package/dist/core/failure-message.js.map +1 -1
  94. package/dist/core/failure.js +49 -14
  95. package/dist/core/failure.js.map +1 -1
  96. package/dist/core/github.js +3 -1
  97. package/dist/core/github.js.map +1 -1
  98. package/dist/core/handoff.js +20 -7
  99. package/dist/core/handoff.js.map +1 -1
  100. package/dist/core/heldout-run.js +6 -3
  101. package/dist/core/heldout-run.js.map +1 -1
  102. package/dist/core/heldout.js +24 -13
  103. package/dist/core/heldout.js.map +1 -1
  104. package/dist/core/inbox.js +3 -1
  105. package/dist/core/inbox.js.map +1 -1
  106. package/dist/core/inputs.js +76 -0
  107. package/dist/core/inputs.js.map +1 -0
  108. package/dist/core/inspect.js +61 -20
  109. package/dist/core/inspect.js.map +1 -1
  110. package/dist/core/intent.js +50 -12
  111. package/dist/core/intent.js.map +1 -1
  112. package/dist/core/knowledge-find.js +31 -12
  113. package/dist/core/knowledge-find.js.map +1 -1
  114. package/dist/core/knowledge.js +22 -12
  115. package/dist/core/knowledge.js.map +1 -1
  116. package/dist/core/lang.js +5 -2
  117. package/dist/core/lang.js.map +1 -1
  118. package/dist/core/ledger.js +44 -20
  119. package/dist/core/ledger.js.map +1 -1
  120. package/dist/core/locations.js +12 -0
  121. package/dist/core/locations.js.map +1 -0
  122. package/dist/core/map/imports.js +20 -9
  123. package/dist/core/map/imports.js.map +1 -1
  124. package/dist/core/map/index.js +28 -13
  125. package/dist/core/map/index.js.map +1 -1
  126. package/dist/core/map/symbols.js +27 -12
  127. package/dist/core/map/symbols.js.map +1 -1
  128. package/dist/core/output-capture.js +7 -4
  129. package/dist/core/output-capture.js.map +1 -1
  130. package/dist/core/paths.js +2 -1
  131. package/dist/core/paths.js.map +1 -1
  132. package/dist/core/performance.js +43 -17
  133. package/dist/core/performance.js.map +1 -1
  134. package/dist/core/profile.js +6 -3
  135. package/dist/core/profile.js.map +1 -1
  136. package/dist/core/reader.js +32 -16
  137. package/dist/core/reader.js.map +1 -1
  138. package/dist/core/readerSession.js +34 -14
  139. package/dist/core/readerSession.js.map +1 -1
  140. package/dist/core/recent.js +20 -6
  141. package/dist/core/recent.js.map +1 -1
  142. package/dist/core/regress.js +21 -12
  143. package/dist/core/regress.js.map +1 -1
  144. package/dist/core/repair.js +22 -4
  145. package/dist/core/repair.js.map +1 -1
  146. package/dist/core/research-queries.js +85 -0
  147. package/dist/core/research-queries.js.map +1 -0
  148. package/dist/core/research.js +89 -88
  149. package/dist/core/research.js.map +1 -1
  150. package/dist/core/results.js +89 -0
  151. package/dist/core/results.js.map +1 -0
  152. package/dist/core/risk-signals.js +55 -16
  153. package/dist/core/risk-signals.js.map +1 -1
  154. package/dist/core/run-id.js +10 -6
  155. package/dist/core/run-id.js.map +1 -1
  156. package/dist/core/scenarios.js +97 -57
  157. package/dist/core/scenarios.js.map +1 -1
  158. package/dist/core/session.js +3 -2
  159. package/dist/core/session.js.map +1 -1
  160. package/dist/core/size-gate.js +57 -0
  161. package/dist/core/size-gate.js.map +1 -0
  162. package/dist/core/size.js +32 -10
  163. package/dist/core/size.js.map +1 -1
  164. package/dist/core/skills.js +66 -28
  165. package/dist/core/skills.js.map +1 -1
  166. package/dist/core/skillsmp.js +26 -12
  167. package/dist/core/skillsmp.js.map +1 -1
  168. package/dist/core/source-basis.js +21 -9
  169. package/dist/core/source-basis.js.map +1 -1
  170. package/dist/core/status.js +26 -13
  171. package/dist/core/status.js.map +1 -1
  172. package/dist/core/table.js +8 -1
  173. package/dist/core/table.js.map +1 -1
  174. package/dist/core/tokens.js +20 -10
  175. package/dist/core/tokens.js.map +1 -1
  176. package/dist/core/tree.js +26 -16
  177. package/dist/core/tree.js.map +1 -1
  178. package/dist/core/verification-plan.js +26 -10
  179. package/dist/core/verification-plan.js.map +1 -1
  180. package/dist/core/verification.js +14 -7
  181. package/dist/core/verification.js.map +1 -1
  182. package/dist/core/view.js +93 -44
  183. package/dist/core/view.js.map +1 -1
  184. package/dist/install/global.js +37 -18
  185. package/dist/install/global.js.map +1 -1
  186. package/dist/install/hooks.js +17 -8
  187. package/dist/install/hooks.js.map +1 -1
  188. package/dist/install/manifest.js +11 -0
  189. package/dist/install/manifest.js.map +1 -0
  190. package/dist/install/mcpb.js +52 -23
  191. package/dist/install/mcpb.js.map +1 -1
  192. package/dist/install/plugin.js +73 -33
  193. package/dist/install/plugin.js.map +1 -1
  194. package/dist/install/project.js +8 -7
  195. package/dist/install/project.js.map +1 -1
  196. package/dist/install/register.js +83 -48
  197. package/dist/install/register.js.map +1 -1
  198. package/dist/install/tree.js +27 -11
  199. package/dist/install/tree.js.map +1 -1
  200. package/dist/install/update.js +46 -20
  201. package/dist/install/update.js.map +1 -1
  202. package/hooks/code-map.cjs +22 -9
  203. package/hooks/file-size.cjs +83 -0
  204. package/hooks/journal.cjs +28 -18
  205. package/hooks/knowledge-auto.cjs +55 -21
  206. package/hooks/knowledge-journal.cjs +47 -20
  207. package/hooks/knowledge-projects.cjs +50 -21
  208. package/hooks/knowledge-recall.cjs +16 -6
  209. package/hooks/knowledge-upkeep.cjs +22 -5
  210. package/hooks/locations.cjs +35 -0
  211. package/hooks/notify.js +52 -24
  212. package/hooks/obsidian.cjs +17 -7
  213. package/hooks/private-store.cjs +25 -10
  214. package/hooks/retest.cjs +39 -16
  215. package/hooks/session-context.cjs +30 -13
  216. package/hooks/session-files.cjs +44 -21
  217. package/hooks/session-repair.cjs +26 -11
  218. package/hooks/session-state.cjs +78 -38
  219. package/hooks/session.js +19 -8
  220. package/hooks/slice-command.cjs +38 -17
  221. package/hooks/slice-guard.cjs +20 -6
  222. package/hooks/slice-store.cjs +25 -12
  223. package/internal/guides/code.md +17 -0
  224. package/mcpb/server/index.js +40 -12
  225. package/package.json +1 -1
package/hooks/notify.js CHANGED
@@ -19,12 +19,28 @@ import path from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import codeMap from './code-map.cjs';
21
21
  import knowledgeAuto from './knowledge-auto.cjs';
22
+ import locations from './locations.cjs';
22
23
  import knowledgeJournal from './knowledge-journal.cjs';
23
24
  import retest from './retest.cjs';
24
25
  import sessionRuntime from './session-state.cjs';
25
26
  import sessionFiles from './session-files.cjs';
26
27
  import sliceRuntime from './slice-guard.cjs';
27
28
 
29
+ const { claudeSettings } = locations;
30
+
31
+ const NOTIFY_HOOK = 'hooks/notify.js';
32
+ const SETTINGS_MAX_BYTES = 64 * 1024;
33
+ const PROJECT_DIR = '.vibe';
34
+ const STATE_TIMEOUT_MS = 15000;
35
+ // An authorize record counts for this long.
36
+ const MS_PER_MINUTE = 60 * 1000;
37
+ const AUTHORIZE_WINDOW_MS = 10 * MS_PER_MINUTE;
38
+ const DEFAULT_READ_ADVISE_LINES = sliceRuntime.READ_ADVISE_LINES;
39
+ // Exit code that stops a tool call in Claude Code.
40
+ const BLOCK_EXIT = 2;
41
+ // Hook modes that run in a project with or without a .vibe folder.
42
+ const ANYWHERE_MODES = ['stop', 'session', 'pre'];
43
+
28
44
  const mode = process.argv[2] || 'post';
29
45
  const asPlugin = process.argv.includes('--plugin');
30
46
  const personal = process.argv.includes('--personal');
@@ -38,11 +54,11 @@ if (asPlugin) {
38
54
  // The reader refuses a link anywhere in a path; a home under one (macOS /var → /private/var) is the person's own.
39
55
  let home = process.env.VIBE_HOME_DIR || os.homedir();
40
56
  try { home = fs.realpathSync(home); } catch { /* a missing home has no settings to read */ }
41
- for (const file of [path.join(home, '.claude', 'settings.json')]) {
57
+ for (const file of [claudeSettings(home)]) {
42
58
  try {
43
- const settings = JSON.parse(sessionFiles.optional(file, 65536) ?? '{}');
59
+ const settings = JSON.parse(sessionFiles.optional(file, SETTINGS_MAX_BYTES) ?? '{}');
44
60
  const owned = Object.values(settings.hooks ?? {}).some(entries => entries.some(entry =>
45
- entry.hooks?.some(hook => hook.command?.replaceAll('\\', '/').includes('hooks/notify.js'))));
61
+ entry.hooks?.some(hook => hook.command?.replaceAll('\\', '/').includes(NOTIFY_HOOK))));
46
62
  if (owned) process.exit(0);
47
63
  } catch {
48
64
  /* no such file — keep going */
@@ -58,19 +74,17 @@ function readPayload() {
58
74
  catch { return { session_id: 'invalid payload' }; }
59
75
  }
60
76
 
61
- function structuralSession(payload) {
62
- const view = sessionRuntime.sessionStatus(payload);
63
- return sessionRuntime.message(view, view.status === 'bound' ? 'session status only; run vibe state for the procedure' : `${view.status}; use explicit vibe session bind in the intended worktree`);
64
- }
77
+ const structuralSession = payload => sessionRuntime.structuralNote(sessionRuntime.sessionStatus(payload));
65
78
  function sliceClient() {
66
79
  const declared = process.env.VIBE_CLIENT;
67
80
  if (declared) return ['codex', 'chatgpt'].includes(declared) ? 'codex' : 'claude';
68
81
  return process.env.CODEX_THREAD_ID && !process.env.CLAUDE_SESSION_ID ? 'codex' : 'claude';
69
82
  }
70
83
 
71
- function emitContext(text) {
72
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: mode === 'post' ? 'PostToolUse' : 'PreToolUse', additionalContext: text } })}\n`);
84
+ function emit(hookEventName, additionalContext) {
85
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } })}\n`);
73
86
  }
87
+ const emitContext = text => emit(mode === 'post' ? 'PostToolUse' : 'PreToolUse', text);
74
88
 
75
89
  // The same actions the check gate names (src/core/checks/mutation.ts), so a blocked tool call and a blocked
76
90
  // check ask for the same `vibe authorize --action`; `send` is the hook's own. A command that only reads —
@@ -91,10 +105,12 @@ const IRREVERSIBLE = [
91
105
  ];
92
106
  const READS_ONLY = /^\s*(?:grep|rg|cat|head|tail|less|ls|find|wc|echo|printf|sed\s+-n|git\s+(?:log|diff|show|status|grep|blame|branch|ls-files))\b/;
93
107
 
108
+ const hasProjectDir = () => fs.existsSync(path.join(root, PROJECT_DIR));
109
+
94
110
  function recentAuthorize(action) {
95
111
  try {
96
- const lines = fs.readFileSync(path.join(root, '.vibe', 'ledger.jsonl'), 'utf-8').trim().split('\n');
97
- const cutoff = Date.now() - 10 * 60 * 1000;
112
+ const lines = fs.readFileSync(path.join(root, PROJECT_DIR, 'ledger.jsonl'), 'utf-8').trim().split('\n');
113
+ const cutoff = Date.now() - AUTHORIZE_WINDOW_MS;
98
114
  return lines.some((line) => {
99
115
  try {
100
116
  const e = JSON.parse(line);
@@ -108,16 +124,25 @@ function recentAuthorize(action) {
108
124
  }
109
125
  }
110
126
 
111
- if (!['stop', 'session', 'pre'].includes(mode) && !fs.existsSync(path.join(root, '.vibe'))) process.exit(0);
127
+ if (!ANYWHERE_MODES.includes(mode) && !hasProjectDir()) process.exit(0);
128
+
129
+ function irreversibleNotice(action, blocking) {
130
+ return [
131
+ `[vibe] "${action}" is irreversible and no authorize record exists in the last ${AUTHORIZE_WINDOW_MS / MS_PER_MINUTE} minutes —`,
132
+ `${blocking ? 'blocked: ' : ''}get a human token with \`vibe ask --needs authorize:${action}\``,
133
+ 'and run `vibe authorize` first, as its own command: the gate reads the ledger before this command runs,',
134
+ 'so an authorize chained in front of the action is not seen',
135
+ ].join(' ');
136
+ }
112
137
 
113
138
  function tokenPolicy() {
114
139
  try {
115
- return JSON.parse(fs.readFileSync(path.join(root, '.vibe', 'config.json'), 'utf-8')).tokens || 'off';
140
+ return JSON.parse(fs.readFileSync(path.join(root, PROJECT_DIR, 'config.json'), 'utf-8')).tokens || 'off';
116
141
  } catch {
117
142
  return 'off';
118
143
  }
119
144
  }
120
- const READ_ADVISE_LINES = Number(process.env.VIBE_READ_ADVISE_LINES || 400);
145
+ const READ_ADVISE_LINES = Number(process.env.VIBE_READ_ADVISE_LINES || DEFAULT_READ_ADVISE_LINES);
121
146
 
122
147
  function countLines(file) {
123
148
  try {
@@ -134,7 +159,10 @@ function adviseRead(payload) {
134
159
  const lines = countLines(path.resolve(root, file));
135
160
  if (lines <= READ_ADVISE_LINES) return;
136
161
  const shown = (path.isAbsolute(file) ? path.relative(root, file) || file : file).split(path.sep).join('/');
137
- emitContext(`[vibe] ${shown} is ${lines} lines — when it only has to be understood, not edited or debugged, \`vibe read ${shown} --ask "<question>"\` lets a low-reasoning model read it and returns the answer with line numbers`);
162
+ emitContext([
163
+ `[vibe] ${shown} is ${lines} lines — when it only has to be understood, not edited or debugged,`,
164
+ `\`vibe read ${shown} --ask "<question>"\` lets a low-reasoning model read it and returns the answer with line numbers`,
165
+ ].join(' '));
138
166
  }
139
167
 
140
168
  const cli = path.join(here, '..', 'dist', 'cli.js');
@@ -158,14 +186,14 @@ if (mode === 'stop') {
158
186
  if (mode === 'session') {
159
187
  const payload = readPayload();
160
188
  const recall = knowledgeAuto.start(payload);
161
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: [structuralSession(payload), recall, codeMap.codeMap(root)].filter(Boolean).join('\n') } })}\n`);
189
+ emit('SessionStart', [structuralSession(payload), recall, codeMap.codeMap(root)].filter(Boolean).join('\n'));
162
190
  process.exit(0);
163
191
  }
164
192
 
165
193
  if (mode === 'pre') {
166
194
  const payload = readPayload();
167
195
  if (payload.tool_name === 'Read') {
168
- if (fs.existsSync(path.join(root, '.vibe'))) adviseRead(payload);
196
+ if (hasProjectDir()) adviseRead(payload);
169
197
  process.exit(0);
170
198
  }
171
199
  const raw = payload.tool_input && payload.tool_input.command;
@@ -176,13 +204,13 @@ if (mode === 'pre') {
176
204
  const leave = (code) => { if (notes.length) emitContext(notes.join('\n')); process.exit(code); };
177
205
  let gate = null;
178
206
  try { gate = retest.before(payload, process.env.VIBE_HOME_DIR || os.homedir()); } catch { /* the command runs */ }
179
- if (gate?.decision === 'block') { process.stderr.write(`${gate.message}\n`); process.exit(2); }
207
+ if (gate?.decision === 'block') { process.stderr.write(`${gate.message}\n`); process.exit(BLOCK_EXIT); }
180
208
  if (gate?.decision === 'warn') notes.push(gate.message);
181
209
  if (personal && tokenPolicy() === 'off') leave(0);
182
210
  const slice = personal ? { decision: 'allow' } : sliceRuntime.sliceGuard(root, command, sliceClient(), cwd);
183
- if (slice.decision === 'block') { process.stderr.write(`${slice.message}\n`); process.exit(2); }
211
+ if (slice.decision === 'block') { process.stderr.write(`${slice.message}\n`); process.exit(BLOCK_EXIT); }
184
212
  if (slice.decision === 'warn') notes.push(slice.message);
185
- if (!fs.existsSync(path.join(root, '.vibe'))) leave(0);
213
+ if (!hasProjectDir()) leave(0);
186
214
  // Every segment is judged — `echo x && git push`, `ls; rm -rf build`, `cat x | git push` carry the action in a later one
187
215
  // Vibe gates its own execution; its approval and inbox commands must remain reachable.
188
216
  const segments = command.split(/&&|\|\||;|\||\n/).map((s) => s.trim()).filter((s) => s && !READS_ONLY.test(s) && !/^vibe(?:\s|$)/.test(s));
@@ -190,8 +218,8 @@ if (mode === 'pre') {
190
218
  for (const [action, re] of IRREVERSIBLE) {
191
219
  if (segments.some((s) => re.test(s)) && !recentAuthorize(action)) {
192
220
  const blocking = tokenPolicy() !== 'off';
193
- process.stderr.write(`[vibe] "${action}" is irreversible and no authorize record exists in the last 10 minutes — ${blocking ? 'blocked: ' : ''}get a human token with \`vibe ask --needs authorize:${action}\` and run \`vibe authorize\` first, as its own command: the gate reads the ledger before this command runs, so an authorize chained in front of the action is not seen\n`);
194
- if (blocking) process.exit(2);
221
+ process.stderr.write(`${irreversibleNotice(action, blocking)}\n`);
222
+ if (blocking) process.exit(BLOCK_EXIT);
195
223
  leave(0);
196
224
  }
197
225
  }
@@ -200,8 +228,8 @@ if (mode === 'pre') {
200
228
 
201
229
  // Inside the npm package dist/cli.js sits next to us; inside a plugin tree it does not, so fall back to `vibe` on PATH.
202
230
  const result = fs.existsSync(cli)
203
- ? spawnSync(process.execPath, [cli, 'state', '--json'], { cwd: root, encoding: 'utf-8', timeout: 15000 })
204
- : spawnSync('vibe', ['state', '--json'], { cwd: root, encoding: 'utf-8', timeout: 15000, shell: process.platform === 'win32' });
231
+ ? spawnSync(process.execPath, [cli, 'state', '--json'], { cwd: root, encoding: 'utf-8', timeout: STATE_TIMEOUT_MS })
232
+ : spawnSync('vibe', ['state', '--json'], { cwd: root, encoding: 'utf-8', timeout: STATE_TIMEOUT_MS, shell: process.platform === 'win32' });
205
233
  if (result.status !== 0 || !result.stdout) process.exit(0);
206
234
  try {
207
235
  const view = JSON.parse(result.stdout);
@@ -4,16 +4,22 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { createHash, randomBytes } = require('node:crypto');
6
6
 
7
+ const REGISTRY_FILE = 'obsidian.json';
8
+ const VAULT_ID_CHARS = 16;
9
+ const TEMP_SUFFIX_BYTES = 6;
10
+
7
11
  function readJson(file) {
8
12
  try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
9
13
  }
10
14
 
11
15
  /** Where Obsidian keeps its vault list on each platform, including the Flatpak build on Linux. */
12
16
  function obsidianRegistries(home, platform = process.platform, env = process.env) {
13
- if (platform === 'darwin') return [path.join(home, 'Library', 'Application Support', 'obsidian', 'obsidian.json')];
14
- if (platform === 'win32') return [path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'obsidian', 'obsidian.json')];
15
- return [path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'obsidian', 'obsidian.json'),
16
- path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', 'obsidian', 'obsidian.json')];
17
+ if (platform === 'darwin') return [path.join(home, 'Library', 'Application Support', 'obsidian', REGISTRY_FILE)];
18
+ if (platform === 'win32') return [path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'obsidian', REGISTRY_FILE)];
19
+ return [
20
+ path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'obsidian', REGISTRY_FILE),
21
+ path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', 'obsidian', REGISTRY_FILE),
22
+ ];
17
23
  }
18
24
 
19
25
  /** Registered vaults that still exist, by real path: the open one first, then the most recently used. */
@@ -29,7 +35,11 @@ function obsidianVaults(home, platform, env) {
29
35
  return [...found.values()].sort((a, b) => Number(b.open) - Number(a.open) || b.ts - a.ts);
30
36
  }
31
37
 
32
- const CLOUD = [['Mobile Documents', 'iCloud'], ['iCloud', 'iCloud'], ['Dropbox', 'Dropbox'], ['OneDrive', 'OneDrive'], ['Google Drive', 'Google Drive'], ['CloudStorage', 'a cloud drive']];
38
+ // A folder name in a path, and the service whose sync folder carries it.
39
+ const CLOUD = [
40
+ ['Mobile Documents', 'iCloud'], ['iCloud', 'iCloud'], ['Dropbox', 'Dropbox'], ['OneDrive', 'OneDrive'],
41
+ ['Google Drive', 'Google Drive'], ['CloudStorage', 'a cloud drive'],
42
+ ];
33
43
 
34
44
  /** The service that copies this folder off the computer, when its path or Obsidian's own Sync plugin shows one. */
35
45
  function syncedBy(folder) {
@@ -76,9 +86,9 @@ function connectVault(folder, home, platform, env) {
76
86
  if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return 'waiting';
77
87
  const real = fs.realpathSync(folder);
78
88
  if (obsidianVaults(home, platform, env).some(v => real === v.path || real.startsWith(`${v.path}${path.sep}`))) return 'in-vault';
79
- const id = createHash('sha256').update(real).digest('hex').slice(0, 16);
89
+ const id = createHash('sha256').update(real).digest('hex').slice(0, VAULT_ID_CHARS);
80
90
  const next = { ...doc, vaults: { ...(doc.vaults ?? {}), [id]: { path: real, ts: Date.now() } } };
81
- const temporary = `${registry}.${randomBytes(6).toString('hex')}.tmp`;
91
+ const temporary = `${registry}.${randomBytes(TEMP_SUFFIX_BYTES).toString('hex')}.tmp`;
82
92
  fs.writeFileSync(temporary, JSON.stringify(next));
83
93
  fs.renameSync(temporary, registry);
84
94
  return 'connected';
@@ -3,31 +3,44 @@ const os = require('node:os');
3
3
  const path = require('node:path');
4
4
  const { randomBytes } = require('node:crypto');
5
5
 
6
+ const STORE_DIR = '.vibe-runtime';
7
+ const DIRECTORY_MODE = 0o700;
8
+ const FILE_MODE = 0o600;
9
+ // Any group or other permission bit.
10
+ const OTHERS_MASK = 0o077;
11
+ const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
12
+ const TEMP_SUFFIX_BYTES = 12;
13
+ const ENTRY_NAME = /^[a-z0-9.-]+$/i;
14
+ const LINKED_ENTRY = 'local store entry must be a real private directory or regular file, never linked';
15
+ const PERMISSIONS = 'local store permissions must be owner-private (0700 directory, 0600 file)';
16
+ const CHANGED_ENTRY = 'local store entry changed or exceeds its read limit';
17
+
6
18
  function inside(parent, file) {
7
19
  const relative = path.relative(parent, file);
8
20
  return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
9
21
  }
10
22
  function privateEntry(stat, directory) {
11
- if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile()) || (!directory && stat.nlink !== 1)) throw new Error('local store entry must be a real private directory or regular file, never linked');
12
- if (process.platform !== 'win32' && (stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)) throw new Error('local store permissions must be owner-private (0700 directory, 0600 file)');
23
+ const wrongKind = directory ? !stat.isDirectory() : !stat.isFile();
24
+ if (stat.isSymbolicLink() || wrongKind || (!directory && stat.nlink !== 1)) throw new Error(LINKED_ENTRY);
25
+ if (process.platform !== 'win32' && (stat.uid !== process.getuid?.() || (stat.mode & OTHERS_MASK) !== 0)) throw new Error(PERMISSIONS);
13
26
  return stat;
14
27
  }
15
28
  function privateStat(file, directory) { return privateEntry(fs.lstatSync(file), directory); }
16
29
  function privateDirectory(root, create, homeDirectory = os.homedir()) {
17
30
  const home = fs.realpathSync(homeDirectory);
18
- const dir = path.join(home, '.vibe-runtime');
31
+ const dir = path.join(home, STORE_DIR);
19
32
  if (inside(fs.realpathSync(root), dir)) throw new Error('local execution consent storage must be outside the project');
20
33
  if (!fs.lstatSync(dir, { throwIfNoEntry: false })) {
21
34
  if (!create) return dir;
22
- fs.mkdirSync(dir, { mode: 0o700 });
35
+ fs.mkdirSync(dir, { mode: DIRECTORY_MODE });
23
36
  }
24
37
  privateStat(dir, true);
25
38
  return dir;
26
39
  }
27
40
  function entryName(name) {
28
- if (!/^[a-z0-9.-]+$/i.test(name)) throw new Error('invalid local store entry name');
41
+ if (!ENTRY_NAME.test(name)) throw new Error('invalid local store entry name');
29
42
  }
30
- function readPrivate(root, name, maxBytes = 2097152, homeDirectory) {
43
+ function readPrivate(root, name, maxBytes = DEFAULT_MAX_BYTES, homeDirectory) {
31
44
  entryName(name);
32
45
  const dir = privateDirectory(root, false, homeDirectory);
33
46
  if (!fs.existsSync(dir)) return null;
@@ -37,11 +50,13 @@ function readPrivate(root, name, maxBytes = 2097152, homeDirectory) {
37
50
  const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
38
51
  try {
39
52
  const actual = privateEntry(fs.fstatSync(fd), false);
40
- if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size > maxBytes) throw new Error('local store entry changed or exceeds its read limit');
53
+ if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size > maxBytes) throw new Error(CHANGED_ENTRY);
41
54
  const buffer = Buffer.alloc(maxBytes + 1);
42
55
  const length = fs.readSync(fd, buffer, 0, buffer.length, 0);
43
56
  const after = privateStat(file, false);
44
- if (length > maxBytes || length !== actual.size || after.ino !== actual.ino || after.dev !== actual.dev || after.size !== actual.size || after.mtimeMs !== actual.mtimeMs || after.ctimeMs !== actual.ctimeMs) throw new Error('local store entry changed or exceeds its read limit');
57
+ const same = after.ino === actual.ino && after.dev === actual.dev && after.size === actual.size
58
+ && after.mtimeMs === actual.mtimeMs && after.ctimeMs === actual.ctimeMs;
59
+ if (length > maxBytes || length !== actual.size || !same) throw new Error(CHANGED_ENTRY);
45
60
  return buffer.subarray(0, length).toString('utf8');
46
61
  } finally { fs.closeSync(fd); }
47
62
  }
@@ -50,8 +65,8 @@ function writePrivate(root, name, text, homeDirectory) {
50
65
  const dir = privateDirectory(root, true, homeDirectory);
51
66
  const file = path.join(dir, name);
52
67
  if (fs.lstatSync(file, { throwIfNoEntry: false })) privateStat(file, false);
53
- const temporary = path.join(dir, `${name}.${randomBytes(12).toString('hex')}.tmp`);
54
- fs.writeFileSync(temporary, text, { flag: 'wx', mode: 0o600 });
68
+ const temporary = path.join(dir, `${name}.${randomBytes(TEMP_SUFFIX_BYTES).toString('hex')}.tmp`);
69
+ fs.writeFileSync(temporary, text, { flag: 'wx', mode: FILE_MODE });
55
70
  try { privateStat(dir, true); fs.renameSync(temporary, file); }
56
71
  finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
57
72
  return file;
package/hooks/retest.cjs CHANGED
@@ -5,19 +5,32 @@
5
5
  // in front of a command lets one run through when the user asked for it or the test depends on more than files.
6
6
  const fs = require('node:fs');
7
7
  const path = require('node:path');
8
+ const { vibeConfig, SETTINGS } = require('./locations.cjs');
8
9
  const { spawnSync } = require('node:child_process');
9
10
  const { createHash } = require('node:crypto');
10
- const { segments, runOutcome } = require('./knowledge-journal.cjs');
11
+ const { DAY, segments, runOutcome } = require('./knowledge-journal.cjs');
11
12
  const { readPrivate, writePrivate } = require('./private-store.cjs');
12
13
 
13
- const DAY = 86400000;
14
14
  const MODES = ['block', 'warn', 'off'];
15
15
  const KEEP_PASSED = 100;
16
16
  const KEEP_EVENTS = 500;
17
17
  const MAX_FILES = 2000;
18
- const HASH_LIMIT = 16777216;
18
+ // A file larger than this is fingerprinted by size and modification time instead of its content.
19
+ const HASH_LIMIT = 16 * 1024 * 1024;
20
+ const STORE_MAX_BYTES = 1024 * 1024;
21
+ const GIT_TIMEOUT_MS = 3000;
22
+ // How long the gate's own stops and warnings are kept and reported.
23
+ const EVENT_DAYS = 30;
24
+ const STORE_HASH_CHARS = 32;
25
+ const STATE_HASH_CHARS = 16;
26
+ const SUMMARY_COMMANDS = 10;
27
+ // The `HH:MM` part of an ISO time.
28
+ const CLOCK_START = 11;
29
+ const CLOCK_END = 16;
30
+ // `git status --porcelain=v1` puts two status letters and a space before the path.
31
+ const STATUS_PREFIX = 3;
19
32
 
20
- const settingsFile = home => path.join(home, '.config', 'vibe', 'retest.json');
33
+ const settingsFile = home => vibeConfig(home, SETTINGS.retest);
21
34
  function mode(home) {
22
35
  try { const raw = JSON.parse(fs.readFileSync(settingsFile(home), 'utf8')); return MODES.includes(raw.mode) ? raw.mode : 'block'; } catch { return 'block'; }
23
36
  }
@@ -51,7 +64,7 @@ function testCommand(command) {
51
64
  const bypassed = command => segments(unwrapped(command)).some(part => /^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*VIBE_RETEST=1(?:\s|$)/.test(part));
52
65
 
53
66
  function git(cwd, args) {
54
- const r = spawnSync('git', args, { cwd, encoding: 'buffer', timeout: 3000, windowsHide: true });
67
+ const r = spawnSync('git', args, { cwd, encoding: 'buffer', timeout: GIT_TIMEOUT_MS, windowsHide: true });
55
68
  return r.status === 0 ? r.stdout : null;
56
69
  }
57
70
  function fileId(file) {
@@ -70,17 +83,17 @@ function workState(cwd) {
70
83
  const [root, head] = top;
71
84
  const status = git(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--no-renames']);
72
85
  if (status === null) return null;
73
- const files = status.toString('utf8').split('\0').filter(Boolean).map(entry => entry.slice(3)).filter(file => !file.startsWith('.vibe/')).sort();
86
+ const files = status.toString('utf8').split('\0').filter(Boolean).map(entry => entry.slice(STATUS_PREFIX)).filter(file => !file.startsWith('.vibe/')).sort();
74
87
  if (files.length > MAX_FILES) return null;
75
88
  const hash = createHash('sha256').update(head);
76
89
  for (const file of files) hash.update(`\n${file}:${fileId(path.join(root, file))}`);
77
- return { root: fs.realpathSync(root), state: hash.digest('hex').slice(0, 16) };
90
+ return { root: fs.realpathSync(root), state: hash.digest('hex').slice(0, STATE_HASH_CHARS) };
78
91
  }
79
92
 
80
- const storeName = root => `retest-${createHash('sha256').update(root).digest('hex').slice(0, 32)}.json`;
93
+ const storeName = root => `retest-${createHash('sha256').update(root).digest('hex').slice(0, STORE_HASH_CHARS)}.json`;
81
94
  function readStore(root, home) {
82
95
  try {
83
- const raw = JSON.parse(readPrivate(root, storeName(root), 1048576, home) ?? '{}');
96
+ const raw = JSON.parse(readPrivate(root, storeName(root), STORE_MAX_BYTES, home) ?? '{}');
84
97
  return { passed: Array.isArray(raw.passed) ? raw.passed : [], events: Array.isArray(raw.events) ? raw.events : [] };
85
98
  } catch { return { passed: [], events: [] }; }
86
99
  }
@@ -91,8 +104,18 @@ function where(payload) {
91
104
  const cwd = input.cwd ?? input.workdir ?? payload.cwd ?? process.cwd();
92
105
  return path.resolve(typeof cwd === 'string' ? cwd : process.cwd());
93
106
  }
94
- const folderOf = (root, cwd) => { try { return path.relative(root, fs.realpathSync(cwd)).split(path.sep).join('/'); } catch { return path.relative(root, cwd); } };
95
- const clock = at => new Date(at).toISOString().slice(11, 16);
107
+ function folderOf(root, cwd) {
108
+ let real = cwd;
109
+ try { real = fs.realpathSync(cwd); } catch { /* a folder that is gone is compared as given */ }
110
+ return path.relative(root, real).split(path.sep).join('/');
111
+ }
112
+ const clock = at => new Date(at).toISOString().slice(CLOCK_START, CLOCK_END);
113
+
114
+ const BLOCKED = [
115
+ 'not run again. Use that result.',
116
+ 'Run it again only when the user asks or the test depends on something outside the files (a service, data, the clock):',
117
+ 'put VIBE_RETEST=1 in front of the command.',
118
+ ].join(' ');
96
119
 
97
120
  /** PreToolUse(Bash): null to let the command run, or the decision and the message for the model. */
98
121
  function before(payload, home, now = Date.now()) {
@@ -107,11 +130,11 @@ function before(payload, home, now = Date.now()) {
107
130
  const folder = folderOf(work.root, cwd);
108
131
  const hit = store.passed.find(p => p.cmd === key && p.cwd === folder && p.state === work.state);
109
132
  if (!hit) return null;
110
- store.events = [...store.events.filter(e => e.at >= now - 30 * DAY), { at: now, cmd: key, action: setting }].slice(-KEEP_EVENTS);
133
+ store.events = [...store.events.filter(e => e.at >= now - EVENT_DAYS * DAY), { at: now, cmd: key, action: setting }].slice(-KEEP_EVENTS);
111
134
  writeStore(work.root, store, home);
112
135
  const said = `[vibe] \`${key}\` already passed on exactly this code at ${clock(hit.at)} UTC and no file has changed since`;
113
136
  return setting === 'block'
114
- ? { decision: 'block', message: `${said}: not run again. Use that result. Run it again only when the user asks or the test depends on something outside the files (a service, data, the clock): put VIBE_RETEST=1 in front of the command.` }
137
+ ? { decision: 'block', message: `${said}: ${BLOCKED}` }
115
138
  : { decision: 'warn', message: `${said}; running it again repeats a known result unless the test depends on something outside the files.` };
116
139
  }
117
140
 
@@ -132,13 +155,13 @@ function after(payload, home, now = Date.now()) {
132
155
  function summary(cwd, home, now = Date.now()) {
133
156
  let root;
134
157
  try { root = fs.realpathSync(git(cwd, ['rev-parse', '--show-toplevel'])?.toString('utf8').trim() || cwd); } catch { root = path.resolve(cwd); }
135
- const events = readStore(root, home).events.filter(e => e.at >= now - 30 * DAY);
158
+ const events = readStore(root, home).events.filter(e => e.at >= now - EVENT_DAYS * DAY);
136
159
  const counts = new Map();
137
160
  for (const e of events) counts.set(e.cmd, (counts.get(e.cmd) ?? 0) + 1);
138
161
  return {
139
- mode: mode(home), days: 30,
162
+ mode: mode(home), days: EVENT_DAYS,
140
163
  stopped: events.filter(e => e.action === 'block').length, warned: events.filter(e => e.action === 'warn').length,
141
- commands: [...counts].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, 10),
164
+ commands: [...counts].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, SUMMARY_COMMANDS),
142
165
  };
143
166
  }
144
167
 
@@ -1,10 +1,23 @@
1
1
  const fs = require('node:fs');
2
2
  const os = require('node:os');
3
3
  const path = require('node:path');
4
+ const { vibeConfig, FOLDERS } = require('./locations.cjs');
4
5
  const { readPrivate } = require('./private-store.cjs');
5
6
  const { bounded, digest } = require('./session-files.cjs');
6
7
 
7
- const ENVIRONMENT = ['HOME', 'USERPROFILE', 'VIBE_HOME_DIR', 'VIBE_REVIEW_CMD', 'VIBE_REVIEW_CLIENT', 'VIBE_REVIEWER_MODEL', 'VIBE_REVIEWER_EFFORT'];
8
+ const WINDOWS_SHELL = 'C:\\Windows\\System32\\cmd.exe';
9
+ const POSIX_SHELL = '/bin/sh';
10
+ const MAX_CHECKS = 1000;
11
+ const MAX_VERIFIERS = 512;
12
+ const VERIFIER_FILE_MAX_BYTES = 1024 * 1024;
13
+ const VERIFIER_TOTAL_MAX_BYTES = 32 * 1024 * 1024;
14
+ const VERIFIER_DEADLINE_MS = 1000;
15
+ const VERIFIER_LIMIT = 'verifier limit';
16
+ const SHA256 = /^[a-f0-9]{64}$/;
17
+ // The environment a recorded plan must still see unchanged.
18
+ const ENVIRONMENT = [
19
+ 'HOME', 'USERPROFILE', 'VIBE_HOME_DIR', 'VIBE_REVIEW_CMD', 'VIBE_REVIEW_CLIENT', 'VIBE_REVIEWER_MODEL', 'VIBE_REVIEWER_EFFORT',
20
+ ];
8
21
  const fingerprint = value => digest(JSON.stringify(value));
9
22
  function resolvedDirectory(directory) {
10
23
  let existing = path.resolve(directory);
@@ -13,31 +26,35 @@ function resolvedDirectory(directory) {
13
26
  return path.join(fs.realpathSync(existing), ...suffix);
14
27
  }
15
28
  function runtimeMatches(root, plan) {
16
- const shell = process.platform === 'win32' ? process.env.ComSpec ?? 'C:\\Windows\\System32\\cmd.exe' : '/bin/sh';
17
- if (plan.project !== fs.realpathSync(root) || plan.platform !== process.platform || plan.shell !== fs.realpathSync(shell) || plan.node !== fs.realpathSync(process.execPath)) return false;
29
+ const shell = process.platform === 'win32' ? process.env.ComSpec ?? WINDOWS_SHELL : POSIX_SHELL;
30
+ const sameRuntime = plan.project === fs.realpathSync(root) && plan.platform === process.platform
31
+ && plan.shell === fs.realpathSync(shell) && plan.node === fs.realpathSync(process.execPath);
32
+ if (!sameRuntime) return false;
18
33
  if (plan.path !== (process.env.PATH ?? '') || plan.pathExt !== (process.env.PATHEXT ?? '')) return false;
19
34
  if (!plan.environment || ENVIRONMENT.some(key => plan.environment[key] !== (process.env[key] ?? null))) return false;
20
- const reviewerCwd = process.env.VIBE_REVIEW_CMD ? root : resolvedDirectory(path.join(process.env.VIBE_HOME_DIR ?? os.homedir(), '.config', 'vibe', 'reader'));
35
+ const readerFolder = vibeConfig(process.env.VIBE_HOME_DIR ?? os.homedir(), FOLDERS.reader);
36
+ const reviewerCwd = process.env.VIBE_REVIEW_CMD ? root : resolvedDirectory(readerFolder);
21
37
  if (!plan.reviewer || plan.reviewer.cwd !== reviewerCwd) return false;
22
38
  return plan.checks.every(item => {
23
- const expected = item.check.type === 'review' ? reviewerCwd : fs.realpathSync(item.check.type === 'run' && item.check.cwd ? path.resolve(root, item.check.cwd) : root);
39
+ const { type, cwd } = item.check;
40
+ const expected = type === 'review' ? reviewerCwd : fs.realpathSync(type === 'run' && cwd ? path.resolve(root, cwd) : root);
24
41
  return item.cwd === expected;
25
42
  });
26
43
  }
27
44
  function verifiersMatch(plan) {
28
45
  const seen = new Map();
29
- const deadline = Date.now() + 1000;
46
+ const deadline = Date.now() + VERIFIER_DEADLINE_MS;
30
47
  let bytes = 0;
31
48
  for (const item of plan.checks) {
32
- if (!Array.isArray(item.verifiers) || item.verifiers.length > 512) throw new Error('verifier limit');
49
+ if (!Array.isArray(item.verifiers) || item.verifiers.length > MAX_VERIFIERS) throw new Error(VERIFIER_LIMIT);
33
50
  for (const verifier of item.verifiers) {
34
- if (Date.now() > deadline) throw new Error('verifier limit');
35
- if (typeof verifier.file !== 'string' || !path.isAbsolute(verifier.file) || !/^[a-f0-9]{64}$/.test(verifier.sha256)) return false;
51
+ if (Date.now() > deadline) throw new Error(VERIFIER_LIMIT);
52
+ if (typeof verifier.file !== 'string' || !path.isAbsolute(verifier.file) || !SHA256.test(verifier.sha256)) return false;
36
53
  if (!seen.has(verifier.file)) {
37
- if (seen.size >= 512) throw new Error('verifier limit');
38
- const content = bounded(verifier.file, 1048576);
54
+ if (seen.size >= MAX_VERIFIERS) throw new Error(VERIFIER_LIMIT);
55
+ const content = bounded(verifier.file, VERIFIER_FILE_MAX_BYTES);
39
56
  bytes += content.length;
40
- if (bytes > 33554432) throw new Error('verifier limit');
57
+ if (bytes > VERIFIER_TOTAL_MAX_BYTES) throw new Error(VERIFIER_LIMIT);
41
58
  seen.set(verifier.file, digest(content));
42
59
  }
43
60
  if (seen.get(verifier.file) !== verifier.sha256) return false;
@@ -47,7 +64,7 @@ function verifiersMatch(plan) {
47
64
  }
48
65
  function contextStatus(root, plan) {
49
66
  try {
50
- if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.checks) || plan.checks.length > 1000) return 'unavailable';
67
+ if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.checks) || plan.checks.length > MAX_CHECKS) return 'unavailable';
51
68
  const receipt = readPrivate(root, `consent-${fingerprint(root)}.json`);
52
69
  if (receipt === null) return 'stale';
53
70
  const saved = JSON.parse(receipt);