sdd-forge 0.1.0-alpha.622 → 0.1.0-alpha.650

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-forge",
3
- "version": "0.1.0-alpha.622",
3
+ "version": "0.1.0-alpha.650",
4
4
  "description": "Spec-Driven Development tooling for automated documentation generation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@ import path from "path";
12
12
  import { runIfDirect } from "../../lib/entrypoint.js";
13
13
  import { parseArgs } from "../../lib/cli.js";
14
14
  import { sddOutputDir } from "../../lib/config.js";
15
- import { callAgent, loadAgentConfig } from "../../lib/agent.js";
15
+ import { callAgentAwaitLog, loadAgentConfig } from "../../lib/agent.js";
16
16
  import { translate } from "../../lib/i18n.js";
17
17
  import { createResolver } from "../lib/resolver-factory.js";
18
18
  import { createLogger } from "../../lib/progress.js";
@@ -201,7 +201,7 @@ async function main(ctx) {
201
201
  const prompt = buildRefinePrompt(projectContent, combinedDocs, config, srcRoot, sddContent);
202
202
 
203
203
  try {
204
- const result = callAgent(agent, prompt, agent.timeoutMs, undefined, { systemPrompt });
204
+ const result = await callAgentAwaitLog(agent, prompt, agent.timeoutMs, undefined, { systemPrompt });
205
205
 
206
206
  let refined = result.trim();
207
207
 
@@ -14,7 +14,7 @@ import path from "path";
14
14
  import { runIfDirect } from "../../lib/entrypoint.js";
15
15
  import { parseArgs } from "../../lib/cli.js";
16
16
  import { sddOutputDir, resolveConcurrency } from "../../lib/config.js";
17
- import { resolveAgent, callAgentAsync, DEFAULT_AGENT_TIMEOUT_MS, resolveWorkDir } from "../../lib/agent.js";
17
+ import { resolveAgent, callAgentAsyncWithLog, DEFAULT_AGENT_TIMEOUT_MS, resolveWorkDir } from "../../lib/agent.js";
18
18
  import { minify } from "../lib/minify.js";
19
19
  import { mapWithConcurrency } from "../lib/concurrency.js";
20
20
  import { resolveCommandContext, loadFullAnalysis } from "../lib/command-context.js";
@@ -454,7 +454,7 @@ async function main(ctx) {
454
454
 
455
455
  let response;
456
456
  try {
457
- response = await callAgentAsync(agent, prompt, timeoutMs, root, {
457
+ response = await callAgentAsyncWithLog(agent, prompt, timeoutMs, root, {
458
458
  retryCount,
459
459
  });
460
460
  } catch (err) {
@@ -22,7 +22,7 @@ import { PKG_DIR, repoRoot, parseArgs } from "../../lib/cli.js";
22
22
  import { loadConfig, resolveConcurrency } from "../../lib/config.js";
23
23
  import { loadFullAnalysis, loadAnalysisData, getChapterFiles, readText } from "../lib/command-context.js";
24
24
  import { createResolver } from "../lib/resolver-factory.js";
25
- import { callAgentAsync, DEFAULT_AGENT_TIMEOUT_MS, resolveAgent } from "../../lib/agent.js";
25
+ import { callAgentAsyncWithLog, DEFAULT_AGENT_TIMEOUT_MS, resolveAgent } from "../../lib/agent.js";
26
26
  import { translate } from "../../lib/i18n.js";
27
27
  import { EXIT_ERROR } from "../../lib/exit-codes.js";
28
28
  import {
@@ -144,7 +144,7 @@ async function invokeAgent(agent, prompt, { cwd, timeoutMs, systemPrompt, verbos
144
144
  : null;
145
145
 
146
146
  try {
147
- return await callAgentAsync(agent, prompt, timeout, cwd, {
147
+ return await callAgentAsyncWithLog(agent, prompt, timeout, cwd, {
148
148
  systemPrompt,
149
149
  onStdout: verbose ? (chunk) => process.stderr.write(chunk) : undefined,
150
150
  onStderr: verbose ? (chunk) => process.stderr.write(chunk) : undefined,
@@ -13,7 +13,7 @@ import path from "path";
13
13
  import { runIfDirect } from "../../lib/entrypoint.js";
14
14
  import { repoRoot, parseArgs } from "../../lib/cli.js";
15
15
  import { loadPackageField } from "../../lib/config.js";
16
- import { callAgent } from "../../lib/agent.js";
16
+ import { callAgentWithLog } from "../../lib/agent.js";
17
17
  import { resolveTemplates, mergeResolved, resolveChaptersOrder, translateTemplate } from "../lib/template-merger.js";
18
18
  import { summaryToText } from "../lib/forge-prompts.js";
19
19
  import { createLogger } from "../../lib/progress.js";
@@ -81,7 +81,7 @@ function aiFilterChapters(chapters, analysis, agent, root, purpose) {
81
81
 
82
82
  let response;
83
83
  try {
84
- response = callAgent(agent, prompt, 60000, root);
84
+ response = callAgentWithLog(agent, prompt, 60000, root);
85
85
  } catch (err) {
86
86
  logger.log(`[init] WARN: AI chapter selection failed: ${err.message}`);
87
87
  return chapters;
@@ -28,7 +28,7 @@ import {
28
28
  import { repoRoot, parseArgs } from "../../lib/cli.js";
29
29
  import { loadConfig, resolveConcurrency, DEFAULT_CONCURRENCY } from "../../lib/config.js";
30
30
  import { createLogger } from "../../lib/progress.js";
31
- import { callAgent as callAgentBase, callAgentAsync as callAgentAsyncBase, ensureAgentWorkDir, loadAgentConfig, DEFAULT_AGENT_TIMEOUT_MS } from "../../lib/agent.js";
31
+ import { callAgentWithLog, callAgentAsyncWithLog, ensureAgentWorkDir, loadAgentConfig, DEFAULT_AGENT_TIMEOUT_MS } from "../../lib/agent.js";
32
32
  import { translate } from "../../lib/i18n.js";
33
33
  import { resolveCommandContext, getChapterFiles, loadFullAnalysis } from "../lib/command-context.js";
34
34
  import { repairJson } from "../../lib/json-parse.js";
@@ -263,12 +263,12 @@ async function processTemplateFileBatch(text, analysis, fileName, agent, timeout
263
263
  // エージェント呼び出し
264
264
  // ---------------------------------------------------------------------------
265
265
  function callAgent(agent, prompt, timeoutMs, cwd, preamblePatterns, systemPrompt) {
266
- const result = callAgentBase(agent, prompt, timeoutMs, cwd, { systemPrompt });
266
+ const result = callAgentWithLog(agent, prompt, timeoutMs, cwd, { systemPrompt });
267
267
  return stripPreamble(result, preamblePatterns);
268
268
  }
269
269
 
270
270
  async function callAgentAsync(agent, prompt, timeoutMs, cwd, preamblePatterns, systemPrompt, extraOptions) {
271
- const result = await callAgentAsyncBase(agent, prompt, timeoutMs, cwd, { systemPrompt, ...extraOptions });
271
+ const result = await callAgentAsyncWithLog(agent, prompt, timeoutMs, cwd, { systemPrompt, ...extraOptions });
272
272
  return stripPreamble(result, preamblePatterns);
273
273
  }
274
274
 
@@ -15,7 +15,7 @@ import { runIfDirect } from "../../lib/entrypoint.js";
15
15
  import { parseArgs } from "../../lib/cli.js";
16
16
  import { resolveOutputConfig } from "../../lib/types.js";
17
17
  import { resolveConcurrency } from "../../lib/config.js";
18
- import { callAgentAsync } from "../../lib/agent.js";
18
+ import { callAgentAsyncWithLog } from "../../lib/agent.js";
19
19
  import { createLogger } from "../../lib/progress.js";
20
20
  import { resolveCommandContext, getChapterFiles, stripResponsePreamble } from "../lib/command-context.js";
21
21
  import { mapWithConcurrency } from "../lib/concurrency.js";
@@ -71,7 +71,7 @@ async function translateDocument(content, fromLang, toLang, agent, root, documen
71
71
 
72
72
  const prompt = content;
73
73
 
74
- const result = await callAgentAsync(agent, prompt, agent.timeoutMs, root, {
74
+ const result = await callAgentAsyncWithLog(agent, prompt, agent.timeoutMs, root, {
75
75
  systemPrompt,
76
76
  });
77
77
 
@@ -13,7 +13,7 @@ import fs from "fs";
13
13
  import path from "path";
14
14
  import { parseBlocks, BLOCK_START_RE, BLOCK_END_RE } from "./directive-parser.js";
15
15
  import { resolveChainSafe, resolveMultiChains } from "../../lib/presets.js";
16
- import { callAgent } from "../../lib/agent.js";
16
+ import { callAgentWithLog } from "../../lib/agent.js";
17
17
 
18
18
  const SPECIAL_FILES = new Set(["README.md", "AGENTS.sdd.md", "layout.md"]);
19
19
 
@@ -430,7 +430,7 @@ export function translateTemplate(content, fromLang, toLang, agent, root) {
430
430
  ].join("\n");
431
431
 
432
432
  try {
433
- return callAgent(agent, prompt, 60000, root);
433
+ return callAgentWithLog(agent, prompt, 60000, root);
434
434
  } catch (err) {
435
435
  // Translation failed — return original
436
436
  return content;
@@ -14,8 +14,16 @@ import path from "path";
14
14
  import { runIfDirect } from "../../lib/entrypoint.js";
15
15
  import { repoRoot, parseArgs } from "../../lib/cli.js";
16
16
  import { loadConfig } from "../../lib/config.js";
17
- import { loadFlowState } from "../../lib/flow-state.js";
18
- import { loadAgentConfig, callAgent, resolveAgent, ensureAgentWorkDir } from "../../lib/agent.js";
17
+ import { loadFlowState, getSpecName } from "../../lib/flow-state.js";
18
+ import { loadAgentConfig, callAgentAwaitLog, resolveAgent, ensureAgentWorkDir } from "../../lib/agent.js";
19
+
20
+ /**
21
+ * Local helper for review-phase agent invocations. All review calls use the
22
+ * same default timeout and pass `root` as cwd, so this collapses the repeated
23
+ * `(agent, prompt, undefined, root, { systemPrompt })` boilerplate.
24
+ */
25
+ const callReviewAgent = (agent, prompt, root, systemPrompt) =>
26
+ callAgentAwaitLog(agent, prompt, undefined, root, { systemPrompt });
19
27
  import { runCmd } from "../../lib/process.js";
20
28
  import { EXIT_ERROR } from "../../lib/exit-codes.js";
21
29
  import { VALID_PHASES } from "../lib/phases.js";
@@ -496,12 +504,10 @@ async function runTestReview(root, flow, config, dryRun) {
496
504
 
497
505
  // Step 1: Generate test design
498
506
  console.error(" [test-review] Generating test design...");
499
- const testDesign = await callAgent(
500
- agent,
501
- buildTestDesignPrompt(requirements),
502
- undefined,
503
- root,
504
- { systemPrompt: "You are a test design expert. Output a structured test design." },
507
+ const testDesignPrompt = buildTestDesignPrompt(requirements);
508
+ const testDesign = await callReviewAgent(
509
+ agent, testDesignPrompt, root,
510
+ "You are a test design expert. Output a structured test design.",
505
511
  );
506
512
  // Save test design as tests/spec.md
507
513
  const testsDir = path.resolve(root, specDir, "tests");
@@ -518,22 +524,18 @@ async function runTestReview(root, flow, config, dryRun) {
518
524
  label: "test-review",
519
525
  dryRun,
520
526
  async detect() {
521
- const raw = await callAgent(
522
- agent,
523
- buildGapAnalysisPrompt(testDesign, testFiles),
524
- undefined,
525
- root,
526
- { systemPrompt: "You are a test quality reviewer. Identify gaps between test design and test code." },
527
+ const detectPrompt = buildGapAnalysisPrompt(testDesign, testFiles);
528
+ const raw = await callReviewAgent(
529
+ agent, detectPrompt, root,
530
+ "You are a test quality reviewer. Identify gaps between test design and test code.",
527
531
  );
528
532
  return { issues: parseGaps(raw), raw };
529
533
  },
530
534
  async fix(raw) {
531
- const fixResult = await callAgent(
532
- agent,
533
- buildTestFixPrompt(raw, testFiles),
534
- undefined,
535
- root,
536
- { systemPrompt: "You are a test engineer. Fix test gaps by writing complete updated test files." },
535
+ const fixPrompt = buildTestFixPrompt(raw, testFiles);
536
+ const fixResult = await callReviewAgent(
537
+ agent, fixPrompt, root,
538
+ "You are a test engineer. Fix test gaps by writing complete updated test files.",
537
539
  );
538
540
  const written = applyTestFixes(fixResult, root);
539
541
  if (written.length > 0) {
@@ -700,12 +702,10 @@ async function runSpecReview(root, flow, config, dryRun) {
700
702
  contextEntries = analysisData.ctxSearch(analysisData.entries, analysisData.analysis, searchQuery, root);
701
703
  }
702
704
  }
703
- const raw = await callAgent(
704
- agent,
705
- buildSpecReviewPrompt(specText, contextEntries),
706
- undefined,
707
- root,
708
- { systemPrompt: "You are a spec completeness reviewer. Identify oversights in the spec." },
705
+ const detectPrompt = buildSpecReviewPrompt(specText, contextEntries);
706
+ const raw = await callReviewAgent(
707
+ agent, detectPrompt, root,
708
+ "You are a spec completeness reviewer. Identify oversights in the spec.",
709
709
  );
710
710
  if (raw.includes("NO_PROPOSALS")) return { issues: [], raw };
711
711
  const issues = parseProposals(raw);
@@ -714,19 +714,16 @@ async function runSpecReview(root, flow, config, dryRun) {
714
714
  async fix(raw) {
715
715
  const specText = fs.readFileSync(specPath, "utf8");
716
716
  const proposals = parseProposals(raw);
717
- const validationResult = await callAgent(
718
- validationAgent,
719
- [
720
- "Validate these spec improvement proposals:",
721
- "",
722
- raw,
723
- "",
724
- "## Current spec for context:",
725
- specText,
726
- ].join("\n"),
727
- undefined,
728
- root,
729
- { systemPrompt: buildFinalSystemPrompt() },
717
+ const validationPrompt = [
718
+ "Validate these spec improvement proposals:",
719
+ "",
720
+ raw,
721
+ "",
722
+ "## Current spec for context:",
723
+ specText,
724
+ ].join("\n");
725
+ const validationResult = await callReviewAgent(
726
+ validationAgent, validationPrompt, root, buildFinalSystemPrompt(),
730
727
  );
731
728
  const results = mergeVerdicts(validationResult, proposals);
732
729
  const approved = results.filter((r) => r.verdict === "APPROVED");
@@ -739,12 +736,10 @@ async function runSpecReview(root, flow, config, dryRun) {
739
736
  console.error(` [spec-review] ${approved.length} proposal(s) approved. Applying to spec...`);
740
737
 
741
738
  const approvedText = approved.map((p, i) => `### ${i + 1}. ${p.title}\n${p.body}`).join("\n\n");
742
- const fixResult = await callAgent(
743
- agent,
744
- buildSpecFixPrompt(specText, approvedText),
745
- undefined,
746
- root,
747
- { systemPrompt: "You are a spec writer. Apply the approved proposals to produce an updated spec." },
739
+ const specFixPrompt = buildSpecFixPrompt(specText, approvedText);
740
+ const fixResult = await callReviewAgent(
741
+ agent, specFixPrompt, root,
742
+ "You are a spec writer. Apply the approved proposals to produce an updated spec.",
748
743
  );
749
744
 
750
745
  // Strip preamble and markdown fences from AI output
@@ -836,13 +831,7 @@ async function main() {
836
831
  console.error(" [draft] Generating proposals...");
837
832
  const draftAgent = loadAgentConfig(config, "flow.review.draft");
838
833
  ensureAgentWorkDir(draftAgent, root);
839
- const draftResult = await callAgent(
840
- draftAgent,
841
- diff,
842
- undefined,
843
- root,
844
- { systemPrompt: buildDraftSystemPrompt() },
845
- );
834
+ const draftResult = await callReviewAgent(draftAgent, diff, root, buildDraftSystemPrompt());
846
835
 
847
836
  if (draftResult.includes("NO_PROPOSALS")) {
848
837
  console.log("No improvement proposals found. Code looks good.");
@@ -874,13 +863,7 @@ async function main() {
874
863
  diff,
875
864
  ].join("\n");
876
865
 
877
- const finalResult = await callAgent(
878
- finalAgent,
879
- finalPrompt,
880
- undefined,
881
- root,
882
- { systemPrompt: buildFinalSystemPrompt() },
883
- );
866
+ const finalResult = await callReviewAgent(finalAgent, finalPrompt, root, buildFinalSystemPrompt());
884
867
 
885
868
  const results = mergeVerdicts(finalResult, proposals);
886
869
  const approved = results.filter((r) => r.verdict === "APPROVED");
@@ -15,7 +15,7 @@ import path from "path";
15
15
  import { sddOutputDir, loadConfig } from "../../lib/config.js";
16
16
  import { FlowCommand } from "./base-command.js";
17
17
  import { ANALYSIS_META_KEYS } from "../../docs/lib/analysis-entry.js";
18
- import { resolveAgent, callAgent } from "../../lib/agent.js";
18
+ import { resolveAgent, callAgentWithLog } from "../../lib/agent.js";
19
19
 
20
20
  const EXCLUDE_FIELDS = new Set(["hash", "mtime", "lines", "id", "enrich", "detail"]);
21
21
 
@@ -221,7 +221,7 @@ function aiSearch(allEntries, analysis, query, root) {
221
221
  const prompt = buildKeywordSelectionPrompt(allKeywords, query);
222
222
  let response;
223
223
  try {
224
- response = callAgent(agent, prompt, 30000, root);
224
+ response = callAgentWithLog(agent, prompt, 30000, root);
225
225
  } catch (_) {
226
226
  return fallbackSearch(allEntries, query);
227
227
  }
@@ -27,9 +27,7 @@ import { FLOW_COMMANDS } from "../registry.js";
27
27
  export function finalizeOnError(stepName, trigger) {
28
28
  return (ctx, err) => {
29
29
  try {
30
- // Use mainRoot in worktree mode so issue-log persists after cleanup
31
- const logRoot = ctx.mainRoot || ctx.root;
32
- const issueLog = loadIssueLog(logRoot, ctx.flowState.spec);
30
+ const issueLog = loadIssueLog(ctx.root, ctx.flowState.spec);
33
31
  const entry = {
34
32
  step: stepName,
35
33
  reason: err.message || String(err),
@@ -37,7 +35,7 @@ export function finalizeOnError(stepName, trigger) {
37
35
  };
38
36
  if (trigger) entry.trigger = trigger;
39
37
  issueLog.entries.push(entry);
40
- saveIssueLog(logRoot, ctx.flowState.spec, issueLog);
38
+ saveIssueLog(ctx.root, ctx.flowState.spec, issueLog);
41
39
  } catch (e) { console.error("[issue-log hook]", e.message); }
42
40
  };
43
41
  }
@@ -13,8 +13,9 @@
13
13
  import fs from "fs";
14
14
  import path from "path";
15
15
  import { runCmd } from "../../lib/process.js";
16
- import { callAgent, resolveAgent } from "../../lib/agent.js";
16
+ import { callAgentWithLog, resolveAgent } from "../../lib/agent.js";
17
17
  import { filterByPhase, loadMergedGuardrails } from "../../lib/guardrail.js";
18
+ import { getSpecName } from "../../lib/flow-state.js";
18
19
  import { FlowCommand } from "./base-command.js";
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -225,7 +226,7 @@ function checkGuardrail(root, targetText, config, phase, role) {
225
226
  const prompt = buildGuardrailPrompt(targetText, guardrails, phase, role);
226
227
  if (!prompt) return { passed: true, results: [] };
227
228
 
228
- const response = callAgent(agent, prompt);
229
+ const response = callAgentWithLog(agent, prompt);
229
230
  const results = parseGuardrailResponse(response);
230
231
  const passed = results.length > 0 && results.every((r) => r.passed);
231
232
 
@@ -426,7 +427,7 @@ export class RunGateCommand extends FlowCommand {
426
427
  if (!agent) throw new Error("no agent configured for spec.gate");
427
428
 
428
429
  const reqPrompt = buildImplCheckPrompt(specText, diff);
429
- const reqResponse = callAgent(agent, reqPrompt);
430
+ const reqResponse = callAgentWithLog(agent, reqPrompt);
430
431
  const reqResults = parseGuardrailResponse(reqResponse);
431
432
 
432
433
  const reasons = reqResults.map((r) => ({
@@ -8,8 +8,9 @@
8
8
  import fs from "fs";
9
9
  import path from "path";
10
10
  import { runCmd } from "../../lib/process.js";
11
- import { callAgent, resolveAgent } from "../../lib/agent.js";
11
+ import { callAgentAwaitLog, resolveAgent } from "../../lib/agent.js";
12
12
  import { repairJson } from "../../lib/json-parse.js";
13
+ import { getSpecName } from "../../lib/flow-state.js";
13
14
  import { FlowCommand } from "./base-command.js";
14
15
 
15
16
  /**
@@ -192,7 +193,7 @@ export class RunRetroCommand extends FlowCommand {
192
193
 
193
194
  let response;
194
195
  try {
195
- response = callAgent(agent, prompt);
196
+ response = await callAgentAwaitLog(agent, prompt);
196
197
  } catch (e) {
197
198
  throw new Error(`AI agent call failed: ${e.message}`);
198
199
  }
@@ -270,8 +270,7 @@ export const FLOW_COMMANDS = {
270
270
  // Auto-record issue-log on gate FAIL
271
271
  if (result?.result !== "pass") {
272
272
  try {
273
- const logRoot = ctx.mainRoot || ctx.root;
274
- const issueLog = loadIssueLog(logRoot, ctx.flowState?.spec);
273
+ const issueLog = loadIssueLog(ctx.root, ctx.flowState?.spec);
275
274
  const reasons = result?.artifacts?.issues?.length
276
275
  ? result.artifacts.issues.join("; ")
277
276
  : (result?.artifacts?.reasons || []).map(r => r.detail || r).join("; ");
@@ -281,21 +280,20 @@ export const FLOW_COMMANDS = {
281
280
  trigger: "gate post hook (auto)",
282
281
  timestamp: new Date().toISOString(),
283
282
  });
284
- saveIssueLog(logRoot, ctx.flowState?.spec, issueLog);
283
+ saveIssueLog(ctx.root, ctx.flowState?.spec, issueLog);
285
284
  } catch (e) { console.error("[gate issue-log hook]", e.message); }
286
285
  }
287
286
  },
288
287
  onError(ctx, err) {
289
288
  try {
290
- const logRoot = ctx.mainRoot || ctx.root;
291
- const issueLog = loadIssueLog(logRoot, ctx.flowState?.spec);
289
+ const issueLog = loadIssueLog(ctx.root, ctx.flowState?.spec);
292
290
  issueLog.entries.push({
293
291
  step: resolveGateStepId(ctx.phase),
294
292
  reason: err.message || String(err),
295
293
  trigger: "gate onError hook (auto)",
296
294
  timestamp: new Date().toISOString(),
297
295
  });
298
- saveIssueLog(logRoot, ctx.flowState?.spec, issueLog);
296
+ saveIssueLog(ctx.root, ctx.flowState?.spec, issueLog);
299
297
  } catch (e) { console.error("[gate issue-log hook]", e.message); }
300
298
  },
301
299
  },
package/src/lib/agent.js CHANGED
@@ -9,6 +9,8 @@ import crypto from "crypto";
9
9
  import fs from "fs";
10
10
  import path from "path";
11
11
  import { execFileSync, spawn } from "child_process";
12
+ import { Logger, generateRequestId } from "./log.js";
13
+
12
14
 
13
15
  /** Default agent timeout in seconds. */
14
16
  export const DEFAULT_AGENT_TIMEOUT = 300;
@@ -109,7 +111,6 @@ export function callAgent(agent, prompt, timeoutMs, cwd, options) {
109
111
  env,
110
112
  ...(stdinContent != null ? { input: stdinContent } : {}),
111
113
  });
112
-
113
114
  return result.trim();
114
115
  }
115
116
 
@@ -134,10 +135,145 @@ export function callAgent(agent, prompt, timeoutMs, cwd, options) {
134
135
  */
135
136
  export function callAgentAsync(agent, prompt, timeoutMs, cwd, options) {
136
137
  const { retryCount = 0, retryDelayMs = 3000, ...restOptions } = options || {};
137
- if (retryCount <= 0) {
138
- return callAgentAsyncOnce(agent, prompt, timeoutMs, cwd, restOptions);
138
+ return retryCount <= 0
139
+ ? callAgentAsyncOnce(agent, prompt, timeoutMs, cwd, restOptions)
140
+ : callAgentAsyncWithRetry(agent, prompt, timeoutMs, cwd, restOptions, retryCount, retryDelayMs);
141
+ }
142
+
143
+ /**
144
+ * Build a Logger.agent() end-event payload from a callAgent invocation.
145
+ */
146
+ function buildAgentLogPayload(agent, prompt, options, response, exitCode, error, startedAt) {
147
+ return {
148
+ agentKey: agent?.command ?? null,
149
+ model: agent?.model ?? null,
150
+ prompt: {
151
+ system: options?.systemPrompt ?? null,
152
+ user: prompt,
153
+ },
154
+ response: {
155
+ text: response,
156
+ exitCode,
157
+ error,
158
+ },
159
+ durationSec: (Date.now() - startedAt) / 1000,
160
+ };
161
+ }
162
+
163
+ const STDERR_LOG = (e) => process.stderr.write(`[sdd-forge] Logger.agent failed: ${e.message}\n`);
164
+
165
+ /**
166
+ * Async runner that wraps an agent invocation in Logger.agent start/end
167
+ * events. Used by both `callAgentAwaitLog` (sync inner call, awaited
168
+ * Logger I/O) and `callAgentAsyncWithLog` (async inner call).
169
+ *
170
+ * The two callers differ only in:
171
+ * - whether the inner invoke is awaited (`asyncInvoke`)
172
+ * - which property of the thrown error holds the exit code (`errCodeKey`)
173
+ *
174
+ * For the truly synchronous variant (`callAgentWithLog`), use
175
+ * `runWithAgentLoggingSync` below — sync mode cannot await Logger I/O,
176
+ * so its control flow differs enough to keep it as a separate small helper.
177
+ *
178
+ * @param {Object} spec
179
+ * @param {boolean} spec.asyncInvoke — true if invoke returns a Promise
180
+ * @param {()=>any} spec.invoke
181
+ * @param {Object} spec.agent
182
+ * @param {string} spec.prompt
183
+ * @param {Object} [spec.options]
184
+ * @param {string} spec.errCodeKey — "status" (execFileSync) or "code" (spawn)
185
+ */
186
+ async function runWithAgentLogging({ asyncInvoke, invoke, agent, prompt, options, errCodeKey }) {
187
+ const requestId = generateRequestId();
188
+ const startedAt = Date.now();
189
+ const logger = Logger.getInstance();
190
+ await logger.agent({ phase: "start", requestId });
191
+
192
+ let result = null;
193
+ let err = null;
194
+ try {
195
+ result = asyncInvoke ? await invoke() : invoke();
196
+ return result;
197
+ } catch (e) {
198
+ err = e;
199
+ throw e;
200
+ } finally {
201
+ const payload = buildAgentLogPayload(
202
+ agent,
203
+ prompt,
204
+ options,
205
+ result,
206
+ err ? (err[errCodeKey] ?? 1) : 0,
207
+ err ? err.message : null,
208
+ startedAt,
209
+ );
210
+ await logger.agent({ phase: "end", requestId, ...payload });
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Sync callAgent wrapped with Logger.agent start/end events.
216
+ * End-event Logger.log() is fire-and-forget.
217
+ */
218
+ export function callAgentWithLog(agent, prompt, timeoutMs, cwd, options) {
219
+ return runWithAgentLoggingSync({
220
+ invoke: () => callAgent(agent, prompt, timeoutMs, cwd, options),
221
+ agent, prompt, options,
222
+ errCodeKey: "status",
223
+ });
224
+ }
225
+
226
+ /** Sync variant — runs invoke synchronously, fires Logger calls without awaiting. */
227
+ function runWithAgentLoggingSync({ invoke, agent, prompt, options, errCodeKey }) {
228
+ const requestId = generateRequestId();
229
+ const startedAt = Date.now();
230
+ const logger = Logger.getInstance();
231
+ logger.agent({ phase: "start", requestId }).catch(STDERR_LOG);
232
+ let result = null;
233
+ let err = null;
234
+ try {
235
+ result = invoke();
236
+ return result;
237
+ } catch (e) {
238
+ err = e;
239
+ throw e;
240
+ } finally {
241
+ const payload = buildAgentLogPayload(
242
+ agent,
243
+ prompt,
244
+ options,
245
+ result,
246
+ err ? (err[errCodeKey] ?? 1) : 0,
247
+ err ? err.message : null,
248
+ startedAt,
249
+ );
250
+ logger.agent({ phase: "end", requestId, ...payload }).catch(STDERR_LOG);
139
251
  }
140
- return callAgentAsyncWithRetry(agent, prompt, timeoutMs, cwd, restOptions, retryCount, retryDelayMs);
252
+ }
253
+
254
+ /**
255
+ * Sync callAgent wrapped with Logger.agent start/end events, awaited.
256
+ * Use in async function contexts where the existing code uses sync callAgent.
257
+ */
258
+ export async function callAgentAwaitLog(agent, prompt, timeoutMs, cwd, options) {
259
+ return runWithAgentLogging({
260
+ asyncInvoke: false,
261
+ invoke: () => callAgent(agent, prompt, timeoutMs, cwd, options),
262
+ agent, prompt, options,
263
+ errCodeKey: "status",
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Async callAgentAsync (spawn-based) wrapped with Logger.agent start/end events.
269
+ */
270
+ export async function callAgentAsyncWithLog(agent, prompt, timeoutMs, cwd, options) {
271
+ return runWithAgentLogging({
272
+ asyncInvoke: true,
273
+ invoke: () => callAgentAsync(agent, prompt, timeoutMs, cwd, options),
274
+ agent, prompt, options,
275
+ errCodeKey: "code",
276
+ });
141
277
  }
142
278
 
143
279
  async function callAgentAsyncWithRetry(agent, prompt, timeoutMs, cwd, options, retryCount, retryDelayMs) {
@@ -15,6 +15,18 @@ import { isInsideWorktree, getMainRepoPath } from "./cli.js";
15
15
  const STATE_FILE = "flow.json";
16
16
  const ACTIVE_FLOW_FILE = ".active-flow";
17
17
 
18
+ /**
19
+ * Extract the spec name (e.g. "152-add-logger-to-callsites") from a flow object or state.
20
+ * Both `flow.spec` and `state.spec` hold a relative path like "specs/152-.../spec.md".
21
+ *
22
+ * @param {{ spec?: string }|null|undefined} flowOrState
23
+ * @returns {string|null}
24
+ */
25
+ export function getSpecName(flowOrState) {
26
+ if (!flowOrState?.spec) return null;
27
+ return path.basename(path.dirname(flowOrState.spec));
28
+ }
29
+
18
30
  /** SDD workflow step IDs in order. */
19
31
  export const FLOW_STEPS = [
20
32
  "approach", "branch", "prepare-spec", "draft", "gate-draft", "spec",