phasegate 0.181.0 → 0.191.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 (48) hide show
  1. package/docs/guide/skills-overview.md +10 -9
  2. package/package.json +1 -1
  3. package/scripts/harness/biome-ast-engine/infrastructure/parsers/comment-density-parser.ts +51 -10
  4. package/scripts/harness/ci-governance/composition-root.ts +1 -1
  5. package/scripts/harness/ci-governance/infrastructure/adapters/adr-foundation-existence-adapter.ts +26 -2
  6. package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +74 -1
  7. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -0
  8. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +8 -0
  9. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +75 -2
  10. package/scripts/harness/setup/skill-deployer.ts +6 -6
  11. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +13 -2
  12. package/scripts/harness/skill-quality/domain/types/skill-kind.ts +6 -0
  13. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +24 -8
  14. package/skills/cascade-updater/SKILL.md +3 -3
  15. package/skills/codebase-mapper/SKILL.md +17 -5
  16. package/skills/codex-delegator/SKILL.md +19 -4
  17. package/skills/codex-delegator/references/prompt-patterns.md +3 -3
  18. package/skills/codex-delegator/references/review-dimensions.md +1 -1
  19. package/skills/consistency-checker/SKILL.md +1 -1
  20. package/skills/doc-freshness-checker/SKILL.md +8 -0
  21. package/skills/domain-designer/SKILL.md +4 -2
  22. package/skills/engineering-perspective/SKILL.md +3 -0
  23. package/skills/environment-designer/SKILL.md +8 -6
  24. package/skills/implementation-planner/SKILL.md +12 -6
  25. package/skills/implementation-readiness-checker/SKILL.md +9 -1
  26. package/skills/it-test-designer/SKILL.md +10 -8
  27. package/skills/it-test-logic-designer/SKILL.md +11 -9
  28. package/skills/it-test-logic-designer/references/repository-test-patterns.md +8 -1
  29. package/skills/logical-designer/SKILL.md +5 -3
  30. package/skills/mock-designer/SKILL.md +10 -6
  31. package/skills/phasegate-config-doctor/SKILL.md +12 -2
  32. package/skills/phasegate-toolkit-guide/SKILL.md +3 -0
  33. package/skills/pointer-validator/SKILL.md +8 -0
  34. package/skills/quick-implementor/SKILL.md +7 -5
  35. package/skills/scenario-test-designer/SKILL.md +27 -14
  36. package/skills/scenario-test-logic-designer/SKILL.md +10 -8
  37. package/skills/scenario-test-logic-designer/references/msw-patterns.md +3 -1
  38. package/skills/scenario-test-logic-designer/references/playwright-patterns.md +3 -1
  39. package/skills/skill-creator/SKILL.md +8 -5
  40. package/skills/story-implementor/SKILL.md +2 -0
  41. package/skills/story-mapper/SKILL.md +4 -4
  42. package/skills/story-writer/SKILL.md +5 -5
  43. package/skills/test-coverage-checker/SKILL.md +4 -6
  44. package/skills/uiux-designer/SKILL.md +4 -2
  45. package/skills/unit-designer/SKILL.md +8 -6
  46. package/skills/unit-test-designer/SKILL.md +25 -11
  47. package/skills/unit-test-logic-designer/SKILL.md +10 -8
  48. package/skills/unit-test-logic-designer/references/test-patterns.md +7 -1
@@ -37,15 +37,16 @@ Artifacts are placed in `inception/{unit}/{US-XXX}/`.
37
37
  Skills run per User Story:
38
38
 
39
39
  1. `/logical-designer` (US-specific mode) — Story-scoped logical design refinement.
