devlog-tracker 0.22.0
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/LICENSE +201 -0
- package/README.md +234 -0
- package/bin/devlog-tracker.js +51 -0
- package/cli/init.js +55 -0
- package/cli/init.test.js +37 -0
- package/cli/merge-hooks.js +60 -0
- package/cli/merge-hooks.test.js +187 -0
- package/cli/platforms/codex.js +13 -0
- package/cli/platforms/cursor.js +13 -0
- package/cli/status.js +14 -0
- package/cli/status.test.js +33 -0
- package/cli/vendor.js +37 -0
- package/cli/vendor.test.js +84 -0
- package/codex/hooks/on-pre-tool.sh +19 -0
- package/codex/hooks/on-session-end.sh +31 -0
- package/codex/hooks/on-session-start.sh +37 -0
- package/codex/hooks/on-stop.sh +18 -0
- package/codex/hooks/on-user-prompt-submit.sh +15 -0
- package/codex/hooks/project-dir.sh +13 -0
- package/codex/hooks/test-adapters.sh +98 -0
- package/codex/hooks.json +54 -0
- package/commands/checkpoint.md +17 -0
- package/commands/clean.md +50 -0
- package/commands/compact.md +24 -0
- package/commands/continue.md +30 -0
- package/commands/keep.md +100 -0
- package/commands/lessons-drift.md +19 -0
- package/commands/lessons-off.md +16 -0
- package/commands/lessons-on.md +20 -0
- package/commands/lessons.md +20 -0
- package/commands/overview.md +32 -0
- package/commands/pause.md +20 -0
- package/commands/resume.md +18 -0
- package/commands/segment-watch.md +21 -0
- package/commands/span.md +13 -0
- package/commands/start.md +19 -0
- package/commands/status.md +14 -0
- package/cursor/hooks/on-pre-tool.sh +24 -0
- package/cursor/hooks/on-session-end.sh +11 -0
- package/cursor/hooks/on-session-start.sh +15 -0
- package/cursor/hooks/on-stop.sh +44 -0
- package/cursor/hooks/on-submit-prompt.sh +28 -0
- package/cursor/hooks/on-tool-failure.sh +16 -0
- package/cursor/hooks/project-dir.sh +10 -0
- package/cursor/hooks/test-adapters.sh +201 -0
- package/cursor/hooks.json +26 -0
- package/hooks/scripts/await-open.sh +23 -0
- package/hooks/scripts/checkpoint-set.sh +44 -0
- package/hooks/scripts/clean-devlog.sh +69 -0
- package/hooks/scripts/close-open-round.sh +186 -0
- package/hooks/scripts/compact-devlog.sh +72 -0
- package/hooks/scripts/detect-pending-question.sh +31 -0
- package/hooks/scripts/devlog-lock.sh +38 -0
- package/hooks/scripts/devlog-md.sh +222 -0
- package/hooks/scripts/devlog-path.sh +70 -0
- package/hooks/scripts/enforce-devlog.sh +629 -0
- package/hooks/scripts/files-snapshot.sh +70 -0
- package/hooks/scripts/json-field.sh +76 -0
- package/hooks/scripts/keep-move.sh +155 -0
- package/hooks/scripts/kept-list.sh +40 -0
- package/hooks/scripts/lessons-append.sh +93 -0
- package/hooks/scripts/lessons-drift-set.sh +42 -0
- package/hooks/scripts/lessons-off.sh +15 -0
- package/hooks/scripts/lessons-on.sh +21 -0
- package/hooks/scripts/lessons-read.sh +60 -0
- package/hooks/scripts/on-session-end.sh +14 -0
- package/hooks/scripts/on-stop-failure.sh +18 -0
- package/hooks/scripts/on-tool-failure.sh +25 -0
- package/hooks/scripts/pause-devlog.sh +13 -0
- package/hooks/scripts/redact-prompt.sh +19 -0
- package/hooks/scripts/resume-devlog.sh +41 -0
- package/hooks/scripts/round-start.sh +333 -0
- package/hooks/scripts/run-tests.sh +26 -0
- package/hooks/scripts/segment-watch-set.sh +47 -0
- package/hooks/scripts/segment-watch.sh +197 -0
- package/hooks/scripts/session-start-devlog.sh +157 -0
- package/hooks/scripts/span-close.sh +9 -0
- package/hooks/scripts/span-open.sh +21 -0
- package/hooks/scripts/start-devlog.sh +24 -0
- package/hooks/scripts/status-devlog.sh +43 -0
- package/hooks/scripts/tests/test-await-open.sh +112 -0
- package/hooks/scripts/tests/test-branch-scoped-integration.sh +108 -0
- package/hooks/scripts/tests/test-checkpoint-set.sh +36 -0
- package/hooks/scripts/tests/test-clean-devlog.sh +160 -0
- package/hooks/scripts/tests/test-cli-init-e2e.sh +37 -0
- package/hooks/scripts/tests/test-close-open-round.sh +325 -0
- package/hooks/scripts/tests/test-compact-devlog.sh +62 -0
- package/hooks/scripts/tests/test-devlog-lock.sh +37 -0
- package/hooks/scripts/tests/test-devlog-md.sh +530 -0
- package/hooks/scripts/tests/test-devlog-path.sh +118 -0
- package/hooks/scripts/tests/test-enforce-devlog-files.sh +233 -0
- package/hooks/scripts/tests/test-enforce-devlog-handoff-order.sh +223 -0
- package/hooks/scripts/tests/test-enforce-devlog-workspace.sh +333 -0
- package/hooks/scripts/tests/test-enforce-devlog.sh +1290 -0
- package/hooks/scripts/tests/test-files-snapshot.sh +142 -0
- package/hooks/scripts/tests/test-json-field.sh +57 -0
- package/hooks/scripts/tests/test-keep-move.sh +127 -0
- package/hooks/scripts/tests/test-kept-list.sh +80 -0
- package/hooks/scripts/tests/test-lessons-append.sh +138 -0
- package/hooks/scripts/tests/test-lessons-drift-set.sh +36 -0
- package/hooks/scripts/tests/test-lessons-on-off.sh +75 -0
- package/hooks/scripts/tests/test-lessons-read.sh +97 -0
- package/hooks/scripts/tests/test-on-interrupt.sh +186 -0
- package/hooks/scripts/tests/test-redact-prompt.sh +27 -0
- package/hooks/scripts/tests/test-resume-devlog.sh +35 -0
- package/hooks/scripts/tests/test-round-start.sh +825 -0
- package/hooks/scripts/tests/test-segment-watch-set.sh +75 -0
- package/hooks/scripts/tests/test-segment-watch.sh +468 -0
- package/hooks/scripts/tests/test-session-start-devlog.sh +508 -0
- package/hooks/scripts/tests/test-start-pause-devlog.sh +84 -0
- package/hooks/scripts/tests/test-status-span.sh +50 -0
- package/hooks/scripts/tests/test-workspace-snapshot.sh +130 -0
- package/hooks/scripts/workspace-snapshot.sh +69 -0
- package/package.json +37 -0
- package/skills/devlog-tracker/SKILL.md +411 -0
- package/skills/devlog-tracker/references/checkpoint-mode.md +42 -0
- package/skills/devlog-tracker/references/lessons-mode.md +32 -0
- package/skills/devlog-tracker/references/reply-fold.md +129 -0
- package/skills/devlog-tracker/references/round-segments.md +55 -0
- package/skills/devlog-tracker/references/span-mode.md +65 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { mergeHooksTemplate } = require('./merge-hooks');
|
|
8
|
+
|
|
9
|
+
function tmpdir() {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-merge-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('creates the target file from the template when target is missing', () => {
|
|
14
|
+
const dir = tmpdir();
|
|
15
|
+
const templatePath = path.join(dir, 'template.json');
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
templatePath,
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
hooks: { Stop: [{ hooks: [{ type: 'command', command: 'bash "${DEVLOG_TRACKER_ROOT}/codex/hooks/on-stop.sh"' }] }] },
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
const targetPath = path.join(dir, '.codex', 'hooks.json');
|
|
23
|
+
|
|
24
|
+
mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' });
|
|
25
|
+
|
|
26
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
27
|
+
assert.equal(written.hooks.Stop[0].hooks[0].command, 'bash "/proj/.devlog-tracker/codex/hooks/on-stop.sh"');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('re-running replaces only its own entries, keeps unrelated hooks untouched', () => {
|
|
31
|
+
const dir = tmpdir();
|
|
32
|
+
const templatePath = path.join(dir, 'template.json');
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
templatePath,
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
hooks: { Stop: [{ hooks: [{ type: 'command', command: 'bash "${DEVLOG_TRACKER_ROOT}/codex/hooks/on-stop.sh"' }] }] },
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
const targetPath = path.join(dir, '.codex', 'hooks.json');
|
|
40
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
targetPath,
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
hooks: {
|
|
45
|
+
Stop: [
|
|
46
|
+
{ hooks: [{ type: 'command', command: 'bash /some/other-tool/on-stop.sh' }] },
|
|
47
|
+
{ hooks: [{ type: 'command', command: 'bash "/old/.devlog-tracker/codex/hooks/on-stop.sh"' }] },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' });
|
|
54
|
+
|
|
55
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
56
|
+
assert.equal(written.hooks.Stop.length, 2);
|
|
57
|
+
assert.equal(written.hooks.Stop[0].hooks[0].command, 'bash /some/other-tool/on-stop.sh');
|
|
58
|
+
assert.equal(written.hooks.Stop[1].hooks[0].command, 'bash "/proj/.devlog-tracker/codex/hooks/on-stop.sh"');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('preserves top-level non-hooks keys such as version', () => {
|
|
62
|
+
const dir = tmpdir();
|
|
63
|
+
const templatePath = path.join(dir, 'template.json');
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
templatePath,
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
version: 1,
|
|
68
|
+
hooks: { stop: [{ command: 'bash "${DEVLOG_TRACKER_ROOT}/cursor/hooks/on-stop.sh"' }] },
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
const targetPath = path.join(dir, '.cursor', 'hooks.json');
|
|
72
|
+
|
|
73
|
+
mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' });
|
|
74
|
+
|
|
75
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
76
|
+
assert.equal(written.version, 1);
|
|
77
|
+
assert.equal(written.hooks.stop[0].command, 'bash "/proj/.devlog-tracker/cursor/hooks/on-stop.sh"');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function writeTemplate(dir) {
|
|
81
|
+
const templatePath = path.join(dir, 'template.json');
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
templatePath,
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
hooks: { Stop: [{ hooks: [{ type: 'command', command: 'bash "${DEVLOG_TRACKER_ROOT}/codex/hooks/on-stop.sh"' }] }] },
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
return templatePath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setup(existingText) {
|
|
92
|
+
const dir = tmpdir();
|
|
93
|
+
const templatePath = writeTemplate(dir);
|
|
94
|
+
const targetPath = path.join(dir, '.codex', 'hooks.json');
|
|
95
|
+
if (existingText !== undefined) {
|
|
96
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(targetPath, existingText);
|
|
98
|
+
}
|
|
99
|
+
return { templatePath, targetPath };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test('malformed existing JSON throws with the file path and re-run guidance', () => {
|
|
103
|
+
const { templatePath, targetPath } = setup('{ not json');
|
|
104
|
+
assert.throws(
|
|
105
|
+
() => mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' }),
|
|
106
|
+
(err) => {
|
|
107
|
+
assert.ok(err.message.startsWith(`Cannot parse existing ${targetPath}: `), err.message);
|
|
108
|
+
assert.ok(err.message.includes('Fix or move the file, then re-run `devlog-tracker init` (safe to re-run).'));
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
assert.equal(fs.readFileSync(targetPath, 'utf8'), '{ not json');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('non-object top-level value throws naming the file path', () => {
|
|
116
|
+
for (const text of ['[]', '"str"', '42']) {
|
|
117
|
+
const { templatePath, targetPath } = setup(text);
|
|
118
|
+
assert.throws(
|
|
119
|
+
() => mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' }),
|
|
120
|
+
(err) => err.message.includes(targetPath),
|
|
121
|
+
text
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('non-object "hooks" value throws naming the file path', () => {
|
|
127
|
+
const { templatePath, targetPath } = setup('{"hooks": []}');
|
|
128
|
+
assert.throws(
|
|
129
|
+
() => mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' }),
|
|
130
|
+
(err) => err.message.includes(targetPath) && err.message.includes('hooks')
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('non-array event value throws naming the file path and event, without overwriting', () => {
|
|
135
|
+
const original = '{"hooks": {"Stop": {"oops": true}}}';
|
|
136
|
+
const { templatePath, targetPath } = setup(original);
|
|
137
|
+
assert.throws(
|
|
138
|
+
() => mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' }),
|
|
139
|
+
(err) => err.message.includes(targetPath) && err.message.includes('Stop')
|
|
140
|
+
);
|
|
141
|
+
assert.equal(fs.readFileSync(targetPath, 'utf8'), original);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('empty, whitespace-only and null existing files are treated as empty', () => {
|
|
145
|
+
for (const text of ['', ' \n\t ', 'null']) {
|
|
146
|
+
const { templatePath, targetPath } = setup(text);
|
|
147
|
+
mergeHooksTemplate({ templatePath, targetPath, vendorRoot: '/proj/.devlog-tracker' });
|
|
148
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
149
|
+
assert.equal(written.hooks.Stop[0].hooks[0].command, 'bash "/proj/.devlog-tracker/codex/hooks/on-stop.sh"');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('vendorRoot containing a double quote or backslash still yields valid JSON', () => {
|
|
154
|
+
const vendorRoot = '/tmp/we"ird\\dir/.devlog-tracker';
|
|
155
|
+
const { templatePath, targetPath } = setup();
|
|
156
|
+
mergeHooksTemplate({ templatePath, targetPath, vendorRoot });
|
|
157
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
158
|
+
assert.equal(written.hooks.Stop[0].hooks[0].command, `bash "${vendorRoot}/codex/hooks/on-stop.sh"`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('real templates quote the vendor path so project paths with spaces work', () => {
|
|
162
|
+
const vendorRoot = '/tmp/my project/.devlog-tracker';
|
|
163
|
+
for (const name of ['cursor', 'codex']) {
|
|
164
|
+
const dir = tmpdir();
|
|
165
|
+
const targetPath = path.join(dir, 'hooks.json');
|
|
166
|
+
mergeHooksTemplate({
|
|
167
|
+
templatePath: path.join(__dirname, '..', name, 'hooks.json'),
|
|
168
|
+
targetPath,
|
|
169
|
+
vendorRoot,
|
|
170
|
+
});
|
|
171
|
+
const written = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
172
|
+
const commands = [];
|
|
173
|
+
(function walk(v) {
|
|
174
|
+
if (Array.isArray(v)) v.forEach(walk);
|
|
175
|
+
else if (v && typeof v === 'object') {
|
|
176
|
+
for (const [k, val] of Object.entries(v)) {
|
|
177
|
+
if (k === 'command') commands.push(val);
|
|
178
|
+
else walk(val);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})(written.hooks);
|
|
182
|
+
assert.ok(commands.length >= 5, `${name} has commands`);
|
|
183
|
+
for (const c of commands) {
|
|
184
|
+
assert.ok(c.includes(`"${vendorRoot}/`), `${name}: unquoted command ${c}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { mergeHooksTemplate } = require('../merge-hooks');
|
|
4
|
+
|
|
5
|
+
function install({ repoRoot, targetDir, vendorRoot }) {
|
|
6
|
+
mergeHooksTemplate({
|
|
7
|
+
templatePath: path.join(repoRoot, 'codex', 'hooks.json'),
|
|
8
|
+
targetPath: path.join(targetDir, '.codex', 'hooks.json'),
|
|
9
|
+
vendorRoot,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { install };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { mergeHooksTemplate } = require('../merge-hooks');
|
|
4
|
+
|
|
5
|
+
function install({ repoRoot, targetDir, vendorRoot }) {
|
|
6
|
+
mergeHooksTemplate({
|
|
7
|
+
templatePath: path.join(repoRoot, 'cursor', 'hooks.json'),
|
|
8
|
+
targetPath: path.join(targetDir, '.cursor', 'hooks.json'),
|
|
9
|
+
vendorRoot,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { install };
|
package/cli/status.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function status({ targetDir, currentVersion }) {
|
|
6
|
+
const versionFile = path.join(targetDir, '.devlog-tracker', 'VERSION');
|
|
7
|
+
if (!fs.existsSync(versionFile)) {
|
|
8
|
+
return { installed: false, vendoredVersion: null, upToDate: false };
|
|
9
|
+
}
|
|
10
|
+
const vendoredVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
|
11
|
+
return { installed: true, vendoredVersion, upToDate: vendoredVersion === currentVersion };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { status };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { status } = require('./status');
|
|
8
|
+
|
|
9
|
+
function tmpdir() {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-status-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('reports not installed when .devlog-tracker/VERSION is missing', () => {
|
|
14
|
+
const targetDir = tmpdir();
|
|
15
|
+
const result = status({ targetDir, currentVersion: '0.21.0' });
|
|
16
|
+
assert.deepEqual(result, { installed: false, vendoredVersion: null, upToDate: false });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('reports up to date when versions match', () => {
|
|
20
|
+
const targetDir = tmpdir();
|
|
21
|
+
fs.mkdirSync(path.join(targetDir, '.devlog-tracker'), { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.join(targetDir, '.devlog-tracker', 'VERSION'), '0.21.0\n');
|
|
23
|
+
const result = status({ targetDir, currentVersion: '0.21.0' });
|
|
24
|
+
assert.deepEqual(result, { installed: true, vendoredVersion: '0.21.0', upToDate: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('reports out of date when versions differ', () => {
|
|
28
|
+
const targetDir = tmpdir();
|
|
29
|
+
fs.mkdirSync(path.join(targetDir, '.devlog-tracker'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(targetDir, '.devlog-tracker', 'VERSION'), '0.20.0\n');
|
|
31
|
+
const result = status({ targetDir, currentVersion: '0.21.0' });
|
|
32
|
+
assert.deepEqual(result, { installed: true, vendoredVersion: '0.20.0', upToDate: false });
|
|
33
|
+
});
|
package/cli/vendor.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const VENDOR_ENTRIES = ['hooks/scripts', 'codex/hooks', 'cursor/hooks', 'skills', 'commands'];
|
|
6
|
+
|
|
7
|
+
function copyRecursive(src, dest) {
|
|
8
|
+
const stat = fs.statSync(src);
|
|
9
|
+
if (stat.isDirectory()) {
|
|
10
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
11
|
+
for (const entry of fs.readdirSync(src)) {
|
|
12
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
17
|
+
fs.copyFileSync(src, dest);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function envShContent(vendorRoot) {
|
|
21
|
+
const escaped = vendorRoot.replace(/[\\"$`]/g, '\\$&');
|
|
22
|
+
return `export DEVLOG_TRACKER_ROOT="${escaped}"\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function vendor({ repoRoot, targetDir, version }) {
|
|
26
|
+
const vendorRoot = path.join(targetDir, '.devlog-tracker');
|
|
27
|
+
for (const entry of VENDOR_ENTRIES) {
|
|
28
|
+
const src = path.join(repoRoot, entry);
|
|
29
|
+
if (!fs.existsSync(src)) continue;
|
|
30
|
+
copyRecursive(src, path.join(vendorRoot, entry));
|
|
31
|
+
}
|
|
32
|
+
fs.writeFileSync(path.join(vendorRoot, 'VERSION'), `${version}\n`);
|
|
33
|
+
fs.writeFileSync(path.join(vendorRoot, 'env.sh'), envShContent(vendorRoot));
|
|
34
|
+
return vendorRoot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { vendor, VENDOR_ENTRIES };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { vendor } = require('./vendor');
|
|
8
|
+
|
|
9
|
+
function tmpdir() {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'devlog-tracker-vendor-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeFakeRepo(dir) {
|
|
14
|
+
fs.mkdirSync(path.join(dir, 'hooks', 'scripts'), { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(dir, 'hooks', 'scripts', 'enforce-devlog.sh'), '#!/usr/bin/env bash\necho hi\n');
|
|
16
|
+
fs.mkdirSync(path.join(dir, 'codex', 'hooks'), { recursive: true });
|
|
17
|
+
fs.writeFileSync(path.join(dir, 'codex', 'hooks', 'on-stop.sh'), '#!/usr/bin/env bash\necho codex\n');
|
|
18
|
+
fs.mkdirSync(path.join(dir, 'cursor', 'hooks'), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(dir, 'cursor', 'hooks', 'on-stop.sh'), '#!/usr/bin/env bash\necho cursor\n');
|
|
20
|
+
fs.mkdirSync(path.join(dir, 'skills', 'devlog-tracker'), { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(dir, 'skills', 'devlog-tracker', 'SKILL.md'), '# skill\n');
|
|
22
|
+
fs.mkdirSync(path.join(dir, 'commands'), { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(dir, 'commands', 'start.md'), '# start\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('vendor copies the expected directories and stamps VERSION + env.sh', () => {
|
|
27
|
+
const repoRoot = tmpdir();
|
|
28
|
+
const targetDir = tmpdir();
|
|
29
|
+
makeFakeRepo(repoRoot);
|
|
30
|
+
|
|
31
|
+
const vendorRoot = vendor({ repoRoot, targetDir, version: '0.21.0' });
|
|
32
|
+
|
|
33
|
+
assert.equal(vendorRoot, path.join(targetDir, '.devlog-tracker'));
|
|
34
|
+
assert.ok(fs.existsSync(path.join(vendorRoot, 'hooks', 'scripts', 'enforce-devlog.sh')));
|
|
35
|
+
assert.ok(fs.existsSync(path.join(vendorRoot, 'codex', 'hooks', 'on-stop.sh')));
|
|
36
|
+
assert.ok(fs.existsSync(path.join(vendorRoot, 'cursor', 'hooks', 'on-stop.sh')));
|
|
37
|
+
assert.ok(fs.existsSync(path.join(vendorRoot, 'skills', 'devlog-tracker', 'SKILL.md')));
|
|
38
|
+
assert.ok(fs.existsSync(path.join(vendorRoot, 'commands', 'start.md')));
|
|
39
|
+
assert.equal(fs.readFileSync(path.join(vendorRoot, 'VERSION'), 'utf8'), '0.21.0\n');
|
|
40
|
+
assert.equal(
|
|
41
|
+
fs.readFileSync(path.join(vendorRoot, 'env.sh'), 'utf8'),
|
|
42
|
+
`export DEVLOG_TRACKER_ROOT="${vendorRoot}"\n`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('vendor preserves the ../../hooks/scripts relative layout codex/cursor wrappers rely on', () => {
|
|
47
|
+
const repoRoot = tmpdir();
|
|
48
|
+
const targetDir = tmpdir();
|
|
49
|
+
makeFakeRepo(repoRoot);
|
|
50
|
+
|
|
51
|
+
const vendorRoot = vendor({ repoRoot, targetDir, version: '0.21.0' });
|
|
52
|
+
|
|
53
|
+
const resolvedFromCodex = path.join(vendorRoot, 'codex', 'hooks', '..', '..', 'hooks', 'scripts', 'enforce-devlog.sh');
|
|
54
|
+
assert.ok(fs.existsSync(resolvedFromCodex));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('re-running vendor overwrites rather than duplicating', () => {
|
|
58
|
+
const repoRoot = tmpdir();
|
|
59
|
+
const targetDir = tmpdir();
|
|
60
|
+
makeFakeRepo(repoRoot);
|
|
61
|
+
|
|
62
|
+
vendor({ repoRoot, targetDir, version: '0.21.0' });
|
|
63
|
+
fs.writeFileSync(path.join(repoRoot, 'commands', 'start.md'), '# start v2\n');
|
|
64
|
+
const vendorRoot = vendor({ repoRoot, targetDir, version: '0.22.0' });
|
|
65
|
+
|
|
66
|
+
assert.equal(fs.readFileSync(path.join(vendorRoot, 'commands', 'start.md'), 'utf8'), '# start v2\n');
|
|
67
|
+
assert.equal(fs.readFileSync(path.join(vendorRoot, 'VERSION'), 'utf8'), '0.22.0\n');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('env.sh escapes shell metacharacters in the vendor path', () => {
|
|
71
|
+
const repoRoot = tmpdir();
|
|
72
|
+
const parent = tmpdir();
|
|
73
|
+
const targetDir = path.join(parent, 'a$(x)"b`c\\d');
|
|
74
|
+
fs.mkdirSync(targetDir);
|
|
75
|
+
makeFakeRepo(repoRoot);
|
|
76
|
+
|
|
77
|
+
const vendorRoot = vendor({ repoRoot, targetDir, version: '0.21.0' });
|
|
78
|
+
|
|
79
|
+
assert.equal(
|
|
80
|
+
fs.readFileSync(path.join(vendorRoot, 'env.sh'), 'utf8'),
|
|
81
|
+
`export DEVLOG_TRACKER_ROOT="${vendorRoot.replace(/[\\"$`]/g, '\\$&')}"\n`
|
|
82
|
+
);
|
|
83
|
+
assert.ok(fs.readFileSync(path.join(vendorRoot, 'env.sh'), 'utf8').includes('a\\$(x)\\"b\\`c\\\\d'));
|
|
84
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex PreToolUse wrapper. Codex's "command" hooks use the same exit-code
|
|
3
|
+
# contract as Claude Code (0 = allow, 2 = block), so this only needs the
|
|
4
|
+
# cwd->CLAUDE_PROJECT_DIR translation; segment-watch.sh's stderr message
|
|
5
|
+
# propagates unchanged. Fail-open like the Cursor adapter: only an exit code of
|
|
6
|
+
# exactly 2 blocks; anything else (including a missing hooks/scripts dir or a
|
|
7
|
+
# crash) allows.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
_src="${BASH_SOURCE[0]}"
|
|
10
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
11
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" 2>/dev/null && pwd)"
|
|
12
|
+
[ -d "$PLUGIN_SCRIPTS" ] || exit 0
|
|
13
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
14
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
15
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
16
|
+
printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/segment-watch.sh"
|
|
17
|
+
RC=$?
|
|
18
|
+
[ "$RC" -eq 2 ] && exit 2
|
|
19
|
+
exit 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex SessionEnd wrapper. on-session-end.sh (shared) reads a "reason"
|
|
3
|
+
# field. Codex's own field for "why the session ended" is documented
|
|
4
|
+
# inconsistently as either "why" or "reason" - try both, default to
|
|
5
|
+
# "unknown" rather than guessing further, and rebuild a synthetic payload
|
|
6
|
+
# so the shared script's json_str_field lookup always finds "reason".
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
|
|
11
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
12
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
13
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
14
|
+
|
|
15
|
+
REASON=""
|
|
16
|
+
if command -v jq >/dev/null 2>&1; then
|
|
17
|
+
REASON="$(printf '%s' "$INPUT" | jq -r '.why // .reason // empty' 2>/dev/null || true)"
|
|
18
|
+
else
|
|
19
|
+
REASON="$(printf '%s' "$INPUT" | grep -o '"why"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"why"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
20
|
+
[ -n "$REASON" ] || REASON="$(printf '%s' "$INPUT" | grep -o '"reason"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
21
|
+
fi
|
|
22
|
+
[ -n "$REASON" ] || REASON="unknown"
|
|
23
|
+
|
|
24
|
+
if command -v jq >/dev/null 2>&1; then
|
|
25
|
+
PAYLOAD="$(jq -n --arg r "$REASON" '{reason:$r}')"
|
|
26
|
+
else
|
|
27
|
+
esc="$(printf '%s' "$REASON" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
28
|
+
PAYLOAD="$(printf '{"reason":"%s"}' "$esc")"
|
|
29
|
+
fi
|
|
30
|
+
printf '%s' "$PAYLOAD" | bash "$PLUGIN_SCRIPTS/on-session-end.sh"
|
|
31
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex SessionStart wrapper. Claude Code's own hooks.json calls
|
|
3
|
+
# session-start-devlog.sh directly and its plain stdout becomes the injected
|
|
4
|
+
# context — no JSON envelope. Codex hooks are documented to share the same
|
|
5
|
+
# JSON-in shape as Claude Code, so we assume the same is true for stdout-out
|
|
6
|
+
# here and skip any envelope too.
|
|
7
|
+
#
|
|
8
|
+
# One documented field-name gap: Codex's SessionStart payload may call the
|
|
9
|
+
# "how did this session start" field `how` instead of Claude Code's `source`.
|
|
10
|
+
# We try both and default to "startup" on purpose: session-start-devlog.sh
|
|
11
|
+
# SKIPS the dangling-round heal when source is missing/unrecognized (same as
|
|
12
|
+
# `compact`); the wrapper forces SRC="startup" so healing runs.
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
_src="${BASH_SOURCE[0]}"
|
|
15
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
16
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
|
|
17
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
18
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
19
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
20
|
+
|
|
21
|
+
SRC=""
|
|
22
|
+
if command -v jq >/dev/null 2>&1; then
|
|
23
|
+
SRC="$(printf '%s' "$INPUT" | jq -r '.how // .source // empty' 2>/dev/null || true)"
|
|
24
|
+
else
|
|
25
|
+
SRC="$(printf '%s' "$INPUT" | grep -o '"how"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"how"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
26
|
+
[ -n "$SRC" ] || SRC="$(printf '%s' "$INPUT" | grep -o '"source"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"source"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
27
|
+
fi
|
|
28
|
+
[ -n "$SRC" ] || SRC="startup"
|
|
29
|
+
|
|
30
|
+
if command -v jq >/dev/null 2>&1; then
|
|
31
|
+
PAYLOAD="$(jq -n --arg s "$SRC" '{source:$s}')"
|
|
32
|
+
else
|
|
33
|
+
esc="$(printf '%s' "$SRC" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
34
|
+
PAYLOAD="$(printf '{"source":"%s"}' "$esc")"
|
|
35
|
+
fi
|
|
36
|
+
printf '%s' "$PAYLOAD" | bash "$PLUGIN_SCRIPTS/session-start-devlog.sh"
|
|
37
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex Stop wrapper. Same exit-code contract as PreToolUse; enforce-devlog.sh
|
|
3
|
+
# reads "stop_hook_active", which is Claude Code's own standard field name
|
|
4
|
+
# and not one of the documented Codex naming gaps, so it is forwarded as-is.
|
|
5
|
+
# Fail-open like the Cursor adapter: only an exit code of exactly 2 blocks;
|
|
6
|
+
# anything else (including a missing hooks/scripts dir or a crash) allows.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" 2>/dev/null && pwd)"
|
|
11
|
+
[ -d "$PLUGIN_SCRIPTS" ] || exit 0
|
|
12
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
13
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
14
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
15
|
+
printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/enforce-devlog.sh"
|
|
16
|
+
RC=$?
|
|
17
|
+
[ "$RC" -eq 2 ] && exit 2
|
|
18
|
+
exit 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex UserPromptSubmit wrapper. round-start.sh is fail-open and side-effect
|
|
3
|
+
# only (writes .devlog files); the one case where it prints anything to
|
|
4
|
+
# stdout (workspace-mismatch notice) relies on the same plain-stdout-as-
|
|
5
|
+
# context contract as SessionStart, so this is a straight pass-through of
|
|
6
|
+
# the original payload after the cwd->CLAUDE_PROJECT_DIR translation.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
SCRIPT_DIR="$(cd "${_src%/*}" && pwd)"
|
|
10
|
+
PLUGIN_SCRIPTS="$(cd "$SCRIPT_DIR/../../hooks/scripts" && pwd)"
|
|
11
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
12
|
+
ROOT="$(printf '%s' "$INPUT" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
13
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
14
|
+
printf '%s' "$INPUT" | bash "$PLUGIN_SCRIPTS/round-start.sh"
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex hook payloads carry the project directory in a "cwd" field (unlike
|
|
3
|
+
# Cursor's workspace_roots[0] array). Same job as cursor/hooks/project-dir.sh,
|
|
4
|
+
# different field name.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
7
|
+
ROOT=""
|
|
8
|
+
if command -v jq >/dev/null 2>&1; then
|
|
9
|
+
ROOT="$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
10
|
+
else
|
|
11
|
+
ROOT="$(printf '%s' "$INPUT" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)"
|
|
12
|
+
fi
|
|
13
|
+
printf '%s\n' "${ROOT:-.}"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
TMP="$(mktemp -d)"
|
|
5
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
6
|
+
FAIL=0
|
|
7
|
+
|
|
8
|
+
# project-dir reads cwd, not workspace_roots
|
|
9
|
+
ROOT="$(printf '{"cwd":"%s"}' "$TMP" | bash "$SCRIPT_DIR/project-dir.sh")"
|
|
10
|
+
if [ "$ROOT" = "$TMP" ]; then echo "PASS: project-dir reads cwd"; else echo "FAIL: project-dir [$ROOT]"; FAIL=1; fi
|
|
11
|
+
|
|
12
|
+
# sessionStart: injects last Round's content as plain stdout
|
|
13
|
+
mkdir -p "$TMP/project/.devlog"
|
|
14
|
+
cat > "$TMP/project/.devlog/devlog.md" <<'EOF'
|
|
15
|
+
## Round 1 — now
|
|
16
|
+
|
|
17
|
+
### Summary
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
### Handoff
|
|
21
|
+
#### 現況
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
### Status
|
|
25
|
+
DONE
|
|
26
|
+
EOF
|
|
27
|
+
OUT="$(printf '{"cwd":"%s","how":"startup"}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
|
|
28
|
+
case "$OUT" in *"Round 1"*) echo "PASS: sessionStart injects context" ;; *) echo "FAIL: sessionStart [$OUT]"; FAIL=1 ;; esac
|
|
29
|
+
|
|
30
|
+
# userPromptSubmit: opens a Round in .round-current.md
|
|
31
|
+
SUBMIT="$TMP/submit"
|
|
32
|
+
mkdir -p "$SUBMIT/.devlog"
|
|
33
|
+
touch "$SUBMIT/.devlog/.enabled"
|
|
34
|
+
bash "$SCRIPT_DIR/on-user-prompt-submit.sh" <<EOF2 >/dev/null
|
|
35
|
+
{"cwd":"$SUBMIT","prompt":"hello","session_id":"codex-1"}
|
|
36
|
+
EOF2
|
|
37
|
+
if grep -q 'hello' "$SUBMIT/.devlog/.round-current.md" 2>/dev/null; then
|
|
38
|
+
echo "PASS: userPromptSubmit writes round"
|
|
39
|
+
else
|
|
40
|
+
echo "FAIL: userPromptSubmit round"; FAIL=1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# preTool: blocks (exit 2) once the silence window has expired, allows fresh writes
|
|
44
|
+
NOW="$(date +%s)"
|
|
45
|
+
OLD=$((NOW - 1000))
|
|
46
|
+
SUM="$(cksum < "$SUBMIT/.devlog/.round-current.md" | tr -d '\n')"
|
|
47
|
+
printf '{"last_change_epoch": %s, "last_seen_cksum": "%s", "max_silent_seconds": 900, "session_id": "codex-1"}\n' "$OLD" "$SUM" > "$SUBMIT/.devlog/.segment-state"
|
|
48
|
+
set +e
|
|
49
|
+
printf '{"cwd":"%s","tool_name":"Bash","tool_input":{},"session_id":"codex-1"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
|
|
50
|
+
RC=$?
|
|
51
|
+
set -e
|
|
52
|
+
if [ "$RC" -eq 2 ]; then echo "PASS: expired tool blocked (exit 2)"; else echo "FAIL: preTool exit [$RC]"; FAIL=1; fi
|
|
53
|
+
|
|
54
|
+
set +e
|
|
55
|
+
printf '{"cwd":"%s","tool_name":"Write","tool_input":{"file_path":"%s/.devlog/.round-current.md"},"session_id":"codex-1"}' "$SUBMIT" "$SUBMIT" | bash "$SCRIPT_DIR/on-pre-tool.sh" >/dev/null 2>/dev/null
|
|
56
|
+
RC=$?
|
|
57
|
+
set -e
|
|
58
|
+
if [ "$RC" -eq 0 ]; then echo "PASS: devlog write allowed (exit 0)"; else echo "FAIL: allowed-tool exit [$RC]"; FAIL=1; fi
|
|
59
|
+
|
|
60
|
+
# stop: blocks (exit 2) while the round is unfinished
|
|
61
|
+
printf '{"round": 1, "opened_at": "now"}\n' > "$SUBMIT/.devlog/.round-open"
|
|
62
|
+
cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
|
|
63
|
+
set +e
|
|
64
|
+
printf '{"cwd":"%s"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-stop.sh" >/dev/null 2>/dev/null
|
|
65
|
+
RC=$?
|
|
66
|
+
set -e
|
|
67
|
+
if [ "$RC" -eq 2 ]; then echo "PASS: stop blocks unfinished round (exit 2)"; else echo "FAIL: stop exit [$RC]"; FAIL=1; fi
|
|
68
|
+
|
|
69
|
+
# sessionEnd: closes the open round using "why" (not "reason")
|
|
70
|
+
printf '\n## Round 2 — now\n\n### Status\nIN_PROGRESS\n' > "$SUBMIT/.devlog/.round-current.md"
|
|
71
|
+
printf '{"round": 2, "opened_at": "now"}\n' > "$SUBMIT/.devlog/.round-open"
|
|
72
|
+
cksum < "$SUBMIT/.devlog/.round-current.md" > "$SUBMIT/.devlog/.turn-start"
|
|
73
|
+
printf '{"cwd":"%s","why":"windowClosed"}' "$SUBMIT" | bash "$SCRIPT_DIR/on-session-end.sh" >/dev/null
|
|
74
|
+
if grep -q 'SessionEnd:windowClosed' "$SUBMIT/.devlog/devlog.md" 2>/dev/null; then
|
|
75
|
+
echo "PASS: sessionEnd closes round via 'why'"
|
|
76
|
+
else
|
|
77
|
+
echo "FAIL: sessionEnd 'why' fallback"; FAIL=1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# fail-open: when ../../hooks/scripts is missing, preTool/stop wrappers must allow (exit 0)
|
|
81
|
+
ORPHAN="$TMP/orphan"
|
|
82
|
+
mkdir -p "$ORPHAN/codex/hooks"
|
|
83
|
+
cp "$SCRIPT_DIR/on-pre-tool.sh" "$SCRIPT_DIR/on-stop.sh" "$SCRIPT_DIR/project-dir.sh" "$ORPHAN/codex/hooks/"
|
|
84
|
+
for W in on-pre-tool on-stop; do
|
|
85
|
+
set +e
|
|
86
|
+
printf '{"cwd":"%s"}' "$ORPHAN" | bash "$ORPHAN/codex/hooks/$W.sh" >/dev/null 2>/dev/null
|
|
87
|
+
RC=$?
|
|
88
|
+
set -e
|
|
89
|
+
if [ "$RC" -eq 0 ]; then echo "PASS: $W fails open without hooks/scripts (exit 0)"; else echo "FAIL: $W orphan exit [$RC]"; FAIL=1; fi
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
# sessionStart: payload with neither "how" nor "source" falls back to startup and still injects
|
|
93
|
+
OUT="$(printf '{"cwd":"%s"}' "$TMP/project" | bash "$SCRIPT_DIR/on-session-start.sh")"
|
|
94
|
+
case "$OUT" in *"Round 1"*) echo "PASS: sessionStart falls back to startup without how/source" ;; *) echo "FAIL: sessionStart no-source [$OUT]"; FAIL=1 ;; esac
|
|
95
|
+
|
|
96
|
+
if [ "$FAIL" -eq 0 ]; then echo "All checks passed."; exit 0
|
|
97
|
+
else echo "Some checks FAILED."; exit 1
|
|
98
|
+
fi
|