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