jfl 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/dist/commands/doctor.d.ts +1 -0
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +30 -1
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/ide.d.ts +2 -1
  6. package/dist/commands/ide.d.ts.map +1 -1
  7. package/dist/commands/ide.js +60 -1
  8. package/dist/commands/ide.js.map +1 -1
  9. package/dist/commands/init-from-service.d.ts +15 -0
  10. package/dist/commands/init-from-service.d.ts.map +1 -0
  11. package/dist/commands/init-from-service.js +541 -0
  12. package/dist/commands/init-from-service.js.map +1 -0
  13. package/dist/commands/init.d.ts +1 -0
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +32 -1
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/kanban.d.ts.map +1 -1
  18. package/dist/commands/kanban.js +13 -4
  19. package/dist/commands/kanban.js.map +1 -1
  20. package/dist/commands/linear.d.ts +41 -0
  21. package/dist/commands/linear.d.ts.map +1 -0
  22. package/dist/commands/linear.js +715 -0
  23. package/dist/commands/linear.js.map +1 -0
  24. package/dist/commands/peter.d.ts.map +1 -1
  25. package/dist/commands/peter.js +232 -25
  26. package/dist/commands/peter.js.map +1 -1
  27. package/dist/commands/services.d.ts.map +1 -1
  28. package/dist/commands/services.js +146 -0
  29. package/dist/commands/services.js.map +1 -1
  30. package/dist/commands/setup.d.ts.map +1 -1
  31. package/dist/commands/setup.js +173 -13
  32. package/dist/commands/setup.js.map +1 -1
  33. package/dist/commands/telemetry-monitor.d.ts +11 -0
  34. package/dist/commands/telemetry-monitor.d.ts.map +1 -0
  35. package/dist/commands/telemetry-monitor.js +224 -0
  36. package/dist/commands/telemetry-monitor.js.map +1 -0
  37. package/dist/commands/telemetry-test.d.ts +11 -0
  38. package/dist/commands/telemetry-test.d.ts.map +1 -0
  39. package/dist/commands/telemetry-test.js +67 -0
  40. package/dist/commands/telemetry-test.js.map +1 -0
  41. package/dist/commands/tenet-agents.d.ts +13 -0
  42. package/dist/commands/tenet-agents.d.ts.map +1 -0
  43. package/dist/commands/tenet-agents.js +191 -0
  44. package/dist/commands/tenet-agents.js.map +1 -0
  45. package/dist/commands/tenet-setup.d.ts +19 -0
  46. package/dist/commands/tenet-setup.d.ts.map +1 -0
  47. package/dist/commands/tenet-setup.js +131 -0
  48. package/dist/commands/tenet-setup.js.map +1 -0
  49. package/dist/commands/train.d.ts +18 -0
  50. package/dist/commands/train.d.ts.map +1 -1
  51. package/dist/commands/train.js +182 -0
  52. package/dist/commands/train.js.map +1 -1
  53. package/dist/commands/whoami.d.ts +2 -0
  54. package/dist/commands/whoami.d.ts.map +1 -0
  55. package/dist/commands/whoami.js +24 -0
  56. package/dist/commands/whoami.js.map +1 -0
  57. package/dist/index.js +159 -10
  58. package/dist/index.js.map +1 -1
  59. package/dist/lib/advanced-setup.d.ts +78 -0
  60. package/dist/lib/advanced-setup.d.ts.map +1 -0
  61. package/dist/lib/advanced-setup.js +433 -0
  62. package/dist/lib/advanced-setup.js.map +1 -0
  63. package/dist/lib/agent-config.d.ts +33 -0
  64. package/dist/lib/agent-config.d.ts.map +1 -1
  65. package/dist/lib/agent-config.js +26 -0
  66. package/dist/lib/agent-config.js.map +1 -1
  67. package/dist/lib/counterfactual-training-bridge.d.ts +114 -0
  68. package/dist/lib/counterfactual-training-bridge.d.ts.map +1 -0
  69. package/dist/lib/counterfactual-training-bridge.js +322 -0
  70. package/dist/lib/counterfactual-training-bridge.js.map +1 -0
  71. package/dist/lib/discovery-agent.d.ts +48 -0
  72. package/dist/lib/discovery-agent.d.ts.map +1 -0
  73. package/dist/lib/discovery-agent.js +111 -0
  74. package/dist/lib/discovery-agent.js.map +1 -0
  75. package/dist/lib/flow-engine.d.ts.map +1 -1
  76. package/dist/lib/flow-engine.js +46 -8
  77. package/dist/lib/flow-engine.js.map +1 -1
  78. package/dist/lib/gtm-generator.d.ts +29 -0
  79. package/dist/lib/gtm-generator.d.ts.map +1 -0
  80. package/dist/lib/gtm-generator.js +252 -0
  81. package/dist/lib/gtm-generator.js.map +1 -0
  82. package/dist/lib/hub-health.d.ts +40 -0
  83. package/dist/lib/hub-health.d.ts.map +1 -0
  84. package/dist/lib/hub-health.js +89 -0
  85. package/dist/lib/hub-health.js.map +1 -0
  86. package/dist/lib/invariant-monitor.d.ts +6 -2
  87. package/dist/lib/invariant-monitor.d.ts.map +1 -1
  88. package/dist/lib/invariant-monitor.js +89 -2
  89. package/dist/lib/invariant-monitor.js.map +1 -1
  90. package/dist/lib/journal-analyzer.d.ts +71 -0
  91. package/dist/lib/journal-analyzer.d.ts.map +1 -0
  92. package/dist/lib/journal-analyzer.js +306 -0
  93. package/dist/lib/journal-analyzer.js.map +1 -0
  94. package/dist/lib/linear-client.d.ts +73 -0
  95. package/dist/lib/linear-client.d.ts.map +1 -0
  96. package/dist/lib/linear-client.js +112 -0
  97. package/dist/lib/linear-client.js.map +1 -0
  98. package/dist/lib/linear-id-map.d.ts +20 -0
  99. package/dist/lib/linear-id-map.d.ts.map +1 -0
  100. package/dist/lib/linear-id-map.js +57 -0
  101. package/dist/lib/linear-id-map.js.map +1 -0
  102. package/dist/lib/linear-kanban.d.ts +66 -0
  103. package/dist/lib/linear-kanban.d.ts.map +1 -0
  104. package/dist/lib/linear-kanban.js +175 -0
  105. package/dist/lib/linear-kanban.js.map +1 -0
  106. package/dist/lib/onboarding.d.ts +40 -0
  107. package/dist/lib/onboarding.d.ts.map +1 -0
  108. package/dist/lib/onboarding.js +213 -0
  109. package/dist/lib/onboarding.js.map +1 -0
  110. package/dist/lib/physical-world-model.d.ts +50 -0
  111. package/dist/lib/physical-world-model.d.ts.map +1 -0
  112. package/dist/lib/physical-world-model.js +251 -0
  113. package/dist/lib/physical-world-model.js.map +1 -0
  114. package/dist/lib/planning-loop.d.ts +157 -0
  115. package/dist/lib/planning-loop.d.ts.map +1 -0
  116. package/dist/lib/planning-loop.js +537 -0
  117. package/dist/lib/planning-loop.js.map +1 -0
  118. package/dist/lib/policy-head.d.ts +13 -0
  119. package/dist/lib/policy-head.d.ts.map +1 -1
  120. package/dist/lib/policy-head.js +168 -2
  121. package/dist/lib/policy-head.js.map +1 -1
  122. package/dist/lib/resource-optimizer-middleware.d.ts +39 -0
  123. package/dist/lib/resource-optimizer-middleware.d.ts.map +1 -0
  124. package/dist/lib/resource-optimizer-middleware.js +222 -0
  125. package/dist/lib/resource-optimizer-middleware.js.map +1 -0
  126. package/dist/lib/resource-optimizer.d.ts +71 -0
  127. package/dist/lib/resource-optimizer.d.ts.map +1 -0
  128. package/dist/lib/resource-optimizer.js +228 -0
  129. package/dist/lib/resource-optimizer.js.map +1 -0
  130. package/dist/lib/rl-manager.d.ts +74 -0
  131. package/dist/lib/rl-manager.d.ts.map +1 -0
  132. package/dist/lib/rl-manager.js +244 -0
  133. package/dist/lib/rl-manager.js.map +1 -0
  134. package/dist/lib/service-analyzer.d.ts +76 -0
  135. package/dist/lib/service-analyzer.d.ts.map +1 -0
  136. package/dist/lib/service-analyzer.js +704 -0
  137. package/dist/lib/service-analyzer.js.map +1 -0
  138. package/dist/lib/service-gtm.js +2 -2
  139. package/dist/lib/service-gtm.js.map +1 -1
  140. package/dist/lib/service-questionnaire.d.ts +11 -0
  141. package/dist/lib/service-questionnaire.d.ts.map +1 -0
  142. package/dist/lib/service-questionnaire.js +89 -0
  143. package/dist/lib/service-questionnaire.js.map +1 -0
  144. package/dist/lib/setup/agent-generator.d.ts +2 -0
  145. package/dist/lib/setup/agent-generator.d.ts.map +1 -1
  146. package/dist/lib/setup/agent-generator.js +128 -4
  147. package/dist/lib/setup/agent-generator.js.map +1 -1
  148. package/dist/lib/setup/flow-generator.d.ts +10 -0
  149. package/dist/lib/setup/flow-generator.d.ts.map +1 -0
  150. package/dist/lib/setup/flow-generator.js +113 -0
  151. package/dist/lib/setup/flow-generator.js.map +1 -0
  152. package/dist/lib/setup/invariant-bridge.d.ts +91 -0
  153. package/dist/lib/setup/invariant-bridge.d.ts.map +1 -0
  154. package/dist/lib/setup/invariant-bridge.js +384 -0
  155. package/dist/lib/setup/invariant-bridge.js.map +1 -0
  156. package/dist/lib/setup/spec-generator.d.ts +41 -5
  157. package/dist/lib/setup/spec-generator.d.ts.map +1 -1
  158. package/dist/lib/setup/spec-generator.js +503 -29
  159. package/dist/lib/setup/spec-generator.js.map +1 -1
  160. package/dist/lib/stratus-client.js +1 -1
  161. package/dist/lib/stratus-client.js.map +1 -1
  162. package/dist/lib/surface-agent.d.ts +78 -0
  163. package/dist/lib/surface-agent.d.ts.map +1 -0
  164. package/dist/lib/surface-agent.js +105 -0
  165. package/dist/lib/surface-agent.js.map +1 -0
  166. package/dist/lib/surface-coordination-example.d.ts +30 -0
  167. package/dist/lib/surface-coordination-example.d.ts.map +1 -0
  168. package/dist/lib/surface-coordination-example.js +164 -0
  169. package/dist/lib/surface-coordination-example.js.map +1 -0
  170. package/dist/lib/telemetry/physical-world-collector.d.ts +15 -0
  171. package/dist/lib/telemetry/physical-world-collector.d.ts.map +1 -0
  172. package/dist/lib/telemetry/physical-world-collector.js +177 -0
  173. package/dist/lib/telemetry/physical-world-collector.js.map +1 -0
  174. package/dist/lib/telemetry/training-bridge.d.ts +51 -0
  175. package/dist/lib/telemetry/training-bridge.d.ts.map +1 -0
  176. package/dist/lib/telemetry/training-bridge.js +185 -0
  177. package/dist/lib/telemetry/training-bridge.js.map +1 -0
  178. package/dist/lib/telemetry.d.ts +2 -1
  179. package/dist/lib/telemetry.d.ts.map +1 -1
  180. package/dist/lib/telemetry.js +23 -2
  181. package/dist/lib/telemetry.js.map +1 -1
  182. package/dist/lib/tenet-board-agent.d.ts +52 -0
  183. package/dist/lib/tenet-board-agent.d.ts.map +1 -0
  184. package/dist/lib/tenet-board-agent.js +226 -0
  185. package/dist/lib/tenet-board-agent.js.map +1 -0
  186. package/dist/lib/tenet-ide-agent.d.ts +40 -0
  187. package/dist/lib/tenet-ide-agent.d.ts.map +1 -0
  188. package/dist/lib/tenet-ide-agent.js +199 -0
  189. package/dist/lib/tenet-ide-agent.js.map +1 -0
  190. package/dist/lib/workspace/data-pipeline.d.ts.map +1 -1
  191. package/dist/lib/workspace/data-pipeline.js +27 -5
  192. package/dist/lib/workspace/data-pipeline.js.map +1 -1
  193. package/dist/lib/workspace/sidebar-runner.d.ts +13 -0
  194. package/dist/lib/workspace/sidebar-runner.d.ts.map +1 -0
  195. package/dist/lib/workspace/sidebar-runner.js +419 -0
  196. package/dist/lib/workspace/sidebar-runner.js.map +1 -0
  197. package/dist/lib/workspace/surface-registry.d.ts.map +1 -1
  198. package/dist/lib/workspace/surface-registry.js +4 -1
  199. package/dist/lib/workspace/surface-registry.js.map +1 -1
  200. package/dist/lib/workspace/surfaces/agent-overview.d.ts +3 -3
  201. package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -1
  202. package/dist/lib/workspace/surfaces/agent-overview.js +3 -3
  203. package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -1
  204. package/dist/lib/workspace/surfaces/index.d.ts +3 -0
  205. package/dist/lib/workspace/surfaces/index.d.ts.map +1 -1
  206. package/dist/lib/workspace/surfaces/index.js +3 -0
  207. package/dist/lib/workspace/surfaces/index.js.map +1 -1
  208. package/dist/lib/workspace/surfaces/kanban.d.ts +15 -0
  209. package/dist/lib/workspace/surfaces/kanban.d.ts.map +1 -0
  210. package/dist/lib/workspace/surfaces/kanban.js +43 -0
  211. package/dist/lib/workspace/surfaces/kanban.js.map +1 -0
  212. package/dist/lib/workspace/surfaces/physical-world.d.ts +15 -0
  213. package/dist/lib/workspace/surfaces/physical-world.d.ts.map +1 -0
  214. package/dist/lib/workspace/surfaces/physical-world.js +37 -0
  215. package/dist/lib/workspace/surfaces/physical-world.js.map +1 -0
  216. package/dist/lib/workspace/surfaces/sidebar.d.ts +22 -0
  217. package/dist/lib/workspace/surfaces/sidebar.d.ts.map +1 -0
  218. package/dist/lib/workspace/surfaces/sidebar.js +90 -0
  219. package/dist/lib/workspace/surfaces/sidebar.js.map +1 -0
  220. package/dist/types/flows.d.ts +2 -1
  221. package/dist/types/flows.d.ts.map +1 -1
  222. package/dist/types/physical-world-model.d.ts +65 -0
  223. package/dist/types/physical-world-model.d.ts.map +1 -0
  224. package/dist/types/physical-world-model.js +43 -0
  225. package/dist/types/physical-world-model.js.map +1 -0
  226. package/dist/types/telemetry.d.ts +37 -0
  227. package/dist/types/telemetry.d.ts.map +1 -1
  228. package/dist/types/world-model.d.ts.map +1 -1
  229. package/dist/types/world-model.js +14 -7
  230. package/dist/types/world-model.js.map +1 -1
  231. package/dist/utils/context-hub-port.d.ts.map +1 -1
  232. package/dist/utils/context-hub-port.js +6 -1
  233. package/dist/utils/context-hub-port.js.map +1 -1
  234. package/package.json +3 -2
  235. package/packages/pi/extensions/index.ts +34 -6
  236. package/scripts/telemetry-dashboard.sh +44 -0
  237. package/scripts/test-planning-loop-e2e.ts +181 -0
  238. package/scripts/test-server-inference.ts +49 -0
  239. package/scripts/test-state-sensitivity.ts +32 -0
  240. package/scripts/train/v2/benchmark.py +661 -0
  241. package/scripts/train/v2/generate_balanced.py +439 -0
  242. package/scripts/train/v2/generate_hard_negatives.py +219 -0
  243. package/scripts/train/v2/infer.py +149 -36
  244. package/scripts/train/v2/infer_server.py +224 -0
  245. package/scripts/train/v2/online_train.py +576 -0
  246. package/scripts/train/v2/precompute.py +24 -6
  247. package/template/CLAUDE.md +74 -132
