jfl 0.2.5 → 0.4.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 (133) hide show
  1. package/README.md +308 -28
  2. package/dist/commands/context-hub.d.ts.map +1 -1
  3. package/dist/commands/context-hub.js +428 -27
  4. package/dist/commands/context-hub.js.map +1 -1
  5. package/dist/commands/eval.d.ts +6 -0
  6. package/dist/commands/eval.d.ts.map +1 -0
  7. package/dist/commands/eval.js +236 -0
  8. package/dist/commands/eval.js.map +1 -0
  9. package/dist/commands/flows.d.ts +4 -1
  10. package/dist/commands/flows.d.ts.map +1 -1
  11. package/dist/commands/flows.js +160 -1
  12. package/dist/commands/flows.js.map +1 -1
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +272 -145
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/peter.d.ts.map +1 -1
  17. package/dist/commands/peter.js +220 -1
  18. package/dist/commands/peter.js.map +1 -1
  19. package/dist/commands/pi.d.ts +21 -0
  20. package/dist/commands/pi.d.ts.map +1 -0
  21. package/dist/commands/pi.js +154 -0
  22. package/dist/commands/pi.js.map +1 -0
  23. package/dist/commands/portfolio.d.ts +6 -0
  24. package/dist/commands/portfolio.d.ts.map +1 -0
  25. package/dist/commands/portfolio.js +249 -0
  26. package/dist/commands/portfolio.js.map +1 -0
  27. package/dist/commands/predict.d.ts +6 -0
  28. package/dist/commands/predict.d.ts.map +1 -0
  29. package/dist/commands/predict.js +234 -0
  30. package/dist/commands/predict.js.map +1 -0
  31. package/dist/commands/scope.d.ts +1 -0
  32. package/dist/commands/scope.d.ts.map +1 -1
  33. package/dist/commands/scope.js +189 -2
  34. package/dist/commands/scope.js.map +1 -1
  35. package/dist/commands/synopsis.d.ts +44 -0
  36. package/dist/commands/synopsis.d.ts.map +1 -1
  37. package/dist/commands/synopsis.js +1 -1
  38. package/dist/commands/synopsis.js.map +1 -1
  39. package/dist/commands/update.d.ts.map +1 -1
  40. package/dist/commands/update.js +49 -1
  41. package/dist/commands/update.js.map +1 -1
  42. package/dist/commands/viz.d.ts +7 -0
  43. package/dist/commands/viz.d.ts.map +1 -0
  44. package/dist/commands/viz.js +460 -0
  45. package/dist/commands/viz.js.map +1 -0
  46. package/dist/commands/voice.js.map +1 -1
  47. package/dist/dashboard/index.d.ts +4 -5
  48. package/dist/dashboard/index.d.ts.map +1 -1
  49. package/dist/dashboard/index.js +57 -119
  50. package/dist/dashboard/index.js.map +1 -1
  51. package/dist/dashboard-static/assets/index-B6kRK9Rq.js +116 -0
  52. package/dist/dashboard-static/assets/index-BpdKJPLu.css +1 -0
  53. package/dist/dashboard-static/index.html +16 -0
  54. package/dist/index.js +120 -20
  55. package/dist/index.js.map +1 -1
  56. package/dist/lib/eval-store.d.ts +15 -0
  57. package/dist/lib/eval-store.d.ts.map +1 -0
  58. package/dist/lib/eval-store.js +179 -0
  59. package/dist/lib/eval-store.js.map +1 -0
  60. package/dist/lib/flow-engine.d.ts +13 -0
  61. package/dist/lib/flow-engine.d.ts.map +1 -1
  62. package/dist/lib/flow-engine.js +164 -3
  63. package/dist/lib/flow-engine.js.map +1 -1
  64. package/dist/lib/hub-client.d.ts +80 -0
  65. package/dist/lib/hub-client.d.ts.map +1 -0
  66. package/dist/lib/hub-client.js +46 -0
  67. package/dist/lib/hub-client.js.map +1 -0
  68. package/dist/lib/predictor.d.ts +99 -0
  69. package/dist/lib/predictor.d.ts.map +1 -0
  70. package/dist/lib/predictor.js +394 -0
  71. package/dist/lib/predictor.js.map +1 -0
  72. package/dist/lib/service-gtm.d.ts +88 -44
  73. package/dist/lib/service-gtm.d.ts.map +1 -1
  74. package/dist/lib/service-gtm.js +451 -243
  75. package/dist/lib/service-gtm.js.map +1 -1
  76. package/dist/lib/telemetry-agent.d.ts +57 -0
  77. package/dist/lib/telemetry-agent.d.ts.map +1 -0
  78. package/dist/lib/telemetry-agent.js +268 -0
  79. package/dist/lib/telemetry-agent.js.map +1 -0
  80. package/dist/lib/telemetry-digest.d.ts.map +1 -1
  81. package/dist/lib/telemetry-digest.js +17 -17
  82. package/dist/lib/telemetry-digest.js.map +1 -1
  83. package/dist/lib/telemetry.d.ts +1 -0
  84. package/dist/lib/telemetry.d.ts.map +1 -1
  85. package/dist/lib/telemetry.js +14 -6
  86. package/dist/lib/telemetry.js.map +1 -1
  87. package/dist/lib/trajectory-loader.d.ts +82 -0
  88. package/dist/lib/trajectory-loader.d.ts.map +1 -0
  89. package/dist/lib/trajectory-loader.js +406 -0
  90. package/dist/lib/trajectory-loader.js.map +1 -0
  91. package/dist/mcp/context-hub-mcp.js +60 -0
  92. package/dist/mcp/context-hub-mcp.js.map +1 -1
  93. package/dist/mcp/service-registry-mcp.js +0 -0
  94. package/dist/types/eval.d.ts +18 -0
  95. package/dist/types/eval.d.ts.map +1 -0
  96. package/dist/types/eval.js +5 -0
  97. package/dist/types/eval.js.map +1 -0
  98. package/dist/types/journal.d.ts +133 -0
  99. package/dist/types/journal.d.ts.map +1 -0
  100. package/dist/types/journal.js +59 -0
  101. package/dist/types/journal.js.map +1 -0
  102. package/dist/types/map.d.ts +1 -1
  103. package/dist/types/map.d.ts.map +1 -1
  104. package/dist/types/map.js.map +1 -1
  105. package/dist/ui/service-dashboard.js.map +1 -1
  106. package/dist/utils/jfl-paths.d.ts +1 -0
  107. package/dist/utils/jfl-paths.d.ts.map +1 -1
  108. package/dist/utils/jfl-paths.js +1 -0
  109. package/dist/utils/jfl-paths.js.map +1 -1
  110. package/dist/utils/wallet.js.map +1 -1
  111. package/package.json +7 -2
  112. package/scripts/generate-changesets.sh +113 -0
  113. package/scripts/migrate-to-branch-sessions.sh +201 -0
  114. package/scripts/pp-branch-pr.sh +115 -0
  115. package/scripts/session/session-cleanup.sh +29 -14
  116. package/scripts/session/session-end.sh +0 -10
  117. package/scripts/session/session-init.sh +0 -16
  118. package/scripts/session/session-sync.sh +0 -10
  119. package/template/.jfl/flows-self-driving.yaml +170 -0
  120. package/template/THEORY.md +26 -0
  121. package/template/scripts/session/session-cleanup.sh +28 -10
  122. package/dist/dashboard/components.d.ts +0 -7
  123. package/dist/dashboard/components.d.ts.map +0 -1
  124. package/dist/dashboard/components.js +0 -163
  125. package/dist/dashboard/components.js.map +0 -1
  126. package/dist/dashboard/pages.d.ts +0 -7
  127. package/dist/dashboard/pages.d.ts.map +0 -1
  128. package/dist/dashboard/pages.js +0 -742
  129. package/dist/dashboard/pages.js.map +0 -1
  130. package/dist/dashboard/styles.d.ts +0 -7
  131. package/dist/dashboard/styles.d.ts.map +0 -1
  132. package/dist/dashboard/styles.js +0 -497
  133. package/dist/dashboard/styles.js.map +0 -1
