repo-harness 0.7.2 → 0.7.3
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/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.es.md +3 -3
- package/README.fr.md +3 -3
- package/README.ja.md +3 -3
- package/README.md +10 -4
- package/README.zh-CN.md +3 -3
- package/SKILL.md +4 -6
- package/assets/hooks/codex-delegation-advisor.sh +157 -0
- package/assets/hooks/codex.hooks.template.json +16 -1
- package/assets/hooks/run-hook.sh +60 -3
- package/assets/hooks/stop-orchestrator.sh +115 -0
- package/assets/hooks/subagent-start-context.sh +124 -0
- package/assets/hooks/subagent-stop-quality.sh +121 -0
- package/assets/reference-configs/external-tooling.md +3 -2
- package/assets/reference-configs/hook-operations.md +6 -2
- package/assets/skill-commands/repo-harness-goal/SKILL.md +2 -0
- package/assets/skill-commands/repo-harness-migrate/SKILL.md +1 -1
- package/assets/skill-version.json +6 -6
- package/assets/templates/helpers/check-task-workflow.sh +83 -4
- package/assets/templates/helpers/ensure-task-workflow.sh +10 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +6 -6
- package/assets/workflow-contract.v1.json +1 -0
- package/package.json +1 -1
- package/scripts/check-task-workflow.sh +83 -4
- package/scripts/ensure-task-workflow.sh +10 -1
- package/scripts/hook-dispatch-diet-report.ts +3 -1
- package/scripts/lib/project-init-lib.sh +14 -4
- package/scripts/migrate-project-template.sh +1 -1
- package/scripts/migrate-workflow-docs.ts +6 -6
- package/scripts/sync-codex-installed-copies.sh +0 -21
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/status.ts +2 -2
- package/src/cli/hook/route-registry.ts +41 -1
- package/src/cli/hook/runtime.ts +54 -5
- package/src/cli/installer/managed-entries.ts +3 -3
- package/src/cli/installer/targets/claude.ts +2 -2
- package/src/cli/installer/targets/codex.ts +3 -3
- package/src/cli/repo-adoption/reclaim-runtime.ts +63 -4
- package/src/core/adoption/gitignore-plan.ts +1 -4
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# repo-harness AGENTS.md
|
|
2
2
|
|
|
3
|
-
This repository self-hosts the `repo-harness` contract; the former `repo-harness-skill` and `project-initializer` names
|
|
3
|
+
This repository self-hosts the `repo-harness` contract; the former `repo-harness-skill` and `project-initializer` names have been fully removed and are no longer recognized by any tooling. Claude and Codex should follow the same repo-local workflow surface.
|
|
4
4
|
|
|
5
5
|
## Canonical Workflow Files
|
|
6
6
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# repo-harness AGENTS.md
|
|
2
2
|
|
|
3
|
-
This repository self-hosts the `repo-harness` contract; the former `repo-harness-skill` and `project-initializer` names
|
|
3
|
+
This repository self-hosts the `repo-harness` contract; the former `repo-harness-skill` and `project-initializer` names have been fully removed and are no longer recognized by any tooling. Claude and Codex should follow the same repo-local workflow surface.
|
|
4
4
|
|
|
5
5
|
## Canonical Workflow Files
|
|
6
6
|
|
package/README.es.md
CHANGED
|
@@ -85,7 +85,7 @@ artifacts.
|
|
|
85
85
|
## Novedades
|
|
86
86
|
|
|
87
87
|
Las notas de versión viven en [`docs/CHANGELOG.md`](docs/CHANGELOG.md). La línea
|
|
88
|
-
actual es `0.7.
|
|
88
|
+
actual es `0.7.3`.
|
|
89
89
|
|
|
90
90
|
## Cómo funciona
|
|
91
91
|
|
|
@@ -419,8 +419,8 @@ Guards habituales:
|
|
|
419
419
|
|
|
420
420
|
## Release actual
|
|
421
421
|
|
|
422
|
-
- npm package: `repo-harness@0.7.
|
|
423
|
-
- Generated workflow stamp: `repo-harness@0.7.
|
|
422
|
+
- npm package: `repo-harness@0.7.3`
|
|
423
|
+
- Generated workflow stamp: `repo-harness@0.7.3+template@0.7.3`
|
|
424
424
|
- GitHub repository: `Ancienttwo/repo-harness`
|
|
425
425
|
- Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
426
426
|
|
package/README.fr.md
CHANGED
|
@@ -85,7 +85,7 @@ l'emportent.
|
|
|
85
85
|
## Nouveautés
|
|
86
86
|
|
|
87
87
|
Les notes de version vivent dans [`docs/CHANGELOG.md`](docs/CHANGELOG.md). La
|
|
88
|
-
ligne actuelle est `0.7.
|
|
88
|
+
ligne actuelle est `0.7.3`.
|
|
89
89
|
|
|
90
90
|
## Comment ça marche
|
|
91
91
|
|
|
@@ -424,8 +424,8 @@ Guards courants :
|
|
|
424
424
|
|
|
425
425
|
## Release actuelle
|
|
426
426
|
|
|
427
|
-
- npm package : `repo-harness@0.7.
|
|
428
|
-
- Generated workflow stamp : `repo-harness@0.7.
|
|
427
|
+
- npm package : `repo-harness@0.7.3`
|
|
428
|
+
- Generated workflow stamp : `repo-harness@0.7.3+template@0.7.3`
|
|
429
429
|
- GitHub repository : `Ancienttwo/repo-harness`
|
|
430
430
|
- Release history : [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
431
431
|
|
package/README.ja.md
CHANGED
|
@@ -75,7 +75,7 @@ review、checks、handoff と食い違う場合は、source artifacts を優先
|
|
|
75
75
|
## What's New
|
|
76
76
|
|
|
77
77
|
リリースノートは [`docs/CHANGELOG.md`](docs/CHANGELOG.md) にあります。現在の
|
|
78
|
-
ラインは `0.7.
|
|
78
|
+
ラインは `0.7.3` です。
|
|
79
79
|
|
|
80
80
|
## 仕組み
|
|
81
81
|
|
|
@@ -398,8 +398,8 @@ hook がブロックしたときは、まず terminal の構造化された出
|
|
|
398
398
|
|
|
399
399
|
## 現在の Release
|
|
400
400
|
|
|
401
|
-
- npm package:`repo-harness@0.7.
|
|
402
|
-
- Generated workflow stamp:`repo-harness@0.7.
|
|
401
|
+
- npm package:`repo-harness@0.7.3`
|
|
402
|
+
- Generated workflow stamp:`repo-harness@0.7.3+template@0.7.3`
|
|
403
403
|
- GitHub repository:`Ancienttwo/repo-harness`
|
|
404
404
|
- Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
405
405
|
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ active plan, contract, review, checks, or handoff, the source artifacts win.
|
|
|
78
78
|
## What's New
|
|
79
79
|
|
|
80
80
|
Release notes live in [`docs/CHANGELOG.md`](docs/CHANGELOG.md). The current line
|
|
81
|
-
is `0.7.
|
|
81
|
+
is `0.7.3`.
|
|
82
82
|
|
|
83
83
|
## How It Works
|
|
84
84
|
|
|
@@ -405,7 +405,8 @@ and not arbitrary shell.
|
|
|
405
405
|
- Debug in this order: user-level adapter config -> `repo-harness-hook` (or fallback `repo-harness hook`) -> route registry -> `.ai/hooks/*`.
|
|
406
406
|
- If `repo-harness-hook` reports `.ai/hooks` drift, refresh the repo-local copy with `repo-harness adopt --repo <root>`.
|
|
407
407
|
|
|
408
|
-
The installed adapter owns eight managed hook routes.
|
|
408
|
+
The installed adapter owns eight shared managed hook routes. Codex also installs
|
|
409
|
+
three Codex-only bounded-delegation routes. The route tuple
|
|
409
410
|
`event + routeId + matcher` is the stable contract; script names are the current
|
|
410
411
|
implementation under `assets/hooks/` or a repo-pinned `.ai/hooks/` copy.
|
|
411
412
|
|
|
@@ -420,6 +421,11 @@ implementation under `assets/hooks/` or a repo-pinned `.ai/hooks/` copy.
|
|
|
420
421
|
| `UserPromptSubmit.default` | all prompts | `prompt-guard.sh` | Classifies prompt intent, routes planning/check/hunt hints, and renders host-safe workflow guidance. |
|
|
421
422
|
| `Stop.default` | session stop | `stop-orchestrator.sh` | Finalizes handoff and guards against ending with unresolved draft-plan or completion evidence gaps. |
|
|
422
423
|
|
|
424
|
+
Codex-only routes are `UserPromptSubmit.delegation`,
|
|
425
|
+
`SubagentStart.context`, and `SubagentStop.quality`. Claude keeps the shared
|
|
426
|
+
`PreToolUse.subagent` return-channel route and does not install those Codex
|
|
427
|
+
delegation lifecycle entries.
|
|
428
|
+
|
|
423
429
|
`SessionStart` resolves hooks central-first, then runs two ordered scripts before
|
|
424
430
|
work begins:
|
|
425
431
|
|
|
@@ -487,8 +493,8 @@ Most common guards:
|
|
|
487
493
|
|
|
488
494
|
## Current Release
|
|
489
495
|
|
|
490
|
-
- npm package: `repo-harness@0.7.
|
|
491
|
-
- Generated workflow stamp: `repo-harness@0.7.
|
|
496
|
+
- npm package: `repo-harness@0.7.3`
|
|
497
|
+
- Generated workflow stamp: `repo-harness@0.7.3+template@0.7.3`
|
|
492
498
|
- GitHub repository: `Ancienttwo/repo-harness`
|
|
493
499
|
- Release history: [`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
494
500
|
|
package/README.zh-CN.md
CHANGED
|
@@ -69,7 +69,7 @@ review、checks 或 handoff 冲突,以 source artifacts 为准。
|
|
|
69
69
|
|
|
70
70
|
## What's New
|
|
71
71
|
|
|
72
|
-
Release notes 见 [`docs/CHANGELOG.md`](docs/CHANGELOG.md),当前版本线是 `0.7.
|
|
72
|
+
Release notes 见 [`docs/CHANGELOG.md`](docs/CHANGELOG.md),当前版本线是 `0.7.3`。
|
|
73
73
|
|
|
74
74
|
## 工作原理
|
|
75
75
|
|
|
@@ -437,8 +437,8 @@ hook block 工作时,先看 terminal 里的结构化输出。核心字段是
|
|
|
437
437
|
|
|
438
438
|
## 当前 Release
|
|
439
439
|
|
|
440
|
-
- npm package:`repo-harness@0.7.
|
|
441
|
-
- Generated workflow stamp:`repo-harness@0.7.
|
|
440
|
+
- npm package:`repo-harness@0.7.3`
|
|
441
|
+
- Generated workflow stamp:`repo-harness@0.7.3+template@0.7.3`
|
|
442
442
|
- GitHub repository:`Ancienttwo/repo-harness`
|
|
443
443
|
- Release history:[`docs/CHANGELOG.md`](docs/CHANGELOG.md)
|
|
444
444
|
|
package/SKILL.md
CHANGED
|
@@ -15,14 +15,12 @@ Compatibility boundary:
|
|
|
15
15
|
- internal engine: CLI plus hook-backed tasks-first harness
|
|
16
16
|
- contract ID: tasks-first-harness-v1
|
|
17
17
|
- canonical skill, CLI, and package name: `repo-harness`
|
|
18
|
-
- retired legacy aliases: `repo-harness-skill`, `project-initializer`
|
|
19
18
|
- new-project creation surface: `repo-harness-scaffold` (secondary generator)
|
|
20
19
|
|
|
21
|
-
`repo-harness-skill` and `project-initializer`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
may remain only as legacy evidence.
|
|
20
|
+
The former `repo-harness-skill` and `project-initializer` aliases have been
|
|
21
|
+
fully removed. They are no longer recognized, synced, or cleaned up by any
|
|
22
|
+
tooling. Historical markers left behind in long-migrated repos are inert
|
|
23
|
+
evidence and are not handled.
|
|
26
24
|
|
|
27
25
|
The skill should not carry the whole workflow contract in prose. It should:
|
|
28
26
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Codex Delegation Advisor - UserPromptSubmit.delegation
|
|
3
|
+
# Converts explicit user delegation requests into bounded Codex subagent context.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
[[ "${HOOK_HOST:-}" == "codex" ]] || exit 0
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
# shellcheck source=/dev/null
|
|
11
|
+
. "$SCRIPT_DIR/hook-input.sh"
|
|
12
|
+
|
|
13
|
+
hook_read_stdin_once
|
|
14
|
+
input="$HOOK_STDIN_JSON"
|
|
15
|
+
[[ -n "$input" ]] || exit 0
|
|
16
|
+
command -v bun >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
18
|
+
JSON_INPUT="$input" REPO_ROOT="${HOOK_REPO_ROOT:-$(pwd)}" bun -e '
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const crypto = require("crypto");
|
|
22
|
+
|
|
23
|
+
function sanitize(value) {
|
|
24
|
+
return String(value || "")
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "")
|
|
28
|
+
.replace(/-{2,}/g, "-")
|
|
29
|
+
.slice(0, 120);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function firstString(input, keys) {
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const value = input?.[key];
|
|
35
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function delegationScope(input) {
|
|
41
|
+
const runId = firstString(input, ["run_id"]);
|
|
42
|
+
if (runId) return { source: "run_id", id: `run-${sanitize(runId)}` };
|
|
43
|
+
|
|
44
|
+
const sessionId = firstString(input, ["session_id"]);
|
|
45
|
+
if (sessionId) return { source: "session_id", id: `session-${sanitize(sessionId)}` };
|
|
46
|
+
|
|
47
|
+
const transcriptPath = firstString(input, ["transcript_path"]);
|
|
48
|
+
if (transcriptPath) {
|
|
49
|
+
const digest = crypto.createHash("sha1").update(transcriptPath).digest("hex").slice(0, 16);
|
|
50
|
+
return { source: "transcript_path", id: `transcript-${digest}` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const envSession = process.env.CODEX_SESSION_ID || process.env.CLAUDE_SESSION_ID || "";
|
|
54
|
+
if (envSession) return { source: "env_session", id: `session-${sanitize(envSession)}` };
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let input;
|
|
60
|
+
try {
|
|
61
|
+
input = JSON.parse(process.env.JSON_INPUT || "");
|
|
62
|
+
} catch {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prompt = firstString(input, [
|
|
67
|
+
"prompt",
|
|
68
|
+
"user_prompt",
|
|
69
|
+
"user_message",
|
|
70
|
+
"message",
|
|
71
|
+
"input",
|
|
72
|
+
]);
|
|
73
|
+
if (!prompt) process.exit(0);
|
|
74
|
+
|
|
75
|
+
function isDelegationDiscussion(text) {
|
|
76
|
+
if (!/\b(spawn|use|run)\s+(bounded\s+)?subagents?\b/i.test(text)) return false;
|
|
77
|
+
if (/^\s*(please\s+)?(spawn|use|run)\s+(bounded\s+)?subagents?\s+(to|for)\b/i.test(text)) return false;
|
|
78
|
+
return [
|
|
79
|
+
/[??]/,
|
|
80
|
+
/\b(should|need|necessary)\b/i,
|
|
81
|
+
/(机制|有必要|必要|是否|为什么|怎么|如何|架构|设计|注册|路由|本来就有)/i,
|
|
82
|
+
/\b(mechanism|architecture|design|registration|route|routing|adapter|hook)\b/i,
|
|
83
|
+
].some((pattern) => pattern.test(text));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const triggers = [
|
|
87
|
+
{ name: "slash-delegate", pattern: /(^|\s)\/(delegate|parallel)\b/i },
|
|
88
|
+
{ name: "spawn-subagents", pattern: /\b(spawn|use|run)\s+(bounded\s+)?subagents?\b/i, skipDiscussion: true },
|
|
89
|
+
{ name: "multiple-agents", pattern: /\buse\s+multiple\s+agents?\b/i },
|
|
90
|
+
{ name: "parallel-agents", pattern: /\bparallel\s+(agents?|workstreams?|investigation|research)\b/i },
|
|
91
|
+
{ name: "chinese-subagent", pattern: /交给\s*子代理|使用多个\s*(agent|代理)|并行(调查|研究|处理|执行|agent|代理)/i },
|
|
92
|
+
];
|
|
93
|
+
const trigger = triggers.find((entry) => entry.pattern.test(prompt) && !(entry.skipDiscussion && isDelegationDiscussion(prompt)));
|
|
94
|
+
if (!trigger) process.exit(0);
|
|
95
|
+
|
|
96
|
+
const repoRoot = process.env.REPO_ROOT || process.cwd();
|
|
97
|
+
const stateDir = path.join(repoRoot, ".ai", "harness", "delegation");
|
|
98
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const now = new Date();
|
|
101
|
+
const scope = delegationScope(input);
|
|
102
|
+
const relativeStateFile = scope ? path.join("turns", `${scope.id}.json`) : "latest.json";
|
|
103
|
+
const state = {
|
|
104
|
+
version: 1,
|
|
105
|
+
eligible: true,
|
|
106
|
+
explicit: true,
|
|
107
|
+
spawned: false,
|
|
108
|
+
fallback_used: false,
|
|
109
|
+
mode: "explicit",
|
|
110
|
+
max_agents: 3,
|
|
111
|
+
max_depth: 1,
|
|
112
|
+
allow_parallel_writers: false,
|
|
113
|
+
stop_fallback: true,
|
|
114
|
+
trigger: trigger.name,
|
|
115
|
+
prompt_hash: crypto.createHash("sha1").update(prompt).digest("hex"),
|
|
116
|
+
scope_source: scope?.source || "unscoped",
|
|
117
|
+
scope_id: scope?.id || "",
|
|
118
|
+
state_file: relativeStateFile,
|
|
119
|
+
created_at: now.toISOString(),
|
|
120
|
+
created_at_epoch: Math.floor(now.getTime() / 1000),
|
|
121
|
+
updated_at: now.toISOString(),
|
|
122
|
+
};
|
|
123
|
+
if (scope) {
|
|
124
|
+
const statePath = path.join(stateDir, relativeStateFile);
|
|
125
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
126
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
127
|
+
}
|
|
128
|
+
fs.writeFileSync(path.join(stateDir, "latest.json"), `${JSON.stringify(state, null, 2)}\n`);
|
|
129
|
+
|
|
130
|
+
const context = [
|
|
131
|
+
"[repo-harness:delegation]",
|
|
132
|
+
"",
|
|
133
|
+
"The current user prompt explicitly enabled bounded delegation.",
|
|
134
|
+
"",
|
|
135
|
+
"If this task contains at least two independent, bounded workstreams, call spawn_agent before doing the corresponding work in the parent.",
|
|
136
|
+
"",
|
|
137
|
+
"Rules:",
|
|
138
|
+
"- Spawn no more than 3 agents.",
|
|
139
|
+
"- Use explorer for read-only code mapping.",
|
|
140
|
+
"- Use worker only for an isolated implementation slice.",
|
|
141
|
+
"- Use reviewer for correctness, regression, security, and missing-test review.",
|
|
142
|
+
"- Never give two agents overlapping write ownership.",
|
|
143
|
+
"- Keep max spawn depth at 1.",
|
|
144
|
+
"- Give every agent a precise scope and required return format.",
|
|
145
|
+
"- Wait for all requested agents.",
|
|
146
|
+
"- Reconcile contradictory findings in the parent.",
|
|
147
|
+
"- Close completed agent threads.",
|
|
148
|
+
"- Do not spawn for a trivial or strictly sequential task.",
|
|
149
|
+
].join("\n");
|
|
150
|
+
|
|
151
|
+
process.stdout.write(`${JSON.stringify({
|
|
152
|
+
hookSpecificOutput: {
|
|
153
|
+
hookEventName: "UserPromptSubmit",
|
|
154
|
+
additionalContext: context,
|
|
155
|
+
},
|
|
156
|
+
})}\n`);
|
|
157
|
+
'
|
|
@@ -45,7 +45,22 @@
|
|
|
45
45
|
"UserPromptSubmit": [
|
|
46
46
|
{
|
|
47
47
|
"hooks": [
|
|
48
|
-
{ "type": "command", "command": "repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; HOOK_HOST=codex HOOK_REPO_ROOT=\"$repo\" bash \"$repo/.ai/hooks/run-hook.sh\" prompt-guard.sh", "timeout": 30 }
|
|
48
|
+
{ "type": "command", "command": "repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; HOOK_HOST=codex HOOK_REPO_ROOT=\"$repo\" bash \"$repo/.ai/hooks/run-hook.sh\" prompt-guard.sh", "timeout": 30 },
|
|
49
|
+
{ "type": "command", "command": "repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; HOOK_HOST=codex HOOK_REPO_ROOT=\"$repo\" bash \"$repo/.ai/hooks/run-hook.sh\" codex-delegation-advisor.sh", "timeout": 30 }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"SubagentStart": [
|
|
54
|
+
{
|
|
55
|
+
"hooks": [
|
|
56
|
+
{ "type": "command", "command": "repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; HOOK_HOST=codex HOOK_REPO_ROOT=\"$repo\" bash \"$repo/.ai/hooks/run-hook.sh\" subagent-start-context.sh", "timeout": 30 }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"SubagentStop": [
|
|
61
|
+
{
|
|
62
|
+
"hooks": [
|
|
63
|
+
{ "type": "command", "command": "repo=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; HOOK_HOST=codex HOOK_REPO_ROOT=\"$repo\" bash \"$repo/.ai/hooks/run-hook.sh\" subagent-stop-quality.sh", "timeout": 30 }
|
|
49
64
|
]
|
|
50
65
|
}
|
|
51
66
|
],
|
package/assets/hooks/run-hook.sh
CHANGED
|
@@ -37,16 +37,73 @@ fi
|
|
|
37
37
|
export HOOK_REPO_ROOT="$REPO_ROOT"
|
|
38
38
|
cd "$REPO_ROOT"
|
|
39
39
|
|
|
40
|
+
hook_stdout_is_json_kind() {
|
|
41
|
+
local stdout_file="$1"
|
|
42
|
+
local kind="$2"
|
|
43
|
+
|
|
44
|
+
[[ -s "$stdout_file" ]] || return 1
|
|
45
|
+
|
|
46
|
+
if command -v jq >/dev/null 2>&1; then
|
|
47
|
+
case "$kind" in
|
|
48
|
+
decision)
|
|
49
|
+
jq -e '(.decision == "block") or (.decision == "allow")' "$stdout_file" >/dev/null 2>&1
|
|
50
|
+
return
|
|
51
|
+
;;
|
|
52
|
+
user_prompt_context)
|
|
53
|
+
jq -e '.hookSpecificOutput.hookEventName == "UserPromptSubmit" and (.hookSpecificOutput.additionalContext | type == "string" and length > 0)' "$stdout_file" >/dev/null 2>&1
|
|
54
|
+
return
|
|
55
|
+
;;
|
|
56
|
+
subagent_start_context)
|
|
57
|
+
jq -e '.hookSpecificOutput.hookEventName == "SubagentStart" and (.hookSpecificOutput.additionalContext | type == "string" and length > 0)' "$stdout_file" >/dev/null 2>&1
|
|
58
|
+
return
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
command -v bun >/dev/null 2>&1 || return 1
|
|
64
|
+
STDOUT_FILE="$stdout_file" JSON_KIND="$kind" bun -e '
|
|
65
|
+
const fs = require("fs");
|
|
66
|
+
const file = process.env.STDOUT_FILE;
|
|
67
|
+
const kind = process.env.JSON_KIND;
|
|
68
|
+
const raw = fs.readFileSync(file, "utf8").trim();
|
|
69
|
+
if (!raw.startsWith("{")) process.exit(1);
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
if (kind === "decision") {
|
|
72
|
+
process.exit(parsed.decision === "block" || parsed.decision === "allow" ? 0 : 1);
|
|
73
|
+
}
|
|
74
|
+
const specific = parsed.hookSpecificOutput || {};
|
|
75
|
+
if (
|
|
76
|
+
kind === "user_prompt_context" &&
|
|
77
|
+
specific.hookEventName === "UserPromptSubmit" &&
|
|
78
|
+
typeof specific.additionalContext === "string" &&
|
|
79
|
+
specific.additionalContext.trim()
|
|
80
|
+
) process.exit(0);
|
|
81
|
+
if (
|
|
82
|
+
kind === "subagent_start_context" &&
|
|
83
|
+
specific.hookEventName === "SubagentStart" &&
|
|
84
|
+
typeof specific.additionalContext === "string" &&
|
|
85
|
+
specific.additionalContext.trim()
|
|
86
|
+
) process.exit(0);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
' >/dev/null 2>&1
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
# Codex swallows hook stdout differently from Claude: success stdout is
|
|
41
|
-
# dropped for
|
|
42
|
-
#
|
|
92
|
+
# dropped for ordinary hooks. Only context/decision JSON for approved routes is
|
|
93
|
+
# surfaced on success.
|
|
43
94
|
if [[ "${HOOK_HOST:-}" == "codex" && "$HOOK_NAME" != "session-start-context.sh" ]]; then
|
|
44
95
|
if ! tmp_stdout="$(mktemp)" || ! tmp_stderr="$(mktemp)"; then
|
|
45
96
|
# No temp space: run unfiltered rather than silently dropping the hook.
|
|
46
97
|
exec bash "$HOOK_PATH" "$@"
|
|
47
98
|
fi
|
|
48
99
|
if bash "$HOOK_PATH" "$@" >"$tmp_stdout" 2>"$tmp_stderr"; then
|
|
49
|
-
if [[ "$HOOK_NAME" == "stop-orchestrator.sh" ]] &&
|
|
100
|
+
if [[ "$HOOK_NAME" == "stop-orchestrator.sh" ]] && hook_stdout_is_json_kind "$tmp_stdout" decision; then
|
|
101
|
+
cat "$tmp_stdout"
|
|
102
|
+
elif [[ "$HOOK_NAME" == "subagent-stop-quality.sh" ]] && hook_stdout_is_json_kind "$tmp_stdout" decision; then
|
|
103
|
+
cat "$tmp_stdout"
|
|
104
|
+
elif [[ "$HOOK_NAME" == "codex-delegation-advisor.sh" ]] && hook_stdout_is_json_kind "$tmp_stdout" user_prompt_context; then
|
|
105
|
+
cat "$tmp_stdout"
|
|
106
|
+
elif [[ "$HOOK_NAME" == "subagent-start-context.sh" ]] && hook_stdout_is_json_kind "$tmp_stdout" subagent_start_context; then
|
|
50
107
|
cat "$tmp_stdout"
|
|
51
108
|
fi
|
|
52
109
|
rm -f "$tmp_stdout" "$tmp_stderr"
|
|
@@ -131,6 +131,115 @@ emit_stop_block_json() {
|
|
|
131
131
|
printf '{"decision":"block","reason":"%s"}\n' "$(workflow_json_escape "$reason")"
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
delegation_state_paths_json() {
|
|
135
|
+
local state_dir
|
|
136
|
+
|
|
137
|
+
state_dir="${HOOK_REPO_ROOT:-$(pwd)}/.ai/harness/delegation"
|
|
138
|
+
[[ -f "$state_dir/latest.json" ]] || return 1
|
|
139
|
+
command -v bun >/dev/null 2>&1 || return 1
|
|
140
|
+
|
|
141
|
+
JSON_INPUT="${HOOK_STDIN_JSON:-}" DELEGATION_STATE_DIR="$state_dir" bun -e '
|
|
142
|
+
const fs = require("fs");
|
|
143
|
+
const path = require("path");
|
|
144
|
+
const crypto = require("crypto");
|
|
145
|
+
|
|
146
|
+
function sanitize(value) {
|
|
147
|
+
return String(value || "")
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
150
|
+
.replace(/^-+|-+$/g, "")
|
|
151
|
+
.replace(/-{2,}/g, "-")
|
|
152
|
+
.slice(0, 120);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function firstString(input, keys) {
|
|
156
|
+
for (const key of keys) {
|
|
157
|
+
const value = input?.[key];
|
|
158
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
159
|
+
}
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseInput() {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(process.env.JSON_INPUT || "{}");
|
|
166
|
+
} catch {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function delegationScope(input) {
|
|
172
|
+
const runId = firstString(input, ["run_id"]);
|
|
173
|
+
if (runId) return { source: "run_id", id: `run-${sanitize(runId)}` };
|
|
174
|
+
|
|
175
|
+
const sessionId = firstString(input, ["session_id"]);
|
|
176
|
+
if (sessionId) return { source: "session_id", id: `session-${sanitize(sessionId)}` };
|
|
177
|
+
|
|
178
|
+
const transcriptPath = firstString(input, ["transcript_path"]);
|
|
179
|
+
if (transcriptPath) {
|
|
180
|
+
const digest = crypto.createHash("sha1").update(transcriptPath).digest("hex").slice(0, 16);
|
|
181
|
+
return { source: "transcript_path", id: `transcript-${digest}` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const envSession = process.env.CODEX_SESSION_ID || process.env.CLAUDE_SESSION_ID || "";
|
|
185
|
+
if (envSession) return { source: "env_session", id: `session-${sanitize(envSession)}` };
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stateDir = process.env.DELEGATION_STATE_DIR;
|
|
191
|
+
const latestPath = path.join(stateDir, "latest.json");
|
|
192
|
+
const latest = JSON.parse(fs.readFileSync(latestPath, "utf8"));
|
|
193
|
+
const scope = delegationScope(parseInput());
|
|
194
|
+
if (latest.scope_id) {
|
|
195
|
+
if (!scope || latest.scope_id !== scope.id) process.exit(1);
|
|
196
|
+
const statePath = path.resolve(stateDir, latest.state_file || path.join("turns", `${latest.scope_id}.json`));
|
|
197
|
+
const stateRoot = path.resolve(stateDir) + path.sep;
|
|
198
|
+
if (!statePath.startsWith(stateRoot)) process.exit(1);
|
|
199
|
+
process.stdout.write(JSON.stringify({ latestPath, statePath }));
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(JSON.stringify({ latestPath, statePath: latestPath }));
|
|
203
|
+
' 2>/dev/null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
delegation_should_block() {
|
|
207
|
+
local stop_active="$1"
|
|
208
|
+
local state_paths
|
|
209
|
+
|
|
210
|
+
[[ "$stop_active" != "true" ]] || return 1
|
|
211
|
+
state_paths="$(delegation_state_paths_json)" || return 1
|
|
212
|
+
command -v bun >/dev/null 2>&1 || return 1
|
|
213
|
+
|
|
214
|
+
DELEGATION_STATE_PATHS="$state_paths" bun -e '
|
|
215
|
+
const fs = require("fs");
|
|
216
|
+
const paths = JSON.parse(process.env.DELEGATION_STATE_PATHS || "{}");
|
|
217
|
+
const state = JSON.parse(fs.readFileSync(paths.statePath, "utf8"));
|
|
218
|
+
const now = Math.floor(Date.now() / 1000);
|
|
219
|
+
const age = Number.isFinite(Number(state.created_at_epoch)) ? now - Number(state.created_at_epoch) : 0;
|
|
220
|
+
const fresh = age >= 0 && age <= 24 * 60 * 60;
|
|
221
|
+
process.exit(state.eligible === true && state.explicit === true && state.spawned !== true && state.fallback_used !== true && state.stop_fallback !== false && fresh ? 0 : 1);
|
|
222
|
+
' >/dev/null 2>&1
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
delegation_mark_fallback_used() {
|
|
226
|
+
local state_paths
|
|
227
|
+
|
|
228
|
+
state_paths="$(delegation_state_paths_json)" || return 0
|
|
229
|
+
command -v bun >/dev/null 2>&1 || return 0
|
|
230
|
+
|
|
231
|
+
DELEGATION_STATE_PATHS="$state_paths" bun -e '
|
|
232
|
+
const fs = require("fs");
|
|
233
|
+
const paths = JSON.parse(process.env.DELEGATION_STATE_PATHS || "{}");
|
|
234
|
+
const state = JSON.parse(fs.readFileSync(paths.statePath, "utf8"));
|
|
235
|
+
state.fallback_used = true;
|
|
236
|
+
state.fallback_used_at = new Date().toISOString();
|
|
237
|
+
state.updated_at = state.fallback_used_at;
|
|
238
|
+
fs.writeFileSync(paths.statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
239
|
+
fs.writeFileSync(paths.latestPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
240
|
+
' >/dev/null 2>&1 || true
|
|
241
|
+
}
|
|
242
|
+
|
|
134
243
|
refresh_handoff() {
|
|
135
244
|
workflow_write_handoff "session-stop"
|
|
136
245
|
echo "[FinalizeHandoff] Refreshed $(workflow_handoff_file)." >&2
|
|
@@ -167,5 +276,11 @@ if should_run_plan_completeness_gate "$stop_hook_active" "$last_assistant_messag
|
|
|
167
276
|
emit_stop_block_json "[PlanCompletenessGate] A first planning answer was produced while pending orchestration is still open: ${summary}
|
|
168
277
|
|
|
169
278
|
${guidance}"
|
|
279
|
+
exit 0
|
|
170
280
|
fi
|
|
171
281
|
fi
|
|
282
|
+
|
|
283
|
+
if delegation_should_block "$stop_hook_active"; then
|
|
284
|
+
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."
|
|
286
|
+
fi
|