@gobing-ai/spur 0.3.26 → 0.3.27

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 (61) hide show
  1. package/config/rules/recommended-pre-check.yaml +1 -0
  2. package/config/rules/strict/runtime-boundaries.yaml +23 -16
  3. package/config/rules/ui/ui-import-boundary.yaml +7 -4
  4. package/config/tasks/section-matrix.yaml +7 -7
  5. package/config/workflows/task-pipeline.yaml +9 -0
  6. package/package.json +1 -1
  7. package/spur.js +2007 -469
  8. package/web/_astro/BoardApp.Bk3T0o6s.js +1 -0
  9. package/web/_astro/BoardApp.BwAdAgK9.js +179 -0
  10. package/web/_astro/{TaskDetail.Dsk9DnGY.js → TaskDetail.L36slEqE.js} +1 -1
  11. package/web/_astro/{arc.Biv8k-OR.js → arc.CO-I_PZe.js} +1 -1
  12. package/web/_astro/{architectureDiagram-3BPJPVTR.BrtW5Sn7.js → architectureDiagram-3BPJPVTR.Si-A1Ncl.js} +1 -1
  13. package/web/_astro/{blockDiagram-GPEHLZMM.CTpFZJhW.js → blockDiagram-GPEHLZMM.CeNy_ta6.js} +1 -1
  14. package/web/_astro/{c4Diagram-AAUBKEIU._ZJj9H0G.js → c4Diagram-AAUBKEIU.BgN_scEU.js} +1 -1
  15. package/web/_astro/channel.CYdGnFHM.js +1 -0
  16. package/web/_astro/{chunk-2J33WTMH.DD9mDiEM.js → chunk-2J33WTMH.C3oDtmdi.js} +1 -1
  17. package/web/_astro/{chunk-4BX2VUAB.ENDQu3TH.js → chunk-4BX2VUAB.Bv8Fxh4H.js} +1 -1
  18. package/web/_astro/{chunk-55IACEB6.DLpJf8Xp.js → chunk-55IACEB6.BT-TqtV6.js} +1 -1
  19. package/web/_astro/{chunk-727SXJPM.BR6vsvIV.js → chunk-727SXJPM.DkGMsI0B.js} +1 -1
  20. package/web/_astro/{chunk-AQP2D5EJ.DGOBzXM6.js → chunk-AQP2D5EJ.B_-SqGnF.js} +1 -1
  21. package/web/_astro/{chunk-FMBD7UC4.DrghnRtZ.js → chunk-FMBD7UC4.DwR3skMv.js} +1 -1
  22. package/web/_astro/{chunk-ND2GUHAM.BqIqI3oO.js → chunk-ND2GUHAM.BaTQutFl.js} +1 -1
  23. package/web/_astro/{chunk-QZHKN3VN.CJ2RJt0K.js → chunk-QZHKN3VN.BJMSqqJp.js} +1 -1
  24. package/web/_astro/{classDiagram-4FO5ZUOK.Qu1p6crX.js → classDiagram-4FO5ZUOK.SaqCNUPF.js} +1 -1
  25. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Qu1p6crX.js → classDiagram-v2-Q7XG4LA2.SaqCNUPF.js} +1 -1
  26. package/web/_astro/{cose-bilkent-S5V4N54A.7VrRetJ0.js → cose-bilkent-S5V4N54A.BkmjA_E2.js} +1 -1
  27. package/web/_astro/{dagre-BM42HDAG.r4lTyJXQ.js → dagre-BM42HDAG.CNUvWhrX.js} +1 -1
  28. package/web/_astro/{diagram-2AECGRRQ.DahExuaR.js → diagram-2AECGRRQ.C68Cg9uE.js} +1 -1
  29. package/web/_astro/{diagram-5GNKFQAL.B-dSm3op.js → diagram-5GNKFQAL.DbHkPLEn.js} +1 -1
  30. package/web/_astro/{diagram-KO2AKTUF.CQGlKa6a.js → diagram-KO2AKTUF.Dt0EdErs.js} +1 -1
  31. package/web/_astro/{diagram-LMA3HP47.CJ6nomD-.js → diagram-LMA3HP47.Bixs6mnm.js} +1 -1
  32. package/web/_astro/{diagram-OG6HWLK6.ijGl68sM.js → diagram-OG6HWLK6.B4KBI6KT.js} +1 -1
  33. package/web/_astro/{erDiagram-TEJ5UH35.DBAe2Iin.js → erDiagram-TEJ5UH35.C_4i9Xvx.js} +1 -1
  34. package/web/_astro/{flowDiagram-I6XJVG4X.z3iXF6Ku.js → flowDiagram-I6XJVG4X.CfGFczAY.js} +1 -1
  35. package/web/_astro/{ganttDiagram-6RSMTGT7.DP2ouiRt.js → ganttDiagram-6RSMTGT7.C2AIvtmg.js} +1 -1
  36. package/web/_astro/{gitGraphDiagram-PVQCEYII.DizVfgVG.js → gitGraphDiagram-PVQCEYII.HlXuGqQg.js} +1 -1
  37. package/web/_astro/index.D5Wqk2Tw.css +1 -0
  38. package/web/_astro/{infoDiagram-5YYISTIA.BPTTgL0f.js → infoDiagram-5YYISTIA.BWQJsJ4Z.js} +1 -1
  39. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CXdMa4-b.js → ishikawaDiagram-YF4QCWOH.CCHgIHik.js} +1 -1
  40. package/web/_astro/{journeyDiagram-JHISSGLW.ChSKZ-1j.js → journeyDiagram-JHISSGLW.D9M6YwOJ.js} +1 -1
  41. package/web/_astro/{kanban-definition-UN3LZRKU.DUUlW-km.js → kanban-definition-UN3LZRKU.K20D-a1_.js} +1 -1
  42. package/web/_astro/{linear.AzHrSiEQ.js → linear.DeP5Qsol.js} +1 -1
  43. package/web/_astro/{mermaid.core.BajX78G7.js → mermaid.core.ClzxNMPU.js} +4 -4
  44. package/web/_astro/{mindmap-definition-RKZ34NQL.BmdUHcac.js → mindmap-definition-RKZ34NQL.CGn9RUNj.js} +1 -1
  45. package/web/_astro/{pieDiagram-4H26LBE5.Dh59Ur2v.js → pieDiagram-4H26LBE5.CoA92Csb.js} +1 -1
  46. package/web/_astro/{quadrantDiagram-W4KKPZXB.D2qYQmFj.js → quadrantDiagram-W4KKPZXB.CJ_UPjM1.js} +1 -1
  47. package/web/_astro/{requirementDiagram-4Y6WPE33.DW5nnrHl.js → requirementDiagram-4Y6WPE33.KkEPNNUU.js} +1 -1
  48. package/web/_astro/{sankeyDiagram-5OEKKPKP.CMl1PJrk.js → sankeyDiagram-5OEKKPKP.CoCWbQgh.js} +1 -1
  49. package/web/_astro/{sequenceDiagram-3UESZ5HK.U5dq1PA0.js → sequenceDiagram-3UESZ5HK.DUFpV3EP.js} +1 -1
  50. package/web/_astro/{stateDiagram-AJRCARHV.zrt55DVx.js → stateDiagram-AJRCARHV.CgWX0_9m.js} +1 -1
  51. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CKBGQ0dG.js → stateDiagram-v2-BHNVJYJU.BKUXeGSf.js} +1 -1
  52. package/web/_astro/{timeline-definition-PNZ67QCA.CKAlx9rU.js → timeline-definition-PNZ67QCA.CXayAguY.js} +1 -1
  53. package/web/_astro/{vennDiagram-CIIHVFJN.Bgt1eF06.js → vennDiagram-CIIHVFJN.DPak_Elh.js} +1 -1
  54. package/web/_astro/{wardley-L42UT6IY.BF02aioC.js → wardley-L42UT6IY.t3ZlDKd0.js} +1 -1
  55. package/web/_astro/{wardleyDiagram-YWT4CUSO.C94wxcnJ.js → wardleyDiagram-YWT4CUSO.CwPSVASQ.js} +1 -1
  56. package/web/_astro/{xychartDiagram-2RQKCTM6.BwK7SRoW.js → xychartDiagram-2RQKCTM6.CJfrQgpF.js} +1 -1
  57. package/web/index.html +2 -2
  58. package/web/_astro/BoardApp.DH9wI8y7.js +0 -179
  59. package/web/_astro/BoardApp.DZiwQCP7.js +0 -1
  60. package/web/_astro/channel.Ih1_8SAH.js +0 -1
  61. package/web/_astro/index.D3Y4Q0mx.css +0 -1
package/spur.js CHANGED
@@ -37197,6 +37197,29 @@ var init_finding_codes = __esm(() => {
37197
37197
  };
37198
37198
  });
37199
37199
 
37200
+ // ../../packages/config/src/projects.ts
37201
+ import { homedir } from "os";
37202
+ import { join } from "path";
37203
+ function getProjectsFilePath() {
37204
+ if (process.env.SPUR_PROJECTS_FILE) {
37205
+ return process.env.SPUR_PROJECTS_FILE;
37206
+ }
37207
+ return join(homedir(), ".config", "spur", "projects.json");
37208
+ }
37209
+ var projectEntrySchema, projectsFileSchema;
37210
+ var init_projects = __esm(() => {
37211
+ init_zod();
37212
+ projectEntrySchema = exports_external.object({
37213
+ name: exports_external.string().min(1),
37214
+ path: exports_external.string().min(1),
37215
+ port: exports_external.number().int().nonnegative().default(0)
37216
+ });
37217
+ projectsFileSchema = exports_external.object({
37218
+ schema_version: exports_external.literal(1).default(1),
37219
+ projects: exports_external.array(projectEntrySchema).default([])
37220
+ });
37221
+ });
37222
+
37200
37223
  // ../../packages/config/src/index.ts
