@hassangameryt/freeflow 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -2
  3. package/.codex-plugin/plugin.json +5 -6
  4. package/.cursor-plugin/plugin.json +25 -0
  5. package/README.md +66 -56
  6. package/capabilities/cognitive-routing/SKILL.md +288 -130
  7. package/com.github.copilot/hooks/hooks.json +11 -0
  8. package/command-surface.json +0 -24
  9. package/gemini-extension.json +5 -0
  10. package/hooks/adapters/claude-session-start.mjs +16 -0
  11. package/hooks/adapters/codex-session-start.mjs +16 -0
  12. package/hooks/adapters/copilot-session-start.mjs +13 -0
  13. package/hooks/adapters/cursor-session-start.mjs +13 -0
  14. package/hooks/adapters/gemini-session-start.mjs +15 -0
  15. package/hooks/claude/hooks.json +18 -0
  16. package/hooks/codex/hooks.json +18 -0
  17. package/hooks/cursor/hooks.json +9 -0
  18. package/hooks/freeflow-runtime-context.mjs +3 -650
  19. package/hooks/hooks.json +4 -30
  20. package/hooks/shared/runtime-context.mjs +210 -0
  21. package/hooks/tests/check-host-adapters.test.mjs +82 -0
  22. package/hooks/tests/check-runtime-context-hook.sh +140 -320
  23. package/package.json +20 -4
  24. package/pi-extension/dist/cognitive-routing/commands.js +3 -3
  25. package/pi-extension/dist/cognitive-routing/config.js +16 -20
  26. package/pi-extension/dist/cognitive-routing/controller.js +478 -48
  27. package/pi-extension/dist/cognitive-routing/history.js +12 -0
  28. package/pi-extension/dist/cognitive-routing/host.js +74 -12
  29. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  30. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  31. package/pi-extension/dist/cognitive-routing/tool.js +1 -0
  32. package/pi-extension/dist/cognitive-routing/types.js +1 -1
  33. package/pi-extension/dist/index.js +236 -219
  34. package/pi-extension/dist/runtime/runtime-context.js +126 -286
  35. package/pi-extension/dist/settings/settings-ui.js +52 -336
  36. package/pi-extension/src/cognitive-routing/commands.ts +15 -5
  37. package/pi-extension/src/cognitive-routing/config.ts +16 -20
  38. package/pi-extension/src/cognitive-routing/controller.ts +561 -51
  39. package/pi-extension/src/cognitive-routing/history.ts +12 -0
  40. package/pi-extension/src/cognitive-routing/host.ts +110 -16
  41. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  42. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  43. package/pi-extension/src/cognitive-routing/tool.ts +1 -0
  44. package/pi-extension/src/cognitive-routing/types.ts +1 -1
  45. package/pi-extension/src/index.ts +249 -239
  46. package/pi-extension/src/runtime/runtime-context.ts +129 -305
  47. package/pi-extension/src/settings/settings-ui.ts +53 -371
  48. package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
  49. package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
  50. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
  51. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  52. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  53. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  54. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
  55. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  56. package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
  57. package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
  58. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  59. package/plugin.json +33 -0
  60. package/runtime/prompts/cognitive-routing.md +5 -4
  61. package/runtime/prompts/conversation-history.md +1 -1
  62. package/runtime/prompts/core.md +77 -6
  63. package/runtime/prompts/interaction-contract.md +6 -2
  64. package/skills/action-selection/SKILL.md +93 -44
  65. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  66. package/skills/bypass/SKILL.md +109 -39
  67. package/skills/commit-work/SKILL.md +94 -55
  68. package/skills/commit-work/references/git-state-edges.md +94 -0
  69. package/skills/commit-work/references/simple-push.md +92 -0
  70. package/skills/commit-work/references/staging-decisions.md +79 -46
  71. package/skills/decision-gate/SKILL.md +80 -22
  72. package/skills/design-for-depth/SKILL.md +208 -43
  73. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  74. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  75. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  76. package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
  77. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  78. package/skills/diagnose-failure/SKILL.md +198 -52
  79. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
  80. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
  81. package/skills/discuss/SKILL.md +118 -76
  82. package/skills/discuss/references/discussion-continuity.md +84 -32
  83. package/skills/evaluate-skill/SKILL.md +166 -52
  84. package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
  85. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  86. package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
  87. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  88. package/skills/execute-work/SKILL.md +98 -59
  89. package/skills/execute-work/references/code-practices.md +4 -4
  90. package/skills/execute-work/references/execute-work-edges.md +42 -22
  91. package/skills/review-artifact/SKILL.md +132 -71
  92. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  93. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
  94. package/skills/review-work/SKILL.md +115 -72
  95. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  96. package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
  97. package/skills/review-work/references/security-risk-lens.md +17 -15
  98. package/skills/setup-freeflow/SKILL.md +14 -17
  99. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  100. package/skills/setup-freeflow/references/host-setup.md +6 -7
  101. package/skills/simplify-code/SKILL.md +97 -39
  102. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  103. package/skills/tdd/SKILL.md +108 -41
  104. package/skills/tdd/references/test-design.md +1 -1
  105. package/skills/track-work/SKILL.md +133 -300
  106. package/skills/track-work/references/working-record-format.md +515 -0
  107. package/skills/track-work/scripts/lib/document.mjs +713 -0
  108. package/skills/track-work/scripts/lib/format.mjs +166 -0
  109. package/skills/track-work/scripts/lib/store.mjs +233 -0
  110. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  111. package/skills/track-work/scripts/lib/views.mjs +27 -430
  112. package/skills/track-work/scripts/working-record.mjs +292 -98
  113. package/skills/verify-work/SKILL.md +46 -20
  114. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  115. package/skills/verify-work/references/integration-evidence.md +1 -7
  116. package/skills/verify-work/references/performance-evidence.md +1 -1
  117. package/skills/workflow/SKILL.md +110 -80
  118. package/skills/workflow/references/domain-skill-composition.md +24 -18
  119. package/skills/write-skill/SKILL.md +218 -62
  120. package/skills/write-skill/references/activation-boundaries.md +63 -21
  121. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  122. package/skills/write-skill/references/progressive-disclosure.md +91 -41
  123. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  124. package/runtime/prompts/skills.md +0 -65
  125. package/skills/mode-contract/SKILL.md +0 -104
  126. package/skills/track-work/scripts/lib/codec.mjs +0 -570
  127. package/skills/track-work/scripts/lib/commands.mjs +0 -568
  128. package/skills/track-work/scripts/lib/contract.mjs +0 -303
  129. package/skills/track-work/scripts/lib/model.mjs +0 -506
  130. package/skills/track-work/scripts/lib/operations.mjs +0 -965
  131. package/skills/track-work/scripts/lib/record-store.mjs +0 -264
  132. package/skills/track-work/scripts/lib/result.mjs +0 -37
  133. package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
  134. package/skills/track-work/scripts/lib/schema.mjs +0 -277
  135. package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
  136. package/skills/track-work/scripts/lib/workspace.mjs +0 -134
