@davesheffer/hunch 1.32.2 → 1.32.3

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
@@ -183,7 +183,7 @@ Hunch may hold durable state **about** external work with credential-free proven
183
183
 
184
184
  The first real-world pilot is **Sofia**, a working operations agent over CRM, Gmail and WhatsApp. Sofia's approved actions, follow-ups, customer/source relationships and cited summaries map naturally to action receipts, commitments, entities/relationships and dependency-bound state.
185
185
 
186
- The pilot measures whether Sofia and a second, different agent stop re-deriving contradictory state when the deterministic state is delivered before they answer or act.
186
+ The pilot measures whether Sofia and a second, different agent stop re-deriving contradictory state when the deterministic state is delivered before they answer or act. First live number (2026-09-12, one user): after one CRM read, the next status questions were answered from held state under a receipt, 0 of 3 without a source; the [roadmap](ROADMAP.md) carries the gate table.
187
187
 
188
188
  ### The state contract, shipped
189
189
 
@@ -223,7 +223,7 @@ The current release can keep a team's **repository-scoped** memory in a dedicate
223
223
  Today Hunch does not host that shared Git repository; teammates/CI use normal Git access and one maintainer connects it:
224
224
 
225
225
  ```bash
226
- npm i -g @davesheffer/hunch@1.28.0
226
+ npm i -g @davesheffer/hunch@1.32.2
227
227
  hunch shared --repo git@github.com:acme/project-hunch-memory.git
228
228
  git add .gitignore .hunch/team.json
229
229
  git commit -m "chore: connect shared Hunch memory"
@@ -233,7 +233,7 @@ git push
233
233
  Teammates then install the same version and run:
234
234
 
235
235
  ```bash
236
- npm i -g @davesheffer/hunch@1.28.0
236
+ npm i -g @davesheffer/hunch@1.32.2
237
237
  git pull
238
238
  hunch init
239
239
  hunch doctor
@@ -264,6 +264,8 @@ The v1.19 correction-search benchmark remains scoped evidence, but it no longer
264
264
  - **v1.21 — Project DNA.** Hunch gained deterministic, revision-specific repository profiles, bounded DNA context delivery, explainable Project Match checks and auditable profile deltas.
265
265
  - **v1.22 — authorized collaboration evidence.** Hosts can contribute bounded PR/review evidence to Project DNA through a typed, sealed contract without raw collaboration persistence or policy-authority change.
266
266
  - **v1.23 — native change proof and proof-carrying evidence work.** Exact Git change identity, graph before/after, decisions/constraints, blast radius and Change Gate result can be bound into a sealed evidence artifact without granting authority.
267
+ - **v1.25 – v1.30 — the state layer.** `nuryel.state/1` as code, `hunch serve` partitions, union reads, subject identity by external reference, replay determinism, human corrections outranking agent writes.
268
+ - **v1.32 — see what Hunch contributed.** Task contribution reports: what was delivered, what the agent reports it applied, what a rule verified, what a command observed; concise card, local evidence view.
267
269
 
268
270
  See the [changelog](CHANGELOG.md) for release detail and the [roadmap](ROADMAP.md) for active work.
269
271
 
