odd-studio 3.5.1 → 3.6.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.
@@ -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,6 +12,7 @@
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):
@@ -19,6 +20,8 @@
19
20
  # store-validate mcp__odd-flow__memory_store — creates ready marker
20
21
  # sync-validate mcp__odd-flow__coordination_sync — creates agents-ready marker
21
22
  # code-quality Write|Edit — code elegance check
23
+ # security-quality Write|Edit — security baseline check + checkpoint dirty marker
24
+ # checkpoint-validate Bash — marks checkpoint clear after a successful scan
22
25
  # brief-quality Write — session brief quality check
23
26
  # outcome-quality Write — outcome/persona quality check
24
27
  #
@@ -59,6 +62,7 @@ get_state_field() {
59
62
  TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || true)
60
63
  FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
61
64
  CURRENT_PHASE=$(get_state_field "currentPhase")
65
+ BUILD_MODE=$(get_state_field "buildMode")
62
66
 
63
67
  # Helper: check marker file exists and is not stale
64
68
  # Default TTL is 24 hours (86400s) — build sessions can last many hours
@@ -213,6 +217,18 @@ verify-gate)
213
217
  [ "$NEW_HAS" -gt "$OLD_HAS" ] || exit 0
214
218
  fi
215
219
 
220
+ if [ "$BUILD_MODE" = "debug" ]; then
221
+ echo "ODD STUDIO [verify-gate]: Cannot mark outcomes verified while debug mode is active." >&2
222
+ echo "Return buildMode to verify first, then run the verification walkthrough again." >&2
223
+ exit 2
224
+ fi
225
+
226
+ if [ -f ".odd/.checkpoint-dirty" ] || [ ! -f ".odd/.checkpoint-clear" ]; then
227
+ echo "ODD STUDIO [verify-gate]: Verification blocked — a fresh Checkpoint scan has not cleared the latest source changes." >&2
228
+ echo "Run: npx @darrenjcoxon/vibeguard --security-only -o json" >&2
229
+ exit 2
230
+ fi
231
+
216
232
  VERIFIED_CONFIRMED=$(get_state_field "verificationConfirmed")
217
233
  if [ "$VERIFIED_CONFIRMED" != "true" ]; then
218
234
  echo "ODD STUDIO [verify-gate]: Cannot mark NEW outcomes as verified." >&2
@@ -222,6 +238,29 @@ verify-gate)
222
238
  exit 0
223
239
  ;;
224
240
 
241
+ # ─────────────────────────────────────────────────────────────────────────────
242
+ # PreToolUse: Bash — blocks commit while checkpoint is dirty
243
+ # ─────────────────────────────────────────────────────────────────────────────
244
+ checkpoint-gate)
245
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
246
+ [ "$CURRENT_PHASE" = "build" ] || exit 0
247
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
248
+ echo "$COMMAND" | grep -qE 'git\s+commit' || exit 0
249
+
250
+ if [ "$BUILD_MODE" = "debug" ]; then
251
+ echo "ODD STUDIO [checkpoint-gate]: Commit blocked — debug mode is active." >&2
252
+ echo "Resolve the failure, return to verify mode, then commit only after verification passes." >&2
253
+ exit 2
254
+ fi
255
+
256
+ if [ -f ".odd/.checkpoint-dirty" ] || [ ! -f ".odd/.checkpoint-clear" ]; then
257
+ echo "ODD STUDIO [checkpoint-gate]: Commit blocked — latest source changes have not passed Checkpoint." >&2
258
+ echo "Run: npx @darrenjcoxon/vibeguard --security-only -o json" >&2
259
+ exit 2
260
+ fi
261
+ exit 0
262
+ ;;
263
+
225
264
  # ─────────────────────────────────────────────────────────────────────────────
226
265
  # PreToolUse: Edit|Write — blocks briefConfirmed without odd-flow store
227
266
  # ─────────────────────────────────────────────────────────────────────────────
@@ -336,6 +375,23 @@ session-save)
336
375
  exit 0
337
376
  ;;
338
377
 
378
+ # ─────────────────────────────────────────────────────────────────────────────
379
+ # PostToolUse: Bash — refresh checkpoint markers after a successful scan
380
+ # ─────────────────────────────────────────────────────────────────────────────
381
+ checkpoint-validate)
382
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
383
+ [ "$CURRENT_PHASE" = "build" ] || exit 0
384
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
385
+ echo "$COMMAND" | grep -qE '@darrenjcoxon/vibeguard|vibeguard' || exit 0
386
+
387
+ EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_response.exit_code // .tool_output.exit_code // .exitCode // empty' 2>/dev/null || true)
388
+ if [ -z "$EXIT_CODE" ] || [ "$EXIT_CODE" = "0" ]; then
389
+ touch .odd/.checkpoint-clear 2>/dev/null
390
+ rm -f .odd/.checkpoint-dirty 2>/dev/null
391
+ fi
392
+ exit 0
393
+ ;;
394
+
339
395
  # ─────────────────────────────────────────────────────────────────────────────
340
396
  # PostToolUse: Write|Edit state.json — blocks phase transition without Steps 9, 9b, 9d
341
397
  # ─────────────────────────────────────────────────────────────────────────────
@@ -474,6 +530,43 @@ code-quality)
474
530
  exit 0
475
531
  ;;
476
532
 
