odd-studio 3.5.1 → 3.7.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/.claude-plugin/plugin.json +5 -1
- package/README.md +28 -1
- package/bin/commands/status.js +8 -0
- package/bin/commands/upgrade.js +10 -0
- package/bin/odd-studio.js +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/hooks.json +16 -0
- package/hooks/odd-studio.sh +143 -2
- package/package.json +1 -1
- package/plugins/plugin-gates.js +34 -3
- package/plugins/plugin-quality-checks.js +20 -0
- package/scripts/command-definitions.js +5 -0
- package/scripts/scaffold-project.js +3 -2
- package/scripts/setup-hooks.js +6 -0
- package/scripts/state-schema.js +48 -0
- package/skill/SKILL.md +95 -11
- package/skill/docs/build/build-protocol.md +34 -0
- package/skill/docs/build/code-excellence.md +37 -1
- package/skill/docs/build/debug-protocol.md +141 -0
- package/skill/docs/chapters/chapter-10.md +4 -4
- package/skill/docs/planning/build-planner.md +32 -9
- package/skill/odd-debug/SKILL.md +166 -0
- package/templates/.odd/state.json +11 -1
- package/templates/AGENTS.md +16 -1
- package/templates/CLAUDE.md +27 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odd-studio",
|
|
3
3
|
"description": "Outcome-Driven Development — a planning and build harness for domain experts building serious software with AI. Installs the /odd skill, safety hooks, and project scaffolding into Claude Code.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.6.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ODD Studio"
|
|
7
7
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"odd",
|
|
10
10
|
"odd-plan",
|
|
11
11
|
"odd-build",
|
|
12
|
+
"odd-debug",
|
|
12
13
|
"odd-swarm",
|
|
13
14
|
"odd-status",
|
|
14
15
|
"odd-deploy"
|
|
@@ -19,11 +20,14 @@
|
|
|
19
20
|
"swarm-write",
|
|
20
21
|
"verify-gate",
|
|
21
22
|
"confirm-gate",
|
|
23
|
+
"checkpoint-gate",
|
|
22
24
|
"commit-gate",
|
|
23
25
|
"session-save",
|
|
24
26
|
"store-validate",
|
|
25
27
|
"sync-validate",
|
|
26
28
|
"code-quality",
|
|
29
|
+
"security-quality",
|
|
30
|
+
"checkpoint-validate",
|
|
27
31
|
"brief-quality",
|
|
28
32
|
"outcome-quality",
|
|
29
33
|
"swarm-guard"
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ npx odd-studio init --agent codex
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
In Codex, start ODD with a natural-language kickoff such as `use ODD`, `start ODD`, or `begin ODD`.
|
|
46
|
-
For a state check, use `ODD status`. To continue building, use `ODD build`.
|
|
46
|
+
For a state check, use `ODD status`. To continue building, use `ODD build`. To debug without leaving the active outcome flow, use `ODD debug`.
|
|
47
47
|
ODD now ships Codex skill-discovery metadata so these prompts can match the plugin directly instead of relying only on `AGENTS.md`.
|
|
48
48
|
|
|
49
49
|
If this project was originally set up for Claude Code or OpenCode and you want to add Codex later, run:
|
|
@@ -150,11 +150,13 @@ ODD Studio installs safety gates that run automatically throughout your build. T
|
|
|
150
150
|
| Brief gate | Before agent spawning | Blocks build agents until the session brief is confirmed |
|
|
151
151
|
| Swarm write gate | Before file writes | Blocks writes during swarm builds unless from an assigned agent |
|
|
152
152
|
| Verify gate | Before state edits | Blocks premature outcome confirmation |
|
|
153
|
+
| Checkpoint gate | Before confirm/commit | Blocks verification and commits until a fresh security scan clears the latest build changes |
|
|
153
154
|
| odd-flow build gate | Before agent spawning | Blocks builds without odd-flow sync |
|
|
154
155
|
| odd-flow commit gate | Before git commit | Blocks commits during build without odd-flow sync |
|
|
155
156
|
| Outcome quality | After writing outcomes | Checks all 6 fields present; flags banned technical vocabulary |
|
|
156
157
|
| Persona quality | After writing personas | Checks all 7 dimensions are present |
|
|
157
158
|
| Code elegance | After writing source files | Checks file length against ODD limits |
|
|
159
|
+
| Security baseline | After writing source files | Flags hardcoded secrets, insecure auth/session patterns, and unsafe rendering/network shortcuts |
|
|
158
160
|
| Session save | After git commit | Auto-saves project state for session continuity |
|
|
159
161
|
|
|
160
162
|
**Claude Code:** Implemented as shell hooks registered in `.claude/settings.local.json` (project-local).
|
|
@@ -174,6 +176,8 @@ When you're ready to build, ODD Studio initialises a odd-flow swarm — a team o
|
|
|
174
176
|
|
|
175
177
|
odd-flow MCP is configured automatically in your project-local agent config — `.mcp.json` for Claude Code, `opencode.json` for OpenCode, and `plugins/odd-studio/.mcp.json` for Codex. Every agent knows the full project state, regardless of which sessions they were spawned in.
|
|
176
178
|
|
|
179
|
+
When verification fails, use `*debug` instead of leaving the ODD flow. ODD Studio records the failure, selects an explicit debug strategy, keeps the outcome active, and routes the work back into verification once the defect is fixed.
|
|
180
|
+
|
|
177
181
|
---
|
|
178
182
|
|
|
179
183
|
## Using OpenCode with local models
|
|
@@ -272,6 +276,7 @@ These are the top-level direct commands you can invoke in Claude Code or OpenCod
|
|
|
272
276
|
| `/odd` | Start or resume an ODD project — the main planning and build orchestrator |
|
|
273
277
|
| `/odd-plan` | Start or continue the planning phase (personas, outcomes, contracts, Master Implementation Plan) |
|
|
274
278
|
| `/odd-build` | Start or continue a build session — reads project state and executes the build protocol |
|
|
279
|
+
| `/odd-debug` | Start or continue controlled debugging inside the active outcome — chooses `ui-behaviour`, `full-stack`, `auth-security`, `integration-contract`, `background-process`, or `performance-state` before any fix |
|
|
275
280
|
| `/odd-status` | Show full project state, phase progress, and what comes next |
|
|
276
281
|
| `/odd-swarm` | Build all independent outcomes in the current phase simultaneously using odd-flow parallel agents |
|
|
277
282
|
| `/odd-deploy` | Verify all outcomes are confirmed, then deploy the current phase to production |
|
|
@@ -285,6 +290,7 @@ Once ODD is active, you can use these sub-commands in Claude Code, OpenCode, or
|
|
|
285
290
|
*persona Work on personas with Diana
|
|
286
291
|
*outcome Write outcomes with Marcus
|
|
287
292
|
*contracts Map contracts with Theo
|
|
293
|
+
*debug Keep a failing build inside ODD and route it through an explicit debug strategy
|
|
288
294
|
*phase-plan Jump to implementation planning with Rachel
|
|
289
295
|
*ui Load UI excellence layer briefing
|
|
290
296
|
*agent Create a custom agent for a domain-specific concern
|
|
@@ -296,6 +302,27 @@ Once ODD is active, you can use these sub-commands in Claude Code, OpenCode, or
|
|
|
296
302
|
*reset Clear state and start over (asks for confirmation)
|
|
297
303
|
```
|
|
298
304
|
|
|
305
|
+
### When verification fails
|
|
306
|
+
|
|
307
|
+
Stay inside the ODD flow:
|
|
308
|
+
|
|
309
|
+
1. Describe the failure in domain language
|
|
310
|
+
2. Run `*debug` inside `/odd`, or use `/odd-debug` in Claude Code or OpenCode, or say `ODD debug` in Codex
|
|
311
|
+
3. Let ODD Studio classify the failure before fixing it:
|
|
312
|
+
- `ui-behaviour`
|
|
313
|
+
- `full-stack`
|
|
314
|
+
- `auth-security`
|
|
315
|
+
- `integration-contract`
|
|
316
|
+
- `background-process`
|
|
317
|
+
- `performance-state`
|
|
318
|
+
4. Verify again only after the fix returns the build to `verify` mode
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
|
|
322
|
+
> “The creator saves the price change, but the course page still shows the old amount.”
|
|
323
|
+
|
|
324
|
+
That should route to `full-stack`, because the defect crosses UI action, server handling, and persisted state. The harness now blocks quick fixes until the failure has been classified and the debug mode is recorded in `.odd/state.json`.
|
|
325
|
+
|
|
299
326
|
---
|
|
300
327
|
|
|
301
328
|
## CLI commands
|
package/bin/commands/status.js
CHANGED
|
@@ -22,7 +22,15 @@ export function registerStatus(program, deps) {
|
|
|
22
22
|
const state = await fs.readJson(stateFile);
|
|
23
23
|
console.log(chalk.bold(' Project: ') + chalk.cyan(state.projectName || 'unnamed'));
|
|
24
24
|
console.log(chalk.bold(' Phase: ') + (state.currentPhase || 'Not started'));
|
|
25
|
+
console.log(chalk.bold(' Mode: ') + (state.buildMode || 'idle'));
|
|
25
26
|
console.log(chalk.bold(' Step: ') + (state.currentStep || 'Not started'));
|
|
27
|
+
if (state.buildMode === 'debug') {
|
|
28
|
+
console.log(chalk.bold(' Debug: ') + (state.debugStrategy || 'strategy not set'));
|
|
29
|
+
console.log(chalk.bold(' Target: ') + (state.debugTarget || 'target not set'));
|
|
30
|
+
}
|
|
31
|
+
if (state.currentPhase === 'build') {
|
|
32
|
+
console.log(chalk.bold(' Checkpoint: ') + (state.checkpointStatus || 'unknown'));
|
|
33
|
+
}
|
|
26
34
|
print.blank();
|
|
27
35
|
|
|
28
36
|
if (state.personas?.length) {
|
package/bin/commands/upgrade.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
+
import { mergeStateWithDefaults } from '../../scripts/state-schema.js';
|
|
7
8
|
|
|
8
9
|
export function registerUpgrade(program, deps) {
|
|
9
10
|
const { print, PACKAGE_ROOT } = deps;
|
|
@@ -33,6 +34,7 @@ export function registerUpgrade(program, deps) {
|
|
|
33
34
|
if (isOpenCode) await upgradeOpenCode(PACKAGE_ROOT, targetDir);
|
|
34
35
|
if (isCodex) await upgradeCodex(PACKAGE_ROOT, targetDir);
|
|
35
36
|
await upgradeMcp(agent, targetDir);
|
|
37
|
+
await upgradeStateSchema(targetDir);
|
|
36
38
|
print.blank();
|
|
37
39
|
print.ok('Upgrade complete.');
|
|
38
40
|
print.blank();
|
|
@@ -83,6 +85,14 @@ async function upgradeMcp(agent, targetDir) {
|
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
async function upgradeStateSchema(targetDir) {
|
|
89
|
+
await runSpinner('Updating local ODD state schema...', 'ODD state schema updated', async () => {
|
|
90
|
+
const stateFile = path.resolve(targetDir, '.odd', 'state.json');
|
|
91
|
+
const state = mergeStateWithDefaults(await fs.readJson(stateFile));
|
|
92
|
+
await fs.writeJson(stateFile, state, { spaces: 2 });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
async function runSpinner(text, successMessage, task) {
|
|
87
97
|
const spinner = ora({ text, indent: 4 }).start();
|
|
88
98
|
try {
|
package/bin/odd-studio.js
CHANGED
|
@@ -15,7 +15,7 @@ import { registerUninstall } from './commands/uninstall.js';
|
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = path.dirname(__filename);
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
|
-
const pkg = require('../package.json'); // v3.
|
|
18
|
+
const pkg = require('../package.json'); // v3.6.0
|
|
19
19
|
|
|
20
20
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
21
21
|
const deps = { PACKAGE_ROOT, print };
|
package/codex-plugin/hooks.json
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
{
|
|
35
35
|
"matcher": "Bash",
|
|
36
36
|
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "./hooks/odd-studio.sh checkpoint-gate"
|
|
40
|
+
},
|
|
37
41
|
{
|
|
38
42
|
"type": "command",
|
|
39
43
|
"command": "./hooks/odd-studio.sh commit-gate"
|
|
@@ -55,6 +59,10 @@
|
|
|
55
59
|
{
|
|
56
60
|
"matcher": "Bash",
|
|
57
61
|
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "./hooks/odd-studio.sh checkpoint-validate"
|
|
65
|
+
},
|
|
58
66
|
{
|
|
59
67
|
"type": "command",
|
|
60
68
|
"command": "./hooks/odd-studio.sh session-save"
|
|
@@ -86,6 +94,10 @@
|
|
|
86
94
|
"type": "command",
|
|
87
95
|
"command": "./hooks/odd-studio.sh code-quality"
|
|
88
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "./hooks/odd-studio.sh security-quality"
|
|
100
|
+
},
|
|
89
101
|
{
|
|
90
102
|
"type": "command",
|
|
91
103
|
"command": "./hooks/odd-studio.sh brief-quality"
|
|
@@ -102,6 +114,10 @@
|
|
|
102
114
|
{
|
|
103
115
|
"type": "command",
|
|
104
116
|
"command": "./hooks/odd-studio.sh code-quality"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"type": "command",
|
|
120
|
+
"command": "./hooks/odd-studio.sh security-quality"
|
|
105
121
|
}
|
|
106
122
|
]
|
|
107
123
|
}
|
package/hooks/odd-studio.sh
CHANGED
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
# swarm-write Write|Edit — blocks source writes without swarm + agent token
|
|
13
13
|
# verify-gate Edit|Write — blocks marking outcomes verified without checklist
|
|
14
14
|
# confirm-gate Edit|Write — blocks briefConfirmed without odd-flow store
|
|
15
|
+
# checkpoint-gate Bash — blocks commits until a fresh Checkpoint scan clears the latest source changes
|
|
15
16
|
# commit-gate Bash — blocks git commit without odd-flow state stored
|
|
16
17
|
#
|
|
17
18
|
# PostToolUse (exit 0 + stderr = coaching):
|
|
18
19
|
# session-save Bash — auto-save state after git commit
|
|
20
|
+
# state-dirty-mark Write|Edit — marks state.json edits as needing odd-flow store
|
|
19
21
|
# store-validate mcp__odd-flow__memory_store — creates ready marker
|
|
20
22
|
# sync-validate mcp__odd-flow__coordination_sync — creates agents-ready marker
|
|
21
23
|
# code-quality Write|Edit — code elegance check
|
|
24
|
+
# security-quality Write|Edit — security baseline check + checkpoint dirty marker
|
|
25
|
+
# checkpoint-validate Bash — marks checkpoint clear after a successful scan
|
|
22
26
|
# brief-quality Write — session brief quality check
|
|
23
27
|
# outcome-quality Write — outcome/persona quality check
|
|
24
28
|
#
|
|
@@ -59,6 +63,7 @@ get_state_field() {
|
|
|
59
63
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || true)
|
|
60
64
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
|
|
61
65
|
CURRENT_PHASE=$(get_state_field "currentPhase")
|
|
66
|
+
BUILD_MODE=$(get_state_field "buildMode")
|
|
62
67
|
|
|
63
68
|
# Helper: check marker file exists and is not stale
|
|
64
69
|
# Default TTL is 24 hours (86400s) — build sessions can last many hours
|
|
@@ -172,6 +177,12 @@ swarm-write)
|
|
|
172
177
|
exit 2
|
|
173
178
|
fi
|
|
174
179
|
|
|
180
|
+
# Debug session bypass: orchestrator writes allowed when *debug mode is active
|
|
181
|
+
DEBUG_SESSION=$(get_state_field "debugSession")
|
|
182
|
+
if [ "$DEBUG_SESSION" = "true" ]; then
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
185
|
+
|
|
175
186
|
# Gate 2: Agent write token must be fresh (120s TTL)
|
|
176
187
|
# Only Task agents create this token — the orchestrator must NOT.
|
|
177
188
|
if ! marker_valid ".odd/.odd-flow-agent-token" 120; then
|
|
@@ -213,6 +224,18 @@ verify-gate)
|
|
|
213
224
|
[ "$NEW_HAS" -gt "$OLD_HAS" ] || exit 0
|
|
214
225
|
fi
|
|
215
226
|
|
|
227
|
+
if [ "$BUILD_MODE" = "debug" ]; then
|
|
228
|
+
echo "ODD STUDIO [verify-gate]: Cannot mark outcomes verified while debug mode is active." >&2
|
|
229
|
+
echo "Return buildMode to verify first, then run the verification walkthrough again." >&2
|
|
230
|
+
exit 2
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
if [ -f ".odd/.checkpoint-dirty" ] || [ ! -f ".odd/.checkpoint-clear" ]; then
|
|
234
|
+
echo "ODD STUDIO [verify-gate]: Verification blocked — a fresh Checkpoint scan has not cleared the latest source changes." >&2
|
|
235
|
+
echo "Run: npx @darrenjcoxon/vibeguard --security-only -o json" >&2
|
|
236
|
+
exit 2
|
|
237
|
+
fi
|
|
238
|
+
|
|
216
239
|
VERIFIED_CONFIRMED=$(get_state_field "verificationConfirmed")
|
|
217
240
|
if [ "$VERIFIED_CONFIRMED" != "true" ]; then
|
|
218
241
|
echo "ODD STUDIO [verify-gate]: Cannot mark NEW outcomes as verified." >&2
|
|
@@ -222,6 +245,29 @@ verify-gate)
|
|
|
222
245
|
exit 0
|
|
223
246
|
;;
|
|
224
247
|
|
|
248
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
# PreToolUse: Bash — blocks commit while checkpoint is dirty
|
|
250
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
checkpoint-gate)
|
|
252
|
+
[ "$TOOL_NAME" = "Bash" ] || exit 0
|
|
253
|
+
[ "$CURRENT_PHASE" = "build" ] || exit 0
|
|
254
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
255
|
+
echo "$COMMAND" | grep -qE 'git\s+commit' || exit 0
|
|
256
|
+
|
|
257
|
+
if [ "$BUILD_MODE" = "debug" ]; then
|
|
258
|
+
echo "ODD STUDIO [checkpoint-gate]: Commit blocked — debug mode is active." >&2
|
|
259
|
+
echo "Resolve the failure, return to verify mode, then commit only after verification passes." >&2
|
|
260
|
+
exit 2
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
if [ -f ".odd/.checkpoint-dirty" ] || [ ! -f ".odd/.checkpoint-clear" ]; then
|
|
264
|
+
echo "ODD STUDIO [checkpoint-gate]: Commit blocked — latest source changes have not passed Checkpoint." >&2
|
|
265
|
+
echo "Run: npx @darrenjcoxon/vibeguard --security-only -o json" >&2
|
|
266
|
+
exit 2
|
|
267
|
+
fi
|
|
268
|
+
exit 0
|
|
269
|
+
;;
|
|
270
|
+
|
|
225
271
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
226
272
|
# PreToolUse: Edit|Write — blocks briefConfirmed without odd-flow store
|
|
227
273
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -268,6 +314,10 @@ commit-gate)
|
|
|
268
314
|
swarm-guard)
|
|
269
315
|
[ "$CURRENT_PHASE" = "build" ] || exit 0
|
|
270
316
|
|
|
317
|
+
# Suppress all warnings during active debug session — reduced ceremony is the point
|
|
318
|
+
DEBUG_SESSION=$(get_state_field "debugSession")
|
|
319
|
+
[ "$DEBUG_SESSION" = "true" ] && exit 0
|
|
320
|
+
|
|
271
321
|
# Gate 1: Dirty state (commit without odd-flow store)
|
|
272
322
|
if [ -f ".odd/.odd-flow-state-dirty" ]; then
|
|
273
323
|
echo ""
|
|
@@ -336,6 +386,23 @@ session-save)
|
|
|
336
386
|
exit 0
|
|
337
387
|
;;
|
|
338
388
|
|
|
389
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
390
|
+
# PostToolUse: Bash — refresh checkpoint markers after a successful scan
|
|
391
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
392
|
+
checkpoint-validate)
|
|
393
|
+
[ "$TOOL_NAME" = "Bash" ] || exit 0
|
|
394
|
+
[ "$CURRENT_PHASE" = "build" ] || exit 0
|
|
395
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
396
|
+
echo "$COMMAND" | grep -qE '@darrenjcoxon/vibeguard|vibeguard' || exit 0
|
|
397
|
+
|
|
398
|
+
EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_response.exit_code // .tool_output.exit_code // .exitCode // empty' 2>/dev/null || true)
|
|
399
|
+
if [ -z "$EXIT_CODE" ] || [ "$EXIT_CODE" = "0" ]; then
|
|
400
|
+
touch .odd/.checkpoint-clear 2>/dev/null
|
|
401
|
+
rm -f .odd/.checkpoint-dirty 2>/dev/null
|
|
402
|
+
fi
|
|
403
|
+
exit 0
|
|
404
|
+
;;
|
|
405
|
+
|
|
339
406
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
340
407
|
# PostToolUse: Write|Edit state.json — blocks phase transition without Steps 9, 9b, 9d
|
|
341
408
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -377,6 +444,19 @@ plan-complete-gate)
|
|
|
377
444
|
exit 0
|
|
378
445
|
;;
|
|
379
446
|
|
|
447
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
448
|
+
# PostToolUse: Write|Edit state.json — mark dirty so swarm-guard nags until stored
|
|
449
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
450
|
+
# This catches the gap between commit-triggered dirty marking and actual edits.
|
|
451
|
+
# Any edit to state.json (by Claude or by another tool) sets the dirty marker.
|
|
452
|
+
# It's cleared only when mcp__odd-flow__memory_store key=odd-project-state succeeds.
|
|
453
|
+
state-dirty-mark)
|
|
454
|
+
[ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ] || exit 0
|
|
455
|
+
echo "$FILE_PATH" | grep -q '\.odd/state\.json$' || exit 0
|
|
456
|
+
touch .odd/.odd-flow-state-dirty 2>/dev/null
|
|
457
|
+
exit 0
|
|
458
|
+
;;
|
|
459
|
+
|
|
380
460
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
381
461
|
# PostToolUse: mcp__odd-flow__memory_store — creates ready marker
|
|
382
462
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -386,12 +466,36 @@ store-validate)
|
|
|
386
466
|
|
|
387
467
|
KEY=$(echo "$INPUT" | jq -r '.tool_input.key // empty')
|
|
388
468
|
|
|
389
|
-
|
|
390
|
-
|
|
469
|
+
# MCP responses may be nested under tool_response or at root — check both
|
|
470
|
+
if ! echo "$INPUT" | grep -qE '"success"[[:space:]]*:[[:space:]]*true'; then
|
|
471
|
+
exit 0
|
|
472
|
+
fi
|
|
391
473
|
|
|
392
474
|
# Create the right marker based on what was stored
|
|
393
475
|
case "$KEY" in
|
|
394
476
|
odd-project-state)
|
|
477
|
+
# Reject partial snapshots — the value MUST contain the full state.json shape.
|
|
478
|
+
# Without this, callers can store {currentBuildPhase: "X"} and silently drift.
|
|
479
|
+
VALUE=$(echo "$INPUT" | jq -c '.tool_input.value // empty' 2>/dev/null)
|
|
480
|
+
if [ -n "$VALUE" ] && [ "$VALUE" != "null" ] && [ "$VALUE" != "empty" ]; then
|
|
481
|
+
MISSING=$(echo "$VALUE" | jq -r '
|
|
482
|
+
[
|
|
483
|
+
(if has("personas") then empty else "personas" end),
|
|
484
|
+
(if has("outcomes") then empty else "outcomes" end),
|
|
485
|
+
(if has("currentBuildPhase") then empty else "currentBuildPhase" end),
|
|
486
|
+
(if has("currentPhase") then empty else "currentPhase" end)
|
|
487
|
+
] | join(", ")
|
|
488
|
+
' 2>/dev/null)
|
|
489
|
+
if [ -n "$MISSING" ]; then
|
|
490
|
+
echo "" >&2
|
|
491
|
+
echo "ODD STUDIO [store-validate]: Partial odd-project-state rejected." >&2
|
|
492
|
+
echo "Missing required keys: $MISSING" >&2
|
|
493
|
+
echo "Store the FULL contents of .odd/state.json, not a hand-built object." >&2
|
|
494
|
+
echo "" >&2
|
|
495
|
+
# Do NOT clear the dirty marker — the next store must include the full file
|
|
496
|
+
exit 0
|
|
497
|
+
fi
|
|
498
|
+
fi
|
|
395
499
|
touch .odd/.odd-flow-state-ready 2>/dev/null
|
|
396
500
|
rm -f .odd/.odd-flow-state-dirty 2>/dev/null
|
|
397
501
|
;;
|
|
@@ -474,6 +578,43 @@ code-quality)
|
|
|
474
578
|
exit 0
|
|
475
579
|
;;
|
|
476
580
|
|
|
581
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
582
|
+
# PostToolUse: Write|Edit — security baseline warnings + checkpoint dirty marker
|
|
583
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
584
|
+
security-quality)
|
|
585
|
+
[ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ] || exit 0
|
|
586
|
+
|
|
587
|
+
echo "$FILE_PATH" | grep -qiE '\.(ts|tsx|js|jsx|py|svelte|vue)$' || exit 0
|
|
588
|
+
echo "$FILE_PATH" | grep -qiE '(\.config\.|\.d\.ts|node_modules|\.next|dist/|build/|\.test\.|\.spec\.|__tests__)' && exit 0
|
|
589
|
+
[ -f "$FILE_PATH" ] || exit 0
|
|
590
|
+
|
|
591
|
+
if [ "$CURRENT_PHASE" = "build" ]; then
|
|
592
|
+
touch .odd/.checkpoint-dirty 2>/dev/null
|
|
593
|
+
rm -f .odd/.checkpoint-clear 2>/dev/null
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
ISSUES=""
|
|
597
|
+
|
|
598
|
+
grep -qEi '\b(api[_-]?key|secret|token|password)\b[^=\n]{0,40}[:=][[:space:]]*["'\''][^"'\'']{8,}["'\'']' "$FILE_PATH" 2>/dev/null \
|
|
599
|
+
&& ISSUES="$ISSUES\n - Possible hardcoded secret or credential literal"
|
|
600
|
+
grep -q 'dangerouslySetInnerHTML' "$FILE_PATH" 2>/dev/null \
|
|
601
|
+
&& ISSUES="$ISSUES\n - Unsafe HTML rendering detected — prove sanitisation or remove it"
|
|
602
|
+
grep -qEi '(localStorage|sessionStorage)\.(setItem|getItem)\([^)]*(token|session|auth|jwt)' "$FILE_PATH" 2>/dev/null \
|
|
603
|
+
&& ISSUES="$ISSUES\n - Client-side token or session storage detected"
|
|
604
|
+
grep -qEi 'strategy[[:space:]]*:[[:space:]]*["'\'']jwt["'\'']' "$FILE_PATH" 2>/dev/null \
|
|
605
|
+
&& ISSUES="$ISSUES\n - JWT session shortcut detected — prefer server-managed session state"
|
|
606
|
+
grep -qEi '(rejectUnauthorized|NODE_TLS_REJECT_UNAUTHORIZED|skipCsrfCheck|verify)[[:space:]]*[:=][[:space:]]*(false|0|["'\'']false["'\''])' "$FILE_PATH" 2>/dev/null \
|
|
607
|
+
&& ISSUES="$ISSUES\n - Security verification appears disabled"
|
|
608
|
+
|
|
609
|
+
if [ -n "$ISSUES" ]; then
|
|
610
|
+
echo "" >&2
|
|
611
|
+
echo "ODD SECURITY BASELINE: $(basename "$FILE_PATH")" >&2
|
|
612
|
+
echo -e "$ISSUES" >&2
|
|
613
|
+
echo "" >&2
|
|
614
|
+
fi
|
|
615
|
+
exit 0
|
|
616
|
+
;;
|
|
617
|
+
|
|
477
618
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
478
619
|
# PostToolUse: Write — session brief quality check
|
|
479
620
|
# ─────────────────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odd-studio",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"description": "Outcome-Driven Development for AI coding agents — a planning and build harness for domain experts building serious software with AI. Works with Claude Code, OpenCode, and Codex.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/plugins/plugin-gates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
-
import { checkBriefQuality, checkCodeElegance, checkOutcomeQuality, checkPersonaQuality } from './plugin-quality-checks.js';
|
|
2
|
+
import { checkBriefQuality, checkCodeElegance, checkOutcomeQuality, checkPersonaQuality, checkSecurityBaseline } from './plugin-quality-checks.js';
|
|
3
3
|
import { isBriefFile, isOutcomeFile, isPersonaFile, isSourceCodePath, isSrcFile } from './plugin-paths.js';
|
|
4
4
|
import { markerExists, markerValid, removeMarker, touchMarker } from './plugin-markers.js';
|
|
5
5
|
|
|
@@ -42,6 +42,12 @@ export function createBeforeHook(readState) {
|
|
|
42
42
|
|
|
43
43
|
if ((toolName === 'Edit' || toolName === 'Write') && filePath.includes('state.json')) {
|
|
44
44
|
const newContent = event.input?.new_string || event.input?.content || '';
|
|
45
|
+
if (newContent.includes('"verified"') && state.buildMode === 'debug') {
|
|
46
|
+
return { blocked: true, message: 'ODD VERIFY GATE: Cannot mark outcomes as verified while debug mode is active. Return buildMode to verify first.' };
|
|
47
|
+
}
|
|
48
|
+
if (newContent.includes('"verified"') && (markerExists('.checkpoint-dirty') || !markerExists('.checkpoint-clear'))) {
|
|
49
|
+
return { blocked: true, message: 'ODD CHECKPOINT GATE: Cannot mark outcomes as verified until a fresh Checkpoint scan clears the latest source changes.' };
|
|
50
|
+
}
|
|
45
51
|
if (newContent.includes('"verified"') && !state.verificationConfirmed) {
|
|
46
52
|
return { blocked: true, message: 'ODD VERIFY GATE: Cannot mark outcomes as verified. Walk through the verification checklist first.' };
|
|
47
53
|
}
|
|
@@ -53,6 +59,12 @@ export function createBeforeHook(readState) {
|
|
|
53
59
|
if (toolName === 'Bash' && state.currentPhase === 'build') {
|
|
54
60
|
const cmd = event.input?.command || '';
|
|
55
61
|
if (/git\s+commit/.test(cmd)) {
|
|
62
|
+
if (state.buildMode === 'debug') {
|
|
63
|
+
return { blocked: true, message: 'ODD DEBUG GATE: Commits are blocked while debug mode is active. Finish the fix and return to verification first.' };
|
|
64
|
+
}
|
|
65
|
+
if (markerExists('.checkpoint-dirty') || !markerExists('.checkpoint-clear')) {
|
|
66
|
+
return { blocked: true, message: 'ODD CHECKPOINT GATE: Commit blocked — run Checkpoint and clear the latest source changes first.' };
|
|
67
|
+
}
|
|
56
68
|
if (!markerExists('.odd-flow-state-ready')) {
|
|
57
69
|
return { blocked: true, message: 'ODD COMMIT GATE: Commit blocked — odd-flow state not stored. Call mcp__odd-flow__memory_store key=odd-project-state first.' };
|
|
58
70
|
}
|
|
@@ -70,13 +82,26 @@ export function createAfterHook(readState, readLastCommit, writeState) {
|
|
|
70
82
|
|
|
71
83
|
if (toolName === 'Write' && isOutcomeFile(filePath)) warnings.push(...checkOutcomeQuality(filePath));
|
|
72
84
|
if (toolName === 'Write' && isPersonaFile(filePath)) warnings.push(...checkPersonaQuality(filePath));
|
|
73
|
-
if ((toolName === 'Write' || toolName === 'Edit') && isSrcFile(filePath))
|
|
85
|
+
if ((toolName === 'Write' || toolName === 'Edit') && isSrcFile(filePath)) {
|
|
86
|
+
warnings.push(...checkCodeElegance(filePath));
|
|
87
|
+
warnings.push(...checkSecurityBaseline(filePath));
|
|
88
|
+
}
|
|
74
89
|
if (toolName === 'Write' && isBriefFile(filePath)) warnings.push(...checkBriefQuality(filePath));
|
|
75
90
|
|
|
91
|
+
const state = readState();
|
|
92
|
+
if ((toolName === 'Write' || toolName === 'Edit') && isSrcFile(filePath) && state?.currentPhase === 'build') {
|
|
93
|
+
touchMarker('.checkpoint-dirty');
|
|
94
|
+
removeMarker('.checkpoint-clear');
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
if (toolName === 'Bash') {
|
|
77
98
|
const cmd = event.input?.command || '';
|
|
99
|
+
const state = readState();
|
|
100
|
+
if (state?.currentPhase === 'build' && /@darrenjcoxon\/vibeguard|vibeguard/.test(cmd) && event.exitCode === 0) {
|
|
101
|
+
touchMarker('.checkpoint-clear');
|
|
102
|
+
removeMarker('.checkpoint-dirty');
|
|
103
|
+
}
|
|
78
104
|
if (/git\s+commit/.test(cmd) && event.exitCode === 0) {
|
|
79
|
-
const state = readState();
|
|
80
105
|
if (state) {
|
|
81
106
|
state.lastCommit = readLastCommit();
|
|
82
107
|
state.lastSaved = new Date().toISOString();
|
|
@@ -112,9 +137,15 @@ export function createChatHook(readState) {
|
|
|
112
137
|
return () => {
|
|
113
138
|
const state = readState();
|
|
114
139
|
if (!state || state.currentPhase !== 'build') return;
|
|
140
|
+
if (state.buildMode === 'debug') {
|
|
141
|
+
return { warnings: [`ODD DEBUG MODE: ${state.debugStrategy || 'Choose a strategy'} — keep the work inside the active outcome and return to verification when the fix is ready.`] };
|
|
142
|
+
}
|
|
115
143
|
if (markerExists('.odd-flow-state-dirty')) {
|
|
116
144
|
return { warnings: ['ODD STATE NOT SAVED: A git commit was made but state was not stored to odd-flow. Store it now.'] };
|
|
117
145
|
}
|
|
146
|
+
if (markerExists('.checkpoint-dirty')) {
|
|
147
|
+
return { warnings: ['ODD CHECKPOINT DIRTY: Source changes have not passed a fresh security scan yet. Run Checkpoint before verification or commit.'] };
|
|
148
|
+
}
|
|
118
149
|
|
|
119
150
|
const swarmPath = resolve(process.cwd(), '.odd', '.odd-flow-swarm-active');
|
|
120
151
|
if (!markerValid(swarmPath, 3600000)) {
|
|
@@ -79,6 +79,26 @@ export function checkCodeElegance(filePath) {
|
|
|
79
79
|
return warnings;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function checkSecurityBaseline(filePath) {
|
|
83
|
+
if (!existsSync(filePath) || !isSrcFile(filePath)) return [];
|
|
84
|
+
const content = readFileSync(filePath, 'utf8');
|
|
85
|
+
const warnings = [];
|
|
86
|
+
|
|
87
|
+
const riskyPatterns = [
|
|
88
|
+
[/\b(api[_-]?key|secret|token|password)\b[^=\n]{0,40}[:=]\s*['"`][^'"`\n]{8,}['"`]/i, 'Possible hardcoded secret or credential literal'],
|
|
89
|
+
[/dangerouslySetInnerHTML/, 'Unsafe HTML rendering detected — prove sanitisation or remove it'],
|
|
90
|
+
[/(localStorage|sessionStorage)\.(setItem|getItem)\([^)]*(token|session|auth|jwt)/i, 'Client-side token/session storage detected'],
|
|
91
|
+
[/strategy\s*:\s*['"]jwt['"]/i, 'JWT session shortcut detected — prefer server-managed session state'],
|
|
92
|
+
[/(rejectUnauthorized|NODE_TLS_REJECT_UNAUTHORIZED|skipCsrfCheck|verify)\s*[:=]\s*(false|0|['"]false['"])/i, 'Security verification appears disabled'],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const [pattern, message] of riskyPatterns) {
|
|
96
|
+
if (pattern.test(content)) warnings.push(message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return warnings;
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
export function checkBriefQuality(filePath) {
|
|
83
103
|
if (!existsSync(filePath)) return [];
|
|
84
104
|
const content = readFileSync(filePath, 'utf8');
|
|
@@ -17,6 +17,11 @@ export const COMMANDS = [
|
|
|
17
17
|
description: 'Start or continue a build session — reads project state and executes the build protocol',
|
|
18
18
|
body: 'You are executing the ODD Studio `*build` command.\n\nRead these two files now:\n1. `.opencode/odd/SKILL.md` — the full ODD Studio coach and build protocol\n2. `.opencode/odd/docs/build/build-protocol.md` — the Build Protocol detail\n\nThen execute the `*build` protocol exactly as documented in those files, starting from the state check.',
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
name: 'odd-debug',
|
|
22
|
+
description: 'Start or continue an in-flow ODD debugging session — selects the correct debug approach and returns to verification',
|
|
23
|
+
body: 'You are executing the ODD Studio `*debug` command.\n\nRead these two files now:\n1. `.opencode/odd/SKILL.md` — the full ODD Studio coach and build protocol\n2. `.opencode/odd/docs/build/debug-protocol.md` — the Debug Protocol detail\n\nBefore you inspect code or propose a fix, classify the failure into exactly one strategy:\n- `ui-behaviour` for visible interface-only failures\n- `full-stack` for browser-to-route-to-service-to-data failures\n- `auth-security` for auth, permissions, trust boundaries, uploads, or webhooks\n- `integration-contract` for producer-consumer or contract mismatches\n- `background-process` for jobs, queues, webhooks, or async delivery\n- `performance-state` for stale data, races, caching, or timing-sensitive defects\n\nIf the evidence is mixed, gather more evidence first and then choose the narrowest matching strategy. Do not guess. Do not apply a quick fix. Then execute the `*debug` protocol exactly as documented in those files, starting from the state check and recording the chosen strategy in `.odd/state.json` before any fix is attempted.',
|
|
24
|
+
},
|
|
20
25
|
{
|
|
21
26
|
name: 'odd-swarm',
|
|
22
27
|
description: 'Build all independent outcomes in the current phase simultaneously using odd-flow parallel agents',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import { mergeStateWithDefaults } from './state-schema.js';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -59,9 +60,9 @@ export default async function scaffoldProject(targetDir, projectName, agent = 'c
|
|
|
59
60
|
// Patch .odd/state.json with project name and timestamp
|
|
60
61
|
const stateFile = path.join(targetDir, '.odd', 'state.json');
|
|
61
62
|
if (fs.existsSync(stateFile)) {
|
|
62
|
-
const state = await fs.readJson(stateFile);
|
|
63
|
+
const state = mergeStateWithDefaults(await fs.readJson(stateFile));
|
|
63
64
|
state.projectName = projectName;
|
|
64
|
-
state.initialisedAt = new Date().toISOString();
|
|
65
|
+
state.initialisedAt = state.initialisedAt || new Date().toISOString();
|
|
65
66
|
await fs.writeJson(stateFile, state, { spaces: 2 });
|
|
66
67
|
}
|
|
67
68
|
|
package/scripts/setup-hooks.js
CHANGED
|
@@ -48,6 +48,7 @@ const GATES = [
|
|
|
48
48
|
event: 'PreToolUse',
|
|
49
49
|
matcher: 'Bash',
|
|
50
50
|
gates: [
|
|
51
|
+
{ name: 'checkpoint-gate', timeout: 5, status: 'ODD checkpoint gate...' },
|
|
51
52
|
{ name: 'commit-gate', timeout: 5, status: 'ODD commit gate...' },
|
|
52
53
|
],
|
|
53
54
|
},
|
|
@@ -65,6 +66,7 @@ const GATES = [
|
|
|
65
66
|
matcher: 'Write',
|
|
66
67
|
gates: [
|
|
67
68
|
{ name: 'plan-complete-gate', timeout: 5, status: 'ODD plan complete gate...' },
|
|
69
|
+
{ name: 'state-dirty-mark', timeout: 5, status: 'ODD state dirty mark...' },
|
|
68
70
|
],
|
|
69
71
|
},
|
|
70
72
|
{
|
|
@@ -72,12 +74,14 @@ const GATES = [
|
|
|
72
74
|
matcher: 'Edit',
|
|
73
75
|
gates: [
|
|
74
76
|
{ name: 'plan-complete-gate', timeout: 5, status: 'ODD plan complete gate...' },
|
|
77
|
+
{ name: 'state-dirty-mark', timeout: 5, status: 'ODD state dirty mark...' },
|
|
75
78
|
],
|
|
76
79
|
},
|
|
77
80
|
{
|
|
78
81
|
event: 'PostToolUse',
|
|
79
82
|
matcher: 'Bash',
|
|
80
83
|
gates: [
|
|
84
|
+
{ name: 'checkpoint-validate', timeout: 10, status: 'ODD checkpoint validate...' },
|
|
81
85
|
{ name: 'session-save', timeout: 10, status: 'ODD session save...' },
|
|
82
86
|
],
|
|
83
87
|
},
|
|
@@ -100,6 +104,7 @@ const GATES = [
|
|
|
100
104
|
matcher: 'Write',
|
|
101
105
|
gates: [
|
|
102
106
|
{ name: 'code-quality', timeout: 5, status: 'ODD code quality...' },
|
|
107
|
+
{ name: 'security-quality', timeout: 5, status: 'ODD security quality...' },
|
|
103
108
|
{ name: 'brief-quality', timeout: 5, status: 'ODD brief quality...' },
|
|
104
109
|
{ name: 'outcome-quality', timeout: 5, status: 'ODD outcome quality...' },
|
|
105
110
|
],
|
|
@@ -109,6 +114,7 @@ const GATES = [
|
|
|
109
114
|
matcher: 'Edit',
|
|
110
115
|
gates: [
|
|
111
116
|
{ name: 'code-quality', timeout: 5, status: 'ODD code quality...' },
|
|
117
|
+
{ name: 'security-quality', timeout: 5, status: 'ODD security quality...' },
|
|
112
118
|
],
|
|
113
119
|
},
|
|
114
120
|
];
|