phasegate 0.335.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/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +4 -4
- package/docs/guide/codex-integration.md +44 -107
- 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/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +1 -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-pre-tool-use-usecase.ts +2 -2
- 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 +6 -52
- 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 +35 -18
- 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 +123 -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/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- 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 +100 -39
- 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/services/quick-mode-judgment-engine.ts +13 -2
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- 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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Grok Build Integration
|
|
2
|
+
|
|
3
|
+
<!-- @work-item-id WI-385 -->
|
|
4
|
+
|
|
5
|
+
Phasegate uses Grok Build's Claude-compatible project hook scanner. It manages `.claude/settings.json` as the single hook source and deliberately does not create `.grok/hooks`, avoiding duplicate invocation.
|
|
6
|
+
|
|
7
|
+
Minimum supported runtime: the hook-capable Grok CLI 1.0.0 series, which is the series used to verify this integration. If hooks appear to be silently inactive, run `grok inspect` and confirm the project hook is loaded and trusted in `/hooks`.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx phasegate install --agent grok --with-husky --apply
|
|
13
|
+
npx phasegate doctor --agent grok
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The managed PreToolUse entries collectively match `Bash|Write|Edit|apply_patch`; each Phasegate command has an explicit 30-second timeout. Grok otherwise defaults a hook to 5 seconds and fails open when the hook times out or crashes, so the distributed timeout gives Phasegate enough time to inspect a write while L2 pre-commit remains the backstop. `run_terminal_command`, `search_replace`, `write`, and `apply_patch` camelCase payloads are normalized to the existing Bash/Write/Edit/patch pipeline. Grok truncates tool input at 128 KB; a truncated command or patch is denied because the full target set cannot be proven. A direct tool with a complete path can still be checked.
|
|
17
|
+
|
|
18
|
+
## Result contract
|
|
19
|
+
|
|
20
|
+
- Deny writes top-level `{ "decision": "deny", "reason": ... }` and Claude-compatible `hookSpecificOutput` to stdout, writes the reason to stderr, and exits 2.
|
|
21
|
+
- Allow exits 0 with empty stdout and does not override permission.
|
|
22
|
+
- Agent/model fields and the selected install target are not authorization inputs.
|
|
23
|
+
|
|
24
|
+
## Trust and residual risk
|
|
25
|
+
|
|
26
|
+
Project hooks can be skipped before trust is approved, and Phasegate cannot inspect Grok's external trust state. Run `grok inspect`, open `/hooks`, and approve with `--trust` or `/hooks-trust`. Keep L2 pre-commit and CI enabled because timeout/crash remains fail-open even with the explicit 30-second limit. The canonical path key for every future Grok `write` variant remains runtime-controlled; unknown supported-write shapes fail closed rather than silently allowing.
|
|
@@ -6,17 +6,28 @@ Phasegate integrates natively with Claude Code through its hooks system. This en
|
|
|
6
6
|
|
|
7
7
|
For new or existing projects, prefer `npx phasegate install --dry-run` followed by `npx phasegate install --apply` so existing hook JSON is merged instead of replaced. Manual editing is still possible, but then `phasegate doctor` may report missing managed targets until the expected PhaseGate entries, skill links, Husky scripts, CI workflow, and manifest are present. See [Setup Artifacts](setup-artifacts.md). <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 -->
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Manual hook configuration
|
|
10
|
+
|
|
11
|
+
If structured install is unavailable, use the same packaged commands and timeout as the distributed `.claude/settings.json`:
|
|
10
12
|
|
|
11
13
|
```jsonc
|
|
12
14
|
{
|
|
13
15
|
"hooks": {
|
|
14
16
|
"PreToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Bash|apply_patch",
|
|
19
|
+
"hooks": [{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "npx phasegate hook pre-tool-use",
|
|
22
|
+
"timeout": 30
|
|
23
|
+
}]
|
|
24
|
+
},
|
|
15
25
|
{
|
|
16
26
|
"matcher": "Write|Edit",
|
|
17
27
|
"hooks": [{
|
|
18
28
|
"type": "command",
|
|
19
|
-
"command": "npx
|
|
29
|
+
"command": "npx phasegate hook pre-tool-use",
|
|
30
|
+
"timeout": 30
|
|
20
31
|
}]
|
|
21
32
|
}
|
|
22
33
|
],
|
|
@@ -25,7 +36,7 @@ Add the following to `.claude/settings.json`:
|
|
|
25
36
|
"matcher": "Write|Edit",
|
|
26
37
|
"hooks": [{
|
|
27
38
|
"type": "command",
|
|
28
|
-
"command": "npx
|
|
39
|
+
"command": "npx phasegate hook post-tool-use"
|
|
29
40
|
}]
|
|
30
41
|
}
|
|
31
42
|
],
|
|
@@ -34,7 +45,7 @@ Add the following to `.claude/settings.json`:
|
|
|
34
45
|
"matcher": "",
|
|
35
46
|
"hooks": [{
|
|
36
47
|
"type": "command",
|
|
37
|
-
"command": "npx
|
|
48
|
+
"command": "npx phasegate hook stop"
|
|
38
49
|
}]
|
|
39
50
|
}
|
|
40
51
|
]
|
|
@@ -42,6 +53,18 @@ Add the following to `.claude/settings.json`:
|
|
|
42
53
|
}
|
|
43
54
|
```
|
|
44
55
|
|
|
56
|
+
## Runtime coverage matrix
|
|
57
|
+
|
|
58
|
+
<!-- @work-item-id WI-385 -->
|
|
59
|
+
|
|
60
|
+
| Runtime payload | Managed source | Pre-edit deny output | Coverage boundary |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| Claude / Codex flat snake_case | `.claude/settings.json` / `.codex/hooks.json` | empty stdout, stderr, exit 2 | Existing contract unchanged |
|
|
63
|
+
| Grok flat camelCase | Claude-compatible `.claude/settings.json` | top-level deny plus `hookSpecificOutput`, stderr, exit 2 | Trust must be checked with `grok inspect` / `/hooks` |
|
|
64
|
+
| Antigravity nested `toolCall` | named `.agents/hooks.json` | top-level `decision` / `reason`, stderr, exit 2 | Hard block supported for `agy` CLI only |
|
|
65
|
+
|
|
66
|
+
All allow paths keep stdout empty and do not override runtime permission. Malformed, ambiguous, truncated command/patch, or supported write tools without extractable targets fail closed. L2 pre-commit and CI remain the backstops for untrusted/skipped hooks, Antigravity IDE/desktop, and unverified timeout/crash behavior.
|
|
67
|
+
|
|
45
68
|
## Hook Behaviors
|
|
46
69
|
|
|
47
70
|
### Responsibility Separation (important)
|
|
@@ -36,7 +36,7 @@ npm install
|
|
|
36
36
|
npx phasegate init --name <project-name>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
This deploys 29 skills to `skills/`, creates the agent-facing skill links (
|
|
39
|
+
This deploys 29 skills to `skills/`, creates the selected agent-facing skill links (`.claude/skills/`, `.codex/skills/`, or `.agents/skills/`), and generates `phasegate.config.json`.
|
|
40
40
|
|
|
41
41
|
`init` is the legacy-compatible bootstrap path for new projects. It also runs the structured install path for the selected agent target so `CLAUDE.md` and/or `AGENTS.md` receive a PhaseGate managed section. Use `install` when the project may already have hooks, package scripts, or CI files that should be preserved. <!-- @work-item-id WI-174 -->
|
|
42
42
|
|
|
@@ -56,7 +56,9 @@ npx phasegate install --apply
|
|
|
56
56
|
npx phasegate doctor
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, deploys selected bundled skills to root `skills/`, creates `.claude/skills
|
|
59
|
+
`install --dry-run` reports whether each target will be created, merged, skipped, or refused. `install --apply` performs the merge, adds package scripts and the `phasegate` devDependency, deploys selected bundled skills to root `skills/`, creates the selected `.claude/skills`, `.codex/skills`, and `.agents/skills` links, writes `CLAUDE.md` / `AGENTS.md` managed sections for selected agent targets, writes `.github/workflows/phasegate-aidlc-gate.yml` when CI is enabled, and records managed entries in `.phasegate/manifest.json`. Husky hook targets (`.husky/*`) and the GitHub Actions target are opt-in, matching `init` and `setup:agent`: pass `--with-husky` and/or `--with-ci` to include them — without these flags `install` does not touch `.husky/` or `.github/workflows/`. <!-- @work-item-id WI-316 --> Existing skills catalogs are merged: PhaseGate refreshes only bundled skill directories selected by `--skills core|all` and preserves user-owned skills. See [Setup Artifacts](setup-artifacts.md) for the full managed target, generated artifact, runtime state, legacy artifact, and user-level setting inventory. <!-- @work-item-id WI-152 --> <!-- @work-item-id WI-169 --> <!-- @work-item-id WI-174 --> <!-- @work-item-id WI-216 -->
|
|
60
|
+
|
|
61
|
+
Agent targets are `claude | codex | both | grok | antigravity | all`. `both` is backward-compatible Claude + Codex. Grok shares `.claude/settings.json` and `.claude/skills`, receives `AGENTS.md` without Claude-only `CLAUDE.md`, and does not receive `.grok/hooks`. Antigravity receives `.agents/hooks.json`, `.agents/skills`, and `AGENTS.md`. `all` installs Claude, Codex, and Antigravity surfaces, with Grok covered once by the Claude-compatible hook. <!-- @work-item-id WI-385 -->
|
|
60
62
|
|
|
61
63
|
For personal evaluation inside a team-owned repository:
|
|
62
64
|
|
|
@@ -14,7 +14,7 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
14
14
|
|
|
15
15
|
| Class | Examples | Owner | Lifecycle |
|
|
16
16
|
|---|---|---|---|
|
|
17
|
-
| Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, root `skills/` bundled skill bodies, `.claude/skills`, `.codex/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, shared skill body, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
|
|
17
|
+
| Managed target | `phasegate.config.json`, `.claude/settings.json`, `.codex/hooks.json`, `.agents/hooks.json`, `CLAUDE.md`, `AGENTS.md`, `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`, `.github/workflows/phasegate-aidlc-gate.yml`, root `skills/` bundled skill bodies, `.claude/skills`, `.codex/skills`, `.agents/skills`, `package.json` PhaseGate scripts/devDependency | PhaseGate managed block, real runtime artifact, shared skill body, or symlink plus user content | Created or merged by `install`, refreshed by `reconcile`, removed or reversed by `uninstall` |
|
|
18
18
|
| Configuration | `phasegate.config.json`, `package.json` | User owned, PhaseGate assisted | Created by `init` or project install when absent; `install` may merge scripts/devDependency into `package.json` |
|
|
19
19
|
| Generated artifact | `.phasegate/manifest.json`, `.phasegate/backups/*`, `.phasegate/uninstalled-*.json`, `.phasegate/baseline.json` | PhaseGate | Written by lifecycle commands and validators; safe to regenerate only through the owning command |
|
|
20
20
|
| Runtime state/report | `.phasegate/hook-skip-events.jsonl`, explicit `doctor --report-out <path>` output, `reports/regression/*`, resolved `reporting.outputDir` reports | PhaseGate command output | Produced while hooks, doctor, and validation commands run |
|
|
@@ -26,11 +26,11 @@ PhaseGate setup is more than `phasegate.config.json`. A healthy installation is
|
|
|
26
26
|
|
|
27
27
|
`install --apply` and `reconcile --apply` manage only explicit targets. The current structured lifecycle covers:
|
|
28
28
|
|
|
29
|
-
- Agent hook JSON: `.claude/settings.json`, `.codex/hooks.json`
|
|
29
|
+
- Agent hook JSON: `.claude/settings.json`, `.codex/hooks.json`, `.agents/hooks.json`
|
|
30
30
|
- Agent context files: `CLAUDE.md` and `AGENTS.md` managed sections. `AGENT.md` singular is not a PhaseGate managed target; treat it as user-owned content or migrate it manually. <!-- @work-item-id WI-174 -->
|
|
31
31
|
- Husky scripts when requested: `.husky/pre-commit`, `.husky/commit-msg`, `.husky/pre-push`
|
|
32
32
|
- CI workflow when requested: `.github/workflows/phasegate-aidlc-gate.yml`
|
|
33
|
-
- Agent skill links: `.claude/skills`, `.codex/skills`
|
|
33
|
+
- Agent skill links: `.claude/skills`, `.codex/skills`, `.agents/skills`
|
|
34
34
|
- Project shared bundled skills: selected `skills/<name>/` directories plus `skills/.harness-version`
|
|
35
35
|
- Project config: `phasegate.config.json` when absent, so installed agent hooks have a discoverable runtime config
|
|
36
36
|
- Package metadata: PhaseGate scripts and `devDependencies.phasegate` in `package.json`
|
|
@@ -48,7 +48,7 @@ If `.claude/*` or `.codex/*` already exists and is not a PhaseGate-managed perso
|
|
|
48
48
|
|
|
49
49
|
Personal agent context is also placed only where the runtime will read it. Claude Code uses `.claude/CLAUDE.md`. Codex uses root `AGENTS.md` when PhaseGate can create or manage it locally; if a team `AGENTS.md` already exists, PhaseGate leaves it unchanged and doctor reports `codex-context-missing` instead of creating `AGENTS.override.md`. <!-- @work-item-id WI-215 -->
|
|
50
50
|
|
|
51
|
-
Project install uses a different topology: selected bundled skills are deployed once to root `skills/`, and `.claude/skills` / `.codex/skills` point to that shared target. Existing root `skills/` is also a mergeable catalog: selected bundled skill directories are refreshed, selection-excluded or user-owned skills are not removed, and uninstall deletes only manifest-managed bundled skills plus metadata. If an older project install has the links but an empty `skills/` target, `phasegate doctor` reports the selected agent skill check and `phasegate reconcile --apply` repairs the shared skill bodies. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-216 -->
|
|
51
|
+
Project install uses a different topology: selected bundled skills are deployed once to root `skills/`, and `.claude/skills` / `.codex/skills` / `.agents/skills` point to that shared target. Existing root `skills/` is also a mergeable catalog: selected bundled skill directories are refreshed, selection-excluded or user-owned skills are not removed, and uninstall deletes only manifest-managed bundled skills plus metadata. If an older project install has the links but an empty `skills/` target, `phasegate doctor` reports the selected agent skill check and `phasegate reconcile --apply` repairs the shared skill bodies. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-216 -->
|
|
52
52
|
|
|
53
53
|
## Doctor Findings
|
|
54
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skills Overview
|
|
2
2
|
|
|
3
|
-
Phasegate provides 29 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills
|
|
3
|
+
Phasegate provides 29 skills covering the full AIDLC (AI-Driven Development Life Cycle). `npx phasegate init` and project `npx phasegate install` deploy skill bodies to root `skills/` and expose them to enabled agents through `.claude/skills/`, `.codex/skills/`, or `.agents/skills/` links. Personal install instead writes real local-only per-agent skill directories. <!-- @work-item-id WI-210 --> <!-- @work-item-id WI-385 -->
|
|
4
4
|
|
|
5
5
|
Bundled `SKILL.md` files include `languages: [typescript]` frontmatter so PhaseGate can distinguish current TypeScript-oriented guidance from future language-specific skill variants. The metadata is advisory for applicability and does not prevent non-TypeScript projects from installing the catalog. <!-- @work-item-id WI-212 -->
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export interface HandlePreToolUseOutput {
|
|
|
26
26
|
phaseGateBlockers?: string[];
|
|
27
27
|
storyReflectionBlockers?: string[];
|
|
28
28
|
storyReflectionWarnings?: string[];
|
|
29
|
-
fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
29
|
+
fullModeRejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
30
30
|
fullModeDominantCategory?: string;
|
|
31
31
|
nextAction?: string;
|
|
32
32
|
// Quick Mode が write を許可した際に dominant カテゴリを presentation 層に伝える。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unit agent-integration
|
|
3
|
+
* @layer application
|
|
4
|
+
* @work-item-id WI-385
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PayloadShape = "FLAT_SNAKE_CASE" | "FLAT_CAMEL_CASE" | "NESTED_TOOL_CALL";
|
|
8
|
+
|
|
9
|
+
export type HookResponseProfile = "LEGACY_EXIT_ONLY" | "COMPATIBILITY_DENY_ENVELOPE" | "TOP_LEVEL_DENY_ENVELOPE";
|
|
10
|
+
|
|
11
|
+
export interface CanonicalPreToolUseInput {
|
|
12
|
+
readonly path?: string;
|
|
13
|
+
readonly file_path?: string;
|
|
14
|
+
readonly paths?: readonly string[];
|
|
15
|
+
readonly command?: string;
|
|
16
|
+
readonly content?: string;
|
|
17
|
+
readonly old_string?: string;
|
|
18
|
+
readonly new_string?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NormalizedPreToolUseRequest {
|
|
22
|
+
readonly shape: PayloadShape;
|
|
23
|
+
readonly responseProfile: HookResponseProfile;
|
|
24
|
+
readonly cwd?: string;
|
|
25
|
+
readonly toolName: string;
|
|
26
|
+
readonly toolInput: CanonicalPreToolUseInput;
|
|
27
|
+
readonly inputTruncated: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PreToolUseNormalizationResult =
|
|
31
|
+
| { readonly ok: true; readonly request: NormalizedPreToolUseRequest }
|
|
32
|
+
| { readonly ok: false; readonly reason: string; readonly responseProfile?: HookResponseProfile };
|
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -278,7 +278,7 @@ export class HandlePreToolUseUseCase {
|
|
|
278
278
|
blockedFilePath: string | undefined,
|
|
279
279
|
result: {
|
|
280
280
|
requiresFullMode: boolean;
|
|
281
|
-
rejectionRule?: "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
|
|
281
|
+
rejectionRule?: "CATEGORY_NOT_ALLOWED" | "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
|
|
282
282
|
rejectionReason?: string;
|
|
283
283
|
dominantCategory?: string;
|
|
284
284
|
},
|
|
@@ -581,7 +581,7 @@ export class HandlePreToolUseUseCase {
|
|
|
581
581
|
// WI-363: .husky/ 配下は L0 runtime の実施点。Quick Mode でも書き換えさせない。
|
|
582
582
|
pattern: /(?:^|\/)\.husky\//,
|
|
583
583
|
message: (fp) =>
|
|
584
|
-
`保護ファイルへの書き込みがブロックされました: ${fp}\n.husky/ 配下は L0 runtime(pre-commit / commit-msg / pre-push)の実施点であり、Quick Mode でも変更できません。\n未導入 hook の配置は npx phasegate setup:agent --apply --with-husky を使用してください。\n
|
|
584
|
+
`保護ファイルへの書き込みがブロックされました: ${fp}\n.husky/ 配下は L0 runtime(pre-commit / commit-msg / pre-push)の実施点であり、Quick Mode でも変更できません。\n未導入 hook の配置は npx phasegate setup:agent --apply --with-husky を使用してください。\n意図的な手編集が必要な場合は、agent セッション外で人間が変更し、通常の検証を実行してください。`,
|
|
585
585
|
},
|
|
586
586
|
];
|
|
587
587
|
|
|
@@ -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
|
}
|
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 コマンド文字列から書き込み先ファイルパスを抽出する。
|
|
@@ -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 {
|
|
@@ -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
|
|