533
+ # ─────────────────────────────────────────────────────────────────────────────
534
+ # PostToolUse: Write|Edit — security baseline warnings + checkpoint dirty marker
535
+ # ─────────────────────────────────────────────────────────────────────────────
536
+ security-quality)
537
+ [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ] || exit 0
538
+
539
+ echo "$FILE_PATH" | grep -qiE '\.(ts|tsx|js|jsx|py|svelte|vue)$' || exit 0
540
+ echo "$FILE_PATH" | grep -qiE '(\.config\.|\.d\.ts|node_modules|\.next|dist/|build/|\.test\.|\.spec\.|__tests__)' && exit 0
541
+ [ -f "$FILE_PATH" ] || exit 0
542
+
543
+ if [ "$CURRENT_PHASE" = "build" ]; then
544
+ touch .odd/.checkpoint-dirty 2>/dev/null
545
+ rm -f .odd/.checkpoint-clear 2>/dev/null
546
+ fi
547
+
548
+ ISSUES=""
549
+
550
+ grep -qEi '\b(api[_-]?key|secret|token|password)\b[^=\n]{0,40}[:=][[:space:]]*["'\''][^"'\'']{8,}["'\'']' "$FILE_PATH" 2>/dev/null \
551
+ && ISSUES="$ISSUES\n - Possible hardcoded secret or credential literal"
552
+ grep -q 'dangerouslySetInnerHTML' "$FILE_PATH" 2>/dev/null \
553
+ && ISSUES="$ISSUES\n - Unsafe HTML rendering detected — prove sanitisation or remove it"
554
+ grep -qEi '(localStorage|sessionStorage)\.(setItem|getItem)\([^)]*(token|session|auth|jwt)' "$FILE_PATH" 2>/dev/null \
555
+ && ISSUES="$ISSUES\n - Client-side token or session storage detected"
556
+ grep -qEi 'strategy[[:space:]]*:[[:space:]]*["'\'']jwt["'\'']' "$FILE_PATH" 2>/dev/null \
557
+ && ISSUES="$ISSUES\n - JWT session shortcut detected — prefer server-managed session state"
558
+ grep -qEi '(rejectUnauthorized|NODE_TLS_REJECT_UNAUTHORIZED|skipCsrfCheck|verify)[[:space:]]*[:=][[:space:]]*(false|0|["'\'']false["'\''])' "$FILE_PATH" 2>/dev/null \
559
+ && ISSUES="$ISSUES\n - Security verification appears disabled"
560
+
561
+ if [ -n "$ISSUES" ]; then
562
+ echo "" >&2
563
+ echo "ODD SECURITY BASELINE: $(basename "$FILE_PATH")" >&2
564
+ echo -e "$ISSUES" >&2
565
+ echo "" >&2
566
+ fi
567
+ exit 0
568
+ ;;
569
+
477
570
  # ─────────────────────────────────────────────────────────────────────────────
478
571
  # PostToolUse: Write — session brief quality check
479
572
  # ─────────────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -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 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
  },
@@ -78,6 +79,7 @@ const GATES = [
78
79
  event: 'PostToolUse',
79
80
  matcher: 'Bash',
80
81
  gates: [
82
+ { name: 'checkpoint-validate', timeout: 10, status: 'ODD checkpoint validate...' },
81
83
  { name: 'session-save', timeout: 10, status: 'ODD session save...' },
82
84
  ],
83
85
  },
@@ -100,6 +102,7 @@ const GATES = [
100
102
  matcher: 'Write',
101
103
  gates: [
102
104
  { name: 'code-quality', timeout: 5, status: 'ODD code quality...' },
105
+ { name: 'security-quality', timeout: 5, status: 'ODD security quality...' },
103
106
  { name: 'brief-quality', timeout: 5, status: 'ODD brief quality...' },
104
107
  { name: 'outcome-quality', timeout: 5, status: 'ODD outcome quality...' },
105
108
  ],
@@ -109,6 +112,7 @@ const GATES = [
109
112
  matcher: 'Edit',
110
113
  gates: [
111
114
  { name: 'code-quality', timeout: 5, status: 'ODD code quality...' },
115
+ { name: 'security-quality', timeout: 5, status: 'ODD security quality...' },
112
116
  ],
113
117
  },
114
118
  ];
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ export const STATE_VERSION = '2.1.0';
4
+
5
+ export const STATE_DEFAULTS = {
6
+ version: STATE_VERSION,
7
+ projectName: null,
8
+ initialisedAt: null,
9
+ lastSaved: null,
10
+ lastCommit: null,
11
+ lastCommitAt: null,
12
+ currentPhase: 'planning',
13
+ planningPhase: 'not-started',
14
+ currentStep: null,
15
+ personas: [],
16
+ outcomes: [],
17
+ contractsMapped: false,
18
+ planApproved: false,
19
+ techStackDecided: false,
20
+ designApproachDecided: false,
21
+ architectureDocGenerated: false,
22
+ servicesConfigured: false,
23
+ sessionBriefExported: false,
24
+ sessionBriefCount: 0,
25
+ briefConfirmed: false,
26
+ verificationConfirmed: false,
27
+ swarmActive: false,
28
+ buildPhase: null,
29
+ currentBuildPhase: null,
30
+ buildMode: 'idle',
31
+ debugStrategy: null,
32
+ debugTarget: null,
33
+ debugSummary: null,
34
+ debugStartedAt: null,
35
+ checkpointStatus: 'unknown',
36
+ lastCheckpointAt: null,
37
+ checkpointFindings: 0,
38
+ securityBaselineVersion: '2026-04-12',
39
+ notes: '',
40
+ };
41
+
42
+ export function mergeStateWithDefaults(state = {}) {
43
+ return {
44
+ ...STATE_DEFAULTS,
45
+ ...state,
46
+ version: STATE_VERSION,
47
+ };
48
+ }