sdd-forge 0.1.0-alpha.692 → 0.1.0-alpha.702

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/README.md CHANGED
@@ -129,12 +129,12 @@ See the [configuration reference](docs/configuration.md) for details.
129
129
  <!-- {{data("cli.docs.chapters", {header: "", labels: "Chapter|Summary", ignoreError: true})}} -->
130
130
  | Chapter | Summary |
131
131
  | --- | --- |
132
- | [Tool Overview and Architecture](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/overview.md) | This chapter provides a concise introduction to sdd-forge — a CLI tool that generates structured technical documentat… |
133
- | [Technology Stack and Operations](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/stack_and_ops.md) | This chapter covers the technology stack and operational procedures for sdd-forge, a Node.js CLI tool built with ES M… |
134
- | [Project Structure](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/project_structure.md) | This chapter describes the source tree of sdd-forge, which is organized into five major directories: src/docs (docume… |
135
- | [CLI Command Reference](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/cli_commands.md) | sdd-forge exposes over 35 commands organized across three namespace groups — docs, flow, and check — plus four standa… |
136
- | [Configuration and Customization](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/configuration.md) | sdd-forge reads a single JSON configuration file per project, through which users can control documentation output la… |
137
- | [Internal Design](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/internal_design.md) | sdd-forge is a Node.js CLI tool organized into three primary source layers — src/docs/ for documentation pipeline com… |
132
+ | [Tool Overview and Architecture](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/overview.md) | |
133
+ | [Technology Stack and Operations](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/stack_and_ops.md) | |
134
+ | [Project Structure](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/project_structure.md) | |
135
+ | [CLI Command Reference](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/cli_commands.md) | |
136
+ | [Configuration and Customization](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/configuration.md) | |
137
+ | [Internal Design](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/internal_design.md) | |
138
138
  <!-- {{/data}} -->
139
139
 
140
140
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-forge",
3
- "version": "0.1.0-alpha.692",
3
+ "version": "0.1.0-alpha.702",
4
4
  "description": "Spec-Driven Development tooling for automated documentation generation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -141,7 +141,7 @@ function main(ctx) {
141
141
  console.log([h.usage, "", h.desc, "", "Options:", ` ${o.type}`, ` ${o.force}`, ` ${o.dryRun}`, ` ${o.help}`].join("\n"));
142
142
  return;
143
143
  }
144
- ctx = resolveCommandContext(cli);
144
+ ctx = resolveCommandContext(cli, { commandId: "docs.init" });
145
145
  ctx.force = cli.force;
146
146
  ctx.dryRun = cli.dryRun;
147
147
  }
