@hassangameryt/freeflow 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
4
+ ROOT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)"
5
5
  HOOK_PATH="$ROOT_DIR/hooks/freeflow-runtime-context.mjs"
6
6
  HOOKS_JSON="$ROOT_DIR/hooks/hooks.json"
7
7
  WORKFLOW_SKILL="$ROOT_DIR/skills/workflow/SKILL.md"
@@ -40,7 +40,11 @@ const fs = require("fs");
40
40
  const hooks = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).hooks || {};
41
41
  if (!Array.isArray(hooks.SessionStart) || hooks.SessionStart.length === 0) process.exit(1);
42
42
  if (hooks.SessionStart[0]?.matcher !== "startup|resume|clear|compact") process.exit(1);
43
- ' "$HOOKS_JSON" || fail "hooks.json does not register the expected SessionStart lifecycle matcher"
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"
44
48
 
45
49
  tmp_dir="$(mktemp -d)"
46
50
  trap 'rm -rf "$tmp_dir"' EXIT
@@ -57,21 +61,23 @@ user_prompt_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$tmp_dir" | node
57
61
  [[ -z "$user_prompt_output" ]] || fail "UserPromptSubmit should not duplicate session-start runtime context"
58
62
 
59
63
  for expected in \
64
+ '"hookEventName":"SessionStart"' \
60
65
  "# Freeflow Runtime Context" \
61
- 'Setup status: configured by `.freeflow/config.json`' \
62
- "Runtime delivery: confirmed for this lifecycle-hook invocation." \
66
+ 'Runtime delivery: confirmed for this lifecycle-hook invocation.' \
67
+ 'Configured default: `workflow` (repository)' \
68
+ 'Effective mode: `workflow` (configured default, active)' \
63
69
  "# Freeflow Interaction Contract" \
64
70
  "Answer questions without inferring action." \
65
71
  "# Freeflow Workflow Bootstrap" \
66
72
  "Use feedback to choose the smallest useful next action." \
67
73
  "$workflow_owner_line" \
68
74
  "$workflow_self_review_line" \
69
- 'Current Freeflow default mode: `workflow`.' \
70
75
  'Interaction Contract: enabled' \
71
- 'Skills: enabled' \
72
- 'Output router: disabled'; do
76
+ 'Skills: enabled'; do
73
77
  assert_contains "$codex_output" "$expected" "Codex config-only context"
74
78
  done
79
+ assert_not_contains "$codex_output" "Output Router" "Codex host context"
80
+ assert_not_contains "$codex_output" "Output router:" "Codex host context"
75
81
  for excluded_heading in \
76
82
  "# Freeflow Runtime Kernel" \
77
83
  "## Freeflow Runtime Priority" \
@@ -93,9 +99,150 @@ assert_contains "$claude_output" "# Freeflow Interaction Contract" "Claude confi
93
99
  assert_contains "$claude_output" "# Freeflow Workflow Bootstrap" "Claude first-turn context"
94
100
  assert_contains "$claude_output" "Answer questions without inferring action." "Claude config-only context"
95
101
 
