phasegate 0.160.13 → 0.160.15

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.
Files changed (25) hide show
  1. package/README.md +3 -3
  2. package/docs/guide/getting-started.md +1 -1
  3. package/docs/guide/installation.md +3 -3
  4. package/docs/guide/setup-artifacts.md +12 -3
  5. package/docs/templates/personal/phasegate-local-config.json +73 -3
  6. package/docs/templates/project/phasegate.config.json +29 -0
  7. package/package.json +1 -1
  8. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +19 -9
  9. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +19 -9
  10. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +23 -12
  11. package/scripts/harness/agent-integration/presentation/stop-hook.ts +19 -9
  12. package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +3 -2
  13. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +10 -4
  14. package/scripts/harness/installation/application/checks/check-utils.ts +5 -0
  15. package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +5 -2
  16. package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +5 -2
  17. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +36 -2
  18. package/scripts/harness/installation/application/usecases/run-install.ts +200 -61
  19. package/scripts/harness/installation/application/usecases/run-uninstall.ts +39 -4
  20. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +2 -0
  21. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +11 -3
  22. package/scripts/harness/main.ts +3 -2
  23. package/skills/README.md +1 -1
  24. package/skills/phasegate-config-doctor/SKILL.md +2 -2
  25. package/skills/phasegate-toolkit-guide/SKILL.md +1 -1
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. It creates `.phasegate-local/phasegate.config.json`, real local-only agent runtime artifacts for the selected agent (`.claude/settings.json` + `.claude/skills/` and/or `.codex/hooks.json` + `.codex/skills/`), a managed local-only block in `.git/info/exclude`, and `.phasegate/manifest.json`. Codex user-level hook feature enablement remains a manual action. <!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
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 and ignored real `.claude/*` / `.codex/*` runtime artifacts expose hooks and skills to selected agents |
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. PhaseGate writes `.phasegate-local/phasegate.config.json`, creates real local-only agent runtime artifacts for the selected agent (`.claude/settings.json` + `.claude/skills/` and/or `.codex/hooks.json` + `.codex/skills/`), records `.phasegate/manifest.json`, and manages a local exclude block in `.git/info/exclude`. Codex user-level hook feature enablement is still reported as a manual action. <!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
69
69
 
70
70
  For agent-readable planning before writing files:
71
71
 
@@ -5,16 +5,18 @@ 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 -->
9
+ <!-- @work-item-id WI-209 -->
8
10
 
9
11
  ## Artifact Classes
10
12
 
11
13
  | Class | Examples | Owner | Lifecycle |
12
14
  |---|---|---|---|
13
- | Managed target | `.claude/settings.json`, `.codex/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
14
- | Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init`; `install` may merge scripts/devDependency into `package.json` |
15
+ | Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
16
+ | Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init` or project install when absent; `install` may merge scripts/devDependency into `package.json` |
15
17
  | 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
18
  | 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 |
19
+ | Personal install artifact | `.phasegate-local/phasegate.config.json`, ignored real `.claude/settings.json` + `.claude/skills/`, ignored real `.codex/hooks.json` + `.codex/skills/`, `.git/info/exclude` PhaseGate block | One developer on one machine | Created by `phasegate install --personal --agent <agent>`; team `.gitignore`, team-owned files, GitHub CLI config, repo secrets, and CI settings are not modified |
18
20
  | 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
21
  | 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
22
 
@@ -27,6 +29,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
27
29
  - Husky scripts when requested: `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`
28
30
  - CI workflow when requested: `.github/workflows/phasegate-aidlc-gate.yml`
29
31
  - Agent skill links: `.claude/skills`, `.codex/skills`
32
+ - Project config: `phasegate.config.json` when absent, so installed agent hooks have a discoverable runtime config
30
33
  - Package metadata: PhaseGate scripts and `devDependencies.phasegate` in `package.json`
31
34
  - Manifest: `.phasegate/manifest.json`
32
35
 
@@ -34,6 +37,12 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
34
37
 
35
38
  `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
39
 
