repo-harness 0.7.4 → 0.7.5
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/README.es.md +3 -3
- package/README.fr.md +3 -3
- package/README.ja.md +3 -3
- package/README.md +50 -8
- package/README.zh-CN.md +10 -3
- package/assets/hooks/lib/minimal-change.sh +52 -0
- package/assets/hooks/minimal-change-context.sh +16 -0
- package/assets/hooks/minimal-change-observer.sh +18 -0
- package/assets/hooks/prompt-guard.sh +149 -2
- package/assets/hooks/stop-orchestrator.sh +122 -4
- package/assets/reference-configs/minimal-change-hooks.md +80 -0
- package/assets/skill-version.json +6 -2
- package/assets/workflow-contract.v1.json +1 -0
- package/package.json +2 -2
- package/scripts/ensure-task-workflow.sh +17 -1
- package/scripts/lib/project-init-lib.sh +18 -2
- package/src/cli/commands/adopt-plan.ts +7 -0
- package/src/cli/commands/init.ts +16 -0
- package/src/cli/commands/mcp.ts +19 -3
- package/src/cli/hook/minimal-change-cli.ts +130 -0
- package/src/cli/hook/minimal-change-context.ts +57 -0
- package/src/cli/hook/minimal-change-policy.ts +197 -0
- package/src/cli/hook/minimal-change-signals.ts +601 -0
- package/src/cli/hook/route-registry.ts +6 -2
- package/src/cli/hook/runtime.ts +39 -2
- package/src/cli/hook-entry.ts +8 -0
- package/src/cli/installer/managed-entries.ts +1 -1
- package/src/cli/mcp/auth.ts +41 -5
- package/src/cli/mcp/instructions.ts +14 -9
- package/src/cli/mcp/oauth.ts +88 -21
- package/src/cli/mcp/paths.ts +13 -5
- package/src/cli/mcp/policy.ts +73 -21
- package/src/cli/mcp/reader-tools.ts +629 -0
- package/src/cli/mcp/server.ts +82 -13
- package/src/cli/mcp/session-store.ts +91 -0
- package/src/cli/mcp/setup.ts +194 -37
- package/src/cli/mcp/tools.ts +143 -40
- package/src/cli/mcp/transports/http.ts +187 -49
- package/src/cli/mcp/types.ts +8 -0
- package/src/cli/mcp/version.ts +10 -0
- package/src/cli/mcp/workspaces.ts +348 -0
- package/src/effects/repo-registry.ts +172 -0
package/README.es.md
CHANGED
|
@@ -85,7 +85,7 @@ artifacts.
|
|
|
85
85
|
## Novedades
|
|
86
86
|
|
|
87
87
|
Las notas de versión viven en [`docs/CHANGELOG.md`](docs/CHANGELOG.md). La línea
|
|
88
|
-
actual es `0.7.
|
|
88
|
+
actual es `0.7.5`.
|
|
89
89
|
|
|
90
90
|
## Cómo funciona
|
|
91
91
|
|
|
@@ -419,8 +419,8 @@ Guards habituales:
|
|
|
419
419
|
|
|
420
420
|
## Release actual
|
|
421
421
|
|
|
422
|
-
- npm package: `repo-harness@0.7.
|
|
423
|
-
- Generated workflow stamp: `repo-harness@0.7.
|
|
422
|
+
- npm package: `repo-harness@0.7.5`
|
|
423
|
+
- Generated workflow stamp: `repo-harness@0.7.5+template@0.7.5`
|
|
424
424
|
- GitHub repository: `Ancienttwo/repo-harness`
|
|
425
425
|
- Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
426
426
|
|
package/README.fr.md
CHANGED
|
@@ -85,7 +85,7 @@ l'emportent.
|
|
|
85
85
|
## Nouveautés
|
|
86
86
|
|
|
87
87
|
Les notes de version vivent dans [`docs/CHANGELOG.md`](docs/CHANGELOG.md). La
|
|
88
|
-
ligne actuelle est `0.7.
|
|
88
|
+
ligne actuelle est `0.7.5`.
|
|
89
89
|
|
|
90
90
|
## Comment ça marche
|
|
91
91
|
|
|
@@ -424,8 +424,8 @@ Guards courants :
|
|
|
424
424
|
|
|
425
425
|
## Release actuelle
|
|
426
426
|
|
|
427
|
-
- npm package : `repo-harness@0.7.
|
|
428
|
-
- Generated workflow stamp : `repo-harness@0.7.
|
|
427
|
+
- npm package : `repo-harness@0.7.5`
|
|
428
|
+
- Generated workflow stamp : `repo-harness@0.7.5+template@0.7.5`
|
|
429
429
|
- GitHub repository : `Ancienttwo/repo-harness`
|
|
430
430
|
- Release history : [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
431
431
|
|
package/README.ja.md
CHANGED
|
@@ -75,7 +75,7 @@ review、checks、handoff と食い違う場合は、source artifacts を優先
|
|
|
75
75
|
## What's New
|
|
76
76
|
|
|
77
77
|
リリースノートは [`docs/CHANGELOG.md`](docs/CHANGELOG.md) にあります。現在の
|
|
78
|
-
ラインは `0.7.
|
|
78
|
+
ラインは `0.7.5` です。
|
|
79
79
|
|
|
80
80
|
## 仕組み
|
|
81
81
|
|
|
@@ -398,8 +398,8 @@ hook がブロックしたときは、まず terminal の構造化された出
|
|
|
398
398
|
|
|
399
399
|
## 現在の Release
|
|
400
400
|
|
|
401
|
-
- npm package:`repo-harness@0.7.
|
|
402
|
-
- Generated workflow stamp:`repo-harness@0.7.
|
|
401
|
+
- npm package:`repo-harness@0.7.5`
|
|
402
|
+
- Generated workflow stamp:`repo-harness@0.7.5+template@0.7.5`
|
|
403
403
|
- GitHub repository:`Ancienttwo/repo-harness`
|
|
404
404
|
- Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
405
405
|
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ active plan, contract, review, checks, or handoff, the source artifacts win.
|
|
|
78
78
|
## What's New
|
|
79
79
|
|
|
80
80
|
Release notes live in [`docs/CHANGELOG.md`](docs/CHANGELOG.md). The current line
|
|
81
|
-
is `0.7.
|
|
81
|
+
is `0.7.5`.
|
|
82
82
|
|
|
83
83
|
## How It Works
|
|
84
84
|
|
|
@@ -97,6 +97,15 @@ it resolves hooks central-first through the packaged install or
|
|
|
97
97
|
`~/.repo-harness/hooks/`, with repo policy able to pin self-host development
|
|
98
98
|
back to `.ai/hooks/*`.
|
|
99
99
|
|
|
100
|
+
Minimal-change hooks sit inside that same route surface without adding a public
|
|
101
|
+
adapter route. `SessionStart` and allowed execution prompts print advisory
|
|
102
|
+
context when policy opts in, `PostToolUse.edit` can write bounded change signals
|
|
103
|
+
to `.ai/harness/checks/minimal-change.latest.json` when
|
|
104
|
+
`post_edit_observer:true` is explicitly enabled, and `Stop` records the latest
|
|
105
|
+
review summary in the handoff. Missing or malformed policy defaults to off; even
|
|
106
|
+
`mode: "enforce"` is normalized to advisory behavior so tests, contracts, and
|
|
107
|
+
human review stay the enforcement boundary.
|
|
108
|
+
|
|
100
109
|
For `UserPromptSubmit`, the public adapter contract stays
|
|
101
110
|
`repo-harness-hook UserPromptSubmit --route default`. The CLI route registry
|
|
102
111
|
dispatches that route to `.ai/hooks/prompt-guard.sh`. The shell hook remains the
|
|
@@ -336,22 +345,55 @@ before applying anything.
|
|
|
336
345
|
|
|
337
346
|
## MCP Connector Quickstart
|
|
338
347
|
|
|
339
|
-
As an optional sidecar, `repo-harness mcp` exposes
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
Codex remains the executor.
|
|
348
|
+
As an optional sidecar, `repo-harness mcp` exposes workflow artifacts to MCP
|
|
349
|
+
clients through the default `planner` profile. ChatGPT acts as a
|
|
350
|
+
planner/reviewer that reads state and moves an idea through PRD, checklist
|
|
351
|
+
Sprint, and Codex goal handoff artifacts — with no source-code write access,
|
|
352
|
+
arbitrary shell execution, or default Codex runner. Codex remains the executor.
|
|
353
|
+
|
|
354
|
+
The same `planner` Connector also exposes read-only workspace tools for
|
|
355
|
+
registered adopted repos. Use `discover_harness_repos` first, then pass
|
|
356
|
+
`repo_path` to workflow tools and use `list_allowed_roots`, `open_workspace`,
|
|
357
|
+
`tree`, `search_text`, and `read_text` to inspect non-ignored docs/source while
|
|
358
|
+
retaining deny rules for secrets, private keys, `.git`, and dependency/build
|
|
359
|
+
output. External non-repo local roots require explicit `--allow-root`
|
|
360
|
+
authorization.
|
|
344
361
|
|
|
345
362
|
This sidecar assumes the CLI is already installed from
|
|
346
363
|
[First 5 Minutes](#first-5-minutes). Use it when you want ChatGPT to plan
|
|
347
364
|
against the real repo state and Codex to execute the resulting file-backed
|
|
348
365
|
Sprint.
|
|
349
366
|
|
|
367
|
+
The ChatGPT Connector registers one endpoint URL, not one repository per URL.
|
|
368
|
+
Adopted repos are discovered from `~/.repo-harness/registered-repos.json`, which
|
|
369
|
+
is updated by `repo-harness adopt`, `repo-harness init`, and user-scope ChatGPT
|
|
370
|
+
setup. Stale registry entries are ignored unless the live repo still has
|
|
371
|
+
repo-harness adoption markers.
|
|
372
|
+
|
|
350
373
|
```bash
|
|
351
374
|
repo-harness mcp setup chatgpt --repo .
|
|
352
375
|
repo-harness mcp serve --repo . --transport http --host 127.0.0.1 --port 8765 --profile planner
|
|
353
376
|
```
|
|
354
377
|
|
|
378
|
+
User-scope global Connector setup:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
repo-harness mcp setup chatgpt --scope user --repo . --endpoint <https-url>/mcp
|
|
382
|
+
repo-harness mcp serve --repo . --transport http --host 127.0.0.1 --port 8765 --profile planner
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Optional external reader roots:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
repo-harness mcp setup chatgpt \
|
|
389
|
+
--scope user \
|
|
390
|
+
--repo . \
|
|
391
|
+
--enable-reader \
|
|
392
|
+
--allow-root "$HOME/Documents" \
|
|
393
|
+
--allow-root "$HOME/Projects" \
|
|
394
|
+
--endpoint <https-url>/mcp
|
|
395
|
+
```
|
|
396
|
+
|
|
355
397
|
Expose that local server through an HTTPS tunnel and create a ChatGPT Connector
|
|
356
398
|
with the `/mcp` URL. The generated guide is written to:
|
|
357
399
|
|
|
@@ -493,8 +535,8 @@ Most common guards:
|
|
|
493
535
|
|
|
494
536
|
## Current Release
|
|
495
537
|
|
|
496
|
-
- npm package: `repo-harness@0.7.
|
|
497
|
-
- Generated workflow stamp: `repo-harness@0.7.
|
|
538
|
+
- npm package: `repo-harness@0.7.5`
|
|
539
|
+
- Generated workflow stamp: `repo-harness@0.7.5+template@0.7.5`
|
|
498
540
|
- GitHub repository: `Ancienttwo/repo-harness`
|
|
499
541
|
- Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
500
542
|
|
package/README.zh-CN.md
CHANGED
|
@@ -69,7 +69,7 @@ review、checks 或 handoff 冲突,以 source artifacts 为准。
|
|
|
69
69
|
|
|
70
70
|
## What's New
|
|
71
71
|
|
|
72
|
-
Release notes 见 [`docs/CHANGELOG.md`](docs/CHANGELOG.md),当前版本线是 `0.7.
|
|
72
|
+
Release notes 见 [`docs/CHANGELOG.md`](docs/CHANGELOG.md),当前版本线是 `0.7.5`。
|
|
73
73
|
|
|
74
74
|
## 工作原理
|
|
75
75
|
|
|
@@ -86,6 +86,13 @@ Release notes 见 [`docs/CHANGELOG.md`](docs/CHANGELOG.md),当前版本线是
|
|
|
86
86
|
时按 central-first 解析 packaged install 或 `~/.repo-harness/hooks/`,repo policy
|
|
87
87
|
也可以把自托管开发钉回 `.ai/hooks/*`。
|
|
88
88
|
|
|
89
|
+
minimal-change hooks 复用同一套路由 surface,不新增公开 adapter route。`SessionStart`
|
|
90
|
+
和允许执行的 prompt 只在 policy opt in 时打印 advisory context;只有显式启用
|
|
91
|
+
`post_edit_observer:true` 时,`PostToolUse.edit` 才会把有界改动信号写到
|
|
92
|
+
`.ai/harness/checks/minimal-change.latest.json`,`Stop` 把最新 review 摘要写进 handoff。
|
|
93
|
+
缺失或损坏的 policy 默认 off;即使配置 `mode: "enforce"` 也会归一化为 advisory 行为,
|
|
94
|
+
真正的 enforcement boundary 仍然是 tests、contracts 和 human review。
|
|
95
|
+
|
|
89
96
|
对 `UserPromptSubmit` 来说,公开 adapter contract 仍然是
|
|
90
97
|
`repo-harness-hook UserPromptSubmit --route default`。CLI route registry 会把这个
|
|
91
98
|
route dispatch 到 `.ai/hooks/prompt-guard.sh`。Shell hook 继续负责 host JSON 解析、
|
|
@@ -437,8 +444,8 @@ hook block 工作时,先看 terminal 里的结构化输出。核心字段是
|
|
|
437
444
|
|
|
438
445
|
## 当前 Release
|
|
439
446
|
|
|
440
|
-
- npm package:`repo-harness@0.7.
|
|
441
|
-
- Generated workflow stamp:`repo-harness@0.7.
|
|
447
|
+
- npm package:`repo-harness@0.7.5`
|
|
448
|
+
- Generated workflow stamp:`repo-harness@0.7.5+template@0.7.5`
|
|
442
449
|
- GitHub repository:`Ancienttwo/repo-harness`
|
|
443
450
|
- Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
444
451
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared minimal-change hook adapter helpers.
|
|
3
|
+
|
|
4
|
+
minimal_change_post_edit_enabled() {
|
|
5
|
+
local repo_root policy_file mode observer compact
|
|
6
|
+
|
|
7
|
+
repo_root="${HOOK_REPO_ROOT:-$(pwd)}"
|
|
8
|
+
policy_file="$repo_root/.ai/harness/policy.json"
|
|
9
|
+
[[ -f "$policy_file" ]] || return 1
|
|
10
|
+
|
|
11
|
+
if command -v jq >/dev/null 2>&1; then
|
|
12
|
+
mode="$(jq -r '.minimal_change.mode // "off"' "$policy_file" 2>/dev/null || true)"
|
|
13
|
+
observer="$(jq -r '.minimal_change.post_edit_observer // false' "$policy_file" 2>/dev/null || true)"
|
|
14
|
+
[[ "$mode" != "off" && "$observer" == "true" ]]
|
|
15
|
+
return $?
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
compact="$(tr -d '[:space:]' < "$policy_file" 2>/dev/null || true)"
|
|
19
|
+
[[ "$compact" == *'"minimal_change":{'* ]] || return 1
|
|
20
|
+
[[ "$compact" == *'"mode":"advice"'* || "$compact" == *'"mode":"enforce"'* ]] || return 1
|
|
21
|
+
[[ "$compact" == *'"post_edit_observer":true'* ]]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
minimal_change_hook_entry() {
|
|
25
|
+
local lib_dir hooks_dir source_hook_cli
|
|
26
|
+
|
|
27
|
+
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
hooks_dir="$(cd "$lib_dir/.." && pwd)"
|
|
29
|
+
source_hook_cli="$(cd "$hooks_dir/../.." 2>/dev/null && pwd)/src/cli/hook-entry.ts"
|
|
30
|
+
|
|
31
|
+
if [[ -n "${REPO_HARNESS_HOOK_CLI:-}" && -f "${REPO_HARNESS_HOOK_CLI:-}" ]] && command -v bun >/dev/null 2>&1; then
|
|
32
|
+
bun "$REPO_HARNESS_HOOK_CLI" minimal-change "$@"
|
|
33
|
+
return $?
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [[ -f "$source_hook_cli" ]] && command -v bun >/dev/null 2>&1; then
|
|
37
|
+
bun "$source_hook_cli" minimal-change "$@"
|
|
38
|
+
return $?
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [[ -n "${HOOK_REPO_ROOT:-}" && -f "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" ]] && command -v bun >/dev/null 2>&1; then
|
|
42
|
+
bun "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" minimal-change "$@"
|
|
43
|
+
return $?
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if command -v repo-harness-hook >/dev/null 2>&1; then
|
|
47
|
+
repo-harness-hook minimal-change "$@"
|
|
48
|
+
return $?
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
return 127
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Minimal-change advisory context — SessionStart.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
# shellcheck source=/dev/null
|
|
8
|
+
. "$SCRIPT_DIR/hook-input.sh"
|
|
9
|
+
# shellcheck source=/dev/null
|
|
10
|
+
. "$SCRIPT_DIR/lib/minimal-change.sh"
|
|
11
|
+
|
|
12
|
+
if output="$(minimal_change_hook_entry context --phase session 2>/dev/null)"; then
|
|
13
|
+
[[ -n "$output" ]] && printf '%s\n' "$output"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Minimal-change objective signal observer — PostToolUse on Edit|Write.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
# shellcheck source=/dev/null
|
|
8
|
+
. "$SCRIPT_DIR/hook-input.sh"
|
|
9
|
+
# shellcheck source=/dev/null
|
|
10
|
+
. "$SCRIPT_DIR/lib/minimal-change.sh"
|
|
11
|
+
|
|
12
|
+
file_path="$(hook_get_file_path "${1:-}")"
|
|
13
|
+
[[ -z "$file_path" ]] && exit 0
|
|
14
|
+
minimal_change_post_edit_enabled || exit 0
|
|
15
|
+
|
|
16
|
+
minimal_change_hook_entry signals --phase post-edit --path "$file_path" >/dev/null 2>&1 || true
|
|
17
|
+
|
|
18
|
+
exit 0
|
|
@@ -22,6 +22,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
22
22
|
. "$SCRIPT_DIR/lib/workflow-state.sh"
|
|
23
23
|
# shellcheck source=/dev/null
|
|
24
24
|
. "$SCRIPT_DIR/lib/session-state.sh"
|
|
25
|
+
if [[ -f "$SCRIPT_DIR/lib/minimal-change.sh" ]]; then
|
|
26
|
+
# shellcheck source=/dev/null
|
|
27
|
+
. "$SCRIPT_DIR/lib/minimal-change.sh"
|
|
28
|
+
fi
|
|
25
29
|
|
|
26
30
|
# --- Filesystem/worktree helpers (shell-owned authority) ---
|
|
27
31
|
|
|
@@ -186,6 +190,111 @@ prompt_intent_text() {
|
|
|
186
190
|
fi
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
prompt_nearest_existing_path() {
|
|
194
|
+
local path="$1"
|
|
195
|
+
while [[ "$path" != "/" && ! -e "$path" && ! -L "$path" ]]; do
|
|
196
|
+
path="$(dirname "$path")"
|
|
197
|
+
done
|
|
198
|
+
[[ -e "$path" || -L "$path" ]] || return 1
|
|
199
|
+
printf '%s' "$path"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
prompt_resolve_probe_path() {
|
|
203
|
+
local path="$1"
|
|
204
|
+
local base target hops followed state
|
|
205
|
+
hops=0
|
|
206
|
+
followed=0
|
|
207
|
+
|
|
208
|
+
while true; do
|
|
209
|
+
path="$(prompt_nearest_existing_path "$path")" || {
|
|
210
|
+
printf 'SKIP\t%s' "$1"
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
if [[ ! -L "$path" ]]; then
|
|
214
|
+
state="OK"
|
|
215
|
+
[[ "$followed" -eq 1 ]] && state="OK_SYMLINK"
|
|
216
|
+
printf '%s\t%s' "$state" "$path"
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
hops=$((hops + 1))
|
|
221
|
+
if [[ "$hops" -gt 20 ]]; then
|
|
222
|
+
printf 'FAIL\t%s' "$path"
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
target="$(readlink "$path" 2>/dev/null || true)"
|
|
226
|
+
if [[ -z "$target" ]]; then
|
|
227
|
+
printf 'FAIL\t%s' "$path"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
followed=1
|
|
231
|
+
|
|
232
|
+
if [[ "$target" == /* ]]; then
|
|
233
|
+
path="$target"
|
|
234
|
+
else
|
|
235
|
+
base="$(dirname "$path")"
|
|
236
|
+
path="$base/$target"
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
prompt_foreign_repo_root() {
|
|
242
|
+
local current_root current_real candidates raw candidate probe_record probe_state probe dir dir_real root root_real
|
|
243
|
+
current_root="${HOOK_REPO_ROOT:-$(pwd)}"
|
|
244
|
+
current_real="$(cd "$current_root" 2>/dev/null && pwd -P || true)"
|
|
245
|
+
[[ -n "$current_real" ]] || current_real="$current_root"
|
|
246
|
+
|
|
247
|
+
candidates="$(printf '%s\n' "$PROMPT_INTENT_TEXT" | awk '
|
|
248
|
+
{
|
|
249
|
+
line = $0
|
|
250
|
+
while (match(line, /\/[^[:space:]<>"`]+/)) {
|
|
251
|
+
print substr(line, RSTART, RLENGTH)
|
|
252
|
+
line = substr(line, RSTART + RLENGTH)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
' | sed -E 's/[][),.;:]+$//' | sort -u)"
|
|
256
|
+
|
|
257
|
+
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
|
258
|
+
[[ -n "$raw" ]] || continue
|
|
259
|
+
candidate="$raw"
|
|
260
|
+
|
|
261
|
+
probe_record="$(prompt_resolve_probe_path "$candidate")"
|
|
262
|
+
probe_state="${probe_record%%$'\t'*}"
|
|
263
|
+
probe="${probe_record#*$'\t'}"
|
|
264
|
+
if [[ "$probe_state" == "FAIL" ]]; then
|
|
265
|
+
printf '%s' "$probe"
|
|
266
|
+
return 0
|
|
267
|
+
fi
|
|
268
|
+
[[ "$probe_state" == "OK" || "$probe_state" == "OK_SYMLINK" ]] || continue
|
|
269
|
+
[[ -n "$probe" ]] || continue
|
|
270
|
+
if [[ -d "$probe" ]]; then
|
|
271
|
+
dir="$probe"
|
|
272
|
+
else
|
|
273
|
+
dir="$(dirname "$probe")"
|
|
274
|
+
fi
|
|
275
|
+
dir_real="$(cd "$dir" 2>/dev/null && pwd -P || true)"
|
|
276
|
+
[[ -n "$dir_real" ]] || continue
|
|
277
|
+
|
|
278
|
+
root="$(git -C "$dir_real" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
279
|
+
if [[ -z "$root" ]]; then
|
|
280
|
+
if [[ "$probe_state" == "OK_SYMLINK" && "$dir_real" != "$current_real" && "$dir_real" != "$current_real"/* ]]; then
|
|
281
|
+
printf '%s' "$dir_real"
|
|
282
|
+
return 0
|
|
283
|
+
fi
|
|
284
|
+
continue
|
|
285
|
+
fi
|
|
286
|
+
root_real="$(cd "$root" 2>/dev/null && pwd -P || true)"
|
|
287
|
+
[[ -n "$root_real" ]] || root_real="$root"
|
|
288
|
+
|
|
289
|
+
if [[ "$root_real" != "$current_real" ]]; then
|
|
290
|
+
printf '%s' "$root"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
done <<< "$candidates"
|
|
294
|
+
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
189
298
|
# --- Decision engine invocation ---
|
|
190
299
|
|
|
191
300
|
prompt_guard_decision_command() {
|
|
@@ -429,6 +538,14 @@ emit_pending_orchestration_capture_gate() {
|
|
|
429
538
|
maybe_start_plan_workflow() {
|
|
430
539
|
pg_fact THINK_PLAN_START || return 0
|
|
431
540
|
|
|
541
|
+
local foreign_repo
|
|
542
|
+
foreign_repo="$(prompt_foreign_repo_root || true)"
|
|
543
|
+
if [[ -n "$foreign_repo" ]]; then
|
|
544
|
+
echo "[RepoIsolationGate] Think/plan intent references a different git repo: $foreign_repo"
|
|
545
|
+
echo "[RepoIsolationGate] Skipping automatic Draft plan workflow in ${HOOK_REPO_ROOT:-$(pwd)}. Continue from the referenced repo before creating repo-local workflow files."
|
|
546
|
+
return 0
|
|
547
|
+
fi
|
|
548
|
+
|
|
432
549
|
if [[ ! -x "scripts/ensure-task-workflow.sh" ]]; then
|
|
433
550
|
echo "[PlanStartGate] Think/plan intent detected, but scripts/ensure-task-workflow.sh is missing. Continue with planning and capture manually."
|
|
434
551
|
return 0
|
|
@@ -508,6 +625,14 @@ normalize_plan_slug() {
|
|
|
508
625
|
maybe_capture_embedded_approved_plan() {
|
|
509
626
|
pg_fact EMBEDDED_APPROVED_PLAN || pg_fact PLAN_SHAPED_MARKDOWN || return 0
|
|
510
627
|
|
|
628
|
+
local foreign_repo
|
|
629
|
+
foreign_repo="$(prompt_foreign_repo_root || true)"
|
|
630
|
+
if [[ -n "$foreign_repo" ]]; then
|
|
631
|
+
echo "[RepoIsolationGate] Embedded plan references a different git repo: $foreign_repo"
|
|
632
|
+
echo "[RepoIsolationGate] Skipping capture-plan in ${HOOK_REPO_ROOT:-$(pwd)}. Re-run the prompt from the referenced repo before projecting workflow files."
|
|
633
|
+
return 0
|
|
634
|
+
fi
|
|
635
|
+
|
|
511
636
|
if [[ ! -x "scripts/capture-plan.sh" ]]; then
|
|
512
637
|
echo "[PlanCaptureGate] Embedded approved plan detected, but scripts/capture-plan.sh is missing."
|
|
513
638
|
hook_structured_error \
|
|
@@ -860,6 +985,19 @@ emit_workflow_file_guards() {
|
|
|
860
985
|
fi
|
|
861
986
|
}
|
|
862
987
|
|
|
988
|
+
emit_minimal_change_prompt_advice() {
|
|
989
|
+
local output
|
|
990
|
+
|
|
991
|
+
[[ "${PG_ACTION:-}" == "allow" ]] || return 0
|
|
992
|
+
[[ -n "${PG_INTENT:-}" ]] || return 0
|
|
993
|
+
declare -F minimal_change_hook_entry >/dev/null 2>&1 || return 0
|
|
994
|
+
|
|
995
|
+
if output="$(minimal_change_hook_entry context --phase prompt --intent "$PG_INTENT" 2>/dev/null)"; then
|
|
996
|
+
[[ -n "$output" ]] && printf '%s\n' "$output"
|
|
997
|
+
fi
|
|
998
|
+
return 0
|
|
999
|
+
}
|
|
1000
|
+
|
|
863
1001
|
# --- Main flow ---
|
|
864
1002
|
|
|
865
1003
|
PROMPT_TEXT="$(hook_get_prompt "${1:-}")"
|
|
@@ -893,8 +1031,14 @@ plan_start_intent=0
|
|
|
893
1031
|
pg_fact PLAN_START && plan_start_intent=1
|
|
894
1032
|
|
|
895
1033
|
plan_research_ready=1
|
|
1034
|
+
plan_repo_isolation_skip=0
|
|
896
1035
|
if [ "$plan_start_intent" -eq 1 ]; then
|
|
897
|
-
|
|
1036
|
+
foreign_repo="$(prompt_foreign_repo_root || true)"
|
|
1037
|
+
if [[ -n "$foreign_repo" ]]; then
|
|
1038
|
+
plan_repo_isolation_skip=1
|
|
1039
|
+
echo "[RepoIsolationGate] Think/plan intent references a different git repo: $foreign_repo"
|
|
1040
|
+
echo "[RepoIsolationGate] Skipping automatic Draft plan workflow in ${HOOK_REPO_ROOT:-$(pwd)}. Continue from the referenced repo before creating repo-local workflow files."
|
|
1041
|
+
elif ! has_research_for_new_plan; then
|
|
898
1042
|
latest_plan="$(get_latest_plan || true)"
|
|
899
1043
|
if [[ -n "$latest_plan" ]]; then
|
|
900
1044
|
plan_research_ready=0
|
|
@@ -908,7 +1052,9 @@ if [ "$plan_start_intent" -eq 1 ]; then
|
|
|
908
1052
|
fi
|
|
909
1053
|
|
|
910
1054
|
if [ "$implement_intent" -eq 0 ] && [ "$done_intent" -eq 0 ]; then
|
|
911
|
-
if [ "$plan_start_intent" -eq 1 ] && [ "$
|
|
1055
|
+
if [ "$plan_start_intent" -eq 1 ] && [ "$plan_repo_isolation_skip" -eq 1 ]; then
|
|
1056
|
+
:
|
|
1057
|
+
elif [ "$plan_start_intent" -eq 1 ] && [ "$plan_research_ready" -eq 0 ]; then
|
|
912
1058
|
echo "[PlanStartGate] Skipping automatic Draft plan workflow until research is refreshed."
|
|
913
1059
|
else
|
|
914
1060
|
maybe_start_plan_workflow
|
|
@@ -929,6 +1075,7 @@ if [ "$implement_intent" -eq 1 ]; then
|
|
|
929
1075
|
prompt_guard_refresh_state
|
|
930
1076
|
prompt_guard_engine_call
|
|
931
1077
|
render_prompt_guard_action "$PG_ACTION"
|
|
1078
|
+
emit_minimal_change_prompt_advice
|
|
932
1079
|
fi
|
|
933
1080
|
|
|
934
1081
|
if [ "$done_intent" -eq 1 ]; then
|
|
@@ -10,6 +10,17 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
10
10
|
. "$SCRIPT_DIR/hook-input.sh"
|
|
11
11
|
# shellcheck source=/dev/null
|
|
12
12
|
. "$SCRIPT_DIR/lib/workflow-state.sh"
|
|
13
|
+
if [[ -f "$SCRIPT_DIR/lib/minimal-change.sh" ]]; then
|
|
14
|
+
# shellcheck source=/dev/null
|
|
15
|
+
. "$SCRIPT_DIR/lib/minimal-change.sh"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
MINIMAL_CHANGE_REVIEW_SUMMARY=""
|
|
19
|
+
MINIMAL_CHANGE_REVIEW_VERDICT=""
|
|
20
|
+
MINIMAL_CHANGE_REVIEW_PATH=""
|
|
21
|
+
MINIMAL_CHANGE_REVIEW_FINDINGS="0"
|
|
22
|
+
MINIMAL_CHANGE_HANDOFF_BEGIN="<!-- repo-harness:minimal-change-review begin -->"
|
|
23
|
+
MINIMAL_CHANGE_HANDOFF_END="<!-- repo-harness:minimal-change-review end -->"
|
|
13
24
|
|
|
14
25
|
plan_completeness_state_file() {
|
|
15
26
|
workflow_repo_relative_path \
|
|
@@ -131,6 +142,106 @@ emit_stop_block_json() {
|
|
|
131
142
|
printf '{"decision":"block","reason":"%s"}\n' "$(workflow_json_escape "$reason")"
|
|
132
143
|
}
|
|
133
144
|
|
|
145
|
+
minimal_change_parse_review_json() {
|
|
146
|
+
local raw="$1"
|
|
147
|
+
local parsed
|
|
148
|
+
|
|
149
|
+
if command -v jq >/dev/null 2>&1; then
|
|
150
|
+
parsed="$(printf '%s' "$raw" | jq -r '
|
|
151
|
+
def line($finding):
|
|
152
|
+
"- [" + ($finding.tag // "review") + "] " + ($finding.path // ".") + ": " + ($finding.question // $finding.evidence // "review required");
|
|
153
|
+
[
|
|
154
|
+
(.verdict // "unknown"),
|
|
155
|
+
(.report_path // ".ai/harness/checks/minimal-change.latest.json"),
|
|
156
|
+
((.findings // []) | length | tostring),
|
|
157
|
+
(
|
|
158
|
+
if (.verdict // "unknown") == "disabled" then ""
|
|
159
|
+
elif (.findings // [] | length) == 0 then ""
|
|
160
|
+
else
|
|
161
|
+
"[MinimalChange] Non-blocking review (" + (.report_path // ".ai/harness/checks/minimal-change.latest.json") + "):\n" +
|
|
162
|
+
((.findings // [])[0:5] | map(line(.)) | join("\n"))
|
|
163
|
+
end
|
|
164
|
+
)
|
|
165
|
+
] | @tsv
|
|
166
|
+
' 2>/dev/null)" || return 1
|
|
167
|
+
elif command -v bun >/dev/null 2>&1; then
|
|
168
|
+
parsed="$(MINIMAL_CHANGE_RAW="$raw" bun -e '
|
|
169
|
+
const report = JSON.parse(process.env.MINIMAL_CHANGE_RAW || "{}");
|
|
170
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
171
|
+
const path = report.report_path || ".ai/harness/checks/minimal-change.latest.json";
|
|
172
|
+
let summary = "";
|
|
173
|
+
if (report.verdict !== "disabled" && findings.length > 0) {
|
|
174
|
+
summary = `[MinimalChange] Non-blocking review (${path}):\n` + findings.slice(0, 5).map((finding) => {
|
|
175
|
+
const tag = finding.tag || "review";
|
|
176
|
+
const file = finding.path || ".";
|
|
177
|
+
const question = finding.question || finding.evidence || "review required";
|
|
178
|
+
return `- [${tag}] ${file}: ${question}`;
|
|
179
|
+
}).join("\n");
|
|
180
|
+
}
|
|
181
|
+
console.log([report.verdict || "unknown", path, String(findings.length), summary].join("\t"));
|
|
182
|
+
' 2>/dev/null)" || return 1
|
|
183
|
+
else
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
IFS=$'\t' read -r \
|
|
188
|
+
MINIMAL_CHANGE_REVIEW_VERDICT \
|
|
189
|
+
MINIMAL_CHANGE_REVIEW_PATH \
|
|
190
|
+
MINIMAL_CHANGE_REVIEW_FINDINGS \
|
|
191
|
+
MINIMAL_CHANGE_REVIEW_SUMMARY <<< "$parsed"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
minimal_change_refresh_review() {
|
|
195
|
+
local raw
|
|
196
|
+
|
|
197
|
+
declare -F minimal_change_hook_entry >/dev/null 2>&1 || return 0
|
|
198
|
+
raw="$(minimal_change_hook_entry review --phase stop 2>/dev/null || true)"
|
|
199
|
+
[[ "$raw" == \{* ]] || return 0
|
|
200
|
+
minimal_change_parse_review_json "$raw" || return 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
minimal_change_render_handoff_section() {
|
|
204
|
+
printf '%s\n' "$MINIMAL_CHANGE_HANDOFF_BEGIN"
|
|
205
|
+
printf '\n## Minimal Change Review\n\n'
|
|
206
|
+
printf -- '- Report: `%s`\n' "${MINIMAL_CHANGE_REVIEW_PATH:-.ai/harness/checks/minimal-change.latest.json}"
|
|
207
|
+
printf -- '- Verdict: `%s`\n' "${MINIMAL_CHANGE_REVIEW_VERDICT:-unknown}"
|
|
208
|
+
printf -- '- Findings: `%s`\n' "${MINIMAL_CHANGE_REVIEW_FINDINGS:-0}"
|
|
209
|
+
if [[ -n "$MINIMAL_CHANGE_REVIEW_SUMMARY" ]]; then
|
|
210
|
+
printf '\n%s\n' "$MINIMAL_CHANGE_REVIEW_SUMMARY"
|
|
211
|
+
fi
|
|
212
|
+
printf '\n%s\n' "$MINIMAL_CHANGE_HANDOFF_END"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
minimal_change_append_handoff() {
|
|
216
|
+
local handoff_file tmp_file
|
|
217
|
+
|
|
218
|
+
[[ -n "$MINIMAL_CHANGE_REVIEW_VERDICT" ]] || return 0
|
|
219
|
+
[[ "$MINIMAL_CHANGE_REVIEW_VERDICT" != "disabled" ]] || return 0
|
|
220
|
+
handoff_file="$(workflow_handoff_file)"
|
|
221
|
+
mkdir -p "$(dirname "$handoff_file")"
|
|
222
|
+
tmp_file="$(mktemp "${handoff_file}.minimal-change.XXXXXX")" || return 0
|
|
223
|
+
|
|
224
|
+
if [[ -f "$handoff_file" ]]; then
|
|
225
|
+
awk -v begin="$MINIMAL_CHANGE_HANDOFF_BEGIN" -v end="$MINIMAL_CHANGE_HANDOFF_END" '
|
|
226
|
+
$0 == begin { skip = 1; next }
|
|
227
|
+
$0 == end { skip = 0; next }
|
|
228
|
+
!skip { print }
|
|
229
|
+
' "$handoff_file" > "$tmp_file" || {
|
|
230
|
+
rm -f "$tmp_file"
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
printf '\n' >> "$tmp_file"
|
|
236
|
+
minimal_change_render_handoff_section >> "$tmp_file"
|
|
237
|
+
mv "$tmp_file" "$handoff_file"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
minimal_change_reason_suffix() {
|
|
241
|
+
[[ -n "$MINIMAL_CHANGE_REVIEW_SUMMARY" ]] || return 0
|
|
242
|
+
printf '\n\n%s' "$MINIMAL_CHANGE_REVIEW_SUMMARY"
|
|
243
|
+
}
|
|
244
|
+
|
|
134
245
|
delegation_state_paths_json() {
|
|
135
246
|
local state_dir
|
|
136
247
|
|
|
@@ -262,11 +373,18 @@ should_run_plan_completeness_gate() {
|
|
|
262
373
|
assistant_message_looks_like_plan "$last_message"
|
|
263
374
|
}
|
|
264
375
|
|
|
265
|
-
refresh_handoff
|
|
266
|
-
|
|
267
376
|
stop_hook_active="$(hook_json_get '.stop_hook_active' 'false')"
|
|
268
377
|
last_assistant_message="$(hook_json_get '.last_assistant_message' '')"
|
|
269
378
|
|
|
379
|
+
if [[ "$stop_hook_active" == "true" ]]; then
|
|
380
|
+
exit 0
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
refresh_handoff
|
|
384
|
+
|
|
385
|
+
minimal_change_refresh_review
|
|
386
|
+
minimal_change_append_handoff
|
|
387
|
+
|
|
270
388
|
if should_run_plan_completeness_gate "$stop_hook_active" "$last_assistant_message"; then
|
|
271
389
|
signature="$(plan_completeness_signature)"
|
|
272
390
|
if [[ "$(plan_completeness_last_signature 2>/dev/null || true)" != "$signature" ]]; then
|
|
@@ -275,12 +393,12 @@ if should_run_plan_completeness_gate "$stop_hook_active" "$last_assistant_messag
|
|
|
275
393
|
guidance="$(plan_completeness_capture_guidance)"
|
|
276
394
|
emit_stop_block_json "[PlanCompletenessGate] A first planning answer was produced while pending orchestration is still open: ${summary}
|
|
277
395
|
|
|
278
|
-
${guidance}"
|
|
396
|
+
${guidance}$(minimal_change_reason_suffix)"
|
|
279
397
|
exit 0
|
|
280
398
|
fi
|
|
281
399
|
fi
|
|
282
400
|
|
|
283
401
|
if delegation_should_block "$stop_hook_active"; then
|
|
284
402
|
delegation_mark_fallback_used
|
|
285
|
-
emit_stop_block_json "[DelegationFallback] This turn explicitly requested bounded delegation, but no SubagentStart event was observed. Continue the task now by spawning the independent explorer/reviewer or isolated worker workstreams first when at least two independent workstreams exist, wait for them, reconcile their findings in the parent, then complete the response. Do not spawn for a trivial or strictly sequential task
|
|
403
|
+
emit_stop_block_json "[DelegationFallback] This turn explicitly requested bounded delegation, but no SubagentStart event was observed. Continue the task now by spawning the independent explorer/reviewer or isolated worker workstreams first when at least two independent workstreams exist, wait for them, reconcile their findings in the parent, then complete the response. Do not spawn for a trivial or strictly sequential task.$(minimal_change_reason_suffix)"
|
|
286
404
|
fi
|