harnery 0.32.0 → 0.33.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/commander.d.ts.map +1 -1
- package/dist/commander.js +3 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +212 -1
- package/dist/commands/claude-desktop.d.ts +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +74 -3
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +45 -6
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -0
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +4 -1
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +5 -3
- package/dist/core/agents/cli.js +5 -1
- package/dist/core/agents/coord-client.d.ts +11 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/render/prompt-context.d.ts +6 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +46 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +8 -2
- package/dist/core/agents/rules/stop-hook.js +4 -4
- package/dist/core/agents/state/heartbeat-projector.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +54 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +15 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +32 -0
- package/dist/core/agents/state/session-state.d.ts +44 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +81 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +1 -0
- package/dist/core/hooks/adapter/parse.d.ts +1 -1
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +2 -0
- package/dist/core/hooks/cli.js +90 -6
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/events/schema.d.ts +30 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/presence/blob.d.ts +4 -0
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +12 -0
- package/dist/core/presence/index.js +15 -1
- package/dist/lib/claude-desktop.d.ts +47 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +169 -1
- package/dist/lib/instructions/bundle.d.ts +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- package/package.json +2 -2
- package/src/commander.ts +3 -1
- package/src/commands/agents.ts +248 -1
- package/src/commands/claude-desktop.ts +106 -5
- package/src/commands/doctor.ts +59 -7
- package/src/commands/init.ts +11 -0
- package/src/commands/instructions.ts +129 -0
- package/src/commands/presence.ts +7 -1
- package/src/core/agents/canonical-emit.ts +6 -4
- package/src/core/agents/cli.ts +6 -1
- package/src/core/agents/coord-client.ts +11 -0
- package/src/core/agents/index.ts +1 -0
- package/src/core/agents/render/prompt-context.ts +71 -6
- package/src/core/agents/render/session-context.ts +14 -2
- package/src/core/agents/rules/stop-hook.ts +5 -5
- package/src/core/agents/state/heartbeat-projector.ts +63 -0
- package/src/core/agents/state/heartbeat-writer.ts +51 -0
- package/src/core/agents/state/session-state.ts +131 -0
- package/src/core/hooks/adapter/events.ts +1 -0
- package/src/core/hooks/adapter/parse.ts +3 -0
- package/src/core/hooks/cli.ts +98 -7
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/events/schema.ts +44 -0
- package/src/core/presence/blob.ts +18 -0
- package/src/core/presence/index.ts +18 -1
- package/src/lib/claude-desktop.ts +232 -1
- package/src/lib/instructions/bundle.ts +199 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Adapter } from "../../core/hooks/events/schema.js";
|
|
2
|
+
export type InstructionComponentRole = "always_loaded" | "just_in_time" | "runtime_config";
|
|
3
|
+
export interface InstructionBundleComponent {
|
|
4
|
+
path: string;
|
|
5
|
+
role: InstructionComponentRole;
|
|
6
|
+
bytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}
|
|
9
|
+
export interface InstructionBundle {
|
|
10
|
+
schema_version: 1;
|
|
11
|
+
adapter: Adapter;
|
|
12
|
+
coord_root: string;
|
|
13
|
+
profile_root: string;
|
|
14
|
+
instruction_roots: string[];
|
|
15
|
+
instruction_bundle_id: string;
|
|
16
|
+
canonical_source_id: string;
|
|
17
|
+
components: InstructionBundleComponent[];
|
|
18
|
+
canonical_sources: InstructionBundleComponent[];
|
|
19
|
+
}
|
|
20
|
+
interface BuildBundleOptions {
|
|
21
|
+
coordRoot: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
adapter: Adapter;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the content identity for the repo-authored instruction surface an
|
|
27
|
+
* adapter can load at one working path. The identity intentionally excludes
|
|
28
|
+
* timestamps and git revisions: two checkouts with byte-identical effective
|
|
29
|
+
* instructions belong to the same cohort.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildInstructionBundle(options: BuildBundleOptions): InstructionBundle;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/bundle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mCAAmC,CAAC;AAEjE,MAAM,MAAM,wBAAwB,GAAG,eAAe,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE3F,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,wBAAwB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,CAAC,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,0BAA0B,EAAE,CAAC;IACzC,iBAAiB,EAAE,0BAA0B,EAAE,CAAC;CACjD;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;CAClB;AAOD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,CAyBrF"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Build the content identity for the repo-authored instruction surface an
|
|
6
|
+
* adapter can load at one working path. The identity intentionally excludes
|
|
7
|
+
* timestamps and git revisions: two checkouts with byte-identical effective
|
|
8
|
+
* instructions belong to the same cohort.
|
|
9
|
+
*/
|
|
10
|
+
export function buildInstructionBundle(options) {
|
|
11
|
+
const coordRoot = resolve(options.coordRoot);
|
|
12
|
+
const cwd = resolve(options.cwd);
|
|
13
|
+
const roots = resolveInstructionRoots(coordRoot, cwd);
|
|
14
|
+
const profileRoot = roots.at(-1) ?? coordRoot;
|
|
15
|
+
const components = dedupeCandidates(roots.flatMap((root) => adapterCandidates(root, options.adapter)), coordRoot);
|
|
16
|
+
const canonicalSources = dedupeCandidates(roots.flatMap((root) => canonicalCandidates(root)), coordRoot);
|
|
17
|
+
return {
|
|
18
|
+
schema_version: 1,
|
|
19
|
+
adapter: options.adapter,
|
|
20
|
+
coord_root: coordRoot,
|
|
21
|
+
profile_root: relativePath(coordRoot, profileRoot),
|
|
22
|
+
instruction_roots: roots.map((root) => relativePath(coordRoot, root)),
|
|
23
|
+
instruction_bundle_id: identity("instruction-bundle-v1", options.adapter, components),
|
|
24
|
+
canonical_source_id: identity("instruction-source-v1", null, canonicalSources),
|
|
25
|
+
components,
|
|
26
|
+
canonical_sources: canonicalSources,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveInstructionRoots(coordRoot, cwd) {
|
|
30
|
+
const roots = [coordRoot];
|
|
31
|
+
if (!isWithin(coordRoot, cwd))
|
|
32
|
+
return roots;
|
|
33
|
+
const ancestors = [];
|
|
34
|
+
let cursor = cwd;
|
|
35
|
+
while (cursor !== coordRoot) {
|
|
36
|
+
if (existsSync(join(cursor, "AGENTS.md")) || existsSync(join(cursor, "CLAUDE.md"))) {
|
|
37
|
+
ancestors.push(cursor);
|
|
38
|
+
}
|
|
39
|
+
const parent = dirname(cursor);
|
|
40
|
+
if (parent === cursor)
|
|
41
|
+
break;
|
|
42
|
+
cursor = parent;
|
|
43
|
+
}
|
|
44
|
+
roots.push(...ancestors.reverse().filter((root) => root !== coordRoot));
|
|
45
|
+
return roots;
|
|
46
|
+
}
|
|
47
|
+
function adapterCandidates(root, adapter) {
|
|
48
|
+
const candidates = [];
|
|
49
|
+
const addFile = (path, role) => {
|
|
50
|
+
if (existsSync(path) && lstatSync(path).isFile())
|
|
51
|
+
candidates.push({ absolutePath: path, role });
|
|
52
|
+
};
|
|
53
|
+
const addTree = (path, role) => {
|
|
54
|
+
for (const file of walkFiles(path))
|
|
55
|
+
candidates.push({ absolutePath: file, role });
|
|
56
|
+
};
|
|
57
|
+
if (adapter === "claude-code") {
|
|
58
|
+
addFile(join(root, "CLAUDE.md"), "always_loaded");
|
|
59
|
+
addTree(join(root, ".claude", "rules"), "always_loaded");
|
|
60
|
+
addTree(join(root, ".claude", "skills"), "just_in_time");
|
|
61
|
+
addFile(join(root, ".claude", "settings.json"), "runtime_config");
|
|
62
|
+
addFile(join(root, ".claude", "settings.local.json"), "runtime_config");
|
|
63
|
+
}
|
|
64
|
+
else if (adapter === "cursor") {
|
|
65
|
+
addFile(join(root, "AGENTS.md"), "always_loaded");
|
|
66
|
+
addTree(join(root, ".cursor", "rules"), "always_loaded");
|
|
67
|
+
addTree(join(root, ".agents", "skills"), "just_in_time");
|
|
68
|
+
addTree(join(root, ".cursor", "skills"), "just_in_time");
|
|
69
|
+
addFile(join(root, ".cursor", "hooks.json"), "runtime_config");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
addFile(join(root, "AGENTS.md"), "always_loaded");
|
|
73
|
+
addTree(join(root, ".harnery", "skills"), "just_in_time");
|
|
74
|
+
addFile(join(root, ".codex", "hooks.json"), "runtime_config");
|
|
75
|
+
}
|
|
76
|
+
// Harnery's host configuration governs hook enforcement for all adapters.
|
|
77
|
+
addFile(join(root, ".harnery", "config.jsonc"), "runtime_config");
|
|
78
|
+
return candidates;
|
|
79
|
+
}
|
|
80
|
+
function canonicalCandidates(root) {
|
|
81
|
+
const candidates = [];
|
|
82
|
+
const agents = join(root, "AGENTS.md");
|
|
83
|
+
if (existsSync(agents) && lstatSync(agents).isFile()) {
|
|
84
|
+
candidates.push({ absolutePath: agents, role: "always_loaded" });
|
|
85
|
+
}
|
|
86
|
+
for (const file of walkFiles(join(root, ".agents"))) {
|
|
87
|
+
candidates.push({ absolutePath: file, role: "just_in_time" });
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
function walkFiles(root) {
|
|
92
|
+
if (!existsSync(root) || !lstatSync(root).isDirectory())
|
|
93
|
+
return [];
|
|
94
|
+
const files = [];
|
|
95
|
+
const visit = (dir) => {
|
|
96
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
97
|
+
const path = join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory())
|
|
99
|
+
visit(path);
|
|
100
|
+
else if (entry.isFile())
|
|
101
|
+
files.push(path);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
visit(root);
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
function dedupeCandidates(candidates, coordRoot) {
|
|
108
|
+
const byPath = new Map();
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
const key = resolve(candidate.absolutePath);
|
|
111
|
+
const prior = byPath.get(key);
|
|
112
|
+
if (!prior || roleRank(candidate.role) < roleRank(prior.role))
|
|
113
|
+
byPath.set(key, candidate);
|
|
114
|
+
}
|
|
115
|
+
return [...byPath.values()]
|
|
116
|
+
.map(({ absolutePath, role }) => {
|
|
117
|
+
const bytes = readFileSync(absolutePath);
|
|
118
|
+
return {
|
|
119
|
+
path: relativePath(coordRoot, absolutePath),
|
|
120
|
+
role,
|
|
121
|
+
bytes: bytes.byteLength,
|
|
122
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.role.localeCompare(b.role));
|
|
126
|
+
}
|
|
127
|
+
function identity(prefix, adapter, components) {
|
|
128
|
+
const body = components.map(({ path, role, sha256 }) => ({ path, role, sha256 }));
|
|
129
|
+
return `sha256:${createHash("sha256")
|
|
130
|
+
.update(JSON.stringify({ prefix, ...(adapter ? { adapter } : {}), components: body }))
|
|
131
|
+
.digest("hex")}`;
|
|
132
|
+
}
|
|
133
|
+
function relativePath(root, path) {
|
|
134
|
+
const rel = relative(root, path);
|
|
135
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
136
|
+
}
|
|
137
|
+
function isWithin(root, path) {
|
|
138
|
+
if (!isAbsolute(root) || !isAbsolute(path))
|
|
139
|
+
return false;
|
|
140
|
+
const rel = relative(root, path);
|
|
141
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== "..");
|
|
142
|
+
}
|
|
143
|
+
function roleRank(role) {
|
|
144
|
+
if (role === "always_loaded")
|
|
145
|
+
return 0;
|
|
146
|
+
if (role === "runtime_config")
|
|
147
|
+
return 1;
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"check:layering": "bun run scripts/check-layering.ts",
|
|
186
186
|
"lint:fix": "bun x @biomejs/biome check --write src",
|
|
187
187
|
"format": "bun x @biomejs/biome format --write src",
|
|
188
|
-
"test": "bun test
|
|
188
|
+
"test": "bun run scripts/test.mjs",
|
|
189
189
|
"test:web": "cd web && bun install && bun test",
|
|
190
190
|
"test:integration": "bash tests/integration/run.sh",
|
|
191
191
|
"smoke": "node scripts/smoke-test.mjs",
|
package/src/commander.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { registerFileHistoryCommand } from "./commands/file-history.ts";
|
|
|
44
44
|
import { registerGovernorCommand } from "./commands/governor.ts";
|
|
45
45
|
import { registerGrepCommand } from "./commands/grep.ts";
|
|
46
46
|
import { registerInitCommand } from "./commands/init.ts";
|
|
47
|
+
import { registerInstructionsCommand } from "./commands/instructions.ts";
|
|
47
48
|
import { registerJournalCommand } from "./commands/journal.ts";
|
|
48
49
|
import { registerOutlineCommand } from "./commands/outline.ts";
|
|
49
50
|
import { registerPolicyCommand } from "./commands/policy.ts";
|
|
@@ -294,10 +295,11 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
|
|
|
294
295
|
registerAgentsCommand(program, emit, opts.context);
|
|
295
296
|
registerCouncilCommands(program);
|
|
296
297
|
registerDoctorCommand(program, emit);
|
|
298
|
+
registerInstructionsCommand(program, emit);
|
|
297
299
|
registerInitCommand(program, emit, opts.binName);
|
|
298
300
|
registerDeinitCommand(program, emit, opts.binName);
|
|
299
301
|
registerBackupCommand(program, emit);
|
|
300
|
-
registerClaudeDesktopCommand(program, emit);
|
|
302
|
+
registerClaudeDesktopCommand(program, emit, opts.context);
|
|
301
303
|
registerSyncCommand(program, emit);
|
|
302
304
|
registerWorkflowCommand(program, emit);
|
|
303
305
|
registerWorkCommand(program, emit);
|
package/src/commands/agents.ts
CHANGED
|
@@ -46,7 +46,15 @@ import {
|
|
|
46
46
|
resolveOwnerBySessionEnv,
|
|
47
47
|
resolveOwnerWithSource,
|
|
48
48
|
} from "../core/agents/index.ts";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
buildLifecycleSuggestedName,
|
|
51
|
+
buildSuggestedName,
|
|
52
|
+
} from "../core/agents/state/heartbeat-writer.ts";
|
|
53
|
+
import {
|
|
54
|
+
type AgentActivity,
|
|
55
|
+
foldSessionState,
|
|
56
|
+
type TaskState,
|
|
57
|
+
} from "../core/agents/state/session-state.ts";
|
|
50
58
|
import { coordFreshnessSeconds, resolveBinName } from "../core/config.ts";
|
|
51
59
|
import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
|
|
52
60
|
import { registerContextCommand } from "./context.ts";
|
|
@@ -182,6 +190,12 @@ interface Row {
|
|
|
182
190
|
last_tool?: string | null;
|
|
183
191
|
last_tool_target?: string | null;
|
|
184
192
|
task?: string | null;
|
|
193
|
+
activity: AgentActivity;
|
|
194
|
+
activity_updated_at?: string | null;
|
|
195
|
+
activity_source?: string | null;
|
|
196
|
+
task_state: TaskState;
|
|
197
|
+
task_state_updated_at?: string | null;
|
|
198
|
+
task_state_reason?: string | null;
|
|
185
199
|
turn_summary?: string | null;
|
|
186
200
|
turn_summary_updated_at?: string | null;
|
|
187
201
|
platform?: string | null;
|
|
@@ -190,6 +204,19 @@ interface Row {
|
|
|
190
204
|
machine?: string | null;
|
|
191
205
|
}
|
|
192
206
|
|
|
207
|
+
function activityOf(hb: Pick<Heartbeat, "activity">): AgentActivity {
|
|
208
|
+
return hb.activity ?? "unknown";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function taskStateOf(hb: Pick<Heartbeat, "task_state">): TaskState {
|
|
212
|
+
return hb.task_state ?? "active";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function lifecycleLabel(hb: Pick<Heartbeat, "task_state" | "task_state_reason">): string {
|
|
216
|
+
const state = taskStateOf(hb);
|
|
217
|
+
return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
|
|
218
|
+
}
|
|
219
|
+
|
|
193
220
|
let emit: EmitContext;
|
|
194
221
|
|
|
195
222
|
export function registerAgentsCommand(
|
|
@@ -317,6 +344,18 @@ export function registerAgentsCommand(
|
|
|
317
344
|
runSetTask(text.join(" "), opts);
|
|
318
345
|
});
|
|
319
346
|
|
|
347
|
+
cmd
|
|
348
|
+
.command("lifecycle <state>")
|
|
349
|
+
.description("Declare this session's task lifecycle: active, blocked, or done")
|
|
350
|
+
.option("--reason <text>", "Explain the lifecycle declaration")
|
|
351
|
+
.option(
|
|
352
|
+
"--session-id <id>",
|
|
353
|
+
"Target the heartbeat with this session_id directly, bypassing the ppid walk.",
|
|
354
|
+
)
|
|
355
|
+
.action((state: string, opts: { reason?: string; sessionId?: string }) => {
|
|
356
|
+
runLifecycle(state, opts);
|
|
357
|
+
});
|
|
358
|
+
|
|
320
359
|
cmd
|
|
321
360
|
.command("release-claim <path>")
|
|
322
361
|
.description(
|
|
@@ -901,6 +940,12 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
901
940
|
last_tool: hb.last_tool ?? null,
|
|
902
941
|
last_tool_target: hb.last_tool_target ?? null,
|
|
903
942
|
task: hb.task ?? null,
|
|
943
|
+
activity: activityOf(hb),
|
|
944
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
945
|
+
activity_source: hb.activity_source ?? null,
|
|
946
|
+
task_state: taskStateOf(hb),
|
|
947
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
948
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
904
949
|
turn_summary: hb.turn_summary ?? null,
|
|
905
950
|
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
906
951
|
platform: hb.platform ?? "claude-code",
|
|
@@ -999,6 +1044,12 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
999
1044
|
last_tool: h.last_tool ?? null,
|
|
1000
1045
|
last_tool_target: h.last_tool_target ?? null,
|
|
1001
1046
|
task: h.task ?? null,
|
|
1047
|
+
activity: activityOf(h),
|
|
1048
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
1049
|
+
activity_source: h.activity_source ?? null,
|
|
1050
|
+
task_state: taskStateOf(h),
|
|
1051
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1052
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
1002
1053
|
turn_summary: h.turn_summary ?? null,
|
|
1003
1054
|
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
1004
1055
|
platform: h.platform ?? "claude-code",
|
|
@@ -1024,6 +1075,12 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1024
1075
|
last_tool: h.last_tool ?? null,
|
|
1025
1076
|
last_tool_target: h.last_tool_target ?? null,
|
|
1026
1077
|
task: h.task ?? null,
|
|
1078
|
+
activity: activityOf(h),
|
|
1079
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
1080
|
+
activity_source: h.activity_source ?? null,
|
|
1081
|
+
task_state: taskStateOf(h),
|
|
1082
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1083
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
1027
1084
|
turn_summary: h.turn_summary ?? null,
|
|
1028
1085
|
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
1029
1086
|
platform: h.platform ?? "claude-code",
|
|
@@ -1052,6 +1109,12 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1052
1109
|
last_tool: a.last_tool ?? null,
|
|
1053
1110
|
last_tool_target: null,
|
|
1054
1111
|
task: a.task ?? null,
|
|
1112
|
+
activity: a.activity ?? "unknown",
|
|
1113
|
+
activity_updated_at: null,
|
|
1114
|
+
activity_source: null,
|
|
1115
|
+
task_state: a.task_state ?? "active",
|
|
1116
|
+
task_state_updated_at: null,
|
|
1117
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
1055
1118
|
turn_summary: a.turn_summary ?? null,
|
|
1056
1119
|
turn_summary_updated_at: null,
|
|
1057
1120
|
platform: a.platform ?? "claude-code",
|
|
@@ -1319,6 +1382,12 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1319
1382
|
age_secs: ageSecs,
|
|
1320
1383
|
last_heartbeat_secs_ago: heartbeatAgeSecs,
|
|
1321
1384
|
task: hb.task ?? null,
|
|
1385
|
+
activity: activityOf(hb),
|
|
1386
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1387
|
+
activity_source: hb.activity_source ?? null,
|
|
1388
|
+
task_state: taskStateOf(hb),
|
|
1389
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1390
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
1322
1391
|
turn_summary: hb.turn_summary ?? null,
|
|
1323
1392
|
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
1324
1393
|
title: report?.title ?? null,
|
|
@@ -1350,6 +1419,10 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1350
1419
|
` session ${formatAge(ageSecs)} old · kind=${data.kind} · session_id=${data.session_id.slice(0, 8)}…`,
|
|
1351
1420
|
);
|
|
1352
1421
|
lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
|
|
1422
|
+
lines.push(` activity: ${data.activity}`);
|
|
1423
|
+
lines.push(
|
|
1424
|
+
` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`,
|
|
1425
|
+
);
|
|
1353
1426
|
if (data.last_tool) {
|
|
1354
1427
|
const target = data.last_tool_target ? ` ${truncate(data.last_tool_target, 80)}` : "";
|
|
1355
1428
|
lines.push(` last activity: ${data.last_tool}${target}`);
|
|
@@ -1526,6 +1599,11 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1526
1599
|
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1527
1600
|
// stamp appears across the mutation.
|
|
1528
1601
|
const priorHb = readHeartbeat(myOwner);
|
|
1602
|
+
const normalizedTask = task.length > 0 ? task : undefined;
|
|
1603
|
+
const lifecycleWarning =
|
|
1604
|
+
priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
|
|
1605
|
+
? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
|
|
1606
|
+
: null;
|
|
1529
1607
|
|
|
1530
1608
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
1531
1609
|
const helper = agentCoordOrExit(root);
|
|
@@ -1573,6 +1651,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1573
1651
|
cleared: !task || task.length === 0,
|
|
1574
1652
|
first_of_session: firstOfSession,
|
|
1575
1653
|
suggested_session_name: suggestedName,
|
|
1654
|
+
...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
|
|
1576
1655
|
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1577
1656
|
// exists; this result is the moment the model holds the string.
|
|
1578
1657
|
...(suggestedName
|
|
@@ -1583,6 +1662,147 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1583
1662
|
});
|
|
1584
1663
|
}
|
|
1585
1664
|
|
|
1665
|
+
function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: string }): void {
|
|
1666
|
+
const state = rawState.trim().toLowerCase();
|
|
1667
|
+
if (state !== "active" && state !== "blocked" && state !== "done") {
|
|
1668
|
+
emit.error({
|
|
1669
|
+
code: "invalid_lifecycle_state",
|
|
1670
|
+
message: "lifecycle state must be one of: active, blocked, done",
|
|
1671
|
+
});
|
|
1672
|
+
process.exitCode = 1;
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const root = monorepoRoot();
|
|
1677
|
+
if (!root) {
|
|
1678
|
+
emit.error({
|
|
1679
|
+
code: "not_in_repo",
|
|
1680
|
+
message: "not in an agent session; coord_root() returned null",
|
|
1681
|
+
});
|
|
1682
|
+
process.exitCode = 1;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (!opts.sessionId) ensureCursorSession(root);
|
|
1686
|
+
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1687
|
+
if (!myOwner) {
|
|
1688
|
+
emit.error({
|
|
1689
|
+
code: "no_pidmap_entry",
|
|
1690
|
+
message:
|
|
1691
|
+
"not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1692
|
+
});
|
|
1693
|
+
process.exitCode = 1;
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const hb = readHeartbeat(myOwner);
|
|
1698
|
+
if (!hb) {
|
|
1699
|
+
emit.error({ code: "no_heartbeat", message: noHeartbeatMessage(myOwner) });
|
|
1700
|
+
process.exitCode = 1;
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1704
|
+
emit.error({
|
|
1705
|
+
code: "lifecycle_not_human_facing",
|
|
1706
|
+
message: "task lifecycle declarations are limited to human-facing sessions",
|
|
1707
|
+
});
|
|
1708
|
+
process.exitCode = 1;
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const reason = opts.reason?.trim() || undefined;
|
|
1713
|
+
if (state === "blocked" && !reason) {
|
|
1714
|
+
emit.error({
|
|
1715
|
+
code: "blocked_reason_required",
|
|
1716
|
+
message: "lifecycle blocked requires --reason <text>",
|
|
1717
|
+
});
|
|
1718
|
+
process.exitCode = 1;
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (state === "done" && !hb.task) {
|
|
1722
|
+
emit.error({
|
|
1723
|
+
code: "task_required_for_done",
|
|
1724
|
+
message: "declare a current task with agents set-task before marking it done",
|
|
1725
|
+
});
|
|
1726
|
+
process.exitCode = 1;
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const priorState: TaskState = hb.task_state ?? "active";
|
|
1731
|
+
const priorReason = hb.task_state_reason || undefined;
|
|
1732
|
+
if (priorState === state && priorReason === reason) {
|
|
1733
|
+
emit.data({
|
|
1734
|
+
instance_id: myOwner,
|
|
1735
|
+
task_state: state,
|
|
1736
|
+
reason: reason ?? null,
|
|
1737
|
+
changed: false,
|
|
1738
|
+
name_reminted: false,
|
|
1739
|
+
git_finalization_checked: false,
|
|
1740
|
+
});
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
let finalization: GitFinalizationResult | null = null;
|
|
1745
|
+
if (state === "done") {
|
|
1746
|
+
const history = readSessionWriteClaims(root, hb.instance_id, hb.session_id ?? myOwner);
|
|
1747
|
+
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1748
|
+
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1749
|
+
claimHistoryComplete: history.complete,
|
|
1750
|
+
});
|
|
1751
|
+
if (!finalization.ok) {
|
|
1752
|
+
emit.error({
|
|
1753
|
+
code: "git_not_finalized",
|
|
1754
|
+
message: formatGitFinalizationFailure(finalization, resolveBinName(root)).replace(
|
|
1755
|
+
"the status box was not issued",
|
|
1756
|
+
"lifecycle was not changed",
|
|
1757
|
+
),
|
|
1758
|
+
});
|
|
1759
|
+
process.exitCode = 1;
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const suggestedName = buildLifecycleSuggestedName(hb.name ?? "unknown", hb.task, state);
|
|
1765
|
+
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1766
|
+
const emitted = emitCanonical({
|
|
1767
|
+
type: "state.task_state",
|
|
1768
|
+
owner: myOwner,
|
|
1769
|
+
session: hb.session_id ?? myOwner,
|
|
1770
|
+
adapter: normalizeAdapter(hb.platform),
|
|
1771
|
+
data: {
|
|
1772
|
+
state,
|
|
1773
|
+
reason: reason ?? null,
|
|
1774
|
+
prior_state: priorState,
|
|
1775
|
+
name_reminted: nameReminted,
|
|
1776
|
+
git_finalization_checked: finalization !== null,
|
|
1777
|
+
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1778
|
+
},
|
|
1779
|
+
});
|
|
1780
|
+
if (!emitted) {
|
|
1781
|
+
emit.error({
|
|
1782
|
+
code: "lifecycle_event_failed",
|
|
1783
|
+
message: "the lifecycle event could not be recorded; heartbeat state was left unchanged",
|
|
1784
|
+
});
|
|
1785
|
+
process.exitCode = 1;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
emit.data({
|
|
1790
|
+
instance_id: myOwner,
|
|
1791
|
+
task_state: state,
|
|
1792
|
+
prior_state: priorState,
|
|
1793
|
+
reason: reason ?? null,
|
|
1794
|
+
changed: true,
|
|
1795
|
+
name_reminted: nameReminted,
|
|
1796
|
+
suggested_session_name: nameReminted ? suggestedName : null,
|
|
1797
|
+
git_finalization_checked: finalization !== null,
|
|
1798
|
+
...(nameReminted
|
|
1799
|
+
? {
|
|
1800
|
+
note: "Reproduce suggested_session_name verbatim, by itself, in a fenced code block at the top of your reply — the operator copies it as the session/tab title.",
|
|
1801
|
+
}
|
|
1802
|
+
: {}),
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1586
1806
|
function runSuggestName(
|
|
1587
1807
|
descriptionParts: string[],
|
|
1588
1808
|
opts: { json?: boolean; sessionId?: string },
|
|
@@ -1790,6 +2010,12 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1790
2010
|
instance_id: hb.instance_id,
|
|
1791
2011
|
kind: normalizeKind(hb.kind),
|
|
1792
2012
|
session_age_secs: ageSecs,
|
|
2013
|
+
activity: activityOf(hb),
|
|
2014
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
2015
|
+
activity_source: hb.activity_source ?? null,
|
|
2016
|
+
task_state: taskStateOf(hb),
|
|
2017
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
2018
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
1793
2019
|
files_held: filesHeld,
|
|
1794
2020
|
peers_live: livePeers.length,
|
|
1795
2021
|
peers_stale: peersStale,
|
|
@@ -1803,6 +2029,9 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1803
2029
|
agents: m.agents.map((a) => ({
|
|
1804
2030
|
name: a.name || "unnamed",
|
|
1805
2031
|
task: a.task ?? null,
|
|
2032
|
+
activity: a.activity ?? "unknown",
|
|
2033
|
+
task_state: a.task_state ?? "active",
|
|
2034
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
1806
2035
|
files: a.files_touched?.length ?? 0,
|
|
1807
2036
|
})),
|
|
1808
2037
|
})),
|
|
@@ -1822,6 +2051,8 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1822
2051
|
|
|
1823
2052
|
const rows: Array<[string, string]> = [
|
|
1824
2053
|
["session", formatAge(ageSecs)],
|
|
2054
|
+
["activity", activityOf(hb)],
|
|
2055
|
+
["lifecycle", lifecycleLabel(hb)],
|
|
1825
2056
|
["context", ctxStr],
|
|
1826
2057
|
["files", filesStr],
|
|
1827
2058
|
["peers", peersStr],
|
|
@@ -2573,6 +2804,12 @@ function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
|
2573
2804
|
case "state.task_set":
|
|
2574
2805
|
detail = d.cleared ? "(cleared)" : clip(s("task"));
|
|
2575
2806
|
break;
|
|
2807
|
+
case "state.task_state":
|
|
2808
|
+
detail = `${s("prior_state") || "active"} → ${s("state") || "active"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
2809
|
+
break;
|
|
2810
|
+
case "interaction.input_requested":
|
|
2811
|
+
detail = s("request_kind") || s("tool_name") || "operator input requested";
|
|
2812
|
+
break;
|
|
2576
2813
|
case "state.status_checked":
|
|
2577
2814
|
detail = "status box rendered";
|
|
2578
2815
|
break;
|
|
@@ -2693,6 +2930,7 @@ function runTrace(
|
|
|
2693
2930
|
}
|
|
2694
2931
|
|
|
2695
2932
|
const events = byId.get(resolvedId) ?? [];
|
|
2933
|
+
const state = foldSessionState(events, { instance_id: resolvedId });
|
|
2696
2934
|
const lines = events
|
|
2697
2935
|
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
2698
2936
|
.filter((l): l is TraceEntry => l !== null)
|
|
@@ -2707,6 +2945,12 @@ function runTrace(
|
|
|
2707
2945
|
instance_id: resolvedId,
|
|
2708
2946
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
2709
2947
|
total_events: events.length,
|
|
2948
|
+
activity: state.activity,
|
|
2949
|
+
activity_updated_at: state.activity_updated_at ?? null,
|
|
2950
|
+
activity_source: state.activity_source ?? null,
|
|
2951
|
+
task_state: state.task_state,
|
|
2952
|
+
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
2953
|
+
task_state_reason: state.task_state_reason ?? null,
|
|
2710
2954
|
shown: shown.length,
|
|
2711
2955
|
entries: shown,
|
|
2712
2956
|
};
|
|
@@ -2718,6 +2962,9 @@ function runTrace(
|
|
|
2718
2962
|
emit.data(result);
|
|
2719
2963
|
const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
|
|
2720
2964
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
2965
|
+
process.stdout.write(
|
|
2966
|
+
` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
|
|
2967
|
+
); // lint-ok-emission: human trace view
|
|
2721
2968
|
if (shown.length === 0) {
|
|
2722
2969
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2723
2970
|
return;
|