@gobing-ai/spur 0.3.14 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/spur.js +125 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
package/spur.js CHANGED
@@ -50887,7 +50887,7 @@ class AgentService {
50887
50887
  return results.some((result) => !result.usable && result.tier === 1) ? 1 : 0;
50888
50888
  }
50889
50889
  async run(prompt, flags, deps) {
50890
- const outcome = await this.executeRun(prompt, flags, deps, false);
50890
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: false });
50891
50891
  if (!outcome.ok) {
50892
50892
  this.ctx.output.error(outcome.message);
50893
50893
  return outcome.exitCode;
@@ -50905,7 +50905,7 @@ class AgentService {
50905
50905
  return 3;
50906
50906
  }
50907
50907
  async runCapture(prompt, flags, deps) {
50908
- const outcome = await this.executeRun(prompt, flags, deps, true);
50908
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: true });
50909
50909
  if (!outcome.ok) {
50910
50910
  return { exitCode: outcome.exitCode, answer: "" };
50911
50911
  }
@@ -50919,7 +50919,25 @@ class AgentService {
50919
50919
  stderr: result.stderr
50920
50920
  };
50921
50921
  }
50922
- async executeRun(prompt, flags, deps, silent) {
50922
+ async runTraced(prompt, flags, deps) {
50923
+ const outcome = await this.executeRun(prompt, flags, deps, { silent: true, nonInteractive: true });
50924
+ if (!outcome.ok) {
50925
+ return { exitCode: outcome.exitCode, stdout: "", message: outcome.message };
50926
+ }
50927
+ const result = outcome.result;
50928
+ const exitCode = result.exitCode === 0 ? 0 : 3;
50929
+ return {
50930
+ exitCode,
50931
+ invocation: outcome.invocation,
50932
+ stdout: result.stdout,
50933
+ stderr: result.stderr,
50934
+ durationMs: result.durationMs,
50935
+ ...result.signal !== undefined ? { signal: result.signal } : {}
50936
+ };
50937
+ }
50938
+ async executeRun(prompt, flags, deps, options) {
50939
+ const silent = options.silent;
50940
+ const nonInteractive = options.nonInteractive === true;
50923
50941
  const mode = stringFlag(flags, "mode", "text");
50924
50942
  if (mode !== "text" && mode !== "json") {
50925
50943
  return { ok: false, exitCode: 2, message: `Invalid mode: ${mode} (must be text or json)` };
@@ -50943,7 +50961,9 @@ class AgentService {
50943
50961
  return { ok: false, exitCode: 2, message: "Prompt is required" };
50944
50962
  }
50945
50963
  const jsonOutput = silent || booleanFlag(flags, "json");
50946
- const outputPolicy = jsonOutput ? { mode: "buffered" } : { mode: "stream", isTTY: isatty3(1) };
50964
+ const forceBuffered = silent || nonInteractive;
50965
+ const outputPolicy = forceBuffered ? { mode: "buffered" } : jsonOutput ? { mode: "buffered" } : { mode: "stream", isTTY: isatty3(1) };
50966
+ const outputMode = outputPolicy.mode;
50947
50967
  const runner = deps?.runner ?? new AiRunner({
50948
50968
  processExecutor: new NodeProcessExecutor3({
50949
50969
  output: outputPolicy,
@@ -50962,7 +50982,8 @@ class AgentService {
50962
50982
  if (!jsonOutput && TIER2_AGENTS.has(agent)) {
50963
50983
  this.ctx.output.error(`Warning: ${agent} is a Tier-2 agent (TUI/gateway only)`);
50964
50984
  }
50965
- const input = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : prompt;
50985
+ const translated = prompt !== undefined && isClaudeStyleSlashCommand(prompt) ? translateSlashCommand(agent, prompt) : undefined;
50986
+ const input = translated ?? prompt;
50966
50987
  const purpose = stringFlag(flags, "purpose", "") || undefined;
50967
50988
  const tags = parseTagsFlag(flags);
50968
50989
  const systemPrompt = stringFlag(flags, "system-prompt", "") || undefined;
@@ -50979,9 +51000,9 @@ class AgentService {
50979
51000
  ...systemPrompt !== undefined ? { systemPrompt } : {},
50980
51001
  ...taskId !== undefined ? { taskId } : {}
50981
51002
  };
51003
+ let shimCommand;
50982
51004
  try {
50983
- const shim = getAgentShim(agent);
50984
- const shimCommand = shim.getPromptCommand(promptOptions);
51005
+ shimCommand = typeof runner.buildPromptCommand === "function" ? runner.buildPromptCommand(agent, promptOptions, { cwd: cwd || undefined }) : getAgentShim(agent).getPromptCommand(promptOptions);
50985
51006
  if (!jsonOutput) {
50986
51007
  const version3 = (await detector.detectOne(agent)).version;
50987
51008
  this.ctx.output.error(`\u2699\uFE0F ${agent}${version3 !== null ? ` v${version3}` : ""}
@@ -50990,6 +51011,21 @@ class AgentService {
50990
51011
  } catch (error51) {
50991
51012
  return { ok: false, exitCode: 2, message: error51 instanceof Error ? error51.message : String(error51) };
50992
51013
  }
51014
+ const traceInput = input === undefined ? undefined : traceSafePrompt(input);
51015
+ const invocation = {
51016
+ agent,
51017
+ source: resolved.source,
51018
+ command: shimCommand.command,
51019
+ argv: sanitizeInvocationArgv(shimCommand.args, input, traceInput),
51020
+ ...cwd !== "" ? { cwd } : {},
51021
+ mode,
51022
+ outputMode,
51023
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
51024
+ continue: continueFlag,
51025
+ stdinInteractive: false,
51026
+ ...model !== undefined ? { model } : {},
51027
+ ...translated !== undefined && prompt !== undefined ? { translatedFrom: traceSafePrompt(prompt) } : {}
51028
+ };
50993
51029
  let result;
50994
51030
  const controller = new AbortController;
50995
51031
  const onTerminate = () => controller.abort();
@@ -51007,7 +51043,7 @@ class AgentService {
51007
51043
  process.off("SIGTERM", onTerminate);
51008
51044
  process.off("SIGINT", onTerminate);
51009
51045
  }
51010
- return { ok: true, result };
51046
+ return { ok: true, result, invocation };
51011
51047
  }
51012
51048
  async resolveAgent(prompt, flags, doctorRunner) {
51013
51049
  const raw = stringFlag(flags, "agent", "auto");
@@ -51131,6 +51167,32 @@ class AgentService {
51131
51167
  function toJson(value) {
51132
51168
  return JSON.stringify(value, null, 2);
51133
51169
  }
51170
+ function traceSafePrompt(prompt) {
51171
+ const trimmed = prompt.trim();
51172
+ if (!TRACE_SAFE_SLASH_COMMAND.test(trimmed)) {
51173
+ return `[redacted prompt: ${prompt.length} chars]`;
51174
+ }
51175
+ const [command = "", ...tokens] = trimmed.split(/\s+/);
51176
+ const safeTokens = tokens.map((token) => TRACE_SAFE_SLASH_TOKEN.test(token) ? token : "[redacted]");
51177
+ return [command, ...safeTokens].join(" ");
51178
+ }
51179
+ function sanitizeInvocationArgv(argv, rawInput, traceInput) {
51180
+ let redactNext = false;
51181
+ return argv.map((arg) => {
51182
+ if (redactNext) {
51183
+ redactNext = false;
51184
+ return "[redacted]";
51185
+ }
51186
+ if (SENSITIVE_FLAG.test(arg)) {
51187
+ redactNext = true;
51188
+ return arg;
51189
+ }
51190
+ if (rawInput !== undefined && traceInput !== undefined && arg.includes(rawInput)) {
51191
+ return traceInput;
51192
+ }
51193
+ return arg.replace(SENSITIVE_INLINE_FLAG, "$1[redacted]").replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_]{12,}\b/g, "[redacted]");
51194
+ });
51195
+ }
51134
51196
  function extractPhase(prompt) {
51135
51197
  if (prompt === undefined)
51136
51198
  return;
@@ -51231,9 +51293,14 @@ function parseTagsFlag(flags) {
51231
51293
  const tags = raw.split(",").map((tag) => tag.trim()).filter(Boolean);
51232
51294
  return tags.length > 0 ? tags : undefined;
51233
51295
  }
51296
+ var TRACE_SAFE_SLASH_COMMAND, TRACE_SAFE_SLASH_TOKEN, SENSITIVE_FLAG, SENSITIVE_INLINE_FLAG;
51234
51297
  var init_agent_service = __esm(() => {
51235
51298
  init_dist7();
51236
51299
  init_dist5();
51300
+ TRACE_SAFE_SLASH_COMMAND = /^(?:\/skill:(?:sp|rd3)-|\/(?:sp|rd3)[:-]|\$(?:sp|rd3)-)[A-Za-z0-9._-]+(?:\s|$)/;
51301
+ TRACE_SAFE_SLASH_TOKEN = /^(?:\d{4}|--(?:auto|next|force|bdd)|--(?:mode|fix|focus)|implement|test|review|verify|all|none|blockers-first|quick|requirements|background|constraints|acceptance)$/;
51302
+ SENSITIVE_FLAG = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
51303
+ SENSITIVE_INLINE_FLAG = /^(--?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
51237
51304
  });
51238
51305
 
51239
51306
  // ../../packages/domain/src/analytics/models.ts
@@ -61556,6 +61623,10 @@ function isPlaceholderBody(body) {
61556
61623
  const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
61557
61624
  return stripped.length === 0;
61558
61625
  }
61626
+ function isPlaceholderCell(cell) {
61627
+ const c3 = cell.trim();
61628
+ return c3 === "" || /^[\u2014\u2013-]+$/.test(c3) || /^n\/?a$/i.test(c3);
61629
+ }
61559
61630
  function hasPopulatedPriorityTable(body) {
61560
61631
  for (const line of body.split(`
61561
61632
  `)) {
@@ -61565,7 +61636,7 @@ function hasPopulatedPriorityTable(body) {
61565
61636
  const severityIdx = cells.findIndex((c3) => /^\s*P[1-4]\s*$/.test(c3));
61566
61637
  if (severityIdx === -1)
61567
61638
  continue;
61568
- const hasContent = cells.some((c3, i2) => i2 !== severityIdx && c3.trim().length > 0);
61639
+ const hasContent = cells.some((c3, i2) => i2 !== severityIdx && !isPlaceholderCell(c3));
61569
61640
  if (hasContent)
61570
61641
  return true;
61571
61642
  }
@@ -61587,7 +61658,7 @@ function isReviewScaffold(body) {
61587
61658
  continue;
61588
61659
  const cells = line.split("|").map((c3) => c3.trim());
61589
61660
  const isSeparator = cells.every((c3) => c3 === "" || /^:?-+:?$/.test(c3));
61590
- const isEmptyPRow = cells.every((c3) => c3 === "" || /^P[1-4]$/.test(c3)) && cells.some((c3) => /^P[1-4]$/.test(c3));
61661
+ const isEmptyPRow = cells.every((c3) => isPlaceholderCell(c3) || /^P[1-4]$/.test(c3)) && cells.some((c3) => /^P[1-4]$/.test(c3));
61591
61662
  const isHeader = cells.some((c3) => /severity|file|finding|recommendation/i.test(c3));
61592
61663
  if (isEmptyPRow)
61593
61664
  sawEmptyPRow = true;
@@ -63817,55 +63888,46 @@ class AgentRunActionRunner {
63817
63888
  const expectFile = asOptionalString(options.expectFile);
63818
63889
  const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
63819
63890
  const agentLabel = agent ?? "<default>";
63820
- if (capture) {
63821
- const captured = await this.agentService.runCapture(input, flags);
63822
- const { exitCode: exitCode2, answer } = captured;
63823
- const ok2 = exitCode2 === 0;
63824
- if (answerFile !== undefined) {
63825
- const target = isAbsolute2(answerFile) ? answerFile : join12(cwd, answerFile);
63826
- await mkdir(dirname10(target), { recursive: true });
63827
- await writeFile(target, answer, "utf8");
63828
- }
63829
- if (ok2 && expectFile !== undefined) {
63830
- const target = isAbsolute2(expectFile) ? expectFile : join12(cwd, expectFile);
63831
- if (!existsSync7(target)) {
63832
- return {
63833
- ok: false,
63834
- data: { exitCode: exitCode2, agent: agentLabel, answer },
63835
- error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
63836
- };
63837
- }
63838
- }
63839
- if (!ok2) {
63840
- await writePartialWorkArtifact(context4, agentLabel, model, captured, cwd);
63841
- }
63842
- return {
63843
- ok: ok2,
63844
- data: { exitCode: exitCode2, agent: agentLabel, answer },
63845
- error: ok2 ? undefined : `agent.run (${agentLabel}) exited with code ${exitCode2}`,
63846
- setVars: ok2 ? { __agentSession: "open" } : undefined
63847
- };
63848
- }
63849
- const exitCode = await this.agentService.run(input, flags);
63891
+ const traced = await this.agentService.runTraced(input, flags);
63892
+ const { exitCode, stdout: answer } = traced;
63850
63893
  const ok = exitCode === 0;
63894
+ const invocation = traced.invocation;
63895
+ if (capture && answerFile !== undefined) {
63896
+ const target = isAbsolute2(answerFile) ? answerFile : join12(cwd, answerFile);
63897
+ await mkdir(dirname10(target), { recursive: true });
63898
+ await writeFile(target, answer, "utf8");
63899
+ }
63851
63900
  if (ok && expectFile !== undefined) {
63852
63901
  const target = isAbsolute2(expectFile) ? expectFile : join12(cwd, expectFile);
63853
63902
  if (!existsSync7(target)) {
63854
63903
  return {
63855
63904
  ok: false,
63856
- data: { exitCode, agent: agentLabel },
63905
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
63857
63906
  error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
63858
63907
  };
63859
63908
  }
63860
63909
  }
63910
+ if (!ok) {
63911
+ await writePartialWorkArtifact(context4, agentLabel, model, traced, cwd);
63912
+ }
63913
+ const stepLabel = context4.stateOrNodeId;
63914
+ 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}`;
63861
63915
  return {
63862
63916
  ok,
63863
- data: { exitCode, agent: agentLabel },
63864
- error: ok ? undefined : `agent.run (${agentLabel}) exited with code ${exitCode}`,
63917
+ data: buildResultData(exitCode, agentLabel, capture, answer, invocation),
63918
+ error: error51,
63865
63919
  setVars: ok ? { __agentSession: "open" } : undefined
63866
63920
  };
63867
63921
  }
63868
63922
  }
63923
+ function buildResultData(exitCode, agentLabel, capture, answer, invocation) {
63924
+ const data = { exitCode, agent: agentLabel };
63925
+ if (capture)
63926
+ data.answer = answer;
63927
+ if (invocation !== undefined)
63928
+ data.invocation = invocation;
63929
+ return data;
63930
+ }
63869
63931
  function asOptionalString(value) {
63870
63932
  if (value === undefined || value === null)
63871
63933
  return;
@@ -63893,13 +63955,16 @@ function asOptionalNumber(value) {
63893
63955
  }
63894
63956
  return;
63895
63957
  }
63896
- async function writePartialWorkArtifact(context4, agentLabel, model, captured, cwd) {
63958
+ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd) {
63897
63959
  try {
63898
- const exitReason = captured.signal !== undefined ? `killed by signal ${captured.signal} (likely timeout)` : `exited with code ${captured.exitCode}`;
63960
+ const signal = traced.signal;
63961
+ const exitReason = signal !== undefined ? `killed by signal ${signal} (likely timeout or cancellation)` : traced.message !== undefined ? `dispatch error: ${traced.message}` : `exited with code ${traced.exitCode}`;
63899
63962
  const diffStat = await gitDiffStat(cwd);
63900
- const stdoutTail = tail(captured.answer, PARTIAL_ARTIFACT_TAIL_CHARS);
63901
- const stderrTail = tail(captured.stderr ?? "", PARTIAL_ARTIFACT_TAIL_CHARS);
63963
+ const stdoutTail = tail(traced.stdout, PARTIAL_ARTIFACT_TAIL_CHARS);
63964
+ const stderrTail = tail(traced.stderr ?? "", PARTIAL_ARTIFACT_TAIL_CHARS);
63902
63965
  const headerLine = model !== undefined ? `${agentLabel} (model: ${model})` : agentLabel;
63966
+ const inv = traced.invocation;
63967
+ const argvLine = inv ? `${inv.command} ${inv.argv.join(" ")}` : "(invocation not captured)";
63903
63968
  const body = [
63904
63969
  `# Partial-work handoff \u2014 ${headerLine}`,
63905
63970
  "",
@@ -63908,7 +63973,18 @@ async function writePartialWorkArtifact(context4, agentLabel, model, captured, c
63908
63973
  `- agent: ${agentLabel}`,
63909
63974
  `- model: ${model ?? "(default)"}`,
63910
63975
  `- exit reason: ${exitReason}`,
63911
- `- elapsed: ${captured.durationMs ?? "unknown"}ms`,
63976
+ `- elapsed: ${traced.durationMs ?? "unknown"}ms`,
63977
+ "",
63978
+ "## resolved invocation",
63979
+ "",
63980
+ `- command: ${argvLine}`,
63981
+ `- cwd: ${inv?.cwd ?? "(inherit)"}`,
63982
+ `- mode: ${inv?.mode ?? "unknown"}`,
63983
+ `- timeoutMs: ${inv?.timeoutMs ?? "(none)"}`,
63984
+ `- continue: ${inv?.continue ?? false}`,
63985
+ `- output: ${inv?.outputMode ?? "unknown"}`,
63986
+ `- stdinInteractive: ${inv?.stdinInteractive ?? false}`,
63987
+ `- translatedFrom: ${inv?.translatedFrom ?? "(none)"}`,
63912
63988
  "",
63913
63989
  "## git diff --stat",
63914
63990
  "```",
@@ -73234,7 +73310,7 @@ import { join as join15, resolve as resolve5 } from "path";
73234
73310
  var CLI_CONFIG = {
73235
73311
  binaryName: "spur",
73236
73312
  binaryLabel: "spur",
73237
- binaryVersion: "0.3.14",
73313
+ binaryVersion: "0.3.15",
73238
73314
  configDir: ".spur",
73239
73315
  configFile: ".spur/config.yaml",
73240
73316
  databaseFile: ".spur/spur.db"