102
+ # Session mode changes are host-managed state: they apply before the same model request,
103
+ # survive lifecycle restoration, remain isolated by session id, and never edit repo config.
104
+ session_data_dir="$(mktemp -d)"
105
+ session_id="session/with unsafe path characters"
106
+ config_before="$(shasum -a 256 "$tmp_dir/.freeflow/config.json" | awk '{print $1}')"
107
+ 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)"
108
+ assert_contains "$mode_change_output" '"hookEventName":"UserPromptSubmit"' "same-turn mode change wrapper"
109
+ assert_contains "$mode_change_output" 'Session override: `conversation`' "same-turn mode change"
110
+ assert_contains "$mode_change_output" 'Effective mode: `conversation` (session override, active)' "same-turn mode change"
111
+ assert_contains "$mode_change_output" "## Conversation Mode Boundary" "same-turn mode change"
112
+
113
+ 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)"
114
+ assert_contains "$restored_mode_output" 'Configured default: `workflow` (repository)' "restored session mode"
115
+ assert_contains "$restored_mode_output" 'Session override: `conversation`' "restored session mode"
116
+ assert_contains "$restored_mode_output" 'Effective mode: `conversation` (session override, active)' "restored session mode"
117
+
118
+ 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)"
119
+ assert_contains "$other_session_output" 'Session override: none' "session mode isolation"
120
+ assert_contains "$other_session_output" 'Effective mode: `workflow` (configured default, active)' "session mode isolation"
121
+
122
+ 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)"
123
+ [[ -z "$ordinary_prompt_output" ]] || fail "ordinary prompts should not duplicate runtime context"
124
+ 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)"
125
+ [[ -z "$question_prompt_output" ]] || fail "mode questions should not mutate session state"
126
+ 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)"
127
+ [[ -z "$hypothetical_prompt_output" ]] || fail "mode hypotheticals should not mutate session state"
128
+ 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)"
129
+ [[ -z "$default_prompt_output" ]] || fail "default-mode requests should remain agent-routed configuration decisions"
130
+
131
+ 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)"
132
+ assert_contains "$native_change_output" 'Session override: `strict-workflow`' "Codex native skill mode control"
133
+ assert_contains "$native_change_output" "## Strict Workflow Overlay" "Codex native skill mode control"
134
+
135
+ claude_data_dir="$(mktemp -d)"
136
+ 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)"
137
+ assert_contains "$claude_mode_output" '"hookEventName":"UserPromptSubmit"' "Claude native skill mode control"
138
+ assert_contains "$claude_mode_output" 'Session override: `conversation`' "Claude native skill mode control"
139
+ 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)"
140
+ assert_contains "$claude_restored_output" 'Effective mode: `conversation` (session override, active)' "Claude restored session mode"
141
+
142
+ # Claude /clear ends one session and starts another. SessionEnd transfers only the
143
+ # active override for this host process/workspace, and SessionStart consumes it exactly once.
144
+ 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)"
145
+ [[ -z "$claude_clear_end_output" ]] || fail "SessionEnd clear should not emit model context"
146
+ 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)"
147
+ assert_contains "$claude_clear_output" 'Session override: `conversation`' "Claude clear session transfer"
148
+ assert_contains "$claude_clear_output" 'Effective mode: `conversation` (session override, active)' "Claude clear session transfer"
149
+ 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)"
150
+ assert_contains "$claude_unrelated_output" 'Session override: none' "Claude clear transfer isolation"
151
+ 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)"
152
+ assert_contains "$claude_second_clear_output" 'Session override: none' "Claude clear transfer one-shot consumption"
153
+
154
+ # A stale clear handoff is consumed without applying it.
155
+ 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)"
156
+ assert_contains "$claude_stale_mode_output" 'Session override: `strict-workflow`' "Claude stale clear setup"
157
+ 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
158
+ clear_transfer_file="$(find "$claude_data_dir/session-modes/claude-clear" -type f -name '*.json' -print -quit)"
159
+ node - "$clear_transfer_file" <<'NODE'
160
+ const fs = require("fs");
161
+ const path = process.argv[2];
162
+ const state = JSON.parse(fs.readFileSync(path, "utf8"));
163
+ state.createdAt = new Date(Date.now() - 120000).toISOString();
164
+ fs.writeFileSync(path, `${JSON.stringify(state)}\n`);
165
+ NODE
166
+ 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)"
167
+ assert_contains "$claude_stale_clear_output" 'Session override: none' "Claude stale clear transfer"
168
+ [[ ! -e "$clear_transfer_file" ]] || fail "stale Claude clear transfer should be consumed"
169
+
170
+ # Malformed plugin-owned transfer content fails closed without suppressing the
171
+ # normal SessionStart context and is consumed rather than retried forever.
172
+ 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
173
+ malformed_transfer_file="$(find "$claude_data_dir/session-modes/claude-clear" -type f -name '*.json' -print -quit)"
174
+ printf 'null\n' >"$malformed_transfer_file"
175
+ 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)"
176
+ assert_contains "$claude_malformed_clear_output" '# Freeflow Runtime Context' "Claude malformed clear transfer"
177
+ assert_contains "$claude_malformed_clear_output" 'Session override: none' "Claude malformed clear transfer"
178
+ [[ ! -e "$malformed_transfer_file" ]] || fail "malformed Claude clear transfer should be consumed"
179
+
180
+ # Overlapping clears in one workspace remain isolated by the host process ID.
181
+ 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
182
+ 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
183
+ 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
184
+ 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
185
+ 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)"
186
+ assert_contains "$claude_overlap_b_output" 'Session override: `strict-workflow`' "Claude overlapping clear B"
187
+ 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)"
188
+ assert_contains "$claude_overlap_a_output" 'Session override: `conversation`' "Claude overlapping clear A"
189
+
190
+ # A no-override clear removes any stale transfer for the same process/workspace.
191
+ 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
192
+ 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)"
193
+ assert_contains "$claude_no_override_clear_output" 'Session override: none' "Claude no-override clear"
194
+
195
+ # Transfer consumption claims the handoff atomically: two concurrent starts can
196
+ # never both receive the same override.
197
+ 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
198
+ race_output_a="$(mktemp)"
199
+ race_output_b="$(mktemp)"
200
+ 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" &
201
+ race_pid_a=$!
202
+ 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" &
203
+ race_pid_b=$!
204
+ wait "$race_pid_a" "$race_pid_b"
205
+ race_override_count="$(grep -Fh 'Session override: `strict-workflow`' "$race_output_a" "$race_output_b" | wc -l | tr -d ' ')"
206
+ [[ "$race_override_count" == "1" ]] || fail "Claude clear transfer should have exactly one concurrent consumer"
207
+ rm -f "$race_output_a" "$race_output_b"
208
+
209
+ # Codex currently reports SessionEnd reason=other, so it must never stage a
210
+ # Claude-style clear handoff.
211
+ codex_clear_data_dir="$(mktemp -d)"
212
+ 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
213
+ 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
214
+ if [[ -d "$codex_clear_data_dir/session-modes/claude-clear" ]]; then
215
+ fail "Codex SessionEnd should not stage Claude clear state"
216
+ fi
217
+ rm -rf "$codex_clear_data_dir" "$claude_data_dir"
218
+
219
+ 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)"
220
+ assert_contains "$reset_output" 'Session override: cleared' "session mode reset"
221
+ assert_contains "$reset_output" 'Effective mode: `workflow` (configured default, active)' "session mode reset"
222
+ config_after="$(shasum -a 256 "$tmp_dir/.freeflow/config.json" | awk '{print $1}')"
223
+ [[ "$config_before" == "$config_after" ]] || fail "session mode controls modified repository config"
224
+ if find "$session_data_dir" -type f -print | grep -Fq "$session_id"; then
225
+ fail "raw session id leaked into the session-state path"
226
+ fi
227
+ rm -rf "$session_data_dir"
228
+
96
229
  for source in startup resume clear compact; do