@@ -17,7 +17,7 @@ import { ensureDaemonInstalled } from "./context-hub.js";
17
17
  const TEMPLATE_REPO = "https://github.com/402goose/jfl-template.git";
18
18
  export async function initCommand(options) {
19
19
  // Start Clawdbot-style flow
20
- p.intro(chalk.hex("#FFD700")("┌ JFL - Initialize GTM Workspace"));
20
+ p.intro(chalk.hex("#FFD700")("┌ JFL - Initialize Workspace"));
21
21
  // Check authentication - owner needs to be verified
22
22
  let ownerName = "";
23
23
  let ownerGithub = "";
@@ -67,6 +67,19 @@ export async function initCommand(options) {
67
67
  }
68
68
  projectName = name;
69
69
  }
70
+ // Ask workspace type
71
+ const workspaceType = await p.select({
72
+ message: "Workspace type:",
73
+ options: [
74
+ { label: "GTM", value: "gtm", hint: "Single product workspace" },
75
+ { label: "Portfolio", value: "portfolio", hint: "Manages multiple products" },
76
+ ],
77
+ });
78
+ if (p.isCancel(workspaceType)) {
79
+ p.cancel("Setup cancelled.");
80
+ process.exit(0);
81
+ }
82
+ const isPortfolio = workspaceType === "portfolio";
70
83
  // If cwd already matches the project name, initialize in place
