devrites 3.2.2 → 3.2.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/command-map.md +3 -3
- package/docs/engine/agent-contract.md +3 -2
- package/docs/harness-compliance.md +2 -2
- package/docs/orchestration.md +15 -12
- package/docs/porting-to-a-new-harness.md +2 -1
- package/docs/skills.md +7 -2
- package/engine/hooks.go +39 -4
- package/engine/hooks_agent_dispatch.go +1193 -0
- package/engine/hooks_workspace.go +78 -10
- package/engine/internal/forge/forge.go +0 -2
- package/engine/internal/harness/compliance.go +1 -1
- package/engine/internal/harness/harness.go +3 -0
- package/engine/internal/lib/doubtcoverage.go +2 -12
- package/engine/internal/lib/reconcile.go +4 -29
- package/engine/internal/lib/reconcile_test.go +14 -10
- package/engine/main.go +4 -1
- package/engine/tests/hook_test.go +874 -1
- package/engine/tests/lib_parity_test.go +5 -17
- package/engine/tests/parity_reconcile_test.go +1 -1
- package/engine/tests/parity_test.go +2 -6
- package/pack/.claude/skills/devrites-api-interface/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-audit/SKILL.md +5 -6
- package/pack/.claude/skills/devrites-browser-proof/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-doubt/SKILL.md +2 -1
- package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-interview/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +14 -8
- package/pack/.claude/skills/devrites-prose-craft/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-source-driven/SKILL.md +1 -0
- package/pack/.claude/skills/devrites-ux-shape/SKILL.md +1 -0
- package/pack/.claude/skills/rite/SKILL.md +1 -0
- package/pack/.claude/skills/rite-adopt/SKILL.md +1 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +1 -0
- package/pack/.claude/skills/rite-build/SKILL.md +2 -1
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/.claude/skills/rite-clarify/SKILL.md +1 -0
- package/pack/.claude/skills/rite-converge/SKILL.md +1 -0
- package/pack/.claude/skills/rite-customize/SKILL.md +1 -0
- package/pack/.claude/skills/rite-define/SKILL.md +1 -0
- package/pack/.claude/skills/rite-doctor/SKILL.md +1 -0
- package/pack/.claude/skills/rite-dogfood/SKILL.md +1 -0
- package/pack/.claude/skills/rite-explain/SKILL.md +4 -2
- package/pack/.claude/skills/rite-frame/SKILL.md +1 -0
- package/pack/.claude/skills/rite-handoff/SKILL.md +1 -0
- package/pack/.claude/skills/rite-learn/SKILL.md +1 -0
- package/pack/.claude/skills/rite-plan/SKILL.md +1 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pov/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pr-feedback/SKILL.md +1 -0
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +1 -0
- package/pack/.claude/skills/rite-prototype/SKILL.md +1 -0
- package/pack/.claude/skills/rite-prove/SKILL.md +1 -0
- package/pack/.claude/skills/rite-quick/SKILL.md +1 -0
- package/pack/.claude/skills/rite-resolve/SKILL.md +1 -0
- package/pack/.claude/skills/rite-review/SKILL.md +1 -0
- package/pack/.claude/skills/rite-seal/SKILL.md +1 -0
- package/pack/.claude/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/.claude/skills/rite-ship/SKILL.md +1 -0
- package/pack/.claude/skills/rite-spec/SKILL.md +1 -0
- package/pack/.claude/skills/rite-status/SKILL.md +1 -0
- package/pack/.claude/skills/rite-temper/SKILL.md +3 -2
- package/pack/.claude/skills/rite-upgrade/SKILL.md +3 -1
- package/pack/.claude/skills/rite-vet/SKILL.md +1 -0
- package/pack/.claude/skills/rite-zoom-out/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-audit/SKILL.md +5 -6
- package/pack/generated/claude/skills/devrites-browser-proof/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-doubt/SKILL.md +2 -1
- package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-interview/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +14 -8
- package/pack/generated/claude/skills/devrites-prose-craft/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-refresh-indexes/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +1 -0
- package/pack/generated/claude/skills/devrites-ux-shape/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-adopt/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-build/SKILL.md +2 -1
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-converge/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-customize/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-define/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-dogfood/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-explain/SKILL.md +4 -2
- package/pack/generated/claude/skills/rite-frame/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-learn/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-plan/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pov/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-prototype/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-prove/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-quick/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-resolve/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-review/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-seal/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/generated/claude/skills/rite-ship/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-spec/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-status/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-temper/SKILL.md +3 -2
- package/pack/generated/claude/skills/rite-upgrade/SKILL.md +3 -1
- package/pack/generated/claude/skills/rite-vet/SKILL.md +1 -0
- package/pack/generated/claude/skills/rite-zoom-out/SKILL.md +1 -0
- package/pack/generated/codex/AGENTS.md +2 -4
- package/pack/generated/codex/hooks.json +24 -5
- package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-audit/SKILL.md +10 -10
- package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-doubt/SKILL.md +7 -5
- package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-interview/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-lib/reference/model-tiers.md +4 -3
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +4 -5
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +14 -8
- package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +6 -4
- package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-adopt/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-build/SKILL.md +7 -5
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -4
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +11 -12
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-converge/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-customize/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-define/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-dogfood/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-explain/SKILL.md +9 -6
- package/pack/generated/codex/skills/rite-frame/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-handoff/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-plan/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-polish/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pov/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-prototype/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-prove/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-quick/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-resolve/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-review/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-seal/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +2 -3
- package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +10 -12
- package/pack/generated/codex/skills/rite-ship/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-spec/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-status/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-temper/SKILL.md +8 -6
- package/pack/generated/codex/skills/rite-upgrade/SKILL.md +8 -5
- package/pack/generated/codex/skills/rite-vet/SKILL.md +6 -4
- package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +6 -4
- package/package.json +1 -1
- package/scripts/codex-generate.sh +32 -18
- package/scripts/live-hosts/agent-result.schema.json +2 -1
- package/scripts/live-hosts/claude.sh +2 -2
- package/scripts/live-hosts/codex.sh +4 -0
- package/scripts/live-hosts/fake-host.py +1 -8
- package/scripts/live-hosts/host-transport.py +1 -2
- package/scripts/run-agent-contract-evals.py +3 -3
- package/scripts/run-evals.sh +3 -34
- package/scripts/validate-frontmatter.py +34 -1
- package/engine/testdata/golden/TestParityA1Guard/inline-fallback-silent.golden +0 -1
- package/engine/testdata/golden/TestParityDoubtCoverage/arg=inline.golden +0 -3
- package/engine/testdata/golden/TestParityDoubtCoverage/arg=missing-inline.golden +0 -3
- package/scripts/eval-runner.py +0 -275
- package/scripts/eval-scorer.mjs +0 -46
- package/scripts/render-eval-summary.py +0 -48
- /package/engine/testdata/golden/TestParityReconcile/{inline-fallback → close-window}/snapshot.golden +0 -0
- /package/engine/testdata/golden/TestParityReconcile/{inline-fallback.golden → close-window.golden} +0 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"bytes"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"fmt"
|
|
10
|
+
"io"
|
|
11
|
+
"io/fs"
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"regexp"
|
|
15
|
+
"sort"
|
|
16
|
+
"strings"
|
|
17
|
+
"time"
|
|
18
|
+
|
|
19
|
+
"github.com/devrites/devrites/internal/harness"
|
|
20
|
+
"github.com/devrites/devrites/internal/orient"
|
|
21
|
+
"github.com/devrites/devrites/internal/safepath"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const (
|
|
25
|
+
agentDispatchStateVersion = "devrites-agent-dispatch/v1"
|
|
26
|
+
agentDispatchStateDir = "devrites-agent-dispatch-v1"
|
|
27
|
+
agentDispatchMetadataRole = "devrites-skill-contract-error"
|
|
28
|
+
maxCodexRolloutLine = 8 << 20
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
var (
|
|
32
|
+
devritesAgentPathRe = regexp.MustCompile(`\.codex/agents/(devrites-[a-z0-9-]+)\.toml`)
|
|
33
|
+
devritesAgentNameRe = regexp.MustCompile(`^devrites-[a-z0-9-]+$`)
|
|
34
|
+
skillInvocationRe = regexp.MustCompile(`(?i)(?:^|[^a-z0-9_./-])(?:\$|/)([a-z][a-z0-9-]*)\b`)
|
|
35
|
+
reconcileTerminalRe = regexp.MustCompile(`(?m)(?:^|[;&|]\s*)(?:rtk\s+)?(?:[A-Za-z0-9_./-]+/)?devrites-engine\s+reconcile\s+(?:check|close)\b`)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
type agentDispatchHookInput struct {
|
|
39
|
+
HookEventName string `json:"hook_event_name"`
|
|
40
|
+
SessionID string `json:"session_id"`
|
|
41
|
+
TurnID string `json:"turn_id"`
|
|
42
|
+
ToolName string `json:"tool_name"`
|
|
43
|
+
ToolUseID string `json:"tool_use_id"`
|
|
44
|
+
AgentType string `json:"agent_type"`
|
|
45
|
+
AgentID string `json:"agent_id"`
|
|
46
|
+
Prompt string `json:"prompt"`
|
|
47
|
+
StopHookActive bool `json:"stop_hook_active"`
|
|
48
|
+
LastAssistantMessage string `json:"last_assistant_message"`
|
|
49
|
+
ToolInputRaw json.RawMessage `json:"-"`
|
|
50
|
+
ToolInput struct {
|
|
51
|
+
Command string `json:"command"`
|
|
52
|
+
AgentType string `json:"agent_type"`
|
|
53
|
+
ForkTurns string `json:"fork_turns"`
|
|
54
|
+
Message string `json:"message"`
|
|
55
|
+
TaskName string `json:"task_name"`
|
|
56
|
+
ReceiverThreadIDs []string `json:"receiver_thread_ids"`
|
|
57
|
+
IDs []string `json:"ids"`
|
|
58
|
+
AgentIDs []string `json:"agent_ids"`
|
|
59
|
+
Target string `json:"target"`
|
|
60
|
+
} `json:"tool_input"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type agentDispatchEvent struct {
|
|
64
|
+
Version string `json:"version"`
|
|
65
|
+
Event string `json:"event"`
|
|
66
|
+
TurnID string `json:"turn_id"`
|
|
67
|
+
ToolUseID string `json:"tool_use_id,omitempty"`
|
|
68
|
+
AgentID string `json:"agent_id,omitempty"`
|
|
69
|
+
AgentType string `json:"agent_type,omitempty"`
|
|
70
|
+
Role string `json:"role,omitempty"`
|
|
71
|
+
WindowID string `json:"window_id,omitempty"`
|
|
72
|
+
ResultSHA256 string `json:"result_sha256,omitempty"`
|
|
73
|
+
AtUnixNano int64 `json:"at_unix_nano"`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type agentDispatchAttempt struct {
|
|
77
|
+
Role string
|
|
78
|
+
AgentType string
|
|
79
|
+
ToolUseID string
|
|
80
|
+
AgentID string
|
|
81
|
+
WindowID string
|
|
82
|
+
Started bool
|
|
83
|
+
Stopped bool
|
|
84
|
+
Waited bool
|
|
85
|
+
ResultSHA256 string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type codexRolloutRecord struct {
|
|
89
|
+
Timestamp string `json:"timestamp"`
|
|
90
|
+
Type string `json:"type"`
|
|
91
|
+
Payload struct {
|
|
92
|
+
Type string `json:"type"`
|
|
93
|
+
SessionID string `json:"session_id"`
|
|
94
|
+
ID string `json:"id"`
|
|
95
|
+
ParentThreadID string `json:"parent_thread_id"`
|
|
96
|
+
CWD string `json:"cwd"`
|
|
97
|
+
AgentPath string `json:"agent_path"`
|
|
98
|
+
AgentRole string `json:"agent_role"`
|
|
99
|
+
Name string `json:"name"`
|
|
100
|
+
Arguments string `json:"arguments"`
|
|
101
|
+
Author string `json:"author"`
|
|
102
|
+
Recipient string `json:"recipient"`
|
|
103
|
+
Role string `json:"role"`
|
|
104
|
+
LastAgentResult string `json:"last_agent_message"`
|
|
105
|
+
Content []struct {
|
|
106
|
+
Type string `json:"type"`
|
|
107
|
+
Text string `json:"text"`
|
|
108
|
+
} `json:"content"`
|
|
109
|
+
Metadata struct {
|
|
110
|
+
TurnID string `json:"turn_id"`
|
|
111
|
+
} `json:"internal_chat_message_metadata_passthrough"`
|
|
112
|
+
} `json:"payload"`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type codexV2Spawn struct {
|
|
116
|
+
Role string
|
|
117
|
+
AgentPath string
|
|
118
|
+
SpawnLine int
|
|
119
|
+
ResultLine int
|
|
120
|
+
SpawnedAt time.Time
|
|
121
|
+
Result string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func parseAgentDispatchHookInput(r io.Reader) (agentDispatchHookInput, error) {
|
|
125
|
+
var in agentDispatchHookInput
|
|
126
|
+
data, err := io.ReadAll(r)
|
|
127
|
+
if err != nil {
|
|
128
|
+
return in, err
|
|
129
|
+
}
|
|
130
|
+
if err := json.Unmarshal(data, &in); err != nil {
|
|
131
|
+
return in, err
|
|
132
|
+
}
|
|
133
|
+
var raw struct {
|
|
134
|
+
ToolInput json.RawMessage `json:"tool_input"`
|
|
135
|
+
}
|
|
136
|
+
if err := json.Unmarshal(data, &raw); err != nil {
|
|
137
|
+
return in, err
|
|
138
|
+
}
|
|
139
|
+
in.ToolInputRaw = raw.ToolInput
|
|
140
|
+
in.HookEventName = strings.TrimSpace(in.HookEventName)
|
|
141
|
+
in.SessionID = strings.TrimSpace(in.SessionID)
|
|
142
|
+
in.TurnID = strings.TrimSpace(in.TurnID)
|
|
143
|
+
in.ToolName = strings.TrimSpace(in.ToolName)
|
|
144
|
+
in.ToolUseID = strings.TrimSpace(in.ToolUseID)
|
|
145
|
+
in.AgentType = strings.TrimSpace(in.AgentType)
|
|
146
|
+
in.AgentID = strings.TrimSpace(in.AgentID)
|
|
147
|
+
in.ToolInput.AgentType = strings.TrimSpace(in.ToolInput.AgentType)
|
|
148
|
+
in.ToolInput.ForkTurns = strings.TrimSpace(in.ToolInput.ForkTurns)
|
|
149
|
+
in.ToolInput.TaskName = strings.TrimSpace(in.ToolInput.TaskName)
|
|
150
|
+
return in, nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func agentDispatchRoot() (string, bool) {
|
|
154
|
+
root, err := orient.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
|
|
155
|
+
if err != nil {
|
|
156
|
+
return "", false
|
|
157
|
+
}
|
|
158
|
+
return root, true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func agentDispatchLogPath(root, sessionID string) (string, error) {
|
|
162
|
+
if strings.TrimSpace(sessionID) == "" {
|
|
163
|
+
return "", fmt.Errorf("missing session_id")
|
|
164
|
+
}
|
|
165
|
+
rootSum := sha256.Sum256([]byte(filepath.Clean(root)))
|
|
166
|
+
sessionSum := sha256.Sum256([]byte(sessionID))
|
|
167
|
+
dir := filepath.Join(os.TempDir(), agentDispatchStateDir, hex.EncodeToString(rootSum[:]))
|
|
168
|
+
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
169
|
+
return "", err
|
|
170
|
+
}
|
|
171
|
+
if err := os.Chmod(dir, 0o700); err != nil {
|
|
172
|
+
return "", err
|
|
173
|
+
}
|
|
174
|
+
return filepath.Join(dir, hex.EncodeToString(sessionSum[:])+".jsonl"), nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func appendAgentDispatchEvent(root, sessionID string, event agentDispatchEvent) error {
|
|
178
|
+
path, err := agentDispatchLogPath(root, sessionID)
|
|
179
|
+
if err != nil {
|
|
180
|
+
return err
|
|
181
|
+
}
|
|
182
|
+
event.Version = agentDispatchStateVersion
|
|
183
|
+
event.AtUnixNano = time.Now().UnixNano()
|
|
184
|
+
data, err := json.Marshal(event)
|
|
185
|
+
if err != nil {
|
|
186
|
+
return err
|
|
187
|
+
}
|
|
188
|
+
data = append(data, '\n')
|
|
189
|
+
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return err
|
|
192
|
+
}
|
|
193
|
+
defer f.Close()
|
|
194
|
+
if err := f.Chmod(0o600); err != nil {
|
|
195
|
+
return err
|
|
196
|
+
}
|
|
197
|
+
_, err = f.Write(data)
|
|
198
|
+
return err
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func readAgentDispatchEvents(root, sessionID string) ([]agentDispatchEvent, error) {
|
|
202
|
+
path, err := agentDispatchLogPath(root, sessionID)
|
|
203
|
+
if err != nil {
|
|
204
|
+
return nil, err
|
|
205
|
+
}
|
|
206
|
+
data, err := os.ReadFile(path)
|
|
207
|
+
if os.IsNotExist(err) {
|
|
208
|
+
return nil, nil
|
|
209
|
+
}
|
|
210
|
+
if err != nil {
|
|
211
|
+
return nil, err
|
|
212
|
+
}
|
|
213
|
+
var events []agentDispatchEvent
|
|
214
|
+
for _, line := range bytes.Split(data, []byte{'\n'}) {
|
|
215
|
+
if len(bytes.TrimSpace(line)) == 0 {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
var event agentDispatchEvent
|
|
219
|
+
if err := json.Unmarshal(line, &event); err != nil {
|
|
220
|
+
return nil, fmt.Errorf("invalid agent dispatch state: %w", err)
|
|
221
|
+
}
|
|
222
|
+
if event.Version != agentDispatchStateVersion {
|
|
223
|
+
return nil, fmt.Errorf("unsupported agent dispatch state %q", event.Version)
|
|
224
|
+
}
|
|
225
|
+
events = append(events, event)
|
|
226
|
+
}
|
|
227
|
+
return events, nil
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func explicitRolesInText(text string) []string {
|
|
231
|
+
matches := devritesAgentPathRe.FindAllStringSubmatch(text, -1)
|
|
232
|
+
seen := map[string]struct{}{}
|
|
233
|
+
var roles []string
|
|
234
|
+
for _, match := range matches {
|
|
235
|
+
role := match[1]
|
|
236
|
+
if _, ok := seen[role]; ok {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
seen[role] = struct{}{}
|
|
240
|
+
roles = append(roles, role)
|
|
241
|
+
}
|
|
242
|
+
sort.Strings(roles)
|
|
243
|
+
return roles
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func requiredAgentRolesFromSkill(path string) ([]string, error) {
|
|
247
|
+
info, err := os.Stat(path)
|
|
248
|
+
if err != nil {
|
|
249
|
+
return nil, err
|
|
250
|
+
}
|
|
251
|
+
if !info.Mode().IsRegular() {
|
|
252
|
+
return nil, fmt.Errorf("skill contract is not a regular file")
|
|
253
|
+
}
|
|
254
|
+
if info.Size() > 256<<10 {
|
|
255
|
+
return nil, fmt.Errorf("skill contract exceeds 256 KiB")
|
|
256
|
+
}
|
|
257
|
+
data, err := os.ReadFile(path)
|
|
258
|
+
if err != nil {
|
|
259
|
+
return nil, err
|
|
260
|
+
}
|
|
261
|
+
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
|
|
262
|
+
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
263
|
+
return nil, fmt.Errorf("skill contract is missing frontmatter")
|
|
264
|
+
}
|
|
265
|
+
value := ""
|
|
266
|
+
found := false
|
|
267
|
+
closed := false
|
|
268
|
+
for _, line := range lines[1:] {
|
|
269
|
+
if strings.TrimSpace(line) == "---" {
|
|
270
|
+
closed = true
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
if !strings.HasPrefix(line, "required-agent-roles:") {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if found {
|
|
277
|
+
return nil, fmt.Errorf("required-agent-roles is duplicated")
|
|
278
|
+
}
|
|
279
|
+
found = true
|
|
280
|
+
value = strings.TrimSpace(strings.TrimPrefix(line, "required-agent-roles:"))
|
|
281
|
+
}
|
|
282
|
+
if !closed {
|
|
283
|
+
return nil, fmt.Errorf("skill contract has unterminated frontmatter")
|
|
284
|
+
}
|
|
285
|
+
if !found {
|
|
286
|
+
return nil, fmt.Errorf("skill contract is missing required-agent-roles")
|
|
287
|
+
}
|
|
288
|
+
value = strings.Trim(value, `"'`)
|
|
289
|
+
if value == "none" {
|
|
290
|
+
return nil, nil
|
|
291
|
+
}
|
|
292
|
+
if value == "" {
|
|
293
|
+
return nil, fmt.Errorf("required-agent-roles is empty")
|
|
294
|
+
}
|
|
295
|
+
seen := map[string]struct{}{}
|
|
296
|
+
var roles []string
|
|
297
|
+
for _, item := range strings.Split(value, ",") {
|
|
298
|
+
role := strings.TrimSpace(item)
|
|
299
|
+
if !devritesAgentNameRe.MatchString(role) {
|
|
300
|
+
return nil, fmt.Errorf("invalid required agent role %q", role)
|
|
301
|
+
}
|
|
302
|
+
if _, ok := seen[role]; ok {
|
|
303
|
+
return nil, fmt.Errorf("duplicate required agent role %q", role)
|
|
304
|
+
}
|
|
305
|
+
seen[role] = struct{}{}
|
|
306
|
+
roles = append(roles, role)
|
|
307
|
+
}
|
|
308
|
+
sort.Strings(roles)
|
|
309
|
+
return roles, nil
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func requiredRolesInPrompt(root, text string) ([]string, error) {
|
|
313
|
+
seen := map[string]struct{}{}
|
|
314
|
+
skillsDir := filepath.Join(filepath.Dir(root), ".agents", "skills")
|
|
315
|
+
for _, match := range skillInvocationRe.FindAllStringSubmatch(text, -1) {
|
|
316
|
+
skill := strings.ToLower(match[1])
|
|
317
|
+
path := filepath.Join(skillsDir, skill, "SKILL.md")
|
|
318
|
+
if !safepath.WithinResolved(path, skillsDir) {
|
|
319
|
+
return nil, fmt.Errorf("unsafe skill path for %s", skill)
|
|
320
|
+
}
|
|
321
|
+
roles, err := requiredAgentRolesFromSkill(path)
|
|
322
|
+
if os.IsNotExist(err) {
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
if err != nil {
|
|
326
|
+
return nil, fmt.Errorf("%s: %w", skill, err)
|
|
327
|
+
}
|
|
328
|
+
for _, role := range roles {
|
|
329
|
+
seen[role] = struct{}{}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
roles := make([]string, 0, len(seen))
|
|
333
|
+
for role := range seen {
|
|
334
|
+
roles = append(roles, role)
|
|
335
|
+
}
|
|
336
|
+
sort.Strings(roles)
|
|
337
|
+
return roles, nil
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
func expectedAgentType(role string) string {
|
|
341
|
+
if role == "devrites-slice-wright" {
|
|
342
|
+
return "worker"
|
|
343
|
+
}
|
|
344
|
+
return "explorer"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
func agentRoleDeveloperInstructions(root, role string) (string, error) {
|
|
348
|
+
agentsDir := filepath.Join(filepath.Dir(root), ".codex", "agents")
|
|
349
|
+
path := filepath.Join(agentsDir, role+".toml")
|
|
350
|
+
if !safepath.WithinResolved(path, agentsDir) {
|
|
351
|
+
return "", fmt.Errorf("unsafe role path")
|
|
352
|
+
}
|
|
353
|
+
info, err := os.Stat(path)
|
|
354
|
+
if err != nil || !info.Mode().IsRegular() {
|
|
355
|
+
return "", fmt.Errorf("role contract is not a regular file")
|
|
356
|
+
}
|
|
357
|
+
if info.Size() > 256<<10 {
|
|
358
|
+
return "", fmt.Errorf("role contract exceeds 256 KiB")
|
|
359
|
+
}
|
|
360
|
+
data, err := os.ReadFile(path)
|
|
361
|
+
if err != nil {
|
|
362
|
+
return "", err
|
|
363
|
+
}
|
|
364
|
+
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
|
|
365
|
+
wantName := `name = "` + role + `"`
|
|
366
|
+
nameMatches := false
|
|
367
|
+
start := -1
|
|
368
|
+
for i, line := range lines {
|
|
369
|
+
switch strings.TrimSpace(line) {
|
|
370
|
+
case wantName:
|
|
371
|
+
nameMatches = true
|
|
372
|
+
case "developer_instructions = '''":
|
|
373
|
+
start = i + 1
|
|
374
|
+
}
|
|
375
|
+
if start >= 0 {
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if !nameMatches || start < 0 {
|
|
380
|
+
return "", fmt.Errorf("role contract name or developer_instructions is invalid")
|
|
381
|
+
}
|
|
382
|
+
for i := start; i < len(lines); i++ {
|
|
383
|
+
if strings.TrimSpace(lines[i]) != "'''" {
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
instructions := strings.TrimSpace(strings.Join(lines[start:i], "\n"))
|
|
387
|
+
if instructions == "" {
|
|
388
|
+
return "", fmt.Errorf("developer_instructions is empty")
|
|
389
|
+
}
|
|
390
|
+
return instructions, nil
|
|
391
|
+
}
|
|
392
|
+
return "", fmt.Errorf("developer_instructions is unterminated")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func currentReconcileWindowID() string {
|
|
396
|
+
_, _, dir, ok := resolveWorkspace()
|
|
397
|
+
if !ok {
|
|
398
|
+
return ""
|
|
399
|
+
}
|
|
400
|
+
names := []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"}
|
|
401
|
+
h := sha256.New()
|
|
402
|
+
for _, name := range names {
|
|
403
|
+
data, err := os.ReadFile(filepath.Join(dir, name))
|
|
404
|
+
if err != nil {
|
|
405
|
+
return ""
|
|
406
|
+
}
|
|
407
|
+
_, _ = io.WriteString(h, name)
|
|
408
|
+
_, _ = h.Write([]byte{0})
|
|
409
|
+
_, _ = h.Write(data)
|
|
410
|
+
_, _ = h.Write([]byte{0})
|
|
411
|
+
}
|
|
412
|
+
return hex.EncodeToString(h.Sum(nil))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func dispatchTurnState(events []agentDispatchEvent, turnID string) (map[string]struct{}, []*agentDispatchAttempt) {
|
|
416
|
+
armed := map[string]struct{}{}
|
|
417
|
+
attemptByTool := map[string]*agentDispatchAttempt{}
|
|
418
|
+
attemptByAgent := map[string]*agentDispatchAttempt{}
|
|
419
|
+
var attempts []*agentDispatchAttempt
|
|
420
|
+
for _, event := range events {
|
|
421
|
+
if event.TurnID != turnID {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
switch event.Event {
|
|
425
|
+
case "armed":
|
|
426
|
+
armed[event.Role] = struct{}{}
|
|
427
|
+
case "pending":
|
|
428
|
+
armed[event.Role] = struct{}{}
|
|
429
|
+
attempt := &agentDispatchAttempt{
|
|
430
|
+
Role: event.Role,
|
|
431
|
+
AgentType: event.AgentType,
|
|
432
|
+
ToolUseID: event.ToolUseID,
|
|
433
|
+
WindowID: event.WindowID,
|
|
434
|
+
}
|
|
435
|
+
attemptByTool[event.ToolUseID] = attempt
|
|
436
|
+
attempts = append(attempts, attempt)
|
|
437
|
+
case "started":
|
|
438
|
+
attempt := attemptByTool[event.ToolUseID]
|
|
439
|
+
if attempt == nil {
|
|
440
|
+
attempt = &agentDispatchAttempt{
|
|
441
|
+
Role: event.Role,
|
|
442
|
+
AgentType: event.AgentType,
|
|
443
|
+
ToolUseID: event.ToolUseID,
|
|
444
|
+
WindowID: event.WindowID,
|
|
445
|
+
}
|
|
446
|
+
attempts = append(attempts, attempt)
|
|
447
|
+
}
|
|
448
|
+
attempt.Started = true
|
|
449
|
+
attempt.AgentID = event.AgentID
|
|
450
|
+
attemptByAgent[event.AgentID] = attempt
|
|
451
|
+
case "waited":
|
|
452
|
+
if attempt := attemptByAgent[event.AgentID]; attempt != nil {
|
|
453
|
+
attempt.Waited = true
|
|
454
|
+
}
|
|
455
|
+
case "stopped":
|
|
456
|
+
if attempt := attemptByAgent[event.AgentID]; attempt != nil {
|
|
457
|
+
attempt.Stopped = true
|
|
458
|
+
attempt.ResultSHA256 = event.ResultSHA256
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return armed, attempts
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
func durableCodexV2DispatchAttempts(
|
|
466
|
+
root, sessionID, turnID string,
|
|
467
|
+
armed map[string]struct{},
|
|
468
|
+
) ([]*agentDispatchAttempt, error) {
|
|
469
|
+
parentPath, sessionsDir, err := findCodexParentRollout(sessionID)
|
|
470
|
+
if err != nil || parentPath == "" {
|
|
471
|
+
return nil, err
|
|
472
|
+
}
|
|
473
|
+
projectRoot := filepath.Dir(root)
|
|
474
|
+
spawns, waitLines, err := readCodexV2ParentRollout(parentPath, projectRoot, sessionID, turnID, armed)
|
|
475
|
+
if err != nil {
|
|
476
|
+
return nil, err
|
|
477
|
+
}
|
|
478
|
+
var attempts []*agentDispatchAttempt
|
|
479
|
+
for _, spawn := range spawns {
|
|
480
|
+
if strings.TrimSpace(spawn.Result) == "" || !lineBetween(waitLines, spawn.SpawnLine, spawn.ResultLine) {
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
483
|
+
instructions, err := agentRoleDeveloperInstructions(root, spawn.Role)
|
|
484
|
+
if err != nil {
|
|
485
|
+
return nil, fmt.Errorf("named role %s is not loadable: %w", spawn.Role, err)
|
|
486
|
+
}
|
|
487
|
+
childID, childResult, err := findCodexV2ChildResult(
|
|
488
|
+
sessionsDir, parentPath, projectRoot, sessionID, spawn.AgentPath, spawn.Role, instructions,
|
|
489
|
+
)
|
|
490
|
+
if err != nil {
|
|
491
|
+
return nil, err
|
|
492
|
+
}
|
|
493
|
+
if childID == "" || strings.TrimSpace(childResult) == "" {
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
windowID := ""
|
|
497
|
+
if spawn.Role == "devrites-slice-wright" {
|
|
498
|
+
windowID = currentReconcileWindowID()
|
|
499
|
+
if windowID == "" || !reconcileWindowPredates(spawn.SpawnedAt) {
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
sum := sha256.Sum256([]byte(childResult))
|
|
504
|
+
attempts = append(attempts, &agentDispatchAttempt{
|
|
505
|
+
Role: spawn.Role,
|
|
506
|
+
AgentType: spawn.Role,
|
|
507
|
+
AgentID: childID,
|
|
508
|
+
WindowID: windowID,
|
|
509
|
+
Started: true,
|
|
510
|
+
Stopped: true,
|
|
511
|
+
Waited: true,
|
|
512
|
+
ResultSHA256: hex.EncodeToString(sum[:]),
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
return attempts, nil
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
func findCodexParentRollout(sessionID string) (string, string, error) {
|
|
519
|
+
codeHome := strings.TrimSpace(os.Getenv("CODEX_HOME"))
|
|
520
|
+
if codeHome == "" {
|
|
521
|
+
home, err := os.UserHomeDir()
|
|
522
|
+
if err != nil {
|
|
523
|
+
return "", "", nil
|
|
524
|
+
}
|
|
525
|
+
codeHome = filepath.Join(home, ".codex")
|
|
526
|
+
}
|
|
527
|
+
sessionsDir := filepath.Join(codeHome, "sessions")
|
|
528
|
+
wantSuffix := "-" + sessionID + ".jsonl"
|
|
529
|
+
var parentPath string
|
|
530
|
+
err := filepath.WalkDir(sessionsDir, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
531
|
+
if walkErr != nil {
|
|
532
|
+
if os.IsNotExist(walkErr) {
|
|
533
|
+
return nil
|
|
534
|
+
}
|
|
535
|
+
return walkErr
|
|
536
|
+
}
|
|
537
|
+
if entry.IsDir() || !strings.HasSuffix(entry.Name(), wantSuffix) {
|
|
538
|
+
return nil
|
|
539
|
+
}
|
|
540
|
+
parentPath = path
|
|
541
|
+
return fs.SkipAll
|
|
542
|
+
})
|
|
543
|
+
if os.IsNotExist(err) {
|
|
544
|
+
return "", sessionsDir, nil
|
|
545
|
+
}
|
|
546
|
+
return parentPath, sessionsDir, err
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func readCodexV2ParentRollout(
|
|
550
|
+
path, projectRoot, sessionID, turnID string,
|
|
551
|
+
armed map[string]struct{},
|
|
552
|
+
) (map[string]*codexV2Spawn, []int, error) {
|
|
553
|
+
file, err := os.Open(path)
|
|
554
|
+
if err != nil {
|
|
555
|
+
return nil, nil, err
|
|
556
|
+
}
|
|
557
|
+
defer file.Close()
|
|
558
|
+
|
|
559
|
+
spawns := map[string]*codexV2Spawn{}
|
|
560
|
+
var waitLines []int
|
|
561
|
+
metaSeen := false
|
|
562
|
+
scanner := bufio.NewScanner(file)
|
|
563
|
+
scanner.Buffer(make([]byte, 64<<10), maxCodexRolloutLine)
|
|
564
|
+
for lineNumber := 1; scanner.Scan(); lineNumber++ {
|
|
565
|
+
line := scanner.Bytes()
|
|
566
|
+
if !bytes.Contains(line, []byte(`"session_meta"`)) &&
|
|
567
|
+
!bytes.Contains(line, []byte(`"spawn_agent"`)) &&
|
|
568
|
+
!bytes.Contains(line, []byte(`"wait_agent"`)) &&
|
|
569
|
+
!bytes.Contains(line, []byte(`"agent_message"`)) {
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
var record codexRolloutRecord
|
|
573
|
+
if err := json.Unmarshal(line, &record); err != nil {
|
|
574
|
+
return nil, nil, fmt.Errorf("invalid Codex parent rollout: %w", err)
|
|
575
|
+
}
|
|
576
|
+
if record.Type == "session_meta" {
|
|
577
|
+
if record.Payload.ID != sessionID || !sameResolvedPath(record.Payload.CWD, projectRoot) {
|
|
578
|
+
return nil, nil, fmt.Errorf("codex parent rollout does not match the current session root")
|
|
579
|
+
}
|
|
580
|
+
metaSeen = true
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
if record.Payload.Metadata.TurnID != turnID {
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
switch {
|
|
587
|
+
case record.Payload.Type == "function_call" && record.Payload.Name == "spawn_agent":
|
|
588
|
+
var args struct {
|
|
589
|
+
AgentType string `json:"agent_type"`
|
|
590
|
+
TaskName string `json:"task_name"`
|
|
591
|
+
ForkTurns string `json:"fork_turns"`
|
|
592
|
+
}
|
|
593
|
+
if json.Unmarshal([]byte(record.Payload.Arguments), &args) != nil {
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
if _, required := armed[args.AgentType]; !required ||
|
|
597
|
+
args.TaskName == "" || strings.Contains(args.TaskName, "/") ||
|
|
598
|
+
args.ForkTurns != "none" {
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
spawnedAt, err := time.Parse(time.RFC3339Nano, record.Timestamp)
|
|
602
|
+
if err != nil {
|
|
603
|
+
return nil, nil, fmt.Errorf("codex spawn timestamp is invalid: %w", err)
|
|
604
|
+
}
|
|
605
|
+
spawns["/root/"+args.TaskName] = &codexV2Spawn{
|
|
606
|
+
Role: args.AgentType,
|
|
607
|
+
AgentPath: "/root/" + args.TaskName,
|
|
608
|
+
SpawnLine: lineNumber,
|
|
609
|
+
SpawnedAt: spawnedAt,
|
|
610
|
+
}
|
|
611
|
+
case record.Payload.Type == "function_call" && record.Payload.Name == "wait_agent":
|
|
612
|
+
waitLines = append(waitLines, lineNumber)
|
|
613
|
+
case record.Payload.Type == "agent_message" &&
|
|
614
|
+
record.Payload.Recipient == "/root":
|
|
615
|
+
spawn := spawns[record.Payload.Author]
|
|
616
|
+
if spawn == nil {
|
|
617
|
+
continue
|
|
618
|
+
}
|
|
619
|
+
spawn.Result = rolloutInputText(record.Payload.Content)
|
|
620
|
+
spawn.ResultLine = lineNumber
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if err := scanner.Err(); err != nil {
|
|
624
|
+
return nil, nil, fmt.Errorf("read Codex parent rollout: %w", err)
|
|
625
|
+
}
|
|
626
|
+
if !metaSeen {
|
|
627
|
+
return nil, nil, fmt.Errorf("codex parent rollout is missing session metadata")
|
|
628
|
+
}
|
|
629
|
+
return spawns, waitLines, nil
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
func findCodexV2ChildResult(
|
|
633
|
+
sessionsDir, parentPath, projectRoot, sessionID, agentPath, role, instructions string,
|
|
634
|
+
) (string, string, error) {
|
|
635
|
+
var childID, result string
|
|
636
|
+
err := filepath.WalkDir(sessionsDir, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
637
|
+
if walkErr != nil {
|
|
638
|
+
if os.IsNotExist(walkErr) {
|
|
639
|
+
return nil
|
|
640
|
+
}
|
|
641
|
+
return walkErr
|
|
642
|
+
}
|
|
643
|
+
if entry.IsDir() || path == parentPath || filepath.Ext(path) != ".jsonl" {
|
|
644
|
+
return nil
|
|
645
|
+
}
|
|
646
|
+
record, ok, err := firstCodexRolloutRecord(path)
|
|
647
|
+
if err != nil || !ok {
|
|
648
|
+
return err
|
|
649
|
+
}
|
|
650
|
+
if record.Type != "session_meta" ||
|
|
651
|
+
record.Payload.ParentThreadID != sessionID ||
|
|
652
|
+
record.Payload.AgentPath != agentPath ||
|
|
653
|
+
record.Payload.AgentRole != role ||
|
|
654
|
+
!sameResolvedPath(record.Payload.CWD, projectRoot) {
|
|
655
|
+
return nil
|
|
656
|
+
}
|
|
657
|
+
if childID != "" {
|
|
658
|
+
return fmt.Errorf("multiple Codex child rollouts match %s", agentPath)
|
|
659
|
+
}
|
|
660
|
+
childID = record.Payload.ID
|
|
661
|
+
result, err = codexChildResult(path, instructions)
|
|
662
|
+
return err
|
|
663
|
+
})
|
|
664
|
+
return childID, result, err
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
func firstCodexRolloutRecord(path string) (codexRolloutRecord, bool, error) {
|
|
668
|
+
var record codexRolloutRecord
|
|
669
|
+
file, err := os.Open(path)
|
|
670
|
+
if err != nil {
|
|
671
|
+
return record, false, err
|
|
672
|
+
}
|
|
673
|
+
defer file.Close()
|
|
674
|
+
scanner := bufio.NewScanner(file)
|
|
675
|
+
scanner.Buffer(make([]byte, 64<<10), maxCodexRolloutLine)
|
|
676
|
+
if !scanner.Scan() {
|
|
677
|
+
return record, false, scanner.Err()
|
|
678
|
+
}
|
|
679
|
+
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
|
|
680
|
+
return record, false, nil
|
|
681
|
+
}
|
|
682
|
+
return record, true, nil
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
func codexChildResult(path, instructions string) (string, error) {
|
|
686
|
+
file, err := os.Open(path)
|
|
687
|
+
if err != nil {
|
|
688
|
+
return "", err
|
|
689
|
+
}
|
|
690
|
+
defer file.Close()
|
|
691
|
+
scanner := bufio.NewScanner(file)
|
|
692
|
+
scanner.Buffer(make([]byte, 64<<10), maxCodexRolloutLine)
|
|
693
|
+
instructionsLoaded := false
|
|
694
|
+
result := ""
|
|
695
|
+
for scanner.Scan() {
|
|
696
|
+
line := scanner.Bytes()
|
|
697
|
+
if !bytes.Contains(line, []byte(`"task_complete"`)) &&
|
|
698
|
+
!bytes.Contains(line, []byte(`"developer"`)) {
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
var record codexRolloutRecord
|
|
702
|
+
if json.Unmarshal(line, &record) != nil {
|
|
703
|
+
continue
|
|
704
|
+
}
|
|
705
|
+
if record.Payload.Type == "message" && record.Payload.Role == "developer" &&
|
|
706
|
+
strings.Contains(rolloutInputText(record.Payload.Content), instructions) {
|
|
707
|
+
instructionsLoaded = true
|
|
708
|
+
}
|
|
709
|
+
if record.Payload.Type == "task_complete" &&
|
|
710
|
+
strings.TrimSpace(record.Payload.LastAgentResult) != "" {
|
|
711
|
+
result = record.Payload.LastAgentResult
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if err := scanner.Err(); err != nil {
|
|
715
|
+
return "", err
|
|
716
|
+
}
|
|
717
|
+
if !instructionsLoaded {
|
|
718
|
+
return "", nil
|
|
719
|
+
}
|
|
720
|
+
return result, nil
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
func rolloutInputText(content []struct {
|
|
724
|
+
Type string `json:"type"`
|
|
725
|
+
Text string `json:"text"`
|
|
726
|
+
}) string {
|
|
727
|
+
var text []string
|
|
728
|
+
for _, item := range content {
|
|
729
|
+
if item.Type == "input_text" && strings.TrimSpace(item.Text) != "" {
|
|
730
|
+
text = append(text, item.Text)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return strings.Join(text, "\n")
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
func lineBetween(lines []int, after, before int) bool {
|
|
737
|
+
for _, line := range lines {
|
|
738
|
+
if line > after && line < before {
|
|
739
|
+
return true
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
func sameResolvedPath(left, right string) bool {
|
|
746
|
+
resolve := func(path string) string {
|
|
747
|
+
absolute, err := filepath.Abs(path)
|
|
748
|
+
if err == nil {
|
|
749
|
+
path = absolute
|
|
750
|
+
}
|
|
751
|
+
if resolved, err := filepath.EvalSymlinks(path); err == nil {
|
|
752
|
+
path = resolved
|
|
753
|
+
}
|
|
754
|
+
return filepath.Clean(path)
|
|
755
|
+
}
|
|
756
|
+
return resolve(left) == resolve(right)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
func reconcileWindowPredates(at time.Time) bool {
|
|
760
|
+
_, _, dir, ok := resolveWorkspace()
|
|
761
|
+
if !ok {
|
|
762
|
+
return false
|
|
763
|
+
}
|
|
764
|
+
for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
|
|
765
|
+
info, err := os.Stat(filepath.Join(dir, name))
|
|
766
|
+
if err != nil || info.ModTime().After(at) {
|
|
767
|
+
return false
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return true
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
func dispatchAttemptComplete(attempt *agentDispatchAttempt) bool {
|
|
774
|
+
return attempt.Started && attempt.Stopped && attempt.ResultSHA256 != ""
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
func roleSatisfied(role, windowID string, attempts []*agentDispatchAttempt) bool {
|
|
778
|
+
for _, attempt := range attempts {
|
|
779
|
+
if attempt.Role != role || !dispatchAttemptComplete(attempt) {
|
|
780
|
+
continue
|
|
781
|
+
}
|
|
782
|
+
if windowID == "" || attempt.WindowID == windowID {
|
|
783
|
+
return true
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return false
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispatchAttempt) string {
|
|
790
|
+
roles := make([]string, 0, len(armed))
|
|
791
|
+
for role := range armed {
|
|
792
|
+
if !roleSatisfied(role, "", attempts) {
|
|
793
|
+
roles = append(roles, role)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if len(roles) == 0 {
|
|
797
|
+
return ""
|
|
798
|
+
}
|
|
799
|
+
sort.Strings(roles)
|
|
800
|
+
role := roles[0]
|
|
801
|
+
if role == agentDispatchMetadataRole {
|
|
802
|
+
return "DevRites could not determine this skill's required agents because its installed required-agent-roles contract is invalid. Reinstall or repair the DevRites skill pack before continuing."
|
|
803
|
+
}
|
|
804
|
+
return fmt.Sprintf(
|
|
805
|
+
"DevRites dispatch for %s is not complete. On MultiAgent V2 call spawn_agent with agent_type=%s, a unique task_name, and fork_turns=\"none\" so Codex loads .codex/agents/%s.toml natively. On V1 use agent_type=%s with fork_turns=\"none\" and name that role TOML in the message. Wait for the returned child and use its non-empty result. Do not call wait before spawn_agent and do not synthesize the agent result.",
|
|
806
|
+
role, role, role, expectedAgentType(role),
|
|
807
|
+
)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
func preToolDeny(h harness.Harness, reason string, stdout, stderr io.Writer) int {
|
|
811
|
+
out, err := h.PreToolDeny(reason)
|
|
812
|
+
if err != nil {
|
|
813
|
+
debugf(stderr, "agent-dispatch: %v", err)
|
|
814
|
+
return exitOK
|
|
815
|
+
}
|
|
816
|
+
fmt.Fprintln(stdout, out)
|
|
817
|
+
return exitOK
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
func preToolRewrite(h harness.Harness, toolInput json.RawMessage, message string, stdout, stderr io.Writer) int {
|
|
821
|
+
var updated map[string]any
|
|
822
|
+
if err := json.Unmarshal(toolInput, &updated); err != nil {
|
|
823
|
+
return preToolDeny(h, "DevRites could not bind developer_instructions to the spawn input.", stdout, stderr)
|
|
824
|
+
}
|
|
825
|
+
updated["message"] = message
|
|
826
|
+
out, err := json.Marshal(map[string]any{
|
|
827
|
+
"hookSpecificOutput": map[string]any{
|
|
828
|
+
"hookEventName": "PreToolUse",
|
|
829
|
+
"permissionDecision": "allow",
|
|
830
|
+
"updatedInput": updated,
|
|
831
|
+
},
|
|
832
|
+
})
|
|
833
|
+
if err != nil {
|
|
834
|
+
return preToolDeny(h, "DevRites could not encode the bound spawn input.", stdout, stderr)
|
|
835
|
+
}
|
|
836
|
+
fmt.Fprintln(stdout, string(out))
|
|
837
|
+
return exitOK
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
func stopDispatchBlock(h harness.Harness, reason string, stdout, stderr io.Writer) int {
|
|
841
|
+
out, err := h.StopBlock(reason)
|
|
842
|
+
if err != nil {
|
|
843
|
+
debugf(stderr, "agent-dispatch: %v", err)
|
|
844
|
+
return exitOK
|
|
845
|
+
}
|
|
846
|
+
fmt.Fprintln(stdout, out)
|
|
847
|
+
return exitOK
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func stopDispatchFailure(reason string, stdout, stderr io.Writer) int {
|
|
851
|
+
out, err := json.Marshal(map[string]any{
|
|
852
|
+
"continue": false,
|
|
853
|
+
"stopReason": reason,
|
|
854
|
+
"systemMessage": reason,
|
|
855
|
+
})
|
|
856
|
+
if err != nil {
|
|
857
|
+
debugf(stderr, "agent-dispatch: %v", err)
|
|
858
|
+
return exitOK
|
|
859
|
+
}
|
|
860
|
+
fmt.Fprintln(stdout, string(out))
|
|
861
|
+
return exitOK
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
865
|
+
if h != harness.Codex {
|
|
866
|
+
_, _ = io.Copy(io.Discard, stdin)
|
|
867
|
+
return exitOK
|
|
868
|
+
}
|
|
869
|
+
in, err := parseAgentDispatchHookInput(stdin)
|
|
870
|
+
if err != nil {
|
|
871
|
+
return exitOK
|
|
872
|
+
}
|
|
873
|
+
root, ok := agentDispatchRoot()
|
|
874
|
+
if !ok || in.SessionID == "" || in.TurnID == "" {
|
|
875
|
+
return exitOK
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
switch in.HookEventName {
|
|
879
|
+
case "UserPromptSubmit":
|
|
880
|
+
roles, rolesErr := requiredRolesInPrompt(root, in.Prompt)
|
|
881
|
+
if rolesErr != nil {
|
|
882
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
883
|
+
Event: "armed",
|
|
884
|
+
TurnID: in.TurnID,
|
|
885
|
+
Role: agentDispatchMetadataRole,
|
|
886
|
+
}); err != nil {
|
|
887
|
+
return stopDispatchBlock(h, "DevRites could not record the invalid agent-role contract: "+err.Error(), stdout, stderr)
|
|
888
|
+
}
|
|
889
|
+
return stopDispatchBlock(h, "DevRites could not read the required agent roles: "+rolesErr.Error(), stdout, stderr)
|
|
890
|
+
}
|
|
891
|
+
for _, role := range roles {
|
|
892
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
893
|
+
Event: "armed",
|
|
894
|
+
TurnID: in.TurnID,
|
|
895
|
+
Role: role,
|
|
896
|
+
}); err != nil {
|
|
897
|
+
return stopDispatchBlock(h, "DevRites could not arm the required agent dispatch: "+err.Error(), stdout, stderr)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if len(roles) > 0 {
|
|
901
|
+
fmt.Fprintf(stdout, "DevRites: this turn requires a confirmed %s subagent start, wait, and non-empty result; a success phrase is not evidence.", strings.Join(roles, ", "))
|
|
902
|
+
}
|
|
903
|
+
return exitOK
|
|
904
|
+
|
|
905
|
+
case "PreToolUse":
|
|
906
|
+
return hookAgentDispatchPreTool(h, root, in, stdout, stderr)
|
|
907
|
+
|
|
908
|
+
case "SubagentStop":
|
|
909
|
+
if in.AgentID == "" {
|
|
910
|
+
return exitOK
|
|
911
|
+
}
|
|
912
|
+
attempt, found, lookupErr := boundDispatchAttempt(root, in.SessionID, in.AgentID)
|
|
913
|
+
if lookupErr != nil {
|
|
914
|
+
return stopDispatchBlock(h, "DevRites could not identify the stopped subagent: "+lookupErr.Error(), stdout, stderr)
|
|
915
|
+
}
|
|
916
|
+
if !found {
|
|
917
|
+
return exitOK
|
|
918
|
+
}
|
|
919
|
+
resultSum := ""
|
|
920
|
+
if strings.TrimSpace(in.LastAssistantMessage) != "" {
|
|
921
|
+
sum := sha256.Sum256([]byte(in.LastAssistantMessage))
|
|
922
|
+
resultSum = hex.EncodeToString(sum[:])
|
|
923
|
+
}
|
|
924
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
925
|
+
Event: "stopped",
|
|
926
|
+
TurnID: attempt.TurnID,
|
|
927
|
+
AgentID: in.AgentID,
|
|
928
|
+
AgentType: attempt.AgentType,
|
|
929
|
+
ResultSHA256: resultSum,
|
|
930
|
+
}); err != nil {
|
|
931
|
+
return stopDispatchBlock(h, "DevRites could not record the subagent result: "+err.Error(), stdout, stderr)
|
|
932
|
+
}
|
|
933
|
+
return exitOK
|
|
934
|
+
|
|
935
|
+
case "Stop":
|
|
936
|
+
events, err := readAgentDispatchEvents(root, in.SessionID)
|
|
937
|
+
if err != nil {
|
|
938
|
+
return stopDispatchBlock(h, "DevRites could not verify the required agent dispatch: "+err.Error(), stdout, stderr)
|
|
939
|
+
}
|
|
940
|
+
armed, attempts := dispatchTurnState(events, in.TurnID)
|
|
941
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" {
|
|
942
|
+
durable, durableErr := durableCodexV2DispatchAttempts(
|
|
943
|
+
root, in.SessionID, in.TurnID, armed,
|
|
944
|
+
)
|
|
945
|
+
if durableErr != nil {
|
|
946
|
+
return stopDispatchBlock(h, "DevRites could not verify the Codex V2 agent dispatch: "+durableErr.Error(), stdout, stderr)
|
|
947
|
+
}
|
|
948
|
+
attempts = append(attempts, durable...)
|
|
949
|
+
}
|
|
950
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" {
|
|
951
|
+
if in.StopHookActive {
|
|
952
|
+
return stopDispatchFailure(reason, stdout, stderr)
|
|
953
|
+
}
|
|
954
|
+
return stopDispatchBlock(h, reason, stdout, stderr)
|
|
955
|
+
}
|
|
956
|
+
return exitOK
|
|
957
|
+
}
|
|
958
|
+
return exitOK
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHookInput, stdout, stderr io.Writer) int {
|
|
962
|
+
events, err := readAgentDispatchEvents(root, in.SessionID)
|
|
963
|
+
if err != nil {
|
|
964
|
+
return preToolDeny(h, "DevRites could not verify agent dispatch state: "+err.Error(), stdout, stderr)
|
|
965
|
+
}
|
|
966
|
+
armed, attempts := dispatchTurnState(events, in.TurnID)
|
|
967
|
+
|
|
968
|
+
if isAgentDispatchTool(in.ToolName) {
|
|
969
|
+
agentType := in.ToolInput.AgentType
|
|
970
|
+
if agentType == "" {
|
|
971
|
+
if len(armed) == 0 {
|
|
972
|
+
return exitOK
|
|
973
|
+
}
|
|
974
|
+
return preToolDeny(h, "DevRites MultiAgent V2 dispatch requires the exact named agent_type=devrites-<role>; omitting agent_type does not load the specialist role contract.", stdout, stderr)
|
|
975
|
+
}
|
|
976
|
+
role := ""
|
|
977
|
+
if strings.HasPrefix(agentType, "devrites-") {
|
|
978
|
+
role = agentType
|
|
979
|
+
} else {
|
|
980
|
+
roles := explicitRolesInText(in.ToolInput.Message)
|
|
981
|
+
if len(roles) == 1 {
|
|
982
|
+
role = roles[0]
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if role == "" {
|
|
986
|
+
if agentType == "explorer" || agentType == "worker" {
|
|
987
|
+
if len(armed) == 0 {
|
|
988
|
+
return exitOK // Unrelated generic subagent in a DevRites repository.
|
|
989
|
+
}
|
|
990
|
+
return preToolDeny(h, "DevRites generic dispatch must name exactly one .codex/agents/devrites-<role>.toml contract.", stdout, stderr)
|
|
991
|
+
}
|
|
992
|
+
return exitOK
|
|
993
|
+
}
|
|
994
|
+
instructions, contractErr := agentRoleDeveloperInstructions(root, role)
|
|
995
|
+
if contractErr != nil {
|
|
996
|
+
return preToolDeny(h, "DevRites role contract .codex/agents/"+role+".toml cannot be loaded: "+contractErr.Error()+".", stdout, stderr)
|
|
997
|
+
}
|
|
998
|
+
if in.ToolInput.ForkTurns != "none" {
|
|
999
|
+
return preToolDeny(h, "DevRites agents require fork_turns=\"none\" so the child starts in the role contract rather than inheriting the root type.", stdout, stderr)
|
|
1000
|
+
}
|
|
1001
|
+
if agentType == "worker" && role != "devrites-slice-wright" {
|
|
1002
|
+
return preToolDeny(h, "Generic worker is reserved for devrites-slice-wright; use generic explorer for read-only DevRites roles.", stdout, stderr)
|
|
1003
|
+
}
|
|
1004
|
+
if agentType == "explorer" && role == "devrites-slice-wright" {
|
|
1005
|
+
return preToolDeny(h, "devrites-slice-wright requires the generic worker identity and exact wright allowlist.", stdout, stderr)
|
|
1006
|
+
}
|
|
1007
|
+
if agentType != "" && agentType != role && agentType != expectedAgentType(role) {
|
|
1008
|
+
return preToolDeny(h, fmt.Sprintf("DevRites role %s requires agent_type=%s or its exposed named role.", role, expectedAgentType(role)), stdout, stderr)
|
|
1009
|
+
}
|
|
1010
|
+
if in.ToolUseID == "" {
|
|
1011
|
+
return preToolDeny(h, "DevRites cannot bind a spawn without tool_use_id.", stdout, stderr)
|
|
1012
|
+
}
|
|
1013
|
+
windowID := currentReconcileWindowID()
|
|
1014
|
+
if role == "devrites-slice-wright" && windowID == "" {
|
|
1015
|
+
return preToolDeny(h, "devrites-slice-wright requires an active reconcile snapshot before spawn_agent.", stdout, stderr)
|
|
1016
|
+
}
|
|
1017
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
1018
|
+
Event: "pending",
|
|
1019
|
+
TurnID: in.TurnID,
|
|
1020
|
+
ToolUseID: in.ToolUseID,
|
|
1021
|
+
AgentType: agentType,
|
|
1022
|
+
Role: role,
|
|
1023
|
+
WindowID: windowID,
|
|
1024
|
+
}); err != nil {
|
|
1025
|
+
return preToolDeny(h, "DevRites could not record the spawn attempt: "+err.Error(), stdout, stderr)
|
|
1026
|
+
}
|
|
1027
|
+
if agentType != role {
|
|
1028
|
+
message := fmt.Sprintf(
|
|
1029
|
+
"DevRites bound developer_instructions for `%s`:\n\n%s\n\n## Unchanged parent packet\n%s",
|
|
1030
|
+
role, instructions, in.ToolInput.Message,
|
|
1031
|
+
)
|
|
1032
|
+
return preToolRewrite(h, in.ToolInputRaw, message, stdout, stderr)
|
|
1033
|
+
}
|
|
1034
|
+
return exitOK
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
switch toolBaseName(in.ToolName) {
|
|
1038
|
+
case "wait", "wait_agent":
|
|
1039
|
+
if len(armed) == 0 {
|
|
1040
|
+
return exitOK
|
|
1041
|
+
}
|
|
1042
|
+
var started []*agentDispatchAttempt
|
|
1043
|
+
for _, attempt := range attempts {
|
|
1044
|
+
if attempt.Started {
|
|
1045
|
+
started = append(started, attempt)
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if len(started) == 0 {
|
|
1049
|
+
return preToolDeny(h, incompleteDispatchReason(armed, attempts), stdout, stderr)
|
|
1050
|
+
}
|
|
1051
|
+
targets := in.ToolInput.ReceiverThreadIDs
|
|
1052
|
+
if len(targets) == 0 {
|
|
1053
|
+
targets = in.ToolInput.IDs
|
|
1054
|
+
}
|
|
1055
|
+
if len(targets) == 0 {
|
|
1056
|
+
targets = in.ToolInput.AgentIDs
|
|
1057
|
+
}
|
|
1058
|
+
if len(targets) > 0 {
|
|
1059
|
+
known := map[string]*agentDispatchAttempt{}
|
|
1060
|
+
for _, attempt := range started {
|
|
1061
|
+
known[attempt.AgentID] = attempt
|
|
1062
|
+
}
|
|
1063
|
+
for _, agentID := range targets {
|
|
1064
|
+
attempt := known[agentID]
|
|
1065
|
+
if attempt == nil {
|
|
1066
|
+
return preToolDeny(h, "DevRites wait target is not a confirmed child from this turn.", stdout, stderr)
|
|
1067
|
+
}
|
|
1068
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
1069
|
+
Event: "waited",
|
|
1070
|
+
TurnID: in.TurnID,
|
|
1071
|
+
AgentID: agentID,
|
|
1072
|
+
}); err != nil {
|
|
1073
|
+
return preToolDeny(h, "DevRites could not record the child wait: "+err.Error(), stdout, stderr)
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return exitOK
|
|
1077
|
+
}
|
|
1078
|
+
for _, attempt := range started {
|
|
1079
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
1080
|
+
Event: "waited",
|
|
1081
|
+
TurnID: in.TurnID,
|
|
1082
|
+
AgentID: attempt.AgentID,
|
|
1083
|
+
}); err != nil {
|
|
1084
|
+
return preToolDeny(h, "DevRites could not record the child wait: "+err.Error(), stdout, stderr)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return exitOK
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if isShellTool(in.ToolName) && reconcileTerminalRe.MatchString(in.ToolInput.Command) {
|
|
1091
|
+
windowID := currentReconcileWindowID()
|
|
1092
|
+
if windowID == "" {
|
|
1093
|
+
return exitOK
|
|
1094
|
+
}
|
|
1095
|
+
durable, durableErr := durableCodexV2DispatchAttempts(
|
|
1096
|
+
root, in.SessionID, in.TurnID, armed,
|
|
1097
|
+
)
|
|
1098
|
+
if durableErr != nil {
|
|
1099
|
+
return preToolDeny(h, "DevRites could not verify the Codex V2 wright result: "+durableErr.Error(), stdout, stderr)
|
|
1100
|
+
}
|
|
1101
|
+
attempts = append(attempts, durable...)
|
|
1102
|
+
if !roleSatisfied("devrites-slice-wright", windowID, attempts) {
|
|
1103
|
+
return preToolDeny(h, "DevRites reconcile check/close requires a confirmed, awaited devrites-slice-wright result bound to the active reconcile snapshot.", stdout, stderr)
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return exitOK
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
func bindAgentDispatchStart(root string, in agentDispatchHookInput) (string, bool, error) {
|
|
1110
|
+
events, err := readAgentDispatchEvents(root, in.SessionID)
|
|
1111
|
+
if err != nil {
|
|
1112
|
+
return "", false, err
|
|
1113
|
+
}
|
|
1114
|
+
started := map[string]struct{}{}
|
|
1115
|
+
for _, event := range events {
|
|
1116
|
+
if event.Event == "started" {
|
|
1117
|
+
started[event.ToolUseID] = struct{}{}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
for i := len(events) - 1; i >= 0; i-- {
|
|
1121
|
+
event := events[i]
|
|
1122
|
+
if event.Event != "pending" || event.AgentType != in.AgentType {
|
|
1123
|
+
continue
|
|
1124
|
+
}
|
|
1125
|
+
if _, ok := started[event.ToolUseID]; ok {
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
1129
|
+
Event: "started",
|
|
1130
|
+
TurnID: event.TurnID,
|
|
1131
|
+
ToolUseID: event.ToolUseID,
|
|
1132
|
+
AgentID: in.AgentID,
|
|
1133
|
+
AgentType: in.AgentType,
|
|
1134
|
+
Role: event.Role,
|
|
1135
|
+
WindowID: event.WindowID,
|
|
1136
|
+
}); err != nil {
|
|
1137
|
+
return "", false, err
|
|
1138
|
+
}
|
|
1139
|
+
return event.Role, true, nil
|
|
1140
|
+
}
|
|
1141
|
+
return "", false, nil
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
type boundAgentDispatchAttempt struct {
|
|
1145
|
+
TurnID string
|
|
1146
|
+
AgentType string
|
|
1147
|
+
Role string
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
func boundDispatchAttempt(root, sessionID, agentID string) (boundAgentDispatchAttempt, bool, error) {
|
|
1151
|
+
events, err := readAgentDispatchEvents(root, sessionID)
|
|
1152
|
+
if err != nil {
|
|
1153
|
+
return boundAgentDispatchAttempt{}, false, err
|
|
1154
|
+
}
|
|
1155
|
+
for i := len(events) - 1; i >= 0; i-- {
|
|
1156
|
+
event := events[i]
|
|
1157
|
+
if event.Event == "started" && event.AgentID == agentID {
|
|
1158
|
+
return boundAgentDispatchAttempt{
|
|
1159
|
+
TurnID: event.TurnID,
|
|
1160
|
+
AgentType: event.AgentType,
|
|
1161
|
+
Role: event.Role,
|
|
1162
|
+
}, true, nil
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return boundAgentDispatchAttempt{}, false, nil
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
func devritesAgentForGuard(h harness.Harness, in harness.GuardInput) devritesAgentKind {
|
|
1169
|
+
kind := devritesAgent(in.AgentType)
|
|
1170
|
+
if h != harness.Codex || kind != devritesAgentNone ||
|
|
1171
|
+
os.Getenv("DEVRITES_CODEX_GENERIC_AGENT_COMPAT") != "1" ||
|
|
1172
|
+
in.SessionID == "" || in.AgentID == "" {
|
|
1173
|
+
return kind
|
|
1174
|
+
}
|
|
1175
|
+
root, ok := agentDispatchRoot()
|
|
1176
|
+
if !ok {
|
|
1177
|
+
return devritesAgentInvalid
|
|
1178
|
+
}
|
|
1179
|
+
attempt, found, err := boundDispatchAttempt(root, in.SessionID, in.AgentID)
|
|
1180
|
+
if err != nil {
|
|
1181
|
+
return devritesAgentInvalid
|
|
1182
|
+
}
|
|
1183
|
+
if !found {
|
|
1184
|
+
return devritesAgentNone
|
|
1185
|
+
}
|
|
1186
|
+
if attempt.Role == "devrites-slice-wright" {
|
|
1187
|
+
return devritesAgentGenericWright
|
|
1188
|
+
}
|
|
1189
|
+
if strings.HasPrefix(attempt.Role, "devrites-") {
|
|
1190
|
+
return devritesAgentReadonly
|
|
1191
|
+
}
|
|
1192
|
+
return devritesAgentInvalid
|
|
1193
|
+
}
|