@haposoft/cafekit 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -9
- package/bin/__tests__/codex-hooks.test.js +331 -0
- package/bin/__tests__/codex-native.test.js +458 -0
- package/bin/__tests__/installer-safety.test.js +115 -1
- package/bin/install.js +21 -7
- package/bin/lib/backup.js +23 -3
- package/bin/lib/codex-frontmatter.js +56 -0
- package/bin/lib/codex-install.js +195 -0
- package/bin/lib/context.js +65 -8
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/managed-writer.js +17 -2
- package/bin/lib/manifest.js +21 -4
- package/bin/lib/opencode-install.js +21 -19
- package/bin/lib/path-safety.js +40 -0
- package/bin/lib/version-check.js +25 -12
- package/bin/phases/claude-runtime.js +3 -5
- package/bin/phases/codex-runtime.js +110 -0
- package/bin/phases/copy-payload.js +36 -16
- package/bin/phases/post-install.js +84 -47
- package/bin/phases/root-config.js +4 -2
- package/bin/phases/select-platform.js +92 -58
- package/bin/phases/summary.js +6 -1
- package/package.json +3 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +8 -3
- package/src/codex/AGENTS.md +30 -0
- package/src/codex/agents-gitignore +11 -0
- package/src/codex/gitignore +12 -0
- package/src/codex/hooks/agent.cjs +66 -0
- package/src/codex/hooks/docs-sync.cjs +66 -0
- package/src/codex/hooks/inspect-block.cjs +40 -0
- package/src/codex/hooks/lib/hook-context.cjs +93 -0
- package/src/codex/hooks/lib/inspect-paths.cjs +199 -0
- package/src/codex/hooks/lib/privacy-state.cjs +199 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +78 -0
- package/src/codex/hooks/lib/spec-utils.cjs +50 -0
- package/src/codex/hooks/lib/state-store.cjs +166 -0
- package/src/codex/hooks/privacy-approval.cjs +59 -0
- package/src/codex/hooks/privacy-block.cjs +182 -0
- package/src/codex/hooks/rules.cjs +79 -0
- package/src/codex/hooks/session.cjs +114 -0
- package/src/codex/hooks/spec-gate.cjs +80 -0
- package/src/codex/hooks/spec-state.cjs +73 -0
- package/src/codex/hooks/state.cjs +157 -0
- package/src/codex/hooks/task-scaffold-guard.cjs +63 -0
- package/src/codex/hooks.json +122 -0
- package/src/codex/rules/hook-protocols.md +32 -0
- package/src/codex/rules/state-sync.md +41 -0
- package/src/codex/runtime.json +28 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
getHookContext,
|
|
8
|
+
logCrash,
|
|
9
|
+
readPayload
|
|
10
|
+
} = require('./lib/hook-context.cjs');
|
|
11
|
+
const {
|
|
12
|
+
archive,
|
|
13
|
+
readData,
|
|
14
|
+
refreshGit,
|
|
15
|
+
stateDir,
|
|
16
|
+
withStateLock,
|
|
17
|
+
writeData
|
|
18
|
+
} = require('./lib/state-store.cjs');
|
|
19
|
+
|
|
20
|
+
const EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
function summary(value, limit = 600) {
|
|
23
|
+
if (value === null || value === undefined) return '';
|
|
24
|
+
let text = '';
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
text = value;
|
|
27
|
+
} else if (typeof value === 'object') {
|
|
28
|
+
const preferred = value.message ?? value.output ?? value.result ?? value.content;
|
|
29
|
+
text = typeof preferred === 'string' ? preferred : JSON.stringify(value);
|
|
30
|
+
} else {
|
|
31
|
+
text = String(value);
|
|
32
|
+
}
|
|
33
|
+
const clean = text.replace(/\0/g, '').trim();
|
|
34
|
+
return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeTodos(items) {
|
|
38
|
+
if (!Array.isArray(items)) return [];
|
|
39
|
+
return items.map((item, index) => ({
|
|
40
|
+
id: String(item?.id ?? item?.task_id ?? item?.taskId ?? index),
|
|
41
|
+
content: summary(item?.step ?? item?.content ?? item?.subject ?? item?.title, 240),
|
|
42
|
+
status: String(item?.status || 'pending')
|
|
43
|
+
})).filter((item) => item.content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function updateTaskState(data, input, response) {
|
|
47
|
+
const completeList = input.plan ?? input.todos ?? input.tasks
|
|
48
|
+
?? response?.plan ?? response?.todos ?? response?.tasks;
|
|
49
|
+
if (Array.isArray(completeList)) {
|
|
50
|
+
data.todos = normalizeTodos(completeList);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const id = input.task_id ?? input.taskId ?? input.id;
|
|
55
|
+
const content = summary(input.step ?? input.content ?? input.subject ?? input.title, 240);
|
|
56
|
+
const status = input.status;
|
|
57
|
+
if (!id && !content) return;
|
|
58
|
+
const todos = Array.isArray(data.todos) ? data.todos : [];
|
|
59
|
+
const index = todos.findIndex((todo) => (
|
|
60
|
+
(id !== undefined && String(todo.id) === String(id))
|
|
61
|
+
|| (content && todo.content === content)
|
|
62
|
+
));
|
|
63
|
+
const next = {
|
|
64
|
+
id: String(id ?? content),
|
|
65
|
+
content: content || todos[index]?.content || `Task ${id}`,
|
|
66
|
+
status: String(status || todos[index]?.status || 'pending')
|
|
67
|
+
};
|
|
68
|
+
if (index >= 0) todos[index] = next;
|
|
69
|
+
else todos.push(next);
|
|
70
|
+
data.todos = todos;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTrackedTool(toolName) {
|
|
74
|
+
const name = String(toolName || '').toLowerCase();
|
|
75
|
+
return name === 'agent'
|
|
76
|
+
|| name === 'spawn_agent'
|
|
77
|
+
|| name === 'update_plan'
|
|
78
|
+
|| name.includes('task')
|
|
79
|
+
|| name.includes('todo');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadPrevious(projectRoot, sessionId) {
|
|
83
|
+
try {
|
|
84
|
+
const dir = stateDir(projectRoot, sessionId);
|
|
85
|
+
if (!dir) return '';
|
|
86
|
+
const file = path.join(dir, 'latest.md');
|
|
87
|
+
if (!fs.existsSync(file)) return '';
|
|
88
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
89
|
+
const generated = text.match(/<!-- Generated: (.+?) -->/)?.[1];
|
|
90
|
+
if (!generated || Date.now() - Date.parse(generated) > EXPIRY_MS) return '';
|
|
91
|
+
return text;
|
|
92
|
+
} catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const payload = readPayload();
|
|
99
|
+
if (!payload) process.exit(0);
|
|
100
|
+
const { projectRoot } = getHookContext(payload);
|
|
101
|
+
const event = payload.hook_event_name || '';
|
|
102
|
+
const sessionId = payload.session_id;
|
|
103
|
+
|
|
104
|
+
if (event === 'SessionStart') {
|
|
105
|
+
const previous = loadPrevious(projectRoot, sessionId);
|
|
106
|
+
if (previous) {
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`=== Prior Execution Context ===\n${previous.trim()}\n=== End of Prior Context ===\n`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (event === 'PostToolUse') {
|
|
115
|
+
const toolName = payload.tool_name || '';
|
|
116
|
+
if (!isTrackedTool(toolName)) process.exit(0);
|
|
117
|
+
withStateLock(projectRoot, sessionId, (dir) => {
|
|
118
|
+
const data = readData(dir);
|
|
119
|
+
const input = payload.tool_input || {};
|
|
120
|
+
const response = payload.tool_response;
|
|
121
|
+
updateTaskState(data, input, response);
|
|
122
|
+
data.lastToolEvent = `${toolName}: ${summary(response) || summary(input)}`.slice(0, 700);
|
|
123
|
+
refreshGit(data, projectRoot);
|
|
124
|
+
writeData(dir, data);
|
|
125
|
+
});
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (event === 'SubagentStop') {
|
|
130
|
+
withStateLock(projectRoot, sessionId, (dir) => {
|
|
131
|
+
const data = readData(dir);
|
|
132
|
+
const id = String(payload.agent_id || 'unknown');
|
|
133
|
+
data.agentResults = (data.agentResults || [])
|
|
134
|
+
.filter((agent) => String(agent.id) !== id);
|
|
135
|
+
data.agentResults.push({
|
|
136
|
+
id,
|
|
137
|
+
type: String(payload.agent_type || 'unknown'),
|
|
138
|
+
message: summary(payload.last_assistant_message)
|
|
139
|
+
});
|
|
140
|
+
refreshGit(data, projectRoot);
|
|
141
|
+
writeData(dir, data);
|
|
142
|
+
});
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event === 'Stop') {
|
|
147
|
+
withStateLock(projectRoot, sessionId, (dir) => {
|
|
148
|
+
const data = readData(dir);
|
|
149
|
+
data.lastAssistant = summary(payload.last_assistant_message);
|
|
150
|
+
refreshGit(data, projectRoot);
|
|
151
|
+
writeData(dir, data);
|
|
152
|
+
archive(dir);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logCrash('state', error);
|
|
157
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const {
|
|
7
|
+
getHookContext,
|
|
8
|
+
logCrash,
|
|
9
|
+
readPayload
|
|
10
|
+
} = require('./lib/hook-context.cjs');
|
|
11
|
+
|
|
12
|
+
function addedFiles(toolName, input) {
|
|
13
|
+
if (toolName === 'Write') {
|
|
14
|
+
return [input.file_path || input.filePath || input.path || ''].filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
if (toolName !== 'apply_patch') return [];
|
|
17
|
+
const patch = typeof input.command === 'string'
|
|
18
|
+
? input.command
|
|
19
|
+
: typeof input.patch === 'string' ? input.patch : '';
|
|
20
|
+
return [...patch.matchAll(/^\*\*\* Add File:\s+(.+)$/gm)]
|
|
21
|
+
.map((match) => match[1].trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function projectRelative(filePath, projectRoot, sessionCwd) {
|
|
25
|
+
const absolute = path.isAbsolute(filePath)
|
|
26
|
+
? path.normalize(filePath)
|
|
27
|
+
: path.resolve(sessionCwd, filePath);
|
|
28
|
+
return path.relative(projectRoot, absolute).replace(/\\/g, '/');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const payload = readPayload();
|
|
33
|
+
if (!payload) process.exit(0);
|
|
34
|
+
const { projectRoot, runtime, sessionCwd } = getHookContext(payload);
|
|
35
|
+
const candidates = addedFiles(payload.tool_name || '', payload.tool_input || {});
|
|
36
|
+
const taskFile = candidates
|
|
37
|
+
.map((requested) => ({
|
|
38
|
+
requested,
|
|
39
|
+
relative: projectRelative(requested, projectRoot, sessionCwd)
|
|
40
|
+
}))
|
|
41
|
+
.find(({ relative }) => (
|
|
42
|
+
/^specs\/[^/]+\/tasks\/task-[^/]+\.md$/.test(relative)
|
|
43
|
+
));
|
|
44
|
+
if (!taskFile) process.exit(0);
|
|
45
|
+
if (runtime.spec?.scaffold_guard === false) process.exit(0);
|
|
46
|
+
|
|
47
|
+
const scaffold = path.join(projectRoot, '.codex', 'scripts', 'spec-scaffold.cjs');
|
|
48
|
+
if (!fs.existsSync(scaffold)) process.exit(0);
|
|
49
|
+
|
|
50
|
+
const feature = taskFile.relative.split('/')[1];
|
|
51
|
+
const command = sessionCwd === projectRoot
|
|
52
|
+
? `node .codex/scripts/spec-scaffold.cjs ${feature}`
|
|
53
|
+
: `node ${JSON.stringify(scaffold)} ${feature}`;
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
'TASK SCAFFOLD REQUIRED: task files must be generated, not hand-written.\n'
|
|
56
|
+
+ `Blocked Write: ${taskFile.requested}\n\n`
|
|
57
|
+
+ 'Generate the stubs, then fill their {{...}} placeholders with apply_patch:\n'
|
|
58
|
+
+ ` ${command} --tasks "R0-01-slug,R1-01-slug,..." --tasks-only\n`
|
|
59
|
+
);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logCrash('task-scaffold-guard', error);
|
|
63
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "CafeKit project-local lifecycle hooks for Codex CLI. Review and trust with /hooks.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume|clear|compact",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/session.cjs\"",
|
|
11
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/session.cjs\""
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"type": "command",
|
|
15
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/docs-sync.cjs\"",
|
|
16
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/docs-sync.cjs\""
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\"",
|
|
21
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\""
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"SubagentStart": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "*",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/agent.cjs\"",
|
|
33
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/agent.cjs\""
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"UserPromptSubmit": [
|
|
39
|
+
{
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/privacy-approval.cjs\"",
|
|
44
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/privacy-approval.cjs\""
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/rules.cjs\"",
|
|
49
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/rules.cjs\""
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/spec-state.cjs\"",
|
|
54
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/spec-state.cjs\""
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"PreToolUse": [
|
|
60
|
+
{
|
|
61
|
+
"matcher": "Bash|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/privacy-block.cjs\"",
|
|
66
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/privacy-block.cjs\""
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/inspect-block.cjs\"",
|
|
71
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/inspect-block.cjs\""
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/task-scaffold-guard.cjs\"",
|
|
76
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/task-scaffold-guard.cjs\""
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"PostToolUse": [
|
|
82
|
+
{
|
|
83
|
+
"matcher": "Agent|spawn_agent|update_plan|task|Task|TodoWrite",
|
|
84
|
+
"hooks": [
|
|
85
|
+
{
|
|
86
|
+
"type": "command",
|
|
87
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\"",
|
|
88
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\""
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"Stop": [
|
|
94
|
+
{
|
|
95
|
+
"hooks": [
|
|
96
|
+
{
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/spec-gate.cjs\"",
|
|
99
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/spec-gate.cjs\""
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\"",
|
|
104
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\""
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"SubagentStop": [
|
|
110
|
+
{
|
|
111
|
+
"matcher": "*",
|
|
112
|
+
"hooks": [
|
|
113
|
+
{
|
|
114
|
+
"type": "command",
|
|
115
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\"",
|
|
116
|
+
"commandWindows": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/state.cjs\""
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Hook Protocols
|
|
2
|
+
|
|
3
|
+
Hooks are instruction boundaries. Do not bypass or work around them.
|
|
4
|
+
|
|
5
|
+
## Privacy Block Hook
|
|
6
|
+
|
|
7
|
+
Codex cannot pause a blocked tool call for an interactive permission question.
|
|
8
|
+
CafeKit therefore denies the sensitive access and emits one exact approval
|
|
9
|
+
phrase tied to the current session, requested path, tool, and expiry.
|
|
10
|
+
|
|
11
|
+
Required flow:
|
|
12
|
+
|
|
13
|
+
1. Stop the blocked access and show the user the hook's reason.
|
|
14
|
+
2. Ask the user to send the exact approval phrase if they want one retry.
|
|
15
|
+
3. Do not write, repeat as a user message, or otherwise simulate that phrase.
|
|
16
|
+
4. Retry only after a later real user prompt exactly matches it.
|
|
17
|
+
5. Use the resulting token once, only for the originally blocked operation.
|
|
18
|
+
|
|
19
|
+
Never use another command, path, encoding, subprocess, or side channel to read a
|
|
20
|
+
privacy-blocked file without that explicit approval.
|
|
21
|
+
|
|
22
|
+
## State And Spec Hooks
|
|
23
|
+
|
|
24
|
+
- If a hook reports state drift, run the appropriate sync or audit flow before continuing.
|
|
25
|
+
- If a hook rejects task completion, keep the task `in_progress` or `blocked` until proof exists.
|
|
26
|
+
- Do not mark a task `done` unless the matching task file contains a valid verification receipt.
|
|
27
|
+
|
|
28
|
+
## Hook Failure Handling
|
|
29
|
+
|
|
30
|
+
- Treat hook errors as blockers when they affect safety, privacy, or task state.
|
|
31
|
+
- Record the blocker in the task or spec state when relevant.
|
|
32
|
+
- If hook output is malformed, stop and report the exact failure instead of guessing.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Tollgate Protocol (State Sync)
|
|
2
|
+
|
|
3
|
+
## Sources Of Truth
|
|
4
|
+
|
|
5
|
+
Spec-driven work persists state in two synchronized layers:
|
|
6
|
+
|
|
7
|
+
1. `spec.json` tracks phase, overall status, and `task_registry`.
|
|
8
|
+
2. `tasks/task-*.md` tracks the human-readable checklist and verification receipt.
|
|
9
|
+
|
|
10
|
+
## Sync-back Rule
|
|
11
|
+
|
|
12
|
+
Before reporting a task completed or blocked:
|
|
13
|
+
|
|
14
|
+
### On success
|
|
15
|
+
|
|
16
|
+
1. Update `spec.json` only after verification. Keep `current_phase`,
|
|
17
|
+
`task_files`, status, timestamps, and the matching `task_registry` entry accurate.
|
|
18
|
+
2. Update the matching task Markdown status, checkboxes, completion criteria,
|
|
19
|
+
and `## Evidence` receipt from the current verification run.
|
|
20
|
+
3. Update native plan or task state when the active Codex session provides it,
|
|
21
|
+
but never use chat-only state as a substitute for the files above.
|
|
22
|
+
|
|
23
|
+
### On block after repeated attempts
|
|
24
|
+
|
|
25
|
+
1. Set the spec and matching registry entry to `blocked`, with the root cause and timestamp.
|
|
26
|
+
2. Mark the task Markdown `blocked` and record the evidence.
|
|
27
|
+
3. Tell the user or orchestrator exactly what external input or state change is required.
|
|
28
|
+
|
|
29
|
+
New specs use `in_progress`; legacy `in-progress` may be read but not emitted.
|
|
30
|
+
Never let `task_registry` disagree with its task Markdown file.
|
|
31
|
+
|
|
32
|
+
## Completion Gate
|
|
33
|
+
|
|
34
|
+
The Stop hook checks newly completed tasks. Each requires:
|
|
35
|
+
|
|
36
|
+
- `Status: done`;
|
|
37
|
+
- a non-placeholder `## Evidence` section with a command block or PASS/FAIL result;
|
|
38
|
+
- a non-empty `task_registry[path].completed_at`.
|
|
39
|
+
|
|
40
|
+
The first run seeds completion history without blocking. Humans can disable the
|
|
41
|
+
gate with `"spec": { "completion_gate": false }` in `.codex/runtime.json`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"privacyBlock": true,
|
|
3
|
+
"inspect": {
|
|
4
|
+
"enabled": true
|
|
5
|
+
},
|
|
6
|
+
"docs": {
|
|
7
|
+
"maxLoc": 800
|
|
8
|
+
},
|
|
9
|
+
"paths": {
|
|
10
|
+
"docs": "docs",
|
|
11
|
+
"specs": "specs",
|
|
12
|
+
"plans": "plans"
|
|
13
|
+
},
|
|
14
|
+
"spec": {
|
|
15
|
+
"scaffold_guard": true,
|
|
16
|
+
"completion_gate": true,
|
|
17
|
+
"tollgate": true
|
|
18
|
+
},
|
|
19
|
+
"develop": {
|
|
20
|
+
"parallel": true
|
|
21
|
+
},
|
|
22
|
+
"locale": {
|
|
23
|
+
"thinkingLanguage": null,
|
|
24
|
+
"responseLanguage": null
|
|
25
|
+
},
|
|
26
|
+
"hooks": {},
|
|
27
|
+
"assertions": []
|
|
28
|
+
}
|