37201
37224
  function folderPathWithDefault(defaultPath) {
37202
37225
  return exports_external.preprocess((v) => v == null ? undefined : v, exports_external.string().default(defaultPath));
@@ -37248,6 +37271,7 @@ var SPUR_ENV_VARS, SPUR_LOG_LEVELS, DEFAULT_TASKS_DIR = "docs/tasks", DEFAULT_FE
37248
37271
  var init_src = __esm(() => {
37249
37272
  init_zod();
37250
37273
  init_finding_codes();
37274
+ init_projects();
37251
37275
  SPUR_ENV_VARS = {
37252
37276
  nodeEnv: "NODE_ENV",
37253
37277
  port: "PORT",
@@ -38938,14 +38962,14 @@ var init_dist5 = __esm(() => {
38938
38962
 
38939
38963
  // ../../packages/config/src/bundled-config.ts
38940
38964
  import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
38941
- import { dirname as dirname3, join } from "path";
38965
+ import { dirname as dirname3, join as join2 } from "path";
38942
38966
  function bundledConfigRoot() {
38943
38967
  if (cachedRoot !== undefined)
38944
38968
  return cachedRoot;
38945
38969
  let dir = import.meta.dirname;
38946
38970
  while (true) {
38947
38971
  for (const name of BUNDLED_CONFIG_DIRS) {
38948
- const candidate = join(dir, name);
38972
+ const candidate = join2(dir, name);
38949
38973
  if (isBundledConfigDir(candidate)) {
38950
38974
  cachedRoot = candidate;
38951
38975
  return cachedRoot;
@@ -38960,7 +38984,7 @@ function bundledConfigRoot() {
38960
38984
  return cachedRoot;
38961
38985
  }
38962
38986
  function isBundledConfigDir(candidate) {
38963
- return existsSync3(candidate) && statSync4(candidate).isDirectory() && existsSync3(join(candidate, "rules")) && existsSync3(join(candidate, "workflows"));
38987
+ return existsSync3(candidate) && statSync4(candidate).isDirectory() && existsSync3(join2(candidate, "rules")) && existsSync3(join2(candidate, "workflows"));
38964
38988
  }
38965
38989
  function listBundledConfigFiles() {
38966
38990
  const root = bundledConfigRoot();
@@ -38971,7 +38995,7 @@ function listBundledConfigFiles() {
38971
38995
  function walk(dir, relPrefix, filter = /\.(ya?ml|json)$/i) {
38972
38996
  const acc = [];
38973
38997
  for (const entry of readdirSync3(dir)) {
38974
- const abs = join(dir, entry);
38998
+ const abs = join2(dir, entry);
38975
38999
  const rel = relPrefix.length > 0 ? `${relPrefix}/${entry}` : entry;
38976
39000
  if (statSync4(abs).isDirectory()) {
38977
39001
  acc.push(...walk(abs, rel, filter));
@@ -38998,8 +39022,8 @@ var init_template_renderer = () => {};
38998
39022
 
38999
39023
  // ../../packages/config/src/loader.ts
39000
39024
  import { existsSync as existsSync4, statSync as statSync5 } from "fs";
39001
- import { homedir } from "os";
39002
- import { dirname as dirname4, join as join2 } from "path";
39025
+ import { homedir as homedir2 } from "os";
39026
+ import { dirname as dirname4, join as join3 } from "path";
39003
39027
  import { fileURLToPath as fileURLToPath3 } from "url";
39004
39028
  function embeddedSchemasId(embeddedSchemas) {
39005
39029
  if (embeddedSchemas === undefined)
@@ -39022,7 +39046,7 @@ function cacheKey(configPath, opts, validateJsonSchema2) {
39022
39046
  }
39023
39047
  function resolveSchemaSpecifier(specifier, manifestSpecifier) {
39024
39048
  if (specifier === manifestSpecifier) {
39025
- const workspaceManifest = join2(LOADER_DIR, "..", "..", "..", "apps", "cli", "package.json");
39049
+ const workspaceManifest = join3(LOADER_DIR, "..", "..", "..", "apps", "cli", "package.json");
39026
39050
  if (existsSync4(workspaceManifest))
39027
39051
  return workspaceManifest;
39028
39052
  }
@@ -39034,7 +39058,7 @@ function resolveSchemaSpecifier(specifier, manifestSpecifier) {
39034
39058
  }
39035
39059
  }
39036
39060
  function resolveConfigFile(cwd) {
39037
- const projectConfig = join2(cwd ?? process.cwd(), SPUR_CONFIG_DIR, SPUR_CONFIG_FILE);
39061
+ const projectConfig = join3(cwd ?? process.cwd(), SPUR_CONFIG_DIR, SPUR_CONFIG_FILE);
39038
39062
  if (existsSync4(projectConfig))
39039
39063
  return projectConfig;
39040
39064
  if (process.env.SPUR_SKIP_GLOBAL_CONFIG === "true")
@@ -39074,9 +39098,9 @@ async function loadSpurConfig(cwd = process.cwd(), opts) {
39074
39098
  }
39075
39099
  function expandTilde(path8) {
39076
39100
  if (path8 === "~")
39077
- return homedir();
39101
+ return homedir2();
39078
39102
  if (path8.startsWith("~/"))
39079
- return join2(homedir(), path8.slice(2));
39103
+ return join3(homedir2(), path8.slice(2));
39080
39104
  return path8;
39081
39105
  }
39082
39106
  function expandTeamTildes(config4) {
@@ -39163,7 +39187,7 @@ async function resolvePlanningFolders(fs2) {
39163
39187
  return promise2;
39164
39188
  }
39165
39189
  async function resolvePlanningFoldersUncached(fs2) {
39166
- const configPath = fs2.resolve(join2(SPUR_CONFIG_DIR, SPUR_CONFIG_FILE));
39190
+ const configPath = fs2.resolve(join3(SPUR_CONFIG_DIR, SPUR_CONFIG_FILE));
39167
39191
  if (!await fs2.exists(configPath))
39168
39192
  return defaultPlanningFolders();
39169
39193
  try {
@@ -39200,7 +39224,8 @@ var init_loader = __esm(() => {
39200
39224
  init_src();
39201
39225
  init_bundled_config();
39202
39226
  init_template_renderer();
39203
- GLOBAL_CONFIG_FILE = join2(homedir(), ".config", "spur", "config.yaml");
39227
+ init_projects();
39228
+ GLOBAL_CONFIG_FILE = join3(homedir2(), ".config", "spur", "config.yaml");
39204
39229
  LOADER_DIR = dirname4(fileURLToPath3(import.meta.url));
39205
39230
  embeddedSchemasIds = new WeakMap;
39206
39231
  spurConfigCache = new Map;
@@ -51376,16 +51401,21 @@ var init_run_dao = __esm(() => {
51376
51401
  });
51377
51402
  }
51378
51403
  traceRows(filter) {
51379
- return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, started_at, completed_at, metadata_json
51404
+ return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json
51380
51405
  FROM runs
51381
51406
  WHERE (?1 IS NULL OR workflow_name = ?1)
51382
51407
  AND (?2 IS NULL OR status = ?2)
51383
51408
  AND (?3 IS NULL OR started_at >= ?3)
51384
- ORDER BY started_at DESC
51385
- LIMIT ?4`, filter.workflow ?? null, filter.status ?? null, filter.since ?? null, filter.limit);
51409
+ AND (
51410
+ ?4 IS NULL
51411
+ OR started_at < ?4
51412
+ OR (started_at = ?4 AND id < ?5)
51413
+ )
51414
+ ORDER BY started_at DESC, id DESC
51415
+ LIMIT ?6`, filter.workflow ?? null, filter.status ?? null, filter.since ?? null, filter.before?.started_at ?? null, filter.before?.id ?? null, filter.limit);
51386
51416
  }
51387
51417
  traceRowById(runId) {
51388
- return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, started_at, completed_at, metadata_json FROM runs WHERE id = ?", runId);
51418
+ return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json FROM runs WHERE id = ?", runId);
51389
51419
  }
51390
51420
  stampMetadata(runId, metadata) {
51391
51421
  return this.adapter.run("UPDATE runs SET metadata_json = ? WHERE id = ?", JSON.stringify(metadata), runId);
@@ -51430,51 +51460,85 @@ class SystemEventDao {
51430
51460
  this.db = db2;
51431
51461
  }
51432
51462
  async insert(input) {
51433
- await this.db.run(`INSERT INTO system_events (id, event_name, occurred_at, actor, payload_json)
51434
- VALUES (?, ?, ?, ?, ?)`, [input.id, input.event_name, input.occurred_at, input.actor ?? null, input.payload_json ?? null]);
51435
- }
51436
- async prune(cap) {
51437
- const before = await this.db.queryFirst("SELECT COUNT(*) AS c FROM system_events");
51438
- await this.db.run(`DELETE FROM system_events
51439
- WHERE id IN (
51440
- SELECT id FROM system_events
51441
- WHERE id NOT IN (
51442
- SELECT id FROM system_events
51443
- ORDER BY occurred_at DESC
51444
- LIMIT ?1
51445
- )
51446
- )`, [cap]);
51447
- const after = await this.db.queryFirst("SELECT COUNT(*) AS c FROM system_events");
51448
- return (before?.c ?? 0) - (after?.c ?? 0);
51463
+ await this.db.run(`INSERT INTO system_events (${SYSTEM_EVENT_COLUMNS})
51464
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`, input.id, input.event_name, input.occurred_at, input.actor ?? null, input.payload_json ?? null, input.run_id ?? null, input.entity_kind ?? null, input.entity_id ?? null, input.sequence ?? null);
51465
+ }
51466
+ async pruneQuotas(quotas, prefix) {
51467
+ try {
51468
+ const entries = prefix !== undefined ? quotas.filter((q) => q.prefix === prefix) : quotas;
51469
+ let deleted = 0;
51470
+ for (const { prefix: p, quota } of entries) {
51471
+ const pattern = `${p}.%`;
51472
+ const before = await this.db.queryFirst("SELECT COUNT(*) AS c FROM system_events WHERE event_name LIKE ?1", pattern);
51473
+ await this.db.run(`DELETE FROM system_events
51474
+ WHERE event_name LIKE ?1
51475
+ AND id NOT IN (
51476
+ SELECT id FROM system_events
51477
+ WHERE event_name LIKE ?1
51478
+ ORDER BY occurred_at DESC
51479
+ LIMIT ?2
51480
+ )`, pattern, quota);
51481
+ const after = await this.db.queryFirst("SELECT COUNT(*) AS c FROM system_events WHERE event_name LIKE ?1", pattern);
51482
+ deleted += (before?.c ?? 0) - (after?.c ?? 0);
51483
+ }
51484
+ return deleted;
51485
+ } catch (error51) {
51486
+ if (error51 instanceof Error && error51.message.includes("no such table: system_events")) {
51487
+ return 0;
51488
+ }
51489
+ throw error51;
51490
+ }
51449
51491
  }
51450
51492
  async query(spec = {}) {
51451
51493
  const limit = spec.limit ?? 100;
51494
+ const clauses = [];
51495
+ const params = [];
51496
+ const pushEq = (column2, value) => {
51497
+ params.push(value);
51498
+ clauses.push(`${column2} = ?${params.length}`);
51499
+ };
51500
+ if (spec.name !== undefined)
51501
+ pushEq("event_name", spec.name);
51502
+ if (spec.names !== undefined && spec.names.length > 0) {
51503
+ const placeholders = [];
51504
+ for (const name of spec.names) {
51505
+ params.push(name);
51506
+ placeholders.push(`?${params.length}`);
51507
+ }
51508
+ clauses.push(`event_name IN (${placeholders.join(", ")})`);
51509
+ }
51510
+ if (spec.prefix !== undefined) {
51511
+ const escaped = spec.prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
51512
+ params.push(`${escaped}.%`);
51513
+ clauses.push(`event_name LIKE ?${params.length} ESCAPE '\\'`);
51514
+ }
51515
+ if (spec.since !== undefined) {
51516
+ params.push(spec.since);
51517
+ clauses.push(`occurred_at > ?${params.length}`);
51518
+ }
51519
+ if (spec.run_id !== undefined)
51520
+ pushEq("run_id", spec.run_id);
51521
+ if (spec.entity_kind !== undefined)
51522
+ pushEq("entity_kind", spec.entity_kind);
51523
+ if (spec.entity_id !== undefined)
51524
+ pushEq("entity_id", spec.entity_id);
51525
+ if (spec.actor !== undefined)
51526
+ pushEq("actor", spec.actor);
51527
+ if (spec.before !== undefined) {
51528
+ params.push(spec.before.occurred_at);
51529
+ const atIdx = params.length;
51530
+ params.push(spec.before.id);
51531
+ const idIdx = params.length;
51532
+ clauses.push(`(occurred_at < ?${atIdx} OR (occurred_at = ?${atIdx} AND id < ?${idIdx}))`);
51533
+ }
51534
+ const where = clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`;
51535
+ params.push(limit);
51452
51536
  try {
51453
- if (spec.name !== undefined && spec.since !== undefined) {
51454
- return await this.db.queryAll(`SELECT id, event_name, occurred_at, actor, payload_json
51455
- FROM system_events
51456
- WHERE event_name = ?1 AND occurred_at > ?2
51457
- ORDER BY occurred_at DESC
51458
- LIMIT ?3`, spec.name, spec.since, limit);
51459
- }
51460
- if (spec.name !== undefined) {
51461
- return await this.db.queryAll(`SELECT id, event_name, occurred_at, actor, payload_json
51462
- FROM system_events
51463
- WHERE event_name = ?1
51464
- ORDER BY occurred_at DESC
51465
- LIMIT ?2`, spec.name, limit);
51466
- }
51467
- if (spec.since !== undefined) {
51468
- return await this.db.queryAll(`SELECT id, event_name, occurred_at, actor, payload_json
51469
- FROM system_events
51470
- WHERE occurred_at > ?1
51471
- ORDER BY occurred_at DESC
51472
- LIMIT ?2`, spec.since, limit);
51473
- }
51474
- return await this.db.queryAll(`SELECT id, event_name, occurred_at, actor, payload_json
51537
+ return await this.db.queryAll(`SELECT ${SYSTEM_EVENT_COLUMNS}
51475
51538
  FROM system_events
51476
- ORDER BY occurred_at DESC
51477
- LIMIT ?1`, limit);
51539
+ ${where}
51540
+ ORDER BY occurred_at DESC, id DESC
51541
+ LIMIT ?${params.length}`, ...params);
51478
51542
  } catch (error51) {
51479
51543
  if (error51 instanceof Error && error51.message.includes("no such table: system_events")) {
51480
51544
  return [];
@@ -51486,6 +51550,7 @@ class SystemEventDao {
51486
51550
  await this.db.run("DELETE FROM system_events");
51487
51551
  }
51488
51552
  }
51553
+ var SYSTEM_EVENT_COLUMNS = "id, event_name, occurred_at, actor, payload_json, run_id, entity_kind, entity_id, sequence";
51489
51554
 
51490
51555
  // ../../packages/domain/src/dao/task-run-link-dao.ts
51491
51556
  class TaskRunLinkDao {
@@ -57586,8 +57651,7 @@ var init_schema3 = __esm(() => {
57586
57651
  });
57587
57652
 
57588
57653
  // ../../packages/domain/src/migrations.ts
57589
- import { readdir as readdir2 } from "fs/promises";
57590
- import { join as join4 } from "path";
57654
+ import { join as join5 } from "path";
57591
57655
  async function applyCliMigrations(adapter, migrations = CLI_MIGRATIONS) {
57592
57656
  await adapter.exec('CREATE TABLE IF NOT EXISTS "__spur_cli_migrations" (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)');
57593
57657
  let applied = 0;
@@ -57608,12 +57672,14 @@ async function applyCliMigrations(adapter, migrations = CLI_MIGRATIONS) {
57608
57672
  return applied;
57609
57673
  }
57610
57674
  async function loadSqlMigrations(folder) {
57611
- const entries = (await readdir2(folder)).filter((entry) => entry.endsWith(".sql") && entry.includes(CLI_MIGRATION_FILE_MARKER)).sort((left, right) => left.localeCompare(right));
57675
+ const fs3 = createNodeFileSystem3();
57676
+ const rawEntries = await fs3.readDir(folder);
57677
+ const entries = rawEntries.filter((entry) => entry.endsWith(".sql") && entry.includes(CLI_MIGRATION_FILE_MARKER)).sort((left, right) => left.localeCompare(right));
57612
57678
  const migrations = [];
57613
57679
  for (const entry of entries) {
57614
57680
  migrations.push({
57615
57681
  id: entry.replace(/\.sql$/, ""),
57616
- sql: await Bun.file(join4(folder, entry)).text()
57682
+ sql: await fs3.readFile(join5(folder, entry))
57617
57683
  });
57618
57684
  }
57619
57685
  return migrations.length > 0 ? migrations : CLI_MIGRATIONS;
@@ -57664,20 +57730,35 @@ CREATE TABLE IF NOT EXISTS system_events (
57664
57730
  event_name TEXT NOT NULL,
57665
57731
  occurred_at TEXT NOT NULL,
57666
57732
  actor TEXT,
57667
- payload_json TEXT
57733
+ payload_json TEXT,
57734
+ run_id TEXT,
57735
+ entity_kind TEXT,
57736
+ entity_id TEXT,
57737
+ sequence INTEGER
57668
57738
  );
57669
57739
 
57670
57740
  CREATE INDEX IF NOT EXISTS idx_system_events_occurred_at ON system_events (occurred_at);
57671
57741
  CREATE INDEX IF NOT EXISTS idx_system_events_event_name ON system_events (event_name);
57742
+ CREATE INDEX IF NOT EXISTS idx_system_events_run_id ON system_events (run_id);
57743
+ CREATE INDEX IF NOT EXISTS idx_system_events_entity ON system_events (entity_kind, entity_id);
57672
57744
  `, CLI_SCHEMA_SQL, RUN_PID_COLUMN_SCHEMA_SQL = `
57673
57745
  ALTER TABLE runs ADD COLUMN pid INTEGER;
57674
57746
  `, RUNS_EXTERNAL_KEY_COLUMN_SCHEMA_SQL = `
57675
57747
  ALTER TABLE runs ADD COLUMN external_key TEXT;
57748
+ `, SYSTEM_EVENTS_CORRELATION_COLUMNS_SCHEMA_SQL = `
57749
+ ALTER TABLE system_events ADD COLUMN run_id TEXT;
57750
+ ALTER TABLE system_events ADD COLUMN entity_kind TEXT;
57751
+ ALTER TABLE system_events ADD COLUMN entity_id TEXT;
57752
+ ALTER TABLE system_events ADD COLUMN sequence INTEGER;
57753
+
57754
+ CREATE INDEX IF NOT EXISTS idx_system_events_run_id ON system_events (run_id);
57755
+ CREATE INDEX IF NOT EXISTS idx_system_events_entity ON system_events (entity_kind, entity_id);
57676
57756
  `, CLI_MIGRATIONS, CLI_MIGRATION_FILE_MARKER = "_spur_cli_";
57677
57757
  var init_migrations = __esm(() => {
57678
57758
  init_dist7();
57679
57759
  init_dist8();
57680
57760
  init_dist10();
57761
+ init_dist5();
57681
57762
  init_schema3();
57682
57763
  CLI_SCHEMA_SQL = `
57683
57764
  ${DOMAIN_SCHEMA_SQL}
@@ -57712,6 +57793,11 @@ ${SYSTEM_EVENTS_SCHEMA_SQL}
57712
57793
  id: "0007_spur_cli_runs_external_key",
57713
57794
  sql: RUNS_EXTERNAL_KEY_COLUMN_SCHEMA_SQL,
57714
57795
  addColumnIfMissing: { table: "runs", column: "external_key" }
57796
+ },
57797
+ {
57798
+ id: "0008_spur_cli_system_events_correlation",
57799
+ sql: SYSTEM_EVENTS_CORRELATION_COLUMNS_SCHEMA_SQL,
57800
+ addColumnIfMissing: { table: "system_events", column: "sequence" }
57715
57801
  }
57716
57802
  ];
57717
57803
  });
@@ -58895,12 +58981,12 @@ var init_envelope = __esm(() => {
58895
58981
 
58896
58982
  // ../../packages/domain/src/planning/locks.ts
58897
58983
  import { closeSync, fsyncSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync7, renameSync as renameSync3, unlinkSync, writeSync } from "fs";
58898
- import { dirname as dirname7, join as join5 } from "path";
58984
+ import { dirname as dirname7, join as join6 } from "path";
58899
58985
  function lockPathForEntity(folder, entityId) {
58900
- return join5(folder, `.${entityId}.lock`);
58986
+ return join6(folder, `.${entityId}.lock`);
58901
58987
  }
58902
58988
  function createLockPath(folder) {
58903
- return join5(folder, ".create.lock");
58989
+ return join6(folder, ".create.lock");
58904
58990
  }
58905
58991
  function parseLockContent(raw) {
58906
58992
  const str = typeof raw === "string" ? raw : raw.toString();
@@ -59005,7 +59091,7 @@ function fsyncPath(path9, swallowErrors = false) {
59005
59091
  function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
59006
59092
  const dir = dirname7(dest);
59007
59093
  const rand = Math.random().toString(36).slice(2, 10);
59008
- const tempPath = join5(dir, `.${projectName}.${entityId}.${rand}.tmp`);
59094
+ const tempPath = join6(dir, `.${projectName}.${entityId}.${rand}.tmp`);
59009
59095
  fs3.writeFile(tempPath, content);
59010
59096
  fsyncPath(tempPath);
59011
59097
  renameSync3(tempPath, dest);
@@ -59014,7 +59100,7 @@ function atomicWrite(dest, content, entityId, fs3, projectName = "spur") {
59014
59100
  async function atomicWriteAsync(dest, content, entityId, fs3, projectName = "spur") {
59015
59101
  const dir = dirname7(dest);
59016
59102
  const rand = Math.random().toString(36).slice(2, 10);
59017
- const tempPath = join5(dir, `.${projectName}.${entityId}.${rand}.tmp`);
59103
+ const tempPath = join6(dir, `.${projectName}.${entityId}.${rand}.tmp`);
59018
59104
  await fs3.writeFile(tempPath, content);
59019
59105
  fsyncPath(tempPath);
59020
59106
  renameSync3(tempPath, dest);
@@ -59029,7 +59115,7 @@ function cleanupTempFiles(folder, entityId, fs3, projectName = "spur") {
59029
59115
  let count3 = 0;
59030
59116
  for (const entry of entries) {
59031
59117
  if (entry.startsWith(prefix) && entry.endsWith(suffix)) {
59032
- fs3.deleteFile(join5(folder, entry));
59118
+ fs3.deleteFile(join6(folder, entry));
59033
59119
  count3++;
59034
59120
  }
59035
59121
  }
@@ -60093,6 +60179,7 @@ __export(exports_src, {
60093
60179
  SystemEventDao: () => SystemEventDao,
60094
60180
  StageRegistryError: () => StageRegistryError,
60095
60181
  SYSTEM_EVENTS_SCHEMA_SQL: () => SYSTEM_EVENTS_SCHEMA_SQL,
60182
+ SYSTEM_EVENTS_CORRELATION_COLUMNS_SCHEMA_SQL: () => SYSTEM_EVENTS_CORRELATION_COLUMNS_SCHEMA_SQL,
60096
60183
  STAGE_REGISTRY_SCHEMA_VERSION: () => STAGE_REGISTRY_SCHEMA_VERSION,
60097
60184
  STAGE_ID_PATTERN: () => STAGE_ID_PATTERN,
60098
60185
  STABLE_LAYER_NAMES: () => STABLE_LAYER_NAMES,
@@ -60163,7 +60250,6 @@ function bridgeEventBus(serverBus) {
60163
60250
  }
60164
60251
 
60165
60252
  // ../../packages/app/src/services/agent-service.ts
60166
- import { stat } from "fs/promises";
60167
60253
  import { isatty as isatty3 } from "tty";
60168
60254
 
60169
60255
  class AgentService {
@@ -60567,7 +60653,7 @@ class AgentService {
60567
60653
  }
60568
60654
  async statCwd(cwd) {
60569
60655
  try {
60570
- return await stat(cwd);
60656
+ return await createNodeFileSystem3(this.ctx.cwd).stat(cwd);
60571
60657
  } catch {
60572
60658
  return null;
60573
60659
  }
@@ -60746,7 +60832,7 @@ var init_agent_service = __esm(() => {
60746
60832
  });
60747
60833
 
60748
60834
  // ../../packages/app/src/services/corpus-migrator.ts
60749
- import { join as join6, relative } from "path";
60835
+ import { join as join7, relative } from "path";
60750
60836
  function applyM1(data, flags) {
60751
60837
  const raw = data.status;
60752
60838
  if (typeof raw !== "string")
@@ -60955,15 +61041,17 @@ function yamlScalar(value) {
60955
61041
  }
60956
61042
  async function loadGitLastModified(cwd, dir) {
60957
61043
  try {
60958
- const proc = Bun.spawn(["git", "log", "--format=%aI", "--name-only", "-z", "--", dir], {
61044
+ const result = await new NodeProcessExecutor3().run({
61045
+ command: "git",
61046
+ args: ["log", "--format=%aI", "--name-only", "-z", "--", dir],
60959
61047
  cwd,
60960
- stdout: "pipe",
60961
- stderr: "pipe"
61048
+ maxOutput: 32 * 1024 * 1024,
61049
+ forceBuffered: true,
61050
+ rejectOnError: false
60962
61051
  });
60963
- const output2 = await new Response(proc.stdout).text();
60964
- if (await proc.exited !== 0)
61052
+ if (result.exitCode !== 0)
60965
61053
  return new Map;
60966
- return parseGitNameOnlyZ(output2);
61054
+ return parseGitNameOnlyZ(result.stdout);
60967
61055
  } catch {
60968
61056
  return new Map;
60969
61057
  }
@@ -61044,7 +61132,7 @@ class CorpusMigrator {
61044
61132
  }
61045
61133
  async discoverFiles() {
61046
61134
  const entries = await this.fs.readDir(this.corpusDir);
61047
- return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) => join6(this.corpusDir, name)).sort();
61135
+ return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) => join7(this.corpusDir, name)).sort();
61048
61136
  }
61049
61137
  async migrateFile(filePath, dryRun, gitDates = new Map) {
61050
61138
  const flags = [];
@@ -61106,8 +61194,8 @@ class CorpusMigrator {
61106
61194
  applyM5(data, filenameParent ?? "", flags);
61107
61195
  applyM6(data);
61108
61196
  const gitDate = gitDates.get(relative(this.fs.getProjectRoot(), filePath)) ?? null;
61109
- const stat2 = await this.fs.stat(filePath);
61110
- const mtime = stat2 ? new Date(stat2.mtimeMs).toISOString() : null;
61197
+ const stat = await this.fs.stat(filePath);
61198
+ const mtime = stat ? new Date(stat.mtimeMs).toISOString() : null;
61111
61199
  applyM7(data, gitDate, mtime, flags);
61112
61200
  const tags = coerceArray(data.tags);
61113
61201
  if (tags !== undefined)
@@ -61167,6 +61255,7 @@ var PRIORITY_MAP, FIELD_ORDER, NULL_PRESERVING_KEYS, ISO_TIMESTAMP_RE;
61167
61255
  var init_corpus_migrator = __esm(() => {
61168
61256
  init_src();
61169
61257
  init_src2();
61258
+ init_dist5();
61170
61259
  init_dist2();
61171
61260
  PRIORITY_MAP = {
61172
61261
  p0: "P0",
@@ -61331,24 +61420,42 @@ function event(name, source, renderer, payloadPolicy = "metadata-only", tier = "
61331
61420
  function systemEventCatalogEntry(name) {
61332
61421
  return SYSTEM_EVENT_CATALOG.find((entry) => entry.name === name);
61333
61422
  }
61334
- function normalizeSystemEventPayload(entry, eventPayload) {
61423
+ function normalizeSystemEventPayload(entry, eventPayload, secretValues = []) {
61335
61424
  if (eventPayload === null || eventPayload === undefined)
61336
61425
  return null;
61337
- if (typeof eventPayload !== "object")
61338
- return { value: eventPayload };
61426
+ if (typeof eventPayload !== "object") {
61427
+ return { value: redactSecretValue(eventPayload, secretValues) };
61428
+ }
61339
61429
  const source = eventPayload;
61340
61430
  if (entry.payloadPolicy === "raw-safe")
61341
- return { ...source };
61431
+ return redactSecretValues({ ...source }, secretValues);
61342
61432
  const redacted = { ...source };
61343
61433
  for (const key of ["body", "content", "message", "prompt", "query", "response", "value"]) {
61344
61434
  if (key in redacted) {
61345
61435
  redacted[key] = "[redacted]";
61346
61436
  }
61347
61437
  }
61348
- return redacted;
61438
+ return redactSecretValues(redacted, secretValues);
61349
61439
  }
61350
- var SYSTEM_EVENT_CATALOG, SYSTEM_EVENT_NAMES, SYSTEM_EVENT_PERSISTED_NAMES, SYSTEM_EVENT_STREAMED_NAMES, SYSTEM_EVENT_PREFIXES, SYSTEM_EVENT_DEFAULT_NAMES, SYSTEM_EVENT_DIAGNOSTIC_NAMES, PLANNING_EVENT_NAMES, SYSTEM_EVENT_CATALOG_METADATA;
61440
+ function redactSecretValue(value, secretValues) {
61441
+ if (typeof value === "string")
61442
+ return redactAndBound(value, secretValues, MAX_FIELD_LENGTH);
61443
+ if (Array.isArray(value))
61444
+ return value.map((item) => redactSecretValue(item, secretValues));
61445
+ if (value !== null && typeof value === "object") {
61446
+ return redactSecretValues({ ...value }, secretValues);
61447
+ }
61448
+ return value;
61449
+ }
61450
+ function redactSecretValues(payload, secretValues) {
61451
+ for (const key of Object.keys(payload)) {
61452
+ payload[key] = redactSecretValue(payload[key], secretValues);
61453
+ }
61454
+ return payload;
61455
+ }
61456
+ var SYSTEM_EVENT_CATALOG, SYSTEM_EVENT_NAMES, SYSTEM_EVENT_PERSISTED_NAMES, SYSTEM_EVENT_STREAMED_NAMES, SYSTEM_EVENT_PREFIXES, SYSTEM_EVENT_DEFAULT_NAMES, SYSTEM_EVENT_DIAGNOSTIC_NAMES, PLANNING_EVENT_NAMES, SYSTEM_EVENT_CATALOG_METADATA, MAX_FIELD_LENGTH = 256;
61351
61457
  var init_event_names = __esm(() => {
61458
+ init_agent_execution();
61352
61459
  SYSTEM_EVENT_CATALOG = [
61353
61460
  event("task.created", "planning", "planning"),
61354
61461
  event("task.updated", "planning", "planning"),
@@ -61358,12 +61465,12 @@ var init_event_names = __esm(() => {
61358
61465
  event("feature.transitioned", "planning", "planning"),
61359
61466
  event("queue.consumer.started", "queue", "queue"),
61360
61467
  event("queue.consumer.stopped", "queue", "queue"),
61361
- event("queue.job.enqueued", "queue", "queue"),
61362
- event("queue.job.completed", "queue", "queue"),
61468
+ event("queue.job.enqueued", "queue", "queue", "metadata-only", "diagnostic"),
61469
+ event("queue.job.completed", "queue", "queue", "metadata-only", "diagnostic"),
61363
61470
  event("queue.job.failed", "queue", "queue"),
61364
61471
  event("queue.job.retrying", "queue", "queue"),
61365
61472
  event("queue.stats", "queue", "queue"),
61366
- event("scheduler.job.executed", "scheduler", "scheduler"),
61473
+ event("scheduler.job.executed", "scheduler", "scheduler", "metadata-only", "diagnostic"),
61367
61474
  event("message.sent", "message", "message"),
61368
61475
  event("message.replied", "message", "message"),
61369
61476
  event("process.spawned", "process", "process"),
@@ -61375,6 +61482,11 @@ var init_event_names = __esm(() => {
61375
61482
  event("agent.started", "agent", "agent"),
61376
61483
  event("agent.stopped", "agent", "agent"),
61377
61484
  event("agent.message.sent", "agent", "agent"),
61485
+ event("team.up", "team", "team"),
61486
+ event("team.down", "team", "team"),
61487
+ event("team.member.assigned", "team", "team"),
61488
+ event("team.member.started", "team", "team"),
61489
+ event("team.member.stopped", "team", "team"),
61378
61490
  event("rule.run.start", "rule", "rule"),
61379
61491
  event("rule.eval.start", "rule", "rule"),
61380
61492
  event("rule.eval.done", "rule", "rule"),
@@ -61403,6 +61515,8 @@ var init_event_names = __esm(() => {
61403
61515
  event("workflow.hitl.response", "workflow", "workflow-hitl", "redacted"),
61404
61516
  event("workflow.hitl.note", "workflow", "workflow-hitl", "redacted"),
61405
61517
  event("workflow.custom", "workflow", "workflow-custom"),
61518
+ event("workflow.agent", "workflow", "workflow-agent", "redacted", "diagnostic"),
61519
+ event("workflow.steering", "workflow", "workflow-steering", "redacted"),
61406
61520
  event("api.request.error", "api", "api"),
61407
61521
  event("bus.emit.done", "bus", "bus", "metadata-only", "diagnostic"),
61408
61522
  event("bus.emit.noop", "bus", "bus", "metadata-only", "diagnostic"),
@@ -61562,13 +61676,13 @@ __export(exports_feature_check, {
61562
61676
  FeatureCheckService: () => FeatureCheckService,
61563
61677
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX
61564
61678
  });
61565
- import { dirname as dirname8, join as join7 } from "path";
61679
+ import { dirname as dirname8, join as join8 } from "path";
61566
61680
  function defaultVerdictRunDir(tasksDir) {
61567
61681
  const norm = tasksDir.replace(/\\/g, "/");
61568
61682
  if (/\/docs\/tasks\d*$/.test(norm) || /\/docs\/tasks$/.test(norm)) {
61569
- return join7(dirname8(dirname8(tasksDir)), ".spur", "run");
61683
+ return join8(dirname8(dirname8(tasksDir)), ".spur", "run");
61570
61684
  }
61571
- return join7(dirname8(tasksDir), ".spur", "run");
61685
+ return join8(dirname8(tasksDir), ".spur", "run");
61572
61686
  }
61573
61687
  function rowMatchesScenario(id, sc) {
61574
61688
  const stripped = id.replace(/^Scenario:\s*/i, "").trim();
@@ -61638,7 +61752,7 @@ var init_feature_check = __esm(() => {
61638
61752
  await this.checkOneActiveGoal(fm, featureId2, options.featuresDir, findings);
61639
61753
  await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
61640
61754
  }
61641
- const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join7(dirname8(options.featuresDir), "dogfood") : undefined);
61755
+ const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join8(dirname8(options.featuresDir), "dogfood") : undefined);
61642
61756
  const runDir = options?.runDir ?? (options?.tasksDir ? defaultVerdictRunDir(options.tasksDir) : undefined);
61643
61757
  await this.runL4(doc2, featureId2, status, options?.tasksDir, dogfoodDir, runDir, findings);
61644
61758
  return { id: featureId2, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
@@ -61831,7 +61945,7 @@ var init_feature_check = __esm(() => {
61831
61945
  for (const taskAc of linkedTaskAc) {
61832
61946
  if (stillOrphan.size === 0)
61833
61947
  break;
61834
- const orphanedByThisTask = new Set(checkAcCoverage(acBody, taskAc).orphans);
61948
+ const orphanedByThisTask = new Set(checkAcCoverage(acBody, taskAc, parseChecklist(taskAc)).orphans);
61835
61949
  stillOrphan = new Set([...stillOrphan].filter((o2) => orphanedByThisTask.has(o2)));
61836
61950
  }
61837
61951
  for (const orphan of stillOrphan) {
@@ -61899,7 +62013,7 @@ var init_feature_check = __esm(() => {
61899
62013
  for (const task of linkedTasks) {
61900
62014
  const taskCov = checkAcCoverage(`Feature: x
61901
62015
  Scenario: ${sc.title}
61902
- Given x`, task.ac);
62016
+ Given x`, task.ac, parseChecklist(task.ac));
61903
62017
  if (!taskCov.orphans.includes(sc.title)) {
61904
62018
  linked.push({ wbs: task.wbs, status: task.status });
61905
62019
  }
@@ -61940,7 +62054,7 @@ var init_feature_check = __esm(() => {
61940
62054
  }
61941
62055
  async readVerdictArtifact(runDir, wbs) {
61942
62056
  try {
61943
- const raw = await this.fs.readFile(join7(runDir, `${wbs}-verdict.json`));
62057
+ const raw = await this.fs.readFile(join8(runDir, `${wbs}-verdict.json`));
61944
62058
  const parsed = JSON.parse(raw);
61945
62059
  if (typeof parsed.verdict !== "string")
61946
62060
  return null;
@@ -62027,6 +62141,9 @@ var init_task_locator = __esm(() => {
62027
62141
  });
62028
62142
 
62029
62143
  // ../../packages/app/src/services/feature-service.ts
62144
+ function isFeatureActionName(action) {
62145
+ return FEATURE_ACTION_NAMES.includes(action);
62146
+ }
62030
62147
  function stripLeadingSectionHeader(body, sectionName) {
62031
62148
  const match = body.match(/^(\s*)#{1,6}\s+(.+?)\s*(?:\n|$)/);
62032
62149
  if (match === null)
@@ -62619,6 +62736,23 @@ Feature: ${name}
62619
62736
  ## History
62620
62737
  `;
62621
62738
  }
62739
+ async fulfillAction(featureId2, action, enqueue, options) {
62740
+ const feature = await this.show(featureId2);
62741
+ if (!feature) {
62742
+ throw new Error(`Feature not found: ${featureId2}`);
62743
+ }
62744
+ if (!isFeatureActionName(action)) {
62745
+ throw new Error(`Unsupported feature action: ${action}`);
62746
+ }
62747
+ const runId = await enqueue({
62748
+ featureId: featureId2,
62749
+ action,
62750
+ command: FEATURE_ACTION_COMMANDS[action](featureId2),
62751
+ channel: options?.channel,
62752
+ skipDeps: options?.skipDeps
62753
+ });
62754
+ return { runId, action, status: "queued" };
62755
+ }
62622
62756
  }
62623
62757
  function renderIndex(features) {
62624
62758
  const sorted = [...features].sort((a2, b) => a2.id.localeCompare(b.id));
@@ -62671,11 +62805,16 @@ ${body}`;
62671
62805
  function escapePipes(text4) {
62672
62806
  return text4.replace(/\|/g, "\\|");
62673
62807
  }
62674
- var FEATURE_FILE_RE;
62808
+ var FEATURE_ACTION_NAMES, FEATURE_ACTION_COMMANDS, FEATURE_FILE_RE;
62675
62809
  var init_feature_service = __esm(() => {
62676
62810
  init_src2();
62677
62811
  init_feature_check();
62678
62812
  init_task_locator();
62813
+ FEATURE_ACTION_NAMES = ["brainstorm", "plan"];
62814
+ FEATURE_ACTION_COMMANDS = {
62815
+ brainstorm: (id) => `spur dev brainstorm --feature ${id}`,
62816
+ plan: (id) => `spur dev plan --feature ${id}`
62817
+ };
62679
62818
  FEATURE_FILE_RE = /^([A-Z][1-9]*)_(.+)\.md$/;
62680
62819
  });
62681
62820
 
@@ -63092,19 +63231,21 @@ function createPsProcessInspector(platform2 = process.platform, runPs = defaultR
63092
63231
  };
63093
63232
  }
63094
63233
  async function defaultRunPs() {
63095
- const proc = Bun.spawn([...PS_LIST_ARGV], { stdout: "pipe", stderr: "pipe" });
63096
- const [stdout, stderr, exitCode] = await Promise.all([
63097
- new Response(proc.stdout).text(),
63098
- new Response(proc.stderr).text(),
63099
- proc.exited
63100
- ]);
63101
- if (exitCode !== 0) {
63102
- throw new Error(`ps failed (exit ${exitCode}): ${stderr.trim() || "no stderr"}`);
63234
+ const [command, ...args] = PS_LIST_ARGV;
63235
+ const result = await new NodeProcessExecutor3().run({
63236
+ command: command ?? "ps",
63237
+ args,
63238
+ forceBuffered: true,
63239
+ rejectOnError: false
63240
+ });
63241
+ if (result.exitCode !== 0) {
63242
+ throw new Error(`ps failed (exit ${result.exitCode}): ${result.stderr.trim() || "no stderr"}`);
63103
63243
  }
63104
- return stdout;
63244
+ return result.stdout;
63105
63245
  }
63106
63246
  var UnsupportedProcessPlatformError, PS_LIST_ARGV;
63107
63247
  var init_process_inspector = __esm(() => {
63248
+ init_dist5();
63108
63249
  UnsupportedProcessPlatformError = class UnsupportedProcessPlatformError extends Error {
63109
63250
  code = "UNSUPPORTED_PLATFORM";
63110
63251
  constructor(platform2) {
@@ -63230,9 +63371,399 @@ var init_process_inventory_service = __esm(() => {
63230
63371
  init_process_inspector();
63231
63372
  });
63232
63373
 
63374
+ // ../../packages/app/src/services/project-registry.ts
63375
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
63376
+ import { connect, createServer } from "net";
63377
+ import { homedir as homedir3 } from "os";
63378
+ import { dirname as dirname9, join as join9, resolve } from "path";
63379
+ function normalizeProjectPath(pathInput) {
63380
+ const trimmed = pathInput.trim();
63381
+ let expanded = trimmed;
63382
+ if (trimmed === "~") {
63383
+ expanded = homedir3();
63384
+ } else if (trimmed.startsWith("~/")) {
63385
+ expanded = join9(homedir3(), trimmed.slice(2));
63386
+ } else {
63387
+ expanded = resolve(trimmed);
63388
+ }
63389
+ try {
63390
+ if (existsSync5(expanded)) {
63391
+ return realpathSync3(expanded);
63392
+ }
63393
+ } catch {}
63394
+ return expanded;
63395
+ }
63396
+ async function isPortLiveOnHost(port, host, timeoutMs) {
63397
+ return new Promise((resolve2) => {
63398
+ const socket = connect({ port, host, timeout: timeoutMs });
63399
+ socket.on("connect", () => {
63400
+ socket.destroy();
63401
+ resolve2(true);
63402
+ });
63403
+ socket.on("timeout", () => {
63404
+ socket.destroy();
63405
+ resolve2(false);
63406
+ });
63407
+ socket.on("error", () => {
63408
+ socket.destroy();
63409
+ resolve2(false);
63410
+ });
63411
+ });
63412
+ }
63413
+ async function isPortLive(port, timeoutMs = 200) {
63414
+ if (port <= 0)
63415
+ return false;
63416
+ if (await isPortLiveOnHost(port, "127.0.0.1", timeoutMs))
63417
+ return true;
63418
+ if (await isPortLiveOnHost(port, "::1", timeoutMs))
63419
+ return true;
63420
+ return false;
63421
+ }
63422
+ async function isPortAvailable(port) {
63423
+ if (port <= 0 || port > 65535)
63424
+ return false;
63425
+ return new Promise((resolve2) => {
63426
+ const server = createServer();
63427
+ server.unref();
63428
+ server.on("error", () => resolve2(false));
63429
+ server.listen({ port, host: "127.0.0.1" }, () => {
63430
+ server.close(() => resolve2(true));
63431
+ });
63432
+ });
63433
+ }
63434
+
63435
+ class ProjectRegistry {
63436
+ filePath;
63437
+ lockDir;
63438
+ constructor(customPath) {
63439
+ this.filePath = customPath ?? getProjectsFilePath();
63440
+ this.lockDir = `${this.filePath}.lock`;
63441
+ }
63442
+ async withLock(fn) {
63443
+ const lockParent = dirname9(this.lockDir);
63444
+ if (!existsSync5(lockParent)) {
63445
+ mkdirSync5(lockParent, { recursive: true });
63446
+ }
63447
+ const maxTries = 50;
63448
+ let acquired = false;
63449
+ for (let i2 = 0;i2 < maxTries; i2++) {
63450
+ try {
63451
+ mkdirSync5(this.lockDir);
63452
+ acquired = true;
63453
+ break;
63454
+ } catch {
63455
+ await new Promise((r) => setTimeout(r, 50));
63456
+ }
63457
+ }
63458
+ if (!acquired) {
63459
+ try {
63460
+ rmSync3(this.lockDir, { recursive: true, force: true });
63461
+ mkdirSync5(this.lockDir);
63462
+ acquired = true;
63463
+ } catch {
63464
+ throw new Error(`Failed to acquire lock for project registry: ${this.lockDir}`);
63465
+ }
63466
+ }
63467
+ try {
63468
+ return await fn();
63469
+ } finally {
63470
+ if (acquired) {
63471
+ try {
63472
+ rmSync3(this.lockDir, { recursive: true, force: true });
63473
+ } catch {}
63474
+ }
63475
+ }
63476
+ }
63477
+ readRaw() {
63478
+ if (!existsSync5(this.filePath)) {
63479
+ return { schema_version: 1, projects: [] };
63480
+ }
63481
+ try {
63482
+ const raw = readFileSync8(this.filePath, "utf-8");
63483
+ const json3 = JSON.parse(raw);
63484
+ return projectsFileSchema.parse(json3);
63485
+ } catch {
63486
+ return { schema_version: 1, projects: [] };
63487
+ }
63488
+ }
63489
+ writeRaw(data) {
63490
+ const parentDir2 = dirname9(this.filePath);
63491
+ if (!existsSync5(parentDir2)) {
63492
+ mkdirSync5(parentDir2, { recursive: true });
63493
+ }
63494
+ const tmpFile = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
63495
+ const content = JSON.stringify(data, null, 2);
63496
+ writeFileSync4(tmpFile, content, "utf-8");
63497
+ renameSync4(tmpFile, this.filePath);
63498
+ }
63499
+ async healTildePaths() {
63500
+ return this.withLock(async () => {
63501
+ const data = this.readRaw();
63502
+ let changed = false;
63503
+ for (const project of data.projects) {
63504
+ const normalized = normalizeProjectPath(project.path);
63505
+ if (normalized !== project.path) {
63506
+ project.path = normalized;
63507
+ changed = true;
63508
+ }
63509
+ }
63510
+ if (changed) {
63511
+ this.writeRaw(data);
63512
+ }
63513
+ });
63514
+ }
63515
+ async list() {
63516
+ await this.healTildePaths();
63517
+ await this.healStale();
63518
+ return this.readRaw().projects;
63519
+ }
63520
+ async getByPath(pathInput) {
63521
+ const normalized = normalizeProjectPath(pathInput);
63522
+ const projects3 = await this.list();
63523
+ return projects3.find((p) => normalizeProjectPath(p.path) === normalized);
63524
+ }
63525
+ async getByName(nameInput) {
63526
+ const trimmed = nameInput.trim();
63527
+ const projects3 = await this.list();
63528
+ return projects3.find((p) => p.name === trimmed) ?? projects3.find((p) => p.name.toLowerCase() === trimmed.toLowerCase());
63529
+ }
63530
+ async upsert(entry) {
63531
+ return this.withLock(async () => {
63532
+ const currentData = this.readRaw();
63533
+ const normalizedPath = normalizeProjectPath(entry.path);
63534
+ const index2 = currentData.projects.findIndex((p) => normalizeProjectPath(p.path) === normalizedPath);
63535
+ const newEntry = {
63536
+ name: entry.name,
63537
+ path: normalizedPath,
63538
+ port: entry.port ?? 0
63539
+ };
63540
+ if (index2 >= 0 && currentData.projects[index2]) {
63541
+ const existing = currentData.projects[index2];
63542
+ currentData.projects[index2] = {
63543
+ ...existing,
63544
+ name: entry.name,
63545
+ port: entry.port ?? existing.port
63546
+ };
63547
+ } else {
63548
+ currentData.projects.push(newEntry);
63549
+ }
63550
+ this.writeRaw(currentData);
63551
+ const res = currentData.projects.find((p) => normalizeProjectPath(p.path) === normalizedPath);
63552
+ if (!res) {
63553
+ throw new Error(`Failed to upsert project: ${normalizedPath}`);
63554
+ }
63555
+ return res;
63556
+ });
63557
+ }
63558
+ async remove(nameOrPath) {
63559
+ return this.withLock(async () => {
63560
+ const currentData = this.readRaw();
63561
+ const normalized = normalizeProjectPath(nameOrPath);
63562
+ const initialLen = currentData.projects.length;
63563
+ currentData.projects = currentData.projects.filter((p) => {
63564
+ const pNorm = normalizeProjectPath(p.path);
63565
+ const nameMatch = p.name.toLowerCase() === nameOrPath.trim().toLowerCase();
63566
+ return pNorm !== normalized && !nameMatch;
63567
+ });
63568
+ if (currentData.projects.length !== initialLen) {
63569
+ this.writeRaw(currentData);
63570
+ return true;
63571
+ }
63572
+ return false;
63573
+ });
63574
+ }
63575
+ async setPort(pathInput, port) {
63576
+ return this.withLock(async () => {
63577
+ const currentData = this.readRaw();
63578
+ const normalized = normalizeProjectPath(pathInput);
63579
+ const project = currentData.projects.find((p) => normalizeProjectPath(p.path) === normalized);
63580
+ if (project) {
63581
+ project.port = port;
63582
+ this.writeRaw(currentData);
63583
+ return true;
63584
+ }
63585
+ return false;
63586
+ });
63587
+ }
63588
+ async allocatePort(preferredPort) {
63589
+ if (preferredPort && preferredPort > 0 && await isPortAvailable(preferredPort)) {
63590
+ return preferredPort;
63591
+ }
63592
+ const data = this.readRaw();
63593
+ const claimedPorts = new Set(data.projects.map((p) => p.port).filter((p) => p > 0));
63594
+ for (let port = 3000;port <= 3999; port++) {
63595
+ if (claimedPorts.has(port))
63596
+ continue;
63597
+ if (await isPortAvailable(port)) {
63598
+ return port;
63599
+ }
63600
+ }
63601
+ throw new Error("No available ports in range 3000\u20133999");
63602
+ }
63603
+ async healStale() {
63604
+ return this.withLock(async () => {
63605
+ const data = this.readRaw();
63606
+ let changed = false;
63607
+ for (const project of data.projects) {
63608
+ if (project.port > 0) {
63609
+ const live = await isPortLive(project.port);
63610
+ if (!live) {
63611
+ project.port = 0;
63612
+ changed = true;
63613
+ }
63614
+ }
63615
+ }
63616
+ if (changed) {
63617
+ this.writeRaw(data);
63618
+ }
63619
+ });
63620
+ }
63621
+ }
63622
+ var init_project_registry = __esm(() => {
63623
+ init_src();
63624
+ });
63625
+
63626
+ // ../../packages/app/src/services/project-start.ts
63627
+ import { existsSync as existsSync6 } from "fs";
63628
+ import { basename as basename2, resolve as resolve2 } from "path";
63629
+ function shQuote(value) {
63630
+ return `'${value.replace(/'/g, `'\\''`)}'`;
63631
+ }
63632
+ function flattenEnv(env) {
63633
+ const out = {};
63634
+ for (const [key, value] of Object.entries(env)) {
63635
+ if (value !== undefined)
63636
+ out[key] = value;
63637
+ }
63638
+ return out;
63639
+ }
63640
+ function setDetachedServeSpawnForTests(spawn2) {
63641
+ testDetachedServeSpawn = spawn2;
63642
+ }
63643
+ function resolveSpurServeCommand() {
63644
+ if (process.env.SPUR_CLI_PATH && existsSync6(process.env.SPUR_CLI_PATH)) {
63645
+ return [process.execPath, process.env.SPUR_CLI_PATH];
63646
+ }
63647
+ const argv1 = process.argv[1];
63648
+ if (typeof argv1 === "string" && argv1.length > 0) {
63649
+ const base2 = argv1.replace(/\\/g, "/");
63650
+ if (base2.endsWith("/spur.js") || base2.endsWith("/spur") || base2.includes("/apps/cli/src/index") || base2.endsWith("apps/cli/src/index.ts")) {
63651
+ return [process.execPath, argv1];
63652
+ }
63653
+ }
63654
+ const monorepoCli = resolve2(process.cwd(), "apps/cli/src/index.ts");
63655
+ if (existsSync6(monorepoCli)) {
63656
+ return [process.execPath, monorepoCli];
63657
+ }
63658
+ const fromPath = typeof Bun !== "undefined" && typeof Bun.which === "function" ? Bun.which("spur") : null;
63659
+ if (fromPath) {
63660
+ return [fromPath];
63661
+ }
63662
+ throw new Error("Could not resolve the spur CLI to spawn `spur serve`. Ensure `spur` is on PATH (or invoke start via the monorepo CLI).");
63663
+ }
63664
+ async function startRegisteredProject(registry2, target, options = {}) {
63665
+ let entry = await registry2.getByName(target) ?? await registry2.getByPath(target);
63666
+ if (!entry) {
63667
+ const absPath = normalizeProjectPath(target);
63668
+ if (existsSync6(absPath)) {
63669
+ entry = await registry2.upsert({ path: absPath, name: basename2(absPath), port: 0 });
63670
+ } else {
63671
+ throw new Error(`Project not found in registry: "${target}"`);
63672
+ }
63673
+ }
63674
+ const projectPath = normalizeProjectPath(entry.path);
63675
+ if (!existsSync6(projectPath)) {
63676
+ throw new Error(`Project path does not exist: "${entry.path}" (resolved to ${projectPath}). Update ~/.config/spur/projects.json.`);
63677
+ }
63678
+ if (projectPath !== entry.path) {
63679
+ await registry2.upsert({ name: entry.name, path: projectPath, port: entry.port });
63680
+ entry = { ...entry, path: projectPath };
63681
+ }
63682
+ if (entry.port > 0 && await isPortLive(entry.port)) {
63683
+ return {
63684
+ name: entry.name,
63685
+ path: projectPath,
63686
+ port: entry.port,
63687
+ running: true,
63688
+ url: `http://127.0.0.1:${entry.port}`,
63689
+ alreadyRunning: true
63690
+ };
63691
+ }
63692
+ const allocatedPort = options.port && options.port > 0 ? options.port : await registry2.allocatePort();
63693
+ const invocation = resolveSpurServeCommand();
63694
+ const spawn2 = options.spawn ?? testDetachedServeSpawn ?? defaultDetachedServeSpawn;
63695
+ const child = await Promise.resolve(spawn2([
63696
+ ...invocation,
63697
+ "serve",
63698
+ "--cwd",
63699
+ projectPath,
63700
+ "--port",
63701
+ String(allocatedPort),
63702
+ "--host",
63703
+ "127.0.0.1",
63704
+ "--no-open"
63705
+ ], {
63706
+ cwd: projectPath,
63707
+ detached: true,
63708
+ stdio: ["ignore", "ignore", "ignore"],
63709
+ env: process.env
63710
+ }));
63711
+ const pollAttempts = options.pollAttempts ?? 100;
63712
+ const pollIntervalMs = options.pollIntervalMs ?? 100;
63713
+ let live = false;
63714
+ for (let i2 = 0;i2 < pollAttempts; i2++) {
63715
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
63716
+ if (await isPortLive(allocatedPort)) {
63717
+ live = true;
63718
+ break;
63719
+ }
63720
+ if (child.exitCode !== null) {
63721
+ child.unref();
63722
+ throw new Error(`Project "${entry.name}" serve process exited with code ${child.exitCode} before port ${allocatedPort} became ready (path: ${projectPath}).`);
63723
+ }
63724
+ }
63725
+ child.unref();
63726
+ if (!live) {
63727
+ throw new Error(`Project "${entry.name}" failed to start on port ${allocatedPort} within ${pollAttempts * pollIntervalMs / 1000}s (path: ${projectPath}).`);
63728
+ }
63729
+ await registry2.setPort(projectPath, allocatedPort);
63730
+ return {
63731
+ name: entry.name,
63732
+ path: projectPath,
63733
+ port: allocatedPort,
63734
+ running: true,
63735
+ url: `http://127.0.0.1:${allocatedPort}`,
63736
+ alreadyRunning: false
63737
+ };
63738
+ }
63739
+ var defaultDetachedServeSpawn = async (cmd, options) => {
63740
+ const executor = new NodeProcessExecutor3;
63741
+ const line = cmd.map(shQuote).join(" ");
63742
+ const shell = process.platform === "win32" ? {
63743
+ command: "cmd",
63744
+ args: ["/c", `start /b "" ${cmd.map((c3) => `"${c3.replace(/"/g, '""')}"`).join(" ")}`]
63745
+ } : {
63746
+ command: "/bin/sh",
63747
+ args: ["-c", `nohup ${line} </dev/null >/dev/null 2>&1 &`]
63748
+ };
63749
+ await executor.run({
63750
+ command: shell.command,
63751
+ args: shell.args,
63752
+ ...options.cwd !== undefined ? { cwd: options.cwd } : {},
63753
+ env: flattenEnv(options.env ?? process.env),
63754
+ forceBuffered: true,
63755
+ rejectOnError: false
63756
+ });
63757
+ return { exitCode: null, unref: () => {} };
63758
+ }, testDetachedServeSpawn;
63759
+ var init_project_start = __esm(() => {
63760
+ init_dist5();
63761
+ init_project_registry();
63762
+ });
63763
+
63233
63764
  // ../../packages/app/src/services/rule-service.ts
63234
- import { homedir as homedir2 } from "os";
63235
- import { delimiter, join as join8, relative as relative2, resolve } from "path";
63765
+ import { homedir as homedir4 } from "os";
63766
+ import { delimiter, join as join10, relative as relative2, resolve as resolve3 } from "path";
63236
63767
 
63237
63768
  class RuleService {
63238
63769
  context;
@@ -63411,22 +63942,22 @@ ${ruleIds.join(`
63411
63942
  if (envValue !== undefined && envValue.length > 0) {
63412
63943
  for (const entry of envValue.split(delimiter)) {
63413
63944
  if (entry.length > 0) {
63414
- layers.push({ id: "env-override", path: resolve(cwd, entry), priority: -10 });
63945
+ layers.push({ id: "env-override", path: resolve3(cwd, entry), priority: -10 });
63415
63946
  }
63416
63947
  }
63417
63948
  }
63418
- layers.push({ id: "local", path: resolve(cwd, LOCAL_RULES_DIR), priority: 0 });
63949
+ layers.push({ id: "local", path: resolve3(cwd, LOCAL_RULES_DIR), priority: 0 });
63419
63950
  const globalOverride = env.SPUR_GLOBAL_RULES_DIR;
63420
63951
  const hasGlobalOverride = globalOverride !== undefined && globalOverride.length > 0;
63421
63952
  layers.push({
63422
63953
  id: "global",
63423
- path: hasGlobalOverride ? resolve(cwd, globalOverride) : join8(homedir2(), GLOBAL_RULES_DIR),
63954
+ path: hasGlobalOverride ? resolve3(cwd, globalOverride) : join10(homedir4(), GLOBAL_RULES_DIR),
63424
63955
  priority: 10
63425
63956
  });
63426
63957
  if (opts.includeBundled && !hasGlobalOverride) {
63427
63958
  const bundledConfig = bundledConfigRoot();
63428
63959
  if (bundledConfig !== null) {
63429
- layers.push({ id: "bundled-config", path: join8(bundledConfig, "rules"), priority: 15 });
63960
+ layers.push({ id: "bundled-config", path: join10(bundledConfig, "rules"), priority: 15 });
63430
63961
  }
63431
63962
  const bundled = await bundledRulesRoot();
63432
63963
  if (bundled !== null)
@@ -63576,7 +64107,7 @@ ${ruleIds.join(`
63576
64107
  async collectValidationErrors(source, validateSchema) {
63577
64108
  const { cwd, fs: fs3 } = this.context;
63578
64109
  if (source.kind === "file") {
63579
- const absolute = resolve(cwd, source.value);
64110
+ const absolute = resolve3(cwd, source.value);
63580
64111
  if (!await fs3.exists(absolute))
63581
64112
  return [`File not found: ${absolute}`];
63582
64113
  try {
@@ -63600,7 +64131,7 @@ ${ruleIds.join(`
63600
64131
  const { fs: fs3 } = this.context;
63601
64132
  for (const root of await this.ruleRoots()) {
63602
64133
  for (const ext of ["yaml", "yml", "json"]) {
63603
- if (await fs3.exists(join8(root, `${name}.${ext}`)))
64134
+ if (await fs3.exists(join10(root, `${name}.${ext}`)))
63604
64135
  return true;
63605
64136
  }
63606
64137
  }
@@ -63636,7 +64167,7 @@ ${ruleIds.join(`
63636
64167
  for (const layer of await this.existingRuleSourceLayers()) {
63637
64168
  const entries = await this.context.fs.readDir(layer.path);
63638
64169
  const named = entries.map((entry) => ({ entry, name: entry.match(/^([\w-]+)\.(?:ya?ml|json)$/i)?.[1] ?? "" })).filter(({ name }) => name.length > 0);
63639
- const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(join8(layer.path, entry))));
64170
+ const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(join10(layer.path, entry))));
63640
64171
  named.forEach(({ name }, i2) => {
63641
64172
  if (stats[i2]?.isFile())
63642
64173
  candidates.add(name);
@@ -63658,7 +64189,7 @@ ${ruleIds.join(`
63658
64189
  for (const layer of layers) {
63659
64190
  for (const file2 of await this.listRuleFilesInLayer(layer.path)) {
63660
64191
  if (!merged.has(file2))
63661
- merged.set(file2, { absolutePath: join8(layer.path, file2), source: layer.id });
64192
+ merged.set(file2, { absolutePath: join10(layer.path, file2), source: layer.id });
63662
64193
  }
63663
64194
  }
63664
64195
  const files = [];
@@ -63726,24 +64257,24 @@ ${ruleIds.join(`
63726
64257
  for (const entry of entries.sort()) {
63727
64258
  if (entry === "presets")
63728
64259
  continue;
63729
- const stat2 = await this.context.fs.stat(join8(root, entry));
63730
- if (stat2?.isDirectory())
64260
+ const stat = await this.context.fs.stat(join10(root, entry));
64261
+ if (stat?.isDirectory())
63731
64262
  dirs.push(entry);
63732
64263
  }
63733
64264
  return dirs;
63734
64265
  }
63735
64266
  async listRuleFiles(root, relativeDir) {
63736
64267
  const { fs: fs3 } = this.context;
63737
- const dir = join8(root, relativeDir);
64268
+ const dir = join10(root, relativeDir);
63738
64269
  const entries = await fs3.readDir(dir);
63739
64270
  const files = [];
63740
64271
  for (const entry of entries.sort()) {
63741
- const relativePath2 = relativeDir.length === 0 ? entry : join8(relativeDir, entry);
63742
- const absolutePath = join8(root, relativePath2);
63743
- const stat2 = await fs3.stat(absolutePath);
63744
- if (stat2?.isDirectory()) {
64272
+ const relativePath2 = relativeDir.length === 0 ? entry : join10(relativeDir, entry);
64273
+ const absolutePath = join10(root, relativePath2);
64274
+ const stat = await fs3.stat(absolutePath);
64275
+ if (stat?.isDirectory()) {
63745
64276
  files.push(...await this.listRuleFiles(root, relativePath2));
63746
- } else if (stat2?.isFile() && /\.(ya?ml|json)$/i.test(entry)) {
64277
+ } else if (stat?.isFile() && /\.(ya?ml|json)$/i.test(entry)) {
63747
64278
  files.push(relativePath2);
63748
64279
  }
63749
64280
  }
@@ -63764,8 +64295,8 @@ var init_rule_service = __esm(() => {
63764
64295
  init_src2();
63765
64296
  init_dist6();
63766
64297
  init_dist10();
63767
- LOCAL_RULES_DIR = join8(".spur", "rules");
63768
- GLOBAL_RULES_DIR = join8(".config", "spur", "rules");
64298
+ LOCAL_RULES_DIR = join10(".spur", "rules");
64299
+ GLOBAL_RULES_DIR = join10(".config", "spur", "rules");
63769
64300
  SEVERITY_RANK2 = {
63770
64301
  info: 0,
63771
64302
  warning: 1,
@@ -63773,6 +64304,212 @@ var init_rule_service = __esm(() => {
63773
64304
  };
63774
64305
  });
63775
64306
 
64307
+ // ../../packages/app/src/services/run-store-service.ts
64308
+ function encodeRunListCursor(id, startedAt) {
64309
+ const payload = { id, startedAt };
64310
+ const bytes = new TextEncoder().encode(JSON.stringify(payload));
64311
+ let binary = "";
64312
+ for (const byte of bytes)
64313
+ binary += String.fromCharCode(byte);
64314
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
64315
+ }
64316
+ function decodeRunListCursor(encoded) {
64317
+ if (encoded.length === 0 || encoded.length > 1024) {
64318
+ throw new RunStoreBadCursorError("malformed cursor: empty or exceeds maximum length");
64319
+ }
64320
+ let json3;
64321
+ try {
64322
+ const base643 = encoded.replaceAll("-", "+").replaceAll("_", "/");
64323
+ const binary = atob(base643);
64324
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
64325
+ json3 = new TextDecoder().decode(bytes);
64326
+ } catch {
64327
+ throw new RunStoreBadCursorError("malformed cursor: not valid base64url");
64328
+ }
64329
+ let parsed;
64330
+ try {
64331
+ parsed = JSON.parse(json3);
64332
+ } catch {
64333
+ throw new RunStoreBadCursorError("malformed cursor: payload is not JSON");
64334
+ }
64335
+ if (!parsed || typeof parsed !== "object") {
64336
+ throw new RunStoreBadCursorError("malformed cursor: payload must be an object");
64337
+ }
64338
+ const obj = parsed;
64339
+ if (typeof obj.id !== "string" || obj.id.length === 0) {
64340
+ throw new RunStoreBadCursorError("malformed cursor: missing or invalid id");
64341
+ }
64342
+ if (typeof obj.startedAt !== "string" || obj.startedAt.length === 0) {
64343
+ throw new RunStoreBadCursorError("malformed cursor: missing or invalid startedAt");
64344
+ }
64345
+ return { id: obj.id, startedAt: obj.startedAt };
64346
+ }
64347
+ function clampRunStoreLimit(raw, defaults = {
64348
+ defaultLimit: RUN_STORE_LIST_DEFAULT_LIMIT,
64349
+ maxLimit: RUN_STORE_LIST_MAX_LIMIT
64350
+ }) {
64351
+ if (raw === undefined || Number.isNaN(raw) || raw <= 0)
64352
+ return defaults.defaultLimit;
64353
+ return Math.min(raw, defaults.maxLimit);
64354
+ }
64355
+ function summarizeActionResult(resultJson, secretValues = []) {
64356
+ if (resultJson === null || resultJson === "")
64357
+ return null;
64358
+ let parsed;
64359
+ try {
64360
+ parsed = JSON.parse(resultJson);
64361
+ } catch {
64362
+ return { summary: redactString(resultJson, secretValues) };
64363
+ }
64364
+ return redactValue3(parsed, secretValues);
64365
+ }
64366
+ function redactString(value, secretValues) {
64367
+ return redactAndBound(value, secretValues, MAX_FIELD_LENGTH2);
64368
+ }
64369
+ function redactValue3(value, secretValues) {
64370
+ if (value === null || value === undefined)
64371
+ return value;
64372
+ if (typeof value === "string")
64373
+ return redactString(value, secretValues);
64374
+ if (typeof value === "number" || typeof value === "boolean")
64375
+ return value;
64376
+ if (Array.isArray(value))
64377
+ return value.map((item) => redactValue3(item, secretValues));
64378
+ if (typeof value === "object") {
64379
+ const out = {};
64380
+ for (const [key, child] of Object.entries(value)) {
64381
+ if (SENSITIVE_KEYS.has(key)) {
64382
+ out[key] = "[redacted]";
64383
+ } else {
64384
+ out[key] = redactValue3(child, secretValues);
64385
+ }
64386
+ }
64387
+ return out;
64388
+ }
64389
+ return redactString(String(value), secretValues);
64390
+ }
64391
+ function toListEntry(row) {
64392
+ return {
64393
+ id: row.id,
64394
+ workflowName: row.workflow_name,
64395
+ status: row.status,
64396
+ mode: row.mode,
64397
+ agent: row.agent,
64398
+ startedAt: row.started_at,
64399
+ completedAt: row.completed_at
64400
+ };
64401
+ }
64402
+
64403
+ class RunStoreService {
64404
+ ctx;
64405
+ constructor(ctx) {
64406
+ this.ctx = ctx;
64407
+ }
64408
+ async list(query = {}) {
64409
+ const limit = clampRunStoreLimit(query.limit);
64410
+ let before;
64411
+ if (query.cursor !== undefined && query.cursor.length > 0) {
64412
+ const decoded = decodeRunListCursor(query.cursor);
64413
+ before = { started_at: decoded.startedAt, id: decoded.id };
64414
+ }
64415
+ const db2 = await this.ctx.getDb();
64416
+ const rows = await new RunDao(db2).traceRows({
64417
+ status: query.status,
64418
+ before,
64419
+ limit: limit + 1
64420
+ });
64421
+ const hasMore = rows.length > limit;
64422
+ const page = hasMore ? rows.slice(0, limit) : rows;
64423
+ const last = page.at(-1);
64424
+ const nextCursor = hasMore && last ? encodeRunListCursor(last.id, last.started_at) : null;
64425
+ return {
64426
+ runs: page.map(toListEntry),
64427
+ count: page.length,
64428
+ nextCursor,
64429
+ hasMore
64430
+ };
64431
+ }
64432
+ async getDetail(runId) {
64433
+ const db2 = await this.ctx.getDb();
64434
+ const row = await new RunDao(db2).traceRowById(runId);
64435
+ if (!row) {
64436
+ throw new RunStoreNotFoundError(runId);
64437
+ }
64438
+ const [phaseRows, transitionRows, actionRows] = await Promise.all([
64439
+ new PhaseRunDao(db2).phaseRowsByRunId(runId),
64440
+ new TransitionRunDao(db2).transitionRowsByRunId(runId),
64441
+ new ActionRunDao(db2).actionRowsByRunId(runId)
64442
+ ]);
64443
+ return {
64444
+ run: toListEntry(row),
64445
+ phases: phaseRows.map((p) => ({
64446
+ phase: p.phase,
64447
+ status: p.status,
64448
+ startedAt: p.started_at,
64449
+ completedAt: p.completed_at
64450
+ })),
64451
+ transitions: transitionRows.map((t) => ({
64452
+ from: t.from_state,
64453
+ to: t.to_state,
64454
+ trigger: t.trigger
64455
+ })),
64456
+ actions: actionRows.map((a2) => ({
64457
+ id: a2.id,
64458
+ node: a2.node,
64459
+ kind: a2.kind,
64460
+ status: a2.status,
64461
+ durationMs: a2.duration_ms,
64462
+ ok: a2.ok === null ? null : a2.ok === 1,
64463
+ resultSummary: summarizeActionResult(a2.result_json, this.ctx.secretValues),
64464
+ startedAt: a2.started_at,
64465
+ completedAt: a2.completed_at
64466
+ }))
64467
+ };
64468
+ }
64469
+ async listByWbs(wbs, limitRaw) {
64470
+ const limit = clampRunStoreLimit(limitRaw, {
64471
+ defaultLimit: RUN_STORE_WBS_DEFAULT_LIMIT,
64472
+ maxLimit: RUN_STORE_WBS_MAX_LIMIT
64473
+ });
64474
+ const db2 = await this.ctx.getDb();
64475
+ const linkRows = await new TaskRunLinkDao(db2).listByWbs(wbs, limit);
64476
+ const runDao = new RunDao(db2);
64477
+ const links = [];
64478
+ for (const link of linkRows) {
64479
+ const runRow = await runDao.traceRowById(link.run_id);
64480
+ links.push({
64481
+ runId: link.run_id,
64482
+ kind: link.kind,
64483
+ linkedAt: link.created_at,
64484
+ run: runRow ? toListEntry(runRow) : null
64485
+ });
64486
+ }
64487
+ return { wbs, links, count: links.length };
64488
+ }
64489
+ }
64490
+ var RUN_STORE_LIST_DEFAULT_LIMIT = 50, RUN_STORE_LIST_MAX_LIMIT = 200, RUN_STORE_WBS_DEFAULT_LIMIT = 50, RUN_STORE_WBS_MAX_LIMIT = 200, MAX_FIELD_LENGTH2 = 256, SENSITIVE_KEYS, RunStoreNotFoundError, RunStoreBadCursorError;
64491
+ var init_run_store_service = __esm(() => {
64492
+ init_src2();
64493
+ init_agent_execution();
64494
+ SENSITIVE_KEYS = new Set(["body", "content", "message", "prompt", "query", "response", "value"]);
64495
+ RunStoreNotFoundError = class RunStoreNotFoundError extends Error {
64496
+ runId;
64497
+ code = "RUN_NOT_FOUND";
64498
+ constructor(runId) {
64499
+ super(`run not found: ${runId}`);
64500
+ this.runId = runId;
64501
+ this.name = "RunStoreNotFoundError";
64502
+ }
64503
+ };
64504
+ RunStoreBadCursorError = class RunStoreBadCursorError extends Error {
64505
+ code = "MALFORMED_CURSOR";
64506
+ constructor(reason) {
64507
+ super(reason);
64508
+ this.name = "RunStoreBadCursorError";
64509
+ }
64510
+ };
64511
+ });
64512
+
63776
64513
  // ../../packages/app/src/services/supervisor-service.ts
63777
64514
  function defaultWrapperArgv(agentId) {
63778
64515
  return {
@@ -63785,6 +64522,7 @@ class SupervisorService {
63785
64522
  processExecutor;
63786
64523
  eventBus;
63787
64524
  configDir;
64525
+ teamMemberStopEmitted = new Set;
63788
64526
  ringBufferSize;
63789
64527
  processes = new Map;
63790
64528
  ringBuffers = new Map;
@@ -63863,6 +64601,12 @@ class SupervisorService {
63863
64601
  teamId,
63864
64602
  agentType: spec.type
63865
64603
  });
64604
+ this.emitTeamMember("team.member.started", {
64605
+ teamId,
64606
+ memberId: agentId,
64607
+ agentType: spec.type ?? null,
64608
+ outcome: "started"
64609
+ });
63866
64610
  handle.exited.then(async (code) => {
63867
64611
  entry.exitCode = code;
63868
64612
  this.emit("process.exited", {
@@ -63872,6 +64616,16 @@ class SupervisorService {
63872
64616
  teamId: entry.teamId ?? null,
63873
64617
  ...entry.agentType ? { agentType: entry.agentType } : {}
63874
64618
  });
64619
+ if (!this.teamMemberStopEmitted.has(agentId)) {
64620
+ this.emitTeamMember("team.member.stopped", {
64621
+ teamId: entry.teamId ?? null,
64622
+ memberId: agentId,
64623
+ agentType: entry.agentType ?? null,
64624
+ outcome: code === 0 ? "exited" : "errored"
64625
+ });
64626
+ } else {
64627
+ this.teamMemberStopEmitted.delete(agentId);
64628
+ }
63875
64629
  if (code === 0 || entry.status === "stopped") {
63876
64630
  entry.status = "exited";
63877
64631
  this.scheduleCleanup(agentId, entry);
@@ -63897,13 +64651,14 @@ class SupervisorService {
63897
64651
  const proc = this.processes.get(agentId);
63898
64652
  if (proc?.entry.status !== "running")
63899
64653
  return;
64654
+ this.teamMemberStopEmitted.add(agentId);
63900
64655
  proc.handle.kill("SIGTERM");
63901
64656
  let timedOut = false;
63902
64657
  const exitPromise = proc.handle.exited;
63903
- const timeout = new Promise((resolve2) => {
64658
+ const timeout = new Promise((resolve4) => {
63904
64659
  setTimeout(() => {
63905
64660
  timedOut = true;
63906
- resolve2();
64661
+ resolve4();
63907
64662
  }, 3000);
63908
64663
  });
63909
64664
  await Promise.race([exitPromise, timeout]);
@@ -63924,6 +64679,12 @@ class SupervisorService {
63924
64679
  teamId: proc.entry.teamId ?? null,
63925
64680
  ...proc.entry.agentType ? { agentType: proc.entry.agentType } : {}
63926
64681
  });
64682
+ this.emitTeamMember("team.member.stopped", {
64683
+ teamId: proc.entry.teamId ?? null,
64684
+ memberId: agentId,
64685
+ agentType: proc.entry.agentType ?? null,
64686
+ outcome: "stopped"
64687
+ });
63927
64688
  }
63928
64689
  async startAutostart(ids) {
63929
64690
  const results = [];
@@ -64004,25 +64765,49 @@ class SupervisorService {
64004
64765
  this.eventBus.emit(name, payload);
64005
64766
  } catch {}
64006
64767
  }
64768
+ emitTeamMember(name, payload) {
64769
+ try {
64770
+ this.eventBus.emit(name, payload);
64771
+ } catch {}
64772
+ }
64007
64773
  }
64008
64774
  var DEFAULT_RING_BUFFER_SIZE = 500, MAX_RESTART_ATTEMPTS = 5, RESTART_BACKOFF_SCHEDULE, MAX_RESTART_BACKOFF = 30000;
64009
64775
  var init_supervisor_service = __esm(() => {
64010
64776
  RESTART_BACKOFF_SCHEDULE = [1000, 2000, 4000, 8000, 16000];
64011
64777
  });
64012
64778
 
64779
+ // ../../packages/app/src/services/system-event-retention.ts
64780
+ function resolveRetentionQuotas(config4 = {}, prefixes = SYSTEM_EVENT_PREFIXES) {
64781
+ const defaultQuota = config4.default ?? DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA;
64782
+ const overrides = config4.prefixes ?? {};
64783
+ return prefixes.map((prefix) => ({
64784
+ prefix,
64785
+ quota: overrides[prefix] ?? defaultQuota
64786
+ }));
64787
+ }
64788
+ var DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA = 1e4;
64789
+ var init_system_event_retention = __esm(() => {
64790
+ init_event_names();
64791
+ });
64792
+
64013
64793
  // ../../packages/app/src/services/system-event-tap.ts
64014
64794
  function registerSystemEventTap(bus, dao2, logger3, options = {}) {
64015
64795
  const handlers = new Map;
64016
64796
  const inFlight = new Set;
64017
64797
  const diagnosticEnabled = options.diagnosticEnabled === true;
64798
+ const quotas = resolveRetentionQuotas(options.retention);
64018
64799
  for (const entry of SYSTEM_EVENT_CATALOG) {
64019
64800
  if (entry.tier === "diagnostic" && !diagnosticEnabled)
64020
64801
  continue;
64021
64802
  const handler = (event2) => {
64022
- const occurredAt = new Date().toISOString();
64023
- const payloadJson = safeStringify(normalizeSystemEventPayload(entry, event2));
64024
- const actor = extractSystemEventActor(event2);
64025
- const p = persist(dao2, entry.name, occurredAt, actor, payloadJson, logger3);
64803
+ const p = persist(dao2, entry.prefix, {
64804
+ id: createId("sev"),
64805
+ event_name: entry.name,
64806
+ occurred_at: new Date().toISOString(),
64807
+ actor: extractSystemEventActor(event2),
64808
+ payload_json: safeStringify(normalizeSystemEventPayload(entry, event2, options.secretValues)),
64809
+ ...extractSystemEventCorrelation(event2)
64810
+ }, quotas, logger3);
64026
64811
  inFlight.add(p);
64027
64812
  p.finally(() => inFlight.delete(p));
64028
64813
  };
@@ -64043,18 +64828,15 @@ function registerSystemEventTap(bus, dao2, logger3, options = {}) {
64043
64828
  }
64044
64829
  };
64045
64830
  }
64046
- async function persist(dao2, name, occurredAt, actor, payloadJson, logger3) {
64831
+ async function persist(dao2, prefix, input, quotas, logger3) {
64047
64832
  try {
64048
- await dao2.insert({
64049
- id: createId("sev"),
64050
- event_name: name,
64051
- occurred_at: occurredAt,
64052
- actor,
64053
- payload_json: payloadJson
64054
- });
64055
- await dao2.prune(SYSTEM_EVENTS_CAP);
64833
+ await dao2.insert(input);
64834
+ await dao2.pruneQuotas(quotas, prefix);
64056
64835
  } catch (error51) {
64057
- logger3.warn("system_events tap: persist failed", { name, error: stringifyError(error51) });
64836
+ logger3.warn("system_events tap: persist failed", {
64837
+ name: input.event_name,
64838
+ error: stringifyError(error51)
64839
+ });
64058
64840
  }
64059
64841
  }
64060
64842
  function safeStringify(event2) {
@@ -64071,27 +64853,52 @@ function extractSystemEventActor(event2) {
64071
64853
  return obj.actor;
64072
64854
  if (typeof obj.agentId === "string" && obj.agentId.length > 0)
64073
64855
  return obj.agentId;
64856
+ if (typeof obj.memberId === "string" && obj.memberId.length > 0)
64857
+ return obj.memberId;
64074
64858
  }
64075
64859
  return null;
64076
64860
  }
64861
+ function extractSystemEventCorrelation(event2) {
64862
+ const correlation = { run_id: null, entity_kind: null, entity_id: null, sequence: null };
64863
+ if (!event2 || typeof event2 !== "object")
64864
+ return correlation;
64865
+ const obj = event2;
64866
+ if (typeof obj.runId === "string" && obj.runId.length > 0)
64867
+ correlation.run_id = obj.runId;
64868
+ if (typeof obj.sequence === "number" && Number.isFinite(obj.sequence))
64869
+ correlation.sequence = obj.sequence;
64870
+ const entity2 = obj.entity;
64871
+ if (entity2 && typeof entity2 === "object") {
64872
+ const { kind, id } = entity2;
64873
+ if (typeof kind === "string" && kind.length > 0)
64874
+ correlation.entity_kind = kind;
64875
+ if (typeof id === "string" && id.length > 0)
64876
+ correlation.entity_id = id;
64877
+ }
64878
+ return correlation;
64879
+ }
64077
64880
  function stringifyError(error51) {
64078
64881
  if (error51 instanceof Error)
64079
64882
  return `${error51.name}: ${error51.message}`;
64080
64883
  return String(error51);
64081
64884
  }
64082
- var SYSTEM_EVENTS_CAP = 1e4;
64083
64885
  var init_system_event_tap = __esm(() => {
64084
64886
  init_src2();
64085
64887
  init_event_names();
64888
+ init_system_event_retention();
64086
64889
  });
64087
64890
 
64088
64891
  // ../../packages/app/src/services/system-event-emitter.ts
64089
64892
  class SystemEventEmitter {
64090
64893
  dao;
64091
64894
  logger;
64092
- constructor(dao2, logger3) {
64895
+ secretValues;
64896
+ quotas;
64897
+ constructor(dao2, logger3, retention = {}, secretValues = []) {
64093
64898
  this.dao = dao2;
64094
64899
  this.logger = logger3;
64900
+ this.secretValues = secretValues;
64901
+ this.quotas = resolveRetentionQuotas(retention);
64095
64902
  }
64096
64903
  async emit(event2) {
64097
64904
  const entry = systemEventCatalogEntry(event2.event);
@@ -64103,9 +64910,10 @@ class SystemEventEmitter {
64103
64910
  event_name: entry.name,
64104
64911
  occurred_at: event2.at,
64105
64912
  actor: extractSystemEventActor(event2),
64106
- payload_json: safeStringify(normalizeSystemEventPayload(entry, event2))
64913
+ payload_json: safeStringify(normalizeSystemEventPayload(entry, event2, this.secretValues)),
64914
+ ...extractSystemEventCorrelation(event2)
64107
64915
  });
64108
- await this.dao.prune(SYSTEM_EVENTS_CAP);
64916
+ await this.dao.pruneQuotas(this.quotas, entry.prefix);
64109
64917
  } catch (error51) {
64110
64918
  this.logger.warn("system_events emitter: persist failed", {
64111
64919
  event: event2.event,
@@ -64117,11 +64925,12 @@ class SystemEventEmitter {
64117
64925
  var init_system_event_emitter = __esm(() => {
64118
64926
  init_src2();
64119
64927
  init_event_names();
64928
+ init_system_event_retention();
64120
64929
  init_system_event_tap();
64121
64930
  });
64122
64931
 
64123
64932
  // ../../packages/app/src/services/task-check.ts
64124
- import { dirname as dirname9, join as join9 } from "path";
64933
+ import { dirname as dirname10, join as join11 } from "path";
64125
64934
  function isPlaceholderBody(body) {
64126
64935
  const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
64127
64936
  return stripped.length === 0;
@@ -64182,9 +64991,9 @@ function isProseOnlyReview(body) {
64182
64991
  function resolveProjectRootFromTasksDir(tasksDir) {
64183
64992
  const norm = tasksDir.replace(/\\/g, "/");
64184
64993
  if (/\/docs\/tasks\d*$/.test(norm) || /\/docs\/tasks$/.test(norm)) {
64185
- return dirname9(dirname9(tasksDir));
64994
+ return dirname10(dirname10(tasksDir));
64186
64995
  }
64187
- return dirname9(tasksDir);
64996
+ return dirname10(tasksDir);
64188
64997
  }
64189
64998
  function extractBacktickLineAnchors(body) {
64190
64999
  const out = [];
@@ -64269,8 +65078,8 @@ var init_task_check = __esm(() => {
64269
65078
  const entry = this.resolveMatrixEntry(variant, status);
64270
65079
  this.runL2(doc2, entry, findings);
64271
65080
  this.runL3(doc2, entry, status, findings);
64272
- const tasksDir = dirname9(filePath);
64273
- const featuresDir = join9(dirname9(tasksDir), "features");
65081
+ const tasksDir = dirname10(filePath);
65082
+ const featuresDir = join11(dirname10(tasksDir), "features");
64274
65083
  await this.runL4(doc2, fm, status, findings, featuresDir, tasksDir);
64275
65084
  await this.runL4Rollup(doc2, wbs, status, findings, tasksDir);
64276
65085
  if (status !== "done" && status !== "cancelled") {
@@ -64651,7 +65460,7 @@ var init_task_check = __esm(() => {
64651
65460
  for (const cite of citations) {
64652
65461
  if (reported >= 5)
64653
65462
  break;
64654
- const abs = join9(projectRoot, cite.path);
65463
+ const abs = join11(projectRoot, cite.path);
64655
65464
  let exists2 = false;
64656
65465
  try {
64657
65466
  exists2 = await this.fs.exists(abs);
@@ -64769,7 +65578,6 @@ var init_task_check = __esm(() => {
64769
65578
  });
64770
65579
 
64771
65580
  // ../../packages/app/src/services/task-record.ts
64772
- import { execSync } from "child_process";
64773
65581
  function escapeTablePipe(s) {
64774
65582
  return s.replace(/\|/g, "\\|");
64775
65583
  }
@@ -64953,19 +65761,23 @@ function extractChangedFiles(diffText) {
64953
65761
  }
64954
65762
  function gitDiffU0(cwd) {
64955
65763
  try {
64956
- return execSync("git diff -U0 HEAD -- '*.ts' '*.tsx' '*.js'", {
64957
- cwd,
64958
- encoding: "utf-8",
64959
- timeout: 1e4
65764
+ const result = new BunSyncProcessExecutor2().runSync({
65765
+ command: "git",
65766
+ args: ["diff", "-U0", "HEAD", "--", "*.ts", "*.tsx", "*.js"],
65767
+ ...cwd !== undefined ? { cwd } : {},
65768
+ rejectOnError: false
64960
65769
  });
65770
+ return result.exitCode === 0 ? result.stdout : "";
64961
65771
  } catch {
64962
65772
  return "";
64963
65773
  }
64964
65774
  }
64965
- var init_task_record = () => {};
65775
+ var init_task_record = __esm(() => {
65776
+ init_dist5();
65777
+ });
64966
65778
 
64967
65779
  // ../../packages/app/src/services/task-scaffold.ts
64968
- import { dirname as dirname10 } from "path";
65780
+ import { dirname as dirname11 } from "path";
64969
65781
 
64970
65782
  class TaskScaffoldService {
64971
65783
  ctx;
@@ -65008,7 +65820,7 @@ class TaskScaffoldService {
65008
65820
  fileExists = false;
65009
65821
  }
65010
65822
  const mergeResult = mergeStubs(existingContent, stubs);
65011
- const dir = dirname10(targetFile);
65823
+ const dir = dirname11(targetFile);
65012
65824
  await this.ctx.fs.ensureDir(dir);
65013
65825
  if (mergeResult.created > 0 || !fileExists) {
65014
65826
  await this.ctx.fs.writeFile(targetFile, mergeResult.content);
@@ -65770,8 +66582,8 @@ ${block}` : block);
65770
66582
  return { wbs: hit.wbs, filePath: hit.filePath };
65771
66583
  if (opts.strict === true)
65772
66584
  return null;
65773
- const basename2 = filePath.split("/").pop() ?? "";
65774
- const capturedWbs = /^(\d{4})_.+\.md$/.exec(basename2)?.[1];
66585
+ const basename3 = filePath.split("/").pop() ?? "";
66586
+ const capturedWbs = /^(\d{4})_.+\.md$/.exec(basename3)?.[1];
65775
66587
  if (capturedWbs) {
65776
66588
  const taskPath = await this.locator.findPathByWbs(capturedWbs);
65777
66589
  if (taskPath !== null)
@@ -66145,7 +66957,7 @@ var init_task_verdict = __esm(() => {
66145
66957
  });
66146
66958
 
66147
66959
  // ../../packages/app/src/services/team-service.ts
66148
- import { join as join10 } from "path";
66960
+ import { join as join12 } from "path";
66149
66961
 
66150
66962
  class TeamService {
66151
66963
  ctx;
@@ -66153,7 +66965,7 @@ class TeamService {
66153
66965
  orchestratorPromise;
66154
66966
  constructor(ctx) {
66155
66967
  this.ctx = ctx;
66156
- this.configDir = join10(ctx.cwd, ".spur", "agents");
66968
+ this.configDir = join12(ctx.cwd, ".spur", "agents");
66157
66969
  }
66158
66970
  async sendMessage(fromId, toId, body, replyTo) {
66159
66971
  validateAgentId(toId);
@@ -66298,6 +67110,14 @@ class TeamService {
66298
67110
  const doc2 = MarkdownDocument.parse(source, "task");
66299
67111
  doc2.setFrontmatterField("assignee", agentId);
66300
67112
  await atomicWriteAsync(path9, doc2.serialize(), taskId, fs3);
67113
+ const identity6 = await this.resolveMemberIdentity(agentId);
67114
+ this.emitTeamMemberEvent("team.member.assigned", {
67115
+ teamId: identity6.teamId,
67116
+ memberId: agentId,
67117
+ agentType: identity6.agentType,
67118
+ outcome: "assigned",
67119
+ taskId
67120
+ });
66301
67121
  }
66302
67122
  async createAgentSpec(input) {
66303
67123
  validateAgentId(input.id);
@@ -66437,12 +67257,18 @@ class TeamService {
66437
67257
  for (const spec of orphaned) {
66438
67258
  await deleteAgentSpec(spec.id, this.configDir);
66439
67259
  }
66440
- return {
67260
+ const result = {
66441
67261
  teamId,
66442
67262
  upserted: toUpsert.map((s) => s.id),
66443
67263
  orphaned: orphaned.map((s) => s.id),
66444
67264
  written: true
66445
67265
  };
67266
+ this.emitTeamLifecycleEvent("team.up", {
67267
+ teamId,
67268
+ memberCount: result.upserted.length,
67269
+ outcome: "ok"
67270
+ });
67271
+ return result;
66446
67272
  }
66447
67273
  async teardownTeam(teamId, opts) {
66448
67274
  const specs = await loadAgentSpecs(this.configDir);
@@ -66453,11 +67279,17 @@ class TeamService {
66453
67279
  await deleteAgentSpec(spec.id, this.configDir);
66454
67280
  }
66455
67281
  }
66456
- return {
67282
+ const result = {
66457
67283
  teamId,
66458
67284
  purged: opts?.purge ? generated.map((s) => s.id) : [],
66459
67285
  stopped: teamSpecs.map((s) => s.id)
66460
67286
  };
67287
+ this.emitTeamLifecycleEvent("team.down", {
67288
+ teamId,
67289
+ memberCount: result.stopped.length,
67290
+ outcome: opts?.purge ? "purged" : "ok"
67291
+ });
67292
+ return result;
66461
67293
  }
66462
67294
  async loadTeamConfig() {
66463
67295
  try {
@@ -66478,18 +67310,72 @@ class TeamService {
66478
67310
  bus.emit(name, payload);
66479
67311
  } catch {}
66480
67312
  }
67313
+ emitTeamLifecycleEvent(name, payload) {
67314
+ const bus = this.ctx.eventBus;
67315
+ if (!bus)
67316
+ return;
67317
+ try {
67318
+ bus.emit(name, payload);
67319
+ } catch {}
67320
+ }
67321
+ emitTeamMemberEvent(name, payload) {
67322
+ const bus = this.ctx.eventBus;
67323
+ if (!bus)
67324
+ return;
67325
+ try {
67326
+ bus.emit(name, payload);
67327
+ } catch {}
67328
+ }
67329
+ async resolveMemberIdentity(agentId) {
67330
+ try {
67331
+ const specs = await loadAgentSpecs(this.configDir);
67332
+ const spec = specs.find((s) => s.id === agentId);
67333
+ if (!spec)
67334
+ return { teamId: null, agentType: null };
67335
+ const teamTag = spec.tags?.find((t) => t.startsWith("team:"));
67336
+ return {
67337
+ teamId: teamTag ? teamTag.slice("team:".length) : null,
67338
+ agentType: typeof spec.type === "string" && spec.type.length > 0 ? spec.type : null
67339
+ };
67340
+ } catch {
67341
+ return { teamId: null, agentType: null };
67342
+ }
67343
+ }
66481
67344
  async inboxRecentDao() {
66482
67345
  const db2 = await this.ctx.getDb();
66483
67346
  return new InboxRecentDao(db2);
66484
67347
  }
66485
67348
  orchestrator() {
66486
- this.orchestratorPromise ??= this.inboxDao().then((dao2) => new TeamOrchestrator(this.configDir, dao2, { events: this.ctx.events }));
67349
+ this.orchestratorPromise ??= this.inboxDao().then((dao2) => {
67350
+ const orch = new TeamOrchestrator(this.configDir, dao2, { events: this.ctx.events });
67351
+ orch.on("agent.started", (event2) => {
67352
+ this.resolveMemberIdentity(event2.agentId).then((identity6) => {
67353
+ this.emitTeamMemberEvent("team.member.started", {
67354
+ teamId: identity6.teamId,
67355
+ memberId: event2.agentId,
67356
+ agentType: event2.agentType ?? identity6.agentType,
67357
+ outcome: "started"
67358
+ });
67359
+ });
67360
+ });
67361
+ orch.on("agent.stopped", (event2) => {
67362
+ this.resolveMemberIdentity(event2.agentId).then((identity6) => {
67363
+ this.emitTeamMemberEvent("team.member.stopped", {
67364
+ teamId: identity6.teamId,
67365
+ memberId: event2.agentId,
67366
+ agentType: identity6.agentType,
67367
+ outcome: "stopped"
67368
+ });
67369
+ });
67370
+ });
67371
+ return orch;
67372
+ });
66487
67373
  return this.orchestratorPromise;
66488
67374
  }
66489
67375
  async resolveTaskFile(taskId) {
66490
67376
  const fs3 = this.ctx.fs;
66491
67377
  const { foldersConfig } = await resolvePlanningFolders(fs3);
66492
- const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) => join10(this.ctx.cwd, dir));
67378
+ const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) => join12(this.ctx.cwd, dir));
66493
67379
  return await TaskLocator.forDirs(fs3, dirs).findPathByWbs(taskId);
66494
67380
  }
66495
67381
  }
@@ -66525,8 +67411,8 @@ var init_team_service = __esm(() => {
66525
67411
  });
66526
67412
 
66527
67413
  // ../../packages/app/src/services/token-ledger-service.ts
66528
- import { closeSync as closeSync2, existsSync as existsSync5, fstatSync, openSync as openSync2, readSync } from "fs";
66529
- import { join as join11 } from "path";
67414
+ import { closeSync as closeSync2, existsSync as existsSync7, fstatSync, openSync as openSync2, readSync } from "fs";
67415
+ import { join as join13 } from "path";
66530
67416
  function isSparseToolActivity(events2) {
66531
67417
  return events2.length === 0 || !events2.some((e) => TOOL_ACTIVITY_TYPES.has(e.type));
66532
67418
  }
@@ -66577,7 +67463,7 @@ function parseLedgerLine(line) {
66577
67463
  }
66578
67464
  }
66579
67465
  function tailTokenLedgerFile(filePath, limit, chunkSize = DEFAULT_CHUNK, before) {
66580
- if (!existsSync5(filePath)) {
67466
+ if (!existsSync7(filePath)) {
66581
67467
  return { events: [], truncated: false };
66582
67468
  }
66583
67469
  const target = limit + 1;
@@ -66627,7 +67513,7 @@ class TokenLedgerService {
66627
67513
  ledgerPath;
66628
67514
  chunkSize;
66629
67515
  constructor(options) {
66630
- this.ledgerPath = options.ledgerPath ?? join11(options.cwd, TOKEN_LEDGER_RELATIVE_PATH);
67516
+ this.ledgerPath = options.ledgerPath ?? join13(options.cwd, TOKEN_LEDGER_RELATIVE_PATH);
66631
67517
  this.chunkSize = options.chunkSize ?? DEFAULT_CHUNK;
66632
67518
  }
66633
67519
  get path() {
@@ -66668,12 +67554,12 @@ class TokenLedgerService {
66668
67554
  var TOOL_ACTIVITY_TYPES, TOKEN_LEDGER_DEFAULT_LIMIT = 200, TOKEN_LEDGER_MAX_LIMIT = 1000, TOKEN_LEDGER_RELATIVE_PATH, DEFAULT_CHUNK;
66669
67555
  var init_token_ledger_service = __esm(() => {
66670
67556
  TOOL_ACTIVITY_TYPES = new Set(["read", "write", "bash", "grep", "glob"]);
66671
- TOKEN_LEDGER_RELATIVE_PATH = join11(".spur", "context", "token-ledger.jsonl");
67557
+ TOKEN_LEDGER_RELATIVE_PATH = join13(".spur", "context", "token-ledger.jsonl");
66672
67558
  DEFAULT_CHUNK = 64 * 1024;
66673
67559
  });
66674
67560
 
66675
67561
  // ../../packages/app/src/services/token-ledger-watcher.ts
66676
- import { closeSync as closeSync3, existsSync as existsSync6, fstatSync as fstatSync2, openSync as openSync3, readSync as readSync2, watch } from "fs";
67562
+ import { closeSync as closeSync3, existsSync as existsSync8, fstatSync as fstatSync2, openSync as openSync3, readSync as readSync2, watch } from "fs";
66677
67563
 
66678
67564
  class TokenLedgerWatcher {
66679
67565
  ledgerPath;
@@ -66694,7 +67580,7 @@ class TokenLedgerWatcher {
66694
67580
  this.started = true;
66695
67581
  this.offset = this.currentSize();
66696
67582
  try {
66697
- const target = existsSync6(this.ledgerPath) ? this.ledgerPath : this.parentDir();
67583
+ const target = existsSync8(this.ledgerPath) ? this.ledgerPath : this.parentDir();
66698
67584
  this.watcher = watch(target, () => this.schedulePoll());
66699
67585
  } catch {
66700
67586
  this.watcher = undefined;
@@ -66729,7 +67615,7 @@ class TokenLedgerWatcher {
66729
67615
  return idx > 0 ? this.ledgerPath.slice(0, idx) : ".";
66730
67616
  }
66731
67617
  currentSize() {
66732
- if (!existsSync6(this.ledgerPath))
67618
+ if (!existsSync8(this.ledgerPath))
66733
67619
  return 0;
66734
67620
  try {
66735
67621
  const fd = openSync3(this.ledgerPath, "r");
@@ -66751,7 +67637,7 @@ class TokenLedgerWatcher {
66751
67637
  }, this.debounceMs);
66752
67638
  }
66753
67639
  pollNewBytes() {
66754
- if (!existsSync6(this.ledgerPath)) {
67640
+ if (!existsSync8(this.ledgerPath)) {
66755
67641
  this.offset = 0;
66756
67642
  this.carry = "";
66757
67643
  return;
@@ -66842,16 +67728,16 @@ class WorkflowSteeringController {
66842
67728
  active.state = "boundary";
66843
67729
  active.version += 1;
66844
67730
  this.publishState(active);
66845
- return await new Promise((resolve2) => {
67731
+ return await new Promise((resolve4) => {
66846
67732
  const settle = (decision) => {
66847
67733
  clearTimeout(timer);
66848
- resolve2(decision);
67734
+ resolve4(decision);
66849
67735
  };
66850
67736
  const timer = setTimeout(() => {
66851
67737
  if (active.resolve !== settle)
66852
67738
  return;
66853
67739
  active.resolve = undefined;
66854
- this.acceptDefault(active, resolve2);
67740
+ this.acceptDefault(active, resolve4);
66855
67741
  }, active.policy.timeoutMs);
66856
67742
  timer.unref?.();
66857
67743
  active.resolve = settle;
@@ -66949,7 +67835,7 @@ class WorkflowSteeringController {
66949
67835
  deadlineAt: new Date(Date.now() + deadlineMs).toISOString()
66950
67836
  });
66951
67837
  }
66952
- acceptDefault(active, resolve2) {
67838
+ acceptDefault(active, resolve4) {
66953
67839
  const command = {
66954
67840
  commandId: crypto.randomUUID(),
66955
67841
  runId: active.runId,
@@ -66974,7 +67860,7 @@ class WorkflowSteeringController {
66974
67860
  at: new Date().toISOString()
66975
67861
  };
66976
67862
  this.onAck?.(ack);
66977
- resolve2({ operation: "continue" });
67863
+ resolve4({ operation: "continue" });
66978
67864
  }
66979
67865
  accept(command, active, note) {
66980
67866
  return this.publish({
@@ -67037,11 +67923,7 @@ var init_steering = __esm(() => {
67037
67923
  });
67038
67924
 
67039
67925
  // ../../packages/app/src/workflow/actions/agent-run.ts
67040
- import { execFile } from "child_process";
67041
- import { existsSync as existsSync7 } from "fs";
67042
- import { mkdir, writeFile } from "fs/promises";
67043
- import { dirname as dirname11, isAbsolute as isAbsolute2, join as join12 } from "path";
67044
- import { promisify as promisify3 } from "util";
67926
+ import { dirname as dirname12, isAbsolute as isAbsolute2, join as join14 } from "path";
67045
67927
 
67046
67928
  class AgentRunActionRunner {
67047
67929
  observabilityBus;
@@ -67135,13 +68017,15 @@ class AgentRunActionRunner {
67135
68017
  const ok = exitCode === 0;
67136
68018
  const invocation = traced.invocation;
67137
68019
  if (capture && answerFile !== undefined) {
67138
- const target = isAbsolute2(answerFile) ? answerFile : join12(cwd, answerFile);
67139
- await mkdir(dirname11(target), { recursive: true });
67140
- await writeFile(target, answer, "utf8");
68020
+ const target = isAbsolute2(answerFile) ? answerFile : join14(cwd, answerFile);
68021
+ const fs3 = createNodeFileSystem3(cwd);
68022
+ await fs3.ensureDir(dirname12(target));
68023
+ await fs3.writeFile(target, answer);
67141
68024
  }
67142
68025
  if (ok && expectFile !== undefined) {
67143
- const target = isAbsolute2(expectFile) ? expectFile : join12(cwd, expectFile);
67144
- if (!existsSync7(target)) {
68026
+ const target = isAbsolute2(expectFile) ? expectFile : join14(cwd, expectFile);
68027
+ const fs3 = createNodeFileSystem3(cwd);
68028
+ if (!await fs3.exists(target)) {
67145
68029
  return {
67146
68030
  ok: false,
67147
68031
  data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
@@ -67248,15 +68132,23 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
67248
68132
  ""
67249
68133
  ].join(`
67250
68134
  `);
67251
- const target = join12(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
67252
- await mkdir(dirname11(target), { recursive: true });
67253
- await writeFile(target, body, "utf8");
68135
+ const target = join14(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
68136
+ const fs3 = createNodeFileSystem3(cwd);
68137
+ await fs3.ensureDir(dirname12(target));
68138
+ await fs3.writeFile(target, body);
67254
68139
  } catch {}
67255
68140
  }
67256
68141
  async function gitDiffStat(cwd) {
67257
68142
  try {
67258
- const { stdout } = await execFileAsync("git", ["diff", "--stat"], { cwd, maxBuffer: 1024 * 1024 });
67259
- return stdout.trim();
68143
+ const result = await new NodeProcessExecutor3().run({
68144
+ command: "git",
68145
+ args: ["diff", "--stat"],
68146
+ cwd,
68147
+ maxOutput: 1024 * 1024,
68148
+ forceBuffered: true,
68149
+ rejectOnError: false
68150
+ });
68151
+ return result.exitCode === 0 ? result.stdout.trim() : "";
67260
68152
  } catch {
67261
68153
  return "";
67262
68154
  }
@@ -67267,10 +68159,10 @@ function tail(text4, maxChars) {
67267
68159
  return `... (truncated) ...
67268
68160
  ${text4.slice(text4.length - maxChars)}`;
67269
68161
  }
67270
- var execFileAsync, PARTIAL_ARTIFACT_TAIL_CHARS = 4000, KIND = "agent.run";
68162
+ var PARTIAL_ARTIFACT_TAIL_CHARS = 4000, KIND = "agent.run";
67271
68163
  var init_agent_run = __esm(() => {
68164
+ init_dist5();
67272
68165
  init_steering();
67273
- execFileAsync = promisify3(execFile);
67274
68166
  });
67275
68167
 
67276
68168
  // ../../packages/app/src/workflow/actions/file-exists.ts
@@ -67326,17 +68218,17 @@ class FileReadActionRunner {
67326
68218
  }
67327
68219
  const maxSize = asNumber(options.maxSize);
67328
68220
  const resolved = joinPath2(context4.workdir ?? ".", rawPath);
67329
- const stat2 = await this.fileSystem.stat(resolved);
67330
- if (stat2 === null) {
68221
+ const stat = await this.fileSystem.stat(resolved);
68222
+ if (stat === null) {
67331
68223
  return { ok: false, error: `file.read: file not found: ${resolved}` };
67332
68224
  }
67333
- if (maxSize !== undefined && stat2.size > maxSize) {
67334
- return { ok: false, error: `file.read: file too large (${stat2.size} > ${maxSize}): ${resolved}` };
68225
+ if (maxSize !== undefined && stat.size > maxSize) {
68226
+ return { ok: false, error: `file.read: file too large (${stat.size} > ${maxSize}): ${resolved}` };
67335
68227
  }
67336
68228
  const content = await this.fileSystem.readFile(resolved);
67337
68229
  return {
67338
68230
  ok: true,
67339
- data: { content, size: stat2.size, path: resolved }
68231
+ data: { content, size: stat.size, path: resolved }
67340
68232
  };
67341
68233
  }
67342
68234
  }
@@ -67374,15 +68266,15 @@ class FileReadIntoVarActionRunner {
67374
68266
  }
67375
68267
  const trim = options.trim === undefined ? true : options.trim !== false;
67376
68268
  const resolved = isAbsolutePath2(rawPath) ? normalizeSeparators2(rawPath) : joinPath2(context4.workdir ?? ".", rawPath);
67377
- const stat2 = await this.fileSystem.stat(resolved);
67378
- if (stat2 === null) {
68269
+ const stat = await this.fileSystem.stat(resolved);
68270
+ if (stat === null) {
67379
68271
  return { ok: false, error: `file.read.into-var: file not found: ${resolved}` };
67380
68272
  }
67381
68273
  const raw = await this.fileSystem.readFile(resolved);
67382
68274
  const projected = trim ? raw.replace(/\s+$/u, "") : raw;
67383
68275
  return {
67384
68276
  ok: true,
67385
- data: { content: projected, raw, var: varName, path: resolved, size: stat2.size },
68277
+ data: { content: projected, raw, var: varName, path: resolved, size: stat.size },
67386
68278
  setVars: { [varName]: projected }
67387
68279
  };
67388
68280
  }
@@ -67826,7 +68718,7 @@ var init_builtins2 = __esm(() => {
67826
68718
  // ../../packages/app/src/workflow/observability.ts
67827
68719
  function bounded(value) {
67828
68720
  const redacted = value.replace(SECRET_PATTERN2, "[REDACTED]");
67829
- return redacted.length <= MAX_FIELD_LENGTH ? redacted : `${redacted.slice(0, MAX_FIELD_LENGTH)}\u2026`;
68721
+ return redacted.length <= MAX_FIELD_LENGTH3 ? redacted : `${redacted.slice(0, MAX_FIELD_LENGTH3)}\u2026`;
67830
68722
  }
67831
68723
  function projectActionMetadata(kind, options) {
67832
68724
  if (options === undefined)
@@ -67971,16 +68863,19 @@ class ObservableWorkflowAdapter {
67971
68863
  return this.inner.listPausedRuns(options);
67972
68864
  }
67973
68865
  }
67974
- var now = () => new Date().toISOString(), MAX_FIELD_LENGTH = 256, SECRET_PATTERN2;
68866
+ var now = () => new Date().toISOString(), MAX_FIELD_LENGTH3 = 256, SECRET_PATTERN2;
67975
68867
  var init_observability = __esm(() => {
67976
68868
  SECRET_PATTERN2 = /(?:sk-[a-z0-9_-]{8,}|(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S+|bearer\s+\S+)/gi;
67977
68869
  });
67978
68870
 
67979
68871
  // ../../packages/app/src/services/workflow-service.ts
67980
- import { access as access2, lstat, readdir as readdir3, readFile as readFile2, realpath } from "fs/promises";
67981
- import { homedir as homedir3 } from "os";
67982
- import { join as join13, resolve as resolve2 } from "path";
68872
+ import { homedir as homedir5 } from "os";
68873
+ import { join as join15, resolve as resolve4 } from "path";
67983
68874
  function signalSubprocess(pid) {
68875
+ if (!Number.isInteger(pid) || pid <= 1)
68876
+ return false;
68877
+ if (pid === process.pid || pid === process.ppid)
68878
+ return false;
67984
68879
  try {
67985
68880
  process.kill(-pid, "SIGTERM");
67986
68881
  return true;
@@ -68045,7 +68940,7 @@ class WorkflowAppService {
68045
68940
  };
68046
68941
  }
68047
68942
  async validate(file2, opts = {}) {
68048
- const absolute = resolve2(this.ctx.cwd, file2);
68943
+ const absolute = resolve4(this.ctx.cwd, file2);
68049
68944
  if (!await fileExists(absolute)) {
68050
68945
  return { ok: false, valid: false, file: file2, errors: [`File not found: ${absolute}`] };
68051
68946
  }
@@ -68096,7 +68991,7 @@ class WorkflowAppService {
68096
68991
  return;
68097
68992
  let workflowName;
68098
68993
  try {
68099
- const def = await loadWorkflowDef(resolve2(this.ctx.cwd, file2));
68994
+ const def = await loadWorkflowDef(resolve4(this.ctx.cwd, file2));
68100
68995
  workflowName = def.name;
68101
68996
  } catch {
68102
68997
  return;
@@ -68156,7 +69051,8 @@ class WorkflowAppService {
68156
69051
  return { runId, finalized, status: after?.status ?? "not_found", killed };
68157
69052
  }
68158
69053
  async continuePaused(runId) {
68159
- const svc = await this.createEngineService();
69054
+ const eventsBus = this.ctx.events?.();
69055
+ const svc = await this.createEngineService({ events: eventsBus });
68160
69056
  const run = await svc.listPausedRuns();
68161
69057
  const target = run.find((r) => r.id === runId);
68162
69058
  if (target === undefined) {
@@ -68166,7 +69062,10 @@ class WorkflowAppService {
68166
69062
  if (workflow === null) {
68167
69063
  throw new Error(`Cannot resume run "${runId}": workflow definition "${target.workflow_name}" not found in the workflow search paths.`);
68168
69064
  }
68169
- const result = await svc.resumeRun(workflow, runId, { workdir: this.ctx.cwd });
69065
+ const result = await svc.resumeRun(workflow, runId, {
69066
+ workdir: this.ctx.cwd,
69067
+ ...eventsBus !== undefined ? { events: bridgeEventBus(eventsBus) } : {}
69068
+ });
68170
69069
  await this.stampFailureReason(runId, result);
68171
69070
  return result;
68172
69071
  }
@@ -68183,7 +69082,7 @@ class WorkflowAppService {
68183
69082
  if (entry === undefined)
68184
69083
  return null;
68185
69084
  for (const layer of listing.layers) {
68186
- const abs = resolve2(layer.path, entry.path);
69085
+ const abs = resolve4(layer.path, entry.path);
68187
69086
  if (await fileExists(abs)) {
68188
69087
  try {
68189
69088
  return await loadWorkflowDef(abs, { validateSchema: false });
@@ -68194,12 +69093,12 @@ class WorkflowAppService {
68194
69093
  }
68195
69094
  async list(workflowPaths = [".spur/workflows/"]) {
68196
69095
  const projectRoot = this.ctx.cwd;
68197
- const globalRoot = join13(homedir3(), ".config", "spur");
69096
+ const globalRoot = join15(homedir5(), ".config", "spur");
68198
69097
  const layers = [];
68199
69098
  const entries = [];
68200
69099
  const scannedPaths = new Set;
68201
69100
  for (const relPath of workflowPaths) {
68202
- const absPath = resolve2(projectRoot, relPath);
69101
+ const absPath = resolve4(projectRoot, relPath);
68203
69102
  layers.push({ id: "project", path: absPath });
68204
69103
  scannedPaths.add(absPath);
68205
69104
  try {
@@ -68208,7 +69107,7 @@ class WorkflowAppService {
68208
69107
  } catch {}
68209
69108
  }
68210
69109
  for (const relPath of workflowPaths) {
68211
- const absPath = resolve2(globalRoot, relPath);
69110
+ const absPath = resolve4(globalRoot, relPath);
68212
69111
  if (scannedPaths.has(absPath))
68213
69112
  continue;
68214
69113
  layers.push({ id: "global", path: absPath });
@@ -68301,7 +69200,8 @@ class WorkflowAppService {
68301
69200
  return { run, events: events2 };
68302
69201
  }
68303
69202
  async createEngineService(opts = {}) {
68304
- const host = createDefaultWorkflowEngineHost();
69203
+ const processExec = this.ctx.processExecutor?.();
69204
+ const host = createDefaultWorkflowEngineHost(processExec !== undefined ? { processExecutor: processExec } : {});
68305
69205
  const bus = this.ctx.observabilityBus?.();
68306
69206
  registerSpurBuiltins(host, {
68307
69207
  agentService: this.ctx.agentService(),
@@ -68322,39 +69222,40 @@ class WorkflowAppService {
68322
69222
  }
68323
69223
  }
68324
69224
  async function fileExists(path9) {
68325
- try {
68326
- await access2(path9);
68327
- return true;
68328
- } catch {
68329
- return false;
68330
- }
69225
+ const fs3 = createNodeFileSystem3();
69226
+ return await fs3.exists(path9);
68331
69227
  }
68332
69228
  async function scanWorkflowFiles(rootPath, source) {
68333
69229
  const entries = [];
68334
- let rootStat;
68335
- try {
68336
- rootStat = await lstat(rootPath);
68337
- } catch {
69230
+ const fs3 = createNodeFileSystem3();
69231
+ const rootStat = await fs3.stat(rootPath);
69232
+ if (!rootStat?.isDirectory()) {
68338
69233
  return entries;
68339
69234
  }
68340
- const realPath = rootStat.isSymbolicLink() ? await realpath(rootPath) : rootPath;
68341
- const dirents = await readdir3(realPath, { withFileTypes: true, recursive: true });
68342
- for (const dirent of dirents) {
68343
- if (!dirent.isFile())
68344
- continue;
68345
- const name = dirent.name;
68346
- if (!name.endsWith(".yaml") && !name.endsWith(".yml"))
68347
- continue;
68348
- const fullPath = resolve2(dirent.parentPath, name);
68349
- const displayPath = fullPath.startsWith(rootPath) ? fullPath.slice(rootPath.length + 1) : fullPath;
68350
- const meta3 = await extractWorkflowMeta(fullPath);
68351
- entries.push({ ...meta3, path: displayPath, source });
69235
+ const realPath = fs3.realPath ? await fs3.realPath(rootPath) : rootPath;
69236
+ async function walk3(dirPath) {
69237
+ const names = await fs3.readDir(dirPath);
69238
+ for (const name of names) {
69239
+ const fullPath = resolve4(dirPath, name);
69240
+ const st = await fs3.stat(fullPath);
69241
+ if (!st)
69242
+ continue;
69243
+ if (st.isDirectory()) {
69244
+ await walk3(fullPath);
69245
+ } else if (st.isFile() && (name.endsWith(".yaml") || name.endsWith(".yml"))) {
69246
+ const displayPath = fullPath.startsWith(rootPath) ? fullPath.slice(rootPath.length + 1) : fullPath;
69247
+ const meta3 = await extractWorkflowMeta(fullPath);
69248
+ entries.push({ ...meta3, path: displayPath, source });
69249
+ }
69250
+ }
68352
69251
  }
69252
+ await walk3(realPath);
68353
69253
  return entries;
68354
69254
  }
68355
69255
  async function extractWorkflowMeta(filePath) {
68356
69256
  try {
68357
- const text4 = await readFile2(filePath, "utf-8");
69257
+ const fs3 = createNodeFileSystem3();
69258
+ const text4 = await fs3.readFile(filePath);
68358
69259
  const parsed = parseYamlObject(text4);
68359
69260
  if (typeof parsed !== "object" || parsed === null) {
68360
69261
  return { name: "<unparseable>", kind: "unknown", valid: false, error: "Top-level value is not an object" };
@@ -68411,8 +69312,6 @@ var init_workflow_service = __esm(() => {
68411
69312
  });
68412
69313
 
68413
69314
  // ../../packages/app/src/workflow/lifecycle-adapter.ts
68414
- import { readFile as readFile3 } from "fs/promises";
68415
-
68416
69315
  class LifecycleAdapter {
68417
69316
  opts;
68418
69317
  workflowCache;
@@ -68523,7 +69422,8 @@ class LifecycleAdapter {
68523
69422
  return this.opts.readTaskMarkdown(ref);
68524
69423
  }
68525
69424
  try {
68526
- return await readFile3(ref.filePath, "utf8");
69425
+ const fs3 = createNodeFileSystem3(this.opts.cwd);
69426
+ return await fs3.readFile(ref.filePath);
68527
69427
  } catch {
68528
69428
  return null;
68529
69429
  }
@@ -68539,6 +69439,7 @@ var TASK_LIFECYCLE_PROFILE, FEATURE_LIFECYCLE_PROFILE;
68539
69439
  var init_lifecycle_adapter = __esm(() => {
68540
69440
  init_src2();
68541
69441
  init_dist7();
69442
+ init_dist5();
68542
69443
  init_task_check();
68543
69444
  TASK_LIFECYCLE_PROFILE = {
68544
69445
  linkKind: "lifecycle",
@@ -68636,8 +69537,7 @@ function formatDuration(ms) {
68636
69537
  var isAgentExecution = (e) => ("executionId" in e), isActionStarted = (e) => ("kind" in e) && ("node" in e), isActionFinished = (e) => ("durationMs" in e) && ("ok" in e), isPhase = (e) => ("phase" in e), isTransition = (e) => ("from" in e) && ("to" in e);
68637
69538
 
68638
69539
  // ../../packages/app/src/workflow/trace-writer.ts
68639
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
68640
- import { dirname as dirname12, join as join14 } from "path";
69540
+ import { dirname as dirname13, join as join16 } from "path";
68641
69541
 
68642
69542
  class WorkflowTraceWriter {
68643
69543
  path;
@@ -68645,7 +69545,7 @@ class WorkflowTraceWriter {
68645
69545
  pending = Promise.resolve();
68646
69546
  constructor(cwd, runId) {
68647
69547
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
68648
- this.path = join14(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
69548
+ this.path = join16(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
68649
69549
  }
68650
69550
  attach(bus) {
68651
69551
  bus.on("workflow.run.started", (event2) => this.enqueue("workflow.run.started", event2));
@@ -68670,18 +69570,26 @@ class WorkflowTraceWriter {
68670
69570
  })}
68671
69571
  `;
68672
69572
  this.pending = this.pending.then(async () => {
68673
- await mkdir2(dirname12(this.path), { recursive: true });
68674
- await appendFile(this.path, line, "utf8");
69573
+ const fs3 = createNodeFileSystem3();
69574
+ await fs3.ensureDir(dirname13(this.path));
69575
+ await fs3.appendFile(this.path, line);
68675
69576
  });
68676
69577
  }
68677
69578
  }
68678
- var init_trace_writer = () => {};
69579
+ var init_trace_writer = __esm(() => {
69580
+ init_dist5();
69581
+ });
68679
69582
 
68680
69583
  // ../../packages/app/src/index.ts
68681
69584
  var exports_src2 = {};
68682
69585
  __export(exports_src2, {
68683
69586
  tailTokenLedgerFile: () => tailTokenLedgerFile,
68684
69587
  systemEventCatalogEntry: () => systemEventCatalogEntry,
69588
+ summarizeActionResult: () => summarizeActionResult,
69589
+ startRegisteredProject: () => startRegisteredProject,
69590
+ setDetachedServeSpawnForTests: () => setDetachedServeSpawnForTests,
69591
+ resolveSpurServeCommand: () => resolveSpurServeCommand,
69592
+ resolveRetentionQuotas: () => resolveRetentionQuotas,
68685
69593
  resolvePlanningFolders: () => resolvePlanningFolders,
68686
69594
  resolveAutostartSet: () => resolveAutostartSet,
68687
69595
  renderTesting: () => renderTesting,
@@ -68701,21 +69609,29 @@ __export(exports_src2, {
68701
69609
  parseLedgerLine: () => parseLedgerLine,
68702
69610
  parseEtimeToSeconds: () => parseEtimeToSeconds,
68703
69611
  normalizeSystemEventPayload: () => normalizeSystemEventPayload,
69612
+ normalizeProjectPath: () => normalizeProjectPath,
69613
+ isPortLive: () => isPortLive,
69614
+ isPortAvailable: () => isPortAvailable,
68704
69615
  isFindingCode: () => isFindingCode,
69616
+ isFeatureActionName: () => isFeatureActionName,
68705
69617
  hitlConfirmDefault: () => hitlConfirmDefault,
68706
69618
  hitlAutoApproveEnabled: () => hitlAutoApproveEnabled,
68707
69619
  hasPopulatedPriorityTable: () => hasPopulatedPriorityTable,
68708
69620
  formatNoopMessage: () => formatNoopMessage,
68709
69621
  formatDenialMessage: () => formatDenialMessage,
69622
+ extractSystemEventCorrelation: () => extractSystemEventCorrelation,
68710
69623
  extractSystemEventActor: () => extractSystemEventActor,
68711
69624
  extractReviewSectionBody: () => extractReviewSectionBody,
68712
69625
  evaluateDoneTransition: () => evaluateDoneTransition,
69626
+ encodeRunListCursor: () => encodeRunListCursor,
68713
69627
  deriveVerdict: () => deriveVerdict,
69628
+ decodeRunListCursor: () => decodeRunListCursor,
68714
69629
  createPsProcessInspector: () => createPsProcessInspector,
68715
69630
  configuredSecretValues: () => configuredSecretValues,
68716
69631
  computeAggregate: () => computeAggregate,
68717
69632
  classifyTaskOutcome: () => classifyTaskOutcome,
68718
69633
  clampToolUseLimit: () => clampToolUseLimit,
69634
+ clampRunStoreLimit: () => clampRunStoreLimit,
68719
69635
  bridgeEventBus: () => bridgeEventBus,
68720
69636
  aggregateBatchVerdicts: () => aggregateBatchVerdicts,
68721
69637
  WorkflowTraceWriter: () => WorkflowTraceWriter,
@@ -68743,10 +69659,17 @@ __export(exports_src2, {
68743
69659
  SYSTEM_EVENT_NAMES: () => SYSTEM_EVENT_NAMES,
68744
69660
  SYSTEM_EVENT_CATALOG_METADATA: () => SYSTEM_EVENT_CATALOG_METADATA,
68745
69661
  SYSTEM_EVENT_CATALOG: () => SYSTEM_EVENT_CATALOG,
68746
- SYSTEM_EVENTS_CAP: () => SYSTEM_EVENTS_CAP,
69662
+ RunStoreService: () => RunStoreService,
69663
+ RunStoreNotFoundError: () => RunStoreNotFoundError,
69664
+ RunStoreBadCursorError: () => RunStoreBadCursorError,
68747
69665
  RuleService: () => RuleService,
68748
69666
  RuleCheckActionRunner: () => RuleCheckActionRunner,
68749
69667
  ResponseValidateActionRunner: () => ResponseValidateActionRunner,
69668
+ RUN_STORE_WBS_MAX_LIMIT: () => RUN_STORE_WBS_MAX_LIMIT,
69669
+ RUN_STORE_WBS_DEFAULT_LIMIT: () => RUN_STORE_WBS_DEFAULT_LIMIT,
69670
+ RUN_STORE_LIST_MAX_LIMIT: () => RUN_STORE_LIST_MAX_LIMIT,
69671
+ RUN_STORE_LIST_DEFAULT_LIMIT: () => RUN_STORE_LIST_DEFAULT_LIMIT,
69672
+ ProjectRegistry: () => ProjectRegistry,
68750
69673
  ProcessInventoryService: () => ProcessInventoryService,
68751
69674
  PluginService: () => PluginService,
68752
69675
  PlanningWriteService: () => PlanningWriteService,
@@ -68768,8 +69691,11 @@ __export(exports_src2, {
68768
69691
  FeatureCheckService: () => FeatureCheckService,
68769
69692
  FINDING_CODES: () => FINDING_CODES,
68770
69693
  FEATURE_LIFECYCLE_PROFILE: () => FEATURE_LIFECYCLE_PROFILE,
69694
+ FEATURE_ACTION_NAMES: () => FEATURE_ACTION_NAMES,
69695
+ FEATURE_ACTION_COMMANDS: () => FEATURE_ACTION_COMMANDS,
68771
69696
  DuplicateFollowUpError: () => DuplicateFollowUpError,
68772
69697
  DependencyMutationError: () => DependencyMutationError,
69698
+ DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA: () => DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA,
68773
69699
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX,
68774
69700
  CorpusMigrator: () => CorpusMigrator,
68775
69701
  BusPlanningEventEmitter: () => BusPlanningEventEmitter,
@@ -68793,9 +69719,13 @@ var init_src3 = __esm(() => {
68793
69719
  init_planning_write_service();
68794
69720
  init_process_inspector();
68795
69721
  init_process_inventory_service();
69722
+ init_project_registry();
69723
+ init_project_start();
68796
69724
  init_rule_service();
69725
+ init_run_store_service();
68797
69726
  init_supervisor_service();
68798
69727
  init_system_event_emitter();
69728
+ init_system_event_retention();
68799
69729
  init_system_event_tap();
68800
69730
  init_task_check();
68801
69731
  init_task_locator();
@@ -76082,6 +77012,8 @@ Font modified May 20, 2012 by patorjk to add the 0xCA0 character
76082
77012
 
76083
77013
  // src/commands/agent.ts
76084
77014
  init_src3();
77015
+ init_dist6();
77016
+ init_dist5();
76085
77017
 
76086
77018
  // src/output.ts
76087
77019
  init_dist();
@@ -76097,6 +77029,40 @@ function toJson2(value) {
76097
77029
  return JSON.stringify(value, null, 2);
76098
77030
  }
76099
77031
 
77032
+ // src/system-event-ledger.ts
77033
+ init_src3();
77034
+ init_src2();
77035
+ async function attachSystemEventLedger(bus, context4, options = {}) {
77036
+ const logger3 = {
77037
+ warn: (msg, data) => {
77038
+ context4.output.error(`${msg}${data ? ` ${JSON.stringify(data)}` : ""}`);
77039
+ }
77040
+ };
77041
+ let tap;
77042
+ try {
77043
+ const dao2 = new SystemEventDao(await context4.getDb());
77044
+ tap = registerSystemEventTap(bus, dao2, logger3, {
77045
+ diagnosticEnabled: options.diagnosticEnabled === true,
77046
+ secretValues: configuredSecretValues(context4.env ?? {})
77047
+ });
77048
+ } catch (error51) {
77049
+ context4.output.error(`system_events ledger attach: ${error51 instanceof Error ? error51.message : String(error51)}`);
77050
+ return {
77051
+ unsubscribe: () => {},
77052
+ flush: async () => {}
77053
+ };
77054
+ }
77055
+ return {
77056
+ unsubscribe: () => {
77057
+ tap?.unsubscribe();
77058
+ tap = undefined;
77059
+ },
77060
+ flush: async () => {
77061
+ await tap?.flush();
77062
+ }
77063
+ };
77064
+ }
77065
+
76100
77066
  // src/commands/agent.ts
76101
77067
  function registerAgentCommand(program2, context4) {
76102
77068
  const agent = program2.command("agent").summary("run and inspect supported coding agents");
@@ -76255,8 +77221,20 @@ async function runAgentEdit(id, context4) {
76255
77221
  context4.output.write(path9);
76256
77222
  return 0;
76257
77223
  }
76258
- const proc = Bun.spawn([...editorArgv, path9], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
76259
- return await proc.exited;
77224
+ const [editorCmd, ...editorArgs] = editorArgv;
77225
+ if (editorCmd === undefined) {
77226
+ context4.output.write(path9);
77227
+ return 0;
77228
+ }
77229
+ const result = await new NodeProcessExecutor3({
77230
+ output: { mode: "stream", isTTY: true }
77231
+ }).run({
77232
+ command: editorCmd,
77233
+ args: [...editorArgs, path9],
77234
+ forceBuffered: false,
77235
+ rejectOnError: false
77236
+ });
77237
+ return result.exitCode ?? 1;
76260
77238
  }
76261
77239
  async function runAgentDelete(id, context4, flags) {
76262
77240
  if (id === undefined) {
@@ -76277,12 +77255,19 @@ async function runAgentDelete(id, context4, flags) {
76277
77255
  }
76278
77256
  }
76279
77257
  async function runAgentRun(prompt, context4, flags, deps) {
76280
- const svc = context4.agentService();
76281
- if (flags.drain === true) {
76282
- const { prompt: drained, flags: rewritten } = await drainIntoPrompt(prompt, context4, flags);
76283
- return svc.run(drained, rewritten, deps);
77258
+ const bus = new EventBus;
77259
+ const ledger = await attachSystemEventLedger(bus, context4);
77260
+ const svc = context4.agentService({ events: bus });
77261
+ try {
77262
+ if (flags.drain === true) {
77263
+ const { prompt: drained, flags: rewritten } = await drainIntoPrompt(prompt, context4, flags);
77264
+ return await svc.run(drained, rewritten, deps);
77265
+ }
77266
+ return await svc.run(prompt, flags, deps);
77267
+ } finally {
77268
+ await ledger.flush();
77269
+ ledger.unsubscribe();
76284
77270
  }
76285
- return svc.run(prompt, flags, deps);
76286
77271
  }
76287
77272
  async function drainIntoPrompt(prompt, context4, flags) {
76288
77273
  const recipient = typeof flags.agent === "string" ? flags.agent : "";
@@ -76313,15 +77298,15 @@ function parseLoopPoll(raw) {
76313
77298
  return Number.isFinite(n2) && n2 > 0 ? n2 : DEFAULT_LOOP_POLL_MS;
76314
77299
  }
76315
77300
  function loopSleep(ms, signal) {
76316
- return new Promise((resolve3) => {
77301
+ return new Promise((resolve5) => {
76317
77302
  if (signal?.aborted) {
76318
- resolve3();
77303
+ resolve5();
76319
77304
  return;
76320
77305
  }
76321
- const timer = setTimeout(resolve3, ms);
77306
+ const timer = setTimeout(resolve5, ms);
76322
77307
  signal?.addEventListener("abort", () => {
76323
77308
  clearTimeout(timer);
76324
- resolve3();
77309
+ resolve5();
76325
77310
  }, { once: true });
76326
77311
  });
76327
77312
  }
@@ -76366,7 +77351,7 @@ function makePlanningEmitter(context4) {
76366
77351
  return {
76367
77352
  async emit(event2) {
76368
77353
  try {
76369
- cached2 ??= new SystemEventEmitter(new SystemEventDao(await context4.getDb()), logger3);
77354
+ cached2 ??= new SystemEventEmitter(new SystemEventDao(await context4.getDb()), logger3, {}, configuredSecretValues(context4.env ?? {}));
76370
77355
  await cached2.emit(event2);
76371
77356
  } catch (error51) {
76372
77357
  context4.output.error(`system_events emitter: ${error51 instanceof Error ? error51.message : String(error51)}`);
@@ -76379,14 +77364,14 @@ function makePlanningEmitter(context4) {
76379
77364
  init_src3();
76380
77365
  init_loader();
76381
77366
  init_src2();
76382
- import { existsSync as existsSync8 } from "fs";
76383
- import { homedir as homedir4 } from "os";
76384
- import { join as join15, resolve as resolve3 } from "path";
77367
+ import { existsSync as existsSync9 } from "fs";
77368
+ import { homedir as homedir6 } from "os";
77369
+ import { join as join17, resolve as resolve5 } from "path";
76385
77370
 
76386
77371
  // src/workflow/resolve-spur-bin.ts
76387
- import { basename as basename2 } from "path";
77372
+ import { basename as basename3 } from "path";
76388
77373
  function resolveSpurBinFrom(launch) {
76389
- const runtime = basename2(launch.execPath).toLowerCase().replace(/\.exe$/, "");
77374
+ const runtime = basename3(launch.execPath).toLowerCase().replace(/\.exe$/, "");
76390
77375
  const isJsRuntime = runtime === "bun" || runtime === "node";
76391
77376
  if (isJsRuntime && launch.mainModule) {
76392
77377
  return `${launch.execPath} ${launch.mainModule}`;
@@ -76399,23 +77384,23 @@ function resolveSpurBin() {
76399
77384
  }
76400
77385
 
76401
77386
  // src/workflow/make-lifecycle-adapter.ts
76402
- var GLOBAL_CONFIG_DIR = join15(".config", "spur");
77387
+ var GLOBAL_CONFIG_DIR = join17(".config", "spur");
76403
77388
  function globalConfigRoot(context4) {
76404
77389
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
76405
- return override !== undefined && override.length > 0 ? resolve3(context4.cwd, override) : join15(homedir4(), GLOBAL_CONFIG_DIR);
77390
+ return override !== undefined && override.length > 0 ? resolve5(context4.cwd, override) : join17(homedir6(), GLOBAL_CONFIG_DIR);
76406
77391
  }
76407
77392
  function resolveWorkflowPath(context4, profile) {
76408
77393
  const bundledRoot = bundledConfigRoot();
76409
77394
  if (bundledRoot !== null) {
76410
- const bundledPath = join15(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
76411
- if (existsSync8(bundledPath))
77395
+ const bundledPath = join17(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
77396
+ if (existsSync9(bundledPath))
76412
77397
  return bundledPath;
76413
77398
  }
76414
- const projectPath = join15(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
76415
- if (existsSync8(projectPath))
77399
+ const projectPath = join17(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
77400
+ if (existsSync9(projectPath))
76416
77401
  return projectPath;
76417
- const globalPath = join15(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
76418
- if (existsSync8(globalPath))
77402
+ const globalPath = join17(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
77403
+ if (existsSync9(globalPath))
76419
77404
  return globalPath;
76420
77405
  return null;
76421
77406
  }
@@ -76815,14 +77800,14 @@ function formatImportResult(r) {
76815
77800
  init_loader();
76816
77801
  init_src2();
76817
77802
  init_dist10();
76818
- import { homedir as homedir5 } from "os";
76819
- import { join as join16, resolve as resolve4 } from "path";
77803
+ import { homedir as homedir7 } from "os";
77804
+ import { join as join18, resolve as resolve6 } from "path";
76820
77805
 
76821
77806
  // src/config.ts
76822
77807
  var CLI_CONFIG = {
76823
77808
  binaryName: "spur",
76824
77809
  binaryLabel: "spur",
76825
- binaryVersion: "0.3.26",
77810
+ binaryVersion: "0.3.27",
76826
77811
  configDir: ".spur",
76827
77812
  configFile: ".spur/config.yaml",
76828
77813
  databaseFile: ".spur/spur.db"
@@ -76873,8 +77858,8 @@ var SCAFFOLD_MANIFEST = [
76873
77858
  ];
76874
77859
 
76875
77860
  // src/commands/init.ts
76876
- var GLOBAL_CONFIG_DIR2 = join16(".config", "spur");
76877
- var GLOBAL_RULES_DIR2 = join16(GLOBAL_CONFIG_DIR2, "rules");
77861
+ var GLOBAL_CONFIG_DIR2 = join18(".config", "spur");
77862
+ var GLOBAL_RULES_DIR2 = join18(GLOBAL_CONFIG_DIR2, "rules");
76878
77863
  var GLOBAL_CONFIG_EXAMPLE = "config.example.yaml";
76879
77864
  var GLOBAL_CONFIG_FILE2 = "config.yaml";
76880
77865
  var INDEXED_CONTEXT_MARKER = "## Indexed context";
@@ -76905,7 +77890,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
76905
77890
  `;
76906
77891
  function globalRulesRoot(context4) {
76907
77892
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
76908
- return override !== undefined && override.length > 0 ? resolve4(context4.cwd, override) : join16(homedir5(), GLOBAL_RULES_DIR2);
77893
+ return override !== undefined && override.length > 0 ? resolve6(context4.cwd, override) : join18(homedir7(), GLOBAL_RULES_DIR2);
76909
77894
  }
76910
77895
  async function writeIfNew(context4, path9, content, force, result) {
76911
77896
  if (!force && await context4.fs.exists(path9)) {
@@ -76922,11 +77907,11 @@ async function seedGlobalRules(context4) {
76922
77907
  const target = globalRulesRoot(context4);
76923
77908
  let written = 0;
76924
77909
  for (const relPath of await listBundledRuleFiles()) {
76925
- const destination = join16(target, relPath);
77910
+ const destination = join18(target, relPath);
76926
77911
  if (await context4.fs.exists(destination))
76927
77912
  continue;
76928
- await context4.fs.ensureDir(join16(target, ...relPath.split("/").slice(0, -1)));
76929
- await context4.fs.writeFile(destination, await context4.fs.readFile(join16(source, relPath)));
77913
+ await context4.fs.ensureDir(join18(target, ...relPath.split("/").slice(0, -1)));
77914
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join18(source, relPath)));
76930
77915
  written += 1;
76931
77916
  }
76932
77917
  return written;
@@ -76936,20 +77921,20 @@ async function seedGlobalConfig(context4) {
76936
77921
  if (source === null)
76937
77922
  return 0;
76938
77923
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
76939
- const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve4(context4.cwd, globalOverride) : join16(homedir5(), GLOBAL_CONFIG_DIR2);
77924
+ const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve6(context4.cwd, globalOverride) : join18(homedir7(), GLOBAL_CONFIG_DIR2);
76940
77925
  let written = 0;
76941
77926
  for (const relPath of listBundledConfigFiles()) {
76942
77927
  if (relPath === GLOBAL_CONFIG_EXAMPLE)
76943
77928
  continue;
76944
- const destination = join16(target, relPath);
77929
+ const destination = join18(target, relPath);
76945
77930
  if (await context4.fs.exists(destination))
76946
77931
  continue;
76947
- await context4.fs.ensureDir(join16(target, ...relPath.split("/").slice(0, -1)));
76948
- await context4.fs.writeFile(destination, await context4.fs.readFile(join16(source, relPath)));
77932
+ await context4.fs.ensureDir(join18(target, ...relPath.split("/").slice(0, -1)));
77933
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join18(source, relPath)));
76949
77934
  written += 1;
76950
77935
  }
76951
- const examplePath = join16(source, GLOBAL_CONFIG_EXAMPLE);
76952
- const globalConfigPath = join16(target, GLOBAL_CONFIG_FILE2);
77936
+ const examplePath = join18(source, GLOBAL_CONFIG_EXAMPLE);
77937
+ const globalConfigPath = join18(target, GLOBAL_CONFIG_FILE2);
76953
77938
  if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
76954
77939
  await context4.fs.ensureDir(target);
76955
77940
  await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
@@ -76963,7 +77948,7 @@ function registerInitCommand(program2, context4) {
76963
77948
  const force = options.force === true;
76964
77949
  const minimal = options.minimal === true;
76965
77950
  const projectName = options.name ?? "default";
76966
- const configPath = join16(context4.cwd, CLI_CONFIG.configFile);
77951
+ const configPath = join18(context4.cwd, CLI_CONFIG.configFile);
76967
77952
  if (!force && await context4.fs.exists(configPath)) {
76968
77953
  const message = `Already initialized: ${CLI_CONFIG.configFile}. Use --force to overwrite.`;
76969
77954
  context4.output.write(json3 ? toJson2({ ok: false, reason: "already-initialized", config: CLI_CONFIG.configFile }) : message);
@@ -76998,13 +77983,13 @@ function registerInitCommand(program2, context4) {
76998
77983
  ].join(`
76999
77984
  `)}
77000
77985
  `;
77001
- await context4.fs.ensureDir(join16(context4.cwd, CLI_CONFIG.configDir));
77986
+ await context4.fs.ensureDir(join18(context4.cwd, CLI_CONFIG.configDir));
77002
77987
  await context4.fs.writeFile(configPath, configYaml);
77003
77988
  result.created.push(configPath);
77004
- const agentsDir = join16(context4.cwd, CLI_CONFIG.configDir, "agents");
77989
+ const agentsDir = join18(context4.cwd, CLI_CONFIG.configDir, "agents");
77005
77990
  await context4.fs.ensureDir(agentsDir);
77006
- await writeIfNew(context4, join16(agentsDir, ".gitkeep"), "", force, result);
77007
- const gitignorePath = join16(context4.cwd, ".gitignore");
77991
+ await writeIfNew(context4, join18(agentsDir, ".gitkeep"), "", force, result);
77992
+ const gitignorePath = join18(context4.cwd, ".gitignore");
77008
77993
  const contextEntry = ".spur/context/";
77009
77994
  if (await context4.fs.exists(gitignorePath)) {
77010
77995
  const existing = await context4.fs.readFile(gitignorePath);
@@ -77026,20 +78011,20 @@ ${contextEntry}
77026
78011
  for (const relPath of listBundledProjectSeedFiles()) {
77027
78012
  if (relPath.startsWith("templates/docs/"))
77028
78013
  continue;
77029
- const sourcePath = join16(configRoot, relPath);
78014
+ const sourcePath = join18(configRoot, relPath);
77030
78015
  if (!await context4.fs.exists(sourcePath))
77031
78016
  continue;
77032
- const targetPath = join16(context4.cwd, CLI_CONFIG.configDir, relPath);
77033
- await context4.fs.ensureDir(join16(targetPath, ".."));
78017
+ const targetPath = join18(context4.cwd, CLI_CONFIG.configDir, relPath);
78018
+ await context4.fs.ensureDir(join18(targetPath, ".."));
77034
78019
  await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
77035
78020
  }
77036
78021
  for (const entry of SCAFFOLD_MANIFEST) {
77037
- const sourcePath = join16(configRoot, entry.source);
78022
+ const sourcePath = join18(configRoot, entry.source);
77038
78023
  if (!await context4.fs.exists(sourcePath))
77039
78024
  continue;
77040
- const baseDir = entry.root === true ? context4.cwd : join16(context4.cwd, CLI_CONFIG.configDir);
77041
- const targetPath = join16(baseDir, entry.target);
77042
- await context4.fs.ensureDir(join16(targetPath, ".."));
78025
+ const baseDir = entry.root === true ? context4.cwd : join18(context4.cwd, CLI_CONFIG.configDir);
78026
+ const targetPath = join18(baseDir, entry.target);
78027
+ await context4.fs.ensureDir(join18(targetPath, ".."));
77043
78028
  const entryForce = entry.preserve === true ? false : force;
77044
78029
  let body = await context4.fs.readFile(sourcePath);
77045
78030
  if (entry.target === "AGENTS.md") {
@@ -77052,7 +78037,7 @@ ${contextEntry}
77052
78037
  }
77053
78038
  }
77054
78039
  }
77055
- const agentsMdPath = join16(context4.cwd, "AGENTS.md");
78040
+ const agentsMdPath = join18(context4.cwd, "AGENTS.md");
77056
78041
  if (await context4.fs.exists(agentsMdPath)) {
77057
78042
  const existing = await context4.fs.readFile(agentsMdPath);
77058
78043
  if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
@@ -77209,15 +78194,15 @@ async function runMessageWatch(svc, output2, options, runtime = {}) {
77209
78194
  }
77210
78195
  }
77211
78196
  function defaultSleep(ms, signal) {
77212
- return new Promise((resolve5) => {
78197
+ return new Promise((resolve7) => {
77213
78198
  if (signal?.aborted) {
77214
- resolve5();
78199
+ resolve7();
77215
78200
  return;
77216
78201
  }
77217
- const timer = setTimeout(resolve5, ms);
78202
+ const timer = setTimeout(resolve7, ms);
77218
78203
  signal?.addEventListener("abort", () => {
77219
78204
  clearTimeout(timer);
77220
- resolve5();
78205
+ resolve7();
77221
78206
  }, { once: true });
77222
78207
  });
77223
78208
  }
@@ -77230,10 +78215,10 @@ function parseInterval2(raw) {
77230
78215
 
77231
78216
  // src/commands/migrate.ts
77232
78217
  init_src2();
77233
- import { join as join17 } from "path";
78218
+ import { join as join19 } from "path";
77234
78219
  function registerMigrateCommand(program2, context4) {
77235
78220
  program2.command("migrate").summary("apply CLI-owned schema migrations").option("--json", "Output machine-readable JSON").action(async (options) => {
77236
- const migrations = await loadSqlMigrations(join17(context4.cwd, "drizzle")).catch(() => {
78221
+ const migrations = await loadSqlMigrations(join19(context4.cwd, "drizzle")).catch(() => {
77237
78222
  return;
77238
78223
  });
77239
78224
  const applied = await applyCliMigrations(await context4.getDb(), migrations);
@@ -77242,6 +78227,170 @@ function registerMigrateCommand(program2, context4) {
77242
78227
  });
77243
78228
  }
77244
78229
 
78230
+ // src/commands/projects.ts
78231
+ init_src3();
78232
+ init_dist5();
78233
+ import { basename as basename4, resolve as resolve7 } from "path";
78234
+ function registerProjectsCommand(program2, context4) {
78235
+ const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
78236
+ projectsCmd.command("add").argument("<path>", "Project root directory path").option("--name <name>", "Display name for the project").option("--json", "Output JSON response").action(async (pathArg, options) => {
78237
+ try {
78238
+ const absolutePath = resolve7(context4.cwd, pathArg);
78239
+ if (!await context4.fs.exists(absolutePath)) {
78240
+ throw new Error(`Directory does not exist: ${absolutePath}`);
78241
+ }
78242
+ const name = options.name ?? basename4(absolutePath);
78243
+ const registry2 = new ProjectRegistry;
78244
+ const entry = await registry2.upsert({ path: absolutePath, name, port: 0 });
78245
+ if (options.json) {
78246
+ context4.output.write(toJson2({ ok: true, project: entry }));
78247
+ } else {
78248
+ context4.output.write(`Registered project "${entry.name}" at ${entry.path}`);
78249
+ }
78250
+ } catch (err) {
78251
+ const message = err instanceof Error ? err.message : String(err);
78252
+ if (options.json) {
78253
+ context4.output.write(toJson2({ ok: false, error: message }));
78254
+ } else {
78255
+ context4.output.error(`Error: ${message}`);
78256
+ }
78257
+ context4.setExitCode(1);
78258
+ }
78259
+ });
78260
+ projectsCmd.command("remove").argument("<target>", "Project display name or directory path").option("--json", "Output JSON response").action(async (target, options) => {
78261
+ try {
78262
+ const registry2 = new ProjectRegistry;
78263
+ const removed = await registry2.remove(target);
78264
+ if (!removed) {
78265
+ throw new Error(`Project not found in registry: "${target}"`);
78266
+ }
78267
+ if (options.json) {
78268
+ context4.output.write(toJson2({ ok: true, removed: target }));
78269
+ } else {
78270
+ context4.output.write(`Removed project "${target}" from registry`);
78271
+ }
78272
+ } catch (err) {
78273
+ const message = err instanceof Error ? err.message : String(err);
78274
+ if (options.json) {
78275
+ context4.output.write(toJson2({ ok: false, error: message }));
78276
+ } else {
78277
+ context4.output.error(`Error: ${message}`);
78278
+ }
78279
+ context4.setExitCode(1);
78280
+ }
78281
+ });
78282
+ projectsCmd.command("list").option("--json", "Output JSON array of projects").action(async (options) => {
78283
+ try {
78284
+ const registry2 = new ProjectRegistry;
78285
+ const rawProjects = await registry2.list();
78286
+ const projects3 = await Promise.all(rawProjects.map(async (p) => ({
78287
+ ...p,
78288
+ running: p.port > 0 ? await isPortLive(p.port) : false
78289
+ })));
78290
+ if (options.json) {
78291
+ context4.output.write(toJson2({ projects: projects3 }));
78292
+ } else {
78293
+ if (projects3.length === 0) {
78294
+ context4.output.write("No projects registered.");
78295
+ return;
78296
+ }
78297
+ context4.output.write(`Registered Projects:
78298
+ `);
78299
+ for (const p of projects3) {
78300
+ const status = p.running ? `[RUNNING: ${p.port}]` : "[STOPPED]";
78301
+ context4.output.write(`- ${p.name} ${status} (${p.path})`);
78302
+ }
78303
+ }
78304
+ } catch (err) {
78305
+ const message = err instanceof Error ? err.message : String(err);
78306
+ if (options.json) {
78307
+ context4.output.write(toJson2({ ok: false, error: message }));
78308
+ } else {
78309
+ context4.output.error(`Error: ${message}`);
78310
+ }
78311
+ context4.setExitCode(1);
78312
+ }
78313
+ });
78314
+ projectsCmd.command("start").argument("<target>", "Project display name or path").option("--port <n>", "Explicit port to bind", parseInt).option("--json", "Output JSON response").action(async (target, options) => {
78315
+ try {
78316
+ const registry2 = new ProjectRegistry;
78317
+ const result = await startRegisteredProject(registry2, target, {
78318
+ port: typeof options.port === "number" && !Number.isNaN(options.port) ? options.port : undefined
78319
+ });
78320
+ if (options.json) {
78321
+ context4.output.write(toJson2({
78322
+ ok: true,
78323
+ project: {
78324
+ name: result.name,
78325
+ path: result.path,
78326
+ port: result.port
78327
+ },
78328
+ running: true,
78329
+ alreadyRunning: result.alreadyRunning,
78330
+ url: result.url
78331
+ }));
78332
+ } else if (result.alreadyRunning) {
78333
+ context4.output.write(`Project "${result.name}" is already running at ${result.url}`);
78334
+ } else {
78335
+ context4.output.write(`Started project "${result.name}" on ${result.url}`);
78336
+ }
78337
+ } catch (err) {
78338
+ const message = err instanceof Error ? err.message : String(err);
78339
+ if (options.json) {
78340
+ context4.output.write(toJson2({ ok: false, error: message }));
78341
+ } else {
78342
+ context4.output.error(`Error: ${message}`);
78343
+ }
78344
+ context4.setExitCode(1);
78345
+ }
78346
+ });
78347
+ projectsCmd.command("stop").argument("<target>", "Project display name or path").option("--json", "Output JSON response").action(async (target, options) => {
78348
+ try {
78349
+ const registry2 = new ProjectRegistry;
78350
+ const entry = await registry2.getByName(target) ?? await registry2.getByPath(target);
78351
+ if (!entry) {
78352
+ throw new Error(`Project not found: "${target}"`);
78353
+ }
78354
+ if (entry.port > 0) {
78355
+ try {
78356
+ const result = await new NodeProcessExecutor3().run({
78357
+ command: "fuser",
78358
+ args: [`${entry.port}/tcp`],
78359
+ forceBuffered: true,
78360
+ rejectOnError: false
78361
+ });
78362
+ const outputStr = `${result.stdout}
78363
+ ${result.stderr}`;
78364
+ const pids = outputStr.trim().split(/\s+/).map(Number).filter((n2) => Number.isInteger(n2) && n2 > 1);
78365
+ const self2 = process.pid;
78366
+ const parent = process.ppid;
78367
+ for (const pid of pids) {
78368
+ if (pid === self2 || pid === parent)
78369
+ continue;
78370
+ try {
78371
+ process.kill(pid, "SIGTERM");
78372
+ } catch {}
78373
+ }
78374
+ } catch {}
78375
+ await registry2.setPort(entry.path, 0);
78376
+ }
78377
+ if (options.json) {
78378
+ context4.output.write(toJson2({ ok: true, stopped: entry.name }));
78379
+ } else {
78380
+ context4.output.write(`Stopped project "${entry.name}"`);
78381
+ }
78382
+ } catch (err) {
78383
+ const message = err instanceof Error ? err.message : String(err);
78384
+ if (options.json) {
78385
+ context4.output.write(toJson2({ ok: false, error: message }));
78386
+ } else {
78387
+ context4.output.error(`Error: ${message}`);
78388
+ }
78389
+ context4.setExitCode(1);
78390
+ }
78391
+ });
78392
+ }
78393
+
77245
78394
  // src/commands/rule.ts
77246
78395
  init_src3();
77247
78396
 
@@ -77485,7 +78634,7 @@ function formatTraceDetail(detail) {
77485
78634
 
77486
78635
  // src/commands/serve.ts
77487
78636
  init_src();
77488
- import { join as join21 } from "path";
78637
+ import { join as join23 } from "path";
77489
78638
 
77490
78639
  // ../server/src/index.ts
77491
78640
  init_src();
@@ -77495,11 +78644,11 @@ init_src3();
77495
78644
  init_src();
77496
78645
  init_loader();
77497
78646
  init_src2();
77498
- import { dirname as dirname14, isAbsolute as isAbsolute3, join as join20 } from "path";
78647
+ import { basename as basename6, dirname as dirname15, isAbsolute as isAbsolute3, join as join22 } from "path";
77499
78648
  init_dist5();
77500
78649
 
77501
78650
  // ../server/src/bootstrap.ts
77502
- import { join as join18 } from "path";
78651
+ import { join as join20 } from "path";
77503
78652
 
77504
78653
  // ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
77505
78654
  var guard = (func, shouldGuard) => {
@@ -77772,8 +78921,8 @@ class EventPublisher {
77772
78921
  if (bufferedEvents.length > 0) {
77773
78922
  return { done: false, value: bufferedEvents.shift() };
77774
78923
  }
77775
- return new Promise((resolve5, reject) => {
77776
- pullResolvers.push([resolve5, reject]);
78924
+ return new Promise((resolve8, reject) => {
78925
+ pullResolvers.push([resolve8, reject]);
77777
78926
  });
77778
78927
  }, async () => {
77779
78928
  unsubscribe();
@@ -82906,6 +84055,56 @@ function enqueueSseFrame(closed, controller, encoder, frame) {
82906
84055
  init_event_names();
82907
84056
 
82908
84057
  // ../server/src/modules/events/index.ts
84058
+ var HISTORY_LIMIT_DEFAULT = 100;
84059
+ var HISTORY_LIMIT_MAX = 500;
84060
+ function encodeHistoryCursor(id, occurredAt) {
84061
+ const payload = { id, occurredAt };
84062
+ const bytes = new TextEncoder().encode(JSON.stringify(payload));
84063
+ let binary = "";
84064
+ for (const byte of bytes)
84065
+ binary += String.fromCharCode(byte);
84066
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
84067
+ }
84068
+ function decodeHistoryCursor(encoded) {
84069
+ if (encoded.length === 0 || encoded.length > 1024) {
84070
+ return { ok: false, reason: "malformed cursor: empty or exceeds maximum length" };
84071
+ }
84072
+ let json3;
84073
+ try {
84074
+ const base643 = encoded.replaceAll("-", "+").replaceAll("_", "/");
84075
+ const binary = atob(base643);
84076
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
84077
+ json3 = new TextDecoder().decode(bytes);
84078
+ } catch {
84079
+ return { ok: false, reason: "malformed cursor: not valid base64url" };
84080
+ }
84081
+ let parsed;
84082
+ try {
84083
+ parsed = JSON.parse(json3);
84084
+ } catch {
84085
+ return { ok: false, reason: "malformed cursor: payload is not JSON" };
84086
+ }
84087
+ if (!parsed || typeof parsed !== "object") {
84088
+ return { ok: false, reason: "malformed cursor: payload must be an object" };
84089
+ }
84090
+ const obj = parsed;
84091
+ if (typeof obj.id !== "string" || obj.id.length === 0) {
84092
+ return { ok: false, reason: "malformed cursor: missing or invalid id" };
84093
+ }
84094
+ if (typeof obj.occurredAt !== "string" || obj.occurredAt.length === 0) {
84095
+ return { ok: false, reason: "malformed cursor: missing or invalid occurredAt" };
84096
+ }
84097
+ if (Number.isNaN(Date.parse(obj.occurredAt))) {
84098
+ return { ok: false, reason: "malformed cursor: occurredAt is not a valid timestamp" };
84099
+ }
84100
+ return { ok: true, value: { id: obj.id, occurredAt: obj.occurredAt } };
84101
+ }
84102
+ function parseHistoryNamesParam(raw2) {
84103
+ if (raw2 === undefined)
84104
+ return;
84105
+ const parts = (Array.isArray(raw2) ? raw2 : [raw2]).flatMap((chunk) => chunk.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
84106
+ return parts.length > 0 ? parts : undefined;
84107
+ }
82909
84108
  function extractSystemEventActor2(event2) {
82910
84109
  if (event2 && typeof event2 === "object") {
82911
84110
  const obj = event2;
@@ -82913,6 +84112,8 @@ function extractSystemEventActor2(event2) {
82913
84112
  return obj.actor;
82914
84113
  if (typeof obj.agentId === "string" && obj.agentId.length > 0)
82915
84114
  return obj.agentId;
84115
+ if (typeof obj.memberId === "string" && obj.memberId.length > 0)
84116
+ return obj.memberId;
82916
84117
  }
82917
84118
  return null;
82918
84119
  }
@@ -83001,29 +84202,71 @@ var eventsModule = {
83001
84202
  const nameParam = c3.req.query("name");
83002
84203
  const sinceParam = c3.req.query("since");
83003
84204
  const limitParam = c3.req.query("limit");
83004
- let limit = 100;
84205
+ const prefixParam = c3.req.query("prefix");
84206
+ const runIdParam = c3.req.query("runId");
84207
+ const actorParam = c3.req.query("actor");
84208
+ const cursorParam = c3.req.query("cursor");
84209
+ const namesRaw = c3.req.queries("names");
84210
+ const namesParam = namesRaw && namesRaw.length > 0 ? parseHistoryNamesParam(namesRaw) : parseHistoryNamesParam(c3.req.query("names"));
84211
+ let limit = HISTORY_LIMIT_DEFAULT;
83005
84212
  if (limitParam !== undefined) {
83006
84213
  const parsed = Number.parseInt(limitParam, 10);
83007
84214
  if (!Number.isNaN(parsed) && parsed > 0) {
83008
- limit = Math.min(parsed, 500);
84215
+ limit = Math.min(parsed, HISTORY_LIMIT_MAX);
83009
84216
  }
83010
84217
  }
83011
- const dao2 = await ctx.systemEventDao();
83012
- const rows = await dao2.query({
84218
+ if (prefixParam !== undefined && prefixParam.length > 0) {
84219
+ if (!SYSTEM_EVENT_PREFIXES.includes(prefixParam)) {
84220
+ return c3.json({
84221
+ error: `unknown prefix: "${prefixParam}" is not a cataloged event family`,
84222
+ code: "UNKNOWN_PREFIX"
84223
+ }, 400);
84224
+ }
84225
+ }
84226
+ let before;
84227
+ if (cursorParam !== undefined && cursorParam.length > 0) {
84228
+ const decoded = decodeHistoryCursor(cursorParam);
84229
+ if (!decoded.ok) {
84230
+ return c3.json({ error: decoded.reason, code: "MALFORMED_CURSOR" }, 400);
84231
+ }
84232
+ before = { occurred_at: decoded.value.occurredAt, id: decoded.value.id };
84233
+ }
84234
+ const query = {
83013
84235
  name: nameParam || undefined,
84236
+ names: namesParam,
84237
+ prefix: prefixParam || undefined,
83014
84238
  since: sinceParam || undefined,
83015
- limit
83016
- });
83017
- const events2 = rows.map((row) => ({
84239
+ run_id: runIdParam || undefined,
84240
+ actor: actorParam || undefined,
84241
+ before,
84242
+ limit: limit + 1
84243
+ };
84244
+ const dao2 = await ctx.systemEventDao();
84245
+ const rows = await dao2.query(query);
84246
+ const hasMore = rows.length > limit;
84247
+ const page = hasMore ? rows.slice(0, limit) : rows;
84248
+ const last = page.at(-1);
84249
+ const nextCursor = hasMore && last ? encodeHistoryCursor(last.id, last.occurred_at) : null;
84250
+ const events2 = page.map((row) => ({
83018
84251
  id: row.id,
83019
84252
  eventName: row.event_name,
83020
84253
  occurredAt: row.occurred_at,
83021
84254
  actor: row.actor,
83022
84255
  prefix: systemEventCatalogEntry(row.event_name)?.prefix ?? row.event_name.split(".")[0],
83023
84256
  renderer: systemEventCatalogEntry(row.event_name)?.renderer ?? "generic",
83024
- payload: row.payload_json ? JSON.parse(row.payload_json) : null
84257
+ payload: row.payload_json ? JSON.parse(row.payload_json) : null,
84258
+ runId: row.run_id,
84259
+ entityKind: row.entity_kind,
84260
+ entityId: row.entity_id,
84261
+ sequence: row.sequence
83025
84262
  }));
83026
- return c3.json({ events: events2, count: events2.length, catalog: SYSTEM_EVENT_CATALOG_METADATA });
84263
+ return c3.json({
84264
+ events: events2,
84265
+ count: events2.length,
84266
+ catalog: SYSTEM_EVENT_CATALOG_METADATA,
84267
+ nextCursor,
84268
+ hasMore
84269
+ });
83027
84270
  });
83028
84271
  }
83029
84272
  };
@@ -83128,7 +84371,12 @@ var featureActionInputSchema = exports_external.object({
83128
84371
  channel: featureActionChannelSchema.optional(),
83129
84372
  skipDeps: exports_external.boolean().optional()
83130
84373
  });
83131
- var featureActionResponseSchema = apiSuccessSchema(exports_external.object({}));
84374
+ var featureActionResultSchema = exports_external.object({
84375
+ runId: exports_external.string(),
84376
+ action: exports_external.string(),
84377
+ status: exports_external.literal("queued")
84378
+ });
84379
+ var featureActionResponseSchema = apiSuccessSchema(featureActionResultSchema);
83132
84380
  var featureCreateChildInputSchema = exports_external.object({
83133
84381
  id: featureIdSchema,
83134
84382
  name: exports_external.string().min(1)
@@ -83490,8 +84738,13 @@ function createFeatureHandlers(ctx) {
83490
84738
  await ctx.featureService().updateBody(input.id, input.body);
83491
84739
  return { ok: true, data: {} };
83492
84740
  }),
83493
- action: os2.feature.action.handler(async () => {
83494
- return { ok: true, data: {} };
84741
+ action: os2.feature.action.handler(async ({ input }) => {
84742
+ const jobQueue = await ctx.jobQueue();
84743
+ const result = await ctx.featureService().fulfillAction(input.id, input.action, async (job) => {
84744
+ const runId = await jobQueue.enqueue("feature-action", job);
84745
+ return runId;
84746
+ }, { channel: input.channel, skipDeps: input.skipDeps });
84747
+ return { ok: true, data: result };
83495
84748
  }),
83496
84749
  children: os2.feature.children.handler(async ({ input }) => {
83497
84750
  const r2 = await ctx.featureService().create(input.name, input.id);
@@ -83535,7 +84788,8 @@ var featureModule = {
83535
84788
  };
83536
84789
 
83537
84790
  // ../server/src/modules/health/index.ts
83538
- import { basename as basename3 } from "path";
84791
+ init_src3();
84792
+ import { basename as basename5 } from "path";
83539
84793
  var startedAt = Date.now();
83540
84794
  var healthModule = {
83541
84795
  name: "health",
@@ -83561,7 +84815,56 @@ var healthModule = {
83561
84815
  return c3.json({ status: "error", db: "unreachable" }, 503);
83562
84816
  });
83563
84817
  app.get("/api/project", (c3) => {
83564
- return c3.json({ name: ctx ? basename3(ctx.cwd) : null });
84818
+ return c3.json({ name: ctx ? basename5(ctx.cwd) : null });
84819
+ });
84820
+ app.get("/api/projects", async (c3) => {
84821
+ if (!ctx) {
84822
+ return c3.json({ projects: [] });
84823
+ }
84824
+ const registry2 = new ProjectRegistry;
84825
+ const rawProjects = await registry2.list();
84826
+ const currentNorm = normalizeProjectPath(ctx.cwd);
84827
+ const projects3 = await Promise.all(rawProjects.map(async (p2) => {
84828
+ const normPath = normalizeProjectPath(p2.path);
84829
+ const running = p2.port > 0 ? await isPortLive(p2.port) : false;
84830
+ return {
84831
+ name: p2.name,
84832
+ path: p2.path,
84833
+ port: p2.port,
84834
+ running,
84835
+ current: normPath === currentNorm
84836
+ };
84837
+ }));
84838
+ return c3.json({ projects: projects3 });
84839
+ });
84840
+ app.post("/api/projects/start", async (c3) => {
84841
+ if (!ctx) {
84842
+ return c3.json({ error: "Multi-project registry unavailable on Cloudflare Workers" }, 501);
84843
+ }
84844
+ let body = {};
84845
+ try {
84846
+ body = await c3.req.json();
84847
+ } catch {}
84848
+ const target = body.name ?? body.path;
84849
+ if (!target) {
84850
+ return c3.json({ error: "Missing name or path in request body" }, 400);
84851
+ }
84852
+ try {
84853
+ const registry2 = new ProjectRegistry;
84854
+ const result = await startRegisteredProject(registry2, target);
84855
+ return c3.json({
84856
+ name: result.name,
84857
+ path: result.path,
84858
+ port: result.port,
84859
+ running: true,
84860
+ alreadyRunning: result.alreadyRunning,
84861
+ url: result.url
84862
+ });
84863
+ } catch (err) {
84864
+ const message = err instanceof Error ? err.message : String(err);
84865
+ const status = message.includes("not found") ? 404 : 500;
84866
+ return c3.json({ error: message }, status);
84867
+ }
83565
84868
  });
83566
84869
  }
83567
84870
  };
@@ -83811,6 +85114,63 @@ var observabilityModule = {
83811
85114
  }
83812
85115
  };
83813
85116
 
85117
+ // ../server/src/modules/runs/index.ts
85118
+ init_src3();
85119
+ var runsModule = {
85120
+ name: "runs",
85121
+ mount(app, ctx) {
85122
+ if (!ctx)
85123
+ return;
85124
+ app.get("/api/runs", async (c3) => {
85125
+ const status = c3.req.query("status") || undefined;
85126
+ const limitParam = c3.req.query("limit");
85127
+ const cursor2 = c3.req.query("cursor") || undefined;
85128
+ let limit;
85129
+ if (limitParam !== undefined) {
85130
+ const parsed = Number.parseInt(limitParam, 10);
85131
+ if (!Number.isNaN(parsed))
85132
+ limit = parsed;
85133
+ }
85134
+ try {
85135
+ const service = ctx.runStoreService();
85136
+ const result = await service.list({ status, limit, cursor: cursor2 });
85137
+ return c3.json(result);
85138
+ } catch (err) {
85139
+ if (err instanceof RunStoreBadCursorError) {
85140
+ return c3.json({ error: err.message, code: err.code }, 400);
85141
+ }
85142
+ throw err;
85143
+ }
85144
+ });
85145
+ app.get("/api/runs/by-wbs/:wbs", async (c3) => {
85146
+ const wbs = c3.req.param("wbs");
85147
+ const limitParam = c3.req.query("limit");
85148
+ let limit;
85149
+ if (limitParam !== undefined) {
85150
+ const parsed = Number.parseInt(limitParam, 10);
85151
+ if (!Number.isNaN(parsed))
85152
+ limit = parsed;
85153
+ }
85154
+ const service = ctx.runStoreService();
85155
+ const result = await service.listByWbs(wbs, limit);
85156
+ return c3.json(result);
85157
+ });
85158
+ app.get("/api/runs/:runId", async (c3) => {
85159
+ const runId = c3.req.param("runId");
85160
+ const service = ctx.runStoreService();
85161
+ try {
85162
+ const detail = await service.getDetail(runId);
85163
+ return c3.json(detail);
85164
+ } catch (err) {
85165
+ if (err instanceof RunStoreNotFoundError) {
85166
+ return c3.json({ error: err.message, code: err.code, runId: err.runId }, 404);
85167
+ }
85168
+ throw err;
85169
+ }
85170
+ });
85171
+ }
85172
+ };
85173
+
83814
85174
  // ../server/src/modules/task/handlers.ts
83815
85175
  init_schema6();
83816
85176
  var os3 = implement(contract);
@@ -83998,6 +85358,7 @@ var teamModule = {
83998
85358
  const closed = { current: false };
83999
85359
  const signal = c3.req.raw.signal;
84000
85360
  let heartbeatInterval;
85361
+ let pollInterval;
84001
85362
  let closeController = () => {};
84002
85363
  const teardown = () => {
84003
85364
  if (closed.current)
@@ -84005,6 +85366,8 @@ var teamModule = {
84005
85366
  closed.current = true;
84006
85367
  if (heartbeatInterval)
84007
85368
  clearInterval(heartbeatInterval);
85369
+ if (pollInterval)
85370
+ clearInterval(pollInterval);
84008
85371
  signal.removeEventListener("abort", teardown);
84009
85372
  closeController();
84010
85373
  };
@@ -84035,29 +85398,24 @@ var teamModule = {
84035
85398
  line: "--replay-done--"
84036
85399
  }))
84037
85400
  return;
84038
- const pollInterval = setInterval(() => {
85401
+ pollInterval = setInterval(() => {
84039
85402
  if (closed.current) {
84040
- clearInterval(pollInterval);
85403
+ if (pollInterval)
85404
+ clearInterval(pollInterval);
84041
85405
  return;
84042
85406
  }
84043
85407
  for (const frame of supervisor.getRingBuffer(id)) {
84044
85408
  if (frame.seq <= lastSeq)
84045
85409
  continue;
84046
85410
  if (!enqueueFrame(closed, controller, encoder, frame)) {
84047
- clearInterval(pollInterval);
85411
+ teardown();
84048
85412
  return;
84049
85413
  }
84050
85414
  lastSeq = frame.seq;
84051
85415
  }
84052
85416
  }, 500);
84053
- const origClose = closeController;
84054
- closeController = () => {
84055
- clearInterval(pollInterval);
84056
- origClose();
84057
- };
84058
85417
  },
84059
85418
  cancel() {
84060
- closeController = () => {};
84061
85419
  teardown();
84062
85420
  }
84063
85421
  });
@@ -84153,7 +85511,8 @@ var builtins = [
84153
85511
  messagesModule,
84154
85512
  jobsModule,
84155
85513
  teamModule,
84156
- observabilityModule
85514
+ observabilityModule,
85515
+ runsModule
84157
85516
  ];
84158
85517
  function registerModules(app, ctx, modules = builtins) {
84159
85518
  for (const mod of modules) {
@@ -86100,21 +87459,55 @@ function createRouter2(ctx) {
86100
87459
  };
86101
87460
  }
86102
87461
 
86103
- // ../server/src/bootstrap.ts
87462
+ // ../server/src/server-config.ts
86104
87463
  function serverBootstrapConfig(env) {
86105
87464
  const isTest = env.NODE_ENV === "test";
86106
87465
  const raw2 = env.SPUR_TEAM_AUTOSTART;
86107
87466
  const teamAutostart = raw2 ? raw2.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
86108
87467
  const diagnosticEvents = env.SPUR_DIAGNOSTIC_EVENTS === "1" || env.SPUR_DIAGNOSTIC_EVENTS === "true";
87468
+ const retentionDefault = parseRetentionNumber(env.SPUR_EVENT_RETENTION_DEFAULT);
87469
+ const retentionPrefixes = parseRetentionPrefixes(env);
86109
87470
  return {
86110
87471
  logging: { enabled: !isTest, level: env.SPUR_LOG_LEVEL ?? "info", console: false },
86111
87472
  telemetry: { enabled: false },
86112
- events: { enabled: true, diagnostic: diagnosticEvents },
87473
+ events: {
87474
+ enabled: true,
87475
+ diagnostic: diagnosticEvents,
87476
+ retention: {
87477
+ ...retentionDefault !== undefined ? { default: retentionDefault } : {},
87478
+ ...Object.keys(retentionPrefixes).length > 0 ? { prefixes: retentionPrefixes } : {}
87479
+ }
87480
+ },
86113
87481
  jobqueue: { enabled: !isTest },
86114
87482
  scheduler: { enabled: !isTest },
86115
87483
  teamAutostart
86116
87484
  };
86117
87485
  }
87486
+ function parseRetentionNumber(raw2) {
87487
+ if (raw2 === undefined || raw2.trim() === "")
87488
+ return;
87489
+ const value2 = Number(raw2);
87490
+ if (!Number.isInteger(value2) || value2 < 0)
87491
+ return;
87492
+ return value2;
87493
+ }
87494
+ function parseRetentionPrefixes(env) {
87495
+ const prefixes = {};
87496
+ for (const [key, value2] of Object.entries(env)) {
87497
+ const match2 = /^SPUR_EVENT_RETENTION_(.+)$/.exec(key);
87498
+ if (!match2 || match2[1] === "DEFAULT")
87499
+ continue;
87500
+ const prefix = match2[1];
87501
+ if (!prefix)
87502
+ continue;
87503
+ const quota = parseRetentionNumber(value2);
87504
+ if (quota !== undefined)
87505
+ prefixes[prefix.toLowerCase()] = quota;
87506
+ }
87507
+ return prefixes;
87508
+ }
87509
+
87510
+ // ../server/src/bootstrap.ts
86118
87511
  function createApp(appRt, opts) {
86119
87512
  const app = new Hono2;
86120
87513
  mountMiddleware(app, appRt);
@@ -86139,7 +87532,7 @@ function createApp(appRt, opts) {
86139
87532
  app.use("*", async (c3, next) => {
86140
87533
  const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
86141
87534
  try {
86142
- const file2 = Bun.file(join18(webDistPath, pathname));
87535
+ const file2 = Bun.file(join20(webDistPath, pathname));
86143
87536
  if (await file2.exists()) {
86144
87537
  const headers = new Headers({ "content-type": file2.type });
86145
87538
  return new Response(file2.stream(), { headers });
@@ -86152,7 +87545,7 @@ function createApp(appRt, opts) {
86152
87545
  return c3.json({ error: "Not Found" }, 404);
86153
87546
  }
86154
87547
  try {
86155
- const indexFile = Bun.file(join18(webDistPath, "index.html"));
87548
+ const indexFile = Bun.file(join20(webDistPath, "index.html"));
86156
87549
  if (await indexFile.exists()) {
86157
87550
  return new Response(indexFile.stream(), {
86158
87551
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -86173,7 +87566,7 @@ init_src3();
86173
87566
  init_src();
86174
87567
  init_src2();
86175
87568
  init_dist5();
86176
- import { dirname as dirname13, join as join19 } from "path";
87569
+ import { dirname as dirname14, join as join21 } from "path";
86177
87570
  var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
86178
87571
 
86179
87572
  class LazyPlanningEventEmitter {
@@ -86208,7 +87601,7 @@ class NotConfiguredError extends Error {
86208
87601
  function createServerContext(appRt, options) {
86209
87602
  const cwd = options.cwd;
86210
87603
  const fs3 = options.fs;
86211
- const dbUrl = options.dbUrl ?? join19(cwd, DEFAULT_DATABASE_URL);
87604
+ const dbUrl = options.dbUrl ?? join21(cwd, DEFAULT_DATABASE_URL);
86212
87605
  const eventsBus = options.eventsBus ?? appRt.events;
86213
87606
  const jobQueueEnabled = options.jobQueueEnabled ?? false;
86214
87607
  const folders = options.folders ?? DEFAULT_PLANNING_FOLDERS;
@@ -86231,6 +87624,7 @@ function createServerContext(appRt, options) {
86231
87624
  let agentSvc;
86232
87625
  let ruleSvc;
86233
87626
  let workflowSvc;
87627
+ let runStoreSvc;
86234
87628
  let systemEventDaoPromise;
86235
87629
  let jobQueuePromise;
86236
87630
  let queueConsumerPromise;
@@ -86242,7 +87636,7 @@ function createServerContext(appRt, options) {
86242
87636
  if (!dbPromise) {
86243
87637
  const created = (async () => {
86244
87638
  if (dbUrl !== IN_MEMORY_DATABASE_URL) {
86245
- await fs3.ensureDir(dirname13(dbUrl));
87639
+ await fs3.ensureDir(dirname14(dbUrl));
86246
87640
  }
86247
87641
  return createMigratedDbViaRuntime({ url: dbUrl });
86248
87642
  })();
@@ -86372,6 +87766,13 @@ function createServerContext(appRt, options) {
86372
87766
  systemEventDaoPromise ??= this.getDb().then((db2) => new SystemEventDao(db2));
86373
87767
  return systemEventDaoPromise;
86374
87768
  },
87769
+ runStoreService() {
87770
+ runStoreSvc ??= new RunStoreService({
87771
+ getDb: this.getDb.bind(this),
87772
+ secretValues: configuredSecretValues(options.env ?? process.env)
87773
+ });
87774
+ return runStoreSvc;
87775
+ },
86375
87776
  planningFolders() {
86376
87777
  return folders;
86377
87778
  },
@@ -86416,15 +87817,23 @@ function createServerContext(appRt, options) {
86416
87817
  init_system_event_tap();
86417
87818
 
86418
87819
  // ../server/src/open-url.ts
86419
- async function openUrl(url2) {
87820
+ init_dist5();
87821
+ async function openUrl(url2, deps = {}) {
86420
87822
  const argv = process.platform === "darwin" ? ["open", url2] : process.platform === "win32" ? ["cmd", "/c", "start", "", url2] : ["xdg-open", url2];
86421
- Bun.spawn(argv, { stdio: ["ignore", "ignore", "ignore"] });
87823
+ const executor = deps.executor ?? new NodeProcessExecutor3;
87824
+ await executor.run({
87825
+ command: argv[0],
87826
+ args: [...argv.slice(1)],
87827
+ forceBuffered: true,
87828
+ rejectOnError: false
87829
+ });
86422
87830
  }
86423
87831
 
86424
87832
  // ../server/src/serve.ts
86425
87833
  var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
86426
87834
  var SMOKE_JOB = "smoke";
86427
87835
  var TASK_ACTION_JOB = "task-action";
87836
+ var FEATURE_ACTION_JOB = "feature-action";
86428
87837
  var SYSTEM_EVENTS_PRUNE_CRON = "300000";
86429
87838
  var SMOKE_CRON = "600000";
86430
87839
  var defaultDeps = {
@@ -86508,16 +87917,59 @@ async function runTaskActionJob(ctx, env, payload, createAgentService = createTa
86508
87917
  throw new Error(`Task action ${job.action} for ${job.wbs} failed with exit code ${exitCode}`);
86509
87918
  }
86510
87919
  }
87920
+ function parseFeatureActionJob(payload) {
87921
+ if (typeof payload !== "object" || payload === null) {
87922
+ throw new Error("Invalid feature-action payload: expected object");
87923
+ }
87924
+ const candidate = payload;
87925
+ if (typeof candidate.featureId !== "string" || typeof candidate.action !== "string") {
87926
+ throw new Error("Invalid feature-action payload: missing featureId/action");
87927
+ }
87928
+ if (typeof candidate.command !== "string" || candidate.command.trim() === "") {
87929
+ throw new Error("Invalid feature-action payload: missing command");
87930
+ }
87931
+ return {
87932
+ featureId: candidate.featureId,
87933
+ action: candidate.action,
87934
+ command: candidate.command,
87935
+ channel: typeof candidate.channel === "string" ? candidate.channel : undefined,
87936
+ skipDeps: typeof candidate.skipDeps === "boolean" ? candidate.skipDeps : undefined
87937
+ };
87938
+ }
87939
+ async function runFeatureActionJob(ctx, env, payload, createAgentService = createTaskActionAgentService) {
87940
+ const job = parseFeatureActionJob(payload);
87941
+ const agentService = createAgentService === createTaskActionAgentService ? ctx.agentService() : createAgentService({
87942
+ cwd: ctx.cwd,
87943
+ env,
87944
+ events: ctx.eventBus(),
87945
+ output: { write: () => {}, error: () => {} }
87946
+ });
87947
+ const flags = {
87948
+ cwd: ctx.cwd,
87949
+ json: true,
87950
+ ...job.channel !== undefined ? { agent: job.channel } : {}
87951
+ };
87952
+ const exitCode = await agentService.run(job.command, flags);
87953
+ if (exitCode !== 0) {
87954
+ throw new Error(`Feature action ${job.action} for ${job.featureId} failed with exit code ${exitCode}`);
87955
+ }
87956
+ }
87957
+ async function handleTaskActionJob(ctx, env, payload) {
87958
+ await runTaskActionJob(ctx, env, payload);
87959
+ }
87960
+ async function handleFeatureActionJob(ctx, env, payload) {
87961
+ await runFeatureActionJob(ctx, env, payload);
87962
+ }
86511
87963
  async function resolveWebDistPath(configuredPath) {
86512
- const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute3(configuredPath) ? configuredPath : join20(process.cwd(), configuredPath)] : [
86513
- join20(process.cwd(), "dist/web"),
86514
- join20(import.meta.dir, "web"),
86515
- join20(dirname14(process.execPath), "web"),
86516
- join20(dirname14(process.execPath), "../web"),
86517
- join20(import.meta.dir, "../../../dist/web")
87964
+ const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute3(configuredPath) ? configuredPath : join22(process.cwd(), configuredPath)] : [
87965
+ join22(process.cwd(), "dist/web"),
87966
+ join22(import.meta.dir, "web"),
87967
+ join22(dirname15(process.execPath), "web"),
87968
+ join22(dirname15(process.execPath), "../web"),
87969
+ join22(import.meta.dir, "../../../dist/web")
86518
87970
  ];
86519
87971
  for (const candidate of candidates) {
86520
- if (await Bun.file(join20(candidate, "index.html")).exists()) {
87972
+ if (await Bun.file(join22(candidate, "index.html")).exists()) {
86521
87973
  return candidate;
86522
87974
  }
86523
87975
  }
@@ -86533,7 +87985,7 @@ async function startServer(options, deps = defaultDeps) {
86533
87985
  async start(appRt) {
86534
87986
  const fs3 = deps.createNodeFileSystem(process.cwd());
86535
87987
  if (options.dbUrl && options.dbUrl !== IN_MEMORY_DATABASE_URL) {
86536
- await fs3.ensureDir(dirname14(options.dbUrl));
87988
+ await fs3.ensureDir(dirname15(options.dbUrl));
86537
87989
  }
86538
87990
  let scheduler3;
86539
87991
  if (bootConfig.scheduler.enabled) {
@@ -86573,7 +88025,9 @@ async function startServer(options, deps = defaultDeps) {
86573
88025
  try {
86574
88026
  const dao2 = new SystemEventDao(await ctx.getDb());
86575
88027
  registerSystemEventTap(ctx.eventBus(), dao2, appRt.logger, {
86576
- diagnosticEnabled: bootConfig.events.diagnostic === true
88028
+ diagnosticEnabled: bootConfig.events.diagnostic === true,
88029
+ retention: bootConfig.events.retention,
88030
+ secretValues: configuredSecretValues(env)
86577
88031
  });
86578
88032
  appRt.logger.debug("system_events tap registered", {
86579
88033
  diagnostic: bootConfig.events.diagnostic === true
@@ -86585,14 +88039,14 @@ async function startServer(options, deps = defaultDeps) {
86585
88039
  const app = deps.createApp(appRt, { fs: fs3, ctx });
86586
88040
  if (bootConfig.jobqueue.enabled) {
86587
88041
  const registry2 = new JobHandlerRegistry;
88042
+ const retentionQuotas = resolveRetentionQuotas(bootConfig.events.retention);
86588
88043
  registry2.register(SYSTEM_EVENTS_PRUNE_JOB, async () => {
86589
88044
  const dao2 = await ctx.systemEventDao();
86590
- await dao2.prune(SYSTEM_EVENTS_CAP);
88045
+ await dao2.pruneQuotas(retentionQuotas);
86591
88046
  });
86592
88047
  registry2.register(SMOKE_JOB, async () => {});
86593
- registry2.register(TASK_ACTION_JOB, async (payload) => {
86594
- await runTaskActionJob(ctx, env, payload);
86595
- });
88048
+ registry2.register(TASK_ACTION_JOB, (payload) => handleTaskActionJob(ctx, env, payload));
88049
+ registry2.register(FEATURE_ACTION_JOB, (payload) => handleFeatureActionJob(ctx, env, payload));
86596
88050
  jobWorker = new JobWorkerService({
86597
88051
  consumer: await ctx.queueConsumer(),
86598
88052
  registry: registry2
@@ -86610,8 +88064,33 @@ async function startServer(options, deps = defaultDeps) {
86610
88064
  port: options.port,
86611
88065
  hostname: options.host
86612
88066
  });
88067
+ const projectRegistry = new ProjectRegistry;
88068
+ const projectCwd = process.cwd();
88069
+ const projectName = basename6(projectCwd);
88070
+ try {
88071
+ await projectRegistry.upsert({ path: projectCwd, name: projectName, port: server.port });
88072
+ } catch (err) {
88073
+ appRt.logger.warn("Failed to register project in ProjectRegistry", { error: String(err) });
88074
+ }
88075
+ let shuttingDown = false;
88076
+ const onSigInt = () => {
88077
+ shutdown("SIGINT");
88078
+ };
88079
+ const onSigTerm = () => {
88080
+ shutdown("SIGTERM");
88081
+ };
86613
88082
  const shutdown = async (signal) => {
88083
+ if (shuttingDown)
88084
+ return;
88085
+ shuttingDown = true;
88086
+ process.off("SIGINT", onSigInt);
88087
+ process.off("SIGTERM", onSigTerm);
86614
88088
  appRt.logger.info("Shutting down server", { signal });
88089
+ try {
88090
+ await projectRegistry.setPort(projectCwd, 0);
88091
+ } catch (err) {
88092
+ appRt.logger.warn("Failed to deregister project port in ProjectRegistry", { error: String(err) });
88093
+ }
86615
88094
  if (scheduler3)
86616
88095
  await scheduler3.stop();
86617
88096
  if (jobWorker)
@@ -86625,8 +88104,8 @@ async function startServer(options, deps = defaultDeps) {
86625
88104
  await appRt.stop("shutdown");
86626
88105
  process.exit(0);
86627
88106
  };
86628
- process.on("SIGINT", () => void shutdown("SIGINT"));
86629
- process.on("SIGTERM", () => void shutdown("SIGTERM"));
88107
+ process.on("SIGINT", onSigInt);
88108
+ process.on("SIGTERM", onSigTerm);
86630
88109
  const url2 = `http://${options.host}:${options.port}`;
86631
88110
  appRt.logger.info("Server started", {
86632
88111
  port: options.port,
@@ -86642,12 +88121,14 @@ async function startServer(options, deps = defaultDeps) {
86642
88121
  }
86643
88122
  });
86644
88123
  }
88124
+ // ../server/src/worker-app.ts
88125
+ var startedAt2 = Date.now();
86645
88126
  // ../server/src/index.ts
86646
88127
  if (false) {}
86647
88128
 
86648
88129
  // src/commands/serve.ts
86649
88130
  function resolveServeDbUrl(cwd, env, configuredUrl) {
86650
- return env.DATABASE_URL === undefined ? join21(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
88131
+ return env.DATABASE_URL === undefined ? join23(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
86651
88132
  }
86652
88133
  function registerServeCommand(program2, context4) {
86653
88134
  program2.command("serve").summary("start the Spur web server (local fallback)").option("--port <n>", "Server port (env: PORT, default: 3000)", parseInt).option("--host <addr>", "Bind address (env: HOST, default: localhost)").option("--no-open", "Skip opening the browser").option("--cwd <path>", "Working directory", context4.cwd).option("--json", "Output { port, url, pid } and exit").action(async (options) => {
@@ -86686,7 +88167,7 @@ function registerServeCommand(program2, context4) {
86686
88167
  }
86687
88168
 
86688
88169
  // src/commands/status.ts
86689
- import { join as join22 } from "path";
88170
+ import { join as join24 } from "path";
86690
88171
 
86691
88172
  // src/errors.ts
86692
88173
  class CommandError extends Error {
@@ -86757,8 +88238,8 @@ function registerStatusCommand(program2, context4) {
86757
88238
  }
86758
88239
  async function runStatusCore(path9, options, context4) {
86759
88240
  const [packageJsonExists, spurConfigExists, git, agentSpecs] = await Promise.all([
86760
- context4.fs.exists(join22(context4.cwd, "package.json")),
86761
- context4.fs.exists(join22(context4.cwd, ".spur", "config.yaml")),
88241
+ context4.fs.exists(join24(context4.cwd, "package.json")),
88242
+ context4.fs.exists(join24(context4.cwd, ".spur", "config.yaml")),
86762
88243
  gitContext(context4.cwd),
86763
88244
  listAgentSpecIds(context4)
86764
88245
  ]);
@@ -86786,26 +88267,27 @@ async function runStatusCore(path9, options, context4) {
86786
88267
  return status.ok ? 0 : 1;
86787
88268
  }
86788
88269
  async function listAgentSpecIds(context4) {
86789
- const dir = join22(context4.cwd, ".spur", "agents");
88270
+ const dir = join24(context4.cwd, ".spur", "agents");
86790
88271
  if (!await context4.fs.exists(dir))
86791
88272
  return [];
86792
88273
  const entries = await context4.fs.readDir(dir);
86793
88274
  return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
86794
88275
  }
86795
88276
  async function readTargetStatus(context4, targetPath) {
86796
- const resolved = join22(context4.cwd, targetPath);
86797
- const stat2 = await context4.fs.stat(resolved);
86798
- if (stat2 === null)
88277
+ const resolved = join24(context4.cwd, targetPath);
88278
+ const stat = await context4.fs.stat(resolved);
88279
+ if (stat === null)
86799
88280
  throw new CommandError(`status failed: path does not exist at ${resolved}`);
86800
- return { path: targetPath, size: stat2.size, isFile: stat2.isFile(), isDirectory: stat2.isDirectory() };
88281
+ return { path: targetPath, size: stat.size, isFile: stat.isFile(), isDirectory: stat.isDirectory() };
86801
88282
  }
86802
88283
 
86803
88284
  // src/commands/task.ts
86804
88285
  init_src3();
86805
88286
  init_loader();
86806
88287
  init_src2();
86807
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
86808
- import { join as join23 } from "path";
88288
+ init_dist5();
88289
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
88290
+ import { join as join25 } from "path";
86809
88291
  // schemas/section-matrix.schema.json
86810
88292
  var section_matrix_schema_default = {
86811
88293
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -87489,7 +88971,7 @@ ${result.content}`);
87489
88971
  let forcedDoneVerdict;
87490
88972
  if (status === "done") {
87491
88973
  const current = await svc.show(wbs);
87492
- const verdictDir = options.verdictDir ?? join23(context4.cwd, ".spur", "run");
88974
+ const verdictDir = options.verdictDir ?? join25(context4.cwd, ".spur", "run");
87493
88975
  const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
87494
88976
  const guardOutcome = evaluateDoneTransition({
87495
88977
  wbs,
@@ -87784,7 +89266,7 @@ ${result.content}`);
87784
89266
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
87785
89267
  let answerText;
87786
89268
  try {
87787
- answerText = readFileSync8(answerPath, "utf-8");
89269
+ answerText = await context4.fs.readFile(context4.fs.resolve(answerPath));
87788
89270
  } catch {
87789
89271
  context4.output.error(`Answer file not found: ${answerPath}`);
87790
89272
  context4.setExitCode(1);
@@ -87793,9 +89275,8 @@ ${result.content}`);
87793
89275
  const taskCheckPassed = true;
87794
89276
  const result = deriveVerdict2(answerText, taskCheckPassed);
87795
89277
  const jsonOut = JSON.stringify({ wbs, ...result, source: "spur-task-verdict" }, null, 2);
87796
- const { mkdirSync: mkdirSync5, writeFileSync: writeFileSync4 } = await import("fs");
87797
- mkdirSync5(".spur/run", { recursive: true });
87798
- writeFileSync4(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
89278
+ await context4.fs.ensureDir(".spur/run");
89279
+ await context4.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
87799
89280
  `);
87800
89281
  if (options.json) {
87801
89282
  context4.output.write(jsonOut);
@@ -87810,7 +89291,7 @@ ${result.content}`);
87810
89291
  const inputPath = options.fromFile ?? ".spur/run/verifyall-batch-input.json";
87811
89292
  let raw2;
87812
89293
  try {
87813
- raw2 = readFileSync8(inputPath, "utf-8");
89294
+ raw2 = await context4.fs.readFile(context4.fs.resolve(inputPath));
87814
89295
  } catch {
87815
89296
  context4.output.error(`Batch input file not found: ${inputPath}`);
87816
89297
  context4.setExitCode(1);
@@ -88030,17 +89511,17 @@ function loadTemplateContent(projectRoot, variant) {
88030
89511
  return templateContentCache.get(variant);
88031
89512
  if (templateMissSet.has(variant))
88032
89513
  return;
88033
- const localPath = join23(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
88034
- if (existsSync9(localPath)) {
88035
- const content = readFileSync8(localPath, "utf8");
89514
+ const localPath = join25(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
89515
+ if (existsSync10(localPath)) {
89516
+ const content = readFileSync9(localPath, "utf8");
88036
89517
  templateContentCache.set(variant, content);
88037
89518
  return content;
88038
89519
  }
88039
89520
  const root = bundledConfigRoot();
88040
89521
  if (root !== null) {
88041
- const templatePath = join23(root, "templates", "task", `${variant}.md`);
88042
- if (existsSync9(templatePath)) {
88043
- const content = readFileSync8(templatePath, "utf8");
89522
+ const templatePath = join25(root, "templates", "task", `${variant}.md`);
89523
+ if (existsSync10(templatePath)) {
89524
+ const content = readFileSync9(templatePath, "utf8");
88044
89525
  templateContentCache.set(variant, content);
88045
89526
  return content;
88046
89527
  }
@@ -88054,15 +89535,15 @@ function loadTemplateBodies(projectRoot, variant) {
88054
89535
  if (cached2 !== undefined)
88055
89536
  return cached2;
88056
89537
  let bodies = {};
88057
- const localPath = join23(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
88058
- if (existsSync9(localPath)) {
88059
- bodies = extractTemplateBodies(readFileSync8(localPath, "utf8"));
89538
+ const localPath = join25(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
89539
+ if (existsSync10(localPath)) {
89540
+ bodies = extractTemplateBodies(readFileSync9(localPath, "utf8"));
88060
89541
  } else {
88061
89542
  const root = bundledConfigRoot();
88062
89543
  if (root !== null) {
88063
- const templatePath = join23(root, "templates", "task", `${variant}.md`);
88064
- if (existsSync9(templatePath)) {
88065
- bodies = extractTemplateBodies(readFileSync8(templatePath, "utf8"));
89544
+ const templatePath = join25(root, "templates", "task", `${variant}.md`);
89545
+ if (existsSync10(templatePath)) {
89546
+ bodies = extractTemplateBodies(readFileSync9(templatePath, "utf8"));
88066
89547
  }
88067
89548
  }
88068
89549
  }
@@ -88102,8 +89583,9 @@ async function loadSectionMatrix(projectRoot) {
88102
89583
  return promise2;
88103
89584
  }
88104
89585
  async function loadSectionMatrixUncached(projectRoot) {
88105
- const localPath = join23(projectRoot, ".spur", "tasks", "section-matrix.yaml");
88106
- if (existsSync9(localPath)) {
89586
+ const fs3 = createNodeFileSystem3(projectRoot);
89587
+ const localPath = fs3.resolve(".spur", "tasks", "section-matrix.yaml");
89588
+ if (await fs3.exists(localPath)) {
88107
89589
  const data = await loadStructuredSpurConfig(localPath, {
88108
89590
  validateJsonSchema: true,
88109
89591
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
@@ -88112,8 +89594,8 @@ async function loadSectionMatrixUncached(projectRoot) {
88112
89594
  }
88113
89595
  const root = bundledConfigRoot();
88114
89596
  if (root !== null) {
88115
- const matrixPath = join23(root, "tasks", "section-matrix.yaml");
88116
- if (existsSync9(matrixPath)) {
89597
+ const matrixPath = join25(root, "tasks", "section-matrix.yaml");
89598
+ if (await fs3.exists(matrixPath)) {
88117
89599
  const data = await loadStructuredSpurConfig(matrixPath, {
88118
89600
  validateJsonSchema: true,
88119
89601
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
@@ -88134,6 +89616,7 @@ var FALLBACK_MATRIX = {
88134
89616
 
88135
89617
  // src/commands/team.ts
88136
89618
  init_src3();
89619
+ init_dist6();
88137
89620
  var _testFetch;
88138
89621
  function teamFetch(url2, init) {
88139
89622
  const fetcher = _testFetch ?? fetch;
@@ -88168,10 +89651,15 @@ function registerTeamCommand(program2, context4) {
88168
89651
  });
88169
89652
  }
88170
89653
  async function runTeamAssign(taskId, agentId, context4) {
88171
- const svc = new TeamService(context4);
88172
- await svc.assignTask(taskId, agentId);
88173
- context4.output.write(`assigned ${taskId} \u2192 ${agentId}`);
88174
- return 0;
89654
+ const { svc, ledger } = await makeTeamServiceWithLedger(context4);
89655
+ try {
89656
+ await svc.assignTask(taskId, agentId);
89657
+ context4.output.write(`assigned ${taskId} \u2192 ${agentId}`);
89658
+ return 0;
89659
+ } finally {
89660
+ await ledger.flush();
89661
+ ledger.unsubscribe();
89662
+ }
88175
89663
  }
88176
89664
  async function runTeamStatus(options, context4) {
88177
89665
  const svc = new TeamService(context4);
@@ -88314,55 +89802,74 @@ function formatTeamBlock(team) {
88314
89802
  return [header, ...rows].join(`
88315
89803
  `);
88316
89804
  }
89805
+ async function makeTeamServiceWithLedger(context4) {
89806
+ const bus = new EventBus;
89807
+ const ledger = await attachSystemEventLedger(bus, context4);
89808
+ const svc = new TeamService({
89809
+ ...context4,
89810
+ eventBus: bus
89811
+ });
89812
+ return { svc, ledger };
89813
+ }
88317
89814
  async function runTeamUp(team, options, context4) {
88318
- const svc = new TeamService(context4);
88319
- let result;
89815
+ const { svc, ledger } = await makeTeamServiceWithLedger(context4);
88320
89816
  try {
88321
- result = await svc.materializeTeam(team, { check: options.check === true });
88322
- } catch (error51) {
88323
- context4.output.error(error51 instanceof Error ? error51.message : String(error51));
88324
- return 1;
88325
- }
88326
- const started = [];
88327
- if (options.check !== true && result.upserted.length > 0) {
88328
- const specs = await svc.listAgentSpecs();
88329
- const autostart = specs.filter((spec) => spec.autoStart === true && result.upserted.includes(spec.id));
88330
- for (const spec of autostart) {
88331
- const res = await performTeamStart(spec.id, { server: options.server });
88332
- if ("ok" in res && res.ok === true)
88333
- started.push(spec.id);
89817
+ let result;
89818
+ try {
89819
+ result = await svc.materializeTeam(team, { check: options.check === true });
89820
+ } catch (error51) {
89821
+ context4.output.error(error51 instanceof Error ? error51.message : String(error51));
89822
+ return 1;
88334
89823
  }
89824
+ const started = [];
89825
+ if (options.check !== true && result.upserted.length > 0) {
89826
+ const specs = await svc.listAgentSpecs();
89827
+ const autostart = specs.filter((spec) => spec.autoStart === true && result.upserted.includes(spec.id));
89828
+ for (const spec of autostart) {
89829
+ const res = await performTeamStart(spec.id, { server: options.server });
89830
+ if ("ok" in res && res.ok === true)
89831
+ started.push(spec.id);
89832
+ }
89833
+ }
89834
+ if (options.json === true) {
89835
+ context4.output.write(toJson2({ ...result, started }));
89836
+ } else {
89837
+ const verb = options.check === true ? "would materialize" : "materialized";
89838
+ const startNote = started.length > 0 ? `, started ${started.length}` : "";
89839
+ context4.output.write(`team ${team}: ${verb} ${result.upserted.length} member(s), prune ${result.orphaned.length}${startNote}`);
89840
+ }
89841
+ return 0;
89842
+ } finally {
89843
+ await ledger.flush();
89844
+ ledger.unsubscribe();
88335
89845
  }
88336
- if (options.json === true) {
88337
- context4.output.write(toJson2({ ...result, started }));
88338
- } else {
88339
- const verb = options.check === true ? "would materialize" : "materialized";
88340
- const startNote = started.length > 0 ? `, started ${started.length}` : "";
88341
- context4.output.write(`team ${team}: ${verb} ${result.upserted.length} member(s), prune ${result.orphaned.length}${startNote}`);
88342
- }
88343
- return 0;
88344
89846
  }
88345
89847
  async function runTeamDown(team, options, context4) {
88346
- const svc = new TeamService(context4);
88347
- let result;
89848
+ const { svc, ledger } = await makeTeamServiceWithLedger(context4);
88348
89849
  try {
88349
- result = await svc.teardownTeam(team, { purge: options.purge === true });
88350
- } catch (error51) {
88351
- context4.output.error(error51 instanceof Error ? error51.message : String(error51));
88352
- return 1;
88353
- }
88354
- const stopped = [];
88355
- for (const id of result.stopped) {
88356
- const res = await performTeamStop(id, { server: options.server });
88357
- if ("ok" in res && res.ok === true)
88358
- stopped.push(id);
88359
- }
88360
- if (options.json === true) {
88361
- context4.output.write(toJson2({ ...result, stopped }));
88362
- } else {
88363
- context4.output.write(`team ${team}: stopped ${stopped.length}, purged ${result.purged.length}`);
89850
+ let result;
89851
+ try {
89852
+ result = await svc.teardownTeam(team, { purge: options.purge === true });
89853
+ } catch (error51) {
89854
+ context4.output.error(error51 instanceof Error ? error51.message : String(error51));
89855
+ return 1;
89856
+ }
89857
+ const stopped = [];
89858
+ for (const id of result.stopped) {
89859
+ const res = await performTeamStop(id, { server: options.server });
89860
+ if ("ok" in res && res.ok === true)
89861
+ stopped.push(id);
89862
+ }
89863
+ if (options.json === true) {
89864
+ context4.output.write(toJson2({ ...result, stopped }));
89865
+ } else {
89866
+ context4.output.write(`team ${team}: stopped ${stopped.length}, purged ${result.purged.length}`);
89867
+ }
89868
+ return 0;
89869
+ } finally {
89870
+ await ledger.flush();
89871
+ ledger.unsubscribe();
88364
89872
  }
88365
- return 0;
88366
89873
  }
88367
89874
 
88368
89875
  // src/commands/workflow.ts
@@ -88370,10 +89877,29 @@ init_src3();
88370
89877
  init_loader();
88371
89878
  init_dist7();
88372
89879
  init_dist6();
88373
- import { spawn as spawn2 } from "child_process";
88374
- import { resolve as resolve5 } from "path";
89880
+ init_dist5();
89881
+ import { resolve as resolve8 } from "path";
88375
89882
  import { createInterface } from "readline";
88376
89883
  import { setTimeout as sleep3 } from "timers/promises";
89884
+ function shQuote2(value2) {
89885
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
89886
+ }
89887
+ async function spawnAsyncWorkflowWorker(spurBin, cmd) {
89888
+ const line = [spurBin, ...cmd].map(shQuote2).join(" ");
89889
+ const env = {};
89890
+ for (const [key, value2] of Object.entries(process.env)) {
89891
+ if (value2 !== undefined)
89892
+ env[key] = value2;
89893
+ }
89894
+ env.SPUR_ASYNC_WORKER = "1";
89895
+ await new NodeProcessExecutor3().run({
89896
+ command: process.platform === "win32" ? "cmd" : "/bin/sh",
89897
+ args: process.platform === "win32" ? ["/c", `start /b "" ${[spurBin, ...cmd].map((c3) => `"${c3.replace(/"/g, '""')}"`).join(" ")}`] : ["-c", `nohup ${line} </dev/null >/dev/null 2>&1 &`],
89898
+ env,
89899
+ forceBuffered: true,
89900
+ rejectOnError: false
89901
+ });
89902
+ }
88377
89903
  function parseVars(raw2) {
88378
89904
  if (raw2 === undefined) {
88379
89905
  return;
@@ -88410,14 +89936,17 @@ async function resolveWorkflowPaths(cwd) {
88410
89936
  }
88411
89937
  }
88412
89938
  function registerWorkflowCommand(program2, context4) {
88413
- const makeSvc = (json3, observabilityBus) => new WorkflowAppService({
89939
+ const makeSvc = (json3, bus) => new WorkflowAppService({
88414
89940
  cwd: context4.cwd,
88415
89941
  getDb: () => context4.getDb(),
88416
89942
  agentService: () => context4.agentService(),
88417
89943
  ruleService: () => context4.ruleService(),
88418
89944
  hitlResponder: () => context4.hitlResponder(json3),
88419
89945
  embeddedSchemas: () => EMBEDDED_SPUR_SCHEMAS,
88420
- ...observabilityBus ? { observabilityBus: () => observabilityBus } : {}
89946
+ ...bus ? {
89947
+ observabilityBus: () => bus,
89948
+ events: () => bus
89949
+ } : {}
88421
89950
  });
88422
89951
  const workflow = program2.command("workflow").summary("validate and execute workflow YAML files");
88423
89952
  workflow.command("validate").description("Validate a workflow definition.").argument("<file>", "Workflow YAML file").option("--no-schema", "Skip schema validation").option("--json", "Output machine-readable JSON where supported").action(async (file2, options) => {
@@ -88475,12 +90004,7 @@ ${result.errors.map((m) => ` - ${m}`).join(`
88475
90004
  cmd.push("--trace-file");
88476
90005
  }
88477
90006
  try {
88478
- const child = spawn2(spurBin, cmd, {
88479
- stdio: "ignore",
88480
- detached: true,
88481
- env: { ...process.env, SPUR_ASYNC_WORKER: "1" }
88482
- });
88483
- child.unref();
90007
+ await spawnAsyncWorkflowWorker(spurBin, cmd);
88484
90008
  } catch {
88485
90009
  const result2 = await makeSvc(options.json).run(file2, {
88486
90010
  runId: runId2,
@@ -88508,21 +90032,19 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88508
90032
  const vars = { spurBin: resolveSpurBin(), ...parseVars(options.vars) };
88509
90033
  const runId = options.runId || crypto.randomUUID();
88510
90034
  const humanProgress = !json3 && !quiet && !silent;
88511
- let bus;
90035
+ const bus = new EventBus;
90036
+ const ledger = await attachSystemEventLedger(bus, context4);
88512
90037
  let traceWriter;
88513
90038
  const heartbeats = new Map;
88514
- if (humanProgress || options.traceFile === true || options.steer === true) {
88515
- bus = new EventBus;
88516
- }
88517
- if (options.traceFile === true && bus !== undefined) {
90039
+ if (options.traceFile === true) {
88518
90040
  traceWriter = new WorkflowTraceWriter(context4.cwd, runId);
88519
90041
  traceWriter.attach(bus);
88520
90042
  }
88521
- if (humanProgress && bus !== undefined) {
90043
+ if (humanProgress) {
88522
90044
  context4.output.write(`Run: ${runId}`);
88523
90045
  if (options.plan !== false) {
88524
90046
  try {
88525
- const def = await loadWorkflowDef(resolve5(context4.cwd, file2), { validateSchema: false });
90047
+ const def = await loadWorkflowDef(resolve8(context4.cwd, file2), { validateSchema: false });
88526
90048
  context4.output.write(renderRunPlan(def));
88527
90049
  } catch {}
88528
90050
  }
@@ -88536,9 +90058,9 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88536
90058
  bus.on("workflow.transition", report);
88537
90059
  bus.on("workflow.action.started", (event2) => {
88538
90060
  report(event2);
88539
- const startedAt2 = Date.parse(event2.at);
90061
+ const startedAt3 = Date.parse(event2.at);
88540
90062
  const timer = setInterval(() => {
88541
- const line = renderActionHeartbeat(event2, Math.max(0, Date.now() - startedAt2), { detail });
90063
+ const line = renderActionHeartbeat(event2, Math.max(0, Date.now() - startedAt3), { detail });
88542
90064
  if (line !== null)
88543
90065
  context4.output.write(line);
88544
90066
  }, 30000);
@@ -88563,7 +90085,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88563
90085
  });
88564
90086
  }
88565
90087
  const steeringController = options.steer === true ? new WorkflowSteeringController((ack) => {
88566
- bus?.emit("workflow.steering", ack);
90088
+ bus.emit("workflow.steering", ack);
88567
90089
  context4.output.write(`[steer] ${ack.accepted ? "ack" : "nack"} ${ack.operation} \xB7 ${ack.reason ?? `version=${ack.version}`}`);
88568
90090
  }, configuredSecretValues(context4.env), new Set(["operator"]), (snapshot) => {
88569
90091
  if (snapshot.state === "boundary") {
@@ -88588,6 +90110,8 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88588
90110
  clearInterval(timer);
88589
90111
  heartbeats.clear();
88590
90112
  await traceWriter?.flush();
90113
+ await ledger.flush();
90114
+ ledger.unsubscribe();
88591
90115
  steeringInput?.close();
88592
90116
  }
88593
90117
  if (json3)
@@ -88599,7 +90123,9 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88599
90123
  });
88600
90124
  workflow.command("continue").description("Resume a paused (HITL) workflow run. Omit run-id to resume the most recent paused run.").argument("[run-id]", "Run ID to resume (default: the most recent paused run)").option("--yes", "Skip the CLI resume confirmation (does not set the persisted HITL answer)").option("--json", "Output machine-readable JSON where supported").action(async (runId, options) => {
88601
90125
  const json3 = options.json === true;
88602
- const svc = makeSvc(json3);
90126
+ const bus = new EventBus;
90127
+ const ledger = await attachSystemEventLedger(bus, context4);
90128
+ const svc = makeSvc(json3, bus);
88603
90129
  try {
88604
90130
  let targetId = runId;
88605
90131
  if (targetId === undefined) {
@@ -88630,6 +90156,9 @@ Monitor with: spur workflow trace ${runId2} --follow`);
88630
90156
  } catch (err) {
88631
90157
  context4.output.error(String(err));
88632
90158
  context4.setExitCode(1);
90159
+ } finally {
90160
+ await ledger.flush();
90161
+ ledger.unsubscribe();
88633
90162
  }
88634
90163
  });
88635
90164
  workflow.command("clean").description("Finalize orphaned runs stuck in running/pending past a staleness threshold (mark as failed). " + "To cancel a single live run by id, use `spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes", "30").option("--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)").option("--dry-run", "List what would be cleaned without writing").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
@@ -88864,7 +90393,7 @@ init_src3();
88864
90393
  init_src();
88865
90394
  init_src2();
88866
90395
  init_dist5();
88867
- import { dirname as dirname15, join as join24, resolve as resolve6 } from "path";
90396
+ import { dirname as dirname16, join as join26, resolve as resolve9 } from "path";
88868
90397
  import { isatty as isatty4 } from "tty";
88869
90398
 
88870
90399
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -90247,7 +91776,7 @@ class DefaultHitlResponder {
90247
91776
 
90248
91777
  // src/context.ts
90249
91778
  function createCliContext(options) {
90250
- const cwd = resolve6(options.cwd ?? process.cwd());
91779
+ const cwd = resolve9(options.cwd ?? process.cwd());
90251
91780
  const env = options.env ?? process.env;
90252
91781
  const fs3 = createNodeFileSystem3(cwd);
90253
91782
  let dbPromise;
@@ -90265,7 +91794,15 @@ function createCliContext(options) {
90265
91794
  setExitCode: options.setExitCode ?? noopSetExitCode,
90266
91795
  output: options.output,
90267
91796
  getDb,
90268
- agentService: () => new AgentService({ cwd, env, output: options.output, agentConfig: options.agentConfig }),
91797
+ ...options.agentConfig !== undefined ? { agentConfig: options.agentConfig } : {},
91798
+ agentService: (serviceOptions) => new AgentService({
91799
+ cwd,
91800
+ env,
91801
+ output: options.output,
91802
+ agentConfig: options.agentConfig,
91803
+ ...serviceOptions?.events !== undefined ? { events: serviceOptions.events } : {},
91804
+ ...serviceOptions?.processRegistry !== undefined ? { processRegistry: serviceOptions.processRegistry } : {}
91805
+ }),
90269
91806
  ruleService: () => new RuleService({ cwd, env, fs: fs3, output: options.output, getDb }),
90270
91807
  hitlResponder: (json3) => {
90271
91808
  if (isatty4(1) && json3 !== true)
@@ -90278,10 +91815,10 @@ function createCliContext(options) {
90278
91815
  function noopSetExitCode(_code) {}
90279
91816
  async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
90280
91817
  const config4 = buildConfigFromEnv(env);
90281
- const configuredUrl = env.DATABASE_URL === undefined ? join24(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
91818
+ const configuredUrl = env.DATABASE_URL === undefined ? join26(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
90282
91819
  const url2 = dbUrl ?? configuredUrl;
90283
91820
  if (url2 !== IN_MEMORY_DATABASE_URL) {
90284
- await createNodeFileSystem3().ensureDir(dirname15(url2));
91821
+ await createNodeFileSystem3().ensureDir(dirname16(url2));
90285
91822
  }
90286
91823
  return createMigratedDb({ url: url2 });
90287
91824
  }
@@ -90343,6 +91880,7 @@ async function runCommandDispatch(argv, context4, output2) {
90343
91880
  registerInitCommand(program2, context4);
90344
91881
  registerMessageCommand(program2, context4);
90345
91882
  registerMigrateCommand(program2, context4);
91883
+ registerProjectsCommand(program2, context4);
90346
91884
  registerRuleCommand(program2, context4);
90347
91885
  registerStatusCommand(program2, context4);
90348
91886
  registerServeCommand(program2, context4);