40
- 2. `/uiux-designer` — Final UI/UX definition incorporating test cases, logical design, and existing UI.
41
- 3. `/unit-test-designer` — Unit test case design from domain model.
42
- 4. `/it-test-designer` — Integration test case design from logical design.
43
- 5. `/scenario-test-designer` — E2E scenario test case design.
44
- 6. `/unit-test-logic-designer` — Vitest implementation logic with pseudocode.
45
- 7. `/it-test-logic-designer` — IT Vitest implementation logic with pseudocode.
46
- 8. `/scenario-test-logic-designer` — Playwright E2E test implementation logic.
47
- 9. `/implementation-readiness-checker` — Automated pre-implementation readiness verification.
48
- 10. `/story-implementor` — TDD implementation with atomic commits.
40
+ 2. `/scenario-test-designer` — E2E scenario test case design. Its `scenario_test_design.md` output is a required input of `/uiux-designer`.
41
+ 3. `/uiux-designer` — Final UI/UX definition incorporating test cases, logical design, and existing UI.
42
+ 4. `/unit-test-designer` — Unit test case design from domain model.
43
+ 5. `/it-test-designer` — Integration test case design from logical design.
44
+ 6. `/test-coverage-checker` — Coverage verification of all three test case designs before test logic design.
45
+ 7. `/unit-test-logic-designer` — Vitest implementation logic with pseudocode.
46
+ 8. `/it-test-logic-designer` — IT Vitest implementation logic with pseudocode.
47
+ 9. `/scenario-test-logic-designer` — Playwright E2E test implementation logic.
48
+ 10. `/implementation-readiness-checker` — Automated pre-implementation readiness verification.
49
+ 11. `/story-implementor` — TDD implementation with atomic commits.
49
50
 
50
51
  ### Phase Dependency Rules
51
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.181.0",
3
+ "version": "0.191.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -11,22 +11,37 @@ export type CommentDensityResult = {
11
11
 
12
12
  const SINGLE_LINE_COMMENT = /^\s*\/\//;
13
13
  const MULTI_LINE_COMMENT_START = /^\s*\/\*/;
14
+ const DOC_COMMENT_START = /^\s*\/\*\*/;
14
15
  const MULTI_LINE_COMMENT_END = /\*\//;
15
16
  const BLANK_LINE = /^\s*$/;
17
+ // L1 ルール (require-unit-comment / require-layer-comment) が必須化する
18
+ // 先頭メタデータヘッダのタグ行。ファイル冒頭の連続コメント領域でのみ効力を持つ。
19
+ const METADATA_HEADER_TAG = /^\s*\/\/\s*@(unit|layer|work-item-id|story|story-id|issue-id|module|feature|epic)\b/;
16
20
 
17
21
  /**
18
22
  * ソースコードのコメント密度と繰り返しブロック数を算出する。
19
23
  *
20
- * - commentLineCount: コメント行数(単一行・複数行両方)
21
- * - logicalLineCount: 空行を除いた行数
22
- * - repeatedCommentBlocks: 連続する同一コメントブロックの検出回数
24
+ * WI-239: commentLineCount(密度分子)は「密度に寄与する narrative コメント行数」
25
+ * を表すよう意味を再定義した。次は分子から除外する:
26
+ * - 先頭の必須メタデータヘッダ行(`// @unit` / `// @layer` / `// @work-item-id` /
27
+ * `// @story` 等。ファイル冒頭の連続コメント領域に限る)
28
+ * - `/** … *\/` doc-comment ブロック(ブロック全体)
29
+ * 分子に残すのは narrative な `//` 行コメントと非 doc の `/* … *\/` ブロックのみ。
30
+ *
31
+ * - commentLineCount: 密度分子となる narrative コメント行数
32
+ * - logicalLineCount: 空行を除いた行数(分母。定義不変)
33
+ * - repeatedCommentBlocks: 連続する同一 narrative コメントブロックの検出回数
23
34
  */
24
35
  export const parseCommentDensity = (sourceCode: string): CommentDensityResult => {
25
- const lines = sourceCode.split('\n');
36
+ const lines = sourceCode.split("\n");
26
37
 
27
38
  let commentLineCount = 0;
28
39
  let logicalLineCount = 0;
29
40
  let insideBlockComment = false;
41
+ let insideDocComment = false;
42
+ // ファイル冒頭の連続したメタデータヘッダ領域内かどうか。
43
+ // 最初のコード行または非メタデータコメントが現れた時点で false になる。
44
+ let insideHeaderZone = true;
30
45
 
31
46
  const commentBlocks: string[] = [];
32
47
  let currentBlock: string[] = [];
@@ -41,11 +56,14 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
41
56
  logicalLineCount += 1;
42
57
 
43
58
  if (insideBlockComment) {
44
- commentLineCount += 1;
45
- currentBlock.push(line.trim());
59
+ if (!insideDocComment) {
60
+ commentLineCount += 1;
61
+ currentBlock.push(line.trim());
62
+ }
46
63
 
47
64
  if (MULTI_LINE_COMMENT_END.test(line)) {
48
65
  insideBlockComment = false;
66
+ insideDocComment = false;
49
67
  flushBlock(currentBlock, commentBlocks);
50
68
  currentBlock = [];
51
69
  }
@@ -54,25 +72,48 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
54
72
  }
55
73
 
56
74
  if (SINGLE_LINE_COMMENT.test(line)) {
75
+ const isMetadataHeader = insideHeaderZone && METADATA_HEADER_TAG.test(line);
76
+
77
+ if (isMetadataHeader) {
78
+ // メタデータヘッダは分子・反復検出のいずれにも寄与しない。
79
+ continue;
80
+ }
81
+
82
+ // narrative な行コメントに到達 → ヘッダ領域は終了。
83
+ insideHeaderZone = false;
57
84
  commentLineCount += 1;
58
85
  currentBlock.push(line.trim());
59
86
  continue;
60
87
  }
61
88
 
62
89
  if (MULTI_LINE_COMMENT_START.test(line)) {
90
+ insideHeaderZone = false;
91
+ const isDocComment = DOC_COMMENT_START.test(line);
92
+ const isClosed = MULTI_LINE_COMMENT_END.test(line);
93
+
94
+ if (isDocComment) {
95
+ // doc-comment ブロックは分子・反復検出のいずれにも寄与しない。
96
+ if (!isClosed) {
97
+ insideBlockComment = true;
98
+ insideDocComment = true;
99
+ }
100
+ continue;
101
+ }
102
+
63
103
  commentLineCount += 1;
64
- insideBlockComment = true;
65
104
  currentBlock.push(line.trim());
66
105
 
67
- if (MULTI_LINE_COMMENT_END.test(line)) {
68
- insideBlockComment = false;
106
+ if (isClosed) {
69
107
  flushBlock(currentBlock, commentBlocks);
70
108
  currentBlock = [];
109
+ } else {
110
+ insideBlockComment = true;
71
111
  }
72
112
 
73
113
  continue;
74
114
  }
75
115
 
116
+ insideHeaderZone = false;
76
117
  flushBlock(currentBlock, commentBlocks);
77
118
  currentBlock = [];
78
119
  }
@@ -86,7 +127,7 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
86
127
 
87
128
  const flushBlock = (current: string[], blocks: string[]): void => {
88
129
  if (current.length > 0) {
89
- blocks.push(current.join('\n'));
130
+ blocks.push(current.join("\n"));
90
131
  }
91
132
  };
92
133
 
@@ -84,7 +84,7 @@ export function buildCiGovernance(
84
84
  const templateRendererPort = new YamlTemplateRendererAdapter(harnessRoot);
85
85
  const fileExistencePort = new FileSystemExistenceAdapter(baseDir);
86
86
  const commandExistencePort = new HarnessApiCommandExistenceAdapter();
87
- const adrExistencePort = new AdrFoundationExistenceAdapter();
87
+ const adrExistencePort = new AdrFoundationExistenceAdapter(baseDir);
88
88
  const agentsMdPort = new AgentsMdFileAdapter(baseDir);
89
89
  const agentContextDocumentPort = new AgentContextFileAdapter(baseDir, harnessRoot);
90
90
  const lessonArtifactReaderPort = new LessonArtifactFileReaderAdapter(baseDir);
@@ -5,12 +5,36 @@
5
5
  * AdrExistencePort実装
6
6
  */
7
7
 
8
+ import * as path from 'node:path';
8
9
  import type { AdrExistencePort } from '../../domain/ports/adr-existence-port.js';
9
10
 
10
11
  export class AdrFoundationExistenceAdapter implements AdrExistencePort {
11
- constructor(private readonly knownAdrIds: string[] = []) {}
12
+ private repositoryPromise: Promise<{
13
+ exists(id: { readonly value: string }): Promise<boolean>;
14
+ }> | null = null;
15
+
16
+ constructor(private readonly rootDir: string = process.cwd()) {}
12
17
 
13
18
  async exists(adrId: string): Promise<boolean> {
14
- return this.knownAdrIds.includes(adrId);
19
+ try {
20
+ const { AdrId } = await import('../../../adr-foundation/domain/value-objects/adr-id.js');
21
+ const id = AdrId.create(adrId);
22
+ const repository = await this.getRepository();
23
+ return repository.exists(id);
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ private async getRepository(): Promise<{
30
+ exists(id: { readonly value: string }): Promise<boolean>;
31
+ }> {
32
+ if (!this.repositoryPromise) {
33
+ this.repositoryPromise = (async () => {
34
+ const { createAdrFoundationModule } = await import('../../../adr-foundation/composition-root.js');
35
+ return createAdrFoundationModule(path.join(this.rootDir, 'docs', 'ADR')).adrRepository;
36
+ })();
37
+ }
38
+ return this.repositoryPromise;
15
39
  }
16
40
  }
@@ -3,10 +3,83 @@
3
3
 
4
4
  import type { CommandExistencePort } from '../../domain/ports/command-existence-port.js';
5
5
 
6
+ const KNOWN_HARNESS_COMMANDS: readonly string[] = [
7
+ 'baseline',
8
+ 'bypass:audit',
9
+ 'check-change-category',
10
+ 'check-phase-gate',
11
+ 'ci-check',
12
+ 'ci:auto-refresh-agent-context',
13
+ 'ci:check-repetition',
14
+ 'ci:generate-template',
15
+ 'ci:migrate-agents-md',
16
+ 'commit-msg',
17
+ 'complete-check',
18
+ 'config:plan',
19
+ 'delegate-sonnet',
20
+ 'disable-feature',
21
+ 'doctor',
22
+ 'emit-agent-rules',
23
+ 'enable-feature',
24
+ 'hook',
25
+ 'init',
26
+ 'install',
27
+ 'lint',
28
+ 'list-adrs',
29
+ 'list-errors',
30
+ 'list-features',
31
+ 'migrate',
32
+ 'p2:check-agent-context',
33
+ 'p2:check-freshness',
34
+ 'p2:check-initial-creation',
35
+ 'p2:generate-e2e-template',
36
+ 'p2:validate-pointers',
37
+ 'phasegate:attest',
38
+ 'phasegate:check-phase',
39
+ 'phasegate:check-ready',
40
+ 'phasegate:ci-check',
41
+ 'phasegate:complete-check',
42
+ 'phasegate:detect-drift',
43
+ 'phasegate:generate-matrix',
44
+ 'phasegate:impact-analysis',
45
+ 'phasegate:lint',
46
+ 'phasegate:status',
47
+ 'phasegate:verify-attestation',
48
+ 'pre-commit',
49
+ 'reconcile',
50
+ 'refresh-claude-md',
51
+ 'regression:analyze-migration',
52
+ 'regression:configure-ci-gate',
53
+ 'regression:migrate-v0-tests',
54
+ 'regression:run-agent-guard',
55
+ 'regression:run-gng-gate',
56
+ 'regression:run-k-requirements',
57
+ 'regression:run-k14-k15',
58
+ 'render-errors',
59
+ 'scaffold-design',
60
+ 'scaffold-wi',
61
+ 'session',
62
+ 'setup:agent',
63
+ 'skill:apply-cascade-update',
64
+ 'skill:check-coverage',
65
+ 'skill:collect-lessons',
66
+ 'skill:execute-tdd-cycle',
67
+ 'skill:validate-structure',
68
+ 'skills',
69
+ 'status',
70
+ 'uninstall',
71
+ 'update-skills',
72
+ 'validate',
73
+ 'validate-adr',
74
+ 'validate-fix',
75
+ 'validate-metadata',
76
+ 'work-items:status',
77
+ ];
78
+
6
79
  export class HarnessApiCommandExistenceAdapter implements CommandExistencePort {
7
80
  private readonly knownCommands: Set<string>;
8
81
 
9
- constructor(knownCommands: string[] = []) {
82
+ constructor(knownCommands: string[] = [...KNOWN_HARNESS_COMMANDS]) {
10
83
  this.knownCommands = new Set(knownCommands);
11
84
  }
12
85
 
@@ -4,6 +4,8 @@
4
4
  export interface StoryReflectionFileSystemPort {
5
5
  listStoryDirectories(unitId: string): Promise<readonly string[]>;
6
6
  storyAffectsUnit(storyId: string, unitId: string): Promise<boolean>;
7
+ /** WI のコミット(Work-Item trailer)が scripts/harness/{unitId}/{layer}/ 配下のソースを変更したか */
8
+ storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean>;
7
9
  fileExists(path: string): Promise<boolean>;
8
10
  /** product 文書内に @story-id {storyId} アノテーションが含まれているか */
9
11
  fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean>;
@@ -49,6 +49,14 @@ export class StoryReflectionChecker {
49
49
  continue;
50
50
  }
51
51
 
52
+ if (
53
+ resolvedInception.isCrossWorkItem &&
54
+ productPath.endsWith("domain_model.md") &&
55
+ !(await this.fsPort.storyTouchesUnitLayer(storyId, unitId, "domain"))
56
+ ) {
57
+ continue;
58
+ }
59
+
52
60
  const hasAnnotation = await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
53
61
 
54
62
  if (!hasAnnotation) {
@@ -4,15 +4,18 @@
4
4
  * @work-item-id WI-115
5
5
  */
6
6
 
7
+ import { execFile } from "node:child_process";
7
8
  import type { Dirent } from "node:fs";
8
9
  import { access, readdir, readFile } from "node:fs/promises";
9
10
  import path from "node:path";
11
+ import { promisify } from "node:util";
10
12
  import type { StoryReflectionFileSystemPort } from "../../domain/ports/story-reflection-file-system-port.js";
11
13
 
12
14
  const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
13
15
  const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
14
16
  const AFFECTS_INLINE_PATTERN = /^affects:\s*\[([^\]]*)\]\s*$/m;
15
17
  const LEGACY_ID_PATTERN = /^legacy_id:\s*([A-Z][\w]+-\d+)\s*$/m;
18
+ const execFileAsync = promisify(execFile);
16
19
 
17
20
  export interface FileSystemStoryReflectionAdapterDeps {
18
21
  readonly rootDir: string;
@@ -34,6 +37,7 @@ export interface FileSystemStoryReflectionAdapterDeps {
34
37
  export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
35
38
  private readonly rootDir: string;
36
39
  private readonly inceptionRoot: string;
40
+ private readonly changedPathsByStoryId = new Map<string, Promise<ReadonlySet<string>>>();
37
41
 
38
42
  constructor(deps: FileSystemStoryReflectionAdapterDeps) {
39
43
  this.rootDir = deps.rootDir;
@@ -110,7 +114,7 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
110
114
 
111
115
  const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
112
116
  if (frontmatter === undefined) {
113
- return true;
117
+ return false;
114
118
  }
115
119
 
116
120
  const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
@@ -119,12 +123,81 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
119
123
  .filter((value) => value.length > 0);
120
124
 
121
125
  if (affects === undefined) {
122
- return true;
126
+ return false;
123
127
  }
124
128
 
125
129
  return affects.includes(unitId);
126
130
  }
127
131
 
132
+ async storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean> {
133
+ const paths = await this.readChangedPathsForStory(storyId);
134
+ const prefix = `scripts/harness/${unitId}/${layer}/`;
135
+
136
+ for (const changedPath of paths) {
137
+ if (changedPath.startsWith(prefix)) {
138
+ return true;
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ private readChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
146
+ const cached = this.changedPathsByStoryId.get(storyId);
147
+ if (cached !== undefined) {
148
+ return cached;
149
+ }
150
+
151
+ const promise = this.fetchChangedPathsForStory(storyId);
152
+ this.changedPathsByStoryId.set(storyId, promise);
153
+ return promise;
154
+ }
155
+
156
+ private async fetchChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
157
+ try {
158
+ const { stdout } = await execFileAsync(
159
+ "git",
160
+ [
161
+ "log",
162
+ "--format=@@COMMIT@@%x00%B%x00",
163
+ "--name-only",
164
+ "--grep",
165
+ `Work-Item:.*\\b${storyId}\\b`,
166
+ ],
167
+ { cwd: this.rootDir, maxBuffer: 32 * 1024 * 1024 },
168
+ );
169
+
170
+ return this.extractChangedPathsForStory(stdout, storyId);
171
+ } catch {
172
+ return new Set<string>();
173
+ }
174
+ }
175
+
176
+ private extractChangedPathsForStory(output: string, storyId: string): ReadonlySet<string> {
177
+ const changedPaths = new Set<string>();
178
+ const trailerPattern = new RegExp(`Work-Item:[^\\n]*\\b${storyId}\\b`);
179
+
180
+ for (const commitBlock of output.split("@@COMMIT@@\u0000")) {
181
+ if (commitBlock.length === 0) continue;
182
+
183
+ const bodyEnd = commitBlock.indexOf("\u0000");
184
+ if (bodyEnd === -1) continue;
185
+
186
+ const body = commitBlock.slice(0, bodyEnd);
187
+ if (!trailerPattern.test(body)) continue;
188
+
189
+ const nameOnly = commitBlock.slice(bodyEnd + 1);
190
+ for (const changedPath of nameOnly.split(/\r?\n/)) {
191
+ const trimmedPath = changedPath.trim();
192
+ if (trimmedPath.length > 0 && trimmedPath !== "@@COMMIT@@") {
193
+ changedPaths.add(trimmedPath);
194
+ }
195
+ }
196
+ }
197
+
198
+ return changedPaths;
199
+ }
200
+
128
201
  async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
129
202
  const absolutePath = path.join(this.rootDir, productPath);
130
203
 
@@ -173,9 +173,12 @@ export function renderSkillForModelDelegation(content: string, policy: ModelDele
173
173
  return content
174
174
  .replace(/^model:\s+\S+\n/gm, "")
175
175
  .replace(/^review:\s+\S+\n/gm, "")
176
- .replaceAll("Sonnet 4.6 に委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)", "メインセッションが成果物を生成する")
176
+ .replaceAll(
177
+ "委任先モデルに委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)",
178
+ "メインセッションが成果物を生成する",
179
+ )
177
180
  .replaceAll("Opus が", "メインセッションが")
178
- .replaceAll("Sonnet 4.6", "メインセッション")
181
+ .replaceAll("委任先モデル", "メインセッション")
179
182
  .replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
180
183
  }
181
184
 
@@ -726,10 +729,7 @@ export async function deployHuskyCommitMsgHook(
726
729
  return { created: true, path: targetPath };
727
730
  }
728
731
 
729
- export async function deployHuskyPrePushHook(
730
- harnessRoot: string,
731
- projectRoot: string,
732
- ): Promise<DeployHuskyHookResult> {
732
+ export async function deployHuskyPrePushHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
733
733
  const targetPath = join(projectRoot, ".husky", "pre-push");
734
734
 
735
735
  try {
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
- * @work-item-id WI-212
4
+ * @work-item-id WI-212, WI-241
5
5
  */
6
6
  import { SkillStructure } from '../value-objects/skill-structure.js';
7
7
  import { SkillValidationResult } from '../value-objects/skill-validation-result.js';
8
8
  import type { SkillFileReaderPort } from '../ports/skill-file-reader-port.js';
9
9
  import type { SectionName } from '../types/section-name.js';
10
+ import type { SkillKind } from '../types/skill-kind.js';
10
11
 
11
12
  export class SkillStructureValidator {
12
13
  constructor(private readonly skillFileReaderPort: SkillFileReaderPort) {}
@@ -14,7 +15,8 @@ export class SkillStructureValidator {
14
15
  async validate(skillFilePath: string): Promise<SkillValidationResult> {
15
16
  const rawContent = await this.skillFileReaderPort.read(skillFilePath);
16
17
  const actualSections = this.extractSections(rawContent);
17
- const structure = SkillStructure.default();
18
+ const kind = this.extractKind(rawContent);
19
+ const structure = SkillStructure.forKind(kind);
18
20
  const missingSections = structure.getMissingSections(actualSections);
19
21
 
20
22
  if (missingSections.length === 0) {
@@ -74,6 +76,15 @@ export class SkillStructureValidator {
74
76
  return sections;
75
77
  }
76
78
 
79
+ private extractKind(content: string): SkillKind {
80
+ const lines = content.split('\n');
81
+ const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
82
+ if (lines[0]?.trim() !== '---' || closingDelimiterIndex < 0) return 'lifecycle';
83
+
84
+ const frontmatterLines = lines.slice(1, closingDelimiterIndex);
85
+ return frontmatterLines.some((line) => /^\s*kind\s*:\s*advisory\s*$/.test(line)) ? 'advisory' : 'lifecycle';
86
+ }
87
+
77
88
  private hasLanguageMetadata(lines: readonly string[]): boolean {
78
89
  const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
79
90
  if (closingDelimiterIndex < 0) return false;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit skill-quality
4
+ * @work-item-id WI-241
5
+ */
6
+ export type SkillKind = 'lifecycle' | 'advisory';
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit skill-quality
4
- * @work-item-id WI-212
4
+ * @work-item-id WI-212, WI-241
5
5
  */
6
6
  import type { SectionName } from '../types/section-name.js';
7
+ import type { SkillKind } from '../types/skill-kind.js';
7
8
 
8
- const REQUIRED_SECTIONS: readonly SectionName[] = [
9
+ const REQUIRED_SECTIONS: readonly SectionName[] = Object.freeze([
9
10
  'frontmatter',
10
11
  'languageMetadata',
11
12
  'purpose',
@@ -13,7 +14,13 @@ const REQUIRED_SECTIONS: readonly SectionName[] = [
13
14
  'outputs',
14
15
  'prerequisites',
15
16
  'executionFlow',
16
- ];
17
+ ]);
18
+
19
+ const ADVISORY_REQUIRED_SECTIONS: readonly SectionName[] = Object.freeze([
20
+ 'frontmatter',
21
+ 'languageMetadata',
22
+ 'purpose',
23
+ ]);
17
24
 
18
25
  export class SkillStructure {
19
26
  readonly requiredSections: readonly SectionName[];
@@ -23,13 +30,22 @@ export class SkillStructure {
23
30
  Object.freeze(this);
24
31
  }
25
32
 
26
- private static _instance: SkillStructure | null = null;
33
+ private static _instances: Partial<Record<SkillKind, SkillStructure>> = {};
27
34
 
28
- static default(): SkillStructure {
29
- if (!SkillStructure._instance) {
30
- SkillStructure._instance = new SkillStructure(REQUIRED_SECTIONS);
35
+ static forKind(kind: SkillKind): SkillStructure {
36
+ const cached = SkillStructure._instances[kind];
37
+ if (cached) {
38
+ return cached;
31
39
  }
32
- return SkillStructure._instance;
40
+
41
+ const requiredSections = kind === 'advisory' ? ADVISORY_REQUIRED_SECTIONS : REQUIRED_SECTIONS;
42
+ const instance = new SkillStructure(requiredSections);
43
+ SkillStructure._instances[kind] = instance;
44
+ return instance;
45
+ }
46
+
47
+ static default(): SkillStructure {
48
+ return SkillStructure.forKind('lifecycle');
33
49
  }
34
50
 
35
51
  getMissingSections(actualSections: readonly SectionName[]): readonly SectionName[] {
@@ -12,7 +12,7 @@ languages: [typescript]
12
12
 
13
13
  ## storyReflection ゲート通過の標準手段
14
14
 
15
- `phaseDependencies.storyReflection`(ADR-013)は、`docs/inception/{unit}/{storyId}/` 配下の US/issue 設計が `docs/product/construction/{unit}/` の累積設計文書に反映されていない場合、`src/{unit}/*` および `scripts/harness/{unit}/*` への Write/Edit(Bash 迂回経由の書き込みも含む)を pre-tool-use hook でブロックする。
15
+ `phaseDependencies.storyReflection`(ADR-013)は、`docs/inception/{unit}/{storyId}/` 配下の US/issue 設計が `docs/product/construction/{unit}/` の累積設計文書に反映されていない場合、`src/{unit}/*`(phasegate 自リポジトリ(dogfood)では `scripts/harness/{unit}/*`)への Write/Edit(Bash 迂回経由の書き込みも含む)を pre-tool-use hook でブロックする。
16
16
 
17
17
  **cascade-updater はこの storyReflection ゲートを通過する標準手段である。**
18
18
 
@@ -21,7 +21,7 @@ languages: [typescript]
21
21
  - `minimal` プリセットでは storyReflection が無効化されるためゲート連動しないが、cascade-updater による設計整合性の維持は推奨される
22
22
  - Quick Mode (`relaxedGates: ["phase-gate"]`) 時は storyReflection も緩和されるため、cascade-updater 実行の必須性は緩まる(ただし品質維持のため実行は推奨)
23
23
 
24
- 詳細は ADR-013 および `docs/guide/configuration.md` の storyReflection セクションを参照。
24
+ 詳細は ADR-013 および `docs/guide/configuration.md` の storyReflection セクションを参照(consumer プロジェクトでは `node_modules/phasegate/docs/ADR/...` / `node_modules/phasegate/docs/guide/...`、phasegate 自リポジトリでは `docs/...` を参照する)。
25
25
 
26
26
  ## 前提条件チェック
27
27
 
@@ -120,7 +120,7 @@ languages: [typescript]
120
120
  ## Phase 3: レビュー(Opus review)
121
121
 
122
122
  ### 実行主体
123
- メインセッション(Opus 4.6)が実行する。Sonnetへの再委任は行わない。
123
+ メインセッション(model-routing.md の Architect ロール)が実行する。Sonnetへの再委任は行わない。
124
124
 
125
125
  ### レビュー手順
126
126
  1. Sonnetが出力したファイルを読み込む
@@ -8,9 +8,21 @@ languages: [typescript]
8
8
 
9
9
  # Codebase Mapper
10
10
 
11
+ ## 目的
12
+
11
13
  全ソースファイルの `@unit`/`@layer` アノテーションとimportグラフを解析し、
12
14
  コードベースの構造マップを生成するスキル。
13
15
 
16
+ ## 入力
17
+
18
+ - 解析対象ソース: `phasegate.config.json` の `sourceDir` 配下の `**/*.ts`(`__tests__/`・`*.test.ts` は除外)
19
+ - 各ソースファイルの `@unit` / `@layer` アノテーションと `import` 文(Unit間依存グラフの構築に使用)
20
+
21
+ ## 前提条件
22
+
23
+ - `phasegate.config.json` に `sourceDir` が設定されていること
24
+ - 解析対象ソースに `@unit` / `@layer` アノテーションが付与されていること(欠落ファイルは後述「アノテーション欠落の対処」で報告される)
25
+
14
26
  ## 出力物
15
27
 
16
28
  `{constructionDir}/codebase-map.md` — Unit・レイヤー分布・依存関係の可視化文書(パスは `phasegate.config.json` で設定)
@@ -99,7 +111,7 @@ Unit間依存グラフを深さ優先探索でサイクル検出する。
99
111
 
100
112
  | Unit | ファイル数 | レイヤー分布 |
101
113
  |------|----------|-----------|
102
- | harness-error | 12 | domain:4, app:4, infra:2, pres:2 |
114
+ | unit-a | 12 | domain:4, app:4, infra:2, pres:2 |
103
115
  | ...
104
116
 
105
117
  ---
@@ -107,11 +119,11 @@ Unit間依存グラフを深さ優先探索でサイクル検出する。
107
119
  ## Unit 間依存グラフ
108
120
 
109
121
  ```
110
- harness-api
111
- └── depends on: biome-ast-engine, validator-system, ...
122
+ unit-a
123
+ └── depends on: unit-b, unit-c, ...
112
124
 
113
- validator-system
114
- └── depends on: harness-error, config-foundation
125
+ unit-c
126
+ └── depends on: unit-d, unit-e
115
127
  ```
116
128
 
117
129
  ---