@@ -3,278 +3,168 @@ set -euo pipefail
3
3
 
4
4
  ROOT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)"
5
5
  HOOK_PATH="$ROOT_DIR/hooks/freeflow-runtime-context.mjs"
6
- HOOKS_JSON="$ROOT_DIR/hooks/hooks.json"
6
+ CLAUDE_HOOKS_JSON="$ROOT_DIR/hooks/claude/hooks.json"
7
+ CODEX_HOOKS_JSON="$ROOT_DIR/hooks/codex/hooks.json"
8
+ GEMINI_HOOKS_JSON="$ROOT_DIR/hooks/hooks.json"
9
+ CURSOR_HOOKS_JSON="$ROOT_DIR/hooks/cursor/hooks.json"
10
+ COPILOT_HOOKS_JSON="$ROOT_DIR/com.github.copilot/hooks/hooks.json"
7
11
  CORE_PROMPT="$ROOT_DIR/runtime/prompts/core.md"
8
12
  INTERACTION_PROMPT="$ROOT_DIR/runtime/prompts/interaction-contract.md"
9
- SKILLS_PROMPT="$ROOT_DIR/runtime/prompts/skills.md"
10
13
 
11
14
  fail() {
12
- echo "runtime-context hook check failed: $*" >&2
13
- exit 1
15
+ echo "runtime-context hook check failed: $*" >&2
16
+ exit 1
14
17
  }
15
18
 
16
19
  assert_contains() {
17
- local text="$1"
18
- local expected="$2"
19
- local label="$3"
20
- grep -Fq -- "$expected" <<<"$text" || fail "$label missing '$expected'"
20
+ local text="$1"
21
+ local expected="$2"
22
+ local label="$3"
23
+ grep -Fq -- "$expected" <<<"$text" || fail "$label missing '$expected'"
21
24
  }
22
25
 
23
26
  assert_not_contains() {
24
- local text="$1"
25
- local unexpected="$2"
26
- local label="$3"
27
- if grep -Fq -- "$unexpected" <<<"$text"; then
28
- fail "$label unexpectedly contains '$unexpected'"
29
- fi
27
+ local text="$1"
28
+ local unexpected="$2"
29
+ local label="$3"
30
+ if grep -Fq -- "$unexpected" <<<"$text"; then
31
+ fail "$label unexpectedly contains '$unexpected'"
32
+ fi
30
33
  }
31
34
 
32
35
  [[ -f "$HOOK_PATH" ]] || fail "missing hook script"
33
- [[ -f "$HOOKS_JSON" ]] || fail "missing hooks manifest"
34
- for prompt_file in "$CORE_PROMPT" "$INTERACTION_PROMPT" "$SKILLS_PROMPT"; do
35
- [[ -f "$prompt_file" ]] || fail "missing runtime prompt fragment: $prompt_file"
36
+ for hooks_manifest in "$CLAUDE_HOOKS_JSON" "$CODEX_HOOKS_JSON" "$GEMINI_HOOKS_JSON" "$CURSOR_HOOKS_JSON" "$COPILOT_HOOKS_JSON"; do
37
+ [[ -f "$hooks_manifest" ]] || fail "missing hooks manifest: $hooks_manifest"
36
38
  done
39
+ [[ -f "$CORE_PROMPT" ]] || fail "missing core prompt"
40
+ [[ -f "$INTERACTION_PROMPT" ]] || fail "missing interaction contract prompt"
41
+ [[ ! -f "$ROOT_DIR/runtime/prompts/skills.md" ]] || fail "retired skills prompt remains"
37
42
  node --check "$HOOK_PATH"
38
43
  node -e '
39
44
  const fs = require("fs");
40
45
  const hooks = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).hooks || {};
41
- if (!Array.isArray(hooks.SessionStart) || hooks.SessionStart.length === 0) process.exit(1);
46
+ if (!Array.isArray(hooks.SessionStart) || hooks.SessionStart.length !== 1) process.exit(1);
42
47
  if (hooks.SessionStart[0]?.matcher !== "startup|resume|clear|compact") process.exit(1);
43
- if (!Array.isArray(hooks.UserPromptSubmit) || hooks.UserPromptSubmit.length !== 1) process.exit(1);
44
- if (hooks.UserPromptSubmit[0]?.matcher != null) process.exit(1);
45
- if (!Array.isArray(hooks.SessionEnd) || hooks.SessionEnd.length !== 1) process.exit(1);
46
- if (hooks.SessionEnd[0]?.matcher !== "clear") process.exit(1);
47
- ' "$HOOKS_JSON" || fail "hooks.json does not register the expected non-overlapping runtime events"
48
+ if (hooks.UserPromptSubmit || hooks.SessionEnd) process.exit(1);
49
+ ' "$CODEX_HOOKS_JSON" || fail "Codex hooks manifest does not expose only SessionStart"
50
+ node -e '
51
+ const fs = require("fs");
52
+ const hooks = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).hooks || {};
53
+ if (!Array.isArray(hooks.SessionStart) || hooks.SessionStart.length !== 1) process.exit(1);
54
+ if (hooks.SessionStart[0]?.matcher !== "startup|resume|clear|compact") process.exit(1);
55
+ ' "$CLAUDE_HOOKS_JSON" || fail "Claude hooks manifest does not preserve lifecycle starts"
56
+ node -e '
57
+ const fs = require("fs");
58
+ const hooks = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).hooks || {};
59
+ if (!Array.isArray(hooks.SessionStart) || hooks.SessionStart.length !== 1) process.exit(1);
60
+ if (Object.prototype.hasOwnProperty.call(hooks.SessionStart[0], "matcher")) process.exit(1);
61
+ ' "$GEMINI_HOOKS_JSON" || fail "Gemini hooks manifest does not expose SessionStart"
62
+ node -e '
63
+ const fs = require("fs");
64
+ const hooks = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).hooks || {};
65
+ if (!Array.isArray(hooks.sessionStart) || hooks.sessionStart.length !== 1) process.exit(1);
66
+ ' "$CURSOR_HOOKS_JSON" || fail "Cursor hooks manifest does not expose sessionStart"
67
+ node -e '
68
+ const fs = require("fs");
69
+ const config = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
70
+ if (config.version !== 1 || !Array.isArray(config.hooks?.SessionStart) || config.hooks.SessionStart.length !== 1) process.exit(1);
71
+ ' "$COPILOT_HOOKS_JSON" || fail "Copilot hooks manifest does not expose versioned SessionStart"
48
72
 
