phasegate 0.335.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 (111) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.ja.md +22 -9
  3. package/README.md +28 -17
  4. package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
  5. package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
  6. package/docs/folder_management_rules.md +10 -1
  7. package/docs/guide/antigravity-integration.md +26 -0
  8. package/docs/guide/cli-reference.md +27 -7
  9. package/docs/guide/codex-integration.md +44 -107
  10. package/docs/guide/configuration.md +28 -2
  11. package/docs/guide/grok-integration.md +26 -0
  12. package/docs/guide/hooks-integration.md +34 -5
  13. package/docs/guide/installation.md +4 -2
  14. package/docs/guide/setup-artifacts.md +4 -4
  15. package/docs/guide/skills-overview.md +4 -1
  16. package/package.json +2 -1
  17. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
  18. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  19. package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
  20. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
  21. package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
  22. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  23. package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
  24. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  25. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
  26. package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
  27. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
  28. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
  29. package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
  30. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
  31. package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
  32. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
  33. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
  34. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
  35. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  36. package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
  37. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
  38. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -70
  39. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  40. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  41. package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
  42. package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
  43. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
  44. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  45. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
  46. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
  47. package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
  48. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
  49. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
  50. package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
  51. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  52. package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
  53. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  54. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  55. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  56. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  57. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  58. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  59. package/scripts/harness/installation/application/usecases/run-install.ts +157 -57
  60. package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
  61. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  62. package/scripts/harness/installation/composition-root.ts +10 -0
  63. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  64. package/scripts/harness/installation/domain/check-id.ts +5 -0
  65. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  66. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  67. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  68. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  69. package/scripts/harness/installation/presentation/cli/install-handler.ts +25 -5
  70. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  71. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  72. package/scripts/harness/main.ts +227 -128
  73. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
  74. package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
  75. package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
  76. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
  77. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
  78. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
  79. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  80. package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
  81. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  82. package/scripts/harness/quick-mode/composition-root.ts +5 -1
  83. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
  84. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  85. package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
  86. package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
  87. package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
  88. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  89. package/scripts/harness/setup/skill-deployer.ts +3 -45
  90. package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
  91. package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
  92. package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
  93. package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
  94. package/scripts/harness/skill-quality/composition-root.ts +14 -7
  95. package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
  96. package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
  97. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
  98. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
  99. package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
  100. package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
  101. package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
  102. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
  103. package/skills/README.md +7 -0
  104. package/skills/cascade-updater/SKILL.md +37 -109
  105. 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
  106. package/skills/release-publisher/SKILL.md +3 -1
  107. package/skills/skill-creator/SKILL.md +8 -9
  108. package/templates/.agents/hooks.json +16 -0
  109. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  110. package/templates/.claude/settings.json +5 -3
  111. package/templates/.codex/hooks.json +2 -2
@@ -7,38 +7,47 @@
7
7
  * CliExecutorPort の実装。子プロセスで CLI コマンドを実行する
8
8
  */
9
9
 
10
- import { spawn } from 'node:child_process';
10
+ import { spawn, spawnSync } from 'node:child_process';
11
+ import { statSync } from 'node:fs';
12
+ import { createRequire } from 'node:module';
11
13
  import { dirname, resolve } from 'node:path';
12
14
  import { fileURLToPath } from 'node:url';
13
15
  import type { CliExecutorPort, CliExecutionResult } from '../../application/ports/cli-executor-port.js';
14
16
  import { TimeoutError } from '../../application/ports/cli-executor-port.js';
15
17
 
18
+ const tsxCliPath = createRequire(import.meta.url).resolve('tsx/cli');
19
+
16
20
  function getHarnessMainPath(): string {
17
21
  return resolve(dirname(fileURLToPath(import.meta.url)), '../../../main.ts');
18
22
  }
19
23
 
