phasegate 0.183.0 → 0.212.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 (100) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.ja.md +52 -15
  3. package/README.md +39 -11
  4. package/docs/ADR/030-injection-threat-model-and-trust-root.md +145 -0
  5. package/docs/guide/hooks-integration.md +50 -1
  6. package/docs/guide/installation.md +1 -1
  7. package/docs/guide/quick-vs-full-mode.md +1 -1
  8. package/docs/guide/skills-overview.md +17 -17
  9. package/package.json +1 -1
  10. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +131 -63
  11. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +31 -4
  12. package/scripts/harness/agent-integration/presentation/spotlight.ts +65 -0
  13. package/scripts/harness/biome-ast-engine/infrastructure/parsers/comment-density-parser.ts +51 -10
  14. package/scripts/harness/ci-governance/application/dto/pin-integrity-input.ts +8 -0
  15. package/scripts/harness/ci-governance/application/dto/pin-integrity-output.ts +9 -0
  16. package/scripts/harness/ci-governance/application/dto/verify-integrity-input.ts +7 -0
  17. package/scripts/harness/ci-governance/application/dto/verify-integrity-output.ts +10 -0
  18. package/scripts/harness/ci-governance/application/usecases/pin-integrity-usecase.ts +60 -0
  19. package/scripts/harness/ci-governance/application/usecases/verify-integrity-usecase.ts +46 -0
  20. package/scripts/harness/ci-governance/composition-root.ts +68 -65
  21. package/scripts/harness/ci-governance/domain/ports/integrity-manifest-repository-port.ts +14 -0
  22. package/scripts/harness/ci-governance/domain/ports/sha256-hasher-port.ts +10 -0
  23. package/scripts/harness/ci-governance/domain/services/integrity-checker.ts +42 -0
  24. package/scripts/harness/ci-governance/domain/value-objects/integrity-drift.ts +16 -0
  25. package/scripts/harness/ci-governance/domain/value-objects/integrity-manifest.ts +49 -0
  26. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +40 -0
  27. package/scripts/harness/ci-governance/infrastructure/adapters/adr-foundation-existence-adapter.ts +26 -2
  28. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha256-hasher-adapter.ts +17 -0
  29. package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +8 -1
  30. package/scripts/harness/ci-governance/infrastructure/adapters/integrity-manifest-json-repository-adapter.ts +83 -0
  31. package/scripts/harness/ci-governance/presentation/handlers/integrity-handler.ts +76 -0
  32. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +53 -28
  33. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +33 -8
  34. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +90 -0
  35. package/scripts/harness/installation/application/bundled-skill-selection.ts +2 -5
  36. package/scripts/harness/main.ts +257 -105
  37. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -0
  38. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +8 -0
  39. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +137 -2
  40. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
  41. package/scripts/harness/setup/skill-deployer.ts +8 -10
  42. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +13 -2
  43. package/scripts/harness/skill-quality/domain/types/skill-kind.ts +6 -0
  44. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +24 -8
  45. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +82 -49
  46. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +87 -58
  47. package/scripts/harness/validator-system/composition-root.ts +141 -99
  48. package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -0
  49. package/scripts/harness/validator-system/domain/ports/injection-scan-policy-port.ts +14 -0
  50. package/scripts/harness/validator-system/domain/services/coverage-attestation-gating-service.ts +56 -0
  51. package/scripts/harness/validator-system/domain/services/injection-pattern-scan-service.ts +118 -0
  52. package/scripts/harness/validator-system/domain/value-objects/coverage-gating-report.ts +67 -0
  53. package/scripts/harness/validator-system/domain/value-objects/injection-scan-report.ts +55 -0
  54. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +35 -31
  55. package/scripts/harness/validator-system/infrastructure/adapters/adr-foundation-reference-adapter.ts +13 -7
  56. package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-gating-adapter.ts +87 -0
  57. package/scripts/harness/validator-system/infrastructure/adapters/file-system-injection-scan-adapter.ts +82 -0
  58. package/skills/README.md +1 -1
  59. package/skills/cascade-updater/SKILL.md +3 -3
  60. package/skills/codebase-mapper/SKILL.md +6 -6
  61. package/skills/codex-delegator/SKILL.md +4 -3
  62. package/skills/codex-delegator/references/prompt-patterns.md +3 -3
  63. package/skills/codex-delegator/references/review-dimensions.md +1 -1
  64. package/skills/consistency-checker/SKILL.md +1 -1
  65. package/skills/consistency-checker/references//343/203/201/343/202/247/343/203/203/343/202/257/343/203/252/343/202/271/343/203/210.md +1 -1
  66. package/skills/doc-health-checker/SKILL.md +148 -0
  67. package/skills/domain-designer/SKILL.md +4 -2
  68. package/skills/engineering-perspective/SKILL.md +1 -0
  69. package/skills/environment-designer/SKILL.md +8 -6
  70. package/skills/implementation-readiness-checker/SKILL.md +2 -1
  71. package/skills/it-test-designer/SKILL.md +10 -8
  72. package/skills/it-test-logic-designer/SKILL.md +11 -9
  73. package/skills/it-test-logic-designer/references/repository-test-patterns.md +8 -1
  74. package/skills/logical-designer/SKILL.md +5 -3
  75. package/skills/mock-designer/SKILL.md +10 -6
  76. package/skills/phasegate-config-doctor/SKILL.md +3 -2
  77. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  78. package/skills/quick-implementor/SKILL.md +1 -1
  79. package/skills/release-publisher/SKILL.md +101 -0
  80. package/skills/scenario-test-designer/SKILL.md +27 -14
  81. package/skills/scenario-test-logic-designer/SKILL.md +10 -8
  82. package/skills/scenario-test-logic-designer/references/msw-patterns.md +3 -1
  83. package/skills/scenario-test-logic-designer/references/playwright-patterns.md +3 -1
  84. package/skills/skill-creator/SKILL.md +75 -332
  85. package/skills/story-implementor/SKILL.md +54 -0
  86. package/skills/story-mapper/SKILL.md +8 -4
  87. package/skills/story-writer/SKILL.md +14 -5
  88. package/skills/test-coverage-checker/SKILL.md +4 -6
  89. package/skills/uiux-designer/SKILL.md +4 -2
  90. package/skills/uiux-designer/references/uiux-design-template.md +4 -4
  91. package/skills/unit-designer/SKILL.md +11 -7
  92. package/skills/unit-test-designer/SKILL.md +25 -11
  93. package/skills/unit-test-logic-designer/SKILL.md +10 -8
  94. package/skills/unit-test-logic-designer/references/test-patterns.md +7 -1
  95. package/skills/doc-freshness-checker/SKILL.md +0 -140
  96. package/skills/implementation-planner/SKILL.md +0 -167
  97. package/skills/implementation-planner/references/document-structure.md +0 -116
  98. package/skills/implementation-planner/references/plan-template.md +0 -177
  99. package/skills/implementation-planner/references/workflow.md +0 -164
  100. package/skills/pointer-validator/SKILL.md +0 -104
