phasegate 0.160.15 → 0.160.17

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 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
@@ -97,7 +97,17 @@ claude
97
97
 
98
98
  `init` は legacy 互換の bootstrap 経路です。既存 hooks / scripts / package metadata に構造化 merge したい場合は `install` を使います。「設計してから書け」を強制する仕組みなので、設計文書はユーザーがスキル経由で作るのが既定動作です。
99
99
 
100
- 既存プロジェクトに導入する場合は、構造化 install で差分を確認してから適用します。
100
+ 既存プロジェクトへの導入は、**チーム共通で入れる (team install)** か **自分だけで試す (personal install)** かで手順が変わります。誰のリポジトリで誰がメンテするかで選んでください。
101
+
102
+ | シナリオ | コマンド | 影響範囲 |
103
+ |---|---|---|
104
+ | 新規プロジェクトを bootstrap | `npx phasegate init --name <name> --with-husky --with-ci` | リポジトリ全体(team) |
105
+ | 既存プロジェクトにチーム共通で導入 | `npx phasegate install --apply` | リポジトリ全体(team) |
106
+ | チーム所有リポジトリで個人評価 | `npx phasegate install --personal --agent claude --apply` | 自分の作業コピーのみ(ローカル) |
107
+
108
+ #### team install — チーム共通で導入する場合
109
+
110
+ `package.json` / `CLAUDE.md` / `.husky/*` / `.github/workflows/*` などのチーム共有ファイルを merge 対象に含めます。**この変更はコミット後にチーム全員へ影響します**。事前に dry-run で差分を確認してから apply してください。
101
111
 
102
112
  ```bash
103
113
  npx phasegate install --dry-run
@@ -105,7 +115,25 @@ npx phasegate install --apply
105
115
  npx phasegate doctor
106
116
  ```
107
117
 
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/` に退避されます。
118
+ `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 -->
119
+
120
+ #### personal install — 自分だけで試す場合(チーム所有リポジトリで評価したい)
121
+
122
+ チーム所有のリポジトリで自分だけ PhaseGate を試したい場合は `--personal` フラグを使います。**チーム共有ファイルには一切触れず、自分の agent runtime だけを設定する** ローカル専用パスです。
123
+
124
+ ```bash
125
+ npx phasegate install --personal --agent claude --dry-run
126
+ npx phasegate install --personal --agent claude --apply
127
+ ```
128
+
129
+ | 項目 | personal install の挙動 |
130
+ |---|---|
131
+ | **触らないファイル** | `package.json` / `AGENTS.md` / `CLAUDE.md` / `.husky/*` / `.github/workflows/*` / `.gitignore` / GitHub CLI 設定 / repo secrets / CI 設定 |
132
+ | **作るファイル** | `.phasegate-local/phasegate.config.json`(ローカル専用 config)/ `.claude/CLAUDE.local.md` または `.codex/AGENTS.local.md`(agent context)/ `.claude/settings.json` + `.claude/skills/` または `.codex/hooks.json` + `.codex/skills/`(選択した agent の runtime artifact)/ `.git/hooks/pre-commit` + `.git/hooks/commit-msg`(ローカル git hook)/ `.phasegate-local/docs/`(設計原則文書コピー)/ `.phasegate/manifest.json` |
133
+ | **コミット漏れ対策** | `.git/info/exclude` にローカル専用 block を管理して、個人用ファイルが誤ってチームの commit に混ざらないようにする。commit 時の L2 防御は `.git/hooks/` で発火する |
134
+ | **Codex hook flag** | user-level feature flag の有効化は手動アクションとして残る(`codex features enable hooks`) |
135
+
136
+ Codex を併用する場合は `--agent codex` または `--agent both` を指定します。アンインストールは team install と同様に `npx phasegate uninstall --apply` を使えば manifest 経由でローカル成果物のみが除去されます。<!-- @work-item-id WI-207 --> <!-- @work-item-id WI-208 --> <!-- @work-item-id WI-209 --> <!-- @work-item-id WI-213 -->
109
137
 
110
138
  後で PhaseGate を外す場合は、manifest ベースの uninstall を使います。
111
139
 
@@ -123,7 +151,7 @@ npx phasegate reconcile --dry-run
123
151
  npx phasegate reconcile --apply
