phasegate 0.315.0 → 0.340.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -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/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +93 -11
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +27 -4
- package/docs/guide/installation.md +4 -2
- package/docs/guide/quick-vs-full-mode.md +36 -1
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -2
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +17 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- 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 +196 -54
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +39 -17
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +129 -73
- 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/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +55 -3
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- 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 +152 -52
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
- 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 +23 -4
- 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 +202 -51
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +59 -19
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- 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/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -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,19 +15,42 @@ export class ProtectedFileListEmptyError extends Error {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
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',
|
|
27
|
+
// baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
|
|
28
|
+
// grandfather bypass を防ぐため、書き込み自体を保護対象とする。
|
|
29
|
+
'.phasegate/baseline.json',
|
|
30
|
+
'**/.phasegate/baseline.json',
|
|
31
|
+
// WI-363: .husky/ 配下は L0 runtime の実施点(pre-commit / commit-msg / pre-push)。
|
|
32
|
+
// WI-352 で config カテゴリに分類されるようになり Quick Mode の書き込み許可対象に
|
|
33
|
+
// 入ったため、防御機構そのものの書き換えを protected file として明示的に止める。
|
|
34
|
+
'.husky/**',
|
|
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 = [
|
|
19
45
|
'biome.json',
|
|
20
46
|
'.biome.json',
|
|
21
47
|
'tsconfig.json',
|
|
22
48
|
'package.json',
|
|
23
49
|
'package-lock.json',
|
|
24
|
-
// baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
|
|
25
|
-
// grandfather bypass を防ぐため、書き込み自体を保護対象とする。
|
|
26
|
-
'.phasegate/baseline.json',
|
|
27
|
-
'**/.phasegate/baseline.json',
|
|
28
50
|
];
|
|
29
51
|
|
|
52
|
+
const DEFAULT_PATTERNS = [...NON_EXCLUDABLE_PATTERNS, ...EXCLUDABLE_DEFAULT_PATTERNS];
|
|
53
|
+
|
|
30
54
|
/**
|
|
31
55
|
* glob パターンのシンプルなマッチング実装
|
|
32
56
|
* micromatch なしで基本的な glob をサポート
|
|
@@ -74,23 +98,21 @@ export class ProtectedFileList {
|
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
static createWithExclusions(exclusions: string[]): ProtectedFileList {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
return new ProtectedFileList([...DEFAULT_PATTERNS]);
|
|
80
|
-
}
|
|
81
|
-
return new ProtectedFileList(filtered);
|
|
101
|
+
const excludable = EXCLUDABLE_DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
|
|
102
|
+
return new ProtectedFileList([...NON_EXCLUDABLE_PATTERNS, ...excludable]);
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
static createWithAdditionalAndExclusions(
|
|
85
106
|
additionalPatterns: string[],
|
|
86
107
|
exclusions: string[],
|
|
87
108
|
): ProtectedFileList {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
]);
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
matches(filePath: string): boolean {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-373
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
FullModeRequirementQueryPort,
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
FullModeTargetChange,
|
|
8
9
|
} from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
9
10
|
import type { ClassifyChangeCategoryUseCase } from '../../../quick-mode/application/usecases/classify-change-category-usecase.js';
|
|
11
|
+
import { QuickModeConfigError } from '../../../quick-mode/domain/errors/quick-mode-config-error.js';
|
|
10
12
|
|
|
11
13
|
export interface QuickModeFullModeRequirementAdapterDeps {
|
|
12
14
|
classifyUseCaseFactory: () => ClassifyChangeCategoryUseCase;
|
|
@@ -45,7 +47,19 @@ export class QuickModeFullModeRequirementAdapter implements FullModeRequirementQ
|
|
|
45
47
|
rejectionReason: contract.rejectionReason,
|
|
46
48
|
dominantCategory: contract.dominantCategory ?? undefined,
|
|
47
49
|
};
|
|
48
|
-
} catch {
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// WI-373: quickMode 設定そのものが不正な場合は fail-closed。
|
|
52
|
+
// allowedCategories / categoryOverrides の enum 検証が入ったことで
|
|
53
|
+
// config の typo が例外になるため、ここで一律 fail-open のままだと
|
|
54
|
+
// 「設定を壊すと全書き込みが素通りする」という最悪の穴になる。
|
|
55
|
+
// config 不在・IO エラー等その他の例外は WI-333 の fail-open を維持する。
|
|
56
|
+
if (error instanceof QuickModeConfigError) {
|
|
57
|
+
return {
|
|
58
|
+
requiresFullMode: true,
|
|
59
|
+
rejectionRule: 'MIXED_CHANGES',
|
|
60
|
+
rejectionReason: `quickMode 設定が不正なため Quick Mode 判定を実行できません: ${error.message}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
49
63
|
return { requiresFullMode: false };
|
|
50
64
|
}
|
|
51
65
|
}
|
|
@@ -243,7 +243,7 @@ export function buildSessionStartContext(status: PhasegateStatus): string {
|
|
|
243
243
|
"",
|
|
244
244
|
"- Do NOT write to protected files without going through `/quick-implementor` skill.",
|
|
245
245
|
'- Do NOT create/structurally modify source files under units listed as "blocked" below — the required design docs (logical_design.md / domain_model.md) are missing, and pre-tool-use hooks will block writes.',
|
|
246
|
-
"-
|
|
246
|
+
"- Native `apply_patch` edits are phase-gated at PreToolUse; the Codex matcher is `Bash|apply_patch`. L2 pre-commit remains the backstop for skipped or untrusted hooks.",
|
|
247
247
|
"",
|
|
248
248
|
];
|
|
249
249
|
|
|
@@ -6,45 +6,50 @@
|
|
|
6
6
|
* @work-item-id WI-208
|
|
7
7
|
* @work-item-id WI-345
|
|
8
8
|
* @work-item-id WI-347
|
|
9
|
+
* @work-item-id WI-376
|
|
10
|
+
* @work-item-id WI-384
|
|
11
|
+
* @work-item-id WI-385
|
|
12
|
+
* @work-item-id WI-386
|
|
9
13
|
*
|
|
10
14
|
* PreToolUse Hook Adapter
|
|
11
15
|
* Claude Code の PreToolUse Hook エントリポイント
|
|
12
16
|
* stdin からJSON を読み取り、HandlePreToolUseUseCase を呼び出す
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import
|
|
19
|
+
import * as fs from "node:fs/promises";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { createQuickModeCompositionRoot } from "../../quick-mode/composition-root.js";
|
|
22
|
+
import type {
|
|
23
|
+
CanonicalPreToolUseInput,
|
|
24
|
+
HookResponseProfile,
|
|
25
|
+
} from "../application/dto/normalized-pre-tool-use-request.js";
|
|
26
|
+
import { HandlePreToolUseUseCase } from "../application/usecases/handle-pre-tool-use-usecase.js";
|
|
27
|
+
import { ApplyPatchWriteTargetExtractor } from "../domain/services/apply-patch-write-target-extractor.js";
|
|
28
|
+
import { BashWriteTargetExtractor } from "../domain/services/bash-write-target-extractor.js";
|
|
29
|
+
import { CiGovernanceBaselineGrandfatherAdapter } from "../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js";
|
|
30
|
+
import { FileSystemFullModeSessionQueryAdapter } from "../infrastructure/adapters/file-system-full-mode-session-query-adapter.js";
|
|
31
|
+
import { FileSystemStoryReflectionQueryAdapter } from "../infrastructure/adapters/file-system-story-reflection-query-adapter.js";
|
|
32
|
+
import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
|
|
33
|
+
import { HarnessErrorGuidanceAdapter } from "../infrastructure/adapters/harness-error-guidance-adapter.js";
|
|
34
|
+
import { PhaseGateQueryAdapter } from "../infrastructure/adapters/phase-gate-query-adapter.js";
|
|
35
|
+
import { QuickModeFullModeRequirementAdapter } from "../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js";
|
|
36
|
+
import { PreToolUsePayloadNormalizer } from "./pre-tool-use-payload-normalizer.js";
|
|
37
|
+
import { PreToolUseResponseRenderer } from "./pre-tool-use-response-renderer.js";
|
|
27
38
|
|
|
39
|
+
/**
|
|
40
|
+
* WI-376 (ADR-039): 呼び出し元 skill 名を受け取るフィールド(caller_skill)は持たない。
|
|
41
|
+
* Claude Code の PreToolUse payload に skill 情報は無く、エージェントの自己申告値は
|
|
42
|
+
* authorization / guidance の入力にしない。未知キーが来ても単に無視される。
|
|
43
|
+
*/
|
|
28
44
|
interface PreToolUseHookInput {
|
|
29
45
|
cwd?: string;
|
|
30
|
-
tool_name
|
|
31
|
-
|
|
32
|
-
tool_input?: {
|
|
33
|
-
path?: string;
|
|
34
|
-
file_path?: string;
|
|
35
|
-
paths?: string[];
|
|
36
|
-
command?: string;
|
|
37
|
-
content?: string;
|
|
38
|
-
old_string?: string;
|
|
39
|
-
new_string?: string;
|
|
40
|
-
old_str?: string;
|
|
41
|
-
new_str?: string;
|
|
42
|
-
[key: string]: unknown;
|
|
43
|
-
};
|
|
46
|
+
tool_name: string;
|
|
47
|
+
tool_input: CanonicalPreToolUseInput;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface TargetChange {
|
|
47
51
|
filePath: string;
|
|
52
|
+
changeKind?: "CREATE" | "MODIFY" | "DELETE";
|
|
48
53
|
beforeContent?: string | null;
|
|
49
54
|
afterContent?: string | null;
|
|
50
55
|
}
|
|
@@ -54,15 +59,15 @@ async function readStdin(): Promise<string> {
|
|
|
54
59
|
for await (const chunk of process.stdin) {
|
|
55
60
|
chunks.push(chunk as Buffer);
|
|
56
61
|
}
|
|
57
|
-
return Buffer.concat(chunks).toString(
|
|
62
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
async function findConfigPath(startDir: string): Promise<string> {
|
|
61
66
|
let dir = startDir;
|
|
62
67
|
while (true) {
|
|
63
68
|
const candidates = [
|
|
64
|
-
path.join(dir,
|
|
65
|
-
path.join(dir,
|
|
69
|
+
path.join(dir, "phasegate.config.json"),
|
|
70
|
+
path.join(dir, ".phasegate-local", "phasegate.config.json"),
|
|
66
71
|
];
|
|
67
72
|
for (const candidate of candidates) {
|
|
68
73
|
try {
|
|
@@ -74,18 +79,22 @@ async function findConfigPath(startDir: string): Promise<string> {
|
|
|
74
79
|
if (parent === dir) break;
|
|
75
80
|
dir = parent;
|
|
76
81
|
}
|
|
77
|
-
return path.join(startDir,
|
|
82
|
+
return path.join(startDir, "phasegate.config.json");
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
function projectRootForConfig(configPath: string): string {
|
|
81
86
|
const configDir = path.dirname(configPath);
|
|
82
|
-
return path.basename(configDir) ===
|
|
87
|
+
return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
function isProjectExternalPath(filePath: string, cwd: string, projectRoot: string): boolean {
|
|
86
91
|
const resolvedPath = path.resolve(cwd, filePath);
|
|
87
92
|
const relativePath = path.relative(projectRoot, resolvedPath);
|
|
88
|
-
return relativePath ===
|
|
93
|
+
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeProjectPath(filePath: string, cwd: string, projectRoot: string): string {
|
|
97
|
+
return path.relative(projectRoot, path.resolve(cwd, filePath)).split(path.sep).join("/");
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
async function main(): Promise<void> {
|
|
@@ -93,29 +102,41 @@ async function main(): Promise<void> {
|
|
|
93
102
|
try {
|
|
94
103
|
raw = await readStdin();
|
|
95
104
|
} catch {
|
|
96
|
-
process.stderr.write(
|
|
105
|
+
process.stderr.write("stdin読み取りエラー\n");
|
|
97
106
|
process.exit(2);
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
let
|
|
109
|
+
let parsed: unknown;
|
|
101
110
|
try {
|
|
102
|
-
|
|
111
|
+
parsed = JSON.parse(raw) as unknown;
|
|
103
112
|
} catch {
|
|
104
113
|
process.stderr.write(`不正なJSONです: ${raw}\n`);
|
|
105
114
|
process.exit(2);
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
const normalizer = new PreToolUsePayloadNormalizer();
|
|
118
|
+
const normalized = normalizer.normalize(parsed);
|
|
119
|
+
if (!normalized.ok) {
|
|
120
|
+
if (normalized.responseProfile === undefined) {
|
|
121
|
+
process.stderr.write(`${normalized.reason}\n`);
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
exitWithRenderedResponse(new PreToolUseResponseRenderer().deny(normalized.responseProfile, normalized.reason));
|
|
112
125
|
}
|
|
126
|
+
const request = normalized.request;
|
|
127
|
+
const responseProfile = request.responseProfile;
|
|
128
|
+
const input: PreToolUseHookInput = {
|
|
129
|
+
cwd: request.cwd,
|
|
130
|
+
tool_name: request.toolName,
|
|
131
|
+
tool_input: request.toolInput,
|
|
132
|
+
};
|
|
133
|
+
const toolName = input.tool_name;
|
|
113
134
|
|
|
114
135
|
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
115
136
|
const toRelative = (p: string): string => {
|
|
116
137
|
if (path.isAbsolute(p)) {
|
|
117
138
|
const rel = path.relative(cwd, p);
|
|
118
|
-
return rel.startsWith(
|
|
139
|
+
return rel.startsWith("..") ? p : rel;
|
|
119
140
|
}
|
|
120
141
|
return p;
|
|
121
142
|
};
|
|
@@ -140,7 +161,26 @@ async function main(): Promise<void> {
|
|
|
140
161
|
// translator の WRITE_TOOLS チェックを通過させる(Bash のままではフェーズゲートが
|
|
141
162
|
// スキップされるため)。
|
|
142
163
|
let effectiveToolName = toolName;
|
|
143
|
-
if (toolName ===
|
|
164
|
+
if (toolName === "apply_patch") {
|
|
165
|
+
const command = input.tool_input?.command;
|
|
166
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
167
|
+
deny(responseProfile, "apply_patch の tool_input.command が必要です");
|
|
168
|
+
}
|
|
169
|
+
const extractor = new ApplyPatchWriteTargetExtractor();
|
|
170
|
+
const patchTargets = extractor.extract(command);
|
|
171
|
+
if (patchTargets.length === 0) {
|
|
172
|
+
deny(responseProfile, "apply_patch command から書き込み対象を抽出できませんでした");
|
|
173
|
+
}
|
|
174
|
+
targetFilePaths.push(...patchTargets.map((target) => toRelative(target.filePath)));
|
|
175
|
+
targetChanges.push(
|
|
176
|
+
...patchTargets.map((target) => ({
|
|
177
|
+
filePath: toRelative(target.filePath),
|
|
178
|
+
changeKind: target.changeKind,
|
|
179
|
+
})),
|
|
180
|
+
);
|
|
181
|
+
effectiveToolName = "Write";
|
|
182
|
+
}
|
|
183
|
+
if (toolName === "Bash" && typeof input.tool_input?.command === "string") {
|
|
144
184
|
const extractor = new BashWriteTargetExtractor();
|
|
145
185
|
const bashTargets = extractor.extract(input.tool_input.command);
|
|
146
186
|
if (bashTargets.length > 0) {
|
|
@@ -155,19 +195,22 @@ async function main(): Promise<void> {
|
|
|
155
195
|
recordedTargetPaths.add(change.filePath);
|
|
156
196
|
}
|
|
157
197
|
}
|
|
158
|
-
effectiveToolName =
|
|
198
|
+
effectiveToolName = "Write";
|
|
159
199
|
}
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
try {
|
|
163
203
|
const configPath = await findConfigPath(cwd);
|
|
164
204
|
const projectRoot = projectRootForConfig(configPath);
|
|
165
|
-
const projectTargetFilePaths = targetFilePaths
|
|
166
|
-
(filePath) => !isProjectExternalPath(filePath, cwd, projectRoot)
|
|
167
|
-
|
|
168
|
-
const projectTargetChanges = targetChanges
|
|
169
|
-
(change) => !isProjectExternalPath(change.filePath, cwd, projectRoot)
|
|
170
|
-
|
|
205
|
+
const projectTargetFilePaths = targetFilePaths
|
|
206
|
+
.filter((filePath) => !isProjectExternalPath(filePath, cwd, projectRoot))
|
|
207
|
+
.map((filePath) => normalizeProjectPath(filePath, cwd, projectRoot));
|
|
208
|
+
const projectTargetChanges = targetChanges
|
|
209
|
+
.filter((change) => !isProjectExternalPath(change.filePath, cwd, projectRoot))
|
|
210
|
+
.map((change) => ({
|
|
211
|
+
...change,
|
|
212
|
+
filePath: normalizeProjectPath(change.filePath, cwd, projectRoot),
|
|
213
|
+
}));
|
|
171
214
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
172
215
|
const phaseGateQueryPort = new PhaseGateQueryAdapter();
|
|
173
216
|
const storyReflectionQueryPort = new FileSystemStoryReflectionQueryAdapter({
|
|
@@ -199,49 +242,60 @@ async function main(): Promise<void> {
|
|
|
199
242
|
fullModeSessionQueryPort,
|
|
200
243
|
});
|
|
201
244
|
|
|
202
|
-
const callerSkill = input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL;
|
|
203
245
|
const output = await useCase.execute({
|
|
204
246
|
toolName: effectiveToolName,
|
|
205
247
|
targetFilePaths: projectTargetFilePaths,
|
|
206
|
-
callerSkill,
|
|
207
248
|
targetChanges: projectTargetChanges,
|
|
208
249
|
});
|
|
209
250
|
|
|
210
251
|
if (output.shouldBlock) {
|
|
211
|
-
const msg =
|
|
212
|
-
?? `ファイル保護によりブロックされました: ${output.blockedFilePath ??
|
|
213
|
-
|
|
214
|
-
process.exit(2);
|
|
252
|
+
const msg =
|
|
253
|
+
output.error?.message ?? `ファイル保護によりブロックされました: ${output.blockedFilePath ?? "不明なファイル"}`;
|
|
254
|
+
deny(responseProfile, msg);
|
|
215
255
|
}
|
|
216
256
|
|
|
217
257
|
// Quick Mode が write を許可した場合に visibility を上げる informational notice。
|
|
218
258
|
// exit 0 は維持し semantics は変えない。WI-087 finding #3。
|
|
219
259
|
if (output.quickModeAllowed !== undefined) {
|
|
220
260
|
const cat = output.quickModeAllowed.dominantCategory;
|
|
221
|
-
const suffix = cat !== undefined && cat !==
|
|
261
|
+
const suffix = cat !== undefined && cat !== "" ? `, category=${cat}` : "";
|
|
222
262
|
process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
|
|
223
263
|
}
|
|
224
264
|
if (output.fullModeSessionAllowed !== undefined) {
|
|
225
265
|
const session = output.fullModeSessionAllowed;
|
|
226
|
-
const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` :
|
|
227
|
-
const unit = session.unit !== undefined ? `, unit=${session.unit}` :
|
|
266
|
+
const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : "";
|
|
267
|
+
const unit = session.unit !== undefined ? `, unit=${session.unit}` : "";
|
|
228
268
|
process.stderr.write(`phasegate: write allowed (Full Mode session${workItem}${unit})\n`);
|
|
229
269
|
}
|
|
230
270
|
|
|
231
271
|
process.exit(0);
|
|
232
272
|
} catch (error) {
|
|
233
|
-
|
|
234
|
-
process.exit(2);
|
|
273
|
+
deny(responseProfile, `実行エラー: ${String(error)}`);
|
|
235
274
|
}
|
|
236
275
|
}
|
|
237
276
|
|
|
277
|
+
function exitWithRenderedResponse(response: {
|
|
278
|
+
readonly stdout: string;
|
|
279
|
+
readonly stderr: string;
|
|
280
|
+
readonly exitCode: 0 | 2;
|
|
281
|
+
}): never {
|
|
282
|
+
if (response.stdout.length > 0) process.stdout.write(response.stdout);
|
|
283
|
+
if (response.stderr.length > 0) process.stderr.write(response.stderr);
|
|
284
|
+
process.exit(response.exitCode);
|
|
285
|
+
throw new Error("process.exit returned unexpectedly");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function deny(profile: HookResponseProfile, reason: string): never {
|
|
289
|
+
exitWithRenderedResponse(new PreToolUseResponseRenderer().deny(profile, reason));
|
|
290
|
+
}
|
|
291
|
+
|
|
238
292
|
async function buildTargetChanges(
|
|
239
293
|
input: PreToolUseHookInput,
|
|
240
294
|
cwd: string,
|
|
241
295
|
toRelative: (p: string) => string,
|
|
242
296
|
): Promise<TargetChange[]> {
|
|
243
297
|
const toolInput = input.tool_input;
|
|
244
|
-
if (toolInput == null || typeof toolInput !==
|
|
298
|
+
if (toolInput == null || typeof toolInput !== "object") {
|
|
245
299
|
return [];
|
|
246
300
|
}
|
|
247
301
|
|
|
@@ -251,18 +305,20 @@ async function buildTargetChanges(
|
|
|
251
305
|
}
|
|
252
306
|
|
|
253
307
|
const filePath = toRelative(rawPath);
|
|
254
|
-
const oldString =
|
|
255
|
-
const newString =
|
|
256
|
-
if (typeof oldString ===
|
|
308
|
+
const oldString = toolInput.old_string;
|
|
309
|
+
const newString = toolInput.new_string;
|
|
310
|
+
if (typeof oldString === "string" && typeof newString === "string") {
|
|
257
311
|
return [{ filePath, beforeContent: oldString, afterContent: newString }];
|
|
258
312
|
}
|
|
259
313
|
|
|
260
|
-
if (typeof toolInput.content ===
|
|
261
|
-
return [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
314
|
+
if (typeof toolInput.content === "string") {
|
|
315
|
+
return [
|
|
316
|
+
{
|
|
317
|
+
filePath,
|
|
318
|
+
beforeContent: await readExistingContent(cwd, rawPath),
|
|
319
|
+
afterContent: toolInput.content,
|
|
320
|
+
},
|
|
321
|
+
];
|
|
266
322
|
}
|
|
267
323
|
|
|
268
324
|
return [];
|
|
@@ -271,7 +327,7 @@ async function buildTargetChanges(
|
|
|
271
327
|
async function readExistingContent(cwd: string, filePath: string): Promise<string | null> {
|
|
272
328
|
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
273
329
|
try {
|
|
274
|
-
return await fs.readFile(absolutePath,
|
|
330
|
+
return await fs.readFile(absolutePath, "utf8");
|
|
275
331
|
} catch {
|
|
276
332
|
return null;
|
|
277
333
|
}
|
|
@@ -283,7 +339,7 @@ async function buildBashTargetChange(
|
|
|
283
339
|
toRelative: (p: string) => string,
|
|
284
340
|
): Promise<TargetChange> {
|
|
285
341
|
const filePath = toRelative(rawPath);
|
|
286
|
-
if (rawPath.includes(
|
|
342
|
+
if (rawPath.includes("$") || rawPath.includes("`") || rawPath.startsWith("~")) {
|
|
287
343
|
// hook ではシェル展開後の実パスを解決できないため、存在チェックを避けて MODIFY 既定にする。
|
|
288
344
|
return { filePath };
|
|
289
345
|
}
|
|
@@ -293,10 +349,10 @@ async function buildBashTargetChange(
|
|
|
293
349
|
return { filePath };
|
|
294
350
|
} catch (error) {
|
|
295
351
|
const code = (error as NodeJS.ErrnoException).code;
|
|
296
|
-
if (code ===
|
|
352
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
297
353
|
// Bash では afterContent を取得できないため、空文字を CREATE 判定用の
|
|
298
354
|
// 「変更後内容あり」sentinel として渡す。実ファイルへの書き込みは行わない。
|
|
299
|
-
return { filePath, beforeContent: null, afterContent:
|
|
355
|
+
return { filePath, beforeContent: null, afterContent: "" };
|
|
300
356
|
}
|
|
301
357
|
// 権限エラー等で存在を確認できない場合は従来どおり MODIFY 既定(安全側)。
|
|
302
358
|
return { filePath };
|