sdd-forge 0.1.0-alpha.8 → 0.1.0-alpha.9
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 +23 -34
- package/src/docs/commands/forge.js +236 -55
- package/src/docs/commands/setup.js +2 -0
- package/src/docs/commands/text.js +45 -51
- package/src/docs/commands/upgrade.js +27 -0
- package/src/lib/agent.js +62 -14
- package/src/lib/types.js +2 -0
- package/src/templates/config.example.json +4 -2
- package/src/templates/locale/en/ui.json +2 -1
- package/src/templates/locale/ja/ui.json +2 -1
package/package.json
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from "fs";
|
|
12
12
|
import path from "path";
|
|
13
|
-
import { execFileSync } from "child_process";
|
|
14
13
|
import { fileURLToPath } from "url";
|
|
15
14
|
import { sourceRoot, repoRoot, parseArgs } from "../../lib/cli.js";
|
|
16
15
|
import { loadJsonFile, loadConfig, resolveProjectContext } from "../../lib/config.js";
|
|
16
|
+
import { callAgent } from "../../lib/agent.js";
|
|
17
17
|
|
|
18
18
|
const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
19
|
|
|
@@ -45,45 +45,33 @@ function loadAgentConfig(cfg, agentName) {
|
|
|
45
45
|
return provider;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function callAgent(agent, prompt, timeoutMs) {
|
|
49
|
-
const args = Array.isArray(agent.args) ? [...agent.args] : [];
|
|
50
|
-
const resolvedArgs = args.map((a) =>
|
|
51
|
-
typeof a === "string" ? a.replaceAll("{{PROMPT}}", prompt) : a
|
|
52
|
-
);
|
|
53
|
-
const hasToken = args.some((a) => typeof a === "string" && a.includes("{{PROMPT}}"));
|
|
54
|
-
const finalArgs = hasToken ? resolvedArgs : [...resolvedArgs, prompt];
|
|
55
|
-
|
|
56
|
-
const env = { ...process.env };
|
|
57
|
-
delete env.CLAUDECODE;
|
|
58
|
-
|
|
59
|
-
return execFileSync(agent.command, finalArgs, {
|
|
60
|
-
encoding: "utf8",
|
|
61
|
-
maxBuffer: 20 * 1024 * 1024,
|
|
62
|
-
timeout: timeoutMs,
|
|
63
|
-
env,
|
|
64
|
-
}).trim();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
// ---------------------------------------------------------------------------
|
|
68
49
|
// AI 要約プロンプト構築
|
|
69
50
|
// ---------------------------------------------------------------------------
|
|
70
51
|
|
|
52
|
+
/**
|
|
53
|
+
* agents コマンド用のシステムプロンプトを構築する。
|
|
54
|
+
* 出力ルール(PROJECT タグ形式、構造要件)を含む。
|
|
55
|
+
*/
|
|
56
|
+
function buildAgentsSystemPrompt() {
|
|
57
|
+
return [
|
|
58
|
+
"以下のソースコード解析データ (analysis.json) を要約し、AGENTS.md の Project Context セクションを生成してください。",
|
|
59
|
+
"",
|
|
60
|
+
"## 出力ルール(厳守)",
|
|
61
|
+
"- <!-- PROJECT:START --> と <!-- PROJECT:END --> タグで囲むこと",
|
|
62
|
+
"- 最初の行は `<!-- PROJECT:START — managed by sdd-forge. Do not edit manually. -->` とすること",
|
|
63
|
+
"- 最後の行は `<!-- PROJECT:END -->` とすること",
|
|
64
|
+
"- `## Project Context` の見出しで始めること",
|
|
65
|
+
"- AI エージェントがプロジェクトを理解するのに役立つ情報を構造的にまとめること",
|
|
66
|
+
"- 技術スタック、プロジェクト構造の概要、主要コンポーネント、DB 構成、利用可能なコマンドを含めること",
|
|
67
|
+
"- マークダウンのテーブルやリストを活用して読みやすくすること",
|
|
68
|
+
"- 前置き・メタコメンタリーは含めないこと",
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
71
72
|
function buildSummaryPrompt(analysis, config, srcRoot) {
|
|
72
73
|
const parts = [];
|
|
73
74
|
|
|
74
|
-
parts.push("以下のソースコード解析データ (analysis.json) を要約し、AGENTS.md の Project Context セクションを生成してください。");
|
|
75
|
-
parts.push("");
|
|
76
|
-
parts.push("## 出力ルール(厳守)");
|
|
77
|
-
parts.push("- <!-- PROJECT:START --> と <!-- PROJECT:END --> タグで囲むこと");
|
|
78
|
-
parts.push("- 最初の行は `<!-- PROJECT:START — managed by sdd-forge. Do not edit manually. -->` とすること");
|
|
79
|
-
parts.push("- 最後の行は `<!-- PROJECT:END -->` とすること");
|
|
80
|
-
parts.push("- `## Project Context` の見出しで始めること");
|
|
81
|
-
parts.push("- AI エージェントがプロジェクトを理解するのに役立つ情報を構造的にまとめること");
|
|
82
|
-
parts.push("- 技術スタック、プロジェクト構造の概要、主要コンポーネント、DB 構成、利用可能なコマンドを含めること");
|
|
83
|
-
parts.push("- マークダウンのテーブルやリストを活用して読みやすくすること");
|
|
84
|
-
parts.push("- 前置き・メタコメンタリーは含めないこと");
|
|
85
|
-
parts.push("");
|
|
86
|
-
|
|
87
75
|
// config info
|
|
88
76
|
if (config.type) {
|
|
89
77
|
parts.push(`## プロジェクト設定`);
|
|
@@ -313,10 +301,11 @@ function main() {
|
|
|
313
301
|
}
|
|
314
302
|
|
|
315
303
|
console.error("[agents] generating PROJECT section with AI...");
|
|
304
|
+
const systemPrompt = buildAgentsSystemPrompt();
|
|
316
305
|
const prompt = buildSummaryPrompt(analysis, config, srcRoot);
|
|
317
306
|
|
|
318
307
|
try {
|
|
319
|
-
const result = callAgent(agent, prompt, 180000);
|
|
308
|
+
const result = callAgent(agent, prompt, 180000, undefined, { systemPrompt });
|
|
320
309
|
|
|
321
310
|
// Extract PROJECT section from AI response
|
|
322
311
|
const projectMatch = result.match(/<!-- PROJECT:START[^>]*-->[\s\S]*?<!-- PROJECT:END -->/);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import fs from "fs";
|
|
15
|
+
import os from "os";
|
|
15
16
|
import path from "path";
|
|
16
17
|
import readline from "readline";
|
|
17
18
|
import { execFile, spawn } from "child_process";
|
|
@@ -28,11 +29,12 @@ import { createResolver } from "../lib/resolver-factory.js";
|
|
|
28
29
|
// パッケージディレクトリを保持する
|
|
29
30
|
const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
30
31
|
|
|
31
|
-
const DEFAULT_AGENT_TIMEOUT_MS =
|
|
32
|
+
const DEFAULT_AGENT_TIMEOUT_MS = 300000;
|
|
32
33
|
const DEFAULT_WAIT_LOG_SEC = 1;
|
|
33
34
|
const DEFAULT_MAX_RUNS = 3;
|
|
34
35
|
const DEFAULT_REVIEW_CMD = "sdd-forge review";
|
|
35
36
|
const DEFAULT_MODE = "local";
|
|
37
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
36
38
|
|
|
37
39
|
function getTargetFiles(root) {
|
|
38
40
|
const docsDir = path.join(root, "docs");
|
|
@@ -152,8 +154,8 @@ function printHelp() {
|
|
|
152
154
|
" --prompt <text> 開始プロンプト",
|
|
153
155
|
" --prompt-file <path> 開始プロンプトファイル",
|
|
154
156
|
" --spec <path> 入力仕様書(spec.md)",
|
|
155
|
-
" --max-runs <n> 反復回数 (default:
|
|
156
|
-
" --review-cmd <cmd> docs レビューコマンド (default:
|
|
157
|
+
" --max-runs <n> 反復回数 (default: 3)",
|
|
158
|
+
" --review-cmd <cmd> docs レビューコマンド (default: sdd-forge review)",
|
|
157
159
|
" --agent <name> AIエージェント: codex|claude (default: config.json の defaultAgent)",
|
|
158
160
|
" --mode <mode> 実行モード: local|assist|agent (default: local)",
|
|
159
161
|
" --dry-run ファイル書き込み・review・agent 呼び出しをスキップ(1 ラウンドで終了)",
|
|
@@ -161,27 +163,59 @@ function printHelp() {
|
|
|
161
163
|
" -v, --verbose エージェント実行ログを逐次表示",
|
|
162
164
|
" -h, --help このヘルプを表示",
|
|
163
165
|
"",
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
" - 質問1",
|
|
168
|
-
" - 質問2",
|
|
166
|
+
"Per-file mode:",
|
|
167
|
+
" provider に systemPromptFlag が設定されている場合、ファイルごとに非同期で agent を呼び出します。",
|
|
168
|
+
" 同時実行数は config.json の limits.concurrency で設定可能(default: 3)。",
|
|
169
169
|
"",
|
|
170
170
|
].join("\n")
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function buildArgs(agent, prompt) {
|
|
174
|
+
function buildArgs(agent, prompt, systemPrompt) {
|
|
175
175
|
const args = Array.isArray(agent.args) ? [...agent.args] : [];
|
|
176
|
+
|
|
177
|
+
// Prepend system prompt flag if provider supports it and systemPrompt given
|
|
178
|
+
const flag = agent.systemPromptFlag;
|
|
179
|
+
let prefix = [];
|
|
180
|
+
let cleanupFile;
|
|
181
|
+
if (flag && systemPrompt) {
|
|
182
|
+
if (flag === "--system-prompt-file") {
|
|
183
|
+
// Write to temp file for providers that require file-based system prompts
|
|
184
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sdd-forge-"));
|
|
185
|
+
const tmpFile = path.join(tmpDir, "system-prompt.md");
|
|
186
|
+
fs.writeFileSync(tmpFile, systemPrompt, "utf8");
|
|
187
|
+
prefix = [flag, tmpFile];
|
|
188
|
+
cleanupFile = tmpFile;
|
|
189
|
+
} else {
|
|
190
|
+
prefix = [flag, systemPrompt];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
176
194
|
const hasToken = args.some(
|
|
177
195
|
(a) => typeof a === "string" && a.includes("{{PROMPT}}")
|
|
178
196
|
);
|
|
197
|
+
let finalArgs;
|
|
179
198
|
if (hasToken) {
|
|
180
|
-
|
|
199
|
+
finalArgs = args.map((a) =>
|
|
181
200
|
typeof a === "string" ? a.replaceAll("{{PROMPT}}", prompt) : a
|
|
182
201
|
);
|
|
202
|
+
} else {
|
|
203
|
+
finalArgs = [...args, prompt];
|
|
183
204
|
}
|
|
184
|
-
|
|
205
|
+
|
|
206
|
+
// If systemPromptFlag not set but systemPrompt given, prepend to prompt
|
|
207
|
+
if (!flag && systemPrompt) {
|
|
208
|
+
const combined = systemPrompt + "\n\n" + prompt;
|
|
209
|
+
if (hasToken) {
|
|
210
|
+
finalArgs = (Array.isArray(agent.args) ? [...agent.args] : []).map((a) =>
|
|
211
|
+
typeof a === "string" ? a.replaceAll("{{PROMPT}}", combined) : a
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
finalArgs = [...(Array.isArray(agent.args) ? [...agent.args] : []), combined];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { args: [...prefix, ...finalArgs], cleanupFile };
|
|
185
219
|
}
|
|
186
220
|
|
|
187
221
|
function runAgent(agent, prompt, options = {}) {
|
|
@@ -198,7 +232,13 @@ function runAgent(agent, prompt, options = {}) {
|
|
|
198
232
|
options.label || agent?.name || agent?.command || "agent"
|
|
199
233
|
);
|
|
200
234
|
const streamOutput = options.streamOutput === true;
|
|
201
|
-
const args = buildArgs(agent, prompt);
|
|
235
|
+
const { args, cleanupFile } = buildArgs(agent, prompt, options.systemPrompt);
|
|
236
|
+
|
|
237
|
+
function cleanup() {
|
|
238
|
+
if (cleanupFile) {
|
|
239
|
+
try { fs.unlinkSync(cleanupFile); fs.rmdirSync(path.dirname(cleanupFile)); } catch (_) {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
202
242
|
|
|
203
243
|
return new Promise((resolve, reject) => {
|
|
204
244
|
const ticker =
|
|
@@ -239,12 +279,14 @@ function runAgent(agent, prompt, options = {}) {
|
|
|
239
279
|
child.on("error", (err) => {
|
|
240
280
|
if (ticker) clearInterval(ticker);
|
|
241
281
|
clearTimeout(timeoutTimer);
|
|
282
|
+
cleanup();
|
|
242
283
|
reject(new Error(`Agent failed: ${agent.command}\n${err.message}`));
|
|
243
284
|
});
|
|
244
285
|
|
|
245
286
|
child.on("close", (code, signal) => {
|
|
246
287
|
if (ticker) clearInterval(ticker);
|
|
247
288
|
clearTimeout(timeoutTimer);
|
|
289
|
+
cleanup();
|
|
248
290
|
if (code === 0 && !signal) {
|
|
249
291
|
resolve(stdoutBuf.trim());
|
|
250
292
|
return;
|
|
@@ -272,6 +314,7 @@ function runAgent(agent, prompt, options = {}) {
|
|
|
272
314
|
{ maxBuffer: 20 * 1024 * 1024, timeout: timeoutMs, cwd: runCwd },
|
|
273
315
|
(err, stdout, stderr) => {
|
|
274
316
|
if (ticker) clearInterval(ticker);
|
|
317
|
+
cleanup();
|
|
275
318
|
if (err) {
|
|
276
319
|
const timedOut = err.killed === true;
|
|
277
320
|
reject(
|
|
@@ -476,6 +519,57 @@ function summarizeNeedsInput(reviewOut) {
|
|
|
476
519
|
return uniq.slice(0, 8);
|
|
477
520
|
}
|
|
478
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Build the system prompt (shared across all files in a round).
|
|
524
|
+
* Contains: role, rules, user request, spec, analysis summary.
|
|
525
|
+
*/
|
|
526
|
+
function buildForgeSystemPrompt({
|
|
527
|
+
userPrompt,
|
|
528
|
+
specPath,
|
|
529
|
+
specText,
|
|
530
|
+
analysisSummary,
|
|
531
|
+
}) {
|
|
532
|
+
const specBlock = specPath
|
|
533
|
+
? ["[SPEC_PATH]", specPath, "", "[SPEC_CONTENT]", specText || "(empty)", ""]
|
|
534
|
+
: [];
|
|
535
|
+
return [
|
|
536
|
+
"あなたは docs-forge です。指定されたドキュメントファイルの品質を改善してください。",
|
|
537
|
+
"",
|
|
538
|
+
"[USER_PROMPT]",
|
|
539
|
+
userPrompt,
|
|
540
|
+
"",
|
|
541
|
+
...specBlock,
|
|
542
|
+
"[RULES]",
|
|
543
|
+
"- 編集対象は指定された TARGET_FILE のみ",
|
|
544
|
+
"- 推測は避け、ソースコードの事実を優先",
|
|
545
|
+
"- 変更は必要最小限にする",
|
|
546
|
+
"- 説明文は簡潔で主語を明確にする",
|
|
547
|
+
"- 不明な場合は編集せずスキップする",
|
|
548
|
+
"",
|
|
549
|
+
...(analysisSummary
|
|
550
|
+
? ["[SOURCE_ANALYSIS]", analysisSummary, ""]
|
|
551
|
+
: []),
|
|
552
|
+
].join("\n");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Build the user prompt for a single file.
|
|
557
|
+
*/
|
|
558
|
+
function buildForgeFilePrompt({ targetFile, round, maxRuns, reviewFeedback }) {
|
|
559
|
+
return [
|
|
560
|
+
`round: ${round}/${maxRuns}`,
|
|
561
|
+
"",
|
|
562
|
+
"[TARGET_FILE]",
|
|
563
|
+
targetFile,
|
|
564
|
+
"",
|
|
565
|
+
"[PREVIOUS_REVIEW_FEEDBACK]",
|
|
566
|
+
reviewFeedback || "なし",
|
|
567
|
+
].join("\n");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Build a combined prompt (for providers without system prompt support).
|
|
572
|
+
*/
|
|
479
573
|
function buildForgePrompt({
|
|
480
574
|
userPrompt,
|
|
481
575
|
round,
|
|
@@ -504,13 +598,10 @@ function buildForgePrompt({
|
|
|
504
598
|
"",
|
|
505
599
|
"[RULES]",
|
|
506
600
|
"- 編集対象は TARGET_FILES のみ",
|
|
507
|
-
"-
|
|
508
|
-
"-
|
|
509
|
-
"-
|
|
510
|
-
"-
|
|
511
|
-
" NEEDS_INPUT",
|
|
512
|
-
" - 質問1",
|
|
513
|
-
" - 質問2",
|
|
601
|
+
"- 推測は避け、ソースコードの事実を優先",
|
|
602
|
+
"- 変更は必要最小限にする",
|
|
603
|
+
"- 説明文は簡潔で主語を明確にする",
|
|
604
|
+
"- 不明な場合は編集せずスキップする",
|
|
514
605
|
"",
|
|
515
606
|
...(analysisSummary
|
|
516
607
|
? ["[SOURCE_ANALYSIS]", analysisSummary, ""]
|
|
@@ -520,6 +611,63 @@ function buildForgePrompt({
|
|
|
520
611
|
].join("\n");
|
|
521
612
|
}
|
|
522
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Run agent for each file with concurrency control.
|
|
616
|
+
* Returns an array of { file, ok, error? } results.
|
|
617
|
+
*/
|
|
618
|
+
async function runPerFile({ agent, targetFiles, systemPrompt, round, maxRuns, reviewFeedback, root, timeoutMs, concurrency, verbose }) {
|
|
619
|
+
const results = [];
|
|
620
|
+
let running = 0;
|
|
621
|
+
let idx = 0;
|
|
622
|
+
|
|
623
|
+
return new Promise((resolve) => {
|
|
624
|
+
function next() {
|
|
625
|
+
// All dispatched and all done
|
|
626
|
+
if (idx >= targetFiles.length && running === 0) {
|
|
627
|
+
resolve(results);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Dispatch up to concurrency limit
|
|
632
|
+
while (running < concurrency && idx < targetFiles.length) {
|
|
633
|
+
const file = targetFiles[idx++];
|
|
634
|
+
running++;
|
|
635
|
+
|
|
636
|
+
const filePrompt = buildForgeFilePrompt({
|
|
637
|
+
targetFile: file,
|
|
638
|
+
round,
|
|
639
|
+
maxRuns,
|
|
640
|
+
reviewFeedback,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
output.write(`[forge] start: ${file}\n`);
|
|
644
|
+
|
|
645
|
+
runAgent(agent, filePrompt, {
|
|
646
|
+
label: `forge:${path.basename(file)}`,
|
|
647
|
+
cwd: root,
|
|
648
|
+
timeoutMs,
|
|
649
|
+
streamOutput: verbose,
|
|
650
|
+
systemPrompt,
|
|
651
|
+
})
|
|
652
|
+
.then(() => {
|
|
653
|
+
output.write(`[forge] done: ${file}\n`);
|
|
654
|
+
results.push({ file, ok: true });
|
|
655
|
+
})
|
|
656
|
+
.catch((e) => {
|
|
657
|
+
output.write(`[forge] failed: ${file} — ${String(e.message || e).slice(0, 200)}\n`);
|
|
658
|
+
results.push({ file, ok: false, error: e.message });
|
|
659
|
+
})
|
|
660
|
+
.finally(() => {
|
|
661
|
+
running--;
|
|
662
|
+
next();
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
next();
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
523
671
|
/**
|
|
524
672
|
* forge の review 成功後に projectContext を自動更新する。
|
|
525
673
|
* docs/ の各章ファイル先頭を LLM に渡し、プロジェクト概要を生成させる。
|
|
@@ -686,6 +834,8 @@ async function main() {
|
|
|
686
834
|
return;
|
|
687
835
|
}
|
|
688
836
|
|
|
837
|
+
const concurrency = Number(cfg.limits?.concurrency || 0) || DEFAULT_CONCURRENCY;
|
|
838
|
+
|
|
689
839
|
let reviewFeedback = "";
|
|
690
840
|
for (let round = 1; round <= effectiveMaxRuns; round += 1) {
|
|
691
841
|
output.write(`\n[forge] round ${round}/${effectiveMaxRuns}\n`);
|
|
@@ -698,45 +848,70 @@ async function main() {
|
|
|
698
848
|
output.write("[forge] assist mode: agent not configured, run local-only.\n");
|
|
699
849
|
}
|
|
700
850
|
} else {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
851
|
+
const targetFiles = getTargetFiles(root);
|
|
852
|
+
const usePerFile = !!agent.systemPromptFlag;
|
|
853
|
+
|
|
854
|
+
if (usePerFile) {
|
|
855
|
+
// Per-file async processing with system prompt separation
|
|
856
|
+
const systemPrompt = buildForgeSystemPrompt({
|
|
857
|
+
userPrompt,
|
|
858
|
+
specPath: specPath ? path.relative(root, specPath) : "",
|
|
859
|
+
specText,
|
|
860
|
+
analysisSummary,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
output.write(`[forge] per-file mode: ${targetFiles.length} files, concurrency=${concurrency}\n`);
|
|
864
|
+
|
|
865
|
+
const results = await runPerFile({
|
|
866
|
+
agent,
|
|
867
|
+
targetFiles,
|
|
868
|
+
systemPrompt,
|
|
869
|
+
round,
|
|
870
|
+
maxRuns: effectiveMaxRuns,
|
|
871
|
+
reviewFeedback,
|
|
872
|
+
root,
|
|
715
873
|
timeoutMs,
|
|
716
|
-
|
|
874
|
+
concurrency,
|
|
875
|
+
verbose: cli.verbose,
|
|
717
876
|
});
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
877
|
+
|
|
878
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
879
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
880
|
+
output.write(`[forge] per-file done: ${succeeded} ok, ${failed} failed\n`);
|
|
881
|
+
|
|
882
|
+
if (succeeded > 0) usedAgent = true;
|
|
883
|
+
if (failed > 0 && succeeded === 0) agentFailed = true;
|
|
884
|
+
} else {
|
|
885
|
+
// Legacy: single prompt with all files
|
|
886
|
+
const prompt = buildForgePrompt({
|
|
887
|
+
userPrompt,
|
|
888
|
+
round,
|
|
889
|
+
maxRuns: effectiveMaxRuns,
|
|
890
|
+
reviewFeedback,
|
|
891
|
+
specPath: specPath ? path.relative(root, specPath) : "",
|
|
892
|
+
specText,
|
|
893
|
+
analysisSummary,
|
|
894
|
+
targetFiles,
|
|
895
|
+
});
|
|
896
|
+
try {
|
|
897
|
+
await runAgent(agent, prompt, {
|
|
898
|
+
label: "forge.generate",
|
|
899
|
+
cwd: root,
|
|
900
|
+
timeoutMs,
|
|
901
|
+
streamOutput: cli.verbose,
|
|
902
|
+
});
|
|
903
|
+
usedAgent = true;
|
|
904
|
+
} catch (e) {
|
|
905
|
+
agentFailed = true;
|
|
906
|
+
if (mode === "agent") {
|
|
907
|
+
throw e;
|
|
726
908
|
}
|
|
727
|
-
|
|
728
|
-
|
|
909
|
+
output.write(
|
|
910
|
+
`[forge] agent step failed. continue with local pipeline.\n${String(
|
|
911
|
+
e instanceof Error ? e.message : e
|
|
912
|
+
).slice(0, 500)}\n`,
|
|
913
|
+
);
|
|
729
914
|
}
|
|
730
|
-
} catch (e) {
|
|
731
|
-
agentFailed = true;
|
|
732
|
-
if (mode === "agent") {
|
|
733
|
-
throw e;
|
|
734
|
-
}
|
|
735
|
-
output.write(
|
|
736
|
-
`[forge] agent step failed. continue with local pipeline.\n${String(
|
|
737
|
-
e instanceof Error ? e.message : e
|
|
738
|
-
).slice(0, 500)}\n`,
|
|
739
|
-
);
|
|
740
915
|
}
|
|
741
916
|
}
|
|
742
917
|
}
|
|
@@ -790,7 +965,13 @@ async function main() {
|
|
|
790
965
|
throw new Error("forge: max runs reached but review still failing.");
|
|
791
966
|
}
|
|
792
967
|
|
|
793
|
-
export {
|
|
968
|
+
export {
|
|
969
|
+
main,
|
|
970
|
+
buildArgs,
|
|
971
|
+
buildForgeSystemPrompt,
|
|
972
|
+
buildForgeFilePrompt,
|
|
973
|
+
runPerFile,
|
|
974
|
+
};
|
|
794
975
|
|
|
795
976
|
const isDirectRun = process.argv[1] &&
|
|
796
977
|
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
@@ -575,6 +575,7 @@ async function main() {
|
|
|
575
575
|
name: "claude-cli",
|
|
576
576
|
command: "claude",
|
|
577
577
|
args: ["--model", "sonnet", "-p", "{{PROMPT}}"],
|
|
578
|
+
systemPromptFlag: "--system-prompt",
|
|
578
579
|
},
|
|
579
580
|
};
|
|
580
581
|
} else if (defaultAgent === "codex") {
|
|
@@ -583,6 +584,7 @@ async function main() {
|
|
|
583
584
|
name: "codex-cli",
|
|
584
585
|
command: "codex",
|
|
585
586
|
args: ["-p", "{{PROMPT}}"],
|
|
587
|
+
systemPromptFlag: "--system-prompt-file",
|
|
586
588
|
},
|
|
587
589
|
};
|
|
588
590
|
}
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import path from "path";
|
|
15
|
-
import { execFileSync } from "child_process";
|
|
16
15
|
import { fileURLToPath } from "url";
|
|
17
16
|
import { parseDirectives } from "../lib/directive-parser.js";
|
|
18
17
|
import { repoRoot, parseArgs } from "../../lib/cli.js";
|
|
19
18
|
import { loadConfig, resolveProjectContext } from "../../lib/config.js";
|
|
20
19
|
import { createLogger } from "../../lib/progress.js";
|
|
20
|
+
import { callAgent as callAgentBase } from "../../lib/agent.js";
|
|
21
21
|
|
|
22
22
|
const logger = createLogger("text");
|
|
23
23
|
|
|
@@ -230,7 +230,35 @@ function formatLimitRule(params) {
|
|
|
230
230
|
return "簡潔かつ正確に(3〜15行程度)";
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
/**
|
|
234
|
+
* システムプロンプトを構築する。
|
|
235
|
+
* documentStyle + projectContext + 共通出力ルールを含む。
|
|
236
|
+
* per-directive / batch 両モードで共有し、provider のプロンプトキャッシュを活用する。
|
|
237
|
+
*
|
|
238
|
+
* @param {string} projectContext
|
|
239
|
+
* @param {import("../lib/types.js").DocumentStyle|undefined} documentStyle
|
|
240
|
+
* @param {string} lang
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
function buildTextSystemPrompt(projectContext, documentStyle, lang) {
|
|
244
|
+
const header = buildPromptHeader(projectContext, documentStyle, lang);
|
|
245
|
+
return [
|
|
246
|
+
...header,
|
|
247
|
+
"",
|
|
248
|
+
"以下の指示に従い、ドキュメントに挿入するマークダウンテキストを生成してください。",
|
|
249
|
+
"",
|
|
250
|
+
"## 出力ルール(厳守)",
|
|
251
|
+
"- 本文のマークダウンテキストのみを出力すること",
|
|
252
|
+
"- 前置き・メタコメンタリーは絶対に含めないこと(例: 「以下に生成します」「Based on the analysis data」「Here is the generated text」等は禁止)",
|
|
253
|
+
"- 水平線(---)を装飾目的で使わないこと",
|
|
254
|
+
"- コードブロック(```)で全体を囲まないこと",
|
|
255
|
+
"- セクション見出し(#)は含めない(挿入先に既にある)",
|
|
256
|
+
"- 解析データに基づく事実のみ記述(推測は避ける)",
|
|
257
|
+
"- 1行目から本文を開始すること(空行や導入文で始めない)",
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildPrompt(directive, fileName, lines, contextData) {
|
|
234
262
|
const directiveLine = directive.line;
|
|
235
263
|
|
|
236
264
|
// ±20行のコンテキストを抽出
|
|
@@ -244,24 +272,11 @@ function buildPrompt(directive, fileName, lines, contextData, projectContext, do
|
|
|
244
272
|
? contextJson.slice(0, 8000) + "\n... (truncated)"
|
|
245
273
|
: contextJson;
|
|
246
274
|
|
|
247
|
-
const header = buildPromptHeader(projectContext, documentStyle, lang);
|
|
248
|
-
header.push("", "以下の指示に従い、ドキュメントに挿入するマークダウンテキストを生成してください。");
|
|
249
|
-
|
|
250
275
|
return [
|
|
251
|
-
...header,
|
|
252
|
-
"",
|
|
253
276
|
"## 指示",
|
|
254
277
|
directive.prompt,
|
|
255
278
|
"",
|
|
256
|
-
"## 出力ルール(厳守)",
|
|
257
|
-
"- 本文のマークダウンテキストのみを出力すること",
|
|
258
|
-
"- 前置き・メタコメンタリーは絶対に含めないこと(例: 「以下に生成します」「Based on the analysis data」「Here is the generated text」等は禁止)",
|
|
259
|
-
"- 水平線(---)を装飾目的で使わないこと",
|
|
260
|
-
"- コードブロック(```)で全体を囲まないこと",
|
|
261
|
-
"- セクション見出し(#)は含めない(挿入先に既にある)",
|
|
262
|
-
"- 解析データに基づく事実のみ記述(推測は避ける)",
|
|
263
279
|
`- ${formatLimitRule(directive.params)}`,
|
|
264
|
-
"- 1行目から本文を開始すること(空行や導入文で始めない)",
|
|
265
280
|
"",
|
|
266
281
|
`## 挿入先コンテキスト(${fileName})`,
|
|
267
282
|
surroundingLines,
|
|
@@ -328,9 +343,7 @@ function countFilledInBatch(fileText) {
|
|
|
328
343
|
* ファイル全体を1つのプロンプトにまとめるバッチプロンプトを構築する。
|
|
329
344
|
* ±20行ウィンドウ・解析データは不要(ファイル全体が文脈になる)。
|
|
330
345
|
*/
|
|
331
|
-
function buildBatchPrompt(fileName, text,
|
|
332
|
-
const header = buildPromptHeader(projectContext, documentStyle, lang);
|
|
333
|
-
|
|
346
|
+
function buildBatchPrompt(fileName, text, textFills) {
|
|
334
347
|
// ディレクティブごとの個別制限ルールがあれば列挙
|
|
335
348
|
const perDirectiveRules = [];
|
|
336
349
|
for (const d of textFills) {
|
|
@@ -343,8 +356,6 @@ function buildBatchPrompt(fileName, text, projectContext, textFills, documentSty
|
|
|
343
356
|
: "- 個別制限のないディレクティブは3〜15行程度の本文を生成すること";
|
|
344
357
|
|
|
345
358
|
return [
|
|
346
|
-
...header,
|
|
347
|
-
"",
|
|
348
359
|
`以下の ${fileName} にある <!-- @text: 指示 --> ディレクティブをすべて埋めてください。`,
|
|
349
360
|
"",
|
|
350
361
|
"## 出力ルール(厳守)",
|
|
@@ -370,14 +381,14 @@ function buildBatchPrompt(fileName, text, projectContext, textFills, documentSty
|
|
|
370
381
|
*
|
|
371
382
|
* @returns {{ text: string, filled: number, skipped: number }}
|
|
372
383
|
*/
|
|
373
|
-
function processTemplateFileBatch(text, analysis, fileName, agent, timeoutMs, cwd, dryRun, _preamblePatterns,
|
|
384
|
+
function processTemplateFileBatch(text, analysis, fileName, agent, timeoutMs, cwd, dryRun, _preamblePatterns, systemPrompt) {
|
|
374
385
|
const directives = parseDirectives(text);
|
|
375
386
|
const textFills = directives.filter((d) => d.type === "text");
|
|
376
387
|
|
|
377
388
|
if (textFills.length === 0) return { text, filled: 0, skipped: 0 };
|
|
378
389
|
|
|
379
390
|
const cleanText = stripFillContent(text);
|
|
380
|
-
const prompt = buildBatchPrompt(fileName, cleanText,
|
|
391
|
+
const prompt = buildBatchPrompt(fileName, cleanText, textFills);
|
|
381
392
|
|
|
382
393
|
if (dryRun) {
|
|
383
394
|
console.log(`[text] DRY-RUN batch ${fileName}: ${textFills.length} directive(s) → 1 call (${prompt.length} chars)`);
|
|
@@ -389,7 +400,7 @@ function processTemplateFileBatch(text, analysis, fileName, agent, timeoutMs, cw
|
|
|
389
400
|
let result;
|
|
390
401
|
try {
|
|
391
402
|
// バッチはファイル全体を返すので preamble パターンは使わない
|
|
392
|
-
result = callAgent(agent, prompt, timeoutMs, cwd, []);
|
|
403
|
+
result = callAgent(agent, prompt, timeoutMs, cwd, [], systemPrompt);
|
|
393
404
|
} catch (err) {
|
|
394
405
|
const parts = [err.message];
|
|
395
406
|
if (err.signal) parts.push(`signal: ${err.signal}`);
|
|
@@ -422,28 +433,9 @@ function processTemplateFileBatch(text, analysis, fileName, agent, timeoutMs, cw
|
|
|
422
433
|
// ---------------------------------------------------------------------------
|
|
423
434
|
// エージェント呼び出し
|
|
424
435
|
// ---------------------------------------------------------------------------
|
|
425
|
-
function callAgent(agent, prompt, timeoutMs, cwd, preamblePatterns) {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
typeof a === "string" ? a.replaceAll("{{PROMPT}}", prompt) : a
|
|
429
|
-
);
|
|
430
|
-
// {{PROMPT}} トークンがなかった場合は末尾に追加
|
|
431
|
-
const hasToken = args.some((a) => typeof a === "string" && a.includes("{{PROMPT}}"));
|
|
432
|
-
const finalArgs = hasToken ? resolvedArgs : [...resolvedArgs, prompt];
|
|
433
|
-
|
|
434
|
-
// CLAUDECODE を外してネスト起動ガードを回避(claude CLI 呼び出し時に必要)
|
|
435
|
-
const env = { ...process.env };
|
|
436
|
-
delete env.CLAUDECODE;
|
|
437
|
-
|
|
438
|
-
const result = execFileSync(agent.command, finalArgs, {
|
|
439
|
-
encoding: "utf8",
|
|
440
|
-
maxBuffer: 20 * 1024 * 1024,
|
|
441
|
-
timeout: timeoutMs,
|
|
442
|
-
cwd,
|
|
443
|
-
env,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
return stripPreamble(result.trim(), preamblePatterns);
|
|
436
|
+
function callAgent(agent, prompt, timeoutMs, cwd, preamblePatterns, systemPrompt) {
|
|
437
|
+
const result = callAgentBase(agent, prompt, timeoutMs, cwd, { systemPrompt });
|
|
438
|
+
return stripPreamble(result, preamblePatterns);
|
|
447
439
|
}
|
|
448
440
|
|
|
449
441
|
/**
|
|
@@ -501,7 +493,7 @@ function stripPreamble(text, preamblePatterns) {
|
|
|
501
493
|
* @param {boolean} dryRun - dry-run モード
|
|
502
494
|
* @returns {{ text: string, filled: number, skipped: number }}
|
|
503
495
|
*/
|
|
504
|
-
function processTemplate(text, analysis, fileName, agent, timeoutMs, cwd, dryRun, preamblePatterns,
|
|
496
|
+
function processTemplate(text, analysis, fileName, agent, timeoutMs, cwd, dryRun, preamblePatterns, systemPrompt, filterId) {
|
|
505
497
|
const directives = parseDirectives(text);
|
|
506
498
|
let textFills = directives.filter((d) => d.type === "text");
|
|
507
499
|
if (filterId) {
|
|
@@ -518,7 +510,7 @@ function processTemplate(text, analysis, fileName, agent, timeoutMs, cwd, dryRun
|
|
|
518
510
|
// 後ろから処理して行番号のズレを防ぐ
|
|
519
511
|
for (let i = textFills.length - 1; i >= 0; i--) {
|
|
520
512
|
const d = textFills[i];
|
|
521
|
-
const prompt = buildPrompt(d, fileName, lines, contextData
|
|
513
|
+
const prompt = buildPrompt(d, fileName, lines, contextData);
|
|
522
514
|
|
|
523
515
|
if (dryRun) {
|
|
524
516
|
console.log(`[text] DRY-RUN ${fileName}:${d.line + 1}: ${d.prompt.slice(0, 80)}`);
|
|
@@ -531,7 +523,7 @@ function processTemplate(text, analysis, fileName, agent, timeoutMs, cwd, dryRun
|
|
|
531
523
|
|
|
532
524
|
let generated;
|
|
533
525
|
try {
|
|
534
|
-
generated = callAgent(agent, prompt, timeoutMs, cwd, preamblePatterns);
|
|
526
|
+
generated = callAgent(agent, prompt, timeoutMs, cwd, preamblePatterns, systemPrompt);
|
|
535
527
|
} catch (err) {
|
|
536
528
|
logger.log(`ERROR calling agent for ${fileName}:${d.line + 1}: ${err.message.slice(0, 200)}`);
|
|
537
529
|
skipped++;
|
|
@@ -599,6 +591,7 @@ export function textFillFromAnalysis(root, analysis, agentName) {
|
|
|
599
591
|
const projectContext = resolveProjectContext(root);
|
|
600
592
|
const documentStyle = cfg.documentStyle;
|
|
601
593
|
const lang = cfg.lang || "ja";
|
|
594
|
+
const systemPrompt = buildTextSystemPrompt(projectContext, documentStyle, lang);
|
|
602
595
|
const docsDir = path.join(root, "docs");
|
|
603
596
|
const docsFiles = fs.readdirSync(docsDir)
|
|
604
597
|
.filter((f) => /^\d{2}_/.test(f) && f.endsWith(".md"))
|
|
@@ -611,7 +604,7 @@ export function textFillFromAnalysis(root, analysis, agentName) {
|
|
|
611
604
|
for (const file of docsFiles) {
|
|
612
605
|
const filePath = path.join(docsDir, file);
|
|
613
606
|
const original = fs.readFileSync(filePath, "utf8");
|
|
614
|
-
const result = processTemplate(original, analysis, file, agent, 120000, root, false, preamblePatterns,
|
|
607
|
+
const result = processTemplate(original, analysis, file, agent, 120000, root, false, preamblePatterns, systemPrompt);
|
|
615
608
|
|
|
616
609
|
totalFilled += result.filled;
|
|
617
610
|
totalSkipped += result.skipped;
|
|
@@ -671,6 +664,7 @@ function main() {
|
|
|
671
664
|
const projectContext = resolveProjectContext(root);
|
|
672
665
|
const documentStyle = cfg.documentStyle;
|
|
673
666
|
const lang = cfg.lang || "ja";
|
|
667
|
+
const systemPrompt = buildTextSystemPrompt(projectContext, documentStyle, lang);
|
|
674
668
|
const docsDir = path.join(root, "docs");
|
|
675
669
|
const docsFiles = fs.readdirSync(docsDir)
|
|
676
670
|
.filter((f) => /^\d{2}_/.test(f) && f.endsWith(".md"))
|
|
@@ -702,14 +696,14 @@ function main() {
|
|
|
702
696
|
if (!hasId) continue;
|
|
703
697
|
}
|
|
704
698
|
|
|
705
|
-
let result = processFn(original, analysis, file, agent, cli.timeout, root, cli.dryRun, preamblePatterns,
|
|
699
|
+
let result = processFn(original, analysis, file, agent, cli.timeout, root, cli.dryRun, preamblePatterns, systemPrompt, cli.id || undefined);
|
|
706
700
|
|
|
707
701
|
// バッチモードで 0 filled になった場合は per-directive モードで再試行
|
|
708
702
|
if (!cli.perDirective && !cli.dryRun && result.filled === 0) {
|
|
709
703
|
const textFills = parseDirectives(original).filter((d) => d.type === "text");
|
|
710
704
|
if (textFills.length > 0) {
|
|
711
705
|
logger.verbose(`Batch returned 0 filled for ${file}. Falling back to per-directive mode...`);
|
|
712
|
-
result = processTemplate(original, analysis, file, agent, cli.timeout, root, cli.dryRun, preamblePatterns,
|
|
706
|
+
result = processTemplate(original, analysis, file, agent, cli.timeout, root, cli.dryRun, preamblePatterns, systemPrompt, cli.id || undefined);
|
|
713
707
|
}
|
|
714
708
|
}
|
|
715
709
|
|
|
@@ -146,6 +146,30 @@ function upgradeAgentsSddSection(workRoot, lang, dryRun) {
|
|
|
146
146
|
return "updated";
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Config hints (non-destructive — just prints suggestions)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
const SYSTEM_PROMPT_FLAGS = {
|
|
154
|
+
claude: "--system-prompt",
|
|
155
|
+
codex: "--system-prompt-file",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check config.json for missing new settings and print hints.
|
|
160
|
+
*/
|
|
161
|
+
function checkConfigHints(config, t) {
|
|
162
|
+
if (!config.providers) return;
|
|
163
|
+
|
|
164
|
+
for (const [key, prov] of Object.entries(config.providers)) {
|
|
165
|
+
if (prov.systemPromptFlag) continue;
|
|
166
|
+
const suggested = SYSTEM_PROMPT_FLAGS[key] || SYSTEM_PROMPT_FLAGS[prov.command];
|
|
167
|
+
if (suggested) {
|
|
168
|
+
console.log(t("upgrade.hintSystemPromptFlag", { provider: key, flag: suggested }));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
// ---------------------------------------------------------------------------
|
|
150
174
|
// Main
|
|
151
175
|
// ---------------------------------------------------------------------------
|
|
@@ -202,6 +226,9 @@ async function main() {
|
|
|
202
226
|
console.log(t("upgrade.agentsUnchanged"));
|
|
203
227
|
}
|
|
204
228
|
|
|
229
|
+
// 3. Config hints — check for missing new settings
|
|
230
|
+
checkConfigHints(config, t);
|
|
231
|
+
|
|
205
232
|
// Summary
|
|
206
233
|
const hasChanges = skillResults.some((r) => r.status === "updated") || agentsStatus === "updated";
|
|
207
234
|
if (!hasChanges) {
|
package/src/lib/agent.js
CHANGED
|
@@ -5,39 +5,87 @@
|
|
|
5
5
|
* Provides a shared interface for calling configured AI agents.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
8
11
|
import { execFileSync } from "child_process";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Call an AI agent with a prompt and return the response.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
16
|
+
* When `options.systemPrompt` is provided:
|
|
17
|
+
* - If agent.systemPromptFlag is set (e.g. "--system-prompt"),
|
|
18
|
+
* the flag + systemPrompt are prepended to the argument list.
|
|
19
|
+
* - If agent.systemPromptFlag is "--system-prompt-file",
|
|
20
|
+
* a temp file is written and cleaned up after execution.
|
|
21
|
+
* - If no systemPromptFlag but systemPrompt is given,
|
|
22
|
+
* the system prompt is prepended to the user prompt (fallback).
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} agent - Agent config ({ command, args, systemPromptFlag? })
|
|
14
25
|
* @param {string} prompt - The prompt text
|
|
15
26
|
* @param {number} [timeoutMs] - Timeout in milliseconds (default: 120000)
|
|
16
27
|
* @param {string} [cwd] - Working directory
|
|
28
|
+
* @param {Object} [options] - Additional options
|
|
29
|
+
* @param {string} [options.systemPrompt] - System prompt text
|
|
17
30
|
* @returns {string} Agent response (trimmed)
|
|
18
31
|
*/
|
|
19
|
-
export function callAgent(agent, prompt, timeoutMs, cwd) {
|
|
32
|
+
export function callAgent(agent, prompt, timeoutMs, cwd, options) {
|
|
33
|
+
const { systemPrompt } = options || {};
|
|
20
34
|
const args = Array.isArray(agent.args) ? [...agent.args] : [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
|
|
36
|
+
// Build system prompt prefix args
|
|
37
|
+
const flag = agent.systemPromptFlag;
|
|
38
|
+
let prefix = [];
|
|
39
|
+
let cleanupFile;
|
|
40
|
+
if (flag && systemPrompt) {
|
|
41
|
+
if (flag === "--system-prompt-file") {
|
|
42
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sdd-forge-"));
|
|
43
|
+
const tmpFile = path.join(tmpDir, "system-prompt.md");
|
|
44
|
+
fs.writeFileSync(tmpFile, systemPrompt, "utf8");
|
|
45
|
+
prefix = [flag, tmpFile];
|
|
46
|
+
cleanupFile = tmpFile;
|
|
47
|
+
} else {
|
|
48
|
+
prefix = [flag, systemPrompt];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Combine system prompt into user prompt when no flag is available
|
|
53
|
+
let effectivePrompt = prompt;
|
|
54
|
+
if (!flag && systemPrompt) {
|
|
55
|
+
effectivePrompt = systemPrompt + "\n\n" + prompt;
|
|
56
|
+
}
|
|
24
57
|
|
|
25
58
|
const hasToken = args.some((a) => typeof a === "string" && a.includes("{{PROMPT}}"));
|
|
26
|
-
|
|
59
|
+
let resolvedArgs;
|
|
60
|
+
if (hasToken) {
|
|
61
|
+
resolvedArgs = args.map((a) =>
|
|
62
|
+
typeof a === "string" ? a.replaceAll("{{PROMPT}}", effectivePrompt) : a,
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
resolvedArgs = [...args, effectivePrompt];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const finalArgs = [...prefix, ...resolvedArgs];
|
|
27
69
|
|
|
28
70
|
// Remove CLAUDECODE env to avoid nested launch guard
|
|
29
71
|
const env = { ...process.env };
|
|
30
72
|
delete env.CLAUDECODE;
|
|
31
73
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
try {
|
|
75
|
+
const result = execFileSync(agent.command, finalArgs, {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
78
|
+
timeout: timeoutMs || 120000,
|
|
79
|
+
cwd: cwd || process.cwd(),
|
|
80
|
+
env,
|
|
81
|
+
});
|
|
39
82
|
|
|
40
|
-
|
|
83
|
+
return result.trim();
|
|
84
|
+
} finally {
|
|
85
|
+
if (cleanupFile) {
|
|
86
|
+
try { fs.unlinkSync(cleanupFile); fs.rmdirSync(path.dirname(cleanupFile)); } catch (_) {}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
41
89
|
}
|
|
42
90
|
|
|
43
91
|
/**
|
package/src/lib/types.js
CHANGED
|
@@ -35,6 +35,7 @@ import { buildTypeAliases } from "../docs/presets/registry.js";
|
|
|
35
35
|
* @property {string} command - 実行コマンド
|
|
36
36
|
* @property {string[]} args - コマンド引数({{PROMPT}} プレースホルダー対応)
|
|
37
37
|
* @property {number} [timeoutMs] - タイムアウト (ms)
|
|
38
|
+
* @property {string} [systemPromptFlag] - system prompt フラグ (例: "--system-prompt", "--system-prompt-file")
|
|
38
39
|
*/
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -56,6 +57,7 @@ import { buildTypeAliases } from "../docs/presets/registry.js";
|
|
|
56
57
|
* @property {string} type - Project type ("webapp/cakephp2" | "cli" | ...)
|
|
57
58
|
* @property {Object} [limits] - Limit settings
|
|
58
59
|
* @property {number} [limits.designTimeoutMs] - Timeout (ms)
|
|
60
|
+
* @property {number} [limits.concurrency] - Per-file concurrency (default: 3)
|
|
59
61
|
* @property {DocumentStyle} [documentStyle] - Document style settings
|
|
60
62
|
* @property {TextFillConfig} [textFill] - text-fill settings
|
|
61
63
|
* @property {string} [defaultAgent] - Default agent name
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"lang": "ja",
|
|
8
8
|
"type": "webapp/cakephp2",
|
|
9
9
|
"limits": {
|
|
10
|
-
"designTimeoutMs": 900000
|
|
10
|
+
"designTimeoutMs": 900000,
|
|
11
|
+
"concurrency": 3
|
|
11
12
|
},
|
|
12
13
|
"documentStyle": {
|
|
13
14
|
"purpose": "developer-guide",
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"claude": {
|
|
26
27
|
"name": "claude-cli",
|
|
27
28
|
"command": "claude",
|
|
28
|
-
"args": ["--model", "sonnet", "-p", "{{PROMPT}}"]
|
|
29
|
+
"args": ["--model", "sonnet", "-p", "{{PROMPT}}"],
|
|
30
|
+
"systemPromptFlag": "--system-prompt"
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
"agentsUnchanged": "[upgrade] AGENTS.md SDD section unchanged.",
|
|
103
103
|
"noChanges": "[upgrade] All files are up to date.",
|
|
104
104
|
"dryRunFooter": "[upgrade] DRY-RUN: no files were modified. Run without --dry-run to apply.",
|
|
105
|
-
"done": "[upgrade] Upgrade complete."
|
|
105
|
+
"done": "[upgrade] Upgrade complete.",
|
|
106
|
+
"hintSystemPromptFlag": "[upgrade] Hint: Add \"systemPromptFlag\": \"{{flag}}\" to providers.{{provider}} to enable per-file async processing in forge."
|
|
106
107
|
},
|
|
107
108
|
"common": {
|
|
108
109
|
"choicePrompt": "> ({{min}}-{{max}}): ",
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
"agentsUnchanged": "[upgrade] AGENTS.md の SDD セクションに変更はありません。",
|
|
103
103
|
"noChanges": "[upgrade] すべてのファイルは最新です。",
|
|
104
104
|
"dryRunFooter": "[upgrade] DRY-RUN: ファイルは変更されませんでした。--dry-run なしで実行してください。",
|
|
105
|
-
"done": "[upgrade] アップグレード完了。"
|
|
105
|
+
"done": "[upgrade] アップグレード完了。",
|
|
106
|
+
"hintSystemPromptFlag": "[upgrade] ヒント: providers.{{provider}} に \"systemPromptFlag\": \"{{flag}}\" を追加すると、forge のファイル単位非同期処理が有効になります。"
|
|
106
107
|
},
|
|
107
108
|
"common": {
|
|
108
109
|
"choicePrompt": "> ({{min}}-{{max}}): ",
|