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.
- package/CHANGELOG.md +16 -0
- package/README.ja.md +22 -9
- package/README.md +28 -17
- package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/folder_management_rules.md +10 -1
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +27 -7
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +28 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +34 -5
- package/docs/guide/installation.md +4 -2
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +4 -1
- package/package.json +2 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
- package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
- package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
- package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
- package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
- package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -70
- package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
- package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
- package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
- package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
- package/scripts/harness/installation/application/named-hook-json.ts +17 -0
- package/scripts/harness/installation/application/operator-notice.ts +42 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
- package/scripts/harness/installation/application/usecases/run-install.ts +157 -57
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
- package/scripts/harness/installation/composition-root.ts +10 -0
- package/scripts/harness/installation/domain/agent-target.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +5 -0
- package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
- package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
- package/scripts/harness/installation/domain/repair-table.ts +5 -0
- package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +25 -5
- package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
- package/scripts/harness/main.ts +227 -128
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
- package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
- package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/composition-root.ts +5 -1
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/scripts/harness/setup/skill-deployer.ts +3 -45
- package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
- package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
- package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
- package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
- package/scripts/harness/skill-quality/composition-root.ts +14 -7
- package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
- package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
- package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
- package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
- package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
- package/skills/README.md +7 -0
- package/skills/cascade-updater/SKILL.md +37 -109
- 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
- package/skills/release-publisher/SKILL.md +3 -1
- package/skills/skill-creator/SKILL.md +8 -9
- package/templates/.agents/hooks.json +16 -0
- package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
- package/templates/.claude/settings.json +5 -3
- 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' → [
|
|
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:
|
|
29
|
-
args: [
|
|
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:
|
|
37
|
-
args: [
|
|
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 =
|
|
54
|
-
spawnArgs = [
|
|
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
|
-
|
|
84
|
-
|
|
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)
|
|
137
|
+
if (timedOut) {
|
|
138
|
+
finishTimeout();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
91
141
|
|
|
92
142
|
resolve({
|
|
93
|
-
exitCode: exitCode ??
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"-
|
|
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 =
|
|
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(
|
|
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:
|
|
64
|
+
let input: unknown;
|
|
67
65
|
try {
|
|
68
|
-
input = JSON.parse(raw) as
|
|
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
|
|
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.
|
|
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);
|