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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +9 -15
  2. package/package.json +6 -3
  3. package/src/AGENTS.md +1 -1
  4. package/src/api.js +12 -0
  5. package/src/docs/commands/agents.js +1 -1
  6. package/src/docs/commands/data.js +2 -2
  7. package/src/docs/commands/forge.js +1 -1
  8. package/src/docs/commands/init.js +2 -1
  9. package/src/docs/commands/readme.js +2 -1
  10. package/src/docs/commands/review.js +2 -2
  11. package/src/docs/commands/scan.js +2 -5
  12. package/src/docs/commands/text.js +2 -2
  13. package/src/docs/commands/translate.js +1 -1
  14. package/src/docs/data/docs.js +2 -2
  15. package/src/docs/lib/command-context.js +3 -1
  16. package/src/docs/lib/resolver-factory.js +12 -8
  17. package/src/docs/lib/template-merger.js +11 -7
  18. package/src/flow/commands/report.js +68 -12
  19. package/src/flow/commands/review.js +6 -6
  20. package/src/flow/lib/get-check.js +4 -5
  21. package/src/flow/lib/get-context.js +3 -2
  22. package/src/flow/lib/get-guardrail.js +2 -2
  23. package/src/flow/lib/get-prompt.js +7 -25
  24. package/src/flow/lib/get-status.js +53 -26
  25. package/src/flow/lib/get-test-result.js +90 -0
  26. package/src/flow/lib/run-finalize.js +41 -2
  27. package/src/flow/lib/run-gate.js +70 -12
  28. package/src/flow/lib/run-impl-confirm.js +5 -1
  29. package/src/flow/lib/run-prepare-spec.js +15 -2
  30. package/src/flow/lib/run-resume.js +2 -0
  31. package/src/flow/lib/run-retro.js +5 -4
  32. package/src/flow/lib/run-review.js +6 -0
  33. package/src/flow/lib/set-auto.js +3 -2
  34. package/src/flow/lib/set-init.js +34 -0
  35. package/src/flow/lib/set-issue.js +8 -3
  36. package/src/flow/lib/set-metric.js +5 -6
  37. package/src/flow/lib/set-req.js +10 -3
  38. package/src/flow/lib/set-step.js +5 -0
  39. package/src/flow/lib/set-summary.js +10 -1
  40. package/src/flow/registry.js +29 -8
  41. package/src/flow.js +89 -29
  42. package/src/help.js +2 -0
  43. package/src/lib/agent.js +10 -10
  44. package/src/lib/config.js +16 -3
  45. package/src/lib/constants.js +109 -0
  46. package/src/lib/flow-state.js +246 -13
  47. package/src/lib/log.js +15 -2
  48. package/src/lib/presets.js +63 -7
  49. package/src/lib/process.js +6 -3
  50. package/src/lib/types.js +1 -1
  51. package/src/loader.js +36 -0
  52. package/src/locale/en/ui.json +1 -0
  53. package/src/locale/ja/ui.json +1 -0
  54. package/src/metrics/commands/token.js +416 -0
  55. package/src/metrics.js +38 -0
  56. package/src/presets/cakephp2/data/config.js +11 -9
  57. package/src/presets/cakephp2/data/email.js +5 -4
  58. package/src/presets/cakephp2/data/libs.js +4 -3
  59. package/src/presets/cakephp2/data/tests.js +2 -1
  60. package/src/presets/cakephp2/data/views.js +2 -1
  61. package/src/presets/laravel/data/commands.js +2 -1
  62. package/src/presets/laravel/data/config.js +6 -5
  63. package/src/presets/laravel/data/controllers.js +2 -1
  64. package/src/presets/laravel/data/models.js +2 -1
  65. package/src/presets/laravel/data/routes.js +2 -1
  66. package/src/presets/laravel/data/tables.js +2 -1
  67. package/src/presets/lib/path-match.js +36 -0
  68. package/src/presets/mysql/NOTICE +26 -0
  69. package/src/presets/mysql/guardrail.json +108 -0
  70. package/src/presets/mysql/preset.json +6 -0
  71. package/src/presets/nextjs/data/components.js +3 -2
  72. package/src/presets/nextjs/data/routes.js +3 -2
  73. package/src/presets/symfony/data/commands.js +2 -1
  74. package/src/presets/symfony/data/config.js +5 -4
  75. package/src/presets/symfony/data/controllers.js +2 -1
  76. package/src/presets/symfony/data/entities.js +2 -1
  77. package/src/presets/symfony/data/routes.js +2 -1
  78. package/src/presets/symfony/data/tables.js +2 -1
  79. package/src/presets/webapp/NOTICE +16 -0
  80. package/src/presets/webapp/guardrail.json +32 -0
  81. package/src/sdd-forge.js +13 -3
  82. package/src/setup.js +11 -11
  83. package/src/templates/config.example.json +7 -7
  84. package/src/templates/partials/core-principle.md +6 -0
  85. package/src/templates/skills/sdd-forge.flow-finalize/SKILL.md +2 -0
  86. package/src/templates/skills/sdd-forge.flow-impl/SKILL.md +3 -1
  87. package/src/templates/skills/sdd-forge.flow-plan/SKILL.md +37 -13
  88. package/src/templates/skills/sdd-forge.flow-status/SKILL.md +2 -1
  89. package/src/upgrade.js +2 -2
  90. package/src/flow/lib/phases.js +0 -15
