phasegate 0.160.11 → 0.160.13
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 +6 -0
- package/README.md +12 -3
- package/docs/guide/cli-reference.md +1 -0
- package/docs/guide/codex-integration.md +4 -4
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/installation.md +10 -1
- package/docs/guide/quick-vs-full-mode.md +9 -0
- package/docs/guide/recipes.md +10 -1
- package/docs/guide/setup-artifacts.md +2 -1
- package/docs/guide/troubleshooting.md +1 -1
- package/docs/templates/agent-context/AGENTS.md.template.md +1 -1
- package/docs/templates/personal/git-info-exclude +6 -0
- package/docs/templates/personal/phasegate-local-config.json +7 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +50 -1
- 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 +14 -0
- package/scripts/harness/installation/application/usecases/run-install.ts +50 -3
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +11 -1
- package/scripts/harness/installation/presentation/cli/install-handler.ts +3 -1
- package/scripts/harness/main.ts +193 -9
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +1 -0
- package/scripts/harness/setup/skill-deployer.ts +1 -0
- package/skills/README.md +2 -0
- package/skills/phasegate-config-doctor/SKILL.md +4 -1
- package/skills/phasegate-toolkit-guide/SKILL.md +2 -0
- package/skills/story-implementor/SKILL.md +17 -0
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
|
@@ -102,6 +102,15 @@ npx phasegate doctor
|
|
|
102
102
|
|
|
103
103
|
`install` merges PhaseGate into the current project without discarding existing Claude/Codex hooks or Husky scripts. It reports planned changes before writing, adds package scripts and the `phasegate` devDependency, creates agent skill links, writes `AGENTS.md` / `CLAUDE.md` PhaseGate managed sections for the selected agent targets, writes the CI workflow when missing, and records managed files in `.phasegate/manifest.json`. If an existing file needs a forced managed update, run `npx phasegate install --apply --force`; PhaseGate backs up replaced files under `.phasegate/backups/`. <!-- @work-item-id WI-174 -->
|
|
104
104
|
|
|
105
|
+
For personal evaluation inside a team-owned repository, use local-only install:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx phasegate install --personal --dry-run
|
|
109
|
+
npx phasegate install --personal --apply
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`--personal` does not plan or write `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, or `.gitignore`. It creates `.phasegate-local/config.json`, adds a managed local-only block to `.git/info/exclude`, records personal artifacts in `.phasegate/manifest.json`, and reports Codex user-level hook setup as a manual action instead of writing `.codex/hooks.json`. <!-- @work-item-id WI-207 -->
|
|
113
|
+
|
|
105
114
|
For agent-driven setup planning, use:
|
|
106
115
|
|
|
107
116
|
```bash
|
|
@@ -135,7 +144,7 @@ npx phasegate reconcile --apply
|
|
|
135
144
|
|
|
136
145
|
```bash
|
|
137
146
|
npx phasegate init --name my-project --agent codex --with-husky
|
|
138
|
-
codex features enable
|
|
147
|
+
codex features enable hooks
|
|
139
148
|
```
|
|
140
149
|
|
|
141
150
|
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 +491,12 @@ Phasegate also integrates with [OpenAI Codex CLI](https://developers.openai.com/
|
|
|
482
491
|
npx phasegate init --name my-project --agent codex --with-husky
|
|
483
492
|
|
|
484
493
|
# 2. Enable the Codex CLI feature flag manually on your machine
|
|
485
|
-
codex features enable
|
|
494
|
+
codex features enable hooks
|
|
486
495
|
```
|
|
487
496
|
|
|
488
497
|
For dual-agent projects (Claude + Codex), use `--agent both`.
|
|
489
498
|
|
|
490
|
-
`init` sets up files inside the project. The Codex CLI user-level
|
|
499
|
+
`init` sets up files inside the project. The Codex CLI user-level hooks feature remains an explicit manual step.
|
|
491
500
|
|
|
492
501
|
### Coverage and known limitation
|
|
493
502
|
|
|
@@ -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
|
|
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
|
|
|
@@ -12,6 +12,7 @@ Start with one question: what kind of repository are you setting up?
|
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| New project with agent hooks | `npx phasegate init --name <project> --agent both --with-husky --with-ci` | `phasegate.config.json`, skills, hooks, agent context files, Husky, and CI targets exist |
|
|
14
14
|
| Existing repository | `npx phasegate install --dry-run` then `npx phasegate install --apply` | Existing hooks/scripts are preserved and `.phasegate/manifest.json` records managed targets |
|
|
15
|
+
| Personal evaluation in a team repository | `npx phasegate install --personal --dry-run` then `npx phasegate install --personal --apply` | Team-owned files stay untouched; `.phasegate-local/config.json` and `.git/info/exclude` local-only entries are recorded |
|
|
15
16
|
| CI-only rollout | `npx phasegate setup:agent --intent ci-only --with-ci --dry-run --json` | The plan explains CI changes without requiring local hooks |
|
|
16
17
|
| Codex-only project | `npx phasegate init --agent codex --with-husky` | `.codex/hooks.json`, `AGENTS.md`, `.codex/skills`, and pre-commit backstop are present |
|
|
17
18
|
| Strict validation rollout | `npx phasegate setup:agent --intent strict --dry-run --json` | The plan lists strict checks, L4 risk, rollback, and validation commands |
|
|
@@ -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
|
|
@@ -58,6 +58,15 @@ npx phasegate doctor
|
|
|
58
58
|
|
|
59
59
|
`install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, creates `.claude/skills` and `.codex/skills` links, writes `CLAUDE.md` / `AGENTS.md` managed sections for selected agent targets, writes `.github/workflows/phasegate-aidlc-gate.yml` when CI is enabled, and records managed entries in `.phasegate/manifest.json`. See [Setup Artifacts](setup-artifacts.md) for the full managed target, generated artifact, runtime state, legacy artifact, and user-level setting inventory. <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 --> <!-- @work-item-id WI-174 -->
|
|
60
60
|
|
|
61
|
+
For personal evaluation inside a team-owned repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx phasegate install --personal --dry-run
|
|
65
|
+
npx phasegate install --personal --apply
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Personal install keeps team-owned files out of both the plan and apply path: `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, and `.gitignore` are not touched. PhaseGate writes only local artifacts: `.phasegate-local/config.json`, `.phasegate/manifest.json`, and a managed block in `.git/info/exclude`. Codex user-level hook setup is reported as a manual action instead of writing project-local `.codex/hooks.json`. <!-- @work-item-id WI-207 -->
|
|
69
|
+
|
|
61
70
|
For agent-readable planning before writing files:
|
|
62
71
|
|
|
63
72
|
```bash
|
|
@@ -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
|
@@ -24,12 +24,21 @@ npx phasegate baseline --dry-run
|
|
|
24
24
|
|
|
25
25
|
Use this when a repository already has meaningful history or custom automation. Treat any refused target as an agent-assisted review task instead of forcing it blindly.
|
|
26
26
|
|
|
27
|
+
## Personal Evaluation Recipe
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx phasegate install --personal --dry-run
|
|
31
|
+
npx phasegate install --personal --apply
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use this inside a team-owned repository when you want local PhaseGate artifacts without changing `package.json`, agent context files, Husky hooks, CI workflows, or team `.gitignore`. <!-- @work-item-id WI-207 -->
|
|
35
|
+
|
|
27
36
|
## Agent Hooks Recipe
|
|
28
37
|
|
|
29
38
|
```bash
|
|
30
39
|
npx phasegate setup:agent --intent agent-hooks --agent both --with-husky --dry-run --json
|
|
31
40
|
npx phasegate install --agent both --apply
|
|
32
|
-
codex features enable
|
|
41
|
+
codex features enable hooks
|
|
33
42
|
npx phasegate doctor --json
|
|
34
43
|
```
|
|
35
44
|
|
|
@@ -14,8 +14,9 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
14
14
|
| Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init`; `install` may merge scripts/devDependency into `package.json` |
|
|
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
|
+
| Personal install artifact | `.phasegate-local/config.json`, `.git/info/exclude` PhaseGate block | One developer on one machine | Created by `phasegate install --personal`; team `.gitignore` and team-owned files are not modified |
|
|
17
18
|
| 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 `
|
|
19
|
+
| 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
20
|
|
|
20
21
|
## Managed Targets
|
|
21
22
|
|
|
@@ -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
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,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) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// @work-item-id WI-177
|
|
8
8
|
// @work-item-id WI-182
|
|
9
9
|
// @work-item-id WI-183
|
|
10
|
+
// @work-item-id WI-207
|
|
10
11
|
|
|
11
12
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink } from "node:fs/promises";
|
|
12
13
|
import { dirname, join } from "node:path";
|
|
@@ -18,7 +19,7 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
18
19
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
19
20
|
|
|
20
21
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
21
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed";
|
|
22
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy";
|
|
22
23
|
|
|
23
24
|
export interface InstallPlanItem {
|
|
24
25
|
readonly path: string;
|
|
@@ -45,6 +46,7 @@ export interface RunInstallInput {
|
|
|
45
46
|
readonly skillSet?: "core" | "all";
|
|
46
47
|
readonly workflow?: "standard" | "strict";
|
|
47
48
|
readonly agent?: "claude" | "codex" | "both";
|
|
49
|
+
readonly personal?: boolean;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface RunInstallResult {
|
|
@@ -79,6 +81,8 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
79
81
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
80
82
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
81
83
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
84
|
+
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
85
|
+
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
82
86
|
|
|
83
87
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
88
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -152,6 +156,14 @@ function mergeShell(existing: string | null, incoming: string): string {
|
|
|
152
156
|
return `${existing.replace(/\s*$/, "\n\n")}${block}\n`;
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
function mergeTextManaged(existing: string | null, incoming: string): string {
|
|
160
|
+
const block = `${TEXT_BEGIN}\n${incoming.trim()}\n${TEXT_END}`;
|
|
161
|
+
if (existing === null || existing.trim().length === 0) return `${block}\n`;
|
|
162
|
+
const pattern = new RegExp(`${escapeRegExp(TEXT_BEGIN)}[\\s\\S]*?${escapeRegExp(TEXT_END)}`);
|
|
163
|
+
if (pattern.test(existing)) return existing.replace(pattern, block).replace(/\s*$/, "\n");
|
|
164
|
+
return `${existing.replace(/\s*$/, "\n\n")}${block}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
function managedMarkdownBlock(content: string): string {
|
|
156
168
|
const start = content.indexOf(MARKDOWN_BEGIN);
|
|
157
169
|
const end = content.indexOf(MARKDOWN_END);
|
|
@@ -282,7 +294,9 @@ export class RunInstallUseCase {
|
|
|
282
294
|
const skillSet = input.skillSet ?? "all";
|
|
283
295
|
const workflow = input.workflow ?? "standard";
|
|
284
296
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
285
|
-
const targets =
|
|
297
|
+
const targets = input.personal
|
|
298
|
+
? this.createPersonalTargets()
|
|
299
|
+
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
286
300
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
287
301
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
288
302
|
let manifest = baseManifest;
|
|
@@ -363,7 +377,7 @@ export class RunInstallUseCase {
|
|
|
363
377
|
}
|
|
364
378
|
}
|
|
365
379
|
|
|
366
|
-
const linkPaths = [
|
|
380
|
+
const linkPaths = input.personal ? [] : [
|
|
367
381
|
...(includeClaude ? [".claude/skills"] : []),
|
|
368
382
|
...(includeCodex ? [".codex/skills"] : []),
|
|
369
383
|
];
|
|
@@ -400,6 +414,19 @@ export class RunInstallUseCase {
|
|
|
400
414
|
}
|
|
401
415
|
}
|
|
402
416
|
|
|
417
|
+
if (input.personal && includeCodex) {
|
|
418
|
+
plan.push({
|
|
419
|
+
path: "~/.codex/hooks.json",
|
|
420
|
+
action: "will-skip",
|
|
421
|
+
repairMode: "manual",
|
|
422
|
+
strategy: "json",
|
|
423
|
+
changed: false,
|
|
424
|
+
summary: "~/.codex/hooks.json: personal mode does not write user-level Codex settings; configure hooks manually",
|
|
425
|
+
diff: "manual user-level Codex hook setup required",
|
|
426
|
+
skillHint: null,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
403
430
|
if (input.apply && changed.length > 0) {
|
|
404
431
|
try {
|
|
405
432
|
await this.manifestRepository.save(input.projectRoot, manifest);
|
|
@@ -498,8 +525,26 @@ export class RunInstallUseCase {
|
|
|
498
525
|
];
|
|
499
526
|
}
|
|
500
527
|
|
|
528
|
+
private createPersonalTargets(): readonly InstallTarget[] {
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
path: ".phasegate-local/config.json",
|
|
532
|
+
strategy: "copy" as const,
|
|
533
|
+
templatePath: "docs/templates/personal/phasegate-local-config.json",
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
path: ".git/info/exclude",
|
|
537
|
+
strategy: "text-managed" as const,
|
|
538
|
+
templatePath: "docs/templates/personal/git-info-exclude",
|
|
539
|
+
block: { start: TEXT_BEGIN, end: TEXT_END, content: "phasegate personal install exclude block" },
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
|
|
501
544
|
private repairMode(target: InstallTarget, before: string | null): RepairMode {
|
|
502
545
|
if (target.strategy === "shell") return shellRepairMode(before);
|
|
546
|
+
if (target.strategy === "text-managed") return "mechanical";
|
|
547
|
+
if (target.strategy === "copy") return "mechanical";
|
|
503
548
|
if (target.strategy === "json") return jsonRepairMode(before);
|
|
504
549
|
if (target.strategy === "markdown-managed") return "mechanical";
|
|
505
550
|
return "mechanical";
|
|
@@ -513,7 +558,9 @@ export class RunInstallUseCase {
|
|
|
513
558
|
|
|
514
559
|
private merge(target: InstallTarget, before: string | null, template: string, version: string): string {
|
|
515
560
|
if (target.strategy === "yaml-add") return before ?? template;
|
|
561
|
+
if (target.strategy === "copy") return before ?? template;
|
|
516
562
|
if (target.strategy === "shell") return mergeShell(before, template);
|
|
563
|
+
if (target.strategy === "text-managed") return mergeTextManaged(before, template);
|
|
517
564
|
if (target.strategy === "markdown-managed") return mergeManagedMarkdown(before, template);
|
|
518
565
|
if (target.strategy === "package-json") {
|
|
519
566
|
const existing = before === null ? {} : (JSON.parse(before) as unknown);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @work-item-id WI-147
|
|
4
4
|
// @work-item-id WI-174
|
|
5
5
|
// @work-item-id WI-199
|
|
6
|
+
// @work-item-id WI-207
|
|
6
7
|
|
|
7
8
|
import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
|
|
8
9
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -12,7 +13,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
|
12
13
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
13
14
|
|
|
14
15
|
type UninstallAction = "missing-manifest" | "delete" | "unlink" | "reverse-merge" | "skip" | "refuse";
|
|
15
|
-
type StrategyType = "created" | "json" | "shell" | "package-json" | "markdown-managed" | "symlink" | "yaml-add" | "unknown";
|
|
16
|
+
type StrategyType = "created" | "json" | "shell" | "package-json" | "markdown-managed" | "symlink" | "yaml-add" | "text-managed" | "unknown";
|
|
16
17
|
|
|
17
18
|
export interface UninstallPlanItem {
|
|
18
19
|
readonly path: string;
|
|
@@ -47,6 +48,8 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
47
48
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
48
49
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
49
50
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
51
|
+
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
52
|
+
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
50
53
|
const PHASEGATE_SCRIPT_PREFIX = "phasegate:";
|
|
51
54
|
const PROTECTED_UNINSTALL_PATHS = new Set(["package.json", "package-lock.json"]);
|
|
52
55
|
|
|
@@ -119,6 +122,11 @@ export function reverseManagedMarkdown(currentContent: string): string {
|
|
|
119
122
|
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
export function reverseTextManaged(currentContent: string): string {
|
|
126
|
+
const pattern = new RegExp(`\\n?${escapeRegExp(TEXT_BEGIN)}[\\s\\S]*?${escapeRegExp(TEXT_END)}\\n?`);
|
|
127
|
+
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
export function reversePackageJsonMerge(currentContent: string): string {
|
|
123
131
|
const parsed = JSON.parse(currentContent) as unknown;
|
|
124
132
|
const result = isRecord(parsed) ? { ...parsed } : {};
|
|
@@ -312,6 +320,7 @@ export class RunUninstallUseCase {
|
|
|
312
320
|
private async reverseMerged(harnessRoot: string, path: string, currentContent: string, strategy: StrategyType): Promise<string> {
|
|
313
321
|
if (strategy === "shell") return currentContent.includes(SHELL_BEGIN) ? reverseShellMerge(currentContent) : currentContent;
|
|
314
322
|
if (strategy === "markdown-managed") return currentContent.includes(MARKDOWN_BEGIN) ? reverseManagedMarkdown(currentContent) : currentContent;
|
|
323
|
+
if (strategy === "text-managed") return currentContent.includes(TEXT_BEGIN) ? reverseTextManaged(currentContent) : currentContent;
|
|
315
324
|
if (strategy === "package-json") return reversePackageJsonMerge(currentContent);
|
|
316
325
|
if (strategy === "json") return reverseJsonMerge(currentContent, await readFile(join(harnessRoot, this.templateFor(path)), "utf8"));
|
|
317
326
|
throw new Error(`Unsupported merged strategy: ${strategy}`);
|
|
@@ -322,6 +331,7 @@ export class RunUninstallUseCase {
|
|
|
322
331
|
if (mode === "created") return path.endsWith(".yml") || path.endsWith(".yaml") ? "yaml-add" : "created";
|
|
323
332
|
if (path === "package.json") return "package-json";
|
|
324
333
|
if (path === "AGENTS.md" || path === "CLAUDE.md") return "markdown-managed";
|
|
334
|
+
if (path === ".git/info/exclude") return "text-managed";
|
|
325
335
|
if (path.endsWith(".json")) return "json";
|
|
326
336
|
if (path.startsWith(".husky/")) return "shell";
|
|
327
337
|
return "unknown";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer presentation
|
|
3
3
|
// @work-item-id WI-146
|
|
4
4
|
// @work-item-id WI-175
|
|
5
|
+
// @work-item-id WI-207
|
|
5
6
|
|
|
6
7
|
import type { RunInstallUseCase } from "../../application/usecases/run-install.js";
|
|
7
8
|
|
|
@@ -20,6 +21,7 @@ export interface InstallHandlerInput {
|
|
|
20
21
|
readonly skillSet?: "core" | "all";
|
|
21
22
|
readonly workflow?: "standard" | "strict";
|
|
22
23
|
readonly agent?: "claude" | "codex" | "both";
|
|
24
|
+
readonly personal?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface InstallHandlerResult {
|
|
@@ -39,7 +41,7 @@ export class InstallHandler {
|
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
const lines = [
|
|
42
|
-
input.apply ?
|
|
44
|
+
input.apply ? `phasegate install${input.personal ? " --personal" : ""} apply` : `phasegate install${input.personal ? " --personal" : ""} dry-run`,
|
|
43
45
|
...result.plan.map((item) => {
|
|
44
46
|
const hint = item.skillHint ? `; hint: ${item.skillHint}` : "";
|
|
45
47
|
return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}`;
|
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";
|
|
@@ -167,7 +171,7 @@ Setup:
|
|
|
167
171
|
scaffold-wi <unit|_cross> <story|issue|chore>
|
|
168
172
|
Create docs/inception/{unit}/WI-XXX/description.md
|
|
169
173
|
emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
|
|
170
|
-
install Install phasegate managed files (--dry-run|--apply, --force)
|
|
174
|
+
install Install phasegate managed files (--dry-run|--apply, --force, --personal)
|
|
171
175
|
uninstall Uninstall phasegate managed files (--dry-run|--apply, --force)
|
|
172
176
|
reconcile Reconcile phasegate managed files (--dry-run|--apply, --force)
|
|
173
177
|
setup:agent Diagnose repo setup and produce/apply an agent-readable setup plan
|
|
@@ -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
|
|
|
@@ -515,6 +625,7 @@ Options:
|
|
|
515
625
|
--workflow <standard|strict> Rendered agent context workflow mode (default: standard)
|
|
516
626
|
--with-husky Include Husky hook targets
|
|
517
627
|
--with-ci Include GitHub Actions target
|
|
628
|
+
--personal Use local-only install: no package.json, agent docs, Husky, CI, or .gitignore writes
|
|
518
629
|
--json Output machine-readable JSON
|
|
519
630
|
--help, -h Show this help`,
|
|
520
631
|
"setup:agent": `Usage: phasegate setup:agent [options]
|
|
@@ -663,6 +774,22 @@ Options:
|
|
|
663
774
|
--force Overwrite an existing target when applying.
|
|
664
775
|
--json Output machine-readable JSON.
|
|
665
776
|
--help, -h Show this help`,
|
|
777
|
+
session: `Usage: phasegate session <begin|end> [options]
|
|
778
|
+
|
|
779
|
+
Manage hook-visible Full Mode sessions.
|
|
780
|
+
|
|
781
|
+
Begin options:
|
|
782
|
+
--mode full Required session mode.
|
|
783
|
+
--unit <id> Unit id allowed by the session.
|
|
784
|
+
--work-item <WI-XXX> Work item authorizing the session.
|
|
785
|
+
--reason <text> Human-readable reason for audit.
|
|
786
|
+
--duration <ttl> TTL such as 30m, 1h, or 3600s. Default: 1h.
|
|
787
|
+
--json Output machine-readable JSON.
|
|
788
|
+
|
|
789
|
+
End options:
|
|
790
|
+
--work-item <WI-XXX> Optional safety check before removing the session.
|
|
791
|
+
--json Output machine-readable JSON.
|
|
792
|
+
--help, -h Show this help`,
|
|
666
793
|
"ci:generate-template": `Usage: phasegate ci:generate-template [options]
|
|
667
794
|
|
|
668
795
|
Generates a CI template configuration.
|
|
@@ -1002,7 +1129,7 @@ function buildSetupCompleteness(input: {
|
|
|
1002
1129
|
];
|
|
1003
1130
|
|
|
1004
1131
|
const externalActions: string[] = [];
|
|
1005
|
-
if (includeCodex) externalActions.push("Run codex features enable
|
|
1132
|
+
if (includeCodex) externalActions.push("Run codex features enable hooks if Codex hooks are not enabled for the user.");
|
|
1006
1133
|
if (input.withCi) externalActions.push("Trigger or inspect the first GitHub Actions PhaseGate workflow run.");
|
|
1007
1134
|
if (input.intent === "strict") externalActions.push("Confirm team policy accepts strict local and CI enforcement.");
|
|
1008
1135
|
entries.push({
|
|
@@ -1108,7 +1235,7 @@ function buildAgentReadiness(input: {
|
|
|
1108
1235
|
"setup:agent will create or refresh the AGENTS.md PhaseGate managed section.",
|
|
1109
1236
|
"setup:agent will deploy bundled skills for Codex.",
|
|
1110
1237
|
],
|
|
1111
|
-
nextAction: "Run setup:agent --agent codex --apply, then enable
|
|
1238
|
+
nextAction: "Run setup:agent --agent codex --apply, then enable hooks at user level if needed.",
|
|
1112
1239
|
risk: "Codex user-level hook feature enablement remains a manual external check.",
|
|
1113
1240
|
}),
|
|
1114
1241
|
setupReadinessEntry({
|
|
@@ -1338,8 +1465,8 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1338
1465
|
"codex-hooks": {
|
|
1339
1466
|
targets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
|
|
1340
1467
|
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
|
|
1342
|
-
commands: ["phasegate install --agent codex --apply", "codex features enable
|
|
1468
|
+
externalActions: [{ id: "codex-hooks-feature", label: "Enable Codex user-level hooks feature.", command: "codex features enable hooks", blocking: true }],
|
|
1469
|
+
commands: ["phasegate install --agent codex --apply", "codex features enable hooks"],
|
|
1343
1470
|
validations: ["phasegate doctor --json", "phasegate phasegate:status --json"],
|
|
1344
1471
|
risks: ["Codex apply_patch writes still require the pre-commit backstop for full coverage."],
|
|
1345
1472
|
},
|
|
@@ -1830,7 +1957,7 @@ async function main(): Promise<void> {
|
|
|
1830
1957
|
console.log(" 3. Edit .claude/scripts/hook-config.json to set target directories");
|
|
1831
1958
|
}
|
|
1832
1959
|
if (deployCodex) {
|
|
1833
|
-
console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable
|
|
1960
|
+
console.log(` ${deployClaude ? "4" : "3"}. Enable Codex hooks: codex features enable hooks`);
|
|
1834
1961
|
console.log(
|
|
1835
1962
|
` ${deployClaude ? "5" : "4"}. (Recommended) Install pre-commit backstop: rerun with --with-husky or set up husky manually`,
|
|
1836
1963
|
);
|
|
@@ -1919,7 +2046,7 @@ async function main(): Promise<void> {
|
|
|
1919
2046
|
}
|
|
1920
2047
|
|
|
1921
2048
|
case "install": {
|
|
1922
|
-
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json", "--agent", "--skills", "--workflow", "--with-husky", "--with-ci"];
|
|
2049
|
+
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json", "--agent", "--skills", "--workflow", "--with-husky", "--with-ci", "--personal"];
|
|
1923
2050
|
const flagError = validateKnownFlags(args, KNOWN_INSTALL_FLAGS);
|
|
1924
2051
|
if (flagError) {
|
|
1925
2052
|
console.error(flagError);
|
|
@@ -1940,6 +2067,7 @@ async function main(): Promise<void> {
|
|
|
1940
2067
|
}
|
|
1941
2068
|
const includeClaude = agent === "claude" || agent === "both";
|
|
1942
2069
|
const includeCodex = agent === "codex" || agent === "both";
|
|
2070
|
+
const personal = hasFlag(args, "--personal");
|
|
1943
2071
|
const mod = createInstallationModule();
|
|
1944
2072
|
const phasegateVersion = await getHarnessVersion(harnessRoot);
|
|
1945
2073
|
const result = await mod.installHandler.execute({
|
|
@@ -1951,11 +2079,12 @@ async function main(): Promise<void> {
|
|
|
1951
2079
|
force: hasFlag(args, "--force"),
|
|
1952
2080
|
includeClaude,
|
|
1953
2081
|
includeCodex,
|
|
1954
|
-
includeHusky: true,
|
|
1955
|
-
includeCi: true,
|
|
2082
|
+
includeHusky: personal ? false : true,
|
|
2083
|
+
includeCi: personal ? false : true,
|
|
1956
2084
|
skillSet: skillSetRaw,
|
|
1957
2085
|
workflow: parseWorkflowMode(workflowRaw),
|
|
1958
2086
|
agent,
|
|
2087
|
+
personal,
|
|
1959
2088
|
json,
|
|
1960
2089
|
});
|
|
1961
2090
|
console.log(result.stdout);
|
|
@@ -2081,6 +2210,61 @@ async function main(): Promise<void> {
|
|
|
2081
2210
|
break;
|
|
2082
2211
|
}
|
|
2083
2212
|
|
|
2213
|
+
case "session": {
|
|
2214
|
+
const subcommand = args[1];
|
|
2215
|
+
const knownFlags = ["--mode", "--unit", "--work-item", "--reason", "--duration", "--json"];
|
|
2216
|
+
const flagError = validateKnownFlags(args.slice(2), knownFlags);
|
|
2217
|
+
if (flagError) {
|
|
2218
|
+
console.error(flagError);
|
|
2219
|
+
process.exit(2);
|
|
2220
|
+
}
|
|
2221
|
+
if (subcommand === "begin") {
|
|
2222
|
+
try {
|
|
2223
|
+
const session = await beginFullModeSession(rootDir, args);
|
|
2224
|
+
if (json) {
|
|
2225
|
+
console.log(JSON.stringify({ ok: true, sessionPath: ".phasegate/session.json", session }, null, 2));
|
|
2226
|
+
} else {
|
|
2227
|
+
console.log(
|
|
2228
|
+
`Full Mode session started: ${session.workItemId} unit=${session.unit} expiresAt=${session.expiresAt}`,
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
process.exit(0);
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2234
|
+
if (json) {
|
|
2235
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
2236
|
+
} else {
|
|
2237
|
+
console.error(`Error: ${message}`);
|
|
2238
|
+
}
|
|
2239
|
+
process.exit(2);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (subcommand === "end") {
|
|
2243
|
+
try {
|
|
2244
|
+
const result = await endFullModeSession(rootDir, args);
|
|
2245
|
+
if (json) {
|
|
2246
|
+
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
2247
|
+
} else if (result.removed) {
|
|
2248
|
+
console.log(`Full Mode session ended: ${result.session?.workItemId ?? "<unknown>"}`);
|
|
2249
|
+
} else {
|
|
2250
|
+
console.log("No Full Mode session was active.");
|
|
2251
|
+
}
|
|
2252
|
+
process.exit(0);
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2255
|
+
if (json) {
|
|
2256
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
2257
|
+
} else {
|
|
2258
|
+
console.error(`Error: ${message}`);
|
|
2259
|
+
}
|
|
2260
|
+
process.exit(2);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
console.error("Error: session subcommand must be begin or end.");
|
|
2264
|
+
process.exit(2);
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2084
2268
|
case "uninstall": {
|
|
2085
2269
|
const KNOWN_UNINSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json"];
|
|
2086
2270
|
const flagError = validateKnownFlags(args, KNOWN_UNINSTALL_FLAGS);
|
package/skills/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
`.agent/skills` は旧 setup 由来の互換パスです。新規導入では管理対象にしません。<!-- @work-item-id WI-157 -->
|
|
18
18
|
|
|
19
|
+
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは `.claude/skills` / `.codex/skills` のリンクを作成せず、`.phasegate-local/config.json` と `.git/info/exclude` の local-only managed block だけを扱います。通常の共有導入が必要になった時点で `phasegate install --dry-run` / `phasegate install --apply` を使います。<!-- @work-item-id WI-207 -->
|
|
20
|
+
|
|
19
21
|
## 新しいスキルの追加
|
|
20
22
|
|
|
21
23
|
新しいスキルを追加する場合は、この `skills` ディレクトリに直接追加してください。あわせて `docs/guide/skills-overview.md`、README の skill 数、必要なら `skills/phasegate-toolkit-guide/SKILL.md` の参照先を更新します。シンボリックリンクを通じて、対応エージェントから利用可能になります。<!-- @work-item-id WI-154 -->
|
|
@@ -35,6 +35,8 @@ phasegate を導入した直後の config は単純な default で、実プロ
|
|
|
35
35
|
| hook config | `.claude/scripts/hook-config.json` | 既存 hook 設定確認 |
|
|
36
36
|
| doctor report | 明示された report path、またはユーザーが指定した `.phasegate/last-doctor-report.json` | `repairMode` / `repairHint` / `suggestedSkill` の確認 |
|
|
37
37
|
| manifest | `.phasegate/manifest.json` | install / reconcile / uninstall の managed target と hash 状態確認 |
|
|
38
|
+
| personal config | `.phasegate-local/config.json` | `install --personal` の local-only 設定確認 |
|
|
39
|
+
| git local exclude | `.git/info/exclude` | `install --personal` の repository-local ignore block 確認 |
|
|
38
40
|
| Claude hooks | `.claude/settings.json` | managed hook JSON と user customization の確認 |
|
|
39
41
|
| Codex hooks | `.codex/hooks.json` | managed hook JSON と Codex hook 配線確認 |
|
|
40
42
|
| Husky scripts | `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push` | pre-commit backstop と bypass audit の確認 |
|
|
@@ -127,11 +129,12 @@ product-architect で Unit を作り、いくつかの logical_design を書い
|
|
|
127
129
|
#### 観点 9: setup lifecycle と doctor finding
|
|
128
130
|
|
|
129
131
|
- `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
|
|
132
|
+
- チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --dry-run` を先に提案する。personal install は `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, `.codex/hooks.json`, skill symlink を変更対象にしない。変更対象は `.phasegate-local/config.json`, `.phasegate/manifest.json`, `.git/info/exclude` の managed block に限定し、Codex user-level hook setup は manual action として扱う。<!-- @work-item-id WI-207 -->
|
|
130
133
|
- Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は raw `repairMode` が current scope の修復指示ではないという印である。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|
|
131
134
|
- `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
|
|
132
135
|
- manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
|
|
133
136
|
- reconcile / uninstall が refuse → user modified managed target として扱い、`--force` のリスクと backup path を説明して承認を取る
|
|
134
|
-
- Codex の `
|
|
137
|
+
- Codex の `hooks` feature flag は user-level setting。project-local `install` では変更されないため、必要なら `codex features enable hooks` を案内。`codex_hooks` は legacy alias として扱う。<!-- @work-item-id WI-205 -->
|
|
135
138
|
- Codex native `apply_patch` bypass は hook で完全捕捉できない。`.husky/pre-commit` の `phasegate pre-commit` が backstop になるため、Husky 配線を診断対象に含める
|
|
136
139
|
- 初回 setup / retrofit / CI-only / agent-hooks の判断が曖昧な場合は `npx phasegate setup:agent --dry-run --json` を先に使い、検出済み状態、質問、変更案、rollback、validation を根拠として提案する。<!-- @work-item-id WI-172 -->
|
|
137
140
|
- 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 -->
|
|
@@ -130,6 +130,8 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
|
|
|
130
130
|
- 既存プロジェクト導入: `docs/guide/retrofit-adoption.md`
|
|
131
131
|
- setup artifact / doctor finding / legacy artifact: `docs/guide/setup-artifacts.md`
|
|
132
132
|
|
|
133
|
+
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal` は `package.json`、`AGENTS.md`、`CLAUDE.md`、`.husky/*`、`.github/workflows/*`、`.gitignore`、`.codex/hooks.json`、skill symlink を変更せず、`.phasegate-local/config.json` と `.git/info/exclude` の managed block を使う。Codex user-level hook setup は manual action として説明する。<!-- @work-item-id WI-207 -->
|
|
134
|
+
|
|
133
135
|
`setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
|
|
134
136
|
|
|
135
137
|
Claude-only / Codex-only setup の確認では、full `phasegate doctor` と scoped doctor を区別する。ユーザーが `setup:agent --agent claude` を選んだ場合は `phasegate doctor --agent claude --json` を優先し、`scopedOutFindings` の Codex finding は「未選択 agent の not-applicable 情報」であり修復対象ではないと説明する。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は `repairMode` が current scope の修復指示ではないという印である。full doctor は両 agent を導入したい場合の診断として扱う。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|
|
@@ -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
|
|