@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.
Files changed (122) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +4 -5
  4. package/README.md +39 -55
  5. package/capabilities/cognitive-routing/SKILL.md +288 -130
  6. package/command-surface.json +0 -24
  7. package/hooks/freeflow-runtime-context.mjs +42 -477
  8. package/hooks/hooks.json +1 -26
  9. package/hooks/tests/check-runtime-context-hook.sh +108 -319
  10. package/package.json +4 -2
  11. package/pi-extension/dist/cognitive-routing/commands.js +3 -3
  12. package/pi-extension/dist/cognitive-routing/config.js +16 -20
  13. package/pi-extension/dist/cognitive-routing/controller.js +478 -48
  14. package/pi-extension/dist/cognitive-routing/history.js +12 -0
  15. package/pi-extension/dist/cognitive-routing/host.js +74 -12
  16. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  17. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  18. package/pi-extension/dist/cognitive-routing/tool.js +1 -0
  19. package/pi-extension/dist/cognitive-routing/types.js +1 -1
  20. package/pi-extension/dist/index.js +236 -219
  21. package/pi-extension/dist/runtime/runtime-context.js +126 -286
  22. package/pi-extension/dist/settings/settings-ui.js +52 -336
  23. package/pi-extension/src/cognitive-routing/commands.ts +15 -5
  24. package/pi-extension/src/cognitive-routing/config.ts +16 -20
  25. package/pi-extension/src/cognitive-routing/controller.ts +561 -51
  26. package/pi-extension/src/cognitive-routing/history.ts +12 -0
  27. package/pi-extension/src/cognitive-routing/host.ts +110 -16
  28. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  29. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  30. package/pi-extension/src/cognitive-routing/tool.ts +1 -0
  31. package/pi-extension/src/cognitive-routing/types.ts +1 -1
  32. package/pi-extension/src/index.ts +249 -239
  33. package/pi-extension/src/runtime/runtime-context.ts +129 -305
  34. package/pi-extension/src/settings/settings-ui.ts +53 -371
  35. package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
  36. package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
  37. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
  38. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  39. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  40. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  41. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
  42. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  43. package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
  44. package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
  45. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  46. package/runtime/prompts/cognitive-routing.md +5 -4
  47. package/runtime/prompts/conversation-history.md +1 -1
  48. package/runtime/prompts/core.md +77 -6
  49. package/runtime/prompts/interaction-contract.md +6 -2
  50. package/skills/action-selection/SKILL.md +93 -44
  51. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  52. package/skills/bypass/SKILL.md +109 -39
  53. package/skills/commit-work/SKILL.md +94 -55
  54. package/skills/commit-work/references/git-state-edges.md +94 -0
  55. package/skills/commit-work/references/simple-push.md +92 -0
  56. package/skills/commit-work/references/staging-decisions.md +79 -46
  57. package/skills/decision-gate/SKILL.md +80 -22
  58. package/skills/design-for-depth/SKILL.md +208 -43
  59. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  60. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  61. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  62. package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
  63. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  64. package/skills/diagnose-failure/SKILL.md +198 -52
  65. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
  66. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
  67. package/skills/discuss/SKILL.md +118 -76
  68. package/skills/discuss/references/discussion-continuity.md +84 -32
  69. package/skills/evaluate-skill/SKILL.md +166 -52
  70. package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
  71. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  72. package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
  73. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  74. package/skills/execute-work/SKILL.md +98 -59
  75. package/skills/execute-work/references/code-practices.md +4 -4
  76. package/skills/execute-work/references/execute-work-edges.md +42 -22
  77. package/skills/review-artifact/SKILL.md +132 -71
  78. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  79. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
  80. package/skills/review-work/SKILL.md +115 -72
  81. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  82. package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
  83. package/skills/review-work/references/security-risk-lens.md +17 -15
  84. package/skills/setup-freeflow/SKILL.md +14 -17
  85. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  86. package/skills/setup-freeflow/references/host-setup.md +6 -7
  87. package/skills/simplify-code/SKILL.md +97 -39
  88. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  89. package/skills/tdd/SKILL.md +108 -41
  90. package/skills/tdd/references/test-design.md +1 -1
  91. package/skills/track-work/SKILL.md +133 -300
  92. package/skills/track-work/references/working-record-format.md +515 -0
  93. package/skills/track-work/scripts/lib/document.mjs +713 -0
  94. package/skills/track-work/scripts/lib/format.mjs +166 -0
  95. package/skills/track-work/scripts/lib/store.mjs +233 -0
  96. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  97. package/skills/track-work/scripts/lib/views.mjs +27 -430
  98. package/skills/track-work/scripts/working-record.mjs +292 -98
  99. package/skills/verify-work/SKILL.md +46 -20
  100. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  101. package/skills/verify-work/references/integration-evidence.md +1 -7
  102. package/skills/verify-work/references/performance-evidence.md +1 -1
  103. package/skills/workflow/SKILL.md +110 -80
  104. package/skills/workflow/references/domain-skill-composition.md +24 -18
  105. package/skills/write-skill/SKILL.md +218 -62
  106. package/skills/write-skill/references/activation-boundaries.md +63 -21
  107. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  108. package/skills/write-skill/references/progressive-disclosure.md +91 -41
  109. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  110. package/runtime/prompts/skills.md +0 -65
  111. package/skills/mode-contract/SKILL.md +0 -104
  112. package/skills/track-work/scripts/lib/codec.mjs +0 -570
  113. package/skills/track-work/scripts/lib/commands.mjs +0 -568
  114. package/skills/track-work/scripts/lib/contract.mjs +0 -303
  115. package/skills/track-work/scripts/lib/model.mjs +0 -506
  116. package/skills/track-work/scripts/lib/operations.mjs +0 -965
  117. package/skills/track-work/scripts/lib/record-store.mjs +0 -264
  118. package/skills/track-work/scripts/lib/result.mjs +0 -37
  119. package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
  120. package/skills/track-work/scripts/lib/schema.mjs +0 -277
  121. package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
  122. 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
- return fs.readFileSync(filePath, "utf8");
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(options = {}) {
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 = 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;
45
+ const interactionContract = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "interaction-contract.md"));
61
46
 
62
- if (!corePrompt || (includeInteractionContract && !interactionContract) || (includeSkills && !skillsPrompt)) {
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, skillsPrompt };
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
- interactionContractEnabled: enabled && core.config.interactionContract,
87
- skillsEnabled: enabled && core.config.skills.enabled,
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, "interactionContract") && typeof value.interactionContract !== "boolean") {
131
- return "interactionContract must be a boolean";
110
+ if (Object.hasOwn(value, "contextVirtualization") && typeof value.contextVirtualization !== "boolean") {
111
+ return "contextVirtualization must be a boolean";
132
112
  }
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
- }
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
- "defaultMode",
152
- "interactionContract",
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
- 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" };
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.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,
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 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");
176
+ function runtimeStateContext() {
177
+ return ["# Freeflow Runtime State", "", "Freeflow: active"].join("\n");
254
178
  }
255
179
 
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
- }
180
+ function renderSessionStart(config) {
181
+ if (!config.valid || !config.enabled) return "";
263
182
 
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");
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
- return false;
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
- 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);
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 and manage session mode.",
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
  }