phasegate 0.283.0 → 0.335.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +16 -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/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  5. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  6. package/docs/guide/cli-reference.md +89 -7
  7. package/docs/guide/configuration.md +43 -2
  8. package/docs/guide/layer-model.md +2 -0
  9. package/docs/guide/quick-vs-full-mode.md +63 -3
  10. package/docs/guide/troubleshooting.md +37 -0
  11. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  12. package/package.json +2 -2
  13. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
  14. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
  15. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
  16. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
  17. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  18. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
  19. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  20. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
  21. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  22. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  23. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  24. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  25. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  26. package/scripts/harness/ci-governance/composition-root.ts +57 -5
  27. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  28. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  29. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  30. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  31. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  32. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  33. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  34. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  35. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  36. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  37. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  38. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  39. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  40. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  41. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -3
  42. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
  43. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
  44. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  45. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  46. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
  47. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  48. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  49. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  50. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  51. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  52. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  53. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  54. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  55. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  56. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  57. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  58. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  59. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  60. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  61. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  62. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  63. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  64. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  65. package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
  66. package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
  67. package/scripts/harness/installation/composition-root.ts +13 -3
  68. package/scripts/harness/installation/domain/check-id.ts +2 -0
  69. package/scripts/harness/installation/domain/config-status.ts +17 -0
  70. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  71. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  72. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  73. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  74. package/scripts/harness/integrations/pre-commit.ts +17 -3
  75. package/scripts/harness/main.ts +159 -21
  76. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  77. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  78. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +64 -33
  79. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  80. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  81. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  82. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  83. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  84. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +116 -18
  85. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  86. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  87. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  88. package/scripts/harness/quick-mode/index.ts +7 -1
  89. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  90. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  91. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  92. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  93. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  94. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  95. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  96. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  97. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
  98. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  99. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  100. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  101. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  102. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  103. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  104. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  105. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  106. package/skills/product-architect/SKILL.md +19 -0
  107. package/skills/quick-implementor/SKILL.md +19 -0
  108. package/skills/story-mapper/SKILL.md +11 -0
  109. package/skills/story-writer/SKILL.md +11 -0
  110. package/skills/unit-designer/SKILL.md +11 -0
  111. package/templates/product_overview.template.md +85 -0
  112. package/templates/product_overview_plan.template.md +55 -0
  113. package/templates/story_mapping_plan.template.md +61 -0
  114. package/templates/story_writer_plan.template.md +61 -0
  115. package/templates/unit_design_plan.template.md +63 -0
@@ -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,11 +41,11 @@ 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";
48
- import { buildCiGovernance } from "./ci-governance/composition-root.js";
48
+ import { buildCiGovernance, type CiGovernanceDocPaths } from "./ci-governance/composition-root.js";
49
49
  import { toPhaseConfigSection } from "./config-foundation/application/mappers/phase-config-section-mapper.js";
50
50
  import { toValidatorSystemConfig } from "./config-foundation/application/mappers/validator-system-config-mapper.js";
51
51
  import { toWorldModelConfig } from "./config-foundation/application/mappers/world-model-config-mapper.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 {
@@ -120,6 +120,19 @@ function toTraceabilityModelOptions(resolvedConfig: HarnessConfigV2 | undefined)
120
120
  return resolvedConfig ? { pathRoots: { designDocsRoot: resolvedConfig.paths.designDocs } } : undefined;
121
121
  }
122
122
 
