repo-harness 0.7.4 → 0.8.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.
Files changed (60) hide show
  1. package/README.es.md +4 -4
  2. package/README.fr.md +4 -4
  3. package/README.ja.md +4 -4
  4. package/README.md +57 -17
  5. package/README.zh-CN.md +17 -9
  6. package/assets/hooks/AGENTS.md +2 -0
  7. package/assets/hooks/CLAUDE.md +2 -0
  8. package/assets/hooks/lib/minimal-change.sh +77 -0
  9. package/assets/hooks/lib/workflow-state.sh +205 -0
  10. package/assets/hooks/minimal-change-context.sh +16 -0
  11. package/assets/hooks/minimal-change-observer.sh +18 -0
  12. package/assets/hooks/projection.json +11 -0
  13. package/assets/hooks/prompt-guard.sh +200 -4
  14. package/assets/hooks/stop-orchestrator.sh +133 -4
  15. package/assets/reference-configs/hook-operations.md +18 -18
  16. package/assets/reference-configs/minimal-change-hooks.md +80 -0
  17. package/assets/skill-version.json +10 -2
  18. package/assets/templates/helpers/ensure-task-workflow.sh +3 -0
  19. package/assets/templates/helpers/plan-to-todo.sh +3 -0
  20. package/assets/templates/review.template.md +3 -0
  21. package/assets/workflow-contract.v1.json +1 -0
  22. package/package.json +4 -2
  23. package/scripts/check-agent-tooling.sh +6 -4
  24. package/scripts/check-ci.sh +3 -0
  25. package/scripts/check-npm-release.sh +1 -0
  26. package/scripts/check-tarball-install-smoke.sh +86 -0
  27. package/scripts/ensure-task-workflow.sh +20 -1
  28. package/scripts/lib/project-init-lib.sh +28 -5
  29. package/scripts/plan-to-todo.sh +3 -0
  30. package/scripts/repo-harness.sh +20 -13
  31. package/scripts/sync-hook-sources.ts +294 -0
  32. package/src/cli/commands/adopt-plan.ts +7 -0
  33. package/src/cli/commands/init.ts +16 -0
  34. package/src/cli/commands/mcp.ts +19 -3
  35. package/src/cli/hook/diff-fingerprint.ts +530 -0
  36. package/src/cli/hook/minimal-change-cli.ts +130 -0
  37. package/src/cli/hook/minimal-change-context.ts +57 -0
  38. package/src/cli/hook/minimal-change-policy.ts +197 -0
  39. package/src/cli/hook/minimal-change-signals.ts +601 -0
  40. package/src/cli/hook/review-rubric.ts +77 -0
  41. package/src/cli/hook/route-registry.ts +6 -2
  42. package/src/cli/hook/runtime.ts +39 -2
  43. package/src/cli/hook-entry.ts +24 -0
  44. package/src/cli/index.ts +39 -0
  45. package/src/cli/installer/managed-entries.ts +1 -1
  46. package/src/cli/mcp/auth.ts +41 -5
  47. package/src/cli/mcp/instructions.ts +14 -9
  48. package/src/cli/mcp/oauth.ts +88 -21
  49. package/src/cli/mcp/paths.ts +13 -5
  50. package/src/cli/mcp/policy.ts +73 -21
  51. package/src/cli/mcp/reader-tools.ts +629 -0
  52. package/src/cli/mcp/server.ts +82 -13
  53. package/src/cli/mcp/session-store.ts +91 -0
  54. package/src/cli/mcp/setup.ts +194 -37
  55. package/src/cli/mcp/tools.ts +143 -40
  56. package/src/cli/mcp/transports/http.ts +187 -49
  57. package/src/cli/mcp/types.ts +8 -0
  58. package/src/cli/mcp/version.ts +10 -0
  59. package/src/cli/mcp/workspaces.ts +348 -0
  60. 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.8.0`.
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.8.0`
423
+ - Generated workflow stamp: `repo-harness@0.8.0+template@0.8.0`
424
424
  - GitHub repository: `Ancienttwo/repo-harness`
