phasegate 0.91.0 → 0.107.0

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 (68) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.ja.md +15 -7
  3. package/README.md +24 -4
  4. package/docs/ADR/ADR-015-architecture-preset.md +183 -0
  5. package/docs/guide/codex-integration.md +7 -2
  6. package/docs/guide/installation.md +10 -2
  7. package/docs/guide/preset-selection.md +170 -0
  8. package/docs/guide/quick-vs-full-mode.md +3 -3
  9. package/docs/guide/retrofit-adoption.md +19 -2
  10. package/docs/guide/skills-overview.md +1 -1
  11. package/package.json +7 -1
  12. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
  13. package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
  14. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
  15. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
  16. package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
  17. package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
  18. package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
  19. package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
  20. package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
  21. package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
  22. package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
  23. package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
  24. package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
  25. package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
  26. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
  27. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
  28. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
  29. package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
  30. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
  31. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
  32. package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
  33. package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
  34. package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
  35. package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
  36. package/scripts/harness/config-foundation/composition-root.ts +8 -0
  37. package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
  38. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
  39. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
  40. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
  41. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
  42. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
  43. package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
  44. package/scripts/harness/integrations/pre-commit.ts +211 -52
  45. package/scripts/harness/main.ts +460 -340
  46. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
  47. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
  48. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
  49. package/scripts/harness/setup/skill-deployer.ts +140 -102
  50. package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
  51. package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
  53. package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
  54. package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
  55. package/scripts/harness/traceability-model/composition-root.ts +48 -30
  56. package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
  57. package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
  58. package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
  59. package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
  60. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
  61. package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
  62. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
  63. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
  64. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
  65. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
  66. package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
  67. package/skills/quick-implementor/SKILL.md +17 -1
  68. package/templates/.husky/commit-msg +1 -0
@@ -1,67 +1,64 @@
1
+ // @unit harness-api
1
2
  // @layer infrastructure
2
3
  // Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
3
4
 
4
- import { join } from 'node:path';
5
- import { promises as fs } from 'node:fs';
5
+ import { promises as fs } from "node:fs";
6
+ import { join } from "node:path";
6
7
 
7
- const HARNESS_VERSION_FILE = '.harness-version';
8
- const SKILLS_SOURCE_DIR = 'skills';
9
- const SKILLS_TARGET_DIR = join('.claude', 'skills');
10
- const HARNESS_CONFIG_FILE = 'phasegate.config.json';
11
- const HOOKS_TEMPLATE_DIR = join('templates', '.claude');
12
- const HOOKS_TARGET_DIR = '.claude';
8
+ const HARNESS_VERSION_FILE = ".harness-version";
9
+ const SKILLS_SOURCE_DIR = "skills";
10
+ const SKILLS_TARGET_DIR = "skills";
11
+ const SKILLS_LINK_TARGET = "../skills";
12
+ const CLAUDE_SKILLS_LINK_DIR = ".claude";
13
+ const CODEX_SKILLS_LINK_DIR = ".codex";
14
+ const HARNESS_CONFIG_FILE = "phasegate.config.json";
15
+ const HOOKS_TEMPLATE_DIR = join("templates", ".claude");
16
+ const HOOKS_TARGET_DIR = ".claude";
13
17
 
14
18
  // ── Skill Category Map ──
15
19
 
16
- export type SkillCategory = 'core' | 'aidlc' | 'utility';
17
- export type SkillSet = 'core' | 'all';
20
+ export type SkillCategory = "core" | "aidlc" | "utility";
21
+ export type SkillSet = "core" | "all";
18
22
 
19
23
  export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
20
24
  core: [
21
- 'cascade-updater',
22
- 'codebase-mapper',
23
- 'consistency-checker',
24
- 'doc-freshness-checker',
25
- 'engineering-perspective',
26
- 'implementation-readiness-checker',
27
- 'pointer-validator',
28
- 'test-coverage-checker',
25
+ "cascade-updater",
26
+ "codebase-mapper",
27
+ "consistency-checker",
28
+ "doc-freshness-checker",
29
+ "engineering-perspective",
30
+ "implementation-readiness-checker",
31
+ "pointer-validator",
32
+ "test-coverage-checker",
29
33
  ],