97
- event_output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s","model":"gpt-5"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
98
- assert_contains "$event_output" "# Freeflow Interaction Contract" "$source SessionStart context"
230
+ codex_event_output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s","model":"gpt-5"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
231
+ claude_event_output="$(printf '{"hook_event_name":"SessionStart","source":"%s","cwd":"%s"}\n' "$source" "$tmp_dir" | node "$HOOK_PATH" SessionStart)"
232
+ for host in codex claude; do
233
+ if [[ "$host" == "codex" ]]; then
234
+ event_output="$codex_event_output"
235
+ else
236
+ event_output="$claude_event_output"
237
+ assert_contains "$event_output" '"hookEventName":"SessionStart"' "Claude $source wrapper"
238
+ fi
239
+ assert_contains "$event_output" "# Freeflow Interaction Contract" "$host $source SessionStart context"
240
+ assert_contains "$event_output" "# Freeflow Workflow Bootstrap" "$host $source SessionStart context"
241
+ contract_count="$(grep -Fo '# Freeflow Interaction Contract' <<<"$event_output" | wc -l | tr -d ' ')"
242
+ bootstrap_count="$(grep -Fo '# Freeflow Workflow Bootstrap' <<<"$event_output" | wc -l | tr -d ' ')"
243
+ [[ "$contract_count" == "1" ]] || fail "$host $source SessionStart should load the Interaction Contract exactly once"
244
+ [[ "$bootstrap_count" == "1" ]] || fail "$host $source SessionStart should load Workflow exactly once"
245
+ done
99
246
  done
100
247
 
101
248
  # Repo-owned host instructions are neither required for activation nor modified by the read-only hook.
@@ -124,8 +271,8 @@ mkdir -p "$local_dir/.freeflow"
124
271
  printf '{"defaultMode":"workflow"}\n' >"$local_dir/.freeflow/config.json"
125
272
  printf '{"defaultMode":"strict-workflow"}\n' >"$local_dir/.freeflow/local.json"
126
273
  local_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_dir" | node "$HOOK_PATH" SessionStart)"
127
- assert_contains "$local_output" 'Current Freeflow default mode: `strict-workflow`.' "local default override"
128
- assert_contains "$local_output" 'defaultMode `strict-workflow` from local' "local default source"
274
+ assert_contains "$local_output" 'Configured default: `strict-workflow` (personal)' "local default override"
275
+ assert_contains "$local_output" 'Effective mode: `strict-workflow` (configured default, active)' "local default source"
129
276
  assert_contains "$local_output" "## Strict Workflow Overlay" "local strict overlay"
130
277
  assert_contains "$local_output" "security, privacy, billing, data loss, migrations, public interfaces" "local strict overlay"
131
278
  assert_contains "$local_output" "# Freeflow Interaction Contract" "local default override"
@@ -134,8 +281,8 @@ rm -rf "$local_dir"
134
281
 
135
282
  local_interaction_dir="$(mktemp -d)"
136
283
  mkdir -p "$local_interaction_dir/.freeflow"
137
- printf '{"defaultMode":"workflow"}\n' > "$local_interaction_dir/.freeflow/config.json"
138
- printf '{"interactionContract":false}\n' > "$local_interaction_dir/.freeflow/local.json"
284
+ printf '{"defaultMode":"workflow"}\n' >"$local_interaction_dir/.freeflow/config.json"
285
+ printf '{"interactionContract":false}\n' >"$local_interaction_dir/.freeflow/local.json"
139
286
  local_interaction_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_interaction_dir" | node "$HOOK_PATH" SessionStart)"
140
287
  assert_contains "$local_interaction_output" 'Interaction Contract: disabled' "independent local interaction override"
141
288
  assert_contains "$local_interaction_output" 'Skills: enabled' "independent local interaction override"
@@ -148,7 +295,7 @@ mkdir -p "$local_skills_dir/.freeflow"
148
295
  printf '{"defaultMode":"workflow"}\n' >"$local_skills_dir/.freeflow/config.json"
