phasegate 0.335.0 → 0.341.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 (111) hide show
  1. package/CHANGELOG.md +16 -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/041-non-excludable-agent-trust-roots.md +68 -0
  6. package/docs/folder_management_rules.md +10 -1
  7. package/docs/guide/antigravity-integration.md +26 -0
  8. package/docs/guide/cli-reference.md +27 -7
  9. package/docs/guide/codex-integration.md +44 -107
  10. package/docs/guide/configuration.md +28 -2
  11. package/docs/guide/grok-integration.md +26 -0
  12. package/docs/guide/hooks-integration.md +34 -5
  13. package/docs/guide/installation.md +4 -2
  14. package/docs/guide/setup-artifacts.md +4 -4
  15. package/docs/guide/skills-overview.md +4 -1
  16. package/package.json +2 -1
  17. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
  18. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  19. package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
  20. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
  21. package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
  22. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  23. package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
  24. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  25. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
  26. package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
  27. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
  28. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
  29. package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
  30. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
  31. package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
  32. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
  33. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
  34. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
  35. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  36. package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
  37. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
  38. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -70
  39. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  40. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  41. package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
  42. package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
  43. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
  44. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  45. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
  46. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
  47. package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
  48. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
  49. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
  50. package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
  51. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  52. package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
  53. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  54. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  55. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  56. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  57. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  58. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  59. package/scripts/harness/installation/application/usecases/run-install.ts +157 -57
  60. package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
  61. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  62. package/scripts/harness/installation/composition-root.ts +10 -0
  63. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  64. package/scripts/harness/installation/domain/check-id.ts +5 -0
  65. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  66. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  67. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  68. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  69. package/scripts/harness/installation/presentation/cli/install-handler.ts +25 -5
  70. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  71. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  72. package/scripts/harness/main.ts +227 -128
  73. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
  74. package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
  75. package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
  76. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
  77. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
  78. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
  79. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  80. package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
  81. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  82. package/scripts/harness/quick-mode/composition-root.ts +5 -1
  83. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
  84. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  85. package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
  86. package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
  87. package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
  88. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  89. package/scripts/harness/setup/skill-deployer.ts +3 -45
  90. package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
  91. package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
  92. package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
  93. package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
  94. package/scripts/harness/skill-quality/composition-root.ts +14 -7
  95. package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
  96. package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
  97. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
  98. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
  99. package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
  100. package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
  101. package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
  102. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
  103. package/skills/README.md +7 -0
  104. package/skills/cascade-updater/SKILL.md +37 -109
  105. package/skills/cascade-updater/references//345/261/244/345/210/245/345/210/244/345/256/232/343/202/254/343/202/244/343/203/211.md +5 -3
  106. package/skills/release-publisher/SKILL.md +3 -1
  107. package/skills/skill-creator/SKILL.md +8 -9
  108. package/templates/.agents/hooks.json +16 -0
  109. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  110. package/templates/.claude/settings.json +5 -3
  111. package/templates/.codex/hooks.json +2 -2
@@ -27,6 +27,7 @@ import type {
27
27
  } from "../../domain/ports/full-mode-session-query-port.js";
28
28
  import type { PhaseGateQueryPort } from "../../domain/ports/phase-gate-query-port.js";
29
29
  import type { StoryReflectionQueryPort } from "../../domain/ports/story-reflection-query-port.js";
30
+ import { StoryReflectionQueryResult } from "../../domain/value-objects/story-reflection-query-result.js";
30
31
  import { AsyncHookToCliTranslator } from "../../domain/services/hook-to-cli-translator.js";
31
32
  import { HookEvent } from "../../domain/value-objects/hook-event.js";
32
33
  import type { BlockMetadata } from "../../domain/value-objects/hook-translation-result.js";
@@ -169,8 +170,15 @@ export class HandlePreToolUseUseCase {
169
170
  fullModeResult.dominantCategory,
170
171
  );
