phasegate 0.160.12 → 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 +9 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/installation.md +9 -0
- package/docs/guide/recipes.md +9 -0
- package/docs/guide/setup-artifacts.md +8 -0
- package/docs/templates/personal/git-info-exclude +6 -0
- package/docs/templates/personal/phasegate-local-config.json +77 -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/usecases/run-doctor-diagnostics.ts +36 -2
- package/scripts/harness/installation/application/usecases/run-install.ts +204 -49
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +49 -5
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +2 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +3 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +11 -3
- package/scripts/harness/main.ts +8 -4
- package/skills/README.md +2 -0
- package/skills/phasegate-config-doctor/SKILL.md +3 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -102,6 +102,15 @@ npx phasegate doctor
|
|
|
102
102
|
|
|
103
103
|
`install` merges PhaseGate into the current project without discarding existing Claude/Codex hooks or Husky scripts. It reports planned changes before writing, adds package scripts and the `phasegate` devDependency, creates agent skill links, writes `AGENTS.md` / `CLAUDE.md` PhaseGate managed sections for the selected agent targets, writes the CI workflow when missing, and records managed files in `.phasegate/manifest.json`. If an existing file needs a forced managed update, run `npx phasegate install --apply --force`; PhaseGate backs up replaced files under `.phasegate/backups/`. <!-- @work-item-id WI-174 -->
|
|
104
104
|
|
|
105
|
+
For personal evaluation inside a team-owned repository, use local-only install:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx phasegate install --personal --agent claude --dry-run
|
|
109
|
+
npx phasegate install --personal --agent claude --apply
|
|
110
|
+
```
|
|
111
|
+
|
|
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
|
+
|
|
105
114
|
For agent-driven setup planning, use:
|
|
106
115
|
|
|
107
116
|
```bash
|
|
@@ -12,6 +12,7 @@ Start with one question: what kind of repository are you setting up?
|
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| New project with agent hooks | `npx phasegate init --name <project> --agent both --with-husky --with-ci` | `phasegate.config.json`, skills, hooks, agent context files, Husky, and CI targets exist |
|
|
14
14
|
| Existing repository | `npx phasegate install --dry-run` then `npx phasegate install --apply` | Existing hooks/scripts are preserved and `.phasegate/manifest.json` records managed targets |
|
|
15
|
+
| Personal evaluation in a team repository | `npx phasegate install --personal --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 |
|
|
15
16
|
| CI-only rollout | `npx phasegate setup:agent --intent ci-only --with-ci --dry-run --json` | The plan explains CI changes without requiring local hooks |
|
|
16
17
|
| Codex-only project | `npx phasegate init --agent codex --with-husky` | `.codex/hooks.json`, `AGENTS.md`, `.codex/skills`, and pre-commit backstop are present |
|
|
17
18
|
| Strict validation rollout | `npx phasegate setup:agent --intent strict --dry-run --json` | The plan lists strict checks, L4 risk, rollback, and validation commands |
|
|
@@ -58,6 +58,15 @@ npx phasegate doctor
|
|
|
58
58
|
|
|
59
59
|
`install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, creates `.claude/skills` and `.codex/skills` links, writes `CLAUDE.md` / `AGENTS.md` managed sections for selected agent targets, writes `.github/workflows/phasegate-aidlc-gate.yml` when CI is enabled, and records managed entries in `.phasegate/manifest.json`. See [Setup Artifacts](setup-artifacts.md) for the full managed target, generated artifact, runtime state, legacy artifact, and user-level setting inventory. <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 --> <!-- @work-item-id WI-174 -->
|
|
60
60
|
|
|
61
|
+
For personal evaluation inside a team-owned repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx phasegate install --personal --agent claude --dry-run
|
|
65
|
+
npx phasegate install --personal --agent claude --apply
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Personal install keeps team-owned files out of both the plan and apply path: `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.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
|
+
|
|
61
70
|
For agent-readable planning before writing files:
|
|
62
71
|
|
|
63
72
|
```bash
|
package/docs/guide/recipes.md
CHANGED
|
@@ -24,6 +24,15 @@ npx phasegate baseline --dry-run
|
|
|
24
24
|
|
|
25
25
|
Use this when a repository already has meaningful history or custom automation. Treat any refused target as an agent-assisted review task instead of forcing it blindly.
|
|
26
26
|
|
|
27
|
+
## Personal Evaluation Recipe
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx phasegate install --personal --dry-run
|
|
31
|
+
npx phasegate install --personal --apply
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use this inside a team-owned repository when you want local PhaseGate artifacts without changing `package.json`, agent context files, Husky hooks, CI workflows, or team `.gitignore`. <!-- @work-item-id WI-207 -->
|
|
35
|
+
|
|
27
36
|
## Agent Hooks Recipe
|
|
28
37
|
|
|
29
38
|
```bash
|
|
@@ -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,6 +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 |
|
|
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 |
|
|
17
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 |
|
|
18
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 |
|
|
19
21
|
|
|
@@ -33,6 +35,12 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
33
35
|
|
|
34
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.
|
|
35
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
|
+
|
|
36
44
|
## Doctor Findings
|
|
37
45
|
|
|
38
46
|
`phasegate doctor` evaluates setup health from the managed targets and related project state. Findings include `repairMode`, optional `repairHint`, and optional `suggestedSkill`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": {
|
|
3
|
+
"name": "personal-phasegate",
|
|
4
|
+
"preset": "standard"
|
|
5
|
+
},
|
|
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
|
+
}
|
|
77
|
+
}
|
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);
|
|
@@ -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;
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// @work-item-id WI-177
|
|
8
8
|
// @work-item-id WI-182
|
|
9
9
|
// @work-item-id WI-183
|
|
10
|
+
// @work-item-id WI-207
|
|
11
|
+
// @work-item-id WI-208
|
|
10
12
|
|
|
11
|
-
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";
|
|
12
14
|
import { dirname, join } from "node:path";
|
|
13
15
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
14
16
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
@@ -18,7 +20,7 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
18
20
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
19
21
|
|
|
20
22
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
21
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed";
|
|
23
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy" | "copy-dir" | "symlink";
|
|
22
24
|
|
|
23
25
|
export interface InstallPlanItem {
|
|
24
26
|
readonly path: string;
|
|
@@ -45,6 +47,7 @@ export interface RunInstallInput {
|
|
|
45
47
|
readonly skillSet?: "core" | "all";
|
|
46
48
|
readonly workflow?: "standard" | "strict";
|
|
47
49
|
readonly agent?: "claude" | "codex" | "both";
|
|
50
|
+
readonly personal?: boolean;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface RunInstallResult {
|
|
@@ -79,6 +82,10 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
79
82
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
80
83
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
81
84
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
85
|
+
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
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";
|
|
82
89
|
|
|
83
90
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
91
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -101,6 +108,20 @@ async function readTextOrNull(path: string): Promise<string | null> {
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
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
|
+
|
|
104
125
|
function normalizeJsonEntry(value: unknown): string {
|
|
105
126
|
return JSON.stringify(value);
|
|
106
127
|
}
|
|
@@ -152,6 +173,14 @@ function mergeShell(existing: string | null, incoming: string): string {
|
|
|
152
173
|
return `${existing.replace(/\s*$/, "\n\n")}${block}\n`;
|
|
153
174
|
}
|
|
154
175
|
|
|
176
|
+
function mergeTextManaged(existing: string | null, incoming: string): string {
|
|
177
|
+
const block = `${TEXT_BEGIN}\n${incoming.trim()}\n${TEXT_END}`;
|
|
178
|
+
if (existing === null || existing.trim().length === 0) return `${block}\n`;
|
|
179
|
+
const pattern = new RegExp(`${escapeRegExp(TEXT_BEGIN)}[\\s\\S]*?${escapeRegExp(TEXT_END)}`);
|
|
180
|
+
if (pattern.test(existing)) return existing.replace(pattern, block).replace(/\s*$/, "\n");
|
|
181
|
+
return `${existing.replace(/\s*$/, "\n\n")}${block}\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
function managedMarkdownBlock(content: string): string {
|
|
156
185
|
const start = content.indexOf(MARKDOWN_BEGIN);
|
|
157
186
|
const end = content.indexOf(MARKDOWN_END);
|
|
@@ -282,7 +311,9 @@ export class RunInstallUseCase {
|
|
|
282
311
|
const skillSet = input.skillSet ?? "all";
|
|
283
312
|
const workflow = input.workflow ?? "standard";
|
|
284
313
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
285
|
-
const targets =
|
|
314
|
+
const targets = input.personal
|
|
315
|
+
? this.createPersonalTargets({ includeClaude })
|
|
316
|
+
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
286
317
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
287
318
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
288
319
|
let manifest = baseManifest;
|
|
@@ -326,23 +357,23 @@ export class RunInstallUseCase {
|
|
|
326
357
|
await this.backup(input.projectRoot, target.path, backupDir);
|
|
327
358
|
}
|
|
328
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) {
|
|
329
371
|
try {
|
|
330
|
-
await
|
|
331
|
-
} catch (error) {
|
|
332
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
await writeFile(absolutePath, next, "utf8");
|
|
372
|
+
await chmod(absolutePath, 0o755);
|
|
336
373
|
} catch (error) {
|
|
337
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "
|
|
338
|
-
}
|
|
339
|
-
if (target.executable) {
|
|
340
|
-
try {
|
|
341
|
-
await chmod(absolutePath, 0o755);
|
|
342
|
-
} catch (error) {
|
|
343
|
-
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
|
|
344
|
-
}
|
|
374
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
|
|
345
375
|
}
|
|
376
|
+
}
|
|
346
377
|
changed.push(item);
|
|
347
378
|
|
|
348
379
|
const mode = before === null ? "created" : "merged";
|
|
@@ -363,41 +394,76 @@ export class RunInstallUseCase {
|
|
|
363
394
|
}
|
|
364
395
|
}
|
|
365
396
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
435
|
plan.push(item);
|
|
373
436
|
if (!input.apply || !item.changed) continue;
|
|
374
437
|
try {
|
|
375
|
-
await mkdir(join(input.projectRoot, "skills"), { recursive: true });
|
|
376
|
-
await mkdir(dirname(join(input.projectRoot,
|
|
438
|
+
if (!input.personal) await mkdir(join(input.projectRoot, "skills"), { recursive: true });
|
|
439
|
+
await mkdir(dirname(join(input.projectRoot, linkSpec.path)), { recursive: true });
|
|
377
440
|
} catch (error) {
|
|
378
|
-
return this.withApplyError({ plan, refused, changed, backupDir },
|
|
441
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "mkdir", error);
|
|
379
442
|
}
|
|
380
443
|
try {
|
|
381
|
-
await symlink(
|
|
444
|
+
await symlink(linkSpec.target, join(input.projectRoot, linkSpec.path), process.platform === "win32" ? "junction" : "dir");
|
|
382
445
|
} catch (error) {
|
|
383
|
-
return this.withApplyError({ plan, refused, changed, backupDir },
|
|
446
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "symlink", error);
|
|
384
447
|
}
|
|
385
448
|
changed.push(item);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
449
|
+
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
450
|
+
path: linkSpec.path,
|
|
451
|
+
mode: "symlink",
|
|
452
|
+
contentForHash: linkSpec.target,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (input.personal && includeCodex) {
|
|
457
|
+
plan.push({
|
|
458
|
+
path: "~/.codex/hooks.json",
|
|
459
|
+
action: "will-skip",
|
|
460
|
+
repairMode: "manual",
|
|
461
|
+
strategy: "json",
|
|
462
|
+
changed: false,
|
|
463
|
+
summary: "~/.codex/hooks.json: personal mode does not write user-level Codex settings; configure hooks manually",
|
|
464
|
+
diff: "manual user-level Codex hook setup required",
|
|
465
|
+
skillHint: null,
|
|
466
|
+
});
|
|
401
467
|
}
|
|
402
468
|
|
|
403
469
|
if (input.apply && changed.length > 0) {
|
|
@@ -498,8 +564,37 @@ export class RunInstallUseCase {
|
|
|
498
564
|
];
|
|
499
565
|
}
|
|
500
566
|
|
|
567
|
+
private createPersonalTargets(options: { readonly includeClaude: boolean }): readonly InstallTarget[] {
|
|
568
|
+
return [
|
|
569
|
+
{
|
|
570
|
+
path: ".phasegate-local/phasegate.config.json",
|
|
571
|
+
strategy: "copy" as const,
|
|
572
|
+
templatePath: "docs/templates/personal/phasegate-local-config.json",
|
|
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
|
+
: []),
|
|
583
|
+
{
|
|
584
|
+
path: ".git/info/exclude",
|
|
585
|
+
strategy: "text-managed" as const,
|
|
586
|
+
templatePath: "docs/templates/personal/git-info-exclude",
|
|
587
|
+
block: { start: TEXT_BEGIN, end: TEXT_END, content: "phasegate personal install exclude block" },
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
|
|
501
592
|
private repairMode(target: InstallTarget, before: string | null): RepairMode {
|
|
502
593
|
if (target.strategy === "shell") return shellRepairMode(before);
|
|
594
|
+
if (target.strategy === "text-managed") return "mechanical";
|
|
595
|
+
if (target.strategy === "copy") return "mechanical";
|
|
596
|
+
if (target.strategy === "copy-dir") return "mechanical";
|
|
597
|
+
if (target.strategy === "symlink") return "mechanical";
|
|
503
598
|
if (target.strategy === "json") return jsonRepairMode(before);
|
|
504
599
|
if (target.strategy === "markdown-managed") return "mechanical";
|
|
505
600
|
return "mechanical";
|
|
@@ -513,7 +608,9 @@ export class RunInstallUseCase {
|
|
|
513
608
|
|
|
514
609
|
private merge(target: InstallTarget, before: string | null, template: string, version: string): string {
|
|
515
610
|
if (target.strategy === "yaml-add") return before ?? template;
|
|
611
|
+
if (target.strategy === "copy") return before ?? template;
|
|
516
612
|
if (target.strategy === "shell") return mergeShell(before, template);
|
|
613
|
+
if (target.strategy === "text-managed") return mergeTextManaged(before, template);
|
|
517
614
|
if (target.strategy === "markdown-managed") return mergeManagedMarkdown(before, template);
|
|
518
615
|
if (target.strategy === "package-json") {
|
|
519
616
|
const existing = before === null ? {} : (JSON.parse(before) as unknown);
|
|
@@ -542,16 +639,59 @@ export class RunInstallUseCase {
|
|
|
542
639
|
};
|
|
543
640
|
}
|
|
544
641
|
|
|
545
|
-
private async
|
|
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> {
|
|
546
685
|
const absolutePath = join(projectRoot, relativePath);
|
|
686
|
+
const parentPath = dirname(absolutePath);
|
|
547
687
|
try {
|
|
548
688
|
const stat = await lstat(absolutePath);
|
|
549
|
-
if (stat.isSymbolicLink() && (await readlink(absolutePath)) ===
|
|
689
|
+
if (stat.isSymbolicLink() && (await readlink(absolutePath)) === target) {
|
|
550
690
|
return {
|
|
551
691
|
path: relativePath,
|
|
552
692
|
action: "will-skip",
|
|
553
693
|
repairMode: "mechanical",
|
|
554
|
-
strategy: "
|
|
694
|
+
strategy: "symlink",
|
|
555
695
|
changed: false,
|
|
556
696
|
summary: `${relativePath}: already linked`,
|
|
557
697
|
diff: "no changes",
|
|
@@ -562,21 +702,36 @@ export class RunInstallUseCase {
|
|
|
562
702
|
path: relativePath,
|
|
563
703
|
action: "will-merge",
|
|
564
704
|
repairMode: "manual",
|
|
565
|
-
strategy: "
|
|
705
|
+
strategy: "symlink",
|
|
566
706
|
changed: false,
|
|
567
707
|
summary: `${relativePath}: existing non-phasegate path requires manual review`,
|
|
568
708
|
diff: "manual review required",
|
|
569
709
|
skillHint: null,
|
|
570
710
|
};
|
|
571
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 {}
|
|
572
727
|
return {
|
|
573
728
|
path: relativePath,
|
|
574
729
|
action: "missing",
|
|
575
730
|
repairMode: "mechanical",
|
|
576
|
-
strategy: "
|
|
731
|
+
strategy: "symlink",
|
|
577
732
|
changed: true,
|
|
578
733
|
summary: `${relativePath}: create symlink`,
|
|
579
|
-
diff:
|
|
734
|
+
diff: `+ symlink ${target}`,
|
|
580
735
|
skillHint: null,
|
|
581
736
|
};
|
|
582
737
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// @work-item-id WI-147
|
|
4
4
|
// @work-item-id WI-174
|
|
5
5
|
// @work-item-id WI-199
|
|
6
|
+
// @work-item-id WI-207
|
|
7
|
+
// @work-item-id WI-208
|
|
6
8
|
|
|
7
9
|
import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
|
|
8
10
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -12,7 +14,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
|
12
14
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
13
15
|
|
|
14
16
|
type UninstallAction = "missing-manifest" | "delete" | "unlink" | "reverse-merge" | "skip" | "refuse";
|
|
15
|
-
type StrategyType = "created" | "json" | "shell" | "package-json" | "markdown-managed" | "symlink" | "yaml-add" | "unknown";
|
|
17
|
+
type StrategyType = "created" | "json" | "shell" | "package-json" | "markdown-managed" | "symlink" | "yaml-add" | "text-managed" | "unknown";
|
|
16
18
|
|
|
17
19
|
export interface UninstallPlanItem {
|
|
18
20
|
readonly path: string;
|
|
@@ -47,6 +49,8 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
47
49
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
48
50
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
49
51
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
52
|
+
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
53
|
+
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
50
54
|
const PHASEGATE_SCRIPT_PREFIX = "phasegate:";
|
|
51
55
|
const PROTECTED_UNINSTALL_PATHS = new Set(["package.json", "package-lock.json"]);
|
|
52
56
|
|
|
@@ -119,6 +123,11 @@ export function reverseManagedMarkdown(currentContent: string): string {
|
|
|
119
123
|
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
export function reverseTextManaged(currentContent: string): string {
|
|
127
|
+
const pattern = new RegExp(`\\n?${escapeRegExp(TEXT_BEGIN)}[\\s\\S]*?${escapeRegExp(TEXT_END)}\\n?`);
|
|
128
|
+
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
export function reversePackageJsonMerge(currentContent: string): string {
|
|
123
132
|
const parsed = JSON.parse(currentContent) as unknown;
|
|
124
133
|
const result = isRecord(parsed) ? { ...parsed } : {};
|
|
@@ -155,6 +164,7 @@ export class RunUninstallUseCase {
|
|
|
155
164
|
repairMode: "manual",
|
|
156
165
|
strategy: "unknown",
|
|
157
166
|
changed: false,
|
|
167
|
+
protected: false,
|
|
158
168
|
summary: "manifest missing; run phasegate doctor and clean up manually",
|
|
159
169
|
diff: "manual cleanup required",
|
|
160
170
|
skillHint: SKILL_HINT,
|
|
@@ -208,8 +218,12 @@ export class RunUninstallUseCase {
|
|
|
208
218
|
readonly apply: () => Promise<void>;
|
|
209
219
|
}> {
|
|
210
220
|
const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
|
|
211
|
-
const currentContent = await readTextOrNull(absolutePath);
|
|
212
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);
|
|
213
227
|
if (currentContent === null && strategy !== "symlink") {
|
|
214
228
|
return {
|
|
215
229
|
item: this.item(entry.path, "skip", "mechanical", strategy, false, `${entry.path}: already absent`, "no changes", null),
|
|
@@ -218,7 +232,6 @@ export class RunUninstallUseCase {
|
|
|
218
232
|
};
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
if (entry.mode === "symlink") return this.planSymlink(input.projectRoot, entry);
|
|
222
235
|
if (entry.mode === "created") return this.planCreated(input.projectRoot, entry, currentContent ?? "");
|
|
223
236
|
return this.planMerged(input, entry, currentContent ?? "", strategy);
|
|
224
237
|
}
|
|
@@ -232,9 +245,9 @@ export class RunUninstallUseCase {
|
|
|
232
245
|
} catch {
|
|
233
246
|
target = null;
|
|
234
247
|
}
|
|
235
|
-
if (target
|
|
248
|
+
if (target !== null && this.hashCalculator.compute(target).equals(entry.hash)) {
|
|
236
249
|
return {
|
|
237
|
-
item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`,
|
|
250
|
+
item: this.item(entry.path, "unlink", "mechanical", "symlink", true, `${entry.path}: remove symlink`, `- symlink ${target}`, null),
|
|
238
251
|
needsBackup: false,
|
|
239
252
|
apply: async () => {
|
|
240
253
|
await rm(absolutePath, { force: true });
|
|
@@ -248,6 +261,35 @@ export class RunUninstallUseCase {
|
|
|
248
261
|
};
|
|
249
262
|
}
|
|
250
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
|
+
|
|
251
293
|
private async planCreated(projectRoot: string, entry: DeploymentEntry, currentContent: string) {
|
|
252
294
|
const absolutePath = this.resolveProjectPath(projectRoot, entry.path);
|
|
253
295
|
const currentHash = this.hashCalculator.compute(currentContent);
|
|
@@ -312,6 +354,7 @@ export class RunUninstallUseCase {
|
|
|
312
354
|
private async reverseMerged(harnessRoot: string, path: string, currentContent: string, strategy: StrategyType): Promise<string> {
|
|
313
355
|
if (strategy === "shell") return currentContent.includes(SHELL_BEGIN) ? reverseShellMerge(currentContent) : currentContent;
|
|
314
356
|
if (strategy === "markdown-managed") return currentContent.includes(MARKDOWN_BEGIN) ? reverseManagedMarkdown(currentContent) : currentContent;
|
|
357
|
+
if (strategy === "text-managed") return currentContent.includes(TEXT_BEGIN) ? reverseTextManaged(currentContent) : currentContent;
|
|
315
358
|
if (strategy === "package-json") return reversePackageJsonMerge(currentContent);
|
|
316
359
|
if (strategy === "json") return reverseJsonMerge(currentContent, await readFile(join(harnessRoot, this.templateFor(path)), "utf8"));
|
|
317
360
|
throw new Error(`Unsupported merged strategy: ${strategy}`);
|
|
@@ -322,6 +365,7 @@ export class RunUninstallUseCase {
|
|
|
322
365
|
if (mode === "created") return path.endsWith(".yml") || path.endsWith(".yaml") ? "yaml-add" : "created";
|
|
323
366
|
if (path === "package.json") return "package-json";
|
|
324
367
|
if (path === "AGENTS.md" || path === "CLAUDE.md") return "markdown-managed";
|
|
368
|
+
if (path === ".git/info/exclude") return "text-managed";
|
|
325
369
|
if (path.endsWith(".json")) return "json";
|
|
326
370
|
if (path.startsWith(".husky/")) return "shell";
|
|
327
371
|
return "unknown";
|
|
@@ -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,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer presentation
|
|
3
3
|
// @work-item-id WI-146
|
|
4
4
|
// @work-item-id WI-175
|
|
5
|
+
// @work-item-id WI-207
|
|
5
6
|
|
|
6
7
|
import type { RunInstallUseCase } from "../../application/usecases/run-install.js";
|
|
7
8
|
|
|
@@ -20,6 +21,7 @@ export interface InstallHandlerInput {
|
|
|
20
21
|
readonly skillSet?: "core" | "all";
|
|
21
22
|
readonly workflow?: "standard" | "strict";
|
|
22
23
|
readonly agent?: "claude" | "codex" | "both";
|
|
24
|
+
readonly personal?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface InstallHandlerResult {
|
|
@@ -39,7 +41,7 @@ export class InstallHandler {
|
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
const lines = [
|
|
42
|
-
input.apply ?
|
|
44
|
+
input.apply ? `phasegate install${input.personal ? " --personal" : ""} apply` : `phasegate install${input.personal ? " --personal" : ""} dry-run`,
|
|
43
45
|
...result.plan.map((item) => {
|
|
44
46
|
const hint = item.skillHint ? `; hint: ${item.skillHint}` : "";
|
|
45
47
|
return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}`;
|
|
@@ -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
|
-
|
|
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.";
|
package/scripts/harness/main.ts
CHANGED
|
@@ -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)
|
|
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,6 +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, .gitignore, GitHub CLI, secrets, or CI setting writes.
|
|
629
|
+
With --agent claude, initializes .phasegate-local config/settings/skills and ignored .claude shims.
|
|
628
630
|
--json Output machine-readable JSON
|
|
629
631
|
--help, -h Show this help`,
|
|
630
632
|
"setup:agent": `Usage: phasegate setup:agent [options]
|
|
@@ -2045,7 +2047,7 @@ async function main(): Promise<void> {
|
|
|
2045
2047
|
}
|
|
2046
2048
|
|
|
2047
2049
|
case "install": {
|
|
2048
|
-
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json", "--agent", "--skills", "--workflow", "--with-husky", "--with-ci"];
|
|
2050
|
+
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json", "--agent", "--skills", "--workflow", "--with-husky", "--with-ci", "--personal"];
|
|
2049
2051
|
const flagError = validateKnownFlags(args, KNOWN_INSTALL_FLAGS);
|
|
2050
2052
|
if (flagError) {
|
|
2051
2053
|
console.error(flagError);
|
|
@@ -2066,6 +2068,7 @@ async function main(): Promise<void> {
|
|
|
2066
2068
|
}
|
|
2067
2069
|
const includeClaude = agent === "claude" || agent === "both";
|
|
2068
2070
|
const includeCodex = agent === "codex" || agent === "both";
|
|
2071
|
+
const personal = hasFlag(args, "--personal");
|
|
2069
2072
|
const mod = createInstallationModule();
|
|
2070
2073
|
const phasegateVersion = await getHarnessVersion(harnessRoot);
|
|
2071
2074
|
const result = await mod.installHandler.execute({
|
|
@@ -2077,11 +2080,12 @@ async function main(): Promise<void> {
|
|
|
2077
2080
|
force: hasFlag(args, "--force"),
|
|
2078
2081
|
includeClaude,
|
|
2079
2082
|
includeCodex,
|
|
2080
|
-
includeHusky: true,
|
|
2081
|
-
includeCi: true,
|
|
2083
|
+
includeHusky: personal ? false : true,
|
|
2084
|
+
includeCi: personal ? false : true,
|
|
2082
2085
|
skillSet: skillSetRaw,
|
|
2083
2086
|
workflow: parseWorkflowMode(workflowRaw),
|
|
2084
2087
|
agent,
|
|
2088
|
+
personal,
|
|
2085
2089
|
json,
|
|
2086
2090
|
});
|
|
2087
2091
|
console.log(result.stdout);
|
package/skills/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
`.agent/skills` は旧 setup 由来の互換パスです。新規導入では管理対象にしません。<!-- @work-item-id WI-157 -->
|
|
18
18
|
|
|
19
|
+
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは `.claude/skills` / `.codex/skills` のリンクを作成せず、`.phasegate-local/config.json` と `.git/info/exclude` の local-only managed block だけを扱います。通常の共有導入が必要になった時点で `phasegate install --dry-run` / `phasegate install --apply` を使います。<!-- @work-item-id WI-207 -->
|
|
20
|
+
|
|
19
21
|
## 新しいスキルの追加
|
|
20
22
|
|
|
21
23
|
新しいスキルを追加する場合は、この `skills` ディレクトリに直接追加してください。あわせて `docs/guide/skills-overview.md`、README の skill 数、必要なら `skills/phasegate-toolkit-guide/SKILL.md` の参照先を更新します。シンボリックリンクを通じて、対応エージェントから利用可能になります。<!-- @work-item-id WI-154 -->
|
|
@@ -35,6 +35,8 @@ phasegate を導入した直後の config は単純な default で、実プロ
|
|
|
35
35
|
| hook config | `.claude/scripts/hook-config.json` | 既存 hook 設定確認 |
|
|
36
36
|
| doctor report | 明示された report path、またはユーザーが指定した `.phasegate/last-doctor-report.json` | `repairMode` / `repairHint` / `suggestedSkill` の確認 |
|
|
37
37
|
| manifest | `.phasegate/manifest.json` | install / reconcile / uninstall の managed target と hash 状態確認 |
|
|
38
|
+
| personal config | `.phasegate-local/phasegate.config.json` | `install --personal` の local-only 設定確認 |
|
|
39
|
+
| git local exclude | `.git/info/exclude` | `install --personal` の repository-local ignore block 確認 |
|
|
38
40
|
| Claude hooks | `.claude/settings.json` | managed hook JSON と user customization の確認 |
|
|
39
41
|
| Codex hooks | `.codex/hooks.json` | managed hook JSON と Codex hook 配線確認 |
|
|
40
42
|
| Husky scripts | `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push` | pre-commit backstop と bypass audit の確認 |
|
|
@@ -127,6 +129,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
|
|
|
127
129
|
#### 観点 9: setup lifecycle と doctor finding
|
|
128
130
|
|
|
129
131
|
- `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
|
|
132
|
+
- チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --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 -->
|
|
130
133
|
- Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は raw `repairMode` が current scope の修復指示ではないという印である。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|
|
131
134
|
- `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
|
|
132
135
|
- manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
|
|
@@ -130,6 +130,8 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
|
|
|
130
130
|
- 既存プロジェクト導入: `docs/guide/retrofit-adoption.md`
|
|
131
131
|
- setup artifact / doctor finding / legacy artifact: `docs/guide/setup-artifacts.md`
|
|
132
132
|
|
|
133
|
+
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal --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
|
+
|
|
133
135
|
`setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
|
|
134
136
|
|
|
135
137
|
Claude-only / Codex-only setup の確認では、full `phasegate doctor` と scoped doctor を区別する。ユーザーが `setup:agent --agent claude` を選んだ場合は `phasegate doctor --agent claude --json` を優先し、`scopedOutFindings` の Codex finding は「未選択 agent の not-applicable 情報」であり修復対象ではないと説明する。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は `repairMode` が current scope の修復指示ではないという印である。full doctor は両 agent を導入したい場合の診断として扱う。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|