149
296
  printf '{"defaultMode":"conversation","interactionContract":false,"skills":{"enabled":false}}\n' >"$local_skills_dir/.freeflow/local.json"
150
297
  local_skills_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_skills_dir" | node "$HOOK_PATH" SessionStart)"
151
- assert_contains "$local_skills_output" 'Resolved default mode: `conversation` (dormant because Skills are disabled).' "local dormant mode"
298
+ assert_contains "$local_skills_output" 'Resolved mode: `conversation` (dormant because Skills are disabled)' "local dormant mode"
152
299
  assert_contains "$local_skills_output" 'Interaction Contract: disabled' "local interaction override"
153
300
  assert_contains "$local_skills_output" 'Skills: disabled' "local skills override"
154
301
  assert_not_contains "$local_skills_output" "# Freeflow Interaction Contract" "local interaction override"
@@ -169,7 +316,7 @@ printf '{"enabled":true,"defaultMode":"workflow"}\n' >"$local_disabled_dir/.free
169
316
  printf '{"enabled":false,"processing":{"unsafeUnsandboxed":{"enabled":true}}}\n' >"$local_disabled_dir/.freeflow/local.json"
170
317
  local_disabled_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$local_disabled_dir" | node "$HOOK_PATH" SessionStart)"
171
318
  assert_contains "$local_disabled_output" "# Freeflow Disabled" "local master override"
172
- assert_contains "$local_disabled_output" "effective \`enabled\` is false from local" "local master source"
319
+ assert_contains "$local_disabled_output" 'Configured but inactive: `enabled` is false (personal).' "local master source"
173
320
  assert_not_contains "$local_disabled_output" "# Freeflow Interaction Contract" "local master override"
174
321
  rm -rf "$local_disabled_dir"
175
322
 
@@ -185,19 +332,22 @@ router_dir="$(mktemp -d)"
185
332
  mkdir -p "$router_dir/.freeflow"
186
333
  printf '{"defaultMode":"workflow","skills":{"enabled":false},"outputRouter":{"enabled":true}}\n' >"$router_dir/.freeflow/config.json"
187
334
  router_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$router_dir" | node "$HOOK_PATH" SessionStart)"
188
- assert_contains "$router_output" "Skills: disabled" "router-only context"
189
- assert_contains "$router_output" "## Loaded Output Router Skill" "router-only context"
190
- assert_contains "$router_output" "# Freeflow Interaction Contract" "router-only context"
191
- assert_not_contains "$router_output" "# Freeflow Workflow Bootstrap" "router-only context"
335
+ assert_contains "$router_output" "Skills: disabled" "Codex context with legacy router config"
336
+ assert_contains "$router_output" "# Freeflow Interaction Contract" "Codex context with legacy router config"
337
+ assert_not_contains "$router_output" "# Freeflow Workflow Bootstrap" "Codex context with disabled skills"
338
+ assert_not_contains "$router_output" "Output Router" "Codex context with legacy router config"
339
+ assert_not_contains "$router_output" "Output router:" "Codex context with legacy router config"
192
340
  rm -rf "$router_dir"
193
341
 
194
342
  all_dir="$(mktemp -d)"
195
343
  mkdir -p "$all_dir/.freeflow"
196
344
  printf '{"defaultMode":"strict-workflow","outputRouter":{"enabled":true}}\n' >"$all_dir/.freeflow/config.json"
197
345
  all_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$all_dir" | node "$HOOK_PATH" SessionStart)"
198
- assert_contains "$all_output" "# Freeflow Interaction Contract" "all-capabilities context"
199
- assert_contains "$all_output" "## Loaded Output Router Skill" "all-capabilities context"
200
- assert_contains "$all_output" 'Current Freeflow default mode: `strict-workflow`.' "all-capabilities context"
346
+ assert_contains "$all_output" "# Freeflow Interaction Contract" "Codex strict context"
347
+ assert_contains "$all_output" "# Freeflow Workflow Bootstrap" "Codex strict context"
348
+ assert_not_contains "$all_output" "Output Router" "Codex strict context"
349
+ assert_not_contains "$all_output" "Output router:" "Codex strict context"
350
+ assert_contains "$all_output" 'Effective mode: `strict-workflow` (configured default, active)' "Codex strict context"
201
351
  assert_contains "$all_output" "## Strict Workflow Overlay" "all-capabilities strict overlay"
202
352
  rm -rf "$all_dir"
203
353
 
@@ -205,8 +355,8 @@ conversation_dir="$(mktemp -d)"
205
355
  mkdir -p "$conversation_dir/.freeflow"
206
356
  printf '{"defaultMode":"conversation"}\n' >"$conversation_dir/.freeflow/config.json"
207
357
  conversation_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$conversation_dir" | node "$HOOK_PATH" SessionStart)"