71
84
  const cwdBasename = basename(process.cwd());
72
85
  const initInPlace = cwdBasename === projectName;
@@ -95,7 +108,7 @@ export async function initCommand(options) {
95
108
  return;
96
109
  }
97
110
  // Clone template to temp directory, copy only template/ folder
98
- const spinner = ora("Downloading GTM template...").start();
111
+ const spinner = ora(`Downloading ${isPortfolio ? "portfolio" : "GTM"} template...`).start();
99
112
  const tempDir = join(tmpdir(), `jfl-init-${Date.now()}`);
100
113
  try {
101
114
  // Clone to temp
@@ -150,7 +163,7 @@ export async function initCommand(options) {
150
163
  catch {
151
164
  execSync(`git init`, { cwd: projectPath, stdio: "pipe" });
152
165
  }
153
- spinner.succeed("GTM workspace created!");
166
+ spinner.succeed(`${isPortfolio ? "Portfolio" : "GTM"} workspace created!`);
154
167
  // Validate and fix .claude/settings.json
155
168
  const settingsPath = join(projectPath, ".claude", "settings.json");
156
169
  if (existsSync(settingsPath)) {
@@ -181,165 +194,233 @@ export async function initCommand(options) {
181
194
  process.exit(0);
182
195
  }
183
196
  let productRepo = null;
184
- // Ask about product repo (registered as service, NOT a submodule)
185
- const productChoice = await p.select({
186
- message: "Product repo:",
187
- options: [
188
- {
189
- label: "I have an existing repo",
190
- value: "existing",
191
- hint: "Register as a service"
192
- },
193
- {
194
- label: "Create a new repo for me",
195
- value: "create",
196
- hint: "Requires: gh CLI"
197
- },
198
- {
199
- label: "I'll add it later",
200
- value: "later",
201
- hint: "Recommended if unsure"
202
- },
203
- ],
204
- });
205
- if (p.isCancel(productChoice)) {
206
- p.cancel("Setup cancelled.");
207
- process.exit(0);
208
- }
209
- if (productChoice !== "later") {
210
- if (productChoice === "existing") {
211
- const repoUrl = await p.text({
212
- message: "Product repo URL:",
213
- placeholder: "https://github.com/user/repo.git",
214
- validate: (input) => {
215
- if (!input.trim()) {
216
- return "Please enter a repo URL";
197
+ const portfolioChildGtms = [];
198
+ if (isPortfolio) {
199
+ // Portfolio: register child GTM workspaces
200
+ p.note("Register existing GTM workspaces under this portfolio.\n" +
201
+ "Each GTM gets its own eval pipeline, journals, and event bus.\n" +
202
+ "The portfolio aggregates across all of them.", chalk.hex("#FFA500")("Portfolio Setup"));
203
+ const registerGtms = await p.confirm({
204
+ message: "Register GTM workspaces now?",
205
+ initialValue: true,
206
+ });
207
+ if (!p.isCancel(registerGtms) && registerGtms) {
208
+ let adding = true;
209
+ let gtmCount = 0;
210
+ while (adding) {
211
+ const gtmPathInput = await p.text({
212
+ message: gtmCount === 0
213
+ ? "GTM workspace path:"
214
+ : "Another GTM path (or Enter to skip):",
215
+ placeholder: "/path/to/my-product-gtm",
216
+ validate: (input) => {
217
+ if (gtmCount === 0 && !input.trim()) {
218
+ return "Enter at least one GTM path";
219
+ }
220
+ },
221
+ });
222
+ if (p.isCancel(gtmPathInput))
223
+ break;
224
+ if (!gtmPathInput || gtmPathInput.trim() === "") {
225
+ adding = false;
226
+ break;
227
+ }
228
+ const resolvedGtmPath = join(process.cwd(), gtmPathInput).replace(/\/+$/, "");
229
+ const absGtmPath = gtmPathInput.startsWith("/") ? gtmPathInput : resolvedGtmPath;
230
+ const gtmConfigPath = join(absGtmPath, ".jfl", "config.json");
231
+ if (!existsSync(gtmConfigPath)) {
232
+ p.log.warning(`No .jfl/config.json at ${absGtmPath} — skipping`);
233
+ continue;
234
+ }
235
+ try {
236
+ const gtmConfig = JSON.parse(readFileSync(gtmConfigPath, "utf-8"));
237
+ if (gtmConfig.type !== "gtm") {
238
+ p.log.warning(`${absGtmPath} is type "${gtmConfig.type}", not "gtm" — skipping`);
239
+ continue;
217
240
  }
218
- },
219
- });
220
- if (p.isCancel(repoUrl)) {
221
- p.cancel("Setup cancelled.");
222
- process.exit(0);
241
+ const { getRegisteredServices } = await import("../lib/service-gtm.js");
242
+ const svcCount = getRegisteredServices(absGtmPath).length;
243
+ portfolioChildGtms.push({
244
+ name: gtmConfig.name,
245
+ path: absGtmPath,
246
+ type: "gtm",
247
+ registered_at: new Date().toISOString(),
248
+ status: "active",
249
+ context_scope: gtmConfig.context_scope,
250
+ });
251
+ // Write portfolio_parent back to child
252
+ gtmConfig.portfolio_parent = projectPath;
253
+ writeFileSync(gtmConfigPath, JSON.stringify(gtmConfig, null, 2));
254
+ p.log.success(`Registered: ${gtmConfig.name} (${svcCount} services)`);
255
+ gtmCount++;
256
+ }
257
+ catch (err) {
258
+ p.log.warning(`Failed to read ${absGtmPath}: ${err.message}`);
259
+ }
223
260
  }
224
- productRepo = repoUrl;
225
- p.log.success(`Product repo registered: ${productRepo}`);
226
- p.log.info("Use service agents to work on product code directly in its own repo.");
227
261
  }
228
- else if (productChoice === "create") {
229
- // Check if gh CLI is available
230
- try {
231
- execSync("gh --version", { stdio: "pipe" });
232
- let repoCreated = false;
233
- let attemptCount = 0;
234
- const maxAttempts = 3;
235
- while (!repoCreated && attemptCount < maxAttempts) {
236
- attemptCount++;
237
- const repoName = await p.text({
238
- message: attemptCount > 1 ? "Try a different name:" : "New repo name:",
239
- placeholder: attemptCount > 1
240
- ? `${projectName.replace(/-gtm$/, "")}-${attemptCount}`
241
- : projectName.replace(/-gtm$/, ""),
242
- validate: (input) => {
243
- if (!input.trim()) {
244
- return "Please enter a repo name";
245
- }
246
- if (/\s/.test(input)) {
247
- return "Repo names cannot contain spaces. Use hyphens instead (e.g., 'my-project')";
248
- }
249
- if (!/^[a-zA-Z0-9._-]+$/.test(input)) {
250
- return "Repo names can only contain letters, numbers, hyphens, underscores, and dots";
251
- }
252
- },
253
- });
254
- if (p.isCancel(repoName)) {
255
- p.cancel("Setup cancelled.");
256
- process.exit(0);
257
- }
258
- const visibility = await p.select({
259
- message: "Visibility:",
260
- options: [
261
- { label: "Private", value: "private" },
262
- { label: "Public", value: "public" },
263
- ],
264
- });
265
- if (p.isCancel(visibility)) {
266
- p.cancel("Setup cancelled.");
267
- process.exit(0);
268
- }
269
- const createSpinner = ora("Creating product repo...").start();
270
- try {
271
- const visFlag = visibility === "private" ? "--private" : "--public";
272
- const finalRepoName = repoName.trim().replace(/\s+/g, '-');
273
- try {
274
- execSync(`gh repo create ${finalRepoName} ${visFlag}`, {
275
- cwd: projectPath,
276
- stdio: "pipe",
277
- encoding: "utf-8",
278
- });
279
- // Get the repo URL
280
- const repoUrl = execSync(`gh repo view ${finalRepoName} --json url -q .url`, {
281
- cwd: projectPath,
282
- encoding: "utf-8",
283
- }).trim();
284
- createSpinner.succeed(`Product repo created: ${repoUrl}`);
285
- productRepo = repoUrl;
286
- repoCreated = true;
262
+ }
263
+ if (!isPortfolio) {
264
+ // Ask about product repo (registered as service, NOT a submodule)
265
+ const productChoice = await p.select({
266
+ message: "Product repo:",
267
+ options: [
268
+ {
269
+ label: "I have an existing repo",
270
+ value: "existing",
271
+ hint: "Register as a service"
272
+ },
273
+ {
274
+ label: "Create a new repo for me",
275
+ value: "create",
276
+ hint: "Requires: gh CLI"
277
+ },
278
+ {
279
+ label: "I'll add it later",
280
+ value: "later",
281
+ hint: "Recommended if unsure"
282
+ },
283
+ ],
284
+ });
285
+ if (p.isCancel(productChoice)) {
286
+ p.cancel("Setup cancelled.");
287
+ process.exit(0);
288
+ }
289
+ if (productChoice !== "later") {
290
+ if (productChoice === "existing") {
291
+ const repoUrl = await p.text({
292
+ message: "Product repo URL:",
293
+ placeholder: "https://github.com/user/repo.git",
294
+ validate: (input) => {
295
+ if (!input.trim()) {
296
+ return "Please enter a repo URL";
297
+ }
298
+ },
299
+ });
300
+ if (p.isCancel(repoUrl)) {
301
+ p.cancel("Setup cancelled.");
302
+ process.exit(0);
303
+ }
304
+ productRepo = repoUrl;
305
+ p.log.success(`Product repo registered: ${productRepo}`);
306
+ p.log.info("Use service agents to work on product code directly in its own repo.");
307
+ }
308
+ else if (productChoice === "create") {
309
+ // Check if gh CLI is available
310
+ try {
311
+ execSync("gh --version", { stdio: "pipe" });
312
+ let repoCreated = false;
313
+ let attemptCount = 0;
314
+ const maxAttempts = 3;
315
+ while (!repoCreated && attemptCount < maxAttempts) {
316
+ attemptCount++;
317
+ const repoName = await p.text({
318
+ message: attemptCount > 1 ? "Try a different name:" : "New repo name:",
319
+ placeholder: attemptCount > 1
320
+ ? `${projectName.replace(/-gtm$/, "")}-${attemptCount}`
321
+ : projectName.replace(/-gtm$/, ""),
322
+ validate: (input) => {
323
+ if (!input.trim()) {
324
+ return "Please enter a repo name";
325
+ }
326
+ if (/\s/.test(input)) {
327
+ return "Repo names cannot contain spaces. Use hyphens instead (e.g., 'my-project')";
328
+ }
329
+ if (!/^[a-zA-Z0-9._-]+$/.test(input)) {
330
+ return "Repo names can only contain letters, numbers, hyphens, underscores, and dots";
331
+ }
332
+ },
333
+ });
334
+ if (p.isCancel(repoName)) {
335
+ p.cancel("Setup cancelled.");
336
+ process.exit(0);
337
+ }
338
+ const visibility = await p.select({
339
+ message: "Visibility:",
340
+ options: [
341
+ { label: "Private", value: "private" },
342
+ { label: "Public", value: "public" },
343
+ ],
344
+ });
345
+ if (p.isCancel(visibility)) {
346
+ p.cancel("Setup cancelled.");
347
+ process.exit(0);
287
348
  }
288
- catch (createErr) {
289
- createSpinner.fail("Failed to create repo");
290
- console.log("");
291
- const errorMsg = createErr.stderr || createErr.message || String(createErr);
292
- if (errorMsg.includes("already exists") || errorMsg.includes("Name already exists")) {
293
- console.log(chalk.yellow("That name is already taken on your GitHub"));
349
+ const createSpinner = ora("Creating product repo...").start();
350
+ try {
351
+ const visFlag = visibility === "private" ? "--private" : "--public";
352
+ const finalRepoName = repoName.trim().replace(/\s+/g, '-');
353
+ try {
354
+ execSync(`gh repo create ${finalRepoName} ${visFlag}`, {
355
+ cwd: projectPath,
356
+ stdio: "pipe",
357
+ encoding: "utf-8",
358
+ });
359
+ // Get the repo URL
360
+ const repoUrl = execSync(`gh repo view ${finalRepoName} --json url -q .url`, {
361
+ cwd: projectPath,
362
+ encoding: "utf-8",
363
+ }).trim();
364
+ createSpinner.succeed(`Product repo created: ${repoUrl}`);
365
+ productRepo = repoUrl;
366
+ repoCreated = true;
367
+ }
368
+ catch (createErr) {
369
+ createSpinner.fail("Failed to create repo");
294
370
  console.log("");
295
- if (attemptCount < maxAttempts) {
296
- const retry = await p.confirm({
297
- message: "Try a different name?",
298
- initialValue: true,
299
- });
300
- if (p.isCancel(retry) || !retry) {
301
- p.log.info("Skipping repo creation. Register it later with:");
371
+ const errorMsg = createErr.stderr || createErr.message || String(createErr);
372
+ if (errorMsg.includes("already exists") || errorMsg.includes("Name already exists")) {
373
+ console.log(chalk.yellow("That name is already taken on your GitHub"));
374
+ console.log("");
375
+ if (attemptCount < maxAttempts) {
376
+ const retry = await p.confirm({
377
+ message: "Try a different name?",
378
+ initialValue: true,
379
+ });
380
+ if (p.isCancel(retry) || !retry) {
381
+ p.log.info("Skipping repo creation. Register it later with:");
382
+ p.log.info(" jfl services add <repo-url>");
383
+ break;
384
+ }
385
+ }
386
+ else {
387
+ p.log.warning("Max attempts reached. Register repo later:");
302
388
  p.log.info(" jfl services add <repo-url>");
303
389
  break;
304
390
  }
305
391
  }
392
+ else if (errorMsg.includes("authentication") || errorMsg.includes("not logged in") || errorMsg.includes("HTTP 401")) {
393
+ console.log(chalk.red("GitHub authentication required"));
394
+ console.log("");
395
+ p.log.info("Authenticate with GitHub:");
396
+ p.log.info(" gh auth login");
397
+ break;
398
+ }
306
399
  else {
307
- p.log.warning("Max attempts reached. Register repo later:");
308
- p.log.info(" jfl services add <repo-url>");
400
+ console.log(chalk.red("Error from GitHub CLI:"));
401
+ console.log(chalk.gray(errorMsg));
402
+ console.log("");
403
+ p.log.info(`Try manually: gh repo create ${finalRepoName} ${visFlag}`);
309
404
  break;
310
405
  }
311
406
  }
312
- else if (errorMsg.includes("authentication") || errorMsg.includes("not logged in") || errorMsg.includes("HTTP 401")) {
313
- console.log(chalk.red("GitHub authentication required"));
314
- console.log("");
315
- p.log.info("Authenticate with GitHub:");
316
- p.log.info(" gh auth login");
317
- break;
318
- }
319
- else {
320
- console.log(chalk.red("Error from GitHub CLI:"));
321
- console.log(chalk.gray(errorMsg));
322
- console.log("");
323
- p.log.info(`Try manually: gh repo create ${finalRepoName} ${visFlag}`);
324
- break;
325
- }
407
+ }
408
+ catch (err) {
409
+ break;
326
410
  }
327
411
  }
328
- catch (err) {
329
- break;
330
- }
412
+ }
413
+ catch {
414
+ p.log.warning("GitHub CLI (gh) not found. Install it to create repos:");
415
+ p.log.info("brew install gh && gh auth login");
331
416
  }
332
417
  }
333
- catch {
334
- p.log.warning("GitHub CLI (gh) not found. Install it to create repos:");
335
- p.log.info("brew install gh && gh auth login");
418
+ else {
419
+ p.log.info("Register your product repo later:");
420
+ p.log.info(" jfl services add <repo-url>");
336
421
  }
337
422
  }
338
- else {
339
- p.log.info("Register your product repo later:");
340
- p.log.info(" jfl services add <repo-url>");
341
- }
342
- }
423
+ } // end if (!isPortfolio)
343
424
  // Update .jfl/config.json
344
425
  const configDir = join(projectPath, ".jfl");
345
426
  if (!existsSync(configDir)) {
@@ -348,7 +429,7 @@ export async function initCommand(options) {
348
429
  const configPath = join(configDir, "config.json");
349
430
  const config = {
350
431
  name: projectName,
351
- type: "gtm",
432
+ type: isPortfolio ? "portfolio" : "gtm",
352
433
  description: description,
353
434
  };
354
435
  // Save owner info if authenticated
@@ -368,6 +449,10 @@ export async function initCommand(options) {
368
449
  config.product_repo = productRepo;
369
450
  config.registered_services = [{ name: "product", repo: productRepo }];
370
451
  }
452
+ // Merge portfolio child GTMs collected earlier
453
+ if (isPortfolio && portfolioChildGtms.length > 0) {
454
+ config.registered_services = portfolioChildGtms;
455
+ }
371
456
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
372
457
  // Assign deterministic port and write to config + .mcp.json
373
458
  const hubPort = persistProjectPort(projectPath);
@@ -386,7 +471,7 @@ export async function initCommand(options) {
386
471
  const profile = getProfile();
387
472
  if (profile) {
388
473
  // Generate CLAUDE.md from profile
389
- const projectDescription = `GTM workspace for ${projectName}`;
474
+ const projectDescription = isPortfolio ? `Portfolio workspace for ${projectName}` : `GTM workspace for ${projectName}`;
390
475
  const claudeContent = generateClaudeMdFromProfile(profile, {
391
476
  name: projectName,
392
477
  description: projectDescription
@@ -714,6 +799,48 @@ export async function initCommand(options) {
714
799
  else {
715
800
  p.log.info("Skip service onboarding. Add services later with: jfl onboard <path|url>");
716
801
  }
802
+ // Configure HTTP hooks for telemetry
803
+ try {
804
+ const { getProjectPort } = await import("../utils/context-hub-port.js");
805
+ const port = getProjectPort(projectPath);
806
+ if (port) {
807
+ const hookUrl = `http://localhost:${port}/api/hooks`;
808
+ const settingsPath = join(projectPath, ".claude", "settings.json");
809
+ if (existsSync(settingsPath)) {
810
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
811
+ if (!settings.hooks)
812
+ settings.hooks = {};
813
+ const hookEvents = ["PostToolUse", "Stop", "PreCompact", "SubagentStart", "SubagentStop"];
814
+ let added = 0;
815
+ let fixed = 0;
816
+ for (const event of hookEvents) {
817
+ if (!settings.hooks[event])
818
+ settings.hooks[event] = [];
819
+ const existingIdx = settings.hooks[event].findIndex((e) => e.hooks?.some((h) => h.type === "http" && h.url?.includes("/api/hooks")));
820
+ if (existingIdx >= 0) {
821
+ const entry = settings.hooks[event][existingIdx];
822
+ const httpHook = entry.hooks.find((h) => h.type === "http" && h.url?.includes("/api/hooks"));
823
+ if (httpHook && httpHook.url !== hookUrl) {
824
+ httpHook.url = hookUrl;
825
+ fixed++;
826
+ }
827
+ }
828
+ else {
829
+ settings.hooks[event].push({ matcher: "", hooks: [{ type: "http", url: hookUrl }] });
830
+ added++;
831
+ }
832
+ }
833
+ if (added > 0 || fixed > 0) {
834
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
835
+ if (added > 0)
836
+ p.log.success(`HTTP hooks added for ${added} events → ${hookUrl}`);
837
+ if (fixed > 0)
838
+ p.log.success(`HTTP hooks updated for ${fixed} events → ${hookUrl}`);
839
+ }
840
+ }
841
+ }
842
+ }
843
+ catch { }
717
844
  // Success message
718
845
  let successMessage = `${projectName}/\n` +
719
846
  "├── .claude/skills/ ← JFL skills\n" +