20
24
  /**
21
25
  * CommandName を実行可能なコマンドに変換する
22
- * 例: 'phasegate:lint' → ['npx', 'tsx', '<package>/scripts/harness/main.ts', 'phasegate:lint']
26
+ * 例: 'phasegate:lint' → [node, '<package>/tsx/cli', '<package>/scripts/harness/main.ts', 'phasegate:lint']
23
27
  * テスト時は直接スクリプトパスで execute を呼ぶことも可能
24
28
  */
25
29
  function resolveCommand(commandName: string): { cmd: string; args: string[] } {
26
30
  if (commandName.startsWith('phasegate:')) {
31
+ const compiledMain = resolve(dirname(getHarnessMainPath()), 'main.js');
32
+ let compiled = false;
33
+ try { compiled = statSync(compiledMain).isFile(); } catch { /* Source-only packages retain the TS entry. */ }
27
34
  return {
28
- cmd: 'npx',
29
- args: ['tsx', getHarnessMainPath(), commandName],
35
+ cmd: process.execPath,
36
+ args: compiled ? [compiledMain, commandName] : [tsxCliPath, getHarnessMainPath(), commandName],
30
37
  };
31
38
  }
32
39
 
33
40
  // Legacy extension commands may still be provided as project-local wrappers.
34
41
  const slug = commandName.replace('phasegate:', '');
35
42
  return {
36
- cmd: 'npx',
37
- args: ['tsx', `scripts/harness/cli/${slug}.ts`],
43
+ cmd: process.execPath,
44
+ args: [tsxCliPath, `scripts/harness/cli/${slug}.ts`],
38
45
  };
39
46
  }
40
47
 
