phasegate 0.283.0 → 0.335.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 +16 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/guide/cli-reference.md +89 -7
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +63 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +57 -5
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +159 -21
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +64 -33
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +116 -18
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/quick-implementor/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
6
|
+
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
|
+
import type { ConfigStatusProbePort } from "../ports/config-status-probe-port.js";
|
|
8
|
+
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
9
|
+
import { createFinding } from "./check-utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* WI-330 (GitHub #40 恒久化): phasegate.config.json の存在・妥当性を doctor で可視化する。
|
|
13
|
+
* - missing → warn(既定設定の fail-open で動作していることをユーザーに知らせる)
|
|
14
|
+
* - invalid-json / invalid-schema → red(意図した設定が適用されていない)
|
|
15
|
+
* - valid → finding なし
|
|
16
|
+
*/
|
|
17
|
+
export class ConfigStatusCheck implements HeuristicCheck {
|
|
18
|
+
readonly checkId = "config-status" as const;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly probe: ConfigStatusProbePort) {}
|
|
21
|
+
|
|
22
|
+
async run(projectRoot: string, _inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
23
|
+
const result = await this.probe.probe(projectRoot);
|
|
24
|
+
if (result.status === "valid") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (result.status === "missing") {
|
|
28
|
+
return createFinding({
|
|
29
|
+
checkId: this.checkId,
|
|
30
|
+
severity: "warn",
|
|
31
|
+
target: result.configPath,
|
|
32
|
+
message:
|
|
33
|
+
"phasegate.config.json が存在しません(既定設定の fail-open モードで動作中)。`phasegate init` で生成できます",
|
|
34
|
+
repairMode: "mechanical",
|
|
35
|
+
repairHint: "npx phasegate init",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const reason = result.status === "invalid-json" ? "JSON 構文エラー" : "スキーマ違反";
|
|
39
|
+
const detail = result.detail !== null && result.detail.length > 0 ? ` — ${result.detail}` : "";
|
|
40
|
+
return createFinding({
|
|
41
|
+
checkId: this.checkId,
|
|
42
|
+
severity: "red",
|
|
43
|
+
target: result.configPath,
|
|
44
|
+
message: `phasegate.config.json が${reason}で壊れており、ユーザーの意図した設定が適用されていません(既定設定の fail-open モードで動作中)${detail}`,
|
|
45
|
+
repairMode: "manual",
|
|
46
|
+
repairHint:
|
|
47
|
+
result.status === "invalid-json"
|
|
48
|
+
? "phasegate.config.json の JSON 構文エラーを修正するか、version control から復元してください"
|
|
49
|
+
: "報告されたパス・型のスキーマ違反を phasegate.config.json 上で修正するか、version control から復元してください",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-340
|
|
4
5
|
|
|
5
6
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
7
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
@@ -22,6 +23,11 @@ export class HuskyPreCommitMissingCheck implements HeuristicCheck {
|
|
|
22
23
|
"main.ts check-phase-gate",
|
|
23
24
|
"$HARNESS_CMD lint",
|
|
24
25
|
"$HARNESS_CMD check-phase-gate",
|
|
26
|
+
"phasegate pre-commit",
|
|
27
|
+
"main.ts pre-commit",
|
|
28
|
+
"$PHASEGATE_CMD lint",
|
|
29
|
+
"$PHASEGATE_CMD check-phase-gate",
|
|
30
|
+
"$PHASEGATE_CMD pre-commit",
|
|
25
31
|
])
|
|
26
32
|
) {
|
|
27
33
|
return null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
import type { ConfigStatusProbeResult } from "../../domain/config-status.js";
|
|
6
|
+
|
|
7
|
+
export interface ConfigStatusProbePort {
|
|
8
|
+
probe(projectRoot: string): Promise<ConfigStatusProbeResult>;
|
|
9
|
+
}
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
// @work-item-id WI-178
|
|
5
5
|
// @work-item-id WI-208
|
|
6
6
|
// @work-item-id WI-215
|
|
7
|
+
// @work-item-id WI-330
|
|
8
|
+
// @work-item-id WI-343
|
|
7
9
|
|
|
8
|
-
import { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
9
10
|
import type { CheckId } from "../../domain/check-id.js";
|
|
11
|
+
import type { ConfigStatus } from "../../domain/config-status.js";
|
|
10
12
|
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
13
|
+
import { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
11
14
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
15
|
+
import type { ConfigStatusProbePort } from "../ports/config-status-probe-port.js";
|
|
12
16
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
13
17
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
14
18
|
|
|
@@ -29,6 +33,7 @@ export interface RunDoctorDiagnosticsOutput {
|
|
|
29
33
|
readonly report: DiagnosticReport;
|
|
30
34
|
readonly agent: DoctorAgentScope;
|
|
31
35
|
readonly installationMode: "project" | "personal";
|
|
36
|
+
readonly configStatus: ConfigStatus;
|
|
32
37
|
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
33
38
|
readonly exitCode: number;
|
|
34
39
|
}
|
|
@@ -48,15 +53,22 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
48
53
|
private readonly checks: readonly HeuristicCheck[],
|
|
49
54
|
private readonly inspector: FileInspectorPort,
|
|
50
55
|
private readonly manifestRepository: ManifestRepositoryPort,
|
|
56
|
+
private readonly configStatusProbe: ConfigStatusProbePort,
|
|
51
57
|
) {}
|
|
52
58
|
|
|
53
59
|
async execute(input: RunDoctorDiagnosticsInput): Promise<RunDoctorDiagnosticsOutput> {
|
|
54
60
|
const agent = input.agent ?? "both";
|
|
55
61
|
const manifest = await this.manifestRepository.load(input.projectRoot).catch(() => null);
|
|
56
|
-
const installationMode =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const installationMode =
|
|
63
|
+
manifest?.findEntry(".phasegate-local/phasegate.config.json") !== null && manifest !== null
|
|
64
|
+
? "personal"
|
|
65
|
+
: "project";
|
|
66
|
+
const configStatus = (await this.configStatusProbe.probe(input.projectRoot)).status;
|
|
67
|
+
const rawFindings = (
|
|
68
|
+
await Promise.all(
|
|
69
|
+
this.checks.map((check) => check.run(input.projectRoot, this.inspector, { installationMode })),
|
|
70
|
+
)
|
|
71
|
+
).filter((finding) => finding !== null);
|
|
60
72
|
const { findings, scopedOutFindings } = this.applyPersonalScope(
|
|
61
73
|
this.applyAgentScope(rawFindings, agent),
|
|
62
74
|
installationMode,
|
|
@@ -66,6 +78,7 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
66
78
|
report,
|
|
67
79
|
agent,
|
|
68
80
|
installationMode,
|
|
81
|
+
configStatus,
|
|
69
82
|
scopedOutFindings,
|
|
70
83
|
exitCode: this.decideExitCode(report, input.strict),
|
|
71
84
|
};
|
|
@@ -74,7 +87,10 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
74
87
|
private applyAgentScope(
|
|
75
88
|
findings: readonly DiagnosticFinding[],
|
|
76
89
|
agent: DoctorAgentScope,
|
|
77
|
-
): {
|
|
90
|
+
): {
|
|
91
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
92
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
93
|
+
} {
|
|
78
94
|
if (agent === "both") return { findings, scopedOutFindings: [] };
|
|
79
95
|
const scopedOutChecks = agent === "claude" ? CODEX_ONLY_CHECKS : CLAUDE_ONLY_CHECKS;
|
|
80
96
|
const applicable: DiagnosticFinding[] = [];
|
|
@@ -93,9 +109,15 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
private applyPersonalScope(
|
|
96
|
-
scoped: {
|
|
112
|
+
scoped: {
|
|
113
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
114
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
115
|
+
},
|
|
97
116
|
installationMode: "project" | "personal",
|
|
98
|
-
): {
|
|
117
|
+
): {
|
|
118
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
119
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
120
|
+
} {
|
|
99
121
|
if (installationMode !== "personal") return scoped;
|
|
100
122
|
const applicable: DiagnosticFinding[] = [];
|
|
101
123
|
const scopedOut: ScopedOutDiagnosticFinding[] = [...scoped.scopedOutFindings];
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// @work-item-id WI-219
|
|
19
19
|
// @work-item-id WI-315
|
|
20
20
|
// @work-item-id WI-326
|
|
21
|
+
// @work-item-id WI-331
|
|
21
22
|
|
|
22
23
|
import {
|
|
23
24
|
access,
|
|
@@ -121,6 +122,7 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
|
121
122
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
122
123
|
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
123
124
|
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
125
|
+
const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
|
|
124
126
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
125
127
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
126
128
|
const PERSONAL_AGENT_RUNTIME_FILES = new Set([
|
|
@@ -301,11 +303,18 @@ function extractUserSectionBody(content: string | null): string | null {
|
|
|
301
303
|
return body.length === 0 ? null : body;
|
|
302
304
|
}
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
function hasUserSectionMarkers(content: string): boolean {
|
|
307
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
308
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
309
|
+
return start !== -1 && end !== -1 && start < end;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Legacy (pre-WI-331) templates nested the user-section inside the managed
|
|
313
|
+
// section, so replacing the managed block wholesale would wipe user-authored
|
|
314
|
+
// instructions with the template placeholder. Re-inject the existing
|
|
315
|
+
// user-section body into the incoming block. Blocks without user-section
|
|
316
|
+
// markers (all current templates) are returned unchanged; the outside-of-block
|
|
317
|
+
// migration for those is handled in mergeManagedMarkdown.
|
|
309
318
|
function restoreUserSection(block: string, existing: string | null): string {
|
|
310
319
|
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
311
320
|
const end = block.indexOf(USER_SECTION_END);
|
|
@@ -317,12 +326,36 @@ function restoreUserSection(block: string, existing: string | null): string {
|
|
|
317
326
|
|
|
318
327
|
function mergeManagedMarkdown(existing: string | null, incoming: string): string {
|
|
319
328
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
320
|
-
const
|
|
329
|
+
const incomingBlock = managedMarkdownBlock(incoming);
|
|
321
330
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
if (!pattern.test(existing)) return `${incomingBlock}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
332
|
+
|
|
333
|
+
const blockStart = existing.indexOf(MARKDOWN_BEGIN);
|
|
334
|
+
const blockEnd = existing.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
335
|
+
const existingBlock = existing.slice(blockStart, blockEnd);
|
|
336
|
+
const outsideBlock = existing.slice(0, blockStart) + existing.slice(blockEnd);
|
|
337
|
+
const block = restoreUserSection(incomingBlock, existing);
|
|
338
|
+
// Replacer function keeps user-authored text (possibly part of the block)
|
|
339
|
+
// from being interpreted as `$`-substitution patterns by String.replace.
|
|
340
|
+
let next = existing.replace(pattern, () => block);
|
|
341
|
+
|
|
342
|
+
// Structure migration (WI-331): the existing file nests the user-section
|
|
343
|
+
// inside the managed block (old template shape) while the incoming block
|
|
344
|
+
// keeps it outside. Replacing the block would drop the user-authored body,
|
|
345
|
+
// so relocate it to just after the managed block — the position the current
|
|
346
|
+
// template uses. When a user-section already exists outside the block, keep
|
|
347
|
+
// that one and do not duplicate.
|
|
348
|
+
if (
|
|
349
|
+
hasUserSectionMarkers(existingBlock) &&
|
|
350
|
+
!hasUserSectionMarkers(incomingBlock) &&
|
|
351
|
+
!hasUserSectionMarkers(outsideBlock)
|
|
352
|
+
) {
|
|
353
|
+
const body = extractUserSectionBody(existingBlock) ?? USER_SECTION_PLACEHOLDER;
|
|
354
|
+
const insertAt = next.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
355
|
+
const relocated = `\n\n## User Section\n\n${USER_SECTION_BEGIN}\n${body}\n${USER_SECTION_END}`;
|
|
356
|
+
next = `${next.slice(0, insertAt)}${relocated}${next.slice(insertAt)}`;
|
|
357
|
+
}
|
|
358
|
+
return next.replace(/\s*$/, "\n");
|
|
326
359
|
}
|
|
327
360
|
|
|
328
361
|
function renderAgentContextTemplate(
|
|
@@ -351,7 +384,7 @@ function renderAgentContextTemplate(
|
|
|
351
384
|
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
352
385
|
.replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- `core skills`" : "- `all bundled skills`")
|
|
353
386
|
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
354
|
-
.replaceAll("{{PHASEGATE_USER_SECTION}}",
|
|
387
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
|
|
355
388
|
}
|
|
356
389
|
|
|
357
390
|
function escapeRegExp(value: string): string {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// @work-item-id WI-264
|
|
10
10
|
// @work-item-id WI-315
|
|
11
11
|
// @work-item-id WI-326
|
|
12
|
+
// @work-item-id WI-331
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
access,
|
|
@@ -92,6 +93,7 @@ const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
|
92
93
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
93
94
|
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
94
95
|
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
96
|
+
const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
|
|
95
97
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
96
98
|
const HARNESS_VERSION_BASENAME = ".harness-version";
|
|
97
99
|
const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
|
|
@@ -265,11 +267,18 @@ function extractUserSectionBody(content: string | null): string | null {
|
|
|
265
267
|
return body.length === 0 ? null : body;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
function hasUserSectionMarkers(content: string): boolean {
|
|
271
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
272
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
273
|
+
return start !== -1 && end !== -1 && start < end;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Legacy (pre-WI-331) templates nested the user-section inside the managed
|
|
277
|
+
// section, so replacing the managed block wholesale would wipe user-authored
|
|
278
|
+
// instructions with the template placeholder. Re-inject the existing
|
|
279
|
+
// user-section body into the incoming block. Blocks without user-section
|
|
280
|
+
// markers (all current templates) are returned unchanged; the outside-of-block
|
|
281
|
+
// migration for those is handled in reconcileManagedMarkdown.
|
|
273
282
|
function restoreUserSection(block: string, existing: string | null): string {
|
|
274
283
|
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
275
284
|
const end = block.indexOf(USER_SECTION_END);
|
|
@@ -281,12 +290,36 @@ function restoreUserSection(block: string, existing: string | null): string {
|
|
|
281
290
|
|
|
282
291
|
function reconcileManagedMarkdown(existing: string | null, incoming: string): string {
|
|
283
292
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
284
|
-
const
|
|
293
|
+
const incomingBlock = managedMarkdownBlock(incoming);
|
|
285
294
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
if (!pattern.test(existing)) return `${incomingBlock}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
296
|
+
|
|
297
|
+
const blockStart = existing.indexOf(MARKDOWN_BEGIN);
|
|
298
|
+
const blockEnd = existing.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
299
|
+
const existingBlock = existing.slice(blockStart, blockEnd);
|
|
300
|
+
const outsideBlock = existing.slice(0, blockStart) + existing.slice(blockEnd);
|
|
301
|
+
const block = restoreUserSection(incomingBlock, existing);
|
|
302
|
+
// Replacer function keeps user-authored text (possibly part of the block)
|
|
303
|
+
// from being interpreted as `$`-substitution patterns by String.replace.
|
|
304
|
+
let next = existing.replace(pattern, () => block);
|
|
305
|
+
|
|
306
|
+
// Structure migration (WI-331): the existing file nests the user-section
|
|
307
|
+
// inside the managed block (old template shape) while the incoming block
|
|
308
|
+
// keeps it outside. Replacing the block would drop the user-authored body,
|
|
309
|
+
// so relocate it to just after the managed block — the position the current
|
|
310
|
+
// template uses. When a user-section already exists outside the block, keep
|
|
311
|
+
// that one and do not duplicate.
|
|
312
|
+
if (
|
|
313
|
+
hasUserSectionMarkers(existingBlock) &&
|
|
314
|
+
!hasUserSectionMarkers(incomingBlock) &&
|
|
315
|
+
!hasUserSectionMarkers(outsideBlock)
|
|
316
|
+
) {
|
|
317
|
+
const body = extractUserSectionBody(existingBlock) ?? USER_SECTION_PLACEHOLDER;
|
|
318
|
+
const insertAt = next.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
319
|
+
const relocated = `\n\n## User Section\n\n${USER_SECTION_BEGIN}\n${body}\n${USER_SECTION_END}`;
|
|
320
|
+
next = `${next.slice(0, insertAt)}${relocated}${next.slice(insertAt)}`;
|
|
321
|
+
}
|
|
322
|
+
return next.replace(/\s*$/, "\n");
|
|
290
323
|
}
|
|
291
324
|
|
|
292
325
|
function renderAgentContextTemplate(template: string): string {
|
|
@@ -308,7 +341,7 @@ function renderAgentContextTemplate(template: string): string {
|
|
|
308
341
|
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
309
342
|
.replaceAll("{{PHASEGATE_SKILLS}}", "- `all bundled skills`")
|
|
310
343
|
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
311
|
-
.replaceAll("{{PHASEGATE_USER_SECTION}}",
|
|
344
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
|
|
312
345
|
}
|
|
313
346
|
|
|
314
347
|
function reconcilePackageJson(existing: Record<string, unknown>, version: string): Record<string, unknown> {
|
|
@@ -3,26 +3,29 @@
|
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-148
|
|
5
5
|
// @work-item-id WI-215
|
|
6
|
+
// @work-item-id WI-330
|
|
6
7
|
|
|
8
|
+
import { CiWorkflowMissingCheck } from "./application/checks/ci-workflow-missing-check.js";
|
|
7
9
|
import { ClaudeContextMissingCheck } from "./application/checks/claude-context-missing-check.js";
|
|
8
10
|
import { ClaudeHookMissingCheck } from "./application/checks/claude-hook-missing-check.js";
|
|
9
11
|
import { ClaudeSkillsSymlinkCheck } from "./application/checks/claude-skills-symlink-check.js";
|
|
10
12
|
import { CodexContextMissingCheck } from "./application/checks/codex-context-missing-check.js";
|
|
11
13
|
import { CodexHookMissingCheck } from "./application/checks/codex-hook-missing-check.js";
|
|
12
14
|
import { CodexSkillsSymlinkCheck } from "./application/checks/codex-skills-symlink-check.js";
|
|
13
|
-
import {
|
|
15
|
+
import { ConfigStatusCheck } from "./application/checks/config-status-check.js";
|
|
14
16
|
import { HuskyCommitMsgMissingCheck } from "./application/checks/husky-commit-msg-missing-check.js";
|
|
15
17
|
import { HuskyPreCommitMissingCheck } from "./application/checks/husky-pre-commit-missing-check.js";
|
|
16
18
|
import { HuskyPrePushMissingCheck } from "./application/checks/husky-pre-push-missing-check.js";
|
|
17
19
|
import { PackageJsonDevdepMissingCheck } from "./application/checks/package-json-devdep-missing-check.js";
|
|
18
20
|
import { WiWorkflowDriftCheck } from "./application/checks/wi-workflow-drift-check.js";
|
|
21
|
+
import { RunDoctorDiagnosticsUseCase } from "./application/usecases/run-doctor-diagnostics.js";
|
|
19
22
|
import { RunInstallUseCase } from "./application/usecases/run-install.js";
|
|
20
23
|
import { RunReconcileUseCase } from "./application/usecases/run-reconcile.js";
|
|
21
24
|
import { RunUninstallUseCase } from "./application/usecases/run-uninstall.js";
|
|
22
|
-
import { RunDoctorDiagnosticsUseCase } from "./application/usecases/run-doctor-diagnostics.js";
|
|
23
25
|
import type { MergeStrategy } from "./domain/ports/merge-strategy.js";
|
|
24
26
|
import type { ReconcileStrategy } from "./domain/ports/reconcile-strategy.js";
|
|
25
27
|
import type { UninstallReverseStrategy } from "./domain/ports/uninstall-reverse-strategy.js";
|
|
28
|
+
import { ConfigStatusProbeAdapter } from "./infrastructure/adapters/config-status-probe-adapter.js";
|
|
26
29
|
import { FileSystemManifestRepositoryAdapter } from "./infrastructure/adapters/file-system-manifest-repository-adapter.js";
|
|
27
30
|
import { NodeCryptoHashAdapter } from "./infrastructure/adapters/node-crypto-hash-adapter.js";
|
|
28
31
|
import { NodeFsFileInspectorAdapter } from "./infrastructure/adapters/node-fs-file-inspector-adapter.js";
|
|
@@ -45,6 +48,7 @@ export function createInstallationModule() {
|
|
|
45
48
|
const manifestRepository = new FileSystemManifestRepositoryAdapter();
|
|
46
49
|
const hashCalculator = new NodeCryptoHashAdapter();
|
|
47
50
|
const modelDelegation = new SkillDeployerModelDelegationAdapter();
|
|
51
|
+
const configStatusProbe = new ConfigStatusProbeAdapter();
|
|
48
52
|
const checks = [
|
|
49
53
|
new ClaudeHookMissingCheck(),
|
|
50
54
|
new ClaudeContextMissingCheck(),
|
|
@@ -58,8 +62,14 @@ export function createInstallationModule() {
|
|
|
58
62
|
new ClaudeSkillsSymlinkCheck(),
|
|
59
63
|
new CodexSkillsSymlinkCheck(),
|
|
60
64
|
new WiWorkflowDriftCheck(),
|
|
65
|
+
new ConfigStatusCheck(configStatusProbe),
|
|
61
66
|
];
|
|
62
|
-
const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(
|
|
67
|
+
const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(
|
|
68
|
+
checks,
|
|
69
|
+
inspector,
|
|
70
|
+
manifestRepository,
|
|
71
|
+
configStatusProbe,
|
|
72
|
+
);
|
|
63
73
|
const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
64
74
|
const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
65
75
|
const runUninstallUseCase = new RunUninstallUseCase(manifestRepository, hashCalculator);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-215
|
|
5
|
+
// @work-item-id WI-330
|
|
5
6
|
|
|
6
7
|
export const CHECK_IDS = [
|
|
7
8
|
"claude-hook-missing",
|
|
@@ -16,6 +17,7 @@ export const CHECK_IDS = [
|
|
|
16
17
|
"claude-skills-symlink",
|
|
17
18
|
"codex-skills-symlink",
|
|
18
19
|
"wi-workflow-drift",
|
|
20
|
+
"config-status",
|
|
19
21
|
] as const;
|
|
20
22
|
|
|
21
23
|
export type CheckId = (typeof CHECK_IDS)[number];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
export const CONFIG_STATUSES = ["missing", "invalid-json", "invalid-schema", "valid"] as const;
|
|
6
|
+
|
|
7
|
+
export type ConfigStatus = (typeof CONFIG_STATUSES)[number];
|
|
8
|
+
|
|
9
|
+
export function isConfigStatus(value: string): value is ConfigStatus {
|
|
10
|
+
return (CONFIG_STATUSES as readonly string[]).includes(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConfigStatusProbeResult {
|
|
14
|
+
readonly status: ConfigStatus;
|
|
15
|
+
readonly configPath: string;
|
|
16
|
+
readonly detail: string | null;
|
|
17
|
+
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-343
|
|
4
5
|
|
|
5
6
|
import type { CheckId } from "../check-id.js";
|
|
6
7
|
import type { DiagnosticFinding } from "../diagnostic-finding.js";
|
|
7
8
|
import type { FileInspector } from "./file-inspector.js";
|
|
8
9
|
|
|
10
|
+
export interface HeuristicCheckContext {
|
|
11
|
+
readonly installationMode: "project" | "personal";
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export interface HeuristicCheck {
|
|
10
15
|
readonly checkId: CheckId;
|
|
11
|
-
run(
|
|
16
|
+
run(
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
inspector: FileInspector,
|
|
19
|
+
context?: HeuristicCheckContext,
|
|
20
|
+
): Promise<DiagnosticFinding | null>;
|
|
12
21
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createConfigFoundationModule } from "../../../config-foundation/composition-root.js";
|
|
8
|
+
import { ConfigValidationError } from "../../../config-foundation/domain/errors/config-validation-error.js";
|
|
9
|
+
import {
|
|
10
|
+
ConfigNotFoundError,
|
|
11
|
+
ConfigParseError,
|
|
12
|
+
} from "../../../config-foundation/infrastructure/repositories/file-system-config-repository.js";
|
|
13
|
+
import type { ConfigStatusProbePort } from "../../application/ports/config-status-probe-port.js";
|
|
14
|
+
import type { ConfigStatusProbeResult } from "../../domain/config-status.js";
|
|
15
|
+
|
|
16
|
+
const PROJECT_CONFIG_PATH = "phasegate.config.json";
|
|
17
|
+
const PERSONAL_CONFIG_PATH = join(".phasegate-local", "phasegate.config.json");
|
|
18
|
+
|
|
19
|
+
async function exists(targetPath: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await access(targetPath);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* WI-330: doctor 用の config 状態 probe。
|
|
30
|
+
*
|
|
31
|
+
* FileSystemConfigRepository と同じ候補順(project 直下 → .phasegate-local/ の personal install)で
|
|
32
|
+
* projectRoot 直下のみを解決する。cwd からの上方探索は行わない — doctor は「このプロジェクトの
|
|
33
|
+
* config」を診断するため、親ディレクトリの config を拾うと診断が偽装される。
|
|
34
|
+
* 妥当性判定は config-foundation の実 load 経路(JSON parse + AJV schema + preset 解決)を
|
|
35
|
+
* そのまま使い、CLI 本体と同じ基準で invalid を検出する。
|
|
36
|
+
*/
|
|
37
|
+
export class ConfigStatusProbeAdapter implements ConfigStatusProbePort {
|
|
38
|
+
private readonly cache = new Map<string, Promise<ConfigStatusProbeResult>>();
|
|
39
|
+
|
|
40
|
+
probe(projectRoot: string): Promise<ConfigStatusProbeResult> {
|
|
41
|
+
const cached = this.cache.get(projectRoot);
|
|
42
|
+
if (cached !== undefined) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
const inspected = this.inspect(projectRoot);
|
|
46
|
+
this.cache.set(projectRoot, inspected);
|
|
47
|
+
return inspected;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async inspect(projectRoot: string): Promise<ConfigStatusProbeResult> {
|
|
51
|
+
for (const relativePath of [PROJECT_CONFIG_PATH, PERSONAL_CONFIG_PATH]) {
|
|
52
|
+
const absolutePath = join(projectRoot, relativePath);
|
|
53
|
+
if (await exists(absolutePath)) {
|
|
54
|
+
return await this.classify(absolutePath, relativePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { status: "missing", configPath: PROJECT_CONFIG_PATH, detail: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async classify(absolutePath: string, relativePath: string): Promise<ConfigStatusProbeResult> {
|
|
61
|
+
try {
|
|
62
|
+
await createConfigFoundationModule().usecases.loadResolvedConfigUseCase.execute(absolutePath);
|
|
63
|
+
return { status: "valid", configPath: relativePath, detail: null };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof ConfigNotFoundError) {
|
|
66
|
+
return { status: "missing", configPath: relativePath, detail: null };
|
|
67
|
+
}
|
|
68
|
+
if (error instanceof ConfigParseError) {
|
|
69
|
+
const detail = error.cause instanceof Error ? error.cause.message : error.message;
|
|
70
|
+
return { status: "invalid-json", configPath: relativePath, detail };
|
|
71
|
+
}
|
|
72
|
+
if (error instanceof ConfigValidationError) {
|
|
73
|
+
return { status: "invalid-schema", configPath: relativePath, detail: error.message };
|
|
74
|
+
}
|
|
75
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
76
|
+
return { status: "invalid-schema", configPath: relativePath, detail };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-178
|
|
5
5
|
// @work-item-id WI-208
|
|
6
|
+
// @work-item-id WI-330
|
|
6
7
|
|
|
7
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
9
|
import { dirname, isAbsolute, join } from "node:path";
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
DoctorAgentScope,
|
|
12
|
+
RunDoctorDiagnosticsUseCase,
|
|
13
|
+
} from "../../application/usecases/run-doctor-diagnostics.js";
|
|
10
14
|
import { DiagnosticReportFormatter } from "../formatters/diagnostic-report-formatter.js";
|
|
11
15
|
|
|
12
16
|
export interface DoctorHandlerInput {
|
|
@@ -39,6 +43,7 @@ export class DoctorHandler {
|
|
|
39
43
|
report: result.report,
|
|
40
44
|
agent: result.agent,
|
|
41
45
|
installationMode: result.installationMode,
|
|
46
|
+
configStatus: result.configStatus,
|
|
42
47
|
scopedOutFindings: result.scopedOutFindings,
|
|
43
48
|
phasegateVersion: input.phasegateVersion,
|
|
44
49
|
projectRoot: input.projectRoot,
|
|
@@ -5,14 +5,20 @@
|
|
|
5
5
|
// @work-item-id WI-179
|
|
6
6
|
// @work-item-id WI-180
|
|
7
7
|
// @work-item-id WI-208
|
|
8
|
+
// @work-item-id WI-330
|
|
8
9
|
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
DoctorAgentScope,
|
|
12
|
+
ScopedOutDiagnosticFinding,
|
|
13
|
+
} from "../../application/usecases/run-doctor-diagnostics.js";
|
|
14
|
+
import type { ConfigStatus } from "../../domain/config-status.js";
|
|
10
15
|
import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
11
16
|
|
|
12
17
|
export interface DiagnosticReportFormatterInput {
|
|
13
18
|
readonly report: DiagnosticReport;
|
|
14
19
|
readonly agent: DoctorAgentScope;
|
|
15
20
|
readonly installationMode: "project" | "personal";
|
|
21
|
+
readonly configStatus: ConfigStatus;
|
|
16
22
|
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
17
23
|
readonly phasegateVersion: string;
|
|
18
24
|
readonly projectRoot: string;
|
|
@@ -32,6 +38,7 @@ export class DiagnosticReportFormatter {
|
|
|
32
38
|
description: scopeDescription(input.agent, input.installationMode),
|
|
33
39
|
},
|
|
34
40
|
overallStatus: input.report.overallStatus,
|
|
41
|
+
configStatus: input.configStatus,
|
|
35
42
|
findings: input.report.findings.map((finding) => ({
|
|
36
43
|
...finding.toJSON(),
|
|
37
44
|
applicability: "applicable",
|
|
@@ -64,6 +71,7 @@ export class DiagnosticReportFormatter {
|
|
|
64
71
|
`phasegate doctor v${input.phasegateVersion}`,
|
|
65
72
|
`Project: ${input.projectRoot}`,
|
|
66
73
|
`Scope: ${input.agent} / ${input.installationMode} (${scopeDescription(input.agent, input.installationMode)})`,
|
|
74
|
+
`Config: ${input.configStatus}`,
|
|
67
75
|
"",
|
|
68
76
|
];
|
|
69
77
|
for (const finding of input.report.findings) {
|
|
@@ -79,10 +87,14 @@ export class DiagnosticReportFormatter {
|
|
|
79
87
|
}
|
|
80
88
|
const redCount = input.report.findings.filter((finding) => finding.severity === "red").length;
|
|
81
89
|
const warnCount = input.report.findings.filter((finding) => finding.severity === "warn").length;
|
|
82
|
-
lines.push(
|
|
90
|
+
lines.push(
|
|
91
|
+
`Status: ${input.report.overallStatus.toUpperCase()} (${input.report.findings.length} findings: ${redCount} red, ${warnCount} warn)`,
|
|
92
|
+
);
|
|
83
93
|
if (input.scopedOutFindings.length > 0) {
|
|
84
94
|
const checkIds = input.scopedOutFindings.map(({ finding }) => finding.checkId).join(", ");
|
|
85
|
-
lines.push(
|
|
95
|
+
lines.push(
|
|
96
|
+
`Scoped out: ${input.scopedOutFindings.length} informational findings not applicable to --agent ${input.agent}; not repair targets for this scope: ${checkIds}.`,
|
|
97
|
+
);
|
|
86
98
|
}
|
|
87
99
|
lines.push(`Exit: ${input.exitCode}`);
|
|
88
100
|
return lines.join("\n");
|
|
@@ -91,8 +103,10 @@ export class DiagnosticReportFormatter {
|
|
|
91
103
|
|
|
92
104
|
function scopeDescription(agent: DoctorAgentScope, installationMode: "project" | "personal"): string {
|
|
93
105
|
if (installationMode === "personal") {
|
|
94
|
-
if (agent === "claude")
|
|
95
|
-
|
|
106
|
+
if (agent === "claude")
|
|
107
|
+
return "Personal Claude Code sandbox; team/project Husky, CI, package, and Codex-only findings are not repair targets.";
|
|
108
|
+
if (agent === "codex")
|
|
109
|
+
return "Personal Codex sandbox; team/project Husky, CI, package, and Claude-only findings are not repair targets.";
|
|
96
110
|
return "Personal sandbox diagnostics; team/project Husky, CI, and package findings are not repair targets.";
|
|
97
111
|
}
|
|
98
112
|
if (agent === "claude") return "Claude Code and shared setup targets; Codex-only findings are not applicable.";
|