zob-harness 0.9.0 → 0.9.2
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/.pi/capabilities/zob-public-runtime-capabilities.json +16 -17
- package/.pi/extensions/zob-harness/index.ts +1 -1
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/dry-run.ts +1107 -0
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/report-writers.ts +325 -0
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/smoke-run.ts +1286 -0
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/types.ts +30 -0
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime/validation.ts +184 -0
- package/.pi/extensions/zob-harness/src/domains/autonomy/autonomous-runtime.ts +4 -2912
- package/.pi/extensions/zob-harness/src/domains/compute/compute-profile.ts +2 -1
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +24 -3
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts +1 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +5 -3
- package/.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts +28 -3
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/constants.ts +19 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/formatting.ts +148 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/normalize.ts +476 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/operations.ts +393 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/parsing.ts +277 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos/reducer.ts +110 -0
- package/.pi/extensions/zob-harness/src/domains/goal/goal-todos.ts +6 -1429
- package/.pi/extensions/zob-harness/src/domains/governance/governed-requests.ts +3 -1
- package/.pi/extensions/zob-harness/src/domains/governance/merge-queue.ts +3 -1
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox/helpers.ts +124 -0
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox/runners.ts +444 -0
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox/simulation.ts +569 -0
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox/types.ts +127 -0
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox/validation.ts +273 -0
- package/.pi/extensions/zob-harness/src/domains/governance/sandbox.ts +4 -1508
- package/.pi/extensions/zob-harness/src/domains/governance/worker-pool.ts +3 -1
- package/.pi/extensions/zob-harness/src/domains/governance/workspace-claims.ts +3 -1
- package/.pi/extensions/zob-harness/src/domains/orchestration/room.ts +8 -2
- package/.pi/extensions/zob-harness/src/domains/promotion/coms.ts +8 -1
- package/.pi/extensions/zob-harness/src/runtime/commands/autonomy.ts +188 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/compute.ts +165 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/daemon.ts +191 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/delegates.ts +47 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/goal.ts +70 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/intent.ts +383 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/misc.ts +229 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/project-dna.ts +130 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/types.ts +3 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/zcommit.ts +145 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +1606 -0
- package/.pi/extensions/zob-harness/src/runtime/commands/zmode.ts +42 -0
- package/.pi/extensions/zob-harness/src/runtime/commands.ts +26 -3109
- package/.pi/extensions/zob-harness/src/runtime/events.ts +67 -33
- package/.pi/extensions/zob-harness/src/runtime/goal-runtime/commands.ts +194 -0
- package/.pi/extensions/zob-harness/src/runtime/goal-runtime/events.ts +81 -0
- package/.pi/extensions/zob-harness/src/runtime/goal-runtime/state.ts +662 -0
- package/.pi/extensions/zob-harness/src/runtime/goal-runtime/tools.ts +1005 -0
- package/.pi/extensions/zob-harness/src/runtime/goal-runtime.ts +5 -1949
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/helpers.ts +786 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/register.ts +1120 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/types.ts +77 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation.ts +1 -1904
- package/.pi/extensions/zob-harness/src/runtime/zob-intro.ts +46 -15
- package/.pi/factories/project-dna/batch-manifest.json +1 -1
- package/.pi/factories/project-dna/pilot-manifest.json +1 -1
- package/.pi/factories/project-dna/smoke-manifest.json +1 -1
- package/README.md +29 -8
- package/package.json +14 -5
- package/scripts/git-ops/commit-policy-smoke.mjs +33 -6
- package/scripts/goal-todo/child-goal-ref-smoke.mjs +30 -3
- package/scripts/goal-todo/handoff-static-smoke.mjs +31 -3
- package/scripts/harness-intake/lib/cli-io.mjs +89 -0
- package/scripts/harness-intake/lib/constants.mjs +59 -0
- package/scripts/harness-intake/lib/infer-spec.mjs +127 -0
- package/scripts/harness-intake/lib/profiles.mjs +458 -0
- package/scripts/harness-intake/lib/run-init.mjs +307 -0
- package/scripts/harness-intake/lib/scan.mjs +266 -0
- package/scripts/harness-intake/lib/tmux.mjs +92 -0
- package/scripts/harness-intake/lib/validate.mjs +152 -0
- package/scripts/harness-intake/lib.mjs +8 -1521
- package/scripts/harness-switch/static-smoke.mjs +1 -1
- package/scripts/model-catalog/validate-economy.mjs +3 -1
- package/scripts/model-catalog/validate.mjs +3 -1
- package/scripts/project-dna/scan/scan.mjs +5 -2
- package/scripts/project-dna/scan/validate-scan-artifacts.mjs +3 -1
- package/scripts/project-dna/validation/validate-ontology.mjs +3 -1
- package/scripts/project-dna/validation/validate-scaffold.mjs +2 -2
- package/scripts/zagent-static-smoke.mjs +30 -2
- package/scripts/zpeer-local-e2e-smoke.mjs +18 -0
- package/scripts/zpeer-static-smoke.mjs +40 -5
- package/scripts/zteam-hot-add/smoke.mjs +30 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ensureDir, repoRel, resolveRunDir, writeJson, writeText } from "./cli-io.mjs";
|
|
3
|
+
import { FACTORY_NAME, SCHEMA_PREFIX } from "./constants.mjs";
|
|
4
|
+
|
|
5
|
+
export function initializeRun(spec) {
|
|
6
|
+
const runDir = resolveRunDir(spec.run_id);
|
|
7
|
+
const relRunDir = repoRel(runDir);
|
|
8
|
+
ensureDir(runDir);
|
|
9
|
+
for (const child of ["checkpoints", "outputs", "validation", "oracle", "generated-proposals/teams", "generated-proposals/agents", "generated-proposals/factories", "generated-proposals/kickoff", "kickoff", "tmux"]) {
|
|
10
|
+
ensureDir(join(runDir, child));
|
|
11
|
+
}
|
|
12
|
+
writeText(join(runDir, "request.md"), `# Harness Intake Request\n\n${spec.request}\n\nRequest hash: \`${spec.request_hash}\`\n`);
|
|
13
|
+
writeJson(join(runDir, "inferred-run-spec.json"), spec);
|
|
14
|
+
const manifest = {
|
|
15
|
+
schema: `${SCHEMA_PREFIX}.manifest.v1`,
|
|
16
|
+
factory: FACTORY_NAME,
|
|
17
|
+
run_id: spec.run_id,
|
|
18
|
+
mode: spec.mode,
|
|
19
|
+
run_dir: relRunDir,
|
|
20
|
+
target: spec.target,
|
|
21
|
+
harness_hint: spec.harness_hint,
|
|
22
|
+
goal: spec.goal,
|
|
23
|
+
sessions: spec.sessions,
|
|
24
|
+
output_policy: spec.output_policy,
|
|
25
|
+
expected_artifacts: [
|
|
26
|
+
"artifact-contracts.json",
|
|
27
|
+
"autonomous-status.md",
|
|
28
|
+
"sources-index.json",
|
|
29
|
+
"harness-profile.json",
|
|
30
|
+
"skills-profile.json",
|
|
31
|
+
"commands-profile.json",
|
|
32
|
+
"workflow-patterns.json",
|
|
33
|
+
"team-candidates.json",
|
|
34
|
+
"factory-candidates.json",
|
|
35
|
+
"validation.json",
|
|
36
|
+
"oracle-review.json",
|
|
37
|
+
],
|
|
38
|
+
team: {
|
|
39
|
+
id: "harness-intake-team",
|
|
40
|
+
entry_agent: "harness-intake-orchestrator",
|
|
41
|
+
agents: HARNESS_INTAKE_AGENTS.map((agent) => agent.id),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
writeJson(join(runDir, "manifest.json"), manifest);
|
|
45
|
+
writeJson(join(runDir, "agentic-plan.json"), buildAgenticPlan(spec, relRunDir));
|
|
46
|
+
writeJson(join(runDir, "artifact-contracts.json"), buildArtifactContracts(spec, relRunDir));
|
|
47
|
+
writeText(join(runDir, "autonomous-status.md"), renderAutonomousStatus(spec, relRunDir));
|
|
48
|
+
writeJson(join(runDir, "status.json"), {
|
|
49
|
+
schema: `${SCHEMA_PREFIX}.status.v1`,
|
|
50
|
+
run_id: spec.run_id,
|
|
51
|
+
status: "initialized",
|
|
52
|
+
no_ship: false,
|
|
53
|
+
phase: "init",
|
|
54
|
+
run_dir: relRunDir,
|
|
55
|
+
updated_at: new Date().toISOString(),
|
|
56
|
+
});
|
|
57
|
+
renderKickoffFiles(spec, runDir);
|
|
58
|
+
return { runDir, manifest };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildArtifactContracts(spec, relRunDir) {
|
|
62
|
+
return {
|
|
63
|
+
schema: `${SCHEMA_PREFIX}.artifact-contracts.v1`,
|
|
64
|
+
run_id: spec.run_id,
|
|
65
|
+
run_dir: relRunDir,
|
|
66
|
+
room: "harness-intake-control",
|
|
67
|
+
entry_agent: "harness-intake-orchestrator",
|
|
68
|
+
startup_file_delivery_required: true,
|
|
69
|
+
raw_prompt_transport_line_by_line: false,
|
|
70
|
+
post_start_tmux_paste_disabled: true,
|
|
71
|
+
contracts: HARNESS_INTAKE_AGENTS.map((agent) => ({
|
|
72
|
+
agent: agent.id,
|
|
73
|
+
alias: agent.alias,
|
|
74
|
+
lane: agent.lane,
|
|
75
|
+
role: agent.role,
|
|
76
|
+
outputs: agent.outputFiles,
|
|
77
|
+
done_when: agent.doneWhen,
|
|
78
|
+
requires_authorization: agent.requiresAuthorization || null,
|
|
79
|
+
})),
|
|
80
|
+
final_gate: {
|
|
81
|
+
validation_ref: "validation.json",
|
|
82
|
+
oracle_review_ref: "oracle-review.json",
|
|
83
|
+
done_sentinel_ref: "DONE.sentinel",
|
|
84
|
+
completion_requires: [
|
|
85
|
+
"validation.json status=pass",
|
|
86
|
+
"oracle-review.json verdict PASS or WARN with no no-ship blockers",
|
|
87
|
+
"generated proposals remain quarantine-only",
|
|
88
|
+
"tmux kickoff-dispatch, when present, proves startup file delivery and no line-by-line prompt paste",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderAutonomousStatus(spec, relRunDir) {
|
|
95
|
+
return `# Harness Intake Autonomous Status
|
|
96
|
+
|
|
97
|
+
Run id: ${spec.run_id}
|
|
98
|
+
Run dir: ${relRunDir}
|
|
99
|
+
Status: initialized
|
|
100
|
+
No-ship: false
|
|
101
|
+
Target: ${spec.target.input}
|
|
102
|
+
Harness hint: ${spec.harness_hint}
|
|
103
|
+
Sessions: ${spec.sessions.authorized ? "authorized" : spec.sessions.mentioned ? "mentioned but not authorized" : "not requested"}
|
|
104
|
+
|
|
105
|
+
## Lanes
|
|
106
|
+
|
|
107
|
+
${HARNESS_INTAKE_AGENTS.map((agent) => `- ${agent.id} (${agent.lane}): planned; outputs: ${agent.outputFiles.join(", ")}`).join("\n")}
|
|
108
|
+
|
|
109
|
+
## Gates
|
|
110
|
+
|
|
111
|
+
- Source project remains read-only.
|
|
112
|
+
- Sessions require explicit authorization.
|
|
113
|
+
- Generated teams/factories stay in generated-proposals/.
|
|
114
|
+
- Tmux launch is startup/visibility only, not completion.
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildAgenticPlan(spec, relRunDir) {
|
|
119
|
+
return {
|
|
120
|
+
schema: `${SCHEMA_PREFIX}.agentic-plan.v1`,
|
|
121
|
+
run_id: spec.run_id,
|
|
122
|
+
run_dir: relRunDir,
|
|
123
|
+
control_plane: "natural-language-launcher-plus-supervised-agent-team",
|
|
124
|
+
deterministic_tools: ["scan-sources", "validate-run", "tmux-launch"],
|
|
125
|
+
stages: [
|
|
126
|
+
"infer_run_spec",
|
|
127
|
+
"source_cartography",
|
|
128
|
+
"harness_interpretation",
|
|
129
|
+
"skill_command_analysis",
|
|
130
|
+
"session_mining_if_authorized",
|
|
131
|
+
"workflow_pattern_mining",
|
|
132
|
+
"team_candidate_generation",
|
|
133
|
+
"factory_candidate_generation",
|
|
134
|
+
"oracle_validation",
|
|
135
|
+
],
|
|
136
|
+
quarantine_only: true,
|
|
137
|
+
activation_enabled: false,
|
|
138
|
+
tmux_optional: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const HARNESS_INTAKE_AGENTS = [
|
|
143
|
+
{
|
|
144
|
+
id: "harness-intake-orchestrator",
|
|
145
|
+
alias: "intake_chief",
|
|
146
|
+
lane: "control",
|
|
147
|
+
role: "Coordinate the full harness intake run and consolidate evidence.",
|
|
148
|
+
outputFiles: ["run-summary.md", "oracle-review.json"],
|
|
149
|
+
doneWhen: ["All required run artifacts exist or blockers are explicit.", "Validation/oracle posture is recorded."],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "harness-source-cartographer",
|
|
153
|
+
alias: "source_cartographer",
|
|
154
|
+
lane: "source",
|
|
155
|
+
role: "Map allowed setup sources and risk posture.",
|
|
156
|
+
outputFiles: ["sources-index.json", "source-risk-report.json"],
|
|
157
|
+
doneWhen: ["Source index exists.", "Forbidden/skipped paths and source risks are recorded."],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "harness-interpreter",
|
|
161
|
+
alias: "harness_interpreter",
|
|
162
|
+
lane: "harness",
|
|
163
|
+
role: "Interpret the external harness model and conventions.",
|
|
164
|
+
outputFiles: ["harness-profile.json"],
|
|
165
|
+
doneWhen: ["Harness model, conventions, unknowns, and ZOB mappings are evidence-backed."],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "harness-skill-command-analyst",
|
|
169
|
+
alias: "skill_command_analyst",
|
|
170
|
+
lane: "skills",
|
|
171
|
+
role: "Analyze skills, commands, prompts, and hooks.",
|
|
172
|
+
outputFiles: ["skills-profile.json", "commands-profile.json", "prompt-patterns.json"],
|
|
173
|
+
doneWhen: ["Skills/commands/prompts are mapped to possible ZOB roles, workflow steps, or validators."],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "harness-session-miner",
|
|
177
|
+
alias: "session_miner",
|
|
178
|
+
lane: "sessions",
|
|
179
|
+
role: "Mine authorized sessions without persisting raw conversation bodies.",
|
|
180
|
+
outputFiles: ["sessions-analysis.json", "session-evidence-index.json"],
|
|
181
|
+
doneWhen: ["Sessions are analyzed only when authorized, or a skip reason is recorded."],
|
|
182
|
+
requiresAuthorization: "sessions.authorized=true",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "harness-workflow-pattern-miner",
|
|
186
|
+
alias: "workflow_miner",
|
|
187
|
+
lane: "patterns",
|
|
188
|
+
role: "Detect reusable workflow patterns from static and behavioral evidence.",
|
|
189
|
+
outputFiles: ["workflow-patterns.json", "workflow-patterns.md"],
|
|
190
|
+
doneWhen: ["Workflow patterns include confidence, candidate flags, and evidence refs."],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "zob-team-architect",
|
|
194
|
+
alias: "team_architect",
|
|
195
|
+
lane: "teams",
|
|
196
|
+
role: "Generate ZOB team candidates in quarantine.",
|
|
197
|
+
outputFiles: ["team-candidates.json", "generated-proposals/teams", "generated-proposals/agents", "generated-proposals/kickoff"],
|
|
198
|
+
doneWhen: ["Team candidates remain quarantine-only and include activation blockers."],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "harness-factory-designer",
|
|
202
|
+
alias: "factory_designer",
|
|
203
|
+
lane: "factories",
|
|
204
|
+
role: "Generate factory candidates and activation blockers.",
|
|
205
|
+
outputFiles: ["factory-candidates.json", "generated-proposals/factories"],
|
|
206
|
+
doneWhen: ["Factory candidates define input contract, validators, smoke posture, and activation blockers."],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "harness-intake-oracle",
|
|
210
|
+
alias: "intake_oracle",
|
|
211
|
+
lane: "oracle",
|
|
212
|
+
role: "Validate evidence, privacy, quarantine posture, and no-ship status.",
|
|
213
|
+
outputFiles: ["validation.json", "oracle-review.json", "DONE.sentinel"],
|
|
214
|
+
doneWhen: ["Validation passes or no-ship blockers are explicit.", "Tmux launch is not treated as completion."],
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
export function renderKickoffFiles(spec, runDir) {
|
|
219
|
+
for (const agent of HARNESS_INTAKE_AGENTS) {
|
|
220
|
+
const isChief = agent.id === "harness-intake-orchestrator";
|
|
221
|
+
const artifactRefs = {
|
|
222
|
+
manifest: repoRel(join(runDir, "manifest.json")),
|
|
223
|
+
runSpec: repoRel(join(runDir, "inferred-run-spec.json")),
|
|
224
|
+
artifactContracts: repoRel(join(runDir, "artifact-contracts.json")),
|
|
225
|
+
status: repoRel(join(runDir, "autonomous-status.md")),
|
|
226
|
+
generatedProposals: repoRel(join(runDir, "generated-proposals")),
|
|
227
|
+
};
|
|
228
|
+
const body = `# Harness Intake Kickoff — ${agent.id}
|
|
229
|
+
|
|
230
|
+
Run id: ${spec.run_id}
|
|
231
|
+
Target: ${spec.target.input}
|
|
232
|
+
Harness hint: ${spec.harness_hint}
|
|
233
|
+
Lane: ${agent.lane}
|
|
234
|
+
Alias: @${agent.alias}
|
|
235
|
+
Run manifest: ${artifactRefs.manifest}
|
|
236
|
+
Run spec: ${artifactRefs.runSpec}
|
|
237
|
+
Artifact contracts: ${artifactRefs.artifactContracts}
|
|
238
|
+
Status: ${artifactRefs.status}
|
|
239
|
+
Generated proposals: ${artifactRefs.generatedProposals}
|
|
240
|
+
|
|
241
|
+
This startup file is the complete task contract. It is passed as \`pi @<kickoff-file>\`; tmux must not paste this prompt line-by-line.
|
|
242
|
+
|
|
243
|
+
## READY handshake — first action
|
|
244
|
+
|
|
245
|
+
If live peer messaging is available, send a short READY/STATUS_UPDATE to @intake_chief. If not, write your status to the run status/artifact lane and continue.
|
|
246
|
+
|
|
247
|
+
\`\`\`text
|
|
248
|
+
READY
|
|
249
|
+
agent: ${agent.id}
|
|
250
|
+
run_id: ${spec.run_id}
|
|
251
|
+
lane: ${agent.lane}
|
|
252
|
+
owned_outputs:
|
|
253
|
+
${agent.outputFiles.map((file) => `- ${file}`).join("\n")}
|
|
254
|
+
blockers: none|...
|
|
255
|
+
\`\`\`
|
|
256
|
+
|
|
257
|
+
## TASK
|
|
258
|
+
|
|
259
|
+
${agent.role}
|
|
260
|
+
${isChief ? "Coordinate the full run, keep artifacts canonical, and make sure blocked lanes are visible." : "Read upstream run artifacts and produce/validate your owned slice only."}
|
|
261
|
+
|
|
262
|
+
## EXPECTED OUTCOME
|
|
263
|
+
|
|
264
|
+
Owned outputs are present, evidence-backed, and safe for downstream team/factory proposal review.
|
|
265
|
+
|
|
266
|
+
## MUST DO
|
|
267
|
+
|
|
268
|
+
- Read the run manifest, run spec, artifact contracts, and status before conclusions.
|
|
269
|
+
- Keep source project reads bounded and read-only.
|
|
270
|
+
- Cite artifact/source refs for every important claim.
|
|
271
|
+
- Keep generated content under the run directory, especially generated-proposals/.
|
|
272
|
+
- Mark blockers instead of inventing missing evidence.
|
|
273
|
+
${agent.requiresAuthorization ? "- Verify sessions.authorized=true before reading any sessions. If not authorized, record skip/blocker only." : "- Treat session artifacts as unavailable unless authorization is recorded."}
|
|
274
|
+
|
|
275
|
+
## MUST NOT
|
|
276
|
+
|
|
277
|
+
- Do not read secrets, credentials, env files, keys, SSH/AWS config, vendor folders, or build artifacts.
|
|
278
|
+
- Do not mutate the source project.
|
|
279
|
+
- Do not persist raw session bodies in generated prompts or proposals.
|
|
280
|
+
- Do not activate generated teams/factories automatically.
|
|
281
|
+
- Do not treat tmux launch as completion.
|
|
282
|
+
|
|
283
|
+
## OUTPUT FILES
|
|
284
|
+
|
|
285
|
+
${agent.outputFiles.map((file) => `- ${file}`).join("\n")}
|
|
286
|
+
|
|
287
|
+
## DONE WHEN
|
|
288
|
+
|
|
289
|
+
${agent.doneWhen.map((item) => `- ${item}`).join("\n")}
|
|
290
|
+
|
|
291
|
+
## Final claim shape
|
|
292
|
+
|
|
293
|
+
\`\`\`text
|
|
294
|
+
ARTIFACT_READY
|
|
295
|
+
agent: ${agent.id}
|
|
296
|
+
run_id: ${spec.run_id}
|
|
297
|
+
outputs:
|
|
298
|
+
- <path>
|
|
299
|
+
validation:
|
|
300
|
+
- <command or artifact check>
|
|
301
|
+
blockers: none|...
|
|
302
|
+
needs_review: yes/no
|
|
303
|
+
\`\`\`
|
|
304
|
+
`;
|
|
305
|
+
writeText(join(runDir, "kickoff", `${agent.id}-kickoff.md`), body);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, relative, sep } from "node:path";
|
|
3
|
+
import { sha256 } from "./cli-io.mjs";
|
|
4
|
+
import { DEFAULT_FORBIDDEN, MAX_FILE_BYTES, MAX_SCAN_FILES, SCHEMA_PREFIX, TEXT_EXTENSIONS } from "./constants.mjs";
|
|
5
|
+
|
|
6
|
+
export function scanSources(spec) {
|
|
7
|
+
const targetRoot = spec.target.path;
|
|
8
|
+
const files = [];
|
|
9
|
+
const skipped = [];
|
|
10
|
+
const startedAt = new Date().toISOString();
|
|
11
|
+
walkRelevant(targetRoot, spec, files, skipped);
|
|
12
|
+
const limited = files.slice(0, MAX_SCAN_FILES);
|
|
13
|
+
if (files.length > limited.length) skipped.push({ path: "<scan-limit>", reason: `max_scan_files>${MAX_SCAN_FILES}`, omitted_count: files.length - limited.length });
|
|
14
|
+
const sources = limited.map((file, index) => inspectSourceFile(targetRoot, file, index, spec));
|
|
15
|
+
const harnesses = summarizeHarnesses(sources, spec.harness_hint);
|
|
16
|
+
return {
|
|
17
|
+
schema: `${SCHEMA_PREFIX}.sources-index.v1`,
|
|
18
|
+
run_id: spec.run_id,
|
|
19
|
+
target: spec.target,
|
|
20
|
+
scanned_at: startedAt,
|
|
21
|
+
source_project_modified: false,
|
|
22
|
+
raw_secret_storage: false,
|
|
23
|
+
session_authorized: spec.sessions.authorized,
|
|
24
|
+
source_count: sources.length,
|
|
25
|
+
sources,
|
|
26
|
+
harnesses,
|
|
27
|
+
skipped,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function walkRelevant(root, spec, files, skipped) {
|
|
32
|
+
function visit(dir, depth) {
|
|
33
|
+
if (files.length >= MAX_SCAN_FILES * 2) return;
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
skipped.push({ path: safeRel(root, dir), reason: `read_error:${error.message}` });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const full = join(dir, entry.name);
|
|
43
|
+
const relPath = safeRel(root, full);
|
|
44
|
+
const forbidden = pathForbidden(relPath, DEFAULT_FORBIDDEN);
|
|
45
|
+
if (forbidden) {
|
|
46
|
+
skipped.push({ path: relPath, reason: `forbidden:${forbidden}`, directory: entry.isDirectory() });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (!isRelevantDirectory(relPath, spec) && depth > 1) {
|
|
51
|
+
skipped.push({ path: relPath, reason: "not_harness_relevant_directory", directory: true });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
visit(full, depth + 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!entry.isFile()) {
|
|
58
|
+
skipped.push({ path: relPath, reason: "not_regular_file" });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!isRelevantFile(relPath, spec)) {
|
|
62
|
+
skipped.push({ path: relPath, reason: "not_harness_relevant_file" });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const size = statSync(full).size;
|
|
66
|
+
if (size > MAX_FILE_BYTES) {
|
|
67
|
+
skipped.push({ path: relPath, reason: `too_large>${MAX_FILE_BYTES}` });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()) && !isExtensionlessRelevant(relPath)) {
|
|
71
|
+
skipped.push({ path: relPath, reason: "unsupported_extension" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
files.push({ full, relPath, size });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
visit(root, 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function safeRel(root, full) {
|
|
81
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
82
|
+
return rel || ".";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isRelevantDirectory(relPath, spec) {
|
|
86
|
+
const normalized = relPath.toLowerCase();
|
|
87
|
+
if (normalized === ".") return true;
|
|
88
|
+
if (isSessionLikePath(normalized) && !spec.sessions.authorized) return false;
|
|
89
|
+
if (normalized.startsWith(".claude") || normalized.startsWith(".codex") || normalized.startsWith(".cursor")) return true;
|
|
90
|
+
if (normalized.startsWith(".pi/agents") || normalized.startsWith(".pi/zagents") || normalized.startsWith(".pi/skills") || normalized.startsWith(".pi/prompts") || normalized.startsWith(".pi/teams") || normalized.startsWith(".pi/zteams") || normalized.startsWith(".pi/factories")) return true;
|
|
91
|
+
if (/^(docs?|prompts?|skills?|commands?|agents?|scripts?|workflows?|hooks?)(\/|$)/.test(normalized)) return true;
|
|
92
|
+
if (spec.sessions.authorized && /(^|\/)(sessions?|transcripts?|conversation-history)(\/|$)/.test(normalized)) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isRelevantFile(relPath, spec) {
|
|
97
|
+
const normalized = relPath.toLowerCase();
|
|
98
|
+
const base = basename(normalized);
|
|
99
|
+
if (isSessionLikePath(normalized) && !spec.sessions.authorized) return false;
|
|
100
|
+
if (["agents.md", "claude.md", "codex.md", "gemini.md", "readme.md", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", ".aider.conf.yml", ".aider.conf.yaml", ".aider.model.settings.yml"].includes(base)) return true;
|
|
101
|
+
if (/\.(team|factory)\.json$/.test(normalized)) return true;
|
|
102
|
+
if (/(kickoff|prompt|agent|team|factory|tmux|workflow|command|skill).*(\.template)?\.(md|json|ya?ml|toml|sh|mjs|js|ts)$/.test(normalized)) return true;
|
|
103
|
+
if (normalized.startsWith(".claude/") || normalized.startsWith(".codex/") || normalized.startsWith(".cursor/")) return true;
|
|
104
|
+
if (normalized.startsWith(".pi/agents/") || normalized.startsWith(".pi/zagents/") || normalized.startsWith(".pi/skills/") || normalized.startsWith(".pi/prompts/") || normalized.startsWith(".pi/teams/") || normalized.startsWith(".pi/zteams/") || normalized.startsWith(".pi/factories/")) return true;
|
|
105
|
+
if (/^(docs?|prompts?|skills?|commands?|agents?|scripts?|workflows?|hooks?)(\/|$)/.test(normalized)) return true;
|
|
106
|
+
if (spec.sessions.authorized && /(^|\/)(sessions?|transcripts?|conversation-history)(\/|$)/.test(normalized)) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isSessionLikePath(normalizedRelPath) {
|
|
111
|
+
return /(^|\/)(sessions?|transcripts?|conversation-history|conversation_history|chat-history|chat_history)(\/|$)/.test(normalizedRelPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isExtensionlessRelevant(relPath) {
|
|
115
|
+
return ["agents", "claude", "codex", "readme"].includes(basename(relPath).toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function pathForbidden(relPath, forbiddenPatterns) {
|
|
119
|
+
const normalized = relPath.split(sep).join("/");
|
|
120
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
121
|
+
for (const pattern of forbiddenPatterns) {
|
|
122
|
+
const clean = String(pattern).replace(/\\/g, "/").replace(/\/$/, "");
|
|
123
|
+
if (!clean) continue;
|
|
124
|
+
if (clean.includes("/")) {
|
|
125
|
+
if (normalized === clean || normalized.startsWith(`${clean}/`)) return clean;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const segment of segments) {
|
|
129
|
+
if (segmentMatches(segment, clean)) return clean;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function segmentMatches(segment, pattern) {
|
|
136
|
+
if (pattern.startsWith("*")) return segment.endsWith(pattern.slice(1));
|
|
137
|
+
if (pattern.endsWith("*")) return segment.startsWith(pattern.slice(0, -1));
|
|
138
|
+
return segment === pattern;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function inspectSourceFile(root, file, index, spec) {
|
|
142
|
+
const text = safeRead(file.full);
|
|
143
|
+
const lines = text.split(/\r?\n/u);
|
|
144
|
+
const sourceId = `S-${String(index + 1).padStart(4, "0")}`;
|
|
145
|
+
const type = classifySource(file.relPath, spec);
|
|
146
|
+
const harness = detectHarness(file.relPath, text, spec.harness_hint);
|
|
147
|
+
const signals = extractSignals(file.relPath, lines);
|
|
148
|
+
const secretLike = detectSecretLike(file.relPath, text);
|
|
149
|
+
return {
|
|
150
|
+
source_id: sourceId,
|
|
151
|
+
path: file.relPath,
|
|
152
|
+
absolute_path_hash: sha256(file.full),
|
|
153
|
+
type,
|
|
154
|
+
harness,
|
|
155
|
+
size_bytes: file.size,
|
|
156
|
+
lines: lines.length,
|
|
157
|
+
content_hash: sha256(text),
|
|
158
|
+
contains_possible_secret: secretLike.length > 0,
|
|
159
|
+
secret_like_reasons: secretLike,
|
|
160
|
+
citations: signals.slice(0, 12),
|
|
161
|
+
used_for: usageForType(type),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function safeRead(path) {
|
|
166
|
+
try {
|
|
167
|
+
return readFileSync(path, "utf8");
|
|
168
|
+
} catch {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function classifySource(relPath, spec) {
|
|
174
|
+
const lower = relPath.toLowerCase();
|
|
175
|
+
const base = basename(lower);
|
|
176
|
+
if (spec.sessions.authorized && /(^|\/)(sessions?|transcripts?|conversation-history)(\/|$)/.test(lower)) return "session";
|
|
177
|
+
if (lower.startsWith(".pi/zagents/prompts/") || lower.includes("/zagents/prompts/")) return "zagent-prompt";
|
|
178
|
+
if (lower.startsWith(".pi/zagents/") || lower.includes("/zagents/")) return "zagent";
|
|
179
|
+
if ((lower.startsWith(".pi/zteams/") || lower.includes("/zteams/")) && lower.endsWith(".tmux.sh")) return "zteam-launcher";
|
|
180
|
+
if ((lower.startsWith(".pi/zteams/") || lower.includes("/zteams/")) && /runtime\.(mjs|js|ts)$/.test(lower)) return "zteam-runtime";
|
|
181
|
+
if (lower.startsWith(".pi/zteams/") || lower.includes("/zteams/")) return "zteam";
|
|
182
|
+
if (base === "agents.md" || lower.includes("/agents/") || lower.startsWith(".pi/agents/")) return "agent-definition";
|
|
183
|
+
if (base === "claude.md" || base === "codex.md" || base === "gemini.md") return "harness-instructions";
|
|
184
|
+
if (lower.includes("/skills/") || lower.startsWith(".pi/skills/")) return "skill";
|
|
185
|
+
if (lower.includes("/commands/") || lower.includes("/hooks/")) return "command";
|
|
186
|
+
if (lower.includes("/prompts/") || lower.includes("prompt")) return "prompt";
|
|
187
|
+
if (lower.includes("/teams/") || lower.endsWith(".team.json") || lower.includes("team")) return "team";
|
|
188
|
+
if (lower.includes("/factories/") || base === "factory.json" || lower.includes("factory")) return "factory";
|
|
189
|
+
if (base === "package.json") return "package-manifest";
|
|
190
|
+
if (lower.startsWith("scripts/") || lower.includes("/scripts/")) return "script";
|
|
191
|
+
if (lower.startsWith("docs/") || lower.includes("/docs/") || base === "readme.md") return "documentation";
|
|
192
|
+
return "config";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function detectHarness(relPath, text, hint) {
|
|
196
|
+
const lower = `${relPath}\n${text.slice(0, 5000)}`.toLowerCase();
|
|
197
|
+
if (lower.includes("claude") || relPath.toLowerCase().startsWith(".claude/")) return "claude-code";
|
|
198
|
+
if (lower.includes("codex") || relPath.toLowerCase().startsWith(".codex/")) return "codex";
|
|
199
|
+
if (lower.includes("cursor") || relPath.toLowerCase().startsWith(".cursor/")) return "cursor";
|
|
200
|
+
if (lower.includes("aider")) return "aider";
|
|
201
|
+
if (lower.includes("zob") || lower.includes(" pi ") || relPath.toLowerCase().startsWith(".pi/")) return "pi-zob";
|
|
202
|
+
return hint || "unknown";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function extractSignals(relPath, lines) {
|
|
206
|
+
const patterns = [
|
|
207
|
+
["agent", /\bagents?\b|\bsub-?agents?\b|\bworker\b|\borchestrator\b/i],
|
|
208
|
+
["skill", /\bskills?\b|\bcapabilit(y|ies)\b/i],
|
|
209
|
+
["command", /\bcommands?\b|slash command|\/\w+/i],
|
|
210
|
+
["tool", /\btools?\b|read|write|edit|bash|grep|find/i],
|
|
211
|
+
["session", /\bsessions?\b|conversation|transcript|history/i],
|
|
212
|
+
["workflow", /\bworkflow\b|\bplan\b|\bimplement\b|\breview\b|\boracle\b|validate/i],
|
|
213
|
+
["factory", /\bfactory\b|factories|batch|smoke|pilot/i],
|
|
214
|
+
["safety", /secret|credential|token|forbidden|must not|never|safety|no-ship/i],
|
|
215
|
+
];
|
|
216
|
+
const citations = [];
|
|
217
|
+
lines.forEach((line, index) => {
|
|
218
|
+
for (const [kind, pattern] of patterns) {
|
|
219
|
+
if (pattern.test(line)) citations.push({ kind, ref: `${relPath}:L${index + 1}` });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return citations.slice(0, 48);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function detectSecretLike(relPath, text) {
|
|
226
|
+
const reasons = [];
|
|
227
|
+
if (pathForbidden(relPath, DEFAULT_FORBIDDEN)) reasons.push("secret_like_path");
|
|
228
|
+
const probes = [
|
|
229
|
+
[/AKIA[0-9A-Z]{16}/, "aws_access_key_like"],
|
|
230
|
+
[/-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/, "private_key_like"],
|
|
231
|
+
[/(?:api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']{12,}/i, "credential_assignment_like"],
|
|
232
|
+
];
|
|
233
|
+
for (const [pattern, reason] of probes) {
|
|
234
|
+
if (pattern.test(text)) reasons.push(reason);
|
|
235
|
+
}
|
|
236
|
+
return [...new Set(reasons)];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function usageForType(type) {
|
|
240
|
+
const map = {
|
|
241
|
+
"agent-definition": ["harness-profile", "team-candidates"],
|
|
242
|
+
zagent: ["harness-profile", "team-candidates"],
|
|
243
|
+
"zagent-prompt": ["prompt-patterns", "team-candidates"],
|
|
244
|
+
zteam: ["team-candidates", "harness-profile"],
|
|
245
|
+
"zteam-launcher": ["commands-profile", "factory-candidates"],
|
|
246
|
+
"zteam-runtime": ["commands-profile", "factory-candidates", "workflow-patterns"],
|
|
247
|
+
"harness-instructions": ["harness-profile", "workflow-patterns"],
|
|
248
|
+
skill: ["skills-profile", "team-candidates"],
|
|
249
|
+
command: ["commands-profile", "workflow-patterns"],
|
|
250
|
+
prompt: ["prompt-patterns", "team-candidates"],
|
|
251
|
+
session: ["sessions-analysis", "workflow-patterns"],
|
|
252
|
+
team: ["team-candidates"],
|
|
253
|
+
factory: ["factory-candidates"],
|
|
254
|
+
"package-manifest": ["commands-profile"],
|
|
255
|
+
script: ["commands-profile", "factory-candidates"],
|
|
256
|
+
documentation: ["harness-profile", "workflow-patterns"],
|
|
257
|
+
config: ["harness-profile"],
|
|
258
|
+
};
|
|
259
|
+
return map[type] ?? ["harness-profile"];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function summarizeHarnesses(sources, hint) {
|
|
263
|
+
const counts = new Map();
|
|
264
|
+
for (const source of sources) counts.set(source.harness, (counts.get(source.harness) ?? 0) + 1);
|
|
265
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name, count]) => ({ name, count, hint_match: name === hint }));
|
|
266
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readJson, repoRel, resolveRunDir, safeRunId, sha256, writeJson } from "./cli-io.mjs";
|
|
4
|
+
import { SCHEMA_PREFIX, repoRoot } from "./constants.mjs";
|
|
5
|
+
import { HARNESS_INTAKE_AGENTS } from "./run-init.mjs";
|
|
6
|
+
|
|
7
|
+
export function launchTmux(specOrRunDir, options = {}) {
|
|
8
|
+
const runDir = typeof specOrRunDir === "string" ? resolveRunDir(specOrRunDir) : resolveRunDir(specOrRunDir.run_id);
|
|
9
|
+
const spec = readJson(join(runDir, "inferred-run-spec.json"));
|
|
10
|
+
const sessionName = tmuxSessionName(spec.run_id);
|
|
11
|
+
const agents = HARNESS_INTAKE_AGENTS.map((agent) => agent.id);
|
|
12
|
+
if (options.command === "status") return tmuxStatus(sessionName, spec.run_id);
|
|
13
|
+
if (options.command === "attach") return tmuxAttach(sessionName, options.agent || agents[0]);
|
|
14
|
+
if (options.command === "stop") return tmuxStop(sessionName, spec.run_id);
|
|
15
|
+
requireTmux();
|
|
16
|
+
if (tmuxSessionExists(sessionName)) {
|
|
17
|
+
return { schema: `${SCHEMA_PREFIX}.tmux-result.v1`, status: "already-running", no_ship: false, run_id: spec.run_id, session: sessionName, attach_command: `npm run harness:intake:tmux -- attach ${spec.run_id}` };
|
|
18
|
+
}
|
|
19
|
+
const first = agents[0];
|
|
20
|
+
spawnTmux(["new-session", "-d", "-s", sessionName, "-n", first, "-c", repoRoot]);
|
|
21
|
+
const panes = [];
|
|
22
|
+
for (let i = 0; i < agents.length; i += 1) {
|
|
23
|
+
const agent = agents[i];
|
|
24
|
+
if (i !== 0) spawnTmux(["new-window", "-t", sessionName, "-n", agent, "-c", repoRoot]);
|
|
25
|
+
const kickoff = join(runDir, "kickoff", `${agent}-kickoff.md`);
|
|
26
|
+
const command = `cd ${shellQuote(repoRoot)} && HARNESS_INTAKE_RUN_ID=${shellQuote(spec.run_id)} ZOB_ZAGENT_ID=${shellQuote(agent)} ${process.env.PI_BIN || "pi"} @${shellQuote(kickoff)}`;
|
|
27
|
+
spawnTmux(["send-keys", "-t", `${sessionName}:${agent}`, command, "C-m"]);
|
|
28
|
+
panes.push({ agent, window: agent, kickoff_ref: repoRel(kickoff), command_hash: sha256(command) });
|
|
29
|
+
}
|
|
30
|
+
const dispatch = {
|
|
31
|
+
schema: `${SCHEMA_PREFIX}.kickoff-dispatch.v1`,
|
|
32
|
+
run_id: spec.run_id,
|
|
33
|
+
session: sessionName,
|
|
34
|
+
panes,
|
|
35
|
+
launched_at: new Date().toISOString(),
|
|
36
|
+
launch_is_not_completion: true,
|
|
37
|
+
startup_file_delivery: true,
|
|
38
|
+
worker_startup_file_delivery: true,
|
|
39
|
+
raw_prompt_transport_line_by_line: false,
|
|
40
|
+
post_start_tmux_paste_disabled: true,
|
|
41
|
+
transport: "pi @startup-kickoff.md for every harness-intake agent",
|
|
42
|
+
};
|
|
43
|
+
writeJson(join(runDir, "tmux", "kickoff-dispatch.json"), dispatch);
|
|
44
|
+
writeJson(join(runDir, "tmux", "session.json"), { schema: `${SCHEMA_PREFIX}.tmux-session.v1`, run_id: spec.run_id, session: sessionName, agents });
|
|
45
|
+
writeJson(join(runDir, "tmux", "panes.json"), { schema: `${SCHEMA_PREFIX}.tmux-panes.v1`, run_id: spec.run_id, panes });
|
|
46
|
+
return { schema: `${SCHEMA_PREFIX}.tmux-result.v1`, status: "started", no_ship: false, run_id: spec.run_id, session: sessionName, attach_command: `npm run harness:intake:tmux -- attach ${spec.run_id}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function tmuxSessionName(runId) {
|
|
50
|
+
return `zob-harness-intake-${safeRunId(runId)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function requireTmux() {
|
|
54
|
+
const found = spawnSync("tmux", ["-V"], { encoding: "utf8" });
|
|
55
|
+
if (found.status !== 0) throw new Error("tmux is required for tmux mode");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function spawnTmux(args) {
|
|
59
|
+
const result = spawnSync("tmux", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
60
|
+
if (result.status !== 0) throw new Error(`tmux ${args.join(" ")} failed: ${result.stderr.trim()}`);
|
|
61
|
+
return result.stdout.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function tmuxSessionExists(sessionName) {
|
|
65
|
+
const result = spawnSync("tmux", ["has-session", "-t", sessionName], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
66
|
+
return result.status === 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function tmuxStatus(sessionName, runId) {
|
|
70
|
+
requireTmux();
|
|
71
|
+
if (!tmuxSessionExists(sessionName)) return { schema: `${SCHEMA_PREFIX}.tmux-status.v1`, status: "not-running", no_ship: false, run_id: runId, session: sessionName };
|
|
72
|
+
const list = spawnTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}:#{pane_current_command}:#{pane_active}"]);
|
|
73
|
+
return { schema: `${SCHEMA_PREFIX}.tmux-status.v1`, status: "running", no_ship: false, run_id: runId, session: sessionName, windows: list.split("\n").filter(Boolean) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function tmuxAttach(sessionName, agent) {
|
|
77
|
+
requireTmux();
|
|
78
|
+
if (!tmuxSessionExists(sessionName)) return { schema: `${SCHEMA_PREFIX}.tmux-attach.v1`, status: "not-running", no_ship: true, session: sessionName };
|
|
79
|
+
const child = spawnSync("tmux", ["attach", "-t", `${sessionName}:${agent}`], { stdio: "inherit" });
|
|
80
|
+
return { schema: `${SCHEMA_PREFIX}.tmux-attach.v1`, status: child.status === 0 ? "attached" : "failed", no_ship: child.status !== 0, session: sessionName, agent };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function tmuxStop(sessionName, runId) {
|
|
84
|
+
requireTmux();
|
|
85
|
+
if (!tmuxSessionExists(sessionName)) return { schema: `${SCHEMA_PREFIX}.tmux-stop.v1`, status: "not-running", no_ship: false, run_id: runId, session: sessionName };
|
|
86
|
+
spawnTmux(["kill-session", "-t", sessionName]);
|
|
87
|
+
return { schema: `${SCHEMA_PREFIX}.tmux-stop.v1`, status: "stopped", no_ship: false, run_id: runId, session: sessionName };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function shellQuote(value) {
|
|
91
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
92
|
+
}
|