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.
- package/README.md +3 -3
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/installation.md +3 -3
- package/docs/guide/setup-artifacts.md +12 -3
- package/docs/templates/personal/phasegate-local-config.json +73 -3
- package/docs/templates/project/phasegate.config.json +29 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +19 -9
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +19 -9
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +23 -12
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +19 -9
- package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +3 -2
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +10 -4
- package/scripts/harness/installation/application/checks/check-utils.ts +5 -0
- package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +5 -2
- package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +5 -2
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +36 -2
- package/scripts/harness/installation/application/usecases/run-install.ts +200 -61
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +39 -4
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +2 -0
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +11 -3
- package/scripts/harness/main.ts +3 -2
- package/skills/README.md +1 -1
- package/skills/phasegate-config-doctor/SKILL.md +2 -2
- 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/*`,
|
|
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
|
|
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/*`,
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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,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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 ?
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
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:
|
|
163
|
+
baseDir: projectRoot,
|
|
153
164
|
configQueryPort,
|
|
154
165
|
});
|
|
155
166
|
const errorGuidanceQueryPort = new HarnessErrorGuidanceAdapter({
|
|
156
|
-
rootDir:
|
|
167
|
+
rootDir: projectRoot,
|
|
157
168
|
});
|
|
158
169
|
const fullModeSessionQueryPort = new FileSystemFullModeSessionQueryAdapter({
|
|
159
|
-
rootDir:
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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 ?
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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.
|
|
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;
|