phasegate 0.315.0 → 0.340.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 (106) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.ja.md +22 -9
  3. package/README.md +28 -17
  4. package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
  5. package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  6. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  7. package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
  8. package/docs/guide/antigravity-integration.md +26 -0
  9. package/docs/guide/cli-reference.md +93 -11
  10. package/docs/guide/codex-integration.md +44 -107
  11. package/docs/guide/configuration.md +43 -2
  12. package/docs/guide/grok-integration.md +26 -0
  13. package/docs/guide/hooks-integration.md +27 -4
  14. package/docs/guide/installation.md +4 -2
  15. package/docs/guide/quick-vs-full-mode.md +36 -1
  16. package/docs/guide/setup-artifacts.md +4 -4
  17. package/docs/guide/skills-overview.md +1 -1
  18. package/package.json +2 -2
  19. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -2
  20. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  21. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +17 -9
  22. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  23. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  24. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +196 -54
  25. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
  26. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +39 -17
  27. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  28. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  29. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
  30. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +129 -73
  31. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  32. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  33. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  34. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  35. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  36. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  37. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  38. package/scripts/harness/ci-governance/composition-root.ts +55 -3
  39. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  40. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  41. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  42. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  43. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  44. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  45. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  46. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  47. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  48. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  49. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  50. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  51. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  52. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
  53. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
  54. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
  55. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  56. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  57. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  58. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  59. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  60. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  61. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  62. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  63. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  64. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  65. package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
  66. package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
  67. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  68. package/scripts/harness/installation/composition-root.ts +10 -0
  69. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  70. package/scripts/harness/installation/domain/check-id.ts +5 -0
  71. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  72. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  73. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  74. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  75. package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
  76. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  77. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  78. package/scripts/harness/main.ts +202 -51
  79. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  80. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  81. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
  82. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
  83. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  84. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  85. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  86. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +59 -19
  87. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  88. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  89. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  90. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  91. package/scripts/harness/quick-mode/index.ts +7 -1
  92. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  93. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  94. package/skills/product-architect/SKILL.md +19 -0
  95. package/skills/story-mapper/SKILL.md +11 -0
  96. package/skills/story-writer/SKILL.md +11 -0
  97. package/skills/unit-designer/SKILL.md +11 -0
  98. package/templates/.agents/hooks.json +16 -0
  99. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  100. package/templates/.claude/settings.json +5 -3
  101. package/templates/.codex/hooks.json +2 -2
  102. package/templates/product_overview.template.md +85 -0
  103. package/templates/product_overview_plan.template.md +55 -0
  104. package/templates/story_mapping_plan.template.md +61 -0
  105. package/templates/story_writer_plan.template.md +61 -0
  106. package/templates/unit_design_plan.template.md +63 -0
@@ -14,7 +14,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
14
14
 
15
15
  | Class | Examples | Owner | Lifecycle |
16
16
  |---|---|---|---|
