phasegate 0.340.0 → 0.341.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 (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/folder_management_rules.md +10 -1
  3. package/docs/guide/cli-reference.md +23 -3
  4. package/docs/guide/configuration.md +28 -2
  5. package/docs/guide/hooks-integration.md +7 -1
  6. package/docs/guide/skills-overview.md +3 -0
  7. package/package.json +2 -1
  8. package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
  9. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +36 -1
  10. package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
  11. package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
  12. package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
  13. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
  14. package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
  15. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
  16. package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
  17. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
  18. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
  19. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
  20. package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
  21. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +20 -27
  22. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +3 -0
  23. package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
  24. package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
  25. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
  26. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
  27. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
  28. package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
  29. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
  30. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
  31. package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
  32. package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
  33. package/scripts/harness/installation/application/usecases/run-install.ts +5 -5
  34. package/scripts/harness/installation/application/usecases/run-reconcile.ts +62 -21
  35. package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
  36. package/scripts/harness/main.ts +128 -90
  37. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
  38. package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
  39. package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
  41. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
  42. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +4 -0
  43. package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
  44. package/scripts/harness/quick-mode/composition-root.ts +5 -1
  45. package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
  46. package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
  47. package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
  48. package/scripts/harness/setup/skill-deployer.ts +3 -45
  49. package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
  50. package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
  51. package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
  52. package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
  53. package/scripts/harness/skill-quality/composition-root.ts +14 -7
  54. package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
  55. package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
  56. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
  57. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
  58. package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
  59. package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
  60. package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
  61. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
  62. package/skills/README.md +7 -0
  63. package/skills/cascade-updater/SKILL.md +37 -109
  64. package/skills/cascade-updater/references//345/261/244/345/210/245/345/210/244/345/256/232/343/202/254/343/202/244/343/203/211.md +5 -3
  65. package/skills/release-publisher/SKILL.md +3 -1
  66. package/skills/skill-creator/SKILL.md +8 -9
@@ -19,7 +19,8 @@
19
19
  * hook 全体をブロックしない(Fail-safe)。
20
20
  */
21
21
 
22
- import { readFile } from 'node:fs/promises';
22
+ import { readFile, stat } from 'node:fs/promises';
23
+ import { dirname, join, posix } from 'node:path';
23
24
 
24
25
  import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
25
26
  import { StoryReflectionQueryResult } from '../../domain/value-objects/story-reflection-query-result.js';
@@ -33,6 +34,8 @@ import {
33
34
  type StoryReflectionSectionInput,
34
35
  } from '../../../phase-dependency-model/infrastructure/config/harness-config-phase-config-provider.js';
35
36
  import type { StoryReflectionResult } from '../../../phase-dependency-model/domain/values/story-reflection-result.js';
37
+ import { FileSystemWorkItemDependencyCatalog, type WorkItemDependencyCatalogEntry } from '../../../phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.js';
38
+ import { WorkItemReflectionScopeResolver } from '../../../phase-dependency-model/domain/services/work-item-reflection-scope-resolver.js';
36
39
 
37
40
  export interface FileSystemStoryReflectionQueryAdapterDeps {
38
41
  readonly rootDir: string;
@@ -46,8 +49,10 @@ interface RawPhaseDependenciesSection {
46
49
  }
47
50
 
48
51
  interface RawConfigDocument {
52
+ readonly agentIntegration?: { readonly preToolUse?: { readonly dependencyReflection?: string } };
49
53
  readonly phaseDependencies?: RawPhaseDependenciesSection;
50
54
  readonly reporting?: { readonly outputDir?: string };
55
+ readonly paths?: { readonly inceptionDocs?: string; readonly designDocs?: string };
51
56
  }
52
57
 
53
58
  export class FileSystemStoryReflectionQueryAdapter
@@ -61,6 +66,106 @@ export class FileSystemStoryReflectionQueryAdapter
61
66
  this.configPath = deps.configPath;
62
67
  }
63
68
 
69
+ /** Defaults to advisory; enforcement requires an explicit trusted configuration value. */
70
+ async checkSessionReflection(unitId: string, workItemId: string): Promise<StoryReflectionQueryResult> {
71
+ const unknown = (reason: string) => StoryReflectionQueryResult.skipped([
72
+ `${workItemId}: WI依存反映は未検証です(既存sessionの許可を維持)。${reason}`,
73
+ ]);
74
+ try {
75
+ const raw = JSON.parse(await readFile(this.configPath, 'utf8')) as RawConfigDocument;
76
+ const enforce = raw.agentIntegration?.preToolUse?.dependencyReflection === 'enforce';
77
+ const finish = (result: StoryReflectionQueryResult) => enforce ? result.withSessionEnforcement() : result;
78
+ const incomplete = (reason: string) => enforce
79
+ ? finish(StoryReflectionQueryResult.block([reason], [])) : unknown(reason);
80
+ const provider = new HarnessConfigPhaseConfigProvider({
81
+ config: {
82
+ customization: { preset: raw.phaseDependencies?.preset, overrideEnabled: raw.phaseDependencies?.override ?? false },
83
+ storyReflection: raw.phaseDependencies?.storyReflection,
84
+ paths: raw.paths,
85
+ },
86
+ defaultOutputDir: raw.reporting?.outputDir ?? '.harness/reports',
87
+ });
88
+ const config = await provider.getStoryReflectionConfig();
89
+ if (!config.enabled) return StoryReflectionQueryResult.skipped();
90
+ // Validate explicit values before provider defaults can hide an empty root.
91
+ for (const value of [raw.paths?.inceptionDocs, raw.paths?.designDocs]) {
92
+ if (value !== undefined) FileSystemStoryReflectionQueryAdapter.normalizeRoot(value);
93
+ }
94
+ const configuredRoots = await provider.getPathRoots();
95
+ const roots = {
96
+ inceptionDocsRoot: FileSystemStoryReflectionQueryAdapter.normalizeRoot(configuredRoots.inceptionDocsRoot),
97
+ designDocsRoot: FileSystemStoryReflectionQueryAdapter.normalizeRoot(configuredRoots.designDocsRoot),
98
+ };
99
+ const catalog = await new FileSystemWorkItemDependencyCatalog({ rootDir: this.rootDir, inceptionRoot: roots.inceptionDocsRoot }).load([workItemId]);
100
+ const scope = new WorkItemReflectionScopeResolver().resolve(unitId, [workItemId], catalog);
101
+ const fs = new FileSystemStoryReflectionAdapter({ rootDir: this.rootDir, inceptionRoot: roots.inceptionDocsRoot, designDocsRoot: roots.designDocsRoot });
102
+ const checker = new StoryReflectionChecker(fs);
103
+ if (scope.status === 'unknown') {
104
+ const diagnostic = catalog.find(item => item.id === scope.workItemId)?.diagnostic;
105
+ const reason = `${scope.code}: ${scope.workItemId ?? workItemId}${diagnostic ? ` (${diagnostic})` : ''}。対象description.mdのID・所属・depends_onを確認してください。`;
106
+ if (!enforce) return unknown(reason);
107
+ const fallback = FileSystemStoryReflectionQueryAdapter.mapResult(await checker.check(unitId, config, roots), true);
108
+ const warnings = [...fallback.warnings, `WI依存反映は未検証です。従来のUnit範囲で検査しました。${reason}`];
109
+ return finish(fallback.passed ? StoryReflectionQueryResult.pass(warnings) : StoryReflectionQueryResult.block([...fallback.blockers], warnings));
110
+ }
111
+ if (config.mappings.length === 0 && catalog.some(item => item.metadata?.type !== 'chore')) {
112
+ return incomplete('非choreの反映先mappingが空です。storyReflection.mappingsを確認してください。');
113
+ }
114
+ for (const item of catalog) {
115
+ if (item.metadata?.type === 'fix') {
116
+ for (const unit of item.unitIds) {
117
+ const candidates = [...new Set(config.mappings.map(mapping => mapping.resolve({ unitId: unit, storyId: item.id }, roots).product))];
118
+ const reflected = await Promise.all(candidates.map(path => fs.fileContainsStoryAnnotation(path, item.id)));
119
+ if (!reflected.some(Boolean)) return incomplete(`${item.id}: 関係productへの反映を確認してください。候補: ${candidates.join(', ')}。関係カテゴリをレビューし内容と@work-item-idを反映してください。`);
120
+ }
121
+ continue;
122
+ }
123
+ const missing = await this.checkSessionArtifacts(item);
124
+ if (missing) return incomplete(missing);
125
+ }
126
+ const result = await checker.checkResolvedScope(scope, config, roots);
127
+ return finish(FileSystemStoryReflectionQueryAdapter.mapResult(result, true));
128
+ } catch (error) {
129
+ return unknown(error instanceof Error ? error.message : String(error));
130
+ }
131
+ }
132
+
133
+ private static normalizeRoot(value: unknown): string {
134
+ if (typeof value !== 'string' || value.trim() === '') throw new Error('Invalid documentation root: expected non-empty project-relative path');
135
+ const slashPath = value.replaceAll('\\', '/');
136
+ if (slashPath.startsWith('/') || /^[a-zA-Z]:/.test(slashPath) || slashPath.split('/').includes('..')) {
137
+ throw new Error(`Invalid documentation root: ${value}`);
138
+ }
139
+ const normalized = posix.normalize(slashPath).replace(/\/$/, '');
140
+ if (normalized === '.') throw new Error(`Invalid documentation root: ${value}`);
141
+ return normalized;
142
+ }
143
+
144
+ private async isSessionArtifactFile(relativePath: string): Promise<boolean> {
145
+ try {
146
+ return (await stat(join(this.rootDir, relativePath))).isFile();
147
+ } catch (error) {
148
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'ENOTDIR') return false;
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ private async checkSessionArtifacts(item: WorkItemDependencyCatalogEntry): Promise<string | undefined> {
154
+ const type = item.metadata?.type;
155
+ if (type === 'chore') return undefined;
156
+ const directory = dirname(item.descriptionPath).replaceAll('\\', '/');
157
+ const required = type === 'refactor' ? ['logical_design.md'] : ['logical_design.md', 'domain_model.md'];
158
+ for (const file of required) {
159
+ const path = `${directory}/${file}`;
160
+ if (!(await this.isSessionArtifactFile(path))) return `${item.id}: 必要成果物を確認できません: ${path}`;
161
+ }
162
+ if (type === 'story' || type === 'issue') {
163
+ const tests = await Promise.all(['unit_test_design.md', 'it_test_design.md', 'scenario_test_design.md'].map(file => this.isSessionArtifactFile(`${directory}/${file}`)));
164
+ if (!tests.some(Boolean)) return `${item.id}: ${directory} のtest設計を確認できません。必要なテスト設計を整えてください。`;
165
+ }
166
+ return undefined;
167
+ }
168
+
64
169
  async checkReflection(unitId: string): Promise<StoryReflectionQueryResult> {
65
170
  let raw: RawConfigDocument;
66
171
  try {
@@ -102,6 +207,7 @@ export class FileSystemStoryReflectionQueryAdapter
102
207
 
103
208
  private static mapResult(
104
209
  result: StoryReflectionResult,
210
+ preserveWarnings = false,
105
211
  ): StoryReflectionQueryResult {
106
212
  const warnings = result.warnings.map(
107
213
  (w) =>
@@ -109,7 +215,7 @@ export class FileSystemStoryReflectionQueryAdapter
109
215
  );
110
216
 
111
217
  if (!result.isBlocked()) {
112
- return StoryReflectionQueryResult.pass();
218
+ return StoryReflectionQueryResult.pass(preserveWarnings ? warnings : []);
113
219
  }
114
220
 
115
221
  const blockers = result.violations.map(
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-220
4
5
  *
5
6
  * HarnessConfigConfigQueryAdapter
6
7
  * HarnessConfigV2 の harnesses セクションから Hook 設定を読み取る
@@ -51,6 +52,8 @@ interface AgentIntegrationStopHookSection {
51
52
 
52
53
  interface AgentIntegrationSection {
53
54
  stopHook?: AgentIntegrationStopHookSection;
55
+ preToolUse?: { enabled?: boolean };
56
+ postToolUse?: { enabled?: boolean };
54
57
  }
55
58
 
56
59
  interface HarnessConfigDocument {
@@ -119,15 +122,14 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
119
122
  const config = this.loadConfig();
120
123
  const harnesses = config.harnesses ?? {};
121
124
 
122
- // Wave 2 マッピング:
123
- // pre-tool-use → agentLessonCollection
124
- // post-tool-use → cascadeUpdate
125
- // stop → デフォルト有効
125
+ // Explicit hook settings take precedence; absent keys preserve legacy behavior.
126
126
  if (hookType === "pre-tool-use") {
127
- return harnesses.agentLessonCollection ?? true;
127
+ const enabled = config.agentIntegration?.preToolUse?.enabled;
128
+ return typeof enabled === 'boolean' ? enabled : harnesses.agentLessonCollection ?? true;
128
129
  }
129
130
  if (hookType === "post-tool-use") {
130
- return harnesses.cascadeUpdate ?? true;
131
+ const enabled = config.agentIntegration?.postToolUse?.enabled;
132
+ return typeof enabled === 'boolean' ? enabled : harnesses.cascadeUpdate ?? true;
131
133
  }
132
134
  // stop はデフォルト有効
133
135
  return true;
@@ -1,6 +1,7 @@
1
1
  // @layer presentation
2
2
  // @unit agent-integration
3
3
  // @work-item-id WI-123
4
+ // @work-item-id WI-220
4
5
 
5
6
  import * as fs from 'node:fs/promises';
6
7
  import * as path from 'node:path';
@@ -11,6 +12,8 @@ export async function recordHookSkipEvent(input: {
11
12
  readonly reason: string;
12
13
  readonly targetPaths: readonly string[];
13
14
  }): Promise<void> {
15
+ // Expected no-op states are not diagnostic events. Preserve existing history.
16
+ if (input.reason === 'HOOK_DISABLED' || input.reason === 'READ_ONLY') return;
14
17
  try {
15
18
  const phasegateDir = path.join(input.projectRoot, '.phasegate');
16
19
  await fs.mkdir(phasegateDir, { recursive: true });
@@ -0,0 +1,39 @@
1
+ // @unit agent-integration
2
+ // @layer presentation
3
+ // @work-item-id WI-220
4
+ import { resolve } from 'node:path';
5
+ import type { HandlePostToolUseOutput } from '../application/dto/handle-post-tool-use-dto.js';
6
+ import { ApplyPatchWriteTargetExtractor } from '../domain/services/apply-patch-write-target-extractor.js';
7
+ import { PreToolUsePayloadNormalizer } from './pre-tool-use-payload-normalizer.js';
8
+
9
+ export function normalizePostToolUse(payload: unknown, processCwd: string): { toolName?: string; cwd: string; targets: string[] } {
10
+ const record = payload !== null && typeof payload === 'object' ? payload as Record<string, unknown> : {};
11
+ const cwd = resolve(typeof record.cwd === 'string' ? record.cwd : processCwd);
12
+ const normalized = new PreToolUsePayloadNormalizer().normalize(payload);
13
+ if (!normalized.ok) {
14
+ // An incomplete known invocation still receives full lint, never a partial target scope.
15
+ const named = typeof record.tool_name === 'string' || typeof record.toolName === 'string' || record.toolCall !== undefined;
16
+ return { toolName: named ? 'unknown' : undefined, cwd, targets: [] };
17
+ }
18
+ const { toolName, toolInput, inputTruncated } = normalized.request;
19
+ let targets: readonly string[] = [];
20
+ if (!inputTruncated && ['Write', 'Edit', 'NotebookEdit', 'str_replace_editor'].includes(toolName)) {
21
+ targets = [toolInput.file_path, toolInput.path, ...(toolInput.paths ?? [])].filter((p): p is string => typeof p === 'string' && p.length > 0);
22
+ } else if (!inputTruncated && toolName === 'apply_patch' && toolInput.command?.trimEnd().endsWith('*** End Patch')
23
+ && (toolInput.command.match(/^\*\*\* Begin Patch\s*$/gm)?.length ?? 0) === (toolInput.command.match(/^\*\*\* End Patch\s*$/gm)?.length ?? 0)) {
24
+ targets = new ApplyPatchWriteTargetExtractor().extract(toolInput.command).map((target) => target.filePath);
25
+ }
26
+ return { toolName, cwd, targets: [...new Set(targets.map((target) => resolve(cwd, target)))] };
27
+ }
28
+
29
+ export function renderPostToolUseFeedback(output: HandlePostToolUseOutput): { exitCode: number; text: string } {
30
+ if (output.skipReason === 'TIMEOUT_EXCEEDED') {
31
+ return { exitCode: 0, text: '検証未完了: TIMEOUT_EXCEEDED。自動再試行はしません。必要なタイミングで phasegate lint を実行してください。\n' };
32
+ }
33
+ if (output.skipReason === 'READ_ONLY' || output.skipReason === 'HOOK_DISABLED') return { exitCode: 0, text: '' };
34
+ if (output.skipReason) return { exitCode: 0, text: `スキップ: ${output.skipReason}\n` };
35
+ if (output.cliResult && output.cliResult.exitCode !== 0) {
36
+ return { exitCode: 0, text: `Lint診断 (exitCode=${output.cliResult.exitCode}):\n${output.cliResult.stdout}\n${output.cliResult.stderr}\n` };
37
+ }
38
+ return { exitCode: 0, text: output.cliResult?.stderr ?? '' };
39
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-220
4
5
  * @work-item-id WI-208
5
6
  * @work-item-id WI-323
6
7
  * @work-item-id WI-384
@@ -16,11 +17,7 @@ import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child
16
17
  import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
17
18
  import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
18
19
  import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
19
-
20
- interface PostToolUseHookInput {
21
- tool_name?: string;
22
- tool_response?: unknown;
23
- }
20
+ import { normalizePostToolUse, renderPostToolUseFeedback } from './post-tool-use-feedback.js';
24
21
 
25
22
  async function readStdin(): Promise<string> {
26
23
  const chunks: Buffer[] = [];
@@ -30,8 +27,8 @@ async function readStdin(): Promise<string> {
30
27
  return Buffer.concat(chunks).toString("utf8");
31
28
  }
32
29
 
33
- async function findConfigPath(): Promise<string> {
34
- let dir = process.cwd();
30
+ async function findConfigPath(startDir: string): Promise<string> {
31
+ let dir = startDir;
35
32
  while (true) {
36
33
  const candidates = [
37
34
  path.join(dir, "phasegate.config.json"),
@@ -47,7 +44,7 @@ async function findConfigPath(): Promise<string> {
47
44
  if (parent === dir) break;
48
45
  dir = parent;
49
46
  }
50
- return path.join(process.cwd(), "phasegate.config.json");
47
+ return path.join(startDir, "phasegate.config.json");
51
48
  }
52
49
 
53
50
  function projectRootForConfig(configPath: string): string {
@@ -64,20 +61,21 @@ async function main(): Promise<void> {
64
61
  process.exit(2);
65
62
  }
66
63
 
67
- let input: PostToolUseHookInput;
64
+ let input: unknown;
68
65
  try {
69
- input = JSON.parse(raw) as PostToolUseHookInput;
66
+ input = JSON.parse(raw) as unknown;
70
67
  } catch {
71
68
  process.stderr.write(`不正なJSONです: ${raw}\n`);
72
69
  process.exit(2);
73
70
  }
74
71
 
75
- const toolName = input.tool_name;
72
+ const request = normalizePostToolUse(input, process.cwd());
73
+ const toolName = request.toolName;
76
74
  if (!toolName) {
77
75
  // WI-323: PostToolUse はツール実行後の lint フィードバックでありゲートではないため、
78
76
  // tool_name 欠落は fail-open でスキップする(WI-314 / github#40 方針)。
79
77
  // ※ pre-tool-use-hook の同ガードは書き込みゲートなので fail-closed (exit 2) を維持する。
80
- const configPath = await findConfigPath();
78
+ const configPath = await findConfigPath(request.cwd);
81
79
  await recordHookSkipEvent({
82
80
  projectRoot: projectRootForConfig(configPath),
83
81
  hookType: "post-tool-use",
@@ -91,10 +89,13 @@ async function main(): Promise<void> {
91
89
  }
92
90
 
93
91
  try {
94
- const configPath = await findConfigPath();
92
+ const configPath = await findConfigPath(request.cwd);
93
+ const projectRoot = projectRootForConfig(configPath);
94
+ const affectedFilePaths = request.targets.map((target) => path.relative(projectRoot, target))
95
+ .filter((target) => target !== '..' && !target.startsWith(`..${path.sep}`) && !path.isAbsolute(target));
95
96
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
96
97
  const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
97
- const cliExecutorPort = new ChildProcessCliExecutorAdapter();
98
+ const cliExecutorPort = new ChildProcessCliExecutorAdapter({ cwd: projectRoot });
98
99
 
99
100
  const useCase = new HandlePostToolUseUseCase({
100
101
  configQueryPort,
@@ -102,27 +103,19 @@ async function main(): Promise<void> {
102
103
  cliCommandRegistryPort,
103
104
  });
104
105
 
105
- const output = await useCase.execute({ toolName, affectedFilePaths: [] });
106
+ const output = await useCase.execute({ toolName, affectedFilePaths });
106
107
 
107
108
  if (output.skipReason) {
108
109
  await recordHookSkipEvent({
109
110
  projectRoot: projectRootForConfig(configPath),
110
111
  hookType: "post-tool-use",
111
112
  reason: output.skipReason,
112
- targetPaths: [],
113
+ targetPaths: affectedFilePaths,
113
114
  });
114
- process.stderr.write(`スキップ: ${output.skipReason}\n`);
115
- process.exit(0);
116
- }
117
-
118
- if (output.executed && output.cliResult) {
119
- if (output.cliResult.exitCode !== 0) {
120
- process.stderr.write(`Lint失敗 (exitCode=${output.cliResult.exitCode})\n`);
121
- }
122
- process.exit(output.cliResult.exitCode);
123
115
  }
124
-
125
- process.exit(0);
116
+ const feedback = renderPostToolUseFeedback(output);
117
+ if (feedback.text) process.stderr.write(feedback.text);
118
+ process.exit(feedback.exitCode);
126
119
  } catch (error) {
127
120
  process.stderr.write(`実行エラー: ${String(error)}\n`);
128
121
  process.exit(2);
@@ -266,6 +266,9 @@ async function main(): Promise<void> {
266
266
  const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : "";
267
267
  const unit = session.unit !== undefined ? `, unit=${session.unit}` : "";
268
268
  process.stderr.write(`phasegate: write allowed (Full Mode session${workItem}${unit})\n`);
269
+ for (const warning of output.storyReflectionWarnings ?? []) {
270
+ process.stderr.write(`phasegate: dependency reflection warning (advisory): ${warning}\n`);
271
+ }
269
272
  }
270
273
 
271
274
  process.exit(0);
@@ -4,6 +4,7 @@
4
4
  * @work-item-id WI-203
5
5
  * @work-item-id WI-208
6
6
  * @work-item-id WI-323
7
+ * @work-item-id WI-220
7
8
  *
8
9
  * Stop Hook Adapter
9
10
  * Claude Code の Stop Hook エントリポイント
@@ -14,7 +15,6 @@ import * as path from "node:path";
14
15
  import { HandleStopUseCase } from "../application/usecases/handle-stop-usecase.js";
15
16
  import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
16
17
  import { EnvFileReentryGuardStateAdapter } from "../infrastructure/adapters/env-file-reentry-guard-state-adapter.js";
17
- import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
18
18
  import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
19
19
  import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
20
20
 
@@ -106,14 +106,12 @@ async function main(): Promise<void> {
106
106
  const configPath = await findConfigPath();
107
107
  const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy: "env" });
108
108
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
109
- const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
110
109
  const cliExecutorPort = new ChildProcessCliExecutorAdapter();
111
110
 
112
111
  const useCase = new HandleStopUseCase({
113
112
  reentryGuardStatePort,
114
113
  cliExecutorPort,
115
114
  configQueryPort,
116
- cliCommandRegistryPort,
117
115
  });
118
116
 
119
117
  const output = await useCase.execute({ sessionId });
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { accessSync, constants, statSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
7
9
  import type { FilePath } from '../../domain/value-objects/file-path.js';
8
10
  import type { BiomeExecutorPort } from '../../domain/ports/biome-executor-port.js';
9
11
 
@@ -34,7 +36,25 @@ export class BiomeCliExecutorAdapter implements BiomeExecutorPort {
34
36
 
35
37
  constructor(deps: BiomeCliExecutorAdapterDeps) {
36
38
  this.cwd = deps.cwd;
37
- this.biomeBin = deps.biomeBin ?? 'npx';
39
+ this.biomeBin = deps.biomeBin ?? this.findLocalBin(deps.cwd) ?? 'npx';
40
+ }
41
+
42
+ private findLocalBin(cwd: string): string | undefined {
43
+ // .cmd execution has different quoting/shell requirements; retain its old route.
44
+ if (process.platform === 'win32') return undefined;
45
+ let directory = resolve(cwd);
46
+ while (true) {
47
+ const candidate = join(directory, 'node_modules', '.bin', 'biome');
48
+ try {
49
+ accessSync(candidate, constants.X_OK);
50
+ if (statSync(candidate).isFile()) return candidate;
51
+ } catch {
52
+ // Match local npm-bin lookup from the working directory toward the root.
53
+ }
54
+ const parent = dirname(directory);
55
+ if (parent === directory) return undefined;
56
+ directory = parent;
57
+ }
38
58
  }
39
59
 
40
60
  async executeCheck(files: readonly FilePath[]): Promise<void> {
@@ -2,6 +2,7 @@
2
2
  * @layer infrastructure
3
3
  * @unit biome-ast-engine
4
4
  * @work-item-id WI-109
5
+ * @work-item-id WI-220
5
6
  */
6
7
 
7
8
  import * as ts from 'typescript';
@@ -74,6 +75,10 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
74
75
  moduleResolution: ts.ModuleResolutionKind.Node16,
75
76
  allowJs: false,
76
77
  noEmit: true,
78
+ // Snapshots use syntax only. The workspace scanner already supplies every
79
+ // graph node; libraries and transitive imports are not snapshot inputs.
80
+ noLib: true,
81
+ noResolve: true,
77
82
  });
78
83
 
79
84
  const snapshots: SourceModuleSnapshot[] = [];
@@ -685,6 +685,21 @@
685
685
  "type": "object",
686
686
  "additionalProperties": false,
687
687
  "properties": {
688
+ "preToolUse": {
689
+ "type": "object",
690
+ "additionalProperties": false,
691
+ "properties": {
692
+ "enabled": { "type": "boolean" },
693
+ "dependencyReflection": { "type": "string", "enum": ["advisory", "enforce"] }
694
+ }
695
+ },
696
+ "postToolUse": {
697
+ "type": "object",
698
+ "additionalProperties": false,
699
+ "properties": {
700
+ "enabled": { "type": "boolean" }
701
+ }
702
+ },
688
703
  "stopHook": {
689
704
  "type": "object",
690
705
  "additionalProperties": false,
@@ -109,13 +109,16 @@ function toHarnessError(error: ErrorObject): HarnessErrorWithPath {
109
109
  });
110
110
  }
111
111
 
112
- const validateSchemaV2 = createValidateFunction(SCHEMA_V2_PATH);
113
- const validateSchemaV3 = createValidateFunction(SCHEMA_V3_PATH);
112
+ // Compile only the selected schema, once per module. Failed compilation is not cached.
113
+ let validateSchemaV2: ValidateFunction | undefined;
114
+ let validateSchemaV3: ValidateFunction | undefined;
114
115
 
115
116
  export class AjvConfigSchemaValidator implements ConfigSchemaValidatorPort {
116
117
  validate(document: unknown): readonly HarnessError[] {
117
118
  const schemaVersion = detectSchemaVersion(document);
118
- const validateSchema = schemaVersion === 'v3' ? validateSchemaV3 : validateSchemaV2;
119
+ const validateSchema = schemaVersion === 'v3'
120
+ ? (validateSchemaV3 ??= createValidateFunction(SCHEMA_V3_PATH))
121
+ : (validateSchemaV2 ??= createValidateFunction(SCHEMA_V2_PATH));
119
122
  const valid = validateSchema(document);
120
123
 
121
124
  if (valid) {
@@ -1,10 +1,11 @@
1
+ // @unit harness-api
1
2
  // @layer domain
2
3
  // biome-lint-port.ts
3
4
 
4
5
  import type { HarnessError } from '../value-objects/harness-api-response.js';
5
6
 
6
7
  export interface BiomeLintPort {
7
- runLint(): Promise<{
8
+ runLint(reportTargets?: readonly string[]): Promise<{
8
9
  passed: boolean;
9
10
  errors: HarnessError[];
10
11
  warnings: HarnessError[];
@@ -312,7 +312,18 @@ export class CommandDispatchService {
312
312
  }
313
313
 
314
314
  case "phasegate:lint": {
315
- const lintResult = await this.ports.biomeLintPort.runLint();
315
+ const target = typeof _flags.target === 'string' && _flags.target.length > 0 ? _flags.target : undefined;
316
+ let reportTargets: readonly string[] | undefined = target === undefined ? undefined : [target];
317
+ if (args.reportTargets !== undefined) {
318
+ const parsed: unknown = JSON.parse(args.reportTargets);
319
+ if (!Array.isArray(parsed) || !parsed.every((item): item is string => typeof item === 'string' && item.length > 0)) {
320
+ throw new Error('lint reportTargets must be an array of non-empty paths');
321
+ }
322
+ reportTargets = parsed;
323
+ }
324
+ const lintResult = reportTargets === undefined
325
+ ? await this.ports.biomeLintPort.runLint()
326
+ : await this.ports.biomeLintPort.runLint(reportTargets);
316
327
  if (lintResult.passed) {
317
328
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
318
329
  return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
@@ -1,3 +1,4 @@
1
+ // @unit harness-api
1
2
  // @layer infrastructure
2
3
  // biome-ast-engine-lint-adapter.ts — BiomeAstEngineLintAdapter
3
4
  // Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
@@ -5,6 +6,7 @@
5
6
 
6
7
  import type { BiomeLintPort } from '../../domain/ports/biome-lint-port.js';
7
8
  import type { HarnessError } from '../../domain/value-objects/harness-api-response.js';
9
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
8
10
 
9
11
  interface RuleViolation {
10
12
  filePath: string;
@@ -37,7 +39,7 @@ function violationToHarnessError(v: RuleViolation): HarnessError {
37
39
  export class BiomeAstEngineLintAdapter implements BiomeLintPort {
38
40
  private readonly stub: IBiomeAstEngineStub;
39
41
 
40
- constructor(stub?: IBiomeAstEngineStub, rootDir = process.cwd()) {
42
+ constructor(stub?: IBiomeAstEngineStub, private readonly rootDir = process.cwd()) {
41
43
  this.stub = stub ?? BiomeAstEngineLintAdapter.createRealImpl(rootDir);
42
44
  }
43
45
 
@@ -62,12 +64,18 @@ export class BiomeAstEngineLintAdapter implements BiomeLintPort {
62
64
  };
63
65
  }
64
66
 
65
- async runLint(): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
67
+ async runLint(reportTargets?: readonly string[]): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
68
+ // Always analyze the complete graph; targets restrict reporting, never analysis.
66
69
  const result = await this.stub.runLint();
70
+ const targets = reportTargets?.map((target) => resolve(this.rootDir, target)) ?? [];
67
71
  const errors: HarnessError[] = [];
68
72
  const warnings: HarnessError[] = [];
69
73
 
70
74
  for (const v of result.violations) {
75
+ if (v.filePath && targets.length > 0 && !targets.some((target) => {
76
+ const rel = relative(target, resolve(this.rootDir, v.filePath));
77
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
78
+ })) continue;
71
79
  const harnessError = violationToHarnessError(v);
72
80
  if (v.severity === 'warning') {
73
81
  warnings.push(harnessError);
@@ -1,8 +1,10 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-216
4
+ // @work-item-id WI-223
4
5
 
5
- export type SkillSet = "core" | "all";
6
+ export type SkillSet = "core" | "all" | "consumer";
7
+ export type SkillCategory = "core" | "aidlc" | "utility" | "guidance";
6
8
 
7
9
  const CORE_SKILLS = [
8
10
  "cascade-updater",
@@ -37,7 +39,31 @@ const AIDLC_SKILLS = [
37
39
  const UTILITY_SKILLS = ["codex-delegator", "skill-creator"] as const;
38
40
  const GUIDANCE_SKILLS = ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"] as const;
39
41
 
42
+ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
43
+ core: CORE_SKILLS,
44
+ aidlc: AIDLC_SKILLS,
45
+ utility: UTILITY_SKILLS,
46
+ guidance: GUIDANCE_SKILLS,
47
+ };
48
+
49
+ export function isSkillSet(value: unknown): value is SkillSet {
50
+ return value === "core" || value === "all" || value === "consumer";
51
+ }
52
+
53
+ export function resolveInstalledSkillSet(metadata: string | null): SkillSet {
54
+ try {
55
+ const value: unknown = JSON.parse(metadata ?? "null");
56
+ if (typeof value === "object" && value !== null && "skillSet" in value && isSkillSet(value.skillSet)) {
57
+ return value.skillSet;
58
+ }
59
+ } catch { /* Legacy installations without readable metadata retain the all set. */ }
60
+ return "all";
61
+ }
62
+
40
63
  export function getBundledSkillsForSet(skillSet: SkillSet): string[] {
41
64
  if (skillSet === "core") return [...CORE_SKILLS];
42
- return [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
65
+ const all = [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
66
+ return skillSet === "consumer"
67
+ ? all.filter(name => name !== "release-publisher" && name !== "skill-creator")
68
+ : all;
43
69
  }
@@ -6,7 +6,7 @@
6
6
  // @work-item-id WI-216
7
7
 
8
8
  import { join } from "node:path";
9
- import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
9
+ import { getBundledSkillsForSet, resolveInstalledSkillSet } from "../bundled-skill-selection.js";
10
10
  import type { CheckId } from "../../domain/check-id.js";
11
11
  import { DiagnosticFinding, type DiagnosticSeverity } from "../../domain/diagnostic-finding.js";
12
12
  import type { RepairMode } from "../../domain/repair-mode.js";
@@ -73,7 +73,7 @@ export function skillDirectoryLooksValid(files: readonly string[]): boolean {
73
73
  }
74
74
 
75
75
  export function phasegateSkillDirectoryLooksComplete(files: readonly string[], metadata: string | null): boolean {
76
- const skillSet: SkillSet = metadata?.includes('"skillSet": "core"') ? "core" : "all";
76
+ const skillSet = resolveInstalledSkillSet(metadata);
77
77
  const expectedSkills = getBundledSkillsForSet(skillSet);
78
78
  return expectedSkills.every((skill) => files.some((file) => {
79
79
  const normalized = file.replaceAll("\\", "/");