@@ -218,7 +218,11 @@ function applyBatchJsonToFile(text, textFills, jsonData) {
218
218
  * @returns {{ text: string, filled: number, skipped: number }}
219
219
  */
220
220
  async function processTemplateFileBatch(text, analysis, fileName, agent, timeoutMs, cwd, dryRun, _preamblePatterns, systemPrompt, _filterId, _concurrency, lang, srcRoot, retryCount) {
221
- const directives = parseDirectives(text);
221
+ // cleanText を先に計算してから parseDirectives を呼ぶ。
222
+ // stripFillContent は既存コンテンツを除去するため行数が変わる。
223
+ // parseDirectives の行番号は applyBatchJsonToFile に渡す text と一致させる必要がある。
224
+ const cleanText = stripFillContent(text);
225
+ const directives = parseDirectives(cleanText);
222
226
  const textFills = directives.filter((d) => d.type === "text");
223
227
 
224
228
  if (textFills.length === 0) return { text, filled: 0, skipped: 0 };
@@ -227,8 +231,6 @@ async function processTemplateFileBatch(text, analysis, fileName, agent, timeout
227
231
  const hasDeep = textFills.some((d) => d.params?.mode === "deep");
228
232
  const batchMode = hasDeep ? "deep" : "light";
229
233
  const enriched = getEnrichedContext(analysis, fileName, batchMode, srcRoot);
230
-
231
- const cleanText = stripFillContent(text);
232
234
  let prompt = buildBatchPrompt(fileName, cleanText, textFills, lang);
233
235
  if (enriched) {
234
236
  prompt = enriched + "\n\n" + prompt;
package/src/docs.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import fs from "fs";
9
9
  import path from "path";
10
10
  import { PKG_DIR } from "./lib/cli.js";
11
+ import { Logger } from "./lib/log.js";
11
12
  import { resolveCommandContext } from "./docs/lib/command-context.js";
12
13
  import { resolveOutputConfig } from "./lib/types.js";
13
14
  import { EXIT_ERROR } from "./lib/exit-codes.js";
@@ -99,11 +100,13 @@ if (subCmd === "build") {
99
100
 
100
101
  try {
101
102
  // 1. scan
103
+ Logger.getInstance().event("pipeline-step", { step: "scan", phase: "start" });
102
104
  progress.start("scan");
103
105
  await scanMain({ ...baseCtx });
104
106
  progress.stepDone();
105
107
 
106
108
  // 2. enrich
109
+ Logger.getInstance().event("pipeline-step", { step: "enrich", phase: "start" });
107
110
  progress.start("enrich");
108
111
  if (hasAgent) {
109
112
  await enrichMain({ ...baseCtx, commandId: "docs.enrich" });
@@ -122,17 +125,20 @@ if (subCmd === "build") {
122
125
  process.exit(EXIT_ERROR);
123
126
  }
124
127
  } else {
128
+ Logger.getInstance().event("pipeline-step", { step: "init", phase: "start" });
125
129
  progress.start("init");
126
130
  await initMain({ ...baseCtx, force: hasForce, dryRun: isDryRun, commandId: "docs.init" });
127
131
  progress.stepDone();
128
132
  }
129
133
 
130
134
  // 4. data
135
+ Logger.getInstance().event("pipeline-step", { step: "data", phase: "start" });
131
136
  progress.start("data");
132
137
  await dataMain({ ...baseCtx, dryRun: isDryRun });
133
138
  progress.stepDone();
134
139
 
135
140
  // 5. text — delegate file selection, diff detection, and strip to text.js
141
+ Logger.getInstance().event("pipeline-step", { step: "text", phase: "start" });
136
142
  progress.start("text");
137
143
  if (hasAgent) {
138
144
  const textResult = await textMain({ ...baseCtx, dryRun: isDryRun, commandId: "docs.text", force: hasForce });
@@ -145,17 +151,20 @@ if (subCmd === "build") {
145
151
  progress.stepDone();
146
152
 
147
153
  // 6. readme
154
+ Logger.getInstance().event("pipeline-step", { step: "readme", phase: "start" });
148
155
  progress.start("readme");
149
156
  await readmeMain({ ...baseCtx, dryRun: isDryRun, commandId: "docs.readme" });
150
157
  progress.stepDone();
151
158
 
152
159
  // 7. agents
160
+ Logger.getInstance().event("pipeline-step", { step: "agents", phase: "start" });
153
161
  progress.start("agents");
154
162
  await agentsMain({ ...baseCtx, dryRun: isDryRun, commandId: "docs.agents" });
155
163
  progress.stepDone();
156
164
 
157
165
  // 8. Multi-language: generate non-default languages
158
166
  if (outputCfg.isMultiLang) {
167
+ Logger.getInstance().event("pipeline-step", { step: "translate", phase: "start" });
159
168
  progress.start("translate");
160
169
  const nonDefaultLangs = outputCfg.languages.filter((l) => l !== outputCfg.default);
161
170
  const docsDir = baseCtx.docsDir;
@@ -210,6 +219,7 @@ if (subCmd === "build") {
210
219
  progress.done();
211
220
  } catch (err) {
212
221
  progress.done();
222
+ Logger.getInstance().event("error", { message: err.message });
213
223
  console.error(`[build] ERROR: ${err.message}`);
214
224
  process.exit(EXIT_ERROR);
215
225
  }
@@ -442,7 +442,7 @@ export class RunGateCommand extends FlowCommand {
442
442
 
443
443
  // Requirements check via AI
444
444
  const agent = resolveAgent(ctx.config, "spec.gate");
445
- if (!agent) throw new Error("no agent configured for spec.gate");
445
+ if (!agent) throw new Error("no AI agent configured (agent.default or agent.profiles.<name>.spec.gate)");
446
446
 
447
447
  const reqPrompt = buildImplCheckPrompt(specText, diff);
448
448
  const reqResponse = callAgentWithLog(agent, reqPrompt);
@@ -185,7 +185,7 @@ export class RunRetroCommand extends FlowCommand {
185
185
 
186
186
  const agent = resolveAgent(config, "flow.retro");
187
187
  if (!agent) {
188
- throw new Error("no AI agent configured (agent.default or agent.commands.flow.retro)");
188
+ throw new Error("no AI agent configured (agent.default or agent.profiles.<name>.flow.retro)");
189
189
  }
190
190
 
191
191
  // Build prompt and call AI
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { FlowCommand } from "./base-command.js";
11
11
  import { updateStepStatus } from "../../lib/flow-state.js";
12
+ import { Logger } from "../../lib/log.js";
12
13
 
13
14
  export default class SetStepCommand extends FlowCommand {
14
15
  execute(ctx) {
@@ -19,6 +20,7 @@ export default class SetStepCommand extends FlowCommand {
19
20
  }
20
21
 
21
22
  updateStepStatus(ctx.root, id, status);
23
+ Logger.getInstance().event("flow-step-change", { step: id, status });
22
24
 
23
25
  return { id, status };
24
26
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { execFileSync, execFile } from "child_process";
9
+ import { Logger } from "./log.js";
9
10
 
10
11
  /**
11
12
  * Run a command synchronously.
@@ -30,9 +31,11 @@ export function runCmd(cmd, args, opts = {}) {
30
31
  stdio: ["pipe", "pipe", "pipe"],
31
32
  ...(opts.env && { env: opts.env }),
32
33
  });
33
- return { ok: true, status: 0, stdout: String(stdout || ""), stderr: "", signal: null, killed: false };
34
+ const result = { ok: true, status: 0, stdout: String(stdout || ""), stderr: "", signal: null, killed: false };
35
+ if (cmd === "git") Logger.getInstance().git({ cmd: [cmd, ...args], exitCode: 0, stderr: "" });
36
+ return result;
34
37
  } catch (e) {
35
- return {
38
+ const result = {
36
39
  ok: false,
37
40
  status: e.status ?? 1,
38
41
  stdout: String(e.stdout || ""),
@@ -40,6 +43,8 @@ export function runCmd(cmd, args, opts = {}) {
40
43
  signal: e.signal ?? null,
41
44
  killed: e.killed ?? false,
42
45
  };
46
+ if (cmd === "git") Logger.getInstance().git({ cmd: [cmd, ...args], exitCode: result.status, stderr: result.stderr });
47
+ return result;
43
48
  }
44
49
  }
45
50
 
@@ -69,25 +74,28 @@ export function runCmdAsync(cmd, args, opts = {}) {
69
74
  ...(opts.env && { env: opts.env }),
70
75
  },
71
76
  (err, stdout, stderr) => {
77
+ let result;
72
78
  if (err) {
73
- resolve({
79
+ result = {
74
80
  ok: false,
75
81
  status: typeof err.code === "number" ? err.code : 1,
76
82
  stdout: String(stdout || ""),
77
83
  stderr: String(stderr || err.message || ""),
78
84
  signal: err.signal ?? null,
79
85
  killed: err.killed ?? false,
80
- });
86
+ };
81
87
  } else {
82
- resolve({
88
+ result = {
83
89
  ok: true,
84
90
  status: 0,
85
91
  stdout: String(stdout || ""),
86
92
  stderr: String(stderr || ""),
87
93
  signal: null,
88
94
  killed: false,
89
- });
95
+ };
90
96
  }
97
+ if (cmd === "git") Logger.getInstance().git({ cmd: [cmd, ...args], exitCode: result.status, stderr: result.stderr });
98
+ resolve(result);
91
99
  },
92
100
  );
93
101
  });
package/src/lib/skills.js CHANGED
@@ -7,6 +7,12 @@ import path from "path";
7
7
  import { PKG_DIR } from "./cli.js";
8
8
  import { resolveIncludes } from "./include.js";
9
9
 
10
+ /** Canonical path to the bundled main skill templates directory. */
11
+ export const MAIN_SKILLS_TEMPLATES_DIR = path.join(PKG_DIR, "templates", "skills");
12
+
13
+ /** Directories under workRoot where skills are deployed. */
14
+ const SKILL_TARGET_BASES = [".agents", ".claude"];
15
+
10
16
  /**
11
17
  * Resolve the skill template file in the given directory.
12
18
  */
@@ -43,8 +49,8 @@ function removeIfSymlink(filePath) {
43
49
  function deploySkillsFromDir({ templatesDir, workRoot, lang, dryRun = false }) {
44
50
  if (!fs.existsSync(templatesDir)) return [];
45
51
 
46
- const agentsSkillsDir = path.join(workRoot, ".agents", "skills");
47
- const claudeSkillsDir = path.join(workRoot, ".claude", "skills");
52
+ const agentsSkillsDir = path.join(workRoot, SKILL_TARGET_BASES[0], "skills");
53
+ const claudeSkillsDir = path.join(workRoot, SKILL_TARGET_BASES[1], "skills");
48
54
 
49
55
  const skillDirs = fs.readdirSync(templatesDir, { withFileTypes: true })
50
56
  .filter((d) => d.isDirectory())
@@ -112,7 +118,7 @@ function deploySkillsFromDir({ templatesDir, workRoot, lang, dryRun = false }) {
112
118
  */
113
119
  export function deploySkills(workRoot, lang, opts = {}) {
114
120
  return deploySkillsFromDir({
115
- templatesDir: path.join(PKG_DIR, "templates", "skills"),
121
+ templatesDir: MAIN_SKILLS_TEMPLATES_DIR,
116
122
  workRoot,
117
123
  lang,
118
124
  dryRun: opts.dryRun,
@@ -139,3 +145,49 @@ export function deployProjectSkills(workRoot, templatesDir, lang, opts = {}) {
139
145
  dryRun: opts.dryRun,
140
146
  });
141
147
  }
148
+
149
+ /**
150
+ * Remove sdd-forge.* skill directories from .claude/skills/ and .agents/skills/
151
+ * that are no longer present in any of the provided template directories.
152
+ *
153
+ * Only directories whose names start with "sdd-forge." are considered.
154
+ * Skills found in any of the validTemplatesDirs are kept; all others are removed.
155
+ *
156
+ * @param {string} workRoot Project root directory
157
+ * @param {string[]} validTemplatesDirs All active skill template directories (main + experimental)
158
+ * @param {object} [opts]
159
+ * @param {boolean} [opts.dryRun=false]
160
+ * @returns {{ name: string, status: "removed" }[]}
161
+ */
162
+ export function cleanupObsoleteSkills(workRoot, validTemplatesDirs, opts = {}) {
163
+ const { dryRun = false } = opts;
164
+
165
+ const validNames = new Set(
166
+ validTemplatesDirs.flatMap((dir) => {
167
+ if (!fs.existsSync(dir)) return [];
168
+ return fs.readdirSync(dir, { withFileTypes: true })
169
+ .filter((d) => d.isDirectory())
170
+ .map((d) => d.name);
171
+ })
172
+ );
173
+
174
+ const obsoleteNames = new Set();
175
+ for (const base of SKILL_TARGET_BASES) {
176
+ const skillsDir = path.join(workRoot, base, "skills");
177
+ if (!fs.existsSync(skillsDir)) continue;
178
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
179
+ if (!entry.isDirectory() || !entry.name.startsWith("sdd-forge.")) continue;
180
+ if (!validNames.has(entry.name)) obsoleteNames.add(entry.name);
181
+ }
182
+ }
183
+
184
+ if (!dryRun) {
185
+ for (const name of obsoleteNames) {
186
+ for (const base of SKILL_TARGET_BASES) {
187
+ fs.rmSync(path.join(workRoot, base, "skills", name), { recursive: true, force: true });
188
+ }
189
+ }
190
+ }
191
+
192
+ return [...obsoleteNames].map((name) => ({ name, status: "removed" }));
193
+ }
package/src/lib/types.js CHANGED
@@ -4,6 +4,8 @@
4
4
  * JSDoc 型定義と config / context のバリデーション関数。
5
5
  */
6
6
 
7
+ import { BUILTIN_PROVIDERS } from "./agent.js";
8
+
7
9
  // ---------------------------------------------------------------------------
8
10
  // JSDoc 型定義
9
11
  // ---------------------------------------------------------------------------
@@ -63,7 +65,7 @@
63
65
  * @property {number} [timeout] - Agent execution timeout in seconds
64
66
  * @property {number} [retryCount] - Retry count for docs enrich agent calls
65
67
  * @property {Object<string, AgentProvider>} [providers] - Agent provider definitions
66
- * @property {Object} [commands] - Per-command agent and profile overrides
68
+ * @property {Object<string, Object<string, string>>} [profiles] - Named profiles mapping commandId prefixes to provider keys
67
69
  */
68
70
 
69
71
  /**
@@ -345,6 +347,37 @@ export function validateConfig(raw) {
345
347
  }
346
348
  }
347
349
 
350
+ // agent.profiles (省略可)
351
+ if (raw.agent?.profiles != null) {
352
+ if (typeof raw.agent.profiles !== "object" || Array.isArray(raw.agent.profiles)) {
353
+ errors.push("'agent.profiles' must be an object");
354
+ } else {
355
+ const allProviders = { ...BUILTIN_PROVIDERS, ...(raw.agent?.providers || {}) };
356
+ for (const [profileName, profile] of Object.entries(raw.agent.profiles)) {
357
+ if (typeof profile !== "object" || profile == null || Array.isArray(profile)) {
358
+ errors.push(`'agent.profiles.${profileName}' must be an object`);
359
+ continue;
360
+ }
361
+ for (const [commandId, providerKey] of Object.entries(profile)) {
362
+ if (typeof providerKey !== "string") {
363
+ errors.push(`'agent.profiles.${profileName}.${commandId}' must be a string`);
364
+ } else if (!allProviders[providerKey]) {
365
+ errors.push(`'agent.profiles.${profileName}.${commandId}': unknown provider "${providerKey}"`);
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ // agent.useProfile (省略可)
373
+ if (raw.agent?.useProfile != null) {
374
+ if (typeof raw.agent.useProfile !== "string") {
375
+ errors.push("'agent.useProfile' must be a string");
376
+ } else if (raw.agent.profiles && !raw.agent.profiles[raw.agent.useProfile]) {
377
+ errors.push(`'agent.useProfile': profile "${raw.agent.useProfile}" is not defined in agent.profiles`);
378
+ }
379
+ }
380
+
348
381
  if (errors.length > 0) {
349
382
  throw new Error(`Config validation failed:\n - ${errors.join("\n - ")}`);
350
383
  }
@@ -75,6 +75,7 @@
75
75
  "dryRunHeader": "[upgrade] DRY-RUN: showing changes without writing files.",
76
76
  "skillUpdated": "[upgrade] skill updated: {{name}}/SKILL.md",
77
77
  "skillUnchanged": "[upgrade] skill unchanged: {{name}}/SKILL.md",
78
+ "skillRemoved": "[upgrade] skill removed (obsolete): {{name}}",
78
79
  "agentsUpdated": "[upgrade] AGENTS.md SDD section updated.",
79
80
  "agentsNotFound": "[upgrade] AGENTS.md not found or has no SDD section. Skipped.",
80
81
  "agentsUnchanged": "[upgrade] AGENTS.md SDD section unchanged.",
@@ -75,6 +75,7 @@
75
75
  "dryRunHeader": "[upgrade] DRY-RUN: ファイルを変更せず差分を表示します。",
76
76
  "skillUpdated": "[upgrade] スキル更新: {{name}}/SKILL.md",
77
77
  "skillUnchanged": "[upgrade] スキル変更なし: {{name}}/SKILL.md",
78
+ "skillRemoved": "[upgrade] スキル削除(廃止): {{name}}",
78
79
  "agentsUpdated": "[upgrade] AGENTS.md の SDD セクションを更新しました。",
79
80
  "agentsNotFound": "[upgrade] AGENTS.md が見つからないか SDD セクションがありません。スキップしました。",
80
81
  "agentsUnchanged": "[upgrade] AGENTS.md の SDD セクションに変更はありません。",
package/src/sdd-forge.js CHANGED
@@ -37,7 +37,7 @@ if (!subCmd || subCmd === "-h" || subCmd === "--help") {
37
37
 
38
38
  // Initialize Logger singleton (best-effort — config may not exist yet)
39
39
  try {
40
- const { loadConfig } = await import("./lib/config.js");
40
+ const { loadConfig, sddConfigPath } = await import("./lib/config.js");
41
41
  const root = repoRoot();
42
42
  const cfg = loadConfig(root);
43
43
  const entryCommand = rawArgs.join(" ");
@@ -46,6 +46,7 @@ try {
46
46
  process.stderr.write("[sdd-forge] WARN: cfg.logs.prompts is deprecated. Use cfg.logs.enabled instead.\n");
47
47
  }
48
48
  Logger.getInstance().init(root, cfg, { entryCommand });
49
+ Logger.getInstance().event("config-loaded", { path: sddConfigPath(root), keys: Object.keys(cfg) });
49
50
  } catch (err) {
50
51
  /* pre-setup or missing config — Logger stays uninitialized */
51
52
  if (err?.code !== "ERR_MISSING_FILE") process.stderr.write(`[sdd-forge] Logger init failed: ${err?.message}\n`);
@@ -12,40 +12,46 @@
12
12
  }
13
13
  },
14
14
  "agent": {
15
- "default": "claude",
15
+ "default": "claude/sonnet",
16
16
  "workDir": ".tmp",
17
17
  "timeout": 300,
18
18
  "retryCount": 1,
19
19
  "providers": {
20
- "claude": {
20
+ "claude/sonnet": {
21
21
  "command": "claude",
22
- "args": ["-p", "{{PROMPT}}"],
22
+ "args": ["-p", "{{PROMPT}}", "--model", "sonnet"],
23
23
  "systemPromptFlag": "--system-prompt",
24
- "jsonOutputFlag": "--output-format json",
25
- "profiles": {
26
- "default": ["--model", "sonnet"],
27
- "opus": ["--model", "opus"],
28
- "sonnet": ["--model", "sonnet"]
29
- }
24
+ "jsonOutputFlag": "--output-format json"
30
25
  },
31
- "codex": {
26
+ "claude/opus": {
27
+ "command": "claude",
28
+ "args": ["-p", "{{PROMPT}}", "--model", "opus"],
29
+ "systemPromptFlag": "--system-prompt",
30
+ "jsonOutputFlag": "--output-format json"
31
+ },
32
+ "codex/gpt-5.4": {
32
33
  "command": "codex",
33
34
  "args": ["exec", "--full-auto", "-C", ".tmp", "{{PROMPT}}"],
34
- "profiles": {
35
- "default": []
36
- }
35
+ "jsonOutputFlag": "--json"
37
36
  }
38
37
  },
39
- "commands": {
40
- "docs.enrich": { "agent": "claude", "profile": "opus" },
41
- "docs.text": { "agent": "claude", "profile": "opus" },
42
- "docs.forge": { "agent": "claude", "profile": "opus" },
43
- "docs.readme": { "agent": "claude", "profile": "sonnet" },
44
- "docs.agents": { "agent": "claude", "profile": "opus" },
45
- "spec.gate": { "agent": "claude", "profile": "sonnet" },
46
- "flow.review.draft": { "agent": "codex" },
47
- "flow.review.final": { "agent": "claude", "profile": "opus" },
48
- "docs.translate": { "agent": "codex" }
38
+ "profiles": {
39
+ "default": {
40
+ "docs.init": "claude/sonnet",
41
+ "docs.enrich": "claude/opus",
42
+ "docs.text": "claude/opus",
43
+ "docs.forge": "claude/opus",
44
+ "docs.readme": "claude/sonnet",
45
+ "docs.agents": "claude/opus",
46
+ "docs.translate": "codex/gpt-5.4",
47
+ "spec.gate": "claude/sonnet",
48
+ "flow.review.draft": "codex/gpt-5.4",
49
+ "flow.review.final": "claude/opus",
50
+ "flow.review.test": "claude/sonnet",
51
+ "flow.review.spec": "claude/sonnet",
52
+ "flow.retro": "claude/sonnet",
53
+ "context.search": "claude/sonnet"
54
+ }
49
55
  }
50
56
  }
51
57
  }
@@ -8,6 +8,6 @@ Before presenting any choice to the user, you MUST run `sdd-forge flow get statu
8
8
  - Continue to the next step without waiting for user input only when `autoApprove: true`.
9
9
  - If a step fails (command error, gate FAIL, test failure), apply the retry limits defined in each skill. If the retry limit is reached, STOP and return control to the user.
10
10
 
11
- **NEVER run `sdd-forge flow set auto on` yourself.** Only the user can enable autoApprove mode (via `/sdd-forge.flow-auto-on` or explicit instruction). The AI reads `autoApprove` from flow.json but never writes it.
11
+ **NEVER run `sdd-forge flow set auto on` yourself.** Only the user can enable autoApprove mode (via `/sdd-forge.flow-auto` or explicit instruction). The AI reads `autoApprove` from flow.json but never writes it.
12
12
 
13
- **NEVER chain or background `sdd-forge` commands.** Each `sdd-forge` command must be run as a separate, foreground Bash invocation. Do not use `&&`, `||`, `;`, pipes, or `run_in_background`. Every command's result determines the next action, so it must complete and be read before proceeding.
13
+ **NEVER chain or background `sdd-forge` commands.** Each `sdd-forge` command must be run as a separate, foreground Bash invocation. Do not use `&&`, `||`, `;`, pipes, or `run_in_background`. If a command nevertheless ends up in the background (e.g., due to tool behavior), wait for its completion notification before proceeding — do not treat it as complete or advance to the next step until the command's result has been received and read.
@@ -1,14 +1,31 @@
1
1
  ---
2
- name: sdd-forge.flow-auto-on
3
- description: Enable autoApprove mode for the current SDD flow. The AI will automatically select default choices (id=1) and proceed without user confirmation.
2
+ name: sdd-forge.flow-auto
3
+ description: Toggle autoApprove mode for the current SDD flow. Use "on" to enable (default) or "off" to disable.
4
4
  ---
5
5
 
6
- # SDD Flow Auto ON
6
+ # SDD Flow Auto
7
7
 
8
- Enable autoApprove mode and continue the current flow automatically.
8
+ Toggle autoApprove mode for the current SDD flow.
9
+
10
+ **Usage:** `/sdd-forge.flow-auto [on|off]`
11
+ - No argument → treated as `on`
12
+ - `on` → enable autoApprove and continue the flow automatically
13
+ - `off` → disable autoApprove
14
+ - Any other argument → show error and stop
9
15
 
10
16
  ## Procedure
11
17
 
18
+ ### If argument is `off`
19
+
20
+ 1. Disable autoApprove.
21
+ - Run `sdd-forge flow set auto off`.
22
+ - If it fails (e.g. no active flow), display the error message and STOP.
23
+
24
+ 2. Confirm.
25
+ - Display: "autoApprove mode has been disabled. The AI will ask for confirmation at each step."
26
+
27
+ ### If argument is `on` or no argument
28
+
12
29
  1. Check flow state.
13
30
  - Run `sdd-forge flow get status`.
14
31
  - If the command fails or returns `ok: false`, display: "No active flow. Start a flow first with `/sdd-forge.flow-plan`." and STOP.
@@ -29,3 +46,7 @@ Enable autoApprove mode and continue the current flow automatically.
29
46
  - If all plan and impl steps are `done` but finalize-phase steps (commit, push, merge, pr-create, branch-cleanup) have any not `done` → invoke `/sdd-forge.flow-finalize`
30
47
  - If all steps are `done` → display "All steps are already complete." and STOP.
31
48
  - Use the Skill tool to invoke the determined skill.
49
+
50
+ ### If argument is anything else
51
+
52
+ - Display: "Unknown argument: '<argument>'. Usage: /sdd-forge.flow-auto [on|off]" and STOP.
@@ -82,7 +82,7 @@ Available status values: `pending`, `in_progress`, `done`, `skipped`
82
82
 
83
83
  - Do not run `sdd-forge flow run finalize` if resolve-context reports `dirty: true` and commit step is not included.
84
84
  - Do not proceed to next step without user confirmation.
85
- - **NEVER chain or background `sdd-forge` commands.** Each `sdd-forge` command must be run as a separate, foreground Bash invocation. Do not use `&&`, `||`, `;`, pipes, or `run_in_background`. Every command's result determines the next action, so it must complete and be read before proceeding.
85
+ - **NEVER chain or background `sdd-forge` commands.** Each `sdd-forge` command must be run as a separate, foreground Bash invocation. Do not use `&&`, `||`, `;`, pipes, or `run_in_background`. If a command nevertheless ends up in the background (e.g., due to tool behavior), wait for its completion notification before proceeding — do not treat it as complete or advance to the next step until the command's result has been received and read.
86
86
 
87
87
  **autoApprove exception:** When `autoApprove: true`, the rule "do not proceed to next step without user confirmation" does NOT apply. All other hard stops remain in effect.
88
88
 
package/src/upgrade.js CHANGED
@@ -19,7 +19,7 @@ import { repoRoot, parseArgs } from "./lib/cli.js";
19
19
  import { EXIT_ERROR } from "./lib/exit-codes.js";
20
20
  import { loadConfig, sddConfigPath } from "./lib/config.js";
21
21
  import { translate } from "./lib/i18n.js";
22
- import { deploySkills, deployProjectSkills } from "./lib/skills.js";
22
+ import { deploySkills, deployProjectSkills, cleanupObsoleteSkills, MAIN_SKILLS_TEMPLATES_DIR } from "./lib/skills.js";
23
23
 
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -65,7 +65,18 @@ async function main() {
65
65
  console.log(t("ui:upgrade.dryRunHeader"));
66
66
  }
67
67
 
68
+ function logSkillResults(results) {
69
+ for (const { name, status } of results) {
70
+ if (status === "updated") {
71
+ console.log(t("ui:upgrade.skillUpdated", { name }));
72
+ } else {
73
+ console.log(t("ui:upgrade.skillUnchanged", { name }));
74
+ }
75
+ }
76
+ }
77
+
68
78
  // 1. Skills upgrade
79
+ const validTemplatesDirs = [MAIN_SKILLS_TEMPLATES_DIR];
69
80
  let skillResults;
70
81
  try {
71
82
  skillResults = deploySkills(root, config.lang, { dryRun });
@@ -73,28 +84,23 @@ async function main() {
73
84
  console.error(`upgrade failed: ${e.message}`);
74
85
  process.exit(EXIT_ERROR);
75
86
  }
76
- for (const { name, status } of skillResults) {
77
- if (status === "updated") {
78
- console.log(t("ui:upgrade.skillUpdated", { name }));
79
- } else {
80
- console.log(t("ui:upgrade.skillUnchanged", { name }));
81
- }
82
- }
87
+ logSkillResults(skillResults);
83
88
 
84
89
  // 1b. Experimental skills (opt-in via config flags)
85
90
  if (config.experimental?.workflow?.enable === true) {
86
91
  const expDir = path.join(root, "experimental", "workflow", "templates", "skills");
92
+ validTemplatesDirs.push(expDir);
87
93
  const expResults = deployProjectSkills(root, expDir, config.lang, { dryRun });
88
- for (const { name, status } of expResults) {
89
- if (status === "updated") {
90
- console.log(t("ui:upgrade.skillUpdated", { name }));
91
- } else {
92
- console.log(t("ui:upgrade.skillUnchanged", { name }));
93
- }
94
- }
94
+ logSkillResults(expResults);
95
95
  skillResults.push(...expResults);
96
96
  }
97
97
 
98
+ // 1c. Remove obsolete sdd-forge.* skills no longer in any template directory
99
+ const removedSkills = cleanupObsoleteSkills(root, validTemplatesDirs, { dryRun });
100
+ for (const { name } of removedSkills) {
101
+ console.log(t("ui:upgrade.skillRemoved", { name }));
102
+ }
103
+
98
104
  // 2. Migrate chapters format (string[] → object[])
99
105
  const configPath = sddConfigPath(root);
100
106
  try {
@@ -111,7 +117,7 @@ async function main() {
111
117
  }
112
118
 
113
119
  // Summary
114
- const hasChanges = skillResults.some((r) => r.status === "updated");
120
+ const hasChanges = skillResults.some((r) => r.status === "updated") || removedSkills.length > 0;
115
121
  if (!hasChanges) {
116
122
  console.log(t("ui:upgrade.noChanges"));
117
123
  } else if (dryRun) {
@@ -1,17 +0,0 @@
1
- ---
2
- name: sdd-forge.flow-auto-off
3
- description: Disable autoApprove mode for the current SDD flow. The AI will resume asking the user for confirmation at each step.
4
- ---
5
-
6
- # SDD Flow Auto OFF
7
-
8
- Disable autoApprove mode.
9
-
10
- ## Procedure
11
-
12
- 1. Disable autoApprove.
13
- - Run `sdd-forge flow set auto off`.
14
- - If it fails (e.g. no active flow), display the error message and STOP.
15
-
16
- 2. Confirm.
17
- - Display: "autoApprove mode has been disabled. The AI will ask for confirmation at each step."