49
73
  tmp_dir="$(mktemp -d)"
50
74
  trap 'rm -rf "$tmp_dir"' EXIT
51
75
  mkdir -p "$tmp_dir/.freeflow"
52
- printf '{"defaultMode":"workflow"}\n' >"$tmp_dir/.freeflow/config.json"
76
+ printf '{}\n' >"$tmp_dir/.freeflow/config.json"
53
77
 
54
78
  codex_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
55
79
  disabled_by_env="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | FREEFLOW_DISABLE_RUNTIME_CONTEXT=1 node "$HOOK_PATH" SessionStart)"
56
80
  [[ -z "$disabled_by_env" ]] || fail "FREEFLOW_DISABLE_RUNTIME_CONTEXT=1 should suppress injection"
57
81
 
58
- post_tool_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" PostToolUse)"
59
- [[ -z "$post_tool_output" ]] || fail "PostToolUse should not inject runtime context"
60
- user_prompt_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" UserPromptSubmit)"
61
- [[ -z "$user_prompt_output" ]] || fail "UserPromptSubmit should not duplicate session-start runtime context"
82
+ for unsupported_event in PostToolUse UserPromptSubmit SessionEnd; do
83
+ unsupported_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" "$unsupported_event")"
84
+ [[ -z "$unsupported_output" ]] || fail "$unsupported_event should not inject runtime context"
85
+ done
62
86
 
63
87
  for expected in \
64
- '"hookEventName":"SessionStart"' \
65
- "# Freeflow Stable Guidance" \
66
- "## Mode" \
67
- "## Shared Terms" \
68
- "## Three Nested Loops" \
69
- "## Workflow Cue" \
70
- "## Action Selection Cue" \
71
- "## Supported Exit" \
72
- "# Freeflow Interaction Contract" \
73
- "Treat questions, criticism, examples, hypotheses, and tentative ideas as" \
74
- 'Configured default: `workflow` (repository)' \
75
- 'Effective mode: `workflow` (configured default, active)' \
76
- 'Interaction Contract: enabled' \
77
- 'Skills: enabled'; do
78
- assert_contains "$codex_output" "$expected" "Codex config-only context"
88
+ '"hookEventName":"SessionStart"' \
89
+ "# Freeflow Stable Guidance" \
90
+ "## Shared Terms" \
91
+ "## Recover After Context Loss" \
92
+ "## Three Nested Loops" \
93
+ "## Workflow Cue" \
94
+ "## Action Selection Cue" \
95
+ "## Supported Exit" \
96
+ "# Freeflow Interaction Contract" \
97
+ "Questions, criticism, examples, hypotheses, and tentative ideas do not authorize action by themselves." \
98
+ "Freeflow: active"; do
99
+ assert_contains "$codex_output" "$expected" "configured core context"
100
+ done
101
+ assert_not_contains "$codex_output" "## Mode" "configured core context"
102
+ assert_not_contains "$codex_output" "Default mode" "configured core context"
103
+ assert_not_contains "$codex_output" "Active mode" "configured core context"
104
+ assert_not_contains "$codex_output" "Interaction Contract: enabled" "configured core context"
105
+ assert_not_contains "$codex_output" "Skills: enabled" "configured core context"
106
+ assert_not_contains "$codex_output" "session-modes" "configured core context"
107
+
108
+ # Empty mandatory fragments fail closed rather than emitting a partial active surface.
109
+ whitespace_root="$(mktemp -d)"
110
+ mkdir -p "$whitespace_root/hooks" "$whitespace_root/runtime/prompts" "$whitespace_root/repo/.freeflow"
111
+ cp "$HOOK_PATH" "$whitespace_root/hooks/freeflow-runtime-context.mjs"
112
+ mkdir -p "$whitespace_root/hooks/adapters" "$whitespace_root/hooks/shared"
113
+ cp "$ROOT_DIR/hooks/adapters/codex-session-start.mjs" "$whitespace_root/hooks/adapters/codex-session-start.mjs"
114
+ cp "$ROOT_DIR/hooks/shared/runtime-context.mjs" "$whitespace_root/hooks/shared/runtime-context.mjs"
115
+ cp -R "$ROOT_DIR/runtime/prompts/." "$whitespace_root/runtime/prompts/"
116
+ printf ' \n\t\n' >"$whitespace_root/runtime/prompts/core.md"
117
+ printf '{}\n' >"$whitespace_root/repo/.freeflow/config.json"
118
+ whitespace_output="$(printf '{\"cwd\":\"%s\"}\n' "$whitespace_root/repo" | node "$whitespace_root/hooks/freeflow-runtime-context.mjs" SessionStart)"
119
+ [[ -z "$whitespace_output" ]] || fail "whitespace-only mandatory prompt should fail closed"
120
+ rm -rf "$whitespace_root"
121
+
122
+ claude_output="$(printf '{"hook_event_name":"SessionStart","source":"startup","cwd":"%s"}\n' "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
123
+ assert_contains "$claude_output" '"hookEventName":"SessionStart"' "Claude wrapper"
124
+ assert_contains "$claude_output" "# Freeflow Interaction Contract" "Claude core context"
125
+ assert_contains "$claude_output" "## Shared Terms" "Claude core context"
126
+ assert_contains "$claude_output" "Freeflow: active" "Claude core context"
127
+
128
+ # Every supported lifecycle start receives the same mandatory core surface.
129
+ for source in startup resume clear compact; do
130
+ output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
131
+ assert_contains "$output" "# Freeflow Stable Guidance" "$source SessionStart context"
132
+ assert_contains "$output" "# Freeflow Interaction Contract" "$source SessionStart context"
133
+ assert_contains "$output" "Freeflow: active" "$source SessionStart context"
79
134
  done
