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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "Apache-2.0",
@@ -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
  }
@@ -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 });