@hassangameryt/freeflow 0.6.0 → 0.7.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 -2
- package/.codex-plugin/plugin.json +4 -5
- package/README.md +39 -55
- package/capabilities/cognitive-routing/SKILL.md +288 -130
- package/command-surface.json +0 -24
- package/hooks/freeflow-runtime-context.mjs +42 -477
- package/hooks/hooks.json +1 -26
- package/hooks/tests/check-runtime-context-hook.sh +108 -319
- package/package.json +4 -2
- 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/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,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import process from "node:process";
|
|
@@ -8,16 +7,10 @@ import { fileURLToPath } from "node:url";
|
|
|
8
7
|
|
|
9
8
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
9
|
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
10
|
|
|
16
11
|
function readStdinJson() {
|
|
17
12
|
const raw = fs.readFileSync(0, "utf8").trim();
|
|
18
|
-
if (!raw) {
|
|
19
|
-
return { input: {}, raw: "" };
|
|
20
|
-
}
|
|
13
|
+
if (!raw) return { input: {}, raw: "" };
|
|
21
14
|
|
|
22
15
|
try {
|
|
23
16
|
return { input: JSON.parse(raw), raw };
|
|
@@ -28,7 +21,8 @@ function readStdinJson() {
|
|
|
28
21
|
|
|
29
22
|
function readText(filePath) {
|
|
30
23
|
try {
|
|
31
|
-
|
|
24
|
+
const text = fs.readFileSync(filePath, "utf8").trim();
|
|
25
|
+
return text || null;
|
|
32
26
|
} catch {
|
|
33
27
|
return null;
|
|
34
28
|
}
|
|
@@ -38,32 +32,23 @@ function findWorkspaceRoot(cwd) {
|
|
|
38
32
|
let current = path.resolve(cwd || process.cwd());
|
|
39
33
|
|
|
40
34
|
while (true) {
|
|
41
|
-
if (fs.existsSync(path.join(current, ".git")))
|
|
42
|
-
return current;
|
|
43
|
-
}
|
|
35
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
44
36
|
|
|
45
37
|
const parent = path.dirname(current);
|
|
46
|
-
if (parent === current)
|
|
47
|
-
return path.resolve(cwd || process.cwd());
|
|
48
|
-
}
|
|
38
|
+
if (parent === current) return path.resolve(cwd || process.cwd());
|
|
49
39
|
current = parent;
|
|
50
40
|
}
|
|
51
41
|
}
|
|
52
42
|
|
|
53
|
-
function loadRuntimeContext(
|
|
54
|
-
const includeInteractionContract = options.interactionContract === true;
|
|
55
|
-
const includeSkills = options.skills === true;
|
|
43
|
+
function loadRuntimeContext() {
|
|
56
44
|
const corePrompt = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "core.md"));
|
|
57
|
-
const interactionContract =
|
|
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;
|
|
45
|
+
const interactionContract = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "interaction-contract.md"));
|
|
61
46
|
|
|
62
|
-
if (!corePrompt ||
|
|
63
|
-
throw new Error("Freeflow runtime prompt files are missing.");
|
|
47
|
+
if (!corePrompt || !interactionContract) {
|
|
48
|
+
throw new Error("Freeflow core runtime prompt files are missing.");
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
return { corePrompt, interactionContract
|
|
51
|
+
return { corePrompt, interactionContract };
|
|
67
52
|
}
|
|
68
53
|
|
|
69
54
|
function readConfig(root) {
|
|
@@ -83,10 +68,8 @@ function readConfig(root) {
|
|
|
83
68
|
localValid,
|
|
84
69
|
error: repository.valid ? local.error : repository.error,
|
|
85
70
|
enabled,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
defaultMode: core.config.defaultMode,
|
|
89
|
-
sources: core.sources,
|
|
71
|
+
contextVirtualizationEnabled: enabled && core.config.contextVirtualization,
|
|
72
|
+
conversationHistoryEnabled: enabled && core.config.conversationHistory,
|
|
90
73
|
};
|
|
91
74
|
}
|
|
92
75
|
|
|
@@ -121,36 +104,25 @@ function isRecord(value) {
|
|
|
121
104
|
}
|
|
122
105
|
|
|
123
106
|
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
107
|
if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") {
|
|
128
108
|
return "enabled must be a boolean";
|
|
129
109
|
}
|
|
130
|
-
if (Object.hasOwn(value, "
|
|
131
|
-
return "
|
|
110
|
+
if (Object.hasOwn(value, "contextVirtualization") && typeof value.contextVirtualization !== "boolean") {
|
|
111
|
+
return "contextVirtualization must be a boolean";
|
|
132
112
|
}
|
|
133
|
-
if (Object.hasOwn(value, "
|
|
134
|
-
|
|
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
|
-
}
|
|
113
|
+
if (Object.hasOwn(value, "conversationHistory") && typeof value.conversationHistory !== "boolean") {
|
|
114
|
+
return "conversationHistory must be a boolean";
|
|
140
115
|
}
|
|
141
116
|
return null;
|
|
142
117
|
}
|
|
143
118
|
|
|
144
119
|
function isValidSetupConfig(value) {
|
|
145
|
-
if (!isRecord(value))
|
|
146
|
-
return "repository config must be a JSON object";
|
|
147
|
-
}
|
|
120
|
+
if (!isRecord(value)) return "repository config must be a JSON object";
|
|
148
121
|
|
|
149
122
|
const allowedKeys = new Set([
|
|
150
123
|
"enabled",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"skills",
|
|
124
|
+
"contextVirtualization",
|
|
125
|
+
"conversationHistory",
|
|
154
126
|
"outputRouter",
|
|
155
127
|
"observedRouting",
|
|
156
128
|
"scriptTransform",
|
|
@@ -161,30 +133,23 @@ function isValidSetupConfig(value) {
|
|
|
161
133
|
}
|
|
162
134
|
|
|
163
135
|
const coreError = validateCoreConfigFields(value);
|
|
164
|
-
if (coreError)
|
|
165
|
-
return coreError;
|
|
166
|
-
}
|
|
136
|
+
if (coreError) return coreError;
|
|
167
137
|
|
|
168
138
|
for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
|
|
169
|
-
if (Object.hasOwn(value, key) && !isRecord(value[key])) {
|
|
170
|
-
return `${key} must be an object`;
|
|
171
|
-
}
|
|
139
|
+
if (Object.hasOwn(value, key) && !isRecord(value[key])) return `${key} must be an object`;
|
|
172
140
|
}
|
|
173
141
|
return null;
|
|
174
142
|
}
|
|
175
143
|
|
|
176
144
|
function isValidLocalConfig(value) {
|
|
177
|
-
if (!isRecord(value))
|
|
178
|
-
return "local config must be a JSON object";
|
|
179
|
-
}
|
|
145
|
+
if (!isRecord(value)) return "local config must be a JSON object";
|
|
180
146
|
|
|
181
147
|
const allowedKeys = new Set([
|
|
182
148
|
"enabled",
|
|
183
|
-
"defaultMode",
|
|
184
|
-
"interactionContract",
|
|
185
|
-
"skills",
|
|
186
149
|
"processing",
|
|
187
150
|
"cognitiveRouting",
|
|
151
|
+
"contextVirtualization",
|
|
152
|
+
"conversationHistory",
|
|
188
153
|
]);
|
|
189
154
|
if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
|
|
190
155
|
return "local config contains unsupported top-level keys";
|
|
@@ -192,426 +157,36 @@ function isValidLocalConfig(value) {
|
|
|
192
157
|
return validateCoreConfigFields(value);
|
|
193
158
|
}
|
|
194
159
|
|
|
195
|
-
function hasOwn(value, key) {
|
|
196
|
-
return Object.hasOwn(value, key);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
160
|
function resolveLayeredValue(repository, local, key, fallback) {
|
|
200
|
-
if (hasOwn(local, key))
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (hasOwn(repository, key)) {
|
|
204
|
-
return { value: repository[key], source: "repository" };
|
|
205
|
-
}
|
|
206
|
-
return { value: fallback, source: "builtin" };
|
|
161
|
+
if (Object.hasOwn(local, key)) return local[key];
|
|
162
|
+
if (Object.hasOwn(repository, key)) return repository[key];
|
|
163
|
+
return fallback;
|
|
207
164
|
}
|
|
208
165
|
|
|
209
166
|
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
167
|
return {
|
|
218
168
|
config: {
|
|
219
|
-
enabled: enabled
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
defaultMode: defaultMode.value,
|
|
223
|
-
},
|
|
224
|
-
sources: {
|
|
225
|
-
enabled: enabled.source,
|
|
226
|
-
interactionContract: interactionContract.source,
|
|
227
|
-
skillsEnabled: skillsEnabled.source,
|
|
228
|
-
defaultMode: defaultMode.source,
|
|
169
|
+
enabled: resolveLayeredValue(repository, local, "enabled", true),
|
|
170
|
+
contextVirtualization: resolveLayeredValue(repository, local, "contextVirtualization", false),
|
|
171
|
+
conversationHistory: resolveLayeredValue(repository, local, "conversationHistory", false),
|
|
229
172
|
},
|
|
230
173
|
};
|
|
231
174
|
}
|
|
232
175
|
|
|
233
|
-
function
|
|
234
|
-
return
|
|
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");
|
|
176
|
+
function runtimeStateContext() {
|
|
177
|
+
return ["# Freeflow Runtime State", "", "Freeflow: active"].join("\n");
|
|
254
178
|
}
|
|
255
179
|
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
if (typeof processId !== "string" || processId.length === 0) {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
return crypto.createHash("sha256").update(processId).digest("hex");
|
|
262
|
-
}
|
|
180
|
+
function renderSessionStart(config) {
|
|
181
|
+
if (!config.valid || !config.enabled) return "";
|
|
263
182
|
|
|
264
|
-
|
|
265
|
-
|
|
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");
|
|
183
|
+
const { corePrompt, interactionContract } = loadRuntimeContext();
|
|
184
|
+
return [corePrompt.trim(), interactionContract.trim(), runtimeStateContext()].filter(Boolean).join("\n\n");
|
|
608
185
|
}
|
|
609
186
|
|
|
610
187
|
function shouldHandle(eventName) {
|
|
611
|
-
if (process.env.FREEFLOW_DISABLE_RUNTIME_CONTEXT === "1")
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
return eventName === "SessionStart" || eventName === "UserPromptSubmit" || eventName === "SessionEnd";
|
|
188
|
+
if (process.env.FREEFLOW_DISABLE_RUNTIME_CONTEXT === "1") return false;
|
|
189
|
+
return eventName === "SessionStart";
|
|
615
190
|
}
|
|
616
191
|
|
|
617
192
|
function emitAdditionalContext(eventName, additionalContext) {
|
|
@@ -629,21 +204,11 @@ function main() {
|
|
|
629
204
|
const { input, raw } = readStdinJson();
|
|
630
205
|
const eventName = process.argv[2] || input.hook_event_name || "";
|
|
631
206
|
|
|
632
|
-
if ((!raw && !eventName) || !shouldHandle(eventName))
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
207
|
+
if ((!raw && !eventName) || !shouldHandle(eventName)) return;
|
|
635
208
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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);
|
|
209
|
+
const root = findWorkspaceRoot(input.cwd || process.cwd());
|
|
210
|
+
const additionalContext = renderSessionStart(readConfig(root));
|
|
211
|
+
if (additionalContext.trim()) emitAdditionalContext(eventName, additionalContext);
|
|
647
212
|
}
|
|
648
213
|
|
|
649
214
|
try {
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Load Freeflow runtime context
|
|
2
|
+
"description": "Load Freeflow core runtime context.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -13,31 +13,6 @@
|
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
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"
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
16
|
]
|
|
42
17
|
}
|
|
43
18
|
}
|