208
- assert_contains "$conversation_output" 'Current Freeflow default mode: `conversation`.' "conversation context"
209
- assert_contains "$conversation_output" '`conversation`: answer, discuss, critique, and inspect read-only' "conversation context"
358
+ assert_contains "$conversation_output" 'Configured default: `conversation` (repository)' "conversation context"
359
+ assert_contains "$conversation_output" 'Effective mode: `conversation` (configured default, active)' "conversation context"
210
360
  assert_contains "$conversation_output" "## Conversation Mode Boundary" "conversation overlay"
211
361
  assert_contains "$conversation_output" "Do not call write, edit, or mutating tools" "conversation overlay"
212
362
  assert_contains "$conversation_output" "an execution skill does not override this boundary" "conversation overlay"
@@ -216,8 +366,10 @@ observed_dir="$(mktemp -d)"
216
366
  mkdir -p "$observed_dir/.freeflow"
217
367
  printf '{"defaultMode":"workflow","outputRouter":{"enabled":true,"observedRouting":{"enabled":true}},"scriptTransform":{"enabled":false}}\n' >"$observed_dir/.freeflow/config.json"
218
368
  observed_output="$(printf '{"cwd":"%s","model":"gpt-5"}\n' "$observed_dir" | node "$HOOK_PATH" SessionStart)"
219
- assert_contains "$observed_output" "# Freeflow Interaction Contract" "observed-routing config"
220
- assert_contains "$observed_output" "## Loaded Output Router Skill" "observed-routing config"
369
+ assert_contains "$observed_output" "# Freeflow Interaction Contract" "Codex context with observed-routing config"
370
+ assert_contains "$observed_output" "# Freeflow Workflow Bootstrap" "Codex context with observed-routing config"
371
+ assert_not_contains "$observed_output" "Output Router" "Codex context with observed-routing config"
372
+ assert_not_contains "$observed_output" "Output router:" "Codex context with observed-routing config"
221
373
  rm -rf "$observed_dir"
222
374
 
223
375
  echo "runtime-context hook checks passed"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hassangameryt/freeflow",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Feedback-based control system for coding agents.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -66,6 +66,7 @@
66
66
  "LICENSE",
67
67
  "README.md",
68
68
  "assets/**",
69
+ "capabilities/**",
69
70
  "command-surface.json",
70
71
  "hooks/**",
71
72
  "pi-extension/**",
@@ -153,7 +153,7 @@ function appendSafetyNetWarning(event, message) {
153
153
  ];
154
154
  }
155
155
  // Keep post-tool native transformations aligned with
156
- // skills/output-router/references/safety-policy.md.
156
+ // capabilities/output-router/references/safety-policy.md.
157
157
  export async function handleNativeToolSafetyNet(event, ctx) {
158
158
  const routerConfigResult = await readOutputRouterConfig(ctx.cwd);
159
159
  notifyRouterConfigWarnings(ctx, routerConfigResult);
@@ -81,7 +81,7 @@ async function loadRuntimeContext(capabilityState = undefined) {
81
81
  ? readFile(new URL("../../../skills/workflow/SKILL.md", import.meta.url), "utf8")
82
82
  : Promise.resolve(null),
83
83
  outputRouterEnabled
84
- ? readFile(new URL("../../../skills/output-router/SKILL.md", import.meta.url), "utf8")
84
+ ? readFile(new URL("../../../capabilities/output-router/SKILL.md", import.meta.url), "utf8")
85
85
  : Promise.resolve(null),
86
86
  ]);
87
87
  return { interactionContract, workflowSkill, outputRouterSkill };
@@ -180,7 +180,7 @@ function appendSafetyNetWarning(event, message) {
180
180
  }
181
181
 
182
182
  // Keep post-tool native transformations aligned with
183
- // skills/output-router/references/safety-policy.md.
183
+ // capabilities/output-router/references/safety-policy.md.
184
184
  export async function handleNativeToolSafetyNet(event, ctx) {
185
185
  const routerConfigResult = await readOutputRouterConfig(ctx.cwd);
186
186
  notifyRouterConfigWarnings(ctx, routerConfigResult);
@@ -94,7 +94,7 @@ async function loadRuntimeContext(capabilityState = undefined) {
94
94
  ? readFile(new URL("../../../skills/workflow/SKILL.md", import.meta.url), "utf8")
95
95
  : Promise.resolve(null),
96
96
  outputRouterEnabled
97
- ? readFile(new URL("../../../skills/output-router/SKILL.md", import.meta.url), "utf8")
97
+ ? readFile(new URL("../../../capabilities/output-router/SKILL.md", import.meta.url), "utf8")
98
98
  : Promise.resolve(null),
99
99
  ]);
100
100
 
@@ -209,9 +209,11 @@ test("Pi exposes canonical model skills and maps published command aliases", asy
209
209
  assert.ok(match, `unexpected skill path: ${path}`);
210
210
  return match[1];
211
211
  });
212
+ assert.equal(skillNames.length, 25);
212
213
  for (const skill of ["discuss", "execute-work", "track-work"]) {
213
214
  assert.ok(skillNames.includes(skill));
214
215
  }
