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.
Files changed (91) hide show
  1. package/README.md +2 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +3 -1
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +212 -1
  6. package/dist/commands/claude-desktop.d.ts +2 -2
  7. package/dist/commands/claude-desktop.d.ts.map +1 -1
  8. package/dist/commands/claude-desktop.js +74 -3
  9. package/dist/commands/doctor.d.ts +4 -2
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +45 -6
  12. package/dist/commands/init.d.ts +1 -0
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +10 -0
  15. package/dist/commands/instructions.d.ts +4 -0
  16. package/dist/commands/instructions.d.ts.map +1 -0
  17. package/dist/commands/instructions.js +100 -0
  18. package/dist/commands/presence.d.ts.map +1 -1
  19. package/dist/commands/presence.js +4 -1
  20. package/dist/core/agents/canonical-emit.d.ts +1 -1
  21. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  22. package/dist/core/agents/canonical-emit.js +5 -3
  23. package/dist/core/agents/cli.js +5 -1
  24. package/dist/core/agents/coord-client.d.ts +11 -0
  25. package/dist/core/agents/coord-client.d.ts.map +1 -1
  26. package/dist/core/agents/index.d.ts +1 -0
  27. package/dist/core/agents/index.d.ts.map +1 -1
  28. package/dist/core/agents/index.js +1 -0
  29. package/dist/core/agents/render/prompt-context.d.ts +6 -0
  30. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/prompt-context.js +46 -5
  32. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  33. package/dist/core/agents/render/session-context.js +8 -2
  34. package/dist/core/agents/rules/stop-hook.js +4 -4
  35. package/dist/core/agents/state/heartbeat-projector.d.ts +11 -0
  36. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  37. package/dist/core/agents/state/heartbeat-projector.js +54 -0
  38. package/dist/core/agents/state/heartbeat-writer.d.ts +15 -0
  39. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  40. package/dist/core/agents/state/heartbeat-writer.js +32 -0
  41. package/dist/core/agents/state/session-state.d.ts +44 -0
  42. package/dist/core/agents/state/session-state.d.ts.map +1 -0
  43. package/dist/core/agents/state/session-state.js +81 -0
  44. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  45. package/dist/core/hooks/adapter/events.js +1 -0
  46. package/dist/core/hooks/adapter/parse.d.ts +1 -1
  47. package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
  48. package/dist/core/hooks/adapter/parse.js +2 -0
  49. package/dist/core/hooks/cli.js +90 -6
  50. package/dist/core/hooks/codex-authorization.d.ts +30 -0
  51. package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
  52. package/dist/core/hooks/codex-authorization.js +199 -0
  53. package/dist/core/hooks/events/schema.d.ts +30 -1
  54. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  55. package/dist/core/presence/blob.d.ts +4 -0
  56. package/dist/core/presence/blob.d.ts.map +1 -1
  57. package/dist/core/presence/blob.js +12 -0
  58. package/dist/core/presence/index.js +15 -1
  59. package/dist/lib/claude-desktop.d.ts +47 -0
  60. package/dist/lib/claude-desktop.d.ts.map +1 -1
  61. package/dist/lib/claude-desktop.js +169 -1
  62. package/dist/lib/instructions/bundle.d.ts +33 -0
  63. package/dist/lib/instructions/bundle.d.ts.map +1 -0
  64. package/dist/lib/instructions/bundle.js +149 -0
  65. package/package.json +2 -2
  66. package/src/commander.ts +3 -1
  67. package/src/commands/agents.ts +248 -1
  68. package/src/commands/claude-desktop.ts +106 -5
  69. package/src/commands/doctor.ts +59 -7
  70. package/src/commands/init.ts +11 -0
  71. package/src/commands/instructions.ts +129 -0
  72. package/src/commands/presence.ts +7 -1
  73. package/src/core/agents/canonical-emit.ts +6 -4
  74. package/src/core/agents/cli.ts +6 -1
  75. package/src/core/agents/coord-client.ts +11 -0
  76. package/src/core/agents/index.ts +1 -0
  77. package/src/core/agents/render/prompt-context.ts +71 -6
  78. package/src/core/agents/render/session-context.ts +14 -2
  79. package/src/core/agents/rules/stop-hook.ts +5 -5
  80. package/src/core/agents/state/heartbeat-projector.ts +63 -0
  81. package/src/core/agents/state/heartbeat-writer.ts +51 -0
  82. package/src/core/agents/state/session-state.ts +131 -0
  83. package/src/core/hooks/adapter/events.ts +1 -0
  84. package/src/core/hooks/adapter/parse.ts +3 -0
  85. package/src/core/hooks/cli.ts +98 -7
  86. package/src/core/hooks/codex-authorization.ts +253 -0
  87. package/src/core/hooks/events/schema.ts +44 -0
  88. package/src/core/presence/blob.ts +18 -0
  89. package/src/core/presence/index.ts +18 -1
  90. package/src/lib/claude-desktop.ts +232 -1
  91. package/src/lib/instructions/bundle.ts +199 -0