123
+ /**
124
+ * WI-369: ci-governance の scaffold 系アダプタに解決済み paths を渡す。
125
+ * これが無いと `scaffold-design` の書き込み先が `docs/product/construction` に
126
+ * 固定され、`paths.designDocs` を移設した PJ で scaffold 先とゲート検査先がズレる。
127
+ */
128
+ function toCiGovernanceDocPaths(resolvedConfig: HarnessConfigV2 | undefined): CiGovernanceDocPaths {
129
+ if (!resolvedConfig) return {};
130
+ return {
131
+ designDocs: resolvedConfig.paths.designDocs,
132
+ inceptionDocs: resolvedConfig.paths.inceptionDocs,
133
+ };
134
+ }
135
+
123
136
  interface PackageJsonDocument {
124
137
  readonly [key: string]: unknown;
125
138
  readonly dependencies?: unknown;
@@ -228,7 +241,7 @@ Commands:
228
241
 
229
242
  lint Run lint checks (--json, --target <path>)
230
243
 
231
- validate Run validators (--layer L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci|json, --json)
244
+ validate Run validators (--layer L0|L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci|json, --json)
232
245
  ci-check CI check (--quick for quick mode, --fail-on-reject, --dry-run, --files)
233
246
  check-change-category Classify changed paths for quick mode (--paths <csv>, --format human|json)
234
247
 
@@ -263,6 +276,7 @@ Gate semantics:
263
276
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
264
277
  baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
265
278
  scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
279
+ scaffold-inception Scaffold an inception/product doc (--kind <product-overview-plan|product-overview|story-writer-plan|story-mapping-plan|unit-design-plan>, --dry-run|--apply, --force, --json)
266
280
  session begin Start a Full Mode session (--mode full, --unit, --work-item, --reason, --duration)
267
281
  session end End a Full Mode session (--work-item)
268
282
 
@@ -294,6 +308,10 @@ Skills:
294
308
  skills list List all available skills
295
309
  skills info <name> Show skill details (SKILL.md)
296
310
 
311
+ Templates:
312
+ templates list List bundled document templates
313
+ templates show <name> Print a bundled template to stdout
314
+
297
315
  Options:
298
316
  --help Show this help message
299
317
  --version Show version number
@@ -501,6 +519,15 @@ function parseValidateFormat(args: readonly string[]): "human" | "agent" | "ci"
501
519
  throw new Error(`Invalid --format value for validate: '${raw}'. Supported values: human, agent, ci, json.`);
502
520
  }
503
521
 
522
+ function parseValidateLayer(args: readonly string[]): "L0" | "L2" | "L3" | "L4" | "all" | undefined {
523
+ const raw = parseFlag(args, "--layer");
524
+ if (raw === undefined) return undefined;
525
+ if (raw === "L0" || raw === "L2" || raw === "L3" || raw === "L4" || raw === "all") return raw;
526
+
527
+ const lintGuidance = raw === "L1" ? "\nL1 は `npx phasegate lint` で実行してください。" : "";
528
+ throw new Error(`不正な --layer 値: ${raw}\n有効値: L0, L2, L3, L4, all${lintGuidance}`);
529
+ }
530
+
504
531
  function levenshtein(a: string, b: string): number {
505
532
  const m = a.length;
506
533
  const n = b.length;
@@ -553,12 +580,25 @@ function validateKnownFlags(args: readonly string[], known: readonly string[]):
553
580
  return null;
554
581
  }
555
582
 
583
+ /**
584
+ * Full Mode session が許可する変更カテゴリ。
585
+ *
586
+ * WI-348: 照合相手は quick-mode の `ChangeCategoryValue`
587
+ * (`bugfix | docs | test | config | feature | domain | api`) であり、
588
+ * レイヤー名(application / infrastructure / presentation)ではない。
589
+ * 旧実装はレイヤー名語彙を書き出していたため、交差が domain / config のみとなり
590
+ * session を張っても feature / api の書き込みがブロックされ続けていた。
591
+ * ChangeCategory の全語彙を許可し、session のスコープ制御は
592
+ * unit / 期限 / target path 側の判定に委ねる。
593
+ */
556
594
  const FULL_MODE_SESSION_ALLOWED_CATEGORIES = Object.freeze([
557
- "domain",
558
- "application",
559
- "infrastructure",
560
- "presentation",
595
+ "bugfix",
596
+ "docs",
597
+ "test",
561
598
  "config",
599
+ "feature",
600
+ "domain",
601
+ "api",
562
602
  ]);
563
603
 