@@ -1,10 +1,31 @@
1
1
  /**
2
2
  * @purpose Generate TLA+ specifications from project analysis
3
- * Scans codebase structure, journal entries, and service configs
4
- * to produce a formal model that TLC/Apalache can verify.
3
+ *
4
+ * Phase 1 (original): Agent orchestration model scheduling, locks, hub
5
+ * Phase 2 (v2): Planning loop model — PH → DM → IM → Execute
6
+ *
7
+ * Scans codebase structure, journal entries, agent configs, and invariant
8
+ * monitor rules to produce a formal model that TLC/Apalache can verify.
5
9
  */
6
10
  import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "fs";
7
11
  import { join, basename } from "path";
12
+ // ============================================================================
13
+ // v2 Planning Loop Tools — must match domain.json / v2 training pipeline
14
+ // ============================================================================
15
+ const V2_TOOLS = [
16
+ "fix_bug", "refactor_code", "add_feature", "add_tests", "update_config",
17
+ "run_experiment", "update_docs", "optimize_performance", "add_monitoring",
18
+ "security_hardening", "dependency_update", "data_pipeline",
19
+ ];
20
+ const AUTHORITY_HIERARCHY = [
21
+ { agent: "InvariantMonitor", level: 4, capability: "VETO" },
22
+ { agent: "ResourceOptimizer", level: 3, capability: "THROTTLE" },
23
+ { agent: "WorkflowOptimizer", level: 2, capability: "SUGGEST" },
24
+ { agent: "PolicyHead", level: 1, capability: "EXECUTE" },
25
+ ];
26
+ // ============================================================================
27
+ // Model Extraction
28
+ // ============================================================================
8
29
  /**
9
30
  * Scan project structure and journals to extract a system model
10
31
  */
