@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
|
@@ -25,9 +25,16 @@ function cacheFile(projectRoot, sessionId) {
|
|
|
25
25
|
return path.join(hookStateDir(projectRoot), `tollgate-${key}.txt`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
let mutationObserver = false;
|
|
28
29
|
try {
|
|
29
30
|
const payload = readPayload();
|
|
31
|
+
mutationObserver = payload?.hook_event_name === 'PostToolUse';
|
|
30
32
|
if (!payload) process.exit(0);
|
|
33
|
+
const context = getHookContext(payload);
|
|
34
|
+
const { touchSession } = require('./lib/spec-session-touch.cjs');
|
|
35
|
+
const touch = touchSession(payload, context);
|
|
36
|
+
// Mutation observers only persist paths. They must not inspect packet state or proof.
|
|
37
|
+
if (payload.hook_event_name === 'PostToolUse') process.exit(0);
|
|
31
38
|
const loaded = loadSharedPolicy();
|
|
32
39
|
if (!loaded.policy) {
|
|
33
40
|
logCrash('spec-state', loaded.error);
|
|
@@ -35,22 +42,57 @@ try {
|
|
|
35
42
|
process.exit(0);
|
|
36
43
|
}
|
|
37
44
|
const POLICY = loaded.policy;
|
|
45
|
+
require(path.join(path.dirname(loaded.path), 'provenance.cjs')).enableSnapshotMemo();
|
|
38
46
|
const receiptLoaded = loadSharedReceipt();
|
|
39
47
|
if (!receiptLoaded.receipt) throw receiptLoaded.error;
|
|
40
48
|
const RECEIPT = receiptLoaded.receipt;
|
|
41
|
-
const { projectRoot, runtime } =
|
|
49
|
+
const { projectRoot, runtime } = context;
|
|
42
50
|
if (runtime.spec?.tollgate === false) process.exit(0);
|
|
43
|
-
const { resolveWorkflowCandidate } = require('./lib/spec-utils.cjs');
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const { findLegacyPackets, resolveWorkflowCandidate } = require('./lib/spec-utils.cjs');
|
|
52
|
+
const sessionId = touch.sessionId;
|
|
53
|
+
// A legacy packet (a specs/<x>/ directory without plan.md that the old flow left
|
|
54
|
+
// behind) is no longer read. Say so once per session instead of on every prompt.
|
|
55
|
+
const legacyPackets = findLegacyPackets(projectRoot, runtime);
|
|
56
|
+
if (legacyPackets.length > 0) {
|
|
57
|
+
const noticeFile = sessionId
|
|
58
|
+
? path.join(hookStateDir(projectRoot), `legacy-notice-${crypto.createHash('sha256').update(String(sessionId)).digest('hex').slice(0, 16)}.json`)
|
|
59
|
+
: null;
|
|
60
|
+
let noticed = [];
|
|
61
|
+
if (noticeFile) {
|
|
62
|
+
try { noticed = JSON.parse(fs.readFileSync(noticeFile, 'utf8')); } catch { noticed = []; }
|
|
63
|
+
if (!Array.isArray(noticed)) noticed = [];
|
|
64
|
+
}
|
|
65
|
+
const fresh = legacyPackets.filter((name) => !noticed.includes(name));
|
|
66
|
+
for (const name of fresh) {
|
|
67
|
+
process.stdout.write(`> ⚠️ \`specs/${name}\` is a legacy Specs packet without plan.md; CafeKit no longer reads it. Move ongoing work to a process-first plan.md packet.\n`);
|
|
68
|
+
}
|
|
69
|
+
if (noticeFile && fresh.length > 0) {
|
|
70
|
+
try { atomicWrite(noticeFile, `${JSON.stringify([...noticed, ...fresh].sort())}\n`); } catch { /* repeats the notice */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const explicitFeature = payload.explicitFeature ?? payload.featureName ?? payload.feature ?? null;
|
|
74
|
+
const explicitPath = payload.explicitPath ?? payload.specPath ?? payload.spec_path ?? payload.featurePath ?? null;
|
|
75
|
+
const hasExplicit = explicitFeature !== null || explicitPath !== null;
|
|
76
|
+
if (sessionId && !hasExplicit && touch.touched.length === 0) process.exit(0);
|
|
77
|
+
let resolved;
|
|
78
|
+
if (sessionId && !hasExplicit) {
|
|
79
|
+
const names = touch.names.length ? touch.names : touch.touched;
|
|
80
|
+
const candidates = names.map((name) => resolveWorkflowCandidate(projectRoot, runtime, name, null));
|
|
81
|
+
resolved = candidates.find((candidate) => candidate?.error);
|
|
82
|
+
const active = candidates.filter((candidate) => candidate && !candidate.error && !candidate.allTasksDone);
|
|
83
|
+
if (!resolved) resolved = active.length > 1
|
|
84
|
+
? { error: 'multiple_active', candidates: active.map((candidate) => candidate.featureName) }
|
|
85
|
+
: active[0] || null;
|
|
86
|
+
} else {
|
|
87
|
+
resolved = resolveWorkflowCandidate(projectRoot, runtime, explicitFeature, explicitPath);
|
|
88
|
+
}
|
|
47
89
|
if (!resolved) process.exit(0);
|
|
48
90
|
if (resolved.error === 'multiple_active') {
|
|
49
91
|
process.stdout.write(`> ⚠️ Multiple active specs detected: ${resolved.candidates.join(', ')}. Provide explicit feature target or resolve ambiguity. Tollgate paused.\n`);
|
|
50
92
|
process.exit(0);
|
|
51
93
|
}
|
|
52
94
|
if (resolved.error === 'invalid_specs') {
|
|
53
|
-
process.stdout.write(`> ⚠️ Invalid
|
|
95
|
+
process.stdout.write(`> ⚠️ Invalid workflow packet detected: ${resolved.candidates.join(', ')}. ${resolved.reason}. Fix the packet's plan.md or task files. Tollgate paused.\n`);
|
|
54
96
|
process.exit(0);
|
|
55
97
|
}
|
|
56
98
|
if (resolved.error === 'explicit_not_found' || resolved.error === 'explicit_malformed') {
|
|
@@ -60,18 +102,16 @@ try {
|
|
|
60
102
|
}
|
|
61
103
|
if (resolved.error) process.exit(0);
|
|
62
104
|
const active = resolved;
|
|
63
|
-
const processWorkflow = active.layoutKind === 'process-v3';
|
|
64
105
|
const runtimeContext = POLICY.deriveRuntimeContext({
|
|
65
106
|
projectRoot,
|
|
66
107
|
specsRoot: active.specsDir,
|
|
67
|
-
specFile: active.stateFile
|
|
108
|
+
specFile: active.stateFile,
|
|
68
109
|
featureName: active.featureName,
|
|
69
|
-
runtimeSession:
|
|
110
|
+
runtimeSession: sessionId,
|
|
70
111
|
});
|
|
71
112
|
|
|
72
|
-
const phase = active.phase ||
|
|
73
|
-
const taskRegistry = active.taskRegistry ||
|
|
74
|
-
const flashTasks = POLICY.flashState(taskRegistry);
|
|
113
|
+
const phase = active.phase || 'unknown';
|
|
114
|
+
const taskRegistry = active.taskRegistry || {};
|
|
75
115
|
const tasks = Object.entries(taskRegistry);
|
|
76
116
|
const counts = tasks.reduce((result, [, task]) => {
|
|
77
117
|
const status = task?.status || 'pending';
|
|
@@ -79,13 +119,7 @@ try {
|
|
|
79
119
|
return result;
|
|
80
120
|
}, {});
|
|
81
121
|
const featureDir = path.join(active.specsDir, active.featureName);
|
|
82
|
-
const
|
|
83
|
-
taskPath,
|
|
84
|
-
task?.status || 'pending'
|
|
85
|
-
]));
|
|
86
|
-
const dependencyProof = processWorkflow
|
|
87
|
-
? RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY)
|
|
88
|
-
: {};
|
|
122
|
+
const dependencyProof = RECEIPT.workflowDependencyProofState(featureDir, taskRegistry, runtimeContext, POLICY);
|
|
89
123
|
const taskState = tasks.map(([taskPath, task]) => [
|
|
90
124
|
taskPath,
|
|
91
125
|
task?.status || 'pending',
|
|
@@ -101,12 +135,8 @@ try {
|
|
|
101
135
|
const next = tasks.find(([, task]) => (
|
|
102
136
|
active.queueReady !== false
|
|
103
137
|
&& (task?.status || 'pending') === 'pending'
|
|
104
|
-
&& (task?.dependencies || []).every((dependency) =>
|
|
105
|
-
processWorkflow ? dependencyProof[dependency]?.eligible === true : statuses.get(dependency) === 'done'
|
|
106
|
-
))
|
|
138
|
+
&& (task?.dependencies || []).every((dependency) => dependencyProof[dependency]?.eligible === true)
|
|
107
139
|
));
|
|
108
|
-
const featureReceiptPresent = !processWorkflow
|
|
109
|
-
&& RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
|
|
110
140
|
const stateKey = JSON.stringify({
|
|
111
141
|
project_root: runtimeContext.project_root,
|
|
112
142
|
specs_root: runtimeContext.specs_root,
|
|
@@ -124,14 +154,13 @@ try {
|
|
|
124
154
|
proof_state: proofState,
|
|
125
155
|
done: counts.done || 0,
|
|
126
156
|
total: tasks.length,
|
|
127
|
-
feature_receipt_present: featureReceiptPresent,
|
|
128
157
|
});
|
|
129
158
|
const cache = cacheFile(projectRoot, runtimeContext.runtime_session);
|
|
130
159
|
let previous = '';
|
|
131
160
|
try { previous = fs.readFileSync(cache, 'utf8').trim(); } catch { /* first run */ }
|
|
132
161
|
|
|
133
162
|
if (previous === stateKey) {
|
|
134
|
-
const migration =
|
|
163
|
+
const migration = active.queueReady === false
|
|
135
164
|
? ' Add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.'
|
|
136
165
|
: '';
|
|
137
166
|
process.stdout.write(
|
|
@@ -145,30 +174,23 @@ try {
|
|
|
145
174
|
const lines = [
|
|
146
175
|
`### Spec state changed: \`${active.featureName}\``,
|
|
147
176
|
`- Phase: \`${phase}\` | Tasks: ${counts.done || 0} done / ${tasks.length} total`
|
|
177
|
+
+ (tasks.length > 0 ? ` (${counts.in_progress || 0} in_progress, ${counts.pending || 0} pending, ${counts.blocked || 0} blocked)` : '')
|
|
148
178
|
];
|
|
149
179
|
if (next) lines.push(`- Next unblocked: \`${next[0]}\``);
|
|
150
|
-
if (
|
|
151
|
-
lines.push(
|
|
152
|
-
}
|
|
153
|
-
if (processWorkflow) {
|
|
154
|
-
if (active.queueReady === false) {
|
|
155
|
-
lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
|
|
156
|
-
}
|
|
157
|
-
lines.push(
|
|
158
|
-
'- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
|
|
159
|
-
`- Workflow source: \`specs/${active.featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`,
|
|
160
|
-
'- Hooks revalidate receipt bytes but never grant approval.'
|
|
161
|
-
);
|
|
162
|
-
} else {
|
|
163
|
-
lines.push(
|
|
164
|
-
'- Sync `spec.json` and task Markdown status only after verified work; task proof belongs in `receipts/<task-basename>.md`.',
|
|
165
|
-
`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`,
|
|
166
|
-
`- Validate with \`node .codex/scripts/validate-spec-output.cjs specs/${active.featureName}\`.`,
|
|
167
|
-
'- Hooks revalidate receipt bytes but never grant approval.'
|
|
168
|
-
);
|
|
180
|
+
if (active.queueReady === false) {
|
|
181
|
+
lines.push('- Migration required: add `Specs-Contract: process-first-ready-v1` after the plan title before tasks can enter Next.');
|
|
169
182
|
}
|
|
183
|
+
lines.push(
|
|
184
|
+
'- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.',
|
|
185
|
+
`- Workflow source: \`specs/${active.featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`,
|
|
186
|
+
'- Hooks revalidate receipt bytes but never grant approval.'
|
|
187
|
+
);
|
|
170
188
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
171
189
|
} catch (error) {
|
|
190
|
+
if (error.code === 'no_git_history') {
|
|
191
|
+
console.log('Repo has no commits yet; create the first commit, then record Base/Head after it.');
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
172
194
|
logCrash('spec-state', error);
|
|
173
|
-
emitControlledFailure(error.message);
|
|
195
|
+
if (!mutationObserver) emitControlledFailure(error.message);
|
|
174
196
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { containsSecretKeyword } = require('./lib/secret-keywords.cjs');
|
|
6
7
|
const {
|
|
7
8
|
getHookContext,
|
|
8
9
|
logCrash,
|
|
@@ -34,6 +35,11 @@ function summary(value, limit = 600) {
|
|
|
34
35
|
return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function assistantSummary(value) {
|
|
39
|
+
const text = summary(value, Number.MAX_SAFE_INTEGER);
|
|
40
|
+
return containsSecretKeyword(text) ? '[omitted: possible secret]' : summary(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
function normalizeTodos(items) {
|
|
38
44
|
if (!Array.isArray(items)) return [];
|
|
39
45
|
return items.map((item, index) => ({
|
|
@@ -74,11 +80,24 @@ function isTrackedTool(toolName) {
|
|
|
74
80
|
const name = String(toolName || '').toLowerCase();
|
|
75
81
|
return name === 'agent'
|
|
76
82
|
|| name === 'spawn_agent'
|
|
83
|
+
|| name === 'collaborationspawn_agent'
|
|
77
84
|
|| name === 'update_plan'
|
|
78
85
|
|| name.includes('task')
|
|
79
86
|
|| name.includes('todo');
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
function hasPriorContent(text) {
|
|
90
|
+
const filler = [
|
|
91
|
+
/^#{1,6}\s/,
|
|
92
|
+
/^<!--.*-->$/,
|
|
93
|
+
/^- \((?:No completed tasks recorded|No pending tasks recorded|All tasks completed|No file changes detected)\)$/,
|
|
94
|
+
/^- Completed at \d{2}:\d{2}:\d{2}$/,
|
|
95
|
+
/^- Completed without a result message\.$/,
|
|
96
|
+
];
|
|
97
|
+
return text.split('\n').map((line) => line.trim())
|
|
98
|
+
.some((line) => line && !filler.some((pattern) => pattern.test(line)));
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
function loadPrevious(projectRoot, sessionId) {
|
|
83
102
|
try {
|
|
84
103
|
const dir = stateDir(projectRoot, sessionId);
|
|
@@ -87,7 +106,9 @@ function loadPrevious(projectRoot, sessionId) {
|
|
|
87
106
|
if (!fs.existsSync(file)) return '';
|
|
88
107
|
const text = fs.readFileSync(file, 'utf8');
|
|
89
108
|
const generated = text.match(/<!-- Generated: (.+?) -->/)?.[1];
|
|
90
|
-
|
|
109
|
+
const timestamp = Date.parse(generated);
|
|
110
|
+
if (!generated || !Number.isFinite(timestamp) || Date.now() - timestamp > EXPIRY_MS) return '';
|
|
111
|
+
if (!hasPriorContent(text)) return '';
|
|
91
112
|
return text;
|
|
92
113
|
} catch {
|
|
93
114
|
return '';
|
|
@@ -118,7 +139,7 @@ try {
|
|
|
118
139
|
const data = readData(dir);
|
|
119
140
|
const input = payload.tool_input || {};
|
|
120
141
|
const response = payload.tool_response;
|
|
121
|
-
updateTaskState(data, input, response);
|
|
142
|
+
if (!['spawn_agent', 'collaborationspawn_agent'].includes(String(toolName).toLowerCase())) updateTaskState(data, input, response);
|
|
122
143
|
data.lastToolEvent = `${toolName}: ${summary(response) || summary(input)}`.slice(0, 700);
|
|
123
144
|
refreshGit(data, projectRoot);
|
|
124
145
|
writeData(dir, data);
|
|
@@ -135,7 +156,7 @@ try {
|
|
|
135
156
|
data.agentResults.push({
|
|
136
157
|
id,
|
|
137
158
|
type: String(payload.agent_type || 'unknown'),
|
|
138
|
-
message:
|
|
159
|
+
message: assistantSummary(payload.last_assistant_message)
|
|
139
160
|
});
|
|
140
161
|
refreshGit(data, projectRoot);
|
|
141
162
|
writeData(dir, data);
|
|
@@ -147,7 +168,7 @@ try {
|
|
|
147
168
|
if (event === 'Stop') {
|
|
148
169
|
withStateLock(projectRoot, sessionId, (dir) => {
|
|
149
170
|
const data = readData(dir);
|
|
150
|
-
data.lastAssistant =
|
|
171
|
+
data.lastAssistant = assistantSummary(payload.last_assistant_message);
|
|
151
172
|
refreshGit(data, projectRoot);
|
|
152
173
|
writeData(dir, data);
|
|
153
174
|
archive(dir);
|
package/src/codex/hooks.json
CHANGED
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
"command": "node \".codex/hooks/session.cjs\"",
|
|
11
11
|
"commandWindows": "node \".codex/hooks/session.cjs\""
|
|
12
12
|
},
|
|
13
|
-
{
|
|
14
|
-
"type": "command",
|
|
15
|
-
"command": "node \".codex/hooks/docs-sync.cjs\"",
|
|
16
|
-
"commandWindows": "node \".codex/hooks/docs-sync.cjs\""
|
|
17
|
-
},
|
|
18
13
|
{
|
|
19
14
|
"type": "command",
|
|
20
15
|
"command": "node \".codex/hooks/state.cjs\"",
|
|
@@ -23,6 +18,18 @@
|
|
|
23
18
|
]
|
|
24
19
|
}
|
|
25
20
|
],
|
|
21
|
+
"PreCompact": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "auto|manual",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "node \".codex/hooks/precompact.cjs\"",
|
|
28
|
+
"commandWindows": "node \".codex/hooks/precompact.cjs\""
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
26
33
|
"SubagentStart": [
|
|
27
34
|
{
|
|
28
35
|
"matcher": "*",
|
|
@@ -57,13 +64,18 @@
|
|
|
57
64
|
"type": "command",
|
|
58
65
|
"command": "node \".codex/hooks/spec-state.cjs\"",
|
|
59
66
|
"commandWindows": "node \".codex/hooks/spec-state.cjs\""
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "node \".codex/hooks/ask-guard.cjs\"",
|
|
71
|
+
"commandWindows": "node \".codex/hooks/ask-guard.cjs\""
|
|
60
72
|
}
|
|
61
73
|
]
|
|
62
74
|
}
|
|
63
75
|
],
|
|
64
76
|
"PreToolUse": [
|
|
65
77
|
{
|
|
66
|
-
"matcher": "Bash|exec_command|apply_patch|
|
|
78
|
+
"matcher": "Bash|exec_command|shell|apply_patch|mcp__.*|collaborationspawn_agent|collaborationwait_agent",
|
|
67
79
|
"hooks": [
|
|
68
80
|
{
|
|
69
81
|
"type": "command",
|
|
@@ -76,11 +88,21 @@
|
|
|
76
88
|
"commandWindows": "node \".codex/hooks/inspect-block.cjs\""
|
|
77
89
|
}
|
|
78
90
|
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"matcher": "apply_patch",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "node \".codex/hooks/ask-guard.cjs\"",
|
|
98
|
+
"commandWindows": "node \".codex/hooks/ask-guard.cjs\""
|
|
99
|
+
}
|
|
100
|
+
]
|
|
79
101
|
}
|
|
80
102
|
],
|
|
81
103
|
"PostToolUse": [
|
|
82
104
|
{
|
|
83
|
-
"matcher": "Agent|spawn_agent|update_plan
|
|
105
|
+
"matcher": "collaborationspawn_agent|Agent|spawn_agent|update_plan",
|
|
84
106
|
"hooks": [
|
|
85
107
|
{
|
|
86
108
|
"type": "command",
|
|
@@ -88,6 +110,16 @@
|
|
|
88
110
|
"commandWindows": "node \".codex/hooks/state.cjs\""
|
|
89
111
|
}
|
|
90
112
|
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"matcher": "apply_patch",
|
|
116
|
+
"hooks": [
|
|
117
|
+
{
|
|
118
|
+
"type": "command",
|
|
119
|
+
"command": "node \".codex/hooks/spec-state.cjs\"",
|
|
120
|
+
"commandWindows": "node \".codex/hooks/spec-state.cjs\""
|
|
121
|
+
}
|
|
122
|
+
]
|
|
91
123
|
}
|
|
92
124
|
],
|
|
93
125
|
"Stop": [
|
|
@@ -5,16 +5,22 @@ Hooks are instruction boundaries. Do not bypass or work around them.
|
|
|
5
5
|
## Privacy Block Hook
|
|
6
6
|
|
|
7
7
|
Codex cannot pause a blocked tool call for an interactive permission question.
|
|
8
|
-
CafeKit
|
|
9
|
-
phrase
|
|
8
|
+
CafeKit denies sensitive access and offers a short reply and an exact fallback
|
|
9
|
+
phrase. A grant is reusable for one hour in the same session for exactly the
|
|
10
|
+
approved paths; it is not tied to a tool.
|
|
10
11
|
|
|
11
12
|
Required flow:
|
|
12
13
|
|
|
13
14
|
1. Stop the blocked access and show the user the hook's reason.
|
|
14
|
-
2. Ask
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
2. Ask one privacy question as the only question at the end of the turn, naming
|
|
16
|
+
exactly the files shown in the denial. Do not add any other yes/no question.
|
|
17
|
+
3. Explain that the user's very next message may be `y`, `yes`, or `có` only
|
|
18
|
+
while exactly one request is pending; any other reply cancels the shortcut.
|
|
19
|
+
The exact `APPROVE CAFEKIT PRIVACY <id>` prompt remains the fallback.
|
|
20
|
+
4. Never send, repeat as a user message, or simulate either a short approval or
|
|
21
|
+
the exact fallback phrase. Wait for a real user prompt and the approval hook.
|
|
22
|
+
5. After approval, tell the user which paths were just granted before reading
|
|
23
|
+
them. Access only those exact paths during the one-hour session grant.
|
|
18
24
|
|
|
19
25
|
Never use another command, path, encoding, subprocess, or side channel to read a
|
|
20
26
|
privacy-blocked file without that explicit approval.
|
|
@@ -24,6 +30,7 @@ privacy-blocked file without that explicit approval.
|
|
|
24
30
|
- If a hook reports state drift, run the appropriate sync or audit flow before continuing.
|
|
25
31
|
- If a hook rejects task completion, keep the task `in_progress` or `blocked` until proof exists.
|
|
26
32
|
- Do not mark a task `done` unless the matching task file contains a valid verification receipt.
|
|
33
|
+
- Read `state-sync.md` for the receipt contract.
|
|
27
34
|
|
|
28
35
|
## Hook Failure Handling
|
|
29
36
|
|
package/src/common/AGENTS.md
CHANGED
|
@@ -33,17 +33,11 @@ Do the work yourself when it takes a handful of tool calls. Delegate genuinely i
|
|
|
33
33
|
- Runtime-specific instructions live in that runtime's own managed block, not in `CORE`.
|
|
34
34
|
- In a combined install, consume `CORE` plus your native block only. Ignore managed blocks not owned by your runtime. If ownership is unclear, treat the file as `CORE`-only (fail-safe).
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Project notes
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<!-- List files, directories, generated artifacts, or secrets that tasks must leave unchanged. -->
|
|
43
|
-
|
|
44
|
-
## Slow or expensive
|
|
45
|
-
|
|
46
|
-
<!-- Note commands, environments, or operations that need explicit planning before running. -->
|
|
38
|
+
Write project-specific commands, files tasks must leave unchanged, and slow or
|
|
39
|
+
expensive operations after the `CAFEKIT CORE END` marker. Everything between the
|
|
40
|
+
CafeKit markers is replaced on every install.
|
|
47
41
|
|
|
48
42
|
## Language Consistency <!-- cafekit:lang -->
|
|
49
43
|
|
|
@@ -38,16 +38,16 @@ export const BRIDGE_TIMEOUT_MS = 8000;
|
|
|
38
38
|
* Mirrors src/claude/settings/settings.json; `omp-bridge.test.js` fails if the two drift.
|
|
39
39
|
*/
|
|
40
40
|
export const DISPATCH = {
|
|
41
|
-
session_start: { event: 'SessionStart', hooks: ['session.cjs', '
|
|
41
|
+
session_start: { event: 'SessionStart', hooks: ['session.cjs', 'state.cjs'] },
|
|
42
42
|
session_compact: { event: 'SessionStart', hooks: ['session.cjs'], source: 'compact' },
|
|
43
43
|
session_before_compact: { event: 'PreCompact', hooks: ['precompact.cjs'] },
|
|
44
44
|
input: { event: 'UserPromptSubmit', hooks: ['secret-output-guardrail.cjs', 'rules.cjs', 'spec-state.cjs'] },
|
|
45
45
|
tool_call: { event: 'PreToolUse', hooks: ['privacy-block.cjs', 'inspect-block.cjs'] },
|
|
46
|
-
tool_result: { event: 'PostToolUse', hooks: ['state.cjs'] },
|
|
46
|
+
tool_result: { event: 'PostToolUse', hooks: ['state.cjs', 'spec-state.cjs'] },
|
|
47
47
|
session_stop: { event: 'Stop', hooks: ['spec-gate.cjs', 'state.cjs'] },
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/** Fallback id per extension load, for an omp whose context has no session manager. */
|
|
51
51
|
const SESSION_ID = `omp-${randomUUID()}`;
|
|
52
52
|
|
|
53
53
|
export function hooksDir() {
|
|
@@ -55,13 +55,21 @@ export function hooksDir() {
|
|
|
55
55
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function sessionIdFrom(ctx) {
|
|
59
|
+
try {
|
|
60
|
+
const id = ctx?.sessionManager?.getSessionId?.();
|
|
61
|
+
return typeof id === 'string' && id ? id : null;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
/** Build the Claude-shaped payload a CafeKit hook reads on stdin. */
|
|
59
66
|
export function shapePayload(kind, event, ctx) {
|
|
60
67
|
const entry = DISPATCH[kind];
|
|
61
68
|
const cwd = ctx?.cwd || process.cwd();
|
|
62
69
|
const payload = {
|
|
63
70
|
hook_event_name: entry.event,
|
|
64
|
-
|
|
71
|
+
// omp's own id first: session_stop carries it, and touch and state must stay in one place.
|
|
72
|
+
session_id: event?.session_id || sessionIdFrom(ctx) || SESSION_ID,
|
|
65
73
|
cwd,
|
|
66
74
|
};
|
|
67
75
|
switch (kind) {
|