@@ -141,6 +141,9 @@ export function registerInitCommand(program, emit, binName) {
141
141
  actions.push(`+ wired ${wired} hook(s), upgraded ${upgraded} stale command(s), and removed ` +
142
142
  `${removed} legacy hook(s) in ${rel(projectRoot, settingsPath)} (${already} already present)`);
143
143
  }
144
+ const authorizationReview = codexHookReviewAction(adapter);
145
+ if (authorizationReview)
146
+ actions.push(authorizationReview);
144
147
  // ── 3. agent-facing instructions block + skills ────────────────────────
145
148
  // A misconfigured host addendum aborts here rather than half-writing: the
146
149
  // apply step validates the configured source before it touches a file, so
@@ -166,6 +169,13 @@ export function registerInitCommand(program, emit, binName) {
166
169
  emit.text(render(projectRoot, dryRun, actions, [...applied.warnings, ...gitHooks.warnings]));
167
170
  });
168
171
  }
172
+ export function codexHookReviewAction(adapter) {
173
+ if (adapter !== "codex")
174
+ return null;
175
+ return ("! Codex authorizes hook commands separately. Before starting work, review them in " +
176
+ "the terminal UI with `/hooks` or in Codex Desktop under Settings > Hooks. " +
177
+ "After approval, start a fresh task so SessionStart can run.");
178
+ }
169
179
  /**
170
180
  * Merge agent-hook entries into a adapter settings object in place, idempotently.
171
181
  * Preserves every existing hook; rewrites events already wired to
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import type { EmitContext } from "../commander.js";
3
+ export declare function registerInstructionsCommand(program: Command, emit: EmitContext): void;
4
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/commands/instructions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAcnD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmDrF"}
@@ -0,0 +1,100 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { countTokens } from "gpt-tokenizer/model/gpt-4o";
5
+ import { monorepoRoot } from "../core/agents/index.js";
6
+ import { buildInstructionBundle, } from "../lib/instructions/bundle.js";
7
+ export function registerInstructionsCommand(program, emit) {
8
+ const command = program
9
+ .command("instructions")
10
+ .description("Inspect and version the repo-authored instruction bundle loaded by each adapter.");
11
+ command
12
+ .command("manifest [paths...]")
13
+ .description("Emit a machine-readable bundle manifest for one or more working paths")
14
+ .option("--adapter <adapter>", "claude-code, cursor, codex, or all", "all")
15
+ .option("--format <type>", "Output format: json or table", "json")
16
+ .option("-o, --output <file>", "Write the complete JSON manifest to a file")
17
+ .action((paths, options) => {
18
+ if (options.format === "json")
19
+ emit.config({ format: "json" });
20
+ const coordRoot = monorepoRoot() ?? process.cwd();
21
+ const adapters = parseAdapters(options.adapter);
22
+ const profiles = (paths.length > 0 ? paths : [process.cwd()]).flatMap((path) => adapters.map((adapter) => enrichBundle(buildInstructionBundle({ coordRoot, cwd: resolve(path), adapter }), coordRoot)));
23
+ const manifest = {
24
+ ok: true,
25
+ schema_version: 1,
26
+ generated_at: new Date().toISOString(),
27
+ source_commit: gitHead(coordRoot),
28
+ tokenizer: "o200k_base",
29
+ coord_root: ".",
30
+ profile_count: profiles.length,
31
+ profiles,
32
+ };
33
+ if (options.output) {
34
+ const output = resolve(options.output);
35
+ writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
36
+ emit.data({
37
+ ok: true,
38
+ output,
39
+ schema_version: manifest.schema_version,
40
+ generated_at: manifest.generated_at,
41
+ source_commit: manifest.source_commit,
42
+ profile_count: manifest.profile_count,
43
+ });
44
+ }
45
+ else {
46
+ emit.data(manifest);
47
+ }
48
+ });
49
+ }
50
+ function parseAdapters(value) {
51
+ if (value === "all")
52
+ return ["claude-code", "cursor", "codex"];
53
+ if (value === "claude-code" || value === "cursor" || value === "codex")
54
+ return [value];
55
+ throw new Error(`unsupported adapter '${value}' (expected claude-code, cursor, codex, or all)`);
56
+ }
57
+ function enrichBundle(bundle, coordRoot) {
58
+ const components = bundle.components.map((component) => enrichComponent(component, coordRoot));
59
+ const canonicalSources = bundle.canonical_sources.map((component) => enrichComponent(component, coordRoot));
60
+ return {
61
+ ...bundle,
62
+ coord_root: ".",
63
+ components,
64
+ canonical_sources: canonicalSources,
65
+ totals: totals(components),
66
+ canonical_source_totals: totals(canonicalSources),
67
+ };
68
+ }
69
+ function enrichComponent(component, coordRoot) {
70
+ const bytes = readFileSync(resolve(coordRoot, component.path));
71
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
72
+ return {
73
+ ...component,
74
+ chars: text?.length ?? null,
75
+ tokens: text === null ? null : countTokens(text),
76
+ };
77
+ }
78
+ function totals(components) {
79
+ const roles = ["always_loaded", "just_in_time", "runtime_config"];
80
+ return {
81
+ files: components.length,
82
+ bytes: components.reduce((sum, component) => sum + component.bytes, 0),
83
+ tokens: components.reduce((sum, component) => sum + (component.tokens ?? 0), 0),
84
+ by_role: Object.fromEntries(roles.map((role) => {
85
+ const selected = components.filter((component) => component.role === role);
86
+ return [
87
+ role,
88
+ {
89
+ files: selected.length,
90
+ bytes: selected.reduce((sum, component) => sum + component.bytes, 0),
91
+ tokens: selected.reduce((sum, component) => sum + (component.tokens ?? 0), 0),
92
+ },
93
+ ];
94
+ })),
95
+ };
96
+ }
97
+ function gitHead(root) {
98
+ const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
99
+ return result.status === 0 ? result.stdout.trim() : null;
100
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/commands/presence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA0BnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwKjF"}
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/commands/presence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA0BnD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8KjF"}
@@ -133,7 +133,10 @@ export function registerPresenceCommand(program, emit) {
133
133
  for (const a of m.agents) {
134
134
  const task = a.task ? ` "${a.task.slice(0, 70)}"` : "";
135
135
  const files = a.files_touched?.length ? ` [${a.files_touched.length} files]` : "";
136
- emit.text(` agent-${a.name ?? a.instance_id.slice(0, 8)}${task}${files}\n`);
136
+ const lifecycle = a.task_state === "blocked" && a.task_state_reason
137
+ ? `${a.task_state}: ${a.task_state_reason}`
138
+ : a.task_state;
139
+ emit.text(` agent-${a.name ?? a.instance_id.slice(0, 8)}${task}${files} activity=${a.activity} lifecycle=${lifecycle}\n`);
137
140
  }
138
141
  }
139
142
  });
@@ -28,7 +28,7 @@ export interface CanonicalEmitInput {
28
28
  * `agents status`, and no sequence of CLI commands can satisfy it.
29
29
  */