@@ -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,24 @@
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
+ /** コミット本文の `Work-Item: WI-NNN` trailer 行から WI を抽出する */
19
+ const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*(WI-\d+)\s*$/gm;
20
+ /** ソースファイル先頭の `@work-item-id` / `@story` タグ配下の WI を抽出する */
21
+ const SOURCE_TAG_PATTERN = /@(?:work-item-id|story)\b[^\n\r]*/g;
22
+ /** harness ソースパスから unit / layer を切り出す(帰属フィルタ適用対象の判定) */
23
+ const HARNESS_SOURCE_PATH_PATTERN = /^scripts\/harness\/[^/]+\/[^/]+\//;
24
+ const execFileAsync = promisify(execFile);
16
25
 
17
26
  export interface FileSystemStoryReflectionAdapterDeps {
18
27
  readonly rootDir: string;
@@ -34,6 +43,7 @@ export interface FileSystemStoryReflectionAdapterDeps {
34
43
  export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
35
44
  private readonly rootDir: string;
36
45
  private readonly inceptionRoot: string;
46
+ private readonly changedPathsByStoryId = new Map<string, Promise<ReadonlySet<string>>>();
37
47
 
38
48
  constructor(deps: FileSystemStoryReflectionAdapterDeps) {
39
49
  this.rootDir = deps.rootDir;
@@ -110,7 +120,7 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
110
120
 
111
121
  const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
112
122
  if (frontmatter === undefined) {
113
- return true;
123
+ return false;
114
124
  }
115
125
 
116
126
  const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
@@ -119,12 +129,137 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
119
129
  .filter((value) => value.length > 0);
120
130
 
121
131
  if (affects === undefined) {
122
- return true;
132
+ return false;
123
133
  }
124
134
 
125
135
  return affects.includes(unitId);
126
136
  }
127
137
 
138
+ async storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean> {
139
+ const paths = await this.readChangedPathsForStory(storyId);
140
+ const prefix = `scripts/harness/${unitId}/${layer}/`;
141
+
142
+ for (const changedPath of paths) {
143
+ if (changedPath.startsWith(prefix)) {
144
+ return true;
145
+ }
146
+ }
147
+
148
+ return false;
149
+ }
150
+
151
+ private readChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
152
+ const cached = this.changedPathsByStoryId.get(storyId);
153
+ if (cached !== undefined) {
154
+ return cached;
155
+ }
156
+
157
+ const promise = this.fetchChangedPathsForStory(storyId);
158
+ this.changedPathsByStoryId.set(storyId, promise);
159
+ return promise;
160
+ }
161
+
162
+ private async fetchChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
163
+ try {
164
+ const { stdout } = await execFileAsync(
165
+ "git",
166
+ [
167
+ "log",
168
+ "--format=@@COMMIT@@%x00%B%x00",
169
+ "--name-only",
170
+ "--grep",
171
+ `Work-Item:.*\\b${storyId}\\b`,
172
+ ],
173
+ { cwd: this.rootDir, maxBuffer: 32 * 1024 * 1024 },
174
+ );
175
+
176
+ return await this.extractChangedPathsForStory(stdout, storyId);
177
+ } catch {
178
+ return new Set<string>();
179
+ }
180
+ }
181
+
182
+ private async extractChangedPathsForStory(output: string, storyId: string): Promise<ReadonlySet<string>> {
183
+ const changedPaths = new Set<string>();
184
+ const trailerPattern = new RegExp(`Work-Item:[^\\n]*\\b${storyId}\\b`);
185
+
186
+ for (const commitBlock of output.split("@@COMMIT@@\u0000")) {
187
+ if (commitBlock.length === 0) continue;
188
+
189
+ const bodyEnd = commitBlock.indexOf("\u0000");
190
+ if (bodyEnd === -1) continue;
191
+
192
+ const body = commitBlock.slice(0, bodyEnd);
193
+ if (!trailerPattern.test(body)) continue;
194
+
195
+ // 複数 Work-Item trailer を同梱するバッチコミットでは、changed paths が
196
+ // 全 trailer WI に一律帰属してしまう(over-attribution)。ソースパスに限り
197
+ // ファイルの帰属タグで storyId への帰属を絞り込む(WI-251)。
198
+ const isMultiWorkItemCommit = this.countWorkItemTrailers(body) >= 2;
199
+ const nameOnly = commitBlock.slice(bodyEnd + 1);
200
+ for (const changedPath of nameOnly.split(/\r?\n/)) {
201
+ const trimmedPath = changedPath.trim();
202
+ if (trimmedPath.length === 0 || trimmedPath === "@@COMMIT@@") continue;
203
+
204
+ if (await this.pathAttributesToStory(trimmedPath, storyId, isMultiWorkItemCommit)) {
205
+ changedPaths.add(trimmedPath);
206
+ }
207
+ }
208
+ }
209
+
210
+ return changedPaths;
211
+ }
212
+
213
+ private countWorkItemTrailers(body: string): number {
214
+ return body.match(WORK_ITEM_TRAILER_PATTERN)?.length ?? 0;
215
+ }
216
+
217
+ /**
218
+ * バッチコミットの changed path を storyId に帰属させてよいか判定する。
219
+ * - 単一 WI trailer コミット → 常に帰属(現行挙動を維持)
220
+ * - 複数 WI trailer × 非ソースパス → 帰属(絞り込み対象外)
221
+ * - 複数 WI trailer × ソースパス → ファイル内容(HEAD)のタグに storyId を含むときのみ帰属。
222
+ * タグ無し / ファイルが読めない → fail-closed で帰属維持(反映要求を緩めない)。
223
+ */
224
+ private async pathAttributesToStory(
225
+ changedPath: string,
226
+ storyId: string,
227
+ isMultiWorkItemCommit: boolean,
228
+ ): Promise<boolean> {
229
+ if (!isMultiWorkItemCommit) {
230
+ return true;
231
+ }
232
+
233
+ if (!HARNESS_SOURCE_PATH_PATTERN.test(changedPath)) {
234
+ return true;
235
+ }
236
+
237
+ const tags = await this.readSourceWorkItemTags(changedPath);
238
+ if (tags === null || tags.size === 0) {
239
+ return true;
240
+ }
241
+
242
+ return tags.has(storyId);
243
+ }
244
+
245
+ private async readSourceWorkItemTags(relativePath: string): Promise<Set<string> | null> {
246
+ let content: string;
247
+ try {
248
+ content = await readFile(path.join(this.rootDir, relativePath), "utf8");
249
+ } catch {
250
+ return null;
251
+ }
252
+
253
+ const tags = new Set<string>();
254
+ for (const line of content.match(SOURCE_TAG_PATTERN) ?? []) {
255
+ for (const id of line.match(/WI-\d+/g) ?? []) {
256
+ tags.add(id);
257
+ }
258
+ }
259
+
260
+ return tags;
261
+ }
262
+
128
263
  async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
