@haposoft/cafekit 0.16.7 → 0.18.1

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 (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -1,527 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('crypto');
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
-
8
- const PREFIX = 'APPROVE CAFEKIT COMPLETION ';
9
- const NONCE_RE = /^[a-f0-9]{24}$/;
10
- const HASH_RE = /^[a-f0-9]{64}$/;
11
- const TTL_MS = 5 * 60 * 1000;
12
- const KEY_BYTES = 32;
13
- const SCHEMA_VERSION = 3;
14
- const LEGACY_SCHEMA_VERSION = 2;
15
- const EPHEMERAL_FILE_RE = /^(pending|grant)-([a-f0-9]{24})\.json$/;
16
- const BASELINE_FILE_RE = /^baseline-([a-f0-9]{64})\.json$/;
17
- const FLOOR_FILE = 'policy-floor.json';
18
-
19
- function plain(value) {
20
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
21
- }
22
-
23
- function canonicalize(value) {
24
- if (Array.isArray(value)) return value.map(canonicalize);
25
- if (!value || typeof value !== 'object') return value;
26
- return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
27
- }
28
-
29
- function stableStringify(value) {
30
- return JSON.stringify(canonicalize(value));
31
- }
32
-
33
- function digest(value) {
34
- return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
35
- }
36
-
37
- function secureMkdir(dir) {
38
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
39
- if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
40
- }
41
-
42
- function homeDirectory() {
43
- const requested = process.platform === 'win32'
44
- ? (process.env.USERPROFILE || (process.env.HOMEDRIVE && process.env.HOMEPATH
45
- ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
46
- : null))
47
- : process.env.HOME;
48
- const home = requested || os.homedir();
49
- if (typeof home !== 'string' || !home.trim()) throw new Error('user home directory is unavailable');
50
- return path.resolve(home);
51
- }
52
-
53
- function userStateRoot() {
54
- return path.join(homeDirectory(), '.cafekit', 'completion-authority');
55
- }
56
-
57
- function keyPath() {
58
- return path.join(userStateRoot(), 'hmac.key');
59
- }
60
-
61
- function canonicalProjectRoot(projectRoot) {
62
- if (typeof projectRoot !== 'string' || !projectRoot.trim()) throw new Error('project root is required');
63
- return fs.realpathSync(path.resolve(projectRoot));
64
- }
65
-
66
- function projectNamespace(projectRoot) {
67
- const canonical = canonicalProjectRoot(projectRoot);
68
- const projectHash = digest({ namespace: 'cafekit-completion-authority-v2', project_root: canonical });
69
- return { canonical, projectHash };
70
- }
71
-
72
- function stateDir(projectRoot) {
73
- return path.join(userStateRoot(), 'projects', projectNamespace(projectRoot).projectHash);
74
- }
75
-
76
- function inside(root, target) {
77
- const relative = path.relative(path.resolve(root), path.resolve(target));
78
- return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
79
- }
80
-
81
- function normalizeIdentity(identity, { allowMissingSpec = false } = {}) {
82
- if (!plain(identity)) throw new Error('baseline identity must be an object');
83
- const projectRoot = canonicalProjectRoot(identity.project_root);
84
- const featureName = identity.feature_name;
85
- if (typeof featureName !== 'string' || !featureName.trim() || featureName.includes('/') || featureName.includes('\\') || featureName === '.' || featureName === '..') {
86
- throw new Error('baseline feature identity is malformed');
87
- }
88
- if (typeof identity.spec_file !== 'string' || !identity.spec_file.trim()) throw new Error('baseline spec identity is malformed');
89
- const requestedSpec = path.resolve(projectRoot, identity.spec_file);
90
- let specFile = requestedSpec;
91
- try {
92
- specFile = fs.realpathSync(requestedSpec);
93
- } catch (error) {
94
- if (!allowMissingSpec || !['ENOENT', 'ENOTDIR'].includes(error.code)) throw new Error(`baseline spec identity cannot be canonicalized (${error.code || error.message})`);
95
- }
96
- if (!inside(projectRoot, specFile) || specFile === projectRoot || path.basename(specFile) !== 'spec.json') {
97
- throw new Error('baseline spec identity escapes the project');
98
- }
99
- if (path.basename(path.dirname(specFile)) !== featureName) throw new Error('baseline feature and spec identities disagree');
100
- return { project_root: projectRoot, spec_file: specFile, feature_name: featureName };
101
- }
102
-
103
- function identityDigest(identity) {
104
- return digest({
105
- namespace: 'cafekit-completion-baseline-v1',
106
- project_root: identity.project_root,
107
- spec_file: identity.spec_file,
108
- feature_name: identity.feature_name,
109
- });
110
- }
111
-
112
- function baselinePath(projectRoot, identity) {
113
- const normalized = normalizeIdentity(identity, { allowMissingSpec: true });
114
- const namespace = projectNamespace(projectRoot);
115
- if (normalized.project_root !== namespace.canonical) throw new Error('baseline project identity does not match state namespace');
116
- return path.join(stateDir(namespace.canonical), `baseline-${identityDigest(normalized)}.json`);
117
- }
118
-
119
- function atomicWrite(file, value) {
120
- const dir = path.dirname(file);
121
- secureMkdir(dir);
122
- const temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
123
- fs.writeFileSync(temp, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600 });
124
- if (process.platform !== 'win32') fs.chmodSync(temp, 0o600);
125
- fs.renameSync(temp, file);
126
- }
127
-
128
- function readJson(file) {
129
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
130
- }
131
-
132
- function recordFiles(projectRoot) {
133
- const dir = stateDir(projectRoot);
134
- try {
135
- return fs.readdirSync(dir, { withFileTypes: true })
136
- .filter((entry) => entry.isFile())
137
- .map((entry) => entry.name);
138
- } catch (error) {
139
- if (error.code === 'ENOENT') return [];
140
- throw error;
141
- }
142
- }
143
-
144
- function anyAuthorityRecords() {
145
- const projects = path.join(userStateRoot(), 'projects');
146
- let entries;
147
- try { entries = fs.readdirSync(projects, { withFileTypes: true }); } catch (error) {
148
- if (error.code === 'ENOENT') return false;
149
- throw error;
150
- }
151
- for (const entry of entries) {
152
- if (entry.isSymbolicLink()) return true;
153
- if (!entry.isDirectory()) return true;
154
- if (fs.readdirSync(path.join(projects, entry.name)).length > 0) return true;
155
- }
156
- return false;
157
- }
158
-
159
- function readKey() {
160
- const file = keyPath();
161
- let stat;
162
- try { stat = fs.lstatSync(file); } catch (error) {
163
- if (error.code === 'ENOENT') return { key: null, missing: true };
164
- throw new Error(`completion authority key cannot be inspected (${error.code || error.message})`);
165
- }
166
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('completion authority key is not a regular file');
167
- if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) throw new Error('completion authority key permissions are too broad');
168
- let key;
169
- try { key = fs.readFileSync(file); } catch (error) { throw new Error(`completion authority key cannot be read (${error.message})`); }
170
- if (key.length !== KEY_BYTES) throw new Error('completion authority key is malformed');
171
- return { key, missing: false };
172
- }
173
-
174
- function ensureKey() {
175
- secureMkdir(userStateRoot());
176
- secureMkdir(path.join(userStateRoot(), 'projects'));
177
- const current = readKey();
178
- if (!current.missing) return current.key;
179
- if (anyAuthorityRecords()) throw new Error('completion authority key is missing while authority records exist');
180
- const key = crypto.randomBytes(KEY_BYTES);
181
- try {
182
- fs.writeFileSync(keyPath(), key, { flag: 'wx', mode: 0o600 });
183
- if (process.platform !== 'win32') fs.chmodSync(keyPath(), 0o600);
184
- return key;
185
- } catch (error) {
186
- if (error.code === 'EEXIST') {
187
- const raced = readKey();
188
- if (!raced.missing) return raced.key;
189
- }
190
- throw new Error(`completion authority key cannot be created (${error.message})`);
191
- }
192
- }
193
-
194
- function unsignedRecord(record) {
195
- const unsigned = { ...record };
196
- delete unsigned.mac;
197
- delete unsigned.file;
198
- return unsigned;
199
- }
200
-
201
- function recordMac(record, key) {
202
- return crypto.createHmac('sha256', key).update(stableStringify(unsignedRecord(record))).digest('hex');
203
- }
204
-
205
- function macMatches(record, key) {
206
- if (typeof record?.mac !== 'string' || !/^[a-f0-9]{64}$/.test(record.mac)) return false;
207
- const actual = Buffer.from(record.mac, 'hex');
208
- const expected = Buffer.from(recordMac(record, key), 'hex');
209
- return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
210
- }
211
-
212
- function exactKeys(value, expected) {
213
- return JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort());
214
- }
215
-
216
- function validTimestamp(value) {
217
- return typeof value === 'string' && value.trim() === value && !Number.isNaN(Date.parse(value));
218
- }
219
-
220
- function validateEphemeral(record, kind, nonce, now, key) {
221
- const expected = kind === 'pending'
222
- ? ['schema_version', 'kind', 'nonce', 'binding', 'issued_at', 'expires_at', 'mac']
223
- : ['schema_version', 'kind', 'nonce', 'binding', 'issued_at', 'expires_at', 'approved_at', 'mac'];
224
- if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed authority record' };
225
- if (!macMatches(record, key)) return { ok: false, reason: 'authority record MAC is invalid' };
226
- if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== kind || record.nonce !== nonce || !NONCE_RE.test(nonce)) return { ok: false, reason: 'authority record identity is invalid' };
227
- if (!plain(record.binding) || typeof record.binding.session_id !== 'string' || !record.binding.session_id.trim()) return { ok: false, reason: 'authority record binding is invalid' };
228
- if (!validTimestamp(record.issued_at) || !Number.isFinite(record.expires_at) || record.expires_at <= Date.parse(record.issued_at)) return { ok: false, reason: 'authority record timestamps are invalid' };
229
- if (kind === 'grant' && !validTimestamp(record.approved_at)) return { ok: false, reason: 'authority grant approval timestamp is invalid' };
230
- if (record.expires_at <= now) return { ok: false, expired: true, reason: 'authority record expired' };
231
- return { ok: true };
232
- }
233
-
234
- function validateBaseline(record, filename, projectRoot, key) {
235
- const expected = ['schema_version', 'kind', 'identity', 'policy', 'issued_at', 'updated_at', 'mac'];
236
- if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed policy baseline' };
237
- if (!macMatches(record, key)) return { ok: false, reason: 'policy baseline MAC is invalid' };
238
- if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'baseline' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) return { ok: false, reason: 'policy baseline identity or timestamps are invalid' };
239
- let identity;
240
- try { identity = normalizeIdentity(record.identity, { allowMissingSpec: true }); } catch (error) { return { ok: false, reason: error.message }; }
241
- if (filename !== `baseline-${identityDigest(identity)}.json` || identity.project_root !== canonicalProjectRoot(projectRoot)) return { ok: false, reason: 'policy baseline namespace is invalid' };
242
- if (!plain(record.policy)) return { ok: false, reason: 'policy baseline policy is malformed' };
243
- return { ok: true, identity };
244
- }
245
-
246
- function validatePolicyFloor(record, filename, key) {
247
- const expected = ['schema_version', 'kind', 'policy', 'issued_at', 'updated_at', 'mac'];
248
- if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed project policy floor' };
249
- if (!macMatches(record, key)) return { ok: false, reason: 'project policy floor MAC is invalid' };
250
- if (filename !== FLOOR_FILE || ![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'policy-floor' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) {
251
- return { ok: false, reason: 'project policy floor identity or timestamps are invalid' };
252
- }
253
- if (!plain(record.policy)) return { ok: false, reason: 'project policy floor policy is malformed' };
254
- return { ok: true };
255
- }
256
-
257
- function readState(projectRoot, now = Date.now()) {
258
- const state = {
259
- pending: null,
260
- grant: null,
261
- baselines: [],
262
- floor: null,
263
- floorMissing: false,
264
- malformed: false,
265
- reason: null,
266
- keyMissing: false,
267
- keyError: null,
268
- };
269
- let files;
270
- try { files = recordFiles(projectRoot); } catch (error) {
271
- state.malformed = true;
272
- state.reason = error.message;
273
- state.keyError = error;
274
- return state;
275
- }
276
- let keyInfo;
277
- try { keyInfo = readKey(); } catch (error) {
278
- state.malformed = true;
279
- state.reason = error.message;
280
- state.keyError = error;
281
- return state;
282
- }
283
- if (keyInfo.missing) {
284
- state.keyMissing = true;
285
- if (files.length > 0) {
286
- state.malformed = true;
287
- state.reason = 'completion authority key is missing while authority records exist';
288
- }
289
- return state;
290
- }
291
- const key = keyInfo.key;
292
- for (const name of files) {
293
- const ephemeral = name.match(EPHEMERAL_FILE_RE);
294
- const baseline = name.match(BASELINE_FILE_RE);
295
- const floor = name === FLOOR_FILE;
296
- if (!ephemeral && !baseline && !floor) {
297
- if (name.endsWith('.json')) {
298
- state.malformed = true;
299
- state.reason ||= `unknown completion authority record ${name}`;
300
- }
301
- continue;
302
- }
303
- const file = path.join(stateDir(projectRoot), name);
304
- const record = readJson(file);
305
- const result = ephemeral
306
- ? validateEphemeral(record, ephemeral[1], ephemeral[2], now, key)
307
- : baseline
308
- ? validateBaseline(record, name, projectRoot, key)
309
- : validatePolicyFloor(record, name, key);
310
- if (!result.ok) {
311
- if (result.expired) {
312
- try { fs.unlinkSync(file); } catch { /* expired state is harmless if cleanup races */ }
313
- } else {
314
- state.malformed = true;
315
- state.reason ||= result.reason;
316
- }
317
- continue;
318
- }
319
- const accepted = { ...record, file };
320
- if (ephemeral) {
321
- if (ephemeral[1] === 'pending') {
322
- if (state.pending) {
323
- state.malformed = true;
324
- state.reason ||= 'multiple pending completion authority records exist';
325
- } else state.pending = accepted;
326
- } else if (state.grant) {
327
- state.malformed = true;
328
- state.reason ||= 'multiple grant completion authority records exist';
329
- } else state.grant = accepted;
330
- } else if (baseline) {
331
- if (state.baselines.some((entry) => stableStringify(entry.identity) === stableStringify(accepted.identity))) {
332
- state.malformed = true;
333
- state.reason ||= 'duplicate policy baselines exist';
334
- } else state.baselines.push(accepted);
335
- } else if (state.floor) {
336
- state.malformed = true;
337
- state.reason ||= 'multiple project policy floors exist';
338
- } else {
339
- state.floor = accepted;
340
- }
341
- }
342
- if (state.pending && state.grant) {
343
- state.malformed = true;
344
- state.reason ||= 'pending and grant completion authority records coexist';
345
- }
346
- if (!state.floor) {
347
- state.floorMissing = true;
348
- }
349
- return state;
350
- }
351
-
352
- function clearState(projectRoot) {
353
- let files;
354
- try { files = recordFiles(projectRoot); } catch { return; }
355
- for (const name of files) {
356
- if (/^(?:pending|grant)-[a-f0-9]+\.json$/.test(name)) {
357
- try { fs.unlinkSync(path.join(stateDir(projectRoot), name)); } catch { /* session cleanup is idempotent */ }
358
- }
359
- }
360
- }
361
-
362
- function sameBinding(record, binding) {
363
- return Boolean(record && binding && stableStringify(record.binding) === stableStringify(binding));
364
- }
365
-
366
- function createPending(projectRoot, binding) {
367
- if (!plain(binding) || typeof binding.session_id !== 'string' || !binding.session_id.trim()) throw new Error('completion authority binding is malformed');
368
- const key = ensureKey();
369
- const state = readState(projectRoot);
370
- if (state.malformed || state.keyError || state.keyMissing) throw new Error(state.reason || 'completion authority state is unavailable');
371
- if (state.pending && sameBinding(state.pending, binding)) {
372
- const { file, ...record } = state.pending;
373
- return record;
374
- }
375
- clearState(projectRoot);
376
- const nonce = crypto.randomBytes(12).toString('hex');
377
- const issuedAt = new Date().toISOString();
378
- const record = {
379
- schema_version: SCHEMA_VERSION,
380
- kind: 'pending',
381
- nonce,
382
- binding: JSON.parse(JSON.stringify(binding)),
383
- issued_at: issuedAt,
384
- expires_at: Date.now() + TTL_MS,
385
- };
386
- record.mac = recordMac(record, key);
387
- atomicWrite(path.join(stateDir(projectRoot), `pending-${nonce}.json`), record);
388
- return record;
389
- }
390
-
391
- function approvePending(projectRoot, sessionId, nonce) {
392
- if (typeof sessionId !== 'string' || !sessionId.trim() || !NONCE_RE.test(nonce || '')) return { ok: false, reason: 'shape' };
393
- let state;
394
- let key;
395
- try {
396
- key = readKey();
397
- if (key.missing) return { ok: false, reason: 'authority-key-missing' };
398
- state = readState(projectRoot);
399
- } catch (error) {
400
- return { ok: false, reason: `authority-unavailable:${error.message}` };
401
- }
402
- if (state.malformed || state.keyError || state.keyMissing) return { ok: false, reason: state.reason || 'authority-state-malformed' };
403
- const pending = state.pending;
404
- if (!pending || pending.binding.session_id !== sessionId || pending.nonce !== nonce) return { ok: false, reason: 'missing-or-bound' };
405
- const { file, mac, ...unsignedPending } = pending;
406
- const grant = {
407
- ...unsignedPending,
408
- kind: 'grant',
409
- approved_at: new Date().toISOString(),
410
- };
411
- grant.mac = recordMac(grant, key.key);
412
- try { fs.unlinkSync(file); } catch { return { ok: false, reason: 'consumed' }; }
413
- try {
414
- atomicWrite(path.join(stateDir(projectRoot), `grant-${nonce}.json`), grant);
415
- } catch (error) {
416
- return { ok: false, reason: `grant-write:${error.message}` };
417
- }
418
- return { ok: true, grant };
419
- }
420
-
421
- function consumeGrant(projectRoot, binding) {
422
- let state;
423
- try { state = readState(projectRoot); } catch (error) { return { ok: false, reason: `authority-unavailable:${error.message}` }; }
424
- if (state.malformed || state.keyError || state.keyMissing) return { ok: false, reason: state.reason || 'authority-state-malformed' };
425
- if (!state.grant) return { ok: false, reason: 'missing' };
426
- if (!sameBinding(state.grant, binding)) {
427
- clearState(projectRoot);
428
- return { ok: false, reason: 'binding' };
429
- }
430
- try {
431
- fs.unlinkSync(state.grant.file);
432
- return { ok: true };
433
- } catch {
434
- return { ok: false, reason: 'consume' };
435
- }
436
- }
437
-
438
- function readBaseline(projectRoot, identity) {
439
- const normalized = normalizeIdentity(identity, { allowMissingSpec: true });
440
- const state = readState(projectRoot);
441
- if (state.malformed || state.keyError || state.keyMissing) throw new Error(state.reason || 'completion authority state is unavailable');
442
- return state.baselines.find((entry) => stableStringify(entry.identity) === stableStringify(normalized)) || null;
443
- }
444
-
445
- function writeBaseline(projectRoot, identity, policy) {
446
- if (!plain(policy)) throw new Error('policy baseline must contain a policy object');
447
- const normalized = normalizeIdentity(identity, { allowMissingSpec: true });
448
- const key = ensureKey();
449
- const state = readState(projectRoot);
450
- if (state.malformed || state.keyError || state.keyMissing) throw new Error(state.reason || 'completion authority state is unavailable');
451
- const existing = state.baselines.find((entry) => stableStringify(entry.identity) === stableStringify(normalized));
452
- const issuedAt = existing?.issued_at || new Date().toISOString();
453
- const record = {
454
- schema_version: SCHEMA_VERSION,
455
- kind: 'baseline',
456
- identity: normalized,
457
- policy: JSON.parse(JSON.stringify(policy)),
458
- issued_at: issuedAt,
459
- updated_at: new Date().toISOString(),
460
- };
461
- record.mac = recordMac(record, key);
462
- atomicWrite(baselinePath(projectRoot, normalized), record);
463
- return record;
464
- }
465
-
466
- function readPolicyFloor(projectRoot) {
467
- const state = readState(projectRoot);
468
- if (state.malformed || state.keyError || state.keyMissing) throw new Error(state.reason || 'completion authority state is unavailable');
469
- return state.floor || null;
470
- }
471
-
472
- function writePolicyFloor(projectRoot, policy) {
473
- if (!plain(policy)) throw new Error('project policy floor must contain a policy object');
474
- const key = ensureKey();
475
- const state = readState(projectRoot);
476
- if (state.malformed || state.keyError || state.keyMissing) throw new Error(state.reason || 'completion authority state is unavailable');
477
- const issuedAt = state.floor?.issued_at || new Date().toISOString();
478
- const record = {
479
- schema_version: SCHEMA_VERSION,
480
- kind: 'policy-floor',
481
- policy: JSON.parse(JSON.stringify(policy)),
482
- issued_at: issuedAt,
483
- updated_at: new Date().toISOString(),
484
- };
485
- record.mac = recordMac(record, key);
486
- atomicWrite(path.join(stateDir(projectRoot), FLOOR_FILE), record);
487
- return record;
488
- }
489
-
490
- function hasState(projectRoot) {
491
- try {
492
- const state = readState(projectRoot);
493
- if (state.malformed || state.keyError) return true;
494
- if (state.pending || state.grant || state.baselines.length > 0 || state.floor) return true;
495
- return !state.keyMissing && recordFiles(projectRoot).length > 0;
496
- } catch {
497
- return true;
498
- }
499
- }
500
-
501
- module.exports = {
502
- PREFIX,
503
- NONCE_RE,
504
- TTL_MS,
505
- SCHEMA_VERSION,
506
- FLOOR_FILE,
507
- stableStringify,
508
- digest,
509
- canonicalProjectRoot,
510
- atomicWrite,
511
- readKey,
512
- recordMac,
513
- macMatches,
514
- userStateRoot,
515
- stateDir,
516
- clearState,
517
- readState,
518
- ensureKey,
519
- createPending,
520
- approvePending,
521
- consumeGrant,
522
- readBaseline,
523
- writeBaseline,
524
- readPolicyFloor,
525
- writePolicyFloor,
526
- hasState,
527
- };
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { getHookContext, readPayload, logCrash } = require('./lib/hook-context.cjs');
5
- const { loadSharedPolicy } = require('./lib/spec-receipt.cjs');
6
- let STATE;
7
- let evaluateCloseout;
8
-
9
- function emitBlock(reason) {
10
- process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\n`);
11
- }
12
-
13
- function context(message) {
14
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: message } })}\n`);
15
- }
16
-
17
- function loadAuthority() {
18
- try {
19
- STATE = require('./completion-authority-state.cjs');
20
- ({ evaluateCloseout } = require('./completion-authority-check.cjs'));
21
- const requiredState = ['createPending', 'approvePending', 'consumeGrant', 'clearState', 'hasState', 'ensureKey', 'readBaseline', 'writeBaseline', 'readPolicyFloor', 'writePolicyFloor'];
22
- if (requiredState.some((name) => typeof STATE[name] !== 'function') || typeof evaluateCloseout !== 'function') {
23
- throw new Error('completion authority dependency exports are malformed');
24
- }
25
- return null;
26
- } catch (error) {
27
- return error;
28
- }
29
- }
30
-
31
- function approve(payload) {
32
- if (payload.hook_event_name !== 'UserPromptSubmit') return;
33
- const { projectRoot } = getHookContext(payload);
34
- const session = typeof payload.session_id === 'string' ? payload.session_id : null;
35
- const prompt = typeof payload.prompt === 'string' ? payload.prompt : null;
36
- if (!session) return context('CafeKit completion approval rejected: host session ID is missing.');
37
- if (!prompt || prompt !== prompt.trim() || !prompt.startsWith(STATE.PREFIX)) return;
38
- const nonce = prompt.slice(STATE.PREFIX.length);
39
- if (!STATE.NONCE_RE.test(nonce) || `${STATE.PREFIX}${nonce}` !== prompt) {
40
- return context('CafeKit completion approval rejected: the prompt did not exactly match a pending request.');
41
- }
42
- const result = STATE.approvePending(projectRoot, session, nonce);
43
- if (!result.ok) return context('CafeKit completion approval rejected: request missing, expired, consumed, or bound to another session.');
44
- context('CafeKit completion approval accepted for one bound closeout retry.');
45
- }
46
-
47
- function stop(payload) {
48
- if (payload.stop_hook_active === true) return;
49
- const loaded = loadSharedPolicy();
50
- if (!loaded.policy) {
51
- emitBlock(`Completion authority unavailable: shared workflow policy could not be loaded (${loaded.error.message}). Repair ${loaded.path} before completing tasks`);
52
- return;
53
- }
54
- const { projectRoot, runtime } = getHookContext(payload);
55
- if (typeof loaded.policy.completionDecisionForSpec !== 'function' || typeof loaded.policy.deriveRuntimeContext !== 'function' || typeof loaded.policy.validateWorkflowPolicySnapshot !== 'function') {
56
- throw new Error('shared workflow policy lacks completion authority functions');
57
- }
58
- let result;
59
- try {
60
- result = evaluateCloseout({ policy: loaded.policy, projectRoot, runtime, payload });
61
- } catch (error) {
62
- STATE.clearState(projectRoot);
63
- emitBlock(`Completion authority controlled failure: ${error.message}. Completion is blocked until the authority is repaired`);
64
- return;
65
- }
66
- if (result.candidate === null) {
67
- if (STATE.hasState(projectRoot)) {
68
- STATE.clearState(projectRoot);
69
- emitBlock('Completion authority: an observed persisted spec disappeared; fresh technical closeout and approval are required');
70
- }
71
- return;
72
- }
73
- if (result.active === false) return;
74
- if (!result.ok) {
75
- STATE.clearState(projectRoot);
76
- emitBlock(`Completion authority: ${result.reason}`);
77
- return;
78
- }
79
- const consumed = STATE.consumeGrant(projectRoot, result.binding);
80
- if (consumed.ok) return;
81
- const pending = STATE.createPending(projectRoot, result.binding);
82
- // Name the feature: a recorded target can influence which packet resolves, so an
83
- // approval the user cannot identify is an approval they cannot withhold.
84
- const feature = result.candidate?.featureName;
85
- emitBlock(`Completion authority: technical proofs pass, but user closeout approval is required for ${feature ? `"${feature}"` : 'the resolved feature'}. Enter exactly "${STATE.PREFIX}${pending.nonce}" as your next user prompt. This approval is one-time and expires in five minutes.`);
86
- }
87
-
88
- let mode = process.argv[2] || null;
89
- try {
90
- const payload = readPayload();
91
- mode = mode || (payload.hook_event_name === 'UserPromptSubmit' ? '--approve' : '--stop');
92
- if (mode === '--stop' && payload.stop_hook_active === true) {
93
- // Host loop prevention is always silent, including when dependencies are unavailable.
94
- } else {
95
- const dependencyError = loadAuthority();
96
- if (dependencyError) {
97
- if (mode === '--approve') context(`CafeKit completion approval failed safely; authority dependency unavailable (${dependencyError.message}).`);
98
- else emitBlock(`Completion authority unavailable: ${dependencyError.message}. Repair the installed authority before completing tasks`);
99
- } else if (mode === '--approve') approve(payload);
100
- else if (mode === '--stop') stop(payload);
101
- else throw new Error('completion authority mode is required');
102
- }
103
- } catch (error) {
104
- if (mode === '--approve') context(`CafeKit completion approval failed safely; no grant was issued (${error.message}).`);
105
- else {
106
- logCrash('completion-authority', error);
107
- emitBlock(`Completion authority controlled failure: ${error.message}. Completion is blocked until the authority is repaired`);
108
- }
109
- }