phasegate 0.160.21 → 0.162.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 (96) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -1
  3. package/docs/contracts/requirement-test-matrix.schema.json +1 -1
  4. package/docs/guide/cli-reference.md +1 -1
  5. package/docs/guide/configuration.md +31 -1
  6. package/docs/guide/skills-overview.md +2 -0
  7. package/docs/principles/model-routing.md +1 -0
  8. package/docs/templates/personal/phasegate-local-config.json +4 -1
  9. package/docs/templates/project/phasegate.config.json +4 -1
  10. package/package.json +6 -3
  11. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +91 -23
  12. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +4 -0
  13. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
  14. package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +25 -2
  15. package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts +17 -2
  16. package/scripts/harness/biome-ast-engine/domain/value-objects/import-graph.ts +28 -1
  17. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +6 -1
  18. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -0
  19. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
  20. package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
  21. package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
  22. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
  23. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -1
  24. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -1
  25. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -1
  26. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +39 -9
  27. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +39 -9
  28. package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +18 -4
  29. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +27 -4
  30. package/scripts/harness/installation/application/ports/model-delegation-port.ts +10 -0
  31. package/scripts/harness/installation/application/usecases/run-install.ts +67 -5
  32. package/scripts/harness/installation/application/usecases/run-reconcile.ts +53 -5
  33. package/scripts/harness/installation/composition-root.ts +4 -2
  34. package/scripts/harness/installation/infrastructure/adapters/skill-deployer-model-delegation-adapter.ts +16 -0
  35. package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
  36. package/scripts/harness/main.ts +99 -13
  37. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +9 -7
  38. package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +27 -2
  39. package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +4 -4
  40. package/scripts/harness/nyquist-validation/domain/value-objects/intent-coverage.ts +33 -0
  41. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +3 -1
  42. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +8 -3
  43. package/scripts/harness/quick-mode/composition-root.ts +3 -0
  44. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
  45. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-quick-mode-execution-adapter.ts +18 -0
  46. package/scripts/harness/quick-mode/presentation/handlers/ci-check-quick-mode-handler.ts +51 -5
  47. package/scripts/harness/setup/skill-deployer.ts +47 -0
  48. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
  49. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
  50. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +12 -2
  51. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +12 -2
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +6 -1
  53. package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +28 -9
  54. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +76 -25
  55. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +20 -8
  56. package/scripts/harness/validator-system/composition-root.ts +8 -1
  57. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
  58. package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
  59. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +16 -6
  60. package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +10 -1
  61. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
  62. package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +35 -3
  63. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +86 -14
  64. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +29 -3
  65. package/skills/cascade-updater/SKILL.md +1 -0
  66. package/skills/codebase-mapper/SKILL.md +1 -0
  67. package/skills/codex-delegator/SKILL.md +1 -0
  68. package/skills/consistency-checker/SKILL.md +1 -0
  69. package/skills/doc-freshness-checker/SKILL.md +1 -0
  70. package/skills/domain-designer/SKILL.md +1 -0
  71. package/skills/engineering-perspective/SKILL.md +1 -0
  72. package/skills/environment-designer/SKILL.md +1 -0
  73. package/skills/implementation-planner/SKILL.md +1 -0
  74. package/skills/implementation-readiness-checker/SKILL.md +1 -0
  75. package/skills/it-test-designer/SKILL.md +1 -0
  76. package/skills/it-test-logic-designer/SKILL.md +1 -0
  77. package/skills/logical-designer/SKILL.md +1 -0
  78. package/skills/mock-designer/SKILL.md +1 -0
  79. package/skills/phasegate-config-doctor/SKILL.md +1 -0
  80. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  81. package/skills/pointer-validator/SKILL.md +1 -0
  82. package/skills/product-architect/SKILL.md +1 -0
  83. package/skills/quick-implementor/SKILL.md +1 -0
  84. package/skills/scenario-test-designer/SKILL.md +1 -0
  85. package/skills/scenario-test-logic-designer/SKILL.md +1 -0
  86. package/skills/skill-creator/SKILL.md +1 -0
  87. package/skills/story-implementor/SKILL.md +1 -0
  88. package/skills/story-mapper/SKILL.md +1 -0
  89. package/skills/story-writer/SKILL.md +1 -0
  90. package/skills/test-coverage-checker/SKILL.md +1 -0
  91. package/skills/uiux-designer/SKILL.md +1 -0
  92. package/skills/unit-designer/SKILL.md +1 -0
  93. package/skills/unit-test-designer/SKILL.md +1 -0
  94. package/skills/unit-test-logic-designer/SKILL.md +1 -0
  95. package/templates/.claude/scripts/deny-check.sh +73 -12
  96. package/templates/.claude/settings.json +4 -0
