@kylecheng3146/agent-ops 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/packages/cli/src/args.js +16 -0
- package/dist/packages/cli/src/bin.js +40 -8
- package/dist/packages/cli/src/cli.js +2 -0
- package/dist/packages/cli/src/commands/batch.js +3 -1
- package/dist/packages/cli/src/commands/update.js +18 -0
- package/dist/packages/cli/src/commands/verify.js +3 -1
- package/dist/packages/cli/src/hook-process.js +48 -0
- package/dist/packages/cli/src/office-entry.js +34 -6
- package/dist/packages/cli/src/run-deps.js +8 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -0
- package/dist/runtime/src/install/update.js +10 -2
- package/dist/runtime/src/office/activity.js +105 -0
- package/dist/runtime/src/office/art.js +999 -0
- package/dist/runtime/src/office/collect.js +137 -10
- package/dist/runtime/src/office/layout.js +200 -0
- package/dist/runtime/src/office/page.js +760 -645
- package/dist/runtime/src/office/scene.js +27 -31
- package/dist/runtime/src/office/sessions.js +18 -1
- package/dist/runtime/src/office/snapshot.js +62 -1
- package/dist/runtime/src/parallel/finish.js +4 -2
- package/dist/runtime/src/review/batch.js +10 -0
- package/dist/runtime/src/review/execute.js +9 -0
- package/dist/runtime/src/run/hosts/codex.js +1 -1
- package/docs/en/guides/configuration.md +7 -0
- package/docs/en/spec/harness-adapters.md +16 -2
- package/docs/zh-TW/guides/configuration.md +6 -0
- package/docs/zh-TW/spec/harness-adapters.md +12 -2
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ export function sceneModel(snapshot) {
|
|
|
35
35
|
return { passed: typeof p.passed === "number" ? p.passed : 0, total: typeof p.total === "number" ? p.total : 0,
|
|
36
36
|
verify: p.verify === "PASS" || p.verify === "FAIL" ? p.verify : null, review: p.review === "PASS" || p.review === "FAIL" ? p.review : null };
|
|
37
37
|
};
|
|
38
|
-
const short = (value, limit = 18) => value.length > limit ? value.slice(0, limit -
|
|
38
|
+
const short = (value, limit = 18) => value.length > limit ? value.slice(0, limit - 6) + "…" + value.slice(-5) : value;
|
|
39
39
|
const optional = (value) => value !== null && typeof value === "object" ? value : {};
|
|
40
40
|
const boardFor = (progress, status, taskId, pending) => ({
|
|
41
41
|
passed: progress?.passed ?? 0, total: progress?.total ?? 0,
|
|
@@ -49,7 +49,7 @@ export function sceneModel(snapshot) {
|
|
|
49
49
|
return typeof repo === "string" && repo.length > 0 ? repo : null;
|
|
50
50
|
};
|
|
51
51
|
const scoped = (repo, key) => repo === null ? key : "repo:" + repo + "|" + key;
|
|
52
|
-
const titled = (repo, title) =>
|
|
52
|
+
const titled = (repo, title) => repo === null ? title : repo + " · " + title;
|
|
53
53
|
const makeAreas = () => PHASES.map(phase => {
|
|
54
54
|
const area = areas[phase];
|
|
55
55
|
return { phase, x: area.x, y: area.y, width: area.width, height: area.height, prop: area.prop, label: labels[phase] };
|
|
@@ -70,7 +70,7 @@ export function sceneModel(snapshot) {
|
|
|
70
70
|
{ kind: "plant", x: 57, y: 34, key: key + ":plant-bottom" },
|
|
71
71
|
{ kind: "door", x: 65, y: 33, key: key + ":door" }
|
|
72
72
|
];
|
|
73
|
-
const makeRun = (run) => {
|
|
73
|
+
const makeRun = (run, index) => {
|
|
74
74
|
const extra = optional(run);
|
|
75
75
|
const completedAt = typeof extra.completedAt === "string" ? extra.completedAt : null;
|
|
76
76
|
const questions = run.questions;
|
|
@@ -95,34 +95,34 @@ export function sceneModel(snapshot) {
|
|
|
95
95
|
total += p?.total ?? 0;
|
|
96
96
|
if (p?.verify === "FAIL")
|
|
97
97
|
verify = "FAIL";
|
|
98
|
-
else if (p?.verify === "PASS" && verify === null)
|
|
99
|
-
verify = "PASS";
|
|
100
98
|
if (p?.review === "FAIL")
|
|
101
99
|
review = "FAIL";
|
|
102
|
-
else if (p?.review === "PASS" && review === null)
|
|
103
|
-
review = "PASS";
|
|
104
100
|
actors.push({ key: key + ":" + agent.id, id: agent.id, kind: agent.role, x: point.x, y: point.y, alert: agent.role === "coordinator" && questions.length > 0,
|
|
105
101
|
label: short(agent.id), status: agent.status, phase, taskId: agent.taskId || "unassigned", progress: p,
|
|
106
102
|
narration: agent.narration, questionCount: agent.role === "coordinator" ? questions.length : 0, host: "unknown" });
|
|
107
103
|
dialogue.push(`${run.title}: ${agent.id} ${agent.narration}.`);
|
|
108
104
|
});
|
|
109
|
-
run.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
actors.push({ key: key + ":review:" + reviewer.slot, id: "reviewer " + reviewer.slot, kind: "reviewer", x: point.x, y: point.y, alert: false,
|
|
114
|
-
label: "reviewer " + reviewer.slot, status: "reviewing", phase: "reviewing", taskId: reviewer.taskId ?? "unassigned",
|
|
115
|
-
progress: null, narration: "reviewing", questionCount: 0, host: "unknown" });
|
|
116
|
-
});
|
|
105
|
+
if (verify === null && run.agents.length && run.agents.every(agent => agent.progress?.verify === "PASS"))
|
|
106
|
+
verify = "PASS";
|
|
107
|
+
if (review === null && run.agents.length && run.agents.every(agent => agent.progress?.review === "PASS"))
|
|
108
|
+
review = "PASS";
|
|
117
109
|
const rootProgress = { passed, total, verify, review };
|
|
118
110
|
const taskId = run.agents.find(agent => agent.role === "coordinator")?.taskId ?? "unassigned";
|
|
119
111
|
if (questions.length > 0)
|
|
120
112
|
dialogue.push(`${run.title}: waiting for your answer — ${questions[0].prompt}`);
|
|
121
|
-
return { key, sourceIndex:
|
|
113
|
+
return { key, sourceIndex: index, kind: "run", title: titled(repo, run.title), top: 0, props, actors, phaseAreas: makeAreas(), questions, criteria: [],
|
|
114
|
+
activities: activitiesOf(optional(run).activities),
|
|
122
115
|
board: boardFor(rootProgress, run.status, taskId, questions.length), papers, books: { lit: passed, total },
|
|
123
116
|
clock: run.budget.limitMs > 0 ? run.budget.remainingMs / run.budget.limitMs : 0, phase: run.phase, status: run.status,
|
|
124
117
|
completedAt, repo, overview: { x: 0, y: 0, width: 0, height: 0 } };
|
|
125
118
|
};
|
|
119
|
+
const activitiesOf = (value) => (Array.isArray(value) ? value : []).map(raw => {
|
|
120
|
+
const item = optional(raw);
|
|
121
|
+
return { kind: item.kind === "verify" || item.kind === "review" || item.kind === "finish" ? item.kind : "verify",
|
|
122
|
+
startedAt: typeof item.startedAt === "string" ? item.startedAt : "",
|
|
123
|
+
targets: Array.isArray(item.targets) ? item.targets.filter((t) => typeof t === "string").slice(0, 2) : [],
|
|
124
|
+
round: item.round === 1 || item.round === 2 ? item.round : null, target: typeof item.target === "string" ? item.target : null };
|
|
125
|
+
});
|
|
126
126
|
const makeDesk = (desk, index) => {
|
|
127
127
|
const extra = optional(desk);
|
|
128
128
|
const phase = extra.phase === "planning" || extra.phase === "implementing" || extra.phase === "verifying" || extra.phase === "reviewing" || extra.phase === "integrating" ? extra.phase : "unknown";
|
|
@@ -135,6 +135,15 @@ export function sceneModel(snapshot) {
|
|
|
135
135
|
return { questionId: typeof item.questionId === "string" ? item.questionId : "unknown", prompt: typeof item.prompt === "string" ? item.prompt : "" };
|
|
136
136
|
});
|
|
137
137
|
const completedAt = typeof extra.completedAt === "string" ? extra.completedAt : null;
|
|
138
|
+
// Inline checks, no named helper: the page embeds sceneModel via toString, so it cannot carry bundler helpers.
|
|
139
|
+
const criteria = (Array.isArray(extra.criteria) ? extra.criteria : []).map(value => {
|
|
140
|
+
const item = optional(value);
|
|
141
|
+
return { id: typeof item.id === "string" ? item.id : "unknown", description: typeof item.description === "string" ? item.description : "",
|
|
142
|
+
status: item.status === "PASS" || item.status === "FAIL" || item.status === "UNKNOWN" ? item.status : null,
|
|
143
|
+
finishedAt: typeof item.finishedAt === "string" ? item.finishedAt : null,
|
|
144
|
+
failureClass: typeof item.failureClass === "string" ? item.failureClass : null,
|
|
145
|
+
exitCode: typeof item.exitCode === "number" ? item.exitCode : null, output: typeof item.output === "string" ? item.output : null };
|
|
146
|
+
});
|
|
138
147
|
const repo = repoOf(desk);
|
|
139
148
|
const key = scoped(repo, "session:" + [desk.sessionId || "unknown", desk.branch || "unknown", desk.name || "session"].join(":"));
|
|
140
149
|
const props = makeProps(key);
|
|
@@ -146,24 +155,11 @@ export function sceneModel(snapshot) {
|
|
|
146
155
|
dialogue.push(`${desk.name}: waiting for your answer.`);
|
|
147
156
|
dialogue.push(`${desk.name}: ${desk.narration}.`);
|
|
148
157
|
return { key, sourceIndex: index, kind: "desk", title: titled(repo, desk.name), top: 0, props, actors: [actor], phaseAreas: makeAreas(),
|
|
149
|
-
board: boardFor(p, status, taskId, questions.length), questions, papers: { [key + ":implementing"]: Math.min(8, desk.diff.files) }, books: { lit: p?.passed ?? 0, total: p?.total ?? 0 },
|
|
158
|
+
board: boardFor(p, status, taskId, questions.length), questions, criteria, activities: activitiesOf(extra.activities), papers: { [key + ":implementing"]: Math.min(8, desk.diff.files) }, books: { lit: p?.passed ?? 0, total: p?.total ?? 0 },
|
|
150
159
|
clock: 1, phase, status, completedAt, repo, overview: { x: 0, y: 0, width: 0, height: 0 } };
|
|
151
160
|
};
|
|
152
|
-
snapshot.runs.forEach(run => floors.push(makeRun(run)));
|
|
161
|
+
snapshot.runs.forEach((run, index) => floors.push(makeRun(run, index)));
|
|
153
162
|
snapshot.lobby.forEach((desk, index) => floors.push(makeDesk(desk, index)));
|
|
154
|
-
snapshot.reviews.forEach((review, index) => {
|
|
155
|
-
const repo = repoOf(review);
|
|
156
|
-
const key = scoped(repo, "review:" + review.slot + ":" + index);
|
|
157
|
-
const props = makeProps(key);
|
|
158
|
-
const point = position("reviewing", 0);
|
|
159
|
-
const actor = { key: key + ":actor", id: "reviewer " + review.slot, kind: "reviewer", x: point.x, y: point.y, alert: false, label: "reviewer " + review.slot,
|
|
160
|
-
status: "reviewing", phase: "reviewing", taskId: review.taskId ?? "unassigned", progress: null,
|
|
161
|
-
narration: "reviewing", questionCount: 0, host: "unknown" };
|
|
162
|
-
dialogue.push(`Review slot ${review.slot}: ${review.taskId ?? "unassigned"}.`);
|
|
163
|
-
floors.push({ key, sourceIndex: index, kind: "review", title: titled(repo, "Review slot " + review.slot), top: 0, props, actors: [actor], phaseAreas: makeAreas(), questions: [],
|
|
164
|
-
board: boardFor(null, "reviewing", review.taskId ?? "unassigned", 0), papers: {}, books: { lit: 0, total: 0 }, clock: 1,
|
|
165
|
-
phase: "reviewing", status: "reviewing", completedAt: null, repo, overview: { x: 0, y: 0, width: 0, height: 0 } });
|
|
166
|
-
});
|
|
167
163
|
// Rooms of one repository sit together; the sort is stable inside each.
|
|
168
164
|
floors.sort((a, b) => (a.repo ?? "").localeCompare(b.repo ?? ""));
|
|
169
165
|
if (dialogue.length === 0)
|
|
@@ -99,7 +99,8 @@ export async function recordOfficeSession(options) {
|
|
|
99
99
|
root: options.projectRoot,
|
|
100
100
|
firstSeenAt: previous?.firstSeenAt ?? at,
|
|
101
101
|
lastSeenAt: at,
|
|
102
|
-
|
|
102
|
+
// A session that only started is waiting at its prompt; work begins with activity.
|
|
103
|
+
status: options.event === "activity" ? "active" : "idle",
|
|
103
104
|
...(options.agentId ?? previous?.agentId ? { agentId: options.agentId ?? previous?.agentId } : {}),
|
|
104
105
|
...(options.runId ?? previous?.runId ? { runId: options.runId ?? previous?.runId } : {}),
|
|
105
106
|
...(options.workerId ?? previous?.workerId ? { workerId: options.workerId ?? previous?.workerId } : {}),
|
|
@@ -116,6 +117,22 @@ export async function recordOfficeSession(options) {
|
|
|
116
117
|
await writePrivateFile(path, JSON.stringify({ schemaVersion: 1, sessions: [...without, next].slice(-MAX_SESSIONS) }) + "\n", options.commonDir);
|
|
117
118
|
});
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* A long command's later step (batch, advance) on the session that runs it,
|
|
122
|
+
* which hooks only see start. Unknown sessions stay unknown.
|
|
123
|
+
*/
|
|
124
|
+
export async function recordOfficeSessionPhase(commonDir, sessionId, phase, now = Date.now()) {
|
|
125
|
+
const path = officeSessionsPath(commonDir);
|
|
126
|
+
await withPrivateFileLock(path, commonDir, async () => {
|
|
127
|
+
const records = prune(await readRecords(commonDir), now);
|
|
128
|
+
if (!records.some(record => record.sessionId === sessionId))
|
|
129
|
+
return;
|
|
130
|
+
const at = new Date(now).toISOString();
|
|
131
|
+
const next = records.map(record => record.sessionId === sessionId
|
|
132
|
+
? { ...record, status: "active", phase, lastSeenAt: at } : record);
|
|
133
|
+
await writePrivateFile(path, JSON.stringify({ schemaVersion: OFFICE_SESSION_SCHEMA_VERSION, sessions: next }) + "\n", commonDir);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
119
136
|
/** Closes a session's room once `worktree finish` merged its work. Unknown sessions stay unknown. */
|
|
120
137
|
export async function markOfficeSessionCompleted(commonDir, sessionId, now = Date.now()) {
|
|
121
138
|
const path = officeSessionsPath(commonDir);
|
|
@@ -57,6 +57,7 @@ export function buildOfficeSnapshot(input) {
|
|
|
57
57
|
runSessionIds.add(worker.nativeSessionId);
|
|
58
58
|
});
|
|
59
59
|
const lobby = [];
|
|
60
|
+
const deskPaths = new Map();
|
|
60
61
|
const represented = new Set();
|
|
61
62
|
for (const worktree of input.worktrees) {
|
|
62
63
|
if (worktree.runId !== undefined)
|
|
@@ -78,9 +79,15 @@ export function buildOfficeSnapshot(input) {
|
|
|
78
79
|
...(worktree.status === undefined && session?.taskStatus === undefined && session?.status === undefined ? {} : { status: worktree.status ?? session?.taskStatus ?? session?.status }),
|
|
79
80
|
...(worktree.taskId === undefined && session?.taskId === undefined ? {} : { taskId: worktree.taskId ?? session?.taskId ?? null }),
|
|
80
81
|
...(worktree.progress === undefined && session?.progress === undefined ? {} : { progress: worktree.progress ?? session?.progress ?? null }),
|
|
82
|
+
...(worktree.title === undefined && session?.title === undefined ? {} : { title: worktree.title ?? session?.title }),
|
|
83
|
+
...(worktree.criteria === undefined && session?.criteria === undefined ? {} : { criteria: worktree.criteria ?? session?.criteria ?? [] }),
|
|
84
|
+
...(worktree.review === undefined && session?.review === undefined ? {} : { review: worktree.review ?? session?.review ?? null }),
|
|
85
|
+
...(worktree.base === undefined ? {} : { base: worktree.base }),
|
|
86
|
+
...(worktree.ahead === undefined ? {} : { ahead: worktree.ahead }),
|
|
81
87
|
...(completedAt === null && session?.completedAt === undefined ? {} : { completedAt }),
|
|
82
88
|
...(worktree.host === undefined && session?.host === undefined ? {} : { host: worktree.host ?? session?.host }),
|
|
83
89
|
});
|
|
90
|
+
deskPaths.set(lobby.length - 1, worktree.path);
|
|
84
91
|
represented.add(worktree.sessionId);
|
|
85
92
|
}
|
|
86
93
|
for (const session of sessions) {
|
|
@@ -100,11 +107,16 @@ export function buildOfficeSnapshot(input) {
|
|
|
100
107
|
status: session.taskStatus ?? session.status,
|
|
101
108
|
...(session.taskId === undefined ? {} : { taskId: session.taskId }),
|
|
102
109
|
...(session.progress === undefined ? {} : { progress: session.progress }),
|
|
110
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
111
|
+
...(session.criteria === undefined ? {} : { criteria: session.criteria }),
|
|
112
|
+
...(session.review === undefined ? {} : { review: session.review }),
|
|
103
113
|
...(completedAt === null ? {} : { completedAt }),
|
|
104
114
|
...(session.host === undefined ? {} : { host: session.host })
|
|
105
115
|
});
|
|
106
116
|
}
|
|
107
|
-
|
|
117
|
+
attachActivities(input, runs, lobby, deskPaths);
|
|
118
|
+
// Review slots no longer make rooms: a running review shows up as reviewers in its own room.
|
|
119
|
+
return { generatedAt: new Date(input.now).toISOString(), runs, lobby, reviews: [] };
|
|
108
120
|
}
|
|
109
121
|
/** One building from several repositories' snapshots; every room names its repository. */
|
|
110
122
|
export function mergeOfficeSnapshots(parts, now) {
|
|
@@ -115,3 +127,52 @@ export function mergeOfficeSnapshots(parts, now) {
|
|
|
115
127
|
reviews: parts.flatMap(({ repo, snapshot }) => snapshot.reviews.map(review => ({ ...review, repo })))
|
|
116
128
|
};
|
|
117
129
|
}
|
|
130
|
+
const ACTIVITY_PHASE = { verify: "verifying", review: "reviewing", finish: "integrating" };
|
|
131
|
+
function activityView(activity) {
|
|
132
|
+
return { kind: activity.kind, startedAt: activity.startedAt, ...(activity.targets === undefined ? {} : { targets: activity.targets }),
|
|
133
|
+
...(activity.round === undefined ? {} : { round: activity.round }), ...(activity.target === undefined ? {} : { target: activity.target }) };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Puts each live activity in its room: the same session, else the same task,
|
|
137
|
+
* else the worktree it runs in. One that matches no room gets a room of its
|
|
138
|
+
* own for as long as it runs, so nothing that is running goes unseen.
|
|
139
|
+
*/
|
|
140
|
+
function attachActivities(input, runs, lobby, deskPaths) {
|
|
141
|
+
const runSessions = (run) => new Set(run.workers.flatMap(worker => [worker.ownerSessionId, worker.nativeSessionId].filter((id) => id !== null)));
|
|
142
|
+
for (const activity of input.activities ?? []) {
|
|
143
|
+
const view = activityView(activity);
|
|
144
|
+
const toDesk = (index) => { const desk = lobby[index]; lobby[index] = { ...desk, activities: [...(desk.activities ?? []), view] }; };
|
|
145
|
+
const toRun = (index) => { const run = runs[index]; runs[index] = { ...run, activities: [...(run.activities ?? []), view] }; };
|
|
146
|
+
const sourceRun = (run) => input.runs.find(state => state.runId === run.runId);
|
|
147
|
+
const matchers = [
|
|
148
|
+
[desk => activity.sessionId !== undefined && desk.sessionId === activity.sessionId,
|
|
149
|
+
run => activity.sessionId !== undefined && runSessions(sourceRun(run)).has(activity.sessionId)],
|
|
150
|
+
[desk => activity.taskId !== undefined && desk.taskId === activity.taskId,
|
|
151
|
+
run => activity.taskId !== undefined && run.agents.some(agent => agent.taskId === activity.taskId)],
|
|
152
|
+
[(_, index) => deskPaths.get(index) === activity.root,
|
|
153
|
+
run => sourceRun(run).workers.some(worker => worker.worktree === activity.root)]
|
|
154
|
+
];
|
|
155
|
+
let placed = false;
|
|
156
|
+
for (const [deskMatch, runMatch] of matchers) {
|
|
157
|
+
const desk = lobby.findIndex(deskMatch);
|
|
158
|
+
if (desk >= 0) {
|
|
159
|
+
toDesk(desk);
|
|
160
|
+
placed = true;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const run = runs.findIndex(candidate => sourceRun(candidate) !== undefined && runMatch(candidate));
|
|
164
|
+
if (run >= 0) {
|
|
165
|
+
toRun(run);
|
|
166
|
+
placed = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (placed)
|
|
171
|
+
continue;
|
|
172
|
+
const name = activity.taskId ?? activity.worktree ?? activity.kind;
|
|
173
|
+
lobby.push({ name, branch: "(no worktree)", sessionId: activity.sessionId ?? `activity-${activity.kind}-${activity.pid}`, sessionActive: true,
|
|
174
|
+
diff: { files: 0, insertions: 0, deletions: 0, paths: [], recent: null }, narration: ACTIVITY_PHASE[activity.kind],
|
|
175
|
+
commands: activity.taskId === undefined ? [] : [`agent-ops task status --task ${quote(activity.taskId)}`],
|
|
176
|
+
phase: ACTIVITY_PHASE[activity.kind], status: "active", ...(activity.taskId === undefined ? {} : { taskId: activity.taskId }), activities: [view] });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withActivity } from "../office/activity.js";
|
|
1
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -233,7 +234,8 @@ export async function finishWorktree(deps, options) {
|
|
|
233
234
|
if (record === null) {
|
|
234
235
|
throw finishError("WORKTREE_NOT_FOUND", `No agent-ops worktree named ${name}; see agent-ops worktree list.`);
|
|
235
236
|
}
|
|
236
|
-
|
|
237
|
+
// Display only: the integrator is at work while the finish runs.
|
|
238
|
+
return await withActivity(commonDir, { kind: "finish", root: record.path, sessionId: record.sessionId, worktree: name }, async () => await withFinishLock(deps, commonDir, async () => {
|
|
237
239
|
const sessionChildren = (await listWorktrees(deps, mainRoot))
|
|
238
240
|
.map(({ record: child }) => child)
|
|
239
241
|
.filter((child) => child.path !== record.path && child.sessionId === record.sessionId &&
|
|
@@ -507,5 +509,5 @@ export async function finishWorktree(deps, options) {
|
|
|
507
509
|
await markRunIntegration(commonDir, record, "cleaned", "coordinator");
|
|
508
510
|
return { record, mergedHead, rebased: !fastForward, taskIds: tree.map(({ task }) => task.task.id), warnings,
|
|
509
511
|
...(receipt === undefined ? {} : { receipt: receipt.path }) };
|
|
510
|
-
});
|
|
512
|
+
}));
|
|
511
513
|
}
|
|
@@ -25,6 +25,14 @@ export async function runBatch(ids, deps, options) {
|
|
|
25
25
|
throw new AgentOpsError("BATCH_INVALID_WIDTH", "Batch width must be a positive integer.");
|
|
26
26
|
}
|
|
27
27
|
const progress = options.onProgress ?? (() => { });
|
|
28
|
+
const phase = (value) => {
|
|
29
|
+
try {
|
|
30
|
+
options.onPhase?.(value);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Display only.
|
|
34
|
+
}
|
|
35
|
+
};
|
|
28
36
|
const controller = new AbortController();
|
|
29
37
|
const outer = options.signal;
|
|
30
38
|
if (outer?.aborted === true) {
|
|
@@ -71,6 +79,7 @@ export async function runBatch(ids, deps, options) {
|
|
|
71
79
|
entry.retried = true;
|
|
72
80
|
}
|
|
73
81
|
let result;
|
|
82
|
+
phase("reviewing");
|
|
74
83
|
try {
|
|
75
84
|
result = await deps.review(id, controller.signal);
|
|
76
85
|
}
|
|
@@ -127,6 +136,7 @@ export async function runBatch(ids, deps, options) {
|
|
|
127
136
|
break;
|
|
128
137
|
}
|
|
129
138
|
let status;
|
|
139
|
+
phase("verifying");
|
|
130
140
|
try {
|
|
131
141
|
status = await deps.verify(id, controller.signal);
|
|
132
142
|
}
|
|
@@ -690,6 +690,13 @@ async function attemptTargetWithRetry(request, options) {
|
|
|
690
690
|
return attemptTarget({ ...request, resumeLostTurn: false }, options);
|
|
691
691
|
}
|
|
692
692
|
/** Builds the `execute` callback with one necessary and one adversarial review. */
|
|
693
|
+
/** Display only: a failing listener never touches the review. */
|
|
694
|
+
function announceRound(options, round, target, targets) {
|
|
695
|
+
try {
|
|
696
|
+
options.onRound?.(round, target, targets);
|
|
697
|
+
}
|
|
698
|
+
catch { /* display only */ }
|
|
699
|
+
}
|
|
693
700
|
export function createReviewExecutor(options) {
|
|
694
701
|
const report = options.onProgress ?? (() => { });
|
|
695
702
|
const host = detectHostTarget(options.env ?? process.env);
|
|
@@ -802,6 +809,7 @@ export function createReviewExecutor(options) {
|
|
|
802
809
|
report("chain: review deadline reached before the first round");
|
|
803
810
|
return { status: "NOT_RUN", reason: "timeout", attempts: [] };
|
|
804
811
|
}
|
|
812
|
+
announceRound(options, 1, primaryTarget, plannedTargets);
|
|
805
813
|
const primaryOutcome = await attemptTargetWithRetry({
|
|
806
814
|
...shared,
|
|
807
815
|
target: primaryTarget,
|
|
@@ -911,6 +919,7 @@ export function createReviewExecutor(options) {
|
|
|
911
919
|
};
|
|
912
920
|
}
|
|
913
921
|
// The second round draws on what is left, never a fresh full budget.
|
|
922
|
+
announceRound(options, 2, adversarialTarget, plannedTargets);
|
|
914
923
|
const adversarialOutcome = await attemptTargetWithRetry({
|
|
915
924
|
...shared,
|
|
916
925
|
target: adversarialTarget,
|
|
@@ -448,6 +448,13 @@ To narrow an existing installation, pass the desired list to `agent-ops update
|
|
|
448
448
|
--harness`; shared paths remain managed while removed harness-owned artifacts,
|
|
449
449
|
markers, and hooks are reconciled.
|
|
450
450
|
|
|
451
|
+
Update keeps the configured profiles. To add or remove the opt-in `run`
|
|
452
|
+
profile on an existing installation, pass `agent-ops update --auto-run on` or
|
|
453
|
+
`--auto-run off`; `off` removes only `run`, and the `loop` it brought stays
|
|
454
|
+
(use `init` to change other profiles). Without the flag, an interactive update
|
|
455
|
+
on macOS with Claude Code or Codex installed asks once, defaulting to the
|
|
456
|
+
current choice; `--yes` and non-interactive updates preserve it.
|
|
457
|
+
|
|
451
458
|
To remove only one integrated harness, use `agent-ops uninstall --harness agy`
|
|
452
459
|
(or another installed id). The remaining manifest and shared paths stay in
|
|
453
460
|
place; omit `--harness` to remove the complete managed installation.
|
|
@@ -145,7 +145,10 @@ The `run` profile MUST be opt-in, select `core` and `loop` with it, and add only
|
|
|
145
145
|
the `auto-run` capability. With it, the Claude Code and Codex managed rules hand
|
|
146
146
|
a change that needs more than five acceptance criteria to `agent-ops run`; five
|
|
147
147
|
or fewer stay in the session. agy rules, and every rule file without `auto-run`,
|
|
148
|
-
MUST stay byte-identical to the rules without the profile.
|
|
148
|
+
MUST stay byte-identical to the rules without the profile. Update MUST preserve
|
|
149
|
+
the profile unless `--auto-run on|off` is supplied or an interactive update on
|
|
150
|
+
macOS with Claude Code or Codex asks (default: the current choice); `off`
|
|
151
|
+
removes only `run` and keeps `loop`.
|
|
149
152
|
|
|
150
153
|
- Trigger: A project selects `run`, and a Claude Code or Codex session meets a
|
|
151
154
|
change that needs more than five acceptance criteria.
|
|
@@ -221,7 +224,18 @@ Claude or Codex transcripts or file contents.
|
|
|
221
224
|
Claude Code also gets a managed `SessionEnd` hook: when the conversation
|
|
222
225
|
closes, its room closes too, even without a task. It writes no output and
|
|
223
226
|
never fails the host. Other hosts publish no end event and rely on the
|
|
224
|
-
activity timeout. A
|
|
227
|
+
activity timeout. A session that has only started is idle; a submitted
|
|
228
|
+
prompt or tool activity marks it active, and agy's per-invocation start
|
|
229
|
+
counts as activity. Outside the project loop, Claude Code and Codex get a
|
|
230
|
+
managed `UserPromptSubmit` hook whenever Office or command policy is enabled:
|
|
231
|
+
Office records the turn's start, and command policy refuses a prompt carrying
|
|
232
|
+
a literal credential in the host's native shape (Claude `decision: "block"`,
|
|
233
|
+
Codex exit code 2). Its runtime failures let the prompt through. Hooks see
|
|
234
|
+
only a command's start, so `agent-ops batch` reports each verify or review
|
|
235
|
+
as it starts to the sessions attached to its tasks (the newest step shows),
|
|
236
|
+
and `task advance` outside a run reports verify, review and integration to
|
|
237
|
+
its own session. These reports update only a known session, never fail the
|
|
238
|
+
command, and record nothing while Office is disabled. A room without a task shows a no-task board rather than
|
|
225
239
|
pending verification and review. Session metadata contains bounded
|
|
226
240
|
identifiers, known work state and activity times, never native transcripts,
|
|
227
241
|
prompts or tool/file contents. Startup and observation failures are advisory.
|
|
@@ -395,6 +395,12 @@ config-hash 與 timestamp evidence,且永遠不會完成 task。Config v1 會
|
|
|
395
395
|
`agent-ops update --harness`;shared path 會繼續受管理,被移除 harness
|
|
396
396
|
擁有的 artifact、marker 與 hook 則會被安全同步。
|
|
397
397
|
|
|
398
|
+
Update 會保留已設定的 profile。若要在既有 installation 加入或移除 opt-in 的
|
|
399
|
+
`run` profile,請使用 `agent-ops update --auto-run on` 或 `--auto-run off`;
|
|
400
|
+
`off` 只移除 `run`,它帶入的 `loop` 會保留(其他 profile 請用 `init` 變更)。
|
|
401
|
+
未提供此旗標時,在已安裝 Claude Code 或 Codex 的 macOS 上互動式 update 會詢問
|
|
402
|
+
一次,預設為目前的選擇;`--yes` 與非互動式 update 會保留原設定。
|
|
403
|
+
|
|
398
404
|
若只要移除單一已整合 harness,可使用
|
|
399
405
|
`agent-ops uninstall --harness agy`(或其他已安裝 id)。剩餘 manifest 與
|
|
400
406
|
shared path 會保留;省略 `--harness` 才會移除整套 managed installation。
|
|
@@ -126,7 +126,9 @@ event metadata、回傳有界且 redacted 的 session context,並在 update
|
|
|
126
126
|
capability。啟用後,Claude Code 與 Codex 的 managed rules 會將需要超過五項
|
|
127
127
|
acceptance criteria 的變更交給 `agent-ops run`;五項以內仍留在 session 中處理。
|
|
128
128
|
agy rules,以及所有未含 `auto-run` 的 rule file,MUST 與未啟用此 profile 時
|
|
129
|
-
byte-identical。
|
|
129
|
+
byte-identical。Update MUST 保留此 profile,除非提供 `--auto-run on|off`,或在已
|
|
130
|
+
安裝 Claude Code 或 Codex 的 macOS 上互動式 update 詢問(預設為目前選擇);
|
|
131
|
+
`off` 只移除 `run`,保留 `loop`。
|
|
130
132
|
|
|
131
133
|
- Trigger: Project 選擇 `run`,且 Claude Code 或 Codex session 遇到需要超過五項
|
|
132
134
|
acceptance criteria 的變更。
|
|
@@ -189,7 +191,15 @@ base 的 `git diff`;旁白只使用路徑(`docs/**` 為「writing docs」、
|
|
|
189
191
|
刪除該紀錄,絕不對其 pid 發送訊號。`worktree finish` 成功後會記錄 session
|
|
190
192
|
完成,使房間關閉;只有新的啟動或 active task 會重新開啟。啟用 Office 時,Claude Code
|
|
191
193
|
另有受管理的 `SessionEnd` hook:對話關閉時房間也隨之關閉,即使沒有 task。它不輸出
|
|
192
|
-
任何內容,也絕不讓 host 失敗。其他 host
|
|
194
|
+
任何內容,也絕不讓 host 失敗。其他 host 沒有結束事件,依活動逾時處理。只有啟動的
|
|
195
|
+
session 為閒置;送出 prompt 或 tool 活動才標為 active,agy 以 invocation 為單位
|
|
196
|
+
的啟動視為活動。不在 project loop 時,只要啟用 Office 或 command policy,Claude
|
|
197
|
+
Code 與 Codex 會有受管理的 `UserPromptSubmit` hook:Office 記錄該輪開始,command
|
|
198
|
+
policy 以 host 原生格式拒絕含字面憑證的 prompt(Claude `decision: "block"`、Codex
|
|
199
|
+
exit code 2)。其 runtime 失敗時放行 prompt。Hook 只看得到指令開始,因此
|
|
200
|
+
`agent-ops batch` 在每個 verify 或 review 開始時回報給其 task 綁定的 session
|
|
201
|
+
(顯示最新一步),不在 run 內的 `task advance` 則把驗證、審查、整合回報給自己
|
|
202
|
+
的 session。這些回報只更新已知 session,絕不讓指令失敗,Office 關閉時不記錄。沒有 task 的
|
|
193
203
|
房間白板顯示「無任務」,而非待驗證與待審查。Session metadata
|
|
194
204
|
只記錄有界限的識別資訊、已知工作狀態與活動時間,不包含 native transcript、
|
|
195
205
|
prompt、tool 或檔案內容。啟動與活動觀察失敗均為 advisory。
|