@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5

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 (101) hide show
  1. package/core/.antigravity-plugin/plugin.json +0 -1
  2. package/core/.claude-plugin/plugin.json +1 -1
  3. package/core/.codex-plugin/plugin.json +1 -1
  4. package/core/.plugin/plugin.json +1 -1
  5. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  6. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  8. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  9. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  10. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  11. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  12. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  13. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  15. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  16. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  17. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  18. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  19. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  20. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  22. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  23. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  24. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  26. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  27. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  28. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  29. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  30. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  31. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  32. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  33. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  34. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  35. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  36. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  37. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  38. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  39. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  40. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  41. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  42. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  43. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  44. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  45. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  46. package/core/registry/assets.json +19 -0
  47. package/core/registry/contracts.json +2 -2
  48. package/core/registry/harnesses.json +196 -10
  49. package/core/registry/lifecycle.json +2626 -7
  50. package/core/registry/storage.json +24 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  52. package/core/shared/skills/do-design/SKILL.md +6 -6
  53. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  54. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  55. package/core/shared/skills/do-implement/SKILL.md +3 -2
  56. package/core/shared/skills/do-plan/SKILL.md +7 -8
  57. package/package.json +1 -1
  58. package/src/adapters/antigravity/index.js +83 -47
  59. package/src/adapters/claude/index.js +15 -1
  60. package/src/adapters/codex/hooks.js +40 -5
  61. package/src/adapters/codex/index.js +52 -9
  62. package/src/adapters/copy-tree.js +4 -4
  63. package/src/adapters/gemini/hooks.js +57 -16
  64. package/src/adapters/gemini/index.js +57 -18
  65. package/src/adapters/kiro/index.js +15 -1
  66. package/src/adapters/pi/hooks.js +119 -0
  67. package/src/adapters/pi/index.js +34 -9
  68. package/src/cli/commands/install.js +1 -1
  69. package/src/cli/commands/update.js +1 -1
  70. package/src/cli/index.js +17 -5
  71. package/src/cli/runtime-commands.js +51 -12
  72. package/src/lifecycle/coverage.js +57 -0
  73. package/src/lifecycle/hook-step.js +61 -0
  74. package/src/lifecycle/index.js +28 -14
  75. package/src/registry/index.js +136 -7
  76. package/src/runtime/candidate.js +505 -0
  77. package/src/runtime/claims.js +14 -5
  78. package/src/runtime/cli-result.js +11 -4
  79. package/src/runtime/cli.js +35 -268
  80. package/src/runtime/context/budget.js +203 -0
  81. package/src/runtime/context/compose.js +1367 -0
  82. package/src/runtime/context/delivery-store.js +102 -0
  83. package/src/runtime/context/mandatory.js +594 -0
  84. package/src/runtime/context/pointer.js +142 -0
  85. package/src/runtime/context-pack.js +1029 -244
  86. package/src/runtime/evidence-fields.js +39 -0
  87. package/src/runtime/evidence-ledger.js +7 -0
  88. package/src/runtime/freshness.js +44 -7
  89. package/src/runtime/outcome.js +1 -1
  90. package/src/runtime/readiness.js +83 -13
  91. package/src/runtime/retrieval-plan.js +422 -73
  92. package/src/runtime/retrieval-record.js +223 -0
  93. package/src/runtime/scaffold/artifacts.js +3 -0
  94. package/src/runtime/source-version.js +289 -0
  95. package/src/runtime/task-attempts.js +1 -1
  96. package/src/runtime/task-scope.js +28 -3
  97. package/src/runtime/task-state.js +72 -7
  98. package/core/.antigravity-plugin/hooks.json +0 -39
  99. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  100. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  101. package/core/harnesses/shared/hooks/hooks.json +0 -47
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # pre-implementation-gate.sh — Canonical Policy Library: PreToolUse(Edit|Write|...)
3
4
  # backstop for the doflow implement gate. The HARD half of the one enforced
4
5
  # gate (the other half is the prompt-level do-prereqs.sh inside
@@ -21,7 +22,7 @@
21
22
  # - edit target is under agent-docs/ -> allow (editing the artifacts themselves)
22
23
  # - edit target outside the repo -> allow
23
24
  # It reads files with jq and lists checkouts with one git call; it never runs
24
- # the DoFlow runtime. Self-contained + fail-open: any uncertainty -> allow (exit 0).
25
+ # the DoFlow runtime. Missing or malformed v1 input is denied as out of step; other faults follow IC-011.
25
26
  #
26
27
  # Canonical Policy Script Contract (design.md §4):
27
28
  # env DOFLOW_PROJECT_DIR (repo root, if the front door sets it),
