@hassangameryt/freeflow 0.7.0 → 0.7.2

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 +2 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +25 -0
  5. package/README.md +57 -26
  6. package/capabilities/cognitive-routing/SKILL.md +94 -311
  7. package/com.github.copilot/hooks/hooks.json +11 -0
  8. package/command-surface.json +7 -1
  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 -215
  19. package/hooks/hooks.json +4 -5
  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 +34 -3
  23. package/package.json +20 -6
  24. package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
  25. package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
  26. package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
  27. package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
  28. package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
  29. package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
  30. package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
  31. package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
  32. package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
  33. package/pi-extension/dist/freeflow-context/tool.js +5 -0
  34. package/pi-extension/dist/index.js +279 -624
  35. package/pi-extension/dist/runtime/runtime-context.js +63 -13
  36. package/pi-extension/dist/session-sources/events.js +195 -0
  37. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  38. package/pi-extension/dist/session-sources/sources.js +233 -0
  39. package/pi-extension/dist/settings/settings-ui.js +44 -117
  40. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  41. package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
  42. package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
  43. package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
  44. package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
  45. package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
  46. package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
  47. package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
  48. package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
  49. package/pi-extension/src/freeflow-context/tool.ts +5 -0
  50. package/pi-extension/src/index.ts +285 -674
  51. package/pi-extension/src/runtime/runtime-context.ts +75 -14
  52. package/pi-extension/src/session-sources/events.ts +208 -0
  53. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  54. package/pi-extension/src/session-sources/sources.ts +263 -0
  55. package/pi-extension/src/settings/settings-ui.ts +54 -131
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
  57. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
  59. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  60. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
  61. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  62. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
  63. package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
  64. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
  65. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  66. package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
  67. package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
  68. package/pi-extension/tests/fixtures/routing-native.js +188 -0
  69. package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
  70. package/pi-extension/tests/integration/pi-extension.test.js +154 -37
  71. package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
  72. package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
  73. package/plugin.json +33 -0
  74. package/runtime/prompts/cognitive-routing.md +4 -5
  75. package/runtime/prompts/core.md +51 -45
  76. package/runtime/prompts/interaction-contract.md +5 -5
  77. package/skills/action-selection/SKILL.md +57 -97
  78. package/skills/design-for-depth/SKILL.md +58 -193
  79. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  80. package/skills/diagnose-failure/SKILL.md +64 -196
  81. package/skills/discuss/SKILL.md +64 -123
  82. package/skills/discuss/references/discussion-continuity.md +7 -3
  83. package/skills/execute-work/SKILL.md +69 -88
  84. package/skills/execute-work/references/execute-work-edges.md +9 -3
  85. package/skills/handoff/SKILL.md +44 -54
  86. package/skills/handoff/references/handoff-templates.md +15 -6
  87. package/skills/review-artifact/SKILL.md +80 -127
  88. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  89. package/skills/review-work/SKILL.md +65 -115
  90. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  91. package/skills/simplify-code/SKILL.md +47 -100
  92. package/skills/track-work/SKILL.md +128 -119
  93. package/skills/track-work/references/working-record-format.md +14 -2
  94. package/skills/verify-work/SKILL.md +58 -62
  95. package/skills/verify-work/references/integration-evidence.md +4 -2
  96. package/skills/verify-work/references/test-design.md +89 -0
  97. package/skills/workflow/SKILL.md +73 -119
  98. package/skills/write-plan/SKILL.md +46 -77
  99. package/skills/write-plan/references/plan-shapes.md +5 -5
  100. package/skills/write-spec/SKILL.md +51 -63
  101. package/skills/write-spec/references/artifact-standards.md +2 -2
  102. package/skills/write-spec/references/decision-records.md +3 -2
  103. package/skills/write-spec/references/spec-shapes.md +15 -0
  104. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  105. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  106. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  107. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  108. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  109. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  110. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  111. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  112. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  113. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  114. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  115. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  116. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  117. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  118. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  119. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  120. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  121. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  122. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  123. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  124. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  125. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  126. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  127. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  128. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  129. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  130. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  131. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  132. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  133. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  134. package/skills/tdd/SKILL.md +0 -143
  135. package/skills/tdd/references/test-design.md +0 -96
  136. /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