80
- assert_not_contains "$codex_output" "Output Router" "Codex host context"
81
- assert_not_contains "$codex_output" "Output router:" "Codex host context"
82
135
 
83
- # The Pi-only capability key is tolerated by shared host validators but is not
84
- # injected into Codex or Claude runtime context.
136
+ # Pi-only configuration remains isolated from the shared non-Pi hook.
85
137
  cognitive_dir="$(mktemp -d)"
86
138
  mkdir -p "$cognitive_dir/.freeflow"
87
- printf '{"defaultMode":"workflow","cognitiveRouting":{"enabled":true,"profiles":{"standard":{"provider":"faux","model":"standard","thinkingLevel":"high"},"reasoning":{"provider":"faux","model":"reasoning","thinkingLevel":"max"}}}}\n' >"$cognitive_dir/.freeflow/config.json"
139
+ printf '{"cognitiveRouting":{"enabled":true}}\n' >"$cognitive_dir/.freeflow/config.json"
88
140
  cognitive_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$cognitive_dir" | node "$HOOK_PATH" SessionStart)"
89
141
  assert_contains "$cognitive_output" "# Freeflow Stable Guidance" "Pi-only capability tolerance"
90
142
  assert_not_contains "$cognitive_output" "Cognitive Routing" "non-Pi capability isolation"
91
143
  rm -rf "$cognitive_dir"