17
- | Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, root `skills/` bundled skill bodies, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, shared skill body, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
17
+ | Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `.agents/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, root `skills/` bundled skill bodies, `.claude/skills`, `.codex/skills`, `.agents/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, shared skill body, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
18
18
  | Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init` or project install when absent; `install` may merge scripts/devDependency into `package.json` |
19
19
  | Generated artifact | `.phasegate/manifest.json`, `.phasegate/backups/*`, `.phasegate/uninstalled-*.json`, `.phasegate/baseline.json` | PhaseGate | Written by lifecycle commands and validators; safe to regenerate only through the owning command |
20
20
  | Runtime state/report | `.phasegate/hook-skip-events.jsonl`, explicit `doctor --report-out <path>` output, `reports/regression/*`, resolved `reporting.outputDir` reports | PhaseGate command output | Produced while hooks, doctor, and validation commands run |
@@ -26,11 +26,11 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
26
26
 
27
27
  `install --apply` and `reconcile --apply` manage only explicit targets. The current structured lifecycle covers:
28
28
 
29
- - Agent hook JSON: `.claude/settings.json`, `.codex/hooks.json`
29
+ - Agent hook JSON: `.claude/settings.json`, `.codex/hooks.json`, `.agents/hooks.json`
30
30
  - Agent context files: `CLAUDE.md` and `AGENTS.md` managed sections. `AGENT.md` singular is not a PhaseGate managed target; treat it as user-owned content or migrate it manually. <!-- @work-item-id WI-174 -->
31
31
  - Husky scripts when requested: `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`
32
32
  - CI workflow when requested: `.github/workflows/phasegate-aidlc-gate.yml`
33
- - Agent skill links: `.claude/skills`, `.codex/skills`
33
+ - Agent skill links: `.claude/skills`, `.codex/skills`, `.agents/skills`
34
34
  - Project shared bundled skills: selected `skills/<name>/` directories plus `skills/.harness-version`
35
35
  - Project config: `phasegate.config.json` when absent, so installed agent hooks have a discoverable runtime config
36
36
  - Package metadata: PhaseGate scripts and `devDependencies.phasegate` in `package.json`
@@ -48,7 +48,7 @@ If `.claude/*` or `.codex/*` already exists and is not a PhaseGate-managed perso
48
48
 
49
49
  Personal agent context is also placed only where the runtime will read it. Claude Code uses `.claude/CLAUDE.md`. Codex uses root `AGENTS.md` when PhaseGate can create or manage it locally; if a team `AGENTS.md` already exists, PhaseGate leaves it unchanged and doctor reports `codex-context-missing` instead of creating `AGENTS.override.md`. <!-- @work-item-id WI-215 -->
50
50
 
51
- Project install uses a different topology: selected bundled skills are deployed once to root `skills/`, and `.claude/skills` / `.codex/skills` point to that shared target. Existing root `skills/` is also a mergeable catalog: selected bundled skill directories are refreshed, selection-excluded or user-owned skills are not removed, and uninstall deletes only manifest-managed bundled skills plus metadata. If an older project install has the links but an empty `skills/` target, `phasegate doctor` reports the selected agent skill check and `phasegate reconcile --apply` repairs the shared skill bodies. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-216 -->
51
+ Project install uses a different topology: selected bundled skills are deployed once to root `skills/`, and `.claude/skills` / `.codex/skills` / `.agents/skills` point to that shared target. Existing root `skills/` is also a mergeable catalog: selected bundled skill directories are refreshed, selection-excluded or user-owned skills are not removed, and uninstall deletes only manifest-managed bundled skills plus metadata. If an older project install has the links but an empty `skills/` target, `phasegate doctor` reports the selected agent skill check and `phasegate reconcile --apply` repairs the shared skill bodies. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-216 -->
52
52
 
53
53
  ## Doctor Findings
54
54
 
@@ -1,6 +1,6 @@
1
1
  # Skills Overview
2
2
 
3
- Phasegate provides 29 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills/` / `.codex/skills/` links. Personal install instead writes real local-only per-agent skill directories. <!-- @work-item-id WI-210 -->
3
+ Phasegate provides 29 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills/`, `.codex/skills/`, or `.agents/skills/` links. Personal install instead writes real local-only per-agent skill directories. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-385 -->
4
4
 
5
5
  Bundled `SKILL.md` files include `languages: [typescript]` frontmatter so PhaseGate can distinguish current TypeScript-oriented guidance from future language-specific skill variants. The metadata is advisory for applicability and does not prevent non-TypeScript projects from installing the catalog. <!-- @work-item-id WI-212 -->
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.315.0",
3
+ "version": "0.340.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",
@@ -60,7 +60,7 @@
60
60
  "harness:check-phase": "pnpm phasegate:check-phase",
61
61
  "harness:check-ready": "pnpm phasegate:check-ready",
62
62
  "test": "vitest run --config scripts/harness/__tests__/vitest.config.forks.ts && vitest run --config scripts/harness/__tests__/vitest.config.ts",
63
- "coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && vitest run --config scripts/harness/__tests__/vitest.config.coverage.forks.ts --reporter=blob --outputFile=coverage/.blob/forks.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --reporter=blob --outputFile=coverage/.blob/threads.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --merge-reports=coverage/.blob",
63
+ "coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && vitest run --config scripts/harness/__tests__/vitest.config.coverage.forks.ts --reporter=blob --reporter=default --outputFile.blob=coverage/.blob/forks.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --reporter=blob --reporter=default --outputFile.blob=coverage/.blob/threads.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --merge-reports=coverage/.blob",
64
64
  "ci": "npm run coverage && npm run test"
65
65
  },
66
66
  "dependencies": {
@@ -3,10 +3,14 @@
3
3
  * @unit agent-integration
4
4
  */
