phasegate 0.160.10 → 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 +12 -0
- package/README.md +3 -3
- package/docs/guide/cli-reference.md +2 -1
- package/docs/guide/codex-integration.md +4 -4
- package/docs/guide/configuration.md +2 -1
- package/docs/guide/installation.md +1 -1
- package/docs/guide/quick-vs-full-mode.md +9 -0
- package/docs/guide/recipes.md +3 -2
- package/docs/guide/setup-artifacts.md +1 -1
- package/docs/guide/troubleshooting.md +1 -1
- package/docs/templates/agent-context/AGENTS.md.template.md +1 -1
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +82 -4
- package/scripts/harness/agent-integration/domain/ports/full-mode-session-query-port.ts +22 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +119 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +33 -6
- package/scripts/harness/main.ts +203 -10
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +11 -9
- package/scripts/harness/setup/skill-deployer.ts +2 -1
- package/skills/phasegate-config-doctor/SKILL.md +10 -3
- package/skills/story-implementor/SKILL.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
|
|
16
|
+
## [0.160.11] - 2026-05-17
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **WI-202 / WI-204 — Quick Mode strict recovery** — aligns strict init Quick Mode categories with quick-implementor scope, adds `config:plan --intent quick-mode-relax` for managed `allowedCategories` recovery, updates config edit block guidance to the matching recovery intent, and excludes CWD-external runtime paths from project-local hook policy.
|
|
21
|
+
|
|
10
22
|
## [0.160.10] - 2026-05-16
|
|
11
23
|
|
|
12
24
|
### 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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -28,7 +28,8 @@ Command names in this document are split into three surfaces:
|
|
|
28
28
|
| `uninstall --dry-run` / `--apply` | Remove PhaseGate-managed files and managed blocks using `.phasegate/manifest.json`; `--force` handles managed conflict cases. |
|
|
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
|
-
| `config:plan` | Agent-readable configuration change planner. Options: `--intent <l4-strict\|codex-hooks\|ci-fail-on-warning\|baseline-reset\|quick-mode-strict\|retrofit-bootstrap\|planning-mode-relax>`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-173 --> <!-- @work-item-id WI-201 --> |
|
|
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
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
|
|
@@ -13,9 +13,10 @@ For agent-assisted changes, preview the intent before editing the config:
|
|
|
13
13
|
```bash
|
|
14
14
|
npx phasegate config:plan --intent l4-strict --dry-run --json
|
|
15
15
|
npx phasegate config:plan --intent quick-mode-strict --dry-run --json
|
|
16
|
+
npx phasegate config:plan --intent quick-mode-relax --dry-run --json
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
The plan identifies target fields, managed artifacts, commands, validation, risks, and rollback so an agent can explain the change before applying it. <!-- @work-item-id WI-173 -->
|
|
19
|
+
The plan identifies target fields, managed artifacts, commands, validation, risks, and rollback so an agent can explain the change before applying it. Use `quick-mode-relax` to restore the supported Quick Mode category set after an over-narrowed strict configuration. <!-- @work-item-id WI-173 --> <!-- @work-item-id WI-204 -->
|
|
19
20
|
|
|
20
21
|
### Full Reference
|
|
21
22
|
|
|
@@ -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
|
|
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:
|
package/docs/guide/recipes.md
CHANGED
|
@@ -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
|
|
32
|
+
codex features enable hooks
|
|
33
33
|
npx phasegate doctor --json
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -58,8 +58,9 @@ Only enable fail-on-warning after reviewing the drift, consistency, dead-code, p
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
npx phasegate config:plan --intent quick-mode-strict --dry-run --json
|
|
61
|
+
npx phasegate config:plan --intent quick-mode-relax --dry-run --json
|
|
61
62
|
npx phasegate check-change-category --paths <changed-files> --format json
|
|
62
63
|
npx phasegate ci-check --quick --dry-run
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
Use `config:plan` before changing `phasegate.config.json`. It separates repo-managed artifacts from user-level settings, previews the local config before/after for config-backed intents, and lists the checks needed after the change. <!-- @work-item-id WI-175 -->
|
|
66
|
+
Use `config:plan` before changing `phasegate.config.json`. It separates repo-managed artifacts from user-level settings, previews the local config before/after for config-backed intents, and lists the checks needed after the change. `quick-mode-relax` is the managed recovery path when strict category narrowing blocks docs/test/config changes. <!-- @work-item-id WI-175 --> <!-- @work-item-id WI-204 -->
|
|
@@ -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 `
|
|
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
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
### Codex Notes
|
|
34
34
|
|
|
35
|
-
- Enable native hook execution with `codex features enable
|
|
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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export interface HandlePreToolUseInput {
|
|
7
7
|
toolName: string;
|
|
8
8
|
targetFilePaths: string[];
|
|
9
|
+
callerSkill?: string;
|
|
9
10
|
targetChanges?: {
|
|
10
11
|
filePath: string;
|
|
11
12
|
beforeContent?: string | null;
|
|
@@ -29,4 +30,9 @@ export interface HandlePreToolUseOutput {
|
|
|
29
30
|
quickModeAllowed?: {
|
|
30
31
|
dominantCategory?: string;
|
|
31
32
|
};
|
|
33
|
+
fullModeSessionAllowed?: {
|
|
34
|
+
workItemId?: string;
|
|
35
|
+
unit?: string;
|
|
36
|
+
expiresAt?: string;
|
|
37
|
+
};
|
|
32
38
|
}
|
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -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,17 +158,33 @@ 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,
|
|
161
185
|
guidance,
|
|
162
186
|
unitIdForGuidance,
|
|
187
|
+
input.callerSkill,
|
|
163
188
|
);
|
|
164
189
|
}
|
|
165
190
|
} else {
|
|
@@ -226,6 +251,25 @@ export class HandlePreToolUseUseCase {
|
|
|
226
251
|
}
|
|
227
252
|
}
|
|
228
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
|
+
|
|
229
273
|
private static buildFullModeRequiredBlockOutput(
|
|
230
274
|
blockedFilePath: string | undefined,
|
|
231
275
|
result: {
|
|
@@ -236,11 +280,12 @@ export class HandlePreToolUseUseCase {
|
|
|
236
280
|
},
|
|
237
281
|
guidance: ErrorGuidance | null,
|
|
238
282
|
unitId: string | undefined,
|
|
283
|
+
callerSkill?: string,
|
|
239
284
|
): HandlePreToolUseOutput {
|
|
240
285
|
const fp = blockedFilePath ?? "不明なファイル";
|
|
241
286
|
if (result.dominantCategory === "config" && /(?:^|\/)phasegate\.config\.json$/.test(fp)) {
|
|
242
|
-
const dryRunCommand = "phasegate config:plan --intent
|
|
243
|
-
const applyCommand = "phasegate config:plan --intent
|
|
287
|
+
const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
|
|
288
|
+
const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
|
|
244
289
|
const lines = [
|
|
245
290
|
`Full mode 必須変更が検出されました: ${fp}`,
|
|
246
291
|
"カテゴリ: config",
|
|
@@ -263,6 +308,33 @@ export class HandlePreToolUseUseCase {
|
|
|
263
308
|
nextAction: `${dryRunCommand} && ${applyCommand}`,
|
|
264
309
|
};
|
|
265
310
|
}
|
|
311
|
+
if (
|
|
312
|
+
callerSkill === "quick-implementor" &&
|
|
313
|
+
result.dominantCategory !== undefined &&
|
|
314
|
+
["bugfix", "docs", "test", "config"].includes(result.dominantCategory)
|
|
315
|
+
) {
|
|
316
|
+
const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
|
|
317
|
+
const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
|
|
318
|
+
const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
|
|
319
|
+
lines.push(`カテゴリ: ${result.dominantCategory}`);
|
|
320
|
+
if (result.rejectionRule) {
|
|
321
|
+
lines.push(`判定ルール: ${result.rejectionRule}`);
|
|
322
|
+
}
|
|
323
|
+
if (result.rejectionReason) {
|
|
324
|
+
lines.push(`理由: ${result.rejectionReason}`);
|
|
325
|
+
}
|
|
326
|
+
lines.push(`次のアクション: Quick Mode の許可カテゴリを確認してください。緩和する場合は ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
shouldBlock: true,
|
|
330
|
+
blockedFilePath,
|
|
331
|
+
blockReason: "FULL_MODE_REQUIRED",
|
|
332
|
+
error: { message: lines.join("\n") },
|
|
333
|
+
fullModeRejectionRule: result.rejectionRule,
|
|
334
|
+
fullModeDominantCategory: result.dominantCategory,
|
|
335
|
+
nextAction: `${dryRunCommand} && ${applyCommand}`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
266
338
|
const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
|
|
267
339
|
if (result.dominantCategory) {
|
|
268
340
|
lines.push(`カテゴリ: ${result.dominantCategory}`);
|
|
@@ -275,6 +347,12 @@ export class HandlePreToolUseUseCase {
|
|
|
275
347
|
}
|
|
276
348
|
const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
|
|
277
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
|
+
}
|
|
278
356
|
HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance, unitId);
|
|
279
357
|
|
|
280
358
|
return {
|
|
@@ -402,7 +480,7 @@ export class HandlePreToolUseUseCase {
|
|
|
402
480
|
{
|
|
403
481
|
pattern: /(?:^|\/)phasegate\.config\.json$/,
|
|
404
482
|
message: (fp) =>
|
|
405
|
-
`保護ファイルへの書き込みがブロックされました: ${fp}\
|
|
483
|
+
`保護ファイルへの書き込みがブロックされました: ${fp}\nQuick Mode 設定の復旧は CLI 経由で計画・適用してください: phasegate config:plan --intent quick-mode-relax --dry-run --json / phasegate config:plan --intent quick-mode-relax --apply --json`,
|
|
406
484
|
},
|
|
407
485
|
{
|
|
408
486
|
pattern: /(?:^|\/)harness\.config\.json$/,
|
|
@@ -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';
|
|
@@ -22,6 +25,7 @@ import * as fs from 'node:fs/promises';
|
|
|
22
25
|
interface PreToolUseHookInput {
|
|
23
26
|
cwd?: string;
|
|
24
27
|
tool_name?: string;
|
|
28
|
+
caller_skill?: string;
|
|
25
29
|
tool_input?: {
|
|
26
30
|
path?: string;
|
|
27
31
|
file_path?: string;
|
|
@@ -50,8 +54,8 @@ async function readStdin(): Promise<string> {
|
|
|
50
54
|
return Buffer.concat(chunks).toString('utf8');
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
async function findConfigPath(): Promise<string> {
|
|
54
|
-
let dir =
|
|
57
|
+
async function findConfigPath(startDir: string): Promise<string> {
|
|
58
|
+
let dir = startDir;
|
|
55
59
|
while (true) {
|
|
56
60
|
const candidate = path.join(dir, 'phasegate.config.json');
|
|
57
61
|
try {
|
|
@@ -63,7 +67,11 @@ async function findConfigPath(): Promise<string> {
|
|
|
63
67
|
dir = parent;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
|
-
return path.join(
|
|
70
|
+
return path.join(startDir, 'phasegate.config.json');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isProjectExternalAbsolutePath(filePath: string): boolean {
|
|
74
|
+
return path.isAbsolute(filePath);
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
async function main(): Promise<void> {
|
|
@@ -89,7 +97,7 @@ async function main(): Promise<void> {
|
|
|
89
97
|
process.exit(2);
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
const cwd = input.cwd ?? process.cwd();
|
|
100
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
93
101
|
const toRelative = (p: string): string => {
|
|
94
102
|
if (path.isAbsolute(p)) {
|
|
95
103
|
const rel = path.relative(cwd, p);
|
|
@@ -128,7 +136,9 @@ async function main(): Promise<void> {
|
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
try {
|
|
131
|
-
const configPath = await findConfigPath();
|
|
139
|
+
const configPath = await findConfigPath(cwd);
|
|
140
|
+
const projectTargetFilePaths = targetFilePaths.filter((filePath) => !isProjectExternalAbsolutePath(filePath));
|
|
141
|
+
const projectTargetChanges = targetChanges.filter((change) => !isProjectExternalAbsolutePath(change.filePath));
|
|
132
142
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
133
143
|
const phaseGateQueryPort = new PhaseGateQueryAdapter();
|
|
134
144
|
const storyReflectionQueryPort = new FileSystemStoryReflectionQueryAdapter({
|
|
@@ -145,6 +155,10 @@ async function main(): Promise<void> {
|
|
|
145
155
|
const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
|
|
146
156
|
rootDir: path.dirname(configPath),
|
|
147
157
|
});
|
|
158
|
+
const fullModeSessionQueryPort = new FileSystemFullModeSessionQueryAdapter({
|
|
159
|
+
rootDir: path.dirname(configPath),
|
|
160
|
+
configQueryPort,
|
|
161
|
+
});
|
|
148
162
|
const useCase = new HandlePreToolUseUseCase({
|
|
149
163
|
configQueryPort,
|
|
150
164
|
phaseGateQueryPort,
|
|
@@ -152,9 +166,16 @@ async function main(): Promise<void> {
|
|
|
152
166
|
fullModeRequirementQueryPort,
|
|
153
167
|
baselineGrandfatherQueryPort,
|
|
154
168
|
errorGuidanceQueryPort,
|
|
169
|
+
fullModeSessionQueryPort,
|
|
155
170
|
});
|
|
156
171
|
|
|
157
|
-
const
|
|
172
|
+
const callerSkill = input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL;
|
|
173
|
+
const output = await useCase.execute({
|
|
174
|
+
toolName: effectiveToolName,
|
|
175
|
+
targetFilePaths: projectTargetFilePaths,
|
|
176
|
+
callerSkill,
|
|
177
|
+
targetChanges: projectTargetChanges,
|
|
178
|
+
});
|
|
158
179
|
|
|
159
180
|
if (output.shouldBlock) {
|
|
160
181
|
const msg = output.error?.message
|
|
@@ -170,6 +191,12 @@ async function main(): Promise<void> {
|
|
|
170
191
|
const suffix = cat !== undefined && cat !== '' ? `, category=${cat}` : '';
|
|
171
192
|
process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
|
|
172
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
|
+
}
|
|
173
200
|
|
|
174
201
|
process.exit(0);
|
|
175
202
|
} catch (error) {
|
package/scripts/harness/main.ts
CHANGED
|
@@ -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>)
|
|
@@ -219,10 +224,12 @@ Gate semantics:
|
|
|
219
224
|
refresh-claude-md Refresh CLAUDE.md standard sections (--dry-run, --apply, --json)
|
|
220
225
|
p2:check-agent-context Check AGENTS.md / CLAUDE.md freshness (--threshold-days <n>, --json)
|
|
221
226
|
setup:agent Plan agent-driven setup (--intent <minimal|recommended|strict|ci-only|agent-hooks|retrofit>, --agent <claude|codex|both>, --dry-run|--apply, --json)
|
|
222
|
-
config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict|retrofit-bootstrap|planning-mode-relax>, --dry-run|--apply, --json)
|
|
227
|
+
config:plan Plan safe config changes (--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)
|
|
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
|
|
|
@@ -536,7 +646,7 @@ Options:
|
|
|
536
646
|
Produce an agent-readable configuration change plan.
|
|
537
647
|
|
|
538
648
|
Intents:
|
|
539
|
-
l4-strict, codex-hooks, ci-fail-on-warning, baseline-reset, quick-mode-strict, retrofit-bootstrap, planning-mode-relax
|
|
649
|
+
l4-strict, codex-hooks, ci-fail-on-warning, baseline-reset, quick-mode-strict, quick-mode-relax, retrofit-bootstrap, planning-mode-relax
|
|
540
650
|
|
|
541
651
|
Options:
|
|
542
652
|
--dry-run
|
|
@@ -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.
|
|
@@ -779,7 +905,7 @@ function parseCoverageThreshold(raw: string | undefined): number {
|
|
|
779
905
|
type InitPhasePreset = "full" | "standard" | "minimal" | "custom";
|
|
780
906
|
type AgentTarget = "claude" | "codex" | "both";
|
|
781
907
|
type SetupIntent = "minimal" | "recommended" | "strict" | "ci-only" | "agent-hooks" | "retrofit";
|
|
782
|
-
type ConfigChangeIntent = "l4-strict" | "codex-hooks" | "ci-fail-on-warning" | "baseline-reset" | "quick-mode-strict" | "retrofit-bootstrap" | "planning-mode-relax";
|
|
908
|
+
type ConfigChangeIntent = "l4-strict" | "codex-hooks" | "ci-fail-on-warning" | "baseline-reset" | "quick-mode-strict" | "quick-mode-relax" | "retrofit-bootstrap" | "planning-mode-relax";
|
|
783
909
|
type SetupCompletenessStatus = "configured" | "planned" | "manual" | "not-applicable" | "unknown";
|
|
784
910
|
|
|
785
911
|
interface SetupCompletenessEntry {
|
|
@@ -864,6 +990,7 @@ function parseConfigChangeIntent(value: string | undefined): ConfigChangeIntent
|
|
|
864
990
|
value === "ci-fail-on-warning" ||
|
|
865
991
|
value === "baseline-reset" ||
|
|
866
992
|
value === "quick-mode-strict" ||
|
|
993
|
+
value === "quick-mode-relax" ||
|
|
867
994
|
value === "retrofit-bootstrap" ||
|
|
868
995
|
value === "planning-mode-relax"
|
|
869
996
|
) {
|
|
@@ -1001,7 +1128,7 @@ function buildSetupCompleteness(input: {
|
|
|
1001
1128
|
];
|
|
1002
1129
|
|
|
1003
1130
|
const externalActions: string[] = [];
|
|
1004
|
-
if (includeCodex) externalActions.push("Run codex features enable
|
|
1131
|
+
if (includeCodex) externalActions.push("Run codex features enable hooks if Codex hooks are not enabled for the user.");
|
|
1005
1132
|
if (input.withCi) externalActions.push("Trigger or inspect the first GitHub Actions PhaseGate workflow run.");
|
|
1006
1133
|
if (input.intent === "strict") externalActions.push("Confirm team policy accepts strict local and CI enforcement.");
|
|
1007
1134
|
entries.push({
|
|
@@ -1107,7 +1234,7 @@ function buildAgentReadiness(input: {
|
|
|
1107
1234
|
"setup:agent will create or refresh the AGENTS.md PhaseGate managed section.",
|
|
1108
1235
|
"setup:agent will deploy bundled skills for Codex.",
|
|
1109
1236
|
],
|
|
1110
|
-
nextAction: "Run setup:agent --agent codex --apply, then enable
|
|
1237
|
+
nextAction: "Run setup:agent --agent codex --apply, then enable hooks at user level if needed.",
|
|
1111
1238
|
risk: "Codex user-level hook feature enablement remains a manual external check.",
|
|
1112
1239
|
}),
|
|
1113
1240
|
setupReadinessEntry({
|
|
@@ -1229,9 +1356,12 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
|
|
|
1229
1356
|
{ pointer: "/layers/L4/failOnWarning", path: ["layers", "L4", "failOnWarning"], value: true },
|
|
1230
1357
|
],
|
|
1231
1358
|
"quick-mode-strict": [
|
|
1232
|
-
{ pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["
|
|
1359
|
+
{ pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["bugfix"] },
|
|
1233
1360
|
{ pointer: "/quickMode/relaxedGates", path: ["quickMode", "relaxedGates"], value: [] },
|
|
1234
1361
|
],
|
|
1362
|
+
"quick-mode-relax": [
|
|
1363
|
+
{ pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["bugfix", "docs", "test", "config"] },
|
|
1364
|
+
],
|
|
1235
1365
|
"codex-hooks": [],
|
|
1236
1366
|
"baseline-reset": [],
|
|
1237
1367
|
"retrofit-bootstrap": [
|
|
@@ -1334,8 +1464,8 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1334
1464
|
"codex-hooks": {
|
|
1335
1465
|
targets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
|
|
1336
1466
|
managedTargets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
|
|
1337
|
-
externalActions: [{ id: "codex-hooks-feature", label: "Enable Codex user-level hooks feature.", command: "codex features enable
|
|
1338
|
-
commands: ["phasegate install --agent codex --apply", "codex features enable
|
|
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"],
|
|
1339
1469
|
validations: ["phasegate doctor --json", "phasegate phasegate:status --json"],
|
|
1340
1470
|
risks: ["Codex apply_patch writes still require the pre-commit backstop for full coverage."],
|
|
1341
1471
|
},
|
|
@@ -1363,6 +1493,14 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1363
1493
|
validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
|
|
1364
1494
|
risks: ["More changes will require Full Mode validation before commit."],
|
|
1365
1495
|
},
|
|
1496
|
+
"quick-mode-relax": {
|
|
1497
|
+
targets: ["phasegate.config.json: quickMode.allowedCategories"],
|
|
1498
|
+
managedTargets: ["phasegate.config.json"],
|
|
1499
|
+
externalActions: [],
|
|
1500
|
+
commands: ["phasegate config:plan --intent quick-mode-relax --json", "phasegate config:plan --intent quick-mode-relax --apply --json", "phasegate check-change-category --paths <changed-files> --format json"],
|
|
1501
|
+
validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
|
|
1502
|
+
risks: ["Small bugfix/docs/test/config changes can proceed through Quick Mode again; protected files remain governed by hook and managed command policies."],
|
|
1503
|
+
},
|
|
1366
1504
|
"retrofit-bootstrap": {
|
|
1367
1505
|
targets: ["phasegate.config.json: planningMode.default", "phasegate.config.json: phaseDependencies.override", "phasegate.config.json: quickMode.relaxedGates"],
|
|
1368
1506
|
managedTargets: ["phasegate.config.json"],
|
|
@@ -1722,7 +1860,7 @@ async function main(): Promise<void> {
|
|
|
1722
1860
|
if (configResult.created) {
|
|
1723
1861
|
console.log(`✓ phasegate.config.json created`);
|
|
1724
1862
|
if (workflow === "strict") {
|
|
1725
|
-
console.log(`✓ strict workflow configured (quickMode.relaxedGates: [], allowedCategories: ["
|
|
1863
|
+
console.log(`✓ strict workflow configured (quickMode.relaxedGates: [], allowedCategories: ["bugfix","docs","test","config"])`);
|
|
1726
1864
|
}
|
|
1727
1865
|
} else {
|
|
1728
1866
|
console.log(` phasegate.config.json already exists, skipped`);
|
|
@@ -1818,7 +1956,7 @@ async function main(): Promise<void> {
|
|
|
1818
1956
|
console.log(" 3. Edit .claude/scripts/hook-config.json to set target directories");
|
|
1819
1957
|
}
|
|
1820
1958
|
if (deployCodex) {
|
|
1821
|
-
console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable
|
|
1959
|
+
console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable hooks`);
|
|
1822
1960
|
console.log(
|
|
1823
1961
|
` ${deployClaude ? "5" : "4"}. (Recommended) Install pre-commit backstop: rerun with --with-husky or set up husky manually`,
|
|
1824
1962
|
);
|
|
@@ -2069,6 +2207,61 @@ async function main(): Promise<void> {
|
|
|
2069
2207
|
break;
|
|
2070
2208
|
}
|
|
2071
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
|
+
|
|
2072
2265
|
case "uninstall": {
|
|
2073
2266
|
const KNOWN_UNINSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json"];
|
|
2074
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
|
*/
|
|
@@ -26,6 +27,16 @@ const RISK_PRIORITY: Record<string, number> = {
|
|
|
26
27
|
function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
27
28
|
const { filePath, changeKind } = file;
|
|
28
29
|
|
|
30
|
+
// Config files must stay config even when Edit payload snippets look like
|
|
31
|
+
// comment/whitespace-only diffs. Config recovery guidance depends on this.
|
|
32
|
+
if (
|
|
33
|
+
filePath.endsWith('.config.json') ||
|
|
34
|
+
filePath.endsWith('.config.ts') ||
|
|
35
|
+
filePath.endsWith('phasegate.config.json')
|
|
36
|
+
) {
|
|
37
|
+
return ChangeCategory.fromString('config');
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
if (isCommentOnlyDiff(file)) {
|
|
30
41
|
return ChangeCategory.fromString('docs');
|
|
31
42
|
}
|
|
@@ -54,15 +65,6 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
54
65
|
return ChangeCategory.fromString('docs');
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
// config: *.config.json / *.config.ts / phasegate.config.json
|
|
58
|
-
if (
|
|
59
|
-
filePath.endsWith('.config.json') ||
|
|
60
|
-
filePath.endsWith('.config.ts') ||
|
|
61
|
-
filePath.endsWith('phasegate.config.json')
|
|
62
|
-
) {
|
|
63
|
-
return ChangeCategory.fromString('config');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
68
|
// feature: domain/ 以外の CREATE
|
|
67
69
|
if (changeKind === 'CREATE') {
|
|
68
70
|
return ChangeCategory.fromString('feature');
|
|
@@ -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";
|
|
@@ -481,7 +482,7 @@ export async function initHarnessConfig(
|
|
|
481
482
|
layers: {},
|
|
482
483
|
quickMode: strictWorkflow
|
|
483
484
|
? {
|
|
484
|
-
allowedCategories: ["
|
|
485
|
+
allowedCategories: ["bugfix", "docs", "test", "config"],
|
|
485
486
|
relaxedGates: [],
|
|
486
487
|
}
|
|
487
488
|
: {},
|
|
@@ -13,7 +13,7 @@ phasegate を導入した直後の config は単純な default で、実プロ
|
|
|
13
13
|
|
|
14
14
|
## 設計原則
|
|
15
15
|
|
|
16
|
-
1. **silent 書き換え禁止** — 提案は diff として提示し、`AskUserQuestion`
|
|
16
|
+
1. **silent 書き換え禁止** — 提案は diff として提示し、`AskUserQuestion` で承認を取ってから適用する。`config:plan` で表現できる変更は managed command を優先し、直接 `Edit` は hook が許可する範囲に限定する
|
|
17
17
|
2. **検出結果を優先** — 機械的に決定可能な部分 (workspace 構造、formatter、bash 互換性) は AI 推論ではなく検出結果を採用
|
|
18
18
|
3. **AI 推論は判断要素のみ** — architecture preset 選定、relaxedGates 推奨値などは AI が判断するが根拠を必ず示す
|
|
19
19
|
4. **schema は enum 違反確認時のみ Read** — 日常診断は本 SKILL 内の判定基準で十分。schema 全文 Read は値域不明時に限定する
|
|
@@ -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 の `
|
|
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 -->
|
|
@@ -200,7 +200,14 @@ options:
|
|
|
200
200
|
- label: "適用しない (情報のみ受け取る)"
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
ユーザーが適用対象を確定したら、対応する managed intent がある場合は `config:plan` で適用する。
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx phasegate config:plan --intent quick-mode-relax --dry-run --json
|
|
207
|
+
npx phasegate config:plan --intent quick-mode-relax --apply --json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
managed intent がない個別提案のみ、hook が `config` category を許可していることを確認したうえで `Edit` で `phasegate.config.json` を変更する。
|
|
204
211
|
|
|
205
212
|
**変更後の検証**:
|
|
206
213
|
|
|
@@ -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
|
|