92
- for excluded_heading in \
93
- "# Freeflow Runtime Kernel" \
94
- "## Freeflow Runtime Priority" \
95
- "## Loaded Mode Contract Skill" \
96
- "## Loaded Workflow Skill" \
97
- "## Loaded Decision Gate Skill" \
98
- "# Freeflow Workflow Bootstrap" \
99
- "## Discovery-light" \
100
- "## Loaded Output Router Skill"; do
101
- assert_not_contains "$codex_output" "$excluded_heading" "default context"
102
- done
103
- skills_prompt_count="$(grep -Fo '## Shared Terms' <<<"$codex_output" | wc -l | tr -d ' ')"
104
- [[ "$skills_prompt_count" == "1" ]] || fail "Codex SessionStart should load the Skills prompt exactly once"
105
- assert_not_contains "$codex_output" "## Conversation Mode Boundary" "workflow context"
106
- assert_not_contains "$codex_output" "## Strict Workflow Overlay" "workflow context"
107
-
108
- claude_output="$(printf '{"hook_event_name":"SessionStart","source":"startup","cwd":"%s"}\n' "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
109
- assert_contains "$claude_output" '"hookEventName":"SessionStart"' "Claude wrapper"
110
- assert_contains "$claude_output" "# Freeflow Interaction Contract" "Claude config-only context"
111
- assert_contains "$claude_output" "## Shared Terms" "Claude skills context"
112
- assert_not_contains "$claude_output" "# Freeflow Workflow Bootstrap" "Claude first-turn context"
113
- assert_contains "$claude_output" "Treat questions, criticism, examples, hypotheses, and tentative ideas as" "Claude config-only context"
114
-
115
- # Session mode changes are host-managed state: they apply before the same model request,
116
- # survive lifecycle restoration, remain isolated by session id, and never edit repo config.
117
- session_data_dir="$(mktemp -d)"
118
- session_id="session/with unsafe path characters"
119
- config_before="$(shasum -a 256 "$tmp_dir/.freeflow/config.json" | awk '{print $1}')"
120
- mode_change_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Switch to conversation mode."}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
121
- assert_contains "$mode_change_output" '"hookEventName":"UserPromptSubmit"' "same-turn mode change wrapper"
122
- assert_contains "$mode_change_output" '# Freeflow Runtime State' "same-turn mode change"
123
- assert_contains "$mode_change_output" 'Session override: `conversation`' "same-turn mode change"
124
- assert_contains "$mode_change_output" 'Effective mode: `conversation` (session override, active)' "same-turn mode change"
125
- assert_not_contains "$mode_change_output" "# Freeflow Mode Update" "same-turn mode change"
126
- assert_not_contains "$mode_change_output" "## Conversation Mode Boundary" "same-turn mode change"
127
- assert_not_contains "$mode_change_output" "Do not call write, edit, or mutating tools" "same-turn mode change"
128
-
129
- restored_mode_output="$(printf '{"hook_event_name":"SessionStart","source":"compact","session_id":"%s","cwd":"%s","model":"gpt-5"}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" SessionStart)"
130
- assert_contains "$restored_mode_output" 'Configured default: `workflow` (repository)' "restored session mode"
131
- assert_contains "$restored_mode_output" 'Session override: `conversation`' "restored session mode"
132
- assert_contains "$restored_mode_output" 'Effective mode: `conversation` (session override, active)' "restored session mode"
133
-
134
- other_session_output="$(printf '{"hook_event_name":"SessionStart","source":"resume","session_id":"other-session","cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" SessionStart)"
135
- assert_contains "$other_session_output" 'Session override: none' "session mode isolation"
136
- assert_contains "$other_session_output" 'Effective mode: `workflow` (configured default, active)' "session mode isolation"
137
-
138
- ordinary_prompt_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Please explain this function."}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
139
- [[ -z "$ordinary_prompt_output" ]] || fail "ordinary prompts should not duplicate runtime context"
140
- question_prompt_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Should we switch to strict-workflow mode?"}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
141
- [[ -z "$question_prompt_output" ]] || fail "mode questions should not mutate session state"
142
- hypothetical_prompt_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Suppose the mode were workflow."}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
143
- [[ -z "$hypothetical_prompt_output" ]] || fail "mode hypotheticals should not mutate session state"
144
- default_prompt_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Change the default mode to strict-workflow."}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
145
- [[ -z "$default_prompt_output" ]] || fail "default-mode requests should remain agent-routed configuration decisions"
146
-
147
- native_change_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"$mode-contract strict-workflow"}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
148
- assert_contains "$native_change_output" '# Freeflow Runtime State' "Codex native skill mode control"
149
- assert_contains "$native_change_output" 'Session override: `strict-workflow`' "Codex native skill mode control"
150
- assert_not_contains "$native_change_output" "# Freeflow Mode Update" "Codex native skill mode control"
151
- assert_not_contains "$native_change_output" "## Strict Workflow Overlay" "Codex native skill mode control"
152
-
153
- claude_data_dir="$(mktemp -d)"
154
- claude_mode_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"claude-session","cwd":"%s","prompt":"/freeflow:mode-contract conversation"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
155
- assert_contains "$claude_mode_output" '"hookEventName":"UserPromptSubmit"' "Claude native skill mode control"
156
- assert_contains "$claude_mode_output" 'Session override: `conversation`' "Claude native skill mode control"
157
- claude_restored_output="$(printf '{"hook_event_name":"SessionStart","source":"resume","session_id":"claude-session","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" SessionStart)"
158
- assert_contains "$claude_restored_output" 'Effective mode: `conversation` (session override, active)' "Claude restored session mode"
159
-
160
- # Claude /clear ends one session and starts another. SessionEnd transfers only the
161
- # active override for this host process/workspace, and SessionStart consumes it exactly once.
162
- claude_clear_end_output="$(printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-session","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-a" node "$HOOK_PATH" SessionEnd)"
163
- [[ -z "$claude_clear_end_output" ]] || fail "SessionEnd clear should not emit model context"
164
- claude_clear_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-session-after-clear","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-a" node "$HOOK_PATH" SessionStart)"
165
- assert_contains "$claude_clear_output" 'Session override: `conversation`' "Claude clear session transfer"
166
- assert_contains "$claude_clear_output" 'Effective mode: `conversation` (session override, active)' "Claude clear session transfer"
167
- claude_unrelated_output="$(printf '{"hook_event_name":"SessionStart","source":"startup","session_id":"claude-unrelated-session","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" SessionStart)"
168
- assert_contains "$claude_unrelated_output" 'Session override: none' "Claude clear transfer isolation"
169
- claude_second_clear_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-second-clear","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" SessionStart)"
170
- assert_contains "$claude_second_clear_output" 'Session override: none' "Claude clear transfer one-shot consumption"
171
-
172
- # A stale clear handoff is consumed without applying it.
173
- claude_stale_mode_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"claude-stale-source","cwd":"%s","prompt":"Switch to strict-workflow mode."}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
174
- assert_contains "$claude_stale_mode_output" 'Session override: `strict-workflow`' "Claude stale clear setup"
175
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-stale-source","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-stale" node "$HOOK_PATH" SessionEnd >/dev/null
176
- clear_transfer_file="$(find "$claude_data_dir/session-modes/claude-clear" -type f -name '*.json' -print -quit)"
177
- node - "$clear_transfer_file" <<'NODE'
178
- const fs = require("fs");
179
- const path = process.argv[2];
180
- const state = JSON.parse(fs.readFileSync(path, "utf8"));
181
- state.createdAt = new Date(Date.now() - 120000).toISOString();
182
- fs.writeFileSync(path, `${JSON.stringify(state)}\n`);
183
- NODE
184
- claude_stale_clear_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-stale-target","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-stale" node "$HOOK_PATH" SessionStart)"
185
- assert_contains "$claude_stale_clear_output" 'Session override: none' "Claude stale clear transfer"
186
- [[ ! -e "$clear_transfer_file" ]] || fail "stale Claude clear transfer should be consumed"
187
-
188
- # Malformed plugin-owned transfer content fails closed without suppressing the
189
- # normal SessionStart context and is consumed rather than retried forever.
190
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-stale-source","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-malformed" node "$HOOK_PATH" SessionEnd >/dev/null
191
- malformed_transfer_file="$(find "$claude_data_dir/session-modes/claude-clear" -type f -name '*.json' -print -quit)"
192
- printf 'null\n' >"$malformed_transfer_file"
193
- claude_malformed_clear_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-malformed-target","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-malformed" node "$HOOK_PATH" SessionStart)"
194
- assert_contains "$claude_malformed_clear_output" '# Freeflow Stable Guidance' "Claude malformed clear transfer"
195
- assert_contains "$claude_malformed_clear_output" 'Session override: none' "Claude malformed clear transfer"
196
- [[ ! -e "$malformed_transfer_file" ]] || fail "malformed Claude clear transfer should be consumed"
197
-
198
- # Overlapping clears in one workspace remain isolated by the host process ID.
199
- printf '{"hook_event_name":"UserPromptSubmit","session_id":"claude-overlap-a","cwd":"%s","prompt":"Switch to conversation mode."}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" UserPromptSubmit >/dev/null
200
- printf '{"hook_event_name":"UserPromptSubmit","session_id":"claude-overlap-b","cwd":"%s","prompt":"Switch to strict-workflow mode."}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" node "$HOOK_PATH" UserPromptSubmit >/dev/null
201
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-overlap-a","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-a" node "$HOOK_PATH" SessionEnd >/dev/null
202
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-overlap-b","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-b" node "$HOOK_PATH" SessionEnd >/dev/null
203
- claude_overlap_b_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-overlap-b-new","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-b" node "$HOOK_PATH" SessionStart)"
204
- assert_contains "$claude_overlap_b_output" 'Session override: `strict-workflow`' "Claude overlapping clear B"
205
- claude_overlap_a_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-overlap-a-new","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-a" node "$HOOK_PATH" SessionStart)"
206
- assert_contains "$claude_overlap_a_output" 'Session override: `conversation`' "Claude overlapping clear A"
207
-
208
- # A no-override clear removes any stale transfer for the same process/workspace.
209
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-no-override","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-a" node "$HOOK_PATH" SessionEnd >/dev/null
210
- claude_no_override_clear_output="$(printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-no-override-new","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-overlap-a" node "$HOOK_PATH" SessionStart)"
211
- assert_contains "$claude_no_override_clear_output" 'Session override: none' "Claude no-override clear"
212
-
213
- # Transfer consumption claims the handoff atomically: two concurrent starts can
214
- # never both receive the same override.
215
- printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"claude-overlap-b-new","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-race" node "$HOOK_PATH" SessionEnd >/dev/null
216
- race_output_a="$(mktemp)"
217
- race_output_b="$(mktemp)"
218
- printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-race-a","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-race" node "$HOOK_PATH" SessionStart >"$race_output_a" &
219
- race_pid_a=$!
220
- printf '{"hook_event_name":"SessionStart","source":"clear","session_id":"claude-race-b","cwd":"%s"}\n' "$tmp_dir" | CLAUDE_PLUGIN_DATA="$claude_data_dir" CLAUDE_PID="claude-process-race" node "$HOOK_PATH" SessionStart >"$race_output_b" &
221
- race_pid_b=$!
222
- wait "$race_pid_a" "$race_pid_b"
223
- race_override_count="$(grep -Fh 'Session override: `strict-workflow`' "$race_output_a" "$race_output_b" | wc -l | tr -d ' ')"
224
- [[ "$race_override_count" == "1" ]] || fail "Claude clear transfer should have exactly one concurrent consumer"
225
- rm -f "$race_output_a" "$race_output_b"
226
144
 
