phasegate 0.155.0 → 0.156.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.
@@ -24,7 +24,7 @@ Command names in this document are split into three surfaces:
24
24
  |---|---|
25
25
  | `init --name <name>` | Legacy-compatible bootstrap for new projects: deploy skills, generate config, and optionally add hooks/CI. Options: `--preset <full\|standard\|minimal\|custom>`, `--skills <core\|all>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--with-husky`, `--with-ci`, `--yes`. |
26
26
  | `install --dry-run` / `--apply` | Idempotently merge PhaseGate into an existing project, preserve user content, add package scripts/devDependency, create selected `AGENTS.md` / `CLAUDE.md` managed sections, and write `.phasegate/manifest.json`. `--agent <claude\|codex\|both>`, `--skills <core\|all>`, and `--workflow <standard\|strict>` affect rendered agent context; `--force` replaces managed targets after backup. |
27
- | `doctor` | Diagnose silent or partial installations and report repair hints (`--json`, `--strict`, `--report-out <path>`). `--report-out` writes exactly to the supplied path, not to `reporting.outputDir`. |
27
+ | `doctor` | Diagnose silent or partial installations and report repair hints (`--json`, `--strict`, `--agent <claude\|codex\|both>`, `--report-out <path>`). `--agent` defaults to `both`; single-agent scopes keep shared targets applicable and mark the other agent's findings as not applicable. `--report-out` writes exactly to the supplied path, not to `reporting.outputDir`. |
28
28
  | `uninstall --dry-run` / `--apply` | Remove PhaseGate-managed files and managed blocks using `.phasegate/manifest.json`; `--force` handles managed conflict cases. |
29
29
  | `reconcile --dry-run` / `--apply` | Update PhaseGate-managed files to current package templates and refresh manifest hashes; `--force` allows managed-file replacement with backup. |
30
30
  | `setup:agent` | Agent-readable setup planner and optional apply path. Options: `--intent <minimal\|recommended\|strict\|ci-only\|agent-hooks\|retrofit>`, `--agent <claude\|codex\|both>`, `--workflow <standard\|strict>`, `--with-husky`, `--with-ci`, `--dry-run`, `--apply`, `--json`. <!-- @work-item-id WI-172 --> |
@@ -40,7 +40,7 @@ Command names in this document are split into three surfaces:
40
40
 
41
41
  <!-- @work-item-id WI-158 -->
42
42
 
43
- Setup lifecycle commands support JSON for automation where shown by help: `install --json`, `reconcile --json`, `uninstall --json`, and `doctor --json`. `doctor --report-out <path>` persists the doctor JSON payload to that exact path. Relative paths are resolved from the project root; absolute paths are used as-is.
43
+ Setup lifecycle commands support JSON for automation where shown by help: `install --json`, `reconcile --json`, `uninstall --json`, and `doctor --json`. `doctor --agent claude --json` and `doctor --agent codex --json` include `scope` and `scopedOutFindings` so agents can distinguish selected-agent readiness from full-install diagnostics. `doctor --report-out <path>` persists the doctor JSON payload to that exact path. Relative paths are resolved from the project root; absolute paths are used as-is. <!-- @work-item-id WI-178 -->
44
44
 
45
45
  This is separate from `reporting.outputDir`. The configured report directory is used by phase-dependency / phase-gate reporting, while regression-suite result files are fixed under `reports/regression/` and status/drift JSON is emitted to stdout.
46
46
 
@@ -63,6 +63,8 @@ The planner output is intentionally agent-readable: detected state, questions, p
63
63
 
64
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
65
 
66
+ By default, `phasegate doctor` inspects the full Claude + Codex + shared setup. For a deliberate single-agent setup, use `phasegate doctor --agent claude --json` or `phasegate doctor --agent codex --json`. Single-agent reports keep shared targets applicable and place the unselected agent's findings under `scopedOutFindings` with `applicability: "not-applicable"`; those findings are explanatory, not repair targets for the selected agent. <!-- @work-item-id WI-178 -->
67
+
66
68
  Check `plan.completeness` in `setup:agent --json`:
67
69
 
68
70
  - `configured`: PhaseGate found local evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.155.0",
