@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/activity.js
CHANGED
|
@@ -34,6 +34,9 @@ export const MILESTONE_EVENTS = [
|
|
|
34
34
|
'pushed',
|
|
35
35
|
'subagent.spawned',
|
|
36
36
|
'artifact.created',
|
|
37
|
+
'task.completed',
|
|
38
|
+
'checklist.created',
|
|
39
|
+
'status.posted',
|
|
37
40
|
];
|
|
38
41
|
const MILESTONE_SET = new Set(MILESTONE_EVENTS);
|
|
39
42
|
export function tierForEvent(event) {
|
|
@@ -82,6 +85,10 @@ function parseLine(line) {
|
|
|
82
85
|
tool: parsed.tool,
|
|
83
86
|
detail: parsed.detail,
|
|
84
87
|
url: parsed.url,
|
|
88
|
+
pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
|
|
89
|
+
launchId: parsed.launchId,
|
|
90
|
+
terminalId: parsed.terminalId,
|
|
91
|
+
tmuxPane: parsed.tmuxPane,
|
|
85
92
|
};
|
|
86
93
|
}
|
|
87
94
|
catch {
|
|
@@ -193,11 +200,11 @@ export function activityEventToRecord(ev) {
|
|
|
193
200
|
hostname: ev.host,
|
|
194
201
|
platform: process.platform,
|
|
195
202
|
arch: process.arch,
|
|
196
|
-
pid: 0,
|
|
203
|
+
pid: ev.pid ?? 0,
|
|
197
204
|
ppid: 0,
|
|
198
205
|
event: ev.event,
|
|
199
206
|
level: 'info',
|
|
200
|
-
caller: 'hook',
|
|
207
|
+
caller: ev.tool === 'feed.post' ? 'agent' : 'hook',
|
|
201
208
|
session: ev.sessionId,
|
|
202
209
|
osUser: ev.agent ?? 'agent',
|
|
203
210
|
transport: 'local',
|
|
@@ -210,6 +217,9 @@ export function activityEventToRecord(ev) {
|
|
|
210
217
|
detail: ev.detail,
|
|
211
218
|
url: ev.url,
|
|
212
219
|
tier: ev.tier,
|
|
220
|
+
...(ev.launchId ? { launchId: ev.launchId } : {}),
|
|
221
|
+
...(ev.terminalId ? { terminalId: ev.terminalId } : {}),
|
|
222
|
+
...(ev.tmuxPane ? { tmuxPane: ev.tmuxPane } : {}),
|
|
213
223
|
};
|
|
214
224
|
}
|
|
215
225
|
/** Read recent activity across sessions as unified {@link EventRecord}s. */
|
|
@@ -230,6 +240,9 @@ export const EVENT_STYLE = {
|
|
|
230
240
|
'pushed': { glyph: '↥', color: chalk.yellow, label: 'pushed' },
|
|
231
241
|
'subagent.spawned': { glyph: '⑂', color: chalk.magenta, label: 'sub-agent spawned' },
|
|
232
242
|
'artifact.created': { glyph: '▤', color: chalk.cyan, label: 'artifact' },
|
|
243
|
+
'task.completed': { glyph: '✓', color: chalk.green, label: 'task completed' },
|
|
244
|
+
'checklist.created': { glyph: '☐', color: chalk.cyan, label: 'checklist created' },
|
|
245
|
+
'status.posted': { glyph: '▸', color: chalk.white, label: 'status' },
|
|
233
246
|
'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
|
|
234
247
|
};
|
|
235
248
|
export function styleForEvent(event) {
|
|
@@ -240,10 +253,13 @@ export function formatActivityLine(ev, opts = {}) {
|
|
|
240
253
|
const s = styleForEvent(ev.event);
|
|
241
254
|
const host = opts.showHost && ev.host && ev.host !== 'unknown' ? chalk.gray(`[${ev.host}] `) : '';
|
|
242
255
|
const label = s.color(`${s.glyph} ${s.label}`);
|
|
243
|
-
|
|
256
|
+
// Status posts are the message; allow a longer snippet than tool-derived detail.
|
|
257
|
+
const detailLimit = ev.event === 'status.posted' ? 100 : 60;
|
|
258
|
+
const detail = ev.detail ? ` ${truncate(ev.detail, detailLimit)}` : '';
|
|
244
259
|
const url = ev.url ? chalk.gray(` ${ev.url}`) : '';
|
|
260
|
+
const agent = ev.event === 'status.posted' && ev.agent ? chalk.gray(` · ${ev.agent}`) : '';
|
|
245
261
|
const when = chalk.gray(relTime(ev.ts).padStart(7));
|
|
246
|
-
return ` ${when} ${host}${label}${detail}${url}`;
|
|
262
|
+
return ` ${when} ${host}${label}${detail}${agent}${url}`;
|
|
247
263
|
}
|
|
248
264
|
// ---------------------------------------------------------------------------
|
|
249
265
|
// Hook installation
|
|
@@ -254,15 +270,17 @@ export function formatActivityLine(ev, opts = {}) {
|
|
|
254
270
|
* line per event to ~/.agents/.history/activity/<sessionId>.jsonl.
|
|
255
271
|
*
|
|
256
272
|
* Matcher-gated to mutating/milestone tools (Bash|Task|ExitPlanMode|Write|
|
|
257
|
-
* Edit|MultiEdit) so
|
|
258
|
-
*
|
|
273
|
+
* Edit|MultiEdit|TodoWrite|update_plan|TaskUpdate|todo_write|TaskCreate) so
|
|
274
|
+
* read-only tools never pay the hook cost. Fail-open: any error is swallowed so
|
|
275
|
+
* a logging hiccup never blocks a tool call.
|
|
259
276
|
*/
|
|
260
277
|
export const ACTIVITY_LOG_HOOK_SCRIPT = `#!/usr/bin/env python3
|
|
261
278
|
"""Append agent-activity events for \`agents feed\` / \`agents activity\`.
|
|
262
279
|
|
|
263
280
|
Bound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,
|
|
264
|
-
MultiEdit
|
|
265
|
-
|
|
281
|
+
MultiEdit, TodoWrite, update_plan, TaskUpdate, todo_write, TaskCreate). One
|
|
282
|
+
append-only file per session; read-only tools never trigger it because the
|
|
283
|
+
manifest matcher excludes them.
|
|
266
284
|
|
|
267
285
|
Sub-agent gate: when the payload carries \`agent_type\`, this is a Task/Agent
|
|
268
286
|
sub-agent -- skip so only the top-level agent logs its own activity.
|
|
@@ -281,7 +299,7 @@ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
|
|
|
281
299
|
MILESTONE_EVENTS = {
|
|
282
300
|
"plan.created", "pr.opened", "pr.merged", "worktree.created",
|
|
283
301
|
"worktree.removed", "commit.created", "pushed", "subagent.spawned",
|
|
284
|
-
"artifact.created",
|
|
302
|
+
"artifact.created", "task.completed", "checklist.created", "status.posted",
|
|
285
303
|
}
|
|
286
304
|
|
|
287
305
|
# Deliverable file types + locations -- a Write here is a recognizable artifact
|
|
@@ -292,6 +310,14 @@ ARTIFACT_EXTS = {
|
|
|
292
310
|
}
|
|
293
311
|
ARTIFACT_DIR_HINTS = ("/tmp/", "/downloads/", "/.agents/artifacts/")
|
|
294
312
|
|
|
313
|
+
# Checklist tools across harnesses. The value is the key that holds the item list.
|
|
314
|
+
CHECKLIST_TOOLS = {
|
|
315
|
+
"TodoWrite": "todos",
|
|
316
|
+
"todo_write": "todos",
|
|
317
|
+
"TaskUpdate": "tasks",
|
|
318
|
+
"update_plan": "plan",
|
|
319
|
+
}
|
|
320
|
+
|
|
295
321
|
|
|
296
322
|
def is_artifact(file_path):
|
|
297
323
|
low = (file_path or "").lower()
|
|
@@ -367,11 +393,418 @@ def extract_url(tool_response):
|
|
|
367
393
|
return m.group(0).rstrip(").,") if m else None
|
|
368
394
|
|
|
369
395
|
|
|
396
|
+
def _checklist_items(tool_input, kind):
|
|
397
|
+
"""Extract normalized checklist items from tool input."""
|
|
398
|
+
if not isinstance(tool_input, dict):
|
|
399
|
+
return []
|
|
400
|
+
if kind == "todos":
|
|
401
|
+
arr = tool_input.get("todos") or []
|
|
402
|
+
elif kind == "tasks":
|
|
403
|
+
arr = tool_input.get("tasks") or []
|
|
404
|
+
if not arr and "taskId" in tool_input:
|
|
405
|
+
arr = [tool_input]
|
|
406
|
+
elif kind == "plan":
|
|
407
|
+
arr = tool_input.get("plan") or []
|
|
408
|
+
else:
|
|
409
|
+
return []
|
|
410
|
+
if not isinstance(arr, list):
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
items = []
|
|
414
|
+
for t in arr:
|
|
415
|
+
if not isinstance(t, dict):
|
|
416
|
+
continue
|
|
417
|
+
subject = (
|
|
418
|
+
t.get("content") or
|
|
419
|
+
t.get("text") or
|
|
420
|
+
t.get("step") or
|
|
421
|
+
t.get("title") or
|
|
422
|
+
t.get("description") or
|
|
423
|
+
t.get("activeForm") or
|
|
424
|
+
""
|
|
425
|
+
)
|
|
426
|
+
if not isinstance(subject, str):
|
|
427
|
+
subject = str(subject)
|
|
428
|
+
subject = subject.strip()
|
|
429
|
+
item_id = t.get("id") or t.get("taskId") or subject
|
|
430
|
+
if not item_id:
|
|
431
|
+
continue
|
|
432
|
+
status = str(t.get("status", "") or "").lower()
|
|
433
|
+
items.append({"id": item_id, "subject": subject, "status": status})
|
|
434
|
+
return items
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _read_transcript_checklists(transcript_path, current_tool, current_items):
|
|
438
|
+
"""Tail-read the session transcript and return the previous checklist state.
|
|
439
|
+
|
|
440
|
+
The most recent checklist entry is assumed to be the current tool call if
|
|
441
|
+
it carries the same ids; skip that one and return the next older checklist
|
|
442
|
+
state (or None if there isn't one).
|
|
443
|
+
"""
|
|
444
|
+
if not transcript_path or not os.path.exists(transcript_path):
|
|
445
|
+
return None
|
|
446
|
+
try:
|
|
447
|
+
size = os.path.getsize(transcript_path)
|
|
448
|
+
start = max(0, size - 512 * 1024)
|
|
449
|
+
with open(transcript_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
450
|
+
f.seek(start)
|
|
451
|
+
if start > 0:
|
|
452
|
+
f.readline() # drop a leading partial line
|
|
453
|
+
lines = f.readlines()
|
|
454
|
+
except Exception:
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
current_ids = {str(item.get("id")) for item in current_items if item.get("id")}
|
|
458
|
+
skipped_current = False
|
|
459
|
+
for line in reversed(lines):
|
|
460
|
+
line = line.strip()
|
|
461
|
+
if not line:
|
|
462
|
+
continue
|
|
463
|
+
try:
|
|
464
|
+
record = json.loads(line)
|
|
465
|
+
except Exception:
|
|
466
|
+
continue
|
|
467
|
+
if not isinstance(record, dict):
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
tool_uses = []
|
|
471
|
+
if isinstance(record.get("tool_use"), list):
|
|
472
|
+
tool_uses = record["tool_use"]
|
|
473
|
+
elif record.get("name"):
|
|
474
|
+
tool_uses = [record]
|
|
475
|
+
elif record.get("tool_name"):
|
|
476
|
+
tool_uses = [record]
|
|
477
|
+
|
|
478
|
+
for tu in reversed(tool_uses):
|
|
479
|
+
if not isinstance(tu, dict):
|
|
480
|
+
continue
|
|
481
|
+
name = tu.get("name") or tu.get("tool_name") or ""
|
|
482
|
+
if name not in CHECKLIST_TOOLS:
|
|
483
|
+
continue
|
|
484
|
+
kind = CHECKLIST_TOOLS[name]
|
|
485
|
+
args = tu.get("input") or tu.get("tool_input") or tu.get("arguments") or {}
|
|
486
|
+
if isinstance(args, str):
|
|
487
|
+
try:
|
|
488
|
+
args = json.loads(args)
|
|
489
|
+
except Exception:
|
|
490
|
+
continue
|
|
491
|
+
items = _checklist_items(args, kind)
|
|
492
|
+
if not items:
|
|
493
|
+
continue
|
|
494
|
+
ids = {str(item.get("id")) for item in items if item.get("id")}
|
|
495
|
+
# Skip the most recent matching checklist entry once; that is the
|
|
496
|
+
# current call already reflected in the transcript.
|
|
497
|
+
if not skipped_current and name == current_tool and ids == current_ids:
|
|
498
|
+
skipped_current = True
|
|
499
|
+
continue
|
|
500
|
+
return items
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _has_previous_task_create(transcript_path, current_tool_use_id=""):
|
|
505
|
+
"""Return True if the transcript contains a TaskCreate before the current one."""
|
|
506
|
+
if not transcript_path or not os.path.exists(transcript_path):
|
|
507
|
+
return False
|
|
508
|
+
try:
|
|
509
|
+
with open(transcript_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
510
|
+
lines = f.readlines()
|
|
511
|
+
except Exception:
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
for line in reversed(lines):
|
|
515
|
+
line = line.strip()
|
|
516
|
+
if not line:
|
|
517
|
+
continue
|
|
518
|
+
try:
|
|
519
|
+
record = json.loads(line)
|
|
520
|
+
except Exception:
|
|
521
|
+
continue
|
|
522
|
+
if not isinstance(record, dict):
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
tool_uses = []
|
|
526
|
+
msg = record.get("message", {})
|
|
527
|
+
if isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
528
|
+
tool_uses = [
|
|
529
|
+
c for c in msg["content"]
|
|
530
|
+
if isinstance(c, dict) and c.get("type") == "tool_use"
|
|
531
|
+
]
|
|
532
|
+
elif isinstance(record.get("tool_use"), list):
|
|
533
|
+
tool_uses = record["tool_use"]
|
|
534
|
+
elif record.get("name"):
|
|
535
|
+
tool_uses = [record]
|
|
536
|
+
|
|
537
|
+
for tu in reversed(tool_uses):
|
|
538
|
+
if not isinstance(tu, dict):
|
|
539
|
+
continue
|
|
540
|
+
if (tu.get("name") or tu.get("tool_name")) == "TaskCreate":
|
|
541
|
+
if tu.get("id") != current_tool_use_id:
|
|
542
|
+
return True
|
|
543
|
+
return False
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _claude_task_state(transcript_path, exclude_task_id=None):
|
|
547
|
+
"""Fold Claude TaskCreate/TaskUpdate calls into a task id -> subject/status map.
|
|
548
|
+
|
|
549
|
+
TaskCreate provides the subject (and id via toolUseResult); TaskUpdate
|
|
550
|
+
provides the status. If exclude_task_id is given, the last TaskUpdate for
|
|
551
|
+
that id is skipped (it is the current call already reflected in the
|
|
552
|
+
transcript).
|
|
553
|
+
"""
|
|
554
|
+
state = {}
|
|
555
|
+
if not transcript_path or not os.path.exists(transcript_path):
|
|
556
|
+
return state
|
|
557
|
+
try:
|
|
558
|
+
with open(transcript_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
559
|
+
lines = f.readlines()
|
|
560
|
+
except Exception:
|
|
561
|
+
return state
|
|
562
|
+
|
|
563
|
+
updates = [] # (task_id, status, line_index)
|
|
564
|
+
for idx, line in enumerate(lines):
|
|
565
|
+
line = line.strip()
|
|
566
|
+
if not line:
|
|
567
|
+
continue
|
|
568
|
+
try:
|
|
569
|
+
record = json.loads(line)
|
|
570
|
+
except Exception:
|
|
571
|
+
continue
|
|
572
|
+
if not isinstance(record, dict):
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
tool_uses = []
|
|
576
|
+
msg = record.get("message", {})
|
|
577
|
+
if isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
578
|
+
tool_uses = [
|
|
579
|
+
c for c in msg["content"]
|
|
580
|
+
if isinstance(c, dict) and c.get("type") == "tool_use"
|
|
581
|
+
]
|
|
582
|
+
elif isinstance(record.get("tool_use"), list):
|
|
583
|
+
tool_uses = record["tool_use"]
|
|
584
|
+
elif record.get("name"):
|
|
585
|
+
tool_uses = [record]
|
|
586
|
+
|
|
587
|
+
for tu in tool_uses:
|
|
588
|
+
if not isinstance(tu, dict):
|
|
589
|
+
continue
|
|
590
|
+
name = tu.get("name") or ""
|
|
591
|
+
args = tu.get("input") or tu.get("tool_input") or {}
|
|
592
|
+
if isinstance(args, str):
|
|
593
|
+
try:
|
|
594
|
+
args = json.loads(args)
|
|
595
|
+
except Exception:
|
|
596
|
+
continue
|
|
597
|
+
if name == "TaskCreate":
|
|
598
|
+
subject = (
|
|
599
|
+
args.get("subject") or
|
|
600
|
+
args.get("description") or
|
|
601
|
+
args.get("title") or
|
|
602
|
+
""
|
|
603
|
+
)
|
|
604
|
+
if subject:
|
|
605
|
+
# Link subject to the tool_use id so the result can map it.
|
|
606
|
+
tool_id = tu.get("id")
|
|
607
|
+
if tool_id:
|
|
608
|
+
state.setdefault("__pending_subject", {})[tool_id] = subject
|
|
609
|
+
|
|
610
|
+
tool_result = record.get("toolUseResult") or {}
|
|
611
|
+
if not isinstance(tool_result, dict):
|
|
612
|
+
continue
|
|
613
|
+
task = tool_result.get("task")
|
|
614
|
+
if isinstance(task, dict):
|
|
615
|
+
task_id = str(task.get("id") or task.get("taskId") or "")
|
|
616
|
+
if task_id:
|
|
617
|
+
state.setdefault(task_id, {})
|
|
618
|
+
if task.get("subject"):
|
|
619
|
+
state[task_id]["subject"] = task["subject"]
|
|
620
|
+
if task.get("status"):
|
|
621
|
+
state[task_id]["status"] = str(task.get("status")).lower()
|
|
622
|
+
# Link any pending subject from the matching tool_use id.
|
|
623
|
+
tool_use_id = record.get("tool_use_id") or ""
|
|
624
|
+
pending = state.get("__pending_subject", {})
|
|
625
|
+
if tool_use_id and tool_use_id in pending:
|
|
626
|
+
state[task_id]["subject"] = pending[tool_use_id]
|
|
627
|
+
task_id = str(tool_result.get("taskId") or "")
|
|
628
|
+
status_change = tool_result.get("statusChange") or {}
|
|
629
|
+
status = status_change.get("to") or tool_result.get("status")
|
|
630
|
+
if task_id and status:
|
|
631
|
+
state.setdefault(task_id, {})
|
|
632
|
+
state[task_id]["status"] = str(status).lower()
|
|
633
|
+
updates.append((task_id, str(status).lower(), idx))
|
|
634
|
+
|
|
635
|
+
if exclude_task_id and updates:
|
|
636
|
+
for i in range(len(updates) - 1, -1, -1):
|
|
637
|
+
if updates[i][0] == exclude_task_id:
|
|
638
|
+
del updates[i]
|
|
639
|
+
break
|
|
640
|
+
# Rebuild statuses from remaining updates.
|
|
641
|
+
for task_id in list(state.keys()):
|
|
642
|
+
if task_id.startswith("__"):
|
|
643
|
+
continue
|
|
644
|
+
if "subject" not in state[task_id]:
|
|
645
|
+
del state[task_id]
|
|
646
|
+
else:
|
|
647
|
+
state[task_id].pop("status", None)
|
|
648
|
+
for task_id, status, _ in updates:
|
|
649
|
+
if task_id in state:
|
|
650
|
+
state[task_id]["status"] = status
|
|
651
|
+
|
|
652
|
+
# Drop tasks removed from the checklist (deleted/cancelled): they are gone
|
|
653
|
+
# from the user-visible list, so they must not count toward the N/M total.
|
|
654
|
+
for task_id in list(state.keys()):
|
|
655
|
+
if task_id.startswith("__"):
|
|
656
|
+
continue
|
|
657
|
+
if state[task_id].get("status") in ("deleted", "cancelled", "canceled", "removed"):
|
|
658
|
+
del state[task_id]
|
|
659
|
+
|
|
660
|
+
# Drop internal bookkeeping.
|
|
661
|
+
state.pop("__pending_subject", None)
|
|
662
|
+
return state
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _checklist_events(payload, hook_event):
|
|
666
|
+
"""Return a list of (event, detail) tuples for checklist tool calls."""
|
|
667
|
+
if hook_event != "PostToolUse":
|
|
668
|
+
return []
|
|
669
|
+
tool_name = payload.get("tool_name", "")
|
|
670
|
+
tool_input = payload.get("tool_input", {}) or {}
|
|
671
|
+
|
|
672
|
+
if tool_name == "TaskCreate":
|
|
673
|
+
subject = (
|
|
674
|
+
tool_input.get("subject") or
|
|
675
|
+
tool_input.get("description") or
|
|
676
|
+
tool_input.get("title") or
|
|
677
|
+
"task"
|
|
678
|
+
)
|
|
679
|
+
# Only announce the checklist on the very first task creation.
|
|
680
|
+
current_tool_use_id = payload.get("tool_use_id", "")
|
|
681
|
+
transcript_path = payload.get("transcript_path")
|
|
682
|
+
if _has_previous_task_create(transcript_path, current_tool_use_id):
|
|
683
|
+
return []
|
|
684
|
+
return [("checklist.created", subject)]
|
|
685
|
+
|
|
686
|
+
if tool_name not in CHECKLIST_TOOLS:
|
|
687
|
+
return []
|
|
688
|
+
|
|
689
|
+
kind = CHECKLIST_TOOLS[tool_name]
|
|
690
|
+
items = _checklist_items(tool_input, kind)
|
|
691
|
+
if not items:
|
|
692
|
+
return []
|
|
693
|
+
|
|
694
|
+
# Full-list tools (TodoWrite, update_plan) send the whole checklist every
|
|
695
|
+
# time. Per-task update tools (TaskUpdate) send only the changed item, so
|
|
696
|
+
# totals and subject must be resolved from the previous full-list state.
|
|
697
|
+
list_key = {"todos": "todos", "tasks": "tasks", "plan": "plan"}.get(kind)
|
|
698
|
+
is_full_list = list_key is not None and list_key in tool_input
|
|
699
|
+
|
|
700
|
+
previous = _read_transcript_checklists(
|
|
701
|
+
payload.get("transcript_path"), tool_name, items
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
if not is_full_list and previous:
|
|
705
|
+
total = len(previous)
|
|
706
|
+
previous_done = {
|
|
707
|
+
str(i.get("id")) for i in previous
|
|
708
|
+
if i.get("status") == "completed" and i.get("id")
|
|
709
|
+
}
|
|
710
|
+
done_count = len(previous_done)
|
|
711
|
+
events = []
|
|
712
|
+
for item in items:
|
|
713
|
+
if item.get("status") != "completed":
|
|
714
|
+
continue
|
|
715
|
+
item_id = str(item.get("id"))
|
|
716
|
+
if item_id in previous_done:
|
|
717
|
+
continue
|
|
718
|
+
subject = item.get("subject")
|
|
719
|
+
if not subject:
|
|
720
|
+
for p in previous:
|
|
721
|
+
if str(p.get("id")) == item_id and p.get("subject"):
|
|
722
|
+
subject = p["subject"]
|
|
723
|
+
break
|
|
724
|
+
if not subject:
|
|
725
|
+
subject = "task"
|
|
726
|
+
done_count += 1
|
|
727
|
+
events.append(("task.completed", f"{subject} {done_count}/{total} done"))
|
|
728
|
+
return events
|
|
729
|
+
|
|
730
|
+
# For TaskUpdate without a previous full-list state, fold the Claude
|
|
731
|
+
# TaskCreate/TaskUpdate history to resolve subject and N/M.
|
|
732
|
+
if tool_name == "TaskUpdate" and previous is None:
|
|
733
|
+
task_id = str(items[0].get("id")) if items else ""
|
|
734
|
+
task_state = _claude_task_state(
|
|
735
|
+
payload.get("transcript_path"), exclude_task_id=task_id
|
|
736
|
+
)
|
|
737
|
+
if task_state:
|
|
738
|
+
total = len(task_state)
|
|
739
|
+
previous_done = {
|
|
740
|
+
tid for tid, info in task_state.items()
|
|
741
|
+
if info.get("status") == "completed"
|
|
742
|
+
}
|
|
743
|
+
done_count = len(previous_done)
|
|
744
|
+
if task_id in task_state and items[0].get("status") == "completed" and task_id not in previous_done:
|
|
745
|
+
subject = task_state[task_id].get("subject") or "task"
|
|
746
|
+
return [("task.completed", f"{subject} {done_count + 1}/{total} done")]
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
total = len(items)
|
|
750
|
+
completed = [i for i in items if i.get("status") == "completed"]
|
|
751
|
+
|
|
752
|
+
if previous is None:
|
|
753
|
+
# First checklist call in this session.
|
|
754
|
+
events = []
|
|
755
|
+
events.append(("checklist.created", f"{total} task{'s' if total != 1 else ''}"))
|
|
756
|
+
for item in completed:
|
|
757
|
+
events.append((
|
|
758
|
+
"task.completed",
|
|
759
|
+
f"{item['subject']} {len(completed)}/{total} done",
|
|
760
|
+
))
|
|
761
|
+
return events
|
|
762
|
+
|
|
763
|
+
previous_done = {
|
|
764
|
+
str(i.get("id")) for i in previous
|
|
765
|
+
if i.get("status") == "completed" and i.get("id")
|
|
766
|
+
}
|
|
767
|
+
newly_completed = [
|
|
768
|
+
i for i in completed
|
|
769
|
+
if str(i.get("id")) not in previous_done
|
|
770
|
+
]
|
|
771
|
+
if not newly_completed:
|
|
772
|
+
return []
|
|
773
|
+
|
|
774
|
+
done_count = len(completed)
|
|
775
|
+
events = []
|
|
776
|
+
for item in newly_completed:
|
|
777
|
+
events.append((
|
|
778
|
+
"task.completed",
|
|
779
|
+
f"{item['subject']} {done_count}/{total} done",
|
|
780
|
+
))
|
|
781
|
+
return events
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _make_record(event, detail, tool_name):
|
|
785
|
+
tier = "milestone" if event in MILESTONE_EVENTS else "activity"
|
|
786
|
+
record = {
|
|
787
|
+
"v": 1,
|
|
788
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
789
|
+
"event": event,
|
|
790
|
+
"tier": tier,
|
|
791
|
+
}
|
|
792
|
+
if detail:
|
|
793
|
+
record["detail"] = detail
|
|
794
|
+
record["tool"] = tool_name
|
|
795
|
+
return record
|
|
796
|
+
|
|
797
|
+
|
|
370
798
|
def build_event(payload, hook_event):
|
|
371
799
|
tool_name = payload.get("tool_name", "")
|
|
372
800
|
tool_input = payload.get("tool_input", {}) or {}
|
|
373
801
|
tool_response = payload.get("tool_response", {})
|
|
374
802
|
|
|
803
|
+
# Checklist completions first -- cheap guard above already filtered tool name.
|
|
804
|
+
checklist = _checklist_events(payload, hook_event)
|
|
805
|
+
if checklist:
|
|
806
|
+
return [_make_record(event, detail, tool_name) for event, detail in checklist], tool_name
|
|
807
|
+
|
|
375
808
|
event = None
|
|
376
809
|
detail = None
|
|
377
810
|
url = None
|
|
@@ -402,21 +835,12 @@ def build_event(payload, hook_event):
|
|
|
402
835
|
detail = os.path.basename(fp) if fp else tool_name
|
|
403
836
|
|
|
404
837
|
if not event:
|
|
405
|
-
return
|
|
838
|
+
return [], tool_name
|
|
406
839
|
|
|
407
|
-
|
|
408
|
-
record = {
|
|
409
|
-
"v": 1,
|
|
410
|
-
"ts": datetime.now(timezone.utc).isoformat(),
|
|
411
|
-
"event": event,
|
|
412
|
-
"tier": tier,
|
|
413
|
-
}
|
|
414
|
-
if detail:
|
|
415
|
-
record["detail"] = detail
|
|
840
|
+
record = _make_record(event, detail, tool_name)
|
|
416
841
|
if url:
|
|
417
842
|
record["url"] = url
|
|
418
|
-
record
|
|
419
|
-
return record, tool_name
|
|
843
|
+
return [record], tool_name
|
|
420
844
|
|
|
421
845
|
|
|
422
846
|
def main():
|
|
@@ -435,8 +859,8 @@ def main():
|
|
|
435
859
|
return
|
|
436
860
|
|
|
437
861
|
hook_event = payload.get("hook_event_name", "")
|
|
438
|
-
|
|
439
|
-
if not
|
|
862
|
+
records, tool_name = build_event(payload, hook_event)
|
|
863
|
+
if not records:
|
|
440
864
|
return
|
|
441
865
|
|
|
442
866
|
safe_session = re.sub(r"[^A-Za-z0-9._-]", "-", session_id) or "unknown"
|
|
@@ -451,27 +875,29 @@ def main():
|
|
|
451
875
|
hostname = os.environ.get("AGENTS_SYNC_MACHINE_ID") or socket.gethostname()
|
|
452
876
|
host = re.sub(r"[^a-z0-9_-]", "-", hostname.split(".")[0].strip().lower()) or "unknown"
|
|
453
877
|
|
|
454
|
-
record
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
878
|
+
for record in records:
|
|
879
|
+
record["sessionId"] = session_id
|
|
880
|
+
record["mailboxId"] = mailbox_id
|
|
881
|
+
record["host"] = host
|
|
882
|
+
record["runtime"] = os.environ.get("AGENTS_RUNTIME", "headless")
|
|
883
|
+
cwd = payload.get("cwd") or os.environ.get("AGENTS_CWD")
|
|
884
|
+
if cwd:
|
|
885
|
+
record["cwd"] = cwd
|
|
886
|
+
agent = os.environ.get("AGENTS_AGENT_NAME") or "claude"
|
|
887
|
+
record["agent"] = agent
|
|
463
888
|
|
|
464
889
|
try:
|
|
465
890
|
os.makedirs(activity_dir, exist_ok=True)
|
|
466
891
|
# Cap growth: once over the size limit, keep only milestones.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
except OSError:
|
|
472
|
-
pass
|
|
892
|
+
try:
|
|
893
|
+
over_limit = os.path.getsize(target) > MAX_LOG_BYTES
|
|
894
|
+
except OSError:
|
|
895
|
+
over_limit = False
|
|
473
896
|
with open(target, "a") as f:
|
|
474
|
-
|
|
897
|
+
for record in records:
|
|
898
|
+
if over_limit and record.get("tier") != "milestone":
|
|
899
|
+
continue
|
|
900
|
+
f.write(json.dumps(record) + "\\n")
|
|
475
901
|
except Exception:
|
|
476
902
|
pass # fail open
|
|
477
903
|
|
|
@@ -494,7 +920,7 @@ export const ACTIVITY_HOOK_DEFINITIONS = {
|
|
|
494
920
|
'activity-log-result': {
|
|
495
921
|
agents: ['claude'],
|
|
496
922
|
events: ['PostToolUse'],
|
|
497
|
-
matcher: 'Bash|Write|Edit|MultiEdit',
|
|
923
|
+
matcher: 'Bash|Write|Edit|MultiEdit|TodoWrite|update_plan|TaskUpdate|todo_write|TaskCreate',
|
|
498
924
|
script: '11-activity-log.py',
|
|
499
925
|
timeout: 5,
|
|
500
926
|
},
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ActorConfig } from './types.js';
|
|
2
|
+
export type ActorKind = 'human' | 'agent';
|
|
3
|
+
export interface ResolvedActor {
|
|
4
|
+
/**
|
|
5
|
+
* Stable id for the responsible entity: the tailnet login (usually an email)
|
|
6
|
+
* for a resolved human, or `UNRESOLVED@<host>` when it can't be determined.
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
kind: ActorKind;
|
|
10
|
+
/** Human-readable name, for git author + display. */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Email, for git author + as a durable key. */
|
|
13
|
+
email?: string;
|
|
14
|
+
/** GitHub handle, when the actors map records one. */
|
|
15
|
+
github?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Result of `tailscale whois --json <ip>` we care about. */
|
|
18
|
+
export interface WhoisIdentity {
|
|
19
|
+
login?: string;
|
|
20
|
+
displayName?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
|
|
24
|
+
* the impure whois/config reads happen in computeActor -- so the enrich/override
|
|
25
|
+
* path is fully testable. No login (local, or an unnameable peer) yields the
|
|
26
|
+
* honest `UNRESOLVED@<host>`; a login without a config entry still credits git
|
|
27
|
+
* from the tailnet DisplayName + login email.
|
|
28
|
+
*/
|
|
29
|
+
export declare function actorFromIdentity(who: WhoisIdentity | undefined, host: string, actors: Record<string, ActorConfig>): ResolvedActor;
|
|
30
|
+
/**
|
|
31
|
+
* Compute the actor for a given environment. Pure with respect to `env` (the
|
|
32
|
+
* only impurity is the `tailscale whois` / config read on the fresh-SSH path),
|
|
33
|
+
* so tests can drive every branch by passing an env explicitly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeActor(env?: NodeJS.ProcessEnv): ResolvedActor;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the actor for the current process, cached for the process lifetime
|
|
38
|
+
* (the SSH `whois` shell-out runs at most once).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveActor(): ResolvedActor;
|
|
41
|
+
/** Clear the per-process cache. For tests, and for env changes within a run. */
|
|
42
|
+
export declare function resetActorCache(): void;
|
|
43
|
+
/**
|
|
44
|
+
* The env an actor propagates to child processes. Always carries the actor id +
|
|
45
|
+
* kind (so children inherit and don't re-resolve). For a resolved human with a
|
|
46
|
+
* real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
|
|
47
|
+
* agent's own commits are credited to the person, not the shared account. An
|
|
48
|
+
* unresolved actor sets no git identity -- git keeps its ambient config.
|
|
49
|
+
*/
|
|
50
|
+
export declare function actorEnv(actor: ResolvedActor): Record<string, string>;
|