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.
Files changed (92) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/installation.md +1 -1
  5. package/docs/guide/layer-model.md +2 -0
  6. package/docs/guide/quick-vs-full-mode.md +28 -3
  7. package/docs/guide/troubleshooting.md +37 -0
  8. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  9. package/docs/templates/ci/aidlc-gate.yml +22 -4
  10. package/package.json +2 -2
  11. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  12. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  13. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  14. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  19. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  20. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  21. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  22. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  23. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  24. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  25. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  26. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  27. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  28. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  29. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  30. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  31. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  32. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
  33. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  34. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  35. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  36. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  37. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  38. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  39. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  40. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  41. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  42. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  43. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  44. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  45. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  46. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  47. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  48. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  49. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  50. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  51. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  52. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  53. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  54. package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
  55. package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
  56. package/scripts/harness/installation/composition-root.ts +13 -3
  57. package/scripts/harness/installation/domain/check-id.ts +2 -0
  58. package/scripts/harness/installation/domain/config-status.ts +17 -0
  59. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  60. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  61. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  62. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  63. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  64. package/scripts/harness/integrations/pre-commit.ts +17 -3
  65. package/scripts/harness/main.ts +83 -15
  66. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  67. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  68. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  69. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  70. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  71. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  72. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  73. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  74. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  75. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  76. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  77. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  78. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  79. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
  80. package/scripts/harness/validator-system/composition-root.ts +4 -1
  81. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  82. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  83. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  84. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  85. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  86. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  87. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  88. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  89. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  90. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  91. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  92. 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(projectRoot: string, inspector: FileInspector): Promise<DiagnosticFinding | null>;
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 { DoctorAgentScope, RunDoctorDiagnosticsUseCase } from "../../application/usecases/run-doctor-diagnostics.js";
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 { DoctorAgentScope, ScopedOutDiagnosticFinding } from "../../application/usecases/run-doctor-diagnostics.js";
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(`Status: ${input.report.overallStatus.toUpperCase()} (${input.report.findings.length} findings: ${redCount} red, ${warnCount} warn)`);
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(`Scoped out: ${input.scopedOutFindings.length} informational findings not applicable to --agent ${input.agent}; not repair targets for this scope: ${checkIds}.`);
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") return "Personal Claude Code sandbox; team/project Husky, CI, package, and Codex-only findings are not repair targets.";
95
- if (agent === "codex") return "Personal Codex sandbox; team/project Husky, CI, package, and Claude-only findings are not repair targets.";
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
- const passed = results.filter((r) => r.passed && !r.skipped).length;
298
- const failed = results.filter((r) => !r.passed && !r.skipped).length;
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
- if (result.passed || result.skipped) return undefined;
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,
@@ -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
- "domain",
558
- "application",
559
- "infrastructure",
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
- async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
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 = parseFlag(args, "--layer") as "L0" | "L2" | "L3" | "L4" | "all" | undefined;
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");