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.
- package/README.es.md +4 -4
- package/README.fr.md +4 -4
- package/README.ja.md +4 -4
- package/README.md +57 -17
- package/README.zh-CN.md +17 -9
- package/assets/hooks/AGENTS.md +2 -0
- package/assets/hooks/CLAUDE.md +2 -0
- package/assets/hooks/lib/minimal-change.sh +77 -0
- package/assets/hooks/lib/workflow-state.sh +205 -0
- package/assets/hooks/minimal-change-context.sh +16 -0
- package/assets/hooks/minimal-change-observer.sh +18 -0
- package/assets/hooks/projection.json +11 -0
- package/assets/hooks/prompt-guard.sh +200 -4
- package/assets/hooks/stop-orchestrator.sh +133 -4
- package/assets/reference-configs/hook-operations.md +18 -18
- package/assets/reference-configs/minimal-change-hooks.md +80 -0
- package/assets/skill-version.json +10 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +3 -0
- package/assets/templates/helpers/plan-to-todo.sh +3 -0
- package/assets/templates/review.template.md +3 -0
- package/assets/workflow-contract.v1.json +1 -0
- package/package.json +4 -2
- package/scripts/check-agent-tooling.sh +6 -4
- package/scripts/check-ci.sh +3 -0
- package/scripts/check-npm-release.sh +1 -0
- package/scripts/check-tarball-install-smoke.sh +86 -0
- package/scripts/ensure-task-workflow.sh +20 -1
- package/scripts/lib/project-init-lib.sh +28 -5
- package/scripts/plan-to-todo.sh +3 -0
- package/scripts/repo-harness.sh +20 -13
- package/scripts/sync-hook-sources.ts +294 -0
- 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/diff-fingerprint.ts +530 -0
- 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/review-rubric.ts +77 -0
- package/src/cli/hook/route-registry.ts +6 -2
- package/src/cli/hook/runtime.ts +39 -2
- package/src/cli/hook-entry.ts +24 -0
- package/src/cli/index.ts +39 -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.
|
|
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.
|
|
423
|
-
- Generated workflow stamp: `repo-harness@0.
|
|
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
|
|
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.
|
|
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.
|
|
428
|
-
- Generated workflow stamp : `repo-harness@0.
|
|
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
|
|
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.
|
|
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.
|
|
402
|
-
- Generated workflow stamp:`repo-harness@0.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -397,13 +439,13 @@ and not arbitrary shell.
|
|
|
397
439
|
|
|
398
440
|
## Hook Authority Map
|
|
399
441
|
|
|
400
|
-
-
|
|
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 ->
|
|
406
|
-
-
|
|
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.
|
|
497
|
-
- Generated workflow stamp: `repo-harness@0.
|
|
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
|
|
708
|
+
## Generated vs Self-Hosted Hook Projection
|
|
667
709
|
|
|
668
|
-
- Downstream hook behavior is defined by
|
|
669
|
-
|
|
670
|
-
-
|
|
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.
|
|
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
|
-
-
|
|
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 ->
|
|
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.
|
|
441
|
-
- Generated workflow stamp:`repo-harness@0.
|
|
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
|
|
622
|
+
## Generated vs Self-Hosted Hook Projection
|
|
615
623
|
|
|
616
|
-
- 下游 hook 行为由 `assets/hooks/` 和 `assets/reference-configs/`
|
|
617
|
-
-
|
|
618
|
-
- 每个 hook
|
|
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
|
|
package/assets/hooks/AGENTS.md
CHANGED
|
@@ -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
|
package/assets/hooks/CLAUDE.md
CHANGED
|
@@ -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
|