@gobing-ai/spur 0.3.28 → 0.3.29

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.
package/spur.js CHANGED
@@ -37121,6 +37121,7 @@ var init_finding_codes = __esm(() => {
37121
37121
  "L3.solution-file-line",
37122
37122
  "L3.review-priority-table",
37123
37123
  "L3.testing-coverage",
37124
+ "L3.required-section-placeholder",
37124
37125
  "L3.plan-format",
37125
37126
  "L3.unchecked-checklist",
37126
37127
  "L3.ac-checklist-text",
@@ -37150,8 +37151,9 @@ var init_finding_codes = __esm(() => {
37150
37151
  "L4.uncovered-feature-scenario",
37151
37152
  "L4.verifying-incomplete-tasks",
37152
37153
  "L4.dogfood-missing",
37153
- "L4.scenario-unverified",
37154
- "L4.stale-line-anchor"
37154
+ "L4.stale-line-anchor",
37155
+ "L4.malformed-verdict-artifact",
37156
+ "L4.scenario-unverified"
37155
37157
  ];
37156
37158
  FINDING_CODES = {
37157
37159
  L1_MARKDOWN_PARSE: "L1.markdown-parse",
@@ -37165,6 +37167,7 @@ var init_finding_codes = __esm(() => {
37165
37167
  L3_SOLUTION_FILE_LINE: "L3.solution-file-line",
37166
37168
  L3_REVIEW_PRIORITY_TABLE: "L3.review-priority-table",
37167
37169
  L3_TESTING_COVERAGE: "L3.testing-coverage",
37170
+ L3_REQUIRED_SECTION_PLACEHOLDER: "L3.required-section-placeholder",
37168
37171
  L3_PLAN_FORMAT: "L3.plan-format",
37169
37172
  L3_UNCHECKED_CHECKLIST: "L3.unchecked-checklist",
37170
37173
  L3_AC_CHECKLIST_TEXT: "L3.ac-checklist-text",
@@ -37192,10 +37195,11 @@ var init_finding_codes = __esm(() => {
37192
37195
  L4_ORPHAN_SCENARIOS: "L4.orphan-scenarios",
37193
37196
  L4_UNCOVERED_TASK_SCENARIO: "L4.uncovered-task-scenario",
37194
37197
  L4_UNCOVERED_FEATURE_SCENARIO: "L4.uncovered-feature-scenario",
37195
- L4_VERIFYING_INCOMPLETE_TASKS: "L4.verifying-incomplete-tasks",
37196
37198
  L4_DOGFOOD_MISSING: "L4.dogfood-missing",
37197
- L4_SCENARIO_UNVERIFIED: "L4.scenario-unverified",
37198
- L4_STALE_LINE_ANCHOR: "L4.stale-line-anchor"
37199
+ L4_VERIFYING_INCOMPLETE_TASKS: "L4.verifying-incomplete-tasks",
37200
+ L4_STALE_LINE_ANCHOR: "L4.stale-line-anchor",
37201
+ L4_MALFORMED_VERDICT_ARTIFACT: "L4.malformed-verdict-artifact",
37202
+ L4_SCENARIO_UNVERIFIED: "L4.scenario-unverified"
37199
37203
  };
37200
37204
  });
37201
37205
 
@@ -37269,7 +37273,7 @@ function buildConfigFromEnv(env = process.env) {
37269
37273
  }
37270
37274
  });
37271
37275
  }
37272
- var SPUR_ENV_VARS, SPUR_LOG_LEVELS, DEFAULT_TASKS_DIR = "docs/tasks", DEFAULT_FEATURES_DIR = "docs/features", DEFAULT_DATABASE_URL = ".spur/spur.db", IN_MEMORY_DATABASE_URL = ":memory:", folderConfigSchema, tasksConfigSchema, featuresConfigSchema, EXECUTOR_CAPABILITY_TIERS, executorCapabilityTierSchema, AgentExecutorConfigSchema, AGENT_ID_REGEX, TeamMemberConfigSchema, TeamConfigSchema, AgentConfigSchema, RulesConfigSchema, WorkflowsConfigSchema, RedactionConfigSchema, spurConfigSchema, configSchema2;
37276
+ var SPUR_ENV_VARS, SPUR_LOG_LEVELS, DEFAULT_TASKS_DIR = "docs/tasks", DEFAULT_FEATURES_DIR = "docs/features", DEFAULT_DATABASE_URL = ".spur/spur.db", IN_MEMORY_DATABASE_URL = ":memory:", folderConfigSchema, tasksConfigSchema, featuresConfigSchema, EXECUTOR_CAPABILITY_TIERS, executorCapabilityTierSchema, AgentExecutorConfigSchema, AGENT_ID_REGEX, TeamMemberConfigSchema, TeamConfigSchema, AgentOutputConfigSchema, AgentConfigSchema, RulesConfigSchema, WorkflowsConfigSchema, RedactionConfigSchema, spurConfigSchema, configSchema2;
37273
37277
  var init_src = __esm(() => {
37274
37278
  init_zod();
37275
37279
  init_finding_codes();
@@ -37339,11 +37343,16 @@ var init_src = __esm(() => {
37339
37343
  autostart: exports_external.boolean().optional(),
37340
37344
  members: exports_external.array(TeamMemberConfigSchema).min(1)
37341
37345
  });
37346
+ AgentOutputConfigSchema = exports_external.object({
37347
+ "max-bytes": exports_external.number().int().positive().optional(),
37348
+ "max-lines": exports_external.number().int().positive().optional()
37349
+ });
37342
37350
  AgentConfigSchema = exports_external.object({
37343
37351
  default: exports_external.string().optional(),
37344
37352
  executors: exports_external.array(AgentExecutorConfigSchema).optional(),
37345
37353
  "default-by-phase": exports_external.record(exports_external.string(), exports_external.string()).optional(),
37346
- team: exports_external.record(exports_external.string(), TeamConfigSchema).optional()
37354
+ team: exports_external.record(exports_external.string(), TeamConfigSchema).optional(),
37355
+ output: AgentOutputConfigSchema.optional()
37347
37356
  }).superRefine((value, ctx) => {
37348
37357
  const executors = value.executors;
37349
37358
  if (executors !== undefined) {
@@ -37356,6 +37365,13 @@ var init_src = __esm(() => {
37356
37365
  path: ["executors", index2, "name"]
37357
37366
  });
37358
37367
  }
37368
+ if (executor.name === "inline" || executor.name === "auto") {
37369
+ ctx.addIssue({
37370
+ code: exports_external.ZodIssueCode.custom,
37371
+ message: `Executor name "${executor.name}" is reserved (the --agent selector value '${executor.name}' has fixed semantics); rename executor at index ${index2}.`,
37372
+ path: ["executors", index2, "name"]
37373
+ });
37374
+ }
37359
37375
  seen.add(executor.name);
37360
37376
  }
37361
37377
  }
@@ -39182,7 +39198,7 @@ async function loadStructuredSpurConfig(configPath, opts) {
39182
39198
  function defaultFoldersConfig() {
39183
39199
  return {
39184
39200
  active_folder: DEFAULT_TASKS_DIR,
39185
- folders: { [DEFAULT_TASKS_DIR]: { base_counter: 0 } }
39201
+ folders: { [DEFAULT_TASKS_DIR]: { baseCounter: 0 } }
39186
39202
  };
39187
39203
  }
39188
39204
  function defaultPlanningFolders() {
@@ -39219,8 +39235,7 @@ async function resolvePlanningFoldersUncached(fs2) {
39219
39235
  const tasks = tasksConfigSchema.parse(parsed.tasks);
39220
39236
  const folders = {};
39221
39237
  for (const [path8, fc] of Object.entries(tasks.folders)) {
39222
- const validated = folderConfigSchema.parse(fc);
39223
- folders[path8] = { base_counter: validated.baseCounter, label: validated.label };
39238
+ folders[path8] = folderConfigSchema.parse(fc);
39224
39239
  }
39225
39240
  const foldersConfig = {
39226
39241
  active_folder: tasks.active,
@@ -58289,9 +58304,6 @@ function normalizeCapabilityTier(value) {
58289
58304
  function isTierEligible(candidateTier, minTier) {
58290
58305
  return TIER_RANK[candidateTier] >= TIER_RANK[minTier];
58291
58306
  }
58292
- function pickStartingTier(policy) {
58293
- return policy.min_tier;
58294
- }
58295
58307
  function getNextFallback(policy, signal, currentTier) {
58296
58308
  const currentRank = currentTier ? TIER_RANK[currentTier] : 0;
58297
58309
  const match = policy.fallback.find((f) => f.trigger === signal && TIER_RANK[f.tier] > currentRank);
@@ -58357,12 +58369,12 @@ function parseStageRecord(raw, consumerVersion = STAGE_REGISTRY_SCHEMA_VERSION)
58357
58369
  function getCanonicalStage(idOrAlias) {
58358
58370
  return CANONICAL_STAGE_BY_KEY.get(idOrAlias);
58359
58371
  }
58360
- var STAGE_REGISTRY_SCHEMA_VERSION, stageSchemaVersionSchema, AUTHORITY_LANES, MUTATION_CLASSES, EXECUTION_KINDS, inlineExecutionSchema, subprocessExecutionSchema, deterministicExecutionSchema, hitlExecutionSchema, irreversibleExecutionSchema, executionKindSchema, ARTIFACT_DIRECTIONS, stageArtifactSchema, stageGateSchema, stageRetryPolicySchema, CAPABILITY_TIERS, LEGACY_CAPABLE_ALIAS = "capable", capabilityTierSchema, stageModelPolicySchema, TIER_RANK, CONTEXT_LAYER_NAMES, stageContextLayerSchema, stageEventSchema, STAGE_ID_PATTERN, stageRecordSchema, StageRegistryError, REGISTERED_CANONICAL_STAGES, CANONICAL_STAGE_BY_KEY;
58372
+ var STAGE_REGISTRY_SCHEMA_VERSION, stageSchemaVersionSchema, AUTHORITY_LANES, MUTATION_CLASSES, EXECUTION_KINDS, inlineExecutionSchema, subprocessExecutionSchema, deterministicExecutionSchema, hitlExecutionSchema, irreversibleExecutionSchema, executionKindSchema, ARTIFACT_DIRECTIONS, stageArtifactSchema, stageGateSchema, stageRetryPolicySchema, CAPABILITY_TIERS, LEGACY_CAPABLE_ALIAS = "capable", capabilityTierSchema, objectiveEscalationTriggerSchema, stageModelPolicySchema, TIER_RANK, CONTEXT_LAYER_NAMES, stageContextLayerSchema, stageEventSchema, STAGE_ID_PATTERN, stageRecordSchema, StageRegistryError, REGISTERED_CANONICAL_STAGES, CANONICAL_STAGE_BY_KEY;
58361
58373
  var init_schema4 = __esm(() => {
58362
58374
  init_zod();
58363
58375
  STAGE_REGISTRY_SCHEMA_VERSION = {
58364
58376
  major: 1,
58365
- minor: 0
58377
+ minor: 1
58366
58378
  };
58367
58379
  stageSchemaVersionSchema = exports_external.object({
58368
58380
  major: exports_external.number().int().nonnegative(),
@@ -58433,11 +58445,18 @@ var init_schema4 = __esm(() => {
58433
58445
  }).strict();
58434
58446
  CAPABILITY_TIERS = ["cheap", "standard", "capable-1", "capable-2", "capable-3"];
58435
58447
  capabilityTierSchema = exports_external.preprocess(normalizeCapabilityTier, exports_external.enum(CAPABILITY_TIERS));
58448
+ objectiveEscalationTriggerSchema = exports_external.enum([
58449
+ "gate-fail",
58450
+ "timeout",
58451
+ "insufficient-evidence",
58452
+ "retry-exhausted",
58453
+ "resource-exhaustion"
58454
+ ]);
58436
58455
  stageModelPolicySchema = exports_external.object({
58437
58456
  min_tier: capabilityTierSchema,
58438
58457
  fallback: exports_external.array(exports_external.object({
58439
58458
  tier: capabilityTierSchema,
58440
- trigger: exports_external.enum(["gate-fail", "timeout", "insufficient-evidence", "retry-exhausted"])
58459
+ trigger: objectiveEscalationTriggerSchema
58441
58460
  }).strict()),
58442
58461
  override_key: exports_external.string().optional()
58443
58462
  }).strict();
@@ -58508,7 +58527,10 @@ var init_schema4 = __esm(() => {
58508
58527
  retry: { max_attempts: 3, terminal_stop: "block" },
58509
58528
  model_policy: {
58510
58529
  min_tier: "standard",
58511
- fallback: [{ tier: "capable-2", trigger: "gate-fail" }]
58530
+ fallback: [
58531
+ { tier: "capable-2", trigger: "gate-fail" },
58532
+ { tier: "capable-2", trigger: "resource-exhaustion" }
58533
+ ]
58512
58534
  },
58513
58535
  context_layers: [],
58514
58536
  observability: [],
@@ -58527,7 +58549,10 @@ var init_schema4 = __esm(() => {
58527
58549
  retry: { max_attempts: 3, terminal_stop: "block" },
58528
58550
  model_policy: {
58529
58551
  min_tier: "capable-2",
58530
- fallback: [{ tier: "capable-3", trigger: "gate-fail" }]
58552
+ fallback: [
58553
+ { tier: "capable-3", trigger: "gate-fail" },
58554
+ { tier: "capable-3", trigger: "resource-exhaustion" }
58555
+ ]
58531
58556
  },
58532
58557
  context_layers: [],
58533
58558
  observability: [],
@@ -58548,7 +58573,8 @@ var init_schema4 = __esm(() => {
58548
58573
  min_tier: "standard",
58549
58574
  fallback: [
58550
58575
  { tier: "capable-1", trigger: "gate-fail" },
58551
- { tier: "capable-1", trigger: "timeout" }
58576
+ { tier: "capable-1", trigger: "timeout" },
58577
+ { tier: "capable-1", trigger: "resource-exhaustion" }
58552
58578
  ]
58553
58579
  },
58554
58580
  context_layers: [],
@@ -58567,8 +58593,11 @@ var init_schema4 = __esm(() => {
58567
58593
  mutation_class: "tests",
58568
58594
  retry: { max_attempts: 3, terminal_stop: "block" },
58569
58595
  model_policy: {
58570
- min_tier: "standard",
58571
- fallback: [{ tier: "capable-1", trigger: "gate-fail" }]
58596
+ fallback: [
58597
+ { tier: "capable-1", trigger: "gate-fail" },
58598
+ { tier: "capable-1", trigger: "resource-exhaustion" }
58599
+ ],
58600
+ min_tier: "standard"
58572
58601
  },
58573
58602
  context_layers: [],
58574
58603
  observability: [],
@@ -58606,7 +58635,10 @@ var init_schema4 = __esm(() => {
58606
58635
  retry: { max_attempts: 2, terminal_stop: "block" },
58607
58636
  model_policy: {
58608
58637
  min_tier: "standard",
58609
- fallback: [{ tier: "capable-1", trigger: "gate-fail" }]
58638
+ fallback: [
58639
+ { tier: "capable-1", trigger: "gate-fail" },
58640
+ { tier: "capable-1", trigger: "resource-exhaustion" }
58641
+ ]
58610
58642
  },
58611
58643
  context_layers: [],
58612
58644
  observability: [],
@@ -58625,7 +58657,10 @@ var init_schema4 = __esm(() => {
58625
58657
  retry: { max_attempts: 2, terminal_stop: "block" },
58626
58658
  model_policy: {
58627
58659
  min_tier: "standard",
58628
- fallback: [{ tier: "capable-1", trigger: "gate-fail" }]
58660
+ fallback: [
58661
+ { tier: "capable-1", trigger: "gate-fail" },
58662
+ { tier: "capable-1", trigger: "resource-exhaustion" }
58663
+ ]
58629
58664
  },
58630
58665
  context_layers: [],
58631
58666
  observability: [],
@@ -60268,13 +60303,13 @@ __export(exports_src, {
60268
60303
  queryEtlRecords: () => queryEtlRecords,
60269
60304
  queryAllEtlRecords: () => queryAllEtlRecords,
60270
60305
  projectSnapshotSchema: () => projectSnapshotSchema,
60271
- pickStartingTier: () => pickStartingTier,
60272
60306
  passAllResolver: () => passAllResolver,
60273
60307
  parseStageRecord: () => parseStageRecord,
60274
60308
  parseHistoryLine: () => parseHistoryLine,
60275
60309
  parseFeature: () => parseFeature,
60276
60310
  parseExistingAcTags: () => parseExistingAcTags,
60277
60311
  parseChecklist: () => parseChecklist,
60312
+ objectiveEscalationTriggerSchema: () => objectiveEscalationTriggerSchema,
60278
60313
  normalizeTitle: () => normalizeTitle,
60279
60314
  normalizeTaskStatusSafe: () => normalizeTaskStatusSafe,
60280
60315
  normalizeTaskStatus: () => normalizeTaskStatus,
@@ -60588,92 +60623,159 @@ class AgentService {
60588
60623
  if (!resolved.ok) {
60589
60624
  return { ok: false, exitCode: resolved.exitCode, message: resolved.message };
60590
60625
  }
60591
- const agent = resolved.agent;
60592
- if (!jsonOutput && TIER2_AGENTS.has(agent)) {
60593
- this.ctx.output.error(`Warning: ${agent} is a Tier-2 agent (TUI/gateway only)`);
60626
+ const runFlags = { ...flags };
60627
+ let currentAgent = resolved.agent;
60628
+ let currentModel = resolved.model;
60629
+ let currentSource = resolved.source;
60630
+ let currentStage = resolved.stage;
60631
+ const maxEscalations = currentStage?.policy.fallback.length ?? 0;
60632
+ const attemptedExecutors = new Set(currentStage ? [currentStage.executorName] : []);
60633
+ if (!jsonOutput && TIER2_AGENTS.has(currentAgent)) {
60634
+ this.ctx.output.error(`Warning: ${currentAgent} is a Tier-2 agent (TUI/gateway only)`);
60594
60635
  }
60595
- const translated = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : undefined;
60596
- const input = translated ?? prompt;
60636
+ const explicitModel = stringFlag(flags, "model", "") || undefined;
60597
60637
  const purpose = stringFlag(flags, "purpose", "") || undefined;
60598
60638
  const tags = parseTagsFlag(flags);
60599
60639
  const systemPrompt = stringFlag(flags, "system-prompt", "") || undefined;
60600
60640
  const taskId = stringFlag(flags, "task", "") || undefined;
60601
- const explicitModel = stringFlag(flags, "model", "") || undefined;
60602
- const model = explicitModel ?? resolved.model;
60603
- const promptOptions = {
60604
- input,
60605
- continue: continueFlag || undefined,
60606
- model,
60607
- mode,
60608
- ...purpose !== undefined ? { purpose } : {},
60609
- ...tags !== undefined ? { tags } : {},
60610
- ...systemPrompt !== undefined ? { systemPrompt } : {},
60611
- ...taskId !== undefined ? { taskId } : {}
60612
- };
60613
- let shimCommand;
60614
- try {
60615
- shimCommand = typeof runner.buildPromptCommand === "function" ? runner.buildPromptCommand(agent, promptOptions, { cwd: cwd || undefined }) : getAgentShim(agent).getPromptCommand(promptOptions);
60616
- if (!jsonOutput) {
60617
- const version3 = (await detector.detectOne(agent)).version;
60618
- this.ctx.output.error(`\u2699\uFE0F ${agent}${version3 !== null ? ` v${version3}` : ""}
60619
- ${shimCommand.command} ${shimCommand.args.join(" ")}`);
60620
- }
60621
- } catch (error51) {
60622
- return { ok: false, exitCode: 2, message: error51 instanceof Error ? error51.message : String(error51) };
60623
- }
60624
- const traceInput = input === undefined ? undefined : traceSafePrompt(input);
60625
- const invocation = {
60626
- agent,
60627
- source: resolved.source,
60628
- command: shimCommand.command,
60629
- argv: sanitizeInvocationArgv(shimCommand.args, input, traceInput),
60630
- ...cwd !== "" ? { cwd } : {},
60631
- mode,
60632
- outputMode,
60633
- ...timeoutMs !== undefined ? { timeoutMs } : {},
60634
- continue: continueFlag,
60635
- stdinInteractive: false,
60636
- ...model !== undefined ? { model } : {},
60637
- ...translated !== undefined && prompt !== undefined ? { translatedFrom: traceSafePrompt(prompt) } : {}
60638
- };
60639
- let result;
60640
60641
  const controller = new AbortController;
60641
60642
  const onTerminate = () => controller.abort();
60642
60643
  const onExternalAbort = () => controller.abort();
60643
60644
  const lifecycle = new AgentExecutionLifecycle(options.execution?.observer, options.execution?.correlation, configuredSecretValues(this.ctx.env), options.execution?.heartbeatMs);
60644
- lifecycle.start({
60645
- agent,
60646
- ...model !== undefined ? { model } : {},
60647
- invocation: `${invocation.command} ${invocation.argv.join(" ")}`,
60648
- ...timeoutMs !== undefined ? { timeoutMs } : {}
60649
- });
60650
- const dispatchStartedAt = Date.now();
60645
+ process.on("SIGTERM", onTerminate);
60646
+ process.on("SIGINT", onTerminate);
60647
+ options.execution?.signal?.addEventListener("abort", onExternalAbort, { once: true });
60648
+ if (options.execution?.signal?.aborted === true)
60649
+ controller.abort();
60650
+ let result;
60651
+ let invocation;
60652
+ let dispatchStartedAt = Date.now();
60651
60653
  try {
60652
- process.on("SIGTERM", onTerminate);
60653
- process.on("SIGINT", onTerminate);
60654
- options.execution?.signal?.addEventListener("abort", onExternalAbort, { once: true });
60655
- if (options.execution?.signal?.aborted === true)
60656
- controller.abort();
60657
- result = await runner.runPromptCommand(agent, promptOptions, {
60658
- cwd: cwd || undefined,
60659
- ...timeoutMs !== undefined ? { timeout: timeoutMs } : {},
60660
- signal: controller.signal,
60661
- correlation: lifecycle.identity,
60662
- onOutput: (output2) => lifecycle.observe(output2)
60663
- });
60664
- } catch (error51) {
60665
- lifecycle.finish({
60666
- exitCode: null,
60667
- durationMs: Date.now() - dispatchStartedAt,
60668
- ...controller.signal.aborted ? { reason: "cancelled" } : {},
60669
- ...!controller.signal.aborted ? { reason: error51 instanceof Error ? error51.message : String(error51) } : {}
60670
- });
60671
- return { ok: false, exitCode: 2, message: error51 instanceof Error ? error51.message : String(error51) };
60654
+ for (let attempt = 0;; attempt++) {
60655
+ const agent = currentAgent;
60656
+ const model = explicitModel ?? currentModel;
60657
+ const translated = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : undefined;
60658
+ const input = translated ?? prompt;
60659
+ const promptOptions = {
60660
+ input,
60661
+ continue: continueFlag || undefined,
60662
+ model,
60663
+ mode,
60664
+ ...purpose !== undefined ? { purpose } : {},
60665
+ ...tags !== undefined ? { tags } : {},
60666
+ ...systemPrompt !== undefined ? { systemPrompt } : {},
60667
+ ...taskId !== undefined ? { taskId } : {}
60668
+ };
60669
+ let shimCommand;
60670
+ try {
60671
+ shimCommand = typeof runner.buildPromptCommand === "function" ? runner.buildPromptCommand(agent, promptOptions, { cwd: cwd || undefined }) : getAgentShim(agent).getPromptCommand(promptOptions);
60672
+ if (!jsonOutput) {
60673
+ const version3 = (await detector.detectOne(agent)).version;
60674
+ this.ctx.output.error(`\u2699\uFE0F ${agent}${version3 !== null ? ` v${version3}` : ""}
60675
+ ${shimCommand.command} ${shimCommand.args.join(" ")}`);
60676
+ }
60677
+ } catch (error51) {
60678
+ if (attempt === 0) {
60679
+ return {
60680
+ ok: false,
60681
+ exitCode: 2,
60682
+ message: error51 instanceof Error ? error51.message : String(error51)
60683
+ };
60684
+ }
60685
+ if (!jsonOutput) {
60686
+ this.ctx.output.error(`Escalation aborted: ${error51 instanceof Error ? error51.message : String(error51)}`);
60687
+ }
60688
+ break;
60689
+ }
60690
+ const traceInput = input === undefined ? undefined : traceSafePrompt(input);
60691
+ const attemptInvocation = {
60692
+ agent,
60693
+ source: currentSource,
60694
+ command: shimCommand.command,
60695
+ argv: sanitizeInvocationArgv(shimCommand.args, input, traceInput),
60696
+ ...cwd !== "" ? { cwd } : {},
60697
+ mode,
60698
+ outputMode,
60699
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
60700
+ continue: continueFlag,
60701
+ stdinInteractive: false,
60702
+ ...model !== undefined ? { model } : {},
60703
+ ...translated !== undefined && prompt !== undefined ? { translatedFrom: traceSafePrompt(prompt) } : {}
60704
+ };
60705
+ if (attempt === 0) {
60706
+ lifecycle.start({
60707
+ agent,
60708
+ ...model !== undefined ? { model } : {},
60709
+ invocation: `${attemptInvocation.command} ${attemptInvocation.argv.join(" ")}`,
60710
+ ...timeoutMs !== undefined ? { timeoutMs } : {}
60711
+ });
60712
+ dispatchStartedAt = Date.now();
60713
+ }
60714
+ try {
60715
+ result = await runner.runPromptCommand(agent, promptOptions, {
60716
+ cwd: cwd || undefined,
60717
+ ...timeoutMs !== undefined ? { timeout: timeoutMs } : {},
60718
+ signal: controller.signal,
60719
+ correlation: lifecycle.identity,
60720
+ onOutput: (output2) => lifecycle.observe(output2)
60721
+ });
60722
+ } catch (error51) {
60723
+ if (attempt === 0) {
60724
+ lifecycle.finish({
60725
+ exitCode: null,
60726
+ durationMs: Date.now() - dispatchStartedAt,
60727
+ ...controller.signal.aborted ? { reason: "cancelled" } : {},
60728
+ ...!controller.signal.aborted ? { reason: error51 instanceof Error ? error51.message : String(error51) } : {}
60729
+ });
60730
+ return {
60731
+ ok: false,
60732
+ exitCode: 2,
60733
+ message: error51 instanceof Error ? error51.message : String(error51)
60734
+ };
60735
+ }
60736
+ if (!jsonOutput) {
60737
+ this.ctx.output.error(`Escalation dispatch failed: ${error51 instanceof Error ? error51.message : String(error51)}`);
60738
+ }
60739
+ break;
60740
+ }
60741
+ invocation = attemptInvocation;
60742
+ if (result.exitCode === 0)
60743
+ break;
60744
+ const escalationSignal = classifyObjectiveFailure(result);
60745
+ if (escalationSignal === undefined || currentStage === undefined || attempt >= maxEscalations) {
60746
+ break;
60747
+ }
60748
+ runFlags.signal = escalationSignal;
60749
+ runFlags["from-executor"] = currentStage.executorName;
60750
+ const nextResolved = await this.resolveAgent(prompt, runFlags, doctorRunner);
60751
+ if (!nextResolved.ok || nextResolved.stage === undefined || attemptedExecutors.has(nextResolved.stage.executorName)) {
60752
+ if (!jsonOutput) {
60753
+ this.ctx.output.error(`Escalation chain exhausted after ${attempt + 1} attempt(s); executors tried: ${[...attemptedExecutors].join(", ")}`);
60754
+ }
60755
+ break;
60756
+ }
60757
+ if (!jsonOutput) {
60758
+ this.ctx.output.error(`Escalating: ${currentStage.executorName} (tier ${currentStage.executorTier}) failed with ${escalationSignal}; retrying on ${nextResolved.stage.executorName} (tier ${nextResolved.stage.executorTier})`);
60759
+ }
60760
+ currentAgent = nextResolved.agent;
60761
+ currentModel = nextResolved.model;
60762
+ currentSource = nextResolved.source;
60763
+ currentStage = nextResolved.stage;
60764
+ attemptedExecutors.add(nextResolved.stage.executorName);
60765
+ }
60672
60766
  } finally {
60673
60767
  process.off("SIGTERM", onTerminate);
60674
60768
  process.off("SIGINT", onTerminate);
60675
60769
  options.execution?.signal?.removeEventListener("abort", onExternalAbort);
60676
60770
  }
60771
+ if (result === undefined || invocation === undefined) {
60772
+ lifecycle.finish({
60773
+ exitCode: null,
60774
+ durationMs: Date.now() - dispatchStartedAt,
60775
+ reason: "no dispatch attempted"
60776
+ });
60777
+ return { ok: false, exitCode: 2, message: "No dispatch attempted" };
60778
+ }
60677
60779
  lifecycle.finish({
60678
60780
  exitCode: result.exitCode,
60679
60781
  durationMs: result.durationMs,
@@ -60703,6 +60805,13 @@ class AgentService {
60703
60805
  const raw = stringFlag(flags, "agent", "auto");
60704
60806
  if (raw === "auto")
60705
60807
  return this.resolveAgentAuto(prompt, flags, doctorRunner);
60808
+ if (raw === "inline") {
60809
+ return {
60810
+ ok: false,
60811
+ exitCode: 2,
60812
+ message: "'inline' selects in-session execution and cannot be passed to 'spur agent run', which always starts a subprocess. Run the backing skill directly in the current session, or use '--agent auto' for subprocess dispatch with a tier-resolved executor."
60813
+ };
60814
+ }
60706
60815
  return this.resolveExecutorSelector(raw, doctorRunner, "explicit");
60707
60816
  }
60708
60817
  async resolveAgentAuto(prompt, flags, doctorRunner) {
@@ -60773,7 +60882,13 @@ class AgentService {
60773
60882
  ok: true,
60774
60883
  agent: canonical,
60775
60884
  model: executor.model,
60776
- source: "stage"
60885
+ source: "stage",
60886
+ stage: {
60887
+ stageId: stageRecord.id,
60888
+ policy,
60889
+ executorName: executor.name,
60890
+ executorTier: getExecutorTier(executor)
60891
+ }
60777
60892
  };
60778
60893
  }
60779
60894
  }
@@ -60821,8 +60936,15 @@ class AgentService {
60821
60936
  };
60822
60937
  }
60823
60938
  const canonical = resolveAgentName(selector);
60824
- if (canonical === undefined)
60825
- return { ok: false, exitCode: 2, message: `Unknown agent: ${selector}` };
60939
+ if (canonical === undefined) {
60940
+ const names = this.ctx.agentConfig?.executors?.map((e) => e.name) ?? [];
60941
+ const available = names.length > 0 ? names.join(", ") : "(no executors configured; use a canonical agent name)";
60942
+ return {
60943
+ ok: false,
60944
+ exitCode: 2,
60945
+ message: `Unknown agent: '${selector}'. Available executors: ${available}.`
60946
+ };
60947
+ }
60826
60948
  const usable = await this.checkUsable(canonical, doctorRunner);
60827
60949
  if (!usable.ok)
60828
60950
  return usable.result;
@@ -61008,6 +61130,17 @@ function getExecutorTier(executor) {
61008
61130
  return "capable-1";
61009
61131
  return "standard";
61010
61132
  }
61133
+ function classifyObjectiveFailure(result) {
61134
+ if (result.signal !== undefined)
61135
+ return "timeout";
61136
+ if (result.exitCode === 0)
61137
+ return;
61138
+ const text4 = `${result.stderr} ${result.stdout}`.toLowerCase();
61139
+ if (/\b(rate[\s-]?limit|429|too many requests|quota|token limit|token budget|context length|maximum context|context window)\b/.test(text4)) {
61140
+ return "resource-exhaustion";
61141
+ }
61142
+ return;
61143
+ }
61011
61144
  var TRACE_SAFE_SLASH_COMMAND, TRACE_SAFE_SLASH_TOKEN, SENSITIVE_FLAG, SENSITIVE_INLINE_FLAG;
61012
61145
  var init_agent_service = __esm(() => {
61013
61146
  init_src2();
@@ -61861,11 +61994,106 @@ var init_planning_check_base = __esm(() => {
61861
61994
  // ../../packages/app/src/services/feature-check.ts
61862
61995
  var exports_feature_check = {};
61863
61996
  __export(exports_feature_check, {
61997
+ findOtherP0InStatus: () => findOtherP0InStatus,
61864
61998
  defaultVerdictRunDir: () => defaultVerdictRunDir,
61865
61999
  FeatureCheckService: () => FeatureCheckService,
61866
62000
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX
61867
62001
  });
61868
62002
  import { dirname as dirname8, join as join8 } from "path";
62003
+ function isGroupFeature(fm) {
62004
+ const tags = fm.tags;
62005
+ return Array.isArray(tags) && tags.includes("group") ? "group" : "standard";
62006
+ }
62007
+ async function findOtherP0InStatus(fs3, featuresDir, currentId, statuses) {
62008
+ try {
62009
+ const entries = await fs3.readDir(featuresDir);
62010
+ for (const entry of entries) {
62011
+ if (!entry.endsWith(".md"))
62012
+ continue;
62013
+ const match = /^([A-Z][1-9]*)_/.exec(entry);
62014
+ if (match === null)
62015
+ continue;
62016
+ const otherId = match[1];
62017
+ if (otherId === undefined)
62018
+ continue;
62019
+ if (otherId === currentId)
62020
+ continue;
62021
+ try {
62022
+ const raw = await fs3.readFile(`${featuresDir}/${entry}`);
62023
+ const doc2 = MarkdownDocument.parse(raw, "feature");
62024
+ const otherFm = doc2.frontmatterData ?? {};
62025
+ const otherPriority = otherFm.priority;
62026
+ const otherStatus = otherFm.status ?? "backlog";
62027
+ if (otherPriority === "P0" && statuses.includes(otherStatus)) {
62028
+ return { id: otherId, status: otherStatus };
62029
+ }
62030
+ } catch {}
62031
+ }
62032
+ } catch {}
62033
+ return null;
62034
+ }
62035
+ function decodeVerdictRows(source, sectionName, required2) {
62036
+ if (source === undefined) {
62037
+ return {
62038
+ rows: [],
62039
+ rejected: 0,
62040
+ invalidFields: required2 ? [`${sectionName} (missing array)`] : [],
62041
+ state: "absent"
62042
+ };
62043
+ }
62044
+ if (!Array.isArray(source)) {
62045
+ return {
62046
+ rows: [],
62047
+ rejected: 0,
62048
+ invalidFields: [`${sectionName} (expected array)`],
62049
+ state: "invalid"
62050
+ };
62051
+ }
62052
+ const rows = [];
62053
+ const invalidFields = new Set;
62054
+ let rejected = 0;
62055
+ for (const r of source) {
62056
+ if (typeof r !== "object" || r === null) {
62057
+ rejected++;
62058
+ invalidFields.add(`${sectionName}[non-object]`);
62059
+ continue;
62060
+ }
62061
+ const row = r;
62062
+ const hasId = Object.hasOwn(row, "id");
62063
+ const hasScenario = Object.hasOwn(row, "scenario");
62064
+ const idStr = typeof row.id === "string";
62065
+ const scStr = typeof row.scenario === "string";
62066
+ const statusOk = typeof row.status === "string";
62067
+ if (!statusOk) {
62068
+ rejected++;
62069
+ invalidFields.add(`${sectionName}.status`);
62070
+ continue;
62071
+ }
62072
+ if (hasId && hasScenario) {
62073
+ if (idStr && scStr && row.id === row.scenario) {
62074
+ rows.push({ id: row.id, status: row.status });
62075
+ } else {
62076
+ rejected++;
62077
+ invalidFields.add(`${sectionName}.id/scenario conflict`);
62078
+ }
62079
+ continue;
62080
+ }
62081
+ if (hasId && idStr) {
62082
+ rows.push({ id: row.id, status: row.status });
62083
+ } else if (hasScenario && scStr) {
62084
+ rows.push({ id: row.scenario, status: row.status });
62085
+ } else {
62086
+ rejected++;
62087
+ invalidFields.add(`${sectionName}.id/scenario missing`);
62088
+ }
62089
+ }
62090
+ return {
62091
+ rows,
62092
+ rejected,
62093
+ invalidFields: [...invalidFields],
62094
+ state: source.length === 0 ? "empty" : "populated"
62095
+ };
62096
+ }
61869
62097
  function defaultVerdictRunDir(tasksDir) {
61870
62098
  const norm = tasksDir.replace(/\\/g, "/");
61871
62099
  if (/\/docs\/tasks\d*$/.test(norm) || /\/docs\/tasks$/.test(norm)) {
@@ -61911,6 +62139,35 @@ var init_feature_check = __esm(() => {
61911
62139
  required: ["Notes"],
61912
62140
  optional: ["Goal", "Scope", "Acceptance Criteria", "Tasks", "History"]
61913
62141
  }
62142
+ },
62143
+ group: {
62144
+ backlog: {
62145
+ required: [],
62146
+ optional: ["Goal", "Scope", "Acceptance Criteria", "Tasks", "Notes", "History"]
62147
+ },
62148
+ active: {
62149
+ required: ["Goal", "Scope"],
62150
+ optional: ["Acceptance Criteria", "Tasks", "Notes", "History"],
62151
+ gate: true
62152
+ },
62153
+ verifying: {
62154
+ required: ["Goal", "Scope"],
62155
+ optional: ["Acceptance Criteria", "Tasks", "Notes", "History"],
62156
+ gate: true
62157
+ },
62158
+ blocked: {
62159
+ required: ["Goal", "Notes"],
62160
+ optional: ["Scope", "Acceptance Criteria", "Tasks", "History"]
62161
+ },
62162
+ done: {
62163
+ required: ["Goal", "Scope", "Tasks"],
62164
+ optional: ["Acceptance Criteria", "Notes", "History"],
62165
+ gate: true
62166
+ },
62167
+ cancelled: {
62168
+ required: ["Notes"],
62169
+ optional: ["Goal", "Scope", "Acceptance Criteria", "Tasks", "History"]
62170
+ }
61914
62171
  }
61915
62172
  }
61916
62173
  };
@@ -61934,11 +62191,11 @@ var init_feature_check = __esm(() => {
61934
62191
  }
61935
62192
  const fm = doc2.frontmatterData ?? {};
61936
62193
  const status = fm.status ?? "backlog";
61937
- const entry = this.resolveMatrixEntry("standard", status);
62194
+ const entry = this.resolveMatrixEntry(isGroupFeature(fm), status);
61938
62195
  this.runL2(doc2, entry, findings);
61939
62196
  this.runL3(doc2, findings);
61940
62197
  if (options?.featuresDir) {
61941
- await this.checkOneActiveGoal(fm, featureId2, options.featuresDir, findings);
62198
+ await this.checkOneActiveGoal(fm, featureId2, options.featuresDir, findings, options.asStatus);
61942
62199
  await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
61943
62200
  }
61944
62201
  const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join8(dirname8(options.featuresDir), "dogfood") : undefined);
@@ -62011,44 +62268,24 @@ var init_feature_check = __esm(() => {
62011
62268
  }
62012
62269
  }
62013
62270
  }
62014
- async checkOneActiveGoal(fm, currentId, featuresDir, findings) {
62271
+ async checkOneActiveGoal(fm, currentId, featuresDir, findings, asStatus) {
62015
62272
  const priority = fm.priority;
62016
- const status = fm.status;
62273
+ const status = fm.status ?? "backlog";
62274
+ const effectiveStatus = asStatus ?? status;
62017
62275
  if (priority !== "P0")
62018
62276
  return;
62019
- if (status !== "active" && status !== "verifying")
62277
+ if (effectiveStatus !== "active")
62020
62278
  return;
62021
- try {
62022
- const entries = await this.fs.readDir(featuresDir);
62023
- for (const entry of entries) {
62024
- if (!entry.endsWith(".md"))
62025
- continue;
62026
- const otherPath = `${featuresDir}/${entry}`;
62027
- const match = /^([A-Z][1-9]*)_/.exec(entry);
62028
- if (!match)
62029
- continue;
62030
- const otherId = match[1];
62031
- if (otherId === currentId)
62032
- continue;
62033
- try {
62034
- const raw = await this.fs.readFile(otherPath);
62035
- const doc2 = MarkdownDocument.parse(raw, "feature");
62036
- const otherFm = doc2.frontmatterData ?? {};
62037
- const otherPriority = otherFm.priority;
62038
- const otherStatus = otherFm.status;
62039
- if (otherPriority === "P0" && (otherStatus === "active" || otherStatus === "verifying")) {
62040
- findings.push({
62041
- layer: "L3",
62042
- code: FINDING_CODES.L3_ONE_ACTIVE_GOAL,
62043
- severity: "error",
62044
- section: "",
62045
- message: `One-active-goal violated: P0 feature "${otherId}" is already ${otherStatus}`
62046
- });
62047
- return;
62048
- }
62049
- } catch {}
62050
- }
62051
- } catch {}
62279
+ const conflict = await findOtherP0InStatus(this.fs, featuresDir, currentId, ["active"]);
62280
+ if (conflict !== null) {
62281
+ findings.push({
62282
+ layer: "L3",
62283
+ code: FINDING_CODES.L3_ONE_ACTIVE_GOAL,
62284
+ severity: "error",
62285
+ section: "",
62286
+ message: `One-active-goal violated: P0 feature "${conflict.id}" is already ${conflict.status}`
62287
+ });
62288
+ }
62052
62289
  }
62053
62290
  async checkChildrenLimit(featureId2, featuresDir, findings) {
62054
62291
  let children = 0;
@@ -62209,11 +62446,42 @@ var init_feature_check = __esm(() => {
62209
62446
  }
62210
62447
  covers[sc.title] = linked;
62211
62448
  }
62449
+ const doneWbs = new Set;
62450
+ for (const sc of scenarioAliases) {
62451
+ for (const task of covers[sc.title] ?? []) {
62452
+ if (task.status === "done")
62453
+ doneWbs.add(task.wbs);
62454
+ }
62455
+ }
62456
+ const artifacts2 = new Map;
62457
+ for (const wbs of doneWbs) {
62458
+ const artifact = await this.readVerdictArtifact(runDir, wbs);
62459
+ artifacts2.set(wbs, artifact);
62460
+ const diagnosticParts = [];
62461
+ if (artifact.diagnostics.artifactError !== undefined) {
62462
+ diagnosticParts.push(artifact.diagnostics.artifactError);
62463
+ }
62464
+ if (artifact.diagnostics.rejectedRowCount > 0) {
62465
+ diagnosticParts.push(`${artifact.diagnostics.rejectedRowCount} rejected coverage row(s)`);
62466
+ }
62467
+ if (artifact.diagnostics.invalidFields.length > 0) {
62468
+ diagnosticParts.push(`invalid fields: ${artifact.diagnostics.invalidFields.join(", ")}`);
62469
+ }
62470
+ if (diagnosticParts.length > 0) {
62471
+ findings.push({
62472
+ layer: "L4",
62473
+ code: FINDING_CODES.L4_MALFORMED_VERDICT_ARTIFACT,
62474
+ severity: "warning",
62475
+ section: "Acceptance Criteria",
62476
+ message: `Task ${wbs} verdict artifact (${artifact.path}) is invalid: ${diagnosticParts.join("; ")}. ` + "Rows were not silently dropped \u2014 verify the artifact uses canonical `id` " + "(or `scenario` alias) and `status` fields."
62477
+ });
62478
+ }
62479
+ }
62212
62480
  for (const sc of scenarioAliases) {
62213
62481
  const linked = covers[sc.title] ?? [];
62214
62482
  if (linked.length === 0)
62215
62483
  continue;
62216
- const verified = await this.isScenarioVerified(sc, linked, runDir);
62484
+ const verified = this.isScenarioVerified(sc, linked, artifacts2);
62217
62485
  if (!verified) {
62218
62486
  findings.push({
62219
62487
  layer: "L4",
@@ -62225,12 +62493,12 @@ var init_feature_check = __esm(() => {
62225
62493
  }
62226
62494
  }
62227
62495
  }
62228
- async isScenarioVerified(sc, linked, runDir) {
62496
+ isScenarioVerified(sc, linked, artifacts2) {
62229
62497
  for (const task of linked) {
62230
62498
  if (task.status !== "done")
62231
62499
  continue;
62232
- const artifact = await this.readVerdictArtifact(runDir, task.wbs);
62233
- if (artifact === null)
62500
+ const artifact = artifacts2.get(task.wbs);
62501
+ if (artifact === undefined)
62234
62502
  continue;
62235
62503
  if (artifact.verdict !== "PASS")
62236
62504
  continue;
@@ -62242,20 +62510,35 @@ var init_feature_check = __esm(() => {
62242
62510
  return false;
62243
62511
  }
62244
62512
  async readVerdictArtifact(runDir, wbs) {
62245
- try {
62246
- const raw = await this.fs.readFile(join8(runDir, `${wbs}-verdict.json`));
62247
- const parsed = JSON.parse(raw);
62248
- if (typeof parsed.verdict !== "string")
62249
- return null;
62250
- const pickRows = (arr) => (arr ?? []).filter((r) => typeof r?.id === "string" && typeof r?.status === "string").map((r) => ({ id: r.id, status: r.status }));
62251
- const requirements = pickRows(parsed.requirements);
62252
- const acceptanceCriteria = pickRows(parsed.acceptanceCriteria);
62253
- if (requirements.length === 0 && acceptanceCriteria.length === 0)
62254
- return null;
62255
- return { verdict: parsed.verdict, requirements, acceptanceCriteria };
62256
- } catch {
62257
- return null;
62513
+ const loaded = await readVerdictArtifact(this.fs, runDir, wbs);
62514
+ if (loaded.artifact === undefined) {
62515
+ return {
62516
+ path: loaded.path,
62517
+ requirements: [],
62518
+ acceptanceCriteria: [],
62519
+ diagnostics: {
62520
+ artifactError: loaded.readError ?? "artifact is missing",
62521
+ rejectedRowCount: 0,
62522
+ invalidFields: [],
62523
+ arrayStates: { requirements: "unavailable", acceptanceCriteria: "unavailable" }
62524
+ }
62525
+ };
62258
62526
  }
62527
+ const parsed = loaded.artifact;
62528
+ const req = decodeVerdictRows(parsed.requirements, "requirements", true);
62529
+ const ac = decodeVerdictRows(parsed.acceptanceCriteria, "acceptanceCriteria", false);
62530
+ return {
62531
+ path: loaded.path,
62532
+ verdict: typeof parsed.verdict === "string" ? parsed.verdict : undefined,
62533
+ requirements: req.rows,
62534
+ acceptanceCriteria: ac.rows,
62535
+ diagnostics: {
62536
+ artifactError: typeof parsed.verdict === "string" ? undefined : "invalid `verdict` field",
62537
+ rejectedRowCount: req.rejected + ac.rejected,
62538
+ invalidFields: [...new Set([...req.invalidFields, ...ac.invalidFields])],
62539
+ arrayStates: { requirements: req.state, acceptanceCriteria: ac.state }
62540
+ }
62541
+ };
62259
62542
  }
62260
62543
  };
62261
62544
  });
@@ -62323,6 +62606,25 @@ class TaskLocator {
62323
62606
  }
62324
62607
  return null;
62325
62608
  }
62609
+ async findDuplicateWbs() {
62610
+ const wbsMap = new Map;
62611
+ for (const dir of this.folderDirs()) {
62612
+ for (const name of await readDirIfPresent(this.fs, dir)) {
62613
+ const captured = TASK_FILENAME_RE.exec(name);
62614
+ if (captured === null)
62615
+ continue;
62616
+ const wbs = captured[1];
62617
+ const hit = { wbs, name, filePath: `${dir}/${name}` };
62618
+ const existing = wbsMap.get(wbs);
62619
+ if (existing) {
62620
+ existing.push(hit);
62621
+ } else {
62622
+ wbsMap.set(wbs, [hit]);
62623
+ }
62624
+ }
62625
+ }
62626
+ return [...wbsMap.values()].filter((hits) => hits.length > 1);
62627
+ }
62326
62628
  }
62327
62629
  var TASK_FILENAME_RE;
62328
62630
  var init_task_locator = __esm(() => {
@@ -62609,11 +62911,32 @@ class FeatureService {
62609
62911
  }
62610
62912
  const appliedHops = [];
62611
62913
  for (const hop of hops) {
62914
+ if (hop === "active") {
62915
+ const conflict = await this.findOneActiveGoalConflict(featureId2);
62916
+ if (conflict !== null) {
62917
+ return {
62918
+ proposal: {
62919
+ ...proposal,
62920
+ reason: `Activation blocked by one-active-goal: P0 feature "${conflict.featureId}" is already ${conflict.status}`
62921
+ },
62922
+ applied: false,
62923
+ appliedHops,
62924
+ goalConflict: conflict
62925
+ };
62926
+ }
62927
+ }
62612
62928
  await this.transition(featureId2, hop);
62613
62929
  appliedHops.push(hop);
62614
62930
  }
62615
62931
  return { proposal, applied: true, appliedHops };
62616
62932
  }
62933
+ async findOneActiveGoalConflict(featureId2) {
62934
+ const feature = await this.show(featureId2);
62935
+ if (feature === null || feature.frontmatter.priority !== "P0")
62936
+ return null;
62937
+ const conflict = await findOtherP0InStatus(this.ctx.fs, this.ctx.featuresDir, featureId2, ["active"]);
62938
+ return conflict === null ? null : { featureId: conflict.id, status: conflict.status };
62939
+ }
62617
62940
  async syncAllFeatures(options) {
62618
62941
  const allFeatures = await this.list();
62619
62942
  const tasksByFeature = await this.collectTasksByFeature();
@@ -65347,6 +65670,21 @@ var init_task_check = __esm(() => {
65347
65670
  });
65348
65671
  }
65349
65672
  }
65673
+ const requiredSections = new Set(entry?.required ?? []);
65674
+ for (const sectionName of ["Testing", "Solution"]) {
65675
+ if (!requiredSections.has(sectionName))
65676
+ continue;
65677
+ const body = doc2.getSection(sectionName);
65678
+ if (body === null || !isPlaceholderBody(body))
65679
+ continue;
65680
+ findings.push({
65681
+ layer: "L3",
65682
+ code: FINDING_CODES.L3_REQUIRED_SECTION_PLACEHOLDER,
65683
+ severity: "error",
65684
+ section: sectionName,
65685
+ message: `${sectionName} is required at status '${status}' but is still placeholder-only \u2014 run \`spur task record <wbs>\` to fill it from the verdict artifact, or author it directly`
65686
+ });
65687
+ }
65350
65688
  const testBody = doc2.getSection("Testing");
65351
65689
  if (testBody !== null && !isPlaceholderBody(testBody)) {
65352
65690
  const hasCoverage = /coverage|\u2265\d+%|\d+\.\d+%|N\/A/i.test(testBody);
@@ -66219,15 +66557,15 @@ class TaskService {
66219
66557
  const variant = params.template ?? (params.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
66220
66558
  const status = params.status ?? (params.featureId !== undefined ? "todo" : "backlog");
66221
66559
  return this.writeService.createAllocated(folder, async () => {
66222
- if (params.dedupeWithinSec !== undefined && params.featureId !== undefined) {
66223
- const collision = await this.findDuplicateFollowUp(params.featureId, params.title, params.dedupeWithinSec);
66560
+ const dedupeWithinSec = params.dedupeWithinSec === null ? undefined : params.dedupeWithinSec ?? 300;
66561
+ if (dedupeWithinSec !== undefined && params.featureId !== undefined) {
66562
+ const collision = await this.findDuplicateFollowUp(params.featureId, params.title, dedupeWithinSec);
66224
66563
  if (collision !== null) {
66225
66564
  throw new DuplicateFollowUpError(collision.wbs, collision.name, params.title);
66226
66565
  }
66227
66566
  }
66228
- const wbs = await this.allocateWbs();
66229
66567
  const slug = this.slugify(params.title);
66230
- const filePath = this.resolveTaskPath(wbs, slug);
66568
+ const { wbs, filePath } = await this.allocateWbsChecked(slug);
66231
66569
  const now = new Date().toISOString();
66232
66570
  const rawTemplate = this.ctx.resolveTemplate?.(variant);
66233
66571
  if (rawTemplate !== undefined) {
@@ -66630,9 +66968,8 @@ ${issues}`);
66630
66968
  const status = hasSpec ? "todo" : "backlog";
66631
66969
  const variant = item.template ?? (item.feature_id !== undefined && item.feature_id !== null ? "feature-impl" : DEFAULT_TASK_VARIANT);
66632
66970
  return this.writeService.createAllocated(folder, async () => {
66633
- const wbs = await this.allocateWbs();
66634
66971
  const slug = this.slugify(item.name);
66635
- const filePath = this.resolveTaskPath(wbs, slug);
66972
+ const { wbs, filePath } = await this.allocateWbsChecked(slug);
66636
66973
  const now = new Date().toISOString();
66637
66974
  const rawTemplate = this.ctx.resolveTemplate?.(variant);
66638
66975
  if (rawTemplate !== undefined) {
@@ -66785,9 +67122,15 @@ ${block}` : block);
66785
67122
  }
66786
67123
  async allocateWbs() {
66787
67124
  let max = 0;
66788
- const dirs = this.ctx.foldersConfig ? [...new Set([this.ctx.tasksDir, ...Object.keys(this.ctx.foldersConfig.folders)])] : [this.ctx.tasksDir];
67125
+ const dirs = this.allFolderDirs();
67126
+ const folderFloors = new Map;
67127
+ if (this.ctx.foldersConfig) {
67128
+ for (const [key, entry] of Object.entries(this.ctx.foldersConfig.folders)) {
67129
+ folderFloors.set(this.ctx.fs.resolve(key), entry.baseCounter);
67130
+ }
67131
+ }
66789
67132
  for (const dir of dirs) {
66790
- const baseCounter = this.ctx.foldersConfig?.folders[dir]?.base_counter ?? 0;
67133
+ const baseCounter = folderFloors.get(dir) ?? 0;
66791
67134
  if (baseCounter > max)
66792
67135
  max = baseCounter;
66793
67136
  try {
@@ -66804,6 +67147,14 @@ ${block}` : block);
66804
67147
  }
66805
67148
  return String(max + 1).padStart(4, "0");
66806
67149
  }
67150
+ async allocateWbsChecked(slug) {
67151
+ const wbs = await this.allocateWbs();
67152
+ const existing = await this.locator.findByWbs(wbs);
67153
+ if (existing !== null) {
67154
+ throw new WbsCollisionError(wbs, existing.filePath, this.resolveTaskPath(wbs, slug));
67155
+ }
67156
+ return { wbs, filePath: this.resolveTaskPath(wbs, slug) };
67157
+ }
66807
67158
  async getFilePath(wbs) {
66808
67159
  const result = await this.findTaskFileName(wbs);
66809
67160
  if (!result)
@@ -66865,7 +67216,7 @@ ${block}` : block);
66865
67216
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
66866
67217
  }
66867
67218
  }
66868
- var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
67219
+ var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
66869
67220
  var init_task_service = __esm(() => {
66870
67221
  init_src2();
66871
67222
  init_task_locator();
@@ -66898,6 +67249,18 @@ var init_task_service = __esm(() => {
66898
67249
  this.attemptedName = attemptedName;
66899
67250
  }
66900
67251
  };
67252
+ WbsCollisionError = class WbsCollisionError extends Error {
67253
+ wbs;
67254
+ existingPath;
67255
+ attemptedPath;
67256
+ constructor(wbs, existingPath, attemptedPath) {
67257
+ super(`wbs-collision: WBS ${wbs} already exists at "${existingPath}". ` + `Refusing to overwrite with "${attemptedPath}". ` + `Re-run create; if the collision persists, inspect the corpus with: spur task check.`);
67258
+ this.name = "WbsCollisionError";
67259
+ this.wbs = wbs;
67260
+ this.existingPath = existingPath;
67261
+ this.attemptedPath = attemptedPath;
67262
+ }
67263
+ };
66901
67264
  ROSTER_REGION_RE = /<!--[ \t]*AUTO-GENERATED[^>]*-->[\s\S]*?<!--[ \t]*END[ \t]*AUTO-GENERATED[^>]*-->/g;
66902
67265
  TASK_ACTION_COMMANDS = {
66903
67266
  refine: (wbs) => `/sp:dev-refine ${wbs} --auto`,
@@ -67884,6 +68247,110 @@ var init_token_ledger_watcher = __esm(() => {
67884
68247
  init_token_ledger_service();
67885
68248
  });
67886
68249
 
68250
+ // ../../packages/app/src/observability/run-output-sink.ts
68251
+ import { closeSync as closeSync4, mkdirSync as mkdirSync6, openSync as openSync4, writeSync as writeSync2 } from "fs";
68252
+ import { join as join14 } from "path";
68253
+
68254
+ class RunOutputSink {
68255
+ filePath;
68256
+ maxBytes;
68257
+ maxLines;
68258
+ fd;
68259
+ bytes = 0;
68260
+ lines = 0;
68261
+ truncated = false;
68262
+ closed = false;
68263
+ constructor(options) {
68264
+ this.filePath = join14(options.dir, `${options.runId}-output.log`);
68265
+ this.maxBytes = options.maxBytes ?? DEFAULT_OUTPUT_MAX_BYTES;
68266
+ this.maxLines = options.maxLines;
68267
+ try {
68268
+ mkdirSync6(options.dir, { recursive: true });
68269
+ this.fd = openSync4(this.filePath, "a");
68270
+ } catch {
68271
+ this.fd = undefined;
68272
+ }
68273
+ }
68274
+ get isTruncated() {
68275
+ return this.truncated;
68276
+ }
68277
+ observe(event2) {
68278
+ if (this.fd === undefined || this.closed)
68279
+ return;
68280
+ switch (event2.kind) {
68281
+ case "output":
68282
+ this.appendChunk(event2.at, event2.stream, event2.chunk);
68283
+ break;
68284
+ case "started":
68285
+ this.append(`# agent output \u2014 run ${event2.runId} \u2014 ${event2.agent} \u2014 ${event2.at}
68286
+ `);
68287
+ this.append(`# invocation: ${event2.invocation}
68288
+ `);
68289
+ break;
68290
+ case "dropped":
68291
+ this.append(`
68292
+ === [dropped] ${event2.chunks} chunk(s) discarded by the lifecycle relay under backpressure ===
68293
+ `);
68294
+ break;
68295
+ case "finished":
68296
+ this.append(`
68297
+ === run ${event2.outcome} (exit ${event2.exitCode}) after ${event2.durationMs}ms ===
68298
+ `);
68299
+ break;
68300
+ default:
68301
+ break;
68302
+ }
68303
+ }
68304
+ close() {
68305
+ if (this.closed)
68306
+ return;
68307
+ this.closed = true;
68308
+ if (this.fd !== undefined) {
68309
+ try {
68310
+ closeSync4(this.fd);
68311
+ } catch {}
68312
+ this.fd = undefined;
68313
+ }
68314
+ }
68315
+ appendChunk(at, stream, chunk) {
68316
+ if (this.truncated)
68317
+ return;
68318
+ const text4 = `[${at}] ${stream}: ${chunk}
68319
+ `;
68320
+ const chunkBytes = Buffer.byteLength(text4);
68321
+ const chunkLines = countNewlines(text4);
68322
+ if (this.bytes + chunkBytes > this.maxBytes || this.maxLines !== undefined && this.lines + chunkLines > this.maxLines) {
68323
+ this.truncated = true;
68324
+ this.append(TRUNCATION_MARKER);
68325
+ return;
68326
+ }
68327
+ this.append(text4);
68328
+ }
68329
+ append(text4) {
68330
+ if (this.fd === undefined || this.closed)
68331
+ return;
68332
+ try {
68333
+ writeSync2(this.fd, text4);
68334
+ this.bytes += Buffer.byteLength(text4);
68335
+ this.lines += countNewlines(text4);
68336
+ } catch {}
68337
+ }
68338
+ }
68339
+ function countNewlines(text4) {
68340
+ let count3 = 0;
68341
+ for (let i2 = 0;i2 < text4.length; i2 += 1) {
68342
+ if (text4.charCodeAt(i2) === 10)
68343
+ count3 += 1;
68344
+ }
68345
+ return count3;
68346
+ }
68347
+ var DEFAULT_OUTPUT_MAX_BYTES, TRUNCATION_MARKER = `
68348
+ === [truncated] agent output capture reached its configured bound; further chunks were not written ===
68349
+ `;
68350
+ var init_run_output_sink = __esm(() => {
68351
+ DEFAULT_OUTPUT_MAX_BYTES = 1024 * 1024;
68352
+ });
68353
+
67887
68354
  // ../../packages/app/src/workflow/steering.ts
67888
68355
  class WorkflowSteeringController {
67889
68356
  onAck;
@@ -68126,16 +68593,18 @@ var init_steering = __esm(() => {
68126
68593
  });
68127
68594
 
68128
68595
  // ../../packages/app/src/workflow/actions/agent-run.ts
68129
- import { dirname as dirname12, isAbsolute as isAbsolute2, join as join14 } from "path";
68596
+ import { dirname as dirname12, isAbsolute as isAbsolute2, join as join15 } from "path";
68130
68597
 
68131
68598
  class AgentRunActionRunner {
68132
68599
  observabilityBus;
68133
68600
  steeringController;
68601
+ outputLog;
68134
68602
  kind = KIND;
68135
68603
  agentService;
68136
- constructor(agentService, observabilityBus, steeringController) {
68604
+ constructor(agentService, observabilityBus, steeringController, outputLog) {
68137
68605
  this.observabilityBus = observabilityBus;
68138
68606
  this.steeringController = steeringController;
68607
+ this.outputLog = outputLog;
68139
68608
  this.agentService = agentService;
68140
68609
  }
68141
68610
  async execute(options, context4) {
@@ -68146,7 +68615,8 @@ class AgentRunActionRunner {
68146
68615
  const cwd = asOptionalString(options.cwd) ?? context4.workdir ?? ".";
68147
68616
  let continueFlag = asOptionalBoolean(options.continue);
68148
68617
  const latch = context4.vars.__agentSession;
68149
- if (continueFlag === undefined && latch === "open") {
68618
+ const latchAutoContinued = continueFlag === undefined && latch === "open";
68619
+ if (latchAutoContinued) {
68150
68620
  continueFlag = true;
68151
68621
  }
68152
68622
  if (input === undefined && !continueFlag) {
@@ -68178,78 +68648,100 @@ class AgentRunActionRunner {
68178
68648
  const expectFile = asOptionalString(options.expectFile);
68179
68649
  const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
68180
68650
  const agentLabel = agent ?? "<default>";
68181
- const observer = this.observabilityBus === undefined ? undefined : (event2) => {
68651
+ const outputLog = this.outputLog;
68652
+ const sink = outputLog === undefined ? undefined : new RunOutputSink({
68653
+ dir: join15(cwd, ".spur", "run"),
68654
+ runId: context4.runId,
68655
+ ...outputLog.maxBytes !== undefined ? { maxBytes: outputLog.maxBytes } : {},
68656
+ ...outputLog.maxLines !== undefined ? { maxLines: outputLog.maxLines } : {}
68657
+ });
68658
+ const observer = this.observabilityBus === undefined && sink === undefined ? undefined : (event2) => {
68659
+ sink?.observe(event2);
68182
68660
  this.observabilityBus?.emit("workflow.agent", event2);
68183
68661
  };
68184
68662
  const actionId = context4.actionId ?? `${context4.runId}:${context4.stateOrNodeId}`;
68185
68663
  const steeringPolicy = parseSteeringPolicy(options);
68186
- let steeringSignal = this.steeringController?.begin(context4.runId, actionId, steeringPolicy);
68664
+ let resumeRetried = false;
68187
68665
  let steeringNote;
68188
68666
  let traced;
68189
- while (true) {
68190
- traced = await this.agentService.runTraced(input, flags, undefined, {
68191
- correlation: {
68192
- runId: context4.runId,
68193
- executionId: crypto.randomUUID(),
68194
- actionId
68195
- },
68196
- ...observer !== undefined ? { observer } : {},
68197
- ...steeringSignal !== undefined ? { signal: steeringSignal } : {}
68198
- });
68199
- if (this.steeringController === undefined)
68667
+ try {
68668
+ for (;; ) {
68669
+ steeringNote = undefined;
68670
+ let steeringSignal = this.steeringController?.begin(context4.runId, actionId, steeringPolicy);
68671
+ while (true) {
68672
+ traced = await this.agentService.runTraced(input, flags, undefined, {
68673
+ correlation: {
68674
+ runId: context4.runId,
68675
+ executionId: crypto.randomUUID(),
68676
+ actionId
68677
+ },
68678
+ ...observer !== undefined ? { observer } : {},
68679
+ ...steeringSignal !== undefined ? { signal: steeringSignal } : {}
68680
+ });
68681
+ if (this.steeringController === undefined)
68682
+ break;
68683
+ const decision = await this.steeringController.boundary(traced.exitCode === 0);
68684
+ if (decision.operation === "retry") {
68685
+ steeringSignal = this.steeringController.nextAttempt();
68686
+ continue;
68687
+ }
68688
+ if (decision.operation === "note")
68689
+ steeringNote = decision.note;
68690
+ if (decision.operation === "abort" && traced.exitCode === 0) {
68691
+ traced = {
68692
+ ...traced,
68693
+ exitCode: 3,
68694
+ signal: "STEERING_ABORT",
68695
+ message: "aborted at steering boundary"
68696
+ };
68697
+ }
68698
+ break;
68699
+ }
68700
+ this.steeringController?.complete();
68701
+ if (!resumeRetried && latchAutoContinued && traced.exitCode === 2) {
68702
+ resumeRetried = true;
68703
+ delete flags.continue;
68704
+ continue;
68705
+ }
68200
68706
  break;
68201
- const decision = await this.steeringController.boundary(traced.exitCode === 0);
68202
- if (decision.operation === "retry") {
68203
- steeringSignal = this.steeringController.nextAttempt();
68204
- continue;
68205
68707
  }
68206
- if (decision.operation === "note")
68207
- steeringNote = decision.note;
68208
- if (decision.operation === "abort" && traced.exitCode === 0) {
68209
- traced = {
68210
- ...traced,
68211
- exitCode: 3,
68212
- signal: "STEERING_ABORT",
68213
- message: "aborted at steering boundary"
68214
- };
68708
+ const { exitCode, stdout: answer } = traced;
68709
+ const ok = exitCode === 0;
68710
+ const invocation = traced.invocation;
68711
+ if (capture && answerFile !== undefined) {
68712
+ const target = isAbsolute2(answerFile) ? answerFile : join15(cwd, answerFile);
68713
+ const fs3 = createNodeFileSystem3(cwd);
68714
+ await fs3.ensureDir(dirname12(target));
68715
+ await fs3.writeFile(target, answer);
68716
+ }
68717
+ if (ok && expectFile !== undefined) {
68718
+ const target = isAbsolute2(expectFile) ? expectFile : join15(cwd, expectFile);
68719
+ const fs3 = createNodeFileSystem3(cwd);
68720
+ if (!await fs3.exists(target)) {
68721
+ return {
68722
+ ok: false,
68723
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
68724
+ error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
68725
+ };
68726
+ }
68215
68727
  }
68216
- break;
68217
- }
68218
- this.steeringController?.complete();
68219
- const { exitCode, stdout: answer } = traced;
68220
- const ok = exitCode === 0;
68221
- const invocation = traced.invocation;
68222
- if (capture && answerFile !== undefined) {
68223
- const target = isAbsolute2(answerFile) ? answerFile : join14(cwd, answerFile);
68224
- const fs3 = createNodeFileSystem3(cwd);
68225
- await fs3.ensureDir(dirname12(target));
68226
- await fs3.writeFile(target, answer);
68227
- }
68228
- if (ok && expectFile !== undefined) {
68229
- const target = isAbsolute2(expectFile) ? expectFile : join14(cwd, expectFile);
68230
- const fs3 = createNodeFileSystem3(cwd);
68231
- if (!await fs3.exists(target)) {
68232
- return {
68233
- ok: false,
68234
- data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
68235
- error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
68236
- };
68728
+ if (!ok) {
68729
+ await writePartialWorkArtifact(context4, agentLabel, model, traced, cwd);
68237
68730
  }
68731
+ const stepLabel = context4.stateOrNodeId;
68732
+ const error51 = ok ? undefined : traced.signal !== undefined ? timeoutMs !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) terminated by signal ${traced.signal} (configured timeout: ${timeoutMs}ms; timeout or cancellation); see partial-work artifact` : `agent.run '${stepLabel}' (${agentLabel}) was cancelled by signal ${traced.signal}; see partial-work artifact` : traced.message !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) dispatch failed: ${traced.message}` : `agent.run '${stepLabel}' (${agentLabel}) exited with code ${exitCode}`;
68733
+ return {
68734
+ ok,
68735
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
68736
+ error: error51,
68737
+ setVars: ok ? {
68738
+ __agentSession: resumeRetried ? "no-resume" : "open",
68739
+ ...steeringNote !== undefined ? { __steeringNote: steeringNote } : {}
68740
+ } : undefined
68741
+ };
68742
+ } finally {
68743
+ sink?.close();
68238
68744
  }
68239
- if (!ok) {
68240
- await writePartialWorkArtifact(context4, agentLabel, model, traced, cwd);
68241
- }
68242
- const stepLabel = context4.stateOrNodeId;
68243
- const error51 = ok ? undefined : traced.signal !== undefined ? timeoutMs !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) terminated by signal ${traced.signal} (configured timeout: ${timeoutMs}ms; timeout or cancellation); see partial-work artifact` : `agent.run '${stepLabel}' (${agentLabel}) was cancelled by signal ${traced.signal}; see partial-work artifact` : traced.message !== undefined ? `agent.run '${stepLabel}' (${agentLabel}) dispatch failed: ${traced.message}` : `agent.run '${stepLabel}' (${agentLabel}) exited with code ${exitCode}`;
68244
- return {
68245
- ok,
68246
- data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
68247
- error: error51,
68248
- setVars: ok ? {
68249
- __agentSession: "open",
68250
- ...steeringNote !== undefined ? { __steeringNote: steeringNote } : {}
68251
- } : undefined
68252
- };
68253
68745
  }
68254
68746
  }
68255
68747
  function buildResultData(exitCode, agentLabel, capture, answer, invocation) {
@@ -68335,7 +68827,7 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
68335
68827
  ""
68336
68828
  ].join(`
68337
68829
  `);
68338
- const target = join14(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
68830
+ const target = join15(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
68339
68831
  const fs3 = createNodeFileSystem3(cwd);
68340
68832
  await fs3.ensureDir(dirname12(target));
68341
68833
  await fs3.writeFile(target, body);
@@ -68365,6 +68857,7 @@ ${text4.slice(text4.length - maxChars)}`;
68365
68857
  var PARTIAL_ARTIFACT_TAIL_CHARS = 4000, KIND = "agent.run";
68366
68858
  var init_agent_run = __esm(() => {
68367
68859
  init_dist5();
68860
+ init_run_output_sink();
68368
68861
  init_steering();
68369
68862
  });
68370
68863
 
@@ -68893,7 +69386,7 @@ var init_rule_check = __esm(() => {
68893
69386
  // ../../packages/app/src/workflow/builtins.ts
68894
69387
  function registerSpurBuiltins(host, options) {
68895
69388
  const fileSystem = options.fileSystem ?? createNodeFileSystem3();
68896
- host.registerAction(new AgentRunActionRunner(options.agentService, options.observabilityBus, options.steeringController), "builtin");
69389
+ host.registerAction(new AgentRunActionRunner(options.agentService, options.observabilityBus, options.steeringController, options.outputLog), "builtin");
68897
69390
  host.registerAction(new RuleCheckActionRunner(options.ruleService), "builtin");
68898
69391
  host.registerAction(new FileExistsActionRunner(fileSystem), "builtin");
68899
69392
  host.registerAction(new FileReadActionRunner(fileSystem), "builtin");
@@ -69073,7 +69566,7 @@ var init_observability = __esm(() => {
69073
69566
 
69074
69567
  // ../../packages/app/src/services/workflow-service.ts
69075
69568
  import { homedir as homedir5 } from "os";
69076
- import { join as join15, resolve as resolve4 } from "path";
69569
+ import { join as join16, resolve as resolve4 } from "path";
69077
69570
  function signalSubprocess(pid) {
69078
69571
  if (!Number.isInteger(pid) || pid <= 1)
69079
69572
  return false;
@@ -69296,7 +69789,7 @@ class WorkflowAppService {
69296
69789
  }
69297
69790
  async list(workflowPaths = [".spur/workflows/"]) {
69298
69791
  const projectRoot = this.ctx.cwd;
69299
- const globalRoot = join15(homedir5(), ".config", "spur");
69792
+ const globalRoot = join16(homedir5(), ".config", "spur");
69300
69793
  const layers = [];
69301
69794
  const entries = [];
69302
69795
  const scannedPaths = new Set;
@@ -69400,7 +69893,12 @@ class WorkflowAppService {
69400
69893
  });
69401
69894
  }
69402
69895
  }
69403
- return { run, events: events2 };
69896
+ const outputArtifact = await outputArtifactForRun(this.ctx.cwd, runId);
69897
+ return {
69898
+ run,
69899
+ events: events2,
69900
+ ...outputArtifact !== undefined ? { outputArtifact } : {}
69901
+ };
69404
69902
  }
69405
69903
  async createEngineService(opts = {}) {
69406
69904
  const processExec = this.ctx.processExecutor?.();
@@ -69413,7 +69911,8 @@ class WorkflowAppService {
69413
69911
  httpRequester: this.ctx.httpRequester?.(),
69414
69912
  hostAllowlist: this.ctx.hostAllowlist?.(),
69415
69913
  ...bus !== undefined ? { observabilityBus: bus } : {},
69416
- ...opts.steeringController !== undefined ? { steeringController: opts.steeringController } : {}
69914
+ ...opts.steeringController !== undefined ? { steeringController: opts.steeringController } : {},
69915
+ outputLog: await resolveOutputLogConfig(this.ctx.cwd)
69417
69916
  });
69418
69917
  const db2 = await this.ctx.getDb();
69419
69918
  let persistence2 = new DbWorkflowPersistenceAdapter(db2);
@@ -69428,6 +69927,23 @@ async function fileExists(path9) {
69428
69927
  const fs3 = createNodeFileSystem3();
69429
69928
  return await fs3.exists(path9);
69430
69929
  }
69930
+ async function resolveOutputLogConfig(cwd) {
69931
+ try {
69932
+ const output2 = (await loadSpurConfig(cwd)).agent?.output;
69933
+ if (output2 === undefined)
69934
+ return {};
69935
+ return {
69936
+ ...output2["max-bytes"] !== undefined ? { maxBytes: output2["max-bytes"] } : {},
69937
+ ...output2["max-lines"] !== undefined ? { maxLines: output2["max-lines"] } : {}
69938
+ };
69939
+ } catch {
69940
+ return {};
69941
+ }
69942
+ }
69943
+ async function outputArtifactForRun(cwd, runId) {
69944
+ const relative4 = join16(".spur", "run", `${runId}-output.log`);
69945
+ return await fileExists(join16(cwd, relative4)) ? relative4 : undefined;
69946
+ }
69431
69947
  async function scanWorkflowFiles(rootPath, source) {
69432
69948
  const entries = [];
69433
69949
  const fs3 = createNodeFileSystem3();
@@ -69507,6 +70023,7 @@ function rowToTraceEntry(row) {
69507
70023
  }
69508
70024
  var TASK_PIPELINE_WORKFLOW = "task-pipeline", EMBEDDED_SCHEMA_PREFIX = "\x00embedded-spur", SPUR_SCHEMA_MANIFEST = "@gobing-ai/spur/package.json", PIPELINE_LINK_KIND = "pipeline";
69509
70025
  var init_workflow_service = __esm(() => {
70026
+ init_loader();
69510
70027
  init_src2();
69511
70028
  init_dist7();
69512
70029
  init_dist5();
@@ -69740,7 +70257,7 @@ function formatDuration(ms) {
69740
70257
  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);
69741
70258
 
69742
70259
  // ../../packages/app/src/workflow/trace-writer.ts
69743
- import { dirname as dirname13, join as join16 } from "path";
70260
+ import { dirname as dirname13, join as join17 } from "path";
69744
70261
 
69745
70262
  class WorkflowTraceWriter {
69746
70263
  path;
@@ -69748,7 +70265,7 @@ class WorkflowTraceWriter {
69748
70265
  pending = Promise.resolve();
69749
70266
  constructor(cwd, runId) {
69750
70267
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
69751
- this.path = join16(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
70268
+ this.path = join17(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
69752
70269
  }
69753
70270
  attach(bus) {
69754
70271
  bus.on("workflow.run.started", (event2) => this.enqueue("workflow.run.started", event2));
@@ -69840,6 +70357,7 @@ __export(exports_src2, {
69840
70357
  WorkflowTraceWriter: () => WorkflowTraceWriter,
69841
70358
  WorkflowSteeringController: () => WorkflowSteeringController,
69842
70359
  WorkflowAppService: () => WorkflowAppService,
70360
+ WbsCollisionError: () => WbsCollisionError,
69843
70361
  UnsupportedProcessPlatformError: () => UnsupportedProcessPlatformError,
69844
70362
  TokenLedgerWatcher: () => TokenLedgerWatcher,
69845
70363
  TokenLedgerService: () => TokenLedgerService,
@@ -77571,7 +78089,7 @@ init_loader();
77571
78089
  init_src2();
77572
78090
  import { existsSync as existsSync9 } from "fs";
77573
78091
  import { homedir as homedir6 } from "os";
77574
- import { join as join17, resolve as resolve5 } from "path";
78092
+ import { join as join18, resolve as resolve5 } from "path";
77575
78093
 
77576
78094
  // src/workflow/resolve-spur-bin.ts
77577
78095
  import { basename as basename3 } from "path";
@@ -77589,22 +78107,22 @@ function resolveSpurBin() {
77589
78107
  }
77590
78108
 
77591
78109
  // src/workflow/make-lifecycle-adapter.ts
77592
- var GLOBAL_CONFIG_DIR = join17(".config", "spur");
78110
+ var GLOBAL_CONFIG_DIR = join18(".config", "spur");
77593
78111
  function globalConfigRoot(context4) {
77594
78112
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
77595
- return override !== undefined && override.length > 0 ? resolve5(context4.cwd, override) : join17(homedir6(), GLOBAL_CONFIG_DIR);
78113
+ return override !== undefined && override.length > 0 ? resolve5(context4.cwd, override) : join18(homedir6(), GLOBAL_CONFIG_DIR);
77596
78114
  }
77597
78115
  function resolveWorkflowPath(context4, profile) {
77598
78116
  const bundledRoot = bundledConfigRoot();
77599
78117
  if (bundledRoot !== null) {
77600
- const bundledPath = join17(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
78118
+ const bundledPath = join18(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
77601
78119
  if (existsSync9(bundledPath))
77602
78120
  return bundledPath;
77603
78121
  }
77604
- const projectPath = join17(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
78122
+ const projectPath = join18(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
77605
78123
  if (existsSync9(projectPath))
77606
78124
  return projectPath;
77607
- const globalPath = join17(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
78125
+ const globalPath = join18(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
77608
78126
  if (existsSync9(globalPath))
77609
78127
  return globalPath;
77610
78128
  return null;
@@ -77745,9 +78263,9 @@ function registerFeatureCommand(program2, context4) {
77745
78263
  let next = forwardPath[current];
77746
78264
  while (next !== undefined) {
77747
78265
  if (current === "active") {
77748
- await assertFeatureCheckPass(context4, id, options.folder, false);
78266
+ await assertFeatureCheckPass(context4, id, options.folder, false, "verifying");
77749
78267
  } else if (current === "verifying") {
77750
- await assertFeatureCheckPass(context4, id, options.folder, true);
78268
+ await assertFeatureCheckPass(context4, id, options.folder, true, "done");
77751
78269
  }
77752
78270
  const result = await svc.transition(id, next);
77753
78271
  history.push({ from: result.fromStatus ?? current, to: result.toStatus ?? next });
@@ -77837,7 +78355,7 @@ function registerFeatureCommand(program2, context4) {
77837
78355
  context4.setExitCode(1);
77838
78356
  }
77839
78357
  });
77840
- feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option("--strict", "Elevate warnings to failures").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
78358
+ feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option("--strict", "Elevate warnings to failures").option("--as <status>", "Evaluate the one-active-goal rule as if the feature were in <status> (0418: lifecycle FSM guards pass the transition target)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
77841
78359
  const resolved = await resolvePlanningFolders(context4.fs);
77842
78360
  const featuresDir = options.folder ?? context4.fs.resolve(resolved.featuresDir);
77843
78361
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
@@ -77860,7 +78378,8 @@ function registerFeatureCommand(program2, context4) {
77860
78378
  featuresDir,
77861
78379
  tasksDir,
77862
78380
  runDir: context4.fs.resolve(".spur/run"),
77863
- severityOverrides: resolved.severityOverrides
78381
+ severityOverrides: resolved.severityOverrides,
78382
+ asStatus: options.as
77864
78383
  });
77865
78384
  results.push(result);
77866
78385
  if (!json3) {
@@ -77900,7 +78419,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
77900
78419
  } else {
77901
78420
  context4.output.write(`Evaluated ${result.evaluated}/${result.totalFeatures} features; updated ${result.updatedCount} feature(s).`);
77902
78421
  for (const res of result.results) {
77903
- const tag = res.applied ? "UPDATED" : res.proposal.from === res.proposal.to ? "NOOP" : "SKIPPED";
78422
+ const tag = res.applied ? "UPDATED" : res.goalConflict !== undefined ? "GOAL-CONFLICT" : res.proposal.from === res.proposal.to ? "NOOP" : "SKIPPED";
77904
78423
  context4.output.write(` [${tag}] ${res.proposal.featureId}: ${res.proposal.from} -> ${res.proposal.to} (${res.proposal.reason})`);
77905
78424
  }
77906
78425
  }
@@ -77912,7 +78431,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
77912
78431
  if (options.json) {
77913
78432
  context4.output.write(toJson2(result));
77914
78433
  } else {
77915
- const tag = result.applied ? "UPDATED" : result.proposal.from === result.proposal.to ? "NOOP" : "SKIPPED";
78434
+ const tag = result.applied ? "UPDATED" : result.goalConflict !== undefined ? "GOAL-CONFLICT" : result.proposal.from === result.proposal.to ? "NOOP" : "SKIPPED";
77916
78435
  context4.output.write(`Feature ${id}: [${tag}] ${result.proposal.from} -> ${result.proposal.to} (${result.proposal.reason})`);
77917
78436
  }
77918
78437
  }
@@ -77940,7 +78459,7 @@ async function makeService(context4, folderOverride) {
77940
78459
  foldersConfig: resolved.foldersConfig
77941
78460
  });
77942
78461
  }
77943
- async function assertFeatureCheckPass(context4, id, folderOverride, strict) {
78462
+ async function assertFeatureCheckPass(context4, id, folderOverride, strict, asStatus) {
77944
78463
  const resolved = await resolvePlanningFolders(context4.fs);
77945
78464
  const featuresDir = folderOverride ?? context4.fs.resolve(resolved.featuresDir);
77946
78465
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
@@ -77952,7 +78471,8 @@ async function assertFeatureCheckPass(context4, id, folderOverride, strict) {
77952
78471
  const result = await new FeatureCheckService(context4.fs).check(`${featuresDir}/${fileName}`, id, {
77953
78472
  strict,
77954
78473
  featuresDir,
77955
- tasksDir
78474
+ tasksDir,
78475
+ asStatus
77956
78476
  });
77957
78477
  if (!result.pass) {
77958
78478
  const details = result.findings.map((f) => `${f.layer} ${f.section}: ${f.message}`).join("; ");
@@ -78006,13 +78526,13 @@ init_loader();
78006
78526
  init_src2();
78007
78527
  init_dist10();
78008
78528
  import { homedir as homedir7 } from "os";
78009
- import { join as join18, resolve as resolve6 } from "path";
78529
+ import { join as join19, resolve as resolve6 } from "path";
78010
78530
 
78011
78531
  // src/config.ts
78012
78532
  var CLI_CONFIG = {
78013
78533
  binaryName: "spur",
78014
78534
  binaryLabel: "spur",
78015
- binaryVersion: "0.3.28",
78535
+ binaryVersion: "0.3.29",
78016
78536
  configDir: ".spur",
78017
78537
  configFile: ".spur/config.yaml",
78018
78538
  databaseFile: ".spur/spur.db"
@@ -78063,8 +78583,8 @@ var SCAFFOLD_MANIFEST = [
78063
78583
  ];
78064
78584
 
78065
78585
  // src/commands/init.ts
78066
- var GLOBAL_CONFIG_DIR2 = join18(".config", "spur");
78067
- var GLOBAL_RULES_DIR2 = join18(GLOBAL_CONFIG_DIR2, "rules");
78586
+ var GLOBAL_CONFIG_DIR2 = join19(".config", "spur");
78587
+ var GLOBAL_RULES_DIR2 = join19(GLOBAL_CONFIG_DIR2, "rules");
78068
78588
  var GLOBAL_CONFIG_EXAMPLE = "config.example.yaml";
78069
78589
  var GLOBAL_CONFIG_FILE2 = "config.yaml";
78070
78590
  var INDEXED_CONTEXT_MARKER = "## Indexed context";
@@ -78095,7 +78615,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
78095
78615
  `;
78096
78616
  function globalRulesRoot(context4) {
78097
78617
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
78098
- return override !== undefined && override.length > 0 ? resolve6(context4.cwd, override) : join18(homedir7(), GLOBAL_RULES_DIR2);
78618
+ return override !== undefined && override.length > 0 ? resolve6(context4.cwd, override) : join19(homedir7(), GLOBAL_RULES_DIR2);
78099
78619
  }
78100
78620
  async function writeIfNew(context4, path9, content, force, result) {
78101
78621
  if (!force && await context4.fs.exists(path9)) {
@@ -78112,11 +78632,11 @@ async function seedGlobalRules(context4) {
78112
78632
  const target = globalRulesRoot(context4);
78113
78633
  let written = 0;
78114
78634
  for (const relPath of await listBundledRuleFiles()) {
78115
- const destination = join18(target, relPath);
78635
+ const destination = join19(target, relPath);
78116
78636
  if (await context4.fs.exists(destination))
78117
78637
  continue;
78118
- await context4.fs.ensureDir(join18(target, ...relPath.split("/").slice(0, -1)));
78119
- await context4.fs.writeFile(destination, await context4.fs.readFile(join18(source, relPath)));
78638
+ await context4.fs.ensureDir(join19(target, ...relPath.split("/").slice(0, -1)));
78639
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join19(source, relPath)));
78120
78640
  written += 1;
78121
78641
  }
78122
78642
  return written;
@@ -78126,20 +78646,20 @@ async function seedGlobalConfig(context4) {
78126
78646
  if (source === null)
78127
78647
  return 0;
78128
78648
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
78129
- const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve6(context4.cwd, globalOverride) : join18(homedir7(), GLOBAL_CONFIG_DIR2);
78649
+ const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve6(context4.cwd, globalOverride) : join19(homedir7(), GLOBAL_CONFIG_DIR2);
78130
78650
  let written = 0;
78131
78651
  for (const relPath of listBundledConfigFiles()) {
78132
78652
  if (relPath === GLOBAL_CONFIG_EXAMPLE)
78133
78653
  continue;
78134
- const destination = join18(target, relPath);
78654
+ const destination = join19(target, relPath);
78135
78655
  if (await context4.fs.exists(destination))
78136
78656
  continue;
78137
- await context4.fs.ensureDir(join18(target, ...relPath.split("/").slice(0, -1)));
78138
- await context4.fs.writeFile(destination, await context4.fs.readFile(join18(source, relPath)));
78657
+ await context4.fs.ensureDir(join19(target, ...relPath.split("/").slice(0, -1)));
78658
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join19(source, relPath)));
78139
78659
  written += 1;
78140
78660
  }
78141
- const examplePath = join18(source, GLOBAL_CONFIG_EXAMPLE);
78142
- const globalConfigPath = join18(target, GLOBAL_CONFIG_FILE2);
78661
+ const examplePath = join19(source, GLOBAL_CONFIG_EXAMPLE);
78662
+ const globalConfigPath = join19(target, GLOBAL_CONFIG_FILE2);
78143
78663
  if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
78144
78664
  await context4.fs.ensureDir(target);
78145
78665
  await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
@@ -78153,7 +78673,7 @@ function registerInitCommand(program2, context4) {
78153
78673
  const force = options.force === true;
78154
78674
  const minimal = options.minimal === true;
78155
78675
  const projectName = options.name ?? "default";
78156
- const configPath = join18(context4.cwd, CLI_CONFIG.configFile);
78676
+ const configPath = join19(context4.cwd, CLI_CONFIG.configFile);
78157
78677
  if (!force && await context4.fs.exists(configPath)) {
78158
78678
  const message = `Already initialized: ${CLI_CONFIG.configFile}. Use --force to overwrite.`;
78159
78679
  context4.output.write(json3 ? toJson2({ ok: false, reason: "already-initialized", config: CLI_CONFIG.configFile }) : message);
@@ -78188,13 +78708,13 @@ function registerInitCommand(program2, context4) {
78188
78708
  ].join(`
78189
78709
  `)}
78190
78710
  `;
78191
- await context4.fs.ensureDir(join18(context4.cwd, CLI_CONFIG.configDir));
78711
+ await context4.fs.ensureDir(join19(context4.cwd, CLI_CONFIG.configDir));
78192
78712
  await context4.fs.writeFile(configPath, configYaml);
78193
78713
  result.created.push(configPath);
78194
- const agentsDir = join18(context4.cwd, CLI_CONFIG.configDir, "agents");
78714
+ const agentsDir = join19(context4.cwd, CLI_CONFIG.configDir, "agents");
78195
78715
  await context4.fs.ensureDir(agentsDir);
78196
- await writeIfNew(context4, join18(agentsDir, ".gitkeep"), "", force, result);
78197
- const gitignorePath = join18(context4.cwd, ".gitignore");
78716
+ await writeIfNew(context4, join19(agentsDir, ".gitkeep"), "", force, result);
78717
+ const gitignorePath = join19(context4.cwd, ".gitignore");
78198
78718
  const contextEntry = ".spur/context/";
78199
78719
  if (await context4.fs.exists(gitignorePath)) {
78200
78720
  const existing = await context4.fs.readFile(gitignorePath);
@@ -78216,20 +78736,20 @@ ${contextEntry}
78216
78736
  for (const relPath of listBundledProjectSeedFiles()) {
78217
78737
  if (relPath.startsWith("templates/docs/"))
78218
78738
  continue;
78219
- const sourcePath = join18(configRoot, relPath);
78739
+ const sourcePath = join19(configRoot, relPath);
78220
78740
  if (!await context4.fs.exists(sourcePath))
78221
78741
  continue;
78222
- const targetPath = join18(context4.cwd, CLI_CONFIG.configDir, relPath);
78223
- await context4.fs.ensureDir(join18(targetPath, ".."));
78742
+ const targetPath = join19(context4.cwd, CLI_CONFIG.configDir, relPath);
78743
+ await context4.fs.ensureDir(join19(targetPath, ".."));
78224
78744
  await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
78225
78745
  }
78226
78746
  for (const entry of SCAFFOLD_MANIFEST) {
78227
- const sourcePath = join18(configRoot, entry.source);
78747
+ const sourcePath = join19(configRoot, entry.source);
78228
78748
  if (!await context4.fs.exists(sourcePath))
78229
78749
  continue;
78230
- const baseDir = entry.root === true ? context4.cwd : join18(context4.cwd, CLI_CONFIG.configDir);
78231
- const targetPath = join18(baseDir, entry.target);
78232
- await context4.fs.ensureDir(join18(targetPath, ".."));
78750
+ const baseDir = entry.root === true ? context4.cwd : join19(context4.cwd, CLI_CONFIG.configDir);
78751
+ const targetPath = join19(baseDir, entry.target);
78752
+ await context4.fs.ensureDir(join19(targetPath, ".."));
78233
78753
  const entryForce = entry.preserve === true ? false : force;
78234
78754
  let body = await context4.fs.readFile(sourcePath);
78235
78755
  if (entry.target === "AGENTS.md") {
@@ -78242,7 +78762,7 @@ ${contextEntry}
78242
78762
  }
78243
78763
  }
78244
78764
  }
78245
- const agentsMdPath = join18(context4.cwd, "AGENTS.md");
78765
+ const agentsMdPath = join19(context4.cwd, "AGENTS.md");
78246
78766
  if (await context4.fs.exists(agentsMdPath)) {
78247
78767
  const existing = await context4.fs.readFile(agentsMdPath);
78248
78768
  if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
@@ -78420,10 +78940,10 @@ function parseInterval2(raw) {
78420
78940
 
78421
78941
  // src/commands/migrate.ts
78422
78942
  init_src2();
78423
- import { join as join19 } from "path";
78943
+ import { join as join20 } from "path";
78424
78944
  function registerMigrateCommand(program2, context4) {
78425
78945
  program2.command("migrate").summary("apply CLI-owned schema migrations").option("--json", "Output machine-readable JSON").action(async (options) => {
78426
- const migrations = await loadSqlMigrations(join19(context4.cwd, "drizzle")).catch(() => {
78946
+ const migrations = await loadSqlMigrations(join20(context4.cwd, "drizzle")).catch(() => {
78427
78947
  return;
78428
78948
  });
78429
78949
  const applied = await applyCliMigrations(await context4.getDb(), migrations);
@@ -78839,7 +79359,7 @@ function formatTraceDetail(detail) {
78839
79359
 
78840
79360
  // src/commands/serve.ts
78841
79361
  init_src();
78842
- import { join as join23 } from "path";
79362
+ import { join as join24 } from "path";
78843
79363
 
78844
79364
  // ../server/src/index.ts
78845
79365
  init_src();
@@ -78849,11 +79369,11 @@ init_src3();
78849
79369
  init_src();
78850
79370
  init_loader();
78851
79371
  init_src2();
78852
- import { basename as basename6, dirname as dirname15, isAbsolute as isAbsolute3, join as join22 } from "path";
79372
+ import { basename as basename6, dirname as dirname15, isAbsolute as isAbsolute3, join as join23 } from "path";
78853
79373
  init_dist5();
78854
79374
 
78855
79375
  // ../server/src/bootstrap.ts
78856
- import { join as join20 } from "path";
79376
+ import { join as join21 } from "path";
78857
79377
 
78858
79378
  // ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
78859
79379
  var guard = (func, shouldGuard) => {
@@ -84088,6 +84608,9 @@ var contextInjector = (appRt) => async function contextInjectorMiddleware(c3, ne
84088
84608
  await next();
84089
84609
  };
84090
84610
 
84611
+ // ../server/src/middleware/error-handler.ts
84612
+ init_src3();
84613
+
84091
84614
  // ../server/src/errors.ts
84092
84615
  init_errors5();
84093
84616
 
@@ -84150,6 +84673,14 @@ function resolveError(err, requestId, isProd) {
84150
84673
  details: { requestId }
84151
84674
  };
84152
84675
  }
84676
+ if (err instanceof WbsCollisionError) {
84677
+ return {
84678
+ status: 409,
84679
+ apiCode: "WBS_COLLISION",
84680
+ message: isProd ? "WBS collision - task already exists" : message,
84681
+ details: { requestId, wbs: err.wbs, existingPath: err.existingPath, attemptedPath: err.attemptedPath }
84682
+ };
84683
+ }
84153
84684
  if (err instanceof LockTimeoutError) {
84154
84685
  return {
84155
84686
  status: 503,
@@ -87737,7 +88268,7 @@ function createApp(appRt, opts) {
87737
88268
  app.use("*", async (c3, next) => {
87738
88269
  const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
87739
88270
  try {
87740
- const file2 = Bun.file(join20(webDistPath, pathname));
88271
+ const file2 = Bun.file(join21(webDistPath, pathname));
87741
88272
  if (await file2.exists()) {
87742
88273
  const headers = new Headers({ "content-type": file2.type });
87743
88274
  return new Response(file2.stream(), { headers });
@@ -87750,7 +88281,7 @@ function createApp(appRt, opts) {
87750
88281
  return c3.json({ error: "Not Found" }, 404);
87751
88282
  }
87752
88283
  try {
87753
- const indexFile = Bun.file(join20(webDistPath, "index.html"));
88284
+ const indexFile = Bun.file(join21(webDistPath, "index.html"));
87754
88285
  if (await indexFile.exists()) {
87755
88286
  return new Response(indexFile.stream(), {
87756
88287
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -87771,7 +88302,7 @@ init_src3();
87771
88302
  init_src();
87772
88303
  init_src2();
87773
88304
  init_dist5();
87774
- import { dirname as dirname14, join as join21 } from "path";
88305
+ import { dirname as dirname14, join as join22 } from "path";
87775
88306
  var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
87776
88307
 
87777
88308
  class LazyPlanningEventEmitter {
@@ -87794,7 +88325,7 @@ class LazyPlanningEventEmitter {
87794
88325
  var DEFAULT_PLANNING_FOLDERS = {
87795
88326
  tasksDir: DEFAULT_TASKS_DIR,
87796
88327
  featuresDir: DEFAULT_FEATURES_DIR,
87797
- foldersConfig: { active_folder: DEFAULT_TASKS_DIR, folders: { [DEFAULT_TASKS_DIR]: { base_counter: 0 } } }
88328
+ foldersConfig: { active_folder: DEFAULT_TASKS_DIR, folders: { [DEFAULT_TASKS_DIR]: { baseCounter: 0 } } }
87798
88329
  };
87799
88330
 
87800
88331
  class NotConfiguredError extends Error {
@@ -87806,7 +88337,7 @@ class NotConfiguredError extends Error {
87806
88337
  function createServerContext(appRt, options) {
87807
88338
  const cwd = options.cwd;
87808
88339
  const fs3 = options.fs;
87809
- const dbUrl = options.dbUrl ?? join21(cwd, DEFAULT_DATABASE_URL);
88340
+ const dbUrl = options.dbUrl ?? join22(cwd, DEFAULT_DATABASE_URL);
87810
88341
  const eventsBus = options.eventsBus ?? appRt.events;
87811
88342
  const jobQueueEnabled = options.jobQueueEnabled ?? false;
87812
88343
  const folders = options.folders ?? DEFAULT_PLANNING_FOLDERS;
@@ -88166,15 +88697,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
88166
88697
  await runFeatureActionJob(ctx, env, payload);
88167
88698
  }
88168
88699
  async function resolveWebDistPath(configuredPath) {
88169
- const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute3(configuredPath) ? configuredPath : join22(process.cwd(), configuredPath)] : [
88170
- join22(process.cwd(), "dist/web"),
88171
- join22(import.meta.dir, "web"),
88172
- join22(dirname15(process.execPath), "web"),
88173
- join22(dirname15(process.execPath), "../web"),
88174
- join22(import.meta.dir, "../../../dist/web")
88700
+ const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute3(configuredPath) ? configuredPath : join23(process.cwd(), configuredPath)] : [
88701
+ join23(process.cwd(), "dist/web"),
88702
+ join23(import.meta.dir, "web"),
88703
+ join23(dirname15(process.execPath), "web"),
88704
+ join23(dirname15(process.execPath), "../web"),
88705
+ join23(import.meta.dir, "../../../dist/web")
88175
88706
  ];
88176
88707
  for (const candidate of candidates) {
88177
- if (await Bun.file(join22(candidate, "index.html")).exists()) {
88708
+ if (await Bun.file(join23(candidate, "index.html")).exists()) {
88178
88709
  return candidate;
88179
88710
  }
88180
88711
  }
@@ -88333,7 +88864,7 @@ if (false) {}
88333
88864
 
88334
88865
  // src/commands/serve.ts
88335
88866
  function resolveServeDbUrl(cwd, env, configuredUrl) {
88336
- return env.DATABASE_URL === undefined ? join23(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
88867
+ return env.DATABASE_URL === undefined ? join24(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
88337
88868
  }
88338
88869
  function registerServeCommand(program2, context4) {
88339
88870
  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) => {
@@ -88372,7 +88903,7 @@ function registerServeCommand(program2, context4) {
88372
88903
  }
88373
88904
 
88374
88905
  // src/commands/status.ts
88375
- import { join as join24 } from "path";
88906
+ import { join as join25 } from "path";
88376
88907
 
88377
88908
  // src/errors.ts
88378
88909
  class CommandError extends Error {
@@ -88443,8 +88974,8 @@ function registerStatusCommand(program2, context4) {
88443
88974
  }
88444
88975
  async function runStatusCore(path9, options, context4) {
88445
88976
  const [packageJsonExists, spurConfigExists, git, agentSpecs] = await Promise.all([
88446
- context4.fs.exists(join24(context4.cwd, "package.json")),
88447
- context4.fs.exists(join24(context4.cwd, ".spur", "config.yaml")),
88977
+ context4.fs.exists(join25(context4.cwd, "package.json")),
88978
+ context4.fs.exists(join25(context4.cwd, ".spur", "config.yaml")),
88448
88979
  gitContext(context4.cwd),
88449
88980
  listAgentSpecIds(context4)
88450
88981
  ]);
@@ -88472,14 +89003,14 @@ async function runStatusCore(path9, options, context4) {
88472
89003
  return status.ok ? 0 : 1;
88473
89004
  }
88474
89005
  async function listAgentSpecIds(context4) {
88475
- const dir = join24(context4.cwd, ".spur", "agents");
89006
+ const dir = join25(context4.cwd, ".spur", "agents");
88476
89007
  if (!await context4.fs.exists(dir))
88477
89008
  return [];
88478
89009
  const entries = await context4.fs.readDir(dir);
88479
89010
  return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
88480
89011
  }
88481
89012
  async function readTargetStatus(context4, targetPath) {
88482
- const resolved = join24(context4.cwd, targetPath);
89013
+ const resolved = join25(context4.cwd, targetPath);
88483
89014
  const stat = await context4.fs.stat(resolved);
88484
89015
  if (stat === null)
88485
89016
  throw new CommandError(`status failed: path does not exist at ${resolved}`);
@@ -88492,7 +89023,7 @@ init_loader();
88492
89023
  init_src2();
88493
89024
  init_dist5();
88494
89025
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
88495
- import { join as join25 } from "path";
89026
+ import { join as join26 } from "path";
88496
89027
  // schemas/section-matrix.schema.json
88497
89028
  var section_matrix_schema_default = {
88498
89029
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -88804,7 +89335,7 @@ var spur_config_schema_default = {
88804
89335
  },
88805
89336
  tasks: {
88806
89337
  type: "object",
88807
- description: "Task-folder registration (design \xA79). Absorbs the legacy docs/.tasks/config.json folders + base_counter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
89338
+ description: "Task-folder registration (design \xA79). Absorbs the legacy docs/.tasks/config.json folders + baseCounter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
88808
89339
  properties: {
88809
89340
  folders: {
88810
89341
  type: "object",
@@ -89083,12 +89614,17 @@ function renderMigrationReport(report, dryRun, corpusDir) {
89083
89614
  }
89084
89615
  function registerTaskCommand(program2, context4) {
89085
89616
  const task = program2.command("task").summary("manage tasks");
89086
- task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--dedupe-within <seconds>", "Refuse creation if an existing task under the same feature has an identical name created within the last N seconds (task 0341 R4)", (v) => Number.parseInt(v, 10)).option("--allow-duplicate-name", "Override the --dedupe-within guard (creates anyway)").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
89617
+ task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
89087
89618
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
89088
89619
  context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
89089
89620
  context4.setExitCode(2);
89090
89621
  return;
89091
89622
  }
89623
+ if (options.dedupeWithin !== undefined && (!Number.isInteger(options.dedupeWithin) || options.dedupeWithin <= 0)) {
89624
+ context4.output.error("--dedupe-within must be a positive integer");
89625
+ context4.setExitCode(2);
89626
+ return;
89627
+ }
89092
89628
  const svc = await makeService2(context4, options.folder);
89093
89629
  try {
89094
89630
  const result = await svc.create({
@@ -89096,7 +89632,7 @@ function registerTaskCommand(program2, context4) {
89096
89632
  featureId: options.feature,
89097
89633
  parentWbs: options.parent,
89098
89634
  template: options.template,
89099
- dedupeWithinSec: options.allowDuplicateName ? undefined : options.dedupeWithin
89635
+ dedupeWithinSec: options.allowDuplicateName ? null : options.dedupeWithin
89100
89636
  });
89101
89637
  if (options.json) {
89102
89638
  context4.output.write(toJson2(result));
@@ -89104,8 +89640,37 @@ function registerTaskCommand(program2, context4) {
89104
89640
  context4.output.write(`Created task ${result.ref.id}: ${result.ref.filePath}`);
89105
89641
  }
89106
89642
  } catch (err) {
89107
- if (err instanceof DuplicateFollowUpError) {
89108
- context4.output.error(err.message);
89643
+ if (err instanceof WbsCollisionError) {
89644
+ if (options.json) {
89645
+ context4.output.write(toJson2({
89646
+ ok: false,
89647
+ error: {
89648
+ code: "wbs-collision",
89649
+ message: err.message,
89650
+ wbs: err.wbs,
89651
+ existingPath: err.existingPath,
89652
+ attemptedPath: err.attemptedPath
89653
+ }
89654
+ }));
89655
+ } else {
89656
+ context4.output.error(err.message);
89657
+ }
89658
+ context4.setExitCode(3);
89659
+ } else if (err instanceof DuplicateFollowUpError) {
89660
+ if (options.json) {
89661
+ context4.output.write(toJson2({
89662
+ ok: false,
89663
+ error: {
89664
+ code: "duplicate-follow-up",
89665
+ message: err.message,
89666
+ existingWbs: err.existingWbs,
89667
+ existingName: err.existingName,
89668
+ attemptedName: err.attemptedName
89669
+ }
89670
+ }));
89671
+ } else {
89672
+ context4.output.error(err.message);
89673
+ }
89109
89674
  context4.setExitCode(3);
89110
89675
  } else {
89111
89676
  context4.output.error(String(err));
@@ -89176,7 +89741,7 @@ ${result.content}`);
89176
89741
  let forcedDoneVerdict;
89177
89742
  if (status === "done") {
89178
89743
  const current = await svc.show(wbs);
89179
- const verdictDir = options.verdictDir ?? join25(context4.cwd, ".spur", "run");
89744
+ const verdictDir = options.verdictDir ?? join26(context4.cwd, ".spur", "run");
89180
89745
  const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
89181
89746
  const guardOutcome = evaluateDoneTransition({
89182
89747
  wbs,
@@ -89435,8 +90000,26 @@ ${result.content}`);
89435
90000
  }
89436
90001
  }
89437
90002
  } catch (err) {
89438
- context4.output.error(String(err));
89439
- context4.setExitCode(1);
90003
+ if (err instanceof WbsCollisionError) {
90004
+ if (options.json) {
90005
+ context4.output.write(toJson2({
90006
+ ok: false,
90007
+ error: {
90008
+ code: "wbs-collision",
90009
+ message: err.message,
90010
+ wbs: err.wbs,
90011
+ existingPath: err.existingPath,
90012
+ attemptedPath: err.attemptedPath
90013
+ }
90014
+ }));
90015
+ } else {
90016
+ context4.output.error(err.message);
90017
+ }
90018
+ context4.setExitCode(3);
90019
+ } else {
90020
+ context4.output.error(String(err));
90021
+ context4.setExitCode(1);
90022
+ }
89440
90023
  }
89441
90024
  });
89442
90025
  task.command("record").summary("Record pipeline results into the task file \u2014 Testing, Review, and optional Solution backfill.").argument("<wbs>", "Task WBS number").option("--verdict-file <path>", "Path to verdict JSON (default: .spur/run/<wbs>-verdict.json)").option("--solution-from-diff", "Backfill Solution from git diff when bare").option("--transition <status>", "Optional lifecycle transition (e.g. testing)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
@@ -89574,6 +90157,23 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
89574
90157
  }
89575
90158
  }
89576
90159
  }
90160
+ if (!wbs) {
90161
+ const locator = await makeTaskLocator(context4);
90162
+ const duplicates2 = await locator.findDuplicateWbs();
90163
+ for (const dup of duplicates2) {
90164
+ const [first] = dup;
90165
+ if (first === undefined)
90166
+ continue;
90167
+ const msg = `Duplicate WBS ${first.wbs} found in ${dup.length} files:
90168
+ ` + dup.map((h2) => ` ${h2.filePath}`).join(`
90169
+ `);
90170
+ if (json3) {
90171
+ results.push({ wbs: first.wbs, pass: false, status: "duplicate", findings: [msg] });
90172
+ } else {
90173
+ context4.output.error(msg);
90174
+ }
90175
+ }
90176
+ }
89577
90177
  if (json3) {
89578
90178
  context4.output.write(toJson2(results));
89579
90179
  }
@@ -89716,7 +90316,7 @@ function loadTemplateContent(projectRoot, variant) {
89716
90316
  return templateContentCache.get(variant);
89717
90317
  if (templateMissSet.has(variant))
89718
90318
  return;
89719
- const localPath = join25(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
90319
+ const localPath = join26(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
89720
90320
  if (existsSync10(localPath)) {
89721
90321
  const content = readFileSync9(localPath, "utf8");
89722
90322
  templateContentCache.set(variant, content);
@@ -89724,7 +90324,7 @@ function loadTemplateContent(projectRoot, variant) {
89724
90324
  }
89725
90325
  const root = bundledConfigRoot();
89726
90326
  if (root !== null) {
89727
- const templatePath = join25(root, "templates", "task", `${variant}.md`);
90327
+ const templatePath = join26(root, "templates", "task", `${variant}.md`);
89728
90328
  if (existsSync10(templatePath)) {
89729
90329
  const content = readFileSync9(templatePath, "utf8");
89730
90330
  templateContentCache.set(variant, content);
@@ -89740,13 +90340,13 @@ function loadTemplateBodies(projectRoot, variant) {
89740
90340
  if (cached2 !== undefined)
89741
90341
  return cached2;
89742
90342
  let bodies = {};
89743
- const localPath = join25(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
90343
+ const localPath = join26(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
89744
90344
  if (existsSync10(localPath)) {
89745
90345
  bodies = extractTemplateBodies(readFileSync9(localPath, "utf8"));
89746
90346
  } else {
89747
90347
  const root = bundledConfigRoot();
89748
90348
  if (root !== null) {
89749
- const templatePath = join25(root, "templates", "task", `${variant}.md`);
90349
+ const templatePath = join26(root, "templates", "task", `${variant}.md`);
89750
90350
  if (existsSync10(templatePath)) {
89751
90351
  bodies = extractTemplateBodies(readFileSync9(templatePath, "utf8"));
89752
90352
  }
@@ -89799,7 +90399,7 @@ async function loadSectionMatrixUncached(projectRoot) {
89799
90399
  }
89800
90400
  const root = bundledConfigRoot();
89801
90401
  if (root !== null) {
89802
- const matrixPath = join25(root, "tasks", "section-matrix.yaml");
90402
+ const matrixPath = join26(root, "tasks", "section-matrix.yaml");
89803
90403
  if (await fs3.exists(matrixPath)) {
89804
90404
  const data = await loadStructuredSpurConfig(matrixPath, {
89805
90405
  validateJsonSchema: true,
@@ -90518,6 +91118,10 @@ function formatTraceTimeline(result) {
90518
91118
  `Started: ${run.startedAt} Completed: ${run.completedAt ?? "-"} Events: ${events2.length}`,
90519
91119
  ""
90520
91120
  ];
91121
+ if (result.outputArtifact !== undefined) {
91122
+ lines.push(`Agent output: ${result.outputArtifact} (tail -f for live view)`);
91123
+ lines.push("");
91124
+ }
90521
91125
  for (const event2 of events2) {
90522
91126
  lines.push(formatTimelineEvent(event2));
90523
91127
  }
@@ -90598,7 +91202,7 @@ init_src3();
90598
91202
  init_src();
90599
91203
  init_src2();
90600
91204
  init_dist5();
90601
- import { dirname as dirname16, join as join26, resolve as resolve9 } from "path";
91205
+ import { dirname as dirname16, join as join27, resolve as resolve9 } from "path";
90602
91206
  import { isatty as isatty4 } from "tty";
90603
91207
 
90604
91208
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -92020,7 +92624,7 @@ function createCliContext(options) {
92020
92624
  function noopSetExitCode(_code) {}
92021
92625
  async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
92022
92626
  const config4 = buildConfigFromEnv(env);
92023
- const configuredUrl = env.DATABASE_URL === undefined ? join26(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
92627
+ const configuredUrl = env.DATABASE_URL === undefined ? join27(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
92024
92628
  const url2 = dbUrl ?? configuredUrl;
92025
92629
  if (url2 !== IN_MEMORY_DATABASE_URL) {
92026
92630
  await createNodeFileSystem3().ensureDir(dirname16(url2));