@kylecheng3146/agent-ops 0.7.1 → 0.7.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/dist/packages/cli/src/hook-process.js +28 -0
- package/dist/packages/cli/src/office-entry.js +2 -2
- package/dist/runtime/src/adapters/claude/config.js +5 -0
- package/dist/runtime/src/adapters/claude/events.js +2 -1
- package/dist/runtime/src/office/page.js +6 -4
- package/dist/runtime/src/office/sessions.js +4 -2
- package/dist/runtime/src/run/hosts/codex.js +1 -1
- package/dist/runtime/src/schema/validate.js +2 -1
- package/docs/en/spec/harness-adapters.md +6 -1
- package/docs/zh-TW/spec/harness-adapters.md +4 -1
- package/package.json +1 -1
- package/schemas/manifest.schema.json +2 -1
|
@@ -240,8 +240,36 @@ function completionGateFor(root, config, gitRunner, harness) {
|
|
|
240
240
|
* Runs one hook invocation. Exit code stays zero because native JSON carries
|
|
241
241
|
* decisions; only an explicitly installed agy completion gate fails closed.
|
|
242
242
|
*/
|
|
243
|
+
/**
|
|
244
|
+
* Claude's SessionEnd: the conversation closed, so its Office room closes too.
|
|
245
|
+
* Display-only and fail-open: it writes no output and never fails the host.
|
|
246
|
+
*/
|
|
247
|
+
async function closeOfficeSession(io, dependencies) {
|
|
248
|
+
try {
|
|
249
|
+
if (process.env.AGENT_OPS_DISABLE === "1" || dependencies.office === undefined)
|
|
250
|
+
return 0;
|
|
251
|
+
const input = parseInput(await readStdin(io.stdin));
|
|
252
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
253
|
+
return 0;
|
|
254
|
+
const fields = input;
|
|
255
|
+
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
256
|
+
const outcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
257
|
+
if (outcome.kind === "invalid" || outcome.config.features.office?.enabled !== true)
|
|
258
|
+
return 0;
|
|
259
|
+
await dependencies.office({
|
|
260
|
+
root, harness: "claude", event: "SessionEnd", input, validated: true,
|
|
261
|
+
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Office never blocks a session, least of all one that is ending.
|
|
266
|
+
}
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
243
269
|
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
244
270
|
const [harness, event] = argv;
|
|
271
|
+
if (harness === "claude" && event === "SessionEnd")
|
|
272
|
+
return await closeOfficeSession(io, dependencies);
|
|
245
273
|
// Both hosts whose Stop hook can actually refuse a stop. codex never fires
|
|
246
274
|
// Stop under `codex exec` and rejects `permissionDecision:ask`, so its
|
|
247
275
|
// escape hatch could not be user-approved; opencode can only deny a tool
|
|
@@ -48,7 +48,7 @@ function sessionIdentity(options) {
|
|
|
48
48
|
return firstText(options.sessionId, recordValue(options.input, "session_id"), recordValue(options.input, "sessionId"), recordValue(options.input, "conversationId"), recordValue(options.input, "thread_id"), recordValue(options.input, "threadId"), recordValue(nested, "sessionID"), recordValue(nested, "session_id"), process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID);
|
|
49
49
|
}
|
|
50
50
|
function officeEvent(event) {
|
|
51
|
-
return event === "SessionStart" ? "start" : event === "Stop" ? "stop" : "activity";
|
|
51
|
+
return event === "SessionStart" ? "start" : event === "Stop" ? "stop" : event === "SessionEnd" ? "end" : "activity";
|
|
52
52
|
}
|
|
53
53
|
/** Phase hints come only from normalized native event kinds and exact agent-ops argv. */
|
|
54
54
|
export function officePhaseHint(event) {
|
|
@@ -129,7 +129,7 @@ export async function observeOfficeSession(options) {
|
|
|
129
129
|
// A resumed session may outlive the ten-minute idle shutdown. Reusing the
|
|
130
130
|
// same ensure path revives the user's server without creating a tab
|
|
131
131
|
// unless this call actually wins a new-server start claim.
|
|
132
|
-
if (options.event !== "Stop")
|
|
132
|
+
if (options.event !== "Stop" && options.event !== "SessionEnd")
|
|
133
133
|
await ensureBackgroundOffice(options.root, undefined, openOfficeBrowser);
|
|
134
134
|
}
|
|
135
135
|
catch {
|
|
@@ -125,6 +125,11 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
|
|
|
125
125
|
else if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
126
126
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
127
127
|
}
|
|
128
|
+
// Stop only ends a turn. SessionEnd is the one signal that the conversation
|
|
129
|
+
// closed, so a session that never had a task can still leave the Office.
|
|
130
|
+
if (capabilities.includes("office-presence")) {
|
|
131
|
+
hooks.SessionEnd = [matcherGroup("SessionEnd", runtimePath)];
|
|
132
|
+
}
|
|
128
133
|
return { hooks };
|
|
129
134
|
}
|
|
130
135
|
function isRecord(value) {
|
|
@@ -305,8 +305,8 @@ var supervisor = {x:43, y:19, targetX:43, targetY:19}, supervisorReady = false,
|
|
|
305
305
|
var roomSupervisor = {x:32, y:34, targetX:32, targetY:34};
|
|
306
306
|
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
307
307
|
var STRINGS = {
|
|
308
|
-
en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", repos:"Repository", allRepos:"All repositories", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
|
|
309
|
-
zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", repos:"專案", allRepos:"所有專案", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
|
|
308
|
+
en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", repos:"Repository", allRepos:"All repositories", noTask:"No task", noTaskHint:"Nothing to verify or review", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
|
|
309
|
+
zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", repos:"專案", allRepos:"所有專案", noTask:"無任務", noTaskHint:"沒有需要驗證或審查的工作", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
|
|
310
310
|
};
|
|
311
311
|
function getLanguage(){
|
|
312
312
|
try { var cookie = document.cookie.split(";").map(function(part){ return part.trim().split("="); }).find(function(pair){ return pair[0] === "agent-office-language"; }); if (cookie && (cookie[1] === "en" || cookie[1] === "zh")) return cookie[1]; } catch (_) {}
|
|
@@ -472,9 +472,11 @@ function drawRoomBase(floor){
|
|
|
472
472
|
var b=floor.board,board=progressBoardGeometry(canvas.width,canvas.height),bx=board.x,by=board.y,bw=board.width,bh=board.height;
|
|
473
473
|
ctx.fillStyle=PALETTE[9];ctx.fillRect(bx,by,bw,bh);ctx.fillStyle=PALETTE[15];ctx.fillRect(bx+2*unit,by+2*unit,bw-4*unit,bh-4*unit);
|
|
474
474
|
fillText('▣ '+t('clickWork'),bx+6*unit,by+5*unit,8,PALETTE[1]);
|
|
475
|
-
|
|
475
|
+
// A room without a task has nothing to verify or review; saying "pending" would claim unfinished work.
|
|
476
|
+
if(b.taskId==='unassigned'&&!b.total){fillText(t('noTask'),bx+6*unit,by+16*unit,10,PALETTE[1]);fillText(t('noTaskHint'),bx+6*unit,by+40*unit,8,PALETTE[4]);}
|
|
477
|
+
else{fillText(t('progress')+' '+b.passed+'/'+(b.total||'?'),bx+6*unit,by+16*unit,10,PALETTE[1]);
|
|
476
478
|
ctx.fillStyle=PALETTE[3];ctx.fillRect(bx+6*unit,by+29*unit,bw-12*unit,5*unit);ctx.fillStyle=PALETTE[2];ctx.fillRect(bx+6*unit,by+29*unit,(bw-12*unit)*Math.min(1,b.total?b.passed/b.total:0),5*unit);
|
|
477
|
-
fillText(t('verify')+' '+outcomeText(b.verify)+' '+t('review')+' '+outcomeText(b.review),bx+6*unit,by+40*unit,8,PALETTE[4]);
|
|
479
|
+
fillText(t('verify')+' '+outcomeText(b.verify)+' '+t('review')+' '+outcomeText(b.review),bx+6*unit,by+40*unit,8,PALETTE[4]);}
|
|
478
480
|
var pendingRow=pendingBoardGeometry(canvas.width,canvas.height);
|
|
479
481
|
if(b.pending)fillText('! '+t('pending')+' '+b.pending,pendingRow.x+2*unit,pendingRow.y+3*unit,8,PALETTE[13]);
|
|
480
482
|
var oldS=S;S=Math.max(1,Math.round(unit));var cx=52*rx,cy=ry;draw('clock',cx,cy);
|
|
@@ -100,7 +100,7 @@ export async function recordOfficeSession(options) {
|
|
|
100
100
|
root: options.projectRoot,
|
|
101
101
|
firstSeenAt: previous?.firstSeenAt ?? at,
|
|
102
102
|
lastSeenAt: at,
|
|
103
|
-
status: options.event === "stop" ? "idle" : "active",
|
|
103
|
+
status: options.event === "stop" || options.event === "end" ? "idle" : "active",
|
|
104
104
|
...(options.agentId ?? previous?.agentId ? { agentId: options.agentId ?? previous?.agentId } : {}),
|
|
105
105
|
...(options.runId ?? previous?.runId ? { runId: options.runId ?? previous?.runId } : {}),
|
|
106
106
|
...(options.workerId ?? previous?.workerId ? { workerId: options.workerId ?? previous?.workerId } : {}),
|
|
@@ -109,7 +109,9 @@ export async function recordOfficeSession(options) {
|
|
|
109
109
|
...(options.host ?? previous?.host ? { host: options.host ?? previous?.host } : {}),
|
|
110
110
|
// A finished session keeps its closed room through the tool calls that
|
|
111
111
|
// report the result; only a new start (or resume) reopens it.
|
|
112
|
-
...(options.event
|
|
112
|
+
...(options.event === "end"
|
|
113
|
+
? { completedAt: previous?.completedAt ?? at }
|
|
114
|
+
: options.event !== "start" && previous?.completedAt !== undefined ? { completedAt: previous.completedAt } : {})
|
|
113
115
|
};
|
|
114
116
|
const without = records.filter(record => record.sessionId !== options.sessionId);
|
|
115
117
|
await writePrivateFile(path, JSON.stringify({ schemaVersion: 1, sessions: [...without, next].slice(-MAX_SESSIONS) }) + "\n", options.commonDir);
|
|
@@ -217,7 +217,12 @@ Claude or Codex transcripts or file contents.
|
|
|
217
217
|
first report from a repository that still has a 0.7 per-repository record
|
|
218
218
|
boots its launchd job out and removes that record; its pid is never signalled.
|
|
219
219
|
A successful `worktree finish` records the session's completion so its room
|
|
220
|
-
closes; only a new start or an active task reopens it.
|
|
220
|
+
closes; only a new start or an active task reopens it. With Office enabled,
|
|
221
|
+
Claude Code also gets a managed `SessionEnd` hook: when the conversation
|
|
222
|
+
closes, its room closes too, even without a task. It writes no output and
|
|
223
|
+
never fails the host. Other hosts publish no end event and rely on the
|
|
224
|
+
activity timeout. A room without a task shows a no-task board rather than
|
|
225
|
+
pending verification and review. Session metadata contains bounded
|
|
221
226
|
identifiers, known work state and activity times, never native transcripts,
|
|
222
227
|
prompts or tool/file contents. Startup and observation failures are advisory.
|
|
223
228
|
The server binds
|
|
@@ -187,7 +187,10 @@ base 的 `git diff`;旁白只使用路徑(`docs/**` 為「writing docs」、
|
|
|
187
187
|
帶有 repository 名稱,key 以其區隔,頁面可依 repository 篩選。仍留有 0.7
|
|
188
188
|
per-repository 紀錄的 repository 第一次回報時,會 bootout 其 launchd job 並
|
|
189
189
|
刪除該紀錄,絕不對其 pid 發送訊號。`worktree finish` 成功後會記錄 session
|
|
190
|
-
完成,使房間關閉;只有新的啟動或 active task
|
|
190
|
+
完成,使房間關閉;只有新的啟動或 active task 會重新開啟。啟用 Office 時,Claude Code
|
|
191
|
+
另有受管理的 `SessionEnd` hook:對話關閉時房間也隨之關閉,即使沒有 task。它不輸出
|
|
192
|
+
任何內容,也絕不讓 host 失敗。其他 host 沒有結束事件,依活動逾時處理。沒有 task 的
|
|
193
|
+
房間白板顯示「無任務」,而非待驗證與待審查。Session metadata
|
|
191
194
|
只記錄有界限的識別資訊、已知工作狀態與活動時間,不包含 native transcript、
|
|
192
195
|
prompt、tool 或檔案內容。啟動與活動觀察失敗均為 advisory。
|
|
193
196
|
Server 綁定 127.0.0.1 的隨機 port,要求 URL 中不可猜測的 token,拒絕該位址與
|
package/package.json
CHANGED