@@ -0,0 +1,13 @@
1
+ import { isMainModule, runSafely, runSessionStartAdapter } from "../shared/runtime-context.mjs";
2
+
3
+ export function main() {
4
+ runSessionStartAdapter({
5
+ eventNames: ["SessionStart"],
6
+ getWorkspaceCwd: (input) => input.cwd || process.cwd(),
7
+ formatOutput: ({ additionalContext }) => ({
8
+ additionalContext,
9
+ }),
10
+ });
11
+ }
12
+
13
+ if (isMainModule(import.meta.url)) runSafely(main);
@@ -0,0 +1,13 @@
1
+ import { isMainModule, runSafely, runSessionStartAdapter } from "../shared/runtime-context.mjs";
2
+
3
+ export function main() {
4
+ runSessionStartAdapter({
5
+ eventNames: ["sessionStart", "SessionStart"],
6
+ getWorkspaceCwd: (input) => input.workspace_roots?.[0] || input.cwd || process.cwd(),
7
+ formatOutput: ({ additionalContext }) => ({
8
+ additional_context: additionalContext,
9
+ }),
10
+ });
11
+ }
12
+
13
+ if (isMainModule(import.meta.url)) runSafely(main);
@@ -0,0 +1,15 @@
1
+ import { isMainModule, runSafely, runSessionStartAdapter } from "../shared/runtime-context.mjs";
2
+
3
+ export function main() {
4
+ runSessionStartAdapter({
5
+ eventNames: ["SessionStart"],
6
+ getWorkspaceCwd: (input) => input.cwd || process.cwd(),
7
+ formatOutput: ({ additionalContext }) => ({
8
+ hookSpecificOutput: {
9
+ additionalContext,
10
+ },
11
+ }),
12
+ });
13
+ }
14
+
15
+ if (isMainModule(import.meta.url)) runSafely(main);
@@ -0,0 +1,18 @@
1
+ {
2
+ "description": "Load Freeflow core runtime context in Claude Code.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume|clear|compact",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/adapters/claude-session-start.mjs\" SessionStart",
11
+ "timeout": 5,
12
+ "statusMessage": "Loading Freeflow context"
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "description": "Load Freeflow core runtime context in Codex.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume|clear|compact",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node \"${PLUGIN_ROOT}/hooks/adapters/codex-session-start.mjs\" SessionStart",
11
+ "timeout": 5,
12
+ "statusMessage": "Loading Freeflow context"
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "hooks": {
3
+ "sessionStart": [
4
+ {
5
+ "command": "node hooks/adapters/cursor-session-start.mjs SessionStart"
6
+ }
7
+ ]
8
+ }
9
+ }
@@ -1,218 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import process from "node:process";
6
- import { fileURLToPath } from "node:url";
3
+ import { main } from "./adapters/codex-session-start.mjs";
4
+ import { runSafely } from "./shared/runtime-context.mjs";
7
5
 
