@davesheffer/hunch 1.33.0 → 1.35.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 CHANGED
@@ -23,6 +23,9 @@ hunch init
23
23
  hunch backfill --since 90d # optional: draft memory from recent history
24
24
  ```
25
25
 
26
+ If the shell reports that `hunch` is not found, initialize without a global binary from the
27
+ repository directory: `npx -y @davesheffer/hunch@latest init`.
28
+
26
29
  Reload your assistant, then ask:
27
30
 
28
31
  > Why is this built this way, and what should I preserve when changing it?
@@ -78,8 +81,13 @@ From each repository that uses Hunch:
78
81
  hunch update
79
82
  ```
80
83
 
84
+ If the shell reports that `hunch` is not found, run
85
+ `npx -y @davesheffer/hunch@latest update` from the repository instead.
86
+
81
87
  Or ask your agent to **“update Hunch.”** The command installs the latest release, aligns configured integration pins, repairs known legacy launch commands, and refreshes Hunch instructions. It preserves unrelated settings and intentionally disabled hooks.
82
88
 
89
+ Installed interactive CLI commands can also show a cached update notice. At most once every 24 hours, a detached worker asks npm for the package's public `latest` version; hooks, MCP, CI, servers, the updater, non-interactive commands, and source checkouts skip that request. Set `HUNCH_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1` to disable it.
90
+
83
91
  - A standalone npm project keeps Hunch in its existing dependency section at an exact version. Without a repository dependency, the global CLI is updated. Add `--global` to update both.
84
92
  - For other package managers or workspaces, update the dependency with that package manager, then run `hunch integrations repair-pins`.
85
93
  - Restart or reconnect active assistants. In Codex, open `/hooks` to review and trust changed commands, then start a new session. A changed version pin changes the command and requires renewed trust.
