@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.
@@ -845,6 +845,10 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
845
845
  parts.push(`\nCOMPONENTS: ${w.components.map((c) => `${c.name} (${c.id})`).join(", ")}`);
846
846
  if (w.symbols.length)
847
847
  parts.push(`\nSYMBOLS: ${w.symbols.slice(0, WHY_CAP * 2).map((s) => `${s.name} [fan-in ${s.metrics.fan_in}, churn ${s.metrics.churn_90d}]`).join(", ")}${more(w.symbols.length, WHY_CAP * 2)}`);
848
+ const recentTasks = store.tasksFor(target, 5);
849
+ if (recentTasks.length) {
850
+ parts.push(`\nRECENT TASKS (agent work that touched this):\n${recentTasks.map((t) => ` • ${t.id} ${t.finished_at.slice(0, 10)} ${t.title} — ${t.lessons.length} lesson(s), ${t.applied.length} applied, ${t.saved.length} saved${t.conformance.some((c) => c.outcome === "violated") ? ", rule VIOLATED" : ""}`).join("\n")}`);
851
+ }
848
852
  if (parts.length === 1)
849
853
  parts.push("\n(No recorded decisions/bugs/constraints yet for this target.)");
850
854
  return ok(parts.join("\n"));
@@ -1071,7 +1075,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1071
1075
  const records = as_of ? [] : snapshotDeliveredRecords(store, envelope);
1072
1076
  // First delivery of a revision in this task earns one line; repeats stay quiet.
1073
1077
  const recalled = renderRecalledLine(unseenLessons(root, task_id, records));
1074
- const occurrence = recordTaskDelivery(root, task_id, envelope, records);
1078
+ const occurrence = recordTaskDelivery(root, task_id, envelope, records, undefined, target);
1075
1079
  result.content.push({ type: "text", text: `${recalled ? `${recalled}\n` : ""}Task evidence: ${task_id} · occurrence ${occurrence}.\n${records.slice(0, 20).map(r => `${r.record_id} @ ${r.content_hash}`).join("\n")}${records.length > 20 ? "\nMore record identities: hunch_report(task_id)." : ""}` });
1076
1080
  }
