phasegate 0.160.13 → 0.160.14

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/README.md CHANGED
@@ -105,11 +105,11 @@ npx phasegate doctor
105
105
  For personal evaluation inside a team-owned repository, use local-only install:
106
106
 
107
107
  ```bash
108
- npx phasegate install --personal --dry-run
109
- npx phasegate install --personal --apply
108
+ npx phasegate install --personal --agent claude --dry-run
109
+ npx phasegate install --personal --agent claude --apply
110
110
  ```
111
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 -->
112
+ `--personal` does not plan or write `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, or CI settings. For Claude Code it automatically creates `.phasegate-local/phasegate.config.json`, `.phasegate-local/claude/settings.json`, `.phasegate-local/skills/`, ignored root `.claude/settings.json` and `.claude/skills` shims, a managed local-only block in `.git/info/exclude`, and `.phasegate/manifest.json`. Codex user-level hook setup remains a manual action instead of a project-local `.codex/hooks.json` write. <!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 -->
113
113
 
114
114
  For agent-driven setup planning, use:
115
115
 
@@ -12,7 +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
+ | Personal evaluation in a team repository | `npx phasegate install --personal --agent claude --dry-run` then `npx phasegate install --personal --agent claude --apply` | Team-owned files stay untouched; `.phasegate-local/` stores config/settings/skills and ignored `.claude/*` shims expose them to Claude Code |
16
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 |
17
17
  | Codex-only project | `npx phasegate init --agent codex --with-husky` | `.codex/hooks.json`, `AGENTS.md`, `.codex/skills`, and pre-commit backstop are present |
18
18
  | Strict validation rollout | `npx phasegate setup:agent --intent strict --dry-run --json` | The plan lists strict checks, L4 risk, rollback, and validation commands |
@@ -61,11 +61,11 @@ npx phasegate doctor
61
61
  For personal evaluation inside a team-owned repository:
62
62
 
63
63
  ```bash
64
- npx phasegate install --personal --dry-run
65
- npx phasegate install --personal --apply
64
+ npx phasegate install --personal --agent claude --dry-run
65
+ npx phasegate install --personal --agent claude --apply
66
66
  ```
67
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 -->
68
+ Personal install keeps team-owned files out of both the plan and apply path: `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, and CI settings are not touched. For Claude Code, PhaseGate writes local-only artifacts under `.phasegate-local/`, creates ignored root `.claude/settings.json` and `.claude/skills` shims for Claude discovery, records `.phasegate/manifest.json`, and manages a local exclude 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 --> <!-- @work-item-id WI-208 -->
69
69
 
70
70
  For agent-readable planning before writing files:
71
71
 
@@ -5,6 +5,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
5
5
  <!-- @work-item-id WI-152 -->
6
6
  <!-- @work-item-id WI-157 -->
7
7
  <!-- @work-item-id WI-169 -->
8
+ <!-- @work-item-id WI-208 -->
8
9
 
9
10
  ## Artifact Classes
10
11
 
@@ -14,7 +15,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
14
15
  | Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init`; `install` may merge scripts/devDependency into `package.json` |
15
16
  | 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
17
  | 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 |
18
+ | Personal install artifact | `.phasegate-local/phasegate.config.json`, `.phasegate-local/claude/settings.json`, `.phasegate-local/skills/`, ignored root `.claude/settings.json` and `.claude/skills` shims, `.git/info/exclude` PhaseGate block | One developer on one machine | Created by `phasegate install --personal --agent claude`; team `.gitignore`, team-owned files, GitHub CLI config, repo secrets, and CI settings are not modified |
18
19
  | 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 |
19
20
  | 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 |
20
21
 
@@ -34,6 +35,12 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
34
35
 
35
36
  `init --with-ci` still deploys the legacy-compatible template set, including `.github/workflows/aidlc-gate.yml`, `.github/workflows/consistency-check.yml`, and `.github/workflows/agent-context-refresh.yml`. Structured `install` uses `.github/workflows/phasegate-aidlc-gate.yml` so it can coexist with existing project CI without taking over a generic workflow filename.
36
37
 
38
+ ## Personal Install Artifacts
39
+
40
+ `phasegate install --personal --agent claude --apply` is a local-only bootstrap for evaluating PhaseGate in a team-owned repository. Substantive files are stored under `.phasegate-local/`, while root `.claude/settings.json` and `.claude/skills` are symlinks so Claude Code can discover the settings and skills. The root shims are hidden by `.git/info/exclude`; PhaseGate does not edit team `.gitignore`.
41
+
42
+ If `.claude/settings.json` or `.claude/skills` already exists and is not the PhaseGate personal shim, personal install reports manual review and preserves the existing path.
43
+
37
44
  ## Doctor Findings
38
45
 
39
46
  `phasegate doctor` evaluates setup health from the managed targets and related project state. Findings include `repairMode`, optional `repairHint`, and optional `suggestedSkill`.
@@ -1,7 +1,77 @@
1
1
  {
2
- "version": "1.0",
3
2
  "project": {
4
- "name": "personal-phasegate"
3
+ "name": "personal-phasegate",
4
+ "preset": "standard"
5
5
  },
6
- "personal": true
6
+ "layers": {
7
+ "L1": {
8
+ "enabled": true
9
+ },
10
+ "L2": {
11
+ "enabled": true
12
+ },
13
+ "L3": {
14
+ "enabled": true
15
+ },
16
+ "L4": {
17
+ "enabled": false
18
+ }
19
+ },
20
+ "quickMode": {
21
+ "allowedCategories": [
22
+ "bugfix",
23
+ "docs",
24
+ "test",
25
+ "config"
26
+ ],
27
+ "maintainedLayers": [
28
+ "L1",
29
+ "L2"
30
+ ],
31
+ "relaxedGates": [
32
+ "phase-gate",
33
+ "2-phase-execution"
34
+ ],
35
+ "fullModeRequiredWhen": {
36
+ "mixedCategories": true,
37
+ "newDomainFile": true,
38
+ "apiContractChange": true
39
+ }
40
+ },
41
+ "phaseDependencies": {
42
+ "preset": "default",
43
+ "override": false,
44
+ "customRules": []
45
+ },
46
+ "planningMode": {
47
+ "default": "interactive",
48
+ "perPhase": {}
49
+ },
50
+ "harnesses": {
51
+ "agentLessonCollection": false,
52
+ "cascadeUpdate": false,
53
+ "bundleSizeLimit": 0,
54
+ "deadCodeGC": false
55
+ },
56
+ "paths": {
57
+ "designDocs": "docs/product/construction",
58
+ "inceptionDocs": "docs/inception"
59
+ },
60
+ "reporting": {
61
+ "format": "json",
62
+ "outputDir": "reports"
63
+ },
64
+ "protectedFiles": {
65
+ "exclude": [
66
+ "tsconfig.json",
67
+ "package.json"
68
+ ]
69
+ },
70
+ "baseline": {
71
+ "enabled": true,
72
+ "path": ".phasegate/baseline.json"
73
+ },
74
+ "architecture": {
75
+ "preset": "clean"
76
+ }
7
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.13",
3
+ "version": "0.160.14",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-208
4
5
  *
5
6
  * Phasegate 状態を context 文字列として組み立てる共有ヘルパー。
6
7
  * SessionStart / UserPromptSubmit hook が共通で使用する。
@@ -54,18 +55,27 @@ const DEFAULT_PROTECTED_PATTERNS = [
54
55
  export async function findConfigPath(startDir: string): Promise<string | null> {
55
56
  let dir = startDir;
56
57
  while (true) {
57
- const candidate = path.join(dir, 'phasegate.config.json');
58
- try {
59
- await fs.access(candidate);
60
- return candidate;
61
- } catch {
62
- const parent = path.dirname(dir);
63
- if (parent === dir) return null;
64
- dir = parent;
58
+ const candidates = [
59
+ path.join(dir, 'phasegate.config.json'),
60
+ path.join(dir, '.phasegate-local', 'phasegate.config.json'),
61
+ ];
62
+ for (const candidate of candidates) {
63
+ try {
64
+ await fs.access(candidate);
65
+ return candidate;
66
+ } catch {}
65
67
  }
68
+ const parent = path.dirname(dir);
69
+ if (parent === dir) return null;
70
+ dir = parent;
66
71
  }
67
72
  }
68
73
 
74
+ export function projectRootForConfig(configPath: string): string {
75
+ const configDir = path.dirname(configPath);
76
+ return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
77
+ }
78
+
69
79
  async function loadConfig(configPath: string): Promise<PhasegateConfig> {
70
80
  try {
71
81
  const raw = await fs.readFile(configPath, 'utf8');
@@ -122,7 +132,7 @@ export async function collectPhasegateStatus(cwd: string): Promise<PhasegateStat
122
132
  const configPath = await findConfigPath(cwd);
123
133
  const configFound = configPath !== null;
124
134
  const config: PhasegateConfig = configFound ? await loadConfig(configPath) : {};
125
- const projectRoot = configFound ? path.dirname(configPath) : cwd;
135
+ const projectRoot = configFound ? projectRootForConfig(configPath) : cwd;
126
136
 
127
137
  const protectedPatterns = computeProtectedPatterns(config);
128
138
  const blockedUnits = await findBlockedUnits(projectRoot, config);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-208
4
5
  *
5
6
  * PostToolUse Hook Adapter
6
7
  * Claude Code の PostToolUse Hook エントリポイント
@@ -30,19 +31,28 @@ async function readStdin(): Promise<string> {
30
31
  async function findConfigPath(): Promise<string> {
31
32
  let dir = process.cwd();
32
33
  while (true) {
33
- const candidate = path.join(dir, 'phasegate.config.json');
34
- try {
35
- await fs.access(candidate);
36
- return candidate;
37
- } catch {
38
- const parent = path.dirname(dir);
39
- if (parent === dir) break;
40
- dir = parent;
34
+ const candidates = [
35
+ path.join(dir, 'phasegate.config.json'),
36
+ path.join(dir, '.phasegate-local', 'phasegate.config.json'),
37
+ ];
38
+ for (const candidate of candidates) {
39
+ try {
40
+ await fs.access(candidate);
41
+ return candidate;
42
+ } catch {}
41
43
  }
44
+ const parent = path.dirname(dir);
45
+ if (parent === dir) break;
46
+ dir = parent;
42
47
  }
43
48
  return path.join(process.cwd(), 'phasegate.config.json');
44
49
  }
45
50
 
51
+ function projectRootForConfig(configPath: string): string {
52
+ const configDir = path.dirname(configPath);
53
+ return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
54
+ }
55
+
46
56
  async function main(): Promise<void> {
47
57
  let raw: string;
48
58
  try {
@@ -82,7 +92,7 @@ async function main(): Promise<void> {
82
92
 
83
93
  if (output.skipReason) {
84
94
  await recordHookSkipEvent({
85
- projectRoot: path.dirname(configPath),
95
+ projectRoot: projectRootForConfig(configPath),
86
96
  hookType: 'post-tool-use',
87
97
  reason: output.skipReason,
88
98
  targetPaths: [],
@@ -3,6 +3,7 @@
3
3
  * @unit agent-integration
4
4
  * @work-item-id WI-202 / WI-204
5
5
  * @work-item-id WI-206
6
+ * @work-item-id WI-208
6
7
  *
7
8
  * PreToolUse Hook Adapter
8
9
  * Claude Code の PreToolUse Hook エントリポイント
@@ -57,19 +58,28 @@ async function readStdin(): Promise<string> {
57
58
  async function findConfigPath(startDir: string): Promise<string> {
58
59
  let dir = startDir;
59
60
  while (true) {
60
- const candidate = path.join(dir, 'phasegate.config.json');
61
- try {
62
- await fs.access(candidate);
63
- return candidate;
64
- } catch {
65
- const parent = path.dirname(dir);
66
- if (parent === dir) break;
67
- dir = parent;
61
+ const candidates = [
62
+ path.join(dir, 'phasegate.config.json'),
63
+ path.join(dir, '.phasegate-local', 'phasegate.config.json'),
64
+ ];
65
+ for (const candidate of candidates) {
66
+ try {
67
+ await fs.access(candidate);
68
+ return candidate;
69
+ } catch {}
68
70
  }
71
+ const parent = path.dirname(dir);
72
+ if (parent === dir) break;
73
+ dir = parent;
69
74
  }
70
75
  return path.join(startDir, 'phasegate.config.json');
71
76
  }
72
77
 
78
+ function projectRootForConfig(configPath: string): string {
79
+ const configDir = path.dirname(configPath);
80
+ return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
81
+ }
82
+
73
83
  function isProjectExternalAbsolutePath(filePath: string): boolean {
74
84
  return path.isAbsolute(filePath);
75
85
  }
@@ -139,24 +149,25 @@ async function main(): Promise<void> {
139
149
  const configPath = await findConfigPath(cwd);
140
150
  const projectTargetFilePaths = targetFilePaths.filter((filePath) => !isProjectExternalAbsolutePath(filePath));
141
151
  const projectTargetChanges = targetChanges.filter((change) => !isProjectExternalAbsolutePath(change.filePath));
152
+ const projectRoot = projectRootForConfig(configPath);
142
153
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
143
154
  const phaseGateQueryPort = new PhaseGateQueryAdapter();
144
155
  const storyReflectionQueryPort = new FileSystemStoryReflectionQueryAdapter({
145
- rootDir: path.dirname(configPath),
156
+ rootDir: projectRoot,
146
157
  configPath,
147
158
  });
148
159
  const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
149
160
  classifyUseCaseFactory: () => createQuickModeCompositionRoot().classifyUseCase,
150
161
  });
151
162
  const baselineGrandfatherQueryPort = new CiGovernanceBaselineGrandfatherAdapter({
152
- baseDir: path.dirname(configPath),
163
+ baseDir: projectRoot,
153
164
  configQueryPort,
154
165
  });
155
166
  const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
156
- rootDir: path.dirname(configPath),
167
+ rootDir: projectRoot,
157
168
  });
158
169
  const fullModeSessionQueryPort = new FileSystemFullModeSessionQueryAdapter({
159
- rootDir: path.dirname(configPath),
170
+ rootDir: projectRoot,
160
171
  configQueryPort,
161
172
  });
162
173
  const useCase = new HandlePreToolUseUseCase({
@@ -2,6 +2,7 @@
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
4
  * @work-item-id WI-203
5
+ * @work-item-id WI-208
5
6
  *
6
7
  * Stop Hook Adapter
7
8
  * Claude Code の Stop Hook エントリポイント
@@ -45,19 +46,28 @@ async function readStdin(): Promise<string> {
45
46
  async function findConfigPath(): Promise<string> {
46
47
  let dir = process.cwd();
47
48
  while (true) {
48
- const candidate = path.join(dir, 'phasegate.config.json');
49
- try {
50
- await fs.access(candidate);
51
- return candidate;
52
- } catch {
53
- const parent = path.dirname(dir);
54
- if (parent === dir) break;
55
- dir = parent;
49
+ const candidates = [
50
+ path.join(dir, 'phasegate.config.json'),
51
+ path.join(dir, '.phasegate-local', 'phasegate.config.json'),
52
+ ];
53
+ for (const candidate of candidates) {
54
+ try {
55
+ await fs.access(candidate);
56
+ return candidate;
57
+ } catch {}
56
58
  }
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) break;
61
+ dir = parent;
57
62
  }
58
63
  return path.join(process.cwd(), 'phasegate.config.json');
59
64
  }
60
65
 
66
+ function projectRootForConfig(configPath: string): string {
67
+ const configDir = path.dirname(configPath);
68
+ return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
69
+ }
70
+
61
71
  async function main(): Promise<void> {
62
72
  let raw: string;
63
73
  try {
@@ -99,7 +109,7 @@ async function main(): Promise<void> {
99
109
 
100
110
  if (output.skipReason === 'REENTRY_DETECTED') {
101
111
  await recordHookSkipEvent({
102
- projectRoot: path.dirname(configPath),
112
+ projectRoot: projectRootForConfig(configPath),
103
113
  hookType: 'stop',
104
114
  reason: output.skipReason,
105
115
  targetPaths: [],
@@ -1,12 +1,13 @@
1
1
  // @unit agent-integration
2
2
  // @layer presentation
3
+ // @work-item-id WI-208
3
4
 
4
- import * as path from 'node:path';
5
5
  import {
6
6
  buildUserPromptSubmitContext,
7
7
  collectPhasegateStatus,
8
8
  collectRecentViolations,
9
9
  findConfigPath,
10
+ projectRootForConfig,
10
11
  } from './phasegate-status-context.js';
11
12
 
12
13
  async function readStdin(): Promise<string> {
@@ -29,7 +30,7 @@ async function main(): Promise<void> {
29
30
  // 違反検知の起点は config が見つかったディレクトリ (= project root)。
30
31
  // config が無ければ cwd をそのまま使う。
31
32
  const configPath = await findConfigPath(cwd);
32
- const projectRoot = configPath !== null ? path.dirname(configPath) : cwd;
33
+ const projectRoot = configPath !== null ? projectRootForConfig(configPath) : cwd;
33
34
  const violations = await collectRecentViolations(projectRoot, status);
34
35
  const additionalContext = buildUserPromptSubmitContext(status, violations);
35
36
 
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import type { ConfigRepositoryPort } from '../../domain/ports/config-repository-port.js';
8
8
 
9
9
  const DEFAULT_CONFIG_FILE_NAME = 'phasegate.config.json';
10
+ const PERSONAL_CONFIG_PATH = path.join('.phasegate-local', DEFAULT_CONFIG_FILE_NAME);
10
11
 
11
12
  export class ConfigNotFoundError extends Error {
12
13
  readonly configPath: string;
@@ -45,10 +46,15 @@ async function findNearestConfig(startDirectory: string): Promise<string | null>
45
46
  let currentDirectory = path.resolve(startDirectory);
46
47
 
47
48
  while (true) {
48
- const candidate = path.join(currentDirectory, DEFAULT_CONFIG_FILE_NAME);
49
-
50
- if (await exists(candidate)) {
51
- return candidate;
49
+ const candidates = [
50
+ path.join(currentDirectory, DEFAULT_CONFIG_FILE_NAME),
51
+ path.join(currentDirectory, PERSONAL_CONFIG_PATH),
52
+ ];
53
+
54
+ for (const candidate of candidates) {
55
+ if (await exists(candidate)) {
56
+ return candidate;
57
+ }
52
58
  }
53
59
 
54
60
  const parentDirectory = path.dirname(currentDirectory);
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-178
5
+ // @work-item-id WI-208
5
6
 
6
7
  import { DiagnosticReport } from "../../domain/diagnostic-report.js";
7
8
  import type { CheckId } from "../../domain/check-id.js";
@@ -26,12 +27,20 @@ export interface RunDoctorDiagnosticsInput {
26
27
  export interface RunDoctorDiagnosticsOutput {
27
28
  readonly report: DiagnosticReport;
28
29
  readonly agent: DoctorAgentScope;
30
+ readonly installationMode: "project" | "personal";
29
31
  readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
30
32
  readonly exitCode: number;
31
33
  }
32
34
 
33
35
  const CLAUDE_ONLY_CHECKS = new Set<CheckId>(["claude-hook-missing", "claude-skills-symlink"]);
34
36
  const CODEX_ONLY_CHECKS = new Set<CheckId>(["codex-hook-missing", "codex-skills-symlink"]);
37
+ const PERSONAL_SCOPED_OUT_CHECKS = new Set<CheckId>([
38
+ "husky-pre-commit-missing",
39
+ "husky-commit-msg-missing",
40
+ "husky-pre-push-missing",
41
+ "ci-workflow-missing",
42
+ "package-json-devdep-missing",
43
+ ]);
35
44
 
36
45
  export class RunDoctorDiagnosticsUseCase {
37
46
  constructor(
@@ -42,15 +51,20 @@ export class RunDoctorDiagnosticsUseCase {
42
51
 
43
52
  async execute(input: RunDoctorDiagnosticsInput): Promise<RunDoctorDiagnosticsOutput> {
44
53
  const agent = input.agent ?? "both";
45
- await this.manifestRepository.load(input.projectRoot).catch(() => null);
54
+ const manifest = await this.manifestRepository.load(input.projectRoot).catch(() => null);
55
+ const installationMode = manifest?.findEntry(".phasegate-local/phasegate.config.json") !== null && manifest !== null ? "personal" : "project";
46
56
  const rawFindings = (await Promise.all(
47
57
  this.checks.map((check) => check.run(input.projectRoot, this.inspector)),
48
58
  )).filter((finding) => finding !== null);
49
- const { findings, scopedOutFindings } = this.applyAgentScope(rawFindings, agent);
59
+ const { findings, scopedOutFindings } = this.applyPersonalScope(
60
+ this.applyAgentScope(rawFindings, agent),
61
+ installationMode,
62
+ );
50
63
  const report = DiagnosticReport.create(findings);
51
64
  return {
52
65
  report,
53
66
  agent,
67
+ installationMode,
54
68
  scopedOutFindings,
55
69
  exitCode: this.decideExitCode(report, input.strict),
56
70
  };
@@ -77,6 +91,26 @@ export class RunDoctorDiagnosticsUseCase {
77
91
  return { findings: applicable, scopedOutFindings: scopedOut };
78
92
  }
79
93
 
94
+ private applyPersonalScope(
95
+ scoped: { readonly findings: readonly DiagnosticFinding[]; readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[] },
96
+ installationMode: "project" | "personal",
97
+ ): { readonly findings: readonly DiagnosticFinding[]; readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[] } {
98
+ if (installationMode !== "personal") return scoped;
99
+ const applicable: DiagnosticFinding[] = [];
100
+ const scopedOut: ScopedOutDiagnosticFinding[] = [...scoped.scopedOutFindings];
101
+ for (const finding of scoped.findings) {
102
+ if (PERSONAL_SCOPED_OUT_CHECKS.has(finding.checkId)) {
103
+ scopedOut.push({
104
+ finding,
105
+ scopeReason: `${finding.checkId} is a team/project install target and is intentionally out of scope for personal install.`,
106
+ });
107
+ } else {
108
+ applicable.push(finding);
109
+ }
110
+ }
111
+ return { findings: applicable, scopedOutFindings: scopedOut };
112
+ }
113
+
80
114
  private decideExitCode(report: DiagnosticReport, strict: boolean): number {
81
115
  if (report.hasRedFlag()) return 1;
82
116
  if (strict && report.hasWarning()) return 1;
@@ -8,8 +8,9 @@
8
8
  // @work-item-id WI-182
9
9
  // @work-item-id WI-183
10
10
  // @work-item-id WI-207
11
+ // @work-item-id WI-208
11
12
 
12
- import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink } from "node:fs/promises";
13
+ import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir } from "node:fs/promises";
13
14
  import { dirname, join } from "node:path";
14
15
  import { DeploymentEntry } from "../../domain/deployment-entry.js";
15
16
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
@@ -19,7 +20,7 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
19
20
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
20
21
 
21
22
  type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
22
- type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy";
23
+ type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy" | "copy-dir" | "symlink";
23
24
 
24
25
  export interface InstallPlanItem {
25
26
  readonly path: string;
@@ -83,6 +84,8 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
83
84
  const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
84
85
  const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
85
86
  const TEXT_END = "# phasegate personal install exclude (END)";
87
+ const PERSONAL_CLAUDE_SETTINGS_LINK = "../.phasegate-local/claude/settings.json";
88
+ const PERSONAL_CLAUDE_SKILLS_LINK = "../.phasegate-local/skills";
86
89
 
87
90
  function isRecord(value: unknown): value is Record<string, unknown> {
88
91
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -105,6 +108,20 @@ async function readTextOrNull(path: string): Promise<string | null> {
105
108
  }
106
109
  }
107
110
 
111
+ async function copyDirectory(src: string, dest: string): Promise<void> {
112
+ await mkdir(dest, { recursive: true });
113
+ const entries = await readdir(src, { withFileTypes: true });
114
+ for (const entry of entries) {
115
+ const srcPath = join(src, entry.name);
116
+ const destPath = join(dest, entry.name);
117
+ if (entry.isDirectory()) {
118
+ await copyDirectory(srcPath, destPath);
119
+ } else if (entry.isFile()) {
120
+ await copyFile(srcPath, destPath);
121
+ }
122
+ }
123
+ }
124
+
108
125
  function normalizeJsonEntry(value: unknown): string {
109
126
  return JSON.stringify(value);
110
127
  }
@@ -295,7 +312,7 @@ export class RunInstallUseCase {
295
312
  const workflow = input.workflow ?? "standard";
296
313
  const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
297
314
  const targets = input.personal
298
- ? this.createPersonalTargets()
315
+ ? this.createPersonalTargets({ includeClaude })
299
316
  : this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
300
317
  const existingManifest = await this.manifestRepository.load(input.projectRoot);
301
318
  const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
@@ -340,23 +357,23 @@ export class RunInstallUseCase {
340
357
  await this.backup(input.projectRoot, target.path, backupDir);
341
358
  }
342
359
 
360
+ try {
361
+ await mkdir(dirname(absolutePath), { recursive: true });
362
+ } catch (error) {
363
+ return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
364
+ }
365
+ try {
366
+ await writeFile(absolutePath, next, "utf8");
367
+ } catch (error) {
368
+ return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "writeFile", error);
369
+ }
370
+ if (target.executable) {
343
371
  try {
344
- await mkdir(dirname(absolutePath), { recursive: true });
345
- } catch (error) {
346
- return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
347
- }
348
- try {
349
- await writeFile(absolutePath, next, "utf8");
372
+ await chmod(absolutePath, 0o755);
350
373
  } catch (error) {
351
- return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "writeFile", error);
352
- }
353
- if (target.executable) {
354
- try {
355
- await chmod(absolutePath, 0o755);
356
- } catch (error) {
357
- return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
358
- }
374
+ return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
359
375
  }
376
+ }
360
377
  changed.push(item);
361
378
 
362
379
  const mode = before === null ? "created" : "merged";
@@ -377,41 +394,63 @@ export class RunInstallUseCase {
377
394
  }
378
395
  }
379
396
 
380
- const linkPaths = input.personal ? [] : [
381
- ...(includeClaude ? [".claude/skills"] : []),
382
- ...(includeCodex ? [".codex/skills"] : []),
383
- ];
384
- for (const linkPath of linkPaths) {
385
- const item = await this.planSkillLink(input.projectRoot, linkPath);
397
+ if (input.personal && includeClaude) {
398
+ const item = await this.planPersonalSkills(input);
399
+ plan.push(item);
400
+ if (input.apply && item.changed) {
401
+ try {
402
+ await copyDirectory(join(input.harnessRoot, "skills"), join(input.projectRoot, ".phasegate-local", "skills"));
403
+ await writeFile(
404
+ join(input.projectRoot, ".phasegate-local", "skills", ".harness-version"),
405
+ `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
406
+ "utf8",
407
+ );
408
+ } catch (error) {
409
+ return this.withApplyError({ plan, refused, changed, backupDir }, item.path, "copyDirectory", error);
410
+ }
411
+ changed.push(item);
412
+ manifest = this.addManifestEntry(baseManifest, manifest, {
413
+ path: item.path,
414
+ mode: "created",
415
+ contentForHash: this.personalSkillsHashInput(input.phasegateVersion, skillSet),
416
+ });
417
+ }
418
+ }
419
+
420
+ const linkSpecs = input.personal
421
+ ? [
422
+ ...(includeClaude
423
+ ? [
424
+ { path: ".claude/settings.json", target: PERSONAL_CLAUDE_SETTINGS_LINK },
425
+ { path: ".claude/skills", target: PERSONAL_CLAUDE_SKILLS_LINK },
426
+ ]
427
+ : []),
428
+ ]
429
+ : [
430
+ ...(includeClaude ? [{ path: ".claude/skills", target: "../skills" }] : []),
431
+ ...(includeCodex ? [{ path: ".codex/skills", target: "../skills" }] : []),
432
+ ];
433
+ for (const linkSpec of linkSpecs) {
434
+ const item = await this.planSkillLink(input.projectRoot, linkSpec.path, linkSpec.target);
386
435
  plan.push(item);
387
436
  if (!input.apply || !item.changed) continue;
388
437
  try {
389
- await mkdir(join(input.projectRoot, "skills"), { recursive: true });
390
- await mkdir(dirname(join(input.projectRoot, linkPath)), { recursive: true });
438
+ if (!input.personal) await mkdir(join(input.projectRoot, "skills"), { recursive: true });
439
+ await mkdir(dirname(join(input.projectRoot, linkSpec.path)), { recursive: true });
391
440
  } catch (error) {
392
- return this.withApplyError({ plan, refused, changed, backupDir }, linkPath, "mkdir", error);
441
+ return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "mkdir", error);
393
442
  }
394
443
  try {
395
- await symlink("../skills", join(input.projectRoot, linkPath), process.platform === "win32" ? "junction" : "dir");
444
+ await symlink(linkSpec.target, join(input.projectRoot, linkSpec.path), process.platform === "win32" ? "junction" : "dir");
396
445
  } catch (error) {
397
- return this.withApplyError({ plan, refused, changed, backupDir }, linkPath, "symlink", error);
446
+ return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "symlink", error);
398
447
  }
399
448
  changed.push(item);
400
- const hash = this.hashCalculator.compute("../skills");
401
- const existingEntry = baseManifest.findEntry(linkPath);
402
- if (existingEntry !== null && existingEntry.hash.equals(hash)) {
403
- manifest = manifest.addEntry(existingEntry);
404
- } else {
405
- manifest = manifest.addEntry(
406
- DeploymentEntry.create({
407
- path: linkPath,
408
- mode: "symlink",
409
- block: null,
410
- hash,
411
- deployedAt: new Date().toISOString(),
412
- }),
413
- );
414
- }
449
+ manifest = this.addManifestEntry(baseManifest, manifest, {
450
+ path: linkSpec.path,
451
+ mode: "symlink",
452
+ contentForHash: linkSpec.target,
453
+ });
415
454
  }
416
455
 
417
456
  if (input.personal && includeCodex) {
@@ -525,13 +564,22 @@ export class RunInstallUseCase {
525
564
  ];
526
565
  }
527
566
 
528
- private createPersonalTargets(): readonly InstallTarget[] {
567
+ private createPersonalTargets(options: { readonly includeClaude: boolean }): readonly InstallTarget[] {
529
568
  return [
530
569
  {
531
- path: ".phasegate-local/config.json",
570
+ path: ".phasegate-local/phasegate.config.json",
532
571
  strategy: "copy" as const,
533
572
  templatePath: "docs/templates/personal/phasegate-local-config.json",
534
573
  },
574
+ ...(options.includeClaude
575
+ ? [
576
+ {
577
+ path: ".phasegate-local/claude/settings.json",
578
+ strategy: "copy" as const,
579
+ templatePath: "templates/.claude/settings.json",
580
+ },
581
+ ]
582
+ : []),
535
583
  {
536
584
  path: ".git/info/exclude",
537
585
  strategy: "text-managed" as const,
@@ -545,6 +593,8 @@ export class RunInstallUseCase {
545
593
  if (target.strategy === "shell") return shellRepairMode(before);
546
594
  if (target.strategy === "text-managed") return "mechanical";
547
595
  if (target.strategy === "copy") return "mechanical";
596
+ if (target.strategy === "copy-dir") return "mechanical";
597
+ if (target.strategy === "symlink") return "mechanical";
548
598
  if (target.strategy === "json") return jsonRepairMode(before);
549
599
  if (target.strategy === "markdown-managed") return "mechanical";
550
600
  return "mechanical";
@@ -589,16 +639,59 @@ export class RunInstallUseCase {
589
639
  };
590
640
  }
591
641
 
592
- private async planSkillLink(projectRoot: string, relativePath: string): Promise<InstallPlanItem> {
642
+ private async planPersonalSkills(input: RunInstallInput): Promise<InstallPlanItem> {
643
+ const versionPath = join(input.projectRoot, ".phasegate-local", "skills", ".harness-version");
644
+ const current = await readTextOrNull(versionPath);
645
+ const expectedNeedle = `"version": "${input.phasegateVersion}"`;
646
+ const changed = current === null || !current.includes(expectedNeedle);
647
+ return {
648
+ path: ".phasegate-local/skills",
649
+ action: changed ? "missing" : "will-skip",
650
+ repairMode: "mechanical",
651
+ strategy: "copy-dir",
652
+ changed,
653
+ summary: changed ? ".phasegate-local/skills: deploy bundled skills" : ".phasegate-local/skills: already up to date",
654
+ diff: changed ? "+ bundled skills" : "no changes",
655
+ skillHint: null,
656
+ };
657
+ }
658
+
659
+ private personalSkillsHashInput(version: string, skillSet: "core" | "all"): string {
660
+ return `personal-skills:${version}:${skillSet}`;
661
+ }
662
+
663
+ private addManifestEntry(
664
+ baseManifest: DeploymentManifest,
665
+ manifest: DeploymentManifest,
666
+ input: { readonly path: string; readonly mode: "created" | "symlink"; readonly contentForHash: string },
667
+ ): DeploymentManifest {
668
+ const hash = this.hashCalculator.compute(input.contentForHash);
669
+ const existingEntry = baseManifest.findEntry(input.path);
670
+ if (existingEntry !== null && existingEntry.hash.equals(hash)) {
671
+ return manifest.addEntry(existingEntry);
672
+ }
673
+ return manifest.addEntry(
674
+ DeploymentEntry.create({
675
+ path: input.path,
676
+ mode: input.mode,
677
+ block: null,
678
+ hash,
679
+ deployedAt: new Date().toISOString(),
680
+ }),
681
+ );
682
+ }
683
+
684
+ private async planSkillLink(projectRoot: string, relativePath: string, target: string): Promise<InstallPlanItem> {
593
685
  const absolutePath = join(projectRoot, relativePath);
686
+ const parentPath = dirname(absolutePath);
594
687
  try {
595
688
  const stat = await lstat(absolutePath);
596
- if (stat.isSymbolicLink() && (await readlink(absolutePath)) === "../skills") {
689
+ if (stat.isSymbolicLink() && (await readlink(absolutePath)) === target) {
597
690
  return {
598
691
  path: relativePath,
599
692
  action: "will-skip",
600
693
  repairMode: "mechanical",
601
- strategy: "yaml-add",
694
+ strategy: "symlink",
602
695
  changed: false,
603
696
  summary: `${relativePath}: already linked`,
604
697
  diff: "no changes",
@@ -609,21 +702,36 @@ export class RunInstallUseCase {
609
702
  path: relativePath,
610
703
  action: "will-merge",
611
704
  repairMode: "manual",
612
- strategy: "yaml-add",
705
+ strategy: "symlink",
613
706
  changed: false,
614
707
  summary: `${relativePath}: existing non-phasegate path requires manual review`,
615
708
  diff: "manual review required",
616
709
  skillHint: null,
617
710
  };
618
711
  } catch {
712
+ try {
713
+ const parentStat = await lstat(parentPath);
714
+ if (!parentStat.isDirectory()) {
715
+ return {
716
+ path: relativePath,
717
+ action: "will-merge",
718
+ repairMode: "manual",
719
+ strategy: "symlink",
720
+ changed: false,
721
+ summary: `${relativePath}: parent path exists and requires manual review`,
722
+ diff: "manual review required",
723
+ skillHint: null,
724
+ };
725
+ }
726
+ } catch {}
619
727
  return {
620
728
  path: relativePath,
621
729
  action: "missing",
622
730
  repairMode: "mechanical",
623
- strategy: "yaml-add",
731
+ strategy: "symlink",
624
732
  changed: true,
625
733
  summary: `${relativePath}: create symlink`,
626
- diff: "+ symlink ../skills",
734
+ diff: `+ symlink ${target}`,
627
735
  skillHint: null,
628
736
  };
629
737
  }
@@ -4,6 +4,7 @@
4
4
  // @work-item-id WI-174
5
5
  // @work-item-id WI-199
6
6
  // @work-item-id WI-207
7
+ // @work-item-id WI-208
7
8
 
8
9
  import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
9
10
  import { dirname, join, relative, resolve } from "node:path";
@@ -163,6 +164,7 @@ export class RunUninstallUseCase {
163
164
  repairMode: "manual",
164
165
  strategy: "unknown",
165
166
  changed: false,
167
+ protected: false,
166
168
  summary: "manifest missing; run phasegate doctor and clean up manually",
167
169
  diff: "manual cleanup required",
168
170
  skillHint: SKILL_HINT,
@@ -216,8 +218,12 @@ export class RunUninstallUseCase {
216
218
  readonly apply: () => Promise<void>;
217
219
  }> {
218
220
  const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
219
- const currentContent = await readTextOrNull(absolutePath);
220
221
  const strategy = this.strategyFor(entry.path, entry.mode);
222
+ if (entry.mode === "symlink") return this.planSymlink(input.projectRoot, entry);
223
+ if (entry.mode === "created" && await this.isDirectory(absolutePath)) {
224
+ return this.planCreatedDirectory(input.projectRoot, entry);
225
+ }
226
+ const currentContent = await readTextOrNull(absolutePath);
221
227
  if (currentContent === null && strategy !== "symlink") {
222
228
  return {
223
229
  item: this.item(entry.path, "skip", "mechanical", strategy, false, `${entry.path}: already absent`, "no changes", null),
@@ -226,7 +232,6 @@ export class RunUninstallUseCase {
226
232
  };
227
233
  }
228
234
 
229
- if (entry.mode === "symlink") return this.planSymlink(input.projectRoot, entry);
230
235
  if (entry.mode === "created") return this.planCreated(input.projectRoot, entry, currentContent ?? "");
231
236
  return this.planMerged(input, entry, currentContent ?? "", strategy);
232
237
  }
@@ -240,9 +245,9 @@ export class RunUninstallUseCase {
240
245
  } catch {
241
246
  target = null;
242
247
  }
243
- if (target === "../skills") {
248
+ if (target !== null && this.hashCalculator.compute(target).equals(entry.hash)) {
244
249
  return {
245
- item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`, "- symlink ../skills", null),
250
+ item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`, `- symlink ${target}`, null),
246
251
  needsBackup: false,
247
252
  apply: async () => {
248
253
  await rm(absolutePath, { force: true });
@@ -256,6 +261,35 @@ export class RunUninstallUseCase {
256
261
  };
257
262
  }
258
263
 
264
+ private async isDirectory(path: string): Promise<boolean> {
265
+ try {
266
+ const stat = await lstat(path);
267
+ return stat.isDirectory();
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+
273
+ private async planCreatedDirectory(projectRoot: string, entry: DeploymentEntry) {
274
+ const absolutePath = this.resolveProjectPath(projectRoot, entry.path);
275
+ return {
276
+ item: this.item(
277
+ entry.path,
278
+ "delete",
279
+ "mechanical",
280
+ "created",
281
+ true,
282
+ `${entry.path}: delete created directory`,
283
+ "- directory",
284
+ null,
285
+ ),
286
+ needsBackup: false,
287
+ apply: async () => {
288
+ await rm(absolutePath, { recursive: true, force: true });
289
+ },
290
+ };
291
+ }
292
+
259
293
  private async planCreated(projectRoot: string, entry: DeploymentEntry, currentContent: string) {
260
294
  const absolutePath = this.resolveProjectPath(projectRoot, entry.path);
261
295
  const currentHash = this.hashCalculator.compute(currentContent);
@@ -2,6 +2,7 @@
2
2
  // @layer presentation
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-178
5
+ // @work-item-id WI-208
5
6
 
6
7
  import { mkdir, writeFile } from "node:fs/promises";
7
8
  import { dirname, isAbsolute, join } from "node:path";
@@ -37,6 +38,7 @@ export class DoctorHandler {
37
38
  const formatInput = {
38
39
  report: result.report,
39
40
  agent: result.agent,
41
+ installationMode: result.installationMode,
40
42
  scopedOutFindings: result.scopedOutFindings,
41
43
  phasegateVersion: input.phasegateVersion,
42
44
  projectRoot: input.projectRoot,
@@ -4,6 +4,7 @@
4
4
  // @work-item-id WI-178
5
5
  // @work-item-id WI-179
6
6
  // @work-item-id WI-180
7
+ // @work-item-id WI-208
7
8
 
8
9
  import type { DoctorAgentScope, ScopedOutDiagnosticFinding } from "../../application/usecases/run-doctor-diagnostics.js";
9
10
  import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
@@ -11,6 +12,7 @@ import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
11
12
  export interface DiagnosticReportFormatterInput {
12
13
  readonly report: DiagnosticReport;
13
14
  readonly agent: DoctorAgentScope;
15
+ readonly installationMode: "project" | "personal";
14
16
  readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
15
17
  readonly phasegateVersion: string;
16
18
  readonly projectRoot: string;
@@ -26,7 +28,8 @@ export class DiagnosticReportFormatter {
26
28
  projectRoot: input.projectRoot,
27
29
  scope: {
28
30
  agent: input.agent,
29
- description: scopeDescription(input.agent),
31
+ installationMode: input.installationMode,
32
+ description: scopeDescription(input.agent, input.installationMode),
30
33
  },
31
34
  overallStatus: input.report.overallStatus,
32
35
  findings: input.report.findings.map((finding) => ({
@@ -60,7 +63,7 @@ export class DiagnosticReportFormatter {
60
63
  const lines = [
61
64
  `phasegate doctor v${input.phasegateVersion}`,
62
65
  `Project: ${input.projectRoot}`,
63
- `Scope: ${input.agent} (${scopeDescription(input.agent)})`,
66
+ `Scope: ${input.agent} / ${input.installationMode} (${scopeDescription(input.agent, input.installationMode)})`,
64
67
  "",
65
68
  ];
66
69
  for (const finding of input.report.findings) {
@@ -86,7 +89,12 @@ export class DiagnosticReportFormatter {
86
89
  }
87
90
  }
88
91
 
89
- function scopeDescription(agent: DoctorAgentScope): string {
92
+ function scopeDescription(agent: DoctorAgentScope, installationMode: "project" | "personal"): string {
93
+ if (installationMode === "personal") {
94
+ if (agent === "claude") return "Personal Claude Code sandbox; team/project Husky, CI, package, and Codex-only findings are not repair targets.";
95
+ if (agent === "codex") return "Personal Codex sandbox; team/project Husky, CI, package, and Claude-only findings are not repair targets.";
96
+ return "Personal sandbox diagnostics; team/project Husky, CI, and package findings are not repair targets.";
97
+ }
90
98
  if (agent === "claude") return "Claude Code and shared setup targets; Codex-only findings are not applicable.";
91
99
  if (agent === "codex") return "Codex and shared setup targets; Claude-only findings are not applicable.";
92
100
  return "Full setup diagnostics for Claude, Codex, and shared targets.";
@@ -171,7 +171,7 @@ Setup:
171
171
  scaffold-wi <unit|_cross> <story|issue|chore>
172
172
  Create docs/inception/{unit}/WI-XXX/description.md
173
173
  emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
174
- install Install phasegate managed files (--dry-run|--apply, --force, --personal)
174
+ install Install phasegate managed files (--dry-run|--apply, --force, --personal, --agent <claude|codex|both>)
175
175
  uninstall Uninstall phasegate managed files (--dry-run|--apply, --force)
176
176
  reconcile Reconcile phasegate managed files (--dry-run|--apply, --force)
177
177
  setup:agent Diagnose repo setup and produce/apply an agent-readable setup plan
@@ -625,7 +625,8 @@ Options:
625
625
  --workflow <standard|strict> Rendered agent context workflow mode (default: standard)
626
626
  --with-husky Include Husky hook targets
627
627
  --with-ci Include GitHub Actions target
628
- --personal Use local-only install: no package.json, agent docs, Husky, CI, or .gitignore writes
628
+ --personal Use local-only install: no package.json, agent docs, Husky, CI, .gitignore, GitHub CLI, secrets, or CI setting writes.
629
+ With --agent claude, initializes .phasegate-local config/settings/skills and ignored .claude shims.
629
630
  --json Output machine-readable JSON
630
631
  --help, -h Show this help`,
631
632
  "setup:agent": `Usage: phasegate setup:agent [options]
@@ -35,7 +35,7 @@ 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 設定確認 |
38
+ | personal config | `.phasegate-local/phasegate.config.json` | `install --personal` の local-only 設定確認 |
39
39
  | git local exclude | `.git/info/exclude` | `install --personal` の repository-local ignore block 確認 |
40
40
  | Claude hooks | `.claude/settings.json` | managed hook JSON と user customization の確認 |
41
41
  | Codex hooks | `.codex/hooks.json` | managed hook JSON と Codex hook 配線確認 |
@@ -129,7 +129,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
129
129
  #### 観点 9: setup lifecycle と doctor finding
130
130
 
131
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 -->
132
+ - チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --agent claude --dry-run` を先に提案する。personal install は `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, CI settings, `.codex/hooks.json` を変更対象にしない。Claude Code向けには `.phasegate-local/phasegate.config.json`, `.phasegate-local/claude/settings.json`, `.phasegate-local/skills/`, ignored root `.claude/settings.json` / `.claude/skills` shim, `.phasegate/manifest.json`, `.git/info/exclude` の managed block を自動作成する。既存 `.claude/*` がある場合は上書きせず manual review として扱う。Codex user-level hook setup は manual action として扱う。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 -->
133
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 -->
134
134
  - `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
135
135
  - manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
@@ -130,7 +130,7 @@ 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 -->
133
+ チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal --agent claude` は `package.json`、`AGENTS.md`、`CLAUDE.md`、`.husky/*`、`.github/workflows/*`、`.gitignore`、GitHub CLI config、repo secrets、CI settings、`.codex/hooks.json` を変更せず、`.phasegate-local/phasegate.config.json`、`.phasegate-local/claude/settings.json`、`.phasegate-local/skills/`、ignored root `.claude/settings.json` / `.claude/skills` shim、`.git/info/exclude` の managed block を使う。Codex user-level hook setup は manual action として説明する。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 -->
134
134
 
135
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 -->
136
136