@@ -30,17 +31,9 @@
30
31
  # (each already read by the harness-specific dispatcher this policy
31
32
  # replaces — kept so an unmodified front door still resolves its own
32
33
  # harness's do-paths.sh resolver)
33
- # stdin a PreToolUse-shaped JSON payload for an edit tool. Every harness's
34
- # own field names differ (Claude/Gemini: tool_name/tool_input.file_path;
35
- # Codex: tool_name=apply_patch with a unified diff in
36
- # tool_input.command; Kiro: field names undocumented) — this script
37
- # reads the union of known shapes rather than assuming one, since a
38
- # native front door (Claude/Codex/Kiro) execs this script directly
39
- # with no payload translation of its own.
40
- # exit 0 = allow, non-zero = deny with the reason on stderr. A front door
41
- # translates this into whatever JSON/exit-code shape its harness
42
- # natively expects — this script never speaks a harness-specific
43
- # decision shape.
34
+ # stdin a native payload encoded as envelope v1 by the lifecycle bridge.
35
+ # exit exit 2 + doflow_decide deny = deny; other faults per IC-011. A front
36
+ # door translates this into its native response shape.
44
37
  #
45
38
  # Reconciliation note (022-normalize-hooks, Phase A): claude/gemini/codex/kiro
46
39
  # each shipped a separately-adapted copy of this gate; antigravity's copy
@@ -58,74 +51,71 @@
58
51
 
59
52
  set -uo pipefail
60
53
 
61
- command -v jq >/dev/null 2>&1 || exit 0 # no jq -> cannot evaluate -> allow
62
-
63
54
  INPUT=$(cat)
64
-
65
- # ── Normalized envelope (preferred; review R7) ────────────────────────────────
66
- # A host adapter that has already decoded its native event sends
67
- # {"doflow_event": {"operation": "edit", "paths": [..], "projectRoot": "..", "taskId": ".."}}
68
- # and this policy consumes it verbatim. The union decoder below remains the
69
- # fallback for front doors that still forward raw native payloads; per-harness
70
- # knowledge belongs in those adapters, and each one that starts sending the
71
- # envelope retires its share of the union.
72
- ENVELOPE_ROOT=""
73
- ENVELOPE_TASK=""
74
- ENVELOPE_OP=$(printf '%s' "$INPUT" | jq -r '.doflow_event.operation // empty' 2>/dev/null)
75
- if [ -n "$ENVELOPE_OP" ]; then
76
- [ "$ENVELOPE_OP" = "edit" ] || exit 0 # only edits are gated
77
- FILES=$(printf '%s' "$INPUT" | jq -r '.doflow_event.paths[]? // empty' 2>/dev/null | sed '/^$/d' | sort -u)
78
- [ -n "$FILES" ] || exit 0
79
- ENVELOPE_ROOT=$(printf '%s' "$INPUT" | jq -r '.doflow_event.projectRoot // empty' 2>/dev/null)
80
- ENVELOPE_TASK=$(printf '%s' "$INPUT" | jq -r '.doflow_event.taskId // empty' 2>/dev/null)
55
+ if [ -f "$_DOFLOW_POLDIR/contract.sh" ]; then
56
+ . "$_DOFLOW_POLDIR/contract.sh"
81
57
  else
58
+ doflow_decision_init() { :; }
59
+ doflow_decide() { :; }
60
+ doflow_read_envelope() { DF_ENV_CLASS=missing; DF_ENV_CODE=contract-missing; }
61
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-implementation-gate contract-missing ) >/dev/null 2>&1 || true
62
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then
63
+ printf '[pre-implementation-gate] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2
64
+ else
65
+ printf '[pre-implementation-gate] DoFlow hooks out of step; run: doflow update\n' >&2
66
+ fi
67
+ doflow_decide deny contract-missing
68
+ exit 2
69
+ fi
70
+ doflow_decision_init pre-implementation-gate 1
71
+ command -v jq >/dev/null 2>&1 || {
72
+ doflow_decide error dependency-missing:jq
73
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-implementation-gate dependency-missing:jq ) >/dev/null 2>&1 || true
74
+ exit 0
75
+ }
82
76
 
83
- # ── Tool name (union of every known field name across harnesses; LEGACY decoder) ──
84
- tool=""
85
- for field in '.tool_name' '.tool' '.toolName' '.name'; do
86
- tool=$(printf '%s' "$INPUT" | jq -r "${field} // empty" 2>/dev/null)
87
- [ -n "$tool" ] && break
88
- done
89
-
90
- is_patch_tool=false
91
- case "$tool" in
92
- Edit|Write|MultiEdit|edit_file|write_file|create_file|replace_file|replace) ;;
93
- write_to_file|replace_file_content|multi_replace_file_content) ;;
94
- apply_patch) is_patch_tool=true ;;
95
- *) exit 0 ;;
96
- esac
97
-
98
- # ── Candidate files to gate ────────────────────────────────────────────────────
99
- # Normal edit tools: one file, from the union of field names harnesses use.
100
- # apply_patch (Codex): the tool has no file_path field at all — the paths are
101
- # embedded in a unified diff under tool_input.command.
102
- FILES=""
103
- if [ "$is_patch_tool" = true ]; then
104
- PATCH=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
105
- [ -n "$PATCH" ] || exit 0
106
- # "#" (not the original codex script's "|") is the sed delimiter here: BSD
107
- # sed (macOS, no GNU coreutils) misparses "s|...(Add|Update|Delete)...|...|"
108
- # because it splits on every unescaped "|", including the ones meant as ERE
109
- # alternation inside the pattern — a portability bug in the source script
110
- # this reconciliation fixes rather than ports, since it silently broke
111
- # apply_patch parsing under this exact invocation on any non-GNU-sed host.
112
- FILES=$(printf '%s\n' "$PATCH" | sed -nE \
77
+ # Parse v1 apply_patch paths using the shared parser.
78
+ parse_patch_paths() {
79
+ printf '%s\n' "$1" | sed -nE \
113
80
  -e 's#^\*\*\* (Add|Update|Delete) File: (.+)$#\2#p' \
114
81
  -e 's#^\+\+\+ (a/|b/)?(.+)$#\2#p' \
115
- | sed '/^\/dev\/null$/d' | sort -u)
116
- [ -n "$FILES" ] || exit 0
82
+ | sed '/^\/dev\/null$/d' | sort -u
83
+ }
84
+
85
+ DF_ENV_CLASS=missing DF_ENV_CODE=envelope-missing
86
+ doflow_read_envelope || true
87
+
88
+ # ── Envelope v1 ───────────────────────────────────────────────────────────────
89
+ ENVELOPE_ROOT=""
90
+ ENVELOPE_TASK=""
91
+ if [ "$DF_ENV_CLASS" = v1 ]; then
92
+ ENVELOPE_OP=$DF_ENV_OP
93
+ [ "$ENVELOPE_OP" = "edit" ] || exit 0 # only edits are gated
94
+ FILES=$DF_ENV_PATHS
95
+ ENVELOPE_ROOT=$DF_ENV_ROOT
96
+ ENVELOPE_TASK=$DF_ENV_TASK
97
+ if [ -z "$FILES" ] && [ -z "$DF_ENV_PATCH" ]; then
98
+ doflow_decide unsupported edit-without-target
99
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-implementation-gate edit-without-target ) >/dev/null 2>&1 || true
100
+ exit 0
101
+ fi
102
+ if [ -z "$FILES" ] && [ -n "$DF_ENV_PATCH" ]; then
103
+ FILES=$(parse_patch_paths "$DF_ENV_PATCH")
104
+ [ -n "$FILES" ] || exit 0
105
+ fi
106
+ elif [ "$DF_ENV_CLASS" = unsupported ]; then
107
+ doflow_decide unsupported envelope-version
108
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-implementation-gate unsupported:envelope-version ) >/dev/null 2>&1 || true
109
+ exit 0
117
110
  else
118
- file=""
119
- for field in '.tool_input.file_path' '.tool_input.path' '.tool_input.target' '.file_path' '.path'; do
120
- file=$(printf '%s' "$INPUT" | jq -r "${field} // empty" 2>/dev/null)
121
- [ -n "$file" ] && break
122
- done
123
- [ -n "$file" ] || exit 0
124
- FILES="$file"
111
+ if [ "${DF_ENV_CODE:-envelope-missing}" != contract-missing ]; then
112
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure pre-implementation-gate envelope-missing ) >/dev/null 2>&1 || true
113
+ fi
114
+ if [[ "${DOFLOW_AGENT:-}" =~ ^[a-z]+$ ]]; then printf '[pre-implementation-gate] DoFlow hooks out of step; run: doflow update -t %s\n' "$DOFLOW_AGENT" >&2; else printf '[pre-implementation-gate] DoFlow hooks out of step; run: doflow update\n' >&2; fi
115
+ doflow_decide deny "${DF_ENV_CODE:-envelope-missing}"
116
+ exit 2
125
117
  fi
126
118
 
127
- fi # end legacy union decoder
128
-
129
119
  # ── Resolve repo root ───────────────────────────────────────────────────────
130
120
  # DOFLOW_PROJECT_DIR (explicit) beats the envelope's projectRoot beats git discovery.
131
121
  ROOT="${DOFLOW_PROJECT_DIR:-$ENVELOPE_ROOT}"
@@ -384,13 +374,14 @@ PLANNED=false
384
374
  # plan.md all exist. Classes that cannot be a feature skip this check.
385
375
  if [ "$exempt" = false ] && [ -n "$FEATURE_ROOT" ] && [ "$PLANNED" = false ]; then
386
376
  echo "[pre-implementation-gate] doflow gate: feature $feature_dir is missing requirement.md, design.md, or plan.md — run /do-brainstorm, /do-design, then /do-plan before editing source. (Edits under agent-docs/ are always allowed; skip the flow by removing the feature dir.)" >&2
377
+ doflow_decide deny missing-artifacts
387
378
  exit 2
388
379
  fi
389
380
 
390
381
  # ── Readiness ────────────────────────────────────────────────────────────────
391
382
  # The workflow registry the installed runtime reads: beside this policy in an
392
383
  # install, the source tree's own, the project's install, the global install.
393
- POLICY_DIR=$(cd -P "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd) || POLICY_DIR=""
384
+ POLICY_DIR=$_DOFLOW_POLDIR
394
385
  REGISTRY=""
395
386
  for candidate in \
396
387
  "${POLICY_DIR:+$POLICY_DIR/../../../runtime/core/registry/workflows.json}" \
@@ -480,16 +471,19 @@ gate="doflow gate readiness-before-implementation: task '$task_id'"
480
471
  rest="Next: $next, then gather what it lists until it reports READY. Nothing was changed."
481
472
  if [ -z "$FOUND" ]; then
482
473
  printf "%s has no readiness record for the '%s' template. %s\n" "$gate" "$STAGE_TEMPLATE" "$rest" >&2
474
+ doflow_decide deny readiness-missing
483
475
  exit 2
484
476
  fi
485
477
  if [ "$record_state" != READY ]; then
486
478
  printf "%s was last evaluated %s at %s against the '%s' template, not READY. %s\n" \
487
479
  "$gate" "$record_state" "$record_at" "$STAGE_TEMPLATE" "$rest" >&2
480
+ doflow_decide deny readiness-not-ready
488
481
  exit 2
489
482
  fi
490
483
  if [ "$record_class" != "$STAGE_TEMPLATE" ]; then
491
484
  printf "%s has a READY record for the '%s' template, and this stage needs '%s'. %s\n" \
492
485
  "$gate" "$record_class" "$STAGE_TEMPLATE" "$rest" >&2
486
+ doflow_decide deny readiness-wrong-template
493
487
  exit 2
494
488
  fi
495
489
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # session-context.sh — Canonical Policy Library: SessionStart guard policy
3
4
  #
4
5
  # Fires when a harness starts a new session. Captures git state into a
@@ -11,24 +12,53 @@
11
12
  # Canonical Policy Script Contract (design.md §4):
12
13
  # env DOFLOW_PROJECT_DIR, DOFLOW_AGENT (both optional here; DOFLOW_AGENT
13
14
  # only affects the "last_agent" field recorded in meta.json)
14
- # stdin the harness's native SessionStart JSON payload — must expose at
15
- # least ".session_id" and ".cwd"
15
+ # stdin envelope v1 from the native SessionStart payload, including
16
+ # the session_id and cwd fields used by this policy.
16
17
  # exit this policy never denies — it always exits 0 (side-effect only)
17
18
  #
18
19
  # Depends on lib.sh (require_jq, json_field, ensure_session_dir,
19
20
  # ensure_project_dir, cwd_hash, run_with_timeout, $SESSIONS_LOG) being
20
21
  # discoverable next to this script when it runs — a front door invoking this
21
22
  # canonical script is responsible for making lib.sh resolvable via
22
- # "$(dirname "$0")/lib.sh", exactly as every harness's own SessionStart script
23
+ # the sibling lib.sh, exactly as every harness's own SessionStart script
23
24
  # already required before this policy moved to a shared location.
24
25
 
25
26
  set -euo pipefail
26
27
  # shellcheck source=lib.sh
27
- source "$(dirname "$0")/lib.sh"
28
- require_jq
29
-
28
+ source "$_DOFLOW_POLDIR/lib.sh"
30
29
  INPUT=$(cat)
31
- SESSION_ID=$(json_field "$INPUT" ".session_id")
30
+ POLICY_DIR=$_DOFLOW_POLDIR
31
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
32
+ # shellcheck source=contract.sh
33
+ source "$POLICY_DIR/contract.sh"
34
+ doflow_decision_init session-context 1
35
+ else
36
+ doflow_decision_init() { :; }
37
+ doflow_decide() { :; }
38
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure session-context contract-missing ) >/dev/null 2>&1 || true
39
+ exit 0
40
+ fi
41
+ if ! command -v jq >/dev/null 2>&1; then
42
+ doflow_decide error dependency-missing:jq
43
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure session-context dependency-missing:jq ) >/dev/null 2>&1 || true
44
+ fi
45
+ require_jq
46
+ SESSION_ID="" ENVELOPE_CLASS=missing
47
+ { IFS= read -r -d '' ENVELOPE_CLASS; IFS= read -r -d '' SESSION_ID; } < <(
48
+ printf '%s' "$INPUT" | jq -j 'if ((. | type) != "object") or ((.doflow_event | type) != "object") or ((.doflow_event | has("version")) | not) then "missing" elif ((.doflow_event.version | type) == "number") and .doflow_event.version == 1 then "v1" else "unsupported" end, "\u0000", (.session_id // "" | if type == "string" then rtrimstr("\n") else tojson end), "\u0000"' 2>/dev/null
49
+ ) || true
50
+ ENVELOPE_CLASS=${ENVELOPE_CLASS:-missing}
51
+ if [ ! -f "$POLICY_DIR/contract.sh" ]; then ENVELOPE_CLASS=missing; fi
52
+ if [ "$ENVELOPE_CLASS" = unsupported ] || [ "$ENVELOPE_CLASS" = missing ]; then
53
+ code=envelope-version
54
+ [ "$ENVELOPE_CLASS" = missing ] && code=envelope-missing
55
+ [ -f "$POLICY_DIR/contract.sh" ] || code=contract-missing
56
+ doflow_decide unsupported "$code"
57
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
58
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure session-context "$code" ) >/dev/null 2>&1 || true
59
+ fi
60
+ exit 0
61
+ fi
32
62
  CWD=$(json_field "$INPUT" ".cwd")
33
63
 
34
64
  # Guard: if we can't determine the session, nothing to do
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # stop-check.sh — Canonical Policy Library: Stop hook
3
4
  #
4
5
  # Two responsibilities:
@@ -24,14 +25,8 @@
24
25
  #
25
26
  # Canonical Policy Script Contract (design.md §4):
26
27
  # env DOFLOW_PROJECT_DIR, DOFLOW_AGENT (both optional here)
27
- # stdin a Stop-shaped JSON payload. session_id and transcript path field
28
- # names both vary by harness (transcript_path vs. Antigravity's
29
- # camelCase transcriptPath) — this script reads the union of known
30
- # field names rather than assuming one, since a native front door
31
- # (Claude/Codex/Kiro) execs this script directly with no payload
32
- # translation of its own.
33
- # exit 0 = allow the stop, non-zero = deny (block the stop) with the
34
- # reason on stderr.
28
+ # stdin envelope v1 from a native front door via the shared lifecycle bridge.
29
+ # exit exit 2 + doflow_decide deny = deny; other faults per IC-011.
35
30
  #
36
31
  # Depends on lib.sh (require_jq, json_field, ensure_session_dir,
37
32
  # run_with_timeout) being discoverable next to this script when it runs — see
@@ -48,11 +43,41 @@
48
43
 
49
44
  set -euo pipefail
50
45
  # shellcheck source=lib.sh
51
- source "$(dirname "$0")/lib.sh"
52
- require_jq
53
-
46
+ source "$_DOFLOW_POLDIR/lib.sh"
54
47
  INPUT=$(cat)
55
- SESSION_ID=$(json_field "$INPUT" ".session_id")
48
+ # Decision records stay off every stream unless a bridge supplied the fd.
49
+ POLICY_DIR=$_DOFLOW_POLDIR
50
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
51
+ # shellcheck source=contract.sh
52
+ source "$POLICY_DIR/contract.sh"
53
+ doflow_decision_init stop-check 1
54
+ else
55
+ doflow_decision_init() { :; }
56
+ doflow_decide() { :; }
57
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure stop-check contract-missing ) >/dev/null 2>&1 || true
58
+ exit 0
59
+ fi
60
+ if ! command -v jq >/dev/null 2>&1; then
61
+ doflow_decide error dependency-missing:jq
62
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure stop-check dependency-missing:jq ) >/dev/null 2>&1 || true
63
+ fi
64
+ require_jq
65
+ SESSION_ID="" ENVELOPE_CLASS=missing
66
+ { IFS= read -r -d '' ENVELOPE_CLASS; IFS= read -r -d '' SESSION_ID; } < <(
67
+ printf '%s' "$INPUT" | jq -j 'if ((. | type) != "object") or ((.doflow_event | type) != "object") or ((.doflow_event | has("version")) | not) then "missing" elif ((.doflow_event.version | type) == "number") and .doflow_event.version == 1 then "v1" else "unsupported" end, "\u0000", (.session_id // "" | if type == "string" then rtrimstr("\n") else tojson end), "\u0000"' 2>/dev/null
68
+ ) || true
69
+ ENVELOPE_CLASS=${ENVELOPE_CLASS:-missing}
70
+ if [ ! -f "$POLICY_DIR/contract.sh" ]; then ENVELOPE_CLASS=missing; fi
71
+ if [ "$ENVELOPE_CLASS" = unsupported ] || [ "$ENVELOPE_CLASS" = missing ]; then
72
+ code=envelope-version
73
+ [ "$ENVELOPE_CLASS" = missing ] && code=envelope-missing
74
+ [ -f "$POLICY_DIR/contract.sh" ] || code=contract-missing
75
+ doflow_decide unsupported "$code"
76
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
77
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure stop-check "$code" ) >/dev/null 2>&1 || true
78
+ fi
79
+ exit 0
80
+ fi
56
81
 
57
82
  TRANSCRIPT_PATH=$(json_field "$INPUT" ".transcript_path")
58
83
  [ -z "$TRANSCRIPT_PATH" ] && TRANSCRIPT_PATH=$(json_field "$INPUT" ".transcriptPath")
@@ -107,7 +132,7 @@ if [ -n "$SESSION_ID" ]; then
107
132
  # before the {} reply is invalid hook output)
108
133
  if [ ${#py_files[@]} -gt 0 ]; then
109
134
  if command -v ruff &>/dev/null; then
110
- nohup ruff format "${py_files[@]}" </dev/null >/dev/null 2>&1 &
135
+ nohup ruff format "${py_files[@]}" </dev/null >/dev/null 2>&1 3>&- 5>&- &
111
136
  run_with_timeout 2 -- ruff check "${py_files[@]}" >&2 || true
112
137
  fi
113
138
  fi
@@ -115,7 +140,7 @@ if [ -n "$SESSION_ID" ]; then
115
140
  # JS/TS: async fix only (eslint --fix rarely needs the caller's immediate attention)
116
141
  if [ ${#js_files[@]} -gt 0 ]; then
117
142
  if command -v eslint &>/dev/null; then
118
- nohup eslint --fix "${js_files[@]}" </dev/null >/dev/null 2>&1 &
143
+ nohup eslint --fix "${js_files[@]}" </dev/null >/dev/null 2>&1 3>&- 5>&- &
119
144
  fi
120
145
  fi
121
146
 
@@ -129,7 +154,7 @@ if [ -n "$SESSION_ID" ]; then
129
154
  # Java: async spotlessApply (slow — only if gradlew exists in cwd)
130
155
  if [ "$java_has_files" = "true" ]; then
131
156
  if [ -x "./gradlew" ]; then
132
- nohup ./gradlew spotlessApply </dev/null >/dev/null 2>&1 &
157
+ nohup ./gradlew spotlessApply </dev/null >/dev/null 2>&1 3>&- 5>&- &
133
158
  fi
134
159
  fi
135
160
 
@@ -143,6 +168,7 @@ if [ -n "$SESSION_ID" ]; then
143
168
  if [ -n "$DOFLOW_RUN" ]; then
144
169
  leak_out="$(run_with_timeout 2 -- "$DOFLOW_RUN" leak-scan "${leak_args[@]}" 2>/dev/null || true)"
145
170
  if [ -n "$leak_out" ] && printf '%s' "$leak_out" | grep -q ':[0-9]'; then
171
+ doflow_decide advisory internal-identifiers-found
146
172
  printf 'doflow: internal identifiers found in shipped files (warning, not a block):\n%s\n' \
147
173
  "$leak_out" >&2
148
174
  fi
@@ -258,6 +284,7 @@ if grep -qiE -- "$STUB_PATTERN" <<<"$STUB_TEXT" 2>/dev/null; then
258
284
  fi
259
285
  [ -n "$MESSAGE_SUM" ] && [ -n "$GUARD_FILE" ] && { printf '%s\n' "$MESSAGE_SUM" > "$GUARD_FILE"; } 2>/dev/null || true
260
286
  echo "[stop-check] The last response contains an unfinished-work marker (TODO/FIXME/stub comment or a not-implemented raise/throw)." >&2
287
+ doflow_decide deny unfinished-work
261
288
  exit 2
262
289
  fi
263
290
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # subagent-audit.sh — Canonical Policy Library: SubagentStart + SubagentStop hook
3
4
  # (pure observability, no deny path)
4
5
  #
@@ -18,11 +19,40 @@
18
19
 
19
20
  set -euo pipefail
20
21
  # shellcheck source=lib.sh
21
- source "$(dirname "$0")/lib.sh"
22
- require_jq
23
-
22
+ source "$_DOFLOW_POLDIR/lib.sh"
24
23
  INPUT=$(cat)
25
- SESSION_ID=$(json_field "$INPUT" ".session_id")
24
+ POLICY_DIR=$_DOFLOW_POLDIR
25
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
26
+ # shellcheck source=contract.sh
27
+ source "$POLICY_DIR/contract.sh"
28
+ doflow_decision_init subagent-audit 1
29
+ else
30
+ doflow_decision_init() { :; }
31
+ doflow_decide() { :; }
32
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure subagent-audit contract-missing ) >/dev/null 2>&1 || true
33
+ exit 0
34
+ fi
35
+ if ! command -v jq >/dev/null 2>&1; then
36
+ doflow_decide error dependency-missing:jq
37
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure subagent-audit dependency-missing:jq ) >/dev/null 2>&1 || true
38
+ fi
39
+ require_jq
40
+ SESSION_ID="" ENVELOPE_CLASS=missing
41
+ { IFS= read -r -d '' ENVELOPE_CLASS; IFS= read -r -d '' SESSION_ID; } < <(
42
+ printf '%s' "$INPUT" | jq -j 'if ((. | type) != "object") or ((.doflow_event | type) != "object") or ((.doflow_event | has("version")) | not) then "missing" elif ((.doflow_event.version | type) == "number") and .doflow_event.version == 1 then "v1" else "unsupported" end, "\u0000", (.session_id // "" | if type == "string" then rtrimstr("\n") else tojson end), "\u0000"' 2>/dev/null
43
+ ) || true
44
+ ENVELOPE_CLASS=${ENVELOPE_CLASS:-missing}
45
+ if [ ! -f "$POLICY_DIR/contract.sh" ]; then ENVELOPE_CLASS=missing; fi
46
+ if [ "$ENVELOPE_CLASS" = unsupported ] || [ "$ENVELOPE_CLASS" = missing ]; then
47
+ code=envelope-version
48
+ [ "$ENVELOPE_CLASS" = missing ] && code=envelope-missing
49
+ [ -f "$POLICY_DIR/contract.sh" ] || code=contract-missing
50
+ doflow_decide unsupported "$code"
51
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
52
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure subagent-audit "$code" ) >/dev/null 2>&1 || true
53
+ fi
54
+ exit 0
55
+ fi
26
56
 
27
57
  # Guard: if we can't determine the session, nothing to do
28
58
  [[ -z "$SESSION_ID" ]] && exit 0
@@ -41,6 +71,6 @@ jq -nc \
41
71
  --arg agent_type "$AGENT_TYPE" \
42
72
  --arg agent_id "$AGENT_ID" \
43
73
  '{timestamp: $ts, session_id: $session, event: $event, agent_type: $agent_type, agent_id: $agent_id}' \
44
- >> "$SESSION_PATH/subagent-audit.log" 2>/dev/null || true
74
+ >> "$SESSION_PATH/subagent-audit.log" 2>/dev/null || doflow_decide error audit-write-failed
45
75
 
46
76
  exit 0
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ case $0 in */*) _DOFLOW_POLDIR=${0%/*} ;; *) _DOFLOW_POLDIR=. ;; esac
2
3
  # user-prompt-submit.sh — Canonical Policy Library: UserPromptSubmit hook
3
4
  #
4
5
  # 022-hooks-remaining-duplication: claude and codex's UserPromptSubmit policies share the same
@@ -26,22 +27,52 @@
26
27
 
27
28
  set -euo pipefail
28
29
  # shellcheck source=lib.sh
29
- source "$(dirname "$0")/lib.sh"
30
- require_jq
31
-
30
+ source "$_DOFLOW_POLDIR/lib.sh"
32
31
  INPUT=$(cat)
32
+ POLICY_DIR=$_DOFLOW_POLDIR
33
+ if [ -f "$POLICY_DIR/contract.sh" ]; then
34
+ # shellcheck source=contract.sh
35
+ source "$POLICY_DIR/contract.sh"
36
+ doflow_decision_init user-prompt-submit 1
37
+ else
38
+ doflow_decision_init() { :; }
39
+ doflow_decide() { :; }
40
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure user-prompt-submit contract-missing ) >/dev/null 2>&1 || true
41
+ printf '{}'
42
+ exit 0
43
+ fi
44
+ if ! command -v jq >/dev/null 2>&1; then
45
+ doflow_decide error dependency-missing:jq
46
+ ( . "$POLICY_DIR/capture-failure.sh"; doflow_capture_failure user-prompt-submit dependency-missing:jq ) >/dev/null 2>&1 || true
47
+ fi
48
+ require_jq
33
49
  # One jq pass over the payload for every field this hook reads (each pass costs about 8 ms on a
34
50
  # 20 KB prompt, and the budget is 100 ms), NUL-separated: session_id, cwd and session_title, null
35
51
  # and false read as empty as json_field does, with any NUL inside a value dropped so no field can
36
52
  # shift into the next; then `subagent` when agent_id or agent_type holds anything but null or "".
37
- SESSION_ID="" CWD="" PAYLOAD_TITLE="" SUBAGENT=""
38
- { IFS= read -r -d '' SESSION_ID; IFS= read -r -d '' CWD; IFS= read -r -d '' PAYLOAD_TITLE; IFS= read -r -d '' SUBAGENT; } < <(
53
+ SESSION_ID="" CWD="" PAYLOAD_TITLE="" SUBAGENT="" ENVELOPE_CLASS=missing
54
+ { IFS= read -r -d '' SESSION_ID; IFS= read -r -d '' CWD; IFS= read -r -d '' PAYLOAD_TITLE; IFS= read -r -d '' SUBAGENT; IFS= read -r -d '' ENVELOPE_CLASS; } < <(
39
55
  printf '%s' "$INPUT" | jq -j '. as $p
40
- | [$p.session_id, $p.cwd, $p.session_title]
56
+ | [if ($p.session_id | type) == "string" then ($p.session_id | rtrimstr("\n")) else $p.session_id end, $p.cwd, $p.session_title]
41
57
  | map(if . == null or . == false then "" elif type == "string" then . else tojson end | split("\u0000") | join(""))
42
58
  | . + [if any($p.agent_id, $p.agent_type; . != null and . != "") then "subagent" else "" end]
59
+ | . + [if (($p | type) != "object") or (($p.doflow_event | type) != "object") or (($p.doflow_event | has("version")) | not) then "missing" elif (($p.doflow_event.version | type) == "number") and $p.doflow_event.version == 1 then "v1" else "unsupported" end]
43
60
  | join("\u0000") + "\u0000"' 2>/dev/null
44
61
  ) || true
62
+ ENVELOPE_CLASS=${ENVELOPE_CLASS:-missing}
63
+ if [ ! -f "$POLICY_DIR/contract.sh" ]; then ENVELOPE_CLASS=missing; fi
64
+
65
+ if [[ "$ENVELOPE_CLASS" == unsupported || "$ENVELOPE_CLASS" == missing ]]; then
66
+ code=envelope-version
67
+ [[ "$ENVELOPE_CLASS" == missing ]] && code=envelope-missing
68
+ [[ -f "$POLICY_DIR/contract.sh" ]] || code=contract-missing
69
+ doflow_decide unsupported "$code"
70
+ if [[ -f "$POLICY_DIR/contract.sh" ]]; then
71
+ ( . "$_DOFLOW_POLDIR/capture-failure.sh"; doflow_capture_failure user-prompt-submit "$code" ) >/dev/null 2>&1 || true
72
+ fi
73
+ printf '{}'
74
+ exit 0
75
+ fi
45
76
 
46
77
  [[ -z "$SESSION_ID" ]] && exit 0
47
78
 
@@ -65,7 +96,7 @@ nudge_message() {
65
96
  # N5: a subagent prompt carries agent_id or agent_type (read with the other fields above).
66
97
  [[ -z "$SUBAGENT" ]] || return 0
67
98
  # N6: the policy script's physical directory, found with built-ins (a subshell costs about 4 ms).
68
- case "$0" in */*) policy_dir="${0%/*}" ;; *) policy_dir="." ;; esac
99
+ policy_dir=$_DOFLOW_POLDIR
69
100
  here="$PWD"
70
101
  cd -P -- "$policy_dir" 2>/dev/null || return 0
71
102
  policy_dir="$PWD"
@@ -103,6 +134,7 @@ emit_envelope() {
103
134
  if [[ -f "$INJECTED_FLAG" ]]; then
104
135
  NUDGE=$(nudge_message 2>/dev/null) || NUDGE=""
105
136
  if [[ -n "$NUDGE" ]]; then
137
+ doflow_decide advisory prompt-nudge
106
138
  emit_envelope "$NUDGE" ""
107
139
  else
108
140
  echo "{}"
@@ -221,6 +253,7 @@ touch "$INJECTED_FLAG"
221
253
  # The nudge, when earned, follows the context after a blank line.
222
254
  NUDGE=$(nudge_message 2>/dev/null) || NUDGE=""
223
255
  if [[ -n "$NUDGE" ]]; then
256
+ doflow_decide advisory prompt-nudge
224
257
  CONTEXT+=$'\n\n'"$NUDGE"
225
258
  fi
226
259