1077
1081
  catch {
@@ -17,7 +17,7 @@ export declare function boundedTaskReport(report: ReturnType<typeof readTaskRepo
17
17
  finished_at: string | null;
18
18
  state: "open" | "completed" | "interrupted";
19
19
  };
20
- coverage: "delivered" | "no-delivery-observed" | "no-relevant-memory";
20
+ coverage: "no-delivery-observed" | "no-relevant-memory" | "delivered";
21
21
  content_hash: string;
22
22
  unknowns: string[];
23
23
  deliveries: {
@@ -73,7 +73,7 @@ export declare function boundedTaskReport(report: ReturnType<typeof readTaskRepo
73
73
  title: string;
74
74
  home: "public" | "private";
75
75
  operation: "updated" | "created";
76
- durability: "committed" | "pushed" | "local";
76
+ durability: "local" | "committed" | "pushed";
77
77
  }[];
78
78
  refusals: ({
79
79
  source: "native-edit-gate";
@@ -106,7 +106,7 @@ export declare function boundedTaskReportForHost(report: ReturnType<typeof readT
106
106
  finished_at: string | null;
107
107
  state: "open" | "completed" | "interrupted";
108
108
  };
109
- coverage: "delivered" | "no-delivery-observed" | "no-relevant-memory";
109
+ coverage: "no-delivery-observed" | "no-relevant-memory" | "delivered";
110
110
  content_hash: string;
111
111
  unknowns: string[];
112
112
  deliveries: {
@@ -162,7 +162,7 @@ export declare function boundedTaskReportForHost(report: ReturnType<typeof readT
162
162
  title: string;
163
163
  home: "public" | "private";
164
164
  operation: "updated" | "created";
165
- durability: "committed" | "pushed" | "local";
165
+ durability: "local" | "committed" | "pushed";
166
166
  }[];
167
167
  refusals: ({
168
168
  source: "native-edit-gate";
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
4
+ import { persistTaskRecord } from "../core/taskRecord.js";
4
5
  import { reportSourceSnapshot, runReportConformance } from "../core/taskReportEvidence.js";
5
6
  import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
6
7
  function applicationReferences(report) {
@@ -73,7 +74,18 @@ export function registerTaskReportTools(server, getRoot, getStore) {
73
74
  if (action === "start") {
74
75
  if (!title || applications?.length || outcome)
75
76
  throw new Error("start requires a short task title and no completion evidence");
76
- const task = startReportTask(root, title, task_id);
77
+ let task;
78
+ try {
79
+ task = startReportTask(root, title, task_id);
80
+ }
81
+ catch (error) {
82
+ // A native prompt task already exists under this exact ID (opened by the
83
+ // host hook). Its persisted title is authoritative; a paraphrased title
84
+ // from the model must not fork a second report or fail the start.
85
+ if (!task_id || !/different title/.test(error.message))
86
+ throw error;
87
+ task = readTaskReport(root, task_id, reportSourceSnapshot(root).hash).task;
88
+ }
77
89
  const launcher = verificationLauncher();
78
90
  return { content: [{ type: "text", text: `Task ${task.task_id} · ${task.state}. Pass task_id to every hunch_context and decision/correction/finding capture call. Before the final response, finish with hunch_task and include its contribution card. For checks use this exact installation (the global hunch binary may be stale): ${launcher.shell} task verify ${task.task_id} -- <command> [arguments]. The default budget is 2 minutes; add --timeout <seconds> before -- for a long suite.` }], structuredContent: { task, verification_argv: [...launcher.argv, "task", "verify", task.task_id, "--"] } };
79
91
  }
@@ -90,6 +102,23 @@ export function registerTaskReportTools(server, getRoot, getStore) {
90
102
  catch { /* unknowns disclose it */ }
91
103
  }
92
104
  finishReportTask(root, task_id, outcome ?? "completed");
105
+ // The finished task becomes graph memory through the normal capture path;
106
+ // a failed write is disclosed on the card, never a reason to lose it.
107
+ let graph = null;
108
+ let graphNote = "";
109
+ try {
110
+ const saved = persistTaskRecord(root, getStore(), task_id);
111
+ if (saved) {
112
+ graph = { id: saved.record.id, home: saved.home, flushed: saved.flushed, changed: saved.changed };
113
+ graphNote = `\nGraph ${saved.changed ? "saved" : "already saved"} as ${saved.record.id} (${saved.home}${saved.flushed ? `, ${saved.flushed}` : ""})`;
114
+ }
115
+ else {
116
+ graphNote = "\nGraph nothing to keep (no observation, or task records disabled)";
117
+ }
118
+ }
119
+ catch (error) {
120
+ graphNote = `\nGraph not saved: ${error.message}`;
121
+ }
93
122
  const report = readTaskReport(root, task_id, reportSourceSnapshot(root).hash);
94
123
  const show = reportPresentationEnabled(root);
95
124
  let file = null;
@@ -97,8 +126,8 @@ export function registerTaskReportTools(server, getRoot, getStore) {
97
126
  file = writeTaskReportHtml(root, task_id);
98
127
  }
99
128
  catch { /* retained report remains inspectable through MCP */ }
100
- const card = file ? renderTaskReport(report).replace(/^Evidence .*$/m, `Evidence [Open local report](<${file}>)`) : renderTaskReport(report);
101
- return { content: [{ type: "text", text: show ? card : "Task report retained. Automatic presentation is disabled; omit the contribution card from the final response." }], structuredContent: { ...boundedTaskReportForHost(report), presentation_enabled: show, contribution_card: show ? card : null, report_path: file } };
129
+ const card = (file ? renderTaskReport(report).replace(/^Evidence .*$/m, `Evidence [Open local report](<${file}>)`) : renderTaskReport(report)) + graphNote;
130
+ return { content: [{ type: "text", text: show ? card : "Task report retained. Automatic presentation is disabled; omit the contribution card from the final response." }], structuredContent: { ...boundedTaskReportForHost(report), presentation_enabled: show, contribution_card: show ? card : null, report_path: file, graph_record: graph } };
102
131
  }
103
132
  catch (error) {
104
133
  const message = `Task report unavailable: ${error.message}`;
@@ -1,5 +1,5 @@
1
1
  import { type HunchPaths } from "../core/paths.js";
2
- import { type Component, type Constraint, type Bug, type Decision, type Symbol, type Edge, type Finding, type EntityKind, type EntityFor } from "../core/types.js";
2
+ import { type Component, type Constraint, type Bug, type Decision, type Symbol, type Edge, type Finding, type EntityKind, type EntityFor, type TaskRecord } from "../core/types.js";
3
3
  import { type DB } from "./db.js";
4
4
  import { type Embedder } from "./embedder.js";
5
5
  import { JsonStore } from "./jsonStore.js";
@@ -405,6 +405,10 @@ export declare class HunchStore {
405
405
  * glob, and the queried scope may be either too. Advisory only — findings never
406
406
  * enter any block path. Sorted worst-first, then id for stable output. */
407
407
  liveFindingsFor(scope: string): Finding[];
408
+ /** Finished agent tasks that touched a file or glob (rule-checked changes and
409
+ * denied edits), newest first. Graph memory, so it spans machines and survives
410
+ * the local ledger's retention window. */
411
+ tasksFor(scope: string, limit?: number): TaskRecord[];
408
412
  /** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
409
413
  * Deterministic graph join: constraint → source_decision (the decision that
410
414
  * motivated the guard) → the bug whose root cause spawned it (via
@@ -491,6 +491,14 @@ export class HunchStore {
491
491
  fts(f.id, "findings", f.title, `${f.observation} ${f.evidence.join(" ")} ${f.affected_files.join(" ")} ${f.affected_symbols.join(" ")} ${f.triage}`);
492
492
  }
493
493
  counts.findings = fnds.length;
494
+ // Tasks (finished agent work as graph memory): same FTS-only ride. Title +
495
+ // the lesson/save/application record ids and touched files, so "what did
496
+ // an agent do around X" and a record id both hit.
497
+ const tasks = this.recs("tasks");
498
+ for (const t of tasks) {
499
+ fts(t.id, "tasks", t.title, `${t.lessons.map((l) => `${l.record_id} ${l.title}`).join(" ")} ${t.applied.map((a) => a.record_id).join(" ")} ${t.saved.map((s) => s.record_id).join(" ")} ${t.files.join(" ")} ${t.state} ${t.coverage}`);
500
+ }
501
+ counts.tasks = tasks.length;
494
502
  // nuryel.state/1 kinds (receipts, commitments, derived, entities, relationships):
495
503
  // advisory records on the same FTS-only ride as runbooks/findings — no dedicated
496
504
  // SQL table. kind = the store kind; title = the subject key; body = the human words
@@ -1435,6 +1443,16 @@ export class HunchStore {
1435
1443
  || f.affected_symbols.some((s) => s === scope))
1436
1444
  .sort((a, b) => (SEV_FINDING[b.severity] ?? 0) - (SEV_FINDING[a.severity] ?? 0) || a.id.localeCompare(b.id));
1437
1445
  }
1446
+ /** Finished agent tasks that touched a file or glob (rule-checked changes and
1447
+ * denied edits), newest first. Graph memory, so it spans machines and survives
1448
+ * the local ledger's retention window. */
1449
+ tasksFor(scope, limit = 8) {
1450
+ const t = toPosixTarget(scope);
1451
+ return this.recs("tasks")
1452
+ .filter((r) => r.files.some((f) => pathMatchesGlob(t, f) || pathMatchesGlob(f, t) || pathsRelated(toPosixTarget(f), t)))
1453
+ .sort((a, b) => b.finished_at.localeCompare(a.finished_at) || a.id.localeCompare(b.id))
1454
+ .slice(0, Math.max(1, limit));
1455
+ }
1438
1456
  /** The causal chain behind a constraint — the WHY a diff-only reviewer can't see.
1439
1457
  * Deterministic graph join: constraint → source_decision (the decision that
1440
1458
  * motivated the guard) → the bug whose root cause spawned it (via
@@ -25,7 +25,7 @@ export declare function createTaskReporter(root: string): {
25
25
  /** The caller supplies the exact envelope it issued plus snapshots of the
26
26
  * included revisions. Return the occurrence with the context to the agent.
27
27
  * Issuance alone does not prove the model applied or even attended to it. */
28
- delivered(taskId: string, envelope: DeliveryEnvelope, records: ReportRecord[], occurrenceId?: string): string;
28
+ delivered(taskId: string, envelope: DeliveryEnvelope, records: ReportRecord[], occurrenceId?: string, target?: string): string;
29
29
  applied(taskId: string, claim: ReportClaim): string;
30
30
  /** Runs locally as argv, without a shell. Only use commands authorized by
31
31
  * the task owner. This API does not accept remote claimed-success receipts. */
@@ -1,6 +1,5 @@
1
1
  /** Provider-neutral, local engine integration. The authorized harness owns
2
2
  * lifecycle and display; neither operation depends on model compliance. */
3
- import { realpathSync } from "node:fs";
4
3
  import { z } from "zod";
5
4
  import { finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, recordTaskDelivery, reportHash, reportPresentationEnabled, startReportTask } from "./core/taskReport.js";
6
5
  import { DEFAULT_CHECK_TIMEOUT_MS, reportSourceSnapshot, runReportCheck, runReportConformance } from "./core/taskReportEvidence.js";
@@ -10,12 +9,14 @@ import { hunchPaths } from "./core/paths.js";
10
9
  import { HunchStore } from "./store/hunchStore.js";
11
10
  export { TASK_REPORT_SCHEMA, TaskIdSchema } from "./core/taskReport.js";
12
11
  export { renderTaskReport, renderTaskReportHtml } from "./core/taskReportRender.js";
12
+ import { canonicalReportRoot } from "./core/taskReportPaths.js";
13
+ import { persistTaskRecord } from "./core/taskRecord.js";
13
14
  const IdentitySchema = z.object({ task: z.string().min(1).max(1024), attempt: z.string().min(1).max(1024) }).strict();
14
15
  /** Create this only after authorizing a local repository/worktree. A task ID is
15
16
  * a correlation reference, never an access token. Remote adapters must enforce
16
17
  * their own principal/partition boundary before reaching this local API. */
17
18
  export function createTaskReporter(root) {
18
- const scope = realpathSync(root);
19
+ const scope = canonicalReportRoot(root);
19
20
  const report = (taskId) => readTaskReport(scope, taskId, reportSourceSnapshot(scope).hash);
20
21
  return {
21
22
  /** Omit identity for a fresh task, or supply the harness's stable task AND
@@ -28,8 +29,8 @@ export function createTaskReporter(root) {
28
29
  /** The caller supplies the exact envelope it issued plus snapshots of the
29
30
  * included revisions. Return the occurrence with the context to the agent.
30
31
  * Issuance alone does not prove the model applied or even attended to it. */
31
- delivered(taskId, envelope, records, occurrenceId) {
32
- return recordTaskDelivery(scope, taskId, envelope, records, occurrenceId);
32
+ delivered(taskId, envelope, records, occurrenceId, target) {
33
+ return recordTaskDelivery(scope, taskId, envelope, records, occurrenceId, target);
33
34
  },
34
35
  applied(taskId, claim) { return recordReportClaim(scope, taskId, claim); },
35
36
  /** Runs locally as argv, without a shell. Only use commands authorized by
@@ -57,6 +58,17 @@ export function createTaskReporter(root) {
57
58
  catch { /* the report's unknowns disclose it */ }
58
59
  }
59
60
  finishReportTask(scope, taskId, outcome);
61
+ // Graph memory of the finished task; disclosed in the report, never blocking.
62
+ try {
63
+ const store = new HunchStore(hunchPaths(scope));
64
+ try {
65
+ persistTaskRecord(scope, store, taskId);
66
+ }
67
+ finally {
68
+ store.close();
69
+ }
70
+ }
71
+ catch { /* the ledger keeps the observations; the graph write is retried on the next finish */ }
60
72
  const result = report(taskId);
61
73
  return { report: result, contribution_card: reportPresentationEnabled(scope) ? renderTaskReport(result) : null };
62
74
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.33.0",
10
+ "version": "1.35.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.33.0",
16
+ "version": "1.35.0",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {