phasegate 0.152.9 → 0.153.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/docs/guide/getting-started.md +4 -2
- package/docs/guide/recipes.md +1 -1
- package/docs/guide/troubleshooting.md +14 -1
- package/package.json +1 -1
- package/scripts/harness/installation/application/usecases/run-install.ts +84 -7
- package/scripts/harness/installation/presentation/cli/install-handler.ts +10 -2
- package/scripts/harness/main.ts +266 -4
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.153.0] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **WI-175 — agent setup completeness and confidence** — adds `setup:agent` completeness summaries, `config:plan` managed/external action separation with `phasegate.config.json` patch previews, target-aware install permission error guidance, and regression coverage for strict setup no-diff behavior.
|
|
15
|
+
|
|
10
16
|
## [0.152.9] - 2026-05-13
|
|
11
17
|
|
|
12
18
|
### Added
|
package/README.md
CHANGED
|
@@ -109,6 +109,8 @@ npx phasegate setup:agent --intent recommended --dry-run --json
|
|
|
109
109
|
npx phasegate config:plan --intent codex-hooks --dry-run --json
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
`setup:agent --json` includes `plan.completeness`, which separates local configured/planned areas from external manual checks such as Codex user-level feature enablement or the first CI run. `config:plan` includes a read-only `configPatch` preview when an intent would change `phasegate.config.json`. <!-- @work-item-id WI-175 -->
|
|
113
|
+
|
|
112
114
|
The first command reports detected setup state, missing questions, planned managed targets, rollback, and validation. The second maps a configuration change intent to files, commands, risks, and checks. <!-- @work-item-id WI-172, WI-173 -->
|
|
113
115
|
|
|
114
116
|
To remove PhaseGate from that project later, run:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
<!-- @work-item-id WI-171 -->
|
|
3
|
+
<!-- @work-item-id WI-171, WI-175 -->
|
|
4
4
|
|
|
5
5
|
Use this page when you want the shortest path from "PhaseGate is installed" to "the next command is obvious".
|
|
6
6
|
|
|
@@ -31,6 +31,8 @@ The first successful run is:
|
|
|
31
31
|
- L2 validators pass or report only issues you intentionally left for a later rollout.
|
|
32
32
|
- The active agent can read `AGENTS.md` or `CLAUDE.md` and see the PhaseGate managed instructions.
|
|
33
33
|
|
|
34
|
+
`setup:agent --json` also returns `plan.completeness`. Treat `configured` and `planned` as local repository evidence, and treat `manual` entries as work PhaseGate cannot prove from local files, such as Codex user-level feature enablement or the first hosted CI run.
|
|
35
|
+
|
|
34
36
|
## Daily Use
|
|
35
37
|
|
|
36
38
|
```bash
|
|
@@ -48,7 +50,7 @@ npx phasegate setup:agent --dry-run --json
|
|
|
48
50
|
npx phasegate config:plan --intent codex-hooks --dry-run --json
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
`setup:agent` reads repository setup state and returns missing targets, questions, risk, rollback, and validation steps. `config:plan` maps natural-language change intents such as "enable Codex hooks" or "make L4 stricter" to concrete files, commands, and
|
|
53
|
+
`setup:agent` reads repository setup state and returns missing targets, completeness, questions, risk, rollback, and validation steps. `config:plan` maps natural-language change intents such as "enable Codex hooks" or "make L4 stricter" to concrete files, commands, checks, and a read-only `phasegate.config.json` patch preview when the intent changes local config.
|
|
52
54
|
|
|
53
55
|
## CI Use
|
|
54
56
|
|
package/docs/guide/recipes.md
CHANGED
|
@@ -62,4 +62,4 @@ npx phasegate check-change-category --paths <changed-files> --format json
|
|
|
62
62
|
npx phasegate ci-check --quick --dry-run
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Use `config:plan` before changing `phasegate.config.json`. It separates repo-managed artifacts from user-level settings and lists the checks needed after the change.
|
|
65
|
+
Use `config:plan` before changing `phasegate.config.json`. It separates repo-managed artifacts from user-level settings, previews the local config before/after for config-backed intents, and lists the checks needed after the change. <!-- @work-item-id WI-175 -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
|
-
<!-- @work-item-id WI-171, WI-172, WI-173 -->
|
|
3
|
+
<!-- @work-item-id WI-171, WI-172, WI-173, WI-175 -->
|
|
4
4
|
|
|
5
5
|
Start with:
|
|
6
6
|
|
|
@@ -58,3 +58,16 @@ npx phasegate config:plan --intent codex-hooks --dry-run --json
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
The planner output is intentionally agent-readable: detected state, questions, planned targets, risks, rollback, and validation commands are all explicit.
|
|
61
|
+
|
|
62
|
+
## Setup Completeness Still Has Manual Checks
|
|
63
|
+
|
|
64
|
+
`doctor` green means the local managed setup is consistent for the inspected targets. It does not prove user-level Codex feature flags, hosted GitHub Actions execution, npm registry state, or team policy acceptance.
|
|
65
|
+
|
|
66
|
+
Check `plan.completeness` in `setup:agent --json`:
|
|
67
|
+
|
|
68
|
+
- `configured`: PhaseGate found local evidence.
|
|
69
|
+
- `planned`: `--apply` will create or refresh the local target.
|
|
70
|
+
- `manual`: complete the listed external action yourself.
|
|
71
|
+
- `not-applicable`: the current intent/options did not select that area.
|
|
72
|
+
|
|
73
|
+
When `install` or `setup:agent --apply` returns a structured `error`, use its `target`, `operation`, `code`, `recovery`, and `partialChanges` fields before rerunning.
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-146
|
|
4
4
|
// @work-item-id WI-174
|
|
5
|
+
// @work-item-id WI-175
|
|
5
6
|
|
|
6
7
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink } from "node:fs/promises";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
@@ -47,6 +48,16 @@ export interface RunInstallResult {
|
|
|
47
48
|
readonly refused: readonly InstallPlanItem[];
|
|
48
49
|
readonly changed: readonly InstallPlanItem[];
|
|
49
50
|
readonly backupDir: string | null;
|
|
51
|
+
readonly error?: TargetAwareApplyError;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TargetAwareApplyError {
|
|
55
|
+
readonly target: string;
|
|
56
|
+
readonly operation: string;
|
|
57
|
+
readonly code: string;
|
|
58
|
+
readonly likelyCause: string;
|
|
59
|
+
readonly recovery: string;
|
|
60
|
+
readonly partialChanges: readonly string[];
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
interface InstallTarget {
|
|
@@ -216,6 +227,26 @@ function hasCustomJson(content: string | null): boolean {
|
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
function errorCode(error: unknown): string {
|
|
231
|
+
if (isRecord(error) && typeof error.code === "string") return error.code;
|
|
232
|
+
return "UNKNOWN";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function likelyCauseFor(code: string): string {
|
|
236
|
+
if (code === "EPERM") return "The filesystem or sandbox denied this write operation.";
|
|
237
|
+
if (code === "EACCES") return "The current user does not have permission to write this target.";
|
|
238
|
+
if (code === "EROFS") return "The project is on a read-only filesystem.";
|
|
239
|
+
if (code === "ENOTDIR") return "A parent path exists but is not a directory.";
|
|
240
|
+
return "The managed target could not be written.";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function recoveryFor(code: string, target: string): string {
|
|
244
|
+
if (code === "EPERM") return `Review sandbox or filesystem permissions for ${target}, then rerun phasegate setup:agent --apply or phasegate install --apply.`;
|
|
245
|
+
if (code === "EACCES") return `Fix ownership or permissions for ${target}, then rerun phasegate install --apply.`;
|
|
246
|
+
if (code === "EROFS") return `Move the project to a writable filesystem or rerun in a writable workspace before applying ${target}.`;
|
|
247
|
+
return `Inspect ${target}, run phasegate install --dry-run --json, then rerun with --apply after resolving the filesystem issue.`;
|
|
248
|
+
}
|
|
249
|
+
|
|
219
250
|
function shellRepairMode(content: string | null): RepairMode {
|
|
220
251
|
if (content === null || content.trim().length === 0 || content.includes(SHELL_BEGIN)) return "mechanical";
|
|
221
252
|
return "ai-assisted";
|
|
@@ -289,9 +320,23 @@ export class RunInstallUseCase {
|
|
|
289
320
|
await this.backup(input.projectRoot, target.path, backupDir);
|
|
290
321
|
}
|
|
291
322
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
try {
|
|
324
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "mkdir", error);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await writeFile(absolutePath, next, "utf8");
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "writeFile", error);
|
|
332
|
+
}
|
|
333
|
+
if (target.executable) {
|
|
334
|
+
try {
|
|
335
|
+
await chmod(absolutePath, 0o755);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, target.path, "chmod", error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
295
340
|
changed.push(item);
|
|
296
341
|
|
|
297
342
|
const mode = before === null ? "created" : "merged";
|
|
@@ -320,9 +365,17 @@ export class RunInstallUseCase {
|
|
|
320
365
|
const item = await this.planSkillLink(input.projectRoot, linkPath);
|
|
321
366
|
plan.push(item);
|
|
322
367
|
if (!input.apply || !item.changed) continue;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
368
|
+
try {
|
|
369
|
+
await mkdir(join(input.projectRoot, "skills"), { recursive: true });
|
|
370
|
+
await mkdir(dirname(join(input.projectRoot, linkPath)), { recursive: true });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkPath, "mkdir", error);
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
await symlink("../skills", join(input.projectRoot, linkPath), process.platform === "win32" ? "junction" : "dir");
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, linkPath, "symlink", error);
|
|
378
|
+
}
|
|
326
379
|
changed.push(item);
|
|
327
380
|
const hash = this.hashCalculator.compute("../skills");
|
|
328
381
|
const existingEntry = baseManifest.findEntry(linkPath);
|
|
@@ -342,12 +395,36 @@ export class RunInstallUseCase {
|
|
|
342
395
|
}
|
|
343
396
|
|
|
344
397
|
if (input.apply && changed.length > 0) {
|
|
345
|
-
|
|
398
|
+
try {
|
|
399
|
+
await this.manifestRepository.save(input.projectRoot, manifest);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return this.withApplyError({ plan, refused, changed, backupDir }, ".phasegate/manifest.json", "manifest-save", error);
|
|
402
|
+
}
|
|
346
403
|
}
|
|
347
404
|
|
|
348
405
|
return { plan, refused, changed, backupDir };
|
|
349
406
|
}
|
|
350
407
|
|
|
408
|
+
private withApplyError(
|
|
409
|
+
result: RunInstallResult,
|
|
410
|
+
target: string,
|
|
411
|
+
operation: string,
|
|
412
|
+
error: unknown,
|
|
413
|
+
): RunInstallResult {
|
|
414
|
+
const code = errorCode(error);
|
|
415
|
+
return {
|
|
416
|
+
...result,
|
|
417
|
+
error: {
|
|
418
|
+
target,
|
|
419
|
+
operation,
|
|
420
|
+
code,
|
|
421
|
+
likelyCause: likelyCauseFor(code),
|
|
422
|
+
recovery: recoveryFor(code, target),
|
|
423
|
+
partialChanges: result.changed.map((item) => item.path),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
351
428
|
private createTargets(options: {
|
|
352
429
|
readonly includeClaude: boolean;
|
|
353
430
|
readonly includeCodex: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer presentation
|
|
3
3
|
// @work-item-id WI-146
|
|
4
|
+
// @work-item-id WI-175
|
|
4
5
|
|
|
5
6
|
import type { RunInstallUseCase } from "../../application/usecases/run-install.js";
|
|
6
7
|
|
|
@@ -34,7 +35,7 @@ export class InstallHandler {
|
|
|
34
35
|
if (input.json) {
|
|
35
36
|
return {
|
|
36
37
|
stdout: JSON.stringify(result, null, 2),
|
|
37
|
-
exitCode: result.refused.length > 0 ? 1 : 0,
|
|
38
|
+
exitCode: result.refused.length > 0 || result.error !== undefined ? 1 : 0,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
const lines = [
|
|
@@ -45,13 +46,20 @@ export class InstallHandler {
|
|
|
45
46
|
}),
|
|
46
47
|
];
|
|
47
48
|
if (result.backupDir !== null) lines.push(`backups: ${result.backupDir}`);
|
|
49
|
+
if (result.error !== undefined) {
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push(`Apply error: ${result.error.target} ${result.error.operation} failed with ${result.error.code}`);
|
|
52
|
+
lines.push(`Cause: ${result.error.likelyCause}`);
|
|
53
|
+
lines.push(`Recovery: ${result.error.recovery}`);
|
|
54
|
+
if (result.error.partialChanges.length > 0) lines.push(`Partial changes: ${result.error.partialChanges.join(", ")}`);
|
|
55
|
+
}
|
|
48
56
|
if (result.refused.length > 0) {
|
|
49
57
|
lines.push("");
|
|
50
58
|
lines.push("Refused ai-assisted/manual targets. Re-run with --force after reviewing the hint.");
|
|
51
59
|
}
|
|
52
60
|
return {
|
|
53
61
|
stdout: lines.join("\n"),
|
|
54
|
-
exitCode: result.refused.length > 0 ? 1 : 0,
|
|
62
|
+
exitCode: result.refused.length > 0 || result.error !== undefined ? 1 : 0,
|
|
55
63
|
};
|
|
56
64
|
}
|
|
57
65
|
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @work-item-id WI-090 / WI-091
|
|
5
5
|
* @work-item-id WI-113 / WI-142
|
|
6
6
|
* @work-item-id WI-171 / WI-172 / WI-173
|
|
7
|
+
* @work-item-id WI-175
|
|
7
8
|
*
|
|
8
9
|
* Phasegate CLI エントリポイント。
|
|
9
10
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -723,6 +724,31 @@ type InitPhasePreset = "full" | "standard" | "minimal" | "custom";
|
|
|
723
724
|
type AgentTarget = "claude" | "codex" | "both";
|
|
724
725
|
type SetupIntent = "minimal" | "recommended" | "strict" | "ci-only" | "agent-hooks" | "retrofit";
|
|
725
726
|
type ConfigChangeIntent = "l4-strict" | "codex-hooks" | "ci-fail-on-warning" | "baseline-reset" | "quick-mode-strict";
|
|
727
|
+
type SetupCompletenessStatus = "configured" | "planned" | "manual" | "not-applicable" | "unknown";
|
|
728
|
+
|
|
729
|
+
interface SetupCompletenessEntry {
|
|
730
|
+
readonly area: string;
|
|
731
|
+
readonly status: SetupCompletenessStatus;
|
|
732
|
+
readonly evidence: readonly string[];
|
|
733
|
+
readonly nextAction: string | null;
|
|
734
|
+
readonly risk: string | null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
interface ConfigPatchOperation {
|
|
738
|
+
readonly op: "add" | "replace";
|
|
739
|
+
readonly pointer: string;
|
|
740
|
+
readonly before: unknown;
|
|
741
|
+
readonly after: unknown;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
interface ConfigPatchPreview {
|
|
745
|
+
readonly path: "phasegate.config.json";
|
|
746
|
+
readonly applicability: "applicable" | "not-applicable" | "blocked";
|
|
747
|
+
readonly blockedReason: string | null;
|
|
748
|
+
readonly before: unknown;
|
|
749
|
+
readonly after: unknown;
|
|
750
|
+
readonly operations: readonly ConfigPatchOperation[];
|
|
751
|
+
}
|
|
726
752
|
|
|
727
753
|
function parseInitPhasePreset(value: string | undefined): InitPhasePreset | undefined {
|
|
728
754
|
if (value === undefined) return undefined;
|
|
@@ -773,6 +799,138 @@ async function projectFileExists(rootDir: string, relativePath: string): Promise
|
|
|
773
799
|
}
|
|
774
800
|
}
|
|
775
801
|
|
|
802
|
+
function setupCompletenessEntry(input: {
|
|
803
|
+
readonly area: string;
|
|
804
|
+
readonly included: boolean;
|
|
805
|
+
readonly configured: boolean;
|
|
806
|
+
readonly plannedEvidence: string;
|
|
807
|
+
readonly configuredEvidence: string;
|
|
808
|
+
readonly nextAction: string;
|
|
809
|
+
readonly risk?: string;
|
|
810
|
+
}): SetupCompletenessEntry {
|
|
811
|
+
if (!input.included) {
|
|
812
|
+
return {
|
|
813
|
+
area: input.area,
|
|
814
|
+
status: "not-applicable",
|
|
815
|
+
evidence: ["Not selected for this setup intent/options."],
|
|
816
|
+
nextAction: null,
|
|
817
|
+
risk: null,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (input.configured) {
|
|
821
|
+
return {
|
|
822
|
+
area: input.area,
|
|
823
|
+
status: "configured",
|
|
824
|
+
evidence: [input.configuredEvidence],
|
|
825
|
+
nextAction: null,
|
|
826
|
+
risk: input.risk ?? null,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
area: input.area,
|
|
831
|
+
status: "planned",
|
|
832
|
+
evidence: [input.plannedEvidence],
|
|
833
|
+
nextAction: input.nextAction,
|
|
834
|
+
risk: input.risk ?? null,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function buildSetupCompleteness(input: {
|
|
839
|
+
readonly intent: SetupIntent;
|
|
840
|
+
readonly agent: AgentTarget;
|
|
841
|
+
readonly withHusky: boolean;
|
|
842
|
+
readonly withCi: boolean;
|
|
843
|
+
readonly checks: {
|
|
844
|
+
readonly packageJson: boolean;
|
|
845
|
+
readonly phasegateConfig: boolean;
|
|
846
|
+
readonly claudeSettings: boolean;
|
|
847
|
+
readonly codexHooks: boolean;
|
|
848
|
+
readonly agentsMd: boolean;
|
|
849
|
+
readonly claudeMd: boolean;
|
|
850
|
+
readonly huskyPreCommit: boolean;
|
|
851
|
+
readonly ciWorkflow: boolean;
|
|
852
|
+
readonly skillsVersion: boolean;
|
|
853
|
+
};
|
|
854
|
+
}): readonly SetupCompletenessEntry[] {
|
|
855
|
+
const includeClaude = input.agent === "claude" || input.agent === "both";
|
|
856
|
+
const includeCodex = input.agent === "codex" || input.agent === "both";
|
|
857
|
+
const agentHooksConfigured = (!includeClaude || input.checks.claudeSettings) && (!includeCodex || input.checks.codexHooks);
|
|
858
|
+
const agentContextConfigured = (!includeClaude || input.checks.claudeMd) && (!includeCodex || input.checks.agentsMd);
|
|
859
|
+
const entries: SetupCompletenessEntry[] = [
|
|
860
|
+
setupCompletenessEntry({
|
|
861
|
+
area: "local-config",
|
|
862
|
+
included: true,
|
|
863
|
+
configured: input.checks.packageJson && input.checks.phasegateConfig,
|
|
864
|
+
configuredEvidence: "package.json and phasegate.config.json are present.",
|
|
865
|
+
plannedEvidence: "setup:agent will create or merge package.json scripts and phasegate.config.json.",
|
|
866
|
+
nextAction: "Run setup:agent --apply, then phasegate doctor.",
|
|
867
|
+
}),
|
|
868
|
+
setupCompletenessEntry({
|
|
869
|
+
area: "agent-hooks",
|
|
870
|
+
included: includeClaude || includeCodex,
|
|
871
|
+
configured: agentHooksConfigured,
|
|
872
|
+
configuredEvidence: "Selected agent hook settings are present.",
|
|
873
|
+
plannedEvidence: "setup:agent will create or refresh selected agent hook settings.",
|
|
874
|
+
nextAction: "Run setup:agent --apply for selected agents.",
|
|
875
|
+
risk: includeCodex ? "Codex user-level hook feature enablement remains a manual external check." : undefined,
|
|
876
|
+
}),
|
|
877
|
+
setupCompletenessEntry({
|
|
878
|
+
area: "agent-context",
|
|
879
|
+
included: includeClaude || includeCodex,
|
|
880
|
+
configured: agentContextConfigured,
|
|
881
|
+
configuredEvidence: "Selected AGENTS.md / CLAUDE.md managed context files are present.",
|
|
882
|
+
plannedEvidence: "setup:agent will create or refresh selected managed agent context sections.",
|
|
883
|
+
nextAction: "Run setup:agent --apply and review preserved user-owned content.",
|
|
884
|
+
}),
|
|
885
|
+
setupCompletenessEntry({
|
|
886
|
+
area: "skills",
|
|
887
|
+
included: true,
|
|
888
|
+
configured: input.checks.skillsVersion,
|
|
889
|
+
configuredEvidence: "skills/.harness-version is present.",
|
|
890
|
+
plannedEvidence: "setup:agent will deploy bundled PhaseGate skills.",
|
|
891
|
+
nextAction: "Run setup:agent --apply to deploy skills.",
|
|
892
|
+
}),
|
|
893
|
+
setupCompletenessEntry({
|
|
894
|
+
area: "git-hooks",
|
|
895
|
+
included: input.withHusky,
|
|
896
|
+
configured: input.checks.huskyPreCommit,
|
|
897
|
+
configuredEvidence: ".husky/pre-commit is present.",
|
|
898
|
+
plannedEvidence: "setup:agent will create or refresh Husky pre-commit, commit-msg, and pre-push hooks.",
|
|
899
|
+
nextAction: "Run setup:agent --apply --with-husky.",
|
|
900
|
+
}),
|
|
901
|
+
setupCompletenessEntry({
|
|
902
|
+
area: "ci",
|
|
903
|
+
included: input.withCi,
|
|
904
|
+
configured: input.checks.ciWorkflow,
|
|
905
|
+
configuredEvidence: ".github/workflows/phasegate-aidlc-gate.yml is present.",
|
|
906
|
+
plannedEvidence: "setup:agent will create or refresh the GitHub Actions PhaseGate workflow.",
|
|
907
|
+
nextAction: "Run setup:agent --apply --with-ci.",
|
|
908
|
+
risk: "A hosted CI run is still an external manual check.",
|
|
909
|
+
}),
|
|
910
|
+
setupCompletenessEntry({
|
|
911
|
+
area: "validation",
|
|
912
|
+
included: true,
|
|
913
|
+
configured: input.checks.packageJson && input.checks.phasegateConfig,
|
|
914
|
+
configuredEvidence: "Local validation commands can be run against the configured project.",
|
|
915
|
+
plannedEvidence: "Validation commands are planned after setup apply.",
|
|
916
|
+
nextAction: "Run phasegate doctor, phasegate phasegate:check-ready, and phasegate validate --layer L2 --format human.",
|
|
917
|
+
}),
|
|
918
|
+
];
|
|
919
|
+
|
|
920
|
+
const externalActions: string[] = [];
|
|
921
|
+
if (includeCodex) externalActions.push("Run codex features enable codex_hooks if Codex hooks are not enabled for the user.");
|
|
922
|
+
if (input.withCi) externalActions.push("Trigger or inspect the first GitHub Actions PhaseGate workflow run.");
|
|
923
|
+
if (input.intent === "strict") externalActions.push("Confirm team policy accepts strict local and CI enforcement.");
|
|
924
|
+
entries.push({
|
|
925
|
+
area: "external-actions",
|
|
926
|
+
status: externalActions.length > 0 ? "manual" : "not-applicable",
|
|
927
|
+
evidence: externalActions.length > 0 ? externalActions : ["No external manual checks selected for this setup run."],
|
|
928
|
+
nextAction: externalActions.length > 0 ? externalActions.join(" ") : null,
|
|
929
|
+
risk: externalActions.length > 0 ? "PhaseGate cannot prove these user-level or hosted-service states from local files." : null,
|
|
930
|
+
});
|
|
931
|
+
return entries;
|
|
932
|
+
}
|
|
933
|
+
|
|
776
934
|
async function buildAgentSetupPlan(rootDir: string, input: {
|
|
777
935
|
readonly intent: SetupIntent;
|
|
778
936
|
readonly agent: AgentTarget;
|
|
@@ -789,6 +947,7 @@ async function buildAgentSetupPlan(rootDir: string, input: {
|
|
|
789
947
|
claudeMd: await projectFileExists(rootDir, "CLAUDE.md"),
|
|
790
948
|
huskyPreCommit: await projectFileExists(rootDir, ".husky/pre-commit"),
|
|
791
949
|
ciWorkflow: await projectFileExists(rootDir, ".github/workflows/phasegate-aidlc-gate.yml"),
|
|
950
|
+
skillsVersion: await projectFileExists(rootDir, "skills/.harness-version"),
|
|
792
951
|
};
|
|
793
952
|
const includeClaude = input.agent === "claude" || input.agent === "both";
|
|
794
953
|
const includeCodex = input.agent === "codex" || input.agent === "both";
|
|
@@ -810,6 +969,7 @@ async function buildAgentSetupPlan(rootDir: string, input: {
|
|
|
810
969
|
intent: input.intent,
|
|
811
970
|
agent: input.agent,
|
|
812
971
|
detected: checks,
|
|
972
|
+
completeness: buildSetupCompleteness({ ...input, checks }),
|
|
813
973
|
questions,
|
|
814
974
|
changes,
|
|
815
975
|
risks: [
|
|
@@ -829,47 +989,148 @@ async function buildAgentSetupPlan(rootDir: string, input: {
|
|
|
829
989
|
};
|
|
830
990
|
}
|
|
831
991
|
|
|
832
|
-
function
|
|
992
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
993
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function readProjectJson(rootDir: string, relativePath: string): Promise<unknown | null> {
|
|
997
|
+
try {
|
|
998
|
+
return JSON.parse(await fsReadFile(join(rootDir, relativePath), "utf8")) as unknown;
|
|
999
|
+
} catch {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function withNestedValue(source: unknown, path: readonly string[], value: unknown): Record<string, unknown> {
|
|
1005
|
+
const root = isPlainRecord(source) ? { ...source } : {};
|
|
1006
|
+
let cursor = root;
|
|
1007
|
+
for (const segment of path.slice(0, -1)) {
|
|
1008
|
+
const next = cursor[segment];
|
|
1009
|
+
cursor[segment] = isPlainRecord(next) ? { ...next } : {};
|
|
1010
|
+
cursor = cursor[segment] as Record<string, unknown>;
|
|
1011
|
+
}
|
|
1012
|
+
cursor[path[path.length - 1]] = value;
|
|
1013
|
+
return root;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function getNestedValue(source: unknown, path: readonly string[]): unknown {
|
|
1017
|
+
let cursor = source;
|
|
1018
|
+
for (const segment of path) {
|
|
1019
|
+
if (!isPlainRecord(cursor)) return undefined;
|
|
1020
|
+
cursor = cursor[segment];
|
|
1021
|
+
}
|
|
1022
|
+
return cursor;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | null): ConfigPatchPreview {
|
|
1026
|
+
const configIntents: Record<ConfigChangeIntent, readonly { readonly pointer: string; readonly path: readonly string[]; readonly value: unknown }[]> = {
|
|
1027
|
+
"l4-strict": [
|
|
1028
|
+
{ pointer: "/layers/L4/enabled", path: ["layers", "L4", "enabled"], value: true },
|
|
1029
|
+
{ pointer: "/layers/L4/failOnWarning", path: ["layers", "L4", "failOnWarning"], value: true },
|
|
1030
|
+
],
|
|
1031
|
+
"ci-fail-on-warning": [
|
|
1032
|
+
{ pointer: "/ci/enabled", path: ["ci", "enabled"], value: true },
|
|
1033
|
+
{ pointer: "/layers/L4/failOnWarning", path: ["layers", "L4", "failOnWarning"], value: true },
|
|
1034
|
+
],
|
|
1035
|
+
"quick-mode-strict": [
|
|
1036
|
+
{ pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["chore"] },
|
|
1037
|
+
{ pointer: "/quickMode/relaxedGates", path: ["quickMode", "relaxedGates"], value: [] },
|
|
1038
|
+
],
|
|
1039
|
+
"codex-hooks": [],
|
|
1040
|
+
"baseline-reset": [],
|
|
1041
|
+
};
|
|
1042
|
+
const changes = configIntents[intent];
|
|
1043
|
+
if (changes.length === 0) {
|
|
1044
|
+
return {
|
|
1045
|
+
path: "phasegate.config.json",
|
|
1046
|
+
applicability: "not-applicable",
|
|
1047
|
+
blockedReason: "Selected intent does not require a local phasegate.config.json mutation.",
|
|
1048
|
+
before,
|
|
1049
|
+
after: before,
|
|
1050
|
+
operations: [],
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
let after: unknown = before;
|
|
1054
|
+
const operations: ConfigPatchOperation[] = [];
|
|
1055
|
+
for (const change of changes) {
|
|
1056
|
+
const previous = getNestedValue(after, change.path);
|
|
1057
|
+
after = withNestedValue(after, change.path, change.value);
|
|
1058
|
+
operations.push({
|
|
1059
|
+
op: previous === undefined ? "add" : "replace",
|
|
1060
|
+
pointer: change.pointer,
|
|
1061
|
+
before: previous ?? null,
|
|
1062
|
+
after: change.value,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
path: "phasegate.config.json",
|
|
1067
|
+
applicability: "applicable",
|
|
1068
|
+
blockedReason: null,
|
|
1069
|
+
before,
|
|
1070
|
+
after,
|
|
1071
|
+
operations,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function hasStructuredInstallError(value: unknown): boolean {
|
|
1076
|
+
return isPlainRecord(value) && isPlainRecord(value.error);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent) {
|
|
833
1080
|
const catalog: Record<ConfigChangeIntent, {
|
|
834
1081
|
readonly targets: readonly string[];
|
|
1082
|
+
readonly managedTargets: readonly string[];
|
|
1083
|
+
readonly externalActions: readonly { readonly id: string; readonly label: string; readonly command: string | null; readonly blocking: boolean }[];
|
|
835
1084
|
readonly commands: readonly string[];
|
|
836
1085
|
readonly validations: readonly string[];
|
|
837
1086
|
readonly risks: readonly string[];
|
|
838
1087
|
}> = {
|
|
839
1088
|
"l4-strict": {
|
|
840
1089
|
targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
|
|
1090
|
+
managedTargets: ["phasegate.config.json"],
|
|
1091
|
+
externalActions: [],
|
|
841
1092
|
commands: ["phasegate validate --layer L4 --fail-on-warning --format human"],
|
|
842
1093
|
validations: ["phasegate phasegate:detect-drift --json", "phasegate phasegate:check-ready"],
|
|
843
1094
|
risks: ["L4 findings may be advisory today but become blocking when fail-on-warning is enabled."],
|
|
844
1095
|
},
|
|
845
1096
|
"codex-hooks": {
|
|
846
1097
|
targets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
|
|
1098
|
+
managedTargets: [".codex/hooks.json", "AGENTS.md", ".codex/skills"],
|
|
1099
|
+
externalActions: [{ id: "codex-hooks-feature", label: "Enable Codex user-level hooks feature.", command: "codex features enable codex_hooks", blocking: true }],
|
|
847
1100
|
commands: ["phasegate install --agent codex --apply", "codex features enable codex_hooks"],
|
|
848
1101
|
validations: ["phasegate doctor --json", "phasegate phasegate:status --json"],
|
|
849
1102
|
risks: ["Codex apply_patch writes still require the pre-commit backstop for full coverage."],
|
|
850
1103
|
},
|
|
851
1104
|
"ci-fail-on-warning": {
|
|
852
1105
|
targets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
|
|
1106
|
+
managedTargets: [".github/workflows/phasegate-aidlc-gate.yml", "phasegate.config.json"],
|
|
1107
|
+
externalActions: [{ id: "github-actions-first-run", label: "Trigger or inspect the first GitHub Actions PhaseGate run.", command: null, blocking: false }],
|
|
853
1108
|
commands: ["phasegate install --with-ci --apply", "phasegate validate --layer L4 --fail-on-warning"],
|
|
854
1109
|
validations: ["phasegate doctor", "phasegate ci:generate-template --type aidlc-gate --render"],
|
|
855
1110
|
risks: ["Existing warning-only projects may start failing CI after rollout."],
|
|
856
1111
|
},
|
|
857
1112
|
"baseline-reset": {
|
|
858
1113
|
targets: [".phasegate/baseline.json"],
|
|
1114
|
+
managedTargets: [".phasegate/baseline.json"],
|
|
1115
|
+
externalActions: [],
|
|
859
1116
|
commands: ["phasegate baseline --dry-run", "phasegate baseline --force --json"],
|
|
860
1117
|
validations: ["phasegate phasegate:status --json", "phasegate phasegate:check-ready"],
|
|
861
1118
|
risks: ["A reset can hide historical drift if reviewed without the generated diff."],
|
|
862
1119
|
},
|
|
863
1120
|
"quick-mode-strict": {
|
|
864
1121
|
targets: ["phasegate.config.json: quickMode"],
|
|
1122
|
+
managedTargets: ["phasegate.config.json"],
|
|
1123
|
+
externalActions: [],
|
|
865
1124
|
commands: ["phasegate check-change-category --paths <changed-files> --format json"],
|
|
866
1125
|
validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
|
|
867
1126
|
risks: ["More changes will require Full Mode validation before commit."],
|
|
868
1127
|
},
|
|
869
1128
|
};
|
|
1129
|
+
const before = await readProjectJson(rootDir, "phasegate.config.json");
|
|
870
1130
|
return {
|
|
871
1131
|
intent,
|
|
872
1132
|
...catalog[intent],
|
|
1133
|
+
configPatch: buildConfigPatchPreview(intent, before),
|
|
873
1134
|
diffExplanation: "Review the listed targets first, apply through PhaseGate managed commands where possible, then run the validations in order.",
|
|
874
1135
|
rollback: "Use git diff for config changes; use phasegate uninstall/reconcile dry-runs for managed setup targets.",
|
|
875
1136
|
};
|
|
@@ -1315,7 +1576,7 @@ async function main(): Promise<void> {
|
|
|
1315
1576
|
console.log(" • Q&A about phasegate concepts: invoke /phasegate-toolkit-guide");
|
|
1316
1577
|
console.log(" • Diagnose & tune phasegate.config.json: invoke /phasegate-config-doctor");
|
|
1317
1578
|
}
|
|
1318
|
-
process.exit(0);
|
|
1579
|
+
process.exit(hasStructuredInstallError(installResult) ? 1 : 0);
|
|
1319
1580
|
break;
|
|
1320
1581
|
}
|
|
1321
1582
|
|
|
@@ -1436,7 +1697,7 @@ async function main(): Promise<void> {
|
|
|
1436
1697
|
const workflow = parseWorkflowMode(parseFlag(args, "--workflow") ?? (intent === "strict" ? "strict" : "standard"));
|
|
1437
1698
|
const withCi = hasFlag(args, "--with-ci") || intent === "ci-only" || intent === "strict";
|
|
1438
1699
|
const withHusky = hasFlag(args, "--with-husky") || intent === "agent-hooks" || intent === "strict";
|
|
1439
|
-
|
|
1700
|
+
let plan = await buildAgentSetupPlan(rootDir, { intent, agent, withHusky, withCi, workflow });
|
|
1440
1701
|
const apply = hasFlag(args, "--apply");
|
|
1441
1702
|
let installResult: unknown = null;
|
|
1442
1703
|
let bootstrapResult: unknown = null;
|
|
@@ -1469,6 +1730,7 @@ async function main(): Promise<void> {
|
|
|
1469
1730
|
workflow,
|
|
1470
1731
|
agent,
|
|
1471
1732
|
});
|
|
1733
|
+
plan = await buildAgentSetupPlan(rootDir, { intent, agent, withHusky, withCi, workflow });
|
|
1472
1734
|
}
|
|
1473
1735
|
const output = { plan, applied: apply, bootstrapResult, installResult };
|
|
1474
1736
|
if (json) {
|
|
@@ -1496,7 +1758,7 @@ async function main(): Promise<void> {
|
|
|
1496
1758
|
console.error(flagError);
|
|
1497
1759
|
process.exit(2);
|
|
1498
1760
|
}
|
|
1499
|
-
const plan = buildConfigChangePlan(parseConfigChangeIntent(parseFlag(args, "--intent")));
|
|
1761
|
+
const plan = await buildConfigChangePlan(rootDir, parseConfigChangeIntent(parseFlag(args, "--intent")));
|
|
1500
1762
|
if (json) {
|
|
1501
1763
|
console.log(JSON.stringify(plan, null, 2));
|
|
1502
1764
|
} else {
|