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
@@ -27,6 +27,43 @@ Each finding has a severity, a repair mode, and optional next-step fields.
27
27
  | `ci-workflow-missing` | `npx phasegate setup:agent --intent ci-only --with-ci --dry-run --json` |
28
28
  | agent context drift | `npx phasegate reconcile --dry-run` |
29
29
 
30
+ ## Phase Gate Blocked a Write and I Do Not Know What the Document Should Contain
31
+
32
+ <!-- @work-item-id WI-356 -->
33
+
34
+ Phase Gate blocks (`L2-001`, `FULL_MODE_REQUIRED`) mean a required inception or product design document is missing. Two things are needed: the file skeleton, and the section structure to fill in.
35
+
36
+ **1. Generate the skeleton.** `scaffold-design` writes a placeholder document for the missing phase:
37
+
38
+ ```bash
39
+ npx phasegate scaffold-design --unit <unit-id> --phase logical --dry-run
40
+ npx phasegate scaffold-design --unit <unit-id> --phase logical --apply
41
+ ```
42
+
43
+ Accepted `--phase` values: `logical`, `domain`, `uiux`, `unit-test`, `it-test`.
44
+
45
+ **2. Read the section structure from the skill definition.** PhaseGate does not ship fill-in templates into your repo; the authoritative structure of every plan and inception artifact lives in the skill markdown that produces it. `install` / `init` copy these into `skills/` in your repository:
46
+
47
+ | Document | Skill definition |
48
+ |---|---|
49
+ | product architecture / unit decomposition | `skills/product-architect/SKILL.md` |
50
+ | story map | `skills/story-mapper/SKILL.md` |
51
+ | story descriptions / acceptance criteria | `skills/story-writer/SKILL.md` |
52
+ | unit plan | `skills/unit-designer/SKILL.md` |
53
+ | `logical_design.md` | `skills/logical-designer/SKILL.md` |
54
+ | `domain_model.md` | `skills/domain-designer/SKILL.md` |
55
+ | UI/UX design | `skills/uiux-designer/SKILL.md` |
56
+ | unit / IT / scenario test plans | `skills/unit-test-designer/SKILL.md`, `skills/it-test-designer/SKILL.md`, `skills/scenario-test-designer/SKILL.md` |
57
+
58
+ If `skills/` is not present in your repository — the personal install path does not copy it — read the same content from PhaseGate's own package instead:
59
+
60
+ ```bash
61
+ npx phasegate skills list
62
+ npx phasegate skills info logical-designer
63
+ ```
64
+
65
+ `skills info` prints the full SKILL.md to stdout, so it works from a sandboxed agent that cannot read `node_modules/`.
66
+
30
67
  ## Refused Managed Target
31
68
 
32
69
  If `install`, `reconcile`, or `uninstall` refuses a target, do not immediately force it. Inspect the diff and ask the agent to explain:
@@ -1,6 +1,6 @@
1
1
  # CLAUDE.md
2
2
 
3
- <!-- @work-item-id WI-174, WI-176, WI-177 -->
3
+ <!-- @work-item-id WI-174, WI-176, WI-177, WI-331 -->
4
4
 
5
5
  <!-- phasegate:managed-section:start -->
6
6
  ## 必読ドキュメント
@@ -38,13 +38,13 @@ If `setup:agent --apply --json` or `install --apply --json` fails with a structu
38
38
 
39
39
  {{PHASEGATE_SKILLS}}
40
40
 
41
+ ## Agent Context Refresh
42
+
43
+ Run `phasegate ci:auto-refresh-agent-context --dry-run` to preview updates and `phasegate ci:auto-refresh-agent-context --apply` to write AGENTS.md / CLAUDE.md.
44
+ <!-- phasegate:managed-section:end -->
45
+
41
46
  ## User Section
42
47
 
43
48
  <!-- phasegate:user-section:start -->
44
49
  {{PHASEGATE_USER_SECTION}}
45
50
  <!-- phasegate:user-section:end -->