227
- # Codex currently reports SessionEnd reason=other, so it must never stage a
228
- # Claude-style clear handoff.
229
- codex_clear_data_dir="$(mktemp -d)"
230
- printf '{"hook_event_name":"UserPromptSubmit","session_id":"codex-source","cwd":"%s","prompt":"Switch to conversation mode."}\n' "$tmp_dir" | PLUGIN_DATA="$codex_clear_data_dir" node "$HOOK_PATH" UserPromptSubmit >/dev/null
231
- printf '{"hook_event_name":"SessionEnd","reason":"other","session_id":"codex-source","cwd":"%s"}\n' "$tmp_dir" | PLUGIN_DATA="$codex_clear_data_dir" node "$HOOK_PATH" SessionEnd >/dev/null
232
- if [[ -d "$codex_clear_data_dir/session-modes/claude-clear" ]]; then
233
- fail "Codex SessionEnd should not stage Claude clear state"
234
- fi
235
- rm -rf "$codex_clear_data_dir" "$claude_data_dir"
236
-
237
- reset_output="$(printf '{"hook_event_name":"UserPromptSubmit","session_id":"%s","cwd":"%s","prompt":"Reset the session mode."}\n' "$session_id" "$tmp_dir" | PLUGIN_DATA="$session_data_dir" node "$HOOK_PATH" UserPromptSubmit)"
238
- assert_contains "$reset_output" 'Session override: cleared' "session mode reset"
239
- assert_contains "$reset_output" 'Effective mode: `workflow` (configured default, active)' "session mode reset"
240
- config_after="$(shasum -a 256 "$tmp_dir/.freeflow/config.json" | awk '{print $1}')"
241
- [[ "$config_before" == "$config_after" ]] || fail "session mode controls modified repository config"
242
- if find "$session_data_dir" -type f -print | grep -Fq "$session_id"; then
243
- fail "raw session id leaked into the session-state path"
244
- fi
245
- rm -rf "$session_data_dir"
246
-
247
- for source in startup resume clear compact; do
248
- codex_event_output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s","model":"gpt-5"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
249
- claude_event_output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
250
- for host in codex claude; do
251
- if [[ "$host" == "codex" ]]; then
252
- event_output="$codex_event_output"
253
- else
254
- event_output="$claude_event_output"
255
- assert_contains "$event_output" '"hookEventName":"SessionStart"' "Claude $source wrapper"
256
- fi
257
- assert_contains "$event_output" "# Freeflow Interaction Contract" "$host $source SessionStart context"
258
- assert_contains "$event_output" "## Shared Terms" "$host $source SessionStart context"
259
- contract_count="$(grep -Fo '# Freeflow Interaction Contract' <<<"$event_output" | wc -l | tr -d ' ')"
260
- skills_count="$(grep -Fo '## Shared Terms' <<<"$event_output" | wc -l | tr -d ' ')"
261
- [[ "$contract_count" == "1" ]] || fail "$host $source SessionStart should load the Interaction Contract exactly once"
262
- [[ "$skills_count" == "1" ]] || fail "$host $source SessionStart should load the Skills prompt exactly once"
263
- assert_not_contains "$event_output" "# Freeflow Workflow Bootstrap" "$host $source SessionStart context"
264
- done
145
+ # Old mode and core-toggle configuration is a breaking invalidation, not a compatibility path.
146
+ for obsolete_config in \
147
+ '{"defaultMode":"workflow"}' \
148
+ '{"interactionContract":false}' \
149
+ '{"skills":{"enabled":false}}'; do
150
+ obsolete_dir="$(mktemp -d)"
151
+ mkdir -p "$obsolete_dir/.freeflow"
152
+ printf '%s\n' "$obsolete_config" >"$obsolete_dir/.freeflow/config.json"
153
+ obsolete_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$obsolete_dir" | node "$HOOK_PATH" SessionStart)"
154
+ [[ -z "$obsolete_output" ]] || fail "obsolete config should not activate: $obsolete_config"
155
+ rm -rf "$obsolete_dir"
265
156
  done
266
157
 
