phasegate 0.160.17 → 0.160.18
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 +8 -0
- package/docs/guide/configuration.md +15 -5
- package/docs/guide/installation.md +2 -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/usecases/run-install.ts +7 -4
- 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
|
@@ -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
|
|
|
@@ -67,6 +67,8 @@ npx phasegate install --personal --agent claude --apply
|
|
|
67
67
|
|
|
68
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
|
+
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 -->
|
|
71
|
+
|
|
70
72
|
For agent-readable planning before writing files:
|
|
71
73
|
|
|
72
74
|
```bash
|
|
@@ -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": {
|
|
@@ -12,6 +12,7 @@
|
|
|
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
|
|
15
16
|
|
|
16
17
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
|
|
17
18
|
import { dirname, join } from "node:path";
|
|
@@ -90,6 +91,8 @@ const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
|
90
91
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
91
92
|
const PERSONAL_AGENT_RUNTIME_FILES = new Set([".claude/settings.json", ".codex/hooks.json"]);
|
|
92
93
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
94
|
+
const PERSONAL_PRINCIPLES_DOCS = ".phasegate-local/docs/principles";
|
|
95
|
+
const PERSONAL_FOLDER_RULES_DOC = ".phasegate-local/docs/folder_management_rules.md";
|
|
93
96
|
|
|
94
97
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
95
98
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -666,22 +669,22 @@ export class RunInstallUseCase {
|
|
|
666
669
|
executable: true,
|
|
667
670
|
},
|
|
668
671
|
{
|
|
669
|
-
path:
|
|
672
|
+
path: PERSONAL_FOLDER_RULES_DOC,
|
|
670
673
|
strategy: "copy" as const,
|
|
671
674
|
templatePath: "docs/folder_management_rules.md",
|
|
672
675
|
},
|
|
673
676
|
{
|
|
674
|
-
path:
|
|
677
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/architecture-philosophy.md`,
|
|
675
678
|
strategy: "copy" as const,
|
|
676
679
|
templatePath: "docs/principles/architecture-philosophy.md",
|
|
677
680
|
},
|
|
678
681
|
{
|
|
679
|
-
path:
|
|
682
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/model-routing.md`,
|
|
680
683
|
strategy: "copy" as const,
|
|
681
684
|
templatePath: "docs/principles/model-routing.md",
|
|
682
685
|
},
|
|
683
686
|
{
|
|
684
|
-
path:
|
|
687
|
+
path: `${PERSONAL_PRINCIPLES_DOCS}/testing-rules.md`,
|
|
685
688
|
strategy: "copy" as const,
|
|
686
689
|
templatePath: "docs/principles/testing-rules.md",
|
|
687
690
|
},
|
|
@@ -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
|
|