30
30
  export declare function resolveEmitRoot(start?: string): string | null;
31
- export declare function emitCanonical(input: CanonicalEmitInput): void;
31
+ export declare function emitCanonical(input: CanonicalEmitInput): boolean;
32
32
  /**
33
33
  * Normalize the heartbeat's `platform` field to the canonical Adapter type.
34
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAsC7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAIjG"}
1
+ {"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAwChE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAIjG"}
@@ -26,10 +26,10 @@ export function resolveEmitRoot(start = process.cwd()) {
26
26
  export function emitCanonical(input) {
27
27
  const root = resolveEmitRoot();
28
28
  if (!root)
29
- return;
29
+ return false;
30
30
  const binary = coordBinPath("agent-coord", root);
31
31
  if (!binary)
32
- return;
32
+ return false;
33
33
  try {
34
34
  const args = [
35
35
  "emit-event",
@@ -62,10 +62,12 @@ export function emitCanonical(input) {
62
62
  if (result.error || result.status !== 0) {
63
63
  const why = result.error ? result.error.message : `exit ${result.status}`;
64
64
  process.stderr.write(`emitCanonical: ${input.type} emit failed (${why})\n`); // lint-ok-emission: soft-fail diagnostic promised by the module doc; silent drops cost blocked turns
65
+ return false;
65
66
  }
67
+ return true;
66
68
  }
67
69
  catch {
68
- /* never break the caller */
70
+ return false;
69
71
  }
