phasegate 0.160.14 → 0.160.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.ja.md +2 -2
- package/README.md +3 -3
- package/docs/guide/getting-started.md +1 -1
- package/docs/guide/installation.md +1 -1
- package/docs/guide/setup-artifacts.md +10 -5
- package/docs/guide/skills-overview.md +1 -1
- package/docs/templates/project/phasegate.config.json +29 -0
- package/package.json +1 -1
- package/scripts/harness/installation/application/checks/check-utils.ts +6 -0
- package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +8 -4
- package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +8 -4
- package/scripts/harness/installation/application/usecases/run-install.ts +164 -38
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +131 -12
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +1 -0
- package/skills/README.md +1 -1
- package/skills/phasegate-config-doctor/SKILL.md +1 -1
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.160.16] - 2026-05-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-210 — project install shared skills** — makes `phasegate install` deploy selected bundled skills to root `skills/` for project installs, validates missing skill target content in `doctor`, repairs older empty-skill installs through `reconcile` / `update-skills`, and keeps uninstall scoped to manifest-managed skill directories.
|
|
15
|
+
|
|
10
16
|
## [0.160.12] - 2026-05-20
|
|
11
17
|
|
|
12
18
|
### Added
|
package/README.ja.md
CHANGED
|
@@ -105,7 +105,7 @@ npx phasegate install --apply
|
|
|
105
105
|
npx phasegate doctor
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
`install` は既存の Claude / Codex hooks や Husky script を捨てずに PhaseGate の設定を merge します。書き込み前に変更予定を表示し、package scripts と `phasegate` devDependency、agent skill symlink、未作成の CI workflow、`.phasegate/manifest.json` を整えます。強制的な managed 更新が必要な場合は `npx phasegate install --apply --force` を使います。この場合、置き換え対象は `.phasegate/backups/`
|
|
108
|
+
`install` は既存の Claude / Codex hooks や Husky script を捨てずに PhaseGate の設定を merge します。書き込み前に変更予定を表示し、package scripts と `phasegate` devDependency、root `skills/` への selected bundled skills 配布、agent skill symlink、未作成の CI workflow、`.phasegate/manifest.json` を整えます。強制的な managed 更新が必要な場合は `npx phasegate install --apply --force` を使います。この場合、置き換え対象は `.phasegate/backups/` に退避されます。<!-- @work-item-id WI-210 -->
|
|
109
109
|
|
|
110
110
|
後で PhaseGate を外す場合は、manifest ベースの uninstall を使います。
|
|
111
111
|
|
|
@@ -123,7 +123,7 @@ npx phasegate reconcile --dry-run
|
|
|
123
123
|
npx phasegate reconcile --apply
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
`reconcile` は PhaseGate 管理部分だけを更新し、ユーザーの hook / script / dependency は保持します。新しい deploy target が追加されていれば install
|
|
126
|
+
`reconcile` は PhaseGate 管理部分だけを更新し、ユーザーの hook / script / dependency は保持します。新しい deploy target が追加されていれば install と同じく追加し、project install の shared skills 実体が欠落している場合も修復し、`.phasegate/manifest.json` の version / hash も更新します。install 後に user 改変された managed file は `--force` 無しでは refuse し、force 時は `.phasegate/backups/reconcile-<timestamp>/` に退避してから上書きします。<!-- @work-item-id WI-210 -->
|
|
127
127
|
|
|
128
128
|
### Codex CLI を使う場合
|
|
129
129
|
|
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ npx phasegate install --apply
|
|
|
100
100
|
npx phasegate doctor
|
|
101
101
|
```
|
|
102
102
|
|
|
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 -->
|
|
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, deploys selected bundled skills to root `skills/`, creates agent skill links to that shared directory, 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 --> <!-- @work-item-id WI-210 -->
|
|
104
104
|
|
|
105
105
|
For personal evaluation inside a team-owned repository, use local-only install:
|
|
106
106
|
|
|
@@ -109,7 +109,7 @@ npx phasegate install --personal --agent claude --dry-run
|
|
|
109
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/*`, `.gitignore`, GitHub CLI config, repo secrets, or CI settings.
|
|
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
|
|
|
@@ -138,7 +138,7 @@ npx phasegate reconcile --dry-run
|
|
|
138
138
|
npx phasegate reconcile --apply
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
`reconcile` updates only PhaseGate-managed portions, keeps user content, adds newly introduced managed targets, and refreshes `.phasegate/manifest.json` with the current version and hashes. If a managed file was edited after install, `reconcile --apply` refuses that entry until you rerun with `--force`; PhaseGate writes a backup under `.phasegate/backups/reconcile-<timestamp>/`.
|
|
141
|
+
`reconcile` updates only PhaseGate-managed portions, keeps user content, adds newly introduced managed targets, repairs missing shared skill bodies for project installs, and refreshes `.phasegate/manifest.json` with the current version and hashes. If a managed file was edited after install, `reconcile --apply` refuses that entry until you rerun with `--force`; PhaseGate writes a backup under `.phasegate/backups/reconcile-<timestamp>/`. <!-- @work-item-id WI-210 -->
|
|
142
142
|
|
|
143
143
|
### Codex CLI
|
|
144
144
|
|
|
@@ -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 --agent claude --dry-run` then `npx phasegate install --personal --agent claude --apply` | Team-owned files stay untouched; `.phasegate-local/` stores config
|
|
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 |
|
|
@@ -65,7 +65,7 @@ npx phasegate install --personal --agent claude --dry-run
|
|
|
65
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/*`, `.gitignore`, GitHub CLI config, repo secrets, and CI settings are not touched.
|
|
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
|
|
|
@@ -6,16 +6,17 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
6
6
|
<!-- @work-item-id WI-157 -->
|
|
7
7
|
<!-- @work-item-id WI-169 -->
|
|
8
8
|
<!-- @work-item-id WI-208 -->
|
|
9
|
+
<!-- @work-item-id WI-209 -->
|
|
9
10
|
|
|
10
11
|
## Artifact Classes
|
|
11
12
|
|
|
12
13
|
| Class | Examples | Owner | Lifecycle |
|
|
13
14
|
|---|---|---|---|
|
|
14
|
-
| 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` |
|
|
15
|
-
| 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`, root `skills/` bundled skill bodies, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, shared skill body, 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` |
|
|
16
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 |
|
|
17
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 |
|
|
18
|
-
| Personal install artifact | `.phasegate-local/phasegate.config.json`, `.
|
|
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 |
|
|
19
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 |
|
|
20
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 |
|
|
21
22
|
|
|
@@ -28,6 +29,8 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
28
29
|
- Husky scripts when requested: `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`
|
|
29
30
|
- CI workflow when requested: `.github/workflows/phasegate-aidlc-gate.yml`
|
|
30
31
|
- Agent skill links: `.claude/skills`, `.codex/skills`
|
|
32
|
+
- Project shared bundled skills: selected `skills/<name>/` directories plus `skills/.harness-version`
|
|
33
|
+
- Project config: `phasegate.config.json` when absent, so installed agent hooks have a discoverable runtime config
|
|
31
34
|
- Package metadata: PhaseGate scripts and `devDependencies.phasegate` in `package.json`
|
|
32
35
|
- Manifest: `.phasegate/manifest.json`
|
|
33
36
|
|
|
@@ -37,9 +40,11 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
37
40
|
|
|
38
41
|
## Personal Install Artifacts
|
|
39
42
|
|
|
40
|
-
`phasegate install --personal --agent claude --apply` is a local-only bootstrap for evaluating PhaseGate in a team-owned repository.
|
|
43
|
+
`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`.
|
|
41
44
|
|
|
42
|
-
If `.claude
|
|
45
|
+
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 -->
|
|
46
|
+
|
|
47
|
+
Project install uses a different topology: selected bundled skills are deployed once to root `skills/`, and `.claude/skills` / `.codex/skills` point to that shared target. If an older project install has the links but an empty `skills/` target, `phasegate doctor` reports the selected agent skill check and `phasegate reconcile --apply` repairs the shared skill bodies. <!-- @work-item-id WI-210 -->
|
|
43
48
|
|
|
44
49
|
## Doctor Findings
|
|
45
50
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skills Overview
|
|
2
2
|
|
|
3
|
-
Phasegate provides 30 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init`
|
|
3
|
+
Phasegate provides 30 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills/` / `.codex/skills/` links. Personal install instead writes real local-only per-agent skill directories. <!-- @work-item-id WI-210 -->
|
|
4
4
|
|
|
5
5
|
## AIDLC Process — Skill Execution Order
|
|
6
6
|
|
|
@@ -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,8 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-209
|
|
5
|
+
// @work-item-id WI-210
|
|
4
6
|
|
|
5
7
|
import { join } from "node:path";
|
|
6
8
|
import type { CheckId } from "../../domain/check-id.js";
|
|
@@ -63,3 +65,7 @@ export function skillTargetLooksValid(target: string | null): boolean {
|
|
|
63
65
|
const normalized = target.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
64
66
|
return normalized === "skills" || normalized === "../skills" || normalized.endsWith("/skills");
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
export function skillDirectoryLooksValid(files: readonly string[]): boolean {
|
|
70
|
+
return files.some((file) => file.endsWith("/SKILL.md") || file.endsWith("\\SKILL.md") || file.endsWith(".harness-version"));
|
|
71
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-209
|
|
5
|
+
// @work-item-id WI-210
|
|
4
6
|
|
|
5
7
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
8
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
9
|
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
8
|
-
import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
|
|
10
|
+
import { createFinding, projectPath, skillDirectoryLooksValid, skillTargetLooksValid } from "./check-utils.js";
|
|
9
11
|
|
|
10
12
|
export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
|
|
11
13
|
readonly checkId = "claude-skills-symlink" as const;
|
|
@@ -13,13 +15,15 @@ export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
|
|
|
13
15
|
|
|
14
16
|
async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
15
17
|
const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
|
|
16
|
-
|
|
18
|
+
const files = await inspector.listFiles(projectPath(projectRoot, this.target));
|
|
19
|
+
if (skillTargetLooksValid(link) && skillDirectoryLooksValid(files)) return null;
|
|
20
|
+
if (link === null && skillDirectoryLooksValid(files)) return null;
|
|
17
21
|
return createFinding({
|
|
18
22
|
checkId: this.checkId,
|
|
19
23
|
severity: "red",
|
|
20
24
|
target: this.target,
|
|
21
|
-
message: ".claude/skills
|
|
22
|
-
repairMode: link === null ? "mechanical" : "manual",
|
|
25
|
+
message: ".claude/skills に phasegate skills がありません",
|
|
26
|
+
repairMode: link === null || skillTargetLooksValid(link) ? "mechanical" : "manual",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-209
|
|
5
|
+
// @work-item-id WI-210
|
|
4
6
|
|
|
5
7
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
8
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
9
|
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
8
|
-
import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
|
|
10
|
+
import { createFinding, projectPath, skillDirectoryLooksValid, skillTargetLooksValid } from "./check-utils.js";
|
|
9
11
|
|
|
10
12
|
export class CodexSkillsSymlinkCheck implements HeuristicCheck {
|
|
11
13
|
readonly checkId = "codex-skills-symlink" as const;
|
|
@@ -13,13 +15,15 @@ export class CodexSkillsSymlinkCheck implements HeuristicCheck {
|
|
|
13
15
|
|
|
14
16
|
async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
15
17
|
const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
|
|
16
|
-
|
|
18
|
+
const files = await inspector.listFiles(projectPath(projectRoot, this.target));
|
|
19
|
+
if (skillTargetLooksValid(link) && skillDirectoryLooksValid(files)) return null;
|
|
20
|
+
if (link === null && skillDirectoryLooksValid(files)) return null;
|
|
17
21
|
return createFinding({
|
|
18
22
|
checkId: this.checkId,
|
|
19
23
|
severity: "red",
|
|
20
24
|
target: this.target,
|
|
21
|
-
message: ".codex/skills
|
|
22
|
-
repairMode: link === null ? "mechanical" : "manual",
|
|
25
|
+
message: ".codex/skills に phasegate skills がありません",
|
|
26
|
+
repairMode: link === null || skillTargetLooksValid(link) ? "mechanical" : "manual",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
// @work-item-id WI-183
|
|
10
10
|
// @work-item-id WI-207
|
|
11
11
|
// @work-item-id WI-208
|
|
12
|
+
// @work-item-id WI-209
|
|
13
|
+
// @work-item-id WI-210
|
|
12
14
|
|
|
13
|
-
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir } from "node:fs/promises";
|
|
15
|
+
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
|
|
14
16
|
import { dirname, join } from "node:path";
|
|
17
|
+
import { getSkillsForSet, type SkillSet } from "../../../setup/skill-deployer.js";
|
|
15
18
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
16
19
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
17
20
|
import type { ManagedBlockInput } from "../../domain/managed-block.js";
|
|
@@ -84,8 +87,8 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
|
84
87
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
85
88
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
86
89
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
87
|
-
const
|
|
88
|
-
const
|
|
90
|
+
const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/settings.json", ".codex/hooks.json"]);
|
|
91
|
+
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
89
92
|
|
|
90
93
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
91
94
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -122,6 +125,16 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet): Promise<string[]> {
|
|
129
|
+
const skillsSource = join(harnessRoot, "skills");
|
|
130
|
+
const allowed = new Set(getSkillsForSet(skillSet));
|
|
131
|
+
const entries = await readdir(skillsSource, { withFileTypes: true });
|
|
132
|
+
return entries
|
|
133
|
+
.filter((entry) => entry.isDirectory() && allowed.has(entry.name))
|
|
134
|
+
.map((entry) => entry.name)
|
|
135
|
+
.sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
function normalizeJsonEntry(value: unknown): string {
|
|
126
139
|
return JSON.stringify(value);
|
|
127
140
|
}
|
|
@@ -312,7 +325,7 @@ export class RunInstallUseCase {
|
|
|
312
325
|
const workflow = input.workflow ?? "standard";
|
|
313
326
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
314
327
|
const targets = input.personal
|
|
315
|
-
? this.createPersonalTargets({ includeClaude })
|
|
328
|
+
? this.createPersonalTargets({ includeClaude, includeCodex })
|
|
316
329
|
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
317
330
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
318
331
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
@@ -330,8 +343,14 @@ export class RunInstallUseCase {
|
|
|
330
343
|
const template = target.strategy === "markdown-managed"
|
|
331
344
|
? renderAgentContextTemplate(rawTemplate, { agent, skillSet, workflow, includeHusky, includeCi })
|
|
332
345
|
: rawTemplate;
|
|
333
|
-
const
|
|
334
|
-
const
|
|
346
|
+
const existingEntry = baseManifest.findEntry(target.path);
|
|
347
|
+
const beforeHash = before === null ? null : this.hashCalculator.compute(before);
|
|
348
|
+
const unmanagedPersonalRuntimeFile = input.personal
|
|
349
|
+
&& PERSONAL_AGENT_RUNTIME_FILES.has(target.path)
|
|
350
|
+
&& before !== null
|
|
351
|
+
&& (existingEntry === null || beforeHash === null || !beforeHash.equals(existingEntry.hash));
|
|
352
|
+
const repairMode = unmanagedPersonalRuntimeFile ? "manual" : this.repairMode(target, before);
|
|
353
|
+
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
335
354
|
const didChange = before !== next;
|
|
336
355
|
const action = this.actionFor(before, didChange, input.force);
|
|
337
356
|
const item: InstallPlanItem = {
|
|
@@ -340,8 +359,10 @@ export class RunInstallUseCase {
|
|
|
340
359
|
repairMode,
|
|
341
360
|
strategy: target.strategy,
|
|
342
361
|
changed: didChange,
|
|
343
|
-
summary:
|
|
344
|
-
|
|
362
|
+
summary: unmanagedPersonalRuntimeFile
|
|
363
|
+
? `${target.path}: existing non-phasegate path requires manual review`
|
|
364
|
+
: didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
|
|
365
|
+
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
345
366
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
346
367
|
};
|
|
347
368
|
plan.push(item);
|
|
@@ -378,9 +399,9 @@ export class RunInstallUseCase {
|
|
|
378
399
|
|
|
379
400
|
const mode = before === null ? "created" : "merged";
|
|
380
401
|
const hash = this.hashCalculator.compute(next);
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
manifest = manifest.addEntry(
|
|
402
|
+
const manifestEntry = baseManifest.findEntry(target.path);
|
|
403
|
+
if (manifestEntry !== null && manifestEntry.hash.equals(hash)) {
|
|
404
|
+
manifest = manifest.addEntry(manifestEntry);
|
|
384
405
|
} else {
|
|
385
406
|
manifest = manifest.addEntry(
|
|
386
407
|
DeploymentEntry.create({
|
|
@@ -394,14 +415,20 @@ export class RunInstallUseCase {
|
|
|
394
415
|
}
|
|
395
416
|
}
|
|
396
417
|
|
|
397
|
-
|
|
398
|
-
|
|
418
|
+
const personalSkillTargets = input.personal
|
|
419
|
+
? [
|
|
420
|
+
...(includeClaude ? [".claude/skills"] : []),
|
|
421
|
+
...(includeCodex ? [".codex/skills"] : []),
|
|
422
|
+
]
|
|
423
|
+
: [];
|
|
424
|
+
for (const skillPath of personalSkillTargets) {
|
|
425
|
+
const item = await this.planPersonalSkillDirectory(input, skillPath, baseManifest);
|
|
399
426
|
plan.push(item);
|
|
400
|
-
if (input.apply && item.changed) {
|
|
427
|
+
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
401
428
|
try {
|
|
402
|
-
await copyDirectory(join(input.harnessRoot, "skills"), join(input.projectRoot,
|
|
429
|
+
await copyDirectory(join(input.harnessRoot, "skills"), join(input.projectRoot, skillPath));
|
|
403
430
|
await writeFile(
|
|
404
|
-
join(input.projectRoot,
|
|
431
|
+
join(input.projectRoot, skillPath, ".harness-version"),
|
|
405
432
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
406
433
|
"utf8",
|
|
407
434
|
);
|
|
@@ -412,20 +439,39 @@ export class RunInstallUseCase {
|
|
|
412
439
|
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
413
440
|
path: item.path,
|
|
414
441
|
mode: "created",
|
|
415
|
-
contentForHash: this.personalSkillsHashInput(input.phasegateVersion, skillSet),
|
|
442
|
+
contentForHash: this.personalSkillsHashInput(item.path, input.phasegateVersion, skillSet),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!input.personal && (includeClaude || includeCodex)) {
|
|
448
|
+
const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
|
|
449
|
+
const item = await this.planSharedSkillDirectory(input, sharedSkills, baseManifest);
|
|
450
|
+
plan.push(item);
|
|
451
|
+
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
452
|
+
try {
|
|
453
|
+
await this.deploySharedSkills(input, sharedSkills, skillSet);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, item.path, "copyDirectory", error);
|
|
456
|
+
}
|
|
457
|
+
changed.push(item);
|
|
458
|
+
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
459
|
+
path: SHARED_SKILLS_VERSION_PATH,
|
|
460
|
+
mode: "created",
|
|
461
|
+
contentForHash: this.sharedSkillsVersionHashInput(input.phasegateVersion, skillSet, sharedSkills),
|
|
416
462
|
});
|
|
463
|
+
for (const skill of sharedSkills) {
|
|
464
|
+
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
465
|
+
path: `skills/${skill}`,
|
|
466
|
+
mode: "created",
|
|
467
|
+
contentForHash: this.sharedSkillHashInput(skill, input.phasegateVersion, skillSet),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
417
470
|
}
|
|
418
471
|
}
|
|
419
472
|
|
|
420
473
|
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
|
-
]
|
|
474
|
+
? []
|
|
429
475
|
: [
|
|
430
476
|
...(includeClaude ? [{ path: ".claude/skills", target: "../skills" }] : []),
|
|
431
477
|
...(includeCodex ? [{ path: ".codex/skills", target: "../skills" }] : []),
|
|
@@ -455,13 +501,13 @@ export class RunInstallUseCase {
|
|
|
455
501
|
|
|
456
502
|
if (input.personal && includeCodex) {
|
|
457
503
|
plan.push({
|
|
458
|
-
path: "~/.codex/
|
|
504
|
+
path: "~/.codex/config.toml",
|
|
459
505
|
action: "will-skip",
|
|
460
506
|
repairMode: "manual",
|
|
461
507
|
strategy: "json",
|
|
462
508
|
changed: false,
|
|
463
|
-
summary: "~/.codex/
|
|
464
|
-
diff: "manual
|
|
509
|
+
summary: "~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
|
|
510
|
+
diff: "manual Codex hooks feature enablement may be required",
|
|
465
511
|
skillHint: null,
|
|
466
512
|
});
|
|
467
513
|
}
|
|
@@ -504,6 +550,11 @@ export class RunInstallUseCase {
|
|
|
504
550
|
readonly includeCi: boolean;
|
|
505
551
|
}): readonly InstallTarget[] {
|
|
506
552
|
return [
|
|
553
|
+
{
|
|
554
|
+
path: "phasegate.config.json",
|
|
555
|
+
strategy: "copy" as const,
|
|
556
|
+
templatePath: "docs/templates/project/phasegate.config.json",
|
|
557
|
+
},
|
|
507
558
|
...(options.includeClaude
|
|
508
559
|
? [
|
|
509
560
|
{ path: ".claude/settings.json", strategy: "json" as const, templatePath: "templates/.claude/settings.json" },
|
|
@@ -564,7 +615,7 @@ export class RunInstallUseCase {
|
|
|
564
615
|
];
|
|
565
616
|
}
|
|
566
617
|
|
|
567
|
-
private createPersonalTargets(options: { readonly includeClaude: boolean }): readonly InstallTarget[] {
|
|
618
|
+
private createPersonalTargets(options: { readonly includeClaude: boolean; readonly includeCodex: boolean }): readonly InstallTarget[] {
|
|
568
619
|
return [
|
|
569
620
|
{
|
|
570
621
|
path: ".phasegate-local/phasegate.config.json",
|
|
@@ -574,12 +625,21 @@ export class RunInstallUseCase {
|
|
|
574
625
|
...(options.includeClaude
|
|
575
626
|
? [
|
|
576
627
|
{
|
|
577
|
-
path: ".
|
|
628
|
+
path: ".claude/settings.json",
|
|
578
629
|
strategy: "copy" as const,
|
|
579
630
|
templatePath: "templates/.claude/settings.json",
|
|
580
631
|
},
|
|
581
632
|
]
|
|
582
633
|
: []),
|
|
634
|
+
...(options.includeCodex
|
|
635
|
+
? [
|
|
636
|
+
{
|
|
637
|
+
path: ".codex/hooks.json",
|
|
638
|
+
strategy: "copy" as const,
|
|
639
|
+
templatePath: "templates/.codex/hooks.json",
|
|
640
|
+
},
|
|
641
|
+
]
|
|
642
|
+
: []),
|
|
583
643
|
{
|
|
584
644
|
path: ".git/info/exclude",
|
|
585
645
|
strategy: "text-managed" as const,
|
|
@@ -639,25 +699,91 @@ export class RunInstallUseCase {
|
|
|
639
699
|
};
|
|
640
700
|
}
|
|
641
701
|
|
|
642
|
-
private async
|
|
643
|
-
|
|
702
|
+
private async planPersonalSkillDirectory(
|
|
703
|
+
input: RunInstallInput,
|
|
704
|
+
relativePath: string,
|
|
705
|
+
baseManifest: DeploymentManifest,
|
|
706
|
+
): Promise<InstallPlanItem> {
|
|
707
|
+
const absolutePath = join(input.projectRoot, relativePath);
|
|
708
|
+
const versionPath = join(absolutePath, ".harness-version");
|
|
644
709
|
const current = await readTextOrNull(versionPath);
|
|
645
710
|
const expectedNeedle = `"version": "${input.phasegateVersion}"`;
|
|
646
|
-
const
|
|
711
|
+
const manifestEntry = baseManifest.findEntry(relativePath);
|
|
712
|
+
const pathExists = await exists(absolutePath);
|
|
713
|
+
const unmanagedExisting = pathExists && (current === null || manifestEntry === null);
|
|
714
|
+
const changed = !unmanagedExisting && (current === null || !current.includes(expectedNeedle));
|
|
715
|
+
return {
|
|
716
|
+
path: relativePath,
|
|
717
|
+
action: changed ? "missing" : unmanagedExisting ? "will-merge" : "will-skip",
|
|
718
|
+
repairMode: unmanagedExisting ? "manual" : "mechanical",
|
|
719
|
+
strategy: "copy-dir",
|
|
720
|
+
changed,
|
|
721
|
+
summary: unmanagedExisting
|
|
722
|
+
? `${relativePath}: existing non-phasegate directory requires manual review`
|
|
723
|
+
: changed ? `${relativePath}: deploy bundled skills` : `${relativePath}: already up to date`,
|
|
724
|
+
diff: unmanagedExisting ? "manual review required" : changed ? "+ bundled skills" : "no changes",
|
|
725
|
+
skillHint: null,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private personalSkillsHashInput(path: string, version: string, skillSet: "core" | "all"): string {
|
|
730
|
+
return `personal-skills:${path}:${version}:${skillSet}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private async planSharedSkillDirectory(
|
|
734
|
+
input: RunInstallInput,
|
|
735
|
+
skills: readonly string[],
|
|
736
|
+
baseManifest: DeploymentManifest,
|
|
737
|
+
): Promise<InstallPlanItem> {
|
|
738
|
+
const versionPath = join(input.projectRoot, SHARED_SKILLS_VERSION_PATH);
|
|
739
|
+
const current = await readTextOrNull(versionPath);
|
|
740
|
+
const skillSet = input.skillSet ?? "all";
|
|
741
|
+
const expectedVersion = `"version": "${input.phasegateVersion}"`;
|
|
742
|
+
const expectedSkillSet = `"skillSet": "${skillSet}"`;
|
|
743
|
+
let missingSkill = false;
|
|
744
|
+
for (const skill of skills) {
|
|
745
|
+
if (!(await exists(join(input.projectRoot, "skills", skill, "SKILL.md")))) {
|
|
746
|
+
missingSkill = true;
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const missingManifest = baseManifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
|
|
751
|
+
|| skills.some((skill) => baseManifest.findEntry(`skills/${skill}`) === null);
|
|
752
|
+
const changed = current === null || !current.includes(expectedVersion) || !current.includes(expectedSkillSet) || missingSkill || missingManifest;
|
|
647
753
|
return {
|
|
648
|
-
path: "
|
|
754
|
+
path: "skills",
|
|
649
755
|
action: changed ? "missing" : "will-skip",
|
|
650
756
|
repairMode: "mechanical",
|
|
651
757
|
strategy: "copy-dir",
|
|
652
758
|
changed,
|
|
653
|
-
summary: changed ? "
|
|
654
|
-
diff: changed ?
|
|
759
|
+
summary: changed ? "skills: deploy shared bundled skills" : "skills: already up to date",
|
|
760
|
+
diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
|
|
655
761
|
skillHint: null,
|
|
656
762
|
};
|
|
657
763
|
}
|
|
658
764
|
|
|
659
|
-
private
|
|
660
|
-
|
|
765
|
+
private async deploySharedSkills(input: RunInstallInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
|
|
766
|
+
const targetRoot = join(input.projectRoot, "skills");
|
|
767
|
+
await mkdir(targetRoot, { recursive: true });
|
|
768
|
+
for (const skill of skills) {
|
|
769
|
+
const source = join(input.harnessRoot, "skills", skill);
|
|
770
|
+
const target = join(targetRoot, skill);
|
|
771
|
+
await rm(target, { recursive: true, force: true });
|
|
772
|
+
await copyDirectory(source, target);
|
|
773
|
+
}
|
|
774
|
+
await writeFile(
|
|
775
|
+
join(targetRoot, ".harness-version"),
|
|
776
|
+
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
777
|
+
"utf8",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private sharedSkillsVersionHashInput(version: string, skillSet: SkillSet, skills: readonly string[]): string {
|
|
782
|
+
return `shared-skills-version:${version}:${skillSet}:${skills.join(",")}`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private sharedSkillHashInput(skill: string, version: string, skillSet: SkillSet): string {
|
|
786
|
+
return `shared-skill:${skill}:${version}:${skillSet}`;
|
|
661
787
|
}
|
|
662
788
|
|
|
663
789
|
private addManifestEntry(
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// @work-item-id WI-148
|
|
4
4
|
// @work-item-id WI-174
|
|
5
5
|
// @work-item-id WI-198
|
|
6
|
+
// @work-item-id WI-210
|
|
6
7
|
|
|
7
|
-
import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, symlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
8
9
|
import { dirname, join, relative, resolve } from "node:path";
|
|
10
|
+
import { getSkillsForSet, type SkillSet } from "../../../setup/skill-deployer.js";
|
|
9
11
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
10
12
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
11
13
|
import type { ManagedBlockInput } from "../../domain/managed-block.js";
|
|
@@ -14,7 +16,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
|
14
16
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
15
17
|
|
|
16
18
|
type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse";
|
|
17
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "symlink" | "unknown";
|
|
19
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "copy-dir" | "symlink" | "unknown";
|
|
18
20
|
|
|
19
21
|
export interface ReconcilePlanItem {
|
|
20
22
|
readonly path: string;
|
|
@@ -56,6 +58,7 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
56
58
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
57
59
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
58
60
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
61
|
+
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
59
62
|
|
|
60
63
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
64
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -78,6 +81,29 @@ async function readTextOrNull(path: string): Promise<string | null> {
|
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
85
|
+
await mkdir(dest, { recursive: true });
|
|
86
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const srcPath = join(src, entry.name);
|
|
89
|
+
const destPath = join(dest, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await copyDirectory(srcPath, destPath);
|
|
92
|
+
} else if (entry.isFile()) {
|
|
93
|
+
await copyFile(srcPath, destPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet): Promise<string[]> {
|
|
99
|
+
const allowed = new Set(getSkillsForSet(skillSet));
|
|
100
|
+
const entries = await readdir(join(harnessRoot, "skills"), { withFileTypes: true });
|
|
101
|
+
return entries
|
|
102
|
+
.filter((entry) => entry.isDirectory() && allowed.has(entry.name))
|
|
103
|
+
.map((entry) => entry.name)
|
|
104
|
+
.sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
function normalizeJsonEntry(value: unknown): string {
|
|
82
108
|
return JSON.stringify(value);
|
|
83
109
|
}
|
|
@@ -259,6 +285,13 @@ export class RunReconcileUseCase {
|
|
|
259
285
|
}
|
|
260
286
|
}
|
|
261
287
|
|
|
288
|
+
if (this.manifestIntendsSharedSkills(manifest)) {
|
|
289
|
+
const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
290
|
+
const outcome = await this.planSharedSkills(input, manifest, sharedSkills, "all");
|
|
291
|
+
outcomes.push(outcome);
|
|
292
|
+
plan.push(outcome.item);
|
|
293
|
+
}
|
|
294
|
+
|
|
262
295
|
if (!input.apply || refused.length > 0) {
|
|
263
296
|
return { plan, refused, changed, backupDir: null };
|
|
264
297
|
}
|
|
@@ -272,16 +305,24 @@ export class RunReconcileUseCase {
|
|
|
272
305
|
const hashContent = await outcome.apply();
|
|
273
306
|
changed.push(outcome.item);
|
|
274
307
|
if (hashContent !== null) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
308
|
+
if (outcome.item.strategy === "copy-dir" && outcome.item.path === "skills") {
|
|
309
|
+
const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
310
|
+
nextManifest = nextManifest.addEntry(this.createdEntry(SHARED_SKILLS_VERSION_PATH, this.sharedSkillsVersionHashInput(input.phasegateVersion, "all", sharedSkills)));
|
|
311
|
+
for (const skill of sharedSkills) {
|
|
312
|
+
nextManifest = nextManifest.addEntry(this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, "all")));
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const mode = outcome.item.strategy === "symlink" ? "symlink" : outcome.item.action === "add" ? "created" : (manifest.findEntry(outcome.item.path)?.mode ?? "merged");
|
|
316
|
+
nextManifest = nextManifest.addEntry(
|
|
317
|
+
DeploymentEntry.create({
|
|
318
|
+
path: outcome.item.path,
|
|
319
|
+
mode,
|
|
320
|
+
block: mode === "merged" ? this.managedBlockFor(outcome.item.path, outcome.item.strategy) : null,
|
|
321
|
+
hash: this.hashCalculator.compute(hashContent),
|
|
322
|
+
deployedAt: new Date().toISOString(),
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
285
326
|
}
|
|
286
327
|
}
|
|
287
328
|
|
|
@@ -388,6 +429,84 @@ export class RunReconcileUseCase {
|
|
|
388
429
|
}
|
|
389
430
|
}
|
|
390
431
|
|
|
432
|
+
private manifestIntendsSharedSkills(manifest: DeploymentManifest): boolean {
|
|
433
|
+
return manifest.findEntry(".claude/skills") !== null
|
|
434
|
+
|| manifest.findEntry(".codex/skills") !== null
|
|
435
|
+
|| manifest.entries.some((entry) => entry.path.startsWith("skills/"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async planSharedSkills(
|
|
439
|
+
input: RunReconcileInput,
|
|
440
|
+
manifest: DeploymentManifest,
|
|
441
|
+
skills: readonly string[],
|
|
442
|
+
skillSet: SkillSet,
|
|
443
|
+
) {
|
|
444
|
+
const versionContent = await readTextOrNull(this.resolveProjectPath(input.projectRoot, SHARED_SKILLS_VERSION_PATH));
|
|
445
|
+
const expectedVersion = `"version": "${input.phasegateVersion}"`;
|
|
446
|
+
let missingSkill = false;
|
|
447
|
+
for (const skill of skills) {
|
|
448
|
+
if (!(await exists(this.resolveProjectPath(input.projectRoot, `skills/${skill}/SKILL.md`)))) {
|
|
449
|
+
missingSkill = true;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const missingManifest = manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
|
|
454
|
+
|| skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
|
|
455
|
+
const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
|
|
456
|
+
return {
|
|
457
|
+
item: this.item(
|
|
458
|
+
"skills",
|
|
459
|
+
changed ? "add" : "skip",
|
|
460
|
+
"mechanical",
|
|
461
|
+
"copy-dir",
|
|
462
|
+
changed,
|
|
463
|
+
changed ? "skills: deploy shared bundled skills" : "skills: already up to date",
|
|
464
|
+
changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
|
|
465
|
+
null,
|
|
466
|
+
),
|
|
467
|
+
needsBackup: false,
|
|
468
|
+
apply: async () => {
|
|
469
|
+
if (!changed) return null;
|
|
470
|
+
await this.deploySharedSkills(input, skills, skillSet);
|
|
471
|
+
return this.sharedSkillsVersionHashInput(input.phasegateVersion, skillSet, skills);
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async deploySharedSkills(input: RunReconcileInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
|
|
477
|
+
const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
|
|
478
|
+
await mkdir(targetRoot, { recursive: true });
|
|
479
|
+
for (const skill of skills) {
|
|
480
|
+
const source = join(input.harnessRoot, "skills", skill);
|
|
481
|
+
const target = join(targetRoot, skill);
|
|
482
|
+
await rm(target, { recursive: true, force: true });
|
|
483
|
+
await copyDirectory(source, target);
|
|
484
|
+
}
|
|
485
|
+
await writeFile(
|
|
486
|
+
join(targetRoot, ".harness-version"),
|
|
487
|
+
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
488
|
+
"utf8",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private createdEntry(path: string, hashInput: string): DeploymentEntry {
|
|
493
|
+
return DeploymentEntry.create({
|
|
494
|
+
path,
|
|
495
|
+
mode: "created",
|
|
496
|
+
block: null,
|
|
497
|
+
hash: this.hashCalculator.compute(hashInput),
|
|
498
|
+
deployedAt: new Date().toISOString(),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private sharedSkillsVersionHashInput(version: string, skillSet: SkillSet, skills: readonly string[]): string {
|
|
503
|
+
return `shared-skills-version:${version}:${skillSet}:${skills.join(",")}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private sharedSkillHashInput(skill: string, version: string, skillSet: SkillSet): string {
|
|
507
|
+
return `shared-skill:${skill}:${version}:${skillSet}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
391
510
|
private reconcileContent(target: ReconcileTarget, before: string | null, template: string, version: string): string {
|
|
392
511
|
if (target.strategy === "yaml-add") return template;
|
|
393
512
|
if (target.strategy === "shell") return reconcileShell(before, template);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// @work-item-id WI-199
|
|
6
6
|
// @work-item-id WI-207
|
|
7
7
|
// @work-item-id WI-208
|
|
8
|
+
// @work-item-id WI-209
|
|
8
9
|
|
|
9
10
|
import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
|
|
10
11
|
import { dirname, join, relative, resolve } from "node:path";
|
package/skills/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
`.agent/skills` は旧 setup 由来の互換パスです。新規導入では管理対象にしません。<!-- @work-item-id WI-157 -->
|
|
18
18
|
|
|
19
|
-
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは
|
|
19
|
+
`phasegate install --personal` はチーム所有リポジトリでの個人評価用です。このモードでは team-owned files を変更せず、`.phasegate-local/phasegate.config.json` と `.git/info/exclude` の local-only managed block、選択 agent 用の real `.claude/*` / `.codex/*` runtime artifacts だけを扱います。通常の共有導入が必要になった時点で `phasegate install --dry-run` / `phasegate install --apply` を使います。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-209 -->
|
|
20
20
|
|
|
21
21
|
## 新しいスキルの追加
|
|
22
22
|
|
|
@@ -129,7 +129,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
|
|
|
129
129
|
#### 観点 9: setup lifecycle と doctor finding
|
|
130
130
|
|
|
131
131
|
- `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
|
|
132
|
-
- チーム所有リポジトリで個人評価だけを行う相談では、通常の `install --apply` ではなく `phasegate install --personal --agent claude --dry-run` を先に提案する。personal install は `package.json`, `AGENTS.md`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, CI settings
|
|
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 を変更対象にしない。選択 agent 向けには `.phasegate-local/phasegate.config.json`, real `.claude/settings.json` / `.claude/skills/`, real `.codex/hooks.json` / `.codex/skills/`, `.phasegate/manifest.json`, `.git/info/exclude` の managed block を自動作成する。既存 `.claude/*` / `.codex/*` がある場合は上書きせず manual review として扱う。Codex user-level hook feature enablement は manual action として扱う。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
|
|
133
133
|
- Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` は raw `repairMode` が current scope の修復指示ではないという印である。<!-- @work-item-id WI-178, WI-179, WI-180 -->
|
|
134
134
|
- `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
|
|
135
135
|
- manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
|
|
@@ -130,7 +130,7 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
|
|
|
130
130
|
- 既存プロジェクト導入: `docs/guide/retrofit-adoption.md`
|
|
131
131
|
- setup artifact / doctor finding / legacy artifact: `docs/guide/setup-artifacts.md`
|
|
132
132
|
|
|
133
|
-
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal --agent
|
|
133
|
+
チーム所有リポジトリで個人評価だけを行いたい場合は、`docs/guide/installation.md` の personal install セクションを読む。`phasegate install --personal --agent <agent>` は `package.json`、`AGENTS.md`、`CLAUDE.md`、`.husky/*`、`.github/workflows/*`、`.gitignore`、GitHub CLI config、repo secrets、CI settings を変更せず、`.phasegate-local/phasegate.config.json`、real `.claude/settings.json` / `.claude/skills/`、real `.codex/hooks.json` / `.codex/skills/`、`.git/info/exclude` の managed block を使う。Codex user-level hook feature enablement は manual action として説明する。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 -->
|
|
134
134
|
|
|
135
135
|
`setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
|
|
136
136
|
|