564
604
  interface FullModeSessionFile {
@@ -878,6 +918,33 @@ Options:
878
918
  --force Overwrite an existing target when applying.
879
919
  --json Output machine-readable JSON.
880
920
  --help, -h Show this help`,
921
+ "scaffold-inception": `Usage: phasegate scaffold-inception --kind <doc-kind> [options]
922
+
923
+ Scaffold an inception plan document or the product overview.
924
+ Targets follow paths.inceptionDocs / paths.designDocs from phasegate.config.json.
925
+
926
+ Options:
927
+ --kind <doc-kind> product-overview-plan, product-overview, story-writer-plan,
928
+ story-mapping-plan, or unit-design-plan.
929
+ --dry-run Preview target and template without writing (default).
930
+ --apply Write the scaffold.
931
+ --force Overwrite an existing target when applying.
932
+ --json Output machine-readable JSON.
933
+ --help, -h Show this help`,
934
+ templates: `Usage: phasegate templates <list|show <name>>
935
+
936
+ Print bundled document templates to stdout, without reading node_modules.
937
+
938
+ Subcommands:
939
+ list List available template names.
940
+ show <name> Print the template body to stdout.
941
+
942
+ Options:
943
+ --json Output machine-readable JSON (list only).
944
+ --help, -h Show this help
945
+
946
+ Notes:
947
+ <name> is matched against the bundled template catalog. Paths are rejected.`,
881
948
  session: `Usage: phasegate session <begin|end> [options]
882
949
 
883
950
  Manage hook-visible Full Mode sessions.
@@ -1901,6 +1968,32 @@ async function loadResolvedConfig(command?: string): Promise<HarnessConfigV2 | u
1901
1968
  }
1902
1969
  }
1903
1970
 
