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.
Files changed (40) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.es.md +3 -3
  4. package/README.fr.md +3 -3
  5. package/README.ja.md +3 -3
  6. package/README.md +10 -4
  7. package/README.zh-CN.md +3 -3
  8. package/SKILL.md +4 -6
  9. package/assets/hooks/codex-delegation-advisor.sh +157 -0
  10. package/assets/hooks/codex.hooks.template.json +16 -1
  11. package/assets/hooks/run-hook.sh +60 -3
  12. package/assets/hooks/stop-orchestrator.sh +115 -0
  13. package/assets/hooks/subagent-start-context.sh +124 -0
  14. package/assets/hooks/subagent-stop-quality.sh +121 -0
  15. package/assets/reference-configs/external-tooling.md +3 -2
  16. package/assets/reference-configs/hook-operations.md +6 -2
  17. package/assets/skill-commands/repo-harness-goal/SKILL.md +2 -0
  18. package/assets/skill-commands/repo-harness-migrate/SKILL.md +1 -1
  19. package/assets/skill-version.json +6 -6
  20. package/assets/templates/helpers/check-task-workflow.sh +83 -4
  21. package/assets/templates/helpers/ensure-task-workflow.sh +10 -1
  22. package/assets/templates/helpers/migrate-workflow-docs.ts +6 -6
  23. package/assets/workflow-contract.v1.json +1 -0
  24. package/package.json +1 -1
  25. package/scripts/check-task-workflow.sh +83 -4
  26. package/scripts/ensure-task-workflow.sh +10 -1
  27. package/scripts/hook-dispatch-diet-report.ts +3 -1
  28. package/scripts/lib/project-init-lib.sh +14 -4
  29. package/scripts/migrate-project-template.sh +1 -1
  30. package/scripts/migrate-workflow-docs.ts +6 -6
  31. package/scripts/sync-codex-installed-copies.sh +0 -21
  32. package/src/cli/commands/mcp.ts +2 -2
  33. package/src/cli/commands/status.ts +2 -2
  34. package/src/cli/hook/route-registry.ts +41 -1
  35. package/src/cli/hook/runtime.ts +54 -5
  36. package/src/cli/installer/managed-entries.ts +3 -3
  37. package/src/cli/installer/targets/claude.ts +2 -2
  38. package/src/cli/installer/targets/codex.ts +3 -3
  39. package/src/cli/repo-adoption/reclaim-runtime.ts +63 -4
  40. 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 are retired and removed by installed-copy sync. Claude and Codex should follow the same repo-local workflow surface.
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 are retired and removed by installed-copy sync. Claude and Codex should follow the same repo-local workflow surface.
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.2`.
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.2`
423
- - Generated workflow stamp: `repo-harness@0.7.2+template@0.7.2`
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.2`.
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.2`
428
- - Generated workflow stamp : `repo-harness@0.7.2+template@0.7.2`
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.2` です。
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.2`
402
- - Generated workflow stamp:`repo-harness@0.7.2+template@0.7.2`
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.2`.
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. The route tuple
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.2`
491
- - Generated workflow stamp: `repo-harness@0.7.2+template@0.7.2`
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.2`。
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.2`
441
- - Generated workflow stamp:`repo-harness@0.7.2+template@0.7.2`
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` are retired: do not create,
22
- sync, or search their directories under `~/.codex/skills` or
23
- `~/.claude/skills` as active upstream roots; installed-copy sync removes any
24
- existing copies. Historical `project-initializer` markers in generated files
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
  ],
@@ -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 every hook except session-start-context.sh, and only
42
- # stop-orchestrator.sh may surface its Stop decision JSON on success.
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" ]] && grep -q '"decision"[[:space:]]*:' "$tmp_stdout"; then
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