267
- # Repo-owned host instructions are neither required for activation nor modified by the read-only hook.
268
- printf '# Repo agents\n\nKeep this guidance.\n' >"$tmp_dir/AGENTS.md"
269
- printf '# Repo Claude\n\nKeep this guidance too.\n' >"$tmp_dir/CLAUDE.md"
270
- cp "$tmp_dir/AGENTS.md" "$tmp_dir/AGENTS.before"
271
- cp "$tmp_dir/CLAUDE.md" "$tmp_dir/CLAUDE.before"
272
- host_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
273
- assert_contains "$host_output" "# Freeflow Interaction Contract" "host-independent activation"
274
- cmp -s "$tmp_dir/AGENTS.before" "$tmp_dir/AGENTS.md" || fail "hook modified AGENTS.md"
275
- cmp -s "$tmp_dir/CLAUDE.before" "$tmp_dir/CLAUDE.md" || fail "hook modified CLAUDE.md"
158
+ # No prompt-submit or clear-transfer path may create or consume legacy session state.
159
+ state_dir="$(mktemp -d)"
160
+ printf '{"hook_event_name":"UserPromptSubmit","session_id":"old-session","cwd":"%s","prompt":"Switch to conversation mode."}\n' "$tmp_dir" |
161
+ PLUGIN_DATA="$state_dir" node "$HOOK_PATH" UserPromptSubmit >/dev/null
162
+ printf '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"old-session","cwd":"%s"}\n' "$tmp_dir" |
163
+ PLUGIN_DATA="$state_dir" node "$HOOK_PATH" SessionEnd >/dev/null
164
+ [[ ! -e "$state_dir/session-modes" ]] || fail "legacy session-mode state directory was created"
165
+ rm -rf "$state_dir"
276
166
 
277
- # Repository config remains the only activation boundary; local config only overrides it.
167
+ # Missing, invalid, disabled, and invalid-local activation remain inert.
278
168
  missing_dir="$(mktemp -d)"
279
169
  invalid_dir="$(mktemp -d)"
280
170
  mkdir -p "$invalid_dir/.freeflow"
@@ -285,108 +175,38 @@ invalid_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$invalid_dir" | node
285
175
  [[ -z "$invalid_output" ]] || fail "invalid config should not activate runtime context"
286
176
  rm -rf "$missing_dir" "$invalid_dir"
287
177
 
288
- local_dir="$(mktemp -d)"
289
- mkdir -p "$local_dir/.freeflow"
290
- printf '{"defaultMode":"workflow"}\n' >"$local_dir/.freeflow/config.json"
291
- printf '{"defaultMode":"strict-workflow"}\n' >"$local_dir/.freeflow/local.json"
292
- local_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_dir" | node "$HOOK_PATH" SessionStart)"
293
- assert_contains "$local_output" 'Configured default: `strict-workflow` (personal)' "local default override"
294
- assert_contains "$local_output" 'Effective mode: `strict-workflow` (configured default, active)' "local default source"
295
- assert_not_contains "$local_output" "## Strict Workflow Overlay" "local strict overlay"
296
- assert_not_contains "$local_output" "security, privacy, billing, data loss, migrations, public interfaces" "local strict overlay"
297
- assert_contains "$local_output" "# Freeflow Interaction Contract" "local default override"
298
- assert_contains "$local_output" "## Shared Terms" "local default override"
299
- rm -rf "$local_dir"
300
-
301
- local_interaction_dir="$(mktemp -d)"
302
- mkdir -p "$local_interaction_dir/.freeflow"
303
- printf '{"defaultMode":"workflow"}\n' >"$local_interaction_dir/.freeflow/config.json"
304
- printf '{"interactionContract":false}\n' >"$local_interaction_dir/.freeflow/local.json"
305
- local_interaction_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_interaction_dir" | node "$HOOK_PATH" SessionStart)"
306
- assert_contains "$local_interaction_output" 'Interaction Contract: disabled' "independent local interaction override"
307
- assert_contains "$local_interaction_output" 'Skills: enabled' "independent local interaction override"
308
- assert_not_contains "$local_interaction_output" "# Freeflow Interaction Contract" "independent local interaction override"
309
- assert_contains "$local_interaction_output" "## Shared Terms" "independent local interaction override"
310
- rm -rf "$local_interaction_dir"
311
-
312
- local_skills_dir="$(mktemp -d)"
313
- mkdir -p "$local_skills_dir/.freeflow"
314
- printf '{"defaultMode":"workflow"}\n' >"$local_skills_dir/.freeflow/config.json"
315
- printf '{"defaultMode":"conversation","interactionContract":false,"skills":{"enabled":false}}\n' >"$local_skills_dir/.freeflow/local.json"
316
- local_skills_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_skills_dir" | node "$HOOK_PATH" SessionStart)"
317
- assert_contains "$local_skills_output" 'Resolved mode: `conversation` (dormant because Skills are disabled)' "local dormant mode"
318
- assert_contains "$local_skills_output" 'Interaction Contract: disabled' "local interaction override"
319
- assert_contains "$local_skills_output" 'Skills: disabled' "local skills override"
320
- assert_not_contains "$local_skills_output" "# Freeflow Interaction Contract" "local interaction override"
321
- assert_not_contains "$local_skills_output" "# Freeflow Mode Update" "local skills override"
322
- assert_not_contains "$local_skills_output" "# Freeflow Workflow Bootstrap" "local skills override"
323
- rm -rf "$local_skills_dir"
178
+ disabled_dir="$(mktemp -d)"
179
+ mkdir -p "$disabled_dir/.freeflow"
180
+ printf '{"enabled":false}\n' >"$disabled_dir/.freeflow/config.json"
181
+ disabled_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$disabled_dir" | node "$HOOK_PATH" SessionStart)"
182
+ [[ -z "$disabled_output" ]] || fail "disabled Freeflow should suppress hook prompt delivery"
183
+ rm -rf "$disabled_dir"
324
184
 
325
185
  invalid_local_dir="$(mktemp -d)"
326
186
  mkdir -p "$invalid_local_dir/.freeflow"
327
- printf '{"defaultMode":"workflow"}\n' >"$invalid_local_dir/.freeflow/config.json"
328
- printf '{"skills":{"enabled":"no"}}\n' >"$invalid_local_dir/.freeflow/local.json"
187
+ printf '{}\n' >"$invalid_local_dir/.freeflow/config.json"
188
+ printf '{"enabled":"no"}\n' >"$invalid_local_dir/.freeflow/local.json"
329
189
  invalid_local_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$invalid_local_dir" | node "$HOOK_PATH" SessionStart)"
330
190
  [[ -z "$invalid_local_output" ]] || fail "invalid local config should fail closed"