40
+ ## Personal Install Artifacts
41
+
42
+ `phasegate install --personal --agent claude --apply` is a local-only bootstrap for evaluating PhaseGate in a team-owned repository. PhaseGate config is stored under `.phasegate-local/`, while selected agent runtime paths are real project-local files/directories: `.claude/settings.json` and `.claude/skills/` for Claude Code, `.codex/hooks.json` and `.codex/skills/` for Codex. These paths are hidden by `.git/info/exclude`; PhaseGate does not edit team `.gitignore`.
43
+
44
+ If `.claude/*` or `.codex/*` already exists and is not a PhaseGate-managed personal artifact, personal install reports manual review and preserves the existing path. <!-- @work-item-id WI-209 -->
45
+
37
46
  ## Doctor Findings
38
47
 
39
48
  `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
  }
@@ -0,0 +1,29 @@
1
+ {
2
+ "project": {
3
+ "name": "phasegate-project",
4
+ "preset": "standard"
5
+ },
6
+ "architecture": {
7
+ "preset": "clean"
8
+ },
9
+ "layers": {},
10
+ "quickMode": {},
11
+ "phaseDependencies": {
12
+ "preset": "default",
13
+ "override": false,
14
+ "customRules": []
15
+ },
16
+ "planningMode": {
17
+ "default": "interactive",
18
+ "perPhase": {}
19
+ },
20
+ "harnesses": {},
21
+ "paths": {
22
+ "designDocs": "docs/product/construction",
23
+ "inceptionDocs": "docs/inception"
24
+ },
25
+ "reporting": {
26
+ "format": "json",
27
+ "outputDir": "reports"
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.13",
3
+ "version": "0.160.15",
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);
@@ -1,6 +1,7 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-209
4
5
 
5
6
  import { join } from "node:path";
6
7
  import type { CheckId } from "../../domain/check-id.js";
@@ -63,3 +64,7 @@ export function skillTargetLooksValid(target: string | null): boolean {
63
64
  const normalized = target.replaceAll("\\", "/").replace(/\/+$/, "");
64
65
  return normalized === "skills" || normalized === "../skills" || normalized.endsWith("/skills");
65
66
  }
67
+
68
+ export function skillDirectoryLooksValid(files: readonly string[]): boolean {
69
+ return files.some((file) => file.endsWith("/SKILL.md") || file.endsWith("\\SKILL.md") || file.endsWith(".harness-version"));
70
+ }
@@ -1,11 +1,12 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-209
4
5
 
5
6
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
7
  import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
8
  import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
- import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
9
+ import { createFinding, projectPath, skillDirectoryLooksValid, skillTargetLooksValid } from "./check-utils.js";
9
10
 
10
11
  export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
11
12
  readonly checkId = "claude-skills-symlink" as const;
@@ -14,11 +15,13 @@ export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
14
15
  async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
16
  const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
16
17
  if (skillTargetLooksValid(link)) return null;
18
+ const files = await inspector.listFiles(projectPath(projectRoot, this.target));
19
+ if (skillDirectoryLooksValid(files)) return null;
17
20
  return createFinding({
18
21
  checkId: this.checkId,
19
22
  severity: "red",
20
23
  target: this.target,
21
- message: ".claude/skills phasegate skills を指していません",
24
+ message: ".claude/skills phasegate skills がありません",
22
25
  repairMode: link === null ? "mechanical" : "manual",
23
26
  });
24
27
  }
@@ -1,11 +1,12 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-209
4
5
 
5
6
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
7
  import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
8
  import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
- import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
9
+ import { createFinding, projectPath, skillDirectoryLooksValid, skillTargetLooksValid } from "./check-utils.js";
9
10
 
10
11
  export class CodexSkillsSymlinkCheck implements HeuristicCheck {
11
12
  readonly checkId = "codex-skills-symlink" as const;
@@ -14,11 +15,13 @@ export class CodexSkillsSymlinkCheck implements HeuristicCheck {
14
15
  async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
16
  const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
16
17
  if (skillTargetLooksValid(link)) return null;
18
+ const files = await inspector.listFiles(projectPath(projectRoot, this.target));
19
+ if (skillDirectoryLooksValid(files)) return null;
17
20
  return createFinding({
18
21
  checkId: this.checkId,
19
22
  severity: "red",
20
23
  target: this.target,
21
- message: ".codex/skills phasegate skills を指していません",
24
+ message: ".codex/skills phasegate skills がありません",
22
25
  repairMode: link === null ? "mechanical" : "manual",
23
26
  });
24
27
  }
@@ -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;