phasegate 0.264.0 → 0.283.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 (29) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/guide/installation.md +1 -1
  3. package/docs/templates/ci/aidlc-gate.yml +22 -4
  4. package/package.json +2 -2
  5. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  6. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  7. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  8. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  9. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  10. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  11. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  12. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  13. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  14. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  15. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  16. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  17. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  18. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  19. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  20. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  21. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  22. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  23. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  24. package/scripts/harness/main.ts +26 -6
  25. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -7
  26. package/scripts/harness/validator-system/composition-root.ts +4 -1
  27. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  29. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
package/CHANGELOG.md CHANGED
@@ -9,6 +9,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Fixed
11
11
 
12
+ - **WI-328 — 実効言語と出所を phasegate:status に表示(GitHub #39 残課題)** — WI-319/320 の言語自動検出の結果がどこにも表示されず、どの validator が有効/SKIP になるか知る術がなかった。`phasegate:status` の JSON に `languages: { effective, source }`(source = `declared` / `detected` / `fallback`)を追加。解決ロジックは validator-system の `resolveProjectLanguages()`(WI-319 の検出テーブル)に一本化して再利用し、validator の有効/SKIP 判定と必ず同じ結果を表示する。ConfigQueryPort への追加は optional メソッドで後方互換。
13
+
14
+ - **WI-327 — 最小 config(project のみ)で動作可能に** — 手書き `phasegate.config.json` のスキーマが top-level 8 項目を required とし、`{"project": {"name": ..., "preset": ...}}` の最小構成で L1-001 が連発していた。プリセット解決(`PresetResolutionService.deepMerge`)は省略セクションの補完を既に完備していたため、v3 **と v2**(architecture キーなしの最小 config は v2 検証に振られる)の top-level required を `["project"]` に緩和し、型定義を実態に一致させた。**検証は弱めていない**: セクション内の required・型・enum・additionalProperties は不変で、書かれているが不正なキーは従来どおりエラー(spawn E2E で exit 2 を固定)。最小 config の解決結果がプリセット定義と一致することも統合テストで固定。
15
+
16
+ - **WI-326 — install フラグ状態を manifest に永続化(GitHub #36 残課題)** — `--with-husky` / `--with-ci` / personal の install 時オプションが `.phasegate/manifest.json` に記録されず、後の reconcile が「全 target 対象」を仮定して opt-out したはずの Husky / CI workflow を追加し直す食い違いがあった。manifest に optional `installationFlags` を追加して install(apply)時に実効フラグを記録し、reconcile は明示指定 > manifest 記録 > 従来挙動の優先順で target を構成する。フィールドを持たない旧 manifest は推測せず従来挙動のまま(load / save round-trip のバイト互換もテストで固定)。
17
+
18
+ - **WI-324 — フレッシュプロジェクトで L3-004 を SKIP に(オンボーディング阻害の解消)** — phasegate 導入直後(story 未作成・requirement-test-matrix 未生成)でも L3-004 が「マトリクス不在」で fail-closed FAIL になり導入体験を阻害していた。「matrix 不在 かつ StoryCatalog(user_stories.md)の story ゼロ」の場合のみ skipWithReason(WI-317 の L3-003 と同表現)で透過 SKIP に変更。**story が 1 件でも存在するのに matrix 不在なら従来どおり fail-closed**(あるべき matrix の消失事故は見逃さない)。story 数の取得に失敗した場合も判定不能として fail-closed 側に倒す。
19
+
20
+ - **WI-323 — 非ゲート hook の payload フィールド欠落を fail-open 化(GitHub #40 残課題)** — stop hook が stdin payload の `session_id` 欠落で exit 2 になり、WI-314 の「hook は開発フローを止めない」方針と不整合だった。stop(`SESSION_ID_MISSING`)と post-tool-use(`TOOL_NAME_MISSING`)を警告 + hook-skip-event 記録 + exit 0 の fail-open に変更。**pre-tool-use の `tool_name` 欠落 exit 2 は書き込みゲートのため意図的に fail-closed を維持**し、回帰ガードテストで固定。usecase 契約(空 sessionId エラー)は presentation 入口ガード方式で不変。
21
+
22
+ - **WI-322 — config なし fallback の coverageThreshold 90 を opt-out の 0 に修正(GitHub #37 残課題)** — WI-317 で「カバレッジゲートはオプトイン、0 = 正規の opt-out」と定義したのに、config なし環境で使われる validator-system の `DEFAULT_CONFIG` fallback が `coverageThreshold: 90` のままで、config を持たない環境だけ勝手に 90% が強制される矛盾を解消。fallback のみの変更でプリセット定義(minimal / standard / strict)は不変。fallback 経由で L3-003 が透過 SKIP になることを回帰テストで固定。
23
+
24
+ - **WI-321 — complete-check の JSON 出力に warning 詳細を含める(GitHub #38 残課題)** — `phasegate:complete-check` が warning を `summary.warnings` の件数のみで返し、どの validator のどんな warning かが出力に含まれなかった非対称を修正。ci-check case と同じく `CiCheckResult` を data ペイロードとして pass / fail 双方で返す(`data.validatorResults[].validatorId / errors[]`)。validator 結果 0 件時は従来どおり data なし。判定ロジック・exit code・lint 合流は不変。
25
+
26
+ - **WI-325 — load 経路の JSON パース失敗が "Failed to persist" と誤表示される問題を修正** — `FileSystemConfigRepository.load()` の JSON 構文エラーが書き込み用の `ConfigPersistenceError`("Failed to persist config")で送出され、読み込み失敗なのに永続化失敗と誤解させるメッセージになっていた。`ConfigParseError extends ConfigPersistenceError` を新設して load 経路を "Failed to parse config JSON" に分離。サブクラス化により main.ts の `instanceof ConfigPersistenceError` fail-open 判定(WI-314)は無変更で互換維持。save 経路は従来どおり。
27
+
28
+ - **WI-320 — 言語自動検出を実 CLI 経路で有効化(GitHub #39 フォローアップ)** — WI-319 の検出は adapter に実装されたが、実 CLI では preset 解決(`preset-resolution-service`)・mapper・`ProjectConfig` VO の 3 箇所が未宣言時に `["typescript"]` を注入するため dead code だった(シナリオ検証で発見)。「未宣言」シグナルを resolved config から adapter まで生存させ、純 Python リポジトリで L3-003 が unsupported-language SKIP になることを実 CLI spawn の E2E で固定。宣言時の挙動・検証は不変。
29
+
30
+ - **WI-319 — project.languages のファイルシステム自動検出(GitHub #39)** — `project.languages` 未宣言時に無条件で `["typescript"]` へフォールバックし、純 Python プロジェクトでも TS 専用 validator(L3-002 / L3-003 / L4-003)が既定で走っていた問題を修正。マーカーファイル(python: pyproject.toml 等 / go.mod / Cargo.toml / pom.xml / Gemfile / composer.json、typescript: tsconfig.json または package.json の typescript 依存のみ — phasegate 導入時の package.json を誤検出しないため存在自体は根拠にしない)から検出し、config 宣言は引き続き最優先、検出ゼロ時は従来どおり typescript フォールバック。
31
+
32
+ - **WI-318 — complete-check に severity-aware 集約を適用(GitHub #38)** — `phasegate:complete-check` が validator の生の `passed` だけを見ており、warning-only failure でも exit 1 になって `validate` / `ci-check` と乖離していた。Stop hook が叩くのは complete-check のため、L2-016 の `ungated-legacy` マーカー(error→可視 warning 降格)が実質無効化されていた。ci-check と同一の `CiCheckResult.fromResults()`(ADR-017 / WI-260 の `isEffectivelyPassed`)を通す集約に修正し、warning 件数を summary に反映。skipped validator が fail 扱いされる潜在バグも同時に解消。lint fail → exit 1 の合流は不変。
33
+
34
+ - **WI-317 — coverageThreshold の opt-out を到達可能に(GitHub #37)** — L3-003 実装は「カバレッジゲートはオプトイン」と明記しつつ、opt-out 手段(`null`)がスキーマで拒否され、`0` を設定してもレポート不在で fail-closed FAIL になる矛盾があった。ドメイン VO `L3Config.hasCoverageGate()` の「threshold > 0 でのみ有効」の意図に run-l3 を揃え、`coverageThreshold: 0`(および null)を正規の opt-out として SKIP にした(minimal preset の 0 も本来の opt-out として機能)。レポート不在 FAIL の suggestion に (a) カバレッジ付き実行 (b) `coverageThreshold: 0` での opt-out (c) 非 JS/TS プロジェクトの `project.languages` 宣言、の 3 択を案内する。
35
+
36
+ - **WI-316 — install の `--with-husky` / `--with-ci` dead flag を修正(GitHub #36)** — install コマンドがこの 2 フラグを一度も読まず、`--personal` なしでは常に `.husky/*` と `.github/workflows/phasegate-aidlc-gate.yml` を書き込んでいた。ヘルプ記載・`init` / `setup:agent` と同じ opt-in(フラグ明示時のみ書き込み)に配線を揃え、CLI 統合テストとドキュメントで固定した。usecase のプログラマティック契約(既定 true)は不変。
37
+
38
+ - **WI-315 — install / reconcile が CLAUDE.md の user-section を上書きする問題を修正(GitHub #35)** — CLAUDE.md テンプレートは user-section が managed-section の内側にあるため、`install --apply` / `reconcile` の managed block 置換がユーザー自身の記述を placeholder で消失させていた。merge 時に既存 user-section 本文を抽出して新 block に再注入する方式で保持するようにした(AGENTS.md の既存挙動は不変)。併せて、ユーザー本文が `String.replace` の置換文字列として `$` シーケンス解釈される潜在破損を replacer 関数で封じた。
39
+
40
+ - **WI-314 — 不正 config の hook / doctor 自己修復デッドロック解消(GitHub #40)** — スキーマ違反の `phasegate.config.json` が dispatch 上流の fail-closed で全コマンドを exit 2 にし、pre-tool-use hook 経由で Bash / Write / Edit を全遮断して config 自身の修復も不能になる問題を修正。`hook` / `doctor` は警告 + 既定設定で続行する fail-open とし(診断・自己修復経路の常時確保)、`validate` / `ci-check` 等の検査系は復旧手順の案内付きで fail-closed を維持。hook 内部 adapter の素 `JSON.parse` throw も同様に fail-open 化し、構文破壊 JSON での再デッドロックを塞いだ。gated スコープへの書き込みは phase-gate 判定が引き続き fail-closed でブロックする。
41
+
42
+ - **WI-313 — typescript peer range を `<7.0.0` に制限(GitHub #34)** — peer 自動インストールで typescript@7 が解決されると classic compiler API(`ts.ScriptTarget` 等)が存在せず全コマンドが `Cannot read properties of undefined (reading 'ESNext')` でクラッシュする問題を修正。TypeScript 7 系を peer range で弾き、packaging contract テストで range を固定した。
43
+
44
+ - **WI-312 — coverage-producing CI gate** — production workflowとbundled `aidlc-gate`がL3-003実行前に`coverage/coverage-summary.json`を生成せず、clean checkoutでcoverageThresholdに対してfail-closedとなる既存欠陥を修正。self-repoはcoverage付きunit / integrationとcoverage対象外E2Eを各1回だけ実行してplain full suiteの二重実行を避け、threads開始時にもforks blobを保持して両poolをmergeする。templateは採用projectの`coverage` scriptをpackage manager別に必須実行し、test / coverage → matrix → World derive二重一致 → L3 → attestation → integrityの順序を維持する。
45
+
46
+ - **WI-311 — hermetic CI-facing harness integration tests** — GitHub Actionsのtest-before-matrix順序で、real lint integrationとCLI E2Eがself-repo cwd、coverage、untracked requirement matrixを暗黙のPASS入力としていた既存欠陥を修正。lint adapterへ明示rootを注入してtracked minimal workspaceだけを実scanし、`phasegate:ci-check --json`とlegacy `complete-check`はL3 / Worldを明示無効化したtemp configで公開command契約を検証する。productionのfail-closedとCI gate順序は変更しない。
47
+
48
+ - **WI-310 — hermetic large-stdout CLI regression** — WI-308の64 KiB超assertionがuntracked requirement matrixとself-repoの604 adopted-legacy warningsへ依存していた欠陥を修正。tracked World-enabled configを使うtemp corpusに160組のduplicate fragment IDを生成し、CIのtest-before-matrix順序でも`validate --layer L2 --format json` / `--json`が決定的に64 KiBを超えて完全parseできる契約へ置換した。
49
+
12
50
  - **WI-309 — WI-305 validator domain reflection repair** — `DesignChangeDeclarationPolicy`を実装・配線しながらvalidator-system `domain_model.md`へのWI-305累積反映を欠落させ、story reflection corpusが検出したbaseline外違反を正規に解消。exact `corpusRole + DeclaredKey`照合、pinned change限定、Work-Item trailer intersection、unique sorted finding、local fast-path / L3 authority境界を実コードどおり記録し、baseline / allowlistには触れていない。
13
51
 
14
52
  - **WI-308 — CLI large stdout drain** — self-repoのWorld enforcement有効化により`validate --layer L2 --json`が604件のadopted-legacy warningを含む64 KiB超のJSONとなり、`console.log`直後の`process.exit`がpipe bufferをdrainせず末尾を切断する回帰を修正。direct stdout resultを返す全top-level CLI経路を共通graceful exitへ統合し、warningを省略せず任意サイズの完全な出力と既存exit codeを維持する。64 KiB超を必須条件とするprocess E2Eで`--format json` / `--json`双方を固定し、同E2E fileの全subprocess caseへ明示60秒timeoutを適用した。
@@ -56,7 +56,7 @@ npx phasegate install --apply
56
56
  npx phasegate doctor
57
57
  ```
58
58
 
59
- `install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, deploys selected bundled skills to root `skills/`, creates `.claude/skills` and `.codex/skills` links, writes `CLAUDE.md` / `AGENTS.md` managed sections for selected agent targets, writes `.github/workflows/phasegate-aidlc-gate.yml` when CI is enabled, and records managed entries in `.phasegate/manifest.json`. Existing skills catalogs are merged: PhaseGate refreshes only bundled skill directories selected by `--skills core|all` and preserves user-owned skills. See [Setup Artifacts](setup-artifacts.md) for the full managed target, generated artifact, runtime state, legacy artifact, and user-level setting inventory. <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 --> <!-- @work-item-id WI-174 --> <!-- @work-item-id WI-216 -->
59
+ `install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, deploys selected bundled skills to root `skills/`, creates `.claude/skills` and `.codex/skills` links, writes `CLAUDE.md` / `AGENTS.md` managed sections for selected agent targets, writes `.github/workflows/phasegate-aidlc-gate.yml` when CI is enabled, and records managed entries in `.phasegate/manifest.json`. Husky hook targets (`.husky/*`) and the GitHub Actions target are opt-in, matching `init` and `setup:agent`: pass `--with-husky` and/or `--with-ci` to include them — without these flags `install` does not touch `.husky/` or `.github/workflows/`. <!-- @work-item-id WI-316 --> Existing skills catalogs are merged: PhaseGate refreshes only bundled skill directories selected by `--skills core|all` and preserves user-owned skills. See [Setup Artifacts](setup-artifacts.md) for the full managed target, generated artifact, runtime state, legacy artifact, and user-level setting inventory. <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 --> <!-- @work-item-id WI-174 --> <!-- @work-item-id WI-216 -->
60
60
 
61
61
  For personal evaluation inside a team-owned repository:
62
62
 
@@ -7,10 +7,11 @@
7
7
  # 実行内容:
8
8
  # 1. bypass:audit — bypass trailer / evidence 監査
9
9
  # 2. phasegate lint — L1 Biome AST ルール検証
10
- # 3. requirement-test matrix をcurrent testsから再生成
11
- # 4. world.enabled=true の場合、world:derive2回実行してbyte一致を検証
12
- # 5. phasegate:ci-check — L3 CI バリデータ(security / performance / coverage / nyquist / World)
13
- # 6. 失敗時: PR にエラーサマリーをコメント
10
+ # 3. package coverage script を実行し、L3-003 の入力を生成
11
+ # 4. requirement-test matrixcurrent testsから再生成
12
+ # 5. world.enabled=true の場合、world:derive を2回実行してbyte一致を検証
13
+ # 6. phasegate:ci-check L3 CI バリデータ(security / performance / coverage / nyquist / World)
14
+ # 7. 失敗時: PR にエラーサマリーをコメント
14
15
 
15
16
  name: AIDLC Quality Gate
16
17
 
@@ -79,6 +80,23 @@ jobs:
79
80
  echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
80
81
  exit $EXIT_CODE
81
82
 
83
+ # L3-003 is fail-closed when coverageThreshold is configured but the
84
+ # report is absent. The adopting project owns this script and must emit
85
+ # coverage/coverage-summary.json; package-manager selection follows the
86
+ # lockfile used during dependency installation above.
87
+ - name: Test and generate coverage
88
+ shell: bash
89
+ run: |
90
+ set -euo pipefail
91
+ node -e "const scripts=require('./package.json').scripts ?? {}; if (!scripts.coverage) { throw new Error('phasegate aidlc-gate requires package.json scripts.coverage to generate coverage/coverage-summary.json'); }"
92
+ if [ -f pnpm-lock.yaml ]; then
93
+ pnpm run coverage
94
+ elif [ -f yarn.lock ]; then
95
+ yarn coverage
96
+ else
97
+ npm run coverage
98
+ fi
99
+
82
100
  # L3-004 consumes the generated requirement-test matrix. Always rebuild it
83
101
  # from this checkout instead of trusting a stale or persisted artifact.
84
102
  - name: Generate requirement-test matrix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.264.0",
3
+ "version": "0.283.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -78,7 +78,7 @@
78
78
  "vitest": "^3.0.0"
79
79
  },
80
80
  "peerDependencies": {
81
- "typescript": ">=5.0.0"
81
+ "typescript": ">=5.0.0 <7.0.0"
82
82
  },
83
83
  "peerDependenciesMeta": {
84
84
  "typescript": {
@@ -6,13 +6,9 @@
6
6
  * HarnessConfigV2 の harnesses セクションから Hook 設定を読み取る
7
7
  */
8
8
 
9
- import * as fs from 'node:fs';
10
- import type {
11
- BaselineConfig,
12
- ConfigQueryPort,
13
- HookType,
14
- } from '../../domain/ports/config-query-port.js';
15
- import { ProjectPaths } from '../../domain/value-objects/project-paths.js';
9
+ import * as fs from "node:fs";
10
+ import type { BaselineConfig, ConfigQueryPort, HookType } from "../../domain/ports/config-query-port.js";
11
+ import { ProjectPaths } from "../../domain/value-objects/project-paths.js";
16
12
 
17
13
  interface ProjectDocsSection {
18
14
  inception?: string;
@@ -84,8 +80,22 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
84
80
  if (this.cachedConfig !== null) {
85
81
  return this.cachedConfig;
86
82
  }
87
- const raw = fs.readFileSync(this.configPath, 'utf8');
88
- const doc = JSON.parse(raw) as HarnessConfigDocument;
83
+ // GitHub #40: config が JSON として壊れている場合に hook プロセス全体を throw で
84
+ // 落とすと、エージェントの全ツール呼び出しが遮断され config の修復自体が不能になる。
85
+ // main.ts の ConfigPersistenceError と同じ意味論(警告 + 既定値で続行)に揃える。
86
+ // ファイル不在等の fs エラーは既存契約どおり throw する(上流の catch が defaults を
87
+ // 適用する)。gated スコープへの書き込みは phase-gate 側が fail-closed でブロックする。
88
+ const raw = fs.readFileSync(this.configPath, "utf8");
89
+ let doc: HarnessConfigDocument;
90
+ try {
91
+ doc = JSON.parse(raw) as HarnessConfigDocument;
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ process.stderr.write(
95
+ `Warning: phasegate.config.json could not be parsed as JSON (${message}); continuing with default hook settings so self-repair stays possible.\n`,
96
+ );
97
+ doc = {};
98
+ }
89
99
  this.cachedConfig = doc;
90
100
  return doc;
91
101
  }
@@ -98,10 +108,10 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
98
108
  // pre-tool-use → agentLessonCollection
99
109
  // post-tool-use → cascadeUpdate
100
110
  // stop → デフォルト有効
101
- if (hookType === 'pre-tool-use') {
111
+ if (hookType === "pre-tool-use") {
102
112
  return harnesses.agentLessonCollection ?? true;
103
113
  }
104
- if (hookType === 'post-tool-use') {
114
+ if (hookType === "post-tool-use") {
105
115
  return harnesses.cascadeUpdate ?? true;
106
116
  }
107
117
  // stop はデフォルト有効
@@ -111,13 +121,9 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
111
121
  async getProtectedFilePatterns(): Promise<string[]> {
112
122
  const config = this.loadConfig();
113
123
  const configured = config.protectedFiles?.patterns ?? [];
114
- const principlesDocs = config.paths?.principlesDocs ?? 'docs/principles';
115
- const folderRulesDoc = config.paths?.folderRulesDoc ?? 'docs/folder_management_rules.md';
116
- return [
117
- ...configured,
118
- `${normalizeProjectPath(principlesDocs)}/**`,
119
- normalizeProjectPath(folderRulesDoc),
120
- ];
124
+ const principlesDocs = config.paths?.principlesDocs ?? "docs/principles";
125
+ const folderRulesDoc = config.paths?.folderRulesDoc ?? "docs/folder_management_rules.md";
126
+ return [...configured, `${normalizeProjectPath(principlesDocs)}/**`, normalizeProjectPath(folderRulesDoc)];
121
127
  }
122
128
 
123
129
  async getProtectedFileExclusions(): Promise<string[]> {
@@ -135,13 +141,10 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
135
141
  const paths = config.project?.paths;
136
142
  const topLevelPaths = config.paths;
137
143
 
138
- return ProjectPaths.create(
139
- paths?.source ?? ['scripts/harness'],
140
- {
141
- construction: paths?.docs?.construction ?? topLevelPaths?.designDocs ?? 'docs/product/construction',
142
- inception: paths?.docs?.inception ?? topLevelPaths?.inceptionDocs ?? 'docs/inception',
143
- },
144
- );
144
+ return ProjectPaths.create(paths?.source ?? ["scripts/harness"], {
145
+ construction: paths?.docs?.construction ?? topLevelPaths?.designDocs ?? "docs/product/construction",
146
+ inception: paths?.docs?.inception ?? topLevelPaths?.inceptionDocs ?? "docs/inception",
147
+ });
145
148
  }
146
149
 
147
150
  async getBaselineConfig(): Promise<BaselineConfig> {
@@ -149,7 +152,7 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
149
152
  const baseline = config.baseline ?? {};
150
153
  return {
151
154
  enabled: baseline.enabled ?? true,
152
- path: baseline.path ?? '.phasegate/baseline.json',
155
+ path: baseline.path ?? ".phasegate/baseline.json",
153
156
  };
154
157
  }
155
158
 
@@ -162,5 +165,5 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
162
165
  }
163
166
 
164
167
  function normalizeProjectPath(path: string): string {
165
- return path.replace(/\\/g, '/').replace(/\/+$/g, '');
168
+ return path.replace(/\\/g, "/").replace(/\/+$/g, "");
166
169
  }
@@ -2,18 +2,19 @@
2
2
  * @layer presentation
3
3
  * @unit agent-integration
4
4
  * @work-item-id WI-208
5
+ * @work-item-id WI-323
5
6
  *
6
7
  * PostToolUse Hook Adapter
7
8
  * Claude Code の PostToolUse Hook エントリポイント
8
9
  */
9
10
 
10
- import { HandlePostToolUseUseCase } from '../application/usecases/handle-post-tool-use-usecase.js';
11
- import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harness-config-config-query-adapter.js';
12
- import { HarnessApiCliCommandRegistryAdapter } from '../infrastructure/adapters/harness-api-cli-command-registry-adapter.js';
13
- import { ChildProcessCliExecutorAdapter } from '../infrastructure/adapters/child-process-cli-executor-adapter.js';
14
- import { recordHookSkipEvent } from './hook-skip-event-recorder.js';
15
- import * as path from 'node:path';
16
- import * as fs from 'node:fs/promises';
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
13
+ import { HandlePostToolUseUseCase } from "../application/usecases/handle-post-tool-use-usecase.js";
14
+ import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
15
+ import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
16
+ import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
17
+ import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
17
18
 
18
19
  interface PostToolUseHookInput {
19
20
  tool_name?: string;
@@ -25,15 +26,15 @@ async function readStdin(): Promise<string> {
25
26
  for await (const chunk of process.stdin) {
26
27
  chunks.push(chunk as Buffer);
27
28
  }
28
- return Buffer.concat(chunks).toString('utf8');
29
+ return Buffer.concat(chunks).toString("utf8");
29
30
  }
30
31
 
31
32
  async function findConfigPath(): Promise<string> {
32
33
  let dir = process.cwd();
33
34
  while (true) {
34
35
  const candidates = [
35
- path.join(dir, 'phasegate.config.json'),
36
- path.join(dir, '.phasegate-local', 'phasegate.config.json'),
36
+ path.join(dir, "phasegate.config.json"),
37
+ path.join(dir, ".phasegate-local", "phasegate.config.json"),
37
38
  ];
38
39
  for (const candidate of candidates) {
39
40
  try {
@@ -45,12 +46,12 @@ async function findConfigPath(): Promise<string> {
45
46
  if (parent === dir) break;
46
47
  dir = parent;
47
48
  }
48
- return path.join(process.cwd(), 'phasegate.config.json');
49
+ return path.join(process.cwd(), "phasegate.config.json");
49
50
  }
50
51
 
51
52
  function projectRootForConfig(configPath: string): string {
52
53
  const configDir = path.dirname(configPath);
53
- return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
54
+ return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
54
55
  }
55
56
 
56
57
  async function main(): Promise<void> {
@@ -58,7 +59,7 @@ async function main(): Promise<void> {
58
59
  try {
59
60
  raw = await readStdin();
60
61
  } catch {
61
- process.stderr.write('stdin読み取りエラー\n');
62
+ process.stderr.write("stdin読み取りエラー\n");
62
63
  process.exit(2);
63
64
  }
64
65
 
@@ -72,8 +73,20 @@ async function main(): Promise<void> {
72
73
 
73
74
  const toolName = input.tool_name;
74
75
  if (!toolName) {
75
- process.stderr.write('tool_nameフィールドが必要です\n');
76
- process.exit(2);
76
+ // WI-323: PostToolUse はツール実行後の lint フィードバックでありゲートではないため、
77
+ // tool_name 欠落は fail-open でスキップする(WI-314 / github#40 方針)。
78
+ // ※ pre-tool-use-hook の同ガードは書き込みゲートなので fail-closed (exit 2) を維持する。
79
+ const configPath = await findConfigPath();
80
+ await recordHookSkipEvent({
81
+ projectRoot: projectRootForConfig(configPath),
82
+ hookType: "post-tool-use",
83
+ reason: "TOOL_NAME_MISSING",
84
+ targetPaths: [],
85
+ });
86
+ process.stderr.write(
87
+ "警告: stdin payload に tool_name が無いため post-tool-use hook の処理をスキップしました (fail-open)\n",
88
+ );
89
+ process.exit(0);
77
90
  }
78
91
 
79
92
  try {
@@ -93,7 +106,7 @@ async function main(): Promise<void> {
93
106
  if (output.skipReason) {
94
107
  await recordHookSkipEvent({
95
108
  projectRoot: projectRootForConfig(configPath),
96
- hookType: 'post-tool-use',
109
+ hookType: "post-tool-use",
97
110
  reason: output.skipReason,
98
111
  targetPaths: [],
99
112
  });
@@ -3,19 +3,20 @@
3
3
  * @unit agent-integration
4
4
  * @work-item-id WI-203
5
5
  * @work-item-id WI-208
6
+ * @work-item-id WI-323
6
7
  *
7
8
  * Stop Hook Adapter
8
9
  * Claude Code の Stop Hook エントリポイント
9
10
  */
10
11
 
11
- import { HandleStopUseCase } from '../application/usecases/handle-stop-usecase.js';
12
- import { EnvFileReentryGuardStateAdapter } from '../infrastructure/adapters/env-file-reentry-guard-state-adapter.js';
13
- import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harness-config-config-query-adapter.js';
14
- import { HarnessApiCliCommandRegistryAdapter } from '../infrastructure/adapters/harness-api-cli-command-registry-adapter.js';
15
- import { ChildProcessCliExecutorAdapter } from '../infrastructure/adapters/child-process-cli-executor-adapter.js';
16
- import { recordHookSkipEvent } from './hook-skip-event-recorder.js';
17
- import * as path from 'node:path';
18
- import * as fs from 'node:fs/promises';
12
+ import * as fs from "node:fs/promises";
13
+ import * as path from "node:path";
14
+ import { HandleStopUseCase } from "../application/usecases/handle-stop-usecase.js";
15
+ import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
16
+ import { EnvFileReentryGuardStateAdapter } from "../infrastructure/adapters/env-file-reentry-guard-state-adapter.js";
17
+ import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
18
+ import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
19
+ import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
19
20
 
20
21
  interface StopHookInput {
21
22
  session_id?: string;
@@ -23,8 +24,7 @@ interface StopHookInput {
23
24
 
24
25
  function isCompleteCheckExecutionWiringFailure(stderr: string): boolean {
25
26
  return (
26
- /scripts\/harness\/cli\/complete-check\.ts/.test(stderr) ||
27
- /ERR_MODULE_NOT_FOUND|Cannot find module/i.test(stderr)
27
+ /scripts\/harness\/cli\/complete-check\.ts/.test(stderr) || /ERR_MODULE_NOT_FOUND|Cannot find module/i.test(stderr)
28
28
  );
29
29
  }
30
30
 
@@ -40,15 +40,15 @@ async function readStdin(): Promise<string> {
40
40
  for await (const chunk of process.stdin) {
41
41
  chunks.push(chunk as Buffer);
42
42
  }
43
- return Buffer.concat(chunks).toString('utf8');
43
+ return Buffer.concat(chunks).toString("utf8");
44
44
  }
45
45
 
46
46
  async function findConfigPath(): Promise<string> {
47
47
  let dir = process.cwd();
48
48
  while (true) {
49
49
  const candidates = [
50
- path.join(dir, 'phasegate.config.json'),
51
- path.join(dir, '.phasegate-local', 'phasegate.config.json'),
50
+ path.join(dir, "phasegate.config.json"),
51
+ path.join(dir, ".phasegate-local", "phasegate.config.json"),
52
52
  ];
53
53
  for (const candidate of candidates) {
54
54
  try {
@@ -60,12 +60,12 @@ async function findConfigPath(): Promise<string> {
60
60
  if (parent === dir) break;
61
61
  dir = parent;
62
62
  }
63
- return path.join(process.cwd(), 'phasegate.config.json');
63
+ return path.join(process.cwd(), "phasegate.config.json");
64
64
  }
65
65
 
66
66
  function projectRootForConfig(configPath: string): string {
67
67
  const configDir = path.dirname(configPath);
68
- return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
68
+ return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
69
69
  }
70
70
 
71
71
  async function main(): Promise<void> {
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
73
73
  try {
74
74
  raw = await readStdin();
75
75
  } catch {
76
- process.stderr.write('stdin読み取りエラー\n');
76
+ process.stderr.write("stdin読み取りエラー\n");
77
77
  process.exit(2);
78
78
  }
79
79
 
@@ -87,13 +87,24 @@ async function main(): Promise<void> {
87
87
 
88
88
  const sessionId = input.session_id;
89
89
  if (!sessionId) {
90
- process.stderr.write('session_idフィールドが必要です\n');
91
- process.exit(2);
90
+ // WI-323: session_id 欠落は呼び出し側環境の不備であり、stop hook はゲートではないため
91
+ // fail-open でスキップする(WI-314 / github#40 の「hook は開発フローを止めない」方針)。
92
+ const configPath = await findConfigPath();
93
+ await recordHookSkipEvent({
94
+ projectRoot: projectRootForConfig(configPath),
95
+ hookType: "stop",
96
+ reason: "SESSION_ID_MISSING",
97
+ targetPaths: [],
98
+ });
99
+ process.stderr.write(
100
+ "警告: stdin payload に session_id が無いため stop hook の処理をスキップしました (fail-open)\n",
101
+ );
102
+ process.exit(0);
92
103
  }
93
104
 
94
105
  try {
95
106
  const configPath = await findConfigPath();
96
- const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy: 'env' });
107
+ const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy: "env" });
97
108
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
98
109
  const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
99
110
  const cliExecutorPort = new ChildProcessCliExecutorAdapter();
@@ -107,14 +118,14 @@ async function main(): Promise<void> {
107
118
 
108
119
  const output = await useCase.execute({ sessionId });
109
120
 
110
- if (output.skipReason === 'REENTRY_DETECTED') {
121
+ if (output.skipReason === "REENTRY_DETECTED") {
111
122
  await recordHookSkipEvent({
112
123
  projectRoot: projectRootForConfig(configPath),
113
- hookType: 'stop',
124
+ hookType: "stop",
114
125
  reason: output.skipReason,
115
126
  targetPaths: [],
116
127
  });
117
- process.stderr.write('ReentryGuard: 再入検出によりスキップ\n');
128
+ process.stderr.write("ReentryGuard: 再入検出によりスキップ\n");
118
129
  process.exit(0);
119
130
  }
120
131
 
@@ -123,10 +134,8 @@ async function main(): Promise<void> {
123
134
  // WI-087 finding #4: enforce=true なら exit 2 + decision JSON で turn block
124
135
  if (output.shouldEnforceFailure === true) {
125
136
  const reason = formatCompleteCheckFailureReason(output.cliResult.exitCode, output.cliResult.stderr);
126
- process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\n`);
127
- process.stderr.write(
128
- `${reason} — strict mode により turn を block します\n`,
129
- );
137
+ process.stdout.write(`${JSON.stringify({ decision: "block", reason })}\n`);
138
+ process.stderr.write(`${reason} — strict mode により turn を block します\n`);
130
139
  process.exit(2);
131
140
  }
132
141
  process.stderr.write(`Complete Check失敗 (exitCode=${output.cliResult.exitCode})\n`);
@@ -10,6 +10,7 @@
10
10
  * @work-item-id WI-300
11
11
  * @work-item-id WI-301
12
12
  * @work-item-id WI-302
13
+ * @work-item-id WI-320
13
14
  */
14
15
  import type { HarnessConfigV2 } from "../../domain/harness-config.js";
15
16
 
@@ -54,7 +55,10 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
54
55
  return {
55
56
  project: {
56
57
  preset: resolvedConfig.project.preset,
57
- languages: resolvedConfig.project.languages ?? ["typescript"],
58
+ // WI-320 (github#39): 未宣言時に ["typescript"] を注入せず undefined のまま渡す。
59
+ // HarnessConfigValidatorConfigAdapter.getProjectLanguages() が未宣言時のみ
60
+ // ファイルシステム検出(WI-319)にフォールバックできるようにするため。
61
+ ...(resolvedConfig.project.languages === undefined ? {} : { languages: resolvedConfig.project.languages }),
58
62
  },
59
63
  paths: {
60
64
  designDocs: resolvedConfig.paths.designDocs,
@@ -5,6 +5,7 @@
5
5
  * @work-item-id WI-212
6
6
  * @work-item-id WI-219
7
7
  * @work-item-id WI-300
8
+ * @work-item-id WI-327
8
9
  */
9
10
  import { ConfigFoundationDomainError } from "./errors/config-foundation-domain-error.js";
10
11
  import { ConfigValidationError } from "./errors/config-validation-error.js";
@@ -48,13 +49,15 @@ export interface HarnessConfigSourceDocument {
48
49
  preset: PresetId;
49
50
  languages?: string[];
50
51
  };
51
- layers: DeepPartial<HarnessConfigResolvedDocument["layers"]>;
52
- quickMode: Partial<HarnessConfigResolvedDocument["quickMode"]>;
53
- phaseDependencies: HarnessConfigResolvedDocument["phaseDependencies"];
54
- planningMode: HarnessConfigResolvedDocument["planningMode"];
55
- harnesses: Partial<HarnessConfigResolvedDocument["harnesses"]>;
56
- paths: HarnessConfigResolvedDocument["paths"];
57
- reporting: HarnessConfigResolvedDocument["reporting"];
52
+ // WI-327: project 以外の top-level セクションは手書き config で省略可。
53
+ // 省略時は PresetResolutionService が防御プリセット(project.preset)のデフォルト値で補完する。
54
+ layers?: DeepPartial<HarnessConfigResolvedDocument["layers"]>;
55
+ quickMode?: Partial<HarnessConfigResolvedDocument["quickMode"]>;
56
+ phaseDependencies?: HarnessConfigResolvedDocument["phaseDependencies"];
57
+ planningMode?: HarnessConfigResolvedDocument["planningMode"];
58
+ harnesses?: Partial<HarnessConfigResolvedDocument["harnesses"]>;
59
+ paths?: HarnessConfigResolvedDocument["paths"];
60
+ reporting?: HarnessConfigResolvedDocument["reporting"];
58
61
  ci?: HarnessConfigResolvedDocument["ci"];
59
62
  validate?: HarnessConfigResolvedDocument["validate"];
60
63
  preCommit?: Partial<HarnessConfigResolvedDocument["preCommit"]>;
@@ -3,6 +3,7 @@
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-212
5
5
  * @work-item-id WI-300
6
+ * @work-item-id WI-320
6
7
  */
7
8
  import { ConfigFoundationDomainError } from "../errors/config-foundation-domain-error.js";
8
9
  import type { HarnessConfigResolvedDocument, HarnessConfigSourceDocument } from "../harness-config.js";
@@ -163,7 +164,9 @@ export class PresetResolutionService {
163
164
  project: {
164
165
  name: sourceDocument.project.name,
165
166
  preset: sourceDocument.project.preset,
166
- languages: sourceDocument.project.languages ?? ["typescript"],
167
+ // WI-320 (github#39): 未宣言時に ["typescript"] を注入しない。「未宣言」を resolved config まで
168
+ // 生存させ、validator-system adapter のファイルシステム言語検出(WI-319)を実 CLI 経路で有効にする。
169
+ ...(sourceDocument.project.languages === undefined ? {} : { languages: sourceDocument.project.languages }),
167
170
  },
168
171
  layers,
169
172
  quickMode: deepMerge(presetDefinition.quickMode, sourceDocument.quickMode, "quickMode"),
@@ -2,11 +2,12 @@
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-212
5
+ * @work-item-id WI-320
5
6
  *
6
7
  * ProjectConfig値オブジェクト - プロジェクト名とPresetを保持する
7
8
  */
8
- import { ConfigValidationError } from '../errors/config-validation-error.js';
9
- import { Preset } from './preset.js';
9
+ import { ConfigValidationError } from "../errors/config-validation-error.js";
10
+ import { Preset } from "./preset.js";
10
11
 
11
12
  interface ProjectConfigProps {
12
13
  readonly name: string;
@@ -17,29 +18,37 @@ interface ProjectConfigProps {
17
18
  export class ProjectConfig {
18
19
  readonly name: string;
19
20
  readonly preset: Preset;
20
- readonly languages: readonly string[];
21
+ /**
22
+ * WI-320 (github#39): 宣言があった場合のみ保持する(undefined = 未宣言)。
23
+ * default `['typescript']` の注入をやめ、「未宣言」シグナルを下流
24
+ * (validator-system のファイルシステム言語検出、WI-319)まで生存させる。
25
+ */
26
+ readonly languages?: readonly string[];
21
27
 
22
28
  constructor(props: ProjectConfigProps) {
23
- if (!props.name || props.name.trim() === '') {
24
- throw new ConfigValidationError('ProjectConfig: name must not be empty');
25
- }
26
- const sourceLanguages = props.languages ?? ['typescript'];
27
- if (sourceLanguages.length === 0) {
28
- throw new ConfigValidationError('ProjectConfig: languages must not be empty');
29
+ if (!props.name || props.name.trim() === "") {
30
+ throw new ConfigValidationError("ProjectConfig: name must not be empty");
29
31
  }
30
- const normalizedLanguages = sourceLanguages.map((language) => language.trim()).filter((language) => language.length > 0);
31
- if (normalizedLanguages.length !== sourceLanguages.length) {
32
- throw new ConfigValidationError('ProjectConfig: languages must contain non-empty strings');
32
+ if (props.languages !== undefined) {
33
+ if (props.languages.length === 0) {
34
+ throw new ConfigValidationError("ProjectConfig: languages must not be empty");
35
+ }
36
+ const normalizedLanguages = props.languages
37
+ .map((language) => language.trim())
38
+ .filter((language) => language.length > 0);
39
+ if (normalizedLanguages.length !== props.languages.length) {
40
+ throw new ConfigValidationError("ProjectConfig: languages must contain non-empty strings");
41
+ }
42
+ this.languages = Object.freeze([...normalizedLanguages]);
33
43
  }
34
44
  this.name = props.name;
35
45
  this.preset = props.preset;
36
- this.languages = Object.freeze([...normalizedLanguages]);
37
46
  Object.freeze(this);
38
47
  }
39
48
 
40
49
  static create(raw: { name: string; preset: string; languages?: readonly string[] }): ProjectConfig {
41
50
  const preset = Preset.create(raw.preset);
42
- return new ProjectConfig({ name: raw.name, preset, languages: raw.languages ?? ['typescript'] });
51
+ return new ProjectConfig({ name: raw.name, preset, languages: raw.languages });
43
52
  }
44
53
 
45
54
  rename(name: string): ProjectConfig {
@@ -51,9 +60,16 @@ export class ProjectConfig {
51
60
  }
52
61
 
53
62
  equals(other: ProjectConfig): boolean {
54
- return this.name === other.name
55
- && this.preset.equals(other.preset)
56
- && this.languages.length === other.languages.length
57
- && this.languages.every((language, index) => language === other.languages[index]);
63
+ return this.name === other.name && this.preset.equals(other.preset) && this.languagesEqual(other.languages);
64
+ }
65
+
66
+ private languagesEqual(otherLanguages: readonly string[] | undefined): boolean {
67
+ if (this.languages === undefined || otherLanguages === undefined) {
68
+ return this.languages === otherLanguages;
69
+ }
70
+ return (
71
+ this.languages.length === otherLanguages.length &&
72
+ this.languages.every((language, index) => language === otherLanguages[index])
73
+ );
58
74
  }
59
75
  }