70
72
  }
71
73
  /**
@@ -564,8 +564,11 @@ async function handlePromptContext(root, rest) {
564
564
  const sessionNameNudge = args["session-name-nudge"] === "true";
565
565
  const taskNudge = args["task-nudge"] === "true";
566
566
  const statusFooterNudge = args["status-footer-nudge"] === "true";
567
+ // Value is the adapter id; a bare flag (parsed as "true") still enables the
568
+ // reminder with the adapter-neutral wording.
569
+ const turnRitualNudge = args["turn-ritual-nudge"] === "true" ? "generic" : args["turn-ritual-nudge"];
567
570
  if (!instanceId) {
568
- process.stderr.write("agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--session-name-nudge] [--task-nudge] [--status-footer-nudge]\n");
571
+ process.stderr.write("agent-coord prompt-context --instance <id> [--session <id>] [--name <agent-name>] [--session-name-nudge] [--task-nudge] [--status-footer-nudge] [--turn-ritual-nudge <adapter>]\n");
569
572
  return 2;
570
573
  }
571
574
  const { renderPromptContext } = await import("./render/prompt-context.js");
@@ -577,6 +580,7 @@ async function handlePromptContext(root, rest) {
577
580
  sessionNameNudge,
578
581
  taskNudge,
579
582
  statusFooterNudge,
583
+ turnRitualNudge,
580
584
  });
581
585
  process.stdout.write(text);
582
586
  return 0;
@@ -40,16 +40,27 @@ export interface Heartbeat {
40
40
  task?: string;
41
41
  /** UTC ISO-8601 timestamp when task was last set/cleared. Used by adapters without Stop enforcement to compute staleness. 2026-05-24. */
42
42
  task_updated_at?: string | null;
43
+ /** Current evidence-derived activity axis. */
44
+ activity?: "unknown" | "working" | "needs_input" | "idle";
45
+ activity_updated_at?: string;
46
+ activity_source?: string;
47
+ /** Explicit task lifecycle axis. */
48
+ task_state?: "active" | "blocked" | "done";
49
+ task_state_updated_at?: string;
50
+ task_state_reason?: string;
43
51
  /** Session name built on the first non-empty set-task; its presence means "this session has been named". 2026-08-09. */
44
52
  suggested_session_name?: string;
45
53
  /** Stamped by turn.stop once the suggested name is seen in assistant reply text. 2026-08-09. */
46
54
  session_name_seen_at?: string;
55
+ /** Which suggested name the latest sighting covered. */
56
+ session_name_seen_for?: string;
47
57
  /** Auto-generated per-turn summary written by the Stop hook via Haiku. 2026-05-23. */
48
58
  turn_summary?: string | null;
49
59
  /** UTC ISO-8601 timestamp when turn_summary was last refreshed. */
50
60
  turn_summary_updated_at?: string | null;
51
61
  /** Hook client: `claude-code` (default) or `cursor`. Cursor Phase 1. */
52
62
  platform?: string;
63
+ workflow_run_id?: string;
53
64
  }
54
65
  /**
55
66
  * Resolve the monorepo root for coord-state purposes.
@@ -1 +1 @@
1
- {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"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"}
@@ -11,4 +11,5 @@
11
11
  export * from "./canonical-emit.js";
12
12
  export * from "./coord-client.js";
13
13
  export * from "./session-events.js";
14
+ export * from "./state/session-state.js";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/agents/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/agents/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC"}
@@ -11,3 +11,4 @@
11
11
  export * from "./canonical-emit.js";
12
12
  export * from "./coord-client.js";
13
13
  export * from "./session-events.js";
14
+ export * from "./state/session-state.js";
@@ -43,6 +43,12 @@ export interface PromptContextOpts {
43
43
  /** When true, append the non-deduplicated Codex status-footer reminder.
44
44
  * The caller enables this only for human-facing Codex sessions. */