46
-
47
- ## Agent Context Refresh
48
-
49
- Run `phasegate ci:auto-refresh-agent-context --dry-run` to preview updates and `phasegate ci:auto-refresh-agent-context --apply` to write AGENTS.md / CLAUDE.md.
50
- <!-- phasegate:managed-section:end -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.283.0",
3
+ "version": "0.335.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;
@@ -6,6 +6,9 @@
6
6
  * @work-item-id WI-202 / WI-204
7
7
  * @work-item-id WI-206
8
8
  * @work-item-id WI-214
9
+ * @work-item-id WI-349
10
+ * @work-item-id WI-354
11
+ * @work-item-id WI-376
9
12
  *
10
13
  * HandlePreToolUseUseCase
11
14
  * PreToolUse Hook処理のオーケストレーション
@@ -17,11 +20,11 @@ import type {
17
20
  } from "../../domain/ports/baseline-grandfather-query-port.js";
18
21
  import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
19
22
  import type { ErrorGuidance, ErrorGuidanceQueryPort } from "../../domain/ports/error-guidance-query-port.js";
23
+ import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
20
24
  import type {
21
25
  FullModeSessionQueryPort,
22
26
  FullModeSessionQueryResult,
23
27
  } from "../../domain/ports/full-mode-session-query-port.js";
24
- import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
25
28
  import type { PhaseGateQueryPort } from "../../domain/ports/phase-gate-query-port.js";
26
29
  import type { StoryReflectionQueryPort } from "../../domain/ports/story-reflection-query-port.js";
27
30
  import { AsyncHookToCliTranslator } from "../../domain/services/hook-to-cli-translator.js";
@@ -185,7 +188,7 @@ export class HandlePreToolUseUseCase {
185
188
  fullModeResult,
186
189
  guidance,
187
190
  unitIdForGuidance,
188
- input.callerSkill,
191
+ sessionResult,
189
192
  );
190
193
  }