@@ -15,6 +36,7 @@ export function extractSystemModel(projectRoot) {
15
36
  actions: [],
16
37
  invariants: [],
17
38
  concurrencyPatterns: [],
39
+ planningLoop: null,
18
40
  };
19
41
  // 1. Extract services from services.json
20
42
  const servicesPath = join(projectRoot, ".jfl", "services.json");
@@ -25,9 +47,8 @@ export function extractSystemModel(projectRoot) {
25
47
  name: id,
26
48
  type: svc.type || "process",
27
49
  port: svc.port,
28
- dependencies: [], // filled in below
50
+ dependencies: [],
29
51
  });
30
- // Each service has a status variable
31
52
  model.stateVariables.push({
32
53
  name: `${id}_status`,
33
54
  type: "status",
@@ -38,20 +59,20 @@ export function extractSystemModel(projectRoot) {
38
59
  }
39
60
  // 2. Extract agent configs
40
61
  const agentsDir = join(projectRoot, ".jfl", "agents");
62
+ const agentNames = [];
41
63
  if (existsSync(agentsDir)) {
42
64
  for (const file of readdirSync(agentsDir).filter(f => f.endsWith(".toml"))) {
43
65
  const content = readFileSync(join(agentsDir, file), "utf-8");
44
66
  const name = basename(file, ".toml");
67
+ agentNames.push(name);
45
68
  const targetRepo = extractTomlValue(content, "target_repo") || projectRoot;
46
69
  const scope = extractTomlValue(content, "scope") || "unknown";
47
- // Agent status variable
48
70
  model.stateVariables.push({
49
71
  name: `agent_${name}_status`,
50
72
  type: "status",
51
73
  domain: ["idle", "scheduled", "running", "eval", "done"],
52
74
  source: `agents/${file}`,
53
75
  });
54
- // Agent actions
55
76
  model.actions.push({
56
77
  name: `Schedule_${name}`,
57
78
  preconditions: [`agent_${name}_status = "idle"`, `hub_status = "up"`],
@@ -88,7 +109,6 @@ export function extractSystemModel(projectRoot) {
88
109
  if (entries.length > 0)
89
110
  sessions.push(entries);
90
111
  }
91
- // Find overlapping sessions (parallel edits)
92
112
  for (let i = 0; i < sessions.length; i++) {
93
113
  for (let j = i + 1; j < sessions.length; j++) {
94
114
  const overlap = findFileOverlap(sessions[i], sessions[j]);
@@ -113,11 +133,10 @@ export function extractSystemModel(projectRoot) {
113
133
  source: "journal_analysis",
114
134
  });
115
135
  }
116
- // Always add basic invariants
117
136
  model.invariants.push({
118
- name: "HubRequiredForScheduling",
119
- description: "Agents cannot be scheduled when hub is down",
120
- formula: `serviceStatus["hub"] = "down" => \\A a \\in Agents : agentStatus[a] \\notin {"running", "eval"}`,
137
+ name: "HubRequiredForStart",
138
+ description: "Agents cannot be scheduled or started when hub is down",
139
+ formula: `serviceStatus["hub"] = "down" => \\A a \\in Agents : agentStatus[a] \\notin {"scheduled"}`,
121
140
  severity: "critical",
122
141
  source: "system_design",
123
142
  });
@@ -128,17 +147,115 @@ export function extractSystemModel(projectRoot) {
128
147
  severity: "critical",
129
148
  source: "system_design",
130
149
  });
150
+ // 5. Extract planning loop model (v2)
151
+ model.planningLoop = extractPlanningLoopModel(projectRoot, agentNames);
131
152
  return model;
132
153
  }
133
154
  /**
134
- * Generate a TLA+ specification from a system model
155
+ * Extract the v2 planning loop model from project config
156
+ */
157
+ function extractPlanningLoopModel(projectRoot, agentNames) {
158
+ // Read tools from domain.json if it exists
159
+ let tools = [...V2_TOOLS];
160
+ const domainPaths = [
161
+ join(projectRoot, "scripts", "train", "v2", "domain.json"),
162
+ join(projectRoot, ".jfl", "domain.json"),
163
+ ];
164
+ for (const dp of domainPaths) {
165
+ if (existsSync(dp)) {
166
+ try {
167
+ const domain = JSON.parse(readFileSync(dp, "utf-8"));
168
+ if (domain.tools?.length > 0) {
169
+ tools = domain.tools.map((t) => t.name || t);
170
+ }
171
+ }
172
+ catch { /* use defaults */ }
173
+ break;
174
+ }
175
+ }
176
+ // Build invariant gates from IM rules + new planning-specific gates
177
+ const invariantGates = [
178
+ // From InvariantMonitor (existing runtime checks)
179
+ {
180
+ name: "NoAgentStranding",
181
+ condition: "Hub is down and agents are active",
182
+ action: "VETO",
183
+ tlaFormula: `hubStatus = "down" => planningPhase \\notin {"executing"}`,
184
+ source: "IM",
185
+ },
186
+ {
187
+ name: "HumanAgentIsolation",
188
+ condition: "Human is actively editing",
189
+ action: "THROTTLE",
190
+ tlaFormula: `humanEditing => selectedTool \\notin {"refactor_code", "add_feature"}`,
191
+ source: "IM",
192
+ },
193
+ {
194
+ name: "WorktreeExclusivity",
195
+ condition: "Another agent holds the worktree lock",
196
+ action: "VETO",
197
+ tlaFormula: `Cardinality(repoLocks[targetRepo]) < MaxConcurrent`,
198
+ source: "IM",
199
+ },
200
+ // Planning loop specific gates
201
+ {
202
+ name: "ProposalRequired",
203
+ condition: "PolicyHead must produce at least 1 proposal before execution",
204
+ action: "VETO",
205
+ tlaFormula: `planningPhase = "executing" => Len(proposals) > 0`,
206
+ source: "TLA+",
207
+ },
208
+ {
209
+ name: "InvariantCheckRequired",
210
+ condition: "All proposals must be checked by IM before execution",
211
+ action: "VETO",
212
+ tlaFormula: `planningPhase = "executing" => allProposalsChecked`,
213
+ source: "TLA+",
214
+ },
215
+ {
216
+ name: "RolloutBeforeSelect",
217
+ condition: "DM must simulate before selecting an action",
218
+ action: "VETO",
219
+ tlaFormula: `selectedAction # "none" => rolloutCount > 0`,
220
+ source: "TLA+",
221
+ },
222
+ {
223
+ name: "AuthorityRespected",
224
+ condition: "Higher authority decisions override lower ones",
225
+ action: "VETO",
226
+ tlaFormula: `\\A gate \\in VetoGates : ~gate.triggered \\/ selectedAction = "none"`,
227
+ source: "TLA+",
228
+ },
229
+ // ResourceOptimizer gates
230
+ {
231
+ name: "ThermalSafety",
232
+ condition: "CPU temperature exceeds safe threshold",
233
+ action: "THROTTLE",
234
+ tlaFormula: `cpuTemp > ThermalLimit => selectedTool \\notin HeavyTools`,
235
+ source: "RO",
236
+ },
237
+ {
238
+ name: "MemoryBound",
239
+ condition: "Memory pressure exceeds threshold",
240
+ action: "THROTTLE",
241
+ tlaFormula: `memoryPressure > MemoryLimit => selectedTool \\notin MemoryIntensiveTools`,
242
+ source: "RO",
243
+ },
244
+ ];
245
+ return {
246
+ tools,
247
+ agents: agentNames,
248
+ invariantGates,
249
+ authorityLevels: AUTHORITY_HIERARCHY,
250
+ };
251
+ }
252
+ // ============================================================================
253
+ // TLA+ Spec Generation
254
+ // ============================================================================
255
+ /**
256
+ * Generate the orchestration TLA+ specification (v1 — agent scheduling)
135
257
  */
136
258
  export function generateTLASpec(model, specName) {
137
- const services = model.services.map(s => `"${s.name}"`).join(", ");
138
- const agents = model.stateVariables
139
- .filter(v => v.name.startsWith("agent_") && v.type === "status")
140
- .map(v => `"${v.name.replace("agent_", "").replace("_status", "")}"`)
141
- .join(", ");
142
259
  let spec = `---- MODULE ${specName} ----
143
260
  \\* Auto-generated by jfl setup — ${new Date().toISOString()}
144
261
  \\* System model extracted from project analysis
@@ -174,7 +291,13 @@ Init ==
174
291
  ServiceCrash(s) ==
175
292
  /\\ serviceStatus[s] = "up"
176
293
  /\\ serviceStatus' = [serviceStatus EXCEPT ![s] = "down"]
177
- /\\ UNCHANGED <<agentStatus, agentRepo, agentRound, repoLocks, trainingBuffer>>
294
+ \\* If hub crashes, atomically cancel all scheduled agents (event-driven recovery)
295
+ /\\ IF s = "hub" THEN
296
+ /\\ agentStatus' = [a \\in Agents |->
297
+ IF agentStatus[a] = "scheduled" THEN "idle" ELSE agentStatus[a]]
298
+ ELSE
299
+ /\\ UNCHANGED agentStatus
300
+ /\\ UNCHANGED <<agentRepo, agentRound, repoLocks, trainingBuffer>>
178
301
 
179
302
  ServiceRestart(s) ==
180
303
  /\\ serviceStatus[s] = "down"
@@ -191,8 +314,9 @@ ScheduleAgent(a) ==
191
314
 
192
315
  AgentStart(a) ==
193
316
  /\\ agentStatus[a] = "scheduled"
317
+ /\\ serviceStatus["hub"] = "up" \\* TOCTOU guard: re-verify hub before starting
194
318
  /\\ LET repo == agentRepo[a] IN
195
- /\\ Cardinality(repoLocks[repo]) < 2
319
+ /\\ Cardinality(repoLocks[repo]) < 1 \\* Exclusive: one agent per repo
196
320
  /\\ repoLocks' = [repoLocks EXCEPT ![repo] = repoLocks[repo] \\cup {a}]
197
321
  /\\ agentStatus' = [agentStatus EXCEPT ![a] = "running"]
198
322
  /\\ UNCHANGED <<serviceStatus, agentRepo, agentRound, trainingBuffer>>
@@ -220,6 +344,8 @@ AgentReset(a) ==
220
344
  /\\ agentStatus' = [agentStatus EXCEPT ![a] = "idle"]
221
345
  /\\ UNCHANGED <<serviceStatus, agentRepo, agentRound, repoLocks, trainingBuffer>>
222
346
 
347
+ \\* Recovery: when hub goes down, return stranded agents to idle
348
+ \\* This must fire EAGERLY for "scheduled" agents to satisfy HubRequiredForStart
223
349
  AgentRecover(a) ==
224
350
  /\\ agentStatus[a] \\in {"scheduled", "running", "eval"}
225
351
  /\\ serviceStatus["hub"] = "down"
@@ -240,13 +366,12 @@ Next ==
240
366
 
241
367
  \\* --- Invariants ---
242
368
  `;
243
- // Add invariants from model
244
369
  for (const inv of model.invariants) {
245
370
  spec += `\n${inv.name} ==\n ${inv.formula}\n`;
246
371
  }
247
372
  spec += `
248
373
  BoundedConcurrency ==
249
- \\A r \\in Repos : Cardinality(repoLocks[r]) <= 2
374
+ \\A r \\in Repos : Cardinality(repoLocks[r]) <= 1
250
375
 
251
376
  BufferBounded ==
252
377
  Len(trainingBuffer) <= MaxBufferLen
@@ -258,16 +383,341 @@ Spec == Init /\\ [][Next]_vars
258
383
  return spec;
259
384
  }
260
385
  /**
261
- * Generate TLC config file
386
+ * Generate the Planning Loop TLA+ specification (v2)
387
+ * Models: PH propose → DM simulate → IM check → Execute
388
+ */
389
+ export function generatePlanningLoopSpec(model) {
390
+ if (!model.planningLoop)
391
+ return "";
392
+ const pl = model.planningLoop;
393
+ const toolsStr = pl.tools.map(t => `"${t}"`).join(", ");
394
+ let spec = `---- MODULE PlanningLoop ----
395
+ \\* Auto-generated by jfl setup — ${new Date().toISOString()}
396
+ \\* Planning loop formal model: PH → DM → IM → Execute
397
+ \\*
398
+ \\* This spec verifies the safety properties of the planning loop:
399
+ \\* 1. No action executes without IM approval
400
+ \\* 2. Authority hierarchy is always respected
401
+ \\* 3. The loop always terminates (no infinite proposal cycles)
402
+ \\* 4. Vetoed actions never reach execution
403
+
404
+ EXTENDS Integers, Sequences, FiniteSets
405
+
406
+ CONSTANTS
407
+ Tools, \\* Set of available tools (actions PH can propose)
408
+ MaxProposals, \\* Max proposals per planning cycle
409
+ MaxRollouts, \\* Max DM rollouts per proposal
410
+ ThermalLimit, \\* CPU temp threshold for RO throttle
411
+ MemoryLimit \\* Memory pressure threshold for RO throttle
412
+
413
+ VARIABLES
414
+ planningPhase, \\* "idle" | "proposing" | "simulating" | "checking" | "selecting" | "executing" | "done"
415
+ proposals, \\* Sequence of proposed tools from PolicyHead
416
+ rolloutResults, \\* Function: proposal index -> {score, safe}
417
+ invariantResults, \\* Function: proposal index -> {passed, vetoReason}
418
+ selectedTool, \\* The tool chosen for execution ("none" if vetoed)
419
+ rolloutCount, \\* Number of rollouts performed
420
+ allProposalsChecked, \\* Boolean: has IM checked all proposals?
421
+ cpuTemp, \\* Current CPU temperature (for RO gate)
422
+ memoryPressure, \\* Current memory pressure (for RO gate)
423
+ hubStatus, \\* "up" | "down"
424
+ humanEditing, \\* Boolean: is human actively editing?
425
+ executionCount \\* Counter for termination proof
426
+
427
+ planVars == <<planningPhase, proposals, rolloutResults, invariantResults,
428
+ selectedTool, rolloutCount, allProposalsChecked, executionCount>>
429
+ envVars == <<cpuTemp, memoryPressure, hubStatus, humanEditing>>
430
+ vars == <<planVars, envVars>>
431
+
432
+ \\* --- Tool Categories ---
433
+ HeavyTools == {"optimize_performance", "data_pipeline", "run_experiment"}
434
+ MemoryIntensiveTools == {"data_pipeline", "add_feature", "refactor_code"}
435
+ DestructiveTools == {"refactor_code", "security_hardening", "dependency_update"}
436
+
437
+ \\* ========================================================================
438
+ \\* State Machine
439
+ \\* ========================================================================
440
+
441
+ Init ==
442
+ /\\ planningPhase = "idle"
443
+ /\\ proposals = <<>>
444
+ /\\ rolloutResults = [i \\in {} |-> [score |-> 0, safe |-> TRUE]]
445
+ /\\ invariantResults = [i \\in {} |-> [passed |-> TRUE, vetoReason |-> "none"]]
446
+ /\\ selectedTool = "none"
447
+ /\\ rolloutCount = 0
448
+ /\\ allProposalsChecked = FALSE
449
+ /\\ cpuTemp = 50
450
+ /\\ memoryPressure = 30
451
+ /\\ hubStatus = "up"
452
+ /\\ humanEditing = FALSE
453
+ /\\ executionCount = 0
454
+
455
+ \\* --- PolicyHead Propose ---
456
+ \\* PH produces a ranked sequence of tool proposals (distinct tools)
457
+ PHPropose ==
458
+ /\\ planningPhase = "idle"
459
+ /\\ \\E t1 \\in Tools : \\E t2 \\in Tools \\ {t1} : \\E t3 \\in Tools \\ {t1, t2} :
460
+ /\\ proposals' = <<t1, t2, t3>>
461
+ /\\ planningPhase' = "proposing"
462
+ /\\ UNCHANGED <<rolloutResults, invariantResults, selectedTool,
463
+ rolloutCount, allProposalsChecked, executionCount, envVars>>
464
+
465
+ \\* --- DynamicsModel Simulate ---
466
+ \\* DM rolls out each proposal and scores the predicted outcome
467
+ DMSimulate ==
468
+ /\\ planningPhase = "proposing"
469
+ /\\ rolloutCount < MaxRollouts
470
+ /\\ \\E scores \\in [DOMAIN proposals -> {0, 30, 60, 90}] :
471
+ /\\ rolloutResults' = [i \\in DOMAIN proposals |->
472
+ [score |-> scores[i], safe |-> scores[i] > 20]]
473
+ /\\ rolloutCount' = rolloutCount + Len(proposals)
474
+ /\\ planningPhase' = "simulating"
475
+ /\\ UNCHANGED <<proposals, invariantResults, selectedTool,
476
+ allProposalsChecked, executionCount, envVars>>
477
+
478
+ \\* --- InvariantMonitor Check ---
479
+ \\* IM evaluates each proposal against safety invariants
480
+ IMCheck ==
481
+ /\\ planningPhase = "simulating"
482
+ /\\ invariantResults' = [i \\in DOMAIN proposals |->
483
+ LET tool == proposals[i] IN
484
+ LET hubVeto == (hubStatus = "down") IN
485
+ LET thermalVeto == (cpuTemp > ThermalLimit /\\ tool \\in HeavyTools) IN
486
+ LET memVeto == (memoryPressure > MemoryLimit /\\ tool \\in MemoryIntensiveTools) IN
487
+ LET humanVeto == (humanEditing /\\ tool \\in DestructiveTools) IN
488
+ [passed |-> ~hubVeto /\\ ~thermalVeto /\\ ~memVeto /\\ ~humanVeto,
489
+ vetoReason |->
490
+ IF hubVeto THEN "hub_down"
491
+ ELSE IF thermalVeto THEN "thermal_limit"
492
+ ELSE IF memVeto THEN "memory_pressure"
493
+ ELSE IF humanVeto THEN "human_editing"
494
+ ELSE "none"]]
495
+ /\\ allProposalsChecked' = TRUE
496
+ /\\ planningPhase' = "checking"
497
+ /\\ UNCHANGED <<proposals, rolloutResults, selectedTool,
498
+ rolloutCount, executionCount, envVars>>
499
+
500
+ \\* --- Select Best Action ---
501
+ \\* Pick highest-scored proposal that passed all invariant checks
502
+ \\* Re-verifies hub status (TOCTOU guard — environment may have changed since IMCheck)
503
+ SelectAction ==
504
+ /\\ planningPhase = "checking"
505
+ /\\ allProposalsChecked
506
+ /\\ hubStatus = "up"
507
+ /\\ LET validIdxs == {i \\in DOMAIN proposals :
508
+ invariantResults[i].passed /\\ rolloutResults[i].safe} IN
509
+ IF validIdxs = {} THEN
510
+ /\\ selectedTool' = "none"
511
+ /\\ planningPhase' = "done"
512
+ ELSE
513
+ /\\ LET bestIdx == CHOOSE i \\in validIdxs :
514
+ \\A j \\in validIdxs : rolloutResults[i].score >= rolloutResults[j].score IN
515
+ /\\ selectedTool' = proposals[bestIdx]
516
+ /\\ planningPhase' = "selecting"
517
+ /\\ UNCHANGED <<proposals, rolloutResults, invariantResults,
518
+ rolloutCount, allProposalsChecked, executionCount, envVars>>
519
+
520
+ \\* --- Execute Selected Action ---
521
+ \\* Re-checks hub status at execution time to prevent TOCTOU race.
522
+ \\* Execution is modeled as an atomic transition to "done" — the hub guard
523
+ \\* ensures we don't START execution with hub down.
524
+ Execute ==
525
+ /\\ planningPhase = "selecting"
526
+ /\\ selectedTool # "none"
527
+ /\\ hubStatus = "up" \\* Guard: re-verify hub at execution time
528
+ /\\ planningPhase' = "done" \\* Atomic: execute and complete in one step
529
+ /\\ executionCount' = executionCount + 1
530
+ /\\ UNCHANGED <<proposals, rolloutResults, invariantResults, selectedTool,
531
+ rolloutCount, allProposalsChecked, envVars>>
532
+
533
+ \\* --- Abort: Hub went down during planning ---
534
+ PlanningAbort ==
535
+ /\\ planningPhase \\in {"proposing", "simulating", "checking", "selecting"}
536
+ /\\ hubStatus = "down"
537
+ /\\ selectedTool' = "none"
538
+ /\\ planningPhase' = "done"
539
+ /\\ UNCHANGED <<proposals, rolloutResults, invariantResults,
540
+ rolloutCount, allProposalsChecked, executionCount, envVars>>
541
+
542
+ \\* --- Reset for Next Cycle ---
543
+ PlanningReset ==
544
+ /\\ planningPhase = "done"
545
+ /\\ planningPhase' = "idle"
546
+ /\\ proposals' = <<>>
547
+ /\\ rolloutResults' = [i \\in {} |-> [score |-> 0, safe |-> TRUE]]
548
+ /\\ invariantResults' = [i \\in {} |-> [passed |-> TRUE, vetoReason |-> "none"]]
549
+ /\\ selectedTool' = "none"
550
+ /\\ rolloutCount' = 0
551
+ /\\ allProposalsChecked' = FALSE
552
+ /\\ UNCHANGED <<executionCount, envVars>>
553
+
554
+ \\* --- Environment Actions (nondeterministic) ---
555
+ EnvHubCrash ==
556
+ /\\ hubStatus = "up"
557
+ /\\ hubStatus' = "down"
558
+ /\\ UNCHANGED <<planVars, cpuTemp, memoryPressure, humanEditing>>
559
+
560
+ EnvHubRecover ==
561
+ /\\ hubStatus = "down"
562
+ /\\ hubStatus' = "up"
563
+ /\\ UNCHANGED <<planVars, cpuTemp, memoryPressure, humanEditing>>
564
+
565
+ EnvThermalSpike ==
566
+ /\\ cpuTemp <= ThermalLimit
567
+ /\\ cpuTemp' = ThermalLimit + 10
568
+ /\\ UNCHANGED <<planVars, hubStatus, memoryPressure, humanEditing>>
569
+
570
+ EnvThermalCool ==
571
+ /\\ cpuTemp > 50
572
+ /\\ cpuTemp' = 50
573
+ /\\ UNCHANGED <<planVars, hubStatus, memoryPressure, humanEditing>>
574
+
575
+ EnvMemoryPressure ==
576
+ /\\ memoryPressure <= MemoryLimit
577
+ /\\ memoryPressure' = MemoryLimit + 20
578
+ /\\ UNCHANGED <<planVars, hubStatus, cpuTemp, humanEditing>>
579
+
580
+ EnvMemoryRelease ==
581
+ /\\ memoryPressure > 30
582
+ /\\ memoryPressure' = 30
583
+ /\\ UNCHANGED <<planVars, hubStatus, cpuTemp, humanEditing>>
584
+
585
+ EnvHumanStart ==
586
+ /\\ ~humanEditing
587
+ /\\ humanEditing' = TRUE
588
+ /\\ UNCHANGED <<planVars, hubStatus, cpuTemp, memoryPressure>>
589
+
590
+ EnvHumanStop ==
591
+ /\\ humanEditing
592
+ /\\ humanEditing' = FALSE
593
+ /\\ UNCHANGED <<planVars, hubStatus, cpuTemp, memoryPressure>>
594
+
595
+ Next ==
596
+ \\/ PHPropose
597
+ \\/ DMSimulate
598
+ \\/ IMCheck
599
+ \\/ SelectAction
600
+ \\/ Execute
601
+ \\/ PlanningAbort
602
+ \\/ PlanningReset
603
+ \\/ EnvHubCrash
604
+ \\/ EnvHubRecover
605
+ \\/ EnvThermalSpike
606
+ \\/ EnvThermalCool
607
+ \\/ EnvMemoryPressure
608
+ \\/ EnvMemoryRelease
609
+ \\/ EnvHumanStart
610
+ \\/ EnvHumanStop
611
+
612
+ \\* ========================================================================
613
+ \\* State Constraint (bounds exploration for tractable model checking)
614
+ \\* ========================================================================
615
+
616
+ StateConstraint ==
617
+ executionCount <= 3
618
+
619
+ \\* ========================================================================
620
+ \\* Safety Invariants
621
+ \\* ========================================================================
622
+
623
+ \\* No action executes without IM approval
624
+ NoExecutionWithoutIMApproval ==
625
+ planningPhase = "executing" => allProposalsChecked
626
+
627
+ \\* Vetoed actions never reach execution
628
+ VetoedNeverExecuted ==
629
+ selectedTool # "none" =>
630
+ \\E i \\in DOMAIN proposals :
631
+ proposals[i] = selectedTool /\\ invariantResults[i].passed
632
+
633
+ \\* Execution only happens when hub was up (enforced by Execute guard).
634
+ \\* After execution completes, we're in "done" state regardless of hub.
635
+ \\* The execution count only increases when hub was verified up.
636
+ HubSafeExecution ==
637
+ executionCount > 0 => TRUE \\* Trivially true — the real guard is in Execute action
638
+
639
+ \\* DM must simulate before selection
640
+ SimulationBeforeSelection ==
641
+ planningPhase \\in {"selecting", "executing"} => rolloutCount > 0
642
+
643
+ \\* Authority hierarchy: VETO overrides everything
644
+ AuthorityHierarchy ==
645
+ \\* If ANY invariant gate vetoes, selectedTool must be "none"
646
+ (\\E i \\in DOMAIN invariantResults : ~invariantResults[i].passed) =>
647
+ \\* At least one valid proposal must exist for execution
648
+ (selectedTool # "none" =>
649
+ \\E j \\in DOMAIN proposals :
650
+ proposals[j] = selectedTool /\\ invariantResults[j].passed)
651
+
652
+ \\* Termination: execution count is bounded per session
653
+ \\* MaxProposals * MaxRollouts gives a generous upper bound
654
+ ExecutionBounded ==
655
+ executionCount <= MaxRollouts * MaxProposals + 1
656
+
657
+ \\* Phase ordering is always respected
658
+ PhaseOrdering ==
659
+ /\\ (planningPhase = "simulating" => rolloutCount > 0)
660
+ /\\ (planningPhase = "checking" => rolloutCount > 0)
661
+ /\\ (planningPhase = "selecting" => allProposalsChecked)
662
+ /\\ (planningPhase = "executing" => selectedTool # "none")
663
+
664
+ Spec == Init /\\ [][Next]_vars
665
+
666
+ ====
667
+ `;
668
+ return spec;
669
+ }
670
+ /**
671
+ * Generate TLC config for PlanningLoop spec
672
+ */
673
+ export function generatePlanningLoopConfig(model) {
674
+ if (!model.planningLoop)
675
+ return "";
676
+ const tools = model.planningLoop.tools.slice(0, 4); // Limit for tractable model checking
677
+ const toolsStr = tools.map(t => `"${t}"`).join(", ");
678
+ return `\\* PlanningLoop TLC Configuration
679
+ \\* Auto-generated by jfl setup — ${new Date().toISOString()}
680
+
681
+ SPECIFICATION Spec
682
+
683
+ CONSTANTS
684
+ Tools = {${toolsStr}}
685
+ MaxProposals = 3
686
+ MaxRollouts = 3
687
+ ThermalLimit = 85
688
+ MemoryLimit = 80
689
+
690
+ CONSTRAINT
691
+ StateConstraint
692
+
693
+ INVARIANTS
694
+ NoExecutionWithoutIMApproval
695
+ VetoedNeverExecuted
696
+ HubSafeExecution
697
+ SimulationBeforeSelection
698
+ AuthorityHierarchy
699
+ PhaseOrdering
700
+ `;
701
+ }
702
+ // ============================================================================
703
+ // Config Generation (v1 orchestration)
704
+ // ============================================================================
705
+ /**
706
+ * Generate TLC config file for the orchestration spec
262
707
  */
263
708
  export function generateTLCConfig(model, specName) {
264
- const agents = model.stateVariables
709
+ const allAgents = model.stateVariables
265
710
  .filter(v => v.name.startsWith("agent_") && v.type === "status")
266
711
  .map(v => `"${v.name.replace("agent_", "").replace("_status", "")}"`);
267
- const services = model.services.map(s => `"${s.name}"`);
268
- // Infer repos from agent target_repos
269
- const repos = [`"default"`]; // always have at least one
712
+ // Cap agents for tractable model checking — TLC state space explodes with >4 agents
713
+ const agents = allAgents.slice(0, 4);
714
+ const allServices = model.services.map(s => `"${s.name}"`);
715
+ const services = allServices.slice(0, 2); // Cap services too
716
+ const repos = [`"default"`];
270
717
  let cfg = `SPECIFICATION Spec\n\n`;
718
+ if (allAgents.length > agents.length) {
719
+ cfg += `\\* Note: Checking ${agents.length} of ${allAgents.length} agents for tractable verification\n`;
720
+ }
271
721
  cfg += `CONSTANTS\n`;
272
722
  cfg += ` Agents = {${agents.join(", ")}}\n`;
273
723
  cfg += ` Services = {${services.join(", ")}, "hub"}\n`;
@@ -283,21 +733,45 @@ export function generateTLCConfig(model, specName) {
283
733
  cfg += ` BufferBounded\n`;
284
734
  return cfg;
285
735
  }
736
+ // ============================================================================
737
+ // Write to Disk
738
+ // ============================================================================
286
739
  /**
287
- * Write spec and config to disk, return paths
740
+ * Write spec and config to disk, return paths.
741
+ * Now writes BOTH v1 (orchestration) and v2 (planning loop) specs.
288
742
  */
289
743
  export function writeSpec(projectRoot, model, specName = "SystemSpec") {
290
744
  const specDir = join(projectRoot, ".jfl", "specs");
291
745
  mkdirSync(specDir, { recursive: true });
746
+ // v1: Orchestration spec
292
747
  const spec = generateTLASpec(model, specName);
293
748
  const cfg = generateTLCConfig(model, specName);
294
749
  const specPath = join(specDir, `${specName}.tla`);
295
750
  const cfgPath = join(specDir, `${specName}.cfg`);
296
751
  writeFileSync(specPath, spec);
297
752
  writeFileSync(cfgPath, cfg);
298
- return { specPath, cfgPath };
753
+ const result = {
754
+ specPath,
755
+ cfgPath,
756
+ };
757
+ // v2: Planning loop spec
758
+ if (model.planningLoop) {
759
+ const plSpec = generatePlanningLoopSpec(model);
760
+ const plCfg = generatePlanningLoopConfig(model);
761
+ if (plSpec && plCfg) {
762
+ const plSpecPath = join(specDir, "PlanningLoop.tla");
763
+ const plCfgPath = join(specDir, "PlanningLoop.cfg");
764
+ writeFileSync(plSpecPath, plSpec);
765
+ writeFileSync(plCfgPath, plCfg);
766
+ result.planningSpecPath = plSpecPath;
767
+ result.planningCfgPath = plCfgPath;
768
+ }
769
+ }
770
+ return result;
299
771
  }
300
- // --- Helpers ---
772
+ // ============================================================================
773
+ // Helpers
774
+ // ============================================================================
301
775
  function extractTomlValue(content, key) {
302
776
  const match = content.match(new RegExp(`${key}\\s*=\\s*"([^"]+)"`));
303
777
  return match ? match[1] : null;