8
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
9
- const PLUGIN_ROOT = path.resolve(SCRIPT_DIR, "..");
10
-
11
- function readStdinJson() {
12
- const raw = fs.readFileSync(0, "utf8").trim();
13
- if (!raw) return { input: {}, raw: "" };
14
-
15
- try {
16
- return { input: JSON.parse(raw), raw };
17
- } catch {
18
- return { input: {}, raw };
19
- }
20
- }
21
-
22
- function readText(filePath) {
23
- try {
24
- const text = fs.readFileSync(filePath, "utf8").trim();
25
- return text || null;
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- function findWorkspaceRoot(cwd) {
32
- let current = path.resolve(cwd || process.cwd());
33
-
34
- while (true) {
35
- if (fs.existsSync(path.join(current, ".git"))) return current;
36
-
37
- const parent = path.dirname(current);
38
- if (parent === current) return path.resolve(cwd || process.cwd());
39
- current = parent;
40
- }
41
- }
42
-
43
- function loadRuntimeContext() {
44
- const corePrompt = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "core.md"));
45
- const interactionContract = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "interaction-contract.md"));
46
-
47
- if (!corePrompt || !interactionContract) {
48
- throw new Error("Freeflow core runtime prompt files are missing.");
49
- }
50
-
51
- return { corePrompt, interactionContract };
52
- }
53
-
54
- function readConfig(root) {
55
- const repository = readConfigLayer(path.join(root, ".freeflow", "config.json"), isValidSetupConfig);
56
- const local = readConfigLayer(path.join(root, ".freeflow", "local.json"), isValidLocalConfig);
57
- const localValid = !local.exists || local.valid;
58
- const configured = repository.valid && localValid;
59
- const core = resolveCoreConfig(repository.valid ? repository.parsed : {}, local.valid ? local.parsed : {});
60
- const enabled = configured && core.config.enabled;
61
-
62
- return {
63
- exists: repository.exists,
64
- valid: configured,
65
- configured,
66
- repositoryValid: repository.valid,
67
- localExists: local.exists,
68
- localValid,
69
- error: repository.valid ? local.error : repository.error,
70
- enabled,
71
- contextVirtualizationEnabled: enabled && core.config.contextVirtualization,
72
- conversationHistoryEnabled: enabled && core.config.conversationHistory,
73
- };
74
- }
75
-
76
- function readConfigLayer(filePath, validate) {
77
- if (!fs.existsSync(filePath)) {
78
- return { path: filePath, exists: false, valid: false, parsed: {}, error: null };
79
- }
80
-
81
- try {
82
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
83
- const error = validate(parsed);
84
- return {
85
- path: filePath,
86
- exists: true,
87
- valid: error === null,
88
- parsed: error === null ? parsed : {},
89
- error,
90
- };
91
- } catch (error) {
92
- return {
93
- path: filePath,
94
- exists: true,
95
- valid: false,
96
- parsed: {},
97
- error: error instanceof Error ? error.message : String(error),
98
- };
99
- }
100
- }
101
-
102
- function isRecord(value) {
103
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
104
- }
105
-
106
- function validateCoreConfigFields(value) {
107
- if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") {
108
- return "enabled must be a boolean";
109
- }
110
- if (Object.hasOwn(value, "contextVirtualization") && typeof value.contextVirtualization !== "boolean") {
111
- return "contextVirtualization must be a boolean";
112
- }
113
- if (Object.hasOwn(value, "conversationHistory") && typeof value.conversationHistory !== "boolean") {
114
- return "conversationHistory must be a boolean";
115
- }
116
- return null;
117
- }
118
-
119
- function isValidSetupConfig(value) {
120
- if (!isRecord(value)) return "repository config must be a JSON object";
121
-
122
- const allowedKeys = new Set([
123
- "enabled",
124
- "contextVirtualization",
125
- "conversationHistory",
126
- "outputRouter",
127
- "observedRouting",
128
- "scriptTransform",
129
- "cognitiveRouting",
130
- ]);
131
- if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
132
- return "repository config contains unsupported top-level keys";
133
- }
134
-
135
- const coreError = validateCoreConfigFields(value);
136
- if (coreError) return coreError;
137
-
138
- for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
139
- if (Object.hasOwn(value, key) && !isRecord(value[key])) return `${key} must be an object`;
140
- }
141
- return null;
142
- }
143
-
144
- function isValidLocalConfig(value) {
145
- if (!isRecord(value)) return "local config must be a JSON object";
146
-
147
- const allowedKeys = new Set([
148
- "enabled",
149
- "processing",
150
- "cognitiveRouting",
151
- "contextVirtualization",
152
- "conversationHistory",
153
- ]);
154
- if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
155
- return "local config contains unsupported top-level keys";
156
- }
157
- return validateCoreConfigFields(value);
158
- }
159
-
160
- function resolveLayeredValue(repository, local, key, fallback) {
161
- if (Object.hasOwn(local, key)) return local[key];
162
- if (Object.hasOwn(repository, key)) return repository[key];
163
- return fallback;
164
- }
165
-
166
- function resolveCoreConfig(repository, local) {
167
- return {
168
- config: {
169
- enabled: resolveLayeredValue(repository, local, "enabled", true),
170
- contextVirtualization: resolveLayeredValue(repository, local, "contextVirtualization", false),
171
- conversationHistory: resolveLayeredValue(repository, local, "conversationHistory", false),
172
- },
173
- };
174
- }
175
-
176
- function runtimeStateContext() {
177
- return ["# Freeflow Runtime State", "", "Freeflow: active"].join("\n");
178
- }
179
-
180
- function renderSessionStart(config) {
181
- if (!config.valid || !config.enabled) return "";
182
-
183
- const { corePrompt, interactionContract } = loadRuntimeContext();
184
- return [corePrompt.trim(), interactionContract.trim(), runtimeStateContext()].filter(Boolean).join("\n\n");
185
- }
186
-
187
- function shouldHandle(eventName) {
188
- if (process.env.FREEFLOW_DISABLE_RUNTIME_CONTEXT === "1") return false;
189
- return eventName === "SessionStart";
190
- }
191
-
192
- function emitAdditionalContext(eventName, additionalContext) {
193
- process.stdout.write(
194
- `${JSON.stringify({
195
- hookSpecificOutput: {
196
- hookEventName: eventName,
197
- additionalContext,
198
- },
199
- })}\n`,
200
- );
201
- }
202
-
203
- function main() {
204
- const { input, raw } = readStdinJson();
205
- const eventName = process.argv[2] || input.hook_event_name || "";
206
-
207
- if ((!raw && !eventName) || !shouldHandle(eventName)) return;
208
-
209
- const root = findWorkspaceRoot(input.cwd || process.cwd());
210
- const additionalContext = renderSessionStart(readConfig(root));
211
- if (additionalContext.trim()) emitAdditionalContext(eventName, additionalContext);
212
- }
213
-
214
- try {
215
- main();
216
- } catch (error) {
217
- process.stderr.write(`Freeflow runtime context hook skipped: ${error.message}\n`);
218
- }
6
+ runSafely(main);
package/hooks/hooks.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
- "description": "Load Freeflow core runtime context.",
2
+ "description": "Load Freeflow core runtime context in Gemini CLI.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
6
- "matcher": "startup|resume|clear|compact",
7
6
  "hooks": [
8
7
  {
9
8
  "type": "command",
10
- "command": "node \"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT}}/hooks/freeflow-runtime-context.mjs\" SessionStart",
11
- "timeout": 5,
12
- "statusMessage": "Loading Freeflow context"
9
+ "name": "freeflow-runtime-context",
10
+ "command": "node \"${extensionPath}/hooks/adapters/gemini-session-start.mjs\" SessionStart",
11
+ "timeout": 5000
13
12
  }
14
13
  ]