45
45
  statusFooterNudge?: boolean;
46
+ /** Adapter id whose Stop hook ENFORCES the end-of-turn ritual (task_set +
47
+ * status check). When set, append a fresh per-prompt reminder of that
48
+ * contract so the model satisfies it before ending the turn instead of
49
+ * being bounced into a retry. Not hash-deduped, same rationale as the
50
+ * status footer. */
51
+ turnRitualNudge?: string;
46
52
  }
47
53
  /**
48
54
  * Build the combined UserPromptSubmit additionalContext string. Returns "" when
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAqCH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA0CnE"}
1
+ {"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA0CH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqDnE"}
@@ -45,7 +45,7 @@ import { formatPendingCouncils } from "./session-context.js";
45
45
  * councils are pending, matching the bash behavior).
46
46
  */
47
47
  export function renderPromptContext(opts) {
48
- const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, statusFooterNudge, } = opts;
48
+ const { coordRoot, instanceId, sessionId, agentName, sessionNameNudge, taskNudge, statusFooterNudge, turnRitualNudge, } = opts;
49
49
  const sections = [];
50
50
  // 1. Peer table with hash dedup.
51
51
  const peerTable = computePeerTableIfChanged(coordRoot, instanceId, sessionId);
@@ -76,8 +76,32 @@ export function renderPromptContext(opts) {
76
76
  if (statusMsg)
77
77
  sections.push(statusMsg);
78
78
  }
79
+ // 5. Adapters whose Stop hook ENFORCES the turn ritual get a fresh reminder
80
+ // on every prompt. Reminding before the reply is drafted is far cheaper than
81
+ // the bounce-and-retry the Stop hook otherwise forces: enforcement-only
82
+ // compliance was measured missing on a double-digit share of turns. Not
83
+ // hash-deduped for the same reason as the Codex footer.
84
+ if (turnRitualNudge) {
85
+ const ritualMsg = renderTurnRitualReminder(coordRoot, instanceId, turnRitualNudge);
86
+ if (ritualMsg)
87
+ sections.push(ritualMsg);
88
+ }
79
89
  return sections.join("\n\n");
80
90
  }
91
+ function renderTurnRitualReminder(coordRoot, selfInstanceId, adapter) {
92
+ const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
93
+ if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
94
+ return "";
95
+ const bin = resolveBinName(coordRoot);
96
+ const statusCommand = endOfTurnStatusCommand(coordRoot);
97
+ // Claude Code renders the status box only if the reply carries it; Cursor
98
+ // shows the box inline on its own, so it is never asked for a second copy.
99
+ const paste = adapter === "claude-code"
100
+ ? " and paste its output verbatim in a fenced code block at the end of your reply"
101
+ : "";
102
+ return (`Turn ritual (Stop-enforced): on any turn that uses tools, run \`${bin} agents set-task "<short focus>"\` at some point during the turn (required every turn, even when the focus is unchanged; pass an empty string when there is no meaningful focus), then make \`${statusCommand}\` your final tool call${paste}. ` +
103
+ "A turn that ends without these is bounced by the Stop hook and costs a retry.");
104
+ }
81
105
  function renderStatusFooterReminder(coordRoot, selfInstanceId) {
82
106
  const hb = readHeartbeat(join(coordRoot, ".harnery", "active", `${selfInstanceId}.json`));
83
107
  if (!hb || hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id)
@@ -116,13 +140,17 @@ function computeFocusNudgeIfChanged(coordRoot, selfInstanceId, opts) {
116
140
  let needsNudge = false;
117
141
  let message = "";
118
142
  let nudgeKind = "";
119
- if (opts.sessionNameNudge && !hb.suggested_session_name) {
143
+ if (opts.sessionNameNudge &&
144
+ (!hb.suggested_session_name ||
145
+ (hb.session_name_seen_for !== undefined &&
146
+ hb.suggested_session_name !== hb.session_name_seen_for))) {
120
147
  // Keyed on "a name was ever produced", not task_updated_at: a bare clear
121
148
  // as the first declaration must not end the naming window.
122
149
  needsNudge = true;
123
150
  nudgeKind = "session-name";
124
- message =
125
- `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
151
+ message = hb.suggested_session_name
152
+ ? `This session's lifecycle changed its suggested name. Reproduce this value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title: ${hb.suggested_session_name}`
153
+ : `This session has no name yet: run \`${bin} agents set-task "<2-5 word session topic>"\` as your first tool call. ` +
126
154
  "When it returns `first_of_session: true`, reproduce its `suggested_session_name` value by itself inside a fenced code block at the very top of your reply so the operator can one-click-copy it as the session/tab title. " +
127
155
  "Then continue with the task; that `set-task` also satisfies this turn's focus declaration.";
128
156
  }
@@ -204,6 +232,9 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
204
232
  started_at: p.started_at ?? null,
205
233
  files_touched: Array.from(p.files_touched ?? []).sort(),
206
234
  platform: p.platform ?? null,
235
+ activity: p.activity ?? "unknown",
236
+ task_state: p.task_state ?? "active",
237
+ task_state_reason: p.task_state_reason ?? null,
207
238
  }))
