phasegate 0.283.0 → 0.315.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/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +28 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
- 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/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/ci-governance/composition-root.ts +2 -2
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -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-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 +57 -9
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
- 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/services/quick-mode-judgment-engine.ts +72 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- 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/quick-implementor/SKILL.md +19 -0
|
@@ -3,73 +3,88 @@
|
|
|
3
3
|
* @unit harness-error
|
|
4
4
|
* @work-item-id WI-156
|
|
5
5
|
*/
|
|
6
|
-
import { AdrRef } from
|
|
7
|
-
import { ErrorCode } from
|
|
8
|
-
import { ErrorDefinition } from
|
|
9
|
-
import { FixExample } from
|
|
10
|
-
import { Severity } from
|
|
6
|
+
import { AdrRef } from "../../domain/value-objects/adr-ref.js";
|
|
7
|
+
import { ErrorCode } from "../../domain/value-objects/error-code.js";
|
|
8
|
+
import { ErrorDefinition } from "../../domain/value-objects/error-definition.js";
|
|
9
|
+
import { FixExample } from "../../domain/value-objects/fix-example.js";
|
|
10
|
+
import { Severity } from "../../domain/value-objects/severity.js";
|
|
11
11
|
|
|
12
12
|
function createDefinition(input: {
|
|
13
13
|
code: string;
|
|
14
14
|
title: string;
|
|
15
|
-
category:
|
|
15
|
+
category:
|
|
16
|
+
| "phase_gate"
|
|
17
|
+
| "architecture"
|
|
18
|
+
| "dependency"
|
|
19
|
+
| "quality"
|
|
20
|
+
| "security"
|
|
21
|
+
| "performance"
|
|
22
|
+
| "consistency"
|
|
23
|
+
| "metadata";
|
|
16
24
|
ownerValidatorId: string;
|
|
17
25
|
defaultFixExample: string;
|
|
26
|
+
defaultRemediationType?: "mechanical" | "ai-assisted" | "manual";
|
|
18
27
|
}): ErrorDefinition {
|
|
19
28
|
return ErrorDefinition.create({
|
|
20
29
|
code: ErrorCode.create(input.code),
|
|
21
30
|
title: input.title,
|
|
22
31
|
category: input.category,
|
|
23
|
-
defaultSeverity: Severity.create(
|
|
32
|
+
defaultSeverity: Severity.create("warning"),
|
|
24
33
|
adrRefRequired: true,
|
|
25
|
-
defaultAdrRef: AdrRef.create(
|
|
34
|
+
defaultAdrRef: AdrRef.create("ADR-004"),
|
|
26
35
|
fixExampleRequired: true,
|
|
27
36
|
defaultFixExample: FixExample.create(input.defaultFixExample),
|
|
28
37
|
ownerValidatorId: input.ownerValidatorId,
|
|
38
|
+
defaultRemediationType: input.defaultRemediationType ?? null,
|
|
29
39
|
});
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export const L4_ERROR_DEFINITIONS = Object.freeze([
|
|
33
43
|
createDefinition({
|
|
34
|
-
code:
|
|
35
|
-
title:
|
|
36
|
-
category:
|
|
37
|
-
ownerValidatorId:
|
|
44
|
+
code: "L4-001",
|
|
45
|
+
title: "設計と実装の乖離が検出された",
|
|
46
|
+
category: "consistency",
|
|
47
|
+
ownerValidatorId: "drift-detect",
|
|
38
48
|
defaultFixExample: 'const actual = "align implementation with design";',
|
|
49
|
+
// WI-335: design drift の解消は設計意図の理解が必要で機械適用不能だが、
|
|
50
|
+
// AI エージェントが設計文書を読んで自己修正できる(ai-assisted)。
|
|
51
|
+
defaultRemediationType: "ai-assisted",
|
|
39
52
|
}),
|
|
40
53
|
createDefinition({
|
|
41
|
-
code:
|
|
42
|
-
title:
|
|
43
|
-
category:
|
|
44
|
-
ownerValidatorId:
|
|
54
|
+
code: "L4-002",
|
|
55
|
+
title: "文書間の整合性が崩れている",
|
|
56
|
+
category: "consistency",
|
|
57
|
+
ownerValidatorId: "consistency-check",
|
|
45
58
|
defaultFixExample: 'const actual = "synchronize documentation";',
|
|
59
|
+
// WI-335: 文書間整合の回復は「どちらの記述が正か」の判断を伴う(ai-assisted)。
|
|
60
|
+
defaultRemediationType: "ai-assisted",
|
|
46
61
|
}),
|
|
47
62
|
createDefinition({
|
|
48
|
-
code:
|
|
49
|
-
title:
|
|
50
|
-
category:
|
|
51
|
-
ownerValidatorId:
|
|
63
|
+
code: "L4-003",
|
|
64
|
+
title: "未使用コードが検出された",
|
|
65
|
+
category: "consistency",
|
|
66
|
+
ownerValidatorId: "dead-code",
|
|
52
67
|
defaultFixExample: 'const actual = "remove unused export";',
|
|
53
68
|
}),
|
|
54
69
|
createDefinition({
|
|
55
|
-
code:
|
|
56
|
-
title:
|
|
57
|
-
category:
|
|
58
|
-
ownerValidatorId:
|
|
70
|
+
code: "L4-004",
|
|
71
|
+
title: "設計ドキュメントの鮮度が閾値を超過した",
|
|
72
|
+
category: "consistency",
|
|
73
|
+
ownerValidatorId: "doc-freshness",
|
|
59
74
|
defaultFixExample: 'const actual = "review or refresh stale design document";',
|
|
60
75
|
}),
|
|
61
76
|
createDefinition({
|
|
62
|
-
code:
|
|
63
|
-
title:
|
|
64
|
-
category:
|
|
65
|
-
ownerValidatorId:
|
|
77
|
+
code: "L4-005",
|
|
78
|
+
title: "設計ドキュメント内のポインタ参照が解決できない",
|
|
79
|
+
category: "consistency",
|
|
80
|
+
ownerValidatorId: "pointer-validation",
|
|
66
81
|
defaultFixExample: 'const actual = "fix unresolved document pointer";',
|
|
67
82
|
}),
|
|
68
83
|
createDefinition({
|
|
69
|
-
code:
|
|
70
|
-
title:
|
|
71
|
-
category:
|
|
72
|
-
ownerValidatorId:
|
|
84
|
+
code: "L4-006",
|
|
85
|
+
title: "スキルカタログ数とドキュメント宣言が一致しない",
|
|
86
|
+
category: "consistency",
|
|
87
|
+
ownerValidatorId: "skill-catalog-drift",
|
|
73
88
|
defaultFixExample: 'const actual = "update skill count declarations with the skill catalog";',
|
|
74
89
|
}),
|
|
75
90
|
]);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-215
|
|
4
|
+
// @work-item-id WI-343
|
|
4
5
|
|
|
5
6
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
|
-
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
|
+
import type { HeuristicCheck, HeuristicCheckContext } from "../../domain/ports/heuristic-check.js";
|
|
7
8
|
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
8
9
|
import { createFinding, projectPath } from "./check-utils.js";
|
|
9
10
|
|
|
@@ -15,20 +16,25 @@ export class ClaudeContextMissingCheck implements HeuristicCheck {
|
|
|
15
16
|
private readonly projectTarget = "CLAUDE.md";
|
|
16
17
|
private readonly legacyTarget = ".claude/CLAUDE.local.md";
|
|
17
18
|
|
|
18
|
-
async run(
|
|
19
|
+
async run(
|
|
20
|
+
projectRoot: string,
|
|
21
|
+
inspector: FileInspectorPort,
|
|
22
|
+
context?: HeuristicCheckContext,
|
|
23
|
+
): Promise<DiagnosticFinding | null> {
|
|
19
24
|
const content = await inspector.readText(projectPath(projectRoot, this.target));
|
|
20
|
-
if (content?.includes(MARKDOWN_BEGIN) === true &&
|
|
25
|
+
if (content?.includes(MARKDOWN_BEGIN) === true && /phasegate/i.test(content)) return null;
|
|
21
26
|
const projectContent = await inspector.readText(projectPath(projectRoot, this.projectTarget));
|
|
22
|
-
if (projectContent?.includes(MARKDOWN_BEGIN) === true &&
|
|
27
|
+
if (projectContent?.includes(MARKDOWN_BEGIN) === true && /phasegate/i.test(projectContent)) return null;
|
|
23
28
|
|
|
24
29
|
const legacyContent = await inspector.readText(projectPath(projectRoot, this.legacyTarget));
|
|
30
|
+
const target = context?.installationMode === "project" ? this.projectTarget : this.target;
|
|
25
31
|
return createFinding({
|
|
26
32
|
checkId: this.checkId,
|
|
27
33
|
severity: "red",
|
|
28
|
-
target
|
|
29
|
-
message: legacyContent
|
|
34
|
+
target,
|
|
35
|
+
message: legacyContent !== null && /phasegate/i.test(legacyContent)
|
|
30
36
|
? ".claude/CLAUDE.local.md は Claude Code の documented discovery path ではありません"
|
|
31
|
-
:
|
|
37
|
+
: `${target} に runtime-visible PhaseGate context がありません`,
|
|
32
38
|
repairMode: "mechanical",
|
|
33
39
|
});
|
|
34
40
|
}
|
|
@@ -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
|
}
|