@hassangameryt/freeflow 0.7.1 → 0.7.3

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 (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +153 -128
  6. package/capabilities/cognitive-routing/SKILL.md +126 -274
  7. package/command-surface.json +7 -1
  8. package/gemini-extension.json +1 -1
  9. package/package.json +4 -4
  10. package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
  11. package/pi-extension/dist/cognitive-routing-v2/config.js +136 -0
  12. package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
  13. package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
  14. package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
  15. package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
  16. package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
  17. package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
  18. package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
  19. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  20. package/pi-extension/dist/freeflow-context/tool.js +12 -6
  21. package/pi-extension/dist/index.js +302 -626
  22. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  23. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  24. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  25. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  26. package/pi-extension/dist/provider-support/index.js +4 -0
  27. package/pi-extension/dist/runtime/request-history.js +216 -0
  28. package/pi-extension/dist/runtime/runtime-context.js +85 -16
  29. package/pi-extension/dist/session-sources/events.js +217 -0
  30. package/pi-extension/dist/session-sources/provenance.js +32 -0
  31. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  32. package/pi-extension/dist/session-sources/sources.js +277 -0
  33. package/pi-extension/dist/settings/settings-ui.js +274 -147
  34. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  35. package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
  36. package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
  37. package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
  38. package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
  39. package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
  40. package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
  41. package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
  42. package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
  43. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  44. package/pi-extension/src/freeflow-context/tool.ts +12 -6
  45. package/pi-extension/src/index.ts +308 -675
  46. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  47. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  48. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  49. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  50. package/pi-extension/src/provider-support/index.ts +5 -0
  51. package/pi-extension/src/runtime/request-history.ts +243 -0
  52. package/pi-extension/src/runtime/runtime-context.ts +100 -17
  53. package/pi-extension/src/session-sources/events.ts +230 -0
  54. package/pi-extension/src/session-sources/provenance.ts +39 -0
  55. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  56. package/pi-extension/src/session-sources/sources.ts +334 -0
  57. package/pi-extension/src/settings/settings-ui.ts +311 -158
  58. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  59. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  60. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  61. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +577 -0
  62. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  63. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  64. package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
  65. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  66. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
  67. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  68. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
  69. package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
  70. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
  71. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  72. package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
  73. package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
  74. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  75. package/pi-extension/tests/fixtures/routing-native.js +191 -0
  76. package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
  77. package/pi-extension/tests/integration/pi-extension.test.js +655 -59
  78. package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
  79. package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
  80. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  81. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  82. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  83. package/pi-extension/tests/provider-support/native.test.js +183 -0
  84. package/plugin.json +1 -1
  85. package/runtime/prompts/cognitive-routing.md +4 -5
  86. package/runtime/prompts/core.md +51 -45
  87. package/runtime/prompts/interaction-contract.md +5 -5
  88. package/skills/action-selection/SKILL.md +57 -97
  89. package/skills/design-for-depth/SKILL.md +58 -193
  90. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  91. package/skills/diagnose-failure/SKILL.md +64 -196
  92. package/skills/discuss/SKILL.md +64 -123
  93. package/skills/discuss/references/discussion-continuity.md +7 -3
  94. package/skills/execute-work/SKILL.md +69 -88
  95. package/skills/execute-work/references/execute-work-edges.md +9 -3
  96. package/skills/handoff/SKILL.md +44 -54
  97. package/skills/handoff/references/handoff-templates.md +15 -6
  98. package/skills/review-artifact/SKILL.md +80 -127
  99. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  100. package/skills/review-work/SKILL.md +65 -115
  101. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  102. package/skills/simplify-code/SKILL.md +47 -100
  103. package/skills/track-work/SKILL.md +128 -119
  104. package/skills/track-work/references/working-record-format.md +14 -2
  105. package/skills/verify-work/SKILL.md +58 -62
  106. package/skills/verify-work/references/integration-evidence.md +4 -2
  107. package/skills/verify-work/references/test-design.md +89 -0
  108. package/skills/workflow/SKILL.md +73 -119
  109. package/skills/write-plan/SKILL.md +46 -77
  110. package/skills/write-plan/references/plan-shapes.md +5 -5
  111. package/skills/write-spec/SKILL.md +51 -63
  112. package/skills/write-spec/references/artifact-standards.md +2 -2
  113. package/skills/write-spec/references/decision-records.md +3 -2
  114. package/skills/write-spec/references/spec-shapes.md +15 -0
  115. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  116. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  117. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  118. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  119. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  120. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  121. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  122. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  123. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  124. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  125. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  126. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  127. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  128. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  129. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  130. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  131. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  132. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  133. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  134. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  135. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  136. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  137. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  138. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  139. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  140. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  141. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  142. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  143. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  144. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  145. package/skills/tdd/SKILL.md +0 -143
  146. package/skills/tdd/references/test-design.md +0 -96
  147. /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { resolveCognitiveRoutingState } from "../cognitive-routing/runtime.js";
5
- import { supportsCognitiveRoutingModelRegistry } from "../cognitive-routing/host.js";
4
+ import { resolveCognitiveRoutingState } from "../cognitive-routing-v2/config.js";
5
+ import { supportsCognitiveRoutingModelRegistry } from "../cognitive-routing-v2/config.js";
6
6
  export const WORKFLOW_COMMANDS = [
7
7
  { command: "discuss", skill: "discuss" },
8
8
  { command: "action-selection", skill: "action-selection" },
@@ -43,7 +43,6 @@ export const FREEFLOW_MODEL_SKILL_NAMES = [
43
43
  "setup-freeflow",
44
44
  "launch-work",
45
45
  "simplify-code",
46
- "tdd",
47
46
  "track-work",
48
47
  "verify-work",
49
48
  "workflow",
@@ -51,6 +50,12 @@ export const FREEFLOW_MODEL_SKILL_NAMES = [
51
50
  "write-skill",
52
51
  "write-spec",
53
52
  ];
53
+ export const STABLE_FREEFLOW_SURFACE = Object.freeze({
54
+ enabled: true,
55
+ cognitiveRouting: { effective: true },
56
+ contextVirtualization: { effective: true },
57
+ conversationHistory: { effective: true },
58
+ });
54
59
  export const FREEFLOW_CAPABILITY_SKILL_NAMES = ["cognitive-routing", "context-virtualization", "conversation-history"];
55
60
  export function freeflowSkillPath(skillName) {
56
61
  return fileURLToPath(new URL(`../../../skills/${skillName}/SKILL.md`, import.meta.url));
@@ -74,7 +79,6 @@ export function freeflowModelSkillPaths(capabilityState = undefined) {
74
79
  }
75
80
  const SESSION_OVERRIDES_ENTRY = "freeflow-session-overrides";
76
81
  const SESSION_CORE_KEYS = new Set(["enabled", "contextVirtualization", "conversationHistory"]);
77
- export const COGNITIVE_ROUTING_SWITCH_TOOL_NAME = "freeflow_switch_profile";
78
82
  export const FREEFLOW_RUNTIME_STATE_MESSAGE_TYPE = "freeflow-runtime-state";
79
83
  export const COGNITIVE_ROUTING_RUNTIME_STATE_MESSAGE_TYPE = "freeflow-cognitive-routing-runtime-state";
80
84
  export const WORKFLOW_BOOTSTRAP_MESSAGE_TYPE = "freeflow-workflow-bootstrap";
@@ -85,6 +89,30 @@ let currentSessionOverrides = {};
85
89
  export function isPromptAvailable(value) {
86
90
  return typeof value === "string" && value.trim().length > 0;
87
91
  }
92
+ // pi-subagents stamps child system prompts with this tag before binding extensions.
93
+ // Keeping detection at the prompt boundary avoids a process-global child flag.
94
+ export const SUBAGENT_AGENT_TAG = /<active_agent\s+name="[^"]+"\s*\/>/;
95
+ export const FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER = "<!-- freeflow-subagent-capabilities: disabled -->";
96
+ const SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE = "Optional Freeflow capabilities are disabled for subagents.";
97
+ export function isSubagentContext(context) {
98
+ try {
99
+ const systemPrompt = context?.getSystemPrompt?.();
100
+ return (
101
+ typeof systemPrompt === "string" &&
102
+ (SUBAGENT_AGENT_TAG.test(systemPrompt) || systemPrompt.includes(FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER))
103
+ );
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+ function disableSubagentCapability(capability) {
109
+ return {
110
+ ...capability,
111
+ enabled: false,
112
+ effective: false,
113
+ blockingReason: { code: "disabled", message: SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE },
114
+ };
115
+ }
88
116
  export function hasUsableMandatoryPrompts(freeflowContext) {
89
117
  return (
90
118
  isPromptAvailable(freeflowContext?.corePrompt) && isPromptAvailable(freeflowContext?.interactionContractPrompt)
@@ -380,7 +408,8 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
380
408
  const enabled = layers.configured && effectiveCore.config.enabled;
381
409
  const contextVirtualizationConfigEnabled = effectiveCore.config.contextVirtualization;
382
410
  const conversationHistoryConfigEnabled = effectiveCore.config.conversationHistory;
383
- const hostSupportsCognitiveRouting = supportsCognitiveRoutingModelRegistry(host);
411
+ const subagentContext = isSubagentContext(host);
412
+ const hostSupportsCognitiveRouting = !subagentContext && supportsCognitiveRoutingModelRegistry(host);
384
413
  const configuredCognitiveRouting = await resolveCognitiveRoutingState(
385
414
  layers.repository.parsed,
386
415
  layers.local.parsed,
@@ -400,7 +429,7 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
400
429
  effective: false,
401
430
  blockingReason: disabledReason,
402
431
  };
403
- return {
432
+ const capabilityState = {
404
433
  configured: layers.configured,
405
434
  repositoryConfigured: layers.repositoryConfigured,
406
435
  configExists: layers.repository.exists,
@@ -421,6 +450,13 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
421
450
  hostSupportsCognitiveRouting,
422
451
  cognitiveRouting,
423
452
  };
453
+ if (!subagentContext) return capabilityState;
454
+ return {
455
+ ...capabilityState,
456
+ contextVirtualization: disableSubagentCapability(capabilityState.contextVirtualization),
457
+ conversationHistory: disableSubagentCapability(capabilityState.conversationHistory),
458
+ cognitiveRouting: disableSubagentCapability(capabilityState.cognitiveRouting),
459
+ };
424
460
  }
425
461
  export const readRuntimeState = readCapabilityState;
426
462
  export function restoreSessionOverrides(ctx) {
@@ -468,11 +504,7 @@ export function setFreeflowStatus(
468
504
  active.push(`cognitive blocked · ${cognitiveRoutingRuntime?.runtimeReason ?? "runtime_blocked"}`);
469
505
  } else if (cognitiveRoutingActive) {
470
506
  const profile = cognitiveRoutingRuntime.activeProfile;
471
- const control =
472
- cognitiveRoutingRuntime.controlMode === "manual-standard" ||
473
- cognitiveRoutingRuntime.controlMode === "manual-reasoning"
474
- ? "manual hold"
475
- : "automatic";
507
+ const control = String(cognitiveRoutingRuntime.controlMode).startsWith("manual-") ? "manual hold" : "automatic";
476
508
  active.push(`${profile} · ${control}`);
477
509
  } else if (cognitiveRouting?.enabled === true) {
478
510
  if (cognitiveRouting.blockingReason?.code === "runtime_disabled") {
@@ -485,8 +517,8 @@ export function setFreeflowStatus(
485
517
  ) {
486
518
  const startupProfile =
487
519
  cognitiveRouting.sessionStart?.control === "manual"
488
- ? (cognitiveRouting.sessionStart.profile ?? "reasoning")
489
- : "reasoning";
520
+ ? (cognitiveRouting.sessionStart.profile ?? "coordinator")
521
+ : "coordinator";
490
522
  active.push(`${startupProfile} · pending`);
491
523
  } else {
492
524
  const reason =
@@ -507,12 +539,12 @@ export function skillPrompt(skill, args) {
507
539
  }
508
540
  function publicCognitiveRoutingControl(controlMode) {
509
541
  if (controlMode === "automatic") return "automatic";
510
- if (controlMode === "manual-standard" || controlMode === "manual-reasoning") return "manual";
542
+ if (["manual-helper", "manual-executor", "manual-coordinator"].includes(controlMode)) return "manual";
511
543
  return "unavailable";
512
544
  }
513
545
  function publicCognitiveRoutingProfile(activeProfile, effective) {
514
546
  if (effective !== true) return "unavailable";
515
- return activeProfile === "standard" || activeProfile === "reasoning" ? activeProfile : "unavailable";
547
+ return ["helper", "executor", "coordinator"].includes(activeProfile) ? activeProfile : "unavailable";
516
548
  }
517
549
  function publicCapabilityStatus(capability) {
518
550
  if (capability?.effective === true) return "active";
@@ -525,10 +557,20 @@ function publicCognitiveRoutingStatus(capability, runtime) {
525
557
  if (runtime?.runtimeStatus === "inactive") return "inactive";
526
558
  return publicCapabilityStatus(capability);
527
559
  }
560
+ function publicCognitiveRoutingProjectionMode(capability, runtime, projectionFailure) {
561
+ if (capability?.projection !== true) return "disabled";
562
+ if (capability?.effective !== true) return "unavailable";
563
+ if (String(runtime?.controlMode).startsWith("manual-")) return "manual-bypass";
564
+ if (projectionFailure) return "blocked";
565
+ if (runtime?.runtimeStatus === "inactive" || runtime?.runtimeStatus === "blocked") return "unavailable";
566
+ if (runtime?.effective === true && runtime.controlMode === "automatic") return "enabled";
567
+ return "pending";
568
+ }
528
569
  export function freeflowRuntimeStateMessage(
529
570
  capabilityState,
530
571
  cognitiveRoutingRuntime = undefined,
531
572
  freeflowContext = undefined,
573
+ options = {},
532
574
  ) {
533
575
  const cognitiveRoutingEffective = capabilityState?.cognitiveRouting?.effective === true;
534
576
  const profile = publicCognitiveRoutingProfile(
@@ -537,6 +579,11 @@ export function freeflowRuntimeStateMessage(
537
579
  );
538
580
  const control =
539
581
  profile === "unavailable" ? "unavailable" : publicCognitiveRoutingControl(cognitiveRoutingRuntime?.controlMode);
582
+ const projectionMode = publicCognitiveRoutingProjectionMode(
583
+ capabilityState?.cognitiveRouting,
584
+ cognitiveRoutingRuntime,
585
+ options.projectionFailure,
586
+ );
540
587
  const mandatoryPromptAvailable = capabilityState?.enabled !== true || hasUsableMandatoryPrompts(freeflowContext);
541
588
  const freeflowStatus = capabilityState?.configured
542
589
  ? capabilityState.enabled
@@ -565,6 +612,8 @@ export function freeflowRuntimeStateMessage(
565
612
  "Cognitive Routing:",
566
613
  `- Control: \`${control}\``,
567
614
  `- Profile: \`${profile}\``,
615
+ `- Delegation: \`${capabilityState?.cognitiveRouting?.delegation ?? "executor"}\``,
616
+ `- Projection: \`${projectionMode}\``,
568
617
  ].join("\n"),
569
618
  display: false,
570
619
  details: { source: "provider-request-runtime-state" },
@@ -596,7 +645,9 @@ export function withFreeflowRuntimeState(
596
645
  options = {},
597
646
  ) {
598
647
  const source = Array.isArray(messages) ? messages : [];
599
- const runtimeState = freeflowRuntimeStateMessage(capabilityState, cognitiveRoutingRuntime, freeflowContext);
648
+ const runtimeState = freeflowRuntimeStateMessage(capabilityState, cognitiveRoutingRuntime, freeflowContext, {
649
+ projectionFailure: options.projectionFailure,
650
+ });
600
651
  const runtimeStateMessages = source.filter(
601
652
  (message) =>
602
653
  message?.customType === FREEFLOW_RUNTIME_STATE_MESSAGE_TYPE ||
@@ -614,6 +665,24 @@ export function withFreeflowRuntimeState(
614
665
  if (unchanged) return source;
615
666
  return insertRuntimeStateBeforeLatestUser(withoutRuntimeState, runtimeState);
616
667
  }
668
+ export function stableRuntimeContext(context) {
669
+ const sections = [
670
+ ["Freeflow core", context?.corePrompt],
671
+ ["Freeflow core", context?.interactionContractPrompt],
672
+ ["Cognitive Routing", context?.cognitiveRoutingPrompt],
673
+ ["Context Virtualization", context?.contextVirtualizationPrompt],
674
+ ["Conversation History", context?.conversationHistoryPrompt],
675
+ ];
676
+ return [
677
+ "# Freeflow availability contract",
678
+ "Freeflow keeps a fixed reference catalog of its instructions, skills, and tools to preserve prompt caching. Presence in this catalog does not mean a feature is active or an operation is permitted.",
679
+ "Apply the following guidance and Freeflow skills only when the latest extension-generated Freeflow Runtime State marks the corresponding feature active. When Freeflow is inactive, unavailable, or not configured, its core and capability guidance is dormant; do not bootstrap or follow it merely because it is listed. Explicit user instructions retain their normal authority.",
680
+ "For Cognitive Routing, follow the latest control/profile/responsibility snapshots on this branch. Earlier snapshots and notices are historical; they grant no current permission. Tool permissions are checked by the runtime, even though all definitions remain visible. Never call a disabled operation.",
681
+ ...sections
682
+ .filter(([, text]) => isPromptAvailable(text))
683
+ .map(([feature, text]) => `## Reference guidance: ${feature}\n\n${text.trim()}`),
684
+ ].join("\n\n");
685
+ }
617
686
  export function runtimeContext(freeflowContext, capabilityState) {
618
687
  if (
619
688
  capabilityState?.configured !== true ||
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { activeReadOnlySessionBranch, readOnlySessionSnapshot, ReadOnlySessionError } from "./read-only-session.js";
3
+ import {
4
+ ROUTING_ENTRY,
5
+ RoutingError,
6
+ canonical,
7
+ eventKey,
8
+ eventValue,
9
+ requireCondition as check,
10
+ } from "../cognitive-routing-v2/types.js";
11
+ import { parseRoutingEvent, reduce, replay } from "../cognitive-routing-v2/state.js";
12
+ export class EventStore {
13
+ pi;
14
+ reader;
15
+ observed = new Map();
16
+ attempted = new Map();
17
+ fault;
18
+ ready = false;
19
+ cachedEntries = [];
20
+ cachedState = replay([]);
21
+ constructor(pi, reader) {
22
+ this.pi = pi;
23
+ this.reader = reader;
24
+ }
25
+ get blocked() {
26
+ return this.fault ?? (!this.ready ? "acknowledgment_unclassified" : undefined);
27
+ }
28
+ state() {
29
+ const entries = this.reader.getBranch();
30
+ const prefix =
31
+ this.cachedEntries.length <= entries.length && this.cachedEntries.every((entry, i) => entry === entries[i]);
32
+ if (!prefix) {
33
+ this.cachedState = replay(entries);
34
+ } else {
35
+ let state = this.cachedState;
36
+ for (const entry of entries.slice(this.cachedEntries.length))
37
+ if (entry.type === "custom" && entry.customType === ROUTING_ENTRY)
38
+ state = reduce(state, parseRoutingEvent(entry.data));
39
+ this.cachedState = state;
40
+ }
41
+ this.cachedEntries = [...entries];
42
+ return this.cachedState;
43
+ }
44
+ block(reason) {
45
+ this.fault = reason;
46
+ this.ready = false;
47
+ }
48
+ make(data, operationId = randomUUID(), stepId = data.type) {
49
+ return parseRoutingEvent({
50
+ version: 2,
51
+ eventId: randomUUID(),
52
+ operationId,
53
+ stepId,
54
+ recordedSessionId: this.reader.getSessionId(),
55
+ data,
56
+ });
57
+ }
58
+ async reconcile() {
59
+ const acknowledged = this.ready && !this.fault;
60
+ this.ready = false;
61
+ const branch = this.reader.getBranch();
62
+ const live = branch.filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
63
+ try {
64
+ const ids = new Set();
65
+ let parent = null;
66
+ for (const entry of branch) {
67
+ check(
68
+ typeof entry.id === "string" && !ids.has(entry.id) && entry.parentId === parent,
69
+ "invalid_native_ancestry",
70
+ );
71
+ ids.add(entry.id);
72
+ parent = entry.id;
73
+ }
74
+ check(parent === this.reader.getLeafId(), "invalid_native_ancestry");
75
+ if (
76
+ acknowledged &&
77
+ live.every((entry) => {
78
+ const event = parseRoutingEvent(entry.data);
79
+ const receipt = this.observed.get(eventKey(event));
80
+ return (
81
+ receipt?.entryId === entry.id && receipt.eventId === event.eventId && receipt.value === eventValue(event)
82
+ );
83
+ })
84
+ ) {
85
+ this.ready = true;
86
+ return;
87
+ }
88
+ if (!live.length && !this.attempted.size) {
89
+ this.observed.clear();
90
+ this.fault = undefined;
91
+ this.ready = true;
92
+ return;
93
+ }
94
+ // Pi defers creating a new session file until its first assistant message.
95
+ // Before that flush boundary, the complete in-memory native session is the
96
+ // only available source; uncertain appends still require persisted readback.
97
+ const preFlush =
98
+ !this.attempted.size &&
99
+ !this.reader.getEntries().some((entry) => entry.type === "message" && entry.message?.role === "assistant");
100
+ if (preFlush) {
101
+ replay(branch);
102
+ const known = new Map();
103
+ for (const entry of branch) {
104
+ if (entry.type === "custom" && entry.customType === ROUTING_ENTRY) {
105
+ const event = parseRoutingEvent(entry.data);
106
+ known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
107
+ }
108
+ }
109
+ this.observed = known;
110
+ this.fault = undefined;
111
+ this.ready = true;
112
+ this.cachedEntries = [];
113
+ this.cachedState = replay([]);
114
+ return;
115
+ }
116
+ const path = this.reader.getSessionFile();
117
+ check(path, "persisted_snapshot_unavailable", "Existing or uncertain routing state needs a persisted snapshot.");
118
+ const snapshot = await readOnlySessionSnapshot(path);
119
+ check(snapshot.sessionId === this.reader.getSessionId(), "session_identity_changed");
120
+ const persistedBranch = activeReadOnlySessionBranch(snapshot, this.reader.getLeafId());
121
+ check(
122
+ persistedBranch.length === branch.length && persistedBranch.every((e, i) => e.id === branch[i].id),
123
+ "snapshot_ancestry_mismatch",
124
+ "Live branch is not the complete persisted ancestry for its claimed leaf.",
125
+ );
126
+ replay(persistedBranch);
127
+ const disk = new Map(persistedBranch.map((e) => [e.id, e]));
128
+ const known = new Map();
129
+ // Compare the live selected ancestry, not whichever branch was last written on disk.
130
+ for (const entry of branch) {
131
+ const captured = disk.get(entry.id);
132
+ check(
133
+ captured && canonical(captured) === canonical(entry),
134
+ "snapshot_divergence",
135
+ "Live ancestry is not fully established by the fresh persisted snapshot; reconcile the session before retrying.",
136
+ );
137
+ if (entry.type === "custom" && entry.customType === ROUTING_ENTRY) {
138
+ const event = parseRoutingEvent(entry.data);
139
+ known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
140
+ }
141
+ }
142
+ for (const [key, event] of this.attempted)
143
+ check(
144
+ known.get(key)?.value === eventValue(event) && known.get(key)?.eventId === event.eventId,
145
+ "append_acknowledgment_uncertain",
146
+ "The attempted routing event is not established by persisted readback.",
147
+ );
148
+ this.observed = known;
149
+ this.attempted.clear();
150
+ this.fault = undefined;
151
+ this.ready = true;
152
+ this.cachedEntries = [];
153
+ this.cachedState = replay([]);
154
+ } catch (error) {
155
+ this.block(
156
+ error instanceof RoutingError
157
+ ? error.code
158
+ : error instanceof ReadOnlySessionError
159
+ ? `${error.code}: ${error.message}`
160
+ : "snapshot_unavailable",
161
+ );
162
+ throw error;
163
+ }
164
+ }
165
+ append(event) {
166
+ const value = parseRoutingEvent(event),
167
+ key = eventKey(value),
168
+ body = eventValue(value);
169
+ if (this.blocked) throw new RoutingError("acknowledgment_uncertain", this.blocked);
170
+ const prior = this.state().events.get(key);
171
+ if (prior) {
172
+ check(eventValue(prior) === body, "operation_conflict");
173
+ const occurrence = this.reader
174
+ .getBranch()
175
+ .find(
176
+ (entry) =>
177
+ entry.type === "custom" && entry.customType === ROUTING_ENTRY && entry.data?.eventId === prior.eventId,
178
+ );
179
+ if (
180
+ this.observed.get(key)?.value !== body ||
181
+ this.observed.get(key)?.eventId !== prior.eventId ||
182
+ this.observed.get(key)?.entryId !== occurrence?.id
183
+ ) {
184
+ this.block("prior_acknowledgment_unclassified");
185
+ throw new RoutingError("acknowledgment_uncertain", this.blocked);
186
+ }
187
+ return prior;
188
+ }
189
+ // Validate the whole state transition before mutating Pi's session memory.
190
+ const candidate = reduce(this.state(), value);
191
+ const baseline = this.cachedEntries;
192
+ this.attempted.set(key, value);
193
+ try {
194
+ this.pi.appendEntry(ROUTING_ENTRY, value);
195
+ const found = this.reader
196
+ .getBranch()
197
+ .find((e) => e.type === "custom" && e.customType === ROUTING_ENTRY && e.data?.eventId === value.eventId);
198
+ check(found && eventValue(parseRoutingEvent(found.data)) === body, "append_not_observed");
199
+ this.observed.set(key, { value: body, eventId: value.eventId, entryId: found.id });
200
+ this.attempted.delete(key);
201
+ const branch = this.reader.getBranch();
202
+ const delta = branch.slice(baseline.length).filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
203
+ if (baseline.every((e, i) => branch[i] === e) && delta.length === 1 && delta[0].id === found.id) {
204
+ // Publish the already validated candidate only after acknowledged native append.
205
+ this.cachedState = candidate;
206
+ this.cachedEntries = [...branch];
207
+ }
208
+ return value;
209
+ } catch (error) {
210
+ this.block("append_acknowledgment_uncertain");
211
+ throw new RoutingError(
212
+ "acknowledgment_uncertain",
213
+ error instanceof Error ? error.message : "Pi append/readback did not acknowledge the event.",
214
+ );
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,32 @@
1
+ import { isWorkerProfile } from "../cognitive-routing-v2/types.js";
2
+ import { isTaskEvidence, textRef } from "./sources.js";
3
+ export function annotateSources(messages, renderedSources, full, sources, instance) {
4
+ const annotated = [];
5
+ for (let i = 0; i < messages.length;) {
6
+ const group = [messages[i++]];
7
+ while (i < messages.length && messages[i].role === "toolResult") group.push(messages[i++]);
8
+ const rows = group.flatMap((message) => {
9
+ const s = renderedSources.get(message);
10
+ if (!s) return [];
11
+ const representation = full.has(s.ref) ? "full" : "structural only; omitted result bodies are not evidence";
12
+ const selection =
13
+ isWorkerProfile(s.producer) && isTaskEvidence(s) && full.has(s.ref)
14
+ ? "task evidence; selection checks apply"
15
+ : "not offered for new evidence selection";
16
+ return [
17
+ `${s.ref} | producer: ${s.producer === "common" ? "unknown/common (no observed routing profile)" : s.producer} | ${s.original ? "assistant-text" : s.message.role}${s.message.toolName ? ` | ${s.message.toolName}` : ""}${s.assignmentId ? ` | assignment: ${s.assignmentId}` : ""} | ${representation} | ${selection}${!s.original && full.has(s.ref) && sources.byRef.has(textRef(s.ref)) ? ` | visible text: ${textRef(s.ref)}` : ""}`,
18
+ ];
19
+ });
20
+ annotated.push(...group);
21
+ if (rows.length)
22
+ annotated.push({
23
+ role: "custom",
24
+ customType: "freeflow-routing-v2-refs",
25
+ display: false,
26
+ content: `Source provenance for the preceding message/exchange:\n${rows.join("\n")}`,
27
+ details: { routingInstance: instance },
28
+ timestamp: 0,
29
+ });
30
+ }
31
+ return annotated;
32
+ }
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+ import { open, stat } from "node:fs/promises";
3
+ import { TextDecoder } from "node:util";
4
+ export class ReadOnlySessionError extends Error {
5
+ code;
6
+ line;
7
+ constructor(code, message, line) {
8
+ super(message);
9
+ this.code = code;
10
+ this.line = line;
11
+ this.name = "ReadOnlySessionError";
12
+ }
13
+ }
14
+ export const SESSION_READ_LIMITS = {
15
+ maxBytes: 512 * 1024 * 1024,
16
+ maxEntryBytes: 128 * 1024 * 1024,
17
+ maxEntries: 250000,
18
+ };
19
+ function decodeEntry(text, line, seen) {
20
+ let value;
21
+ try {
22
+ value = JSON.parse(text);
23
+ } catch {
24
+ throw new ReadOnlySessionError("invalid_json", "Incomplete or invalid session JSON.", line);
25
+ }
26
+ if (!value || Array.isArray(value) || typeof value !== "object" || typeof value.type !== "string")
27
+ throw new ReadOnlySessionError("invalid_entry", "Invalid session entry.", line);
28
+ if (line === 1) {
29
+ if (
30
+ value.type !== "session" ||
31
+ typeof value.id !== "string" ||
32
+ !value.id ||
33
+ ![2, 3].includes(value.version) ||
34
+ typeof value.cwd !== "string"
35
+ )
36
+ throw new ReadOnlySessionError("invalid_header", "A supported native session header is required.", line);
37
+ } else {
38
+ if (value.type === "session" || typeof value.id !== "string" || !value.id || seen.has(value.id))
39
+ throw new ReadOnlySessionError("invalid_id", "Missing, duplicate, or misplaced session identity.", line);
40
+ if (value.parentId !== null && (typeof value.parentId !== "string" || !seen.has(value.parentId)))
41
+ throw new ReadOnlySessionError("invalid_parent", "Parent is absent or occurs after its child.", line);
42
+ seen.add(value.id);
43
+ }
44
+ return value;
45
+ }
46
+ export function parseReadOnlySessionText(text) {
47
+ const lines = text.split("\n");
48
+ if (lines.at(-1) === "") lines.pop();
49
+ if (!lines.length || !lines[0]) throw new ReadOnlySessionError("empty", "Session snapshot is empty.");
50
+ const seen = new Set();
51
+ return lines.map((line, i) => decodeEntry(line, i + 1, seen));
52
+ }
53
+ export async function readOnlySessionSnapshot(path, options = {}) {
54
+ const limits = { ...SESSION_READ_LIMITS, ...options };
55
+ if (Object.values(limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
56
+ throw new ReadOnlySessionError("invalid_limit", "Invalid snapshot limit.");
57
+ let file;
58
+ try {
59
+ file = await open(path, "r");
60
+ const before = await file.stat({ bigint: true });
61
+ if (!before.isFile()) throw new ReadOnlySessionError("read_limit", "Session is not a regular file.");
62
+ if (before.size > BigInt(limits.maxBytes))
63
+ throw new ReadOnlySessionError(
64
+ "read_limit",
65
+ `Session exceeds the supported ${limits.maxBytes} byte snapshot limit.`,
66
+ );
67
+ const hash = createHash("sha256"),
68
+ entries = [],
69
+ seen = new Set();
70
+ let total = 0,
71
+ lineBytes = 0,
72
+ parts = [];
73
+ const append = (part) => {
74
+ lineBytes += part.length;
75
+ if (lineBytes > limits.maxEntryBytes)
76
+ throw new ReadOnlySessionError(
77
+ "entry_limit",
78
+ `Session entry exceeds ${limits.maxEntryBytes} bytes.`,
79
+ entries.length + 1,
80
+ );
81
+ if (part.length) parts.push(part);
82
+ };
83
+ const decode = () => {
84
+ if (entries.length >= limits.maxEntries)
85
+ throw new ReadOnlySessionError("entry_count_limit", `Session exceeds ${limits.maxEntries} entries.`);
86
+ let text;
87
+ try {
88
+ text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(parts, lineBytes));
89
+ } catch {
90
+ throw new ReadOnlySessionError("invalid_encoding", "Session is not complete UTF-8.", entries.length + 1);
91
+ }
92
+ entries.push(decodeEntry(text, entries.length + 1, seen));
93
+ parts = [];
94
+ lineBytes = 0;
95
+ };
96
+ for (;;) {
97
+ const buffer = Buffer.alloc(64 * 1024),
98
+ { bytesRead } = await file.read(buffer, 0, buffer.length, null);
99
+ if (!bytesRead) break;
100
+ total += bytesRead;
101
+ if (total > limits.maxBytes)
102
+ throw new ReadOnlySessionError("read_limit", "Session grew beyond the supported snapshot limit.");
103
+ const bytes = buffer.subarray(0, bytesRead);
104
+ hash.update(bytes);
105
+ let start = 0,
106
+ end;
107
+ while ((end = bytes.indexOf(10, start)) !== -1) {
108
+ append(bytes.subarray(start, end));
109
+ decode();
110
+ start = end + 1;
111
+ }
112
+ append(bytes.subarray(start));
113
+ }
114
+ if (lineBytes) decode();
115
+ if (!entries.length) throw new ReadOnlySessionError("empty", "Session snapshot is empty.");
116
+ const after = await file.stat({ bigint: true }),
117
+ current = await stat(path, { bigint: true });
118
+ const signature = (v) => [v.dev, v.ino, v.size, v.mtimeNs, v.ctimeNs].join(":");
119
+ if (
120
+ signature(before) !== signature(after) ||
121
+ signature(after) !== signature(current) ||
122
+ after.size !== BigInt(total)
123
+ )
124
+ throw new ReadOnlySessionError("source_changed", "Session changed during snapshot capture.");
125
+ return { path, sessionId: entries[0].id, version: entries[0].version, hash: hash.digest("hex"), entries };
126
+ } catch (error) {
127
+ if (error instanceof ReadOnlySessionError) throw error;
128
+ throw new ReadOnlySessionError("read_failed", error instanceof Error ? error.message : String(error));
129
+ } finally {
130
+ await file?.close();
131
+ }
132
+ }
133
+ export function activeReadOnlySessionBranch(snapshot, leafId) {
134
+ if (leafId === null) return [];
135
+ const byId = new Map(snapshot.entries.slice(1).map((entry) => [entry.id, entry]));
136
+ const path = [];
137
+ const seen = new Set();
138
+ let cursor = leafId;
139
+ while (cursor !== null) {
140
+ const entry = byId.get(cursor);
141
+ if (!entry || seen.has(cursor))
142
+ throw new ReadOnlySessionError("ancestry_unavailable", "Requested native ancestry is unavailable.");
143
+ seen.add(cursor);
144
+ path.push(entry);
145
+ cursor = entry.parentId;
146
+ }
147
+ return path.reverse();
148
+ }