phasegate 0.160.17 → 0.160.19
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.ja.md +7 -0
- package/README.md +9 -1
- package/docs/guide/configuration.md +15 -5
- package/docs/guide/installation.md +3 -1
- package/docs/guide/setup-artifacts.md +4 -1
- package/docs/templates/personal/git-info-exclude +1 -0
- package/docs/templates/personal/phasegate-local-config.json +3 -1
- package/docs/templates/project/phasegate.config.json +3 -1
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +29 -7
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +23 -4
- package/scripts/harness/config-foundation/domain/harness-config.ts +2 -0
- package/scripts/harness/config-foundation/domain/value-objects/paths-config.ts +14 -1
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +19 -1
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +35 -0
- package/scripts/harness/installation/application/checks/codex-context-missing-check.ts +32 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +3 -2
- package/scripts/harness/installation/application/usecases/run-install.ts +19 -11
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +5 -1
- package/scripts/harness/installation/composition-root.ts +5 -0
- package/scripts/harness/installation/domain/check-id.ts +3 -0
- package/scripts/harness/installation/domain/repair-table.ts +3 -0
- package/scripts/harness/setup/skill-deployer.ts +35 -11
package/README.ja.md
CHANGED
|
@@ -305,6 +305,12 @@ L2-STORY-REFLECTION バリデータがこのアノテーションを検出し、
|
|
|
305
305
|
"phaseDependencies": { "preset": "standard", "storyReflection": { "enabled": true } },
|
|
306
306
|
"quickMode": { "allowedCategories": ["bugfix", "docs", "test", "config"] },
|
|
307
307
|
"protectedFiles": { "exclude": ["package.json"] },
|
|
308
|
+
"paths": {
|
|
309
|
+
"designDocs": "docs/product/construction",
|
|
310
|
+
"inceptionDocs": "docs/inception",
|
|
311
|
+
"principlesDocs": "docs/principles",
|
|
312
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
313
|
+
},
|
|
308
314
|
"baseline": { "enabled": true, "path": ".phasegate/baseline.json" }
|
|
309
315
|
}
|
|
310
316
|
```
|
|
@@ -329,6 +335,7 @@ phasegate には独立した 3 系統の preset があります。役割が違
|
|
|
329
335
|
|---|---|
|
|
330
336
|
| `quickMode.fullModeRequiredWhen` | Quick Mode → Full Mode への強制エスカレート条件(複数カテゴリ混在 / 新規ドメインファイル / API 契約変更)。安全側の default は全 `true` |
|
|
331
337
|
| `protectedFiles.exclude` | デフォルト保護対象(`package.json`, `tsconfig.json`, `biome.json` 等)から除外したいファイル |
|
|
338
|
+
| `paths.designDocs` / `paths.inceptionDocs` / `paths.principlesDocs` / `paths.folderRulesDoc` | PhaseGate が参照・配置する設計/原則/配置ルール文書の場所。`docs/` 以外の既存規約にもマッピング可能 |
|
|
332
339
|
| `baseline.enabled` | 既存リポジトリ導入時の retrofit grandfather。default `true`。`npx phasegate baseline` で snapshot 生成 |
|
|
333
340
|
| `phaseDependencies.storyReflection` | inception 設計が product docs に反映されるまで `src/{unit}/` への書き込みをブロック |
|
|
334
341
|
|
package/README.md
CHANGED
|
@@ -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`, `
|
|
112
|
+
`--personal` does not plan or write `package.json`, `CLAUDE.md`, `.husky/*`, `.github/workflows/*`, `.gitignore`, GitHub CLI config, repo secrets, or CI settings. It creates `.phasegate-local/phasegate.config.json`, runtime-visible local agent context (`.claude/CLAUDE.md` for Claude, and `AGENTS.md` for Codex only when that root file is absent or already PhaseGate-managed), 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`. If a team `AGENTS.md` already exists, personal Codex install leaves it unchanged and `doctor --personal --agent codex` reports the remaining context step instead of hiding it behind `AGENTS.override.md`. 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 --> <!-- @work-item-id WI-215 -->
|
|
113
113
|
|
|
114
114
|
For agent-driven setup planning, use:
|
|
115
115
|
|
|
@@ -416,6 +416,12 @@ For selection guidance and config examples see [Preset Selection Guide](docs/gui
|
|
|
416
416
|
"protectedFiles": {
|
|
417
417
|
"exclude": ["tsconfig.json", "package.json"]
|
|
418
418
|
},
|
|
419
|
+
"paths": {
|
|
420
|
+
"designDocs": "docs/product/construction",
|
|
421
|
+
"inceptionDocs": "docs/inception",
|
|
422
|
+
"principlesDocs": "docs/principles",
|
|
423
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
424
|
+
},
|
|
419
425
|
"baseline": {
|
|
420
426
|
"enabled": true,
|
|
421
427
|
"path": ".phasegate/baseline.json"
|
|
@@ -425,6 +431,8 @@ For selection guidance and config examples see [Preset Selection Guide](docs/gui
|
|
|
425
431
|
|
|
426
432
|
`quickMode.fullModeRequiredWhen` declares which conditions force a Quick Mode change to escalate to the full `/story-implementor` flow. All three triggers default to `true` so retrofits stay safe; flip individual flags to `false` only when a project intentionally accepts the risk.
|
|
427
433
|
|
|
434
|
+
`paths` maps PhaseGate's design and reference documentation roots. Repositories that do not use `docs/` can point `designDocs`, `inceptionDocs`, `principlesDocs`, and `folderRulesDoc` at their own documentation layout; product-wide Level 1 artifacts are overridden separately with custom `phaseDependencies.gates[]`. <!-- @work-item-id WI-214 -->
|
|
435
|
+
|
|
428
436
|
`baseline` opts in to the **Phase A-2 retrofit grandfather**: pre-existing files captured in `.phasegate/baseline.json` are exempted from `phase-gate` until they are structurally modified. Generate the snapshot with `npx phasegate baseline` before introducing the harness to an existing repository. Since v0.71.0 the `baseline.enabled` flag defaults to `true`, so simply running `npx phasegate baseline` after `init` is enough — no manual config edit needed. For a step-by-step retrofit walkthrough see [Retrofit Adoption Guide](docs/guide/retrofit-adoption.md).
|
|
429
437
|
|
|
430
438
|
---
|
|
@@ -64,7 +64,9 @@ The plan identifies target fields, managed artifacts, commands, validation, risk
|
|
|
64
64
|
},
|
|
65
65
|
"paths": {
|
|
66
66
|
"designDocs": "docs/product/construction",
|
|
67
|
-
"inceptionDocs": "docs/inception"
|
|
67
|
+
"inceptionDocs": "docs/inception",
|
|
68
|
+
"principlesDocs": "docs/principles",
|
|
69
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
68
70
|
},
|
|
69
71
|
"reporting": {
|
|
70
72
|
"format": "json",
|
|
@@ -463,10 +465,14 @@ Quick Mode with `relaxedGates: ["phase-gate"]` relaxes `storyReflection` as well
|
|
|
463
465
|
|
|
464
466
|
#### `paths`
|
|
465
467
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
|
469
|
-
|
|
468
|
+
<!-- @work-item-id WI-214 -->
|
|
469
|
+
|
|
470
|
+
| Sub-field | Type | Default | Description |
|
|
471
|
+
|------------------|----------|--------------------------------------|-------------------------------------------------------|
|
|
472
|
+
| `designDocs` | `string` | `"docs/product/construction"` | Root directory for confirmed design documents (logical design, domain model). |
|
|
473
|
+
| `inceptionDocs` | `string` | `"docs/inception"` | Root directory for inception-phase documents (stories, unit designs). |
|
|
474
|
+
| `principlesDocs` | `string` | `"docs/principles"` | Root directory for immutable PhaseGate principles docs. |
|
|
475
|
+
| `folderRulesDoc` | `string` | `"docs/folder_management_rules.md"` | File path for the folder management rules document. |
|
|
470
476
|
|
|
471
477
|
#### `reporting`
|
|
472
478
|
|
|
@@ -611,6 +617,8 @@ docs/
|
|
|
611
617
|
|
|
612
618
|
If you move your design documents to a non-default location, update `paths` accordingly so that all validators and hooks resolve files correctly.
|
|
613
619
|
|
|
620
|
+
`paths.principlesDocs` and `paths.folderRulesDoc` also control where setup deploys PhaseGate reference docs and what the pre-tool-use hook protects. Existing repositories can keep their own documentation layout, for example `documentation/principles` and `documentation/folder_rules.md`, without adopting the default `docs/` tree. <!-- @work-item-id WI-214 -->
|
|
621
|
+
|
|
614
622
|
**How paths flow into the L2 phase-gate validator (since v0.117.0 / WI-085):**
|
|
615
623
|
|
|
616
624
|
<!-- @work-item-id WI-149 -->
|
|
@@ -621,6 +629,8 @@ If you move your design documents to a non-default location, update `paths` acco
|
|
|
621
629
|
|---|---|---|
|
|
622
630
|
| `{designDocsRoot}` | `paths.designDocs` | `docs/product/construction` |
|
|
623
631
|
| `{inceptionDocsRoot}` | `paths.inceptionDocs` | `docs/inception` |
|
|
632
|
+
| `{principlesDocsRoot}` | `paths.principlesDocs` | `docs/principles` |
|
|
633
|
+
| `{folderRulesPath}` | `paths.folderRulesDoc` | `docs/folder_management_rules.md` |
|
|
624
634
|
|
|
625
635
|
Setting `paths.designDocs` to `mydocs/product/construction` makes the L2 phase-gate require `mydocs/product/construction/{unit}/domain_model.md` instead of the default. This setting points to the construction subtree, not the broader product root; product-wide artifacts under `docs/product/` stay literal unless you define a custom `phaseDependencies.gates[]` preset. Default values match the v0.115.0 layout for full backward compatibility.
|
|
626
636
|
|
|
@@ -65,7 +65,9 @@ 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
|
|
68
|
+
Personal install keeps team-owned files out of the apply path: `package.json`, `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 runtime-visible local agent context (`.claude/CLAUDE.md` for Claude, and `AGENTS.md` for Codex only when that root file is absent or already PhaseGate-managed), 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`. If a team `AGENTS.md` already exists, personal Codex install leaves it unchanged and `doctor --personal --agent codex` reports the remaining context step instead of hiding it behind `AGENTS.override.md`. 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 --> <!-- @work-item-id WI-215 -->
|
|
69
|
+
|
|
70
|
+
If an existing repository keeps design or governance docs outside `docs/`, set `paths.designDocs`, `paths.inceptionDocs`, `paths.principlesDocs`, and `paths.folderRulesDoc` in `phasegate.config.json` before setup/reconcile. PhaseGate deploy and hook protection use those mappings instead of forcing the default `docs/` layout. <!-- @work-item-id WI-214 -->
|
|
69
71
|
|
|
70
72
|
For agent-readable planning before writing files:
|
|
71
73
|
|
|
@@ -7,6 +7,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
7
7
|
<!-- @work-item-id WI-169 -->
|
|
8
8
|
<!-- @work-item-id WI-208 -->
|
|
9
9
|
<!-- @work-item-id WI-209 -->
|
|
10
|
+
<!-- @work-item-id WI-215 -->
|
|
10
11
|
|
|
11
12
|
## Artifact Classes
|
|
12
13
|
|
|
@@ -16,7 +17,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
16
17
|
| 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
18
|
| 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
19
|
| 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 |
|
|
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 |
|
|
20
|
+
| Personal install artifact | `.phasegate-local/phasegate.config.json`, ignored real `.claude/CLAUDE.md` + `.claude/settings.json` + `.claude/skills/`, ignored real `.codex/hooks.json` + `.codex/skills/`, optional ignored root `AGENTS.md`, `.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 |
|
|
20
21
|
| 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 |
|
|
21
22
|
| 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 |
|
|
22
23
|
|
|
@@ -44,6 +45,8 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
44
45
|
|
|
45
46
|
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
|
|
|
48
|
+
Personal agent context is also placed only where the runtime will read it. Claude Code uses `.claude/CLAUDE.md`. Codex uses root `AGENTS.md` when PhaseGate can create or manage it locally; if a team `AGENTS.md` already exists, PhaseGate leaves it unchanged and doctor reports `codex-context-missing` instead of creating `AGENTS.override.md`. <!-- @work-item-id WI-215 -->
|
|
49
|
+
|
|
47
50
|
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
51
|
|
|
49
52
|
## Doctor Findings
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"paths": {
|
|
57
57
|
"designDocs": ".phasegate-local/product/construction",
|
|
58
|
-
"inceptionDocs": ".phasegate-local/inception"
|
|
58
|
+
"inceptionDocs": ".phasegate-local/inception",
|
|
59
|
+
"principlesDocs": ".phasegate-local/docs/principles",
|
|
60
|
+
"folderRulesDoc": ".phasegate-local/docs/folder_management_rules.md"
|
|
59
61
|
},
|
|
60
62
|
"reporting": {
|
|
61
63
|
"format": "json",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"harnesses": {},
|
|
21
21
|
"paths": {
|
|
22
22
|
"designDocs": "docs/product/construction",
|
|
23
|
-
"inceptionDocs": "docs/inception"
|
|
23
|
+
"inceptionDocs": "docs/inception",
|
|
24
|
+
"principlesDocs": "docs/principles",
|
|
25
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
24
26
|
},
|
|
25
27
|
"reporting": {
|
|
26
28
|
"format": "json",
|
package/package.json
CHANGED
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @work-item-id WI-201
|
|
6
6
|
* @work-item-id WI-202 / WI-204
|
|
7
7
|
* @work-item-id WI-206
|
|
8
|
+
* @work-item-id WI-214
|
|
8
9
|
*
|
|
9
10
|
* HandlePreToolUseUseCase
|
|
10
11
|
* PreToolUse Hook処理のオーケストレーション
|
|
@@ -116,7 +117,7 @@ export class HandlePreToolUseUseCase {
|
|
|
116
117
|
const blockedFilePath = metadata?.blockedFilePath ?? input.targetFilePaths[0];
|
|
117
118
|
|
|
118
119
|
if (metadata?.reason === "PROTECTED_FILE") {
|
|
119
|
-
return
|
|
120
|
+
return this.buildProtectedFileBlockOutput(blockedFilePath);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
if (metadata?.reason === "PHASE_GATE") {
|
|
@@ -492,15 +493,22 @@ export class HandlePreToolUseUseCase {
|
|
|
492
493
|
message: (fp) =>
|
|
493
494
|
`保護ファイルへの書き込みがブロックされました: ${fp}\nClaude Code の設定変更は /update-config スキルを使用してください。`,
|
|
494
495
|
},
|
|
495
|
-
{
|
|
496
|
-
pattern: /(?:^|\/)docs\/principles\//,
|
|
497
|
-
message: (fp) =>
|
|
498
|
-
`保護ファイルへの書き込みがブロックされました: ${fp}\n原則ドキュメントは immutable です。変更はできません。`,
|
|
499
|
-
},
|
|
500
496
|
];
|
|
501
497
|
|
|
502
|
-
private
|
|
498
|
+
private async buildProtectedFileBlockOutput(blockedFilePath: string | undefined): Promise<HandlePreToolUseOutput> {
|
|
503
499
|
const fp = blockedFilePath ?? "不明なファイル";
|
|
500
|
+
const dynamicPrinciplesPattern = await this.findMatchingDynamicProtectedPattern(fp, "principles");
|
|
501
|
+
if (dynamicPrinciplesPattern !== null) {
|
|
502
|
+
return {
|
|
503
|
+
shouldBlock: true,
|
|
504
|
+
blockedFilePath,
|
|
505
|
+
blockReason: "PROTECTED_FILE",
|
|
506
|
+
error: {
|
|
507
|
+
message: `保護ファイルへの書き込みがブロックされました: ${fp}\n原則ドキュメントは immutable です。変更はできません。対象パターン: ${dynamicPrinciplesPattern}`,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
504
512
|
const matched = HandlePreToolUseUseCase.PROTECTED_FILE_GUIDANCE.find(({ pattern }) => pattern.test(fp));
|
|
505
513
|
const message = matched
|
|
506
514
|
? matched.message(fp)
|
|
@@ -514,6 +522,20 @@ export class HandlePreToolUseUseCase {
|
|
|
514
522
|
};
|
|
515
523
|
}
|
|
516
524
|
|
|
525
|
+
private async findMatchingDynamicProtectedPattern(filePath: string, kind: "principles"): Promise<string | null> {
|
|
526
|
+
const patterns = await this.configQueryPort.getProtectedFilePatterns();
|
|
527
|
+
for (const pattern of patterns) {
|
|
528
|
+
if (kind === "principles" && !pattern.endsWith("/**")) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const prefix = pattern.slice(0, -"/**".length);
|
|
532
|
+
if (filePath === prefix || filePath.startsWith(`${prefix}/`)) {
|
|
533
|
+
return pattern;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
517
539
|
private static buildStoryReflectionBlockOutput(
|
|
518
540
|
blockedFilePath: string | undefined,
|
|
519
541
|
blockers: readonly string[],
|
|
@@ -36,6 +36,7 @@ interface HarnessesSection {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
interface ProtectedFilesSection {
|
|
39
|
+
patterns?: string[];
|
|
39
40
|
exclude?: string[];
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -57,6 +58,12 @@ interface AgentIntegrationSection {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
interface HarnessConfigDocument {
|
|
61
|
+
paths?: {
|
|
62
|
+
designDocs?: string;
|
|
63
|
+
inceptionDocs?: string;
|
|
64
|
+
principlesDocs?: string;
|
|
65
|
+
folderRulesDoc?: string;
|
|
66
|
+
};
|
|
60
67
|
harnesses?: HarnessesSection;
|
|
61
68
|
project?: ProjectSection;
|
|
62
69
|
protectedFiles?: ProtectedFilesSection;
|
|
@@ -102,8 +109,15 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
async getProtectedFilePatterns(): Promise<string[]> {
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
const config = this.loadConfig();
|
|
113
|
+
const configured = config.protectedFiles?.patterns ?? [];
|
|
114
|
+
const principlesDocs = config.paths?.principlesDocs ?? 'docs/principles';
|
|
115
|
+
const folderRulesDoc = config.paths?.folderRulesDoc ?? 'docs/folder_management_rules.md';
|
|
116
|
+
return [
|
|
117
|
+
...configured,
|
|
118
|
+
`${normalizeProjectPath(principlesDocs)}/**`,
|
|
119
|
+
normalizeProjectPath(folderRulesDoc),
|
|
120
|
+
];
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
async getProtectedFileExclusions(): Promise<string[]> {
|
|
@@ -119,12 +133,13 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
119
133
|
getProjectPaths(): ProjectPaths {
|
|
120
134
|
const config = this.loadConfig();
|
|
121
135
|
const paths = config.project?.paths;
|
|
136
|
+
const topLevelPaths = config.paths;
|
|
122
137
|
|
|
123
138
|
return ProjectPaths.create(
|
|
124
139
|
paths?.source ?? ['scripts/harness'],
|
|
125
140
|
{
|
|
126
|
-
construction: paths?.docs?.construction ?? 'docs/product/construction',
|
|
127
|
-
inception: paths?.docs?.inception ?? 'docs/inception',
|
|
141
|
+
construction: paths?.docs?.construction ?? topLevelPaths?.designDocs ?? 'docs/product/construction',
|
|
142
|
+
inception: paths?.docs?.inception ?? topLevelPaths?.inceptionDocs ?? 'docs/inception',
|
|
128
143
|
},
|
|
129
144
|
);
|
|
130
145
|
}
|
|
@@ -145,3 +160,7 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
145
160
|
return enforce === true;
|
|
146
161
|
}
|
|
147
162
|
}
|
|
163
|
+
|
|
164
|
+
function normalizeProjectPath(path: string): string {
|
|
165
|
+
return path.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
166
|
+
}
|
|
@@ -10,18 +10,29 @@ import { ConfigValidationError } from '../errors/config-validation-error.js';
|
|
|
10
10
|
export interface PathsConfigProps {
|
|
11
11
|
readonly designDocs: string;
|
|
12
12
|
readonly inceptionDocs: string;
|
|
13
|
+
readonly principlesDocs?: string;
|
|
14
|
+
readonly folderRulesDoc?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export class PathsConfig {
|
|
16
18
|
readonly designDocs: string;
|
|
17
19
|
readonly inceptionDocs: string;
|
|
20
|
+
readonly principlesDocs: string;
|
|
21
|
+
readonly folderRulesDoc: string;
|
|
18
22
|
|
|
19
23
|
constructor(props: PathsConfigProps) {
|
|
24
|
+
const principlesDocs = props.principlesDocs ?? 'docs/principles';
|
|
25
|
+
const folderRulesDoc = props.folderRulesDoc ?? 'docs/folder_management_rules.md';
|
|
26
|
+
|
|
20
27
|
PathsConfig.validatePath(props.designDocs, 'designDocs');
|
|
21
28
|
PathsConfig.validatePath(props.inceptionDocs, 'inceptionDocs');
|
|
29
|
+
PathsConfig.validatePath(principlesDocs, 'principlesDocs');
|
|
30
|
+
PathsConfig.validatePath(folderRulesDoc, 'folderRulesDoc');
|
|
22
31
|
|
|
23
32
|
this.designDocs = props.designDocs;
|
|
24
33
|
this.inceptionDocs = props.inceptionDocs;
|
|
34
|
+
this.principlesDocs = principlesDocs;
|
|
35
|
+
this.folderRulesDoc = folderRulesDoc;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
private static validatePath(value: string, fieldName: string): void {
|
|
@@ -44,7 +55,9 @@ export class PathsConfig {
|
|
|
44
55
|
equals(other: PathsConfig): boolean {
|
|
45
56
|
return (
|
|
46
57
|
this.designDocs === other.designDocs &&
|
|
47
|
-
this.inceptionDocs === other.inceptionDocs
|
|
58
|
+
this.inceptionDocs === other.inceptionDocs &&
|
|
59
|
+
this.principlesDocs === other.principlesDocs &&
|
|
60
|
+
this.folderRulesDoc === other.folderRulesDoc
|
|
48
61
|
);
|
|
49
62
|
}
|
|
50
63
|
}
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"paths": {
|
|
43
43
|
"designDocs": "docs/product/construction",
|
|
44
|
-
"inceptionDocs": "docs/inception"
|
|
44
|
+
"inceptionDocs": "docs/inception",
|
|
45
|
+
"principlesDocs": "docs/principles",
|
|
46
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
45
47
|
},
|
|
46
48
|
"reporting": {
|
|
47
49
|
"format": "json",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"paths": {
|
|
43
43
|
"designDocs": "docs/product/construction",
|
|
44
|
-
"inceptionDocs": "docs/inception"
|
|
44
|
+
"inceptionDocs": "docs/inception",
|
|
45
|
+
"principlesDocs": "docs/principles",
|
|
46
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
45
47
|
},
|
|
46
48
|
"reporting": {
|
|
47
49
|
"format": "json",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"paths": {
|
|
43
43
|
"designDocs": "docs/product/construction",
|
|
44
|
-
"inceptionDocs": "docs/inception"
|
|
44
|
+
"inceptionDocs": "docs/inception",
|
|
45
|
+
"principlesDocs": "docs/principles",
|
|
46
|
+
"folderRulesDoc": "docs/folder_management_rules.md"
|
|
45
47
|
},
|
|
46
48
|
"reporting": {
|
|
47
49
|
"format": "json",
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json
CHANGED
|
@@ -435,7 +435,9 @@
|
|
|
435
435
|
"additionalProperties": false,
|
|
436
436
|
"required": [
|
|
437
437
|
"designDocs",
|
|
438
|
-
"inceptionDocs"
|
|
438
|
+
"inceptionDocs",
|
|
439
|
+
"principlesDocs",
|
|
440
|
+
"folderRulesDoc"
|
|
439
441
|
],
|
|
440
442
|
"properties": {
|
|
441
443
|
"designDocs": {
|
|
@@ -445,6 +447,14 @@
|
|
|
445
447
|
"inceptionDocs": {
|
|
446
448
|
"type": "string",
|
|
447
449
|
"minLength": 1
|
|
450
|
+
},
|
|
451
|
+
"principlesDocs": {
|
|
452
|
+
"type": "string",
|
|
453
|
+
"minLength": 1
|
|
454
|
+
},
|
|
455
|
+
"folderRulesDoc": {
|
|
456
|
+
"type": "string",
|
|
457
|
+
"minLength": 1
|
|
448
458
|
}
|
|
449
459
|
}
|
|
450
460
|
},
|
|
@@ -488,6 +498,14 @@
|
|
|
488
498
|
"type": "object",
|
|
489
499
|
"additionalProperties": false,
|
|
490
500
|
"properties": {
|
|
501
|
+
"patterns": {
|
|
502
|
+
"type": "array",
|
|
503
|
+
"items": {
|
|
504
|
+
"type": "string",
|
|
505
|
+
"minLength": 1
|
|
506
|
+
},
|
|
507
|
+
"uniqueItems": true
|
|
508
|
+
},
|
|
491
509
|
"exclude": {
|
|
492
510
|
"type": "array",
|
|
493
511
|
"items": {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-215
|
|
4
|
+
|
|
5
|
+
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
|
+
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
|
+
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
8
|
+
import { createFinding, projectPath } from "./check-utils.js";
|
|
9
|
+
|
|
10
|
+
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
11
|
+
|
|
12
|
+
export class ClaudeContextMissingCheck implements HeuristicCheck {
|
|
13
|
+
readonly checkId = "claude-context-missing" as const;
|
|
14
|
+
private readonly target = ".claude/CLAUDE.md";
|
|
15
|
+
private readonly projectTarget = "CLAUDE.md";
|
|
16
|
+
private readonly legacyTarget = ".claude/CLAUDE.local.md";
|
|
17
|
+
|
|
18
|
+
async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
19
|
+
const content = await inspector.readText(projectPath(projectRoot, this.target));
|
|
20
|
+
if (content?.includes(MARKDOWN_BEGIN) === true && content.includes("PhaseGate")) return null;
|
|
21
|
+
const projectContent = await inspector.readText(projectPath(projectRoot, this.projectTarget));
|
|
22
|
+
if (projectContent?.includes(MARKDOWN_BEGIN) === true && projectContent.includes("PhaseGate")) return null;
|
|
23
|
+
|
|
24
|
+
const legacyContent = await inspector.readText(projectPath(projectRoot, this.legacyTarget));
|
|
25
|
+
return createFinding({
|
|
26
|
+
checkId: this.checkId,
|
|
27
|
+
severity: "red",
|
|
28
|
+
target: this.target,
|
|
29
|
+
message: legacyContent?.includes("PhaseGate") === true
|
|
30
|
+
? ".claude/CLAUDE.local.md は Claude Code の documented discovery path ではありません"
|
|
31
|
+
: ".claude/CLAUDE.md に runtime-visible PhaseGate context がありません",
|
|
32
|
+
repairMode: "mechanical",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-215
|
|
4
|
+
|
|
5
|
+
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
|
+
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
|
+
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
8
|
+
import { createFinding, projectPath } from "./check-utils.js";
|
|
9
|
+
|
|
10
|
+
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
11
|
+
|
|
12
|
+
export class CodexContextMissingCheck implements HeuristicCheck {
|
|
13
|
+
readonly checkId = "codex-context-missing" as const;
|
|
14
|
+
private readonly target = "AGENTS.md";
|
|
15
|
+
private readonly legacyTarget = ".codex/AGENTS.local.md";
|
|
16
|
+
|
|
17
|
+
async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
18
|
+
const content = await inspector.readText(projectPath(projectRoot, this.target));
|
|
19
|
+
if (content?.includes(MARKDOWN_BEGIN) === true && content.includes("PhaseGate")) return null;
|
|
20
|
+
|
|
21
|
+
const legacyContent = await inspector.readText(projectPath(projectRoot, this.legacyTarget));
|
|
22
|
+
return createFinding({
|
|
23
|
+
checkId: this.checkId,
|
|
24
|
+
severity: "red",
|
|
25
|
+
target: this.target,
|
|
26
|
+
message: legacyContent?.includes("PhaseGate") === true
|
|
27
|
+
? ".codex/AGENTS.local.md は Codex の default discovery path ではありません"
|
|
28
|
+
: "AGENTS.md に runtime-visible PhaseGate context がありません",
|
|
29
|
+
repairMode: content === null ? "mechanical" : "manual",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-178
|
|
5
5
|
// @work-item-id WI-208
|
|
6
|
+
// @work-item-id WI-215
|
|
6
7
|
|
|
7
8
|
import { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
8
9
|
import type { CheckId } from "../../domain/check-id.js";
|
|
@@ -32,8 +33,8 @@ export interface RunDoctorDiagnosticsOutput {
|
|
|
32
33
|
readonly exitCode: number;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const CLAUDE_ONLY_CHECKS = new Set<CheckId>(["claude-hook-missing", "claude-skills-symlink"]);
|
|
36
|
-
const CODEX_ONLY_CHECKS = new Set<CheckId>(["codex-hook-missing", "codex-skills-symlink"]);
|
|
36
|
+
const CLAUDE_ONLY_CHECKS = new Set<CheckId>(["claude-hook-missing", "claude-context-missing", "claude-skills-symlink"]);
|
|
37
|
+
const CODEX_ONLY_CHECKS = new Set<CheckId>(["codex-hook-missing", "codex-context-missing", "codex-skills-symlink"]);
|
|
37
38
|
const PERSONAL_SCOPED_OUT_CHECKS = new Set<CheckId>([
|
|
38
39
|
"husky-pre-commit-missing",
|
|
39
40
|
"husky-commit-msg-missing",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// @work-item-id WI-209
|
|
13
13
|
// @work-item-id WI-210
|
|
14
14
|
// @work-item-id WI-213
|
|
15
|
+
// @work-item-id WI-214
|
|
16
|
+
// @work-item-id WI-215
|
|
15
17
|
|
|
16
18
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
|
|
17
19
|
import { dirname, join } from "node:path";
|
|
@@ -77,6 +79,7 @@ interface InstallTarget {
|
|
|
77
79
|
readonly templatePath: string;
|
|
78
80
|
readonly executable?: boolean;
|
|
79
81
|
readonly block?: ManagedBlockInput;
|
|
82
|
+
readonly personalManualIfUnmanaged?: boolean;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
const SKILL_HINT = "invoke /phasegate-config-doctor";
|
|
@@ -88,8 +91,10 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
|
88
91
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
89
92
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
90
93
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
91
|
-
const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/settings.json", ".codex/hooks.json"]);
|
|
94
|
+
const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/CLAUDE.md", ".claude/settings.json", "AGENTS.md", ".codex/hooks.json"]);
|
|
92
95
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
96
|
+
const PERSONAL_PRINCIPLES_DOCS = ".phasegate-local/docs/principles";
|
|
97
|
+
const PERSONAL_FOLDER_RULES_DOC = ".phasegate-local/docs/folder_management_rules.md";
|
|
93
98
|
|
|
94
99
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
95
100
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -347,8 +352,9 @@ export class RunInstallUseCase {
|
|
|
347
352
|
const existingEntry = baseManifest.findEntry(target.path);
|
|
348
353
|
const beforeHash = before === null ? null : this.hashCalculator.compute(before);
|
|
349
354
|
const unmanagedPersonalRuntimeFile = input.personal
|
|
350
|
-
&& PERSONAL_AGENT_RUNTIME_FILES.has(target.path)
|
|
355
|
+
&& (PERSONAL_AGENT_RUNTIME_FILES.has(target.path) || target.personalManualIfUnmanaged === true)
|
|
351
356
|
&& before !== null
|
|
357
|
+
&& !before.includes(MARKDOWN_BEGIN)
|
|
352
358
|
&& (existingEntry === null || beforeHash === null || !beforeHash.equals(existingEntry.hash));
|
|
353
359
|
const repairMode = unmanagedPersonalRuntimeFile ? "manual" : this.repairMode(target, before);
|
|
354
360
|
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
@@ -361,7 +367,7 @@ export class RunInstallUseCase {
|
|
|
361
367
|
strategy: target.strategy,
|
|
362
368
|
changed: didChange,
|
|
363
369
|
summary: unmanagedPersonalRuntimeFile
|
|
364
|
-
? `${target.path}: existing non-phasegate path requires manual review`
|
|
370
|
+
? `${target.path}: existing non-phasegate runtime path requires manual review`
|
|
365
371
|
: didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
|
|
366
372
|
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
367
373
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
@@ -626,10 +632,11 @@ export class RunInstallUseCase {
|
|
|
626
632
|
...(options.includeClaude
|
|
627
633
|
? [
|
|
628
634
|
{
|
|
629
|
-
path: ".claude/CLAUDE.
|
|
635
|
+
path: ".claude/CLAUDE.md",
|
|
630
636
|
strategy: "markdown-managed" as const,
|
|
631
637
|
templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
|
|
632
|
-
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal CLAUDE.
|
|
638
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal .claude/CLAUDE.md managed section" },
|
|
639
|
+
personalManualIfUnmanaged: true,
|
|
633
640
|
},
|
|
634
641
|
{
|
|
635
642
|
path: ".claude/settings.json",
|
|
@@ -641,10 +648,11 @@ export class RunInstallUseCase {
|
|
|
641
648
|
...(options.includeCodex
|
|
642
649
|
? [
|
|
643
650
|
{
|
|
644
|
-
path: "
|
|
651
|
+
path: "AGENTS.md",
|
|
645
652
|
strategy: "markdown-managed" as const,
|
|
646
653
|
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
647
|
-
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal AGENTS.
|
|
654
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate personal AGENTS.md managed section" },
|
|
655
|
+
personalManualIfUnmanaged: true,
|
|
648
656
|
},
|
|
649
657
|
{
|
|
650
658
|
path: ".codex/hooks.json",
|
|
@@ -666,22 +674,22 @@ export class RunInstallUseCase {
|
|
|
666
674
|
executable: true,
|
|
667
675
|
},
|
|
668
676
|
{
|
|
669
|
-
path:
|
|
677
|
+
path: PERSONAL_FOLDER_RULES_DOC,
|
|
670
678
|
strategy: "copy" as const,
|
|
671
679
|
templatePath: "docs/folder_management_rules.md",
|
|
672
680
|
},
|
|
673
681
|
{
|
|
674
|
-
path:
|
|
682
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/architecture-philosophy.md`,
|
|
675
683
|
strategy: "copy" as const,
|
|
676
684
|
templatePath: "docs/principles/architecture-philosophy.md",
|
|
677
685
|
},
|
|
678
686
|
{
|
|
679
|
-
path:
|
|
687
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/model-routing.md`,
|
|
680
688
|
strategy: "copy" as const,
|
|
681
689
|
templatePath: "docs/principles/model-routing.md",
|
|
682
690
|
},
|
|
683
691
|
{
|
|
684
|
-
path:
|
|
692
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/testing-rules.md`,
|
|
685
693
|
strategy: "copy" as const,
|
|
686
694
|
templatePath: "docs/principles/testing-rules.md",
|
|
687
695
|
},
|
|
@@ -365,7 +365,9 @@ export class RunUninstallUseCase {
|
|
|
365
365
|
if (mode === "symlink") return "symlink";
|
|
366
366
|
if (mode === "created") return path.endsWith(".yml") || path.endsWith(".yaml") ? "yaml-add" : "created";
|
|
367
367
|
if (path === "package.json") return "package-json";
|
|
368
|
-
if (path === "AGENTS.md" || path === "CLAUDE.md"
|
|
368
|
+
if (path === "AGENTS.md" || path === "CLAUDE.md" || path === ".claude/CLAUDE.md" || path === ".claude/CLAUDE.local.md" || path === ".codex/AGENTS.local.md") {
|
|
369
|
+
return "markdown-managed";
|
|
370
|
+
}
|
|
369
371
|
if (path === ".git/info/exclude") return "text-managed";
|
|
370
372
|
if (path.endsWith(".json")) return "json";
|
|
371
373
|
if (path.startsWith(".husky/")) return "shell";
|
|
@@ -376,7 +378,9 @@ export class RunUninstallUseCase {
|
|
|
376
378
|
if (path === ".claude/settings.json") return "templates/.claude/settings.json";
|
|
377
379
|
if (path === ".codex/hooks.json") return "templates/.codex/hooks.json";
|
|
378
380
|
if (path === "CLAUDE.md") return "docs/templates/agent-context/CLAUDE.md.template.md";
|
|
381
|
+
if (path === ".claude/CLAUDE.md" || path === ".claude/CLAUDE.local.md") return "docs/templates/agent-context/CLAUDE.md.template.md";
|
|
379
382
|
if (path === "AGENTS.md") return "docs/templates/agent-context/AGENTS.md.template.md";
|
|
383
|
+
if (path === ".codex/AGENTS.local.md") return "docs/templates/agent-context/AGENTS.md.template.md";
|
|
380
384
|
throw new Error(`No template for ${path}`);
|
|
381
385
|
}
|
|
382
386
|
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
// @layer presentation
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-148
|
|
5
|
+
// @work-item-id WI-215
|
|
5
6
|
|
|
7
|
+
import { ClaudeContextMissingCheck } from "./application/checks/claude-context-missing-check.js";
|
|
6
8
|
import { ClaudeHookMissingCheck } from "./application/checks/claude-hook-missing-check.js";
|
|
7
9
|
import { ClaudeSkillsSymlinkCheck } from "./application/checks/claude-skills-symlink-check.js";
|
|
10
|
+
import { CodexContextMissingCheck } from "./application/checks/codex-context-missing-check.js";
|
|
8
11
|
import { CodexHookMissingCheck } from "./application/checks/codex-hook-missing-check.js";
|
|
9
12
|
import { CodexSkillsSymlinkCheck } from "./application/checks/codex-skills-symlink-check.js";
|
|
10
13
|
import { CiWorkflowMissingCheck } from "./application/checks/ci-workflow-missing-check.js";
|
|
@@ -42,7 +45,9 @@ export function createInstallationModule() {
|
|
|
42
45
|
const hashCalculator = new NodeCryptoHashAdapter();
|
|
43
46
|
const checks = [
|
|
44
47
|
new ClaudeHookMissingCheck(),
|
|
48
|
+
new ClaudeContextMissingCheck(),
|
|
45
49
|
new CodexHookMissingCheck(),
|
|
50
|
+
new CodexContextMissingCheck(),
|
|
46
51
|
new HuskyPreCommitMissingCheck(),
|
|
47
52
|
new HuskyCommitMsgMissingCheck(),
|
|
48
53
|
new HuskyPrePushMissingCheck(),
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-215
|
|
4
5
|
|
|
5
6
|
export const CHECK_IDS = [
|
|
6
7
|
"claude-hook-missing",
|
|
8
|
+
"claude-context-missing",
|
|
7
9
|
"codex-hook-missing",
|
|
10
|
+
"codex-context-missing",
|
|
8
11
|
"husky-pre-commit-missing",
|
|
9
12
|
"husky-commit-msg-missing",
|
|
10
13
|
"husky-pre-push-missing",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-215
|
|
4
5
|
|
|
5
6
|
import type { CheckId } from "./check-id.js";
|
|
6
7
|
import { SuggestedSkill } from "./suggested-skill.js";
|
|
@@ -23,7 +24,9 @@ export class RepairTable {
|
|
|
23
24
|
constructor() {
|
|
24
25
|
this.table = new Map<CheckId, SuggestedSkill | null>([
|
|
25
26
|
["claude-hook-missing", CONFIG_DOCTOR],
|
|
27
|
+
["claude-context-missing", CONFIG_DOCTOR],
|
|
26
28
|
["codex-hook-missing", CONFIG_DOCTOR],
|
|
29
|
+
["codex-context-missing", CONFIG_DOCTOR],
|
|
27
30
|
["husky-pre-commit-missing", CONFIG_DOCTOR],
|
|
28
31
|
["husky-commit-msg-missing", CONFIG_DOCTOR],
|
|
29
32
|
["husky-pre-push-missing", null],
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
|
|
8
8
|
|
|
9
9
|
import { promises as fs } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
11
|
|
|
12
12
|
const HARNESS_VERSION_FILE = ".harness-version";
|
|
13
13
|
const SKILLS_SOURCE_DIR = "skills";
|
|
@@ -18,6 +18,8 @@ const CODEX_SKILLS_LINK_DIR = ".codex";
|
|
|
18
18
|
const HARNESS_CONFIG_FILE = "phasegate.config.json";
|
|
19
19
|
const HOOKS_TEMPLATE_DIR = join("templates", ".claude");
|
|
20
20
|
const HOOKS_TARGET_DIR = ".claude";
|
|
21
|
+
const DEFAULT_PRINCIPLES_DOCS = join("docs", "principles");
|
|
22
|
+
const DEFAULT_FOLDER_RULES_DOC = join("docs", "folder_management_rules.md");
|
|
21
23
|
|
|
22
24
|
// ── Skill Category Map ──
|
|
23
25
|
|
|
@@ -516,33 +518,55 @@ export interface DeployDesignDocsResult {
|
|
|
516
518
|
skippedFiles: string[];
|
|
517
519
|
}
|
|
518
520
|
|
|
521
|
+
interface DesignDocsPathConfig {
|
|
522
|
+
readonly principlesDocs: string;
|
|
523
|
+
readonly folderRulesDoc: string;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function resolveDesignDocsPathConfig(projectRoot: string): Promise<DesignDocsPathConfig> {
|
|
527
|
+
try {
|
|
528
|
+
const raw = await fs.readFile(join(projectRoot, HARNESS_CONFIG_FILE), "utf-8");
|
|
529
|
+
const config = JSON.parse(raw) as { paths?: { principlesDocs?: unknown; folderRulesDoc?: unknown } };
|
|
530
|
+
const principlesDocs =
|
|
531
|
+
typeof config.paths?.principlesDocs === "string" && config.paths.principlesDocs.length > 0
|
|
532
|
+
? config.paths.principlesDocs
|
|
533
|
+
: DEFAULT_PRINCIPLES_DOCS;
|
|
534
|
+
const folderRulesDoc =
|
|
535
|
+
typeof config.paths?.folderRulesDoc === "string" && config.paths.folderRulesDoc.length > 0
|
|
536
|
+
? config.paths.folderRulesDoc
|
|
537
|
+
: DEFAULT_FOLDER_RULES_DOC;
|
|
538
|
+
return { principlesDocs, folderRulesDoc };
|
|
539
|
+
} catch {
|
|
540
|
+
return { principlesDocs: DEFAULT_PRINCIPLES_DOCS, folderRulesDoc: DEFAULT_FOLDER_RULES_DOC };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
519
544
|
export async function deployDesignDocs(harnessRoot: string, projectRoot: string): Promise<DeployDesignDocsResult> {
|
|
520
545
|
const copiedFiles: string[] = [];
|
|
521
546
|
const skippedFiles: string[] = [];
|
|
522
|
-
const
|
|
523
|
-
const principlesTargetDir = join(
|
|
547
|
+
const pathConfig = await resolveDesignDocsPathConfig(projectRoot);
|
|
548
|
+
const principlesTargetDir = join(projectRoot, pathConfig.principlesDocs);
|
|
524
549
|
|
|
525
|
-
await fs.mkdir(
|
|
550
|
+
await fs.mkdir(join(projectRoot, dirname(pathConfig.folderRulesDoc)), { recursive: true });
|
|
526
551
|
await fs.mkdir(principlesTargetDir, { recursive: true });
|
|
527
552
|
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const folderRulesTarget = join(projectRoot, folderRulesRelativePath);
|
|
553
|
+
const folderRulesSource = join(harnessRoot, DEFAULT_FOLDER_RULES_DOC);
|
|
554
|
+
const folderRulesTarget = join(projectRoot, pathConfig.folderRulesDoc);
|
|
531
555
|
|
|
532
556
|
try {
|
|
533
557
|
await fs.access(folderRulesTarget);
|
|
534
|
-
skippedFiles.push(
|
|
558
|
+
skippedFiles.push(pathConfig.folderRulesDoc);
|
|
535
559
|
} catch {
|
|
536
560
|
try {
|
|
537
561
|
await fs.access(folderRulesSource);
|
|
538
562
|
await fs.copyFile(folderRulesSource, folderRulesTarget);
|
|
539
|
-
copiedFiles.push(
|
|
563
|
+
copiedFiles.push(pathConfig.folderRulesDoc);
|
|
540
564
|
} catch {
|
|
541
565
|
// 配置元が存在しない場合はスキップ
|
|
542
566
|
}
|
|
543
567
|
}
|
|
544
568
|
|
|
545
|
-
const principlesSourceDir = join(harnessRoot,
|
|
569
|
+
const principlesSourceDir = join(harnessRoot, DEFAULT_PRINCIPLES_DOCS);
|
|
546
570
|
|
|
547
571
|
try {
|
|
548
572
|
const principleEntries = await fs.readdir(principlesSourceDir, { withFileTypes: true });
|
|
@@ -552,7 +576,7 @@ export async function deployDesignDocs(harnessRoot: string, projectRoot: string)
|
|
|
552
576
|
.sort();
|
|
553
577
|
|
|
554
578
|
for (const principleFile of principleFiles) {
|
|
555
|
-
const relativePath = join(
|
|
579
|
+
const relativePath = join(pathConfig.principlesDocs, principleFile);
|
|
556
580
|
const sourcePath = join(principlesSourceDir, principleFile);
|
|
557
581
|
const targetPath = join(projectRoot, relativePath);
|
|
558
582
|
|