331
191
  rm -rf "$invalid_local_dir"
332
192
 
193
+ # Personal disablement remains separate from shared activation.
333
194
  local_disabled_dir="$(mktemp -d)"
334
195
  mkdir -p "$local_disabled_dir/.freeflow"
335
- printf '{"enabled":true,"defaultMode":"workflow"}\n' >"$local_disabled_dir/.freeflow/config.json"
336
- printf '{"enabled":false,"processing":{"unsafeUnsandboxed":{"enabled":true}}}\n' >"$local_disabled_dir/.freeflow/local.json"
196
+ printf '{}\n' >"$local_disabled_dir/.freeflow/config.json"
197
+ printf '{"enabled":false}\n' >"$local_disabled_dir/.freeflow/local.json"
337
198
  local_disabled_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_disabled_dir" | node "$HOOK_PATH" SessionStart)"
338
199
  [[ -z "$local_disabled_output" ]] || fail "local master override should suppress hook prompt delivery"
339
200
  rm -rf "$local_disabled_dir"
340
201
 
341
- disabled_dir="$(mktemp -d)"
342
- mkdir -p "$disabled_dir/.freeflow"
343
- printf '{"enabled":false,"defaultMode":"workflow"}\n' >"$disabled_dir/.freeflow/config.json"
344
- disabled_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$disabled_dir" | node "$HOOK_PATH" SessionStart)"
345
- [[ -z "$disabled_output" ]] || fail "disabled Freeflow should suppress hook prompt delivery"
346
- rm -rf "$disabled_dir"
347
-
348
- router_dir="$(mktemp -d)"
349
- mkdir -p "$router_dir/.freeflow"
350
- printf '{"defaultMode":"workflow","skills":{"enabled":false},"outputRouter":{"enabled":true}}\n' >"$router_dir/.freeflow/config.json"
351
- router_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$router_dir" | node "$HOOK_PATH" SessionStart)"
352
- assert_contains "$router_output" "Skills: disabled" "Codex context with legacy router config"
353
- assert_contains "$router_output" "# Freeflow Interaction Contract" "Codex context with legacy router config"
354
- assert_not_contains "$router_output" "# Freeflow Workflow Bootstrap" "Codex context with disabled skills"
355
- assert_not_contains "$router_output" "Output Router" "Codex context with legacy router config"
356
- assert_not_contains "$router_output" "Output router:" "Codex context with legacy router config"
357
- rm -rf "$router_dir"
358
-
359
- all_dir="$(mktemp -d)"
360
- mkdir -p "$all_dir/.freeflow"
361
- printf '{"defaultMode":"strict-workflow","outputRouter":{"enabled":true}}\n' >"$all_dir/.freeflow/config.json"
362
- all_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$all_dir" | node "$HOOK_PATH" SessionStart)"
363
- assert_contains "$all_output" "# Freeflow Interaction Contract" "Codex strict context"
364
- assert_contains "$all_output" "## Shared Terms" "Codex strict context"
365
- assert_not_contains "$all_output" "Output Router" "Codex strict context"
366
- assert_not_contains "$all_output" "Output router:" "Codex strict context"
367
- assert_contains "$all_output" 'Effective mode: `strict-workflow` (configured default, active)' "Codex strict context"
368
- assert_not_contains "$all_output" "## Strict Workflow Overlay" "all-capabilities strict overlay"
369
- rm -rf "$all_dir"
370
-
371
- conversation_dir="$(mktemp -d)"
372
- mkdir -p "$conversation_dir/.freeflow"
373
- printf '{"defaultMode":"conversation"}\n' >"$conversation_dir/.freeflow/config.json"
374
- conversation_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$conversation_dir" | node "$HOOK_PATH" SessionStart)"
375
- assert_contains "$conversation_output" 'Configured default: `conversation` (repository)' "conversation context"
376
- assert_contains "$conversation_output" 'Effective mode: `conversation` (configured default, active)' "conversation context"
377
- assert_not_contains "$conversation_output" "## Conversation Mode Boundary" "conversation overlay"
378
- assert_not_contains "$conversation_output" "Do not call write, edit, or mutating tools" "conversation overlay"
379
- assert_not_contains "$conversation_output" "an execution skill does not override this boundary" "conversation overlay"
380
- rm -rf "$conversation_dir"
381
-
382
- observed_dir="$(mktemp -d)"
383
- mkdir -p "$observed_dir/.freeflow"
384
- printf '{"defaultMode":"workflow","outputRouter":{"enabled":true,"observedRouting":{"enabled":true}},"scriptTransform":{"enabled":false}}\n' >"$observed_dir/.freeflow/config.json"
385
- observed_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$observed_dir" | node "$HOOK_PATH" SessionStart)"
386
- assert_contains "$observed_output" "# Freeflow Interaction Contract" "Codex context with observed-routing config"
387
- assert_contains "$observed_output" "## Shared Terms" "Codex context with observed-routing config"
388
- assert_not_contains "$observed_output" "Output Router" "Codex context with observed-routing config"
389
- assert_not_contains "$observed_output" "Output router:" "Codex context with observed-routing config"
390
- rm -rf "$observed_dir"
202
+ # Repo-owned host instructions are not activation markers and are not modified.
203
+ printf '# Repo agents\n\nKeep this guidance.\n' >"$tmp_dir/AGENTS.md"
204
+ printf '# Repo Claude\n\nKeep this guidance too.\n' >"$tmp_dir/CLAUDE.md"
205
+ cp "$tmp_dir/AGENTS.md" "$tmp_dir/AGENTS.before"
206
+ cp "$tmp_dir/CLAUDE.md" "$tmp_dir/CLAUDE.before"
207
+ host_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
208
+ assert_contains "$host_output" "# Freeflow Interaction Contract" "host-independent activation"
209
+ cmp -s "$tmp_dir/AGENTS.before" "$tmp_dir/AGENTS.md" || fail "hook modified AGENTS.md"
210
+ cmp -s "$tmp_dir/CLAUDE.before" "$tmp_dir/CLAUDE.md" || fail "hook modified CLAUDE.md"
391
211
 
392
212
  echo "runtime-context hook checks passed"