phasegate 0.65.0 → 0.66.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/package.json +1 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +72 -17
- package/scripts/harness/agent-integration/domain/ports/baseline-grandfather-query-port.ts +12 -0
- package/scripts/harness/agent-integration/domain/ports/config-query-port.ts +6 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +90 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +20 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -0
package/package.json
CHANGED
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -14,6 +14,10 @@ import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
|
14
14
|
import type { PhaseGateQueryPort } from '../../domain/ports/phase-gate-query-port.js';
|
|
15
15
|
import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
|
|
16
16
|
import type { FullModeRequirementQueryPort } from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
17
|
+
import type {
|
|
18
|
+
BaselineGrandfatherCheckResult,
|
|
19
|
+
BaselineGrandfatherQueryPort,
|
|
20
|
+
} from '../../domain/ports/baseline-grandfather-query-port.js';
|
|
17
21
|
import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
|
|
18
22
|
import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
|
|
19
23
|
|
|
@@ -22,6 +26,8 @@ export interface HandlePreToolUseUseCasePorts {
|
|
|
22
26
|
phaseGateQueryPort: PhaseGateQueryPort;
|
|
23
27
|
storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
24
28
|
fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
29
|
+
baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
|
|
30
|
+
grandfatherLogger?: (reason: string, targetFilePaths: readonly string[]) => void;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export class HandlePreToolUseInputValidationError extends Error {
|
|
@@ -41,11 +47,23 @@ export class HandlePreToolUseUseCase {
|
|
|
41
47
|
private readonly configQueryPort: ConfigQueryPort;
|
|
42
48
|
private readonly storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
43
49
|
private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
50
|
+
private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
|
|
51
|
+
private readonly grandfatherLogger: (
|
|
52
|
+
reason: string,
|
|
53
|
+
targetFilePaths: readonly string[],
|
|
54
|
+
) => void;
|
|
44
55
|
|
|
45
56
|
constructor(ports: HandlePreToolUseUseCasePorts) {
|
|
46
57
|
this.configQueryPort = ports.configQueryPort;
|
|
47
58
|
this.storyReflectionQueryPort = ports.storyReflectionQueryPort;
|
|
48
59
|
this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
|
|
60
|
+
this.baselineGrandfatherQueryPort = ports.baselineGrandfatherQueryPort;
|
|
61
|
+
this.grandfatherLogger =
|
|
62
|
+
ports.grandfatherLogger ??
|
|
63
|
+
((reason, paths) =>
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`[baseline] grandfather skip (${reason}): ${paths.join(', ')}\n`,
|
|
66
|
+
));
|
|
49
67
|
this.translator = new AsyncHookToCliTranslator({
|
|
50
68
|
configQueryPort: ports.configQueryPort,
|
|
51
69
|
reentryGuard: { isActive: () => false } as never,
|
|
@@ -59,6 +77,8 @@ export class HandlePreToolUseUseCase {
|
|
|
59
77
|
throw new HandlePreToolUseInputValidationError('toolNameは必須です(空文字不可)');
|
|
60
78
|
}
|
|
61
79
|
|
|
80
|
+
const grandfather = await this.checkGrandfather(input.targetFilePaths);
|
|
81
|
+
|
|
62
82
|
const hookEvent = HookEvent.createPreToolUse(input.toolName, input.targetFilePaths);
|
|
63
83
|
const result = await this.translator.translate(hookEvent);
|
|
64
84
|
|
|
@@ -66,32 +86,41 @@ export class HandlePreToolUseUseCase {
|
|
|
66
86
|
const metadata = result.blockMetadata;
|
|
67
87
|
const blockedFilePath = metadata?.blockedFilePath ?? input.targetFilePaths[0];
|
|
68
88
|
|
|
69
|
-
if (metadata?.reason === 'PHASE_GATE') {
|
|
70
|
-
return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
89
|
if (metadata?.reason === 'PROTECTED_FILE') {
|
|
74
90
|
return HandlePreToolUseUseCase.buildProtectedFileBlockOutput(blockedFilePath);
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
if (metadata?.reason === 'PHASE_GATE') {
|
|
94
|
+
if (grandfather.allGrandfathered) {
|
|
95
|
+
this.grandfatherLogger('phase-gate', input.targetFilePaths);
|
|
96
|
+
// fallthrough: continue to full-mode / story-reflection checks (which may also grandfather)
|
|
97
|
+
} else {
|
|
98
|
+
return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
return {
|
|
102
|
+
shouldBlock: true,
|
|
103
|
+
blockedFilePath,
|
|
104
|
+
error: {
|
|
105
|
+
message: `ブロックされました: ${blockedFilePath ?? '不明なファイル'}`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
if (HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)
|
|
87
112
|
&& this.fullModeRequirementQueryPort !== undefined
|
|
88
113
|
&& input.targetFilePaths.length > 0) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
if (grandfather.allGrandfathered) {
|
|
115
|
+
this.grandfatherLogger('full-mode', input.targetFilePaths);
|
|
116
|
+
} else {
|
|
117
|
+
const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
|
|
118
|
+
if (fullModeResult.requiresFullMode) {
|
|
119
|
+
return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
|
|
120
|
+
input.targetFilePaths[0],
|
|
121
|
+
fullModeResult,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
|
|
@@ -100,6 +129,11 @@ export class HandlePreToolUseUseCase {
|
|
|
100
129
|
return { shouldBlock: false };
|
|
101
130
|
}
|
|
102
131
|
|
|
132
|
+
if (grandfather.allGrandfathered) {
|
|
133
|
+
this.grandfatherLogger('story-reflection', input.targetFilePaths);
|
|
134
|
+
return { shouldBlock: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
const reflectionResult = await this.storyReflectionQueryPort.checkReflection(scope.unitId!);
|
|
104
138
|
|
|
105
139
|
if (reflectionResult.skipped || reflectionResult.passed) {
|
|
@@ -113,6 +147,27 @@ export class HandlePreToolUseUseCase {
|
|
|
113
147
|
);
|
|
114
148
|
}
|
|
115
149
|
|
|
150
|
+
private async checkGrandfather(
|
|
151
|
+
targetFilePaths: readonly string[],
|
|
152
|
+
): Promise<BaselineGrandfatherCheckResult> {
|
|
153
|
+
if (this.baselineGrandfatherQueryPort === undefined) {
|
|
154
|
+
return {
|
|
155
|
+
allGrandfathered: false,
|
|
156
|
+
baselineEnabled: false,
|
|
157
|
+
grandfatheredPaths: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return await this.baselineGrandfatherQueryPort.check(targetFilePaths);
|
|
162
|
+
} catch {
|
|
163
|
+
return {
|
|
164
|
+
allGrandfathered: false,
|
|
165
|
+
baselineEnabled: false,
|
|
166
|
+
grandfatheredPaths: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
116
171
|
private static buildFullModeRequiredBlockOutput(
|
|
117
172
|
blockedFilePath: string | undefined,
|
|
118
173
|
result: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
export interface BaselineGrandfatherCheckResult {
|
|
5
|
+
readonly allGrandfathered: boolean;
|
|
6
|
+
readonly baselineEnabled: boolean;
|
|
7
|
+
readonly grandfatheredPaths: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BaselineGrandfatherQueryPort {
|
|
11
|
+
check(targetFilePaths: readonly string[]): Promise<BaselineGrandfatherCheckResult>;
|
|
12
|
+
}
|
|
@@ -5,10 +5,16 @@ import type { ProjectPaths } from '../value-objects/project-paths.js';
|
|
|
5
5
|
|
|
6
6
|
export type HookType = 'pre-tool-use' | 'post-tool-use' | 'stop';
|
|
7
7
|
|
|
8
|
+
export interface BaselineConfig {
|
|
9
|
+
readonly enabled: boolean;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface ConfigQueryPort {
|
|
9
14
|
isHookEnabled(hookType: HookType): Promise<boolean>;
|
|
10
15
|
getProtectedFilePatterns(): Promise<string[]>;
|
|
11
16
|
getProtectedFileExclusions(): Promise<string[]>;
|
|
12
17
|
getRelaxedGates(): Promise<readonly string[]>;
|
|
13
18
|
getProjectPaths(): ProjectPaths;
|
|
19
|
+
getBaselineConfig(): Promise<BaselineConfig>;
|
|
14
20
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
BaselineGrandfatherQueryPort,
|
|
6
|
+
BaselineGrandfatherCheckResult,
|
|
7
|
+
} from '../../domain/ports/baseline-grandfather-query-port.js';
|
|
8
|
+
import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
9
|
+
import type { BaselineRepositoryPort } from '../../../ci-governance/domain/ports/baseline-repository-port.js';
|
|
10
|
+
import { BaselineJsonRepositoryAdapter } from '../../../ci-governance/infrastructure/adapters/baseline-json-repository-adapter.js';
|
|
11
|
+
|
|
12
|
+
export interface CiGovernanceBaselineGrandfatherAdapterDeps {
|
|
13
|
+
readonly baseDir: string;
|
|
14
|
+
readonly configQueryPort: ConfigQueryPort;
|
|
15
|
+
readonly baselineRepositoryFactory?: (
|
|
16
|
+
baseDir: string,
|
|
17
|
+
relativePath: string,
|
|
18
|
+
) => BaselineRepositoryPort;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CiGovernanceBaselineGrandfatherAdapter
|
|
22
|
+
implements BaselineGrandfatherQueryPort
|
|
23
|
+
{
|
|
24
|
+
constructor(private readonly deps: CiGovernanceBaselineGrandfatherAdapterDeps) {}
|
|
25
|
+
|
|
26
|
+
async check(
|
|
27
|
+
targetFilePaths: readonly string[],
|
|
28
|
+
): Promise<BaselineGrandfatherCheckResult> {
|
|
29
|
+
try {
|
|
30
|
+
const baselineConfig = await this.deps.configQueryPort.getBaselineConfig();
|
|
31
|
+
|
|
32
|
+
if (!baselineConfig.enabled) {
|
|
33
|
+
return {
|
|
34
|
+
allGrandfathered: false,
|
|
35
|
+
baselineEnabled: false,
|
|
36
|
+
grandfatheredPaths: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (targetFilePaths.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
allGrandfathered: false,
|
|
43
|
+
baselineEnabled: true,
|
|
44
|
+
grandfatheredPaths: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const factory =
|
|
49
|
+
this.deps.baselineRepositoryFactory ??
|
|
50
|
+
((baseDir, relativePath) =>
|
|
51
|
+
new BaselineJsonRepositoryAdapter(baseDir, relativePath));
|
|
52
|
+
const repository = factory(this.deps.baseDir, baselineConfig.path);
|
|
53
|
+
|
|
54
|
+
if (!(await repository.exists())) {
|
|
55
|
+
return {
|
|
56
|
+
allGrandfathered: false,
|
|
57
|
+
baselineEnabled: true,
|
|
58
|
+
grandfatheredPaths: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const snapshot = await repository.load();
|
|
63
|
+
if (snapshot === null) {
|
|
64
|
+
return {
|
|
65
|
+
allGrandfathered: false,
|
|
66
|
+
baselineEnabled: true,
|
|
67
|
+
grandfatheredPaths: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const grandfathered: string[] = [];
|
|
72
|
+
for (const p of targetFilePaths) {
|
|
73
|
+
if (snapshot.contains(p)) grandfathered.push(p);
|
|
74
|
+
}
|
|
75
|
+
const allGrandfathered = grandfathered.length === targetFilePaths.length;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
allGrandfathered,
|
|
79
|
+
baselineEnabled: true,
|
|
80
|
+
grandfatheredPaths: grandfathered,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return {
|
|
84
|
+
allGrandfathered: false,
|
|
85
|
+
baselineEnabled: false,
|
|
86
|
+
grandfatheredPaths: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
BaselineConfig,
|
|
12
|
+
ConfigQueryPort,
|
|
13
|
+
HookType,
|
|
14
|
+
} from '../../domain/ports/config-query-port.js';
|
|
11
15
|
import { ProjectPaths } from '../../domain/value-objects/project-paths.js';
|
|
12
16
|
|
|
13
17
|
interface ProjectDocsSection {
|
|
@@ -39,11 +43,17 @@ interface QuickModeSection {
|
|
|
39
43
|
relaxedGates?: string[];
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
interface BaselineSection {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
path?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
interface HarnessConfigDocument {
|
|
43
52
|
harnesses?: HarnessesSection;
|
|
44
53
|
project?: ProjectSection;
|
|
45
54
|
protectedFiles?: ProtectedFilesSection;
|
|
46
55
|
quickMode?: QuickModeSection;
|
|
56
|
+
baseline?: BaselineSection;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
@@ -109,4 +119,13 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
109
119
|
},
|
|
110
120
|
);
|
|
111
121
|
}
|
|
122
|
+
|
|
123
|
+
async getBaselineConfig(): Promise<BaselineConfig> {
|
|
124
|
+
const config = this.loadConfig();
|
|
125
|
+
const baseline = config.baseline ?? {};
|
|
126
|
+
return {
|
|
127
|
+
enabled: baseline.enabled ?? false,
|
|
128
|
+
path: baseline.path ?? '.phasegate/baseline.json',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
112
131
|
}
|
|
@@ -13,6 +13,7 @@ import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harn
|
|
|
13
13
|
import { PhaseGateQueryAdapter } from '../infrastructure/adapters/phase-gate-query-adapter.js';
|
|
14
14
|
import { FileSystemStoryReflectionQueryAdapter } from '../infrastructure/adapters/file-system-story-reflection-query-adapter.js';
|
|
15
15
|
import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
|
|
16
|
+
import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
|
|
16
17
|
import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
|
|
17
18
|
import * as path from 'node:path';
|
|
18
19
|
import * as fs from 'node:fs/promises';
|
|
@@ -123,11 +124,16 @@ async function main(): Promise<void> {
|
|
|
123
124
|
const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
|
|
124
125
|
classifyUseCaseFactory: () => createQuickModeCompositionRoot().classifyUseCase,
|
|
125
126
|
});
|
|
127
|
+
const baselineGrandfatherQueryPort = new CiGovernanceBaselineGrandfatherAdapter({
|
|
128
|
+
baseDir: path.dirname(configPath),
|
|
129
|
+
configQueryPort,
|
|
130
|
+
});
|
|
126
131
|
const useCase = new HandlePreToolUseUseCase({
|
|
127
132
|
configQueryPort,
|
|
128
133
|
phaseGateQueryPort,
|
|
129
134
|
storyReflectionQueryPort,
|
|
130
135
|
fullModeRequirementQueryPort,
|
|
136
|
+
baselineGrandfatherQueryPort,
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
const output = await useCase.execute({ toolName: effectiveToolName, targetFilePaths });
|