208
239
  .sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
209
240
  // Remote basis: machine + per-agent identity/task/files (published_at is
@@ -214,6 +245,9 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
214
245
  instance_id: a.instance_id,
215
246
  name: a.name ?? null,
216
247
  task: a.task ?? null,
248
+ activity: a.activity,
249
+ task_state: a.task_state,
250
+ task_state_reason: a.task_state_reason ?? null,
217
251
  files_touched: Array.from(a.files_touched ?? []).sort(),
218
252
  })),
219
253
  }));
@@ -393,6 +427,7 @@ function renderSubtable(rows, header, nowSec) {
393
427
  }
394
428
  function formatRow(r, nowSec) {
395
429
  const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
430
+ const statePart = formatState(r);
396
431
  // Fall back started_at → last_heartbeat; if neither is a valid timestamp,
397
432
  // show "age unknown" rather than the epoch-derived "20608d ago" ghost.
398
433
  const startedSec = parseIsoSec(r.started_at) ?? parseIsoSec(r.last_heartbeat);
@@ -403,7 +438,13 @@ function formatRow(r, nowSec) {
403
438
  // Prefer a short instance_id over a bare "unknown" so an incomplete row is
404
439
  // still identifiable.
405
440
  const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
406
- return ` - agent-${label}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
441
+ return ` - agent-${label}${taskPart} (${statePart}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
442
+ }
443
+ function formatState(r) {
444
+ const activity = r.activity ?? "unknown";
445
+ const lifecycle = r.task_state ?? "active";
446
+ const reason = lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
447
+ return `activity=${activity}, lifecycle=${lifecycle}${reason}`;
407
448
  }
408
449
  function fmtFiles(files) {
409
450
  if (files.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAsB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6E7D;AA+ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
1
+ {"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AA0B1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6E7D;AAuJD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
@@ -101,7 +101,10 @@ function formatRemoteMachines(coordRoot) {
101
101
  const files = a.files_touched?.length
102
102
  ? `holds: ${a.files_touched.slice(0, 3).join(", ")}${a.files_touched.length > 3 ? `, +${a.files_touched.length - 3} more` : ""}`
103
103
  : "nothing held";
104
- lines.push(` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (${files})`);
104
+ const reason = a.task_state === "blocked" && a.task_state_reason
105
+ ? `: ${a.task_state_reason.slice(0, 80)}`
106
+ : "";
107
+ lines.push(` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (activity=${a.activity}, lifecycle=${a.task_state}${reason}, ${files})`);
105
108
  }
106
109
  }
107
110
  return `Sessions on other machines (advisory, via presence refs):\n${lines.join("\n")}`;
@@ -179,11 +182,14 @@ function renderSubtable(rows, header, nowSec) {
179
182
  }
180
183
  function formatRow(r, nowSec) {
181
184
  const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
185
+ const activity = r.activity ?? "unknown";
186
+ const lifecycle = r.task_state ?? "active";
187
+ const reason = lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
182
188
  const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
183
189
  const filesPart = fmtFiles(r.display_files);
184
190
  const lastActivity = fmtLastActivity(r, nowSec);
185
191
  const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
186
- return ` - agent-${r.name ?? "unknown"}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
192
+ return ` - agent-${r.name ?? "unknown"}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
187
193
  }
188
194
  function fmtFiles(files) {
189
195
  if (files.length === 0)
@@ -222,18 +222,18 @@ export function evaluateStopHook(coordRoot, req) {
222
222
  // `session_name_present` (assistant-text-only transcript scan), mirroring
223
223
  // how rule 2/3 rides `status_box_present`.
224
224
  if (ackSignal === "status_box_present") {
225
- const namingTaskSet = inTurn.find((e) => e.event_type === "state.task_set" &&
226
- e.data.first_of_session === true &&
225
+ const namingEvent = inTurn.find((e) => ((e.event_type === "state.task_set" && e.data.first_of_session === true) ||
226
+ (e.event_type === "state.task_state" && e.data.name_reminted === true)) &&
227
227
  typeof e.data.suggested_session_name === "string" &&
228
228
  e.data.suggested_session_name.length > 0);
229
- if (namingTaskSet) {
229
+ if (namingEvent) {
230
230
  // ANY in-window turn.stop with the flag satisfies the rule, not just the
231
231
  // latest: once seen, `session_name_seen_at` is stamped and later
232
232
  // turn.stops OMIT the field, so a later block on a different rule (whose
233
233
  // repair fires a fresh turn.stop) must not un-satisfy this one.
234
234
  const namePresent = inTurn.some((e) => e.event_type === "turn.stop" && e.data.session_name_present === true);
235
235
  if (!namePresent) {
236
- return sessionNameBlock(String(namingTaskSet.data.suggested_session_name));
236
+ return sessionNameBlock(String(namingEvent.data.suggested_session_name));
237
237
  }
238
238
  }
239
239
  }
@@ -9,6 +9,7 @@
9
9
  * through the canonical event stream) survive each projector run.
10
10
  */
11
11
  import type { CanonicalEvent } from "../events/consume.js";
12
+ import { type AgentActivity, type TaskState } from "./session-state.js";
12
13
  export interface V2Heartbeat {
13
14
  instance_id: string;
14
15
  session_id: string;
@@ -17,6 +18,10 @@ export interface V2Heartbeat {
17
18
  name?: string;
18
19
  kind?: "session" | "subagent" | "transient";
19
20
  model?: string;
21
+ instruction_bundle_id?: string;
22
+ instruction_source_id?: string;
23
+ instruction_profile_root?: string;
24
+ instruction_component_count?: number;
20
25
  platform?: string;
21
26
  subagent_call_id?: string;
22
27
  parent_session_id?: string;
@@ -30,6 +35,12 @@ export interface V2Heartbeat {
30
35
  last_tool_at?: string;
31
36
  task?: string;
32
37
  task_updated_at?: string;
38
+ activity?: AgentActivity;
39
+ activity_updated_at?: string;
40
+ activity_source?: string;
41
+ task_state?: TaskState;
42
+ task_state_updated_at?: string;
43
+ task_state_reason?: string;
33
44
  suggested_session_name?: string;
34
45
  session_name_seen_at?: string;
35
46
  session_name_seen_for?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,OAAO,EAAE;QACP,cAAc,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoD9D;AAsbD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3E"}
1
+ {"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
12
  import { dirname, join } from "node:path";
13
+ import { applySessionStateEvent } from "./session-state.js";
13
14
  export function projectHeartbeats(coordRoot, events) {
14
15
  const perOwner = {};
15
16
  // Events that must NOT seed a heartbeat for an owner with no live file.
@@ -122,6 +123,13 @@ function apply(hb, ev, coordRoot) {
122
123
  hb.v2_meta.last_projected = new Date().toISOString();
123
124
  if (ev.turn_id)
124
125
  hb.last_turn_id = ev.turn_id;
126
+ const sessionState = applySessionStateEvent(hb, ev);
127
+ hb.activity = sessionState.activity;
128
+ hb.activity_updated_at = sessionState.activity_updated_at;
129
+ hb.activity_source = sessionState.activity_source;
130
+ hb.task_state = sessionState.task_state;
131
+ hb.task_state_updated_at = sessionState.task_state_updated_at;
132
+ hb.task_state_reason = sessionState.task_state_reason;
125
133
  const d = ev.data;
126
134
  switch (ev.event_type) {
127
135
  case "session.start":
@@ -131,6 +139,19 @@ function apply(hb, ev, coordRoot) {
131
139
  const model = pickStr(d, "model");
132
140
  if (model)
133
141
  hb.model = model;
142
+ const instructionBundleId = pickStr(d, "instruction_bundle_id");
143
+ if (instructionBundleId)
144
+ hb.instruction_bundle_id = instructionBundleId;
145
+ const instructionSourceId = pickStr(d, "instruction_source_id");
146
+ if (instructionSourceId)
147
+ hb.instruction_source_id = instructionSourceId;
148
+ const instructionProfileRoot = pickStr(d, "instruction_profile_root");
149
+ if (instructionProfileRoot)
150
+ hb.instruction_profile_root = instructionProfileRoot;
151
+ const instructionComponentCount = pickNum(d, "instruction_component_count");
152
+ if (instructionComponentCount !== undefined) {
153
+ hb.instruction_component_count = instructionComponentCount;
154
+ }
134
155
  const platform = pickStr(d, "platform") ?? adapterToPlatform(ev.adapter);
135
156
  hb.platform = platform;
136
157
  const name = pickStr(d, "name");
@@ -273,6 +294,20 @@ function apply(hb, ev, coordRoot) {
273
294
  }
274
295
  break;
275
296
  }
297
+ case "state.task_state": {
298
+ const suggested = pickStr(d, "suggested_session_name");
299
+ if (suggested && suggested !== hb.suggested_session_name) {
300
+ // Older heartbeats predate session_name_seen_for. A lifecycle re-mint
301
+ // still needs a mismatch the prompt and Stop paths can enforce, so
302
+ // attribute the prior title before replacing it. Ordinary legacy
303
+ // heartbeats remain quiet until a real lifecycle event changes them.
304
+ if (!hb.session_name_seen_for && hb.suggested_session_name) {
305
+ hb.session_name_seen_for = hb.suggested_session_name;
306
+ }
307
+ hb.suggested_session_name = suggested;
308
+ }
309
+ break;
310
+ }
276
311
  case "state.status_checked":
277
312
  hb.last_status_at = ev.ts;
278
313
  break;
@@ -464,6 +499,10 @@ function writeHeartbeat(coordRoot, instanceId, hb) {
464
499
  setIfDefined(merged, "workflow_run_id", hb.workflow_run_id);
465
500
  setIfDefined(merged, "workflow_agent_id", hb.workflow_agent_id);
466
501
  setIfDefined(merged, "model", hb.model);
502
+ setIfDefined(merged, "instruction_bundle_id", hb.instruction_bundle_id);
503
+ setIfDefined(merged, "instruction_source_id", hb.instruction_source_id);
504
+ setIfDefined(merged, "instruction_profile_root", hb.instruction_profile_root);
505
+ setIfDefined(merged, "instruction_component_count", hb.instruction_component_count);
467
506
  setIfDefined(merged, "platform", hb.platform);
468
507
  setIfDefined(merged, "started_at", hb.started_at);
469
508
  // files_touched is a required-array invariant for every reader
@@ -477,6 +516,17 @@ function writeHeartbeat(coordRoot, instanceId, hb) {
477
516
  setIfDefined(merged, "last_tool_at", hb.last_tool_at);
478
517
  setIfDefined(merged, "task", hb.task);
479
518
  setIfDefined(merged, "task_updated_at", hb.task_updated_at);
519
+ setIfDefined(merged, "activity", hb.activity);
520
+ setIfDefined(merged, "activity_updated_at", hb.activity_updated_at);
521
+ setIfDefined(merged, "activity_source", hb.activity_source);
522
+ setIfDefined(merged, "task_state", hb.task_state);
523
+ setIfDefined(merged, "task_state_updated_at", hb.task_state_updated_at);
524
+ if (hb.task_state_reason) {
525
+ merged.task_state_reason = hb.task_state_reason;
526
+ }
527
+ else if (hb.task_state) {
528
+ delete merged.task_state_reason;
529
+ }
480
530
  setIfDefined(merged, "suggested_session_name", hb.suggested_session_name);
481
531
  setIfDefined(merged, "session_name_seen_at", hb.session_name_seen_at);
482
532
  if (hb.session_name_seen_for) {
@@ -525,3 +575,7 @@ function pickBool(o, k) {
525
575
  const v = o[k];
526
576
  return typeof v === "boolean" ? v : undefined;
527
577
  }
578
+ function pickNum(o, k) {
579
+ const v = o[k];
580
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
581
+ }