216
+ assert.ok(!skillNames.includes("output-router"));
215
217
  assert.ok(!skillNames.includes("discover"));
216
218
  assert.ok(!skillNames.includes("execute-plan"));
217
219
  await Promise.all(resources.skillPaths.map((path) => readFile(path, "utf8")));
@@ -6,7 +6,7 @@ import type {
6
6
  ScriptTransformConfig,
7
7
  VaultRetentionPolicy,
8
8
  } from "./types.js";
9
- export declare const OUTPUT_ROUTER_SKILL_PATH = "skills/output-router/SKILL.md";
9
+ export declare const OUTPUT_ROUTER_SKILL_PATH = "capabilities/output-router/SKILL.md";
10
10
  export declare const DEFAULT_VAULT_ROOT = "~/.cache/freeflow-router/vault";
11
11
  export declare const DEFAULT_POST_TOOL_ROUTING = "off";
12
12
  export declare const DEFAULT_OUTPUT_ROUTER_ENABLED = false;
@@ -6,7 +6,7 @@ import {
6
6
  RESERVED_OBSERVED_ROUTING_PERSISTENCE_MODES,
7
7
  STORAGE_POLICY_MODES,
8
8
  } from "./types.js";
9
- export const OUTPUT_ROUTER_SKILL_PATH = "skills/output-router/SKILL.md";
9
+ export const OUTPUT_ROUTER_SKILL_PATH = "capabilities/output-router/SKILL.md";
10
10
  export const DEFAULT_VAULT_ROOT = "~/.cache/freeflow-router/vault";
11
11
  export const DEFAULT_POST_TOOL_ROUTING = "off";
12
12
  export const DEFAULT_OUTPUT_ROUTER_ENABLED = false;
@@ -833,7 +833,7 @@ function commandRoutingFailureResult(options) {
833
833
  };
834
834
  }
835
835
  // Keep exactness-sensitive command routing aligned with