425
425
  - Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
426
426
 
@@ -584,7 +584,7 @@ bun run benchmark:skills --eval repair-agents-task-sync
584
584
  - `scripts/create-project-dirs.sh`
585
585
  - Legacy-doc migrator: `scripts/migrate-workflow-docs.ts`
586
586
 
587
- ## Generated vs Self-Hosted Hook Parity
587
+ ## Generated vs Self-Hosted Hook Projection
588
588
 
589
589
  - El comportamiento downstream de hooks lo define la salida generada desde `assets/hooks/` y `assets/reference-configs/`.
590
590
  - Este repo dogfoodea el mismo contract, pero el comportamiento self-host no se sincroniza mágicamente con los generated repos; cada cambio debe actualizar explícitamente ambas superficies cuando aplique.
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.8.0`.
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.8.0`
428
+ - Generated workflow stamp : `repo-harness@0.8.0+template@0.8.0`
429
429
  - GitHub repository : `Ancienttwo/repo-harness`
430
430
  - Release history : [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
431
431
 
@@ -590,7 +590,7 @@ bun run benchmark:skills --eval repair-agents-task-sync
590
590
  - `scripts/create-project-dirs.sh`
591
591
  - Legacy-doc migrator : `scripts/migrate-workflow-docs.ts`
592
592
 
593
- ## Generated vs Self-Hosted Hook Parity
593
+ ## Generated vs Self-Hosted Hook Projection
594
594
 
595
595
  - Le comportement downstream des hooks est défini par la sortie générée depuis `assets/hooks/` et `assets/reference-configs/`.
596
596
  - Ce repo dogfoode le même contract, mais le comportement self-host ne se synchronise pas magiquement avec les generated repos ; un changement doit mettre à jour explicitement les deux surfaces lorsque nécessaire.
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.8.0` です。
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.8.0`
402
+ - Generated workflow stamp:`repo-harness@0.8.0+template@0.8.0`
403
403
  - GitHub repository:`Ancienttwo/repo-harness`
404
404
  - Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
405
405
 
@@ -558,7 +558,7 @@ bun run benchmark:skills --eval repair-agents-task-sync
558
558
  - `scripts/create-project-dirs.sh`
559
559
  - Legacy-doc migrator:`scripts/migrate-workflow-docs.ts`
560
560
 
561
- ## Generated vs Self-Hosted Hook Parity
561
+ ## Generated vs Self-Hosted Hook Projection
562
562
 
563
563
  - Downstream hook behavior は `assets/hooks/` と `assets/reference-configs/` から生成される出力で定義されます。
564
564
  - この repo は同じ contract を dogfood しますが、self-host behavior は generated repos と自動同期されません。変更は両方の surface を明示的に更新する必要があります。
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.8.0`.
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
 
@@ -397,13 +439,13 @@ and not arbitrary shell.
397
439
 
398
440
  ## Hook Authority Map
399
441
 
400
- - `.ai/hooks/` is the only shared hook implementation you should edit first.
442
+ - `assets/hooks/` is the only human-authored shared hook implementation. This self-host repo keeps `.ai/hooks/` as a checked-in generated projection for `"hook_source": "repo"` dogfood.
401
443
  - `~/.claude/settings.json` is the user-level Claude adapter that dispatches into opted-in repos.
402
444
  - `~/.codex/hooks.json` is the user-level Codex adapter that dispatches into the same runner.
403
445
  - Repo-local `.claude/settings.json` and `.codex/hooks.json` hook adapters are legacy project-level config and should be retired during migration.
404
446
  - Codex must mark `~/.codex/hooks.json` as trusted in Codex Settings before those hooks run.
405
- - Debug in this order: user-level adapter config -> `repo-harness-hook` (or fallback `repo-harness hook`) -> route registry -> `.ai/hooks/*`.
406
- - If `repo-harness-hook` reports `.ai/hooks` drift, refresh the repo-local copy with `repo-harness adopt --repo <root>`.
447
+ - Debug in this order: user-level adapter config -> `repo-harness-hook` (or fallback `repo-harness hook`) -> route registry -> active hook source.
448
+ - For hook product changes, edit `assets/hooks/<path>`, run `bun run sync:hooks`, then verify with `bun run check:hooks`. Package-only templates are classified in `assets/hooks/projection.json` and are not projected into `.ai/hooks/`.
407
449
 
408
450
  The installed adapter owns eight shared managed hook routes. Codex also installs
409
451
  three Codex-only bounded-delegation routes. The route tuple
@@ -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.8.0`
539
+ - Generated workflow stamp: `repo-harness@0.8.0+template@0.8.0`
498
540
  - GitHub repository: `Ancienttwo/repo-harness`
499
541
  - Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
500
542
 
@@ -663,13 +705,11 @@ bun run benchmark:skills --eval repair-agents-task-sync
663
705
  - `scripts/init-project.sh`
664
706
  - `scripts/create-project-dirs.sh`
665
707
 
666
- ## Generated vs Self-Hosted Hook Parity
708
+ ## Generated vs Self-Hosted Hook Projection
667
709
 
668
- - Downstream hook behavior is defined by generated output from `assets/hooks/` plus
669
- `assets/reference-configs/`.
670
- - This repo dogfoods the same contract, but self-host behavior is not magically in
671
- sync with generated repos unless a change explicitly updates both surfaces.
672
- - Every hook change should say whether it affects `self-host`, `generated`, or `both`.
710
+ - Downstream hook behavior is defined by `assets/hooks/` plus `assets/reference-configs/`.
711
+ - This repo dogfoods the same hook runtime through `.ai/hooks/`, but that tree is generated from `assets/hooks/projection.json`.
712
+ - Every hook change should update canonical `assets/hooks/` once, run `bun run sync:hooks`, and include `bun run check:hooks` in verification.
673
713
 
674
714
  ## Package Manager Defaults
675
715
 
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.8.0`。
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 解析、
@@ -353,12 +360,13 @@ repo-harness mcp serve --repo . --transport http --profile orchestrator --enable
353
360
 
354
361
  ## Hook Authority Map
355
362
 
356
- - `.ai/hooks/` 是唯一应该优先编辑的 shared hook implementation。
363
+ - `assets/hooks/` 是唯一人工维护的 shared hook implementation。本仓库的 `.ai/hooks/` 是给 `"hook_source": "repo"` dogfood 使用的 checked-in generated projection。
357
364
  - `~/.claude/settings.json` 是 user-level Claude adapter,负责 dispatch 到 opted-in repos。
358
365
  - `~/.codex/hooks.json` 是 user-level Codex adapter,dispatch 到同一个 runner。
359
366
  - Repo-local `.claude/settings.json` 和 `.codex/hooks.json` hook adapters 是 legacy project-level config,迁移时应退休。
360
367
  - Codex 必须在 Settings 里信任 `~/.codex/hooks.json`,hooks 才会执行。
361
- - 调试顺序:user-level adapter config -> `repo-harness-hook` 或 fallback `repo-harness hook` -> route registry -> `.ai/hooks/*`。
368
+ - 调试顺序:user-level adapter config -> `repo-harness-hook` 或 fallback `repo-harness hook` -> route registry -> active hook source。
369
+ - Hook 产品变更只改 `assets/hooks/<path>`,然后运行 `bun run sync:hooks` 和 `bun run check:hooks`。Package-only templates 在 `assets/hooks/projection.json` 分类,不投影到 `.ai/hooks/`。
362
370
 
363
371
 
364
372
  The installed adapter owns eight managed hook routes. The route tuple
@@ -437,8 +445,8 @@ hook block 工作时,先看 terminal 里的结构化输出。核心字段是
437
445
 
438
446
  ## 当前 Release
439
447
 
440
- - npm package:`repo-harness@0.7.4`
441
- - Generated workflow stamp:`repo-harness@0.7.4+template@0.7.4`
448
+ - npm package:`repo-harness@0.8.0`
449
+ - Generated workflow stamp:`repo-harness@0.8.0+template@0.8.0`
442
450
  - GitHub repository:`Ancienttwo/repo-harness`
443
451
  - Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
444
452
 
@@ -611,11 +619,11 @@ bun run benchmark:skills --eval repair-agents-task-sync
611
619
  - `scripts/create-project-dirs.sh`
612
620
  - Legacy-doc migrator:`scripts/migrate-workflow-docs.ts`
613
621
 
614
- ## Generated vs Self-Hosted Hook Parity
622
+ ## Generated vs Self-Hosted Hook Projection
615
623
 
616
- - 下游 hook 行为由 `assets/hooks/` 和 `assets/reference-configs/` 的生成输出定义。
617
- - 本仓库 dogfood 同一套 contract,但 self-host 行为不会自动与 generated repos 同步;变更必须显式更新两侧 surface。
618
- - 每个 hook 变更都要说明影响 `self-host`、`generated` 还是 `both`。
624
+ - 下游 hook 行为由 `assets/hooks/` 和 `assets/reference-configs/` 定义。
625
+ - 本仓库通过 `.ai/hooks/` dogfood 同一套 hook runtime,但这棵树由 `assets/hooks/projection.json` 生成。
626
+ - 每个 hook 变更只应更新 canonical `assets/hooks/`,运行 `bun run sync:hooks`,并在验证里包含 `bun run check:hooks`。
619
627
 
620
628
  ## Package Manager Defaults
621
629
 
@@ -11,6 +11,8 @@ Keep this file focused on the local contract for this primary functional block.
11
11
  - Treat `.ai/context/context-map.json` as the index of discoverable context files.
12
12
  - Do not keep pushing context files deeper by default; add lower-level files only for a separately owned functional block with its own commands and invariants.
13
13
  - Prefer repo-local workflow artifacts over tool-specific chat memory.
14
+ - `assets/hooks/` is the only human-authored hook source. Treat `.ai/hooks/` as the generated self-host projection; after editing hook files, run `bun run sync:hooks`, then `bun run check:hooks`.
15
+ - Do not hand-edit generated `.ai/hooks/` drift. Classify package-only or repo-only exceptions in `assets/hooks/projection.json`.
14
16
 
15
17
  <!-- BEGIN CAPABILITY CONTEXT -->
16
18
  ## Capability Context
@@ -11,6 +11,8 @@ Keep this file focused on the local contract for this primary functional block.
11
11
  - Treat `.ai/context/context-map.json` as the index of discoverable context files.
12
12
  - Do not keep pushing context files deeper by default; add lower-level files only for a separately owned functional block with its own commands and invariants.
13
13
  - Prefer repo-local workflow artifacts over tool-specific chat memory.
14
+ - `assets/hooks/` is the only human-authored hook source. Treat `.ai/hooks/` as the generated self-host projection; after editing hook files, run `bun run sync:hooks`, then `bun run check:hooks`.
15
+ - Do not hand-edit generated `.ai/hooks/` drift. Classify package-only or repo-only exceptions in `assets/hooks/projection.json`.
14
16
 
15
17
  <!-- BEGIN CAPABILITY CONTEXT -->
16
18
  ## Capability Context
@@ -0,0 +1,77 @@
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
+ repo_harness_hook_cli() {
25
+ local lib_dir hooks_dir repo_root source_cli source_hook_cli
26
+
27
+ lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ hooks_dir="$(cd "$lib_dir/.." && pwd)"
29
+ repo_root="$(cd "$hooks_dir/../.." 2>/dev/null && pwd)"
30
+ source_cli="$repo_root/src/cli/index.ts"
31
+ source_hook_cli="$repo_root/src/cli/hook-entry.ts"
32
+
33
+ if [[ -n "${REPO_HARNESS_HOOK_CLI:-}" && -f "${REPO_HARNESS_HOOK_CLI:-}" ]] && command -v bun >/dev/null 2>&1; then
34
+ bun "$REPO_HARNESS_HOOK_CLI" "$@"
35
+ return $?
36
+ fi
37
+
38
+ if [[ -f "$source_hook_cli" ]] && command -v bun >/dev/null 2>&1; then
39
+ bun "$source_hook_cli" "$@"
40
+ return $?
41
+ fi
42
+
43
+ if [[ -n "${HOOK_REPO_ROOT:-}" && -f "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" ]] && command -v bun >/dev/null 2>&1; then
44
+ bun "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" "$@"
45
+ return $?
46
+ fi
47
+
48
+ if [[ -n "${REPO_HARNESS_CLI:-}" && -f "${REPO_HARNESS_CLI:-}" ]] && command -v bun >/dev/null 2>&1; then
49
+ bun "$REPO_HARNESS_CLI" "$@"
50
+ return $?
51
+ fi
52
+
53
+ if [[ -f "$source_cli" ]] && command -v bun >/dev/null 2>&1; then
54
+ bun "$source_cli" "$@"
55
+ return $?
56
+ fi
57
+
58
+ if command -v repo-harness-hook >/dev/null 2>&1; then
59
+ repo-harness-hook "$@"
60
+ return $?
61
+ fi
62
+
63
+ if command -v repo-harness >/dev/null 2>&1; then
64
+ repo-harness "$@"
65
+ return $?
66
+ fi
67
+
68
+ return 127
69
+ }
70
+
71
+ minimal_change_hook_entry() {
72
+ repo_harness_hook_cli minimal-change "$@"
73
+ }
74
+
75
+ review_rubric_prompt() {
76
+ repo_harness_hook_cli review-rubric --format prompt 2>/dev/null || true
77
+ }
@@ -1288,6 +1288,166 @@ workflow_review_recommends_pass() {
1288
1288
  grep -Eq '^> \*\*Recommendation\*\*:[[:space:]]*pass[[:space:]]*$' "$review_file"
1289
1289
  }
1290
1290
 
1291
+ workflow_review_metadata_field() {
1292
+ local review_file="${1:-}"
1293
+ local field="${2:-}"
1294
+ [[ -n "$review_file" && -f "$review_file" && -n "$field" ]] || return 1
1295
+ awk -v field="$field" '
1296
+ # Top-of-file metadata only: stop at the first section heading so a
1297
+ # section-level "> **<field>**:" line (e.g. the External Acceptance section
1298
+ # carries its own Reviewed Diff Fingerprint) can never be read as a top-level
1299
+ # value when the real header omits it.
1300
+ /^## / { exit }
1301
+ index($0, "> **" field "**:") == 1 {
1302
+ sub("^> \\*\\*" field "\\*\\*:[[:space:]]*", "");
1303
+ gsub(/\r/, "");
1304
+ print;
1305
+ exit;
1306
+ }
1307
+ ' "$review_file"
1308
+ }
1309
+
1310
+ workflow_review_fingerprint() {
1311
+ workflow_review_metadata_field "${1:-}" "Reviewed Diff Fingerprint"
1312
+ }
1313
+
1314
+ workflow_review_rubric_version() {
1315
+ workflow_review_metadata_field "${1:-}" "Review Rubric Version"
1316
+ }
1317
+
1318
+ # Classify the top-of-file Review Rubric Version. Echoes one of:
1319
+ # absent - no rubric line at all (a genuine pre-rubric legacy artifact)
1320
+ # 1 - the supported modern rubric version
1321
+ # malformed - a rubric line is present but is not a supported version
1322
+ # (non-numeric, 0, an unsupported number, or quote/space garbage)
1323
+ # A present-but-unsupported rubric means the artifact claims a schema this gate
1324
+ # cannot evaluate, so callers must fail closed rather than fall through to the
1325
+ # lenient legacy path. Only a genuinely absent rubric stays lenient.
1326
+ workflow_review_rubric_class() {
1327
+ local raw trimmed
1328
+ raw="$(workflow_review_rubric_version "${1:-}")"
1329
+ # Trim surrounding whitespace WITHOUT xargs: an unbalanced quote in the value
1330
+ # makes xargs fail and emit nothing, which would silently downgrade a malformed
1331
+ # rubric to "absent" (legacy lenient) — exactly the fail-open this prevents.
1332
+ trimmed="${raw#"${raw%%[![:space:]]*}"}"
1333
+ trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
1334
+ if [[ -z "$trimmed" ]]; then
1335
+ printf 'absent'
1336
+ elif [[ "$trimmed" == "1" ]]; then
1337
+ printf '1'
1338
+ else
1339
+ printf 'malformed'
1340
+ fi
1341
+ }
1342
+
1343
+ workflow_hook_cli_json() {
1344
+ if [[ -n "${REPO_HARNESS_HOOK_CLI:-}" && -f "${REPO_HARNESS_HOOK_CLI:-}" ]] && command -v bun >/dev/null 2>&1; then
1345
+ bun "$REPO_HARNESS_HOOK_CLI" "$@"
1346
+ return $?
1347
+ fi
1348
+ if [[ -n "${HOOK_REPO_ROOT:-}" && -f "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" ]] && command -v bun >/dev/null 2>&1; then
1349
+ bun "$HOOK_REPO_ROOT/src/cli/hook-entry.ts" "$@"
1350
+ return $?
1351
+ fi
1352
+ if [[ -n "${REPO_HARNESS_CLI:-}" && -f "${REPO_HARNESS_CLI:-}" ]] && command -v bun >/dev/null 2>&1; then
1353
+ bun "$REPO_HARNESS_CLI" "$@"
1354
+ return $?
1355
+ fi
1356
+ if command -v repo-harness-hook >/dev/null 2>&1; then
1357
+ repo-harness-hook "$@"
1358
+ return $?
1359
+ fi
1360
+ if command -v repo-harness >/dev/null 2>&1; then
1361
+ repo-harness "$@"
1362
+ return $?
1363
+ fi
1364
+ return 127
1365
+ }
1366
+
1367
+ workflow_json_field() {
1368
+ local json="${1:-}"
1369
+ local field="${2:-}"
1370
+ [[ -n "$json" && -n "$field" ]] || return 1
1371
+ if command -v jq >/dev/null 2>&1; then
1372
+ printf '%s' "$json" | jq -r ".$field // empty" 2>/dev/null || true
1373
+ return
1374
+ fi
1375
+ if command -v bun >/dev/null 2>&1; then
1376
+ JSON_INPUT="$json" JSON_FIELD="$field" bun -e '
1377
+ try {
1378
+ const parsed = JSON.parse(process.env.JSON_INPUT ?? "");
1379
+ const value = parsed?.[process.env.JSON_FIELD ?? ""];
1380
+ if (value != null) process.stdout.write(String(value));
1381
+ } catch {}
1382
+ ' 2>/dev/null || true
1383
+ fi
1384
+ }
1385
+
1386
+ workflow_current_review_fingerprint_json() {
1387
+ # Bind the fingerprint to the resolved target branch so base_rev tracks the
1388
+ # target tip; without --base the CLI falls back to HEAD, making the branch diff
1389
+ # HEAD...HEAD (empty) and blinding the gate to target movement.
1390
+ local target
1391
+ target="$(workflow_target_branch)"
1392
+ workflow_hook_cli_json review-fingerprint --base "$target" --format json 2>/dev/null || true
1393
+ }
1394
+
1395
+ workflow_current_review_fingerprint_value() {
1396
+ local json status fingerprint
1397
+ json="$(workflow_current_review_fingerprint_json)"
1398
+ status="$(workflow_json_field "$json" "status")"
1399
+ [[ "$status" == "ok" ]] || return 1
1400
+ fingerprint="$(workflow_json_field "$json" "fingerprint")"
1401
+ [[ -n "$fingerprint" ]] || return 1
1402
+ printf '%s' "$fingerprint"
1403
+ }
1404
+
1405
+ workflow_review_freshness_status() {
1406
+ local review_file="${1:-}"
1407
+ local reviewed rubric_class current_json current_status current_fingerprint current_scope
1408
+
1409
+ reviewed="$(workflow_review_fingerprint "$review_file" | xargs || true)"
1410
+ rubric_class="$(workflow_review_rubric_class "$review_file")"
1411
+ if [[ "$rubric_class" == "malformed" ]]; then
1412
+ # A malformed/unsupported rubric claims a schema this gate cannot evaluate;
1413
+ # fail closed at the freshness stage instead of treating it as legacy.
1414
+ printf 'malformed_schema\t-\tReview Rubric Version is malformed or unsupported; rerun /check to record the review under a supported rubric, or record a Manual Override.\n'
1415
+ return 0
1416
+ fi
1417
+ if [[ -z "$reviewed" || "$reviewed" == "pending" || "$reviewed" == "unknown" ]]; then
1418
+ # A supported rubric (v1+) with no concrete fingerprint was never bound to the
1419
+ # diff: fail closed (`missing`). An `absent` rubric stays on the advisory legacy
1420
+ # path here — the external-acceptance gate is the authority that requires a
1421
+ # supported rubric (a rubric-less review fails external acceptance), so absent
1422
+ # is still blocked at every Done/finish/verify gate that enforces external.
1423
+ if [[ "$rubric_class" == "1" ]]; then
1424
+ printf 'missing\t-\tReview fingerprint is missing for rubric v%s; rerun /check and peer acceptance to record the current Reviewed Diff Fingerprint.\n' "$rubric_class"
1425
+ return 0
1426
+ fi
1427
+ printf 'legacy_missing\t-\tReview fingerprint is missing; rerun /check to refresh the review metadata.\n'
1428
+ return 0
1429
+ fi
1430
+ if ! [[ "$reviewed" =~ ^sha256:[0-9a-f]{64}$ ]]; then
1431
+ printf 'malformed\t%s\tReview fingerprint is malformed; rerun /check and peer acceptance.\n' "$reviewed"
1432
+ return 0
1433
+ fi
1434
+
1435
+ current_json="$(workflow_current_review_fingerprint_json)"
1436
+ current_status="$(workflow_json_field "$current_json" "status")"
1437
+ current_fingerprint="$(workflow_json_field "$current_json" "fingerprint")"
1438
+ current_scope="$(workflow_json_field "$current_json" "scope")"
1439
+ if [[ "$current_status" != "ok" || -z "$current_fingerprint" ]]; then
1440
+ printf 'unknown\t-\tCurrent implementation diff fingerprint is unknown; rerun /check after git state is readable.\n'
1441
+ return 0
1442
+ fi
1443
+ if [[ "$reviewed" != "$current_fingerprint" ]]; then
1444
+ printf 'stale\t%s\tReview is stale for current implementation diff fingerprint %s; rerun /check and peer acceptance.\n' "$reviewed" "$current_fingerprint"
1445
+ return 0
1446
+ fi
1447
+
1448
+ printf 'pass\t%s\tReview fingerprint is fresh for %s.\n' "$reviewed" "${current_scope:-branch+staged+unstaged+untracked}"
1449
+ }
1450
+
1291
1451
  workflow_external_acceptance_expected_reviewer() {
1292
1452
  local host="${HOOK_HOST:-}"
1293
1453
 
@@ -1409,6 +1569,51 @@ workflow_external_acceptance_status() {
1409
1569
  return 0
1410
1570
  fi
1411
1571
 
1572
+ # Bind the peer's acceptance to the exact diff they reviewed. A supported rubric
1573
+ # (v1) requires the External Acceptance section to carry its own current Reviewed
1574
+ # Diff Fingerprint and scope; otherwise a stale F1 acceptance keeps satisfying the
1575
+ # gate after the implementation moves to F2, because the top-of-file fingerprint
1576
+ # is agent-editable. An absent or malformed rubric fails closed here — external
1577
+ # acceptance is the authority that requires a supported rubric; Manual Override
1578
+ # above is the only escape for a genuine pre-rubric legacy artifact.
1579
+ local rubric_class section_fp section_scope current_fp
1580
+ rubric_class="$(workflow_review_rubric_class "$review_file")"
1581
+ case "$rubric_class" in
1582
+ absent)
1583
+ # An absent rubric cannot be proven to predate the rubric (it may have been
1584
+ # stripped to skip binding), so fail closed. Manual Override above is the
1585
+ # escape hatch for a genuine pre-rubric legacy artifact.
1586
+ printf 'fail\t%s\t%s\tReview Rubric Version is missing; rerun peer acceptance under a supported rubric or record a Manual Override.\n' "${reviewer:--}" "${source:--}"
1587
+ return 0
1588
+ ;;
1589
+ malformed)
1590
+ # An unsupported rubric must not silently disable the binding check.
1591
+ printf 'fail\t%s\t%s\tReview Rubric Version is malformed or unsupported; rerun peer acceptance under a supported rubric or record a Manual Override.\n' "${reviewer:--}" "${source:--}"
1592
+ return 0
1593
+ ;;
1594
+ *)
1595
+ section_fp="$(workflow_external_acceptance_field "$section" "Reviewed Diff Fingerprint" | xargs || true)"
1596
+ section_scope="$(workflow_external_acceptance_field "$section" "Reviewed Scope" | xargs || true)"
1597
+ current_fp="$(workflow_current_review_fingerprint_value || true)"
1598
+ if [[ -z "$current_fp" ]]; then
1599
+ printf 'fail\t%s\t%s\tCurrent implementation diff fingerprint is unknown; rerun peer acceptance after git state is readable.\n' "${reviewer:--}" "${source:--}"
1600
+ return 0
1601
+ fi
1602
+ if ! [[ "$section_fp" =~ ^sha256:[0-9a-f]{64}$ ]]; then
1603
+ printf 'fail\t%s\t%s\tExternal acceptance is missing a valid Reviewed Diff Fingerprint for rubric v%s; rerun peer acceptance for the current diff.\n' "${reviewer:--}" "${source:--}" "$rubric_class"
1604
+ return 0
1605
+ fi
1606
+ if [[ "$section_fp" != "$current_fp" ]]; then
1607
+ printf 'fail\t%s\t%s\tExternal acceptance fingerprint %s is stale for current implementation diff %s; rerun peer acceptance.\n' "${reviewer:--}" "${source:--}" "$section_fp" "$current_fp"
1608
+ return 0
1609
+ fi
1610
+ if [[ "$section_scope" != "branch+staged+unstaged+untracked" ]]; then
1611
+ printf 'fail\t%s\t%s\tExternal acceptance scope is %s; expected branch+staged+unstaged+untracked.\n' "${reviewer:--}" "${source:--}" "${section_scope:-missing}"
1612
+ return 0
1613
+ fi
1614
+ ;;
1615
+ esac
1616
+
1412
1617
  printf 'pass\t%s\t%s\tExternal acceptance passed.\n' "$reviewer" "$source"
1413
1618
  }
1414
1619
 
@@ -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
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "canonical_root": "assets/hooks",
4
+ "projection_target": ".ai/hooks",
5
+ "package_only": [
6
+ "projection.json",
7
+ "codex.hooks.template.json",
8
+ "settings.template.json"
9
+ ],
10
+ "repo_only": []
11
+ }