30
34
  aidlc: [
31
- 'domain-designer',
32
- 'environment-designer',
33
- 'implementation-planner',
34
- 'it-test-designer',
35
- 'it-test-logic-designer',
36
- 'logical-designer',
37
- 'mock-designer',
38
- 'product-architect',
39
- 'quick-implementor',
40
- 'scenario-test-designer',
41
- 'scenario-test-logic-designer',
42
- 'story-implementor',
43
- 'story-mapper',
44
- 'story-writer',
45
- 'uiux-designer',
46
- 'unit-designer',
47
- 'unit-test-designer',
48
- 'unit-test-logic-designer',
49
- ],
50
- utility: [
51
- 'codex-delegator',
52
- 'skill-creator',
35
+ "domain-designer",
36
+ "environment-designer",
37
+ "implementation-planner",
38
+ "it-test-designer",
39
+ "it-test-logic-designer",
40
+ "logical-designer",
41
+ "mock-designer",
42
+ "product-architect",
43
+ "quick-implementor",
44
+ "scenario-test-designer",
45
+ "scenario-test-logic-designer",
46
+ "story-implementor",
47
+ "story-mapper",
48
+ "story-writer",
49
+ "uiux-designer",
50
+ "unit-designer",
51
+ "unit-test-designer",
52
+ "unit-test-logic-designer",
53
53
  ],
54
+ utility: ["codex-delegator", "skill-creator"],
54
55
  } as const;
55
56
 