1971
+ /**
1972
+ * WI-351: quick-mode composition root へ解決済み configPath / rootDir を注入する。
1973
+ *
1974
+ * 無指定だと `HarnessConfigQuickModeConfigAdapter` は `process.cwd()/phasegate.config.json`、
1975
+ * `FsFileExistenceAdapter` は `process.cwd()` を基準にする。サブディレクトリから CLI を
1976
+ * 実行すると config を見失い、相対パスの存在判定(= CREATE/MODIFY 推定)も
1977
+ * プロジェクトルート基準の hook とずれて分類結果が食い違う。
1978
+ * config-foundation が上方探索で解決した sourcePath を基準に揃える
1979
+ * (hook 側で WI-346 が行ったのと同じ整合)。
1980
+ * config 未検出・不正時は従来どおり cwd 基準へフォールバックする(fail-open)。
1981
+ */
1982
+ async function resolveQuickModeCompositionOptions(): Promise<QuickModeCompositionRootOptions> {
1983
+ try {
1984
+ const configModule = createConfigFoundationModule();
1985
+ const { sourcePath } = await configModule.usecases.loadResolvedConfigUseCase.execute();
1986
+ if (typeof sourcePath === "string" && sourcePath !== "") {
1987
+ const configDir = dirname(sourcePath);
1988
+ const rootDir = basename(configDir) === ".phasegate-local" ? dirname(configDir) : configDir;
1989
+ return { configPath: sourcePath, rootDir };
1990
+ }
1991
+ } catch {
1992
+ // 解決できない場合はフォールバック(下の return)
1993
+ }
1994
+ return { rootDir: getProjectRoot() };
1995
+ }
1996
+
1904
1997
  async function loadWorldResolvedConfig() {
1905
1998
  try {
1906
1999
  const configModule = createConfigFoundationModule();
@@ -2888,7 +2981,7 @@ async function main(): Promise<void> {
2888
2981
  // ── validator-system ──
2889
2982
  case "validate": {
2890
2983
  const mod = createValidatorSystemModule(toValidatorSystemConfig(resolvedConfig));
2891
- const layer = parseFlag(args, "--layer") as "L0" | "L2" | "L3" | "L4" | "all" | undefined;
2984
+ const layer = parseValidateLayer(args);
2892
2985
  const unit = parseFlag(args, "--unit");
2893
2986
  const phase = parseFlag(args, "--phase");
2894
2987
  const format = parseValidateFormat(args) ?? (json ? "ci" : undefined);
@@ -2917,7 +3010,10 @@ async function main(): Promise<void> {
2917
3010
  case "ci-check": {
2918
3011
  const quick = hasFlag(args, "--quick");
2919
3012
  if (quick) {
2920
- const mod = createQuickModeCompositionRoot();
3013
+ // WI-364: check-change-category (WI-351) と同様に解決済み configPath / rootDir を注入する。
3014
+ // 無指定だと quickMode 設定を cwd 基準でしか探せず、サブディレクトリ実行で
3015
+ // HarnessConfigNotFoundError → exit 2 になっていた。
3016
+ const mod = createQuickModeCompositionRoot(await resolveQuickModeCompositionOptions());
2921
3017
  const failOnReject = hasFlag(args, "--fail-on-reject");
2922
3018
  const dryRun = hasFlag(args, "--dry-run");
2923
3019
  const files = parseFlag(args, "--files");
@@ -2956,7 +3052,7 @@ async function main(): Promise<void> {
2956
3052
  );
2957
3053
  return;
2958
3054
  }
2959
- const mod = createQuickModeCompositionRoot();
3055
+ const mod = createQuickModeCompositionRoot(await resolveQuickModeCompositionOptions());
2960
3056
  const paths = parseFlag(args, "--paths");
2961
3057
  const format = parseFlag(args, "--format") as "human" | "json" | undefined;
2962
3058
  const failOnFullRequired = hasFlag(args, "--fail-on-full-required");
@@ -3193,7 +3289,7 @@ Examples:
3193
3289
  console.error(flagError);
3194
3290
  process.exit(2);
3195
3291
  }
3196
- const mod = buildCiGovernance(rootDir, harnessRoot);
3292
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3197
3293
  const presetId = parseFlag(args, "--preset") ?? "standard";
3198
3294
  const templateType = parseFlag(args, "--type") ?? "aidlc-gate";
3199
3295
  const render = hasFlag(args, "--render");
@@ -3205,7 +3301,7 @@ Examples:
3205
3301
  }
3206
3302
 
3207
3303
  case "ci:migrate-agents-md": {
3208
- const mod = buildCiGovernance(rootDir, harnessRoot);
3304
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3209
3305
  const dryRun = hasFlag(args, "--dry-run");
3210
3306
  const validateOnly = hasFlag(args, "--validate-only");
3211
3307
  const format = json ? "json" : "human";
@@ -3216,7 +3312,7 @@ Examples:
3216
3312
  }
3217
3313
 
3218
3314
  case "ci:auto-refresh-agent-context": {
3219
- const mod = buildCiGovernance(rootDir, harnessRoot);
3315
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3220
3316
  const dryRun = hasFlag(args, "--dry-run");
3221
3317
  const apply = hasFlag(args, "--apply");
3222
3318
  const format = json ? "json" : "human";
@@ -3227,7 +3323,7 @@ Examples:
3227
3323
  }
3228
3324
 
3229
3325
  case "refresh-claude-md": {
3230
- const mod = buildCiGovernance(rootDir, harnessRoot);
3326
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3231
3327
  const dryRun = hasFlag(args, "--dry-run");
3232
3328
  const apply = hasFlag(args, "--apply");
3233
3329
  const format = json ? "json" : "human";
@@ -3238,7 +3334,7 @@ Examples:
3238
3334
  }
3239
3335
 
3240
3336
  case "p2:check-agent-context": {
3241
- const mod = buildCiGovernance(rootDir, harnessRoot);
3337
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3242
3338
  const thresholdRaw = parseFlag(args, "--threshold-days");
3243
3339
  const thresholdDays = thresholdRaw === undefined ? undefined : Number(thresholdRaw);
3244
3340
  const format = json ? "json" : "human";
@@ -3249,7 +3345,7 @@ Examples:
3249
3345
  }
3250
3346
 
3251
3347
  case "ci:check-repetition": {
3252
- const mod = buildCiGovernance(rootDir, harnessRoot);
3348
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3253
3349
  const errorCode = parseFlag(args, "--code") ?? "";
3254
3350
  const reset = hasFlag(args, "--reset");
3255
3351
  const format = json ? "json" : "human";
@@ -3260,7 +3356,7 @@ Examples:
3260
3356
  }
3261
3357
 
3262
3358
  case "baseline": {
3263
- const mod = buildCiGovernance(rootDir, harnessRoot);
3359
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3264
3360
  const dryRun = hasFlag(args, "--dry-run");
3265
3361
  const force = hasFlag(args, "--force");
3266
3362
  const pathsFlag = parseFlag(args, "--paths");
@@ -3283,7 +3379,7 @@ Examples:
3283
3379
  }
3284
3380
 
3285
3381
  case "scaffold-design": {
3286
- const mod = buildCiGovernance(rootDir, harnessRoot);
3382
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3287
3383
  const unit = parseFlag(args, "--unit") ?? "";
3288
3384
  const phase = parseFlag(args, "--phase") ?? "";
3289
3385
  const dryRun = hasFlag(args, "--dry-run");
@@ -3303,8 +3399,50 @@ Examples:
3303
3399
  return;
3304
3400
  }
3305
3401
 
3402
+ case "scaffold-inception": {
3403
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3404
+ const kind = parseFlag(args, "--kind") ?? "";
3405
+ const dryRun = hasFlag(args, "--dry-run");
3406
+ const apply = hasFlag(args, "--apply");
3407
+ const force = hasFlag(args, "--force");
3408
+ const format = json ? "json" : "human";
3409
+ const result = await mod.scaffoldInceptionHandler.handle({
3410
+ kind,
3411
+ dryRun,
3412
+ apply,
3413
+ force,
3414
+ format,
3415
+ });
3416
+ console.log(result.output);
3417
+ await finishCliExit(result.exitCode);
3418
+ return;
3419
+ }
3420
+
3421
+ case "templates": {
3422
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3423
+ const subCommand = args[1];
3424
+ const emit = async (result: { exitCode: number; output: string; errorOutput: string }): Promise<void> => {
3425
+ if (result.output.length > 0) console.log(result.output);
3426
+ if (result.errorOutput.length > 0) console.error(result.errorOutput);
3427
+ await finishCliExit(result.exitCode);
3428
+ };
3429
+
3430
+ if (subCommand === "list") {
3431
+ await emit(await mod.templatesHandler.list({ format: json ? "json" : "human" }));
3432
+ return;
3433
+ }
3434
+
3435
+ if (subCommand === "show") {
3436
+ await emit(await mod.templatesHandler.show({ name: args[2] }));
3437
+ return;
3438
+ }
3439
+
3440
+ await emit(mod.templatesHandler.usage());
3441
+ return;
3442
+ }
3443
+
3306
3444
  case "integrity:pin": {
3307
- const mod = buildCiGovernance(rootDir, harnessRoot);
3445
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3308
3446
  const dryRun = hasFlag(args, "--dry-run");
3309
3447
  const format = json ? "json" : "human";
3310
3448
  const result = await mod.integrityHandler.pin({ dryRun, format });
@@ -3314,7 +3452,7 @@ Examples:
3314
3452
  }
3315
3453
 
3316
3454
  case "integrity:verify": {
3317
- const mod = buildCiGovernance(rootDir, harnessRoot);
3455
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3318
3456
  const format = json ? "json" : "human";
3319
3457
  const result = await mod.integrityHandler.verify({ format });
3320
3458
  console.log(result.output);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
6
7
  import type { ArtifactExistenceCheckerPort } from '../../domain/ports/artifact-existence-checker-port.js';
@@ -68,7 +69,7 @@ export class EvidenceBundleAssembler {
68
69
  continue;
69
70
  }
70
71
 
71
- const actual = await this.planDocumentReader.readEvidence(node, scope, planningMode);
72
+ const actual = await this.planDocumentReader.readEvidence(node, scope, planningMode, pathRoots);
72
73
  planEvidences.set(node.nodeKey(), actual);
73
74
  }
74
75
 
@@ -1,16 +1,28 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
7
+ import type { PathRoots } from '../values/artifact.js';
6
8
  import type { PhaseNode } from '../values/phase-node.js';
7
9
  import type { PlanEvidence } from '../values/plan-evidence.js';
8
10
  import type { PlanningMode } from '../values/planning-mode.js';
9
11
 
10
12
  export interface PlanDocumentReaderPort {
13
+ /**
14
+ * plan 文書の存在と QA evidence を読む。
15
+ *
16
+ * `pathRoots` は `phasegate.config.json` の `paths` を解決したもの。
17
+ * 省略時は既定ルート(`docs/inception` / `docs/product/construction`)を使う。
18
+ * WI-369: これを渡さないと `paths.inceptionDocs` を移設した PJ で
19
+ * plan 文書だけが既定パスから探され、成果物検査と plan evidence の
20
+ * 探索先がズレる(成果物は在るのに「plan文書が不足」で落ちる)。
21
+ */
11
22
  readEvidence(
12
23
  node: PhaseNode,
13
24
  scope: { unitId?: string; storyId?: string },
14
25
  expectedMode: PlanningMode,
26
+ pathRoots?: PathRoots,
15
27
  ): Promise<PlanEvidence>;
16
28
  }
@@ -1,22 +1,36 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
6
- import { readFile } from 'node:fs/promises';
7
- import * as path from 'node:path';
8
- import type { PlanDocumentReaderPort } from '../../domain/ports/plan-document-reader-port.js';
9
- import type { PhaseNode } from '../../domain/values/phase-node.js';
10
- import type { PlanningMode } from '../../domain/values/planning-mode.js';
11
- import { PlanEvidence } from '../../domain/values/plan-evidence.js';
7
+ import { readFile } from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import type { PlanDocumentReaderPort } from "../../domain/ports/plan-document-reader-port.js";
10
+ import { DEFAULT_PATH_ROOTS, type PathRoots } from "../../domain/values/artifact.js";
11
+ import type { PhaseNode } from "../../domain/values/phase-node.js";
12
+ import { PlanEvidence } from "../../domain/values/plan-evidence.js";
13
+ import type { PlanningMode } from "../../domain/values/planning-mode.js";
12
14
 
13
15
  export interface MarkdownPlanDocumentReaderDeps {
14
16
  readonly rootDir: string;
15
17
  }
16
18
 
17
- const QA_SECTION_PATTERN = /^## QA\b/m;
18
- const QUESTION_PATTERN = /^Q:/gm;
19
- const ANSWER_PATTERN = /^A:/gm;
19
+ /**
20
+ * WI-358 (issue #29): 見出しの表記ゆれで Planning Mode の evidence 判定が落ちる罠を潰す。
21
+ *
22
+ * 旧パターンは "QA" 綴りしか受け付けず、エージェントが自然に書く `## Q&A` /
23
+ * 全角の `## Q&A` を弾いていた。中身は同一なのに phase gate だけが落ち、
24
+ * 原因が見出しの綴りだと気付けないまま止まる。
25
+ *
26
+ * 緩和方向のみの変更であり、既存の `## QA` / `## 4. QA(不明点・確認事項)` は
27
+ * すべて従来どおりマッチする。
28
+ */
29
+ const QA_SECTION_PATTERN = /^##+\s*(?:\d+[..]\s*)?Q[&&]?A\b/m;
30
+ const QUESTION_PATTERN = /^(?:Q:|###\s*\[Question\])/gm;
31
+ const LEGACY_ANSWER_PATTERN = /^A:/gm;
32
+ const STRUCTURED_ANSWER_PATTERN = /^\[Answer\][ \t]*$/gm;
33
+ const ANSWER_BODY_BOUNDARY_PATTERN = /^(?:###\s*\[Question\]|#{1,6}\s+)/m;
20
34
 
21
35
  export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
22
36
  private readonly rootDir: string;
@@ -29,6 +43,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
29
43
  node: PhaseNode,
30
44
  scope: { unitId?: string; storyId?: string },
31
45
  expectedMode: PlanningMode,
46
+ pathRoots: PathRoots = DEFAULT_PATH_ROOTS,
32
47
  ): Promise<PlanEvidence> {
33
48
  const planArtifacts = node.planArtifacts();
34
49
 
@@ -43,7 +58,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
43
58
  const artifact = planArtifacts[0];
44
59
  let resolvedPath: string;
45
60
  try {
46
- resolvedPath = artifact.resolve(scope);
61
+ resolvedPath = artifact.resolve(scope, pathRoots);
47
62
  } catch {
48
63
  return PlanEvidence.create({
49
64
  exists: false,
@@ -55,7 +70,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
55
70
 
56
71
  let content: string;
57
72
  try {
58
- content = await readFile(absolutePath, 'utf8');
73
+ content = await readFile(absolutePath, "utf8");
59
74
  } catch {
60
75
  return PlanEvidence.create({
61
76
  exists: false,
@@ -66,11 +81,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
66
81
 
67
82
  const hasQaSection = QA_SECTION_PATTERN.test(content);
68
83
  const qaComplete = this.isQaComplete(content, hasQaSection, expectedMode);
69
- const planningModeMatch = this.detectPlanningModeMatch(
70
- content,
71
- hasQaSection,
72
- expectedMode,
73
- );
84
+ const planningModeMatch = this.detectPlanningModeMatch(content, hasQaSection, expectedMode);
74
85
 
75
86
  return PlanEvidence.create({
76
87
  exists: true,
@@ -79,16 +90,12 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
79
90
  });
80
91
  }
81
92
 
82
- private isQaComplete(
83
- content: string,
84
- hasQaSection: boolean,
85
- expectedMode: PlanningMode,
86
- ): boolean {
93
+ private isQaComplete(content: string, hasQaSection: boolean, expectedMode: PlanningMode): boolean {
87
94
  if (expectedMode.requiresAnsweredQa()) {
88
95
  if (!hasQaSection) return false;
89
96
  const qaSection = this.extractQaSection(content);
90
97
  const questionCount = (qaSection.match(QUESTION_PATTERN) ?? []).length;
91
- const answerCount = (qaSection.match(ANSWER_PATTERN) ?? []).length;
98
+ const answerCount = this.countAnswers(qaSection);
92
99
  return questionCount > 0 && questionCount === answerCount;
93
100
  }
94
101
 
@@ -99,16 +106,12 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
99
106
  return true;
100
107
  }
101
108
 
102
- private detectPlanningModeMatch(
103
- content: string,
104
- hasQaSection: boolean,
105
- expectedMode: PlanningMode,
106
- ): boolean {
109
+ private detectPlanningModeMatch(content: string, hasQaSection: boolean, expectedMode: PlanningMode): boolean {
107
110
  if (expectedMode.requiresAnsweredQa()) {
108
111
  if (!hasQaSection) return false;
109
112
  const qaSection = this.extractQaSection(content);
110
113
  const questionCount = (qaSection.match(QUESTION_PATTERN) ?? []).length;
111
- const answerCount = (qaSection.match(ANSWER_PATTERN) ?? []).length;
114
+ const answerCount = this.countAnswers(qaSection);
112
115
  return questionCount > 0 && answerCount === questionCount;
113
116
  }
114
117
 
@@ -121,14 +124,42 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
121
124
 
122
125
  private extractQaSection(content: string): string {
123
126
  const match = content.match(QA_SECTION_PATTERN);
124
- if (!match || match.index === undefined) return '';
127
+ if (!match || match.index === undefined) return "";
125
128
 
126
129
  const start = match.index;
127
- const nextSectionMatch = content.slice(start + match[0].length).match(/^## /m);
128
- const end = nextSectionMatch?.index
129
- ? start + match[0].length + nextSectionMatch.index
130
- : content.length;
130
+ const sectionContentStart = start + match[0].length;
131
+ const sectionContent = content.slice(sectionContentStart);
132
+ const headingMarker = match[0].match(/^##+/)?.[0];
133
+ if (!headingMarker) return "";
134
+
135
+ const sameLevelHeadingPattern = new RegExp(`^${headingMarker}(?!#)[ \\t]+(.+)$`, "gm");
136
+ let end = content.length;
137
+ for (const headingMatch of sectionContent.matchAll(sameLevelHeadingPattern)) {
138
+ if (/^\[Question\](?:\s|$)/.test(headingMatch[1].trimStart())) continue;
139
+ if (headingMatch.index !== undefined) {
140
+ end = sectionContentStart + headingMatch.index;
141
+ break;
142
+ }
143
+ }
131
144
 
132
145
  return content.slice(start, end);
133
146
  }
147
+
148
+ private countAnswers(qaSection: string): number {
149
+ const legacyAnswerCount = (qaSection.match(LEGACY_ANSWER_PATTERN) ?? []).length;
150
+ let structuredAnswerCount = 0;
151
+
152
+ for (const answerMatch of qaSection.matchAll(STRUCTURED_ANSWER_PATTERN)) {
153
+ if (answerMatch.index === undefined) continue;
154
+ const bodyStart = answerMatch.index + answerMatch[0].length;
155
+ const followingContent = qaSection.slice(bodyStart);
156
+ const boundaryMatch = followingContent.match(ANSWER_BODY_BOUNDARY_PATTERN);
157
+ const answerBody =
158
+ boundaryMatch?.index === undefined ? followingContent : followingContent.slice(0, boundaryMatch.index);
159
+ const hasBody = answerBody.split(/\r?\n/).some((line) => line.trim().length > 0);
160
+ if (hasBody) structuredAnswerCount += 1;
161
+ }
162
+
163
+ return legacyAnswerCount + structuredAnswerCount;
164
+ }
134
165
  }
@@ -2,8 +2,8 @@
2
2
  * @layer presentation
3
3
  * @unit phase2-extensions
4
4
  */
5
- import type { CheckDocFreshnessUseCase } from '../../application/usecases/check-doc-freshness-usecase.js';
6
- import { FreshnessResultFormatter } from '../formatters/freshness-result-formatter.js';
5
+ import type { CheckDocFreshnessUseCase } from "../../application/usecases/check-doc-freshness-usecase.js";
6
+ import { FreshnessResultFormatter } from "../formatters/freshness-result-formatter.js";
7
7
 
8
8
  export class CheckFreshnessHandler {
9
9
  private readonly formatter = new FreshnessResultFormatter();
@@ -12,26 +12,34 @@ export class CheckFreshnessHandler {
12
12
 
13
13
  async handle(args: string[]): Promise<{ exitCode: number; stdout: string }> {
14
14
  let targetPattern: string | undefined;
15
- let format: 'text' | 'json' = 'text';
15
+ let format: "text" | "json" = "text";
16
16
  let dryRun = false;
17
17
 
18
18
  for (let index = 0; index < args.length; index += 1) {
19
19
  const arg = args[index];
20
- if (arg === '--pattern') {
20
+ if (arg === "--pattern") {
21
21
  targetPattern = args[index + 1];
22
22
  index += 1;
23
- } else if (arg === '--format') {
24
- format = (args[index + 1] as 'text' | 'json') ?? 'text';
23
+ } else if (arg === "--format") {
24
+ format = (args[index + 1] as "text" | "json") ?? "text";
25
25
  index += 1;
26
- } else if (arg === '--dry-run') {
26
+ } else if (arg === "--dry-run") {
27
27
  dryRun = true;
28
28
  }
29
29
  }
30
30
 
31
31
  const result = await this.useCase.execute({ targetPattern, format, dryRun });
32
+ // WI-359: `--dry-run` は「副作用なしで診断のみ」(skills/doc-health-checker/SKILL.md)であり、
33
+ // シナリオ仕様 SC-P2-002 の期待結果も exit 0 である。従来は dryRun をパースだけして
34
+ // 捨てており、error 閾値超過の文書が 1 件でもあると診断のみの実行でも exit 1 になっていた。
35
+ // その結果 SC-P2-002 の e2e は「コードを一切変えなくても暦の経過だけで落ちる」状態になっていた。
36
+ // 実運用の鮮度ゲートは L4-004(`validate --layer L4`)が担い、そちらは本 handler を
37
+ // 経由しないため、dry-run を report-only にしてもゲートは緩まない。
38
+ // フラグ未指定時の exit 1 は従来どおり維持する。
39
+ const exitCode = dryRun ? 0 : result.summary.error > 0 ? 1 : 0;
32
40
  return {
33
- exitCode: result.summary.error > 0 ? 1 : 0,
34
- stdout: format === 'json' ? this.formatter.formatJson(result) : this.formatter.formatText(result),
41
+ exitCode,
42
+ stdout: format === "json" ? this.formatter.formatJson(result) : this.formatter.formatText(result),
35
43
  };
36
44
  }
37
45
  }