171
172
  if (sessionResult.allowed) {
173
+ const reflection = await this.checkSessionReflection(input, sessionResult);
174
+ if (reflection.sessionEnforced && !reflection.passed && !reflection.skipped) {
175
+ const blocked = HandlePreToolUseUseCase.buildStoryReflectionBlockOutput(input.targetFilePaths[0], reflection.blockers, reflection.warnings);
176
+ return { ...blocked, error: { message: `[L2-STORY-REFLECTION] 明示設定による依存反映チェック\n${reflection.blockers.join('\n')}\n修正方法: inception/productを単独で編集し、必要な上位判断と設計内容を反映してから同じ操作を再評価してください。タグだけでは意味的承認を証明しません。\n依存不明の場合はdescription.mdのID・所属・depends_onを確認してください。` } };
177
+ }
178
+ const warnings = [...reflection.blockers, ...reflection.warnings];
172
179
  return {
173
180
  shouldBlock: false,
181
+ ...(warnings.length > 0 ? { storyReflectionWarnings: warnings } : {}),
174
182
  fullModeSessionAllowed: {
175
183
  workItemId: sessionResult.workItemId,
176
184
  unit: sessionResult.unit,
@@ -187,7 +195,7 @@ export class HandlePreToolUseUseCase {
187
195
  input.targetFilePaths[0],
188
196
  fullModeResult,
189
197
  guidance,
190
- unitIdForGuidance,
198
+ this.deriveRecoveryUnitId(input.targetFilePaths),
191
199
  sessionResult,
192
200
  );
193
201
  }
@@ -225,6 +233,19 @@ export class HandlePreToolUseUseCase {
225
233
  );
226
234
  }
227
235
 
236
+ private async checkSessionReflection(
237
+ input: HandlePreToolUseInput,
238
+ session: FullModeSessionQueryResult,
239
+ ): Promise<StoryReflectionQueryResult> {
240
+ if (!session.active || !session.unit || !session.workItemId ||
241
+ !this.storyReflectionQueryPort?.checkSessionReflection || !this.resolveStoryReflectionScope(input)) return StoryReflectionQueryResult.skipped();
242
+ try {
243
+ return await this.storyReflectionQueryPort.checkSessionReflection(session.unit, session.workItemId);
244
+ } catch (error) {
245
+ return StoryReflectionQueryResult.skipped([`WI依存反映は未検証です(既存sessionの許可を維持): ${error instanceof Error ? error.message : String(error)}`]);
246
+ }
247
+ }
248
+
228
249
  private async checkGrandfather(targetFilePaths: readonly string[]): Promise<BaselineGrandfatherCheckResult> {
229
250
  if (this.baselineGrandfatherQueryPort === undefined) {
230
251
  return {
@@ -278,7 +299,7 @@ export class HandlePreToolUseUseCase {
278
299
  blockedFilePath: string | undefined,
279
300
  result: {
280
301
  requiresFullMode: boolean;
281
- rejectionRule?: "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
302
+ rejectionRule?: "CATEGORY_NOT_ALLOWED" | "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
282
303
  rejectionReason?: string;
283
304
  dominantCategory?: string;
284
305
  },
@@ -352,6 +373,7 @@ export class HandlePreToolUseUseCase {
352
373
  HandlePreToolUseUseCase.appendJudgmentContextLines(lines, sessionResult);
353
374
  const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
354
375
  lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
376
+ lines.push(' inceptionの計画編集と実装を分け、実装は対象Unitごとに設計反映とsession開始を行ってください。');
355
377
  if (unitId !== undefined && unitId !== "") {
356
378
  if (HandlePreToolUseUseCase.isSessionActiveButRejected(sessionResult)) {
357
379
  lines.push(
@@ -362,6 +384,8 @@ export class HandlePreToolUseUseCase {
362
384
  ` 実装フェーズ開始時: phasegate session begin --mode full --unit ${unitId} --work-item <WI-XXX> --reason "<reason>" --duration 1h`,
363
385
  );
364
386
  lines.push(" 実装完了時: phasegate session end --work-item <WI-XXX>");
387
+ } else {
388
+ lines.push(' 対象Unitが一意に決まりません。実装対象パスのUnitを確認してください(_crossはsessionのUnitに指定できません)。');
365
389
  }
366
390
  HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance, unitId);
367
391
 
@@ -458,6 +482,17 @@ export class HandlePreToolUseUseCase {
458
482
  return `${templatePath}(未配置なら: npx phasegate skills info ${skillMatch[1]})`;
459
483
  }
460
484
 
485
+ private deriveRecoveryUnitId(targetFilePaths: readonly string[]): string | undefined {
486
+ const projectPaths = this.configQueryPort.getProjectPaths();
487
+ const units = new Set<string>();
488
+ for (const targetFilePath of targetFilePaths) {
489
+ if (HandlePreToolUseUseCase.isUnderInception(targetFilePath, projectPaths.getDocsInception())) continue;
490
+ const unit = WriteTargetScope.fromPath(targetFilePath, projectPaths)?.unitId;
491
+ if (unit !== undefined && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(unit)) units.add(unit);
492
+ }
493
+ return units.size === 1 ? [...units][0] : undefined;
494
+ }
495
+
461
496
  private deriveUnitIdFromPaths(targetFilePaths: readonly string[]): string | undefined {
462
497
  const projectPaths = this.configQueryPort.getProjectPaths();
463
498
  for (const targetFilePath of targetFilePaths) {
@@ -581,7 +616,7 @@ export class HandlePreToolUseUseCase {
581
616
  // WI-363: .husky/ 配下は L0 runtime の実施点。Quick Mode でも書き換えさせない。
582
617
  pattern: /(?:^|\/)\.husky\//,
583
618
  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 に対象パターンを明示してください。`,
619
+ `保護ファイルへの書き込みがブロックされました: ${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
620
  },
586
621
  ];
587
622
 
@@ -2,14 +2,12 @@
2
2
  * @layer application
3
3
  * @unit agent-integration
4
4
  * @story H11-04
5
+ * @work-item-id WI-220
5
6
  *
6
7
  * HandleStopUseCase
7
8
  * Stop Hook処理のオーケストレーション。ReentryGuardライフサイクル管理の唯一の制御点
8
9
  */
9
10
 
10
- import { HookEvent } from '../../domain/value-objects/hook-event.js';
11
- import { ReentryGuard } from '../../domain/entities/reentry-guard.js';
12
- import { AsyncHookToCliTranslator } from '../../domain/services/hook-to-cli-translator.js';
13
11
  import type { ReentryGuardStatePort } from '../../domain/ports/reentry-guard-state-port.js';
14
12
  import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
15
13
  import type { CliExecutorPort } from '../ports/cli-executor-port.js';
@@ -19,7 +17,8 @@ export interface HandleStopUseCasePorts {
19
17
  reentryGuardStatePort: ReentryGuardStatePort;
20
18
  cliExecutorPort: CliExecutorPort;
21
19
  configQueryPort: ConfigQueryPort;
22
- cliCommandRegistryPort: {
20
+ /** Legacy constructor input; Stop dispatches complete-check directly. */
21
+ cliCommandRegistryPort?: {
23
22
  hasCommand(commandName: string): Promise<boolean>;
24
23
  listCommands(): Promise<readonly string[]>;
25
24
  };
@@ -37,16 +36,11 @@ export class HandleStopUseCase {
37
36
  private readonly reentryGuardStatePort: ReentryGuardStatePort;
38
37
  private readonly cliExecutorPort: CliExecutorPort;
39
38
  private readonly configQueryPort: ConfigQueryPort;
40
- private readonly cliCommandRegistryPort: {
41
- hasCommand(commandName: string): Promise<boolean>;
42
- listCommands(): Promise<readonly string[]>;
43
- };
44
39
 
45
40
  constructor(ports: HandleStopUseCasePorts) {
46
41
  this.reentryGuardStatePort = ports.reentryGuardStatePort;
47
42
  this.cliExecutorPort = ports.cliExecutorPort;
48
43
  this.configQueryPort = ports.configQueryPort;
49
- this.cliCommandRegistryPort = ports.cliCommandRegistryPort;
50
44
  }
51
45
 
52
46
  async execute(input: HandleStopInput): Promise<HandleStopOutput> {
@@ -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
  }
@@ -5,4 +5,6 @@ import type { StoryReflectionQueryResult } from '../value-objects/story-reflecti
5
5
 
6
6
  export interface StoryReflectionQueryPort {
7
7
  checkReflection(unitId: string): Promise<StoryReflectionQueryResult>;
8
+ /** WI-220: optional session path, advisory by default; caller supplies authenticated identity. */
9
+ checkSessionReflection?(unitId: string, workItemId: string): Promise<StoryReflectionQueryResult>;
8
10
  }
@@ -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
  *
@@ -25,6 +26,8 @@
25
26
  * `*** Update|Add|Delete File: <path>` 行) — Codex CLI 対応 (ISSUE-013 Wave 1)
26
27
  */
27
28
 
29
+ import { ApplyPatchWriteTargetExtractor } from './apply-patch-write-target-extractor.js';
30
+
28
31
  /** 引数トークン (値とクォート種別) */
29
32
  type Token = {
30
33
  readonly value: string;
@@ -508,62 +511,13 @@ function extractFromCommandString(command: string): string[] {
508
511
  }
509
512
  }
510
513
  // ネスト内の apply_patch heredoc も拾う
511
- for (const p of extractApplyPatchTargets(command)) {
512
- collected.push(p);
514
+ const patchExtractor = new ApplyPatchWriteTargetExtractor();
515
+ for (const target of patchExtractor.extract(command)) {
516
+ collected.push(target.filePath);
513
517
  }
514
518
  return collected;
515
519
  }
516
520
 
517
- /** apply_patch ブロック境界マーカー (Begin/End) */
518
- const APPLY_PATCH_BEGIN_SOURCE = String.raw`\*\*\*\s+Begin\s+Patch`;
519
- const APPLY_PATCH_END_SOURCE = String.raw`\*\*\*\s+End\s+Patch`;
520
- /** ブロック内のファイル行: `*** (Update|Add|Delete) File: <path>` */
521
- const APPLY_PATCH_FILE_LINE_SOURCE = String.raw`^\s*\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+?)\s*$`;
522
-
523
- /**
524
- * apply_patch ヒアドキュメント構文から対象ファイルパスを抽出する。
525
- *
526
- * Codex CLI 等が採用する unified-diff 風パッチフォーマット:
527
- * *** Begin Patch
528
- * *** Update File: <path>
529
- * *** Add File: <path>
530
- * *** Delete File: <path>
531
- * *** End Patch
532
- *
533
- * `*** End Patch` が欠けている場合は command 末尾までをブロックとして扱う
534
- * (保護側に倒す — phase-gate の取りこぼしよりも誤検出のほうが許容される)。
535
- */
536
- function extractApplyPatchTargets(command: string): string[] {
537
- const beginGlobal = new RegExp(APPLY_PATCH_BEGIN_SOURCE, 'g');
538
- const beginStarts: number[] = [];
539
- let m: RegExpExecArray | null;
540
- while ((m = beginGlobal.exec(command)) !== null) {
541
- beginStarts.push(m.index + m[0].length);
542
- }
543
- if (beginStarts.length === 0) return [];
544
-
545
- const endGlobal = new RegExp(APPLY_PATCH_END_SOURCE, 'g');
546
- const endStarts: number[] = [];
547
- while ((m = endGlobal.exec(command)) !== null) {
548
- endStarts.push(m.index);
549
- }
550
-
551
- const results: string[] = [];
552
- for (let i = 0; i < beginStarts.length; i += 1) {
553
- const start = beginStarts[i];
554
- const nextBegin = i + 1 < beginStarts.length ? beginStarts[i + 1] : command.length;
555
- const endInRange = endStarts.find((e) => e > start && e <= nextBegin);
556
- const end = endInRange ?? nextBegin;
557
- const body = command.slice(start, end);
558
-
559
- const fileRegex = new RegExp(APPLY_PATCH_FILE_LINE_SOURCE, 'gm');
560
- while ((m = fileRegex.exec(body)) !== null) {
561
- results.push(m[1]);
562
- }
563
- }
564
- return results;
565
- }
566
-
567
521
  export class BashWriteTargetExtractor {
568
522
  /**
569
523
  * Bash コマンド文字列から書き込み先ファイルパスを抽出する。
@@ -140,7 +140,7 @@ export class HookToCliTranslator {
140
140
  throw new CommandNotRegisteredError(commandName);
141
141
  }
142
142
 
143
- return HookTranslationResult.execute(commandName, ['--fast'], 0, 500);
143
+ return HookTranslationResult.execute(commandName, [...new Set(event.affectedFilePaths)].flatMap((target) => ['--target', target]), 0, 5000);
144
144
  }
145
145
 
146
146
  private translateStop(_event: StopEvent): HookTranslationResult {
@@ -248,7 +248,10 @@ export class AsyncHookToCliTranslator {
248
248
  const projectPaths = await (this.configQueryPort as ConfigQueryPort & {
249
249
  getProjectPaths(): unknown;
250
250
  }).getProjectPaths();
251
- const detectedScope = event.targetFilePaths
251
+ const gatePaths = event.targetFilePaths.filter(filePath => !WriteTargetScope.isInceptionDocument(
252
+ filePath, projectPaths as Parameters<typeof WriteTargetScope.fromPath>[1],
253
+ ));
254
+ const detectedScope = gatePaths
252
255
  .map((filePath) => WriteTargetScope.fromPath(filePath, projectPaths as Parameters<typeof WriteTargetScope.fromPath>[1]))
253
256
  .find((scope): scope is WriteTargetScope => scope !== null);
254
257
 
@@ -272,7 +275,7 @@ export class AsyncHookToCliTranslator {
272
275
 
273
276
  const phaseGateResult = await this.phaseGateQueryPort.checkGate(
274
277
  detectedScope,
275
- event.targetFilePaths[0],
278
+ gatePaths.find(filePath => WriteTargetScope.fromPath(filePath, projectPaths as Parameters<typeof WriteTargetScope.fromPath>[1]) !== null),
276
279
  );
277
280
  if (!phaseGateResult.hasPassed()) {
278
281
  return HookTranslationResult.block({
@@ -292,7 +295,7 @@ export class AsyncHookToCliTranslator {
292
295
  });
293
296
  }
294
297
 
295
- private async translatePostToolUse(_event: PostToolUseEvent): Promise<HookTranslationResult> {
298
+ private async translatePostToolUse(event: PostToolUseEvent): Promise<HookTranslationResult> {
296
299
  const isEnabled = await this.configQueryPort.isHookEnabled('post-tool-use');
297
300
  if (!isEnabled) {
298
301
  return HookTranslationResult.skip('HOOK_DISABLED');
@@ -304,7 +307,7 @@ export class AsyncHookToCliTranslator {
304
307
  throw new CommandNotRegisteredError(commandName);
305
308
  }
306
309
 
307
- return HookTranslationResult.execute(commandName, ['--fast'], 0, 500);
310
+ return HookTranslationResult.execute(commandName, [...new Set(event.affectedFilePaths)].flatMap((target) => ['--target', target]), 0, 5000);
308
311
  }
309
312
 
310
313
  private async translateStop(_event: StopEvent): Promise<HookTranslationResult> {
@@ -6,7 +6,7 @@
6
6
  * HookEvent → CLI実行指示への変換結果
7
7
  */
8
8
 
9
- export type SkipReason = 'REENTRY_DETECTED' | 'HOOK_DISABLED' | 'TIMEOUT_EXCEEDED';
9
+ export type SkipReason = 'REENTRY_DETECTED' | 'HOOK_DISABLED' | 'TIMEOUT_EXCEEDED' | 'READ_ONLY';
10
10
 
11
11
  export type BlockReason = 'PROTECTED_FILE' | 'PHASE_GATE' | 'FULL_MODE_REQUIRED';
12
12
 
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-390
4
5
  *
5
6
  * ProtectedFileList 値オブジェクト
6
7
  * 変更をブロックすべきファイルのパターンリスト
@@ -14,13 +15,15 @@ export class ProtectedFileListEmptyError extends Error {
14
15
  }
15
16
  }
16
17
 
17
- /** デフォルトの保護対象ファイルパターン */
18
- const DEFAULT_PATTERNS = [
19
- 'biome.json',
20
- '.biome.json',
21
- 'tsconfig.json',
22
- 'package.json',
23
- 'package-lock.json',
18
+ /**
19
+ * 設定から除外できない agent trust roots。
20
+ * これらを protectedFiles.exclude で解除できると、防御機構そのものを agent が
21
+ * 無効化してから書き換えられるため、通常の保護対象とは別の集合で保持する。
22
+ */
23
+ const NON_EXCLUDABLE_PATTERNS = [
24
+ 'phasegate.config.json',
25
+ '.phasegate-local/phasegate.config.json',
26
+ '**/phasegate.config.json',
24
27
  // baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
25
28
  // grandfather bypass を防ぐため、書き込み自体を保護対象とする。
26
29
  '.phasegate/baseline.json',
@@ -30,8 +33,24 @@ const DEFAULT_PATTERNS = [
30
33
  // 入ったため、防御機構そのものの書き換えを protected file として明示的に止める。
31
34
  '.husky/**',
32
35
  '**/.husky/**',
36
+ // Root agent instructions determine the permissions and operating procedure
37
+ // used by coding agents, so direct agent writes must remain blocked.
38
+ 'CLAUDE.md',
39
+ 'AGENTS.md',
40
+ 'GEMINI.md',
41
+ ];
42
+
43
+ /** 利用者設定で除外可能な通常のデフォルト保護対象。 */
44
+ const EXCLUDABLE_DEFAULT_PATTERNS = [
45
+ 'biome.json',
46
+ '.biome.json',
47
+ 'tsconfig.json',
48
+ 'package.json',
49
+ 'package-lock.json',
33
50
  ];
34
51
 
52
+ const DEFAULT_PATTERNS = [...NON_EXCLUDABLE_PATTERNS, ...EXCLUDABLE_DEFAULT_PATTERNS];
53
+
35
54
  /**
36
55
  * glob パターンのシンプルなマッチング実装
37
56
  * micromatch なしで基本的な glob をサポート
@@ -79,23 +98,21 @@ export class ProtectedFileList {
79
98
  }
80
99
 
81
100
  static createWithExclusions(exclusions: string[]): ProtectedFileList {
82
- const filtered = DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
83
- if (filtered.length === 0) {
84
- return new ProtectedFileList([...DEFAULT_PATTERNS]);
85
- }
86
- return new ProtectedFileList(filtered);
101
+ const excludable = EXCLUDABLE_DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
102
+ return new ProtectedFileList([...NON_EXCLUDABLE_PATTERNS, ...excludable]);
87
103
  }
88
104
 
89
105
  static createWithAdditionalAndExclusions(
90
106
  additionalPatterns: string[],
91
107
  exclusions: string[],
92
108
  ): ProtectedFileList {
93
- const base = DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
94
- const allPatterns = [...base, ...additionalPatterns];
95
- if (allPatterns.length === 0) {
96
- return new ProtectedFileList([...DEFAULT_PATTERNS]);
97
- }
98
- return new ProtectedFileList(allPatterns);
109
+ const excludableDefaults = EXCLUDABLE_DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
110
+ const excludableAdditional = additionalPatterns.filter((p) => !exclusions.includes(p));
111
+ return new ProtectedFileList([
112
+ ...NON_EXCLUDABLE_PATTERNS,
113
+ ...excludableDefaults,
114
+ ...excludableAdditional,
115
+ ]);
99
116
  }
100
117
 
101
118
  matches(filePath: string): boolean {
@@ -14,6 +14,7 @@ export class StoryReflectionQueryResult {
14
14
  readonly blockers: readonly string[];
15
15
  readonly warnings: readonly string[];
16
16
  readonly skipped: boolean;
17
+ declare readonly sessionEnforced?: true;
17
18
 
18
19
  private constructor(
19
20
  passed: boolean,
@@ -27,8 +28,8 @@ export class StoryReflectionQueryResult {
27
28
  this.skipped = skipped;
28
29
  }
29
30
 
30
- static pass(): StoryReflectionQueryResult {
31
- return new StoryReflectionQueryResult(true, [], [], false);
31
+ static pass(warnings: string[] = []): StoryReflectionQueryResult {
32
+ return new StoryReflectionQueryResult(true, [], warnings, false);
32
33
  }
33
34
 
34
35
  static skipped(warnings: string[] = []): StoryReflectionQueryResult {
@@ -45,6 +46,13 @@ export class StoryReflectionQueryResult {
45
46
  return new StoryReflectionQueryResult(false, blockers, warnings, false);
46
47
  }
47
48
 
49
+ /** Only trusted configuration readers may mark the optional session enforcement path. */
50
+ withSessionEnforcement(): StoryReflectionQueryResult {
51
+ return Object.assign(new StoryReflectionQueryResult(this.passed, this.blockers, this.warnings, this.skipped), {
52
+ sessionEnforced: true as const,
53
+ });
54
+ }
55
+
48
56
  hasPassed(): boolean {
49
57
  return this.passed;
50
58
  }
@@ -61,6 +61,9 @@ export class WriteTargetScope {
61
61
  if (sourceMatch !== null) {
62
62
  const [unitId] = sourceMatch;
63
63
  if (unitId !== undefined) {
64
+ // A source-root file (e.g. main.ts) is not a Unit directory.
65
+ // Keep nested paths, including dotted directory names, under the gate.
66
+ if (sourceMatch.length === 1 && unitId.includes('.')) continue;
64
67
  return WriteTargetScope.create({ level: 3, unitId });
65
68
  }
66
69
  }
@@ -124,6 +127,15 @@ export class WriteTargetScope {
124
127
  return null;
125
128
  }
126
129
 
130
+ /** Phase-1 Markdown repair is not implementation; source-root overlaps remain gated. */
131
+ static isInceptionDocument(filePath: string, projectPaths: ProjectPaths): boolean {
132
+ const normalized = normalize(filePath);
133
+ return normalized.toLowerCase().endsWith('.md')
134
+ && matchPrefix(normalized, projectPaths.getDocsInception()) !== null
135
+ && !projectPaths.getSource().some(root => matchPrefix(normalized, root) !== null)
136
+ && WriteTargetScope.fromPath(normalized, projectPaths)?.level === 3;
137
+ }
138
+
127
139
  equals(other: WriteTargetScope): boolean {
128
140
  return this.level === other.level && this.unitId === other.unitId && this.storyId === other.storyId;
129
141
  }