56
57
  export function getSkillsForSet(skillSet: SkillSet): string[] {
57
- if (skillSet === 'core') {
58
+ if (skillSet === "core") {
58
59
  return [...SKILL_CATEGORIES.core];
59
60
  }
60
- return [
61
- ...SKILL_CATEGORIES.core,
62
- ...SKILL_CATEGORIES.aidlc,
63
- ...SKILL_CATEGORIES.utility,
64
- ];
61
+ return [...SKILL_CATEGORIES.core, ...SKILL_CATEGORIES.aidlc, ...SKILL_CATEGORIES.utility];
65
62
  }
66
63
 
67
64
  export function getCategoryForSkill(skillName: string): SkillCategory | null {
@@ -92,8 +89,8 @@ interface PkgJson {
92
89
  }
93
90
 
94
91
  export async function getHarnessVersion(harnessRoot: string): Promise<string> {
95
- const packageJsonPath = join(harnessRoot, 'package.json');
96
- const content = await fs.readFile(packageJsonPath, 'utf-8');
92
+ const packageJsonPath = join(harnessRoot, "package.json");
93
+ const content = await fs.readFile(packageJsonPath, "utf-8");
97
94
  const pkg: PkgJson = JSON.parse(content);
98
95
  return pkg.version;
99
96
  }
@@ -118,7 +115,7 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
118
115
  export async function deploySkills(
119
116
  harnessRoot: string,
120
117
  projectRoot: string,
121
- skillSet: SkillSet = 'all',
118
+ skillSet: SkillSet = "all",
122
119
  ): Promise<DeployResult> {
123
120
  const skillsSource = join(harnessRoot, SKILLS_SOURCE_DIR);
124
121
  const skillsTarget = join(projectRoot, SKILLS_TARGET_DIR);
@@ -139,11 +136,7 @@ export async function deploySkills(
139
136
  }
140
137
 
141
138
  const versionInfo: VersionInfo = { version, deployedAt, skillSet };
142
- await fs.writeFile(
143
- join(skillsTarget, HARNESS_VERSION_FILE),
144
- JSON.stringify(versionInfo, null, 2) + '\n',
145
- 'utf-8',
146
- );
139
+ await fs.writeFile(join(skillsTarget, HARNESS_VERSION_FILE), JSON.stringify(versionInfo, null, 2) + "\n", "utf-8");
147
140
 
148
141
  return { deployedSkills: skills, targetDir: skillsTarget, version, deployedAt, skillSet };
149
142
  }
@@ -151,7 +144,7 @@ export async function deploySkills(
151
144
  export async function getDeployedVersion(projectRoot: string): Promise<VersionInfo | null> {
152
145
  const versionFile = join(projectRoot, SKILLS_TARGET_DIR, HARNESS_VERSION_FILE);
153
146
  try {
154
- const content = await fs.readFile(versionFile, 'utf-8');
147
+ const content = await fs.readFile(versionFile, "utf-8");
155
148
  const info: VersionInfo = JSON.parse(content);
156
149
  return info;
157
150
  } catch {
@@ -159,21 +152,60 @@ export async function getDeployedVersion(projectRoot: string): Promise<VersionIn
159
152
  }
160
153
  }
161
154
 
155
+ export interface AgentSkillLinkResult {
156
+ created: boolean;
157
+ path: string;
158
+ }
159
+
160
+ export interface DeployAgentSkillLinksResult {
161
+ claude: AgentSkillLinkResult | null;
162
+ codex: AgentSkillLinkResult | null;
163
+ }
164
+
165
+ async function ensureSkillLink(projectRoot: string, agentDir: string): Promise<AgentSkillLinkResult> {
166
+ const parentDir = join(projectRoot, agentDir);
167
+ const targetPath = join(parentDir, "skills");
168
+
169
+ try {
170
+ const stats = await fs.lstat(targetPath);
171
+ if (stats.isSymbolicLink()) {
172
+ const existingTarget = await fs.readlink(targetPath);
173
+ if (existingTarget === SKILLS_LINK_TARGET) {
174
+ return { created: false, path: targetPath };
175
+ }
176
+ }
177
+ return { created: false, path: targetPath };
178
+ } catch {}
179
+
180
+ await fs.mkdir(parentDir, { recursive: true });
181
+ await fs.symlink(SKILLS_LINK_TARGET, targetPath, process.platform === "win32" ? "junction" : "dir");
182
+ return { created: true, path: targetPath };
183
+ }
184
+
185
+ export async function deployAgentSkillLinks(
186
+ projectRoot: string,
187
+ agents: {
188
+ claude: boolean;
189
+ codex: boolean;
190
+ },
191
+ ): Promise<DeployAgentSkillLinksResult> {
192
+ const claude = agents.claude ? await ensureSkillLink(projectRoot, CLAUDE_SKILLS_LINK_DIR) : null;
193
+ const codex = agents.codex ? await ensureSkillLink(projectRoot, CODEX_SKILLS_LINK_DIR) : null;
194
+ return { claude, codex };
195
+ }
196
+
162
197
  export interface DeployHooksResult {
163
198
  scriptsDeployed: number;
164
199
  settingsCreated: boolean;
165
200
  }
166
201
 
167
- export async function deployHookScripts(
168
- harnessRoot: string,
169
- projectRoot: string,
170
- ): Promise<DeployHooksResult> {
202
+ export async function deployHookScripts(harnessRoot: string, projectRoot: string): Promise<DeployHooksResult> {
171
203
  const templateDir = join(harnessRoot, HOOKS_TEMPLATE_DIR);
172
204
  const targetDir = join(projectRoot, HOOKS_TARGET_DIR);
173
205
 
174
206
  // scripts/ ディレクトリをコピー
175
- const scriptsSource = join(templateDir, 'scripts');
176
- const scriptsTarget = join(targetDir, 'scripts');
207
+ const scriptsSource = join(templateDir, "scripts");
208
+ const scriptsTarget = join(targetDir, "scripts");
177
209
  let scriptsDeployed = 0;
178
210
 
179
211
  try {
@@ -183,16 +215,15 @@ export async function deployHookScripts(
183
215
  // シェルスクリプトに実行権限を付与
184
216
  const entries = await fs.readdir(scriptsTarget);
185
217
  for (const entry of entries) {
186
- if (entry.endsWith('.sh')) {
218
+ if (entry.endsWith(".sh")) {
187
219
  await fs.chmod(join(scriptsTarget, entry), 0o755);
188
220
  }
189
221
  }
190
- } catch {
191
- }
222
+ } catch {}
192
223
 
193
224
  // settings.json を作成(既存があればスキップ)
194
- const settingsSource = join(templateDir, 'settings.json');
195
- const settingsTarget = join(targetDir, 'settings.json');
225
+ const settingsSource = join(templateDir, "settings.json");
226
+ const settingsTarget = join(targetDir, "settings.json");
196
227
  let settingsCreated = false;
197
228
 
198
229
  try {
@@ -204,8 +235,7 @@ export async function deployHookScripts(
204
235
  await fs.mkdir(targetDir, { recursive: true });
205
236
  await fs.copyFile(settingsSource, settingsTarget);
206
237
  settingsCreated = true;
207
- } catch {
208
- }
238
+ } catch {}
209
239
  }
210
240
 
211
241
  return { scriptsDeployed, settingsCreated };
@@ -214,7 +244,7 @@ export async function deployHookScripts(
214
244
  export async function initHarnessConfig(
215
245
  projectRoot: string,
216
246
  projectName: string,
217
- phasePreset?: 'full' | 'standard' | 'minimal' | 'custom',
247
+ phasePreset?: "full" | "standard" | "minimal" | "custom",
218
248
  ): Promise<{ created: boolean; path: string }> {
219
249
  const configPath = join(projectRoot, HARNESS_CONFIG_FILE);
220
250
  try {
@@ -227,31 +257,31 @@ export async function initHarnessConfig(
227
257
  const template = {
228
258
  project: {
229
259
  name: projectName,
230
- preset: 'standard',
260
+ preset: "standard",
231
261
  },
232
262
  layers: {},
233
263
  quickMode: {},
234
264
  phaseDependencies: {
235
- preset: phasePreset ?? 'default',
265
+ preset: phasePreset ?? "default",
236
266
  override: false,
237
267
  customRules: [],
238
268
  },
239
269
  planningMode: {
240
- default: 'interactive',
270
+ default: "interactive",
241
271
  perPhase: {},
242
272
  },
243
273
  harnesses: {},
244
274
  paths: {
245
- designDocs: 'docs/product/construction',
246
- inceptionDocs: 'docs/inception',
275
+ designDocs: "docs/product/construction",
276
+ inceptionDocs: "docs/inception",
247
277
  },
248
278
  reporting: {
249
- format: 'json',
250
- outputDir: 'reports',
279
+ format: "json",
280
+ outputDir: "reports",
251
281
  },
252
282
  };
253
283
 
254
- await fs.writeFile(configPath, JSON.stringify(template, null, 2) + '\n', 'utf-8');
284
+ await fs.writeFile(configPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
255
285
  return { created: true, path: configPath };
256
286
  }
257
287
 
@@ -260,19 +290,16 @@ export interface DeployDesignDocsResult {
260
290
  skippedFiles: string[];
261
291
  }
262
292
 
263
- export async function deployDesignDocs(
264
- harnessRoot: string,
265
- projectRoot: string,
266
- ): Promise<DeployDesignDocsResult> {
293
+ export async function deployDesignDocs(harnessRoot: string, projectRoot: string): Promise<DeployDesignDocsResult> {
267
294
  const copiedFiles: string[] = [];
268
295
  const skippedFiles: string[] = [];
269
- const docsTargetDir = join(projectRoot, 'docs');
270
- const principlesTargetDir = join(docsTargetDir, 'principles');
296
+ const docsTargetDir = join(projectRoot, "docs");
297
+ const principlesTargetDir = join(docsTargetDir, "principles");
271
298
 
272
299
  await fs.mkdir(docsTargetDir, { recursive: true });
273
300
  await fs.mkdir(principlesTargetDir, { recursive: true });
274
301
 
275
- const folderRulesRelativePath = join('docs', 'folder_management_rules.md');
302
+ const folderRulesRelativePath = join("docs", "folder_management_rules.md");
276
303
  const folderRulesSource = join(harnessRoot, folderRulesRelativePath);
277
304
  const folderRulesTarget = join(projectRoot, folderRulesRelativePath);
278
305
 
@@ -289,17 +316,17 @@ export async function deployDesignDocs(
289
316
  }
290
317
  }
291
318
 
292
- const principlesSourceDir = join(harnessRoot, 'docs', 'principles');
319
+ const principlesSourceDir = join(harnessRoot, "docs", "principles");
293
320
 
294
321
  try {
295
322
  const principleEntries = await fs.readdir(principlesSourceDir, { withFileTypes: true });
296
323
  const principleFiles = principleEntries
297
- .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
324
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
298
325
  .map((entry) => entry.name)
299
326
  .sort();
300
327
 
301
328
  for (const principleFile of principleFiles) {
302
- const relativePath = join('docs', 'principles', principleFile);
329
+ const relativePath = join("docs", "principles", principleFile);
303
330
  const sourcePath = join(principlesSourceDir, principleFile);
304
331
  const targetPath = join(projectRoot, relativePath);
305
332
 
@@ -323,20 +350,35 @@ export interface DeployHuskyHookResult {
323
350
  path: string;
324
351
  }
325
352
 
326
- export async function deployHuskyHook(
353
+ export async function deployHuskyHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
354
+ const targetPath = join(projectRoot, ".husky", "pre-commit");
355
+
356
+ try {
357
+ await fs.access(targetPath);
358
+ return { created: false, path: targetPath };
359
+ } catch {}
360
+
361
+ const sourcePath = join(harnessRoot, "templates", ".husky", "pre-commit");
362
+ await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
363
+ await fs.copyFile(sourcePath, targetPath);
364
+ await fs.chmod(targetPath, 0o755);
365
+
366
+ return { created: true, path: targetPath };
367
+ }
368
+
369
+ export async function deployHuskyCommitMsgHook(
327
370
  harnessRoot: string,
328
371
  projectRoot: string,
329
372
  ): Promise<DeployHuskyHookResult> {
330
- const targetPath = join(projectRoot, '.husky', 'pre-commit');
373
+ const targetPath = join(projectRoot, ".husky", "commit-msg");
331
374
 
332
375
  try {
333
376
  await fs.access(targetPath);
334
377
  return { created: false, path: targetPath };
335
- } catch {
336
- }
378
+ } catch {}
337
379
 
338
- const sourcePath = join(harnessRoot, 'templates', '.husky', 'pre-commit');
339
- await fs.mkdir(join(projectRoot, '.husky'), { recursive: true });
380
+ const sourcePath = join(harnessRoot, "templates", ".husky", "commit-msg");
381
+ await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
340
382
  await fs.copyFile(sourcePath, targetPath);
341
383
  await fs.chmod(targetPath, 0o755);
342
384
 
@@ -348,20 +390,16 @@ export interface DeployCodexHooksResult {
348
390
  path: string;
349
391
  }
350
392
 
351
- export async function deployCodexHooks(
352
- harnessRoot: string,
353
- projectRoot: string,
354
- ): Promise<DeployCodexHooksResult> {
355
- const targetPath = join(projectRoot, '.codex', 'hooks.json');
393
+ export async function deployCodexHooks(harnessRoot: string, projectRoot: string): Promise<DeployCodexHooksResult> {
394
+ const targetPath = join(projectRoot, ".codex", "hooks.json");
356
395
 
357
396
  try {
358
397
  await fs.access(targetPath);
359
398
  return { created: false, path: targetPath };
360
- } catch {
361
- }
399
+ } catch {}
362
400
 
363
- const sourcePath = join(harnessRoot, 'templates', '.codex', 'hooks.json');
364
- await fs.mkdir(join(projectRoot, '.codex'), { recursive: true });
401
+ const sourcePath = join(harnessRoot, "templates", ".codex", "hooks.json");
402
+ await fs.mkdir(join(projectRoot, ".codex"), { recursive: true });
365
403
  await fs.copyFile(sourcePath, targetPath);
366
404
 
367
405
  return { created: true, path: targetPath };
@@ -6,28 +6,29 @@
6
6
  */
7
7
 
8
8
  export type SkillQualityErrorCode =
9
- | 'LOOP_ALREADY_COMPLETED'
10
- | 'LOOP_MAX_RETRIES_EXCEEDED'
11
- | 'LOOP_GAPS_NOT_EMPTY'
12
- | 'INVALID_STORY_ID'
13
- | 'DUPLICATE_LESSON_FINGERPRINT'
14
- | 'EMPTY_COMMIT_FIELD'
15
- | 'INVALID_COVERAGE_REPORT'
16
- | 'INVALID_REQUIREMENT_COVERAGE'
17
- | 'INVALID_COVERAGE_RANGE'
18
- | 'INVALID_LOOP_ATTEMPT'
19
- | 'EMPTY_LESSON_CONTENT'
20
- | 'EMPTY_SOURCE_CONTEXT'
21
- | 'EMPTY_FILE_PATH'
22
- | 'INVALID_UPDATED_COUNT'
23
- | 'EMPTY_MISSING_SECTIONS'
24
- | 'EMPTY_VIOLATIONS'
25
- | 'TDD_CYCLE_INCOMPLETE'
26
- | 'SKILL_FILE_NOT_FOUND'
27
- | 'GIT_COMMIT_FAILED'
28
- | 'COVERAGE_RUN_FAILED'
29
- | 'MATRIX_FILE_NOT_FOUND'
30
- | 'LESSON_ARTIFACT_SCHEMA_VIOLATION';
9
+ | "LOOP_ALREADY_COMPLETED"
10
+ | "LOOP_MAX_RETRIES_EXCEEDED"
11
+ | "LOOP_GAPS_NOT_EMPTY"
12
+ | "INVALID_STORY_ID"
13
+ | "DUPLICATE_LESSON_FINGERPRINT"
14
+ | "EMPTY_COMMIT_FIELD"
15
+ | "INVALID_WORK_ITEM_ID"
16
+ | "INVALID_COVERAGE_REPORT"
17
+ | "INVALID_REQUIREMENT_COVERAGE"
18
+ | "INVALID_COVERAGE_RANGE"
19
+ | "INVALID_LOOP_ATTEMPT"
20
+ | "EMPTY_LESSON_CONTENT"
21
+ | "EMPTY_SOURCE_CONTEXT"
22
+ | "EMPTY_FILE_PATH"
23
+ | "INVALID_UPDATED_COUNT"
24
+ | "EMPTY_MISSING_SECTIONS"
25
+ | "EMPTY_VIOLATIONS"
26
+ | "TDD_CYCLE_INCOMPLETE"
27
+ | "SKILL_FILE_NOT_FOUND"
28
+ | "GIT_COMMIT_FAILED"
29
+ | "COVERAGE_RUN_FAILED"
30
+ | "MATRIX_FILE_NOT_FOUND"
31
+ | "LESSON_ARTIFACT_SCHEMA_VIOLATION";
31
32
 
32
33
  export class SkillQualityError extends Error {
33
34
  readonly code: string;
@@ -35,6 +36,6 @@ export class SkillQualityError extends Error {
35
36
  constructor(code: SkillQualityErrorCode, message: string) {
36
37
  super(message);
37
38
  this.code = code;
38
- this.name = 'SkillQualityError';
39
+ this.name = "SkillQualityError";
39
40
  }
40
41
  }
@@ -2,32 +2,48 @@
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
4
  */
5
- import { SkillQualityError } from '../errors/skill-quality-error.js';
5
+ import { SkillQualityError } from "../errors/skill-quality-error.js";
6
+
7
+ const WORK_ITEM_ID_PATTERN = /^WI-\d+$/;
6
8
 
7
9
  export class CommitMessage {
8
10
  readonly unit: string;
9
11
  readonly storyId: string;
10
12
  readonly description: string;
13
+ readonly workItemId: string | undefined;
11
14
 
12
- private constructor(unit: string, storyId: string, description: string) {
15
+ private constructor(unit: string, storyId: string, description: string, workItemId?: string) {
13
16
  this.unit = unit;
14
17
  this.storyId = storyId;
15
18
  this.description = description;
19
+ this.workItemId = workItemId;
16
20
  Object.freeze(this);
17
21
  }
18
22
 
19
- static create(unit: string, storyId: string, description: string): CommitMessage {
23
+ static create(unit: string, storyId: string, description: string, workItemId?: string): CommitMessage {
20
24
  if (!unit || !storyId || !description) {
21
- throw new SkillQualityError('EMPTY_COMMIT_FIELD', 'unit, storyId, description must be non-empty');
25
+ throw new SkillQualityError("EMPTY_COMMIT_FIELD", "unit, storyId, description must be non-empty");
26
+ }
27
+ if (workItemId !== undefined && !WORK_ITEM_ID_PATTERN.test(workItemId)) {
28
+ throw new SkillQualityError("INVALID_WORK_ITEM_ID", "workItemId must match WI-<number>");
22
29
  }
23
- return new CommitMessage(unit, storyId, description);
30
+ return new CommitMessage(unit, storyId, description, workItemId);
24
31
  }
25
32
 
26
33
  format(): string {
27
- return `feat(${this.unit}/${this.storyId}): ${this.description}`;
34
+ const subject = `feat(${this.unit}/${this.storyId}): ${this.description}`;
35
+ if (this.workItemId === undefined) {
36
+ return subject;
37
+ }
38
+ return `${subject}\n\nWork-Item: ${this.workItemId}`;
28
39
  }
29
40
 
30
41
  equals(other: CommitMessage): boolean {
31
- return this.unit === other.unit && this.storyId === other.storyId && this.description === other.description;
42
+ return (
43
+ this.unit === other.unit &&
44
+ this.storyId === other.storyId &&
45
+ this.description === other.description &&
46
+ this.workItemId === other.workItemId
47
+ );
32
48
  }
33
49
  }
@@ -0,0 +1,52 @@
1
+ // @unit traceability-model
2
+ // @layer application
3
+
4
+ import type { WorkItemMigrationApplyPort } from "../../domain/ports/work-item-migration-apply-port.js";
5
+ import type {
6
+ WorkItemMigrationAppliedCandidate,
7
+ WorkItemMigrationApplyResult,
8
+ WorkItemMigrationCandidate,
9
+ } from "../../domain/value-objects/work-item-migration-candidate.js";
10
+ import type { PlanWorkItemMigrationUseCase } from "./plan-work-item-migration-usecase.js";
11
+
12
+ type PlanUseCase = Pick<PlanWorkItemMigrationUseCase, "execute">;
13
+
14
+ export interface ApplyWorkItemMigrationUseCaseDeps {
15
+ readonly planWorkItemMigrationUseCase: PlanUseCase;
16
+ readonly applyPort: WorkItemMigrationApplyPort;
17
+ }
18
+
19
+ export class ApplyWorkItemMigrationUseCase {
20
+ private readonly planUseCase: PlanUseCase;
21
+ private readonly applyPort: WorkItemMigrationApplyPort;
22
+
23
+ constructor(deps: ApplyWorkItemMigrationUseCaseDeps) {
24
+ this.planUseCase = deps.planWorkItemMigrationUseCase;
25
+ this.applyPort = deps.applyPort;
26
+ }
27
+
28
+ async execute(): Promise<WorkItemMigrationApplyResult> {
29
+ const plan = await this.planUseCase.execute();
30
+ const conflicts = plan.candidates.filter((candidate) => candidate.conflict);
31
+ if (conflicts.length > 0) {
32
+ return Object.freeze({
33
+ applied: Object.freeze([]),
34
+ skipped: Object.freeze(conflicts),
35
+ warnings: plan.warnings,
36
+ blocked: true,
37
+ });
38
+ }
39
+
40
+ const applied: WorkItemMigrationAppliedCandidate[] = [];
41
+ for (const candidate of plan.candidates) {
42
+ applied.push(await this.applyPort.apply(candidate));
43
+ }
44
+
45
+ return Object.freeze({
46
+ applied: Object.freeze(applied),
47
+ skipped: Object.freeze([] as WorkItemMigrationCandidate[]),
48
+ warnings: plan.warnings,
49
+ blocked: false,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,29 @@
1
+ // @unit traceability-model
2
+ // @layer application
3
+
4
+ import type { WorkItemMigrationSourcePort } from "../../domain/ports/work-item-migration-source-port.js";
5
+ import { WorkItemMigrationPlanner } from "../../domain/services/work-item-migration-planner.js";
6
+ import type { WorkItemMigrationPlan } from "../../domain/value-objects/work-item-migration-candidate.js";
7
+
8
+ export interface PlanWorkItemMigrationUseCaseDeps {
9
+ readonly sourcePort: WorkItemMigrationSourcePort;
10
+ readonly planner?: WorkItemMigrationPlanner;
11
+ }
12
+
13
+ export class PlanWorkItemMigrationUseCase {
14
+ private readonly sourcePort: WorkItemMigrationSourcePort;
15
+ private readonly planner: WorkItemMigrationPlanner;
16
+
17
+ constructor(deps: PlanWorkItemMigrationUseCaseDeps) {
18
+ this.sourcePort = deps.sourcePort;
19
+ this.planner = deps.planner ?? new WorkItemMigrationPlanner();
20
+ }
21
+
22
+ async execute(): Promise<WorkItemMigrationPlan> {
23
+ const [entries, existingWorkItemIds] = await Promise.all([
24
+ this.sourcePort.listLegacyIssueDirectories(),
25
+ this.sourcePort.listExistingWorkItemIds(),
26
+ ]);
27
+ return this.planner.plan(entries, existingWorkItemIds);
28
+ }
29
+ }