191
194
  } else {
@@ -281,22 +284,20 @@ export class HandlePreToolUseUseCase {
281
284
  },
282
285
  guidance: ErrorGuidance | null,
283
286
  unitId: string | undefined,
284
- callerSkill?: string,
287
+ sessionResult?: FullModeSessionQueryResult,
285
288
  ): HandlePreToolUseOutput {
286
289
  const fp = blockedFilePath ?? "不明なファイル";
287
290
  if (result.dominantCategory === "config" && /(?:^|\/)phasegate\.config\.json$/.test(fp)) {
288
291
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
289
292
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
290
- const lines = [
291
- `Full mode 必須変更が検出されました: ${fp}`,
292
- "カテゴリ: config",
293
- ];
293
+ const lines = [`Full mode 必須変更が検出されました: ${fp}`, "カテゴリ: config"];
294
294
  if (result.rejectionRule) {
295
295
  lines.push(`判定ルール: ${result.rejectionRule}`);
296
296
  }
297
297
  if (result.rejectionReason) {
298
298
  lines.push(`理由: ${result.rejectionReason}`);
299
299
  }
300
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
300
301
  lines.push(`次のアクション: ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
301
302
 
302
303
  return {
@@ -309,22 +310,24 @@ export class HandlePreToolUseUseCase {
309
310
  nextAction: `${dryRunCommand} && ${applyCommand}`,
310
311
  };
311
312
  }
312
- if (
313
- callerSkill === "quick-implementor" &&
314
- result.dominantCategory !== undefined &&
315
- ["bugfix", "docs", "test", "config"].includes(result.dominantCategory)
316
- ) {
313
+ if (HandlePreToolUseUseCase.shouldGuideQuickModeRelax(result.dominantCategory)) {
317
314
  const dryRunCommand = "phasegate config:plan --intent quick-mode-relax --dry-run --json";
318
315
  const applyCommand = "phasegate config:plan --intent quick-mode-relax --apply --json";
319
316
  const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
320
- lines.push(`カテゴリ: ${result.dominantCategory}`);
317
+ if (result.dominantCategory) {
318
+ lines.push(`カテゴリ: ${result.dominantCategory}`);
319
+ }
321
320
  if (result.rejectionRule) {
322
321
  lines.push(`判定ルール: ${result.rejectionRule}`);
323
322
  }
324
323
  if (result.rejectionReason) {
325
324
  lines.push(`理由: ${result.rejectionReason}`);
326
325
  }
327
- lines.push(`次のアクション: Quick Mode の許可カテゴリを確認してください。緩和する場合は ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`);
326
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
327
+ lines.push(
328
+ `次のアクション: Quick Mode の許可カテゴリを確認してください。緩和する場合は ${dryRunCommand} で差分を確認し、承認後に ${applyCommand} を実行してください。`,
329
+ );
330
+ lines.push(` 分類の確認: npx phasegate check-change-category --paths ${fp}`);
328
331
 
329
332
  return {
330
333
  shouldBlock: true,
@@ -346,9 +349,15 @@ export class HandlePreToolUseUseCase {
346
349
  if (result.rejectionReason) {
347
350
  lines.push(`理由: ${result.rejectionReason}`);
348
351
  }
352
+ HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
349
353
  const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
350
354
  lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
351
355
  if (unitId !== undefined && unitId !== "") {
356
+ if (HandlePreToolUseUseCase.isSessionActiveButRejected(sessionResult)) {
357
+ lines.push(
358
+ ` 既存 session は上記理由で今回の書き込みに使えません。張り直す場合: phasegate session end --work-item ${sessionResult?.workItemId ?? "<WI-XXX>"} を実行してから下記を実行してください。`,
359
+ );
360
+ }
352
361
  lines.push(
353
362
  ` 実装フェーズ開始時: phasegate session begin --mode full --unit ${unitId} --work-item <WI-XXX> --reason "<reason>" --duration 1h`,
354
363
  );
@@ -367,6 +376,59 @@ export class HandlePreToolUseUseCase {
367
376
  };
368
377
  }
369
378
 
379
+ /**
380
+ * Quick Mode スコープのカテゴリ(bugfix / docs / test / config)は quick-implementor で
381
+ * 完遂できる変更であり、遮断の実体は「allowedCategories が絞られている」ことである。
382
+ * ここで /story-implementor を案内すると、設計フェーズからやり直せという誤った指示になる。
383
+ *
384
+ * WI-354: 従来は callerSkill === "quick-implementor" を条件にしていたが、
385
+ * callerSkill を供給する producer(hook input の caller_skill / PHASEGATE_CALLER_SKILL)は
386
+ * 実運用で設定されず、この分岐は到達不能だった。カテゴリを一次条件にして
387
+ * skill context なしでも実用的な復旧手順を出す。
388
+ * feature / domain / api は従来どおり /story-implementor 誘導を維持する。
389
+ *
390
+ * WI-376 (ADR-039): 判定入力は hook が自ら観測できる state のみとする方針に従い、
391
+ * カテゴリ未確定時の callerSkill フォールバックを削除した。カテゴリが取れない場合は
392
+ * 一律 /story-implementor 誘導(従来のフォールバックも実運用では常にこの経路だった)。
393
+ */
394
+ private static readonly QUICK_MODE_SCOPE_CATEGORIES: readonly string[] = ["bugfix", "docs", "test", "config"];
395
+
396
+ private static shouldGuideQuickModeRelax(dominantCategory: string | undefined): boolean {
397
+ if (dominantCategory === undefined) {
398
+ return false;
399
+ }
400
+ return HandlePreToolUseUseCase.QUICK_MODE_SCOPE_CATEGORIES.includes(dominantCategory);
401
+ }
402
+
403
+ /**
404
+ * WI-349: ブロック理由に判定根拠を明示する。
405
+ *
406
+ * - 判定対象は「今回の書き込み対象パス」だけであり、ワークツリー上の未コミット変更は
407
+ * 一切含まれない。この誤解が「無関係な変更のせいでブロックされている」という
408
+ * 誤った原因究明を招いていた(issue #41 症状②)。
409
+ * - アクティブな Full Mode session がありながら不許可だった場合は、その理由
410
+ * (unit 不一致 / category 不一致 / 期限切れ 等)を提示する。これがないと
411
+ * 「session begin せよ」と案内されながら session は既に有効、という最悪の混乱になる。
412
+ */
413
+ private static appendJudgmentContextLines(
414
+ lines: string[],
415
+ sessionResult: FullModeSessionQueryResult | undefined,
416
+ ): void {
417
+ lines.push("判定対象: 今回の書き込み対象パスのみです(ワークツリー上の他の未コミット変更は判定に含まれません)。");
418
+ if (!HandlePreToolUseUseCase.isSessionActiveButRejected(sessionResult)) {
419
+ return;
420
+ }
421
+ const workItemId = sessionResult?.workItemId ?? "<unknown>";
422
+ const unit = sessionResult?.unit ?? "<unknown>";
423
+ const expiresAt = sessionResult?.expiresAt ?? "<unknown>";
424
+ lines.push(`アクティブな Full Mode session: ${workItemId} (unit=${unit}, expiresAt=${expiresAt})`);
425
+ lines.push(`session が書き込みを許可しなかった理由: ${sessionResult?.reason ?? "<unknown>"}`);
426
+ }
427
+
428
+ private static isSessionActiveButRejected(sessionResult: FullModeSessionQueryResult | undefined): boolean {
429
+ return sessionResult !== undefined && sessionResult.active && !sessionResult.allowed;
430
+ }
431
+
370
432
  private static appendGuidanceLines(lines: string[], guidance: ErrorGuidance | null, unitId?: string): void {
371
433
  if (guidance === null) return;
372
434
  if (guidance.scaffoldCommand !== null) {
@@ -377,10 +439,25 @@ export class HandlePreToolUseUseCase {
377
439
  lines.push(` scaffold: ${command}`);
378
440
  }
379
441
  if (guidance.templatePath !== null) {
380
- lines.push(` テンプレ: ${guidance.templatePath}`);
442
+ lines.push(` 構成リファレンス: ${HandlePreToolUseUseCase.describeStructureReference(guidance.templatePath)}`);
381
443
  }
382
444
  }
383
445
 
446
+ /**
447
+ * WI-356 (issue #29): 参照先が skill 定義(skills/<name>/SKILL.md)の場合、
448
+ * skills/ をユーザー repo に配置しない personal install 経路でも読めるよう
449
+ * stdout 経路(phasegate skills info)を併記する。
450
+ */
451
+ private static readonly SKILL_REFERENCE_PATTERN = /^skills\/([^/]+)\/SKILL\.md$/;
452
+
453
+ private static describeStructureReference(templatePath: string): string {
454
+ const skillMatch = HandlePreToolUseUseCase.SKILL_REFERENCE_PATTERN.exec(templatePath);
455
+ if (skillMatch === null) {
456
+ return templatePath;
457
+ }
458
+ return `${templatePath}(未配置なら: npx phasegate skills info ${skillMatch[1]})`;
459
+ }
460
+
384
461
  private deriveUnitIdFromPaths(targetFilePaths: readonly string[]): string | undefined {
385
462
  const projectPaths = this.configQueryPort.getProjectPaths();
386
463
  for (const targetFilePath of targetFilePaths) {
@@ -419,8 +496,15 @@ export class HandlePreToolUseUseCase {
419
496
  }
420
497
 
421
498
  private static isUnderInception(targetFilePath: string, inceptionPath: string): boolean {
422
- const normalizedTarget = targetFilePath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
423
- const normalizedBase = inceptionPath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/").replace(/\/$/, "");
499
+ const normalizedTarget = targetFilePath
500
+ .replaceAll("\\", "/")
501
+ .replace(/^\.\/+/, "")
502
+ .replace(/\/+/g, "/");
503
+ const normalizedBase = inceptionPath
504
+ .replaceAll("\\", "/")
505
+ .replace(/^\.\/+/, "")
506
+ .replace(/\/+/g, "/")
507
+ .replace(/\/$/, "");
424
508
  return normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`);
425
509
  }
426
510
 
@@ -493,6 +577,12 @@ export class HandlePreToolUseUseCase {
493
577
  message: (fp) =>
494
578
  `保護ファイルへの書き込みがブロックされました: ${fp}\nClaude Code の設定変更は /update-config スキルを使用してください。`,
495
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意図的に手編集する場合は phasegate.config.json の protectedFiles.exclude に対象パターンを明示してください。`,
585
+ },
496
586
  ];
497
587
 
498
588
  private async buildProtectedFileBlockOutput(blockedFilePath: string | undefined): Promise<HandlePreToolUseOutput> {
@@ -577,9 +667,7 @@ export class HandlePreToolUseUseCase {
577
667
  lines.push("");
578
668
  lines.push("修正方法:");
579
669
  lines.push(" 1. cascade-updater を実行して product 文書を更新");
580
- lines.push(
581
- ` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`,
582
- );
670
+ lines.push(` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`);
583
671
  lines.push("");
584
672
  lines.push("参照: ADR-XXX");
585
673
 
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * 副作用なし・純粋関数相当。対応パターンは以下:
11
11
  * - リダイレクト `>` / `>>` / `>|` (clobber)
12
- * - heredoc (`<<EOF > path`)
12
+ * - heredoc (`<<EOF > path`) — ヒアドキュメント**本文**はシェル構文解析の対象外
13
+ * (本文中の `<...>` をリダイレクトと誤認しないため。WI-362)
13
14
  * - `tee` / `tee -a`
14
15
  * - `sed -i` / `sed -i ''` (BSD)
15
16
  * - `cp` / `mv` (宛先のみ)
@@ -111,6 +112,191 @@ function tokenize(input: string): Token[] {
111
112
  return tokens;
112
113
  }
113
114
 
115
+ /** ヒアドキュメント開始演算子で宣言された終端デリミタ */
116
+ type HeredocDelimiter = {
117
+ /** クォート・エスケープを解除した終端語 */
118
+ readonly word: string;
119
+ /** `<<-` 形式か (終端行の先頭タブを無視する) */
120
+ readonly allowIndent: boolean;
121
+ };
122
+
123
+ /** デリミタ語の終端となる文字 (空白およびシェル演算子) */
124
+ const DELIMITER_TERMINATORS = new Set([' ', '\t', ';', '&', '|', '<', '>', '(', ')']);
125
+
126
+ /**
127
+ * 1 行から ヒアドキュメント開始演算子 (`<<WORD` / `<<-WORD` / `<<'WORD'`) を検出し、
128
+ * 宣言順に終端デリミタを返す。
129
+ *
130
+ * クォート状態を追跡し、クォート内の `<<` は演算子として扱わない
131
+ * (例: `echo "a << b" > out.txt` の `<<` はヒアドキュメントではない)。
132
+ * `$( ... )` コマンド置換の内側ではクォート文脈がリセットされるため、
133
+ * `git commit -m "$(cat <<'EOF'` のような入れ子も検出できる。
134
+ * ヒアストリング `<<<` は本文を持たないため対象外。
135
+ */
136
+ function findHeredocDelimiters(line: string): HeredocDelimiter[] {
137
+ const found: HeredocDelimiter[] = [];
138
+ const substitutionStack: ('none' | 'single' | 'double')[] = [];
139
+ let quote: 'none' | 'single' | 'double' = 'none';
140
+ let i = 0;
141
+
142
+ while (i < line.length) {
143
+ const ch = line[i];
144
+
145
+ if (quote === 'single') {
146
+ if (ch === "'") quote = 'none';
147
+ i += 1;
148
+ continue;
149
+ }
150
+
151
+ if (ch === '\\') {
152
+ i += 2;
153
+ continue;
154
+ }
155
+
156
+ if (ch === '$' && line[i + 1] === '(') {
157
+ substitutionStack.push(quote);
158
+ quote = 'none';
159
+ i += 2;
160
+ continue;
161
+ }
162
+
163
+ if (quote === 'double') {
164
+ if (ch === '"') quote = 'none';
165
+ i += 1;
166
+ continue;
167
+ }
168
+
169
+ if (ch === "'") {
170
+ quote = 'single';
171
+ i += 1;
172
+ continue;
173
+ }
174
+
175
+ if (ch === '"') {
176
+ quote = 'double';
177
+ i += 1;
178
+ continue;
179
+ }
180
+
181
+ if (ch === ')' && substitutionStack.length > 0) {
182
+ quote = substitutionStack.pop() ?? 'none';
183
+ i += 1;
184
+ continue;
185
+ }
186
+
187
+ if (ch === '<' && line[i + 1] === '<') {
188
+ if (line[i + 2] === '<') {
189
+ // ヒアストリング `<<<` は本文行を持たない
190
+ i += 3;
191
+ continue;
192
+ }
193
+ i += 2;
194
+ let allowIndent = false;
195
+ if (line[i] === '-') {
196
+ allowIndent = true;
197
+ i += 1;
198
+ }
199
+ while (i < line.length && (line[i] === ' ' || line[i] === '\t')) i += 1;
200
+ const word = readHeredocDelimiterWord(line, i);
201
+ i = word.nextIndex;
202
+ if (word.value.length > 0) {
203
+ found.push({ word: word.value, allowIndent });
204
+ }
205
+ continue;
206
+ }
207
+
208
+ i += 1;
209
+ }
210
+
211
+ return found;
212
+ }
213
+
214
+ /** `<<` 直後のデリミタ語を読む。クォート/バックスラッシュを解除した値を返す。 */
215
+ function readHeredocDelimiterWord(line: string, start: number): { value: string; nextIndex: number } {
216
+ let value = '';
217
+ let i = start;
218
+
219
+ while (i < line.length) {
220
+ const ch = line[i];
221
+
222
+ if (ch === "'") {
223
+ i += 1;
224
+ while (i < line.length && line[i] !== "'") {
225
+ value += line[i];
226
+ i += 1;
227
+ }
228
+ i += 1;
229
+ continue;
230
+ }
231
+
232
+ if (ch === '"') {
233
+ i += 1;
234
+ while (i < line.length && line[i] !== '"') {
235
+ if (line[i] === '\\' && i + 1 < line.length) {
236
+ value += line[i + 1];
237
+ i += 2;
238
+ continue;
239
+ }
240
+ value += line[i];
241
+ i += 1;
242
+ }
243
+ i += 1;
244
+ continue;
245
+ }
246
+
247
+ if (ch === '\\' && i + 1 < line.length) {
248
+ value += line[i + 1];
249
+ i += 2;
250
+ continue;
251
+ }
252
+
253
+ if (DELIMITER_TERMINATORS.has(ch)) break;
254
+
255
+ value += ch;
256
+ i += 1;
257
+ }
258
+
259
+ return { value, nextIndex: i };
260
+ }
261
+
262
+ /** 終端行判定 (`<<-` は先頭タブを無視し、行末の空白・CR は許容する) */
263
+ function isHeredocTerminator(line: string, delimiter: HeredocDelimiter): boolean {
264
+ const candidate = delimiter.allowIndent ? line.replace(/^\t+/, '') : line;
265
+ return candidate.trimEnd() === delimiter.word;
266
+ }
267
+
268
+ /**
269
+ * ヒアドキュメント本文行を除去する。
270
+ *
271
+ * 本文はシェル構文ではなくデータであり、`Co-Authored-By: Claude <noreply@example.com>`
272
+ * のような `<...>` をリダイレクトと誤解釈すると無関係なコマンドが誤ブロックされる (WI-362)。
273
+ * 除去対象は**本文行と終端行のみ**で、開始演算子のある行はそのまま残すため
274
+ * `cat <<EOF > path` の `> path` は従来どおり検出される (fail-closed 維持)。
275
+ */
276
+ function stripHeredocBodies(command: string): string {
277
+ if (!command.includes('<<')) return command;
278
+
279
+ const lines = command.split('\n');
280
+ const kept: string[] = [];
281
+ const pending: HeredocDelimiter[] = [];
282
+
283
+ for (const line of lines) {
284
+ if (pending.length > 0) {
285
+ // 本文行 / 終端行はいずれも構文解析対象から外す
286
+ if (isHeredocTerminator(line, pending[0])) {
287
+ pending.shift();
288
+ }
289
+ continue;
290
+ }
291
+ kept.push(line);
292
+ for (const delimiter of findHeredocDelimiters(line)) {
293
+ pending.push(delimiter);
294
+ }
295
+ }
296
+
297
+ return kept.join('\n');
298
+ }
299
+
114
300
  const OPERATOR_SEPARATORS = new Set(['&&', '||', ';', '|']);
115
301
 
116
302
  /** トークン列を複合・パイプ境界で分割 */
@@ -140,6 +326,28 @@ function getCommandName(tokens: Token[]): string | undefined {
140
326
  return undefined;
141
327
  }
142
328
 
329
+ /** リダイレクト演算子の右辺を解釈し、fd 複製の場合は書き込み先なしとして扱う。 */
330
+ function getRedirectTarget(tokens: Token[], redirectIndex: number): string | undefined {
331
+ const next = tokens[redirectIndex + 1];
332
+ if (next === undefined) return undefined;
333
+
334
+ if (next.quoted === 'none' && /^&\d+$/.test(next.value)) {
335
+ return undefined;
336
+ }
337
+
338
+ if (next.quoted === 'none' && next.value === '&') {
339
+ const afterAmpersand = tokens[redirectIndex + 2];
340
+ if (afterAmpersand === undefined) return undefined;
341
+ if (afterAmpersand.quoted === 'none' && /^\d+$/.test(afterAmpersand.value)) {
342
+ return undefined;
343
+ }
344
+ // `>& file` は csh 形式の実ファイル書き込みとして安全側で抽出する。
345
+ return afterAmpersand.value;
346
+ }
347
+
348
+ return next.value;
349
+ }
350
+
143
351
  /**
144
352
  * 1 コマンド分のトークン列から書き込み先を抽出する。
145
353
  * リダイレクト先はコマンド種別によらず検出する (全コマンド共通)。
@@ -151,9 +359,9 @@ function extractFromSingleCommand(tokens: Token[]): string[] {
151
359
  for (let i = 0; i < tokens.length; i += 1) {
152
360
  const t = tokens[i];
153
361
  if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '>|')) {
154
- const next = tokens[i + 1];
155
- if (next !== undefined) {
156
- results.push(next.value);
362
+ const target = getRedirectTarget(tokens, i);
363
+ if (target !== undefined) {
364
+ results.push(target);
157
365
  }
158
366
  }
159
367
  }
@@ -289,7 +497,9 @@ function collectPositionals(tokens: Token[]): Token[] {
289
497
 
290
498
  /** コマンド文字列を token 化 → 演算子分割 → 各コマンドから書き込み先を抽出する内部関数。 */
291
499
  function extractFromCommandString(command: string): string[] {
292
- const tokens = tokenize(command);
500
+ // ヒアドキュメント本文はデータなのでシェル構文解析の対象外にする (WI-362)
501
+ // apply_patch 抽出は元の command 文字列を直接走査するため影響を受けない。
502
+ const tokens = tokenize(stripHeredocBodies(command));
293
503
  const groups = splitByOperators(tokens);
294
504
  const collected: string[] = [];
295
505
  for (const group of groups) {
@@ -25,6 +25,11 @@ const DEFAULT_PATTERNS = [
25
25
  // grandfather bypass を防ぐため、書き込み自体を保護対象とする。
26
26
  '.phasegate/baseline.json',
27
27
  '**/.phasegate/baseline.json',
28
+ // WI-363: .husky/ 配下は L0 runtime の実施点(pre-commit / commit-msg / pre-push)。
29
+ // WI-352 で config カテゴリに分類されるようになり Quick Mode の書き込み許可対象に
30
+ // 入ったため、防御機構そのものの書き換えを protected file として明示的に止める。
31
+ '.husky/**',
32
+ '**/.husky/**',
28
33
  ];
29
34
 
30
35
  /**