phasegate 0.160.11 → 0.160.12

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 CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.160.12] - 2026-05-20
11
+
12
+ ### Added
13
+
14
+ - **WI-206 — hook-visible Full Mode sessions** — adds `phasegate session begin/end` for TTL-scoped Full Mode authorization, lets PreToolUse allow matching unit/category edits through `.phasegate/session.json`, updates `/story-implementor` to use the session route, and improves full-mode block guidance so users are not sent to a guide-only route.
15
+
10
16
  ## [0.160.11] - 2026-05-17
11
17
 
12
18
  ### Fixed
package/README.md CHANGED
@@ -135,7 +135,7 @@ npx phasegate reconcile --apply
135
135
 
136
136
  ```bash
137
137
  npx phasegate init --name my-project --agent codex --with-husky
138
- codex features enable codex_hooks
138
+ codex features enable hooks
139
139
  ```
140
140
 
141
141
  Use `--agent both` for projects that use Claude Code and Codex together. Codex native `apply_patch` currently cannot be intercepted before the edit, so those violations are caught at pre-commit; Bash-based writes are blocked before execution.
@@ -482,12 +482,12 @@ Phasegate also integrates with [OpenAI Codex CLI](https://developers.openai.com/
482
482
  npx phasegate init --name my-project --agent codex --with-husky
483
483
 
484
484
  # 2. Enable the Codex CLI feature flag manually on your machine
485
- codex features enable codex_hooks
485
+ codex features enable hooks
486
486
  ```
487
487
 
488
488
  For dual-agent projects (Claude + Codex), use `--agent both`.
489
489
 
490
- `init` sets up files inside the project. The Codex CLI user-level setting (`codex_hooks`) remains an explicit manual step.
490
+ `init` sets up files inside the project. The Codex CLI user-level hooks feature remains an explicit manual step.
491
491
 
492
492
  ### Coverage and known limitation
493
493
 
@@ -29,6 +29,7 @@ Command names in this document are split into three surfaces:
29
29
  | `reconcile --dry-run` / `--apply` | Update PhaseGate-managed files to current package templates and refresh manifest hashes; `--force` allows managed-file replacement with backup. |
30
30
  | `setup:agent` | Agent-readable setup planner and optional apply path. Options: `--intent <minimal\|recommended\|strict\|ci-only\|agent-hooks\|retrofit>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--with-husky`, `--with-ci`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-172 --> |
31
31
  | `config:plan` | Agent-readable configuration change planner. Options: `--intent <l4-strict\|codex-hooks\|ci-fail-on-warning\|baseline-reset\|quick-mode-strict\|quick-mode-relax\|retrofit-bootstrap\|planning-mode-relax>`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-173 --> <!-- @work-item-id WI-201 --> <!-- @work-item-id WI-204 --> |
32
+ | `session begin` / `session end` | Hook-visible Full Mode session manager. Use `session begin --mode full --unit <unit> --work-item <WI-XXX> --reason <text> --duration <ttl>` before implementation that must touch Quick Mode disallowed categories, and `session end --work-item <WI-XXX>` after completion. <!-- @work-item-id WI-206 --> |
32
33
  | `update-skills` | Compatibility alias for `reconcile`; use `reconcile` for new automation. |
33
34
  | `scaffold-wi <unit> <type>` | Create `docs/inception/{unit}/WI-XXX/description.md` using the next free WI number. |
34
35
  | `emit-agent-rules` | Print the AGENTS.md / CLAUDE.md WI workflow rules block. |
@@ -11,7 +11,7 @@ Phasegate supports [OpenAI Codex CLI](https://developers.openai.com/codex/cli) t
11
11
  npx phasegate init --name my-project --agent codex --with-husky
12
12
 
13
13
  # 2. Enable the Codex CLI feature flag manually
14
- codex features enable codex_hooks
14
+ codex features enable hooks
15
15
  ```
16
16
 
17
17
  For dual-agent projects (Claude + Codex), use `--agent both`.
@@ -19,7 +19,7 @@ For dual-agent projects (Claude + Codex), use `--agent both`.
19
19
  Responsibility split:
20
20
 
21
21
  - `phasegate init --agent codex` sets up **project-local artifacts** such as `phasegate.config.json`, `skills/`, `.codex/hooks.json`, and `.codex/skills`
22
- - `codex features enable codex_hooks` updates the **Codex CLI user environment** and is intentionally left as a manual step
22
+ - `codex features enable hooks` updates the **Codex CLI user environment** and is intentionally left as a manual step
23
23
 
24
24
  ### Manual setup
25
25
 
@@ -31,7 +31,7 @@ Add to `~/.codex/config.toml` (or project `.codex/config.toml`):
31
31
 
32
32
  ```toml
33
33
  [features]
34
- codex_hooks = true
34
+ hooks = true
35
35
  ```
36
36
 
37
37
  #### 2. Install Phasegate hooks
@@ -148,7 +148,7 @@ Codex documents that the newer `unified_exec` mechanism has incomplete intercept
148
148
 
149
149
  ### Hooks don't seem to run
150
150
 
151
- - Verify `codex_hooks = true` is set in `config.toml`
151
+ - Verify `hooks = true` is set in `config.toml`
152
152
  - Verify `.codex/hooks.json` is in the project root or `~/.codex/`
153
153
  - Run `codex --version` to ensure you're on a version that supports hooks
154
154
 
@@ -43,7 +43,7 @@ This deploys 30 skills to `skills/`, creates the agent-facing skill links (for e
43
43
  For Codex, project initialization stops at the project boundary. After `npx phasegate init --agent codex`, enable the Codex CLI feature flag manually:
44
44
 
45
45
  ```bash
46
- codex features enable codex_hooks
46
+ codex features enable hooks
47
47
  ```
48
48
 
49
49
  ### Existing Projects
@@ -62,6 +62,15 @@ Even when you launch `/quick-implementor`, the harness re-checks whether the in-
62
62
 
63
63
  Introduced in v0.63.0 (ISSUE-006 Story A — config-driven flags + `check-change-category` CLI) and wired into the hook in v0.64.0 (Story B). Each flag can be flipped to `false` only when a project intentionally accepts the risk of merging that category of change without the design ceremony — e.g. early-stage prototypes where new domain files churn freely.
64
64
 
65
+ When the escalation is intentional, do not widen `quickMode.allowedCategories` by hand. Start a hook-visible Full Mode session for the approved Unit/WI before the implementation step, then end it after completion:
66
+
67
+ ```bash
68
+ npx phasegate session begin --mode full --unit <unit> --work-item <WI-XXX> --reason "story-implementor Phase 2" --duration 1h
69
+ npx phasegate session end --work-item <WI-XXX>
70
+ ```
71
+
72
+ The PreToolUse hook validates `.phasegate/session.json` by TTL, unit, and dominant category. The `/story-implementor` skill is still the design/TDD guide; the session marker is the authorization the hook can actually observe. <!-- @work-item-id WI-206 -->
73
+
65
74
  ### Dry-running the classifier
66
75
 
67
76
  Use `check-change-category` to evaluate an arbitrary file list without actually starting an implementation:
@@ -29,7 +29,7 @@ Use this when a repository already has meaningful history or custom automation.
29
29
  ```bash
30
30
  npx phasegate setup:agent --intent agent-hooks --agent both --with-husky --dry-run --json
31
31
  npx phasegate install --agent both --apply
32
- codex features enable codex_hooks
32
+ codex features enable hooks
33
33
  npx phasegate doctor --json
34
34
  ```
35
35
 
@@ -15,7 +15,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
15
15
  | Generated artifact | `.phasegate/manifest.json`, `.phasegate/backups/*`, `.phasegate/uninstalled-*.json`, `.phasegate/baseline.json` | PhaseGate | Written by lifecycle commands and validators; safe to regenerate only through the owning command |
16
16
  | Runtime state/report | `.phasegate/hook-skip-events.jsonl`, explicit `doctor --report-out <path>` output, `reports/regression/*`, resolved `reporting.outputDir` reports | PhaseGate command output | Produced while hooks, doctor, and validation commands run |
17
17
  | Legacy artifact | `.harness-hooks.yml`, old Fuse hook files, `.harness/session-state.json`, `.harness/context-priority.json`, `.harness/reports` fallback | Compatibility only | Not required for current install lifecycle unless a project intentionally keeps an archived integration |
18
- | User-level setting | Codex CLI `codex_hooks` feature flag | User machine | Must be enabled manually with `codex features enable codex_hooks`; project commands do not modify it |
18
+ | User-level setting | Codex CLI `hooks` feature flag | User machine | Must be enabled manually with `codex features enable hooks`; project commands do not modify it |
19
19
 
20
20
  ## Managed Targets
21
21
 
@@ -42,7 +42,7 @@ Check both project and user-level state:
42
42
 
43
43
  ```bash
44
44
  npx phasegate doctor --json
45
- codex features enable codex_hooks
45
+ codex features enable hooks
46
46
  npx phasegate phasegate:status --json
47
47
  ```
48
48
 
@@ -32,7 +32,7 @@
32
32
 
33
33
  ### Codex Notes
34
34
 
35
- - Enable native hook execution with `codex features enable codex_hooks` when Codex hooks are installed.
35
+ - Enable native hook execution with `codex features enable hooks` when Codex hooks are installed.
36
36
  - Keep local-only Codex settings outside repository-managed targets unless PhaseGate explicitly reports them as managed.
37
37
  <!-- phasegate:managed-section:end -->
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.11",
3
+ "version": "0.160.12",
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": "MIT",
@@ -30,4 +30,9 @@ export interface HandlePreToolUseOutput {
30
30
  quickModeAllowed?: {
31
31
  dominantCategory?: string;
32
32
  };
33
+ fullModeSessionAllowed?: {
34
+ workItemId?: string;
35
+ unit?: string;
36
+ expiresAt?: string;
37
+ };
33
38
  }
@@ -3,6 +3,8 @@
3
3
  * @unit agent-integration
4
4
  * @story H11-02
5
5
  * @work-item-id WI-201
6
+ * @work-item-id WI-202 / WI-204
7
+ * @work-item-id WI-206
6
8
  *
7
9
  * HandlePreToolUseUseCase
8
10
  * PreToolUse Hook処理のオーケストレーション
@@ -14,6 +16,10 @@ import type {
14
16
  } from "../../domain/ports/baseline-grandfather-query-port.js";
15
17
  import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
16
18
  import type { ErrorGuidance, ErrorGuidanceQueryPort } from "../../domain/ports/error-guidance-query-port.js";
19
+ import type {
20
+ FullModeSessionQueryPort,
21
+ FullModeSessionQueryResult,
22
+ } from "../../domain/ports/full-mode-session-query-port.js";
17
23
  import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
18
24
  import type { PhaseGateQueryPort } from "../../domain/ports/phase-gate-query-port.js";
19
25
  import type { StoryReflectionQueryPort } from "../../domain/ports/story-reflection-query-port.js";
@@ -31,6 +37,7 @@ export interface HandlePreToolUseUseCasePorts {
31
37
  baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
32
38
  grandfatherLogger?: (reason: string, targetFilePaths: readonly string[]) => void;
33
39
  errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
40
+ fullModeSessionQueryPort?: FullModeSessionQueryPort;
34
41
  }
35
42
 
36
43
  export class HandlePreToolUseInputValidationError extends Error {
@@ -56,6 +63,7 @@ export class HandlePreToolUseUseCase {
56
63
  private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
57
64
  private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
58
65
  private readonly errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
66
+ private readonly fullModeSessionQueryPort?: FullModeSessionQueryPort;
59
67
  private readonly grandfatherLogger: (reason: string, targetFilePaths: readonly string[]) => void;
60
68
 
61
69
  constructor(ports: HandlePreToolUseUseCasePorts) {
@@ -65,6 +73,7 @@ export class HandlePreToolUseUseCase {
65
73
  this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
66
74
  this.baselineGrandfatherQueryPort = ports.baselineGrandfatherQueryPort;
67
75
  this.errorGuidanceQueryPort = ports.errorGuidanceQueryPort;
76
+ this.fullModeSessionQueryPort = ports.fullModeSessionQueryPort;
68
77
  this.grandfatherLogger =
69
78
  ports.grandfatherLogger ??
70
79
  ((reason, paths) => process.stderr.write(`[baseline] grandfather skip (${reason}): ${paths.join(", ")}\n`));
@@ -149,12 +158,27 @@ export class HandlePreToolUseUseCase {
149
158
  input.targetChanges,
150
159
  );
151
160
  if (fullModeResult.requiresFullMode) {
161
+ const unitIdForGuidance = this.deriveUnitIdFromPaths(input.targetFilePaths);
162
+ const sessionResult = await this.checkFullModeSession(
163
+ input.targetFilePaths,
164
+ unitIdForGuidance,
165
+ fullModeResult.dominantCategory,
166
+ );
167
+ if (sessionResult.allowed) {
168
+ return {
169
+ shouldBlock: false,
170
+ fullModeSessionAllowed: {
171
+ workItemId: sessionResult.workItemId,
172
+ unit: sessionResult.unit,
173
+ expiresAt: sessionResult.expiresAt,
174
+ },
175
+ };
176
+ }
152
177
  // ISSUE-021: 当該Unitの必須設計文書が揃っている場合は full mode block を bypass
153
178
  //(hook がスキルコンテキストを参照できない構造的ギャップへの対処)
154
179
  const bypassedByDesignDocs = await this.isFullModeBypassedByDesignDocs(input.targetFilePaths);
155
180
  if (!bypassedByDesignDocs) {
156
181
  const guidance = await this.resolveGuidance("L2-001");
157
- const unitIdForGuidance = this.deriveUnitIdFromPaths(input.targetFilePaths);
158
182
  return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
159
183
  input.targetFilePaths[0],
160
184
  fullModeResult,
@@ -227,6 +251,25 @@ export class HandlePreToolUseUseCase {
227
251
  }
228
252
  }
229
253
 
254
+ private async checkFullModeSession(
255
+ targetFilePaths: readonly string[],
256
+ unitId: string | undefined,
257
+ dominantCategory: string | undefined,
258
+ ): Promise<FullModeSessionQueryResult> {
259
+ if (this.fullModeSessionQueryPort === undefined) {
260
+ return { active: false, allowed: false };
261
+ }
262
+ try {
263
+ return await this.fullModeSessionQueryPort.check({
264
+ targetFilePaths,
265
+ unitId,
266
+ dominantCategory,
267
+ });
268
+ } catch {
269
+ return { active: false, allowed: false };
270
+ }
271
+ }
272
+
230
273
  private static buildFullModeRequiredBlockOutput(
231
274
  blockedFilePath: string | undefined,
232
275
  result: {
@@ -304,6 +347,12 @@ export class HandlePreToolUseUseCase {
304
347
  }
305
348
  const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
306
349
  lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
350
+ if (unitId !== undefined && unitId !== "") {
351
+ lines.push(
352
+ ` 実装フェーズ開始時: phasegate session begin --mode full --unit ${unitId} --work-item <WI-XXX> --reason "<reason>" --duration 1h`,
353
+ );
354
+ lines.push(" 実装完了時: phasegate session end --work-item <WI-XXX>");
355
+ }
307
356
  HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance, unitId);
308
357
 
309
358
  return {
@@ -0,0 +1,22 @@
1
+ // @unit agent-integration
2
+ // @layer domain
3
+ // @work-item-id WI-206
4
+
5
+ export interface FullModeSessionQueryInput {
6
+ readonly targetFilePaths: readonly string[];
7
+ readonly unitId?: string;
8
+ readonly dominantCategory?: string;
9
+ }
10
+
11
+ export interface FullModeSessionQueryResult {
12
+ readonly active: boolean;
13
+ readonly allowed: boolean;
14
+ readonly reason?: string;
15
+ readonly workItemId?: string;
16
+ readonly unit?: string;
17
+ readonly expiresAt?: string;
18
+ }
19
+
20
+ export interface FullModeSessionQueryPort {
21
+ check(input: FullModeSessionQueryInput): Promise<FullModeSessionQueryResult>;
22
+ }
@@ -0,0 +1,119 @@
1
+ // @unit agent-integration
2
+ // @layer infrastructure
3
+ // @work-item-id WI-206
4
+
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+
8
+ import type {
9
+ FullModeSessionQueryInput,
10
+ FullModeSessionQueryPort,
11
+ FullModeSessionQueryResult,
12
+ } from '../../domain/ports/full-mode-session-query-port.js';
13
+ import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
14
+ import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
15
+
16
+ interface FullModeSessionDocument {
17
+ readonly mode?: unknown;
18
+ readonly unit?: unknown;
19
+ readonly workItemId?: unknown;
20
+ readonly allowedCategories?: unknown;
21
+ readonly reason?: unknown;
22
+ readonly startedAt?: unknown;
23
+ readonly expiresAt?: unknown;
24
+ }
25
+
26
+ export class FileSystemFullModeSessionQueryAdapter implements FullModeSessionQueryPort {
27
+ constructor(
28
+ private readonly options: {
29
+ readonly rootDir: string;
30
+ readonly configQueryPort: ConfigQueryPort;
31
+ readonly now?: () => Date;
32
+ },
33
+ ) {}
34
+
35
+ async check(input: FullModeSessionQueryInput): Promise<FullModeSessionQueryResult> {
36
+ let document: FullModeSessionDocument;
37
+ try {
38
+ const raw = await fs.readFile(path.join(this.options.rootDir, '.phasegate', 'session.json'), 'utf8');
39
+ document = JSON.parse(raw) as FullModeSessionDocument;
40
+ } catch {
41
+ return { active: false, allowed: false, reason: 'session marker not found or unreadable' };
42
+ }
43
+
44
+ if (document.mode !== 'full') {
45
+ return { active: false, allowed: false, reason: 'session mode is not full' };
46
+ }
47
+ if (typeof document.unit !== 'string' || document.unit === '') {
48
+ return { active: true, allowed: false, reason: 'session unit is missing' };
49
+ }
50
+ if (typeof document.workItemId !== 'string' || !/^WI-\d+$/.test(document.workItemId)) {
51
+ return { active: true, allowed: false, reason: 'session work item is invalid' };
52
+ }
53
+ if (typeof document.expiresAt !== 'string' || Number.isNaN(Date.parse(document.expiresAt))) {
54
+ return { active: true, allowed: false, reason: 'session expiry is invalid' };
55
+ }
56
+ if ((this.options.now?.() ?? new Date()).getTime() >= Date.parse(document.expiresAt)) {
57
+ return {
58
+ active: true,
59
+ allowed: false,
60
+ reason: 'session expired',
61
+ workItemId: document.workItemId,
62
+ unit: document.unit,
63
+ expiresAt: document.expiresAt,
64
+ };
65
+ }
66
+ if (!Array.isArray(document.allowedCategories) || document.allowedCategories.some((value) => typeof value !== 'string')) {
67
+ return { active: true, allowed: false, reason: 'session allowedCategories is invalid' };
68
+ }
69
+ if (input.dominantCategory === undefined || !document.allowedCategories.includes(input.dominantCategory)) {
70
+ return {
71
+ active: true,
72
+ allowed: false,
73
+ reason: `category ${input.dominantCategory ?? '<unknown>'} is not allowed by session`,
74
+ workItemId: document.workItemId,
75
+ unit: document.unit,
76
+ expiresAt: document.expiresAt,
77
+ };
78
+ }
79
+ if (input.unitId === undefined || input.unitId !== document.unit) {
80
+ return {
81
+ active: true,
82
+ allowed: false,
83
+ reason: `target unit ${input.unitId ?? '<unknown>'} does not match session unit ${document.unit}`,
84
+ workItemId: document.workItemId,
85
+ unit: document.unit,
86
+ expiresAt: document.expiresAt,
87
+ };
88
+ }
89
+ if (!this.allTargetPathsBelongToUnit(input.targetFilePaths, document.unit)) {
90
+ return {
91
+ active: true,
92
+ allowed: false,
93
+ reason: `one or more target paths are outside session unit ${document.unit}`,
94
+ workItemId: document.workItemId,
95
+ unit: document.unit,
96
+ expiresAt: document.expiresAt,
97
+ };
98
+ }
99
+
100
+ return {
101
+ active: true,
102
+ allowed: true,
103
+ workItemId: document.workItemId,
104
+ unit: document.unit,
105
+ expiresAt: document.expiresAt,
106
+ };
107
+ }
108
+
109
+ private allTargetPathsBelongToUnit(targetFilePaths: readonly string[], unit: string): boolean {
110
+ const projectPaths = this.options.configQueryPort.getProjectPaths();
111
+ for (const targetFilePath of targetFilePaths) {
112
+ const scope = WriteTargetScope.fromPath(targetFilePath, projectPaths);
113
+ if (scope?.unitId !== undefined && scope.unitId !== unit) {
114
+ return false;
115
+ }
116
+ }
117
+ return true;
118
+ }
119
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-202 / WI-204
5
+ * @work-item-id WI-206
4
6
  *
5
7
  * PreToolUse Hook Adapter
6
8
  * Claude Code の PreToolUse Hook エントリポイント
@@ -15,6 +17,7 @@ import { FileSystemStoryReflectionQueryAdapter } from '../infrastructure/adapter
15
17
  import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
16
18
  import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
17
19
  import { HarnessErrorGuidanceAdapter } from '../infrastructure/adapters/harness-error-guidance-adapter.js';
20
+ import { FileSystemFullModeSessionQueryAdapter } from '../infrastructure/adapters/file-system-full-mode-session-query-adapter.js';
18
21
  import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
19
22
  import * as path from 'node:path';
20
23
  import * as fs from 'node:fs/promises';
@@ -152,6 +155,10 @@ async function main(): Promise<void> {
152
155
  const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
153
156
  rootDir: path.dirname(configPath),
154
157
  });
158
+ const fullModeSessionQueryPort = new FileSystemFullModeSessionQueryAdapter({
159
+ rootDir: path.dirname(configPath),
160
+ configQueryPort,
161
+ });
155
162
  const useCase = new HandlePreToolUseUseCase({
156
163
  configQueryPort,
157
164
  phaseGateQueryPort,
@@ -159,6 +166,7 @@ async function main(): Promise<void> {
159
166
  fullModeRequirementQueryPort,
160
167
  baselineGrandfatherQueryPort,
161
168
  errorGuidanceQueryPort,
169
+ fullModeSessionQueryPort,
162
170
  });
163
171
 
164
172
  const callerSkill = input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL;
@@ -183,6 +191,12 @@ async function main(): Promise<void> {
183
191
  const suffix = cat !== undefined && cat !== '' ? `, category=${cat}` : '';
184
192
  process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
185
193
  }
194
+ if (output.fullModeSessionAllowed !== undefined) {
195
+ const session = output.fullModeSessionAllowed;
196
+ const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : '';
197
+ const unit = session.unit !== undefined ? `, unit=${session.unit}` : '';
198
+ process.stderr.write(`phasegate: write allowed (Full Mode session${workItem}${unit})\n`);
199
+ }
186
200
 
187
201
  process.exit(0);
188
202
  } catch (error) {
@@ -14,6 +14,9 @@
14
14
  * @work-item-id WI-197
15
15
  * @work-item-id WI-200
16
16
  * @work-item-id WI-201
17
+ * @work-item-id WI-202 / WI-204
18
+ * @work-item-id WI-205
19
+ * @work-item-id WI-206
17
20
  *
18
21
  * Phasegate CLI エントリポイント。
19
22
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -27,6 +30,7 @@ import {
27
30
  readdir as fsReaddir,
28
31
  readlink as fsReadlink,
29
32
  rename as fsRename,
33
+ rm as fsRm,
30
34
  writeFile as fsWriteFile,
31
35
  } from "node:fs/promises";
32
36
  import { dirname, join, resolve } from "node:path";
@@ -180,6 +184,7 @@ Commands:
180
184
  migrate Migrate phasegate.config.json (--schema v3, --config <path>)
181
185
  migrate work-items Migrate legacy inception work item directories (--dry-run|--apply)
182
186
  work-items:status Report or apply derived WI frontmatter status (--dry-run|--apply, --id, --fail-on-stale, --json)
187
+ session begin|end Manage hook-visible Full Mode sessions
183
188
 
184
189
  render-errors Render harness errors (--format human|agent|ci)
185
190
  validate-fix Validate fix examples (--code <code>)
@@ -223,6 +228,8 @@ Gate semantics:
223
228
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
224
229
  baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
225
230
  scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
231
+ session begin Start a Full Mode session (--mode full, --unit, --work-item, --reason, --duration)
232
+ session end End a Full Mode session (--work-item)
226
233
 
227
234
  skill:execute-tdd-cycle Execute TDD cycle (--unit, --story, --desc, --phase RED|GREEN|REFACTOR, --passed)
228
235
  skill:check-coverage Check coverage (--story <storyId>, --json)
@@ -476,6 +483,109 @@ function validateKnownFlags(args: readonly string[], known: readonly string[]):
476
483
  return null;
477
484
  }
478
485
 
486
+ const FULL_MODE_SESSION_ALLOWED_CATEGORIES = Object.freeze([
487
+ "domain",
488
+ "application",
489
+ "infrastructure",
490
+ "presentation",
491
+ "config",
492
+ ]);
493
+
494
+ interface FullModeSessionFile {
495
+ readonly mode: "full";
496
+ readonly unit: string;
497
+ readonly workItemId: string;
498
+ readonly allowedCategories: readonly string[];
499
+ readonly reason: string;
500
+ readonly startedAt: string;
501
+ readonly expiresAt: string;
502
+ }
503
+
504
+ function parseSessionDurationMs(raw: string | undefined): number {
505
+ const value = raw ?? "1h";
506
+ const match = value.match(/^(\d+)(s|m|h)$/);
507
+ if (match === null) {
508
+ throw new Error("Invalid --duration. Use values like 30m, 1h, or 3600s.");
509
+ }
510
+ const amount = Number(match[1]);
511
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
512
+ throw new Error("Invalid --duration. Duration must be a positive integer.");
513
+ }
514
+ const unit = match[2];
515
+ const multiplier = unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
516
+ return amount * multiplier;
517
+ }
518
+
519
+ function validateWorkItemId(value: string | undefined): string {
520
+ if (value === undefined || !/^WI-\d+$/.test(value)) {
521
+ throw new Error("--work-item must be a WI id such as WI-206.");
522
+ }
523
+ return value;
524
+ }
525
+
526
+ function validateSessionUnit(value: string | undefined): string {
527
+ if (value === undefined || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value)) {
528
+ throw new Error("--unit must be a non-empty unit id.");
529
+ }
530
+ return value;
531
+ }
532
+
533
+ async function readFullModeSession(sessionPath: string): Promise<FullModeSessionFile | null> {
534
+ try {
535
+ return JSON.parse(await fsReadFile(sessionPath, "utf8")) as FullModeSessionFile;
536
+ } catch {
537
+ return null;
538
+ }
539
+ }
540
+
541
+ async function beginFullModeSession(rootDir: string, args: readonly string[]): Promise<FullModeSessionFile> {
542
+ if ((parseFlag(args, "--mode") ?? "full") !== "full") {
543
+ throw new Error("--mode must be full.");
544
+ }
545
+ const unit = validateSessionUnit(parseFlag(args, "--unit"));
546
+ const workItemId = validateWorkItemId(parseFlag(args, "--work-item"));
547
+ const reason = parseFlag(args, "--reason");
548
+ if (reason === undefined || reason.trim() === "") {
549
+ throw new Error("--reason is required.");
550
+ }
551
+ const startedAt = new Date();
552
+ const expiresAt = new Date(startedAt.getTime() + parseSessionDurationMs(parseFlag(args, "--duration")));
553
+ const session: FullModeSessionFile = {
554
+ mode: "full",
555
+ unit,
556
+ workItemId,
557
+ allowedCategories: FULL_MODE_SESSION_ALLOWED_CATEGORIES,
558
+ reason,
559
+ startedAt: startedAt.toISOString(),
560
+ expiresAt: expiresAt.toISOString(),
561
+ };
562
+ const phasegateDir = join(rootDir, ".phasegate");
563
+ await fsMkdir(phasegateDir, { recursive: true });
564
+ await fsWriteFile(join(phasegateDir, "session.json"), `${JSON.stringify(session, null, 2)}\n`, "utf8");
565
+ return session;
566
+ }
567
+
568
+ async function endFullModeSession(
569
+ rootDir: string,
570
+ args: readonly string[],
571
+ ): Promise<{ removed: boolean; session: FullModeSessionFile | null }> {
572
+ const sessionPath = join(rootDir, ".phasegate", "session.json");
573
+ const session = await readFullModeSession(sessionPath);
574
+ const requestedWorkItemId = parseFlag(args, "--work-item");
575
+ if (requestedWorkItemId !== undefined && session?.workItemId !== requestedWorkItemId) {
576
+ throw new Error(`Active session work item is ${session?.workItemId ?? "<none>"}, not ${requestedWorkItemId}.`);
577
+ }
578
+ try {
579
+ await fsRm(sessionPath);
580
+ return { removed: true, session };
581
+ } catch (error) {
582
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
583
+ return { removed: false, session: null };
584
+ }
585
+ throw error;
586
+ }
587
+ }
588
+
479
589
  const SUBCOMMAND_HELP: Record<string, string> = {
480
590
  init: `Usage: phasegate init [options]
481
591
 
@@ -663,6 +773,22 @@ Options:
663
773
  --force Overwrite an existing target when applying.
664
774
  --json Output machine-readable JSON.
665
775
  --help, -h Show this help`,
776
+ session: `Usage: phasegate session <begin|end> [options]
777
+
778
+ Manage hook-visible Full Mode sessions.
779
+
780
+ Begin options:
781
+ --mode full Required session mode.
782
+ --unit <id> Unit id allowed by the session.
783
+ --work-item <WI-XXX> Work item authorizing the session.
784
+ --reason <text> Human-readable reason for audit.
785
+ --duration <ttl> TTL such as 30m, 1h, or 3600s. Default: 1h.
786
+ --json Output machine-readable JSON.
787
+
788
+ End options:
789
+ --work-item <WI-XXX> Optional safety check before removing the session.
790
+ --json Output machine-readable JSON.
791
+ --help, -h Show this help`,
666
792
  "ci:generate-template": `Usage: phasegate ci:generate-template [options]
667
793
 
668
794
  Generates a CI template configuration.
@@ -1002,7 +1128,7 @@ function buildSetupCompleteness(input: {
1002
1128
  ];
1003
1129
 
1004
1130
  const externalActions: string[] = [];
1005
- if (includeCodex) externalActions.push("Run codex features enable codex_hooks if Codex hooks are not enabled for the user.");
1131
+ if (includeCodex) externalActions.push("Run codex features enable hooks if Codex hooks are not enabled for the user.");
1006
1132
  if (input.withCi) externalActions.push("Trigger or inspect the first GitHub Actions PhaseGate workflow run.");
1007
1133
  if (input.intent === "strict") externalActions.push("Confirm team policy accepts strict local and CI enforcement.");
1008
1134
  entries.push({
@@ -1108,7 +1234,7 @@ function buildAgentReadiness(input: {
1108
1234
  "setup:agent will create or refresh the AGENTS.md PhaseGate managed section.",
1109
1235
  "setup:agent will deploy bundled skills for Codex.",
1110
1236
  ],
1111
- nextAction: "Run setup:agent --agent codex --apply, then enable codex_hooks at user level if needed.",
1237
+ nextAction: "Run setup:agent --agent codex --apply, then enable hooks at user level if needed.",
1112
1238
  risk: "Codex user-level hook feature enablement remains a manual external check.",
1113
1239
  }),
1114
1240
  setupReadinessEntry({
@@ -1338,8 +1464,8 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
1338
1464
  "codex-hooks": {
1339
1465
  targets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
1340
1466
  managedTargets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
1341
- externalActions: [{ id: "codex-hooks-feature", label: "Enable Codex user-level hooks feature.", command: "codex features enable codex_hooks", blocking: true }],
1342
- commands: ["phasegate install --agent codex --apply", "codex features enable codex_hooks"],
1467
+ externalActions: [{ id: "codex-hooks-feature", label: "Enable Codex user-level hooks feature.", command: "codex features enable hooks", blocking: true }],
1468
+ commands: ["phasegate install --agent codex --apply", "codex features enable hooks"],
1343
1469
  validations: ["phasegate doctor --json", "phasegate phasegate:status --json"],
1344
1470
  risks: ["Codex apply_patch writes still require the pre-commit backstop for full coverage."],
1345
1471
  },
@@ -1830,7 +1956,7 @@ async function main(): Promise<void> {
1830
1956
  console.log(" 3. Edit .claude/scripts/hook-config.json to set target directories");
1831
1957
  }
1832
1958
  if (deployCodex) {
1833
- console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable codex_hooks`);
1959
+ console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable hooks`);
1834
1960
  console.log(
1835
1961
  ` ${deployClaude ? "5" : "4"}. (Recommended) Install pre-commit backstop: rerun with --with-husky or set up husky manually`,
1836
1962
  );
@@ -2081,6 +2207,61 @@ async function main(): Promise<void> {
2081
2207
  break;
2082
2208
  }
2083
2209
 
2210
+ case "session": {
2211
+ const subcommand = args[1];
2212
+ const knownFlags = ["--mode", "--unit", "--work-item", "--reason", "--duration", "--json"];
2213
+ const flagError = validateKnownFlags(args.slice(2), knownFlags);
2214
+ if (flagError) {
2215
+ console.error(flagError);
2216
+ process.exit(2);
2217
+ }
2218
+ if (subcommand === "begin") {
2219
+ try {
2220
+ const session = await beginFullModeSession(rootDir, args);
2221
+ if (json) {
2222
+ console.log(JSON.stringify({ ok: true, sessionPath: ".phasegate/session.json", session }, null, 2));
2223
+ } else {
2224
+ console.log(
2225
+ `Full Mode session started: ${session.workItemId} unit=${session.unit} expiresAt=${session.expiresAt}`,
2226
+ );
2227
+ }
2228
+ process.exit(0);
2229
+ } catch (error) {
2230
+ const message = error instanceof Error ? error.message : String(error);
2231
+ if (json) {
2232
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
2233
+ } else {
2234
+ console.error(`Error: ${message}`);
2235
+ }
2236
+ process.exit(2);
2237
+ }
2238
+ }
2239
+ if (subcommand === "end") {
2240
+ try {
2241
+ const result = await endFullModeSession(rootDir, args);
2242
+ if (json) {
2243
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
2244
+ } else if (result.removed) {
2245
+ console.log(`Full Mode session ended: ${result.session?.workItemId ?? "<unknown>"}`);
2246
+ } else {
2247
+ console.log("No Full Mode session was active.");
2248
+ }
2249
+ process.exit(0);
2250
+ } catch (error) {
2251
+ const message = error instanceof Error ? error.message : String(error);
2252
+ if (json) {
2253
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
2254
+ } else {
2255
+ console.error(`Error: ${message}`);
2256
+ }
2257
+ process.exit(2);
2258
+ }
2259
+ }
2260
+ console.error("Error: session subcommand must be begin or end.");
2261
+ process.exit(2);
2262
+ break;
2263
+ }
2264
+
2084
2265
  case "uninstall": {
2085
2266
  const KNOWN_UNINSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json"];
2086
2267
  const flagError = validateKnownFlags(args, KNOWN_UNINSTALL_FLAGS);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit quick-mode
4
+ * @work-item-id WI-204
4
5
  *
5
6
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
6
7
  */
@@ -3,6 +3,7 @@
3
3
  // @work-item-id WI-086 / WI-087
4
4
  // @work-item-id WI-127
5
5
  // @work-item-id WI-184
6
+ // @work-item-id WI-202
6
7
  // Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
7
8
 
8
9
  import { promises as fs } from "node:fs";
@@ -131,7 +131,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
131
131
  - `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
132
132
  - manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
133
133
  - reconcile / uninstall が refuse → user modified managed target として扱い、`--force` のリスクと backup path を説明して承認を取る
134
- - Codex の `codex_hooks` feature flag は user-level setting。project-local `install` では変更されないため、必要なら `codex features enable codex_hooks` を案内
134
+ - Codex の `hooks` feature flag は user-level setting。project-local `install` では変更されないため、必要なら `codex features enable hooks` を案内。`codex_hooks` は legacy alias として扱う。<!-- @work-item-id WI-205 -->
135
135
  - Codex native `apply_patch` bypass は hook で完全捕捉できない。`.husky/pre-commit` の `phasegate pre-commit` が backstop になるため、Husky 配線を診断対象に含める
136
136
  - 初回 setup / retrofit / CI-only / agent-hooks の判断が曖昧な場合は `npx phasegate setup:agent --dry-run --json` を先に使い、検出済み状態、質問、変更案、rollback、validation を根拠として提案する。<!-- @work-item-id WI-172 -->
137
137
  - Claude Code setup 相談では `npx phasegate setup:agent --agent claude --dry-run --json` を先に使い、`plan.agentReadiness` の `claude` / `shared` が `configured` なら setup 修復ではなく WI 起票、inception 計画、product reflection、validation の順で作業開始を案内する。`setup:agent --apply --json` または `install --apply --json` の structured `error` がある場合は、`target`, `operation`, `code`, `likelyCause`, `recovery`, `partialChanges` を読み、`.claude` が file として存在する path conflict、sandbox/permission denial、PhaseGate managed target hash mismatch を区別して説明する。<!-- @work-item-id WI-177 -->
@@ -263,6 +263,23 @@ TDD実装の順序・スコープ・不明点を整理し、人間の承認を
263
263
  ### 開始条件
264
264
  - 人間がPhase 1の計画を承認した
265
265
  - QAセクションの全[Question]に[Answer]が記入されている(QAがある場合)
266
+ - Quick Mode の許可カテゴリ外(domain/application/infrastructure/presentation/config)を変更する場合、実装前に hook-visible Full Mode session を開始している
267
+
268
+ ### Full Mode session
269
+
270
+ Quick Mode の許可カテゴリ外を変更する Phase 2 では、手作業で `quickMode.allowedCategories` を広げない。以下の CLI で対象 Unit / WI に限定した一時 session を作成し、実装完了時に終了する。
271
+
272
+ ```bash
273
+ phasegate session begin --mode full --unit <unit> --work-item <WI-XXX> --reason "story-implementor Phase 2" --duration 1h
274
+ ```
275
+
276
+ 実装完了または中断時:
277
+
278
+ ```bash
279
+ phasegate session end --work-item <WI-XXX>
280
+ ```
281
+
282
+ PreToolUse hook は `.phasegate/session.json` の TTL・unit・カテゴリを検証する。スキル名だけでは hook 通過の根拠にならない。
266
283
 
267
284
  ### ワークフロー
268
285