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.
@@ -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.5.1",
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
@@ -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) {
@@ -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.5.1
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 };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odd-studio",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "Outcome-Driven Development planning and build harness for domain experts building serious software with AI.",
5
5
  "author": {
6
6
  "name": "ODD Studio"
@@ -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
  }
@@ -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
- SUCCESS=$(echo "$INPUT" | jq -r '.tool_response.success // false' 2>/dev/null || echo "false")
390
- [ "$SUCCESS" = "true" ] || exit 0
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.5.1",
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",
@@ -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)) warnings.push(...checkCodeElegance(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
 
@@ -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
  ];