@@ -178,7 +186,7 @@ Profiles retain their revision, sources, confidence, and freshness. [Project DNA
178
186
  - [Review memory](docs/review-memory.md)
179
187
  - [Agent-origin handling](docs/agent-origin.md)
180
188
  - [Autonomy ladder](docs/autonomy-ladder.md)
181
- - [Autonomous development](docs/autonomous-development.md)
189
+ - [Autonomous development](https://github.com/davesheffer/hunch-private/blob/main/projects/hunch/docs/autonomous-development.md)
182
190
  - [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
183
191
  - [VS Code extension](vscode-extension/README.md)
184
192
  - [Architecture benchmark](bench/architectural-conformance.md)
package/dist/cli/index.js CHANGED
@@ -85,7 +85,7 @@ import { recordServed, servedSummary } from "../core/served.js";
85
85
  import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
86
86
  import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
87
87
  import { renderRecalledLine } from "../core/taskReportRender.js";
88
- import { hookReportTaskId, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
88
+ import { hookReportTaskId, nativeHookCwd, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
89
89
  import { recordHookObservation } from "../core/hookObservations.js";
90
90
  import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
91
91
  import { PIPELINE_LOOP, armExecutionObligations, beforeEditProbeVerdict, compileExecutableProbes, environmentExecutableProbes, environmentExecutionObligations, executionObligationBrief, isProductPath, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, proofCheckpoint, savePipelineState, stopVerdict, unverifiedNag, } from "../core/pipeline.js";
@@ -131,6 +131,7 @@ import { ENTITY_KINDS } from "../core/types.js";
131
131
  import { planCompaction } from "../store/compact.js";
132
132
  import { repairDecisionReference } from "../core/refrepair.js";
133
133
  import { resolveInvocation, dim, synthesisStatusLines, maybeWarnOllamaContext } from "./invocation.js";
134
+ import { formatUpdateNotice, scheduleUpdateCheck, shouldCheckForUpdate } from "../core/updatecheck.js";
134
135
  const program = new Command();
135
136
  program.name("hunch").description("Hunch — engineering memory and a deterministic Change Gate for AI-assisted codebases.").version(HUNCH_VERSION);
136
137
  program.option("--initiator <name>", "bind agent launches to the originating CLI (Claude, Codex, Kimi, or a configured adapter)")
@@ -200,6 +201,29 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
200
201
  return { root, existing: store.captureHome(privateOnly) === "private"
201
202
  ? store.recs("constraints") : store.json.loadAll("constraints") };
202
203
  });
204
+ // Read an already-known update and schedule any registry refresh in a detached
205
+ // worker. No network handle is opened in this command's process, so the
206
+ // advisory cannot delay command completion or process exit.
207
+ program.hook("preAction", (_thisCommand, actionCommand) => {
208
+ try {
209
+ const path = [actionCommand.name()];
210
+ for (let parent = actionCommand.parent; parent && parent !== program; parent = parent.parent)
211
+ path.unshift(parent.name());
212
+ const gate = {
213
+ commandName: path.join(" "),
214
+ isTTY: process.stderr.isTTY === true,
215
+ installed: resolveInvocation().installed,
216
+ };
217
+ if (!shouldCheckForUpdate(gate))
218
+ return;
219
+ const result = scheduleUpdateCheck();
220
+ if (result)
221
+ console.error(dim(formatUpdateNotice(result)));
222
+ }
223
+ catch {
224
+ // Never let the update-check advisory abort the command it's piggybacking on.
225
+ }
226
+ });
203
227
  let openStore = null;
204
228
  function openTeamStore(root, opts = {}) {
205
229
  // A committed team.json is an explicit declaration that this checkout belongs
@@ -4193,7 +4217,7 @@ program
4193
4217
  try {
4194
4218
  const records = asOf ? [] : snapshotDeliveredRecords(store, envelope);
4195
4219
  const recalled = renderRecalledLine(unseenLessons(root, opts.task, records));
4196
- const occurrence = recordTaskDelivery(root, opts.task, envelope, records);
4220
+ const occurrence = recordTaskDelivery(root, opts.task, envelope, records, undefined, target);
4197
4221
  console.log(`\n${recalled ? `${recalled}\n` : ""}Task evidence: ${opts.task} · occurrence ${occurrence}`);
4198
4222
  }
4199
4223
  catch {
@@ -4582,6 +4606,12 @@ program
4582
4606
  // explorers get the indexed shape, planners get live decisions + what
4583
4607
  // was already rejected, everyone else gets the invariant digest. Public
4584
4608
  // store only; cheap reads.
4609
+ const routedCwd = nativeHookCwd(root, provider, evt);
4610
+ // A native host that supplied cwd made an explicit scope claim. If it is
4611
+ // malformed or names another checkout, serving this process root's memory
4612
+ // would cross worktrees; stay silent instead of guessing which side is right.
4613
+ if ((provider === "claude" || provider === "codex") && evt.cwd !== undefined && !routedCwd)
4614
+ return;
4585
4615
  const s = new HunchStore(paths);
4586
4616
  try {
4587
4617
  const clip1 = (text, max) => {
@@ -4591,11 +4621,19 @@ program
4591
4621
  const type = (evt.agent_type ?? "").toLowerCase();
4592
4622
  const L = [];
4593
4623
  const served = [];
4624
+ const route = routedCwd
4625
+ ? `Worktree routing: call hunch_context first with cwd: ${JSON.stringify(routedCwd)}, and pass the same cwd to hunch_task, hunch_report, and every Hunch capture/write call in this delegated task.`
4626
+ : null;
4627
+ const activeProvider = provider;
4628
+ const emitRouteOnly = () => { if (route)
4629
+ emitContext(activeProvider, "SubagentStart", route); };
4594
4630
  if (/explore|search|investigat/.test(type)) {
4595
4631
  // Orient from the graph, not grep rounds: the component map IS the shape.
4596
4632
  const components = s.advisoryRecs("components").filter((c) => c.status === "active");
4597
- if (!components.length)
4633
+ if (!components.length) {
4634
+ emitRouteOnly();
4598
4635
  return;
4636
+ }
4599
4637
  L.push(`🧠 Hunch — repo shape for a delegated explorer: ${components.length} component(s).`);
4600
4638
  for (const c of components.slice(0, 12)) {
4601
4639
  const line = `- ${c.name}${c.paths.length ? ` (${c.paths.slice(0, 2).join(", ")})` : ""}${c.responsibility ? ` — ${clip1(c.responsibility, 90)}` : ""}`;
@@ -4611,8 +4649,10 @@ program
4611
4649
  const decisions = s.advisoryRecs("decisions")
4612
4650
  .filter((d) => d.status === "accepted")
4613
4651
  .sort((a, b) => (a.date < b.date ? 1 : -1));
4614
- if (!decisions.length)
4652
+ if (!decisions.length) {
4653
+ emitRouteOnly();
4615
4654
  return;
4655
+ }
4616
4656
  L.push(`🧠 Hunch — live decisions for a delegated planner (${decisions.length} in force; plans must not re-propose the rejected).`);
4617
4657
  for (const d of decisions.slice(0, 6)) {
4618
4658
  const line = `- ${d.title} (${d.id})${d.alternatives_rejected.length ? ` — rejected: ${clip1(d.alternatives_rejected[0], 80)}` : ""}`;
@@ -4626,8 +4666,10 @@ program
4626
4666
  const constraints = s.advisoryRecs("constraints")
4627
4667
  .filter((c) => c.status === "active")
4628
4668
  .sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
4629
- if (!constraints.length)
4669
+ if (!constraints.length) {
4670
+ emitRouteOnly();
4630
4671
  return;
4672
+ }
4631
4673
  L.push(`🧠 Hunch — delegated agent grounding: ${constraints.length} invariant(s) in force in this repo.`);
4632
4674
  for (const c of constraints.slice(0, 8)) {
4633
4675
  const line = `- [${c.severity}] ${clip1(c.statement, 140)}${c.scope.length ? ` (scope: ${c.scope.slice(0, 3).join(", ")})` : ""}`;
@@ -4638,6 +4680,8 @@ program
4638
4680
  L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
4639
4681
  L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
4640
4682
  }
4683
+ if (route)
4684
+ L.push(route);
4641
4685
  // No dedup here: the hook event carries the PARENT session id, but each
4642
4686
  // spawned agent is a fresh empty context — deduping would ground the
4643
4687
  // first Explore and silently starve every later one.
@@ -4900,7 +4944,7 @@ program
4900
4944
  // The first time a lesson reaches this prompt's task, tell the USER in one
4901
4945
  // line (systemMessage); repeats of the same revision stay silent.
4902
4946
  recalled = reportPresentationEnabled(root) ? renderRecalledLine(unseenLessons(root, reportTaskId, snapshots)) : null;
4903
- const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots);
4947
+ const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots, undefined, target);
4904
4948
  reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
4905
4949
  }
4906
4950
  catch {
@@ -7,6 +7,9 @@ export interface ResolvedInvocation {
7
7
  agentHookShell: string;
8
8
  /** Structured command/args for .mcp.json (subcommand appended by the writer). */
9
9
  mcp: Invocation;
10
+ /** True only when running from an installed/published copy (global, local,
11
+ * or npx cache), never from a source checkout. */
12
+ installed: boolean;
10
13
  }
11
14
  export declare function dim(s: string): string;
12
15
  /** Portable invocation written into committed MCP/provider configuration.
@@ -32,4 +35,9 @@ export declare function synthesisStatusLines(resolution: ProviderResolution, env
32
35
  * separate from synthesisStatusLines (sync, already fully covered) because
33
36
  * this one makes a best-effort network call. */
34
37
  export declare function maybeWarnOllamaContext(providerName: string, env: NodeJS.ProcessEnv): Promise<string | null>;
38
+ /** Classify the entry path without faking import.meta.url in tests. */
39
+ export declare function classifyEntry(entry: string): {
40
+ isDev: boolean;
41
+ installed: boolean;
42
+ };
35
43
  export declare function resolveInvocation(): ResolvedInvocation;
@@ -77,27 +77,32 @@ export async function maybeWarnOllamaContext(providerName, env) {
77
77
  return null;
78
78
  return probeOllamaNumCtx(env.HUNCH_SYNTH_BASE_URL ?? "", env.HUNCH_SYNTH_MODEL ?? "");
79
79
  }
80
+ /** Classify the entry path without faking import.meta.url in tests. */
81
+ export function classifyEntry(entry) {
82
+ const isDev = entry.endsWith(".ts");
83
+ const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
84
+ return { isDev, installed };
85
+ }
80
86
  export function resolveInvocation() {
81
87
  const entry = fileURLToPath(import.meta.url).replace(/invocation\.(js|ts)$/, "index.$1");
82
- const isDev = entry.endsWith(".ts");
88
+ const { isDev, installed } = classifyEntry(entry);
83
89
  // JSON.stringify yields a double-quoted, backslash-escaped token /bin/sh
84
90
  // accepts — so install paths with spaces don't break the hook command.
85
91
  const q = (s) => JSON.stringify(s);
86
- // Running from an installed copy (global, local, or npx cache i.e. NOT a
87
- // source checkout we're hacking on). The MCP/provider config files we write
88
- // are committed and shared across a team via git, so they must NOT embed this
89
- // machine's absolute path or OS-specific separators. Reference the exact
90
- // published Hunch package instead, which `npx` resolves the same on any OS
91
- // and any clone without floating to a newer release. The git hook lives in
92
- // per-machine .git/hooks (never committed), so it keeps the PATH-robust
93
- // absolute-node invocation below.
94
- const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
92
+ // The MCP/provider config files we write are committed and shared across a
93
+ // team via git, so they must NOT embed this machine's absolute path or
94
+ // OS-specific separators. Reference the exact published Hunch package
95
+ // instead, which `npx` resolves the same on any OS and any clone without
96
+ // floating to a newer release. The git hook lives in per-machine
97
+ // .git/hooks (never committed), so it keeps the PATH-robust absolute-node
98
+ // invocation below.
95
99
  if (installed) {
96
100
  const mcp = publishedMcpInvocation();
97
101
  return {
98
102
  shell: `${q(process.execPath)} ${q(entry)}`,
99
103
  agentHookShell: shellInvocation(mcp),
100
104
  mcp,
105
+ installed,
101
106
  };
102
107
  }
103
108
  if (isDev) {
@@ -106,6 +111,7 @@ export function resolveInvocation() {
106
111
  shell: `npx tsx ${q(entry)}`,
107
112
  agentHookShell: shellInvocation(mcp),
108
113
  mcp,
114
+ installed,
109
115
  };
110
116
  }
111
117
  // Source-checkout dist run (e.g. `node dist/cli/index.js`, npm link): inherently
@@ -116,6 +122,7 @@ export function resolveInvocation() {
116
122
  shell: `${q(process.execPath)} ${q(entry)}`,
117
123
  agentHookShell: shellInvocation(mcp),
118
124
  mcp,
125
+ installed,
119
126
  };
120
127
  }
121
128
  //# sourceMappingURL=invocation.js.map
@@ -7,6 +7,7 @@ import { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS, reportSourceSnapshot, r
7
7
  import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
8
8
  import { assertReportPath } from "../core/taskReportPaths.js";
9
9
  import { publicTaskReport } from "../core/taskReportPublic.js";
10
+ import { mergeDurableTaskSummaries, persistTaskRecord } from "../core/taskRecord.js";
10
11
  export function registerTaskReportCommands(program, openStore) {
11
12
  const task = program.command("task").description("Record an explicit task lifecycle for Hunch contribution reports");
12
13
  task.command("start <title>").option("--id <id>", "retry an exact existing task identity")
@@ -39,14 +40,44 @@ export function registerTaskReportCommands(program, openStore) {
39
40
  catch { /* disclosed as unverified */ }
40
41
  }
41
42
  finishReportTask(root, id, opts.interrupted ? "interrupted" : "completed");
42
- console.log(renderTaskReport(readTaskReport(root, id, reportSourceSnapshot(root).hash)));
43
+ // The finished task becomes graph memory (.hunch/tasks/) through the normal
44
+ // capture path. A failed write is disclosed, never a reason to lose the card.
45
+ let graph = "";
46
+ try {
47
+ const opened = openStore();
48
+ try {
49
+ const saved = persistTaskRecord(opened.root, opened.store, id);
50
+ graph = saved
51
+ ? `\nGraph ${saved.changed ? "saved" : "already saved"} as ${saved.record.id} (${saved.home}${saved.flushed ? `, ${saved.flushed}` : ""})`
52
+ : "\nGraph nothing to keep (no observation, or task records disabled)";
53
+ }
54
+ finally {
55
+ opened.store.close();
56
+ }
57
+ }
58
+ catch (error) {
59
+ graph = `\nGraph not saved: ${error.message}`;
60
+ }
61
+ console.log(renderTaskReport(readTaskReport(root, id, reportSourceSnapshot(root).hash)) + graph);
43
62
  });
44
63
  task.command("list").description("Recent tasks observed in this repository with what Hunch delivered, saved, guarded, and checked")
45
64
  .option("--limit <n>", "how many recent tasks (max 30)", "30")
46
65
  .option("--json", "machine-readable summaries (consumed by the VS Code Contribution view)")
47
66
  .action((opts) => {
48
67
  const root = findRoot();
49
- const summaries = listTaskSummaries(root, Number(opts.limit) || 30, reportSourceSnapshot(root).hash);
68
+ const limit = Number(opts.limit) || 30;
69
+ let summaries = listTaskSummaries(root, limit, reportSourceSnapshot(root).hash);
70
+ // Graph records (this machine's or a teammate's) join the local ledger view.
71
+ try {
72
+ const opened = openStore();
73
+ try {
74
+ summaries = mergeDurableTaskSummaries(opened.store, summaries, limit);
75
+ }
76
+ finally {
77
+ opened.store.close();
78
+ }
79
+ }
80
+ catch { /* ledger-only view when the store is unavailable */ }
50
81
  if (opts.json) {
51
82
  console.log(JSON.stringify(summaries, null, 2));
52
83
  return;
@@ -56,7 +87,7 @@ export function registerTaskReportCommands(program, openStore) {
56
87
  return;
57
88
  }
58
89
  for (const s of summaries)
59
- console.log(`${s.task.started_at.slice(0, 16).replace("T", " ")} ${s.task.task_id} ${s.task.state.padEnd(11)} ${renderTaskStatusLine(s) || "nothing observed"}`);
90
+ console.log(`${s.task.started_at.slice(0, 16).replace("T", " ")} ${s.task.task_id} ${s.task.state.padEnd(11)} ${renderTaskStatusLine(s) || "nothing observed"}${s.durable ? ` [graph: ${s.durable.home}]` : ""}`);
60
91
  });
61
92
  task.command("stats").description("Adherence over a window: how many prompts Hunch reached (delivery), checked, saved, or guarded — from the ledger, never from agent claims")
62
93
  .option("--days <days>", "window in days", "7")
@@ -188,7 +219,24 @@ export function registerTaskReportCommands(program, openStore) {
188
219
  console.log(JSON.stringify(publicTaskReport(root, id), null, 2));
189
220
  return;
190
221
  }
191
- const report = readTaskReport(root, id, reportSourceSnapshot(root).hash);
222
+ let report;
223
+ try {
224
+ report = readTaskReport(root, id, reportSourceSnapshot(root).hash);
225
+ }
226
+ catch (error) {
227
+ // Not in this machine's ledger: the graph record (if any) is what remains.
228
+ const opened = openStore();
229
+ try {
230
+ const record = opened.store.getRec("tasks", id);
231
+ if (!record)
232
+ throw error;
233
+ console.log(opts.json ? JSON.stringify(record, null, 2) : `Task ${record.id} · ${record.state} · ${record.title}\nGraph record only (no local observation ledger for it here): ${record.lessons.length} lesson(s), ${record.applied.length} applied, ${record.saved.length} saved, ${record.checks.length} check(s), ${record.refusals} denied. Files: ${record.files.join(", ") || "none recorded"}.`);
234
+ return;
235
+ }
236
+ finally {
237
+ opened.store.close();
238
+ }
239
+ }
192
240
  console.log(opts.json ? JSON.stringify(report, null, 2) : renderTaskReport(report));
193
241
  });
194
242
  }
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { findRoot } from "../core/paths.js";
5
- const PACKAGE = "@davesheffer/hunch";
5
+ import { HUNCH_PACKAGE_NAME } from "../core/version.js";
6
6
  /** Arguments come only from fixed commands and a validated registry version.
7
7
  * Windows needs the shell to resolve npm.cmd; cwd is never interpolated. */
8
8
  export function runNpm(root, args, capture = false) {
@@ -26,24 +26,24 @@ export function updateHunch(root, opts = {}, run = (args, capture) => runNpm(roo
26
26
  const manifest = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
27
27
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
28
28
  throw new Error("package.json must contain an object");
29
- if (manifest.name === PACKAGE)
29
+ if (manifest.name === HUNCH_PACKAGE_NAME)
30
30
  throw new Error("Run hunch update in a consumer repository, not Hunch's own source checkout.");
31
31
  const sections = ["dependencies", "devDependencies", "optionalDependencies"];
32
32
  const declared = sections.filter(section => {
33
33
  const deps = manifest[section];
34
34
  if (deps !== undefined && (!deps || typeof deps !== "object" || Array.isArray(deps)))
35
35
  throw new Error(`invalid ${section} in package.json`);
36
- return deps && Object.hasOwn(deps, PACKAGE);
36
+ return deps && Object.hasOwn(deps, HUNCH_PACKAGE_NAME);
37
37
  });
38
38
  if (declared.length > 1)
39
39
  throw new Error("Hunch is declared in multiple dependency sections; resolve the duplicate before updating.");
40
40
  if (declared.length && (manifest.workspaces || (manifest.packageManager && !/^npm@/.test(manifest.packageManager)) || ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].some(name => existsSync(join(root, name))))) {
41
41
  throw new Error("Automatic dependency updates currently support standalone npm projects. Update Hunch to an exact version with your package manager, then run hunch integrations repair-pins.");
42
42
  }
43
- const version = JSON.parse(run(["view", `${PACKAGE}@latest`, "version", "--json"], true));
43
+ const version = JSON.parse(run(["view", `${HUNCH_PACKAGE_NAME}@latest`, "version", "--json"], true));
44
44
  if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version))
45
45
  throw new Error("npm returned an invalid Hunch version");
46
- const spec = `${PACKAGE}@${version}`;
46
+ const spec = `${HUNCH_PACKAGE_NAME}@${version}`;
47
47
  const commands = [];
48
48
  if (declared.length) {
49
49
  const flag = { dependencies: "--save-prod", devDependencies: "--save-dev", optionalDependencies: "--save-optional" }[declared[0]];
@@ -178,7 +178,7 @@ export declare function createStateClient(opts: StateClientOptions): {
178
178
  schema: "nuryel.state.write/1";
179
179
  record_id: string;
180
180
  record_hash: string;
181
- durability: "committed" | "pushed" | "local";
181
+ durability: "local" | "committed" | "pushed";
182
182
  outcome: "updated" | "superseded" | "created" | "replayed";
183
183
  conflict: {
184
184
  incumbent_id: string;
@@ -190,7 +190,7 @@ export declare function createStateClient(opts: StateClientOptions): {
190
190
  schema: "nuryel.state.write/1";
191
191
  record_id: string;
192
192
  record_hash: string;
193
- durability: "committed" | "pushed" | "local";
193
+ durability: "local" | "committed" | "pushed";
194
194
  outcome: "updated" | "superseded" | "created" | "replayed";
195
195
  conflict: {
196
196
  incumbent_id: string;
@@ -207,7 +207,7 @@ export declare function createStateClient(opts: StateClientOptions): {
207
207
  schema: "nuryel.state.write/1";
208
208
  record_id: string;
209
209
  record_hash: string;
210
- durability: "committed" | "pushed" | "local";
210
+ durability: "local" | "committed" | "pushed";
211
211
  outcome: "updated" | "superseded" | "created" | "replayed";
212
212
  conflict: {
213
213
  incumbent_id: string;
@@ -228,7 +228,7 @@ export declare function createStateClient(opts: StateClientOptions): {
228
228
  schema: "nuryel.state.write/1";
229
229
  record_id: string;
230
230
  record_hash: string;
231
- durability: "committed" | "pushed" | "local";
231
+ durability: "local" | "committed" | "pushed";
232
232
  outcome: "updated" | "superseded" | "created" | "replayed";
233
233
  conflict: {
234
234
  incumbent_id: string;
@@ -370,13 +370,13 @@ export declare const PolicyStateSchema: z.ZodEnum<{
370
370
  proposed: "proposed";
371
371
  rejected: "rejected";
372
372
  superseded: "superseded";
373
+ repaired: "repaired";
374
+ active_advisory: "active_advisory";
375
+ active_blocking: "active_blocking";
373
376
  compiled: "compiled";
377
+ validating: "validating";
374
378
  uncompilable: "uncompilable";
375
379
  drafted: "drafted";
376
- validating: "validating";
377
- active_advisory: "active_advisory";
378
- active_blocking: "active_blocking";
379
- repaired: "repaired";
380
380
  }>;
381
381
  export type PolicyState = z.infer<typeof PolicyStateSchema>;
382
382
  export declare const PolicySelectorSchema: z.ZodObject<{
@@ -470,8 +470,8 @@ export declare const PolicyAuditEventSchema: z.ZodObject<{
470
470
  retired: "retired";
471
471
  withdrawn: "withdrawn";
472
472
  rejected: "rejected";
473
- compiled: "compiled";
474
473
  repaired: "repaired";
474
+ compiled: "compiled";
475
475
  enriched: "enriched";
476
476
  linked_exception: "linked_exception";
477
477
  proved: "proved";
@@ -518,13 +518,13 @@ export declare const PolicySpecSchema: z.ZodObject<{
518
518
  proposed: "proposed";
519
519
  rejected: "rejected";
520
520
  superseded: "superseded";
521
+ repaired: "repaired";
522
+ active_advisory: "active_advisory";
523
+ active_blocking: "active_blocking";
521
524
  compiled: "compiled";
525
+ validating: "validating";
522
526
  uncompilable: "uncompilable";
523
527
  drafted: "drafted";
524
- validating: "validating";
525
- active_advisory: "active_advisory";
526
- active_blocking: "active_blocking";
527
- repaired: "repaired";
528
528
  }>;
529
529
  statement: z.ZodString;
530
530
  rationale: z.ZodDefault<z.ZodString>;
@@ -610,10 +610,10 @@ export declare const PolicySpecSchema: z.ZodObject<{
610
610
  }>>;
611
611
  surfaces: z.ZodDefault<z.ZodArray<z.ZodEnum<{
612
612
  ci: "ci";
613
- pre_edit: "pre_edit";
614
- pre_commit: "pre_commit";
615
613
  mcp: "mcp";
616
614
  cli: "cli";
615
+ pre_edit: "pre_edit";
616
+ pre_commit: "pre_commit";
617
617
  }>>>;
618
618
  authority: z.ZodDefault<z.ZodNullable<z.ZodObject<{
619
619
  kind: z.ZodLiteral<"human">;
@@ -663,8 +663,8 @@ export declare const PolicySpecSchema: z.ZodObject<{
663
663
  retired: "retired";
664
664
  withdrawn: "withdrawn";
665
665
  rejected: "rejected";
666
- compiled: "compiled";
667
666
  repaired: "repaired";
667
+ compiled: "compiled";
668
668
  enriched: "enriched";
669
669
  linked_exception: "linked_exception";
670
670
  proved: "proved";
@@ -780,9 +780,9 @@ export declare const WriteResultSchema: z.ZodObject<{
780
780
  record_id: z.ZodString;
781
781
  record_hash: z.ZodString;
782
782
  durability: z.ZodEnum<{
783
+ local: "local";
783
784
  committed: "committed";
784
785
  pushed: "pushed";
785
- local: "local";
786
786
  }>;
787
787
  outcome: z.ZodEnum<{
788
788
  updated: "updated";
@@ -807,9 +807,9 @@ export declare const CaptureBatchResultSchema: z.ZodObject<{
807
807
  record_id: z.ZodString;
808
808
  record_hash: z.ZodString;
809
809
  durability: z.ZodEnum<{
810
+ local: "local";
810
811
  committed: "committed";
811
812
  pushed: "pushed";
812
- local: "local";
813
813
  }>;
814
814
  outcome: z.ZodEnum<{
815
815
  updated: "updated";
@@ -837,9 +837,9 @@ export declare const CaptureBatchResultSchema: z.ZodObject<{
837
837
  record_id: z.ZodString;
838
838
  record_hash: z.ZodString;
839
839
  durability: z.ZodEnum<{
840
+ local: "local";
840
841
  committed: "committed";
841
842
  pushed: "pushed";
842
- local: "local";
843
843
  }>;
844
844
  outcome: z.ZodEnum<{
845
845
  updated: "updated";
@@ -0,0 +1,39 @@
1
+ import type { HunchStore } from "../store/hunchStore.js";
2
+ import { type TaskReport, type TaskSummary } from "./taskReport.js";
3
+ import { type TaskRecord } from "./types.js";
4
+ export type TaskRecordHome = "public" | "private";
5
+ /** `taskRecords: false` in `.hunch/local.json` keeps tasks ledger-only. */
6
+ export declare function taskRecordsEnabled(root: string): boolean;
7
+ /** `taskRecordsFlush: "batch"` writes the record but leaves the commit to the
8
+ * next capture flush (decision, finding, correction), so a busy repository
9
+ * gets one memory commit per real capture instead of one per prompt. Default
10
+ * "each": every finished task commits like any other capture. */
11
+ export declare function taskRecordFlushMode(root: string): "each" | "batch";
12
+ /** A delivery target that names code (a path or dotted symbol), not a task
13
+ * phrase like "fix the login redirect". Phrases never become file anchors. */
14
+ export declare function targetLooksLikePath(target: string): boolean;
15
+ /** The durable summary of a finished report, or null when there is nothing to keep. */
16
+ export declare function taskRecordFromReport(report: TaskReport): TaskRecord | null;
17
+ /** Where the record belongs. Anything that touched the private overlay — a
18
+ * private save, or a delivered lesson that lives only there — must not be named
19
+ * in a public record; the store's own routing (unified/shared mode) wins first. */
20
+ export declare function taskRecordHome(store: HunchStore, record: TaskRecord): TaskRecordHome;
21
+ export interface PersistedTaskRecord {
22
+ record: TaskRecord;
23
+ home: TaskRecordHome;
24
+ flushed: "pushed" | "committed" | null;
25
+ /** False when the same report revision was already in the graph. */
26
+ changed: boolean;
27
+ }
28
+ /** Write (or refresh) the graph record for a finished task. Idempotent on the
29
+ * report hash. A record never changes home once written. Returns null for an
30
+ * open task, an empty report, or when task records are disabled locally. */
31
+ export declare function persistTaskRecord(root: string, store: HunchStore, taskId: string, options?: {
32
+ flush?: boolean;
33
+ }): PersistedTaskRecord | null;
34
+ /** A ledger-shaped summary for a task known only from the graph (another
35
+ * machine, a teammate, or a pruned local ledger). */
36
+ export declare function summaryFromTaskRecord(record: TaskRecord, home: TaskRecordHome): TaskSummary;
37
+ /** Ledger summaries annotated with their graph home, plus graph-only tasks the
38
+ * local ledger never saw. Newest first, bounded. */
39
+ export declare function mergeDurableTaskSummaries(store: HunchStore, summaries: TaskSummary[], limit?: number): TaskSummary[];