phasegate 0.152.8 → 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 +12 -0
- package/README.md +19 -1
- package/docs/guide/cli-reference.md +4 -2
- package/docs/guide/configuration.md +9 -0
- package/docs/guide/getting-started.md +66 -0
- package/docs/guide/installation.md +13 -4
- package/docs/guide/recipes.md +65 -0
- package/docs/guide/setup-artifacts.md +4 -1
- package/docs/guide/troubleshooting.md +73 -0
- package/docs/templates/agent-context/AGENTS.md.template.md +45 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +4 -0
- package/package.json +1 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/agents-md-file-adapter.ts +31 -5
- package/scripts/harness/installation/application/usecases/run-install.ts +162 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +57 -4
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +13 -1
- package/scripts/harness/installation/presentation/cli/install-handler.ts +17 -2
- package/scripts/harness/main.ts +556 -2
- package/skills/phasegate-config-doctor/SKILL.md +4 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +5 -0
package/scripts/harness/main.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @layer presentation
|
|
4
4
|
* @work-item-id WI-090 / WI-091
|
|
5
5
|
* @work-item-id WI-113 / WI-142
|
|
6
|
+
* @work-item-id WI-171 / WI-172 / WI-173
|
|
7
|
+
* @work-item-id WI-175
|
|
6
8
|
*
|
|
7
9
|
* Phasegate CLI エントリポイント。
|
|
8
10
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -155,6 +157,8 @@ Setup:
|
|
|
155
157
|
install Install phasegate managed files (--dry-run|--apply, --force)
|
|
156
158
|
uninstall Uninstall phasegate managed files (--dry-run|--apply, --force)
|
|
157
159
|
reconcile Reconcile phasegate managed files (--dry-run|--apply, --force)
|
|
160
|
+
setup:agent Diagnose repo setup and produce/apply an agent-readable setup plan
|
|
161
|
+
config:plan Produce an agent-readable configuration change plan
|
|
158
162
|
|
|
159
163
|
Commands:
|
|
160
164
|
enable-feature <name> Enable a harness feature
|
|
@@ -193,6 +197,8 @@ Commands:
|
|
|
193
197
|
ci:auto-refresh-agent-context Refresh AGENTS.md / CLAUDE.md (--dry-run, --apply, --json)
|
|
194
198
|
refresh-claude-md Refresh CLAUDE.md standard sections (--dry-run, --apply, --json)
|
|
195
199
|
p2:check-agent-context Check AGENTS.md / CLAUDE.md freshness (--threshold-days <n>, --json)
|
|
200
|
+
setup:agent Plan agent-driven setup (--intent <minimal|recommended|strict|ci-only|agent-hooks|retrofit>, --agent <claude|codex|both>, --dry-run|--apply, --json)
|
|
201
|
+
config:plan Plan safe config changes (--intent <l4-strict|codex-hooks|ci-fail-on-warning|baseline-reset|quick-mode-strict>, --dry-run, --json)
|
|
196
202
|
ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
|
|
197
203
|
baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
|
|
198
204
|
scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --force, --json)
|
|
@@ -482,8 +488,38 @@ Options:
|
|
|
482
488
|
--dry-run Preview target actions without writing (default)
|
|
483
489
|
--apply Write merge results and manifest
|
|
484
490
|
--force Force ai-assisted/manual targets after backing up existing files
|
|
491
|
+
--agent <claude|codex|both> Agent context and hook targets (default: both)
|
|
492
|
+
--skills <core|all> Rendered agent context skill mode (default: all)
|
|
493
|
+
--workflow <standard|strict> Rendered agent context workflow mode (default: standard)
|
|
494
|
+
--with-husky Include Husky hook targets
|
|
495
|
+
--with-ci Include GitHub Actions target
|
|
485
496
|
--json Output machine-readable JSON
|
|
486
497
|
--help, -h Show this help`,
|
|
498
|
+
"setup:agent": `Usage: phasegate setup:agent [options]
|
|
499
|
+
|
|
500
|
+
Diagnose repository setup and produce an agent-readable setup plan.
|
|
501
|
+
|
|
502
|
+
Options:
|
|
503
|
+
--intent <minimal|recommended|strict|ci-only|agent-hooks|retrofit>
|
|
504
|
+
--agent <claude|codex|both>
|
|
505
|
+
--workflow <standard|strict>
|
|
506
|
+
--with-husky
|
|
507
|
+
--with-ci
|
|
508
|
+
--dry-run
|
|
509
|
+
--apply
|
|
510
|
+
--json
|
|
511
|
+
--help, -h Show this help`,
|
|
512
|
+
"config:plan": `Usage: phasegate config:plan --intent <intent> [options]
|
|
513
|
+
|
|
514
|
+
Produce an agent-readable configuration change plan.
|
|
515
|
+
|
|
516
|
+
Intents:
|
|
517
|
+
l4-strict, codex-hooks, ci-fail-on-warning, baseline-reset, quick-mode-strict
|
|
518
|
+
|
|
519
|
+
Options:
|
|
520
|
+
--dry-run
|
|
521
|
+
--json
|
|
522
|
+
--help, -h Show this help`,
|
|
487
523
|
reconcile: `Usage: phasegate reconcile [options]
|
|
488
524
|
|
|
489
525
|
Reconcile PhaseGate-managed files with the current bundled templates.
|
|
@@ -685,6 +721,34 @@ function parseCoverageThreshold(raw: string | undefined): number {
|
|
|
685
721
|
}
|
|
686
722
|
|
|
687
723
|
type InitPhasePreset = "full" | "standard" | "minimal" | "custom";
|
|
724
|
+
type AgentTarget = "claude" | "codex" | "both";
|
|
725
|
+
type SetupIntent = "minimal" | "recommended" | "strict" | "ci-only" | "agent-hooks" | "retrofit";
|
|
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
|
+
}
|
|
688
752
|
|
|
689
753
|
function parseInitPhasePreset(value: string | undefined): InitPhasePreset | undefined {
|
|
690
754
|
if (value === undefined) return undefined;
|
|
@@ -694,6 +758,384 @@ function parseInitPhasePreset(value: string | undefined): InitPhasePreset | unde
|
|
|
694
758
|
return undefined;
|
|
695
759
|
}
|
|
696
760
|
|
|
761
|
+
function parseAgentTarget(value: string | undefined, fallback: AgentTarget = "both"): AgentTarget {
|
|
762
|
+
if (value === "claude" || value === "codex" || value === "both") return value;
|
|
763
|
+
return fallback;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function parseSetupIntent(value: string | undefined): SetupIntent {
|
|
767
|
+
if (
|
|
768
|
+
value === "minimal" ||
|
|
769
|
+
value === "recommended" ||
|
|
770
|
+
value === "strict" ||
|
|
771
|
+
value === "ci-only" ||
|
|
772
|
+
value === "agent-hooks" ||
|
|
773
|
+
value === "retrofit"
|
|
774
|
+
) {
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
return "recommended";
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function parseConfigChangeIntent(value: string | undefined): ConfigChangeIntent {
|
|
781
|
+
if (
|
|
782
|
+
value === "l4-strict" ||
|
|
783
|
+
value === "codex-hooks" ||
|
|
784
|
+
value === "ci-fail-on-warning" ||
|
|
785
|
+
value === "baseline-reset" ||
|
|
786
|
+
value === "quick-mode-strict"
|
|
787
|
+
) {
|
|
788
|
+
return value;
|
|
789
|
+
}
|
|
790
|
+
return "l4-strict";
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function projectFileExists(rootDir: string, relativePath: string): Promise<boolean> {
|
|
794
|
+
try {
|
|
795
|
+
await fsReadFile(join(rootDir, relativePath));
|
|
796
|
+
return true;
|
|
797
|
+
} catch {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
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
|
+
|
|
934
|
+
async function buildAgentSetupPlan(rootDir: string, input: {
|
|
935
|
+
readonly intent: SetupIntent;
|
|
936
|
+
readonly agent: AgentTarget;
|
|
937
|
+
readonly withHusky: boolean;
|
|
938
|
+
readonly withCi: boolean;
|
|
939
|
+
readonly workflow: WorkflowMode;
|
|
940
|
+
}) {
|
|
941
|
+
const checks = {
|
|
942
|
+
packageJson: await projectFileExists(rootDir, "package.json"),
|
|
943
|
+
phasegateConfig: await projectFileExists(rootDir, "phasegate.config.json"),
|
|
944
|
+
claudeSettings: await projectFileExists(rootDir, ".claude/settings.json"),
|
|
945
|
+
codexHooks: await projectFileExists(rootDir, ".codex/hooks.json"),
|
|
946
|
+
agentsMd: await projectFileExists(rootDir, "AGENTS.md"),
|
|
947
|
+
claudeMd: await projectFileExists(rootDir, "CLAUDE.md"),
|
|
948
|
+
huskyPreCommit: await projectFileExists(rootDir, ".husky/pre-commit"),
|
|
949
|
+
ciWorkflow: await projectFileExists(rootDir, ".github/workflows/phasegate-aidlc-gate.yml"),
|
|
950
|
+
skillsVersion: await projectFileExists(rootDir, "skills/.harness-version"),
|
|
951
|
+
};
|
|
952
|
+
const includeClaude = input.agent === "claude" || input.agent === "both";
|
|
953
|
+
const includeCodex = input.agent === "codex" || input.agent === "both";
|
|
954
|
+
const changes = [
|
|
955
|
+
!checks.packageJson ? "Add phasegate devDependency and phasegate scripts to package.json." : "Keep existing package.json and merge missing phasegate scripts only.",
|
|
956
|
+
!checks.phasegateConfig ? `Create phasegate.config.json for ${input.workflow} workflow.` : "Keep existing phasegate.config.json; review config:plan before changing policy.",
|
|
957
|
+
includeClaude ? "Create or refresh .claude/settings.json and CLAUDE.md managed section." : null,
|
|
958
|
+
includeCodex ? "Create or refresh .codex/hooks.json and AGENTS.md managed section." : null,
|
|
959
|
+
input.withHusky ? "Create or refresh Husky pre-commit, commit-msg, and pre-push backstops." : "Leave Husky hooks unmanaged in this setup run.",
|
|
960
|
+
input.withCi ? "Create or refresh GitHub Actions PhaseGate workflow." : "Leave CI workflow unmanaged in this setup run.",
|
|
961
|
+
].filter((item): item is string => item !== null);
|
|
962
|
+
const questions = [
|
|
963
|
+
input.intent === "recommended" ? "Do you want both Claude and Codex context files, or only the agent you actively use?" : null,
|
|
964
|
+
input.intent === "retrofit" ? "Should existing hooks/workflows be preserved as user-owned content or migrated into PhaseGate managed blocks?" : null,
|
|
965
|
+
input.intent === "ci-only" ? "Should local hooks remain disabled while CI enforces the same checks?" : null,
|
|
966
|
+
input.workflow !== "strict" ? "Do you want strict WI/product reflection enforcement now, or after the first green run?" : null,
|
|
967
|
+
].filter((item): item is string => item !== null);
|
|
968
|
+
return {
|
|
969
|
+
intent: input.intent,
|
|
970
|
+
agent: input.agent,
|
|
971
|
+
detected: checks,
|
|
972
|
+
completeness: buildSetupCompleteness({ ...input, checks }),
|
|
973
|
+
questions,
|
|
974
|
+
changes,
|
|
975
|
+
risks: [
|
|
976
|
+
"Existing user content outside PhaseGate managed markers is preserved.",
|
|
977
|
+
"Manual review is still required when a managed target has complex user edits and --force is not used.",
|
|
978
|
+
],
|
|
979
|
+
rollback: [
|
|
980
|
+
"Run phasegate uninstall --dry-run to preview reversal.",
|
|
981
|
+
"Run phasegate uninstall --apply after reviewing the managed targets.",
|
|
982
|
+
"Backups are written under .phasegate/backups when force is used on changed managed files.",
|
|
983
|
+
],
|
|
984
|
+
validation: [
|
|
985
|
+
"phasegate doctor",
|
|
986
|
+
"phasegate phasegate:check-ready",
|
|
987
|
+
"phasegate validate --layer L2 --format human",
|
|
988
|
+
],
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
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) {
|
|
1080
|
+
const catalog: Record<ConfigChangeIntent, {
|
|
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 }[];
|
|
1084
|
+
readonly commands: readonly string[];
|
|
1085
|
+
readonly validations: readonly string[];
|
|
1086
|
+
readonly risks: readonly string[];
|
|
1087
|
+
}> = {
|
|
1088
|
+
"l4-strict": {
|
|
1089
|
+
targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: layers.L4.failOnWarning"],
|
|
1090
|
+
managedTargets: ["phasegate.config.json"],
|
|
1091
|
+
externalActions: [],
|
|
1092
|
+
commands: ["phasegate validate --layer L4 --fail-on-warning --format human"],
|
|
1093
|
+
validations: ["phasegate phasegate:detect-drift --json", "phasegate phasegate:check-ready"],
|
|
1094
|
+
risks: ["L4 findings may be advisory today but become blocking when fail-on-warning is enabled."],
|
|
1095
|
+
},
|
|
1096
|
+
"codex-hooks": {
|
|
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 }],
|
|
1100
|
+
commands: ["phasegate install --agent codex --apply", "codex features enable codex_hooks"],
|
|
1101
|
+
validations: ["phasegate doctor --json", "phasegate phasegate:status --json"],
|
|
1102
|
+
risks: ["Codex apply_patch writes still require the pre-commit backstop for full coverage."],
|
|
1103
|
+
},
|
|
1104
|
+
"ci-fail-on-warning": {
|
|
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 }],
|
|
1108
|
+
commands: ["phasegate install --with-ci --apply", "phasegate validate --layer L4 --fail-on-warning"],
|
|
1109
|
+
validations: ["phasegate doctor", "phasegate ci:generate-template --type aidlc-gate --render"],
|
|
1110
|
+
risks: ["Existing warning-only projects may start failing CI after rollout."],
|
|
1111
|
+
},
|
|
1112
|
+
"baseline-reset": {
|
|
1113
|
+
targets: [".phasegate/baseline.json"],
|
|
1114
|
+
managedTargets: [".phasegate/baseline.json"],
|
|
1115
|
+
externalActions: [],
|
|
1116
|
+
commands: ["phasegate baseline --dry-run", "phasegate baseline --force --json"],
|
|
1117
|
+
validations: ["phasegate phasegate:status --json", "phasegate phasegate:check-ready"],
|
|
1118
|
+
risks: ["A reset can hide historical drift if reviewed without the generated diff."],
|
|
1119
|
+
},
|
|
1120
|
+
"quick-mode-strict": {
|
|
1121
|
+
targets: ["phasegate.config.json: quickMode"],
|
|
1122
|
+
managedTargets: ["phasegate.config.json"],
|
|
1123
|
+
externalActions: [],
|
|
1124
|
+
commands: ["phasegate check-change-category --paths <changed-files> --format json"],
|
|
1125
|
+
validations: ["phasegate ci-check --quick --dry-run", "phasegate phasegate:check-ready"],
|
|
1126
|
+
risks: ["More changes will require Full Mode validation before commit."],
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
const before = await readProjectJson(rootDir, "phasegate.config.json");
|
|
1130
|
+
return {
|
|
1131
|
+
intent,
|
|
1132
|
+
...catalog[intent],
|
|
1133
|
+
configPatch: buildConfigPatchPreview(intent, before),
|
|
1134
|
+
diffExplanation: "Review the listed targets first, apply through PhaseGate managed commands where possible, then run the validations in order.",
|
|
1135
|
+
rollback: "Use git diff for config changes; use phasegate uninstall/reconcile dry-runs for managed setup targets.",
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
697
1139
|
type RuleSeverity = "error" | "warning" | "off";
|
|
698
1140
|
|
|
699
1141
|
function toRuleSeverity(value: string): RuleSeverity {
|
|
@@ -999,6 +1441,9 @@ async function main(): Promise<void> {
|
|
|
999
1441
|
includeCodex: deployCodex,
|
|
1000
1442
|
includeHusky: withHusky,
|
|
1001
1443
|
includeCi: withCi,
|
|
1444
|
+
skillSet,
|
|
1445
|
+
workflow,
|
|
1446
|
+
agent,
|
|
1002
1447
|
});
|
|
1003
1448
|
console.log(
|
|
1004
1449
|
`✓ Skills deployed to ${result.targetDir} (${result.deployedSkills.length} skills, set: ${skillSet})`,
|
|
@@ -1131,7 +1576,7 @@ async function main(): Promise<void> {
|
|
|
1131
1576
|
console.log(" • Q&A about phasegate concepts: invoke /phasegate-toolkit-guide");
|
|
1132
1577
|
console.log(" • Diagnose & tune phasegate.config.json: invoke /phasegate-config-doctor");
|
|
1133
1578
|
}
|
|
1134
|
-
process.exit(0);
|
|
1579
|
+
process.exit(hasStructuredInstallError(installResult) ? 1 : 0);
|
|
1135
1580
|
break;
|
|
1136
1581
|
}
|
|
1137
1582
|
|
|
@@ -1196,7 +1641,7 @@ async function main(): Promise<void> {
|
|
|
1196
1641
|
}
|
|
1197
1642
|
|
|
1198
1643
|
case "install": {
|
|
1199
|
-
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json"];
|
|
1644
|
+
const KNOWN_INSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json", "--agent", "--skills", "--workflow", "--with-husky", "--with-ci"];
|
|
1200
1645
|
const flagError = validateKnownFlags(args, KNOWN_INSTALL_FLAGS);
|
|
1201
1646
|
if (flagError) {
|
|
1202
1647
|
console.error(flagError);
|
|
@@ -1204,6 +1649,19 @@ async function main(): Promise<void> {
|
|
|
1204
1649
|
}
|
|
1205
1650
|
const apply = hasFlag(args, "--apply");
|
|
1206
1651
|
const dryRun = hasFlag(args, "--dry-run") || !apply;
|
|
1652
|
+
const agent = parseAgentTarget(parseFlag(args, "--agent"), "both");
|
|
1653
|
+
const skillSetRaw = parseFlag(args, "--skills") ?? "all";
|
|
1654
|
+
if (skillSetRaw !== "core" && skillSetRaw !== "all") {
|
|
1655
|
+
console.error(`Invalid --skills value: "${skillSetRaw}". Use "core" or "all".`);
|
|
1656
|
+
process.exit(2);
|
|
1657
|
+
}
|
|
1658
|
+
const workflowRaw = parseFlag(args, "--workflow");
|
|
1659
|
+
if (workflowRaw !== undefined && workflowRaw !== "standard" && workflowRaw !== "strict") {
|
|
1660
|
+
console.error(`Invalid --workflow value: "${workflowRaw}". Use "standard" or "strict".`);
|
|
1661
|
+
process.exit(2);
|
|
1662
|
+
}
|
|
1663
|
+
const includeClaude = agent === "claude" || agent === "both";
|
|
1664
|
+
const includeCodex = agent === "codex" || agent === "both";
|
|
1207
1665
|
const mod = createInstallationModule();
|
|
1208
1666
|
const phasegateVersion = await getHarnessVersion(harnessRoot);
|
|
1209
1667
|
const result = await mod.installHandler.execute({
|
|
@@ -1213,6 +1671,13 @@ async function main(): Promise<void> {
|
|
|
1213
1671
|
dryRun,
|
|
1214
1672
|
apply,
|
|
1215
1673
|
force: hasFlag(args, "--force"),
|
|
1674
|
+
includeClaude,
|
|
1675
|
+
includeCodex,
|
|
1676
|
+
includeHusky: true,
|
|
1677
|
+
includeCi: true,
|
|
1678
|
+
skillSet: skillSetRaw,
|
|
1679
|
+
workflow: parseWorkflowMode(workflowRaw),
|
|
1680
|
+
agent,
|
|
1216
1681
|
json,
|
|
1217
1682
|
});
|
|
1218
1683
|
console.log(result.stdout);
|
|
@@ -1220,6 +1685,95 @@ async function main(): Promise<void> {
|
|
|
1220
1685
|
break;
|
|
1221
1686
|
}
|
|
1222
1687
|
|
|
1688
|
+
case "setup:agent": {
|
|
1689
|
+
const KNOWN_SETUP_AGENT_FLAGS = ["--intent", "--agent", "--workflow", "--with-husky", "--with-ci", "--dry-run", "--apply", "--force", "--json"];
|
|
1690
|
+
const flagError = validateKnownFlags(args, KNOWN_SETUP_AGENT_FLAGS);
|
|
1691
|
+
if (flagError) {
|
|
1692
|
+
console.error(flagError);
|
|
1693
|
+
process.exit(2);
|
|
1694
|
+
}
|
|
1695
|
+
const intent = parseSetupIntent(parseFlag(args, "--intent"));
|
|
1696
|
+
const agent = parseAgentTarget(parseFlag(args, "--agent"), "both");
|
|
1697
|
+
const workflow = parseWorkflowMode(parseFlag(args, "--workflow") ?? (intent === "strict" ? "strict" : "standard"));
|
|
1698
|
+
const withCi = hasFlag(args, "--with-ci") || intent === "ci-only" || intent === "strict";
|
|
1699
|
+
const withHusky = hasFlag(args, "--with-husky") || intent === "agent-hooks" || intent === "strict";
|
|
1700
|
+
let plan = await buildAgentSetupPlan(rootDir, { intent, agent, withHusky, withCi, workflow });
|
|
1701
|
+
const apply = hasFlag(args, "--apply");
|
|
1702
|
+
let installResult: unknown = null;
|
|
1703
|
+
let bootstrapResult: unknown = null;
|
|
1704
|
+
if (apply) {
|
|
1705
|
+
const skillResult = await deploySkills(harnessRoot, rootDir, "all");
|
|
1706
|
+
const packageResult = await ensurePhasegatePackageDependency(rootDir, skillResult.version);
|
|
1707
|
+
const configResult = await initHarnessConfig(rootDir, "my-project", workflow === "strict" ? "full" : "standard", {
|
|
1708
|
+
ciEnabled: withCi,
|
|
1709
|
+
workflow,
|
|
1710
|
+
});
|
|
1711
|
+
bootstrapResult = {
|
|
1712
|
+
skillsDeployed: skillResult.deployedSkills.length,
|
|
1713
|
+
packageDependency: packageResult,
|
|
1714
|
+
configCreated: configResult.created,
|
|
1715
|
+
};
|
|
1716
|
+
const mod = createInstallationModule();
|
|
1717
|
+
const phasegateVersion = await getHarnessVersion(harnessRoot);
|
|
1718
|
+
installResult = await mod.runInstallUseCase.execute({
|
|
1719
|
+
projectRoot: rootDir,
|
|
1720
|
+
harnessRoot,
|
|
1721
|
+
phasegateVersion,
|
|
1722
|
+
dryRun: false,
|
|
1723
|
+
apply: true,
|
|
1724
|
+
force: hasFlag(args, "--force"),
|
|
1725
|
+
includeClaude: agent === "claude" || agent === "both",
|
|
1726
|
+
includeCodex: agent === "codex" || agent === "both",
|
|
1727
|
+
includeHusky: withHusky,
|
|
1728
|
+
includeCi: withCi,
|
|
1729
|
+
skillSet: "all",
|
|
1730
|
+
workflow,
|
|
1731
|
+
agent,
|
|
1732
|
+
});
|
|
1733
|
+
plan = await buildAgentSetupPlan(rootDir, { intent, agent, withHusky, withCi, workflow });
|
|
1734
|
+
}
|
|
1735
|
+
const output = { plan, applied: apply, bootstrapResult, installResult };
|
|
1736
|
+
if (json) {
|
|
1737
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1738
|
+
} else {
|
|
1739
|
+
console.log(`phasegate setup:agent ${apply ? "apply" : "dry-run"} (${intent}, ${agent})`);
|
|
1740
|
+
for (const change of plan.changes) console.log(`- ${change}`);
|
|
1741
|
+
if (plan.questions.length > 0) {
|
|
1742
|
+
console.log("");
|
|
1743
|
+
console.log("Questions:");
|
|
1744
|
+
for (const question of plan.questions) console.log(`- ${question}`);
|
|
1745
|
+
}
|
|
1746
|
+
console.log("");
|
|
1747
|
+
console.log("Validation:");
|
|
1748
|
+
for (const command of plan.validation) console.log(`- ${command}`);
|
|
1749
|
+
}
|
|
1750
|
+
process.exit(0);
|
|
1751
|
+
break;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
case "config:plan": {
|
|
1755
|
+
const KNOWN_CONFIG_PLAN_FLAGS = ["--intent", "--dry-run", "--json"];
|
|
1756
|
+
const flagError = validateKnownFlags(args, KNOWN_CONFIG_PLAN_FLAGS);
|
|
1757
|
+
if (flagError) {
|
|
1758
|
+
console.error(flagError);
|
|
1759
|
+
process.exit(2);
|
|
1760
|
+
}
|
|
1761
|
+
const plan = await buildConfigChangePlan(rootDir, parseConfigChangeIntent(parseFlag(args, "--intent")));
|
|
1762
|
+
if (json) {
|
|
1763
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
1764
|
+
} else {
|
|
1765
|
+
console.log(`phasegate config:plan (${plan.intent})`);
|
|
1766
|
+
console.log("Targets:");
|
|
1767
|
+
for (const target of plan.targets) console.log(`- ${target}`);
|
|
1768
|
+
console.log("Commands:");
|
|
1769
|
+
for (const command of plan.commands) console.log(`- ${command}`);
|
|
1770
|
+
console.log("Validation:");
|
|
1771
|
+
for (const validation of plan.validations) console.log(`- ${validation}`);
|
|
1772
|
+
}
|
|
1773
|
+
process.exit(0);
|
|
1774
|
+
break;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1223
1777
|
case "uninstall": {
|
|
1224
1778
|
const KNOWN_UNINSTALL_FLAGS = ["--dry-run", "--apply", "--force", "--json"];
|
|
1225
1779
|
const flagError = validateKnownFlags(args, KNOWN_UNINSTALL_FLAGS);
|
|
@@ -39,6 +39,7 @@ phasegate を導入した直後の config は単純な default で、実プロ
|
|
|
39
39
|
| Codex hooks | `.codex/hooks.json` | managed hook JSON と Codex hook 配線確認 |
|
|
40
40
|
| Husky scripts | `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push` | pre-commit backstop と bypass audit の確認 |
|
|
41
41
|
| CI workflows | `.github/workflows/*` | `phasegate-aidlc-gate.yml` や既存 workflow との競合確認 |
|
|
42
|
+
| agent context | `AGENTS.md`, `CLAUDE.md` | PhaseGate managed section、lesson pointer section、user-owned content の境界確認 |
|
|
42
43
|
|
|
43
44
|
**schema は必要なときだけ Read** (enum 違反疑い時など): `node_modules/phasegate/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json` (or `harness-config-v2.schema.json` if v2)。
|
|
44
45
|
|
|
@@ -131,6 +132,9 @@ product-architect で Unit を作り、いくつかの logical_design を書い
|
|
|
131
132
|
- reconcile / uninstall が refuse → user modified managed target として扱い、`--force` のリスクと backup path を説明して承認を取る
|
|
132
133
|
- Codex の `codex_hooks` feature flag は user-level setting。project-local `install` では変更されないため、必要なら `codex features enable codex_hooks` を案内
|
|
133
134
|
- Codex native `apply_patch` bypass は hook で完全捕捉できない。`.husky/pre-commit` の `phasegate pre-commit` が backstop になるため、Husky 配線を診断対象に含める
|
|
135
|
+
- 初回 setup / retrofit / CI-only / agent-hooks の判断が曖昧な場合は `npx phasegate setup:agent --dry-run --json` を先に使い、検出済み状態、質問、変更案、rollback、validation を根拠として提案する。<!-- @work-item-id WI-172 -->
|
|
136
|
+
- 「L4 を厳しめにして」「Codex hook を有効にして」「CI で warning を fail にして」などの自然言語依頼は、`npx phasegate config:plan --intent <intent> --dry-run --json` で対象ファイル、コマンド、リスク、検証を確認してから diff を提案する。<!-- @work-item-id WI-173 -->
|
|
137
|
+
- `AGENTS.md` は標準運用ルールの managed section と lesson pointer section を分ける。`ci:auto-refresh-agent-context --apply` は lesson pointers 専用 section を更新し、標準運用ルールや user-owned content を置換してはいけない。<!-- @work-item-id WI-174 -->
|
|
134
138
|
|
|
135
139
|
### Step 3: 診断レポート
|
|
136
140
|
|
|
@@ -123,12 +123,17 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
|
|
|
123
123
|
- 「.phasegate/manifest.json や hook-skip-events は何?」
|
|
124
124
|
|
|
125
125
|
**参照先**:
|
|
126
|
+
- 最短導線: `docs/guide/getting-started.md`
|
|
127
|
+
- recipes: `docs/guide/recipes.md`
|
|
128
|
+
- troubleshooting: `docs/guide/troubleshooting.md`
|
|
126
129
|
- 新規導入: `docs/guide/installation.md`
|
|
127
130
|
- 既存プロジェクト導入: `docs/guide/retrofit-adoption.md`
|
|
128
131
|
- setup artifact / doctor finding / legacy artifact: `docs/guide/setup-artifacts.md`
|
|
129
132
|
|
|
130
133
|
`setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
|
|
131
134
|
|
|
135
|
+
`setup:agent` は初回 setup / retrofit / CI-only / agent hook 有効化の agent-readable planner、`config:plan` は安全な設定変更 intent planner。質問が「次に何を実行するか」「この変更はどの file/validation に対応するか」に寄っている場合は、これらの guide と CLI を案内する。<!-- @work-item-id WI-171, WI-172, WI-173 -->
|
|
136
|
+
|
|
132
137
|
### 8. skill 一覧と使い分け
|
|
133
138
|
|
|
134
139
|
ユーザー質問例:
|