phasegate 0.264.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 +54 -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/installation.md +1 -1
- 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/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- 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 +45 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- 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/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- 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/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- 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 +234 -53
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
- 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/deployment-manifest.ts +43 -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 +83 -15
- 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 +32 -7
- package/scripts/harness/validator-system/composition-root.ts +4 -1
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- 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/harness-config-validator-config-adapter.ts +89 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- 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
|
@@ -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,19 +1,32 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-326
|
|
4
5
|
|
|
5
6
|
import { DeploymentEntry, type DeploymentEntryJson } from "./deployment-entry.js";
|
|
6
7
|
|
|
8
|
+
// Records which install-time options produced this manifest so a later
|
|
9
|
+
// reconcile can honor the original opt-in state instead of assuming defaults.
|
|
10
|
+
// Optional for backward compatibility: manifests written before WI-326 carry
|
|
11
|
+
// no flags, and consumers must fall back to legacy behavior in that case.
|
|
12
|
+
export interface InstallationFlags {
|
|
13
|
+
readonly includeHusky: boolean;
|
|
14
|
+
readonly includeCi: boolean;
|
|
15
|
+
readonly personal: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export interface DeploymentManifestInput {
|
|
8
19
|
readonly version: string;
|
|
9
20
|
readonly installedAt: string;
|
|
10
21
|
readonly entries: readonly DeploymentEntry[];
|
|
22
|
+
readonly installationFlags?: InstallationFlags;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export interface DeploymentManifestJson {
|
|
14
26
|
readonly version: string;
|
|
15
27
|
readonly installedAt: string;
|
|
16
28
|
readonly entries: readonly DeploymentEntryJson[];
|
|
29
|
+
readonly installationFlags?: InstallationFlags;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export class DeploymentManifest {
|
|
@@ -22,6 +35,7 @@ export class DeploymentManifest {
|
|
|
22
35
|
readonly version: string;
|
|
23
36
|
readonly installedAt: string;
|
|
24
37
|
readonly entries: readonly DeploymentEntry[];
|
|
38
|
+
readonly installationFlags?: InstallationFlags;
|
|
25
39
|
|
|
26
40
|
private constructor(input: DeploymentManifestInput) {
|
|
27
41
|
if (!DeploymentManifest.SEMVER_PATTERN.test(input.version)) {
|
|
@@ -30,6 +44,12 @@ export class DeploymentManifest {
|
|
|
30
44
|
if (!Number.isFinite(Date.parse(input.installedAt))) {
|
|
31
45
|
throw new Error("DeploymentManifest installedAt must be ISO8601-compatible");
|
|
32
46
|
}
|
|
47
|
+
if (input.installationFlags !== undefined) {
|
|
48
|
+
const { includeHusky, includeCi, personal } = input.installationFlags;
|
|
49
|
+
if (typeof includeHusky !== "boolean" || typeof includeCi !== "boolean" || typeof personal !== "boolean") {
|
|
50
|
+
throw new Error("DeploymentManifest installationFlags must contain boolean includeHusky/includeCi/personal");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
33
53
|
const paths = new Set<string>();
|
|
34
54
|
for (const entry of input.entries) {
|
|
35
55
|
if (paths.has(entry.path)) {
|
|
@@ -40,6 +60,14 @@ export class DeploymentManifest {
|
|
|
40
60
|
this.version = input.version;
|
|
41
61
|
this.installedAt = input.installedAt;
|
|
42
62
|
this.entries = Object.freeze([...input.entries]);
|
|
63
|
+
this.installationFlags =
|
|
64
|
+
input.installationFlags === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: Object.freeze({
|
|
67
|
+
includeHusky: input.installationFlags.includeHusky,
|
|
68
|
+
includeCi: input.installationFlags.includeCi,
|
|
69
|
+
personal: input.installationFlags.personal,
|
|
70
|
+
});
|
|
43
71
|
Object.freeze(this);
|
|
44
72
|
}
|
|
45
73
|
|
|
@@ -56,6 +84,7 @@ export class DeploymentManifest {
|
|
|
56
84
|
version: input.version,
|
|
57
85
|
installedAt: input.installedAt,
|
|
58
86
|
entries: input.entries.map((entry) => DeploymentEntry.fromJSON(entry)),
|
|
87
|
+
installationFlags: input.installationFlags,
|
|
59
88
|
});
|
|
60
89
|
}
|
|
61
90
|
|
|
@@ -65,6 +94,7 @@ export class DeploymentManifest {
|
|
|
65
94
|
version: this.version,
|
|
66
95
|
installedAt: this.installedAt,
|
|
67
96
|
entries: [...entries, entry],
|
|
97
|
+
installationFlags: this.installationFlags,
|
|
68
98
|
});
|
|
69
99
|
}
|
|
70
100
|
|
|
@@ -73,6 +103,16 @@ export class DeploymentManifest {
|
|
|
73
103
|
version: this.version,
|
|
74
104
|
installedAt: this.installedAt,
|
|
75
105
|
entries: this.entries.filter((entry) => entry.path !== path),
|
|
106
|
+
installationFlags: this.installationFlags,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
withInstallationFlags(installationFlags: InstallationFlags): DeploymentManifest {
|
|
111
|
+
return DeploymentManifest.reconstitute({
|
|
112
|
+
version: this.version,
|
|
113
|
+
installedAt: this.installedAt,
|
|
114
|
+
entries: this.entries,
|
|
115
|
+
installationFlags,
|
|
76
116
|
});
|
|
77
117
|
}
|
|
78
118
|
|
|
@@ -85,6 +125,9 @@ export class DeploymentManifest {
|
|
|
85
125
|
version: this.version,
|
|
86
126
|
installedAt: this.installedAt,
|
|
87
127
|
entries: this.entries.map((entry) => entry.toJSON()),
|
|
128
|
+
// Omit the key entirely for legacy manifests so serialized output stays
|
|
129
|
+
// byte-compatible with pre-WI-326 manifest.json files.
|
|
130
|
+
...(this.installationFlags === undefined ? {} : { installationFlags: this.installationFlags }),
|
|
88
131
|
};
|
|
89
132
|
}
|
|
90
133
|
}
|
|
@@ -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.";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @work-item-id WI-109
|
|
6
6
|
* @work-item-id WI-189
|
|
7
7
|
* @work-item-id WI-305
|
|
8
|
+
* @work-item-id WI-332
|
|
8
9
|
*
|
|
9
10
|
* Pre-commit CLI entry.
|
|
10
11
|
* Runs L2 validators against staged TypeScript files AND design-document
|
|
@@ -27,6 +28,7 @@ import type { ValidateMetadataCommandOutput } from "../traceability-model/presen
|
|
|
27
28
|
import type { AggregatedValidationReport } from "../validator-system/application/dto/aggregated-validation-report.js";
|
|
28
29
|
import type { ValidationResultContract } from "../validator-system/application/dto/validation-result-contract.js";
|
|
29
30
|
import { createValidatorSystemModule } from "../validator-system/composition-root.js";
|
|
31
|
+
import { isEffectivelyPassed } from "../validator-system/domain/services/effective-severity-policy.js";
|
|
30
32
|
import { HumanValidationResultFormatter } from "../validator-system/presentation/formatters/human-validation-result-formatter.js";
|
|
31
33
|
import { createWorldModelModule } from "../world-model/index.js";
|
|
32
34
|
|
|
@@ -293,9 +295,19 @@ function getStagedDesignChangeFiles(): string[] {
|
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
/**
|
|
299
|
+
* WI-332 / ADR-017: pre-commit 経路には config の failOnWarning 配線が存在しないため、
|
|
300
|
+
* ADR-017 の既定値 (false) に固定する。validate.failOnWarning を pre-commit にも効かせる
|
|
301
|
+
* 判断をする場合は、この定数を options 経由の配線に置き換えること。
|
|
302
|
+
*/
|
|
303
|
+
const PRE_COMMIT_FAIL_ON_WARNING = false;
|
|
304
|
+
|
|
296
305
|
function buildReport(results: readonly ValidationResultContract[]): AggregatedValidationReport {
|
|
297
|
-
|
|
298
|
-
|
|
306
|
+
// WI-332 / ADR-017: 手動集約 (`failed === 0` の raw passed 判定) をやめ、
|
|
307
|
+
// validate / ci-check / complete-check と同じ共有実効判定 isEffectivelyPassed を通す。
|
|
308
|
+
// warning-only の validator failure は既定で effectively passed = exit 0。
|
|
309
|
+
const passed = results.filter((r) => !r.skipped && isEffectivelyPassed(r, PRE_COMMIT_FAIL_ON_WARNING)).length;
|
|
310
|
+
const failed = results.filter((r) => !r.skipped && !isEffectivelyPassed(r, PRE_COMMIT_FAIL_ON_WARNING)).length;
|
|
299
311
|
const skipped = results.filter((r) => r.skipped).length;
|
|
300
312
|
const allErrors = results.flatMap((r) => r.errors);
|
|
301
313
|
const errorCount = allErrors.filter((e) => e.severity === "error").length;
|
|
@@ -452,7 +464,9 @@ export async function validateBypassTrailers(
|
|
|
452
464
|
}
|
|
453
465
|
|
|
454
466
|
function classifyValidatorFailure(result: ValidationResultContract): BypassBlockerClass | undefined {
|
|
455
|
-
|
|
467
|
+
// WI-332: blocker 分類も buildReport と同じ実効判定を通す。warning-only failure は
|
|
468
|
+
// exit 0 (effectively passed) なので、bypass audit の blocker としても数えない。
|
|
469
|
+
if (isEffectivelyPassed(result, PRE_COMMIT_FAIL_ON_WARNING)) return undefined;
|
|
456
470
|
const nonBypassable = NON_BYPASSABLE_VALIDATOR_IDS.includes(result.validatorId);
|
|
457
471
|
return {
|
|
458
472
|
code: result.validatorId,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
rm as fsRm,
|
|
42
42
|
writeFile as fsWriteFile,
|
|
43
43
|
} from "node:fs/promises";
|
|
44
|
-
import { dirname, join, resolve } from "node:path";
|
|
44
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { createAdrFoundationModule } from "./adr-foundation/composition-root.js";
|
|
47
47
|
import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
import { FileSystemStoryReflectionAdapter } from "./phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.js";
|
|
76
76
|
import { StoryReflectionStatusPresenter } from "./phase-dependency-model/presentation/cli/story-reflection-status-presenter.js";
|
|
77
77
|
import { buildPhase2Extensions } from "./phase2-extensions/composition-root.js";
|
|
78
|
-
import { createQuickModeCompositionRoot } from "./quick-mode/composition-root.js";
|
|
78
|
+
import { createQuickModeCompositionRoot, type QuickModeCompositionRootOptions } from "./quick-mode/composition-root.js";
|
|
79
79
|
import { buildRegressionSuite } from "./regression-suite/composition-root.js";
|
|
80
80
|
import type { SkillSet } from "./setup/skill-deployer.js";
|
|
81
81
|
import {
|
|
@@ -228,7 +228,7 @@ Commands:
|
|
|
228
228
|
|
|
229
229
|
lint Run lint checks (--json, --target <path>)
|
|
230
230
|
|
|
231
|
-
validate Run validators (--layer L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci|json, --json)
|
|
231
|
+
validate Run validators (--layer L0|L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci|json, --json)
|
|
232
232
|
ci-check CI check (--quick for quick mode, --fail-on-reject, --dry-run, --files)
|
|
233
233
|
check-change-category Classify changed paths for quick mode (--paths <csv>, --format human|json)
|
|
234
234
|
|
|
@@ -501,6 +501,15 @@ function parseValidateFormat(args: readonly string[]): "human" | "agent" | "ci"
|
|
|
501
501
|
throw new Error(`Invalid --format value for validate: '${raw}'. Supported values: human, agent, ci, json.`);
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
function parseValidateLayer(args: readonly string[]): "L0" | "L2" | "L3" | "L4" | "all" | undefined {
|
|
505
|
+
const raw = parseFlag(args, "--layer");
|
|
506
|
+
if (raw === undefined) return undefined;
|
|
507
|
+
if (raw === "L0" || raw === "L2" || raw === "L3" || raw === "L4" || raw === "all") return raw;
|
|
508
|
+
|
|
509
|
+
const lintGuidance = raw === "L1" ? "\nL1 は `npx phasegate lint` で実行してください。" : "";
|
|
510
|
+
throw new Error(`不正な --layer 値: ${raw}\n有効値: L0, L2, L3, L4, all${lintGuidance}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
504
513
|
function levenshtein(a: string, b: string): number {
|
|
505
514
|
const m = a.length;
|
|
506
515
|
const n = b.length;
|
|
@@ -553,12 +562,25 @@ function validateKnownFlags(args: readonly string[], known: readonly string[]):
|
|
|
553
562
|
return null;
|
|
554
563
|
}
|
|
555
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Full Mode session が許可する変更カテゴリ。
|
|
567
|
+
*
|
|
568
|
+
* WI-348: 照合相手は quick-mode の `ChangeCategoryValue`
|
|
569
|
+
* (`bugfix | docs | test | config | feature | domain | api`) であり、
|
|
570
|
+
* レイヤー名(application / infrastructure / presentation)ではない。
|
|
571
|
+
* 旧実装はレイヤー名語彙を書き出していたため、交差が domain / config のみとなり
|
|
572
|
+
* session を張っても feature / api の書き込みがブロックされ続けていた。
|
|
573
|
+
* ChangeCategory の全語彙を許可し、session のスコープ制御は
|
|
574
|
+
* unit / 期限 / target path 側の判定に委ねる。
|
|
575
|
+
*/
|
|
556
576
|
const FULL_MODE_SESSION_ALLOWED_CATEGORIES = Object.freeze([
|
|
557
|
-
"
|
|
558
|
-
"
|
|
559
|
-
"
|
|
560
|
-
"presentation",
|
|
577
|
+
"bugfix",
|
|
578
|
+
"docs",
|
|
579
|
+
"test",
|
|
561
580
|
"config",
|
|
581
|
+
"feature",
|
|
582
|
+
"domain",
|
|
583
|
+
"api",
|
|
562
584
|
]);
|
|
563
585
|
|
|
564
586
|
interface FullModeSessionFile {
|
|
@@ -694,8 +716,8 @@ Options:
|
|
|
694
716
|
--agent <claude|codex|both> Agent context and hook targets (default: both)
|
|
695
717
|
--skills <core|all> Rendered agent context skill mode (default: all)
|
|
696
718
|
--workflow <standard|strict> Rendered agent context workflow mode (default: standard)
|
|
697
|
-
--with-husky Include Husky hook targets
|
|
698
|
-
--with-ci Include GitHub Actions target
|
|
719
|
+
--with-husky Include Husky hook targets (opt-in; omitted by default)
|
|
720
|
+
--with-ci Include GitHub Actions target (opt-in; omitted by default)
|
|
699
721
|
--personal Use local-only install: no package.json, agent docs, Husky, CI, .gitignore, GitHub CLI, secrets, or CI setting writes.
|
|
700
722
|
With --agent claude, initializes .phasegate-local config/settings/skills and ignored .claude shims.
|
|
701
723
|
--json Output machine-readable JSON
|
|
@@ -1855,7 +1877,18 @@ function emitV2SchemaWarningOnce(sourcePath: string): void {
|
|
|
1855
1877
|
);
|
|
1856
1878
|
}
|
|
1857
1879
|
|
|
1858
|
-
|
|
1880
|
+
/**
|
|
1881
|
+
* 不正 config でも fail-open(警告 + 既定設定で続行)にするコマンド。
|
|
1882
|
+
* GitHub #40: config 検証の fail-closed が dispatch より上流にあると、pre-tool-use hook
|
|
1883
|
+
* 経由で Write/Edit/Bash が全遮断され、config 自身を修復する経路が消える(自己修復
|
|
1884
|
+
* デッドロック)。hook(エージェントのツール遮断点)と doctor(自己診断)は復旧経路
|
|
1885
|
+
* として常に起動可能でなければならない。validate / ci-check 等の検査系コマンドは
|
|
1886
|
+
* fail-closed を維持する(gated スコープへの書き込みは hook 内の phase-gate 判定が
|
|
1887
|
+
* 引き続き fail-closed でブロックする)。
|
|
1888
|
+
*/
|
|
1889
|
+
const CONFIG_FAIL_OPEN_COMMANDS: ReadonlySet<string> = new Set(["hook", "doctor"]);
|
|
1890
|
+
|
|
1891
|
+
async function loadResolvedConfig(command?: string): Promise<HarnessConfigV2 | undefined> {
|
|
1859
1892
|
try {
|
|
1860
1893
|
const configModule = createConfigFoundationModule();
|
|
1861
1894
|
const result = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
@@ -1866,6 +1899,15 @@ async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
|
|
|
1866
1899
|
} catch (error) {
|
|
1867
1900
|
if (error instanceof ConfigValidationError) {
|
|
1868
1901
|
process.stderr.write(`Invalid phasegate.config.json: ${error.message}\n`);
|
|
1902
|
+
if (command !== undefined && CONFIG_FAIL_OPEN_COMMANDS.has(command)) {
|
|
1903
|
+
process.stderr.write(
|
|
1904
|
+
"Warning: continuing with default settings so diagnosis and self-repair stay possible. Fix the reported path in phasegate.config.json to restore full gating.\n",
|
|
1905
|
+
);
|
|
1906
|
+
return undefined;
|
|
1907
|
+
}
|
|
1908
|
+
process.stderr.write(
|
|
1909
|
+
"Recovery: fix the reported path/type in phasegate.config.json (or restore it from version control). `phasegate doctor` and agent hooks remain available while the config is invalid.\n",
|
|
1910
|
+
);
|
|
1869
1911
|
process.exit(2);
|
|
1870
1912
|
}
|
|
1871
1913
|
if (error instanceof ConfigNotFoundError) {
|
|
@@ -1881,6 +1923,32 @@ async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
|
|
|
1881
1923
|
}
|
|
1882
1924
|
}
|
|
1883
1925
|
|
|
1926
|
+
/**
|
|
1927
|
+
* WI-351: quick-mode composition root へ解決済み configPath / rootDir を注入する。
|
|
1928
|
+
*
|
|
1929
|
+
* 無指定だと `HarnessConfigQuickModeConfigAdapter` は `process.cwd()/phasegate.config.json`、
|
|
1930
|
+
* `FsFileExistenceAdapter` は `process.cwd()` を基準にする。サブディレクトリから CLI を
|
|
1931
|
+
* 実行すると config を見失い、相対パスの存在判定(= CREATE/MODIFY 推定)も
|
|
1932
|
+
* プロジェクトルート基準の hook とずれて分類結果が食い違う。
|
|
1933
|
+
* config-foundation が上方探索で解決した sourcePath を基準に揃える
|
|
1934
|
+
* (hook 側で WI-346 が行ったのと同じ整合)。
|
|
1935
|
+
* config 未検出・不正時は従来どおり cwd 基準へフォールバックする(fail-open)。
|
|
1936
|
+
*/
|
|
1937
|
+
async function resolveQuickModeCompositionOptions(): Promise<QuickModeCompositionRootOptions> {
|
|
1938
|
+
try {
|
|
1939
|
+
const configModule = createConfigFoundationModule();
|
|
1940
|
+
const { sourcePath } = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
1941
|
+
if (typeof sourcePath === "string" && sourcePath !== "") {
|
|
1942
|
+
const configDir = dirname(sourcePath);
|
|
1943
|
+
const rootDir = basename(configDir) === ".phasegate-local" ? dirname(configDir) : configDir;
|
|
1944
|
+
return { configPath: sourcePath, rootDir };
|
|
1945
|
+
}
|
|
1946
|
+
} catch {
|
|
1947
|
+
// 解決できない場合はフォールバック(下の return)
|
|
1948
|
+
}
|
|
1949
|
+
return { rootDir: getProjectRoot() };
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1884
1952
|
async function loadWorldResolvedConfig() {
|
|
1885
1953
|
try {
|
|
1886
1954
|
const configModule = createConfigFoundationModule();
|
|
@@ -1942,7 +2010,7 @@ async function main(): Promise<void> {
|
|
|
1942
2010
|
const json = hasFlag(args, "--json");
|
|
1943
2011
|
|
|
1944
2012
|
// Cross-unit wiring: 設定を先に解決し、各Unit に注入する
|
|
1945
|
-
const resolvedConfig = command.startsWith("world:") ? undefined : await loadResolvedConfig();
|
|
2013
|
+
const resolvedConfig = command.startsWith("world:") ? undefined : await loadResolvedConfig(command);
|
|
1946
2014
|
|
|
1947
2015
|
try {
|
|
1948
2016
|
switch (command) {
|
|
@@ -2346,8 +2414,8 @@ async function main(): Promise<void> {
|
|
|
2346
2414
|
force: hasFlag(args, "--force"),
|
|
2347
2415
|
includeClaude,
|
|
2348
2416
|
includeCodex,
|
|
2349
|
-
includeHusky: !personal,
|
|
2350
|
-
includeCi: !personal,
|
|
2417
|
+
includeHusky: !personal && hasFlag(args, "--with-husky"),
|
|
2418
|
+
includeCi: !personal && hasFlag(args, "--with-ci"),
|
|
2351
2419
|
skillSet: skillSetRaw,
|
|
2352
2420
|
workflow: parseWorkflowMode(workflowRaw),
|
|
2353
2421
|
agent,
|
|
@@ -2868,7 +2936,7 @@ async function main(): Promise<void> {
|
|
|
2868
2936
|
// ── validator-system ──
|
|
2869
2937
|
case "validate": {
|
|
2870
2938
|
const mod = createValidatorSystemModule(toValidatorSystemConfig(resolvedConfig));
|
|
2871
|
-
const layer =
|
|
2939
|
+
const layer = parseValidateLayer(args);
|
|
2872
2940
|
const unit = parseFlag(args, "--unit");
|
|
2873
2941
|
const phase = parseFlag(args, "--phase");
|
|
2874
2942
|
const format = parseValidateFormat(args) ?? (json ? "ci" : undefined);
|
|
@@ -2936,7 +3004,7 @@ async function main(): Promise<void> {
|
|
|
2936
3004
|
);
|
|
2937
3005
|
return;
|
|
2938
3006
|
}
|
|
2939
|
-
const mod = createQuickModeCompositionRoot();
|
|
3007
|
+
const mod = createQuickModeCompositionRoot(await resolveQuickModeCompositionOptions());
|
|
2940
3008
|
const paths = parseFlag(args, "--paths");
|
|
2941
3009
|
const format = parseFlag(args, "--format") as "human" | "json" | undefined;
|
|
2942
3010
|
const failOnFullRequired = hasFlag(args, "--fail-on-full-required");
|