124
152
  ```
125
153
 
126
- `reconcile` は PhaseGate 管理部分だけを更新し、ユーザーの hook / script / dependency は保持します。新しい deploy target が追加されていれば install と同じく追加し、`.phasegate/manifest.json` の version / hash も更新します。install 後に user 改変された managed file は `--force` 無しでは refuse し、force 時は `.phasegate/backups/reconcile-<timestamp>/` に退避してから上書きします。
154
+ `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
155
 
128
156
  ### Codex CLI を使う場合
129
157
 
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. 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 -->
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`, local agent context (`.claude/CLAUDE.local.md` and/or `.codex/AGENTS.local.md`), real local-only agent runtime artifacts for the selected agent (`.claude/settings.json` + `.claude/skills/` and/or `.codex/hooks.json` + `.codex/skills/`), local git hooks under `.git/hooks/`, local reference docs under `.phasegate-local/docs/`, 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 --> <!-- @work-item-id WI-213 -->
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
 
@@ -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. 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 -->
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 local agent context (`.claude/CLAUDE.local.md` and/or `.codex/AGENTS.local.md`), creates real local-only agent runtime artifacts for the selected agent (`.claude/settings.json` + `.claude/skills/` and/or `.codex/hooks.json` + `.codex/skills/`), deploys local git hooks under `.git/hooks/`, copies reference docs under `.phasegate-local/docs/`, 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 --> <!-- @work-item-id WI-213 -->
69
69
 
70
70
  For agent-readable planning before writing files:
71
71
 
@@ -12,7 +12,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
12
12
 
13
13
  | Class | Examples | Owner | Lifecycle |
14
14
  |---|---|---|---|
15
- | Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
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
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` |
17
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 |
18
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 |
@@ -29,6 +29,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
29
29
  - Husky scripts when requested: `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`
30
30
  - CI workflow when requested: `.github/workflows/phasegate-aidlc-gate.yml`
31
31
  - Agent skill links: `.claude/skills`, `.codex/skills`
32
+ - Project shared bundled skills: selected `skills/<name>/` directories plus `skills/.harness-version`
32
33
  - Project config: `phasegate.config.json` when absent, so installed agent hooks have a discoverable runtime config
33
34
  - Package metadata: PhaseGate scripts and `devDependencies.phasegate` in `package.json`
34
35
  - Manifest: `.phasegate/manifest.json`
@@ -43,6 +44,8 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
43
44
 
44
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 -->
45
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 -->
48
+
46
49
  ## Doctor Findings
47
50
 
48
51
  `phasegate doctor` evaluates setup health from the managed targets and related project state. Findings include `repairMode`, optional `repairHint`, and optional `suggestedSkill`.