15
14
  }
@@ -0,0 +1,210 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+ export const PLUGIN_ROOT = path.resolve(SCRIPT_DIR, "../..");
8
+
9
+ export function readHookInput() {
10
+ const raw = fs.readFileSync(0, "utf8").trim();
11
+ if (!raw) return { input: {}, raw: "" };
12
+
13
+ try {
14
+ return { input: JSON.parse(raw), raw };
15
+ } catch {
16
+ return { input: {}, raw };
17
+ }
18
+ }
19
+
20
+ function readText(filePath) {
21
+ try {
22
+ const text = fs.readFileSync(filePath, "utf8").trim();
23
+ return text || null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export function findWorkspaceRoot(cwd) {
30
+ let current = path.resolve(cwd || process.cwd());
31
+
32
+ while (true) {
33
+ if (fs.existsSync(path.join(current, ".git"))) return current;
34
+
35
+ const parent = path.dirname(current);
36
+ if (parent === current) return path.resolve(cwd || process.cwd());
37
+ current = parent;
38
+ }
39
+ }
40
+
41
+ function readConfigLayer(filePath, validate) {
42
+ if (!fs.existsSync(filePath)) {
43
+ return { exists: false, valid: false, parsed: {}, error: null };
44
+ }
45
+
46
+ try {
47
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
48
+ const error = validate(parsed);
49
+ return {
50
+ exists: true,
51
+ valid: error === null,
52
+ parsed: error === null ? parsed : {},
53
+ error,
54
+ };
55
+ } catch (error) {
56
+ return {
57
+ exists: true,
58
+ valid: false,
59
+ parsed: {},
60
+ error: error instanceof Error ? error.message : String(error),
61
+ };
62
+ }
63
+ }
64
+
65
+ function isRecord(value) {
66
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
67
+ }
68
+
69
+ function validateCoreConfigFields(value) {
70
+ if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") {
71
+ return "enabled must be a boolean";
72
+ }
73
+ if (Object.hasOwn(value, "contextVirtualization") && typeof value.contextVirtualization !== "boolean") {
74
+ return "contextVirtualization must be a boolean";
75
+ }
76
+ if (Object.hasOwn(value, "conversationHistory") && typeof value.conversationHistory !== "boolean") {
77
+ return "conversationHistory must be a boolean";
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function isValidSetupConfig(value) {
83
+ if (!isRecord(value)) return "repository config must be a JSON object";
84
+
85
+ const allowedKeys = new Set([
86
+ "enabled",
87
+ "contextVirtualization",
88
+ "conversationHistory",
89
+ "outputRouter",
90
+ "observedRouting",
91
+ "scriptTransform",
92
+ "cognitiveRouting",
93
+ ]);
94
+ if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
95
+ return "repository config contains unsupported top-level keys";
96
+ }
97
+
98
+ const coreError = validateCoreConfigFields(value);
99
+ if (coreError) return coreError;
100
+
101
+ for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
102
+ if (Object.hasOwn(value, key) && !isRecord(value[key])) return `${key} must be an object`;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function isValidLocalConfig(value) {
108
+ if (!isRecord(value)) return "local config must be a JSON object";
109
+
110
+ const allowedKeys = new Set([
111
+ "enabled",
112
+ "processing",
113
+ "cognitiveRouting",
114
+ "contextVirtualization",
115
+ "conversationHistory",
116
+ ]);
117
+ if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
118
+ return "local config contains unsupported top-level keys";
119
+ }
120
+ return validateCoreConfigFields(value);
121
+ }
122
+
123
+ function resolveLayeredValue(repository, local, key, fallback) {
124
+ if (Object.hasOwn(local, key)) return local[key];
125
+ if (Object.hasOwn(repository, key)) return repository[key];
126
+ return fallback;
127
+ }
128
+
129
+ function resolveCoreConfig(repository, local) {
130
+ return {
131
+ enabled: resolveLayeredValue(repository, local, "enabled", true),
132
+ contextVirtualization: resolveLayeredValue(repository, local, "contextVirtualization", false),
133
+ conversationHistory: resolveLayeredValue(repository, local, "conversationHistory", false),
134
+ };
135
+ }
136
+
137
+ export function readConfig(root) {
138
+ const repository = readConfigLayer(path.join(root, ".freeflow", "config.json"), isValidSetupConfig);
139
+ const local = readConfigLayer(path.join(root, ".freeflow", "local.json"), isValidLocalConfig);
140
+ const localValid = !local.exists || local.valid;
141
+ const configured = repository.valid && localValid;
142
+ const core = resolveCoreConfig(repository.valid ? repository.parsed : {}, local.valid ? local.parsed : {});
143
+ const enabled = configured && core.enabled;
144
+
145
+ return {
146
+ exists: repository.exists,
147
+ valid: configured,
148
+ configured,
149
+ repositoryValid: repository.valid,
150
+ localExists: local.exists,
151
+ localValid,
152
+ error: repository.valid ? local.error : repository.error,
153
+ enabled,
154
+ contextVirtualizationEnabled: enabled && core.contextVirtualization,
155
+ conversationHistoryEnabled: enabled && core.conversationHistory,
156
+ };
157
+ }
158
+
159
+ function loadRuntimeContext() {
160
+ const corePrompt = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "core.md"));
161
+ const interactionContract = readText(path.join(PLUGIN_ROOT, "runtime", "prompts", "interaction-contract.md"));
162
+
163
+ if (!corePrompt || !interactionContract) {
164
+ throw new Error("Freeflow core runtime prompt files are missing.");
165
+ }
166
+
167
+ return { corePrompt, interactionContract };
168
+ }
169
+
170
+ function runtimeStateContext() {
171
+ return ["# Freeflow Runtime State", "", "Freeflow: active"].join("\n");
172
+ }
173
+
174
+ export function renderRuntimeContext(root) {
175
+ const config = readConfig(root);
176
+ if (!config.valid || !config.enabled) return "";
177
+
178
+ const { corePrompt, interactionContract } = loadRuntimeContext();
179
+ return [corePrompt, interactionContract, runtimeStateContext()].filter(Boolean).join("\n\n");
180
+ }
181
+
182
+ export function emitHookOutput(output) {
183
+ process.stdout.write(`${JSON.stringify(output)}\n`);
184
+ }
185
+
186
+ export function runSessionStartAdapter({ eventNames, getWorkspaceCwd, formatOutput }) {
187
+ const { input, raw } = readHookInput();
188
+ const eventName = process.argv[2] || input.hook_event_name || "";
189
+ if ((!raw && !eventName) || process.env.FREEFLOW_DISABLE_RUNTIME_CONTEXT === "1") return;
190
+ if (!eventNames.includes(eventName)) return;
191
+
192
+ const root = findWorkspaceRoot(getWorkspaceCwd(input));
193
+ const additionalContext = renderRuntimeContext(root);
194
+ if (additionalContext.trim()) {
195
+ emitHookOutput(formatOutput({ eventName, input, additionalContext }));
196
+ }
197
+ }
198
+
199
+ export function runSafely(run) {
200
+ try {
201
+ run();
202
+ } catch (error) {
203
+ const message = error instanceof Error ? error.message : String(error);
204
+ process.stderr.write(`Freeflow runtime context hook skipped: ${message}\n`);
205
+ }
206
+ }
207
+
208
+ export function isMainModule(metaUrl) {
209
+ return process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === metaUrl;
210
+ }
@@ -0,0 +1,82 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+
8
+ const repoRoot = resolve(import.meta.dirname, "../..");
9
+ const adapters = {
10
+ legacy: ["hooks/freeflow-runtime-context.mjs", "hookSpecificOutput.additionalContext"],
11
+ claude: ["hooks/adapters/claude-session-start.mjs", "hookSpecificOutput.additionalContext"],
12
+ codex: ["hooks/adapters/codex-session-start.mjs", "hookSpecificOutput.additionalContext"],
13
+ gemini: ["hooks/adapters/gemini-session-start.mjs", "hookSpecificOutput.additionalContext"],
14
+ copilot: ["hooks/adapters/copilot-session-start.mjs", "additionalContext"],
15
+ cursor: ["hooks/adapters/cursor-session-start.mjs", "additional_context"],
16
+ };
17
+
18
+ function createWorkspace(config = "{}\n") {
19
+ const root = mkdtempSync(join(tmpdir(), "freeflow-host-adapter-"));
20
+ mkdirSync(join(root, ".freeflow"), { recursive: true });
21
+ writeFileSync(join(root, ".freeflow", "config.json"), config, "utf8");
22
+ return root;
23
+ }
24
+
25
+ function runAdapter(relativePath, workspace, input, event = "SessionStart") {
26
+ const result = spawnSync(process.execPath, [join(repoRoot, relativePath), event], {
27
+ cwd: repoRoot,
28
+ input: `${JSON.stringify(input)}\n`,
29
+ encoding: "utf8",
30
+ });
31
+ assert.equal(result.status, 0, `${relativePath} stderr: ${result.stderr}`);
32
+ assert.equal(result.stderr, "", `${relativePath} should not log on the happy path`);
33
+ return result.stdout.trim() ? JSON.parse(result.stdout) : undefined;
34
+ }
35
+
36
+ test("host adapters share rendered mandatory context but emit host-native envelopes", () => {
37
+ const workspace = createWorkspace();
38
+ const outputs = {};
39
+ for (const [host, [relativePath]] of Object.entries(adapters)) {
40
+ const input =
41
+ host === "cursor"
42
+ ? { hook_event_name: "sessionStart", workspace_roots: [workspace] }
43
+ : { hook_event_name: "SessionStart", cwd: workspace };
44
+ outputs[host] = runAdapter(relativePath, workspace, input, host === "cursor" ? "sessionStart" : "SessionStart");
45
+ assert.ok(outputs[host], `${host} should emit context`);
46
+ }
47
+
48
+ const legacyContext = outputs.legacy.hookSpecificOutput.additionalContext;
49
+ for (const host of ["claude", "codex", "gemini"]) {
50
+ assert.equal(outputs[host].hookSpecificOutput.additionalContext, legacyContext);
51
+ }
52
+ assert.deepEqual(Object.keys(outputs.copilot).sort(), ["additionalContext"]);
53
+ assert.equal(outputs.copilot.additionalContext, legacyContext);
54
+ assert.equal(outputs.copilot.hookSpecificOutput, undefined);
55
+ assert.equal(outputs.cursor.additional_context, legacyContext);
56
+ assert.equal(outputs.cursor.hookSpecificOutput, undefined);
57
+ assert.match(legacyContext, /# Freeflow Stable Guidance/);
58
+ assert.match(legacyContext, /# Freeflow Interaction Contract/);
59
+ assert.match(legacyContext, /Freeflow: active/);
60
+ assert.doesNotMatch(legacyContext, /Cognitive Routing/);
61
+ });
62
+
63
+ test("host adapters stay inert for disabled, invalid, and unsupported lifecycle inputs", () => {
64
+ const disabledWorkspace = createWorkspace('{"enabled":false}\n');
65
+ const invalidWorkspace = createWorkspace('{"enabled":"no"}\n');
66
+ for (const [host, [relativePath]] of Object.entries(adapters)) {
67
+ const input =
68
+ host === "cursor"
69
+ ? { hook_event_name: "sessionStart", workspace_roots: [disabledWorkspace] }
70
+ : { hook_event_name: "SessionStart", cwd: disabledWorkspace };
71
+ assert.equal(
72
+ runAdapter(relativePath, disabledWorkspace, input, host === "cursor" ? "sessionStart" : "SessionStart"),
73
+ undefined,
74
+ );
75
+ const invalidInput = { ...input, cwd: invalidWorkspace, workspace_roots: [invalidWorkspace] };
76
+ assert.equal(
77
+ runAdapter(relativePath, invalidWorkspace, invalidInput, host === "cursor" ? "sessionStart" : "SessionStart"),
78
+ undefined,
79
+ );
80
+ assert.equal(runAdapter(relativePath, disabledWorkspace, input, "UserPromptSubmit"), undefined);
81
+ }
82
+ });