phasegate 0.63.0 → 0.64.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/dto/handle-pre-tool-use-dto.ts +3 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +51 -0
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +13 -0
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +4 -1
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +45 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -0
package/package.json
CHANGED
|
@@ -11,10 +11,12 @@ export interface HandlePreToolUseInput {
|
|
|
11
11
|
export interface HandlePreToolUseOutput {
|
|
12
12
|
shouldBlock: boolean;
|
|
13
13
|
blockedFilePath?: string;
|
|
14
|
-
blockReason?: 'PROTECTED_FILE' | 'PHASE_GATE' | 'STORY_REFLECTION';
|
|
14
|
+
blockReason?: 'PROTECTED_FILE' | 'PHASE_GATE' | 'STORY_REFLECTION' | 'FULL_MODE_REQUIRED';
|
|
15
15
|
error?: { message: string };
|
|
16
16
|
phaseGateBlockers?: string[];
|
|
17
17
|
storyReflectionBlockers?: string[];
|
|
18
18
|
storyReflectionWarnings?: string[];
|
|
19
|
+
fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
20
|
+
fullModeDominantCategory?: string;
|
|
19
21
|
nextAction?: string;
|
|
20
22
|
}
|
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { BlockMetadata } from '../../domain/value-objects/hook-translation-
|
|
|
13
13
|
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
|
+
import type { FullModeRequirementQueryPort } from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
16
17
|
import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
|
|
17
18
|
import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export interface HandlePreToolUseUseCasePorts {
|
|
|
20
21
|
configQueryPort: ConfigQueryPort;
|
|
21
22
|
phaseGateQueryPort: PhaseGateQueryPort;
|
|
22
23
|
storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
24
|
+
fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export class HandlePreToolUseInputValidationError extends Error {
|
|
@@ -38,10 +40,12 @@ export class HandlePreToolUseUseCase {
|
|
|
38
40
|
private readonly translator: AsyncHookToCliTranslator;
|
|
39
41
|
private readonly configQueryPort: ConfigQueryPort;
|
|
40
42
|
private readonly storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
43
|
+
private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
41
44
|
|
|
42
45
|
constructor(ports: HandlePreToolUseUseCasePorts) {
|
|
43
46
|
this.configQueryPort = ports.configQueryPort;
|
|
44
47
|
this.storyReflectionQueryPort = ports.storyReflectionQueryPort;
|
|
48
|
+
this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
|
|
45
49
|
this.translator = new AsyncHookToCliTranslator({
|
|
46
50
|
configQueryPort: ports.configQueryPort,
|
|
47
51
|
reentryGuard: { isActive: () => false } as never,
|
|
@@ -79,6 +83,18 @@ export class HandlePreToolUseUseCase {
|
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
if (HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)
|
|
87
|
+
&& this.fullModeRequirementQueryPort !== undefined
|
|
88
|
+
&& input.targetFilePaths.length > 0) {
|
|
89
|
+
const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
|
|
90
|
+
if (fullModeResult.requiresFullMode) {
|
|
91
|
+
return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
|
|
92
|
+
input.targetFilePaths[0],
|
|
93
|
+
fullModeResult,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
const scope = this.resolveStoryReflectionScope(input);
|
|
83
99
|
if (scope === null || this.storyReflectionQueryPort === undefined) {
|
|
84
100
|
return { shouldBlock: false };
|
|
@@ -97,6 +113,41 @@ export class HandlePreToolUseUseCase {
|
|
|
97
113
|
);
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
private static buildFullModeRequiredBlockOutput(
|
|
117
|
+
blockedFilePath: string | undefined,
|
|
118
|
+
result: {
|
|
119
|
+
requiresFullMode: boolean;
|
|
120
|
+
rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
121
|
+
rejectionReason?: string;
|
|
122
|
+
dominantCategory?: string;
|
|
123
|
+
},
|
|
124
|
+
): HandlePreToolUseOutput {
|
|
125
|
+
const fp = blockedFilePath ?? '不明なファイル';
|
|
126
|
+
const lines: string[] = [
|
|
127
|
+
`Full mode 必須変更が検出されました: ${fp}`,
|
|
128
|
+
];
|
|
129
|
+
if (result.dominantCategory) {
|
|
130
|
+
lines.push(`カテゴリ: ${result.dominantCategory}`);
|
|
131
|
+
}
|
|
132
|
+
if (result.rejectionRule) {
|
|
133
|
+
lines.push(`判定ルール: ${result.rejectionRule}`);
|
|
134
|
+
}
|
|
135
|
+
if (result.rejectionReason) {
|
|
136
|
+
lines.push(`理由: ${result.rejectionReason}`);
|
|
137
|
+
}
|
|
138
|
+
lines.push('次のアクション: /story-implementor スキルを使用して設計フェーズから開始してください。');
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
shouldBlock: true,
|
|
142
|
+
blockedFilePath,
|
|
143
|
+
blockReason: 'FULL_MODE_REQUIRED',
|
|
144
|
+
error: { message: lines.join('\n') },
|
|
145
|
+
fullModeRejectionRule: result.rejectionRule,
|
|
146
|
+
fullModeDominantCategory: result.dominantCategory,
|
|
147
|
+
nextAction: '/story-implementor',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
100
151
|
private resolveStoryReflectionScope(input: HandlePreToolUseInput): WriteTargetScope | null {
|
|
101
152
|
if (!HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)) {
|
|
102
153
|
return null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
export interface FullModeRequirementQueryResult {
|
|
5
|
+
readonly requiresFullMode: boolean;
|
|
6
|
+
readonly rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
7
|
+
readonly rejectionReason?: string;
|
|
8
|
+
readonly dominantCategory?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FullModeRequirementQueryPort {
|
|
12
|
+
check(targetFilePaths: readonly string[]): Promise<FullModeRequirementQueryResult>;
|
|
13
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export type SkipReason = 'REENTRY_DETECTED' | 'HOOK_DISABLED' | 'TIMEOUT_EXCEEDED';
|
|
10
10
|
|
|
11
|
-
export type BlockReason = 'PROTECTED_FILE' | 'PHASE_GATE';
|
|
11
|
+
export type BlockReason = 'PROTECTED_FILE' | 'PHASE_GATE' | 'FULL_MODE_REQUIRED';
|
|
12
12
|
|
|
13
13
|
export interface BlockMetadata {
|
|
14
14
|
readonly reason: BlockReason;
|
|
@@ -18,6 +18,9 @@ export interface BlockMetadata {
|
|
|
18
18
|
readonly scopeLevel?: 1 | 2 | 3;
|
|
19
19
|
readonly unitId?: string;
|
|
20
20
|
readonly storyId?: string;
|
|
21
|
+
readonly fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
22
|
+
readonly fullModeRejectionReason?: string;
|
|
23
|
+
readonly fullModeDominantCategory?: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export class HookTranslationResultInvariantError extends Error {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
FullModeRequirementQueryPort,
|
|
6
|
+
FullModeRequirementQueryResult,
|
|
7
|
+
} from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
8
|
+
import type { ClassifyChangeCategoryUseCase } from '../../../quick-mode/application/usecases/classify-change-category-usecase.js';
|
|
9
|
+
|
|
10
|
+
export interface QuickModeFullModeRequirementAdapterDeps {
|
|
11
|
+
classifyUseCaseFactory: () => ClassifyChangeCategoryUseCase;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class QuickModeFullModeRequirementAdapter implements FullModeRequirementQueryPort {
|
|
15
|
+
private readonly classifyUseCaseFactory: () => ClassifyChangeCategoryUseCase;
|
|
16
|
+
|
|
17
|
+
constructor(deps: QuickModeFullModeRequirementAdapterDeps) {
|
|
18
|
+
this.classifyUseCaseFactory = deps.classifyUseCaseFactory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async check(targetFilePaths: readonly string[]): Promise<FullModeRequirementQueryResult> {
|
|
22
|
+
if (targetFilePaths.length === 0) {
|
|
23
|
+
return { requiresFullMode: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const useCase = this.classifyUseCaseFactory();
|
|
28
|
+
const contract = await useCase.execute({ paths: [...targetFilePaths] });
|
|
29
|
+
if (!contract.fullModeRequired) {
|
|
30
|
+
return {
|
|
31
|
+
requiresFullMode: false,
|
|
32
|
+
dominantCategory: contract.dominantCategory ?? undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
requiresFullMode: true,
|
|
37
|
+
rejectionRule: contract.rejectionRule,
|
|
38
|
+
rejectionReason: contract.rejectionReason,
|
|
39
|
+
dominantCategory: contract.dominantCategory ?? undefined,
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return { requiresFullMode: false };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -12,6 +12,8 @@ import { BashWriteTargetExtractor } from '../domain/services/bash-write-target-e
|
|
|
12
12
|
import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harness-config-config-query-adapter.js';
|
|
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
|
+
import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
|
|
16
|
+
import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
|
|
15
17
|
import * as path from 'node:path';
|
|
16
18
|
import * as fs from 'node:fs/promises';
|
|
17
19
|
|
|
@@ -118,10 +120,14 @@ async function main(): Promise<void> {
|
|
|
118
120
|
rootDir: path.dirname(configPath),
|
|
119
121
|
configPath,
|
|
120
122
|
});
|
|
123
|
+
const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
|
|
124
|
+
classifyUseCaseFactory: () => createQuickModeCompositionRoot().classifyUseCase,
|
|
125
|
+
});
|
|
121
126
|
const useCase = new HandlePreToolUseUseCase({
|
|
122
127
|
configQueryPort,
|
|
123
128
|
phaseGateQueryPort,
|
|
124
129
|
storyReflectionQueryPort,
|
|
130
|
+
fullModeRequirementQueryPort,
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
const output = await useCase.execute({ toolName: effectiveToolName, targetFilePaths });
|