@haposoft/cafekit 0.18.0 → 0.18.2
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.
- package/README.md +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +938 -237
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +222 -149
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +102 -1362
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -49
- package/bin/phases/copy-payload.js +11 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +17 -89
- package/src/claude/hooks/spec-state.cjs +89 -96
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +20 -8
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +32 -74
- package/src/codex/hooks/spec-state.cjs +68 -46
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/common/AGENTS.md +4 -10
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -1,218 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const {
|
|
7
|
-
APPROVAL_PREFIX,
|
|
8
|
-
createPending,
|
|
9
|
-
hasGrant
|
|
10
|
-
} = require('./lib/privacy-state.cjs');
|
|
11
|
-
const {
|
|
12
|
-
getHookContext,
|
|
13
|
-
readPayload
|
|
14
|
-
} = require('./lib/hook-context.cjs');
|
|
15
|
-
const { commandAccess } = require('./lib/privacy-command-analysis.cjs');
|
|
16
|
-
|
|
17
|
-
const RESTRICTED = [
|
|
18
|
-
/^\.env(?:[.\[*?{]|$)/i,
|
|
19
|
-
/^credentials/i,
|
|
20
|
-
/secrets?\.(ya?ml|json)$/i,
|
|
21
|
-
/\.pem$/i,
|
|
22
|
-
/\.key$/i,
|
|
23
|
-
/\.p12$/i,
|
|
24
|
-
/\.pfx$/i,
|
|
25
|
-
/^id_(rsa|ed25519|ecdsa|dsa)$/i,
|
|
26
|
-
/\.netrc$/i,
|
|
27
|
-
/\.pgpass$/i,
|
|
28
|
-
/kubeconfig/i,
|
|
29
|
-
/\.keystore$/i,
|
|
30
|
-
/\.jks$/i,
|
|
31
|
-
/auth\.json$/i,
|
|
32
|
-
/token(s)?\.json$/i
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const EXEMPT = [/\.env\.(example|sample|template|test)$/i];
|
|
36
|
-
|
|
37
|
-
function isPathKey(key) {
|
|
38
|
-
return /^(?:file_?paths?|paths?|search_?paths?|director(?:y|ies)|cwd|root|locations?)$/i
|
|
39
|
-
.test(key);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function matchesAny(filePath, rules) {
|
|
43
|
-
const base = path.basename(filePath);
|
|
44
|
-
return rules.some((rule) => rule.test(base) || rule.test(filePath));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isSafe(filePath) {
|
|
48
|
-
return matchesAny(filePath, EXEMPT);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isSensitive(filePath) {
|
|
52
|
-
return matchesAny(filePath, RESTRICTED);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resolveTarget(filePath, cwd) {
|
|
56
|
-
try {
|
|
57
|
-
const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
58
|
-
return fs.realpathSync(requested);
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* When realpath fails, check whether the requested path is a symlink (or chain)
|
|
66
|
-
* whose lexical target is sensitive. Dangling link to protected target -> true (fail-closed deny);
|
|
67
|
-
* dangling benign link or link to allowed example/template -> false (allow).
|
|
68
|
-
* Limit hops to avoid loops; malformed/looping -> fail closed.
|
|
69
|
-
* TOCTOU note: check and use are not atomic; target could be swapped between this check and the actual read.
|
|
70
|
-
* This is a best-effort guardrail, not an arbitrary-shell security boundary. For strong isolation use OS sandbox.
|
|
71
|
-
*/
|
|
72
|
-
function isSymlinkToSensitive(filePath, cwd) {
|
|
73
|
-
try {
|
|
74
|
-
const requested = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
75
|
-
let current = requested;
|
|
76
|
-
let hops = 0;
|
|
77
|
-
const MAX_HOPS = 8;
|
|
78
|
-
while (hops < MAX_HOPS) {
|
|
79
|
-
let stat;
|
|
80
|
-
try {
|
|
81
|
-
stat = fs.lstatSync(current);
|
|
82
|
-
} catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
if (!stat.isSymbolicLink()) return false;
|
|
86
|
-
let linkTarget;
|
|
87
|
-
try {
|
|
88
|
-
linkTarget = fs.readlinkSync(current);
|
|
89
|
-
} catch {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
if (isSensitive(linkTarget) && !isSafe(linkTarget)) return true;
|
|
93
|
-
if (isSafe(linkTarget)) return false;
|
|
94
|
-
const next = path.isAbsolute(linkTarget) ? linkTarget : path.resolve(path.dirname(current), linkTarget);
|
|
95
|
-
if (next === current) return true;
|
|
96
|
-
try {
|
|
97
|
-
const real = fs.realpathSync(next);
|
|
98
|
-
if (isSensitive(real) && !isSafe(real)) return true;
|
|
99
|
-
if (isSafe(real)) return false;
|
|
100
|
-
return false;
|
|
101
|
-
} catch {
|
|
102
|
-
current = next;
|
|
103
|
-
hops += 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
} catch {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function extractCommandPaths(command) {
|
|
114
|
-
// The analyzer parses the command and returns the operands of every reading
|
|
115
|
-
// command it recognises, nested shells and `find -exec` included. Treating
|
|
116
|
-
// every unresolved expansion as sensitive, as this did before, denied
|
|
117
|
-
// `cat $FILE`, `wc -l *.cjs` and `cat ~/.zshrc` alike -- and every denial here
|
|
118
|
-
// costs a round trip through a one-shot approval prompt.
|
|
119
|
-
// A heredoc whose delimiter is quoted (<<'BODY') is inert: the shell expands
|
|
120
|
-
// nothing inside it, so no word in the body is ever read. The analyzer has no
|
|
121
|
-
// notion of heredocs and parses the body as shell, so prose that quotes a
|
|
122
|
-
// command -- a PR body carrying `cat .env` in backticks -- came back as a real
|
|
123
|
-
// command substitution. Blank those bodies out first. An unquoted delimiter
|
|
124
|
-
// (<<BODY) does expand, so it is left to the analyzer.
|
|
125
|
-
const scrubbed = String(command).replace(
|
|
126
|
-
/(<<-?[ \t]*)(['"])([A-Za-z_]\w*)\2[^\n]*\n[\s\S]*?\n[ \t]*\3\b/g,
|
|
127
|
-
'$1$2$3$2'
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const paths = new Set(commandAccess(scrubbed).paths);
|
|
131
|
-
|
|
132
|
-
// Two shapes the analyzer cannot return. It drops any word carrying an
|
|
133
|
-
// expansion, so `~/.ssh/id_rsa` and `certs/*.pem` still have to be matched by
|
|
134
|
-
// name. And it only walks commands it recognises, so an unknown tool handed a
|
|
135
|
-
// secret through a file-shaped option, such as
|
|
136
|
-
// `docker compose --env-file .env`, is invisible to it.
|
|
137
|
-
const FILE_OPTION = /^--?[\w-]*(?:file|config|key|cert|identity)$/i;
|
|
138
|
-
const tokens = scrubbed.split(/[\s|;&<>"'()`]+/);
|
|
139
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
140
|
-
const token = tokens[index];
|
|
141
|
-
const shaped = /^~|[*?[{]/.test(token);
|
|
142
|
-
const optionFed = index > 0 && FILE_OPTION.test(tokens[index - 1]);
|
|
143
|
-
if (!shaped && !optionFed) continue;
|
|
144
|
-
const value = token.replace(/^~/, '').replace(/^["'()`]+|["'(),]+$/g, '');
|
|
145
|
-
if (value && isSensitive(value) && !isSafe(value)) paths.add(value);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return [...paths];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function extractPatchPaths(patch) {
|
|
152
|
-
return [...String(patch).matchAll(
|
|
153
|
-
/^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:)\s+(.+)$/gm
|
|
154
|
-
)]
|
|
155
|
-
.map((match) => match[1].trim())
|
|
156
|
-
.filter(Boolean);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function shellInput(input) {
|
|
160
|
-
if (typeof input?.command === 'string') return input.command;
|
|
161
|
-
if (typeof input?.cmd === 'string') return input.cmd;
|
|
162
|
-
return '';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function patchInput(input) {
|
|
166
|
-
if (typeof input === 'string') return input;
|
|
167
|
-
for (const key of ['command', 'patch', 'input']) {
|
|
168
|
-
if (typeof input?.[key] === 'string') return input[key];
|
|
169
|
-
}
|
|
170
|
-
return '';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function collectPathValues(value, key, out) {
|
|
174
|
-
if (typeof value === 'string' && isPathKey(key)) {
|
|
175
|
-
out.push(value);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (Array.isArray(value)) {
|
|
179
|
-
if (isPathKey(key)) out.push(...value.filter((item) => typeof item === 'string'));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (!value || typeof value !== 'object') return;
|
|
183
|
-
for (const [childKey, childValue] of Object.entries(value)) {
|
|
184
|
-
collectPathValues(childValue, childKey, out);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function extractPaths(toolName, input) {
|
|
189
|
-
const paths = [];
|
|
190
|
-
collectPathValues(input, '', paths);
|
|
191
|
-
const normalizedTool = String(toolName).toLowerCase();
|
|
192
|
-
if (['bash', 'exec_command', 'shell'].includes(normalizedTool)) {
|
|
193
|
-
paths.push(...extractCommandPaths(shellInput(input)));
|
|
194
|
-
} else if (['apply_patch', 'edit', 'write'].includes(normalizedTool)) {
|
|
195
|
-
paths.push(...extractPatchPaths(patchInput(input)));
|
|
196
|
-
}
|
|
197
|
-
return [...new Set(paths.filter(Boolean))];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function sensitiveRequest(filePath, cwd) {
|
|
201
|
-
const target = resolveTarget(filePath, cwd);
|
|
202
|
-
if (target) {
|
|
203
|
-
if (isSensitive(target) && !isSafe(target)) return true;
|
|
204
|
-
if (isSafe(target)) return false;
|
|
205
|
-
} else {
|
|
206
|
-
if (isSymlinkToSensitive(filePath, cwd)) return true;
|
|
207
|
-
}
|
|
208
|
-
return !isSafe(filePath) && isSensitive(filePath);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function sensitivePaths(toolName, input, cwd) {
|
|
212
|
-
return extractPaths(toolName, input)
|
|
213
|
-
.filter((filePath) => sensitiveRequest(filePath, cwd));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
4
|
function deny(reason) {
|
|
217
5
|
process.stdout.write(`${JSON.stringify({
|
|
218
6
|
hookSpecificOutput: {
|
|
@@ -224,9 +12,14 @@ function deny(reason) {
|
|
|
224
12
|
}
|
|
225
13
|
|
|
226
14
|
try {
|
|
15
|
+
const { APPROVAL_PREFIX, createPending, hasGrant } = require('./lib/privacy-state.cjs');
|
|
16
|
+
const { PROJECT_ROOT, readRuntime, resolveSessionCwd, readPayload } = require('./lib/hook-context.cjs');
|
|
17
|
+
const { sensitivePaths } = require('./lib/privacy-paths.cjs');
|
|
227
18
|
const data = readPayload();
|
|
228
19
|
if (!data) process.exit(0);
|
|
229
|
-
const
|
|
20
|
+
const projectRoot = PROJECT_ROOT;
|
|
21
|
+
const runtime = readRuntime(PROJECT_ROOT);
|
|
22
|
+
const sessionCwd = resolveSessionCwd(data);
|
|
230
23
|
if (runtime.privacyBlock === false) process.exit(0);
|
|
231
24
|
|
|
232
25
|
const filePaths = sensitivePaths(data.tool_name || '', data.tool_input || {}, sessionCwd);
|
|
@@ -250,8 +43,9 @@ try {
|
|
|
250
43
|
const pending = createPending(grantInput);
|
|
251
44
|
deny(
|
|
252
45
|
`Sensitive file access denied: ${pending.displayName}. ` +
|
|
253
|
-
`To
|
|
254
|
-
`
|
|
46
|
+
`To allow exactly these paths for one hour of this Codex session, the user replies ` +
|
|
47
|
+
`y (or yes / có) as their very next message; any other reply cancels that shortcut, ` +
|
|
48
|
+
`and it only works while this is the only pending request. Fallback exact prompt: ` +
|
|
255
49
|
`${APPROVAL_PREFIX}${pending.requestId}`
|
|
256
50
|
);
|
|
257
51
|
process.exit(0);
|
|
@@ -1,40 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const crypto = require('crypto');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const {
|
|
7
|
-
|
|
8
|
-
hookStateDir,
|
|
6
|
+
PROJECT_ROOT,
|
|
9
7
|
logCrash,
|
|
10
8
|
readPayload,
|
|
11
9
|
resolveProjectPath
|
|
12
10
|
} = require('./lib/hook-context.cjs');
|
|
13
|
-
|
|
14
|
-
function reservationFile(projectRoot, sessionId) {
|
|
15
|
-
const key = crypto.createHash('sha256')
|
|
16
|
-
.update(String(sessionId || 'unknown'))
|
|
17
|
-
.digest('hex')
|
|
18
|
-
.slice(0, 16);
|
|
19
|
-
return path.join(hookStateDir(projectRoot), `rules-${key}.json`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Reserve one injection slot per session. Returns null on reservation errors. */
|
|
23
|
-
function reserveSession(projectRoot, sessionId) {
|
|
24
|
-
if (!sessionId) return null;
|
|
25
|
-
try {
|
|
26
|
-
const file = reservationFile(projectRoot, sessionId);
|
|
27
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
-
fs.writeFileSync(file, JSON.stringify({ sessionId: String(sessionId) }), {
|
|
29
|
-
encoding: 'utf8',
|
|
30
|
-
flag: 'wx',
|
|
31
|
-
mode: 0o600
|
|
32
|
-
});
|
|
33
|
-
return true;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
return error.code === 'EEXIST' ? false : null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
11
|
+
const { reserveSession } = require('./lib/rules-reservation.cjs');
|
|
38
12
|
|
|
39
13
|
/**
|
|
40
14
|
* Body of the output style for `codingLevel`, or null.
|
|
@@ -70,10 +44,10 @@ function readRuntime(projectRoot) {
|
|
|
70
44
|
try {
|
|
71
45
|
const payload = readPayload();
|
|
72
46
|
if (!payload) process.exit(0);
|
|
73
|
-
const
|
|
74
|
-
if (reserveSession(projectRoot, payload.session_id) !== true) process.exit(0);
|
|
47
|
+
const projectRoot = PROJECT_ROOT;
|
|
75
48
|
const runtime = readRuntime(projectRoot);
|
|
76
49
|
if (runtime === null) process.exit(0);
|
|
50
|
+
if (reserveSession(projectRoot, payload.session_id) !== true) process.exit(0);
|
|
77
51
|
|
|
78
52
|
const respondLang = runtime.locale?.responseLanguage || '';
|
|
79
53
|
const thinkLang = runtime.locale?.thinkingLanguage || (respondLang ? 'en' : '');
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { logCrash, readPayload } = require('./lib/hook-context.cjs');
|
|
18
|
-
const { REMINDER, containsSecretKeyword } = require('./lib/secret-keywords.cjs');
|
|
19
18
|
|
|
20
19
|
try {
|
|
20
|
+
const { REMINDER, containsSecretKeyword } = require('./lib/secret-keywords.cjs');
|
|
21
21
|
const payload = readPayload();
|
|
22
22
|
if (!payload) process.exit(0);
|
|
23
23
|
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const { execFileSync } = require('child_process');
|
|
7
8
|
const {
|
|
8
|
-
|
|
9
|
+
PROJECT_ROOT,
|
|
10
|
+
hookStateDir,
|
|
11
|
+
readRuntime,
|
|
12
|
+
resolveSessionCwd,
|
|
9
13
|
logCrash,
|
|
10
14
|
readPayload
|
|
11
15
|
} = require('./lib/hook-context.cjs');
|
|
12
16
|
const { clearState } = require('./lib/privacy-state.cjs');
|
|
17
|
+
const { clearSessionReservation } = require('./lib/rules-reservation.cjs');
|
|
13
18
|
|
|
14
19
|
function readPackage(projectRoot) {
|
|
15
20
|
try {
|
|
@@ -69,10 +74,48 @@ function gitBranch(projectRoot) {
|
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
function printCompactAnchors(projectRoot, sessionId) {
|
|
78
|
+
if (!sessionId) return;
|
|
79
|
+
try {
|
|
80
|
+
const key = crypto.createHash('sha256').update(sessionId).digest('hex');
|
|
81
|
+
const file = path.join(hookStateDir(projectRoot), `compact-recovery-${key}.json`);
|
|
82
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
83
|
+
if (record.session_id !== sessionId) return;
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (record.branch) parts.push(`branch ${record.branch}`);
|
|
86
|
+
if (record.head) parts.push(`HEAD ${record.head}`);
|
|
87
|
+
if (typeof record.dirty_count === 'number') parts.push(`${record.dirty_count} uncommitted path(s)`);
|
|
88
|
+
process.stdout.write('\n=== State before compaction ===\n');
|
|
89
|
+
if (record.captured_at) process.stdout.write(`Captured: ${record.captured_at}\n`);
|
|
90
|
+
if (parts.length) process.stdout.write(`Repository: ${parts.join(' | ')}\n`);
|
|
91
|
+
process.stdout.write('Re-read these from disk before acting; they describe the moment before compaction.\n');
|
|
92
|
+
process.stdout.write('=== End of pre-compaction state ===\n');
|
|
93
|
+
} catch { /* no matching capture available */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
(async () => {
|
|
72
97
|
try {
|
|
73
98
|
const payload = readPayload();
|
|
74
99
|
if (!payload) process.exit(0);
|
|
75
|
-
|
|
100
|
+
// Invalidate this session before runtime configuration can interrupt startup.
|
|
101
|
+
const sessionId = [payload.session_id, payload.sessionId, payload.sessionID, payload.session?.id]
|
|
102
|
+
.find((value) => typeof value === 'string' && value.trim());
|
|
103
|
+
clearState(PROJECT_ROOT, sessionId);
|
|
104
|
+
let updateNotice = '';
|
|
105
|
+
try {
|
|
106
|
+
const { checkCafeKitUpdate } = require('./lib/update-check.cjs');
|
|
107
|
+
updateNotice = await checkCafeKitUpdate(path.join(PROJECT_ROOT, '.codex'));
|
|
108
|
+
} catch { /* an update failure must not interrupt session startup */ }
|
|
109
|
+
if (payload.source === 'compact') {
|
|
110
|
+
clearSessionReservation(PROJECT_ROOT, sessionId);
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
'Session compacted. Pending confirmations may have been lost. Pending CafeKit privacy approvals were invalidated; request fresh user approval before continuing.\n'
|
|
113
|
+
);
|
|
114
|
+
printCompactAnchors(PROJECT_ROOT, sessionId);
|
|
115
|
+
}
|
|
116
|
+
const projectRoot = PROJECT_ROOT;
|
|
117
|
+
const runtime = readRuntime(PROJECT_ROOT);
|
|
118
|
+
const sessionCwd = resolveSessionCwd(payload);
|
|
76
119
|
const pkg = readPackage(projectRoot);
|
|
77
120
|
const configured = runtime.project || {};
|
|
78
121
|
const projectType = configured.type && configured.type !== 'auto'
|
|
@@ -86,9 +129,6 @@ try {
|
|
|
86
129
|
: detectFramework(pkg);
|
|
87
130
|
const branch = gitBranch(projectRoot);
|
|
88
131
|
|
|
89
|
-
// Approval tokens never survive a session boundary or compaction.
|
|
90
|
-
clearState(projectRoot);
|
|
91
|
-
|
|
92
132
|
// Orca (onorca.dev) sets ORCA_PANE_KEY in every pane; it is a presence marker, not a
|
|
93
133
|
// secret. ORCA_AGENT_HOOK_TOKEN and ORCA_AGENT_LAUNCH_TOKEN live in the same
|
|
94
134
|
// environment and must never be read or printed here.
|
|
@@ -101,7 +141,7 @@ try {
|
|
|
101
141
|
insideOrcaPane && 'Orca: pane'
|
|
102
142
|
].filter(Boolean);
|
|
103
143
|
process.stdout.write(
|
|
104
|
-
`Session ${payload.source || 'unknown'}. ${parts.join(' | ') || 'No project info detected.'}\n`
|
|
144
|
+
`Session ${payload.source || 'unknown'}. ${parts.join(' | ') || 'No project info detected.'}${updateNotice}\n`
|
|
105
145
|
);
|
|
106
146
|
|
|
107
147
|
if (sessionCwd !== projectRoot) {
|
|
@@ -109,11 +149,7 @@ try {
|
|
|
109
149
|
`CafeKit project root: ${projectRoot} (session working directory: ${sessionCwd}).\n`
|
|
110
150
|
);
|
|
111
151
|
}
|
|
112
|
-
if (payload.source === 'compact') {
|
|
113
|
-
process.stdout.write(
|
|
114
|
-
'Session compacted. Pending CafeKit privacy approvals were invalidated; request fresh user approval if needed.\n'
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
152
|
} catch (error) {
|
|
118
153
|
logCrash('session', error);
|
|
119
154
|
}
|
|
155
|
+
})();
|
|
@@ -11,14 +11,11 @@ const {
|
|
|
11
11
|
readPayload
|
|
12
12
|
} = require('./lib/hook-context.cjs');
|
|
13
13
|
const {
|
|
14
|
-
checkFeatureReceipt,
|
|
15
|
-
checkReceiptDetails,
|
|
16
14
|
checkWorkflowReceiptDetails,
|
|
17
15
|
checkWorkflowReceiptSet,
|
|
18
16
|
loadSharedPolicy,
|
|
19
17
|
receiptFixHint,
|
|
20
18
|
} = require('./lib/spec-receipt.cjs');
|
|
21
|
-
const { taskStatusMap } = require('./lib/spec-utils.cjs');
|
|
22
19
|
|
|
23
20
|
function emitBlock(reason) {
|
|
24
21
|
process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\n`);
|
|
@@ -34,6 +31,7 @@ function sessionIdentity(payload) {
|
|
|
34
31
|
return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
let hasDoneTasks = false;
|
|
37
35
|
try {
|
|
38
36
|
const payload = readPayload();
|
|
39
37
|
if (payload.stop_hook_active === true) process.exit(0);
|
|
@@ -44,8 +42,10 @@ try {
|
|
|
44
42
|
process.exit(0);
|
|
45
43
|
}
|
|
46
44
|
const POLICY = loaded.policy;
|
|
45
|
+
// Match Claude: one invocation validates receipts against one checkout snapshot.
|
|
46
|
+
require(path.join(path.dirname(loaded.path), 'provenance.cjs')).enableSnapshotMemo();
|
|
47
47
|
const { projectRoot, runtime } = getHookContext(payload);
|
|
48
|
-
if (typeof POLICY.
|
|
48
|
+
if (typeof POLICY.deriveRuntimeContext !== 'function') {
|
|
49
49
|
throw new Error('shared workflow policy lacks completion authority functions');
|
|
50
50
|
}
|
|
51
51
|
const resolver = require('./lib/spec-utils.cjs');
|
|
@@ -59,6 +59,8 @@ try {
|
|
|
59
59
|
}
|
|
60
60
|
if (!resolved) process.exit(0);
|
|
61
61
|
if (resolved.layoutKind === 'process-v3-completed-set') {
|
|
62
|
+
hasDoneTasks = resolved.candidates.some(candidate =>
|
|
63
|
+
Object.values(candidate.taskRegistry || {}).some(task => task?.status === 'done'));
|
|
62
64
|
const configuredGate = runtime.spec?.completion_gate;
|
|
63
65
|
if (configuredGate !== undefined && configuredGate !== true) {
|
|
64
66
|
emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
|
|
@@ -88,7 +90,7 @@ try {
|
|
|
88
90
|
if (resolved.error === 'invalid_specs') {
|
|
89
91
|
process.stdout.write(`${JSON.stringify({
|
|
90
92
|
decision: 'block',
|
|
91
|
-
reason: `Completion gate: invalid
|
|
93
|
+
reason: `Completion gate: invalid workflow packet detected (${resolved.candidates.join(', ')}): ${resolved.reason}. Fix the packet's plan.md or task files before completing tasks.`
|
|
92
94
|
})}\n`);
|
|
93
95
|
process.exit(0);
|
|
94
96
|
}
|
|
@@ -101,20 +103,11 @@ try {
|
|
|
101
103
|
}
|
|
102
104
|
if (resolved.error) process.exit(0);
|
|
103
105
|
const active = resolved;
|
|
104
|
-
|
|
105
|
-
const processWorkflow = active.layoutKind === 'process-v3';
|
|
106
|
-
const lifecyclePhase = activeSpec.current_phase || activeSpec.phase;
|
|
107
|
-
const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
|
|
108
|
-
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
109
|
-
// Legacy 2.1 closeout is retired; a 2.1 packet mid-execution keeps the ordinary receipt checks.
|
|
110
|
-
if (!processWorkflow && activeSpec.schema_version === '2.1' && explicitCloseout) {
|
|
111
|
-
emitBlock('Completion gate: Legacy spec.json closeout is no longer supported; move the packet to plan.md with flat task files');
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
106
|
+
hasDoneTasks = Object.values(active.taskRegistry || {}).some(task => task?.status === 'done');
|
|
114
107
|
const runtimeContext = POLICY.deriveRuntimeContext({
|
|
115
108
|
projectRoot,
|
|
116
109
|
specsRoot: active.specsDir,
|
|
117
|
-
specFile: active.stateFile
|
|
110
|
+
specFile: active.stateFile,
|
|
118
111
|
featureName: active.featureName,
|
|
119
112
|
runtimeSession: sessionIdentity(payload),
|
|
120
113
|
});
|
|
@@ -123,10 +116,10 @@ try {
|
|
|
123
116
|
emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
|
|
124
117
|
process.exit(0);
|
|
125
118
|
}
|
|
126
|
-
const registry = active.taskRegistry ||
|
|
127
|
-
const currentStatuses =
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
const registry = active.taskRegistry || {};
|
|
120
|
+
const currentStatuses = Object.fromEntries(
|
|
121
|
+
Object.entries(registry).map(([taskPath, task]) => [taskPath, task?.status || 'pending']),
|
|
122
|
+
);
|
|
130
123
|
const cacheFile = path.join(hookStateDir(projectRoot), 'spec-gate-last.json');
|
|
131
124
|
const cacheExists = fs.existsSync(cacheFile);
|
|
132
125
|
let cache = {};
|
|
@@ -152,86 +145,51 @@ try {
|
|
|
152
145
|
const previous = cacheExists && cacheEntries[runtimeContext.context_id]
|
|
153
146
|
? cacheEntries[runtimeContext.context_id].tasks || {}
|
|
154
147
|
: {};
|
|
155
|
-
const staleFlashTasks = processWorkflow ? [] : Object.entries(registry)
|
|
156
|
-
.filter(([, task]) => POLICY.isStaleFlashDone(task))
|
|
157
|
-
.map(([taskPath]) => taskPath);
|
|
158
|
-
if (staleFlashTasks.length > 0) {
|
|
159
|
-
process.stdout.write(`${JSON.stringify({
|
|
160
|
-
decision: 'block',
|
|
161
|
-
reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /cf:test, then use explicit sync-finalize.`
|
|
162
|
-
})}\n`);
|
|
163
|
-
process.exit(0);
|
|
164
|
-
}
|
|
165
148
|
const allDoneTasks = Object.keys(registry).filter((taskPath) => (
|
|
166
149
|
currentStatuses[taskPath] === 'done'
|
|
167
150
|
));
|
|
168
151
|
const featureDir = path.join(active.specsDir, active.featureName);
|
|
169
|
-
const receiptBodies = new Map();
|
|
170
152
|
const failures = allDoneTasks
|
|
171
153
|
.map((taskPath) => {
|
|
172
|
-
const proof =
|
|
173
|
-
? checkWorkflowReceiptDetails(featureDir, taskPath, runtimeContext)
|
|
174
|
-
: checkReceiptDetails(featureDir, taskPath, registry[taskPath], runtimeContext);
|
|
175
|
-
if (proof.body) receiptBodies.set(taskPath, proof.body);
|
|
154
|
+
const proof = checkWorkflowReceiptDetails(featureDir, taskPath, runtimeContext);
|
|
176
155
|
return {
|
|
177
156
|
taskPath,
|
|
157
|
+
body: proof.body,
|
|
178
158
|
failures: proof.failures
|
|
179
159
|
};
|
|
180
160
|
})
|
|
181
161
|
.filter((result) => result.failures.length);
|
|
182
162
|
|
|
183
|
-
const featureCloseoutRequired = !processWorkflow && explicitCloseout && allDoneTasks.length > 0;
|
|
184
|
-
const featureReceipt = featureCloseoutRequired
|
|
185
|
-
? checkFeatureReceipt(featureDir, runtimeContext)
|
|
186
|
-
: null;
|
|
187
|
-
if (featureReceipt && featureReceipt.failures.length) {
|
|
188
|
-
failures.push({ taskPath: 'feature-receipt.md', failures: featureReceipt.failures });
|
|
189
|
-
}
|
|
190
|
-
const completion = featureCloseoutRequired && featureReceipt?.failures.length === 0
|
|
191
|
-
&& Object.prototype.hasOwnProperty.call(active.spec, 'workflow_policy')
|
|
192
|
-
? POLICY.completionDecisionForSpec(active.spec, {
|
|
193
|
-
runtimeContext,
|
|
194
|
-
executionReceipt: featureReceipt.body,
|
|
195
|
-
taskContext: {},
|
|
196
|
-
})
|
|
197
|
-
: null;
|
|
198
|
-
|
|
199
163
|
const next = { ...previous, ...currentStatuses };
|
|
200
164
|
for (const result of failures) {
|
|
201
165
|
if (previous[result.taskPath] === undefined) delete next[result.taskPath];
|
|
202
166
|
else next[result.taskPath] = previous[result.taskPath];
|
|
203
167
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
168
|
+
try {
|
|
169
|
+
atomicWrite(cacheFile, `${JSON.stringify({
|
|
170
|
+
entries: { ...cacheEntries, [runtimeContext.context_id]: { identity: cacheIdentity, tasks: next } },
|
|
171
|
+
})}\n`);
|
|
172
|
+
} catch { /* Cache persistence is not completion authority. */ }
|
|
208
173
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const lines = [
|
|
212
|
-
failures.length > 0
|
|
213
|
-
? `Completion gate: ${failures.length} done task(s) lack a verification receipt.`
|
|
214
|
-
: 'Completion gate: workflow completion proof is incomplete.'
|
|
215
|
-
];
|
|
174
|
+
if (!failures.length) process.exit(0);
|
|
175
|
+
const lines = [`Completion gate: ${failures.length} done task(s) lack a verification receipt.`];
|
|
216
176
|
for (const result of failures) {
|
|
217
177
|
lines.push(`- \`${result.taskPath}\`: failed check(s) ${result.failures.join(', ')}`);
|
|
218
|
-
lines.push(result.taskPath
|
|
219
|
-
? ` Run final integration proof, then write \`specs/${active.featureName}/feature-receipt.md\`.`
|
|
220
|
-
: processWorkflow
|
|
221
|
-
? ` In \`specs/${active.featureName}/${path.posix.basename(result.taskPath)}\`: ${receiptFixHint(result.failures, result.body) || 'write a runtime-bound `## Receipt` with command output'}.`
|
|
222
|
-
: ` Write canonical proof to \`specs/${active.featureName}/receipts/${path.posix.basename(result.taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
|
|
223
|
-
}
|
|
224
|
-
if (completionBlocked) {
|
|
225
|
-
lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
|
|
226
|
-
if (Array.isArray(completion.missingProof) && completion.missingProof.length > 0) {
|
|
227
|
-
lines.push(` Missing proof: ${completion.missingProof.join(', ')}`);
|
|
228
|
-
}
|
|
178
|
+
lines.push(` In \`specs/${active.featureName}/${path.posix.basename(result.taskPath)}\`: ${receiptFixHint(result.failures, result.body) || 'write a runtime-bound `## Receipt` with command output'}.`);
|
|
229
179
|
}
|
|
230
180
|
process.stdout.write(`${JSON.stringify({
|
|
231
181
|
decision: 'block',
|
|
232
182
|
reason: lines.slice(0, 8).join('\n')
|
|
233
183
|
})}\n`);
|
|
234
184
|
} catch (error) {
|
|
185
|
+
if (error.code === 'no_git_history') {
|
|
186
|
+
if (hasDoneTasks) emitBlock('Completion gate: repo has no commits yet; create the first commit, then record Base/Head after it.');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
235
189
|
logCrash('spec-gate', error);
|
|
236
|
-
|
|
190
|
+
if (/^(?:runtime\.paths\.specs|configured specs root)/.test(error.message)) {
|
|
191
|
+
emitBlock(`Completion gate: invalid workflow packet detected (<specs>): ${error.message}. Fix the packet's plan.md or task files before completing tasks.`);
|
|
192
|
+
} else {
|
|
193
|
+
emitBlock(`Completion gate controlled failure: ${error.message}. Completion is blocked until the hook is repaired.`);
|
|
194
|
+
}
|
|
237
195
|
}
|