@@ -23,11 +23,25 @@ interface RawStoryMappingEntry {
23
23
  storyId: string;
24
24
  }
25
25
 
26
+ /**
27
+ * requirement-test-matrix.json から storyId を持つエントリを抽出する。
28
+ *
29
+ * スキーマ準拠の実データはトップレベルキー `stories`(各要素が `{ storyId, storyMappings }`)
30
+ * を用いる。旧来のテスト・入力ではトップレベル `storyMappings` を用いる場合があるため
31
+ * 両形式に対応する。以前は `storyMappings` のみを参照していたため、スキーマ準拠の実データ
32
+ * (`stories` 形式)に対しては常に空配列を返し、storyId 整合性検査が事実上 no-op だった。
33
+ */
26
34
  function extractStoryMappings(rawData: unknown): RawStoryMappingEntry[] {
27
35
  if (rawData === null || typeof rawData !== 'object') return [];
28
36
  const obj = rawData as Record<string, unknown>;
29
- if (!Array.isArray(obj['storyMappings'])) return [];
30
- return (obj['storyMappings'] as unknown[]).map((item) => {
37
+
38
+ const source = Array.isArray(obj['stories'])
39
+ ? (obj['stories'] as unknown[])
40
+ : Array.isArray(obj['storyMappings'])
41
+ ? (obj['storyMappings'] as unknown[])
42
+ : [];
43
+
44
+ return source.map((item) => {
31
45
  const entry = item as Record<string, unknown>;
32
46
  return { storyId: String(entry['storyId'] ?? '') };
33
47
  });
@@ -53,7 +67,9 @@ export class MatrixValidationService {
53
67
  const storyMappings = extractStoryMappings(rawData);
54
68
 
55
69
  const errors: NyquistHarnessError[] = [];
70
+ const seenStoryIds = new Set<string>();
56
71
  for (const sm of storyMappings) {
72
+ // storyId 整合性: 有効 storyId 一覧に含まれるか照合する
57
73
  if (!validStoryIds.includes(sm.storyId)) {
58
74
  errors.push({
59
75
  code: 'L3-004',
@@ -61,6 +77,15 @@ export class MatrixValidationService {
61
77
  message: `未登録のstoryIdです: ${sm.storyId}`,
62
78
  });
63
79
  }
80
+ // INV-1: 同一 storyId の StoryMapping は1つのみ存在する(重複を検出)
81
+ if (seenStoryIds.has(sm.storyId)) {
82
+ errors.push({
83
+ code: 'L3-004',
84
+ severity: 'error',
85
+ message: `storyIdが重複しています: ${sm.storyId}`,
86
+ });
87
+ }
88
+ seenStoryIds.add(sm.storyId);
64
89
  }
65
90
 
66
91
  if (errors.length === 0) {
@@ -3,12 +3,12 @@
3
3
  // @work-item-id WI-131
4
4
 
5
5
  import type {
6
- IntentCoverageItemDto,
7
- MatrixStoryDto,
8
- } from '../../application/dto/generate-matrix-output.js';
6
+ IntentCoverageItem,
7
+ IntentCoverageStory,
8
+ } from '../value-objects/intent-coverage.js';
9
9
 
10
10
  export class RequirementIntentCoverageService {
11
- evaluate(stories: readonly MatrixStoryDto[]): readonly IntentCoverageItemDto[] {
11
+ evaluate(stories: readonly IntentCoverageStory[]): readonly IntentCoverageItem[] {
12
12
  return stories.flatMap((story) => story.storyMappings.map((mapping) => {
13
13
  if (mapping.testReferences.length === 0) {
14
14
  return {
@@ -0,0 +1,33 @@
1
+ // @layer domain
2
+ // @unit nyquist-validation
3
+ // @work-item-id WI-131
4
+ //
5
+ // RequirementIntentCoverageService が入出力に用いるドメイン層の型定義。
6
+ // 依存方向是正: これまで domain 層のサービスが application/dto から型を import しており
7
+ // domain → application の逆流(レイヤー依存違反)が発生していた。型の正準を domain 層へ
8
+ // 移し、application 層の DTO はここを参照する(domain ← application の正しい向き)。
9
+
10
+ export interface IntentCoverageTestReference {
11
+ readonly filePath: string;
12
+ readonly testType: 'unit' | 'it' | 'scenario';
13
+ readonly testName?: string;
14
+ }
15
+
16
+ export interface IntentCoverageAcMapping {
17
+ readonly acId: string;
18
+ readonly testReferences: readonly IntentCoverageTestReference[];
19
+ }
20
+
21
+ export interface IntentCoverageStory {
22
+ readonly storyId: string;
23
+ readonly storyMappings: readonly IntentCoverageAcMapping[];
24
+ }
25
+
26
+ export type IntentCoverageStatus = 'observed' | 'weakly-observed' | 'unobserved';
27
+
28
+ export interface IntentCoverageItem {
29
+ readonly storyId: string;
30
+ readonly acId: string;
31
+ readonly status: IntentCoverageStatus;
32
+ readonly warnings: readonly string[];
33
+ }
@@ -6,7 +6,9 @@ import { readFile } from 'node:fs/promises';
6
6
  import type { RequirementSourcePort } from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
7
7
  import type { RequirementSourceDto } from '../../application/dto/generate-matrix-output.js';
8
8
 
9
- const STORY_HEADING = /^###\s+(H\d{2}-\d{2}):/;
9
+ // StoryId HXX-XX 形式に加え Phase 2 拡張 Epic の HF\d+-XX 形式も許容する
10
+ // (traceability-model の StoryId 正規表現と整合)。旧 /H\d{2}-\d{2}/ は HF2-01 を取りこぼしていた。
11
+ const STORY_HEADING = /^###\s+(H(?:F\d+|\d{2})-\d{2}):/;
10
12
  const AC_ITEM = /^\s*-\s+\[[ xX]\]\s+(AC-[1-9][0-9]*):/;
11
13
 
12
14
  export class MarkdownRequirementSourceAdapter implements RequirementSourcePort {
@@ -50,11 +50,16 @@ export class ClassifyChangeCategoryUseCase {
50
50
  const targetChanges = new Map((input.targetChanges ?? []).map((change) => [change.filePath, change]));
51
51
  const changedFiles = input.paths.map((p) => {
52
52
  const targetChange = targetChanges.get(p);
53
+ const beforeContent = targetChange?.beforeContent ?? null;
54
+ const afterContent = targetChange?.afterContent ?? null;
53
55
  return ChangedFile.create({
54
56
  filePath: p,
55
- changeKind: 'MODIFY',
56
- beforeContent: targetChange?.beforeContent ?? null,
57
- afterContent: targetChange?.afterContent ?? null,
57
+ // 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
58
+ // 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
59
+ // NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
60
+ changeKind: beforeContent === null && afterContent !== null ? 'CREATE' : 'MODIFY',
61
+ beforeContent,
62
+ afterContent,
58
63
  });
59
64
  });
60
65
 
@@ -8,6 +8,7 @@
8
8
  import { GitDiffChangedFilesAdapter } from './infrastructure/adapters/git-diff-changed-files-adapter.js';
9
9
  import { HarnessConfigQuickModeConfigAdapter } from './infrastructure/adapters/harness-config-quick-mode-config-adapter.js';
10
10
  import { ValidatorSystemValidatorIdRegistryAdapter } from './infrastructure/adapters/validator-system-validator-id-registry-adapter.js';
11
+ import { ValidatorSystemQuickModeExecutionAdapter } from './infrastructure/adapters/validator-system-quick-mode-execution-adapter.js';
11
12
  import { QuickModeJudgmentEngine } from './domain/services/quick-mode-judgment-engine.js';
12
13
  import { ValidatorRelaxationService } from './domain/services/validator-relaxation-service.js';
13
14
  import { QuickModeDecisionContractMapper } from './application/mappers/quick-mode-decision-contract-mapper.js';
@@ -32,6 +33,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
32
33
  const gitDiffAdapter = new GitDiffChangedFilesAdapter();
33
34
  const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter();
34
35
  const validatorIdRegistryAdapter = new ValidatorSystemValidatorIdRegistryAdapter();
36
+ const validatorExecutionPort = new ValidatorSystemQuickModeExecutionAdapter();
35
37
 
36
38
  // Domain Services
37
39
  const judgmentEngine = new QuickModeJudgmentEngine();
@@ -64,6 +66,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
64
66
  const executeUseCase = new ExecuteQuickCiCheckUseCase({
65
67
  judgeUseCase,
66
68
  buildUseCase,
69
+ validatorExecutionPort,
67
70
  });
68
71
 
69
72
  const classifyUseCase = new ClassifyChangeCategoryUseCase({
@@ -77,7 +77,12 @@ export class QuickModeConfig {
77
77
  }
78
78
 
79
79
  isMaintained(validatorId: string): boolean {
80
- return this.maintainedLayers.includes(validatorId);
80
+ // maintainedLayers の各エントリ P について、レイヤー名エントリ(例 "L2")は
81
+ // そのレイヤーの全 ID(例 "L2-002")にマッチし、ID 完全一致(例 "L2-002")も
82
+ // 引き続きサポートする。
83
+ return this.maintainedLayers.some(
84
+ (entry) => validatorId === entry || validatorId.startsWith(`${entry}-`)
85
+ );
81
86
  }
82
87
 
83
88
  isRelaxed(validatorId: string): boolean {
@@ -0,0 +1,18 @@
1
+ // @layer infrastructure
2
+ // @unit quick-mode
3
+
4
+ import type { ValidatorExecutionPort } from '../../application/ports/validator-execution-port.js';
5
+ import type { ValidatorRelaxationProfileContract } from '../../application/dto/validator-relaxation-profile-contract.js';
6
+
7
+ export class ValidatorSystemQuickModeExecutionAdapter implements ValidatorExecutionPort {
8
+ async executeWithProfile(profile: ValidatorRelaxationProfileContract): Promise<void> {
9
+ const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
10
+ const module = createValidatorSystemModule();
11
+ await module.runQuickModeUseCase.execute({
12
+ relaxationProfile: profile,
13
+ targetPaths: [],
14
+ unitName: '',
15
+ currentPhase: '',
16
+ });
17
+ }
18
+ }
@@ -5,12 +5,56 @@
5
5
  * phasegate:ci-check --quick フラグを受け取り、ExecuteQuickCiCheckUseCase を呼ぶハンドラー
6
6
  */
7
7
 
8
+ import * as childProcess from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
8
10
  import { HumanQuickModeFormatter } from '../formatters/human-quick-mode-formatter.js';
9
11
  import { AgentQuickModeFormatter } from '../formatters/agent-quick-mode-formatter.js';
10
12
  import { JsonQuickModeFormatter } from '../formatters/json-quick-mode-formatter.js';
11
13
  import type { ExecuteQuickCiCheckUseCase } from '../../application/usecases/execute-quick-ci-check-usecase.js';
12
14
  import type { QuickModeRenderOptions } from '../dto/quick-mode-render-options.js';
13
15
 
16
+ /**
17
+ * `--files` で列挙されたパスの変更種別 (CREATE/MODIFY) を推定する。
18
+ *
19
+ * WI: 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
20
+ * NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。git のステージ状態を
21
+ * 参照し、追加 (A) なら CREATE、それ以外は MODIFY とする。git 情報が取れない
22
+ * 場合は「ファイルが未追跡(作業ツリーにのみ存在しコミット履歴に無い)」を
23
+ * CREATE のヒューリスティックとして用いる。
24
+ */
25
+ function resolveChangeKind(filePath: string): 'CREATE' | 'MODIFY' {
26
+ const gitStatus = readGitStatus(filePath);
27
+ if (gitStatus === 'A') return 'CREATE';
28
+ if (gitStatus !== null) return 'MODIFY';
29
+ return isUntrackedNewFile(filePath) ? 'CREATE' : 'MODIFY';
30
+ }
31
+
32
+ function readGitStatus(filePath: string): string | null {
33
+ try {
34
+ const output = childProcess.execSync(
35
+ `git diff --name-status --cached -- ${JSON.stringify(filePath)}`,
36
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
37
+ ) as string;
38
+ const line = output.trim().split('\n').find((l) => l.trim().length > 0);
39
+ return line ? (line.split('\t')[0]?.trim() ?? null) : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function isUntrackedNewFile(filePath: string): boolean {
46
+ if (!existsSync(filePath)) return false;
47
+ try {
48
+ childProcess.execSync(
49
+ `git ls-files --error-unmatch -- ${JSON.stringify(filePath)}`,
50
+ { stdio: ['pipe', 'pipe', 'pipe'] },
51
+ );
52
+ return false; // tracked → 既存ファイル
53
+ } catch {
54
+ return true; // 未追跡 → 新規ファイル
55
+ }
56
+ }
57
+
14
58
  export interface CiCheckQuickModeHandlerDeps {
15
59
  useCase: Pick<ExecuteQuickCiCheckUseCase, 'execute'>;
16
60
  }
@@ -25,13 +69,15 @@ export class CiCheckQuickModeHandler {
25
69
  async handle(options: QuickModeRenderOptions): Promise<void> {
26
70
  const { files, dryRun = false, format = 'human', failOnReject = false } = options;
27
71
 
28
- // --files 解析: カンマ区切りのファイルパスを MODIFY として扱う
72
+ // --files 解析: カンマ区切りのファイルパスを git ステージ状態から
73
+ // CREATE/MODIFY へ分類する(新規 domain/ ファイルの NEW_DOMAIN 判定のため)
29
74
  let changedFiles: { filePath: string; changeKind: string }[] | undefined;
30
75
  if (files) {
31
- changedFiles = files.split(',').map((p) => ({
32
- filePath: p.trim(),
33
- changeKind: 'MODIFY',
34
- }));
76
+ changedFiles = files
77
+ .split(',')
78
+ .map((p) => p.trim())
79
+ .filter((p) => p.length > 0)
80
+ .map((filePath) => ({ filePath, changeKind: resolveChangeKind(filePath) }));
35
81
  }
36
82
 
37
83
  let decision: Awaited<ReturnType<typeof this.useCase.execute>>;
@@ -4,6 +4,8 @@
4
4
  // @work-item-id WI-127
5
5
  // @work-item-id WI-184
6
6
  // @work-item-id WI-202
7
+ // @work-item-id WI-212
8
+ // @work-item-id WI-219
7
9
  // Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
8
10
 
9
11
  import { promises as fs } from "node:fs";
@@ -151,6 +153,44 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
151
153
  return count;
152
154
  }
153
155
 
156
+ type ModelDelegationPolicy = "delegate-sonnet" | "none";
157
+
158
+ export async function readModelDelegationPolicy(projectRoot: string): Promise<ModelDelegationPolicy> {
159
+ try {
160
+ const raw = await fs.readFile(join(projectRoot, HARNESS_CONFIG_FILE), "utf-8");
161
+ const config = JSON.parse(raw) as { modelRouting?: { delegation?: unknown } };
162
+ return config.modelRouting?.delegation === "none" ? "none" : "delegate-sonnet";
163
+ } catch {
164
+ return "delegate-sonnet";
165
+ }
166
+ }
167
+
168
+ export function renderSkillForModelDelegation(content: string, policy: ModelDelegationPolicy): string {
169
+ if (policy !== "none") {
170
+ return content;
171
+ }
172
+
173
+ return content
174
+ .replace(/^model:\s+\S+\n/gm, "")
175
+ .replace(/^review:\s+\S+\n/gm, "")
176
+ .replaceAll("Sonnet 4.6 に委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)", "メインセッションが成果物を生成する")
177
+ .replaceAll("Opus が", "メインセッションが")
178
+ .replaceAll("Sonnet 4.6", "メインセッション")
179
+ .replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
180
+ }
181
+
182
+ async function applyModelDelegationPolicyToSkill(skillDir: string, policy: ModelDelegationPolicy): Promise<void> {
183
+ if (policy !== "none") {
184
+ return;
185
+ }
186
+
187
+ const skillPath = join(skillDir, "SKILL.md");
188
+ try {
189
+ const content = await fs.readFile(skillPath, "utf-8");
190
+ await fs.writeFile(skillPath, renderSkillForModelDelegation(content, policy), "utf-8");
191
+ } catch {}
192
+ }
193
+
154
194
  export async function deploySkills(
155
195
  harnessRoot: string,
156
196
  projectRoot: string,
@@ -160,6 +200,7 @@ export async function deploySkills(
160
200
  const skillsTarget = join(projectRoot, SKILLS_TARGET_DIR);
161
201
  const version = await getHarnessVersion(harnessRoot);
162
202
  const deployedAt = new Date().toISOString();
203
+ const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
163
204
 
164
205
  const skillDirs = await fs.readdir(skillsSource, { withFileTypes: true });
165
206
  const allSkills = skillDirs.filter((d) => d.isDirectory()).map((d) => d.name);
@@ -172,6 +213,7 @@ export async function deploySkills(
172
213
  const srcSkillDir = join(skillsSource, skill);
173
214
  const destSkillDir = join(skillsTarget, skill);
174
215
  await copyDirectory(srcSkillDir, destSkillDir);
216
+ await applyModelDelegationPolicyToSkill(destSkillDir, modelDelegationPolicy);
175
217
  }
176
218
 
177
219
  const versionInfo: VersionInfo = { version, deployedAt, skillSet };
@@ -456,6 +498,7 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
456
498
  export interface InitHarnessConfigOptions {
457
499
  ciEnabled?: boolean;
458
500
  workflow?: "standard" | "strict";
501
+ language?: string;
459
502
  }
460
503
 
461
504
  export async function initHarnessConfig(
@@ -477,6 +520,7 @@ export async function initHarnessConfig(
477
520
  project: {
478
521
  name: projectName,
479
522
  preset: "standard",
523
+ languages: [options.language ?? "typescript"],
480
524
  },
481
525
  architecture: {
482
526
  preset: "clean",
@@ -497,6 +541,9 @@ export async function initHarnessConfig(
497
541
  default: "interactive",
498
542
  perPhase: {},
499
543
  },
544
+ modelRouting: {
545
+ delegation: "delegate-sonnet",
546
+ },
500
547
  harnesses: {},
501
548
  paths: {
502
549
  designDocs: "docs/product/construction",
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-212
4
5
  */
5
6
  import { SkillStructure } from '../value-objects/skill-structure.js';
6
7
  import { SkillValidationResult } from '../value-objects/skill-validation-result.js';
@@ -29,6 +30,9 @@ export class SkillStructureValidator {
29
30
  // Check for frontmatter (YAML between --- delimiters)
30
31
  if (lines[0]?.trim() === '---') {
31
32
  sections.push('frontmatter');
33
+ if (this.hasLanguageMetadata(lines)) {
34
+ sections.push('languageMetadata');
35
+ }
32
36
  }
33
37
 
34
38
  // Look for headings that correspond to section names
@@ -69,4 +73,21 @@ export class SkillStructureValidator {
69
73
 
70
74
  return sections;
71
75
  }
76
+
77
+ private hasLanguageMetadata(lines: readonly string[]): boolean {
78
+ const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
79
+ if (closingDelimiterIndex < 0) return false;
80
+
81
+ const frontmatterLines = lines.slice(1, closingDelimiterIndex);
82
+ const languageLineIndex = frontmatterLines.findIndex((line) => /^\s*languages\s*:/.test(line));
83
+ if (languageLineIndex < 0) return false;
84
+
85
+ const languageLine = frontmatterLines[languageLineIndex] ?? '';
86
+ if (/\[[^\]]*\S[^\]]*\]/.test(languageLine)) return true;
87
+
88
+ return frontmatterLines.slice(languageLineIndex + 1).some((line) => {
89
+ if (/^\S/.test(line)) return false;
90
+ return /^\s*-\s*\S+/.test(line);
91
+ });
92
+ }
72
93
  }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-212
4
5
  */
5
6
  import type { SectionName } from '../types/section-name.js';
6
7
 
7
8
  const REQUIRED_SECTIONS: readonly SectionName[] = [
8
9
  'frontmatter',
10
+ 'languageMetadata',
9
11
  'purpose',
10
12
  'inputs',
11
13
  'outputs',
@@ -18,8 +18,18 @@ export class L1BiomeValidatorAdapter implements L1ValidatorPort {
18
18
  message: violation.message,
19
19
  location: `${violation.filePath.toString()}:${violation.line}:${violation.column}`,
20
20
  }));
21
- } catch (_err) {
22
- return [];
21
+ } catch (err) {
22
+ // Fail-closed: a validator failure must NOT be treated as "合格".
23
+ // Surface the error as a blocking violation so the commit gate stays closed.
24
+ const message = err instanceof Error ? err.message : String(err);
25
+ console.error(`[skill-quality] L1 Biome validator failed: ${message}`);
26
+ return [
27
+ {
28
+ ruleId: 'L1-VALIDATOR-ERROR',
29
+ message: `L1 Biome validator failed to run; treating as NOT compliant (fail-closed): ${message}`,
30
+ location: '',
31
+ },
32
+ ];
23
33
  }
24
34
  }
25
35
  }
@@ -23,8 +23,18 @@ export class L2ValidatorSystemAdapter implements L2ValidatorPort {
23
23
  message: error.message,
24
24
  location: '',
25
25
  }));
26
- } catch {
27
- return [];
26
+ } catch (err) {
27
+ // Fail-closed: a validator failure must NOT be treated as "合格".
28
+ // Surface the error as a blocking violation so the commit gate stays closed.
29
+ const message = err instanceof Error ? err.message : String(err);
30
+ console.error(`[skill-quality] L2 validator-system failed: ${message}`);
31
+ return [
32
+ {
33
+ ruleId: 'L2-VALIDATOR-ERROR',
34
+ message: `L2 validator-system failed to run; treating as NOT compliant (fail-closed): ${message}`,
35
+ location: '',
36
+ },
37
+ ];
28
38
  }
29
39
  }
30
40
  }
@@ -42,12 +42,17 @@ export class ApplyWorkItemStatusUseCase {
42
42
  }
43
43
  }
44
44
 
45
+ // `completed` is a manually promoted terminal state that the automatic
46
+ // derivation never emits (derivation tops out at `tested`). It must rank
47
+ // strictly above `tested` here so that deriving `tested` for an already
48
+ // `completed` work item is detected as a downgrade and blocked, preventing a
49
+ // `completed` -> `tested` regression on `work-items --apply`.
45
50
  const STATUS_ORDER: Record<WorkItemStatus, number> = {
46
51
  drafted: 0,
47
52
  reflected: 1,
48
53
  implemented: 2,
49
54
  tested: 3,
50
- completed: 3,
55
+ completed: 4,
51
56
  };
52
57
 
53
58
  function statusOrder(status: WorkItemStatus): number {
@@ -12,9 +12,10 @@ export interface ParsedStoryCatalog {
12
12
 
13
13
  const STORY_ID_LINE_PATTERN = /\bH(?:F\d+|[0-9]{2})-[0-9]{2}\b/g;
14
14
  const TABLE_ALIAS_PATTERN =
15
- /\|\s*(H[0-9]{2}-[0-9]{2})\s*\|.*?\|\s*(US-[0-9]{3})\s*\|/g;
15
+ /\|\s*(H(?:F\d+|[0-9]{2})-[0-9]{2})\s*\|.*?\|\s*(US-[0-9]{3})\s*\|/g;
16
+ // 旧US ラベルは Markdown 強調(**旧US**:)で囲まれることがあるため `\**` を許容する。
16
17
  const INLINE_ALIAS_PATTERN =
17
- /(H[0-9]{2}-[0-9]{2}).*?旧US\s*[::]\s*(US-[0-9]{3})/g;
18
+ /(H(?:F\d+|[0-9]{2})-[0-9]{2}).*?旧US\**\s*[::]\s*(US-[0-9]{3})/g;
18
19
 
19
20
  function execAll(pattern: RegExp, text: string): RegExpExecArray[] {
20
21
  const results: RegExpExecArray[] = [];
@@ -56,14 +57,32 @@ export function parseStoryCatalog(content: string): ParsedStoryCatalog {
56
57
  aliasMap.set(inlineMatches[i][2], inlineMatches[i][1]);
57
58
  }
58
59
 
59
- // 前後行コンテキスト形式: H03-01 の直後に 旧US: US-001
60
+ // 見出しスコープ形式:
61
+ // ### H01-01: タイトル
62
+ //
63
+ // **旧US**: US-036
64
+ // のように、`### HXX-XX:` 見出しの後、次の見出しが現れるまでの範囲に `旧US: US-xxx`
65
+ // が出現するパターンに対応する。旧実装は直後行(lines[i+1])のみを見ていたため、
66
+ // 実際の user_stories.md(見出しと旧US行の間に空行や Epic 行が挟まる)では alias が
67
+ // 一切抽出されず、レガシー StoryId 解決が事実上機能していなかった。
68
+ const HEADING_STORY_ID = /^#{1,6}\s+(H(?:F\d+|[0-9]{2})-[0-9]{2})\b/;
69
+ const LEGACY_IN_LINE = /旧US\**\s*[::]\s*(US-[0-9]{3})/;
70
+ let currentHeadingStoryId: string | null = null;
60
71
  for (let i = 0; i < lines.length; i++) {
61
- const storyIdMatch = /\b(H(?:F\d+|[0-9]{2})-[0-9]{2})\b/.exec(lines[i]);
62
- if (storyIdMatch) {
63
- const nextLine = i + 1 < lines.length ? lines[i + 1] : '';
64
- const legacyMatch = /旧US\s*[::]\s*(US-[0-9]{3})/.exec(nextLine);
65
- if (legacyMatch) {
66
- aliasMap.set(legacyMatch[1], storyIdMatch[1]);
72
+ const headingMatch = HEADING_STORY_ID.exec(lines[i]);
73
+ if (headingMatch) {
74
+ currentHeadingStoryId = headingMatch[1];
75
+ continue;
76
+ }
77
+ if (/^#{1,6}\s+/.test(lines[i])) {
78
+ // StoryId を含まない別の見出しに入ったらスコープを閉じる
79
+ currentHeadingStoryId = null;
80
+ continue;
81
+ }
82
+ if (currentHeadingStoryId) {
83
+ const legacyMatch = LEGACY_IN_LINE.exec(lines[i]);
84
+ if (legacyMatch && !aliasMap.has(legacyMatch[1])) {
85
+ aliasMap.set(legacyMatch[1], currentHeadingStoryId);
67
86
  }
68
87
  }
69
88
  }