836
- // skills/output-router/references/safety-policy.md.
836
+ // capabilities/output-router/references/safety-policy.md.
837
837
  function routeCommandOutput(options) {
838
838
  const outputBytes = byteLength(options.combined);
839
839
  const outputLines = countLines(options.combined);
@@ -55,27 +55,49 @@ The user owns mode changes. Recommend `strict-workflow` when risk warrants it, b
55
55
 
56
56
  A task type, risk classification, direct skill call, or workflow route does not switch mode. Invoking an execution skill during Conversation still does not authorize mutation.
57
57
 
58
- Use host-native mode control when available. In Pi:
58
+ Use host-managed session control so the change takes effect before the model acts. A clear natural-language instruction may use the same control. Questions, examples, hypotheses, and tentative language do not change mode.
59
59
 
60
60
  ```text
61
- /freeflow mode
61
+ Switch to conversation mode. -> session override
62
+ Use strict-workflow for this session. -> session override
63
+ Should we switch to strict-workflow? -> answer or discuss; no override
64
+ Suppose the mode were conversation. -> discussion; no override
65
+ ```
66
+
67
+ Host-native controls are:
68
+
69
+ ```text
70
+ Pi:
62
71
  /freeflow mode conversation
63
72
  /freeflow mode workflow
64
73
  /freeflow mode strict-workflow
65
74
  /freeflow mode reset
75
+
76
+ Claude:
77
+ /freeflow:mode-contract <conversation|workflow|strict-workflow|reset>
78
+
79
+ Codex:
80
+ $mode-contract <conversation|workflow|strict-workflow|reset>
66
81
  ```
67
82
 
68
- Pi handles these commands before the model runs. The three setting commands create a session override; `reset` clears it and returns to the configured default, including a personal override when present. `/freeflow mode` inspects or selects mode according to host UI support.
83
+ Claude and Codex persist their override in plugin-owned session state keyed by the host session identifier. Pi persists it in branch-aware Pi session state. The override survives ordinary turns and supported resume, clear, and compact boundaries for that session; `reset` clears it and returns to the configured default. A new session starts from configured state.
69
84
 
70
- For a natural-language request to change Pi session mode, direct the user to the native command. Do not claim runtime state changed until the host reports the new effective mode.
85
+ Treat only runtime-reported effective state as proof that the control succeeded. If the host reports missing session identity, unavailable writable plugin state, disabled Skills, or another failure, say that mode did not change. Do not compensate by editing a configured default.
71
86
 
72
87
  Mention mode only when the user asks, configuration is being discussed, or it changes the next action.
73
88
 
74
89
  ## Change Defaults Separately
75
90
 
76
- A persistent default change is not a session-mode change. An unqualified personal default request targets `.freeflow/local.json`; changing the shared repository default requires explicit repository, team, or shared-default wording.
91
+ A persistent default change is not a session-mode change. Determine which configured layer the user means before editing:
92
+
93
+ - explicit **personal** or **local** default targets `.freeflow/local.json`;
94
+ - explicit **shared**, **repository**, or **team** default targets `.freeflow/config.json`;
95
+ - an unqualified “change the default mode” request leaves that scope user-owned—ask one direct local-versus-repository question and wait;
96
+ - “global” is not a defined Freeflow layer—ask whether the user means this checkout's personal default or the shared repository default.
97
+
98
+ Do not infer the target from which file already exists or from the current mode. Preserve unrelated settings in the selected layer.
77
99
 
78
- In Pi, use `/freeflow mode` or `/freeflow settings session` for a temporary mode override, `/freeflow settings` for personal overrides, and `/freeflow settings repo` for shared defaults. In another host, use its native settings when available or update only the explicitly selected valid layer while preserving unrelated settings.
100
+ In Pi, `/freeflow settings` edits personal overrides and `/freeflow settings repo` edits shared defaults. In another host, an agent may update only the explicitly selected valid configuration layer. Session controls never modify either file.
79
101
 
80
102
  For agent-performed config edits, obey the effective mode when one exists. Conversation remains read-only. When no mode is effective because Freeflow is unconfigured, inactive, blocked by invalid config, or Skills are dormant, an explicit configuration or [Setup Freeflow](../setup-freeflow/SKILL.md) request governs the selected change; any dormant resolved mode is neither authority nor prohibition. User-operated host controls change their own state before the model continues.
81
103
 
@@ -13,7 +13,7 @@ Create or repair the smallest valid layered setup. Keep repository activation, p
13
13
 
14
14
  Answer setup questions and delivery checks read-only. An explicit action request authorizes the smallest selected setup change under the state rules below. When repository config is missing, first activation authorizes minimal shared config; it does not authorize personal overrides, optional capabilities, replacement of invalid config, or edits to repo-owned host instructions.
15
15
 
16
- Read [the activation contract](references/activation-contract.md) before changing setup state. Read [host setup](references/host-setup.md) when runtime delivery, lifecycle, or host trust matters. Read [Output Router setup](references/output-router-setup.md) only when the user explicitly requests it during setup or repair; later tuning belongs to [Output Router](../output-router/SKILL.md).
16
+ Read [the activation contract](references/activation-contract.md) before changing setup state. Read [host setup](references/host-setup.md) when runtime delivery, lifecycle, or host trust matters.
17
17
 
18
18
  ## Establish Mutation Authority
19
19
 
@@ -64,17 +64,12 @@ Before writing local config in a Git checkout, verify it is untracked and ignore
64
64
 
65
65
  If local config is invalid, report that it blocks effective runtime. Repair or remove it only with authorization; never discard it and fall back silently.
66
66
 
67
- ## Keep Optional Setup Explicit
68
-
69
- Do not ask an automatic capabilities question. Configure Output Router only when explicitly requested, using [Output Router setup](references/output-router-setup.md). Preserve valid existing capability config during idempotent setup.
70
-
71
67
  ## Establish Same-Turn Context
72
68
 
73
69
  After successful verification, apply newly effective context directly for the remainder of the setup turn:
74
70
 
75
71
  - read the [Interaction Contract](../../runtime/interaction-contract.md) when it is effective;
76
- - read [Workflow](../workflow/SKILL.md) and [Mode Contract](../mode-contract/SKILL.md) when Skills are effective;
77
- - read an explicitly configured capability skill only when runtime state says it is effective.
72
+ - read [Workflow](../workflow/SKILL.md) and [Mode Contract](../mode-contract/SKILL.md) when Skills are effective.
78
73
 
79
74
  This direct read establishes same-turn guidance; it does not prove that a lifecycle adapter ran. After first activation, use [host setup](references/host-setup.md) to name the exact host-native lifecycle action for automatic delivery and resource refresh, state what applies before that boundary, and keep delivery confirmed, unavailable, or unconfirmed from evidence.
80
75
 
@@ -86,13 +81,12 @@ Verify:
86
81
  - local config is missing or valid, personal, untracked, and ignored when applicable;
87
82
  - effective `enabled`, Interaction Contract, Skills, and configured default values have the expected sources;
88
83
  - Skills-disabled mode remains resolved but dormant;
89
- - optional capability config changed only when requested;
90
84
  - no repo-owned host instructions or unrelated files changed.
91
85
 
92
86
  Report separately:
93
87
 
94
88
  1. shared repository activation;
95
89
  2. personal override state and effective sources;
96
- 3. effective capability state;
90
+ 3. effective Interaction Contract, Skills, and mode state;
97
91
  4. same-turn direct context reads;
98
92
  5. automatic runtime delivery as **confirmed**, **unavailable**, or **unconfirmed**.
@@ -19,8 +19,6 @@ Supported defaults are `conversation`, `workflow`, and `strict-workflow`. Missin
19
19
  - `skills.enabled: true`
20
20
  - `defaultMode: workflow`
21
21
 
22
- Repository config may also contain documented Output Router settings. Add optional settings only after an explicit request and preserve valid existing settings during idempotent setup.
23
-
24
22
  Do not add current mode, task state, phase, file inventories, plans, version metadata, activation paths, empty optional sections, host instruction copies, or generated workflow text.
25
23
 
26
24
  Missing or invalid repository config means repository activation is absent. A valid config with effective `enabled: false` remains configured but inactive.
@@ -44,8 +42,6 @@ It may override these core values:
44
42
 
45
43
  Every field is optional. Omission inherits the repository value, then the built-in default. Local core precedence is property-level, not whole-file replacement.
46
44
 
47
- The local file may also contain documented local-only `processing` config. It must not contain repository-owned Output Router config.
48
-
49
45
  Keep local config untracked and ignored. In a Git checkout, prefer `.git/info/exclude` or an existing ignore rule over an unsolicited shared `.gitignore` edit. Refuse to write personal overrides to a tracked local file.
50
46
 
51
47
  A missing local file means full inheritance. An invalid existing local file blocks effective Freeflow and must not silently fall back to repository settings. Repair or removal requires authorization.
@@ -58,7 +54,7 @@ Resolve core values in this order:
58
54
  host session override -> personal override -> repository value -> built-in default
59
55
  ```
60
56
 
61
- Host session overrides, where supported, may temporarily change Freeflow master enablement, Interaction Contract, Skills, and mode. They are host session state, not config, and cannot bypass missing or invalid repository activation.
57
+ Host session overrides are host state, not config, and cannot bypass missing or invalid repository activation. Pi may temporarily override Freeflow master enablement, Interaction Contract, Skills, and mode. Claude and Codex support mode-only overrides in plugin-owned session state.
62
58
 
63
59
  Effective Freeflow requires:
64
60
 
@@ -68,8 +64,6 @@ Effective Freeflow requires:
68
64
 
69
65
  The Interaction Contract and Skills are independent resolved switches. When Skills are disabled, the configured mode remains visible as resolved state but no Freeflow mode is effective.
70
66
 
71
- Repository-owned Output Router settings remain independent capability config and take effect only while top-level Freeflow is enabled.
72
-
73
67
  ## Mutation Authority By State
74
68
 
75
69
  Mode constrains agent-performed setup only when a mode is effective:
@@ -88,8 +82,9 @@ When effective, host adapters may deliver:
88
82
 
89
83
  - `runtime/interaction-contract.md` when the Interaction Contract switch is enabled;
90
84
  - one full `skills/workflow/SKILL.md` bootstrap when Skills are enabled;
91
- - compact active or dormant mode and capability state;
92
- - explicitly enabled capability context owned by that capability.
85
+ - compact active or dormant mode state.
86
+
87
+ Pi may additionally deliver explicitly enabled Pi capability context through its extension. Codex and Claude `SessionStart` hooks deliver only the core Interaction Contract, Workflow bootstrap, and mode state; their `UserPromptSubmit` hook delivers only an explicit session-mode change or failure delta.
93
88
 
94
89
  The full Mode Contract and other workflow skills remain on demand. Adapters load context only; they do not enforce policy, block tools, grant permissions, or replace repo instructions.
95
90
 
@@ -45,9 +45,9 @@ Confirm automatic delivery through current extension status or runtime evidence
45
45
 
46
46
  ## Codex And Claude
47
47
 
48
- The packaged lifecycle hook reads the same repository and local layers at supported session start, resume, clear, and compact boundaries.
48
+ The packaged runtime hook reads the same repository and local layers plus plugin-owned session mode state. At supported session start, resume, clear, and compact boundaries, `SessionStart` restores the complete enabled context. On submitted prompts, `UserPromptSubmit` changes mode only for an explicit native or conservative natural-language session control and otherwise emits nothing.
49
49
 
50
- When top-level Freeflow is effective, the hook loads the Interaction Contract if enabled and the full Workflow bootstrap if Skills are enabled. It reports layered value sources and active or dormant mode state. Optional capabilities remain independently gated by valid repository config and top-level Freeflow.
50
+ When top-level Freeflow is effective, the hook loads the Interaction Contract if enabled and the full Workflow bootstrap if Skills are enabled. It reports configured, session, resolved, and effective mode state without treating a configured default as the current session mode.
51
51
 
52
52
  After first-time setup, tell the user to trigger the least disruptive supported host-native lifecycle boundary: resume, clear, compact, or a new session/startup as available. Name the exact control supported by the current host; do not prescribe Pi's `/reload`. Until that boundary runs, use same-turn direct reads without claiming automatic delivery.
53
53
 
@@ -61,10 +61,6 @@ Installed and trusted registration can establish availability for the next lifec
61
61
 
62
62
  Do not compensate for unavailable or unconfirmed delivery by copying Freeflow text into repository instructions.
63
63
 
64
- ## Optional Capabilities
65
-
66
- Do not ask a generic capability question during normal setup. If Output Router is explicitly requested, use `output-router-setup.md` and keep its config in the shared repository layer unless the documented setting is specifically local-only.
67
-
68
64
  Do not install repo-local hooks, CLI commands, lint rules, global standards, documentation inventories, or state files during setup.
69
65
 
70
66
  ## Conflicts