package/README.md CHANGED
@@ -88,9 +88,9 @@ If you already have source code, generate documentation to get a complete pictur
88
88
 
89
89
  | Command | Phase |
90
90
  |---|---|
91
- | `$sdd-forge flow prepare --title "..." --base main` | plan (create spec + branch) |
92
- | `$sdd-forge flow run review` | implement (AI code review) |
93
- | `$sdd-forge flow run finalize --mode all` | finalize (wrap-up) |
91
+ | `$sdd-forge.flow-plan` | plan (specification) |
92
+ | `$sdd-forge.flow-impl` | implement (coding + review) |
93
+ | `$sdd-forge.flow-finalize` | finalize (commit, merge, docs sync, cleanup) |
94
94
 
95
95
  ## Commands
96
96
 
@@ -98,12 +98,6 @@ If you already have source code, generate documentation to get a complete pictur
98
98
  |---|---|
99
99
  | `setup` | Register project and generate config |
100
100
  | `docs build` | Run the full documentation pipeline |
101
- | `docs readme` | Generate `README.md` from `docs/` |
102
- | `docs review` | Check documentation quality |
103
- | `flow prepare` | Create spec and branch |
104
- | `flow get status` | Show flow progress |
105
- | `presets` | List available presets |
106
- | `help` | Show all commands |
107
101
 
108
102
  See `sdd-forge help` or the [command reference](docs/cli_commands.md) for the full list.
109
103
 
@@ -129,12 +123,12 @@ See the [configuration reference](docs/configuration.md) for details.
129
123
  <!-- {{data("cli.docs.chapters", {header: "", labels: "Chapter|Summary", ignoreError: true})}} -->
130
124
  | Chapter | Summary |
131
125
  | --- | --- |
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) | |
126
+ | [Tool Overview and Architecture](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/overview.md) | This chapter introduces sdd-forge, a CLI tool that automates documentation generation from source code analysis and e… |
127
+ | [Technology Stack and Operations](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/stack_and_ops.md) | This chapter covers the technology stack, dependency management, deployment, and operations procedures for sdd-forge,… |
128
+ | [Project Structure](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/project_structure.md) | This chapter describes the overall directory organization of the sdd-forge project, which is structured around seven … |
129
+ | [CLI Command Reference](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/cli_commands.md) | This chapter documents 8 CLI entrypoints and routed commands in the analyzed files: setup, check, check freshness, do… |
130
+ | [Configuration and Customization](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/configuration.md) | sdd-forge is configured through a single project-level JSON file (.sdd-forge/config.json) and optionally extended by … |
131
+ | [Internal Design](https://github.com/SpreadWorks/sdd-forge/blob/main/docs/internal_design.md) | This chapter describes the internal structure of sdd-forge: a layered CLI tool built entirely on Node.js built-in mod… |
138
132
  <!-- {{/data}} -->
139
133
 
140
134
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-forge",
3
- "version": "0.1.0-alpha.702",
3
+ "version": "0.1.0-alpha.759",
4
4
  "description": "Spec-Driven Development tooling for automated documentation generation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,12 +10,16 @@
10
10
  "bin": {
11
11
  "sdd-forge": "./src/sdd-forge.js"
12
12
  },
13
+ "exports": {
14
+ ".": "./src/sdd-forge.js",
15
+ "./api": "./src/api.js"
16
+ },
13
17
  "files": [
14
18
  "src/",
15
19
  "!src/presets/*/tests/"
16
20
  ],
17
21
  "engines": {
18
- "node": ">=18.0.0"
22
+ "node": ">=18.19.0"
19
23
  },
20
24
  "keywords": [
21
25
  "sdd",
@@ -25,7 +29,6 @@
25
29
  "technical-docs"
26
30
  ],
27
31
  "scripts": {
28
- "test": "node tests/run.js",
29
32
  "test:unit": "node tests/run.js --scope unit",
30
33
  "test:e2e": "node tests/run.js --scope e2e",
31
34
  "test:acceptance": "node tests/acceptance/run.js"
package/src/AGENTS.md CHANGED
@@ -43,7 +43,7 @@ src/
43
43
  │ ├── set.js set サブディスパッチャ
44
44
  │ ├── run.js run サブディスパッチャ
45
45
  │ ├── get/ status, resolve-context, check, prompt, qa-count, guardrail, issue
46
- │ ├── set/ step, request, issue, note, summary, req, metric, redo
46
+ │ ├── set/ step, request, issue, note, summary, req, metric
47
47
  │ ├── run/ prepare-spec, gate, review, impl-confirm, finalize, sync
48
48
  │ └── commands/ 内部ヘルパー(merge, cleanup, review の実体)
49
49
  ├── spec/commands/ init, gate, guardrail(flow/run/prepare-spec, gate が内部で呼ぶ)
package/src/api.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * src/api.js — sdd-forge public API entry point
3
+ *
4
+ * Re-exports the base classes needed by external presets and
5
+ * .sdd-forge/data/ DataSource overrides.
6
+ *
7
+ * Usage (from external preset files):
8
+ * import { DataSource, Scannable, AnalysisEntry } from 'sdd-forge/api';
9
+ */
10
+ export { DataSource } from "./docs/lib/data-source.js";
11
+ export { Scannable } from "./docs/lib/scan-source.js";
12
+ export { AnalysisEntry } from "./docs/lib/analysis-entry.js";
@@ -178,7 +178,7 @@ async function main(ctx) {
178
178
 
179
179
  // Load generated docs as context (instead of raw analysis.json)
180
180
  const docsDir = path.join(root, "docs");
181
- const chapterFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters });
181
+ const chapterFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters, projectRoot: root });
182
182
  const docsContent = chapterFiles.map((f) => readText(path.join(docsDir, f))).join("\n\n");