41
48
  export class ChildProcessCliExecutorAdapter implements CliExecutorPort {
49
+ constructor(private readonly options: { cwd?: string } = {}) {}
50
+
42
51
  async execute(
43
52
  command: string,
44
53
  args: string[],
@@ -49,9 +58,9 @@ export class ChildProcessCliExecutorAdapter implements CliExecutorPort {
49
58
  let spawnArgs: string[];
50
59
 
51
60
  // If the command looks like a file path (contains / or .ts), run it directly
52
- if (command.includes('/') || command.endsWith('.ts')) {
53
- cmd = 'npx';
54
- spawnArgs = ['tsx', command, ...args];
61
+ if (command.includes('/') || command.includes('\\') || command.endsWith('.ts')) {
62
+ cmd = process.execPath;
63
+ spawnArgs = [tsxCliPath, command, ...args];
55
64
  } else {
56
65
  const resolved = resolveCommand(command);
57
66
  cmd = resolved.cmd;
@@ -61,11 +70,16 @@ export class ChildProcessCliExecutorAdapter implements CliExecutorPort {
61
70
  let stdout = '';
62
71
  let stderr = '';
63
72
  let timedOut = false;
73
+ let closed = false;
74
+ let cleanupDone = false;
64
75
 
65
76
  const child = spawn(cmd, spawnArgs, {
66
77
  stdio: ['pipe', 'pipe', 'pipe'],
67
78
  shell: false,
79
+ cwd: this.options.cwd,
80
+ detached: process.platform !== 'win32',
68
81
  });
82
+ child.stdin?.end();
69
83
 
70
84
  child.stdout?.on('data', (data: Buffer) => {
71
85
  stdout += data.toString();
@@ -76,21 +90,57 @@ export class ChildProcessCliExecutorAdapter implements CliExecutorPort {
76
90
  });
77
91
 
78
92
  let timer: NodeJS.Timeout | undefined;
93
+ let killTimer: NodeJS.Timeout | undefined;
94
+ const finishTimeout = () => {
95
+ if (closed && cleanupDone) reject(new TimeoutError(command, timeoutMs!));
96
+ };
97
+ const signalTree = (signal: NodeJS.Signals) => {
98
+ if (child.pid === undefined) return;
99
+ try {
100
+ process.kill(-child.pid, signal);
101
+ } catch (error) {
102
+ if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error;
103
+ }
104
+ };
79
105
 
80
106
  if (timeoutMs !== undefined) {
81
107
  timer = setTimeout(() => {
82
108
  timedOut = true;
83
- child.kill('SIGTERM');
84
- reject(new TimeoutError(command, timeoutMs));
109
+ try {
110
+ if (process.platform === 'win32' && child.pid !== undefined) {
111
+ const cleanup = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { shell: false, timeout: 5000 });
112
+ if (cleanup.error || cleanup.status !== 0) throw cleanup.error ?? new Error(`Process tree cleanup failed: ${cleanup.status}`);
113
+ cleanupDone = true;
114
+ finishTimeout();
115
+ } else {
116
+ signalTree('SIGTERM');
117
+ // Keep the group cleanup even if its leader closes before descendants.
118
+ killTimer = setTimeout(() => {
119
+ try {
120
+ signalTree('SIGKILL');
121
+ cleanupDone = true;
122
+ finishTimeout();
123
+ } catch (error) {
124
+ reject(error);
125
+ }
126
+ }, 250);
127
+ }
128
+ } catch (error) {
129
+ reject(error);
130
+ }
85
131
  }, timeoutMs);
86
132
  }
87
133
 
88
134
  child.on('close', (exitCode) => {
135
+ closed = true;
89
136
  if (timer) clearTimeout(timer);
90
- if (timedOut) return;
137
+ if (timedOut) {
138
+ finishTimeout();
139
+ return;
140
+ }
91
141
 
92
142
  resolve({
93
- exitCode: exitCode ?? 0,
143
+ exitCode: exitCode ?? 1,
94
144
  stdout,
95
145
  stderr,
96
146
  timedOut: false,
@@ -99,6 +149,7 @@ export class ChildProcessCliExecutorAdapter implements CliExecutorPort {
99
149
 
100
150
  child.on('error', (error) => {
101
151
  if (timer) clearTimeout(timer);
152
+ if (killTimer) clearTimeout(killTimer);
102
153
  reject(error);
103
154
  });
104
155
  });
@@ -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 });
@@ -243,7 +243,7 @@ export function buildSessionStartContext(status: PhasegateStatus): string {
243
243
  "",
244
244
  "- Do NOT write to protected files without going through `/quick-implementor` skill.",
245
245
  '- Do NOT create/structurally modify source files under units listed as "blocked" below — the required design docs (logical_design.md / domain_model.md) are missing, and pre-tool-use hooks will block writes.',
246
- "- Prefer the native `apply_patch` tool for edits, BUT note that Codex's apply_patch bypasses pre-edit hooks. Violations surface at pre-commit time.",
246
+ "- Native `apply_patch` edits are phase-gated at PreToolUse; the Codex matcher is `Bash|apply_patch`. L2 pre-commit remains the backstop for skipped or untrusted hooks.",
247
247
  "",
248
248
  ];
249
249
 
@@ -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,8 +1,10 @@
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
7
+ * @work-item-id WI-384
6
8
  *
7
9
  * PostToolUse Hook Adapter
8
10
  * Claude Code の PostToolUse Hook エントリポイント
@@ -15,11 +17,7 @@ import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child
15
17
  import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
16
18
  import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
17
19
  import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
18
-
19
- interface PostToolUseHookInput {
20
- tool_name?: string;
21
- tool_response?: unknown;
22
- }
20
+ import { normalizePostToolUse, renderPostToolUseFeedback } from './post-tool-use-feedback.js';
23
21
 
24
22
  async function readStdin(): Promise<string> {
25
23
  const chunks: Buffer[] = [];
@@ -29,8 +27,8 @@ async function readStdin(): Promise<string> {
29
27
  return Buffer.concat(chunks).toString("utf8");
30
28
  }
31
29
 
32
- async function findConfigPath(): Promise<string> {
33
- let dir = process.cwd();
30
+ async function findConfigPath(startDir: string): Promise<string> {
31
+ let dir = startDir;
34
32
  while (true) {
35
33
  const candidates = [
36
34
  path.join(dir, "phasegate.config.json"),
@@ -46,7 +44,7 @@ async function findConfigPath(): Promise<string> {
46
44
  if (parent === dir) break;
47
45
  dir = parent;
48
46
  }
49
- return path.join(process.cwd(), "phasegate.config.json");
47
+ return path.join(startDir, "phasegate.config.json");
50
48
  }
51
49
 
52
50
  function projectRootForConfig(configPath: string): string {
@@ -63,20 +61,21 @@ async function main(): Promise<void> {
63
61
  process.exit(2);
64
62
  }
65
63
 
66
- let input: PostToolUseHookInput;
64
+ let input: unknown;
67
65
  try {
68
- input = JSON.parse(raw) as PostToolUseHookInput;
66
+ input = JSON.parse(raw) as unknown;
69
67
  } catch {
70
68
  process.stderr.write(`不正なJSONです: ${raw}\n`);
71
69
  process.exit(2);
72
70
  }
73
71
 
74
- const toolName = input.tool_name;
72
+ const request = normalizePostToolUse(input, process.cwd());
73
+ const toolName = request.toolName;
75
74
  if (!toolName) {
76
75
  // WI-323: PostToolUse はツール実行後の lint フィードバックでありゲートではないため、
77
76
  // tool_name 欠落は fail-open でスキップする(WI-314 / github#40 方針)。
78
77
  // ※ pre-tool-use-hook の同ガードは書き込みゲートなので fail-closed (exit 2) を維持する。
79
- const configPath = await findConfigPath();
78
+ const configPath = await findConfigPath(request.cwd);
80
79
  await recordHookSkipEvent({
81
80
  projectRoot: projectRootForConfig(configPath),
82
81
  hookType: "post-tool-use",
@@ -90,10 +89,13 @@ async function main(): Promise<void> {
90
89
  }
91
90
 
92
91
  try {
93
- 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));
94
96
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
95
97
  const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
96
- const cliExecutorPort = new ChildProcessCliExecutorAdapter();
98
+ const cliExecutorPort = new ChildProcessCliExecutorAdapter({ cwd: projectRoot });
97
99
 
98
100
  const useCase = new HandlePostToolUseUseCase({
99
101
  configQueryPort,
@@ -101,27 +103,19 @@ async function main(): Promise<void> {
101
103
  cliCommandRegistryPort,
102
104
  });
103
105
 
104
- const output = await useCase.execute({ toolName, affectedFilePaths: [] });
106
+ const output = await useCase.execute({ toolName, affectedFilePaths });
105
107
 
106
108
  if (output.skipReason) {
107
109
  await recordHookSkipEvent({
108
110
  projectRoot: projectRootForConfig(configPath),
109
111
  hookType: "post-tool-use",
110
112
  reason: output.skipReason,
111
- targetPaths: [],
113
+ targetPaths: affectedFilePaths,
112
114
  });
113
- process.stderr.write(`スキップ: ${output.skipReason}\n`);
114
- process.exit(0);
115
- }
116
-
117
- if (output.executed && output.cliResult) {
118
- if (output.cliResult.exitCode !== 0) {
119
- process.stderr.write(`Lint失敗 (exitCode=${output.cliResult.exitCode})\n`);
120
- }
121
- process.exit(output.cliResult.exitCode);
122
115
  }
123
-
124
- process.exit(0);
116
+ const feedback = renderPostToolUseFeedback(output);
117
+ if (feedback.text) process.stderr.write(feedback.text);
118
+ process.exit(feedback.exitCode);
125
119
  } catch (error) {
126
120
  process.stderr.write(`実行エラー: ${String(error)}\n`);
127
121
  process.exit(2);