@@ -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` deploys the skill bodies to `skills/` and exposes them to enabled agents through `.claude/skills/` / `.codex/skills/` links.
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,7 @@
1
+ #!/usr/bin/env sh
2
+ # PhaseGate personal commit-msg hook.
3
+ # Local-only counterpart of the Husky commit-msg defense.
4
+
5
+ PHASEGATE_CMD="${PHASEGATE_CMD:-npx phasegate}"
6
+
7
+ $PHASEGATE_CMD commit-msg "$1"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ # PhaseGate personal pre-commit hook.
3
+ # Local-only counterpart of the Husky pre-commit defense.
4
+
5
+ PHASEGATE_CMD="${PHASEGATE_CMD:-npx phasegate}"
6
+
7
+ echo "PhaseGate personal pre-commit"
8
+
9
+ $PHASEGATE_CMD lint
10
+ L1_EXIT=$?
11
+ if [ $L1_EXIT -ne 0 ]; then
12
+ echo "PhaseGate lint failed."
13
+ exit 1
14
+ fi
15
+
16
+ if [ "${HARNESS_QUICK_MODE:-0}" = "1" ]; then
17
+ $PHASEGATE_CMD validate --layer L2 --format human --no-l4
18
+ else
19
+ $PHASEGATE_CMD validate --layer L2 --format human
20
+ fi
21
+
@@ -54,8 +54,8 @@
54
54
  "deadCodeGC": false
55
55
  },
56
56
  "paths": {
57
- "designDocs": "docs/product/construction",
58
- "inceptionDocs": "docs/inception"
57
+ "designDocs": ".phasegate-local/product/construction",
58
+ "inceptionDocs": ".phasegate-local/inception"
59
59
  },
60
60
  "reporting": {
61
61
  "format": "json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.160.15",
3
+ "version": "0.160.17",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-209
5
+ // @work-item-id WI-210
5
6
 
6
7
  import { join } from "node:path";
7
8
  import type { CheckId } from "../../domain/check-id.js";
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-209
5
+ // @work-item-id WI-210
5
6
 
6
7
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
7
8
  import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
@@ -14,15 +15,15 @@ export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
14
15
 
15
16
  async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
16
17
  const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
17
- if (skillTargetLooksValid(link)) return null;
18
18
  const files = await inspector.listFiles(projectPath(projectRoot, this.target));
19
- if (skillDirectoryLooksValid(files)) return null;
19
+ if (skillTargetLooksValid(link) && skillDirectoryLooksValid(files)) return null;
20
+ if (link === null && skillDirectoryLooksValid(files)) return null;
20
21
  return createFinding({
21
22
  checkId: this.checkId,
22
23
  severity: "red",
23
24
  target: this.target,
24
25
  message: ".claude/skills に phasegate skills がありません",
25
- repairMode: link === null ? "mechanical" : "manual",
26
+ repairMode: link === null || skillTargetLooksValid(link) ? "mechanical" : "manual",
26
27
  });
27
28
  }
28
29
  }
@@ -2,6 +2,7 @@
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-209
5
+ // @work-item-id WI-210
5
6
 
6
7
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
7
8
  import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
@@ -14,15 +15,15 @@ export class CodexSkillsSymlinkCheck implements HeuristicCheck {
14
15
 
15
16
  async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
16
17
  const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
17
- if (skillTargetLooksValid(link)) return null;
18
18
  const files = await inspector.listFiles(projectPath(projectRoot, this.target));
19
- if (skillDirectoryLooksValid(files)) return null;
19
+ if (skillTargetLooksValid(link) && skillDirectoryLooksValid(files)) return null;
20
+ if (link === null && skillDirectoryLooksValid(files)) return null;
20
21
  return createFinding({
21
22
  checkId: this.checkId,
22
23
  severity: "red",
23
24
  target: this.target,
24
25
  message: ".codex/skills に phasegate skills がありません",
25
- repairMode: link === null ? "mechanical" : "manual",
26
+ repairMode: link === null || skillTargetLooksValid(link) ? "mechanical" : "manual",
26
27
  });
27
28
  }
28
29
  }
@@ -10,9 +10,12 @@
10
10
  // @work-item-id WI-207
11
11
  // @work-item-id WI-208
12
12
  // @work-item-id WI-209
13
+ // @work-item-id WI-210
14
+ // @work-item-id WI-213
13
15
 
14
- import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir } from "node:fs/promises";
16
+ import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
15
17
  import { dirname, join } from "node:path";
18
+ import { getSkillsForSet, type SkillSet } from "../../../setup/skill-deployer.js";
16
19
  import { DeploymentEntry } from "../../domain/deployment-entry.js";
17
20
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
18
21
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
@@ -86,6 +89,7 @@ const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
86
89
  const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
87
90
  const TEXT_END = "# phasegate personal install exclude (END)";
88
91
  const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/settings.json", ".codex/hooks.json"]);
92
+ const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
89
93
 
90
94
  function isRecord(value: unknown): value is Record<string, unknown> {
91
95
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -122,6 +126,16 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
122
126
  }
123
127
  }
124
128
 
129
+ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet): Promise<string[]> {
130
+ const skillsSource = join(harnessRoot, "skills");
131
+ const allowed = new Set(getSkillsForSet(skillSet));
132
+ const entries = await readdir(skillsSource, { withFileTypes: true });
133
+ return entries
134
+ .filter((entry) => entry.isDirectory() && allowed.has(entry.name))
135
+ .map((entry) => entry.name)
136
+ .sort();
137
+ }
138
+
125
139
  function normalizeJsonEntry(value: unknown): string {
126
140
  return JSON.stringify(value);
127
141
  }
@@ -431,6 +445,32 @@ export class RunInstallUseCase {
431
445
  }
432
446
  }
433
447
 
448
+ if (!input.personal && (includeClaude || includeCodex)) {
449
+ const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
450
+ const item = await this.planSharedSkillDirectory(input, sharedSkills, baseManifest);
451
+ plan.push(item);
452
+ if (input.apply && item.changed && item.repairMode === "mechanical") {
453
+ try {
454
+ await this.deploySharedSkills(input, sharedSkills, skillSet);
455
+ } catch (error) {
456
+ return this.withApplyError({ plan, refused, changed, backupDir }, item.path, "copyDirectory", error);
457
+ }
458
+ changed.push(item);
459
+ manifest = this.addManifestEntry(baseManifest, manifest, {
460
+ path: SHARED_SKILLS_VERSION_PATH,
461
+ mode: "created",
462
+ contentForHash: this.sharedSkillsVersionHashInput(input.phasegateVersion, skillSet, sharedSkills),
463
+ });
464
+ for (const skill of sharedSkills) {
465
+ manifest = this.addManifestEntry(baseManifest, manifest, {
466
+ path: `skills/${skill}`,
467
+ mode: "created",
468
+ contentForHash: this.sharedSkillHashInput(skill, input.phasegateVersion, skillSet),
469
+ });
470
+ }
471
+ }
472
+ }
473
+
434
474
  const linkSpecs = input.personal
435
475
  ? []
436
476
  : [
@@ -585,6 +625,12 @@ export class RunInstallUseCase {
585
625
  },
586
626
  ...(options.includeClaude
587
627
  ? [
628
+ {
629
+ path: ".claude/CLAUDE.local.md",
630
+ strategy: "markdown-managed" as const,
631
+ templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
632
+ block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal CLAUDE.local.md managed section" },
633
+ },
588
634
  {
589
635
  path: ".claude/settings.json",
590
636
  strategy: "copy" as const,
@@ -594,6 +640,12 @@ export class RunInstallUseCase {
594
640
  : []),
595
641
  ...(options.includeCodex
596
642
  ? [
643
+ {
644
+ path: ".codex/AGENTS.local.md",
645
+ strategy: "markdown-managed" as const,
646
+ templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
647
+ block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal AGENTS.local.md managed section" },
648
+ },
597
649
  {
598
650
  path: ".codex/hooks.json",
599
651
  strategy: "copy" as const,
@@ -601,6 +653,38 @@ export class RunInstallUseCase {
601
653
  },
602
654
  ]
603
655
  : []),
656
+ {
657
+ path: ".git/hooks/pre-commit",
658
+ strategy: "copy" as const,
659
+ templatePath: "docs/templates/personal/hooks/pre-commit",
660
+ executable: true,
661
+ },
662
+ {
663
+ path: ".git/hooks/commit-msg",
664
+ strategy: "copy" as const,
665
+ templatePath: "docs/templates/personal/hooks/commit-msg",
666
+ executable: true,
667
+ },
668
+ {
669
+ path: ".phasegate-local/docs/folder_management_rules.md",
670
+ strategy: "copy" as const,
671
+ templatePath: "docs/folder_management_rules.md",
672
+ },
673
+ {
674
+ path: ".phasegate-local/docs/principles/architecture-philosophy.md",
675
+ strategy: "copy" as const,
676
+ templatePath: "docs/principles/architecture-philosophy.md",
677
+ },
678
+ {
679
+ path: ".phasegate-local/docs/principles/model-routing.md",
680
+ strategy: "copy" as const,
681
+ templatePath: "docs/principles/model-routing.md",
682
+ },
683
+ {
684
+ path: ".phasegate-local/docs/principles/testing-rules.md",
685
+ strategy: "copy" as const,
686
+ templatePath: "docs/principles/testing-rules.md",
687
+ },
604
688
  {
605
689
  path: ".git/info/exclude",
606
690
  strategy: "text-managed" as const,
@@ -691,6 +775,62 @@ export class RunInstallUseCase {
691
775
  return `personal-skills:${path}:${version}:${skillSet}`;
692
776
  }
693
777
 
778
+ private async planSharedSkillDirectory(
779
+ input: RunInstallInput,
780
+ skills: readonly string[],
781
+ baseManifest: DeploymentManifest,
782
+ ): Promise<InstallPlanItem> {
783
+ const versionPath = join(input.projectRoot, SHARED_SKILLS_VERSION_PATH);
784
+ const current = await readTextOrNull(versionPath);
785
+ const skillSet = input.skillSet ?? "all";
786
+ const expectedVersion = `"version": "${input.phasegateVersion}"`;
787
+ const expectedSkillSet = `"skillSet": "${skillSet}"`;
788
+ let missingSkill = false;
789
+ for (const skill of skills) {
790
+ if (!(await exists(join(input.projectRoot, "skills", skill, "SKILL.md")))) {
791
+ missingSkill = true;
792
+ break;
793
+ }
794
+ }
795
+ const missingManifest = baseManifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
796
+ || skills.some((skill) => baseManifest.findEntry(`skills/${skill}`) === null);
797
+ const changed = current === null || !current.includes(expectedVersion) || !current.includes(expectedSkillSet) || missingSkill || missingManifest;
798
+ return {
799
+ path: "skills",
800
+ action: changed ? "missing" : "will-skip",
801
+ repairMode: "mechanical",
802
+ strategy: "copy-dir",
803
+ changed,
804
+ summary: changed ? "skills: deploy shared bundled skills" : "skills: already up to date",
805
+ diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
806
+ skillHint: null,
807
+ };
808
+ }
809
+
810
+ private async deploySharedSkills(input: RunInstallInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
811
+ const targetRoot = join(input.projectRoot, "skills");
812
+ await mkdir(targetRoot, { recursive: true });
813
+ for (const skill of skills) {
814
+ const source = join(input.harnessRoot, "skills", skill);
815
+ const target = join(targetRoot, skill);
816
+ await rm(target, { recursive: true, force: true });
817
+ await copyDirectory(source, target);
818
+ }
819
+ await writeFile(
820
+ join(targetRoot, ".harness-version"),
821
+ `${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
822
+ "utf8",
823
+ );
824
+ }
825
+
826
+ private sharedSkillsVersionHashInput(version: string, skillSet: SkillSet, skills: readonly string[]): string {
827
+ return `shared-skills-version:${version}:${skillSet}:${skills.join(",")}`;
828
+ }
829
+
830
+ private sharedSkillHashInput(skill: string, version: string, skillSet: SkillSet): string {
831
+ return `shared-skill:${skill}:${version}:${skillSet}`;
832
+ }
833
+
694
834
  private addManifestEntry(
695
835
  baseManifest: DeploymentManifest,
696
836
  manifest: DeploymentManifest,
@@ -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
- const mode = outcome.item.strategy === "symlink" ? "symlink" : outcome.item.action === "add" ? "created" : (manifest.findEntry(outcome.item.path)?.mode ?? "merged");
276
- nextManifest = nextManifest.addEntry(
277
- DeploymentEntry.create({
278
- path: outcome.item.path,
279
- mode,
280
- block: mode === "merged" ? this.managedBlockFor(outcome.item.path, outcome.item.strategy) : null,
281
- hash: this.hashCalculator.compute(hashContent),
282
- deployedAt: new Date().toISOString(),
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);
@@ -17,6 +17,7 @@
17
17
  * @work-item-id WI-202 / WI-204
18
18
  * @work-item-id WI-205
19
19
  * @work-item-id WI-206
20
+ * @work-item-id WI-213
20
21
  *
21
22
  * Phasegate CLI エントリポイント。
22
23
  * 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
@@ -167,7 +168,7 @@ Setup:
167
168
  --skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
168
169
  --with-husky, --with-ci, --yes)
169
170
  update-skills Alias for reconcile (kept for compatibility)
170
- doctor Diagnose silent installation failures (--json, --strict, --agent <claude|codex|both>, --report-out <path>)
171
+ doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
171
172
  scaffold-wi <unit|_cross> <story|issue|chore>
172
173
  Create docs/inception/{unit}/WI-XXX/description.md
173
174
  emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
@@ -2001,7 +2002,7 @@ async function main(): Promise<void> {
2001
2002
  }
2002
2003
 
2003
2004
  case "doctor": {
2004
- const KNOWN_DOCTOR_FLAGS = ["--json", "--strict", "--agent", "--report-out"];
2005
+ const KNOWN_DOCTOR_FLAGS = ["--json", "--strict", "--personal", "--agent", "--report-out"];
2005
2006
  const flagError = validateKnownFlags(args, KNOWN_DOCTOR_FLAGS);
2006
2007
  if (flagError) {
2007
2008
  console.error(flagError);