183
183
  const readmeContent = readText(path.join(srcRoot, "README.md"));
184
184
  const combinedDocs = [docsContent, readmeContent].filter(Boolean).join("\n\n---\n\n");
@@ -76,7 +76,7 @@ export function populateFromAnalysis(root, analysis, resolveFn, opts) {
76
76
  const docsDir = path.join(root, "docs");
77
77
  const changedFiles = [];
78
78
 
79
- const docsFiles = getChapterFiles(docsDir, { type: opts?.type, configChapters: opts?.configChapters });
79
+ const docsFiles = getChapterFiles(docsDir, { type: opts?.type, configChapters: opts?.configChapters, projectRoot: root });
80
80
 
81
81
  for (const file of docsFiles) {
82
82
  const filePath = path.join(docsDir, file);
@@ -137,7 +137,7 @@ async function main(ctx) {
137
137
  throw new Error(t("messages:data.resolverFailed", { message: err.message }));
138
138
  }
139
139
 
140
- const docsFiles = getChapterFiles(docsDir, { type, configChapters: ctx.config?.chapters });
140
+ const docsFiles = getChapterFiles(docsDir, { type, configChapters: ctx.config?.chapters, projectRoot: root });
141
141
 
142
142
  // Determine relative path prefix for lang.links context
143
143
  const docsDirRel = path.relative(root, docsDir).replace(/\\/g, "/");
@@ -44,7 +44,7 @@ const DEFAULT_MODE = "local";
44
44
 
45
45
  function getTargetFiles(root, type, configChapters) {
46
46
  const docsDir = path.join(root, "docs");
47
- return getChapterFiles(docsDir, { type, configChapters }).map((f) => `docs/${f}`);
47
+ return getChapterFiles(docsDir, { type, configChapters, projectRoot: root }).map((f) => `docs/${f}`);
48
48
  }
49
49
 
50
50
  /**
@@ -169,12 +169,13 @@ function main(ctx) {
169
169
  ? configLangs
170
170
  : [...configLangs, "en"];
171
171
  const configChapters = config?.chapters;
172
- const chaptersOrder = resolveChaptersOrder(type, configChapters);
172
+ const chaptersOrder = resolveChaptersOrder(type, configChapters, root);
173
173
 
174
174
  const resolutions = resolveTemplates(type, lang, {
175
175
  projectLocalDir,
176
176
  fallbackLangs,
177
177
  chaptersOrder,
178
+ projectRoot: root,
178
179
  });
179
180
 
180
181
  // 解決結果からチャプターを生成(README は除外)
@@ -75,11 +75,12 @@ async function main(ctx) {
75
75
 
76
76
  // ボトムアップでテンプレート解決
77
77
  const configChapters = config?.chapters;
78
- const chaptersOrder = resolveChaptersOrder(type, configChapters);
78
+ const chaptersOrder = resolveChaptersOrder(type, configChapters, root);
79
79
  const resolutions = resolveTemplates(type, lang, {
80
80
  projectLocalDir,
81
81
  fallbackLangs,
82
82
  chaptersOrder,
83
+ projectRoot: root,
83
84
  });
84
85
 
85
86
  const readmeRes = resolutions.find((r) => r.fileName === "README.md");
@@ -114,7 +114,7 @@ function main() {
114
114
  // Discover chapter files
115
115
  let chapterFiles = [];
116
116
  if (fs.existsSync(targetDir) && fs.statSync(targetDir).isDirectory()) {
117
- chapterFiles = getChapterFiles(targetDir, { type, configChapters: config.chapters });
117
+ chapterFiles = getChapterFiles(targetDir, { type, configChapters: config.chapters, projectRoot: root });
118
118
  }
119
119
 
120
120
  if (chapterFiles.length === 0) {
@@ -232,7 +232,7 @@ function main() {
232
232
  reportFail("messages:review.langDirMissing", { lang });
233
233
  continue;
234
234
  }
235
- const langFiles = getChapterFiles(langDir, { type, configChapters: config.chapters });
235
+ const langFiles = getChapterFiles(langDir, { type, configChapters: config.chapters, projectRoot: root });
236
236
  if (langFiles.length === 0) {
237
237
  reportFail("messages:review.langNoChapters", { lang });
238
238
  continue;
@@ -22,7 +22,7 @@ import crypto from "crypto";
22
22
  import { fileURLToPath } from "url";
23
23
  import { runIfDirect } from "../../lib/entrypoint.js";
24
24
  import { repoRoot, parseArgs } from "../../lib/cli.js";
25
- import { sddDataDir, sddOutputDir } from "../../lib/config.js";
25
+ import { sddOutputDir } from "../../lib/config.js";
26
26
  import { collectFiles } from "../lib/scanner.js";
27
27
  import { loadDataSources } from "../lib/data-source-loader.js";
28
28
  import { presetByLeaf, resolveChainSafe, resolveMultiChains } from "../../lib/presets.js";
@@ -289,7 +289,7 @@ async function main(ctx) {
289
289
  const currentFilePaths = new Set(files.map((f) => f.relPath));
290
290
 
291
291
  // 3. Load Scannable DataSources from preset chain
292
- const chains = resolveMultiChains(types);
292
+ const chains = resolveMultiChains(types, root);
293
293
  const seenDirs = new Set();
294
294
 
295
295
  let dataSources = new Map();
@@ -301,9 +301,6 @@ async function main(ctx) {
301
301
  }
302
302
  }
303
303
 
304
- const projectDataDir = sddDataDir(root);
305
- dataSources = await loadScanSources(projectDataDir, dataSources);
306
-
307
304
  // 3b. DataSource hash detection: clear entry hashes for categories whose DataSource changed
308
305
  if (existing) {
309
306
  for (const [name, source] of dataSources) {
@@ -489,7 +489,7 @@ export async function textFillFromAnalysis(root, analysis, commandId, srcRoot, o
489
489
  const docsDir = path.join(root, "docs");
490
490
  const resolvedSrcRoot = srcRoot || root;
491
491
 
492
- const targetFiles = opts?.files || getChapterFiles(docsDir, { type, configChapters: cfg.chapters });
492
+ const targetFiles = opts?.files || getChapterFiles(docsDir, { type, configChapters: cfg.chapters, projectRoot: root });
493
493
 
494
494
  const changedFiles = [];
495
495
  let totalFilled = 0;
@@ -640,7 +640,7 @@ async function main(ctx) {
640
640
  if (ctx.files) {
641
641
  targetFiles = ctx.files;
642
642
  } else {
643
- targetFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: cfg.chapters });
643
+ targetFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: cfg.chapters, projectRoot: root });
644
644
 
645
645
  // Diff-based chapter filtering: skip chapters whose entries are unchanged
646
646
  if (!ctx.force) {
@@ -175,7 +175,7 @@ async function main(ctx) {
175
175
  throw new Error("docs/ directory not found. Run 'sdd-forge init' first.");
176
176
  }
177
177
 
178
- const sourceFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters });
178
+ const sourceFiles = getChapterFiles(docsDir, { type: ctx.type, configChapters: ctx.config?.chapters, projectRoot: root });
179
179
  const readmePath = path.join(root, "README.md");
180
180
  const hasReadme = fs.existsSync(readmePath);
181
181
 
@@ -170,7 +170,7 @@ export default class DocsSource extends DataSource {
170
170
  const docsDir = this._docsDir || path.join(this._root, "docs");
171
171
  if (!fs.existsSync(docsDir)) return null;
172
172
 
173
- const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters });
173
+ const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters, projectRoot: this._root });
174
174
 
175
175
  if (files.length === 0) return null;
176
176
 
@@ -240,7 +240,7 @@ export default class DocsSource extends DataSource {
240
240
  const docsDir = this._docsDir || path.join(this._root, "docs");
241
241
  if (!fs.existsSync(docsDir)) return null;
242
242
 
243
- const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters });
243
+ const files = getChapterFiles(docsDir, { type: this._type, configChapters: this._configChapters, projectRoot: this._root });
244
244
  if (files.length <= 1) return null;
245
245
 
246
246
  // Find current file in the chapter list
@@ -128,6 +128,7 @@ export function loadFullAnalysis(root) {
128
128
  * @param {Object} [options]
129
129
  * @param {string} [options.type] - プロジェクトタイプ(例: "cli/node-cli")
130
130
  * @param {string[]} [options.configChapters] - config.json の chapters 配列(最優先)
131
+ * @param {string} [options.projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
131
132
  * @returns {string[]} ファイル名の配列(順序付き)
132
133
  */
133
134
  export function getChapterFiles(docsDir, options) {
@@ -135,10 +136,11 @@ export function getChapterFiles(docsDir, options) {
135
136
 
136
137
  const type = options?.type;
137
138
  const configChapters = options?.configChapters;
139
+ const projectRoot = options?.projectRoot;
138
140
  const EXCLUDE = new Set(["README.md", "AGENTS.sdd.md", "layout.md"]);
139
141
 
140
142
  if (type || configChapters?.length) {
141
- const chapters = resolveChaptersOrder(type || "base", configChapters);
143
+ const chapters = resolveChaptersOrder(type || "base", configChapters, projectRoot);
142
144
  if (chapters.length > 0) {
143
145
  const existing = chapters.filter((f) => fs.existsSync(path.join(docsDir, f)));
144
146
  if (existing.length > 0) return existing;
@@ -8,7 +8,7 @@
8
8
 
9
9
  import fs from "fs";
10
10
  import path from "path";
11
- import { sddDir, sddDataDir } from "../../lib/config.js";
11
+ import { sddDir } from "../../lib/config.js";
12
12
  import { loadDataSources as loadDataSourcesBase } from "./data-source-loader.js";
13
13
  import { resolveMultiChains, resolveChainSafe } from "../../lib/presets.js";
14
14
  import { createLogger } from "../../lib/progress.js";
@@ -66,7 +66,7 @@ const COMMON_DATA_DIR = path.resolve(
66
66
  * @param {Object} ctx - 共有コンテキスト
67
67
  * @returns {Promise<Map<string, Object>>} DataSource マップ
68
68
  */
69
- async function loadChainDataSources(chain, root, ctx) {
69
+ async function loadChainDataSources(chain, ctx) {
70
70
  let dataSources = await loadDataSources(COMMON_DATA_DIR, ctx);
71
71
 
72
72
  for (const preset of chain) {
@@ -74,10 +74,6 @@ async function loadChainDataSources(chain, root, ctx) {
74
74
  dataSources = await loadDataSources(dataDir, ctx, dataSources);
75
75
  }
76
76
 
77
- // プロジェクト固有 DataSource(最高優先)
78
- const projectDataDir = sddDataDir(root);
79
- dataSources = await loadDataSources(projectDataDir, ctx, dataSources);
80
-
81
77
  return dataSources;
82
78
  }
83
79
 
@@ -92,11 +88,19 @@ async function loadChainDataSources(chain, root, ctx) {
92
88
  * @returns {Promise<{ resolve: (preset: string, source: string, method: string, analysis: Object, labels: string[]) => string|null }>}
93
89
  */
94
90
  export async function createResolver(type, root, opts) {
91
+ // Warn about deprecated .sdd-forge/data/ directory
92
+ if (root) {
93
+ const deprecatedDataDir = path.join(root, ".sdd-forge", "data");
94
+ if (fs.existsSync(deprecatedDataDir)) {
95
+ process.stderr.write(`[sdd-forge] WARN: .sdd-forge/data/ is deprecated. Move DataSources to .sdd-forge/presets/<type>/data/ instead.\n`);
96
+ }
97
+ }
98
+
95
99
  const desc = descFactory(root);
96
100
  const loadOverrides = () => loadOverridesFor(root);
97
101
  const ctx = { desc, loadOverrides, root, docsDir: opts?.docsDir, type, configChapters: opts?.configChapters };
98
102
 
99
- const chains = resolveMultiChains(type);
103
+ const chains = resolveMultiChains(type, root);
100
104
 
101
105
  // 各チェーンの leaf key → DataSource マップ
102
106
  const resolverMap = new Map();
@@ -104,7 +108,7 @@ export async function createResolver(type, root, opts) {
104
108
  const ancestorMap = new Map();
105
109
  for (const chain of chains) {
106
110
  const leafKey = chain[chain.length - 1].key;
107
- const dataSources = await loadChainDataSources(chain, root, ctx);
111
+ const dataSources = await loadChainDataSources(chain, ctx);
108
112
  resolverMap.set(leafKey, dataSources);
109
113
  // chain 内の全プリセットを ancestor マップに登録
110
114
  for (const p of chain) {
@@ -28,9 +28,10 @@ const SPECIAL_FILES = new Set(["README.md", "AGENTS.sdd.md", "layout.md"]);
28
28
  * @param {string} presetKey - preset 名(例: "cakephp2", "node-cli")
29
29
  * @param {string} lang - ロケール(例: "ja")
30
30
  * @param {string|null} [projectLocalDir] - プロジェクトローカルテンプレートディレクトリ
31
+ * @param {string} [projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
31
32
  * @returns {string[]} レイヤーディレクトリ配列(優先度高い順)
32
33
  */
33
- export function buildLayers(presetKey, lang, projectLocalDir) {
34
+ export function buildLayers(presetKey, lang, projectLocalDir, projectRoot) {
34
35
  const layers = [];
35
36
 
36
37
  // 1. project-local(最高優先)
@@ -39,7 +40,7 @@ export function buildLayers(presetKey, lang, projectLocalDir) {
39
40
  }
40
41
 
41
42
  // 2. parent チェーンを leaf → root の順で追加(優先度高い順)
42
- const chain = resolveChainSafe(presetKey);
43
+ const chain = resolveChainSafe(presetKey, projectRoot);
43
44
 
44
45
  // chain は root → leaf の順なので、逆順(leaf → root)で追加
45
46
  for (let i = chain.length - 1; i >= 0; i--) {
@@ -195,20 +196,21 @@ function mergeSourcesAdditive(sources) {
195
196
  * @param {string|null} [opts.projectLocalDir] - プロジェクトローカルテンプレートディレクトリ
196
197
  * @param {string[]} [opts.fallbackLangs] - フォールバック言語リスト
197
198
  * @param {string[]} [opts.chaptersOrder] - preset.json の chapters 順序配列
199
+ * @param {string} [opts.projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
198
200
  * @returns {FileResolution[]}
199
201
  */
200
202
  export function resolveTemplates(typePath, lang, opts = {}) {
201
- const { projectLocalDir, fallbackLangs, chaptersOrder } = opts;
203
+ const { projectLocalDir, fallbackLangs, chaptersOrder, projectRoot } = opts;
202
204
 
203
205
  const types = Array.isArray(typePath) ? typePath : [typePath];
204
206
 
205
207
  // 各チェーンの leaf key ごとにレイヤーセットを構築
206
- const chains = resolveMultiChains(types);
208
+ const chains = resolveMultiChains(types, projectRoot);
207
209
  const chainLayerSets = chains.map((chain) => {
208
210
  const leafKey = chain[chain.length - 1].key;
209
211
  return {
210
212
  leafKey,
211
- layers: buildLayers(leafKey, lang, projectLocalDir),
213
+ layers: buildLayers(leafKey, lang, projectLocalDir, projectRoot),
212
214
  fallbackSets: (fallbackLangs || [])
213
215
  .filter((l) => l !== lang)
214
216
  .map((fbLang) => ({
@@ -217,6 +219,7 @@ export function resolveTemplates(typePath, lang, opts = {}) {
217
219
  leafKey,
218
220
  fbLang,
219
221
  deriveFallbackProjectLocalDir(projectLocalDir, fbLang),
222
+ projectRoot,
220
223
  ),
221
224
  })),
222
225
  };
@@ -359,9 +362,10 @@ function discoverFileNames(layers, fallbackSets, chaptersOrder) {
359
362
  *
360
363
  * @param {string|string[]} presetKeys - preset 名または配列
361
364
  * @param {string[]} [configChapters] - config.json の chapters 配列(最優先)
365
+ * @param {string} [projectRoot] - プロジェクトルート(.sdd-forge/presets/ 検索用)
362
366
  * @returns {string[]} 章ファイル名の順序配列
363
367
  */
364
- export function resolveChaptersOrder(presetKeys, configChapters) {
368
+ export function resolveChaptersOrder(presetKeys, configChapters, projectRoot) {
365
369
  // config.json の chapters が定義されていればプリセットを完全上書き
366
370
  if (configChapters?.length) {
367
371
  // Support both old string[] and new object[] formats
@@ -377,7 +381,7 @@ export function resolveChaptersOrder(presetKeys, configChapters) {
377
381
  const result = [];
378
382
 
379
383
  for (const key of keys) {
380
- const chain = resolveChainSafe(key);
384
+ const chain = resolveChainSafe(key, projectRoot);
381
385
 
382
386
  // chain 内で最も具体的な(leaf 側の)chapters を使用する。
383
387
  // 子が chapters を定義していれば親の chapters は含めない(上書き)。
@@ -11,19 +11,53 @@ import { loadIssueLog } from "../lib/set-issue-log.js";
11
11
  import { pushSection, DIVIDER } from "../../lib/formatter.js";
12
12
 
13
13
  /**
14
- * Aggregate metrics across all phases.
15
- * @param {Object|null} metrics - flow.json metrics object keyed by phase
16
- * @returns {{ docsRead: number, srcRead: number, question: number, redo: number }}
14
+ * Iterate over each phase object in metrics, skipping null/non-object entries.
15
+ * @param {Object|null} metrics
16
+ * @param {(phase: Object) => void} fn
17
17
  */
18
- function aggregateMetrics(metrics) {
19
- const totals = { docsRead: 0, srcRead: 0, question: 0, issueLog: 0 };
20
- if (!metrics) return totals;
18
+ function forEachPhase(metrics, fn) {
19
+ if (!metrics) return;
21
20
  for (const phase of Object.values(metrics)) {
22
21
  if (!phase || typeof phase !== "object") continue;
22
+ fn(phase);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Aggregate activity metrics (docs/src reads, Q&A, issue-log) across all phases.
28
+ * @param {Object|null} metrics - flow.json metrics object keyed by phase
29
+ * @returns {{ docsRead: number, srcRead: number, question: number, issueLog: number }}
30
+ */
31
+ function aggregateActivityMetrics(metrics) {
32
+ const totals = { docsRead: 0, srcRead: 0, question: 0, issueLog: 0 };
33
+ forEachPhase(metrics, (phase) => {
23
34
  for (const key of Object.keys(totals)) {
24
35
  totals[key] += phase[key] || 0;
25
36
  }
26
- }
37
+ });
38
+ return totals;
39
+ }
40
+
41
+ /**
42
+ * Aggregate token/cost metrics across all phases (R3-1, R3-2).
43
+ * @param {Object|null} metrics - flow.json metrics object keyed by phase
44
+ * @returns {{ input: number, output: number, cacheRead: number, cacheCreation: number, cost: number|null, callCount: number }}
45
+ */
46
+ function aggregateTokenMetrics(metrics) {
47
+ const totals = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, cost: null, callCount: 0 };
48
+ forEachPhase(metrics, (phase) => {
49
+ if (phase.tokens) {
50
+ totals.input += phase.tokens.input || 0;
51
+ totals.output += phase.tokens.output || 0;
52
+ totals.cacheRead += phase.tokens.cacheRead || 0;
53
+ totals.cacheCreation += phase.tokens.cacheCreation || 0;
54
+ }
55
+ // R3-2: only accumulate cost when it has been recorded (never treat null as 0)
56
+ if (phase.cost != null) {
57
+ totals.cost = (totals.cost || 0) + phase.cost;
58
+ }
59
+ totals.callCount += phase.callCount || 0;
60
+ });
27
61
  return totals;
28
62
  }
29
63
 
@@ -64,7 +98,8 @@ export function generateReport(input) {
64
98
  };
65
99
 
66
100
  // Metrics
67
- const metrics = aggregateMetrics(state.metrics);
101
+ const metrics = aggregateActivityMetrics(state.metrics);
102
+ const tokenMetrics = aggregateTokenMetrics(state.metrics);
68
103
 
69
104
  // Sync
70
105
  let sync;
@@ -88,7 +123,7 @@ export function generateReport(input) {
88
123
  }
89
124
 
90
125
  // Tests (R1, R3)
91
- const testSummary = state.metrics?.test?.summary;
126
+ const testSummary = state.test?.summary;
92
127
  let tests = null;
93
128
  if (testSummary) {
94
129
  const unit = testSummary.unit || 0;
@@ -97,7 +132,7 @@ export function generateReport(input) {
97
132
  tests = { unit, integration, acceptance, total: unit + integration + acceptance };
98
133
  }
99
134
 
100
- const data = { implementation, retro, issueLog: issueLogData, metrics, tests, sync };
135
+ const data = { implementation, retro, issueLog: issueLogData, metrics, tokenMetrics, tests, sync };
101
136
  const text = formatText(data);
102
137
 
103
138
  return { data, text };
@@ -148,10 +183,31 @@ function formatText(data) {
148
183
  lines.push(" -");
149
184
  }
150
185
 
151
- // Metrics (single line)
186
+ const formatInt = (value) => Number(value || 0).toLocaleString("en-US");
187
+ const metricLine = (label, value) => {
188
+ const dots = ".".repeat(Math.max(1, 28 - label.length));
189
+ return ` ${label} ${dots} ${value}`;
190
+ };
191
+
192
+ // Metrics
152
193
  pushSection(lines, "Metrics", thin);
153
194
  const m = data.metrics;
154
- lines.push(` docs read ${m.docsRead} src read ${m.srcRead} Q&A ${m.question} issue-log ${m.issueLog}`);
195
+ lines.push(metricLine("docs read", formatInt(m.docsRead)));
196
+ lines.push(metricLine("src read", formatInt(m.srcRead)));
197
+ lines.push(metricLine("Q&A", formatInt(m.question)));
198
+ lines.push(metricLine("issue-log", formatInt(m.issueLog)));
199
+
200
+ // Agent metrics (token/cost) — R3-1, R3-2
201
+ if (data.tokenMetrics && data.tokenMetrics.callCount > 0) {
202
+ const t = data.tokenMetrics;
203
+ const costStr = t.cost != null ? `$${t.cost.toFixed(4)}` : "N/A";
204
+ lines.push(metricLine("agent calls", formatInt(t.callCount)));
205
+ lines.push(metricLine("input tokens", formatInt(t.input)));
206
+ lines.push(metricLine("output tokens", formatInt(t.output)));
207
+ lines.push(metricLine("cache-read tokens", formatInt(t.cacheRead)));
208
+ lines.push(metricLine("cache-create tokens", formatInt(t.cacheCreation)));
209
+ lines.push(metricLine("cost", costStr));
210
+ }
155
211
 
156
212
  // Tests (always shown)
157
213
  pushSection(lines, "Tests", thin);
@@ -26,7 +26,7 @@ const callReviewAgent = (agent, prompt, root, systemPrompt) =>
26
26
  callAgentAwaitLog(agent, prompt, undefined, root, { systemPrompt });
27
27
  import { runCmd } from "../../lib/process.js";
28
28
  import { EXIT_ERROR } from "../../lib/exit-codes.js";
29
- import { VALID_PHASES } from "../lib/phases.js";
29
+ import { VALID_PHASES } from "../../lib/constants.js";
30
30
 
31
31
  /** Maximum retry iterations for review auto-fix loops (test and spec). */
32
32
  const MAX_REVIEW_RETRIES = 3;
@@ -499,7 +499,7 @@ async function runTestReview(root, flow, config, dryRun) {
499
499
  process.exit(EXIT_ERROR);
500
500
  }
501
501
 
502
- const agent = loadAgentConfig(config, "flow.review.test");
502
+ const agent = loadAgentConfig(config, "flow.test.review");
503
503
  ensureAgentWorkDir(agent, root);
504
504
 
505
505
  // Step 1: Generate test design
@@ -683,9 +683,9 @@ async function runSpecReview(root, flow, config, dryRun) {
683
683
  console.error(` [spec-review] Warning: failed to load codebase context: ${e.message}`);
684
684
  }
685
685
 
686
- const agent = loadAgentConfig(config, "flow.review.spec");
686
+ const agent = loadAgentConfig(config, "flow.spec.review");
687
687
  ensureAgentWorkDir(agent, root);
688
- const validationAgent = loadAgentConfig(config, "flow.review.final");
688
+ const validationAgent = loadAgentConfig(config, "flow.impl.review.final");
689
689
  ensureAgentWorkDir(validationAgent, root);
690
690
 
691
691
  const { history, finalIssues, verdict } = await runReviewLoop({
@@ -829,7 +829,7 @@ async function main() {
829
829
 
830
830
  // --- Draft phase ---
831
831
  console.error(" [draft] Generating proposals...");
832
- const draftAgent = loadAgentConfig(config, "flow.review.draft");
832
+ const draftAgent = loadAgentConfig(config, "flow.impl.review.draft");
833
833
  ensureAgentWorkDir(draftAgent, root);
834
834
  const draftResult = await callReviewAgent(draftAgent, diff, root, buildDraftSystemPrompt());
835
835
 
@@ -852,7 +852,7 @@ async function main() {
852
852
 
853
853
  // --- Final phase ---
854
854
  console.error(" [final] Validating proposals...");
855
- const finalAgent = loadAgentConfig(config, "flow.review.final");
855
+ const finalAgent = loadAgentConfig(config, "flow.impl.review.final");
856
856
  ensureAgentWorkDir(finalAgent, root);
857
857
  const finalPrompt = [
858
858
  "Validate these refactoring proposals:",
@@ -8,10 +8,9 @@
8
8
 
9
9
  import { runCmd } from "../../lib/process.js";
10
10
  import { isGhAvailable } from "../../lib/git-helpers.js";
11
+ import { VALID_CHECK_TARGETS } from "../../lib/constants.js";
11
12
  import { FlowCommand } from "./base-command.js";
12
13
 
13
- const VALID_TARGETS = ["impl", "finalize", "dirty", "gh"];
14
-
15
14
  const PREREQS = {
16
15
  impl: ["gate", "test"],
17
16
  finalize: ["implement"],
@@ -58,11 +57,11 @@ export default class GetCheckCommand extends FlowCommand {
58
57
  const target = ctx.target;
59
58
 
60
59
  if (!target) {
61
- throw new Error(`target required. valid: ${VALID_TARGETS.join(", ")}`);
60
+ throw new Error(`target required. valid: ${VALID_CHECK_TARGETS.join(", ")}`);
62
61
  }
63
62
 
64
- if (!VALID_TARGETS.includes(target)) {
65
- throw new Error(`unknown target '${target}'. valid: ${VALID_TARGETS.join(", ")}`);
63
+ if (!VALID_CHECK_TARGETS.includes(target)) {
64
+ throw new Error(`unknown target '${target}'. valid: ${VALID_CHECK_TARGETS.join(", ")}`);
66
65
  }
67
66
 
68
67
  if (target === "dirty") {
@@ -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, callAgentWithLog } from "../../lib/agent.js";
18
+ import { resolveAgent, callAgentWithLog, ensureAgentWorkDir } from "../../lib/agent.js";
19
19
 
20
20
  const EXCLUDE_FIELDS = new Set(["hash", "mtime", "lines", "id", "enrich", "detail"]);
21
21
 
@@ -215,10 +215,11 @@ function aiSearch(allEntries, analysis, query, root) {
215
215
 
216
216
  let config;
217
217
  try { config = loadConfig(root); } catch (_) { config = {}; }
218
- const agent = resolveAgent(config, "context.search");
218
+ const agent = resolveAgent(config, "flow.context.search");
219
219
  if (!agent) return fallbackSearch(allEntries, query);
220
220
 
221
221
  const prompt = buildKeywordSelectionPrompt(allKeywords, query);
222
+ ensureAgentWorkDir(agent, root);
222
223
  let response;
223
224
  try {
224
225
  response = callAgentWithLog(agent, prompt, 30000, root);