129
264
  const absolutePath = path.join(this.rootDir, productPath);
130
265
 
@@ -6,12 +6,12 @@
6
6
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
7
7
  */
8
8
 
9
- import { ChangeCategory } from '../value-objects/change-category.js';
10
- import { ChangeClassification } from '../value-objects/change-classification.js';
11
- import { QuickModeEligibility } from '../value-objects/quick-mode-eligibility.js';
12
- import { isCommentOnlyDiff } from './comment-only-diff-detector.js';
13
- import type { ChangedFile } from '../value-objects/changed-file.js';
14
- import type { QuickModeConfig } from '../value-objects/quick-mode-config.js';
9
+ import { ChangeCategory } from "../value-objects/change-category.js";
10
+ import { ChangeClassification } from "../value-objects/change-classification.js";
11
+ import type { ChangedFile } from "../value-objects/changed-file.js";
12
+ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
13
+ import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
14
+ import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
15
15
 
16
16
  // リスク順優先度(api > domain > feature > bugfix > test > config > docs)
17
17
  const RISK_PRIORITY: Record<string, number> = {
@@ -30,48 +30,54 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
30
30
  // Config files must stay config even when Edit payload snippets look like
31
31
  // comment/whitespace-only diffs. Config recovery guidance depends on this.
32
32
  if (
33
- filePath.endsWith('.config.json') ||
34
- filePath.endsWith('.config.ts') ||
35
- filePath.endsWith('phasegate.config.json')
33
+ filePath.endsWith(".config.json") ||
34
+ filePath.endsWith(".config.ts") ||
35
+ filePath.endsWith("phasegate.config.json")
36
36
  ) {
37
- return ChangeCategory.fromString('config');
37
+ return ChangeCategory.fromString("config");
38
38
  }
39
39
 
40
40
  if (isCommentOnlyDiff(file)) {
41
- return ChangeCategory.fromString('docs');
41
+ return ChangeCategory.fromString("docs");
42
42
  }
43
43
 
44
44
  // api: *port.ts or *adapter.ts(最高優先度)
45
- if (filePath.endsWith('port.ts') || filePath.endsWith('adapter.ts')) {
46
- return ChangeCategory.fromString('api');
45
+ if (filePath.endsWith("port.ts") || filePath.endsWith("adapter.ts")) {
46
+ return ChangeCategory.fromString("api");
47
47
  }
48
48
 
49
49
  // test: __tests__/ 配下 or *.test.ts or *.spec.ts(domainより優先)
50
- if (
51
- filePath.includes('__tests__/') ||
52
- filePath.endsWith('.test.ts') ||
53
- filePath.endsWith('.spec.ts')
54
- ) {
55
- return ChangeCategory.fromString('test');
50
+ if (filePath.includes("__tests__/") || filePath.endsWith(".test.ts") || filePath.endsWith(".spec.ts")) {
51
+ return ChangeCategory.fromString("test");
56
52
  }
57
53
 
58
54
  // domain: domain/ 配下
59
- if (filePath.includes('/domain/') || filePath.startsWith('domain/')) {
60
- return ChangeCategory.fromString('domain');
55
+ if (filePath.includes("/domain/") || filePath.startsWith("domain/")) {
56
+ return ChangeCategory.fromString("domain");
61
57
  }
62
58
 
63
59
  // docs: docs/ 配下
64
- if (filePath.startsWith('docs/') || filePath.includes('/docs/')) {
65
- return ChangeCategory.fromString('docs');
60
+ if (filePath.startsWith("docs/") || filePath.includes("/docs/")) {
61
+ return ChangeCategory.fromString("docs");
62
+ }
63
+
64
+ // docs: skills/ 配下の .md(指示文書。SKILL.md / references/*.md 等)は
65
+ // ソースコードのフェーズゲート対象外とし docs に分類する。品質・完全性は
66
+ // 専用防御(skill-quality corpus 適合テスト・advisory allowlist pin・
67
+ // integrity pin(WI-254)・L3-006 injection scanner(WI-259))が担う。
68
+ // skills/ 配下の非 .md は下の feature/bugfix フォールバックに落ちて fail-closed を維持する。
69
+ // @work-item-id WI-261
70
+ if (filePath.startsWith("skills/") && filePath.endsWith(".md")) {
71
+ return ChangeCategory.fromString("docs");
66
72
  }
67
73
 
68
74
  // feature: domain/ 以外の CREATE
69
- if (changeKind === 'CREATE') {
70
- return ChangeCategory.fromString('feature');
75
+ if (changeKind === "CREATE") {
76
+ return ChangeCategory.fromString("feature");
71
77
  }
72
78
 
73
79
  // bugfix: domain/ 以外の MODIFY/DELETE
74
- return ChangeCategory.fromString('bugfix');
80
+ return ChangeCategory.fromString("bugfix");
75
81
  }
76
82
 
77
83
  export class QuickModeJudgmentEngine {
@@ -107,7 +113,7 @@ export class QuickModeJudgmentEngine {
107
113
  const classification = this.classify(changedFiles, config);
108
114
 
109
115
  // 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
110
- if (config.isFullModeRequiredFor('mixedCategories')) {
116
+ if (config.isFullModeRequiredFor("mixedCategories")) {
111
117
  const notAllowedFiles: ChangedFile[] = [];
112
118
  classification.categorizedFiles.forEach((files, categoryKey) => {
113
119
  if (!config.isAllowed(categoryKey)) {
@@ -117,45 +123,43 @@ export class QuickModeJudgmentEngine {
117
123
 
118
124
  if (notAllowedFiles.length > 0) {
119
125
  return QuickModeEligibility.rejected(
120
- 'MIXED_CHANGES',
126
+ "MIXED_CHANGES",
121
127
  notAllowedFiles,
122
- `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(', ')}`
128
+ `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(", ")}`,
123
129
  );
124
130
  }
125
131
  }
126
132
 
127
133
  // 2. NEW_DOMAIN評価: domain/ 配下かつ changeKind=CREATE
128
- if (config.isFullModeRequiredFor('newDomainFile')) {
134
+ if (config.isFullModeRequiredFor("newDomainFile")) {
129
135
  const newDomainFiles = changedFiles.filter(
130
- (f) =>
131
- (f.filePath.includes('/domain/') || f.filePath.startsWith('domain/')) &&
132
- f.changeKind === 'CREATE'
136
+ (f) => (f.filePath.includes("/domain/") || f.filePath.startsWith("domain/")) && f.changeKind === "CREATE",
133
137
  );
134
138
 
135
139
  if (newDomainFiles.length > 0) {
136
140
  return QuickModeEligibility.rejected(
137
- 'NEW_DOMAIN',
141
+ "NEW_DOMAIN",
138
142
  newDomainFiles,
139
- `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(', ')}`
143
+ `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(", ")}`,
140
144
  );
141
145
  }
142
146
  }
143
147
 
144
148
  // 3. API_CONTRACT評価: *port.ts / *adapter.ts の変更
145
- if (config.isFullModeRequiredFor('apiContractChange')) {
149
+ if (config.isFullModeRequiredFor("apiContractChange")) {
146
150
  const apiContractFiles = changedFiles.filter(
147
- (f) => (f.filePath.endsWith('port.ts') || f.filePath.endsWith('adapter.ts')) && !isCommentOnlyDiff(f)
151
+ (f) => (f.filePath.endsWith("port.ts") || f.filePath.endsWith("adapter.ts")) && !isCommentOnlyDiff(f),
148
152
  );
149
153
 
150
154
  if (apiContractFiles.length > 0) {
151
155
  return QuickModeEligibility.rejected(
152
- 'API_CONTRACT',
156
+ "API_CONTRACT",
153
157
  apiContractFiles,
154
- `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(', ')}`
158
+ `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(", ")}`,
155
159
  );
156
160
  }
157
161
  }
158
162
 
159
- return QuickModeEligibility.eligible('すべてのファイルが許可カテゴリ内です');
163
+ return QuickModeEligibility.eligible("すべてのファイルが許可カテゴリ内です");
160
164
  }
161
165
  }
@@ -33,16 +33,14 @@ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
33
33
  "cascade-updater",
34
34
  "codebase-mapper",
35
35
  "consistency-checker",
36
- "doc-freshness-checker",
36
+ "doc-health-checker",
37
37
  "engineering-perspective",
38
38
  "implementation-readiness-checker",
39
- "pointer-validator",
40
39
  "test-coverage-checker",
41
40
  ],
42
41
  aidlc: [
43
42
  "domain-designer",
44
43
  "environment-designer",
45
- "implementation-planner",
46
44
  "it-test-designer",
47
45
  "it-test-logic-designer",
48
46
  "logical-designer",
@@ -60,7 +58,7 @@ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
60
58
  "unit-test-logic-designer",
61
59
  ],
62
60
  utility: ["codex-delegator", "skill-creator"],
63
- guidance: ["phasegate-toolkit-guide", "phasegate-config-doctor"],
61
+ guidance: ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"],
64
62
  } as const;
65
63
 
66
64
  export function getSkillsForSet(skillSet: SkillSet): string[] {
@@ -173,9 +171,12 @@ export function renderSkillForModelDelegation(content: string, policy: ModelDele
173
171
  return content
174
172
  .replace(/^model:\s+\S+\n/gm, "")
175
173
  .replace(/^review:\s+\S+\n/gm, "")
176
- .replaceAll("Sonnet 4.6 に委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)", "メインセッションが成果物を生成する")
174
+ .replaceAll(
175
+ "委任先モデルに委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)",
176
+ "メインセッションが成果物を生成する",
177
+ )
177
178
  .replaceAll("Opus が", "メインセッションが")
178
- .replaceAll("Sonnet 4.6", "メインセッション")
179
+ .replaceAll("委任先モデル", "メインセッション")
179
180
  .replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
180
181
  }
181
182
 
@@ -726,10 +727,7 @@ export async function deployHuskyCommitMsgHook(
726
727
  return { created: true, path: targetPath };
727
728
  }
728
729
 
729
- export async function deployHuskyPrePushHook(
730
- harnessRoot: string,
731
- projectRoot: string,
732
- ): Promise<DeployHuskyHookResult> {
730
+ export async function deployHuskyPrePushHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
733
731
  const targetPath = join(projectRoot, ".husky", "pre-push");
734
732
 
735
733
  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[] {