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.
Files changed (42) hide show
  1. package/README.es.md +3 -3
  2. package/README.fr.md +3 -3
  3. package/README.ja.md +3 -3
  4. package/README.md +50 -8
  5. package/README.zh-CN.md +10 -3
  6. package/assets/hooks/lib/minimal-change.sh +52 -0
  7. package/assets/hooks/minimal-change-context.sh +16 -0
  8. package/assets/hooks/minimal-change-observer.sh +18 -0
  9. package/assets/hooks/prompt-guard.sh +149 -2
  10. package/assets/hooks/stop-orchestrator.sh +122 -4
  11. package/assets/reference-configs/minimal-change-hooks.md +80 -0
  12. package/assets/skill-version.json +6 -2
  13. package/assets/workflow-contract.v1.json +1 -0
  14. package/package.json +2 -2
  15. package/scripts/ensure-task-workflow.sh +17 -1
  16. package/scripts/lib/project-init-lib.sh +18 -2
  17. package/src/cli/commands/adopt-plan.ts +7 -0
  18. package/src/cli/commands/init.ts +16 -0
  19. package/src/cli/commands/mcp.ts +19 -3
  20. package/src/cli/hook/minimal-change-cli.ts +130 -0
  21. package/src/cli/hook/minimal-change-context.ts +57 -0
  22. package/src/cli/hook/minimal-change-policy.ts +197 -0
  23. package/src/cli/hook/minimal-change-signals.ts +601 -0
  24. package/src/cli/hook/route-registry.ts +6 -2
  25. package/src/cli/hook/runtime.ts +39 -2
  26. package/src/cli/hook-entry.ts +8 -0
  27. package/src/cli/installer/managed-entries.ts +1 -1
  28. package/src/cli/mcp/auth.ts +41 -5
  29. package/src/cli/mcp/instructions.ts +14 -9
  30. package/src/cli/mcp/oauth.ts +88 -21
  31. package/src/cli/mcp/paths.ts +13 -5
  32. package/src/cli/mcp/policy.ts +73 -21
  33. package/src/cli/mcp/reader-tools.ts +629 -0
  34. package/src/cli/mcp/server.ts +82 -13
  35. package/src/cli/mcp/session-store.ts +91 -0
  36. package/src/cli/mcp/setup.ts +194 -37
  37. package/src/cli/mcp/tools.ts +143 -40
  38. package/src/cli/mcp/transports/http.ts +187 -49
  39. package/src/cli/mcp/types.ts +8 -0
  40. package/src/cli/mcp/version.ts +10 -0
  41. package/src/cli/mcp/workspaces.ts +348 -0
  42. 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.4`.
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.4`
423
- - Generated workflow stamp: `repo-harness@0.7.4+template@0.7.4`
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.4`.
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.4`
428
- - Generated workflow stamp : `repo-harness@0.7.4+template@0.7.4`
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.4` です。
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.4`
402
- - Generated workflow stamp:`repo-harness@0.7.4+template@0.7.4`
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.4`.
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 only workflow artifacts to
340
- MCP clients. ChatGPT acts as a planner/reviewer that reads state and moves an
341
- idea through PRD, checklist Sprint, and Codex goal handoff artifacts — with no
342
- source-code write access, arbitrary shell execution, or default Codex runner.
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.4`
497
- - Generated workflow stamp: `repo-harness@0.7.4+template@0.7.4`
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.4`。
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.4`
441
- - Generated workflow stamp:`repo-harness@0.7.4+template@0.7.4`
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
- if ! has_research_for_new_plan; then
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 ] && [ "$plan_research_ready" -eq 0 ]; then
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