5
5
 
6
+ /**
7
+ * WI-376 (ADR-039): 呼び出し元 skill 名を受け取るフィールドは持たない。
8
+ * 判定・案内の入力は hook が自ら観測・検証できる state(対象パス、変更カテゴリ、
9
+ * session marker、設計文書の存在、解決済み config 等)に限る。
10
+ */
6
11
  export interface HandlePreToolUseInput {
7
12
  toolName: string;
8
13
  targetFilePaths: string[];
9
- callerSkill?: string;
10
14
  targetChanges?: {
11
15
  filePath: string;
12
16
  beforeContent?: string | null;
@@ -22,7 +26,7 @@ export interface HandlePreToolUseOutput {
22
26
  phaseGateBlockers?: string[];
23
27
  storyReflectionBlockers?: string[];
24
28
  storyReflectionWarnings?: string[];
25
- fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
29
+ fullModeRejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
26
30
  fullModeDominantCategory?: string;
27
31
  nextAction?: string;
28
32
  // Quick Mode が write を許可した際に dominant カテゴリを presentation 層に伝える。
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @unit agent-integration
3
+ * @layer application
4
+ * @work-item-id WI-385
5
+ */
6
+
7
+ export type PayloadShape = "FLAT_SNAKE_CASE" | "FLAT_CAMEL_CASE" | "NESTED_TOOL_CALL";
8
+
9
+ export type HookResponseProfile = "LEGACY_EXIT_ONLY" | "COMPATIBILITY_DENY_ENVELOPE" | "TOP_LEVEL_DENY_ENVELOPE";
10
+
11
+ export interface CanonicalPreToolUseInput {
12
+ readonly path?: string;
13
+ readonly file_path?: string;
14
+ readonly paths?: readonly string[];
15
+ readonly command?: string;
16
+ readonly content?: string;
17
+ readonly old_string?: string;
18
+ readonly new_string?: string;
19
+ }
20
+
21
+ export interface NormalizedPreToolUseRequest {
22
+ readonly shape: PayloadShape;
23
+ readonly responseProfile: HookResponseProfile;
24
+ readonly cwd?: string;
25
+ readonly toolName: string;
26
+ readonly toolInput: CanonicalPreToolUseInput;
27
+ readonly inputTruncated: boolean;
28
+ }
29
+
30
+ export type PreToolUseNormalizationResult =
31
+ | { readonly ok: true; readonly request: NormalizedPreToolUseRequest }
32
+ | { readonly ok: false; readonly reason: string; readonly responseProfile?: HookResponseProfile };
@@ -8,6 +8,7 @@
8
8
  * @work-item-id WI-214
9
9
  * @work-item-id WI-349
10
10
  * @work-item-id WI-354
11
+ * @work-item-id WI-376
11
12
  *
12
13
  * HandlePreToolUseUseCase
13
14
  * PreToolUse Hook処理のオーケストレーション
@@ -187,7 +188,6 @@ export class HandlePreToolUseUseCase {
187
188
  fullModeResult,
188
189
  guidance,
189
190
  unitIdForGuidance,
190
- input.callerSkill,
191
191
  sessionResult,
192
192
  );
193
193
  }
@@ -278,13 +278,12 @@ export class HandlePreToolUseUseCase {
278
278
  blockedFilePath: string | undefined,
279
279
  result: {
280
280
  requiresFullMode: boolean;
281
- rejectionRule?: "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
281
+ rejectionRule?: "CATEGORY_NOT_ALLOWED" | "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
282
282
  rejectionReason?: string;
283
283
  dominantCategory?: string;
284
284
  },
285
285
  guidance: ErrorGuidance | null,
286
286
  unitId: string | undefined,
287
- callerSkill?: string,
288
287
  sessionResult?: FullModeSessionQueryResult,
289
288
  ): HandlePreToolUseOutput {
290
289
  const fp = blockedFilePath ?? "不明なファイル";
@@ -311,7 +310,7 @@ export class HandlePreToolUseUseCase {
311
310
  nextAction: `${dryRunCommand} && ${applyCommand}`,
312
311
  };
313
312
  }
314
- if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory, callerSkill)) {
313
+ if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory)) {
315
314
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
316
315
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
317
316
  const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
@@ -387,15 +386,18 @@ export class HandlePreToolUseUseCase {
387
386
  * 実運用で設定されず、この分岐は到達不能だった。カテゴリを一次条件にして
388
387
  * skill context なしでも実用的な復旧手順を出す。
389
388
  * feature / domain / api は従来どおり /story-implementor 誘導を維持する。
389
+ *
390
+ * WI-376 (ADR-039): 判定入力は hook が自ら観測できる state のみとする方針に従い、
391
+ * カテゴリ未確定時の callerSkill フォールバックを削除した。カテゴリが取れない場合は
392
+ * 一律 /story-implementor 誘導(従来のフォールバックも実運用では常にこの経路だった)。
390
393
  */
391
394
  private static readonly QUICK_MODE_SCOPE_CATEGORIES: readonly string[] = ["bugfix", "docs", "test", "config"];
392
395
 
393
- private static shouldGuideQuickModeRelax(dominantCategory: string | undefined, callerSkill?: string): boolean {
394
- if (dominantCategory !== undefined) {
395
- return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
396
+ private static shouldGuideQuickModeRelax(dominantCategory: string | undefined): boolean {
397
+ if (dominantCategory === undefined) {
398
+ return false;
396
399
  }
397
- // カテゴリ不明時は skill context だけが手掛かり
398
- return callerSkill === "quick-implementor";
400
+ return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
399
401
  }
400
402
 
401
403
  /**
@@ -575,6 +577,12 @@ export class HandlePreToolUseUseCase {
575
577
  message: (fp) =>
576
578
  `保護ファイルへの書き込みがブロックされました: ${fp}\nClaude Code の設定変更は /update-config スキルを使用してください。`,
577
579
  },
580
+ {
581
+ // WI-363: .husky/ 配下は L0 runtime の実施点。Quick Mode でも書き換えさせない。
582
+ pattern: /(?:^|\/)\.husky\//,
583
+ message: (fp) =>
584
+ `保護ファイルへの書き込みがブロックされました: ${fp}\n.husky/ 配下は L0 runtime(pre-commit / commit-msg / pre-push)の実施点であり、Quick Mode でも変更できません。\n未導入 hook の配置は npx phasegate setup:agent --apply --with-husky を使用してください。\n意図的な手編集が必要な場合は、agent セッション外で人間が変更し、通常の検証を実行してください。`,
585
+ },
578
586
  ];
579
587
 
580
588
  private async buildProtectedFileBlockOutput(blockedFilePath: string | undefined): Promise<HandlePreToolUseOutput> {
@@ -1,15 +1,17 @@
1
1
  // @unit agent-integration
2
2
  // @layer domain
3
+ // @work-item-id WI-384
3
4
 
4
5
  export interface FullModeRequirementQueryResult {
5
6
  readonly requiresFullMode: boolean;
6
- readonly rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
7
+ readonly rejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
7
8
  readonly rejectionReason?: string;
8
9
  readonly dominantCategory?: string;
9
10
  }
10
11
 
11
12
  export interface FullModeTargetChange {
12
13
  readonly filePath: string;
14
+ readonly changeKind?: 'CREATE' | 'MODIFY' | 'DELETE';
13
15
  readonly beforeContent?: string | null;
14
16
  readonly afterContent?: string | null;
15
17
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit agent-integration
4
+ * @work-item-id WI-384
5
+ *
6
+ * Codex apply_patch の raw patch から書き込み対象と変更種別を抽出する。
7
+ */
8
+
9
+ export type PatchChangeKind = "CREATE" | "MODIFY" | "DELETE";
10
+
11
+ export interface PatchWriteTarget {
12
+ readonly filePath: string;
13
+ readonly changeKind: PatchChangeKind;
14
+ }
15
+
16
+ const APPLY_PATCH_BEGIN_SOURCE = String.raw`\*\*\*\s+Begin\s+Patch`;
17
+ const APPLY_PATCH_END_SOURCE = String.raw`\*\*\*\s+End\s+Patch`;
18
+ const APPLY_PATCH_FILE_LINE_SOURCE = String.raw`^\*\*\*\s+(Update|Add|Delete)\s+File:\s*(.+?)\s*$`;
19
+ const APPLY_PATCH_MOVE_LINE_SOURCE = String.raw`^\*\*\*\s+Move\s+to:\s*(.+?)\s*$`;
20
+
21
+ const CHANGE_KIND_BY_DIRECTIVE = Object.freeze({
22
+ Add: "CREATE",
23
+ Update: "MODIFY",
24
+ Delete: "DELETE",
25
+ } as const satisfies Readonly<Record<string, PatchChangeKind>>);
26
+
27
+ const EMPTY_TARGETS: readonly PatchWriteTarget[] = Object.freeze([]);
28
+
29
+ export class ApplyPatchWriteTargetExtractor {
30
+ extract(rawPatch: string): readonly PatchWriteTarget[] {
31
+ if (typeof rawPatch !== "string" || rawPatch.length === 0) {
32
+ return EMPTY_TARGETS;
33
+ }
34
+
35
+ const beginGlobal = new RegExp(APPLY_PATCH_BEGIN_SOURCE, "g");
36
+ const beginStarts: number[] = [];
37
+ let match = beginGlobal.exec(rawPatch);
38
+ while (match !== null) {
39
+ beginStarts.push(match.index + match[0].length);
40
+ match = beginGlobal.exec(rawPatch);
41
+ }
42
+ if (beginStarts.length === 0) {
43
+ return EMPTY_TARGETS;
44
+ }
45
+
46
+ const endGlobal = new RegExp(APPLY_PATCH_END_SOURCE, "g");
47
+ const endStarts: number[] = [];
48
+ match = endGlobal.exec(rawPatch);
49
+ while (match !== null) {
50
+ endStarts.push(match.index);
51
+ match = endGlobal.exec(rawPatch);
52
+ }
53
+
54
+ const targets: PatchWriteTarget[] = [];
55
+ const seen = new Set<string>();
56
+ for (let index = 0; index < beginStarts.length; index += 1) {
57
+ const start = beginStarts[index];
58
+ const nextBegin = index + 1 < beginStarts.length ? beginStarts[index + 1] : rawPatch.length;
59
+ const endInRange = endStarts.find((candidate) => candidate > start && candidate <= nextBegin);
60
+ const body = rawPatch.slice(start, endInRange ?? nextBegin);
61
+ let acceptsMoveDestination = false;
62
+
63
+ for (const line of body.split(/\r?\n/)) {
64
+ const fileMatch = new RegExp(APPLY_PATCH_FILE_LINE_SOURCE).exec(line);
65
+ if (fileMatch !== null) {
66
+ const directive = fileMatch[1] as keyof typeof CHANGE_KIND_BY_DIRECTIVE;
67
+ const filePath = fileMatch[2].trim();
68
+ acceptsMoveDestination = directive === "Update";
69
+ if (filePath.length === 0) continue;
70
+ const changeKind = CHANGE_KIND_BY_DIRECTIVE[directive];
71
+ this.appendTarget(targets, seen, filePath, changeKind);
72
+ continue;
73
+ }
74
+
75
+ const moveMatch = acceptsMoveDestination ? new RegExp(APPLY_PATCH_MOVE_LINE_SOURCE).exec(line) : null;
76
+ acceptsMoveDestination = false;
77
+ if (moveMatch === null) continue;
78
+ const filePath = moveMatch[1].trim();
79
+ if (filePath.length === 0) continue;
80
+ this.appendTarget(targets, seen, filePath, "CREATE");
81
+ }
82
+ }
83
+
84
+ return targets.length === 0 ? EMPTY_TARGETS : Object.freeze(targets);
85
+ }
86
+
87
+ private appendTarget(
88
+ targets: PatchWriteTarget[],
89
+ seen: Set<string>,
90
+ filePath: string,
91
+ changeKind: PatchChangeKind,
92
+ ): void {
93
+ const key = `${changeKind}\u0000${filePath}`;
94
+ if (seen.has(key)) return;
95
+ seen.add(key);
96
+ targets.push(Object.freeze({ filePath, changeKind }));
97
+ }
98
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-384
4
5
  *
5
6
  * BashWriteTargetExtractor ドメインサービス
6
7
  *
@@ -9,7 +10,8 @@
9
10
  *
10
11
  * 副作用なし・純粋関数相当。対応パターンは以下:
11
12
  * - リダイレクト `>` / `>>` / `>|` (clobber)
12
- * - heredoc (`<<EOF > path`)
13
+ * - heredoc (`<<EOF > path`) — ヒアドキュメント**本文**はシェル構文解析の対象外
14
+ * (本文中の `<...>` をリダイレクトと誤認しないため。WI-362)
13
15
  * - `tee` / `tee -a`
14
16
  * - `sed -i` / `sed -i ''` (BSD)
15
17
  * - `cp` / `mv` (宛先のみ)
@@ -24,6 +26,8 @@
24
26
  * `*** Update|Add|Delete File: <path>` 行) — Codex CLI 対応 (ISSUE-013 Wave 1)
25
27
  */
26
28
 
29
+ import { ApplyPatchWriteTargetExtractor } from './apply-patch-write-target-extractor.js';
30
+
27
31
  /** 引数トークン (値とクォート種別) */
28
32
  type Token = {
29
33
  readonly value: string;
@@ -111,6 +115,191 @@ function tokenize(input: string): Token[] {
111
115
  return tokens;
112
116
  }
113
117
 
118
+ /** ヒアドキュメント開始演算子で宣言された終端デリミタ */
119
+ type HeredocDelimiter = {
120
+ /** クォート・エスケープを解除した終端語 */
121
+ readonly word: string;
122
+ /** `<<-` 形式か (終端行の先頭タブを無視する) */
123
+ readonly allowIndent: boolean;
124
+ };
125
+
126
+ /** デリミタ語の終端となる文字 (空白およびシェル演算子) */
127
+ const DELIMITER_TERMINATORS = new Set([' ', '\t', ';', '&', '|', '<', '>', '(', ')']);
128
+
129
+ /**
130
+ * 1 行から ヒアドキュメント開始演算子 (`<<WORD` / `<<-WORD` / `<<'WORD'`) を検出し、
131
+ * 宣言順に終端デリミタを返す。
132
+ *
133
+ * クォート状態を追跡し、クォート内の `<<` は演算子として扱わない
134
+ * (例: `echo "a << b" > out.txt` の `<<` はヒアドキュメントではない)。
135
+ * `$( ... )` コマンド置換の内側ではクォート文脈がリセットされるため、
136
+ * `git commit -m "$(cat <<'EOF'` のような入れ子も検出できる。
137
+ * ヒアストリング `<<<` は本文を持たないため対象外。
138
+ */
139
+ function findHeredocDelimiters(line: string): HeredocDelimiter[] {
140
+ const found: HeredocDelimiter[] = [];
141
+ const substitutionStack: ('none' | 'single' | 'double')[] = [];
142
+ let quote: 'none' | 'single' | 'double' = 'none';
143
+ let i = 0;
144
+
145
+ while (i < line.length) {
146
+ const ch = line[i];
147
+
148
+ if (quote === 'single') {
149
+ if (ch === "'") quote = 'none';
150
+ i += 1;
151
+ continue;
152
+ }
153
+
154
+ if (ch === '\\') {
155
+ i += 2;
156
+ continue;
157
+ }
158
+
159
+ if (ch === '$' && line[i + 1] === '(') {
160
+ substitutionStack.push(quote);
161
+ quote = 'none';
162
+ i += 2;
163
+ continue;
164
+ }
165
+
166
+ if (quote === 'double') {
167
+ if (ch === '"') quote = 'none';
168
+ i += 1;
169
+ continue;
170
+ }
171
+
172
+ if (ch === "'") {
173
+ quote = 'single';
174
+ i += 1;
175
+ continue;
176
+ }
177
+
178
+ if (ch === '"') {
179
+ quote = 'double';
180
+ i += 1;
181
+ continue;
182
+ }
183
+
184
+ if (ch === ')' && substitutionStack.length > 0) {
185
+ quote = substitutionStack.pop() ?? 'none';
186
+ i += 1;
187
+ continue;
188
+ }
189
+
190
+ if (ch === '<' && line[i + 1] === '<') {
191
+ if (line[i + 2] === '<') {
192
+ // ヒアストリング `<<<` は本文行を持たない
193
+ i += 3;
194
+ continue;
195
+ }
196
+ i += 2;
197
+ let allowIndent = false;
198
+ if (line[i] === '-') {
199
+ allowIndent = true;
200
+ i += 1;
201
+ }
202
+ while (i < line.length && (line[i] === ' ' || line[i] === '\t')) i += 1;
203
+ const word = readHeredocDelimiterWord(line, i);
204
+ i = word.nextIndex;
205
+ if (word.value.length > 0) {
206
+ found.push({ word: word.value, allowIndent });
207
+ }
208
+ continue;
209
+ }
210
+
211
+ i += 1;
212
+ }
213
+
214
+ return found;
215
+ }
216
+
217
+ /** `<<` 直後のデリミタ語を読む。クォート/バックスラッシュを解除した値を返す。 */
218
+ function readHeredocDelimiterWord(line: string, start: number): { value: string; nextIndex: number } {
219
+ let value = '';
220
+ let i = start;
221
+
222
+ while (i < line.length) {
223
+ const ch = line[i];
224
+
225
+ if (ch === "'") {
226
+ i += 1;
227
+ while (i < line.length && line[i] !== "'") {
228
+ value += line[i];
229
+ i += 1;
230
+ }
231
+ i += 1;
232
+ continue;
233
+ }
234
+
235
+ if (ch === '"') {
236
+ i += 1;
237
+ while (i < line.length && line[i] !== '"') {
238
+ if (line[i] === '\\' && i + 1 < line.length) {
239
+ value += line[i + 1];
240
+ i += 2;
241
+ continue;
242
+ }
243
+ value += line[i];
244
+ i += 1;
245
+ }
246
+ i += 1;
247
+ continue;
248
+ }
249
+
250
+ if (ch === '\\' && i + 1 < line.length) {
251
+ value += line[i + 1];
252
+ i += 2;
253
+ continue;
254
+ }
255
+
256
+ if (DELIMITER_TERMINATORS.has(ch)) break;
257
+
258
+ value += ch;
259
+ i += 1;
260
+ }
261
+
262
+ return { value, nextIndex: i };
263
+ }
264
+
265
+ /** 終端行判定 (`<<-` は先頭タブを無視し、行末の空白・CR は許容する) */
266
+ function isHeredocTerminator(line: string, delimiter: HeredocDelimiter): boolean {
267
+ const candidate = delimiter.allowIndent ? line.replace(/^\t+/, '') : line;
268
+ return candidate.trimEnd() === delimiter.word;
269
+ }
270
+
271
+ /**
272
+ * ヒアドキュメント本文行を除去する。
273
+ *
274
+ * 本文はシェル構文ではなくデータであり、`Co-Authored-By: Claude <noreply@example.com>`
275
+ * のような `<...>` をリダイレクトと誤解釈すると無関係なコマンドが誤ブロックされる (WI-362)。
276
+ * 除去対象は**本文行と終端行のみ**で、開始演算子のある行はそのまま残すため
277
+ * `cat <<EOF > path` の `> path` は従来どおり検出される (fail-closed 維持)。
278
+ */
279
+ function stripHeredocBodies(command: string): string {
280
+ if (!command.includes('<<')) return command;
281
+
282
+ const lines = command.split('\n');
283
+ const kept: string[] = [];
284
+ const pending: HeredocDelimiter[] = [];
285
+
286
+ for (const line of lines) {
287
+ if (pending.length > 0) {
288
+ // 本文行 / 終端行はいずれも構文解析対象から外す
289
+ if (isHeredocTerminator(line, pending[0])) {
290
+ pending.shift();
291
+ }
292
+ continue;
293
+ }
294
+ kept.push(line);
295
+ for (const delimiter of findHeredocDelimiters(line)) {
296
+ pending.push(delimiter);
297
+ }
298
+ }
299
+
300
+ return kept.join('\n');
301
+ }
302
+
114
303
  const OPERATOR_SEPARATORS = new Set(['&&', '||', ';', '|']);
115
304
 
116
305
  /** トークン列を複合・パイプ境界で分割 */
@@ -311,7 +500,9 @@ function collectPositionals(tokens: Token[]): Token[] {
311
500
 
312
501
  /** コマンド文字列を token 化 → 演算子分割 → 各コマンドから書き込み先を抽出する内部関数。 */
313
502
  function extractFromCommandString(command: string): string[] {
314
- const tokens = tokenize(command);
503
+ // ヒアドキュメント本文はデータなのでシェル構文解析の対象外にする (WI-362)
504
+ // apply_patch 抽出は元の command 文字列を直接走査するため影響を受けない。
505
+ const tokens = tokenize(stripHeredocBodies(command));
315
506
  const groups = splitByOperators(tokens);
316
507
  const collected: string[] = [];
317
508
  for (const group of groups) {
@@ -320,62 +511,13 @@ function extractFromCommandString(command: string): string[] {
320
511
  }
321
512
  }
322
513
  // ネスト内の apply_patch heredoc も拾う
323
- for (const p of extractApplyPatchTargets(command)) {
324
- collected.push(p);
514
+ const patchExtractor = new ApplyPatchWriteTargetExtractor();
515
+ for (const target of patchExtractor.extract(command)) {
516
+ collected.push(target.filePath);
325
517
  }
326
518
  return collected;
327
519
  }
328
520
 
329
- /** apply_patch ブロック境界マーカー (Begin/End) */
330
- const APPLY_PATCH_BEGIN_SOURCE = String.raw`\*\*\*\s+Begin\s+Patch`;
331
- const APPLY_PATCH_END_SOURCE = String.raw`\*\*\*\s+End\s+Patch`;
332
- /** ブロック内のファイル行: `*** (Update|Add|Delete) File: <path>` */
333
- const APPLY_PATCH_FILE_LINE_SOURCE = String.raw`^\s*\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+?)\s*$`;
334
-
335
- /**
336
- * apply_patch ヒアドキュメント構文から対象ファイルパスを抽出する。
337
- *
338
- * Codex CLI 等が採用する unified-diff 風パッチフォーマット:
339
- * *** Begin Patch
340
- * *** Update File: <path>
341
- * *** Add File: <path>
342
- * *** Delete File: <path>
343
- * *** End Patch
344
- *
345
- * `*** End Patch` が欠けている場合は command 末尾までをブロックとして扱う
346
- * (保護側に倒す — phase-gate の取りこぼしよりも誤検出のほうが許容される)。
347
- */
348
- function extractApplyPatchTargets(command: string): string[] {
349
- const beginGlobal = new RegExp(APPLY_PATCH_BEGIN_SOURCE, 'g');
350
- const beginStarts: number[] = [];
351
- let m: RegExpExecArray | null;
352
- while ((m = beginGlobal.exec(command)) !== null) {
353
- beginStarts.push(m.index + m[0].length);
354
- }
355
- if (beginStarts.length === 0) return [];
356
-
357
- const endGlobal = new RegExp(APPLY_PATCH_END_SOURCE, 'g');
358
- const endStarts: number[] = [];
359
- while ((m = endGlobal.exec(command)) !== null) {
360
- endStarts.push(m.index);
361
- }
362
-
363
- const results: string[] = [];
364
- for (let i = 0; i < beginStarts.length; i += 1) {
365
- const start = beginStarts[i];
366
- const nextBegin = i + 1 < beginStarts.length ? beginStarts[i + 1] : command.length;
367
- const endInRange = endStarts.find((e) => e > start && e <= nextBegin);
368
- const end = endInRange ?? nextBegin;
369
- const body = command.slice(start, end);
370
-
371
- const fileRegex = new RegExp(APPLY_PATCH_FILE_LINE_SOURCE, 'gm');
372
- while ((m = fileRegex.exec(body)) !== null) {
373
- results.push(m[1]);
374
- }
375
- }
376
- return results;
377
- }
378
-
379
521
  export class BashWriteTargetExtractor {
380
522
  /**
381
523
  * Bash コマンド文字列から書き込み先ファイルパスを抽出する。
@@ -18,7 +18,7 @@ export interface BlockMetadata {
18
18
  readonly scopeLevel?: 1 | 2 | 3;
19
19
  readonly unitId?: string;
20
20
  readonly storyId?: string;
21
- readonly fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
21
+ readonly fullModeRejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
22
22
  readonly fullModeRejectionReason?: string;
23
23
  readonly fullModeDominantCategory?: string;
24
24
  }