@@ -277,6 +279,9 @@ See the [changelog](CHANGELOG.md) for release detail and the [roadmap](ROADMAP.m
277
279
  - [Project DNA](docs/project-dna.md)
278
280
  - [Native change proof](docs/change-proof.md)
279
281
  - [The autonomy ladder](docs/autonomy-ladder.md)
282
+ - [Task contribution reports](docs/task-reports.md)
283
+ - [Autonomous development](docs/autonomous-development.md)
284
+ - [Local cookbook](docs/cookbook.md)
280
285
  - [Engineering Landscape Graph](docs/engineering-landscape.md)
281
286
  - [Hunch roadmap](ROADMAP.md)
282
287
  - [VS Code extension](vscode-extension/README.md)
@@ -961,6 +961,9 @@ export declare const STATE_INVARIANTS: readonly [{
961
961
  }, {
962
962
  readonly id: "derived-state-writer-owns-currentness";
963
963
  readonly statement: "No source writes the drawer. The writer of a current derived statement owns keeping its dependencies true: re-validate them on a schedule or on a source event, and write the statement back stale with the moved pointer as cause when one no longer holds. Without this duty an agent may capture source-backed observations only as unknown; observations never assert currentness.";
964
+ }, {
965
+ readonly id: "one-current-derived-per-subject-transform";
966
+ readonly statement: "A subject holds at most one current derived statement per transform: a new one must name the incumbent in supersedes (refused 409 with the incumbent named otherwise); the same identity written again updates or replays that record.";
964
967
  }];
965
968
  /** The memory supply chain's top tier: a record whose provenance a human signed. Same tier rule
966
969
  * as the strict gate's (strictgate.isHumanConfirmed), applied to a record instead of a source. */
@@ -306,6 +306,7 @@ export const STATE_INVARIANTS = [
306
306
  { id: "one-entity-per-external-ref", statement: "One external record is one entity in a partition: a second active entity carrying an external key an incumbent already carries is refused with the incumbent named, and a subject written as that record's external key is refused with the entity's id named. Identity is explicit refs, never similarity; merge is explicit — a retired entity names the survivor in `merged_into`, the ledger holds the `retired` event, nothing under the old id is rewritten and reads resolve to the survivor — and split is the explicit reverse; never a silent rewrite." },
307
307
  { id: "human-correction-outranks-agent-writes", statement: "A record a human confirmed is never overwritten or superseded by an agent or service principal: the agent may replay it, write derived state back stale with the external cause that moved, or close a commitment with a receipt on record. Changing what the human said takes a human." },
308
308
  { id: "derived-state-writer-owns-currentness", statement: "No source writes the drawer. The writer of a current derived statement owns keeping its dependencies true: re-validate them on a schedule or on a source event, and write the statement back stale with the moved pointer as cause when one no longer holds. Without this duty an agent may capture source-backed observations only as unknown; observations never assert currentness." },
309
+ { id: "one-current-derived-per-subject-transform", statement: "A subject holds at most one current derived statement per transform: a new one must name the incumbent in supersedes (refused 409 with the incumbent named otherwise); the same identity written again updates or replays that record." },
309
310
  ];
310
311
  const grantKey = (scope) => scopePath(scope);
311
312
  /** The memory supply chain's top tier: a record whose provenance a human signed. Same tier rule
@@ -509,6 +509,14 @@ const READ_REMOTE_TIMEOUT_MS = 5_000;
509
509
  // drains every already-durable JSON write itself. This removes the old "maybe a
510
510
  // third capture sweeps it later" liveness hole.
511
511
  const CAPTURE_LOCK_HANDOFF_MS = 120_000;
512
+ /** Longest one git call inside a memory flush may take before it is stopped and the flush
513
+ * reports durability "local" (HUNCH_COMMIT_GIT_TIMEOUT_MS overrides; tests use a short one). */
514
+ const COMMIT_GIT_TIMEOUT_MS = 60_000;
515
+ const SLOW_FLUSH_MS = 5_000;
516
+ function commitGitTimeoutMs() {
517
+ const raw = Number(process.env.HUNCH_COMMIT_GIT_TIMEOUT_MS);
518
+ return Number.isFinite(raw) && raw > 0 ? raw : COMMIT_GIT_TIMEOUT_MS;
519
+ }
512
520
  function unsafeOverlayPublication(hunchDir, protectedRepoRoot) {
513
521
  let currentOverlayRoot = dirname(resolve(hunchDir));
514
522
  try {
@@ -566,10 +574,21 @@ export function commitAndPushHunch(hunchDir, message, opts) {
566
574
  for (let attempt = 0; attempt < 2; attempt++) {
567
575
  const startedAt = Date.now();
568
576
  try {
569
- execFileSync("git", ["-C", hunchDir, ...args], { stdio: "ignore", env });
577
+ // A served write blocks on this call: bound it, and never let a commit trigger git's
578
+ // automatic gc (minutes of repacking inside one write, fnd_4318727d35). A timed-out
579
+ // call returns false, the flush reports durability "local", and the next flush
580
+ // sweeps the same files up — nothing is lost, and the server is not frozen.
581
+ execFileSync("git", ["-C", hunchDir, "-c", "gc.auto=0", ...args], { stdio: "ignore", env, timeout: commitGitTimeoutMs() });
582
+ const took = Date.now() - startedAt;
583
+ if (took > SLOW_FLUSH_MS)
584
+ console.error(`hunch: git ${args.find((a) => !a.startsWith("-") && a !== "core.autocrlf=false") ?? args[0]} in "${hunchDir}" took ${took} ms`);
570
585
  return true;
571
586
  }
572
587
  catch (error) {
588
+ if (error.signal === "SIGTERM" || error.code === "ETIMEDOUT") {
589
+ console.error(`hunch: git ${args.find((a) => !a.startsWith("-")) ?? args[0]} in "${hunchDir}" exceeded ${commitGitTimeoutMs()} ms and was stopped; the write stays local until the next flush`);
590
+ return false;
591
+ }
573
592
  // best-effort: nothing staged / not a repo / offline — EXCEPT a
574
593
  // stranded index.lock, which would otherwise fail every future
575
594
  // flush silently (issue #53); heal it and retry once.
@@ -623,7 +642,7 @@ export function commitAndPushHunch(hunchDir, message, opts) {
623
642
  const staged = stagedMemoryPaths(hunchDir, env, opts.push !== false);
624
643
  if (staged === null) {
625
644
  try {
626
- execFileSync("git", ["-C", hunchDir, "reset", "-q", "--", "."], { stdio: "ignore", env });
645
+ execFileSync("git", ["-C", hunchDir, "reset", "-q", "--", "."], { stdio: "ignore", env, timeout: commitGitTimeoutMs() });
627
646
  }
628
647
  catch { /* best-effort unstage */ }
629
648
  // Public-store commits (push:false) skip QUIETLY: a non-memory staged set there is
@@ -691,8 +710,9 @@ export function commitAndPushHunch(hunchDir, message, opts) {
691
710
  ...(opts.push === false ? [] : ["-c", `core.attributesFile=${gitNullDevice()}`]),
692
711
  "-c", "core.autocrlf=false",
693
712
  "-c", "commit.gpgsign=false",
713
+ "-c", "gc.auto=0",
694
714
  "commit", "--no-gpg-sign", "--only", "-m", message, "--", ...commitPaths,
695
- ], { stdio: "ignore", env, timeout: 15_000 });
715
+ ], { stdio: "ignore", env, timeout: commitGitTimeoutMs() });
696
716
  committed = true;
697
717
  }
698
718
  catch (error) {
@@ -775,7 +795,7 @@ function stagedMemoryPaths(hunchDir, env, allowMemoryDeletions = false) {
775
795
  let out = "";
776
796
  let prefix = "";
777
797
  try {
778
- prefix = execFileSync("git", ["-C", hunchDir, "rev-parse", "--show-prefix"], { encoding: "utf8", env }).trim().replace(/\\/g, "/");
798
+ prefix = execFileSync("git", ["-C", hunchDir, "rev-parse", "--show-prefix"], { encoding: "utf8", env, timeout: commitGitTimeoutMs() }).trim().replace(/\\/g, "/");
779
799
  }
780
800
  catch {
781
801
  return null;
@@ -786,7 +806,7 @@ function stagedMemoryPaths(hunchDir, env, allowMemoryDeletions = false) {
786
806
  // heuristic rename presentation so the exact paths remain independently
787
807
  // auditable against the contained-memory rules below.
788
808
  try {
789
- out = execFileSync("git", ["-C", hunchDir, "diff", "--cached", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-status"], { encoding: "utf8", env });
809
+ out = execFileSync("git", ["-C", hunchDir, "diff", "--cached", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-status"], { encoding: "utf8", env, timeout: commitGitTimeoutMs() });
790
810
  }
791
811
  catch {
792
812
  return null;
@@ -806,6 +806,22 @@ export function writeState(store, input, opts = {}) {
806
806
  supersedes = null; // already closed by this record: nothing to close again, no second "superseded" event
807
807
  }
808
808
  }
809
+ // one-current-derived-per-subject-transform: a NEW current derived statement on a subject that
810
+ // already holds a current statement under the same transform must name it in `supersedes`.
811
+ // Otherwise a writer that never names its predecessor leaves a growing pile of "current"
812
+ // statements that every reader has to reconcile (season finding fnd_1939ced249: up to 58 on
813
+ // one subject over half a year). Writing the same identity again is an update or a replay of
814
+ // that record and is not affected; a different transform is a different statement.
815
+ if (facet === "derived" && record.state === "current") {
816
+ const d = record;
817
+ const incumbent = store.recsInHome("derived", home).find((r) => {
818
+ const x = r;
819
+ return x.id !== id && x.id !== supersedes && x.subject === d.subject && x.transform_version === d.transform_version && x.state === "current" && x.valid_to === null;
820
+ });
821
+ if (incumbent) {
822
+ throw new StateRefusal("conflict", `${d.subject} already has a current ${d.transform_version} statement ${incumbent.id}; pass supersedes: "${incumbent.id}" to replace it, or write that identity to update it`, { incumbent_id: incumbent.id, reason: "one-current-derived-per-subject-transform" });
823
+ }
824
+ }
809
825
  // The chain (Gate 4): a receipt names what it rested on, a closure names the receipt.
810
826
  // Both are checked against the drawer, grants first, before anything lands.
811
827
  if (facet === "receipts")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.32.2",
3
+ "version": "1.32.3",
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.32.2",
10
+ "version": "1.32.3",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.32.2",
16
+ "version": "1.32.3",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {