gru-ai 0.2.0 → 0.3.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.
Files changed (128) hide show
  1. package/.claude/hooks/validate-gate.sh +231 -77
  2. package/.claude/hooks/validate-project-json.sh +38 -3
  3. package/.claude/hooks/validate-reviews.sh +50 -11
  4. package/.claude/skills/directive/SKILL.md +31 -18
  5. package/.claude/skills/directive/docs/pipeline/00-delegation-and-triage.md +13 -7
  6. package/.claude/skills/directive/docs/pipeline/01-checkpoint.md +1 -1
  7. package/.claude/skills/directive/docs/pipeline/02-read-directive.md +24 -1
  8. package/.claude/skills/directive/docs/pipeline/03-read-context.md +5 -0
  9. package/.claude/skills/directive/docs/pipeline/04-brainstorm.md +77 -0
  10. package/.claude/skills/directive/docs/pipeline/04b-clarification.md +222 -0
  11. package/.claude/skills/directive/docs/pipeline/05-planning.md +21 -9
  12. package/.claude/skills/directive/docs/pipeline/06-technical-audit.md +32 -23
  13. package/.claude/skills/directive/docs/pipeline/07-plan-approval.md +53 -37
  14. package/.claude/skills/directive/docs/pipeline/07b-project-brainstorm.md +45 -5
  15. package/.claude/skills/directive/docs/pipeline/08-worktree-and-state.md +1 -1
  16. package/.claude/skills/directive/docs/pipeline/09-execute-projects.md +229 -499
  17. package/.claude/skills/directive/docs/pipeline/10-wrapup.md +33 -12
  18. package/.claude/skills/directive/docs/pipeline/11-completion-gate.md +229 -35
  19. package/.claude/skills/directive/docs/reference/rules/failure-handling.md +7 -3
  20. package/.claude/skills/directive/docs/reference/rules/phase-definitions.md +10 -2
  21. package/.claude/skills/directive/docs/reference/rules/scope-and-dod.md +188 -18
  22. package/.claude/skills/directive/docs/reference/schemas/audit-output.md +8 -4
  23. package/.claude/skills/directive/docs/reference/schemas/brainstorm-output.md +2 -1
  24. package/.claude/skills/directive/docs/reference/schemas/checkpoint.md +2 -2
  25. package/.claude/skills/directive/docs/reference/schemas/directive-json.md +95 -21
  26. package/.claude/skills/directive/docs/reference/schemas/investigation-output.md +4 -4
  27. package/.claude/skills/directive/docs/reference/templates/architect-prompt.md +26 -14
  28. package/.claude/skills/directive/docs/reference/templates/brainstorm-prompt.md +23 -10
  29. package/.claude/skills/directive/docs/reference/templates/investigator-prompt.md +6 -6
  30. package/.claude/skills/directive/docs/reference/templates/planner-prompt.md +42 -4
  31. package/.claude/skills/smoke-test/SKILL.md +84 -0
  32. package/.claude/skills/smoke-test/run-smoke-test.sh +590 -0
  33. package/.claude/skills/smoke-test/scenarios.md +34 -0
  34. package/.claude/skills/walkthrough/SKILL.md +96 -0
  35. package/README.md +261 -110
  36. package/cli/templates/gruai.config.json.template +2 -0
  37. package/dist/assets/GamePage-OJgWSZBK.js +49 -0
  38. package/dist/assets/{index-Bh01am7W.js → index-BjwyXPf7.js} +5 -5
  39. package/dist/assets/index-D2wJ_yhU.css +1 -0
  40. package/dist/assets/metrocity/Character Model.png +0 -0
  41. package/dist/assets/metrocity/Hairs.png +0 -0
  42. package/dist/assets/metrocity/Outfit1.png +0 -0
  43. package/dist/assets/metrocity/Outfit2.png +0 -0
  44. package/dist/assets/metrocity/Outfit3.png +0 -0
  45. package/dist/assets/metrocity/Outfit4.png +0 -0
  46. package/dist/assets/metrocity/Outfit5.png +0 -0
  47. package/dist/assets/metrocity/Outfit6.png +0 -0
  48. package/dist/assets/office/anim-bathroom-cabinet.tsx +18 -0
  49. package/dist/assets/office/atlas.png +0 -0
  50. package/dist/assets/office/gruai.tmx +364 -0
  51. package/dist/assets/office/ui.png +0 -0
  52. package/dist/gruai.tmx +104 -0
  53. package/dist/index.html +4 -4
  54. package/dist-cli/commands/init.js +18 -12
  55. package/dist-cli/commands/scaffold.js +6 -1
  56. package/dist-cli/commands/validate-init.d.ts +18 -0
  57. package/dist-cli/commands/validate-init.js +39 -0
  58. package/dist-cli/index.js +1 -1
  59. package/dist-cli/lib/roles.js +15 -0
  60. package/dist-cli/lib/types.d.ts +12 -0
  61. package/dist-server/server/config.js +13 -2
  62. package/dist-server/server/index.js +16 -1
  63. package/dist-server/server/parsers/session-scanner.d.ts +9 -0
  64. package/dist-server/server/parsers/session-scanner.js +36 -0
  65. package/dist-server/server/parsers/session-state.d.ts +13 -4
  66. package/dist-server/server/parsers/session-state.js +24 -55
  67. package/dist-server/server/platform/claude-code-spawn.js +2 -0
  68. package/dist-server/server/platform/claude-code.d.ts +4 -0
  69. package/dist-server/server/platform/claude-code.js +39 -3
  70. package/dist-server/server/platform/types.d.ts +16 -1
  71. package/dist-server/server/platform/types.js +1 -1
  72. package/dist-server/server/types.d.ts +3 -0
  73. package/dist-server/server/watchers/directive-watcher.d.ts +2 -0
  74. package/dist-server/server/watchers/directive-watcher.js +74 -13
  75. package/dist-server/server/watchers/state-watcher.js +3 -0
  76. package/package.json +3 -2
  77. package/.claude/skills/directive/docs/pipeline/04-challenge.md +0 -38
  78. package/.claude/skills/directive/docs/reference/schemas/challenger-output.md +0 -13
  79. package/.claude/skills/directive/docs/reference/templates/challenger-prompt.md +0 -35
  80. package/dist/00_Modern_Office_Singles.tsx +0 -4
  81. package/dist/Game.tiled-project +0 -14
  82. package/dist/Game.tiled-session +0 -90
  83. package/dist/Interiors.tsx +0 -4
  84. package/dist/Interiors_32x32.tsx +0 -4
  85. package/dist/Office_Design_1.tsx +0 -4
  86. package/dist/Office_Design_2.tsx +0 -4
  87. package/dist/assets/GamePage-B2OsBjXm.js +0 -49
  88. package/dist/assets/characters/char_0.png +0 -0
  89. package/dist/assets/characters/char_1.png +0 -0
  90. package/dist/assets/characters/char_10.png +0 -0
  91. package/dist/assets/characters/char_11.png +0 -0
  92. package/dist/assets/characters/char_2.png +0 -0
  93. package/dist/assets/characters/char_3.png +0 -0
  94. package/dist/assets/characters/char_4.png +0 -0
  95. package/dist/assets/characters/char_5.png +0 -0
  96. package/dist/assets/characters/char_6.png +0 -0
  97. package/dist/assets/characters/char_7.png +0 -0
  98. package/dist/assets/characters/char_8.png +0 -0
  99. package/dist/assets/characters/char_9.png +0 -0
  100. package/dist/assets/index-DCNBE1pw.css +0 -1
  101. package/dist/assets/office/Interiors.png +0 -0
  102. package/dist/assets/office/classroom.png +0 -0
  103. package/dist/assets/office/conference.png +0 -0
  104. package/dist/assets/office/furniture.png +0 -0
  105. package/dist/assets/office/generic.png +0 -0
  106. package/dist/assets/office/kitchen.png +0 -0
  107. package/dist/assets/office/livingroom.png +0 -0
  108. package/dist/assets/office/music-sport.png +0 -0
  109. package/dist/assets/office/room-builder.png +0 -0
  110. package/dist/classroom.tsx +0 -4
  111. package/dist/conference.tsx +0 -4
  112. package/dist/furniture.tsx +0 -4
  113. package/dist/generic.tsx +0 -4
  114. package/dist/kitchen.tsx +0 -4
  115. package/dist/livingroom.tsx +0 -4
  116. package/dist/music-sport.tsx +0 -4
  117. package/dist/office.tmx +0 -398
  118. package/dist/room-builder.tsx +0 -4
  119. package/dist-server/scripts/intelligence-trends.d.ts +0 -100
  120. package/dist-server/scripts/intelligence-trends.js +0 -365
  121. package/dist-server/server/actions/cleanup.d.ts +0 -4
  122. package/dist-server/server/actions/cleanup.js +0 -30
  123. package/dist-server/server/parsers/team-parser.d.ts +0 -3
  124. package/dist-server/server/parsers/team-parser.js +0 -67
  125. package/dist-server/server/watchers/claude-watcher.d.ts +0 -17
  126. package/dist-server/server/watchers/claude-watcher.js +0 -130
  127. package/dist-server/server/watchers/context-watcher.d.ts +0 -22
  128. package/dist-server/server/watchers/context-watcher.js +0 -125
@@ -35,12 +35,20 @@ set -euo pipefail
35
35
  # path = relative to directive dir, supports {project-id} and {task-id} placeholders
36
36
  # required_fields = comma-separated jq paths (for json type only)
37
37
  #
38
- # Weight skip rules (from directive-watcher.ts SKIPPED_STEPS):
39
- # lightweight: skips challenge, brainstorm, approve (Morgan still plans, audit + project-brainstorm still run)
40
- # medium: skips challenge
38
+ # Pipeline step order (from SKILL.md):
39
+ # triage checkpoint read context audit brainstorm clarification →
40
+ # plan approve → project-brainstorm → setup → execute → review-gate → wrapup → completion
41
+ #
42
+ # Weight skip rules (from SKILL.md / 00-delegation-and-triage.md):
43
+ # lightweight: skips brainstorm (no C-suite challenges, no separate brainstorm agents)
44
+ # medium: skips brainstorm (COO's inline challenge is included, but no brainstorm step)
41
45
  # heavyweight: skips nothing
42
46
  # strategic: skips nothing
43
47
  #
48
+ # Note: 'challenge' was merged into 'brainstorm' — it is no longer a separate step ID.
49
+ # Clarification is auto-approved (not skipped) for lightweight/medium — it still runs.
50
+ # Approve is auto-approved (not skipped) for lightweight/medium — it still runs.
51
+ #
44
52
  # .skip marker convention:
45
53
  # For weight-conditional steps, a file named "{step}.skip" in the directive dir
46
54
  # satisfies the gate. Example: brainstorm.skip means brainstorm was legitimately
@@ -49,8 +57,8 @@ set -euo pipefail
49
57
 
50
58
  # Steps that can be skipped per weight class
51
59
  # Format: SKIP_<WEIGHT> is a space-separated list of skippable steps
52
- SKIP_lightweight="challenge brainstorm project-brainstorm audit approve"
53
- SKIP_medium="challenge"
60
+ SKIP_lightweight="brainstorm"
61
+ SKIP_medium="brainstorm"
54
62
  SKIP_heavyweight=""
55
63
  SKIP_strategic=""
56
64
 
@@ -77,14 +85,18 @@ TARGET_STEP="$2"
77
85
  TASK_ID="${3:-}"
78
86
 
79
87
  # Resolve to repo root for consistent paths
80
- REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
81
-
82
- # If directive-dir is relative, make it relative to repo root
83
- if [[ ! "$DIRECTIVE_DIR" = /* ]]; then
88
+ # If directive-dir is absolute and contains .context/directives/, derive repo root
89
+ # from it (supports worktrees where git rev-parse returns the wrong root).
90
+ if [[ "$DIRECTIVE_DIR" = /* ]] && [[ "$DIRECTIVE_DIR" == */.context/directives/* ]]; then
91
+ REPO_ROOT="${DIRECTIVE_DIR%%/.context/directives/*}"
92
+ DIRECTIVE_DIR_ABS="$DIRECTIVE_DIR"
93
+ DIRECTIVE_DIR="${DIRECTIVE_DIR#${REPO_ROOT}/}"
94
+ elif [[ ! "$DIRECTIVE_DIR" = /* ]]; then
95
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
84
96
  DIRECTIVE_DIR_ABS="${REPO_ROOT}/${DIRECTIVE_DIR}"
85
97
  else
98
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
86
99
  DIRECTIVE_DIR_ABS="$DIRECTIVE_DIR"
87
- # Make relative for output
88
100
  DIRECTIVE_DIR="${DIRECTIVE_DIR#${REPO_ROOT}/}"
89
101
  fi
90
102
 
@@ -227,85 +239,106 @@ check_directive_field() {
227
239
  add_artifact "directive.json:${field_path}"
228
240
  }
229
241
 
242
+ # Check a pipeline step is completed or skipped
243
+ check_step_completed_or_skipped() {
244
+ local step_id="$1" # the step that must be completed
245
+
246
+ # First check if the step has a completed status in directive.json
247
+ local status
248
+ status=$(jq -r ".pipeline.\"${step_id}\".status // empty" "$DIRECTIVE_JSON" 2>/dev/null)
249
+
250
+ if [[ "$status" == "completed" || "$status" == "skipped" ]]; then
251
+ add_artifact "directive.json:pipeline.${step_id}.status"
252
+ return 0
253
+ fi
254
+
255
+ # Check .skip marker for weight-conditional steps
256
+ if is_skippable "$step_id"; then
257
+ local skip_marker="${DIRECTIVE_DIR_ABS}/${step_id}.skip"
258
+ if [[ -f "$skip_marker" ]]; then
259
+ add_artifact "${DIRECTIVE_DIR}/${step_id}.skip"
260
+ return 0
261
+ fi
262
+ # Skippable but not completed/skipped — still valid (step was legitimately not run)
263
+ add_artifact "${DIRECTIVE_DIR}/${step_id} (skippable, not yet run)"
264
+ return 0
265
+ fi
266
+
267
+ add_violation "prerequisite_incomplete" "Prerequisite step '${step_id}' not completed (status: ${status:-missing}) (weight: ${WEIGHT})"
268
+ return 0
269
+ }
270
+
230
271
  # ---------------------------------------------------------------------------
231
272
  # Gate Definitions: what each step requires before it can run
232
273
  # ---------------------------------------------------------------------------
233
- # The NEXT step's gate validates the PREVIOUS step's artifact.
234
- # Chain: triage -> read -> context -> challenge -> brainstorm -> plan -> audit ->
235
- # approve -> project-brainstorm -> setup -> execute -> review-gate -> wrapup -> completion
236
- # Note: approve runs BEFORE project-brainstorm (project-brainstorm depends on approval).
274
+ # The gate for each step checks that its PREREQUISITE step is completed.
275
+ #
276
+ # Pipeline order (from SKILL.md):
277
+ # triage checkpoint read context → audit → brainstorm clarification
278
+ # plan → approve → project-brainstorm → setup → execute → review-gate → wrapup → completion
237
279
  # ---------------------------------------------------------------------------
238
280
 
239
- gate_read() {
281
+ gate_checkpoint() {
240
282
  # Requires: triage completed (weight set in directive.json)
241
283
  check_directive_field ".weight" "triage"
242
284
  check_directive_field ".pipeline.triage.status" "triage"
243
285
  }
244
286
 
287
+ gate_read() {
288
+ # Requires: checkpoint completed (or skipped — checkpoint just checks for resume)
289
+ # Checkpoint is not skippable per se but is always fast — require triage at minimum
290
+ check_directive_field ".weight" "triage"
291
+ check_directive_field ".pipeline.triage.status" "triage"
292
+ }
293
+
245
294
  gate_context() {
246
295
  # Requires: read completed
247
- check_directive_field ".pipeline.read.status" "read"
296
+ check_step_completed_or_skipped "read"
248
297
  }
249
298
 
250
- gate_brainstorm() {
299
+ gate_audit() {
251
300
  # Requires: context completed
252
- check_directive_field ".pipeline.context.status" "context"
301
+ check_step_completed_or_skipped "context"
253
302
  }
254
303
 
255
- gate_plan() {
256
- # Requires: brainstorm completed (or .skip for lightweight)
257
- check_file "brainstorm.md" "brainstorm"
304
+ gate_brainstorm() {
305
+ # Requires: audit completed (or skipped — audit always runs for all weights)
306
+ check_step_completed_or_skipped "audit"
258
307
  }
259
308
 
260
- gate_audit() {
261
- # Requires: plan completed (plan.json exists)
262
- check_json "plan.json" "plan" ".projects"
309
+ gate_clarification() {
310
+ # Requires: brainstorm completed (or skipped for lightweight/medium)
311
+ check_step_completed_or_skipped "brainstorm"
263
312
  }
264
313
 
265
- gate_approve() {
266
- # Requires: audit completed (audit artifact exists) + plan.json
267
- # Audit can produce audit.md, investigation.md, or conflicts-audit.md
268
- local found=false
269
- for f in audit.md investigation.md conflicts-audit.md; do
270
- if [[ -f "${DIRECTIVE_DIR_ABS}/${f}" ]]; then
271
- add_artifact "${DIRECTIVE_DIR}/${f}"
272
- found=true
273
- break
274
- fi
275
- done
276
- if [[ "$found" == "false" ]]; then
277
- if is_skippable "audit"; then
278
- local skip_marker="${DIRECTIVE_DIR_ABS}/audit.skip"
279
- if [[ -f "$skip_marker" ]]; then
280
- add_artifact "${DIRECTIVE_DIR}/audit.skip"
281
- else
282
- add_violation "missing_artifact" "Missing audit artifact: audit.md (weight: ${WEIGHT})"
283
- fi
284
- else
285
- add_violation "missing_artifact" "Missing audit artifact: audit.md (weight: ${WEIGHT})"
286
- fi
287
- fi
288
-
289
- # Also require plan.json
290
- check_json "plan.json" "plan" ".projects"
314
+ gate_plan() {
315
+ # Requires: clarification completed (or skipped)
316
+ # Clarification is auto-approved for lightweight/medium but still produces a
317
+ # pipeline.clarification.status entry. Check it completed or was skipped.
318
+ check_step_completed_or_skipped "clarification"
291
319
  }
292
320
 
293
- gate_challenge() {
294
- # Requires: context completed
295
- check_directive_field ".pipeline.context.status" "context"
321
+ gate_approve() {
322
+ # Requires: plan completed (plan.json exists with .projects)
323
+ check_json "plan.json" "plan" ".projects"
324
+ check_step_completed_or_skipped "plan"
296
325
  }
297
326
 
298
327
  gate_project_brainstorm() {
299
328
  # Requires: approve completed (approve runs before project-brainstorm)
300
- check_directive_field ".pipeline.approve.status" "approve"
329
+ check_step_completed_or_skipped "approve"
301
330
  # Also require plan.json (input to brainstorm)
302
331
  check_json "plan.json" "plan" ".projects"
303
332
  }
304
333
 
334
+ gate_setup() {
335
+ # Requires: project-brainstorm completed
336
+ check_step_completed_or_skipped "project-brainstorm"
337
+ }
338
+
305
339
  gate_execute() {
306
- # Requires: project-brainstorm completed (project.json with tasks exists)
307
- # Also requires approval
308
- check_directive_field ".pipeline.approve.status" "approve"
340
+ # Requires: setup completed + project.json(s) with tasks exist
341
+ check_step_completed_or_skipped "setup"
309
342
 
310
343
  # Check project.json(s) exist with tasks (output of project-brainstorm)
311
344
  local found_project=false
@@ -318,16 +351,7 @@ gate_execute() {
318
351
  done
319
352
  fi
320
353
  if [[ "$found_project" == "false" ]]; then
321
- if is_skippable "project-brainstorm"; then
322
- local skip_marker="${DIRECTIVE_DIR_ABS}/project-brainstorm.skip"
323
- if [[ -f "$skip_marker" ]]; then
324
- add_artifact "${DIRECTIVE_DIR}/project-brainstorm.skip"
325
- else
326
- add_violation "missing_artifact" "No projects/*/project.json found (project-brainstorm not completed)"
327
- fi
328
- else
329
- add_violation "missing_artifact" "No projects/*/project.json found (project-brainstorm not completed)"
330
- fi
354
+ add_violation "missing_artifact" "No projects/*/project.json found (project-brainstorm not completed)"
331
355
  fi
332
356
 
333
357
  # Per-task gate: if task-id provided, check that specific task exists in a project
@@ -349,12 +373,10 @@ gate_execute() {
349
373
  fi
350
374
  }
351
375
 
352
- gate_setup() {
353
- # Requires: project-brainstorm completed (or skipped for lightweight)
354
- check_directive_field ".pipeline.approve.status" "approve"
355
- }
356
-
357
376
  gate_review_gate() {
377
+ # Requires: execute completed
378
+ check_step_completed_or_skipped "execute"
379
+
358
380
  # Per-task gate: requires build-{task-id}.md exists for the task being reviewed
359
381
  if [[ -n "$TASK_ID" ]]; then
360
382
  # Find which project this task belongs to
@@ -381,6 +403,12 @@ gate_review_gate() {
381
403
  local task_ids
382
404
  task_ids=$(jq -r '.tasks[].id' "${pdir}project.json" 2>/dev/null)
383
405
  for tid in $task_ids; do
406
+ local task_status
407
+ task_status=$(jq -r --arg tid "$tid" '.tasks[] | select(.id == $tid) | .status' "${pdir}project.json" 2>/dev/null)
408
+ # Skipped/blocked tasks don't need build artifacts
409
+ if [[ "$task_status" == "skipped" || "$task_status" == "blocked" ]]; then
410
+ continue
411
+ fi
384
412
  if [[ ! -f "${pdir}build-${tid}.md" ]]; then
385
413
  add_violation "missing_artifact" "Missing build artifact: projects/${project_id}/build-${tid}.md"
386
414
  else
@@ -393,7 +421,9 @@ gate_review_gate() {
393
421
  }
394
422
 
395
423
  gate_wrapup() {
396
- # Requires: all tasks have review-{task-id}.md
424
+ # Requires: review-gate completed + all non-skipped tasks have review artifacts
425
+ check_step_completed_or_skipped "review-gate"
426
+
397
427
  for pdir in "${DIRECTIVE_DIR_ABS}"/projects/*/; do
398
428
  if [[ -f "${pdir}project.json" ]]; then
399
429
  local project_id
@@ -418,22 +448,146 @@ gate_wrapup() {
418
448
  }
419
449
 
420
450
  gate_completion() {
421
- # Requires: digest.md exists
422
- check_file "digest.md" "wrapup"
451
+ # Requires: wrapup completed + digest file exists at wrapup.digest_path
452
+ check_step_completed_or_skipped "wrapup"
453
+
454
+ # Check that the digest file exists. Wrapup writes to .context/reports/{name}-{date}.md
455
+ # and stores the path in directive.json at wrapup.digest_path
456
+ local digest_path
457
+ digest_path=$(jq -r '.wrapup.digest_path // empty' "$DIRECTIVE_JSON" 2>/dev/null)
458
+
459
+ if [[ -n "$digest_path" ]]; then
460
+ # digest_path may be relative to repo root or absolute
461
+ local full_digest_path
462
+ if [[ "$digest_path" = /* ]]; then
463
+ full_digest_path="$digest_path"
464
+ else
465
+ full_digest_path="${REPO_ROOT}/${digest_path}"
466
+ fi
467
+ if [[ -f "$full_digest_path" ]]; then
468
+ add_artifact "$digest_path"
469
+ else
470
+ add_violation "missing_artifact" "Digest file not found at wrapup.digest_path: ${digest_path}"
471
+ fi
472
+ else
473
+ # Fallback: check for report field (older convention: report = filename without extension)
474
+ local report
475
+ report=$(jq -r '.report // empty' "$DIRECTIVE_JSON" 2>/dev/null)
476
+ if [[ -n "$report" ]]; then
477
+ local report_path=".context/reports/${report}.md"
478
+ local full_report_path="${REPO_ROOT}/${report_path}"
479
+ if [[ -f "$full_report_path" ]]; then
480
+ add_artifact "$report_path"
481
+ else
482
+ add_violation "missing_artifact" "Digest file not found: ${report_path} (from directive.json .report)"
483
+ fi
484
+ else
485
+ add_violation "missing_field" "directive.json missing wrapup.digest_path (digest not written by wrapup step)"
486
+ fi
487
+ fi
488
+ }
489
+
490
+ # ---------------------------------------------------------------------------
491
+ # Pipeline state consistency check
492
+ # ---------------------------------------------------------------------------
493
+ # Verifies that directive.json.current_step matches the target step and that
494
+ # ALL prior steps in the pipeline have status "completed" or "skipped".
495
+ # This catches the failure mode where the orchestrator executes steps but
496
+ # forgets to update directive.json — a mechanical enforcement, not advisory.
497
+
498
+ FULL_PIPELINE_ORDER=(triage checkpoint read context audit brainstorm clarification plan approve project-brainstorm setup execute review-gate wrapup completion)
499
+
500
+ check_pipeline_state_consistency() {
501
+ local target="$1"
502
+
503
+ # 1. Check current_step matches target
504
+ local current_step
505
+ current_step=$(jq -r '.current_step // empty' "$DIRECTIVE_JSON" 2>/dev/null)
506
+
507
+ if [[ -n "$current_step" && "$current_step" != "$target" ]]; then
508
+ # Find positions of current_step and target in pipeline order
509
+ local current_pos=-1
510
+ local target_pos=-1
511
+ for i in "${!FULL_PIPELINE_ORDER[@]}"; do
512
+ if [[ "${FULL_PIPELINE_ORDER[$i]}" == "$current_step" ]]; then
513
+ current_pos=$i
514
+ fi
515
+ if [[ "${FULL_PIPELINE_ORDER[$i]}" == "$target" ]]; then
516
+ target_pos=$i
517
+ fi
518
+ done
519
+
520
+ # Only flag if target is AHEAD of current_step (steps were skipped)
521
+ if [[ $target_pos -gt $current_pos && $current_pos -ge 0 ]]; then
522
+ # Build list of steps between current_step and target that need updating
523
+ local stale_steps=""
524
+ for (( j=current_pos; j<target_pos; j++ )); do
525
+ local step_id="${FULL_PIPELINE_ORDER[$j]}"
526
+ local step_status
527
+ step_status=$(jq -r ".pipeline.\"${step_id}\".status // \"pending\"" "$DIRECTIVE_JSON" 2>/dev/null)
528
+ if [[ "$step_status" != "completed" && "$step_status" != "skipped" ]]; then
529
+ if [[ -n "$stale_steps" ]]; then
530
+ stale_steps="${stale_steps}, ${step_id}"
531
+ else
532
+ stale_steps="${step_id}"
533
+ fi
534
+ fi
535
+ done
536
+
537
+ if [[ -n "$stale_steps" ]]; then
538
+ add_violation "stale_pipeline_state" "directive.json.current_step is '${current_step}' but entering '${target}'. These steps were executed but not updated in directive.json: [${stale_steps}]. Update their pipeline status to 'completed' with output.summary, then set current_step to '${target}'."
539
+ fi
540
+ fi
541
+ fi
542
+
543
+ # 2. Check ALL prior steps have status completed or skipped
544
+ for step_id in "${FULL_PIPELINE_ORDER[@]}"; do
545
+ # Stop when we reach the target step
546
+ if [[ "$step_id" == "$target" ]]; then
547
+ break
548
+ fi
549
+
550
+ local step_status
551
+ step_status=$(jq -r ".pipeline.\"${step_id}\".status // \"pending\"" "$DIRECTIVE_JSON" 2>/dev/null)
552
+
553
+ if [[ "$step_status" == "completed" || "$step_status" == "skipped" ]]; then
554
+ continue
555
+ fi
556
+
557
+ # Check if this step is skippable for this weight
558
+ if is_skippable "$step_id"; then
559
+ continue
560
+ fi
561
+
562
+ # Step is not completed, not skipped, and not skippable — violation
563
+ local has_output
564
+ has_output=$(jq -r ".pipeline.\"${step_id}\".output.summary // empty" "$DIRECTIVE_JSON" 2>/dev/null)
565
+ if [[ -z "$has_output" ]]; then
566
+ add_violation "step_not_persisted" "Step '${step_id}' has status '${step_status}' with no output.summary. It was likely executed but not persisted to directive.json. Set pipeline.${step_id}.status to 'completed' with output.summary before proceeding."
567
+ else
568
+ add_violation "step_incomplete" "Step '${step_id}' has status '${step_status}' (expected 'completed' or 'skipped')"
569
+ fi
570
+ done
423
571
  }
424
572
 
573
+ # Run consistency check before per-step gates (skip for triage — first step)
574
+ if [[ "$TARGET_STEP" != "triage" ]]; then
575
+ check_pipeline_state_consistency "$TARGET_STEP"
576
+ fi
577
+
425
578
  # ---------------------------------------------------------------------------
426
579
  # Run the gate for the target step
427
580
  # ---------------------------------------------------------------------------
428
581
 
429
582
  case "$TARGET_STEP" in
430
583
  triage) ;; # No prerequisites for first step
584
+ checkpoint) gate_checkpoint ;;
431
585
  read) gate_read ;;
432
586
  context) gate_context ;;
433
- challenge) gate_challenge ;;
587
+ audit) gate_audit ;;
434
588
  brainstorm) gate_brainstorm ;;
589
+ clarification) gate_clarification ;;
435
590
  plan) gate_plan ;;
436
- audit) gate_audit ;;
437
591
  approve) gate_approve ;;
438
592
  project-brainstorm) gate_project_brainstorm ;;
439
593
  setup) gate_setup ;;
@@ -12,6 +12,7 @@
12
12
  #
13
13
  # Exit 0, no output = valid
14
14
  # Exit 0, JSON output = validation result (valid: true/false, violations: [...])
15
+ # Exit 1 = validation failure (violations found)
15
16
 
16
17
  set -euo pipefail
17
18
 
@@ -44,7 +45,7 @@ violations=()
44
45
  if [[ ! -f "$PROJECT_PATH" ]]; then
45
46
  violations+=("project.json does not exist at ${PROJECT_PATH}. The approve step must create it before execution begins.")
46
47
  echo "{\"valid\": false, \"violations\": $(printf '%s\n' "${violations[@]}" | jq -R . | jq -s .)}"
47
- exit 0
48
+ exit 1
48
49
  fi
49
50
 
50
51
  # Validate required fields
@@ -102,8 +103,42 @@ for i in $(seq 0 $((TASK_COUNT - 1))); do
102
103
  fi
103
104
  done
104
105
 
106
+ ## DOD completion check: completed tasks must have at least one dod[].met = true
107
+ for i in $(seq 0 $((TASK_COUNT - 1))); do
108
+ TASK_ID=$(jq -r ".tasks[$i].id // \"task-$i\"" "$PROJECT_PATH" 2>/dev/null)
109
+ TASK_STATUS=$(jq -r ".tasks[$i].status // \"\"" "$PROJECT_PATH" 2>/dev/null)
110
+ if [[ "$TASK_STATUS" == "completed" ]]; then
111
+ # Count how many DOD items have met=true
112
+ MET_COUNT=$(jq "[.tasks[$i].dod[]? | select(.met == true)] | length" "$PROJECT_PATH" 2>/dev/null || echo "0")
113
+ TOTAL_DOD=$(jq ".tasks[$i].dod | length" "$PROJECT_PATH" 2>/dev/null || echo "0")
114
+ if [[ "$TOTAL_DOD" -gt 0 && "$MET_COUNT" -eq 0 ]]; then
115
+ violations+=("tasks[$TASK_ID] is marked completed but ALL dod items have met=false — task was completed without DOD verification")
116
+ fi
117
+ fi
118
+ done
119
+
120
+ ## Browser test check: if browser_test is true, warn if no design-review.md
121
+ warnings=()
122
+ BROWSER_TEST=$(jq -r '.browser_test // false' "$PROJECT_PATH" 2>/dev/null)
123
+ if [[ "$BROWSER_TEST" == "true" ]]; then
124
+ PROJECT_DIR=$(dirname "$PROJECT_PATH")
125
+ if [[ ! -f "${PROJECT_DIR}/design-review.md" ]]; then
126
+ warnings+=("browser_test is true but no design-review.md found in ${PROJECT_DIR} — visual review may not have been recorded")
127
+ fi
128
+ fi
129
+
105
130
  if [[ ${#violations[@]} -eq 0 ]]; then
106
- echo '{"valid": true, "violations": []}'
131
+ if [[ ${#warnings[@]} -gt 0 ]]; then
132
+ echo "{\"valid\": true, \"violations\": [], \"warnings\": $(printf '%s\n' "${warnings[@]}" | jq -R . | jq -s .)}"
133
+ else
134
+ echo '{"valid": true, "violations": []}'
135
+ fi
136
+ exit 0
107
137
  else
108
- echo "{\"valid\": false, \"violations\": $(printf '%s\n' "${violations[@]}" | jq -R . | jq -s .)}"
138
+ if [[ ${#warnings[@]} -gt 0 ]]; then
139
+ echo "{\"valid\": false, \"violations\": $(printf '%s\n' "${violations[@]}" | jq -R . | jq -s .), \"warnings\": $(printf '%s\n' "${warnings[@]}" | jq -R . | jq -s .)}"
140
+ else
141
+ echo "{\"valid\": false, \"violations\": $(printf '%s\n' "${violations[@]}" | jq -R . | jq -s .)}"
142
+ fi
143
+ exit 1
109
144
  fi
@@ -7,10 +7,10 @@
7
7
  # Usage: echo '{"directive_dir":".context/directives/my-dir","project_id":"my-project"}' | ./validate-reviews.sh
8
8
  #
9
9
  # Checks project.json for completed tasks and verifies that:
10
- # 1. Tasks with "review" in their phases array were actually reviewed
11
- # (at least one DOD criterion evidence of external review)
12
- # 2. No completed task has ALL dod items marked true with zero reviewer spawns
13
- # (self-certification detection)
10
+ # 1. Project has reviewers assigned (existing check)
11
+ # 2. Review artifact files exist: review-{task-id}.md in the project directory
12
+ # 3. Task agent != project reviewers (catches self-review)
13
+ # 4. Self-certification heuristic (all DOD met with no review artifact)
14
14
  #
15
15
  # Exit 0, JSON output = validation result (valid: true/false, violations: [...])
16
16
 
@@ -40,10 +40,14 @@ if [[ ! -f "$PROJECT_PATH" ]]; then
40
40
  exit 0
41
41
  fi
42
42
 
43
+ # Derive project directory from project.json path
44
+ PROJECT_DIR=$(dirname "$PROJECT_PATH")
45
+
43
46
  violations=()
44
47
 
45
- # Get project-level reviewers
46
- PROJECT_REVIEWERS=$(jq -r '.reviewers | length' "$PROJECT_PATH" 2>/dev/null || echo "0")
48
+ # Get project-level reviewers as newline-separated list (bash 3.2 safe — no associative arrays)
49
+ PROJECT_REVIEWERS_COUNT=$(jq -r '.reviewers | length' "$PROJECT_PATH" 2>/dev/null || echo "0")
50
+ PROJECT_REVIEWERS_LIST=$(jq -r '.reviewers[]?' "$PROJECT_PATH" 2>/dev/null || echo "")
47
51
 
48
52
  TASK_COUNT=$(jq '.tasks | length' "$PROJECT_PATH" 2>/dev/null || echo "0")
49
53
 
@@ -60,16 +64,51 @@ for i in $(seq 0 $((TASK_COUNT - 1))); do
60
64
  HAS_REVIEW_PHASE=$(jq -r ".tasks[$i].phases | if . then (. | index(\"review\")) else null end" "$PROJECT_PATH" 2>/dev/null)
61
65
 
62
66
  if [[ "$HAS_REVIEW_PHASE" != "null" && "$HAS_REVIEW_PHASE" != "" ]]; then
63
- # Task has a review phase and is completed — verify project has reviewers
64
- if [[ "$PROJECT_REVIEWERS" -eq 0 ]]; then
67
+
68
+ # --- Check 1: Project has reviewers assigned ---
69
+ if [[ "$PROJECT_REVIEWERS_COUNT" -eq 0 ]]; then
65
70
  violations+=("Task '${TASK_ID}' is completed with review phase but has NO reviewers assigned")
66
71
  fi
67
72
 
68
- # Check if ALL DOD criteria are met — flag if the task was likely self-certified
69
- # (This is a heuristic: if all DOD = true but no review artifact directory exists, suspicious)
73
+ # --- Check 2: Review artifact file exists ---
74
+ # 09-execute-projects.md specifies: review-{task-id}.md or build-{task-id}.md
75
+ REVIEW_ARTIFACT="${PROJECT_DIR}/review-${TASK_ID}.md"
76
+ BUILD_ARTIFACT="${PROJECT_DIR}/build-${TASK_ID}.md"
77
+ if [[ ! -f "$REVIEW_ARTIFACT" && ! -f "$BUILD_ARTIFACT" ]]; then
78
+ violations+=("Task '${TASK_ID}' has no review artifact (expected review-${TASK_ID}.md or build-${TASK_ID}.md in ${PROJECT_DIR})")
79
+ fi
80
+
81
+ # --- Check 3: Self-review detection (task agent == project reviewer) ---
82
+ # Get the task-level agent (could be a string or array)
83
+ TASK_AGENT=$(jq -r ".tasks[$i].agent | if type == \"array\" then .[0] else . end // empty" "$PROJECT_PATH" 2>/dev/null)
84
+
85
+ if [[ -n "$TASK_AGENT" && -n "$PROJECT_REVIEWERS_LIST" ]]; then
86
+ # Check if the task agent appears in the project reviewers list
87
+ SELF_REVIEW="false"
88
+ while IFS= read -r reviewer; do
89
+ if [[ -n "$reviewer" && "$reviewer" = "$TASK_AGENT" ]]; then
90
+ SELF_REVIEW="true"
91
+ break
92
+ fi
93
+ done <<EOF
94
+ $PROJECT_REVIEWERS_LIST
95
+ EOF
96
+ if [[ "$SELF_REVIEW" = "true" ]]; then
97
+ # Self-review: task builder is also a reviewer — only flag if they are the ONLY reviewer
98
+ if [[ "$PROJECT_REVIEWERS_COUNT" -eq 1 ]]; then
99
+ violations+=("Task '${TASK_ID}' builder ('${TASK_AGENT}') is the only project reviewer — self-review detected")
100
+ fi
101
+ fi
102
+ fi
103
+
104
+ # --- Check 4: Self-certification heuristic ---
105
+ # All DOD met + no review artifact = likely self-certified
70
106
  DOD_COUNT=$(jq ".tasks[$i].dod | length" "$PROJECT_PATH" 2>/dev/null || echo "0")
71
107
  DOD_MET=$(jq "[.tasks[$i].dod[] | select(.met == true)] | length" "$PROJECT_PATH" 2>/dev/null || echo "0")
72
- DOD_UNMET=$(jq "[.tasks[$i].dod[] | select(.met == false)] | length" "$PROJECT_PATH" 2>/dev/null || echo "0")
108
+
109
+ if [[ "$DOD_COUNT" -gt 0 && "$DOD_MET" -eq "$DOD_COUNT" && ! -f "$REVIEW_ARTIFACT" ]]; then
110
+ violations+=("Task '${TASK_ID}' has ALL DOD criteria met but no review artifact — possible self-certification")
111
+ fi
73
112
 
74
113
  # Check for VISUAL GATE criteria that require browser verification
75
114
  VISUAL_GATES=$(jq -r "[.tasks[$i].dod[] | select(.criterion | test(\"VISUAL GATE|browser screenshot|verified by human\"; \"i\")) | select(.met == true)] | length" "$PROJECT_PATH" 2>/dev/null || echo "0")