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 +1 -1
- package/src/docs/commands/agents.js +2 -2
- package/src/docs/commands/enrich.js +2 -2
- package/src/docs/commands/forge.js +2 -2
- package/src/docs/commands/init.js +2 -2
- package/src/docs/commands/text.js +3 -3
- package/src/docs/commands/translate.js +2 -2
- package/src/docs/lib/template-merger.js +2 -2
- package/src/flow/commands/review.js +42 -59
- package/src/flow/lib/get-context.js +2 -2
- package/src/flow/lib/run-finalize.js +2 -4
- package/src/flow/lib/run-gate.js +4 -3
- package/src/flow/lib/run-retro.js +3 -2
- package/src/flow/registry.js +4 -6
- package/src/lib/agent.js +140 -4
- package/src/lib/flow-state.js +12 -0
- package/src/lib/log.js +410 -0
- package/src/lib/skills.js +49 -10
- package/src/lib/types.js +49 -0
- package/src/presets/base/templates/en/guardrail.json +96 -0
- package/src/presets/base/templates/ja/guardrail.json +96 -0
- package/src/sdd-forge.js +15 -1
- package/src/templates/skills/sdd-forge.flow-impl/SKILL.md +7 -12
- package/src/templates/skills/sdd-forge.flow-plan/SKILL.md +12 -42
- package/src/upgrade.js +16 -1
package/package.json
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
38
|
+
saveIssueLog(ctx.root, ctx.flowState.spec, issueLog);
|
|
41
39
|
} catch (e) { console.error("[issue-log hook]", e.message); }
|
|
42
40
|
};
|
|
43
41
|
}
|
package/src/flow/lib/run-gate.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
196
|
+
response = await callAgentAwaitLog(agent, prompt);
|
|
196
197
|
} catch (e) {
|
|
197
198
|
throw new Error(`AI agent call failed: ${e.message}`);
|
|
198
199
|
}
|
package/src/flow/registry.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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) {
|
package/src/lib/flow-state.js
CHANGED
|
@@ -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",
|