@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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-manager
|
|
3
|
-
description: 'Ecosystem Orchestrator.
|
|
3
|
+
description: 'Ecosystem Orchestrator. Aggregates evidence for a process-first feature packet (plan.md, flat task files, receipts, worker and review reports) and reports scope drift, task and receipt status, blockers, and the pending GATE-DONE decision. Use when several tasks or agents have finished and the overall state needs reconciling; it does not write task Status or Receipts.'
|
|
4
4
|
model: haiku
|
|
5
5
|
tools: Glob, Grep, Read, Edit, Write, NotebookEdit, Bash, WebFetch, TaskCreate, TaskGet, TaskUpdate, TaskList, WebSearch, ListMcpResourcesTool, ReadMcpResourceTool, SendMessage
|
|
6
6
|
---
|
|
@@ -45,7 +45,7 @@ Before you declare any phase complete or issue a final status report, you must i
|
|
|
45
45
|
decision; never end with a vague conclusion.
|
|
46
46
|
|
|
47
47
|
## Format & Output Constraints
|
|
48
|
-
- **
|
|
48
|
+
- **Concision:** Keep reports scannable and limited to what the minimum report below requires.
|
|
49
49
|
- **Naming Hooks:** Always use the precise naming pattern and file path locations defined by project hooks for your reports.
|
|
50
50
|
- **Minimum report:** Include GATE-SCOPE scope drift, accepted GATE-REVIEW finding coverage,
|
|
51
51
|
task/receipt status, executed command results, blockers, docs impact, and the
|
|
@@ -17,7 +17,7 @@ removes product decisions from implementation. The output is Markdown under
|
|
|
17
17
|
- existing plans or decisions explicitly placed in scope;
|
|
18
18
|
- the three human-gate decisions from `cf:specs`.
|
|
19
19
|
|
|
20
|
-
Read
|
|
20
|
+
Read `.claude/skills/specs/SKILL.md` and `.claude/skills/specs/references/templates.md` before
|
|
21
21
|
routing; they are the canonical risk and coverage authority. Do not duplicate
|
|
22
22
|
their taxonomy here. Apply their risk-first route before GATE-SCOPE and stop when the
|
|
23
23
|
request qualifies for direct work; hand off when it requires Brainstorm-only exploration.
|
|
@@ -58,7 +58,7 @@ task `Status: blocked` while GATE-REVIEW is open, and keep its `## Receipt` empt
|
|
|
58
58
|
|
|
59
59
|
### 4. Review from fresh context
|
|
60
60
|
|
|
61
|
-
Read
|
|
61
|
+
Read `.claude/skills/specs/references/review.md`. Route fresh reviewers by capability
|
|
62
62
|
and give them only the plan packet plus repository access. Require a severity,
|
|
63
63
|
plan location, concrete failure, current `path:line` evidence, and smallest
|
|
64
64
|
repair for every finding.
|
|
@@ -91,14 +91,14 @@ chooses when execution begins.
|
|
|
91
91
|
- Ownership overlap or dependency uncertainty prevents a parallel claim.
|
|
92
92
|
- A command that cannot be run from the named work context is not verification.
|
|
93
93
|
- User approval and test proof are different facts; never infer either.
|
|
94
|
-
-
|
|
94
|
+
- Leave an existing packet directory without `plan.md` untouched unless migration is explicitly asked.
|
|
95
95
|
|
|
96
96
|
## Output
|
|
97
97
|
|
|
98
98
|
End with:
|
|
99
99
|
|
|
100
100
|
```text
|
|
101
|
-
Status: DONE |
|
|
101
|
+
Status: DONE | CONCERNS | BLOCKED | NEEDS_INFO
|
|
102
102
|
Files: <plan and task paths>
|
|
103
103
|
GATE-SCOPE: <decision>
|
|
104
104
|
GATE-REVIEW: <accepted/rejected/revised counts>
|
|
@@ -16,11 +16,12 @@ separate concise redacted report. Do not invent proof from remembered output.
|
|
|
16
16
|
When the target is a feature or task, classify current bytes before execution:
|
|
17
17
|
|
|
18
18
|
- A valid process-first packet has a regular `plan.md` with
|
|
19
|
-
`Specs-Contract: process-first-ready-v1
|
|
20
|
-
|
|
21
|
-
- A
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
`Specs-Contract: process-first-ready-v1` and regular flat `task-NN-*.md`
|
|
20
|
+
files. Read the exact current Verification Plan.
|
|
21
|
+
- A packet directory with no `plan.md` that holds the old JSON planning state is no
|
|
22
|
+
longer read; return `BLOCKED` and name it.
|
|
23
|
+
- Orphaned, malformed, symlinked, nonregular, or identity-conflicting packet
|
|
24
|
+
markers return `BLOCKED`; never migrate or repair them.
|
|
24
25
|
- With no packet marker, use ordinary repository-aware test selection.
|
|
25
26
|
|
|
26
27
|
For process-first work, the task's exact Command, exact unique Named probes,
|
|
@@ -125,9 +126,5 @@ proof and does not rerun it.
|
|
|
125
126
|
Do not paste the full payload, secrets, verbose logs, or screenshots into this
|
|
126
127
|
report. `PASS_WITH_WARNINGS` remains unfinished.
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
After a valid legacy route, resolve its current spec/task contract and emit the
|
|
131
|
-
existing legacy evidence through its separate-receipt adapter. Normalize legacy
|
|
132
|
-
diagnostics to the canonical four verdicts. Never search for or write a separate
|
|
133
|
-
receipt for process-first work, and never copy legacy proof into a flat task.
|
|
129
|
+
Proof for process-first work lives only in the task's inline `## Receipt`; never
|
|
130
|
+
search for or write a receipt file beside it.
|
|
@@ -30,7 +30,6 @@ You are an award-caliber UI/UX designer. You merge aesthetic excellence with eng
|
|
|
30
30
|
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<mood>" --domain typography
|
|
31
31
|
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<industry>" --domain color
|
|
32
32
|
```
|
|
33
|
-
- Study current design trends sourced from Dribbble, Awwwards, Mobbin via the python extractor outputs.
|
|
34
33
|
- Review existing `docs/design-guidelines.md` if it exists.
|
|
35
34
|
- For competitive analysis, use bounded `WebSearch`/`WebFetch` directly or return a `NEEDS_RESEARCH` note for the controller to delegate.
|
|
36
35
|
|
package/src/claude/gitignore
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const { test } = require('node:test');
|
|
10
|
+
|
|
11
|
+
const CLAUDE = path.resolve(__dirname, '../..');
|
|
12
|
+
const MANIFEST = require('../../migration-manifest.json');
|
|
13
|
+
const RUNTIME = { paths: { specs: 'work-specs', plans: 'work-plans', docs: 'work-docs' }, inspect: { enabled: false } };
|
|
14
|
+
|
|
15
|
+
function fixture(run, withRuntime = true) {
|
|
16
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-installed-root-'));
|
|
17
|
+
const sessions = new Set();
|
|
18
|
+
try {
|
|
19
|
+
for (const relative of MANIFEST.runtime.files) {
|
|
20
|
+
const target = path.join(root, '.claude', relative);
|
|
21
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
22
|
+
fs.copyFileSync(path.join(CLAUDE, relative), target);
|
|
23
|
+
}
|
|
24
|
+
for (const relative of MANIFEST.scripts.required) {
|
|
25
|
+
const target = path.join(root, '.claude/scripts', relative);
|
|
26
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
27
|
+
fs.copyFileSync(path.join(CLAUDE, 'scripts', relative), target);
|
|
28
|
+
}
|
|
29
|
+
const runtimeFile = path.join(root, '.claude/runtime.json');
|
|
30
|
+
const writeRuntime = () => fs.writeFileSync(runtimeFile, JSON.stringify(RUNTIME));
|
|
31
|
+
if (withRuntime) writeRuntime();
|
|
32
|
+
else fs.unlinkSync(runtimeFile);
|
|
33
|
+
const cwd = path.join(root, 'sub/dir');
|
|
34
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
35
|
+
const env = { ...process.env };
|
|
36
|
+
delete env.CLAUDE_PROJECT_DIR;
|
|
37
|
+
delete env.PROJECT_ROOT;
|
|
38
|
+
const invoke = (name, payload) => {
|
|
39
|
+
if (payload.session_id) sessions.add(payload.session_id);
|
|
40
|
+
return spawnSync(process.execPath, [path.join(root, '.claude/hooks', `${name}.cjs`)], {
|
|
41
|
+
cwd, env, encoding: 'utf8', input: JSON.stringify({ cwd, ...payload }),
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
run({ root: fs.realpathSync(root), cwd, invoke, writeRuntime });
|
|
45
|
+
} finally {
|
|
46
|
+
for (const session of sessions) {
|
|
47
|
+
const key = crypto.createHash('sha256').update(session).digest('hex').slice(0, 16);
|
|
48
|
+
fs.rmSync(path.join(os.tmpdir(), `cafekit-rules-${key}.json`), { force: true });
|
|
49
|
+
}
|
|
50
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('installed rules read the project runtime from a nested session cwd', () => {
|
|
55
|
+
fixture(({ root, invoke }) => {
|
|
56
|
+
const result = invoke('rules', { hook_event_name: 'UserPromptSubmit', session_id: crypto.randomUUID() });
|
|
57
|
+
assert.equal(result.status, 0, result.stderr);
|
|
58
|
+
assert.match(result.stdout, /## Rules/);
|
|
59
|
+
assert.ok(result.stdout.includes(`${root}/work-specs/`));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('rules wait for runtime before reserving the session injection', () => {
|
|
64
|
+
fixture(({ invoke, writeRuntime }) => {
|
|
65
|
+
const payload = { hook_event_name: 'UserPromptSubmit', session_id: crypto.randomUUID() };
|
|
66
|
+
const absent = invoke('rules', payload);
|
|
67
|
+
assert.equal(absent.status, 0, absent.stderr);
|
|
68
|
+
assert.equal(absent.stdout, '');
|
|
69
|
+
writeRuntime();
|
|
70
|
+
const present = invoke('rules', payload);
|
|
71
|
+
assert.equal(present.status, 0, present.stderr);
|
|
72
|
+
assert.match(present.stdout, /## Rules/);
|
|
73
|
+
}, false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('installed inspect respects a disabled project runtime from a nested cwd', () => {
|
|
77
|
+
fixture(({ invoke }) => {
|
|
78
|
+
const result = invoke('inspect-block', { hook_event_name: 'PreToolUse', tool_name: 'Read', tool_input: { file_path: 'node_modules/pkg/index.js' } });
|
|
79
|
+
assert.equal(result.status, 0, result.stderr);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('installed agent context uses project paths from a nested session cwd', () => {
|
|
84
|
+
fixture(({ root, invoke }) => {
|
|
85
|
+
const result = invoke('agent', { hook_event_name: 'SubagentStart', agent_id: 'nested-agent' });
|
|
86
|
+
assert.equal(result.status, 0, result.stderr);
|
|
87
|
+
assert.match(result.stdout, /additionalContext/);
|
|
88
|
+
const context = JSON.parse(result.stdout).hookSpecificOutput.additionalContext;
|
|
89
|
+
for (const relative of ['work-specs', 'work-plans', 'work-docs']) assert.ok(context.includes(`${root}/${relative}/`));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('installed spec gate checks root packet receipts from a nested session cwd', () => {
|
|
94
|
+
fixture(({ root, invoke }) => {
|
|
95
|
+
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
|
|
96
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
97
|
+
assert.equal(result.status, 0, result.stderr);
|
|
98
|
+
}
|
|
99
|
+
const packet = path.join(root, 'work-specs/demo');
|
|
100
|
+
fs.mkdirSync(packet, { recursive: true });
|
|
101
|
+
fs.writeFileSync(path.join(packet, 'plan.md'), '# Demo\nSpecs-Contract: process-first-ready-v1\n');
|
|
102
|
+
fs.writeFileSync(path.join(packet, 'task-01-work.md'), '# Work\n\nStatus: done\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: node --test\n');
|
|
103
|
+
const result = invoke('spec-gate', { hook_event_name: 'Stop', session_id: crypto.randomUUID() });
|
|
104
|
+
assert.equal(result.status, 0, result.stderr);
|
|
105
|
+
const block = JSON.parse(result.stdout);
|
|
106
|
+
assert.equal(block.decision, 'block');
|
|
107
|
+
assert.match(block.reason, /receipt/i);
|
|
108
|
+
assert.match(block.reason, /task-01-work\.md/);
|
|
109
|
+
assert.doesNotMatch(block.reason, /unavailable|controlled failure/i);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -11,11 +11,21 @@ const { spawnSync } = require('node:child_process');
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const os = require('node:os');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const crypto = require('node:crypto');
|
|
14
15
|
|
|
15
16
|
const PRECOMPACT = path.join(__dirname, '..', 'precompact.cjs');
|
|
16
17
|
const SESSION = path.join(__dirname, '..', 'session.cjs');
|
|
17
18
|
const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
|
|
18
|
-
const
|
|
19
|
+
const CAPTURES = new Set([path.join(hookStateDir(), 'compact-recovery.json')]);
|
|
20
|
+
function recoveryPath(session = 'compact-session') {
|
|
21
|
+
const key = crypto.createHash('sha256').update(session).digest('hex');
|
|
22
|
+
const file = path.join(hookStateDir(), `compact-recovery-${key}.json`);
|
|
23
|
+
CAPTURES.add(file);
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
const HOOK_ENV = { ...process.env };
|
|
27
|
+
delete HOOK_ENV.CLAUDE_PROJECT_DIR;
|
|
28
|
+
delete HOOK_ENV.PROJECT_ROOT;
|
|
19
29
|
|
|
20
30
|
function fixture() {
|
|
21
31
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-'));
|
|
@@ -36,21 +46,24 @@ function fixture() {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function run(hook, dir, payload) {
|
|
49
|
+
if (typeof payload?.session_id === 'string' && payload.session_id.trim()) recoveryPath(payload.session_id);
|
|
39
50
|
const result = spawnSync(process.execPath, [hook], {
|
|
40
51
|
cwd: dir,
|
|
41
52
|
input: JSON.stringify(payload),
|
|
42
53
|
encoding: 'utf8',
|
|
43
|
-
env:
|
|
54
|
+
env: HOOK_ENV,
|
|
44
55
|
});
|
|
45
56
|
return { code: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function clearCapture() {
|
|
49
|
-
|
|
60
|
+
for (const file of CAPTURES) {
|
|
61
|
+
try { fs.unlinkSync(file); } catch { /* absent */ }
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
function readCapture() {
|
|
53
|
-
return JSON.parse(fs.readFileSync(
|
|
65
|
+
function readCapture(session) {
|
|
66
|
+
return JSON.parse(fs.readFileSync(recoveryPath(session), 'utf8'));
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
test('PreCompact records branch, HEAD, dirty count and the in-flight task', () => {
|
|
@@ -93,8 +106,8 @@ test('a compact restart prints the captured anchors alongside the authorization
|
|
|
93
106
|
const dir = fixture();
|
|
94
107
|
try {
|
|
95
108
|
clearCapture();
|
|
96
|
-
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', cwd: dir }).code, 0);
|
|
97
|
-
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
|
|
109
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', session_id: 'compact-session', cwd: dir }).code, 0);
|
|
110
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'compact-session', cwd: dir });
|
|
98
111
|
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
99
112
|
assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/, 'the existing warning must survive');
|
|
100
113
|
assert.match(restart.stdout, /State before compaction/);
|
|
@@ -119,6 +132,53 @@ test('a compact restart without a capture still prints the warning and nothing i
|
|
|
119
132
|
}
|
|
120
133
|
});
|
|
121
134
|
|
|
135
|
+
test('a compact restart in another session prints the warning without foreign anchors', () => {
|
|
136
|
+
const dir = fixture();
|
|
137
|
+
try {
|
|
138
|
+
clearCapture();
|
|
139
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'session-a', cwd: dir }).code, 0);
|
|
140
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'session-b', cwd: dir });
|
|
141
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
142
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
143
|
+
assert.doesNotMatch(restart.stdout, /State before compaction/);
|
|
144
|
+
} finally {
|
|
145
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
146
|
+
clearCapture();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a compact restart in the same session restores its captured anchors', () => {
|
|
151
|
+
const dir = fixture();
|
|
152
|
+
try {
|
|
153
|
+
clearCapture();
|
|
154
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'session-a', cwd: dir }).code, 0);
|
|
155
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: 'session-a', cwd: dir });
|
|
156
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
157
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
158
|
+
assert.match(restart.stdout, /State before compaction[\s\S]*branch work/);
|
|
159
|
+
} finally {
|
|
160
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
161
|
+
clearCapture();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
for (const [captureId, restartId] of [[undefined, 'session-a'], ['session-a', undefined], [undefined, undefined]]) {
|
|
166
|
+
test(`a compact restart without ${captureId ? 'restart' : restartId ? 'capture' : 'either'} session identity keeps only the warning`, () => {
|
|
167
|
+
const dir = fixture();
|
|
168
|
+
try {
|
|
169
|
+
clearCapture();
|
|
170
|
+
assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: captureId, cwd: dir }).code, 0);
|
|
171
|
+
const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', session_id: restartId, cwd: dir });
|
|
172
|
+
assert.strictEqual(restart.code, 0, restart.stderr);
|
|
173
|
+
assert.match(restart.stdout, /SESSION COMPRESSED/);
|
|
174
|
+
assert.doesNotMatch(restart.stdout, /State before compaction/);
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
177
|
+
clearCapture();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
122
182
|
test('a normal session start prints no compaction anchors', () => {
|
|
123
183
|
const dir = fixture();
|
|
124
184
|
try {
|
|
@@ -138,7 +198,7 @@ test('PreCompact fails open outside a repository and on malformed input', () =>
|
|
|
138
198
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-bare-'));
|
|
139
199
|
try {
|
|
140
200
|
clearCapture();
|
|
141
|
-
const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir });
|
|
201
|
+
const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', session_id: 'compact-session', cwd: dir });
|
|
142
202
|
assert.strictEqual(noGit.code, 0, 'a directory outside git must not fail the hook');
|
|
143
203
|
const record = readCapture();
|
|
144
204
|
assert.strictEqual(record.branch, null, 'unavailable git facts are recorded as null, never guessed');
|
|
@@ -147,12 +207,148 @@ test('PreCompact fails open outside a repository and on malformed input', () =>
|
|
|
147
207
|
|
|
148
208
|
clearCapture();
|
|
149
209
|
const malformed = spawnSync(process.execPath, [PRECOMPACT], {
|
|
150
|
-
cwd: dir, input: 'not json', encoding: 'utf8', env:
|
|
210
|
+
cwd: dir, input: 'not json', encoding: 'utf8', env: HOOK_ENV,
|
|
151
211
|
});
|
|
152
212
|
assert.strictEqual(malformed.status, 0, 'malformed payload must fail open');
|
|
153
|
-
assert.strictEqual(fs.existsSync(
|
|
213
|
+
assert.strictEqual(fs.existsSync(recoveryPath()), false, 'a malformed payload writes no capture');
|
|
154
214
|
} finally {
|
|
155
215
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
216
|
clearCapture();
|
|
157
217
|
}
|
|
158
218
|
});
|
|
219
|
+
|
|
220
|
+
function installedFixture(callback) {
|
|
221
|
+
const root = fixture();
|
|
222
|
+
const source = path.resolve(__dirname, '../..');
|
|
223
|
+
const manifest = require('../../migration-manifest.json');
|
|
224
|
+
const sessions = new Set();
|
|
225
|
+
try {
|
|
226
|
+
for (const relative of manifest.runtime.files) {
|
|
227
|
+
const target = path.join(root, '.claude', relative);
|
|
228
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
229
|
+
fs.copyFileSync(path.join(source, relative), target);
|
|
230
|
+
}
|
|
231
|
+
for (const relative of manifest.scripts.required) {
|
|
232
|
+
const target = path.join(root, '.claude/scripts', relative);
|
|
233
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
234
|
+
fs.copyFileSync(path.join(source, 'scripts', relative), target);
|
|
235
|
+
}
|
|
236
|
+
const hooks = path.join(root, '.claude/hooks');
|
|
237
|
+
const invoke = (name, payload, overrides = {}) => {
|
|
238
|
+
if (typeof payload?.session_id === 'string' && payload.session_id.trim()) sessions.add(payload.session_id);
|
|
239
|
+
return spawnSync(process.execPath, [path.join(hooks, `${name}.cjs`)], {
|
|
240
|
+
cwd: root, env: HOOK_ENV, encoding: 'utf8', timeout: 10000,
|
|
241
|
+
input: typeof payload === 'string' ? payload : JSON.stringify(payload), ...overrides,
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
callback({ root: fs.realpathSync(root), hooks, invoke });
|
|
245
|
+
} finally {
|
|
246
|
+
for (const session of sessions) {
|
|
247
|
+
const key = crypto.createHash('sha256').update(session).digest('hex').slice(0, 16);
|
|
248
|
+
fs.rmSync(path.join(os.tmpdir(), `cafekit-rules-${key}.json`), { force: true });
|
|
249
|
+
}
|
|
250
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
test('two compacting sessions keep separate anchors and restore only their own branch', () => {
|
|
255
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
256
|
+
const sessions = [crypto.randomUUID(), crypto.randomUUID()];
|
|
257
|
+
const files = [];
|
|
258
|
+
for (const [index, session_id] of sessions.entries()) {
|
|
259
|
+
const branch = `anchor-${index === 0 ? 'first' : 'second'}`;
|
|
260
|
+
const checkout = spawnSync('git', ['-C', root, 'checkout', '-qb', branch], { encoding: 'utf8' });
|
|
261
|
+
assert.strictEqual(checkout.status, 0, checkout.stderr);
|
|
262
|
+
const capture = invoke('precompact', { hook_event_name: 'PreCompact', session_id, cwd: root });
|
|
263
|
+
assert.strictEqual(capture.status, 0, capture.stderr);
|
|
264
|
+
const hash = crypto.createHash('sha256').update(session_id).digest('hex');
|
|
265
|
+
files.push(path.join(hooks, '.logs', `compact-recovery-${hash}.json`));
|
|
266
|
+
}
|
|
267
|
+
for (const [index, file] of files.entries()) {
|
|
268
|
+
assert.ok(fs.existsSync(file), 'both session captures must survive');
|
|
269
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
270
|
+
assert.strictEqual(record.session_id, sessions[index]);
|
|
271
|
+
assert.strictEqual(record.branch, `anchor-${index === 0 ? 'first' : 'second'}`);
|
|
272
|
+
}
|
|
273
|
+
for (const [index, session_id] of sessions.entries()) {
|
|
274
|
+
const compact = invoke('session', { hook_event_name: 'SessionStart', source: 'compact', session_id, cwd: root });
|
|
275
|
+
assert.strictEqual(compact.status, 0, compact.stderr);
|
|
276
|
+
assert.match(compact.stdout, /SESSION COMPRESSED/);
|
|
277
|
+
assert.match(compact.stdout, /State before compaction/);
|
|
278
|
+
assert.ok(compact.stdout.includes(`branch anchor-${index === 0 ? 'first' : 'second'}`));
|
|
279
|
+
assert.ok(!compact.stdout.includes(`branch anchor-${index === 0 ? 'second' : 'first'}`));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('PreCompact without a usable session identity creates no anchor or state directory', () => {
|
|
285
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
286
|
+
for (const session_id of [undefined, null, '', 42, ' ']) {
|
|
287
|
+
const capture = invoke('precompact', { hook_event_name: 'PreCompact', session_id, cwd: root });
|
|
288
|
+
assert.strictEqual(capture.status, 0, capture.stderr);
|
|
289
|
+
assert.strictEqual(fs.existsSync(path.join(hooks, '.logs')), false);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('compact reinjects rules only for its own session even without matching anchors', () => {
|
|
295
|
+
installedFixture(({ root, hooks, invoke }) => {
|
|
296
|
+
const session = crypto.randomUUID();
|
|
297
|
+
const other = crypto.randomUUID();
|
|
298
|
+
const prompt = id => invoke('rules', { hook_event_name: 'UserPromptSubmit', session_id: id, cwd: root });
|
|
299
|
+
for (const id of [session, other]) {
|
|
300
|
+
const first = prompt(id);
|
|
301
|
+
assert.strictEqual(first.status, 0, first.stderr);
|
|
302
|
+
assert.match(first.stdout, /## Rules/);
|
|
303
|
+
assert.strictEqual(prompt(id).stdout, '');
|
|
304
|
+
}
|
|
305
|
+
fs.mkdirSync(path.join(hooks, '.logs'), { recursive: true });
|
|
306
|
+
const otherHash = crypto.createHash('sha256').update(other).digest('hex');
|
|
307
|
+
fs.writeFileSync(path.join(hooks, '.logs', `compact-recovery-${otherHash}.json`), JSON.stringify({ session_id: other }));
|
|
308
|
+
const compact = invoke('session', { hook_event_name: 'SessionStart', source: 'compact', session_id: session, cwd: root });
|
|
309
|
+
assert.strictEqual(compact.status, 0, compact.stderr);
|
|
310
|
+
assert.match(compact.stdout, /SESSION COMPRESSED/);
|
|
311
|
+
assert.doesNotMatch(compact.stdout, /State before compaction/);
|
|
312
|
+
const again = prompt(session);
|
|
313
|
+
assert.strictEqual(again.status, 0, again.stderr);
|
|
314
|
+
assert.match(again.stdout, /## Rules/);
|
|
315
|
+
assert.strictEqual(prompt(other).stdout, '');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('malformed session input fails open without a stack and logs its crash', () => {
|
|
320
|
+
installedFixture(({ hooks, invoke }) => {
|
|
321
|
+
const result = invoke('session', '{');
|
|
322
|
+
assert.strictEqual(result.status, 0);
|
|
323
|
+
assert.strictEqual(result.stderr, '');
|
|
324
|
+
const rows = fs.readFileSync(path.join(hooks, '.logs/hook-log.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
325
|
+
assert.strictEqual(rows.length, 1);
|
|
326
|
+
assert.strictEqual(rows[0].hook, 'session');
|
|
327
|
+
assert.strictEqual(rows[0].status, 'crash');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('installed session explains a nested cwd but omits the root line without a cwd', () => {
|
|
332
|
+
installedFixture(({ root, invoke }) => {
|
|
333
|
+
const cwd = path.join(root, 'sub/dir');
|
|
334
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
335
|
+
const nested = invoke('session', { source: 'startup', cwd }, { cwd });
|
|
336
|
+
assert.strictEqual(nested.status, 0, nested.stderr);
|
|
337
|
+
assert.ok(nested.stdout.includes(`CafeKit project root: ${root} (session working directory: ${cwd}).`));
|
|
338
|
+
const missing = invoke('session', { source: 'startup' }, { cwd });
|
|
339
|
+
assert.strictEqual(missing.status, 0, missing.stderr);
|
|
340
|
+
assert.doesNotMatch(missing.stdout, /CafeKit project root:/);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('PreCompact bounds slow git calls and finishes within ten seconds', { timeout: 15000 }, () => {
|
|
345
|
+
installedFixture(({ root, invoke }) => {
|
|
346
|
+
const bin = path.join(root, 'fake-bin');
|
|
347
|
+
fs.mkdirSync(bin);
|
|
348
|
+
fs.writeFileSync(path.join(bin, 'git'), '#!/bin/sh\nexec /bin/sleep 30\n', { mode: 0o755 });
|
|
349
|
+
const start = Date.now();
|
|
350
|
+
const result = invoke('precompact', { hook_event_name: 'PreCompact', session_id: crypto.randomUUID(), cwd: root }, { env: { ...HOOK_ENV, PATH: bin } });
|
|
351
|
+
assert.strictEqual(result.status, 0, 'slow git must not exhaust the hook timeout');
|
|
352
|
+
assert.ok(Date.now() - start < 10000);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -11,12 +11,16 @@ const os = require('node:os');
|
|
|
11
11
|
const path = require('node:path');
|
|
12
12
|
|
|
13
13
|
const HOOK = path.join(__dirname, '..', 'privacy-block.cjs');
|
|
14
|
+
const HOOK_ENV = { ...process.env };
|
|
15
|
+
delete HOOK_ENV.CLAUDE_PROJECT_DIR;
|
|
16
|
+
delete HOOK_ENV.PROJECT_ROOT;
|
|
14
17
|
|
|
15
18
|
/** Run the hook with a tool payload; return process output. */
|
|
16
19
|
function runHook(payload, cwd) {
|
|
17
20
|
const res = spawnSync(process.execPath, [HOOK], {
|
|
18
21
|
input: JSON.stringify({ ...payload, cwd: cwd || payload.cwd }),
|
|
19
22
|
encoding: 'utf8',
|
|
23
|
+
env: HOOK_ENV,
|
|
20
24
|
});
|
|
21
25
|
return { code: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
|
22
26
|
}
|
|
@@ -30,6 +34,29 @@ function permissionDecision(stdout) {
|
|
|
30
34
|
return JSON.parse(stdout).hookSpecificOutput?.permissionDecision || null;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
test('sensitive reads list sorted distinct basenames and the remaining count', () => {
|
|
38
|
+
const dir = tmpDir();
|
|
39
|
+
try {
|
|
40
|
+
const command = 'cat token.json nested/.env secrets.json credentials.json .env';
|
|
41
|
+
const { code, stdout } = runHook({ tool_name: 'Bash', tool_input: { command } }, dir);
|
|
42
|
+
assert.strictEqual(code, 0);
|
|
43
|
+
const output = JSON.parse(stdout).hookSpecificOutput;
|
|
44
|
+
assert.strictEqual(output.permissionDecision, 'ask');
|
|
45
|
+
assert.strictEqual(output.permissionDecisionReason,
|
|
46
|
+
'Sensitive file access requires approval: .env, credentials.json, secrets.json (+1 more)');
|
|
47
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('empty stdin asks because sensitive access cannot be evaluated safely', () => {
|
|
51
|
+
const result = spawnSync(process.execPath, [HOOK], { input: '', encoding: 'utf8', env: HOOK_ENV });
|
|
52
|
+
assert.strictEqual(result.status, 0);
|
|
53
|
+
assert.strictEqual(result.stderr, '');
|
|
54
|
+
assert.deepStrictEqual(JSON.parse(result.stdout).hookSpecificOutput, {
|
|
55
|
+
hookEventName: 'PreToolUse', permissionDecision: 'ask',
|
|
56
|
+
permissionDecisionReason: 'Sensitive access could not be evaluated safely.',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
33
60
|
test('asks natively for a direct Read of .env', () => {
|
|
34
61
|
const dir = tmpDir();
|
|
35
62
|
try {
|
|
@@ -17,6 +17,31 @@ const path = require('node:path');
|
|
|
17
17
|
|
|
18
18
|
const HOOK = path.join(__dirname, '..', 'session.cjs');
|
|
19
19
|
|
|
20
|
+
test('session.cjs does not export GIT_URL or remote userinfo', () => {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-session-remote-'));
|
|
22
|
+
const envFile = path.join(dir, 'env.sh');
|
|
23
|
+
try {
|
|
24
|
+
git(['init', '-q'], dir);
|
|
25
|
+
const userinfo = 'fixture-user:fixture-placeholder';
|
|
26
|
+
const remote = `https://${userinfo}@example.invalid/r.git`;
|
|
27
|
+
const configured = spawnSync('git', ['remote', 'add', 'origin', remote], { cwd: dir, encoding: 'utf8' });
|
|
28
|
+
assert.strictEqual(configured.status, 0, 'fake remote setup must succeed');
|
|
29
|
+
const result = spawnSync(process.execPath, [HOOK], {
|
|
30
|
+
cwd: dir, input: JSON.stringify({ source: 'startup', cwd: dir }), encoding: 'utf8',
|
|
31
|
+
env: { ...process.env, CLAUDE_ENV_FILE: envFile },
|
|
32
|
+
});
|
|
33
|
+
assert.strictEqual(result.status, 0, 'session hook must exit successfully');
|
|
34
|
+
assert.ok(fs.existsSync(envFile), 'session env file must exist');
|
|
35
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
36
|
+
assert.deepStrictEqual({
|
|
37
|
+
exportsGitUrl: /^\s*(?:export\s+)?GIT_URL=/m.test(content),
|
|
38
|
+
containsUserInfo: content.includes(userinfo),
|
|
39
|
+
}, { exportsGitUrl: false, containsUserInfo: false });
|
|
40
|
+
} finally {
|
|
41
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
20
45
|
function git(args, cwd) {
|
|
21
46
|
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
22
47
|
if (res.status !== 0) {
|