3
+ "version": "0.156.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -1,22 +1,38 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-178
4
5
 
5
6
  import { DiagnosticReport } from "../../domain/diagnostic-report.js";
7
+ import type { CheckId } from "../../domain/check-id.js";
8
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
6
9
  import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
10
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
8
11
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
9
12
 
13
+ export type DoctorAgentScope = "claude" | "codex" | "both";
14
+
15
+ export interface ScopedOutDiagnosticFinding {
16
+ readonly finding: DiagnosticFinding;
17
+ readonly scopeReason: string;
18
+ }
19
+
10
20
  export interface RunDoctorDiagnosticsInput {
11
21
  readonly projectRoot: string;
12
22
  readonly strict: boolean;
23
+ readonly agent?: DoctorAgentScope;
13
24
  }
14
25
 
15
26
  export interface RunDoctorDiagnosticsOutput {
16
27
  readonly report: DiagnosticReport;
28
+ readonly agent: DoctorAgentScope;
29
+ readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
17
30
  readonly exitCode: number;
18
31
  }
19
32
 
33
+ const CLAUDE_ONLY_CHECKS = new Set<CheckId>(["claude-hook-missing", "claude-skills-symlink"]);
34
+ const CODEX_ONLY_CHECKS = new Set<CheckId>(["codex-hook-missing", "codex-skills-symlink"]);
35
+
20
36
  export class RunDoctorDiagnosticsUseCase {
21
37
  constructor(
22
38
  private readonly checks: readonly HeuristicCheck[],
@@ -25,17 +41,42 @@ export class RunDoctorDiagnosticsUseCase {
25
41
  ) {}
26
42
 
27
43
  async execute(input: RunDoctorDiagnosticsInput): Promise<RunDoctorDiagnosticsOutput> {
44
+ const agent = input.agent ?? "both";
28
45
  await this.manifestRepository.load(input.projectRoot).catch(() => null);
29
- const findings = (await Promise.all(
46
+ const rawFindings = (await Promise.all(
30
47
  this.checks.map((check) => check.run(input.projectRoot, this.inspector)),
31
48
  )).filter((finding) => finding !== null);
49
+ const { findings, scopedOutFindings } = this.applyAgentScope(rawFindings, agent);
32
50
  const report = DiagnosticReport.create(findings);
33
51
  return {
34
52
  report,
53
+ agent,
54
+ scopedOutFindings,
35
55
  exitCode: this.decideExitCode(report, input.strict),
36
56
  };
37
57
  }
38
58
 
59
+ private applyAgentScope(
60
+ findings: readonly DiagnosticFinding[],
61
+ agent: DoctorAgentScope,
62
+ ): { readonly findings: readonly DiagnosticFinding[]; readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[] } {
63
+ if (agent === "both") return { findings, scopedOutFindings: [] };
64
+ const scopedOutChecks = agent === "claude" ? CODEX_ONLY_CHECKS : CLAUDE_ONLY_CHECKS;
65
+ const applicable: DiagnosticFinding[] = [];
66
+ const scopedOut: ScopedOutDiagnosticFinding[] = [];
67
+ for (const finding of findings) {
68
+ if (scopedOutChecks.has(finding.checkId)) {
69
+ scopedOut.push({
70
+ finding,
71
+ scopeReason: `${finding.checkId} belongs to an unselected agent for doctor --agent ${agent}.`,
72
+ });
73
+ } else {
74
+ applicable.push(finding);
75
+ }
76
+ }
77
+ return { findings: applicable, scopedOutFindings: scopedOut };
78
+ }
79
+
39
80
  private decideExitCode(report: DiagnosticReport, strict: boolean): number {
40
81
  if (report.hasRedFlag()) return 1;
41
82
  if (strict && report.hasWarning()) return 1;
@@ -1,10 +1,11 @@
1
1
  // @unit installation
2
2
  // @layer presentation
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-178
4
5
 
5
6
  import { mkdir, writeFile } from "node:fs/promises";
6
7
  import { dirname, isAbsolute, join } from "node:path";
7
- import type { RunDoctorDiagnosticsUseCase } from "../../application/usecases/run-doctor-diagnostics.js";
8
+ import type { DoctorAgentScope, RunDoctorDiagnosticsUseCase } from "../../application/usecases/run-doctor-diagnostics.js";
8
9
  import { DiagnosticReportFormatter } from "../formatters/diagnostic-report-formatter.js";
9
10
 
10
11
  export interface DoctorHandlerInput {
@@ -13,6 +14,7 @@ export interface DoctorHandlerInput {
13
14
  readonly json: boolean;
14
15
  readonly reportOut: string | null;
15
16
  readonly phasegateVersion: string;
17
+ readonly agent?: DoctorAgentScope;
16
18
  }
17
19
 
18
20
  export interface DoctorHandlerOutput {
@@ -30,9 +32,12 @@ export class DoctorHandler {
30
32
  const result = await this.useCase.execute({
31
33
  projectRoot: input.projectRoot,
32
34
  strict: input.strict,
35
+ agent: input.agent,
33
36
  });
34
37
  const formatInput = {
35
38
  report: result.report,
39
+ agent: result.agent,
40
+ scopedOutFindings: result.scopedOutFindings,
36
41
  phasegateVersion: input.phasegateVersion,
37
42
  projectRoot: input.projectRoot,
38
43
  exitCode: result.exitCode,
@@ -1,11 +1,15 @@
1
1
  // @unit installation
2
2
  // @layer presentation
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-178
4
5
 
6
+ import type { DoctorAgentScope, ScopedOutDiagnosticFinding } from "../../application/usecases/run-doctor-diagnostics.js";
5
7
  import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
6
8
 
7
9
  export interface DiagnosticReportFormatterInput {
8
10
  readonly report: DiagnosticReport;
11
+ readonly agent: DoctorAgentScope;
12
+ readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
9
13
  readonly phasegateVersion: string;
10
14
  readonly projectRoot: string;
11
15
  readonly exitCode: number;
@@ -18,8 +22,20 @@ export class DiagnosticReportFormatter {
18
22
  schemaVersion: "1.0",
19
23
  phasegateVersion: input.phasegateVersion,
20
24
  projectRoot: input.projectRoot,
25
+ scope: {
26
+ agent: input.agent,
27
+ description: scopeDescription(input.agent),
28
+ },
21
29
  overallStatus: input.report.overallStatus,
22
- findings: input.report.findings.map((finding) => finding.toJSON()),
30
+ findings: input.report.findings.map((finding) => ({
31
+ ...finding.toJSON(),
32
+ applicability: "applicable",
33
+ })),
34
+ scopedOutFindings: input.scopedOutFindings.map(({ finding, scopeReason }) => ({
35
+ ...finding.toJSON(),
36
+ applicability: "not-applicable",
37
+ scopeReason,
38
+ })),
23
39
  exitCode: input.exitCode,
24
40
  },
25
41
  null,
@@ -31,6 +47,7 @@ export class DiagnosticReportFormatter {
31
47
  const lines = [
32
48
  `phasegate doctor v${input.phasegateVersion}`,
33
49
  `Project: ${input.projectRoot}`,
50
+ `Scope: ${input.agent} (${scopeDescription(input.agent)})`,
34
51
  "",
35
52
  ];
36
53
  for (const finding of input.report.findings) {
@@ -47,7 +64,16 @@ export class DiagnosticReportFormatter {
47
64
  const redCount = input.report.findings.filter((finding) => finding.severity === "red").length;
48
65
  const warnCount = input.report.findings.filter((finding) => finding.severity === "warn").length;
49
66
  lines.push(`Status: ${input.report.overallStatus.toUpperCase()} (${input.report.findings.length} findings: ${redCount} red, ${warnCount} warn)`);
67
+ if (input.scopedOutFindings.length > 0) {
68
+ lines.push(`Scoped out: ${input.scopedOutFindings.length} findings not applicable to --agent ${input.agent}`);
69
+ }
50
70
  lines.push(`Exit: ${input.exitCode}`);
51
71
  return lines.join("\n");
52
72
  }
53
73
  }
74
+
75
+ function scopeDescription(agent: DoctorAgentScope): string {
76
+ if (agent === "claude") return "Claude Code and shared setup targets; Codex-only findings are not applicable.";
77
+ if (agent === "codex") return "Codex and shared setup targets; Claude-only findings are not applicable.";
78
+ return "Full setup diagnostics for Claude, Codex, and shared targets.";
79
+ }
@@ -152,7 +152,7 @@ Setup:
152
152
  --skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
153
153
  --with-husky, --with-ci, --yes)
154
154
  update-skills Alias for reconcile (kept for compatibility)
155
- doctor Diagnose silent installation failures (--json, --strict, --report-out <path>)
155
+ doctor Diagnose silent installation failures (--json, --strict, --agent <claude|codex|both>, --report-out <path>)
156
156
  scaffold-wi <unit> <type> Create docs/inception/{unit}/WI-XXX/description.md
157
157
  emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
158
158
  install Install phasegate managed files (--dry-run|--apply, --force)
@@ -772,6 +772,16 @@ function parseAgentTarget(value: string | undefined, fallback: AgentTarget = "bo
772
772
  return fallback;
773
773
  }
774
774
 
775
+ function parseRequiredAgentTarget(value: string | undefined, fallback: AgentTarget = "both"): AgentTarget | null {
776
+ if (value === undefined) return fallback;
777
+ if (value === "claude" || value === "codex" || value === "both") return value;
778
+ return null;
779
+ }
780
+
781
+ function doctorValidationCommand(agent: AgentTarget): string {
782
+ return agent === "both" ? "phasegate doctor" : `phasegate doctor --agent ${agent}`;
783
+ }
784
+
775
785
  function parseSetupIntent(value: string | undefined): SetupIntent {
776
786
  if (
777
787
  value === "minimal" ||
@@ -863,6 +873,7 @@ function buildSetupCompleteness(input: {
863
873
  }): readonly SetupCompletenessEntry[] {
864
874
  const includeClaude = input.agent === "claude" || input.agent === "both";
865
875
  const includeCodex = input.agent === "codex" || input.agent === "both";
876
+ const doctorCommand = doctorValidationCommand(input.agent);
866
877
  const agentHooksConfigured = (!includeClaude || input.checks.claudeSettings) && (!includeCodex || input.checks.codexHooks);
867
878
  const agentContextConfigured = (!includeClaude || input.checks.claudeMd) && (!includeCodex || input.checks.agentsMd);
868
879
  const entries: SetupCompletenessEntry[] = [
@@ -872,7 +883,7 @@ function buildSetupCompleteness(input: {
872
883
  configured: input.checks.packageJson && input.checks.phasegateConfig,
873
884
  configuredEvidence: "package.json and phasegate.config.json are present.",
874
885
  plannedEvidence: "setup:agent will create or merge package.json scripts and phasegate.config.json.",
875
- nextAction: "Run setup:agent --apply, then phasegate doctor.",
886
+ nextAction: `Run setup:agent --apply, then ${doctorCommand}.`,
876
887
  }),
877
888
  setupCompletenessEntry({
878
889
  area: "agent-hooks",
@@ -922,7 +933,7 @@ function buildSetupCompleteness(input: {
922
933
  configured: input.checks.packageJson && input.checks.phasegateConfig,
923
934
  configuredEvidence: "Local validation commands can be run against the configured project.",
924
935
  plannedEvidence: "Validation commands are planned after setup apply.",
925
- nextAction: "Run phasegate doctor, phasegate phasegate:check-ready, and phasegate validate --layer L2 --format human.",
936
+ nextAction: `Run ${doctorCommand}, phasegate phasegate:check-ready, and phasegate validate --layer L2 --format human.`,
926
937
  }),
927
938
  ];
928
939
 
@@ -994,6 +1005,7 @@ function buildAgentReadiness(input: {
994
1005
  }): readonly AgentReadinessEntry[] {
995
1006
  const includeClaude = input.agent === "claude" || input.agent === "both";
996
1007
  const includeCodex = input.agent === "codex" || input.agent === "both";
1008
+ const doctorCommand = doctorValidationCommand(input.agent);
997
1009
  const sharedConfigured =
998
1010
  input.checks.packageJson &&
999
1011
  input.checks.phasegateConfig &&
@@ -1045,7 +1057,7 @@ function buildAgentReadiness(input: {
1045
1057
  plannedEvidence: [
1046
1058
  "setup:agent will create or refresh package scripts, phasegate.config.json, skills, and selected Husky/CI targets.",
1047
1059
  ],
1048
- nextAction: "Run setup:agent --apply, then phasegate doctor, phasegate phasegate:check-ready, and phasegate validate --layer L2 --format human.",
1060
+ nextAction: `Run setup:agent --apply, then ${doctorCommand}, phasegate phasegate:check-ready, and phasegate validate --layer L2 --format human.`,
1049
1061
  risk: input.withCi ? "A hosted CI run remains an external manual check." : undefined,
1050
1062
  }),
1051
1063
  ];
@@ -1103,7 +1115,7 @@ async function buildAgentSetupPlan(rootDir: string, input: {
1103
1115
  "Backups are written under .phasegate/backups when force is used on changed managed files.",
1104
1116
  ],
1105
1117
  validation: [
1106
- "phasegate doctor",
1118
+ doctorValidationCommand(input.agent),
1107
1119
  "phasegate phasegate:check-ready",
1108
1120
  "phasegate validate --layer L2 --format human",
1109
1121
  ],
@@ -1728,6 +1740,17 @@ async function main(): Promise<void> {
1728
1740
  }
1729
1741
 
1730
1742
  case "doctor": {
1743
+ const KNOWN_DOCTOR_FLAGS = ["--json", "--strict", "--agent", "--report-out"];
1744
+ const flagError = validateKnownFlags(args, KNOWN_DOCTOR_FLAGS);
1745
+ if (flagError) {
1746
+ console.error(flagError);
1747
+ process.exit(2);
1748
+ }
1749
+ const agent = parseRequiredAgentTarget(parseFlag(args, "--agent"), "both");
1750
+ if (agent === null) {
1751
+ console.error(`Invalid --agent value: "${parseFlag(args, "--agent")}". Use "claude", "codex", or "both".`);
1752
+ process.exit(2);
1753
+ }
1731
1754
  const mod = createInstallationModule();
1732
1755
  const phasegateVersion = await getHarnessVersion(harnessRoot);
1733
1756
  const result = await mod.doctorHandler.execute({
@@ -1736,6 +1759,7 @@ async function main(): Promise<void> {
1736
1759
  json,
1737
1760
  reportOut: parseFlag(args, "--report-out") ?? null,
1738
1761
  phasegateVersion,
1762
+ agent,
1739
1763
  });
1740
1764
  console.log(result.stdout);
1741
1765
  process.exit(result.exitCode);
@@ -127,6 +127,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
127
127
  #### 観点 9: setup lifecycle と doctor finding
128
128
 
129
129
  - `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
130
+ - Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。<!-- @work-item-id WI-178 -->
130
131
  - `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
131
132
  - manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
132
133
  - reconcile / uninstall が refuse → user modified managed target として扱い、`--force` のリスクと backup path を説明して承認を取る
@@ -132,6 +132,8 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
132
132
 
133
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 -->
134
134
 
135
+ Claude-only / Codex-only setup の確認では、full `phasegate doctor` と scoped doctor を区別する。ユーザーが `setup:agent --agent claude` を選んだ場合は `phasegate doctor --agent claude --json` を優先し、`scopedOutFindings` の Codex finding は「未選択 agent の not-applicable 情報」であり修復対象ではないと説明する。full doctor は両 agent を導入したい場合の診断として扱う。<!-- @work-item-id WI-178 -->
136
+
135
137
  `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
138
 
137
139
  Claude Code readiness の質問では `npx phasegate setup:agent --agent claude --dry-run --json` を案内し、`plan.agentReadiness` の `claude` / `shared` が `configured` なら setup ではなく作業導線へ進める。最短ルートは、WI の確認または起票、`docs/inception/.../{WI-XXX}/` への計画/設計、`docs/product/...` への `@work-item-id WI-XXX` 反映、`phasegate phasegate:check-ready` または該当 `validate` の実行である。`setup:agent --apply --json` の structured `error` は troubleshooting に委譲し、`target` / `operation` / `code` / `recovery` を読ませる。<!-- @work-item-id WI-177 -->