@hassangameryt/freeflow 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +5 -6
- package/.cursor-plugin/plugin.json +25 -0
- package/README.md +66 -56
- package/capabilities/cognitive-routing/SKILL.md +288 -130
- package/com.github.copilot/hooks/hooks.json +11 -0
- package/command-surface.json +0 -24
- package/gemini-extension.json +5 -0
- package/hooks/adapters/claude-session-start.mjs +16 -0
- package/hooks/adapters/codex-session-start.mjs +16 -0
- package/hooks/adapters/copilot-session-start.mjs +13 -0
- package/hooks/adapters/cursor-session-start.mjs +13 -0
- package/hooks/adapters/gemini-session-start.mjs +15 -0
- package/hooks/claude/hooks.json +18 -0
- package/hooks/codex/hooks.json +18 -0
- package/hooks/cursor/hooks.json +9 -0
- package/hooks/freeflow-runtime-context.mjs +3 -650
- package/hooks/hooks.json +4 -30
- package/hooks/shared/runtime-context.mjs +210 -0
- package/hooks/tests/check-host-adapters.test.mjs +82 -0
- package/hooks/tests/check-runtime-context-hook.sh +140 -320
- package/package.json +20 -4
- package/pi-extension/dist/cognitive-routing/commands.js +3 -3
- package/pi-extension/dist/cognitive-routing/config.js +16 -20
- package/pi-extension/dist/cognitive-routing/controller.js +478 -48
- package/pi-extension/dist/cognitive-routing/history.js +12 -0
- package/pi-extension/dist/cognitive-routing/host.js +74 -12
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
- package/pi-extension/dist/cognitive-routing/tool.js +1 -0
- package/pi-extension/dist/cognitive-routing/types.js +1 -1
- package/pi-extension/dist/index.js +236 -219
- package/pi-extension/dist/runtime/runtime-context.js +126 -286
- package/pi-extension/dist/settings/settings-ui.js +52 -336
- package/pi-extension/src/cognitive-routing/commands.ts +15 -5
- package/pi-extension/src/cognitive-routing/config.ts +16 -20
- package/pi-extension/src/cognitive-routing/controller.ts +561 -51
- package/pi-extension/src/cognitive-routing/history.ts +12 -0
- package/pi-extension/src/cognitive-routing/host.ts +110 -16
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
- package/pi-extension/src/cognitive-routing/tool.ts +1 -0
- package/pi-extension/src/cognitive-routing/types.ts +1 -1
- package/pi-extension/src/index.ts +249 -239
- package/pi-extension/src/runtime/runtime-context.ts +129 -305
- package/pi-extension/src/settings/settings-ui.ts +53 -371
- package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
- package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
- package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
- package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
- package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
- package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
- package/plugin.json +33 -0
- package/runtime/prompts/cognitive-routing.md +5 -4
- package/runtime/prompts/conversation-history.md +1 -1
- package/runtime/prompts/core.md +77 -6
- package/runtime/prompts/interaction-contract.md +6 -2
- package/skills/action-selection/SKILL.md +93 -44
- package/skills/action-selection/references/trajectory-stalls.md +71 -0
- package/skills/bypass/SKILL.md +109 -39
- package/skills/commit-work/SKILL.md +94 -55
- package/skills/commit-work/references/git-state-edges.md +94 -0
- package/skills/commit-work/references/simple-push.md +92 -0
- package/skills/commit-work/references/staging-decisions.md +79 -46
- package/skills/decision-gate/SKILL.md +80 -22
- package/skills/design-for-depth/SKILL.md +208 -43
- package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
- package/skills/design-for-depth/references/interface-design-loop.md +77 -97
- package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
- package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
- package/skills/diagnose-failure/SKILL.md +198 -52
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
- package/skills/discuss/SKILL.md +118 -76
- package/skills/discuss/references/discussion-continuity.md +84 -32
- package/skills/evaluate-skill/SKILL.md +166 -52
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
- package/skills/evaluate-skill/references/evaluation-design.md +56 -30
- package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
- package/skills/evaluate-skill/references/review-and-revision.md +62 -21
- package/skills/execute-work/SKILL.md +98 -59
- package/skills/execute-work/references/code-practices.md +4 -4
- package/skills/execute-work/references/execute-work-edges.md +42 -22
- package/skills/review-artifact/SKILL.md +132 -71
- package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
- package/skills/review-work/SKILL.md +115 -72
- package/skills/review-work/references/adjudicate-work-review.md +97 -0
- package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
- package/skills/review-work/references/security-risk-lens.md +17 -15
- package/skills/setup-freeflow/SKILL.md +14 -17
- package/skills/setup-freeflow/references/activation-contract.md +24 -30
- package/skills/setup-freeflow/references/host-setup.md +6 -7
- package/skills/simplify-code/SKILL.md +97 -39
- package/skills/simplify-code/references/simplification-patterns.md +1 -1
- package/skills/tdd/SKILL.md +108 -41
- package/skills/tdd/references/test-design.md +1 -1
- package/skills/track-work/SKILL.md +133 -300
- package/skills/track-work/references/working-record-format.md +515 -0
- package/skills/track-work/scripts/lib/document.mjs +713 -0
- package/skills/track-work/scripts/lib/format.mjs +166 -0
- package/skills/track-work/scripts/lib/store.mjs +233 -0
- package/skills/track-work/scripts/lib/transitions.mjs +790 -0
- package/skills/track-work/scripts/lib/views.mjs +27 -430
- package/skills/track-work/scripts/working-record.mjs +292 -98
- package/skills/verify-work/SKILL.md +46 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
- package/skills/verify-work/references/integration-evidence.md +1 -7
- package/skills/verify-work/references/performance-evidence.md +1 -1
- package/skills/workflow/SKILL.md +110 -80
- package/skills/workflow/references/domain-skill-composition.md +24 -18
- package/skills/write-skill/SKILL.md +218 -62
- package/skills/write-skill/references/activation-boundaries.md +63 -21
- package/skills/write-skill/references/agent-first-instructions.md +61 -27
- package/skills/write-skill/references/progressive-disclosure.md +91 -41
- package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
- package/runtime/prompts/skills.md +0 -65
- package/skills/mode-contract/SKILL.md +0 -104
- package/skills/track-work/scripts/lib/codec.mjs +0 -570
- package/skills/track-work/scripts/lib/commands.mjs +0 -568
- package/skills/track-work/scripts/lib/contract.mjs +0 -303
- package/skills/track-work/scripts/lib/model.mjs +0 -506
- package/skills/track-work/scripts/lib/operations.mjs +0 -965
- package/skills/track-work/scripts/lib/record-store.mjs +0 -264
- package/skills/track-work/scripts/lib/result.mjs +0 -37
- package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
- package/skills/track-work/scripts/lib/schema.mjs +0 -277
- package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
- package/skills/track-work/scripts/lib/workspace.mjs +0 -134
|
@@ -1,653 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import process from "node:process";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { main } from "./adapters/codex-session-start.mjs";
|
|
4
|
+
import { runSafely } from "./shared/runtime-context.mjs";
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
const PLUGIN_ROOT = path.resolve(SCRIPT_DIR, "..");
|
|
11
|
-
const VALID_MODES = new Set(["conversation", "workflow", "strict-workflow"]);
|
|
12
|
-
const SESSION_STATE_VERSION = 1;
|
|
13
|
-
const SESSION_STATE_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
14
|
-
const CLEAR_TRANSFER_MAX_AGE_MS = 60 * 1000;
|
|
15
|
-
|
|
16
|
-
function readStdinJson() {
|
|
17
|
-
const raw = fs.readFileSync(0, "utf8").trim();
|
|
18
|
-
if (!raw) {
|
|
19
|
-
return { input: {}, raw: "" };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
return { input: JSON.parse(raw), raw };
|
|
24
|
-
} catch {
|
|
25
|
-
return { input: {}, raw };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function readText(filePath) {
|
|
30
|
-
try {
|
|
31
|
-
return fs.readFileSync(filePath, "utf8");
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function findWorkspaceRoot(cwd) {
|
|
38
|
-
let current = path.resolve(cwd || process.cwd());
|
|
39
|
-
|
|
40
|
-
while (true) {
|
|
41
|
-
if (fs.existsSync(path.join(current, ".git"))) {
|
|
42
|
-
return current;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const parent = path.dirname(current);
|
|
46
|
-
if (parent === current) {
|
|
47
|
-
return path.resolve(cwd || process.cwd());
|
|
48
|
-
}
|
|
49
|
-
current = parent;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function loadRuntimeContext(options = {}) {
|
|
54
|
-
const includeInteractionContract = options.interactionContract === true;
|
|
55
|
-
const includeSkills = options.skills === true;
|
|
56
|
-
const corePrompt = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "core.md"));
|
|
57
|
-
const interactionContract = includeInteractionContract
|
|
58
|
-
? readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "interaction-contract.md"))
|
|
59
|
-
: null;
|
|
60
|
-
const skillsPrompt = includeSkills ? readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "skills.md")) : null;
|
|
61
|
-
|
|
62
|
-
if (!corePrompt || (includeInteractionContract && !interactionContract) || (includeSkills && !skillsPrompt)) {
|
|
63
|
-
throw new Error("Freeflow runtime prompt files are missing.");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return { corePrompt, interactionContract, skillsPrompt };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readConfig(root) {
|
|
70
|
-
const repository = readConfigLayer(path.join(root, ".freeflow", "config.json"), isValidSetupConfig);
|
|
71
|
-
const local = readConfigLayer(path.join(root, ".freeflow", "local.json"), isValidLocalConfig);
|
|
72
|
-
const localValid = !local.exists || local.valid;
|
|
73
|
-
const configured = repository.valid && localValid;
|
|
74
|
-
const core = resolveCoreConfig(repository.valid ? repository.parsed : {}, local.valid ? local.parsed : {});
|
|
75
|
-
const enabled = configured && core.config.enabled;
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
exists: repository.exists,
|
|
79
|
-
valid: configured,
|
|
80
|
-
configured,
|
|
81
|
-
repositoryValid: repository.valid,
|
|
82
|
-
localExists: local.exists,
|
|
83
|
-
localValid,
|
|
84
|
-
error: repository.valid ? local.error : repository.error,
|
|
85
|
-
enabled,
|
|
86
|
-
interactionContractEnabled: enabled && core.config.interactionContract,
|
|
87
|
-
skillsEnabled: enabled && core.config.skills.enabled,
|
|
88
|
-
defaultMode: core.config.defaultMode,
|
|
89
|
-
sources: core.sources,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readConfigLayer(filePath, validate) {
|
|
94
|
-
if (!fs.existsSync(filePath)) {
|
|
95
|
-
return { path: filePath, exists: false, valid: false, parsed: {}, error: null };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
100
|
-
const error = validate(parsed);
|
|
101
|
-
return {
|
|
102
|
-
path: filePath,
|
|
103
|
-
exists: true,
|
|
104
|
-
valid: error === null,
|
|
105
|
-
parsed: error === null ? parsed : {},
|
|
106
|
-
error,
|
|
107
|
-
};
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return {
|
|
110
|
-
path: filePath,
|
|
111
|
-
exists: true,
|
|
112
|
-
valid: false,
|
|
113
|
-
parsed: {},
|
|
114
|
-
error: error instanceof Error ? error.message : String(error),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isRecord(value) {
|
|
120
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function validateCoreConfigFields(value) {
|
|
124
|
-
if (Object.hasOwn(value, "defaultMode") && !VALID_MODES.has(value.defaultMode)) {
|
|
125
|
-
return "defaultMode must be conversation, workflow, or strict-workflow";
|
|
126
|
-
}
|
|
127
|
-
if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") {
|
|
128
|
-
return "enabled must be a boolean";
|
|
129
|
-
}
|
|
130
|
-
if (Object.hasOwn(value, "interactionContract") && typeof value.interactionContract !== "boolean") {
|
|
131
|
-
return "interactionContract must be a boolean";
|
|
132
|
-
}
|
|
133
|
-
if (Object.hasOwn(value, "skills")) {
|
|
134
|
-
if (!isRecord(value.skills)) {
|
|
135
|
-
return "skills must be an object";
|
|
136
|
-
}
|
|
137
|
-
if (Object.hasOwn(value.skills, "enabled") && typeof value.skills.enabled !== "boolean") {
|
|
138
|
-
return "skills.enabled must be a boolean";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function isValidSetupConfig(value) {
|
|
145
|
-
if (!isRecord(value)) {
|
|
146
|
-
return "repository config must be a JSON object";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const allowedKeys = new Set([
|
|
150
|
-
"enabled",
|
|
151
|
-
"defaultMode",
|
|
152
|
-
"interactionContract",
|
|
153
|
-
"skills",
|
|
154
|
-
"outputRouter",
|
|
155
|
-
"observedRouting",
|
|
156
|
-
"scriptTransform",
|
|
157
|
-
"cognitiveRouting",
|
|
158
|
-
]);
|
|
159
|
-
if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
|
|
160
|
-
return "repository config contains unsupported top-level keys";
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const coreError = validateCoreConfigFields(value);
|
|
164
|
-
if (coreError) {
|
|
165
|
-
return coreError;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
|
|
169
|
-
if (Object.hasOwn(value, key) && !isRecord(value[key])) {
|
|
170
|
-
return `${key} must be an object`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function isValidLocalConfig(value) {
|
|
177
|
-
if (!isRecord(value)) {
|
|
178
|
-
return "local config must be a JSON object";
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const allowedKeys = new Set([
|
|
182
|
-
"enabled",
|
|
183
|
-
"defaultMode",
|
|
184
|
-
"interactionContract",
|
|
185
|
-
"skills",
|
|
186
|
-
"processing",
|
|
187
|
-
"cognitiveRouting",
|
|
188
|
-
]);
|
|
189
|
-
if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
|
|
190
|
-
return "local config contains unsupported top-level keys";
|
|
191
|
-
}
|
|
192
|
-
return validateCoreConfigFields(value);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function hasOwn(value, key) {
|
|
196
|
-
return Object.hasOwn(value, key);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function resolveLayeredValue(repository, local, key, fallback) {
|
|
200
|
-
if (hasOwn(local, key)) {
|
|
201
|
-
return { value: local[key], source: "local" };
|
|
202
|
-
}
|
|
203
|
-
if (hasOwn(repository, key)) {
|
|
204
|
-
return { value: repository[key], source: "repository" };
|
|
205
|
-
}
|
|
206
|
-
return { value: fallback, source: "builtin" };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function resolveCoreConfig(repository, local) {
|
|
210
|
-
const enabled = resolveLayeredValue(repository, local, "enabled", true);
|
|
211
|
-
const interactionContract = resolveLayeredValue(repository, local, "interactionContract", true);
|
|
212
|
-
const defaultMode = resolveLayeredValue(repository, local, "defaultMode", "workflow");
|
|
213
|
-
const repositorySkills = isRecord(repository.skills) ? repository.skills : {};
|
|
214
|
-
const localSkills = isRecord(local.skills) ? local.skills : {};
|
|
215
|
-
const skillsEnabled = resolveLayeredValue(repositorySkills, localSkills, "enabled", true);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
config: {
|
|
219
|
-
enabled: enabled.value,
|
|
220
|
-
interactionContract: interactionContract.value,
|
|
221
|
-
skills: { enabled: skillsEnabled.value },
|
|
222
|
-
defaultMode: defaultMode.value,
|
|
223
|
-
},
|
|
224
|
-
sources: {
|
|
225
|
-
enabled: enabled.source,
|
|
226
|
-
interactionContract: interactionContract.source,
|
|
227
|
-
skillsEnabled: skillsEnabled.source,
|
|
228
|
-
defaultMode: defaultMode.source,
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function hostName() {
|
|
234
|
-
return process.env.PLUGIN_DATA ? "codex" : "claude";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function pluginDataRoot() {
|
|
238
|
-
const configured = process.env.PLUGIN_DATA || process.env.CLAUDE_PLUGIN_DATA;
|
|
239
|
-
return configured ? path.resolve(configured) : null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function sessionStatePath(sessionId) {
|
|
243
|
-
const dataRoot = pluginDataRoot();
|
|
244
|
-
if (!dataRoot || typeof sessionId !== "string" || sessionId.length === 0) {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const key = crypto.createHash("sha256").update(`${hostName()}\0${sessionId}`).digest("hex");
|
|
249
|
-
return path.join(dataRoot, "session-modes", hostName(), `${key}.json`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function workspaceKey(root) {
|
|
253
|
-
return crypto.createHash("sha256").update(path.resolve(root)).digest("hex");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function claudeProcessKey() {
|
|
257
|
-
const processId = process.env.CLAUDE_PID;
|
|
258
|
-
if (typeof processId !== "string" || processId.length === 0) {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
return crypto.createHash("sha256").update(processId).digest("hex");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function clearTransferPath(root) {
|
|
265
|
-
const dataRoot = pluginDataRoot();
|
|
266
|
-
const processKey = claudeProcessKey();
|
|
267
|
-
if (!dataRoot || hostName() !== "claude" || !processKey) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
return path.join(dataRoot, "session-modes", "claude-clear", `${workspaceKey(root)}-${processKey}.json`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function writeJsonAtomic(filePath, value) {
|
|
274
|
-
const directory = path.dirname(filePath);
|
|
275
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
276
|
-
const temporaryPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
277
|
-
try {
|
|
278
|
-
fs.writeFileSync(temporaryPath, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
279
|
-
fs.renameSync(temporaryPath, filePath);
|
|
280
|
-
} catch (error) {
|
|
281
|
-
try {
|
|
282
|
-
fs.unlinkSync(temporaryPath);
|
|
283
|
-
} catch {
|
|
284
|
-
// Best-effort cleanup of a failed atomic write.
|
|
285
|
-
}
|
|
286
|
-
throw error;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function readSessionMode(sessionId) {
|
|
291
|
-
const filePath = sessionStatePath(sessionId);
|
|
292
|
-
if (!filePath) {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const state = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
298
|
-
if (state.version !== SESSION_STATE_VERSION || !VALID_MODES.has(state.mode)) {
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
return state.mode;
|
|
302
|
-
} catch {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function writeSessionMode(sessionId, mode) {
|
|
308
|
-
const filePath = sessionStatePath(sessionId);
|
|
309
|
-
if (!filePath) {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
writeJsonAtomic(filePath, {
|
|
314
|
-
version: SESSION_STATE_VERSION,
|
|
315
|
-
mode,
|
|
316
|
-
updatedAt: new Date().toISOString(),
|
|
317
|
-
});
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function clearSessionMode(sessionId) {
|
|
322
|
-
const filePath = sessionStatePath(sessionId);
|
|
323
|
-
if (!filePath) {
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
fs.unlinkSync(filePath);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
if (error?.code !== "ENOENT") {
|
|
331
|
-
throw error;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function stageClearTransfer(input) {
|
|
338
|
-
if (hostName() !== "claude" || input.reason !== "clear") {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const root = findWorkspaceRoot(input.cwd || process.cwd());
|
|
343
|
-
const mode = readSessionMode(input.session_id);
|
|
344
|
-
const filePath = clearTransferPath(root);
|
|
345
|
-
if (!filePath) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!mode) {
|
|
350
|
-
try {
|
|
351
|
-
fs.unlinkSync(filePath);
|
|
352
|
-
} catch (error) {
|
|
353
|
-
if (error?.code !== "ENOENT") {
|
|
354
|
-
throw error;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
writeJsonAtomic(filePath, {
|
|
361
|
-
version: SESSION_STATE_VERSION,
|
|
362
|
-
mode,
|
|
363
|
-
sourceSessionKey: crypto.createHash("sha256").update(String(input.session_id)).digest("hex"),
|
|
364
|
-
createdAt: new Date().toISOString(),
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function consumeClearTransfer(input, root) {
|
|
369
|
-
if (hostName() !== "claude" || input.source !== "clear") {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const filePath = clearTransferPath(root);
|
|
374
|
-
if (!filePath) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const claimedPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.claimed`;
|
|
379
|
-
try {
|
|
380
|
-
fs.renameSync(filePath, claimedPath);
|
|
381
|
-
} catch (error) {
|
|
382
|
-
if (error?.code === "ENOENT") {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
let state;
|
|
389
|
-
try {
|
|
390
|
-
state = JSON.parse(fs.readFileSync(claimedPath, "utf8"));
|
|
391
|
-
} catch {
|
|
392
|
-
return null;
|
|
393
|
-
} finally {
|
|
394
|
-
try {
|
|
395
|
-
fs.unlinkSync(claimedPath);
|
|
396
|
-
} catch {
|
|
397
|
-
// The atomic claim already prevents reuse; cleanup is best-effort.
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (!isRecord(state)) {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
const createdAt = Date.parse(state.createdAt);
|
|
405
|
-
if (
|
|
406
|
-
state.version !== SESSION_STATE_VERSION ||
|
|
407
|
-
!VALID_MODES.has(state.mode) ||
|
|
408
|
-
!Number.isFinite(createdAt) ||
|
|
409
|
-
Date.now() - createdAt > CLEAR_TRANSFER_MAX_AGE_MS ||
|
|
410
|
-
Date.now() < createdAt
|
|
411
|
-
) {
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return writeSessionMode(input.session_id, state.mode) ? state.mode : null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function cleanupExpiredSessionModes() {
|
|
419
|
-
const root = pluginDataRoot();
|
|
420
|
-
if (!root) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const stateRoot = path.join(root, "session-modes");
|
|
425
|
-
const cutoff = Date.now() - SESSION_STATE_MAX_AGE_MS;
|
|
426
|
-
for (const host of ["claude", "codex", "claude-clear"]) {
|
|
427
|
-
const directory = path.join(stateRoot, host);
|
|
428
|
-
let entries = [];
|
|
429
|
-
try {
|
|
430
|
-
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
431
|
-
} catch {
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
for (const entry of entries) {
|
|
436
|
-
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
const filePath = path.join(directory, entry.name);
|
|
440
|
-
try {
|
|
441
|
-
if (fs.statSync(filePath).mtimeMs < cutoff) {
|
|
442
|
-
fs.unlinkSync(filePath);
|
|
443
|
-
}
|
|
444
|
-
} catch {
|
|
445
|
-
// Cleanup is best-effort and must not prevent runtime delivery.
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function sourceLabel(source) {
|
|
452
|
-
if (source === "local") {
|
|
453
|
-
return "personal";
|
|
454
|
-
}
|
|
455
|
-
return source;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function resolveRuntimeState(input) {
|
|
459
|
-
const root = findWorkspaceRoot(input.cwd || process.cwd());
|
|
460
|
-
const config = readConfig(root);
|
|
461
|
-
const restoredMode = config.valid && config.enabled ? consumeClearTransfer(input, root) : null;
|
|
462
|
-
const sessionMode = config.valid && config.enabled ? restoredMode || readSessionMode(input.session_id) : null;
|
|
463
|
-
const resolvedMode = sessionMode || config.defaultMode;
|
|
464
|
-
|
|
465
|
-
return {
|
|
466
|
-
root,
|
|
467
|
-
config,
|
|
468
|
-
sessionMode,
|
|
469
|
-
resolvedMode,
|
|
470
|
-
effectiveMode: config.enabled && config.skillsEnabled ? resolvedMode : null,
|
|
471
|
-
modeSource: sessionMode ? "session override" : "configured default",
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function stateLines(state, sessionChange = null) {
|
|
476
|
-
const { config } = state;
|
|
477
|
-
const lines = [`Configured default: \`${config.defaultMode}\` (${sourceLabel(config.sources.defaultMode)})`];
|
|
478
|
-
|
|
479
|
-
if (sessionChange === "cleared") {
|
|
480
|
-
lines.push("Session override: cleared");
|
|
481
|
-
} else if (state.sessionMode) {
|
|
482
|
-
lines.push(`Session override: \`${state.sessionMode}\``);
|
|
483
|
-
} else {
|
|
484
|
-
lines.push("Session override: none");
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (state.effectiveMode) {
|
|
488
|
-
lines.push(`Effective mode: \`${state.effectiveMode}\` (${state.modeSource}, active)`);
|
|
489
|
-
} else {
|
|
490
|
-
lines.push(`Resolved mode: \`${state.resolvedMode}\` (dormant because Skills are disabled)`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return lines;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function capabilityLines(config) {
|
|
497
|
-
return [
|
|
498
|
-
`Interaction Contract: ${config.interactionContractEnabled ? "enabled" : "disabled"}`,
|
|
499
|
-
`Skills: ${config.skillsEnabled ? "enabled" : "disabled"}`,
|
|
500
|
-
];
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function runtimeStateContext(state, sessionChange = null) {
|
|
504
|
-
const { config } = state;
|
|
505
|
-
return ["# Freeflow Runtime State", "", ...stateLines(state, sessionChange), "", ...capabilityLines(config)].join(
|
|
506
|
-
"\n",
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function renderSessionStart(state) {
|
|
511
|
-
const { config } = state;
|
|
512
|
-
if (!config.valid) {
|
|
513
|
-
return "";
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (!config.enabled) {
|
|
517
|
-
return "";
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const { corePrompt, interactionContract, skillsPrompt } = loadRuntimeContext({
|
|
521
|
-
interactionContract: config.interactionContractEnabled,
|
|
522
|
-
skills: config.skillsEnabled,
|
|
523
|
-
});
|
|
524
|
-
const context = [corePrompt.trim()];
|
|
525
|
-
|
|
526
|
-
if (config.interactionContractEnabled) {
|
|
527
|
-
context.push(interactionContract.trim());
|
|
528
|
-
}
|
|
529
|
-
if (config.skillsEnabled) {
|
|
530
|
-
context.push(skillsPrompt.trim());
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
context.push(runtimeStateContext(state));
|
|
534
|
-
return context.filter(Boolean).join("\n\n");
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function normalizePrompt(prompt) {
|
|
538
|
-
return prompt
|
|
539
|
-
.trim()
|
|
540
|
-
.replace(/[.!]+$/u, "")
|
|
541
|
-
.replace(/\s+/gu, " ");
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function parseModeControl(prompt) {
|
|
545
|
-
if (typeof prompt !== "string" || prompt.length > 300 || /\?\s*$/u.test(prompt.trim())) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const normalized = normalizePrompt(prompt);
|
|
550
|
-
const nativeSet = normalized.match(
|
|
551
|
-
/^(?:\/freeflow mode|\$mode-contract|\/(?:freeflow:)?mode-contract) (conversation|workflow|strict-workflow)$/iu,
|
|
552
|
-
);
|
|
553
|
-
if (nativeSet) {
|
|
554
|
-
return { action: "set", mode: nativeSet[1].toLowerCase() };
|
|
555
|
-
}
|
|
556
|
-
if (/^(?:\/freeflow mode|\$mode-contract|\/(?:freeflow:)?mode-contract) reset$/iu.test(normalized)) {
|
|
557
|
-
return { action: "reset" };
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const setPatterns = [
|
|
561
|
-
/^switch to (conversation|workflow|strict-workflow)(?: mode)?$/iu,
|
|
562
|
-
/^switch (?:this|the) session to (conversation|workflow|strict-workflow)(?: mode)?$/iu,
|
|
563
|
-
/^use (conversation|workflow|strict-workflow)(?: mode)? for (?:this|the) session$/iu,
|
|
564
|
-
/^(?:change|set) (?:the )?(?:current|active|session) mode to (conversation|workflow|strict-workflow)$/iu,
|
|
565
|
-
/^switch (?:the )?(?:current|active|session) mode to (conversation|workflow|strict-workflow)$/iu,
|
|
566
|
-
];
|
|
567
|
-
for (const pattern of setPatterns) {
|
|
568
|
-
const match = normalized.match(pattern);
|
|
569
|
-
if (match) {
|
|
570
|
-
return { action: "set", mode: match[1].toLowerCase() };
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (/^(?:reset|clear|remove) (?:the )?(?:current |active )?session mode(?: override)?$/iu.test(normalized)) {
|
|
575
|
-
return { action: "reset" };
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return null;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function handleUserPromptSubmit(input) {
|
|
582
|
-
const control = parseModeControl(input.prompt);
|
|
583
|
-
if (!control) {
|
|
584
|
-
return "";
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const before = resolveRuntimeState(input);
|
|
588
|
-
if (!before.config.valid || !before.config.enabled) {
|
|
589
|
-
return "";
|
|
590
|
-
}
|
|
591
|
-
if (!before.config.skillsEnabled) {
|
|
592
|
-
return runtimeStateContext(before);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (control.action === "set") {
|
|
596
|
-
if (!writeSessionMode(input.session_id, control.mode)) {
|
|
597
|
-
return runtimeStateContext(before);
|
|
598
|
-
}
|
|
599
|
-
const state = resolveRuntimeState(input);
|
|
600
|
-
return runtimeStateContext(state, "set");
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!clearSessionMode(input.session_id)) {
|
|
604
|
-
return runtimeStateContext(before);
|
|
605
|
-
}
|
|
606
|
-
const state = resolveRuntimeState(input);
|
|
607
|
-
return runtimeStateContext(state, "cleared");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function shouldHandle(eventName) {
|
|
611
|
-
if (process.env.FREEFLOW_DISABLE_RUNTIME_CONTEXT === "1") {
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
|
-
return eventName === "SessionStart" || eventName === "UserPromptSubmit" || eventName === "SessionEnd";
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function emitAdditionalContext(eventName, additionalContext) {
|
|
618
|
-
process.stdout.write(
|
|
619
|
-
`${JSON.stringify({
|
|
620
|
-
hookSpecificOutput: {
|
|
621
|
-
hookEventName: eventName,
|
|
622
|
-
additionalContext,
|
|
623
|
-
},
|
|
624
|
-
})}\n`,
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function main() {
|
|
629
|
-
const { input, raw } = readStdinJson();
|
|
630
|
-
const eventName = process.argv[2] || input.hook_event_name || "";
|
|
631
|
-
|
|
632
|
-
if ((!raw && !eventName) || !shouldHandle(eventName)) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
cleanupExpiredSessionModes();
|
|
637
|
-
if (eventName === "SessionEnd") {
|
|
638
|
-
stageClearTransfer(input);
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
const additionalContext =
|
|
642
|
-
eventName === "SessionStart" ? renderSessionStart(resolveRuntimeState(input)) : handleUserPromptSubmit(input);
|
|
643
|
-
if (!additionalContext.trim()) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
emitAdditionalContext(eventName, additionalContext);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
main();
|
|
651
|
-
} catch (error) {
|
|
652
|
-
process.stderr.write(`Freeflow runtime context hook skipped: ${error.message}\n`);
|
|
653
|
-
}
|
|
6
|
+
runSafely(main);
|
package/hooks/hooks.json
CHANGED
|
@@ -1,40 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Load Freeflow runtime context
|
|
2
|
+
"description": "Load Freeflow core runtime context in Gemini CLI.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "startup|resume|clear|compact",
|
|
7
6
|
"hooks": [
|
|
8
7
|
{
|
|
9
8
|
"type": "command",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
}
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
|
-
"UserPromptSubmit": [
|
|
18
|
-
{
|
|
19
|
-
"hooks": [
|
|
20
|
-
{
|
|
21
|
-
"type": "command",
|
|
22
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT}}/hooks/freeflow-runtime-context.mjs\" UserPromptSubmit",
|
|
23
|
-
"timeout": 5,
|
|
24
|
-
"statusMessage": "Checking Freeflow mode"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"SessionEnd": [
|
|
30
|
-
{
|
|
31
|
-
"matcher": "clear",
|
|
32
|
-
"hooks": [
|
|
33
|
-
{
|
|
34
|
-
"type": "command",
|
|
35
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT}}/hooks/freeflow-runtime-context.mjs\" SessionEnd",
|
|
36
|
-
"timeout": 3,
|
|
37
|
-
"statusMessage": "Preserving Freeflow mode"
|
|
9
|
+
"name": "freeflow-runtime-context",
|
|
10
|
+
"command": "node \"${extensionPath}/hooks/adapters/gemini-session-start.mjs\" SessionStart",
|
|
11
|
+
"timeout": 5000
|
|
38
12
|
}
|
|
39
13
|
]
|
|
40
14
|
}
|