@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/plugin.json +2 -1
- package/README.md +20 -19
- package/{skills → capabilities}/output-router/SKILL.md +1 -1
- package/{skills/setup-freeflow/references/output-router-setup.md → capabilities/output-router/references/setup.md} +1 -1
- package/command-surface.json +11 -0
- package/hooks/freeflow-runtime-context.mjs +398 -108
- package/hooks/hooks.json +26 -1
- package/hooks/tests/check-runtime-context-hook.sh +178 -26
- package/package.json +2 -1
- package/pi-extension/dist/output-router/native-safety-net.js +1 -1
- package/pi-extension/dist/runtime/runtime-context.js +1 -1
- package/pi-extension/src/output-router/native-safety-net.ts +1 -1
- package/pi-extension/src/runtime/runtime-context.ts +1 -1
- package/pi-extension/tests/integration/pi-extension.test.js +2 -0
- package/router/dist/config/config.d.ts +1 -1
- package/router/dist/config/config.js +1 -1
- package/router/dist/tools/run.js +1 -1
- package/skills/mode-contract/SKILL.md +28 -6
- package/skills/setup-freeflow/SKILL.md +3 -9
- package/skills/setup-freeflow/references/activation-contract.md +4 -9
- package/skills/setup-freeflow/references/host-setup.md +2 -6
- /package/{skills → capabilities}/output-router/references/safety-policy.md +0 -0
|
@@ -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
|
-
|
|
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
|
-
'
|
|
62
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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" '
|
|
128
|
-
assert_contains "$local_output" '
|
|
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' >
|
|
138
|
-
printf '{"interactionContract":false}\n' >
|
|
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
|
|
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"
|
|
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
|
|
189
|
-
assert_contains "$router_output" "
|
|
190
|
-
|
|
191
|
-
assert_not_contains "$router_output" "
|
|
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" "
|
|
199
|
-
assert_contains "$all_output" "
|
|
200
|
-
|
|
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" '
|
|
209
|
-
assert_contains "$conversation_output" '`conversation
|
|
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" "
|
|
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.
|
|
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
|
-
//
|
|
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("../../../
|
|
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
|
-
//
|
|
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("../../../
|
|
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 = "
|
|
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 = "
|
|
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;
|
package/router/dist/tools/run.js
CHANGED
|
@@ -833,7 +833,7 @@ function commandRoutingFailureResult(options) {
|
|
|
833
833
|
};
|
|
834
834
|
}
|
|
835
835
|
// Keep exactness-sensitive command routing aligned with
|
|
836
|
-
//
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
92
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|