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.
- package/CHANGELOG.md +16 -0
- package/README.ja.md +22 -9
- package/README.md +28 -17
- package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/folder_management_rules.md +10 -1
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +27 -7
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +28 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +34 -5
- package/docs/guide/installation.md +4 -2
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +4 -1
- package/package.json +2 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
- package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
- package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
- package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
- package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
- package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -70
- package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
- package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
- package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
- package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
- package/scripts/harness/installation/application/named-hook-json.ts +17 -0
- package/scripts/harness/installation/application/operator-notice.ts +42 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
- package/scripts/harness/installation/application/usecases/run-install.ts +157 -57
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
- package/scripts/harness/installation/composition-root.ts +10 -0
- package/scripts/harness/installation/domain/agent-target.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +5 -0
- package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
- package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
- package/scripts/harness/installation/domain/repair-table.ts +5 -0
- package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +25 -5
- package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
- package/scripts/harness/main.ts +227 -128
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
- package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
- package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/composition-root.ts +5 -1
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/scripts/harness/setup/skill-deployer.ts +3 -45
- package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
- package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
- package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
- package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
- package/scripts/harness/skill-quality/composition-root.ts +14 -7
- package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
- package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
- package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
- package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
- package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
- package/skills/README.md +7 -0
- package/skills/cascade-updater/SKILL.md +37 -109
- 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
- package/skills/release-publisher/SKILL.md +3 -1
- package/skills/skill-creator/SKILL.md +8 -9
- package/templates/.agents/hooks.json +16 -0
- package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
- package/templates/.claude/settings.json +5 -3
- package/templates/.codex/hooks.json +2 -2
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts
ADDED
|
@@ -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
|
-
|
|
512
|
-
|
|
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, ['--
|
|
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
|
|
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
|
-
|
|
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(
|
|
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, ['--
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'
|
|
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
|
|
83
|
-
|
|
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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 {
|
package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts
CHANGED
|
@@ -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, [],
|
|
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
|
}
|