@davesheffer/hunch 1.46.0 → 1.47.1
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/dist/cli/index.js +280 -358
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.d.ts +2 -0
- package/dist/core/correction.js +23 -10
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskRecord.d.ts +1 -1
- package/dist/core/taskRecord.js +2 -2
- package/dist/core/taskReport.d.ts +8 -3
- package/dist/core/taskReport.js +7 -2
- package/dist/core/taskReportEvidence.d.ts +7 -0
- package/dist/core/taskReportEvidence.js +16 -4
- package/dist/core/taskReportHook.js +2 -2
- package/dist/core/taskReportPublic.js +2 -2
- package/dist/core/taskReportRender.js +2 -2
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +18 -23
- package/dist/extractors/git.js +72 -64
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.js +1 -1
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/integrations/probe.d.ts +11 -0
- package/dist/integrations/probe.js +29 -4
- package/dist/integrations/sync.d.ts +16 -4
- package/dist/integrations/sync.js +54 -3
- package/dist/integrations/workspaceLedger.d.ts +1 -1
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +71 -83
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/core/paths.js
CHANGED
|
@@ -3,12 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
export const HUNCH_DIR = ".hunch";
|
|
6
|
-
/**
|
|
7
|
-
* path with "/"
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* repo-relative — an absolute path passes through with its separators flipped,
|
|
6
|
+
/** Convert a user- or agent-supplied path/target to forward-slash form. Hunch
|
|
7
|
+
* stores every path with "/" (git emits it on every OS), so a Windows-style
|
|
8
|
+
* backslash path would otherwise never match the stored form. Harmless on
|
|
9
|
+
* symbol names, which carry no backslashes. It does NOT make a path
|
|
10
|
+
* repo-relative: an absolute path only has its separators flipped and is left
|
|
12
11
|
* unchanged otherwise; use `repoRelativeTarget` for that.
|
|
13
12
|
*
|
|
14
13
|
* Inherently ambiguous for a string like `docs/notes\notes.md`: it could be a
|
|
@@ -21,47 +20,42 @@ export const HUNCH_DIR = ".hunch";
|
|
|
21
20
|
export function toPosixTarget(target) {
|
|
22
21
|
return target.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* to
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
/** Canonical real path of `p`, tolerant of paths that do not exist yet (a file
|
|
24
|
+
* about to be written, a glob with non-literal segments): the longest existing
|
|
25
|
+
* ancestor is resolved through the native realpath and the missing trailing
|
|
26
|
+
* segments are re-appended unchanged. Terminates at the filesystem root,
|
|
27
|
+
* is idempotent on a resolved path and never throws for a missing one. */
|
|
28
28
|
export function realpathNorm(p) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const tail = [];
|
|
30
|
+
let cur = p;
|
|
31
|
+
for (;;) {
|
|
32
|
+
try {
|
|
33
|
+
const real = realpathSync.native(cur);
|
|
34
|
+
return tail.length ? join(real, ...tail.reverse()) : real;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
const parent = dirname(cur);
|
|
38
|
+
if (parent === cur)
|
|
39
|
+
return p;
|
|
40
|
+
tail.push(basename(cur));
|
|
41
|
+
cur = parent;
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
|
-
/** Rewrite an ABSOLUTE target to repo-relative
|
|
40
|
-
* `root`. Hunch
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* BOTH ends are realpath-normalized before the relative-path computation: on
|
|
48
|
-
* macOS `process.cwd()` (hence `findRoot`) resolves /var → /private/var, but a
|
|
49
|
-
* hook event's `file_path` arrives UN-resolved, so a naive `relative()` yields a
|
|
50
|
-
* bogus "../" path under a symlinked root (/var, /tmp, symlinked $HOME) and the
|
|
51
|
-
* caller treats the file as outside the repo, silently dropping all context
|
|
52
|
-
* (dec_e0a36efbf5). */
|
|
45
|
+
/** Rewrite an ABSOLUTE `target` to repo-relative forward-slash form when it lies
|
|
46
|
+
* inside `root`. Stored Hunch paths are always repo-relative, so an absolute
|
|
47
|
+
* path lifted from an edit payload otherwise matches nothing (issue #296).
|
|
48
|
+
* Both ends go through `realpathNorm`, so a symlinked root (macOS /var vs
|
|
49
|
+
* /private/var, /tmp, a symlinked home) cancels out (dec_e0a36efbf5). Anything
|
|
50
|
+
* with no safe repo-relative form (the root itself, an escape, a drive-letter
|
|
51
|
+
* remainder) is returned in its normalized form unchanged. */
|
|
53
52
|
export function repoRelativeTarget(target, root) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// so an un-posix'd root and a posix'd target would decompose into differently-shaped
|
|
61
|
-
// ancestor chains and never share a common realpath'd prefix.
|
|
62
|
-
const rel = toPosixTarget(relative(realpathNorm(toPosixTarget(root)), realpathNorm(t)));
|
|
63
|
-
if (!rel || rel === ".." || rel.startsWith("../") || isAbsolute(rel) || /^[a-zA-Z]:/.test(rel))
|
|
64
|
-
return t;
|
|
53
|
+
const norm = toPosixTarget(target);
|
|
54
|
+
if (!isAbsolute(norm) && !/^[A-Za-z]:/.test(norm))
|
|
55
|
+
return norm;
|
|
56
|
+
const rel = relative(realpathNorm(toPosixTarget(root)), realpathNorm(norm)).replace(/\\/g, "/");
|
|
57
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel) || /^[A-Za-z]:/.test(rel))
|
|
58
|
+
return norm;
|
|
65
59
|
return rel;
|
|
66
60
|
}
|
|
67
61
|
/** True when `target` (already repo-relative POSIX form) names a regular FILE
|
package/dist/core/refrepair.d.ts
CHANGED
|
@@ -12,10 +12,11 @@ export interface DecisionReferenceRepair {
|
|
|
12
12
|
relatedFiles: number;
|
|
13
13
|
evidence: number;
|
|
14
14
|
}
|
|
15
|
-
export
|
|
15
|
+
export type ExactReplacement = {
|
|
16
16
|
values: string[];
|
|
17
17
|
changed: number;
|
|
18
18
|
};
|
|
19
|
+
export declare function replaceExact(refs: string[], target: string, replacement: string): ExactReplacement;
|
|
19
20
|
/**
|
|
20
21
|
* Return a corrected copy of a decision, or `null` when the source reference is
|
|
21
22
|
* not present. The decision's semantic content and verification timestamp are
|
package/dist/core/refrepair.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function replaceExact(
|
|
1
|
+
export function replaceExact(refs, target, replacement) {
|
|
2
2
|
let changed = 0;
|
|
3
|
-
const replaced =
|
|
4
|
-
if (value !==
|
|
3
|
+
const replaced = refs.map((value) => {
|
|
4
|
+
if (value !== target)
|
|
5
5
|
return value;
|
|
6
6
|
changed++;
|
|
7
|
-
return
|
|
7
|
+
return replacement;
|
|
8
8
|
});
|
|
9
9
|
// A decision may already cite the destination. Keep the reference list a set
|
|
10
10
|
// after the repair so a correction cannot create duplicate scope/evidence.
|
|
@@ -1,40 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
* is local scratch state, never a source of truth worth failing loudly over. */
|
|
1
|
+
import type { CommitRewrite, DroppedRewrite } from "./commitrepair.js";
|
|
2
|
+
/** The raw queue. Missing, corrupt or non-array files read as `[]`; never throws. */
|
|
4
3
|
export declare function readPendingRepairs(root: string): CommitRewrite[];
|
|
4
|
+
/** Atomically replace the queue. Writing `[]` clears it. */
|
|
5
5
|
export declare function writePendingRepairs(root: string, rewrites: readonly CommitRewrite[]): void;
|
|
6
|
-
/**
|
|
6
|
+
/** The tombstones of rewrites rejected with `--drop`. Same tolerance as the queue. */
|
|
7
7
|
export declare function readDroppedRepairs(root: string): DroppedRewrite[];
|
|
8
|
+
/** Atomically replace the tombstone file. Writing `[]` clears it. */
|
|
8
9
|
export declare function writeDroppedRepairs(root: string, dropped: readonly DroppedRewrite[]): void;
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
* `repair-provenance`'s own action is the one exception: it needs the RAW
|
|
16
|
-
* read (via readPendingRepairs) because it diffs raw-vs-swept to report what
|
|
17
|
-
* it cleaned up, and it persists the sweep back to disk. Every other
|
|
18
|
-
* reader — `hunch escalations`, SessionStart orientation, the `hunch_now`
|
|
19
|
-
* and `hunch_escalations` MCP tools — must call this instead, or a
|
|
20
|
-
* tombstoned entry that hasn't yet been swept by a `repair-provenance` run
|
|
21
|
-
* re-surfaces as if the human never answered it. */
|
|
10
|
+
/**
|
|
11
|
+
* The raw queue minus anything tombstoned. Read-only: it never changes either
|
|
12
|
+
* file. Every read-only consumer uses this -- `hunch escalations`, SessionStart
|
|
13
|
+
* orientation, and the `hunch_now` and `hunch_escalations` MCP tools. Only
|
|
14
|
+
* `repair-provenance` itself reads the raw queue.
|
|
15
|
+
*/
|
|
22
16
|
export declare function readActivePendingRepairs(root: string): CommitRewrite[];
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
* `--apply --only <id>` that's guaranteed to no-op forever. `null` from
|
|
30
|
-
* commitsExist (the check itself failed to run) is treated as fail-open —
|
|
31
|
-
* same discipline as the apply path — so nothing is misreported as
|
|
32
|
-
* withheld just because the check couldn't run.
|
|
33
|
-
*
|
|
34
|
-
* Returns the OBJECTS, not their ids, and every caller must pass the exact
|
|
35
|
-
* same `queued` array into both this function and whatever it hands the
|
|
36
|
-
* result to (commitRepairEscalations) — an id-keyed set would collapse a
|
|
37
|
-
* corrupted queue's two same-id entries (one resolvable, one not) into one
|
|
38
|
-
* unit, exactly the identity confusion withheldForUnresolvableTo's own
|
|
39
|
-
* docstring warns about. */
|
|
17
|
+
/**
|
|
18
|
+
* The queued entries whose proposed `to` does not resolve in this repository.
|
|
19
|
+
* Fails open (nothing withheld) when git cannot answer. The set holds the
|
|
20
|
+
* caller's own objects, so callers must pass the same `queued` array here and to
|
|
21
|
+
* whatever consumes the result.
|
|
22
|
+
*/
|
|
40
23
|
export declare function withheldRewrites(root: string, queued: readonly CommitRewrite[]): Set<CommitRewrite>;
|
package/dist/core/repairqueue.js
CHANGED
|
@@ -1,131 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local-only queue of commit-provenance repairs
|
|
3
|
-
* (typically by the post-merge hook, src/integrations/hooks.ts) but not yet
|
|
4
|
-
* applied. `.hunch/pending-commit-repairs.json` is gitignored the same way
|
|
5
|
-
* `.hunch/local.json` is — never committed, never pushed. It exists because
|
|
6
|
-
* the opportunistic detection window is narrow: `ORIG_HEAD` gets overwritten
|
|
7
|
-
* by the next merge, and the matched-away commit can eventually be gc'd —
|
|
8
|
-
* queuing the exact {id, from, to} triple here lets a human confirm the match
|
|
9
|
-
* later (`hunch repair-provenance --apply`) without losing it.
|
|
2
|
+
* Local-only queue of detected-but-not-yet-applied commit-provenance repairs.
|
|
10
3
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* It is filled opportunistically, usually by the post-merge hook, and is
|
|
5
|
+
* gitignored like `.hunch/local.json` -- never committed. It exists because the
|
|
6
|
+
* detection window is narrow: `ORIG_HEAD` gets overwritten and the matched-away
|
|
7
|
+
* commit can be garbage-collected. A human confirms later with
|
|
8
|
+
* `hunch repair-provenance --apply`. A sibling tombstone file records the exact
|
|
9
|
+
* `{id, from, to}` triples rejected with `--drop`, so re-detection does not
|
|
10
|
+
* re-queue them.
|
|
18
11
|
*/
|
|
19
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
13
|
import { join } from "node:path";
|
|
21
14
|
import { writeFileAtomic } from "./io.js";
|
|
22
15
|
import { withoutDropped, withheldForUnresolvableTo } from "./commitrepair.js";
|
|
23
16
|
import { commitsExist } from "../extractors/git.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
const queuePath = (root) => join(root, ".hunch", "pending-commit-repairs.json");
|
|
18
|
+
const droppedPath = (root) => join(root, ".hunch", "dropped-commit-repairs.json");
|
|
19
|
+
const nonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
20
|
+
/**
|
|
21
|
+
* Shape-only validation: non-empty string id/from/to, and `to !== from` (a no-op
|
|
22
|
+
* is rejected). There is deliberately no hex check -- a bad `to` is caught later
|
|
23
|
+
* by the apply-time existence check, so this loader stays shape-only.
|
|
24
|
+
*/
|
|
25
|
+
function isRewrite(v) {
|
|
26
|
+
if (typeof v !== "object" || v === null)
|
|
32
27
|
return false;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
// and a no-op entry would apply "successfully" while accomplishing nothing.
|
|
36
|
-
// Deliberately NOT a hex-shape check: from/to are opaque strings elsewhere
|
|
37
|
-
// in this codebase's test fixtures, and repairDecisionCommit's own
|
|
38
|
-
// `d.commit === mine.from` guard already makes a garbage `from` inert (it
|
|
39
|
-
// can never match a real decision). A garbage `to` is caught elsewhere,
|
|
40
|
-
// not here: `hunch repair-provenance --apply` runs commitsExist against
|
|
41
|
-
// every `to` it's actually about to act on (the whole queue, or just the
|
|
42
|
-
// one entry `--only <id>` targets) before repairDecisionCommit ever sees
|
|
43
|
-
// it, and leaves a non-resolving entry queued rather than applying it
|
|
44
|
-
// (withheldForUnresolvableTo, src/core/commitrepair.ts) — with the caveat
|
|
45
|
-
// that commitsExist itself fails OPEN if the check can't run at all (not a
|
|
46
|
-
// git repo, git missing, timeout), same as drift.ts's own use of it. This
|
|
47
|
-
// filter stays shape-only on purpose — the existence check needs a
|
|
48
|
-
// repository to run against, which this read-only queue loader doesn't
|
|
49
|
-
// have.
|
|
50
|
-
return typeof id === "string" && !!id
|
|
51
|
-
&& typeof from === "string" && !!from
|
|
52
|
-
&& typeof to === "string" && !!to
|
|
53
|
-
&& to !== from;
|
|
28
|
+
const r = v;
|
|
29
|
+
return nonEmpty(r.id) && nonEmpty(r.from) && nonEmpty(r.to) && r.to !== r.from;
|
|
54
30
|
}
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
31
|
+
function isDropped(v) {
|
|
32
|
+
if (typeof v !== "object" || v === null)
|
|
57
33
|
return false;
|
|
58
|
-
const
|
|
59
|
-
return
|
|
60
|
-
&& typeof from === "string" && !!from
|
|
61
|
-
&& typeof to === "string" && !!to;
|
|
34
|
+
const r = v;
|
|
35
|
+
return nonEmpty(r.id) && nonEmpty(r.from) && nonEmpty(r.to);
|
|
62
36
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export function readPendingRepairs(root) {
|
|
66
|
-
const path = queuePath(root);
|
|
67
|
-
if (!existsSync(path))
|
|
37
|
+
function readList(file, guard) {
|
|
38
|
+
if (!existsSync(file))
|
|
68
39
|
return [];
|
|
69
40
|
try {
|
|
70
|
-
const parsed = JSON.parse(readFileSync(
|
|
71
|
-
return Array.isArray(parsed) ? parsed.filter(
|
|
41
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
42
|
+
return Array.isArray(parsed) ? parsed.filter(guard) : [];
|
|
72
43
|
}
|
|
73
44
|
catch {
|
|
74
45
|
return [];
|
|
75
46
|
}
|
|
76
47
|
}
|
|
48
|
+
/** The raw queue. Missing, corrupt or non-array files read as `[]`; never throws. */
|
|
49
|
+
export function readPendingRepairs(root) {
|
|
50
|
+
return readList(queuePath(root), isRewrite);
|
|
51
|
+
}
|
|
52
|
+
/** Atomically replace the queue. Writing `[]` clears it. */
|
|
77
53
|
export function writePendingRepairs(root, rewrites) {
|
|
78
54
|
writeFileAtomic(queuePath(root), JSON.stringify(rewrites, null, 2) + "\n");
|
|
79
55
|
}
|
|
80
|
-
/**
|
|
56
|
+
/** The tombstones of rewrites rejected with `--drop`. Same tolerance as the queue. */
|
|
81
57
|
export function readDroppedRepairs(root) {
|
|
82
|
-
|
|
83
|
-
if (!existsSync(path))
|
|
84
|
-
return [];
|
|
85
|
-
try {
|
|
86
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
87
|
-
return Array.isArray(parsed) ? parsed.filter(isDroppedRewrite) : [];
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
58
|
+
return readList(droppedPath(root), isDropped);
|
|
92
59
|
}
|
|
60
|
+
/** Atomically replace the tombstone file. Writing `[]` clears it. */
|
|
93
61
|
export function writeDroppedRepairs(root, dropped) {
|
|
94
62
|
writeFileAtomic(droppedPath(root), JSON.stringify(dropped, null, 2) + "\n");
|
|
95
63
|
}
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
* `repair-provenance`'s own action is the one exception: it needs the RAW
|
|
103
|
-
* read (via readPendingRepairs) because it diffs raw-vs-swept to report what
|
|
104
|
-
* it cleaned up, and it persists the sweep back to disk. Every other
|
|
105
|
-
* reader — `hunch escalations`, SessionStart orientation, the `hunch_now`
|
|
106
|
-
* and `hunch_escalations` MCP tools — must call this instead, or a
|
|
107
|
-
* tombstoned entry that hasn't yet been swept by a `repair-provenance` run
|
|
108
|
-
* re-surfaces as if the human never answered it. */
|
|
64
|
+
/**
|
|
65
|
+
* The raw queue minus anything tombstoned. Read-only: it never changes either
|
|
66
|
+
* file. Every read-only consumer uses this -- `hunch escalations`, SessionStart
|
|
67
|
+
* orientation, and the `hunch_now` and `hunch_escalations` MCP tools. Only
|
|
68
|
+
* `repair-provenance` itself reads the raw queue.
|
|
69
|
+
*/
|
|
109
70
|
export function readActivePendingRepairs(root) {
|
|
110
71
|
return withoutDropped(readPendingRepairs(root), readDroppedRepairs(root));
|
|
111
72
|
}
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
* `--apply --only <id>` that's guaranteed to no-op forever. `null` from
|
|
119
|
-
* commitsExist (the check itself failed to run) is treated as fail-open —
|
|
120
|
-
* same discipline as the apply path — so nothing is misreported as
|
|
121
|
-
* withheld just because the check couldn't run.
|
|
122
|
-
*
|
|
123
|
-
* Returns the OBJECTS, not their ids, and every caller must pass the exact
|
|
124
|
-
* same `queued` array into both this function and whatever it hands the
|
|
125
|
-
* result to (commitRepairEscalations) — an id-keyed set would collapse a
|
|
126
|
-
* corrupted queue's two same-id entries (one resolvable, one not) into one
|
|
127
|
-
* unit, exactly the identity confusion withheldForUnresolvableTo's own
|
|
128
|
-
* docstring warns about. */
|
|
73
|
+
/**
|
|
74
|
+
* The queued entries whose proposed `to` does not resolve in this repository.
|
|
75
|
+
* Fails open (nothing withheld) when git cannot answer. The set holds the
|
|
76
|
+
* caller's own objects, so callers must pass the same `queued` array here and to
|
|
77
|
+
* whatever consumes the result.
|
|
78
|
+
*/
|
|
129
79
|
export function withheldRewrites(root, queued) {
|
|
130
80
|
const existing = commitsExist(queued.map((r) => r.to), root);
|
|
131
81
|
return new Set(withheldForUnresolvableTo(queued, existing).withheld);
|
|
@@ -35,7 +35,7 @@ export declare function computeSupersedes(record: TaskRecord, others: readonly T
|
|
|
35
35
|
export interface PersistedTaskRecord {
|
|
36
36
|
record: TaskRecord;
|
|
37
37
|
home: TaskRecordHome;
|
|
38
|
-
flushed: "pushed" | "committed" | null;
|
|
38
|
+
flushed: "pushed" | "committed" | "held" | null;
|
|
39
39
|
/** False when the same report revision was already in the graph. */
|
|
40
40
|
changed: boolean;
|
|
41
41
|
}
|
package/dist/core/taskRecord.js
CHANGED
|
@@ -15,7 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { flushCapture } from "../integrations/sync.js";
|
|
16
16
|
import { hunchPaths } from "./paths.js";
|
|
17
17
|
import { episodeTasks, isEmptyTaskReport, readTaskReport, reportHash, sessionsOverlap } from "./taskReport.js";
|
|
18
|
-
import {
|
|
18
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
19
19
|
import { ENTITY_KINDS, TaskRecordSchema } from "./types.js";
|
|
20
20
|
import { refreshRankEval } from "./taskRankingMode.js";
|
|
21
21
|
import { gitTouchedFiles } from "./taskTouched.js";
|
|
@@ -172,7 +172,7 @@ export function computeSupersedes(record, others, limit = 20) {
|
|
|
172
172
|
export function persistTaskRecord(root, store, taskId, options = {}) {
|
|
173
173
|
if (!taskRecordsEnabled(root))
|
|
174
174
|
return null;
|
|
175
|
-
const snapshot =
|
|
175
|
+
const snapshot = currentSource(root);
|
|
176
176
|
const own = readTaskReport(root, taskId, snapshot);
|
|
177
177
|
if (own.task.state === "open")
|
|
178
178
|
return null;
|
|
@@ -30,6 +30,7 @@ export declare const ReportCheckSchema: z.ZodObject<{
|
|
|
30
30
|
snapshot_limitations: z.ZodArray<z.ZodString>;
|
|
31
31
|
timed_out: z.ZodBoolean;
|
|
32
32
|
cancelled: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
work_root: z.ZodOptional<z.ZodString>;
|
|
33
34
|
source: z.ZodLiteral<"local-command-runner">;
|
|
34
35
|
}, z.core.$strict>;
|
|
35
36
|
export type ReportCheck = z.infer<typeof ReportCheckSchema>;
|
|
@@ -321,8 +322,8 @@ export interface TaskSummary {
|
|
|
321
322
|
}
|
|
322
323
|
/** One bounded summary per recent task for status lines and host views; the
|
|
323
324
|
* card and evidence view remain the authoritative renderings. */
|
|
324
|
-
export declare function summarizeTaskReport(root: string, taskId: string, currentSnapshot?:
|
|
325
|
-
export declare function listTaskSummaries(root: string, limit?: number, currentSnapshot?:
|
|
325
|
+
export declare function summarizeTaskReport(root: string, taskId: string, currentSnapshot?: CurrentSnapshot): TaskSummary;
|
|
326
|
+
export declare function listTaskSummaries(root: string, limit?: number, currentSnapshot?: CurrentSnapshot): TaskSummary[];
|
|
326
327
|
/** One line for a terminal status line. Empty string when nothing was observed
|
|
327
328
|
* for the task, so a bare prompt shows no Hunch noise at all. */
|
|
328
329
|
export declare function renderTaskStatusLine(summary: TaskSummary | null): string;
|
|
@@ -348,5 +349,9 @@ export declare function reportActivity(root: string): string;
|
|
|
348
349
|
export declare function forgetReportTask(root: string, taskId: string): void;
|
|
349
350
|
export declare function pruneReportHistory(root: string, olderThanDays?: number): number;
|
|
350
351
|
export declare function reportPresentationEnabled(root: string): boolean;
|
|
351
|
-
|
|
352
|
+
/** What a reader compares evidence snapshots against: the report root's current source
|
|
353
|
+
* hash, or a resolver that also hashes another worktree a check ran in. A plain hash
|
|
354
|
+
* never vouches for a check that ran elsewhere. */
|
|
355
|
+
export type CurrentSnapshot = string | null | ((dir?: string) => string | null);
|
|
356
|
+
export declare function readTaskReport(root: string, taskId: string, currentSnapshot?: CurrentSnapshot): TaskReport;
|
|
352
357
|
export {};
|
package/dist/core/taskReport.js
CHANGED
|
@@ -43,6 +43,9 @@ export const ReportCheckSchema = z.object({
|
|
|
43
43
|
snapshot_limitations: z.array(z.string().max(300)).max(16),
|
|
44
44
|
timed_out: z.boolean(),
|
|
45
45
|
cancelled: z.boolean().optional(),
|
|
46
|
+
/** Where the command ran, recorded only when that is not this ledger's root (a
|
|
47
|
+
* sibling worktree): its currentness is judged against that tree's source. */
|
|
48
|
+
work_root: z.string().min(1).max(4_096).optional(),
|
|
46
49
|
source: z.literal("local-command-runner"),
|
|
47
50
|
}).strict();
|
|
48
51
|
export const ReportSaveSchema = z.object({
|
|
@@ -717,6 +720,7 @@ export function reportPresentationEnabled(root) {
|
|
|
717
720
|
}
|
|
718
721
|
}
|
|
719
722
|
export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
723
|
+
const snapshotAt = (dir) => typeof currentSnapshot === "function" ? currentSnapshot(dir) : dir === undefined ? currentSnapshot : null;
|
|
720
724
|
const { task, events } = taskDb(root, db => transaction(db, () => {
|
|
721
725
|
const task = readTask(db, root, taskId);
|
|
722
726
|
const { bytes } = db.prepare("SELECT COALESCE(SUM(length(CAST(body AS BLOB))), 0) AS bytes FROM report_events WHERE task_id = ?").get(taskId);
|
|
@@ -743,7 +747,7 @@ export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
|
743
747
|
claims.push({ ...ReportClaimSchema.parse(value), at: event.at, attribution: "agent-reported", supported_by: null });
|
|
744
748
|
else if (event.kind === "conformance") {
|
|
745
749
|
const rule = ReportConformanceSchema.parse(value);
|
|
746
|
-
conformance.push({ ...rule, event_id: event.event_id, at: event.at, current:
|
|
750
|
+
conformance.push({ ...rule, event_id: event.event_id, at: event.at, current: snapshotAt() !== null && rule.snapshot === snapshotAt() });
|
|
747
751
|
}
|
|
748
752
|
else if (event.kind === "save")
|
|
749
753
|
saves.push({ ...ReportSaveSchema.parse(value), event_id: event.event_id, at: event.at, durability: "local", proofs: [] });
|
|
@@ -760,7 +764,8 @@ export function readTaskReport(root, taskId, currentSnapshot = null) {
|
|
|
760
764
|
refusals.push({ ...ReportRefusalSchema.parse(value), event_id: event.event_id, at: event.at });
|
|
761
765
|
else if (event.kind === "check") {
|
|
762
766
|
const check = ReportCheckSchema.parse(value);
|
|
763
|
-
|
|
767
|
+
const now = snapshotAt(check.work_root);
|
|
768
|
+
checks.push({ ...check, at: event.at, current: now !== null && check.before_snapshot === now && check.after_snapshot === now });
|
|
764
769
|
}
|
|
765
770
|
else if (event.kind !== "check-start")
|
|
766
771
|
throw new Error("unsupported report event kind; update Hunch before reading this report");
|
|
@@ -9,6 +9,9 @@ export interface ReportSnapshot {
|
|
|
9
9
|
/** Bounded source snapshot, not a committed change proof. Ignored files and
|
|
10
10
|
* Hunch's own memory/cache are excluded and explicitly disclosed. */
|
|
11
11
|
export declare function reportSourceSnapshot(root: string): ReportSnapshot;
|
|
12
|
+
/** The current source of `root` for report readers, as a resolver that also hashes the
|
|
13
|
+
* sibling worktree a check ran in (its `work_root`). Each tree is hashed at most once. */
|
|
14
|
+
export declare function currentSource(root: string): (dir?: string) => string | null;
|
|
12
15
|
/** Evaluate every delivered lesson's DECLARED rule against the files this task
|
|
13
16
|
* changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
|
|
14
17
|
* neither chooses the rule nor submits the verdict. A constraint is judged by its
|
|
@@ -28,4 +31,8 @@ export declare function runReportCheck(root: string, taskId: string, command: st
|
|
|
28
31
|
signal?: AbortSignal;
|
|
29
32
|
onStdout?: (chunk: Buffer) => void;
|
|
30
33
|
onStderr?: (chunk: Buffer) => void;
|
|
34
|
+
/** Where the command runs and whose source is snapshotted, when the task's
|
|
35
|
+
* ledger lives in a sibling worktree of the same repository (the hook opened
|
|
36
|
+
* the task at the main checkout, the agent works in a linked one). */
|
|
37
|
+
workRoot?: string;
|
|
31
38
|
}): Promise<ReportCheck>;
|
|
@@ -87,6 +87,16 @@ export function reportSourceSnapshot(root) {
|
|
|
87
87
|
return { hash: null, limitations: [...limitations, "A complete bounded source snapshot was unavailable; no current-change verification is claimed."] };
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/** The current source of `root` for report readers, as a resolver that also hashes the
|
|
91
|
+
* sibling worktree a check ran in (its `work_root`). Each tree is hashed at most once. */
|
|
92
|
+
export function currentSource(root) {
|
|
93
|
+
const seen = new Map();
|
|
94
|
+
return (dir = root) => {
|
|
95
|
+
if (!seen.has(dir))
|
|
96
|
+
seen.set(dir, reportSourceSnapshot(dir).hash);
|
|
97
|
+
return seen.get(dir);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
90
100
|
/** Evaluate every delivered lesson's DECLARED rule against the files this task
|
|
91
101
|
* changed (working tree vs HEAD). Deterministic and Hunch-owned: the agent
|
|
92
102
|
* neither chooses the rule nor submits the verdict. A constraint is judged by its
|
|
@@ -210,7 +220,8 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
210
220
|
const task = readTaskReport(root, taskId).task;
|
|
211
221
|
if (task.state !== "open" && task.closed_by !== "host")
|
|
212
222
|
throw new Error("cannot verify a closed task");
|
|
213
|
-
const
|
|
223
|
+
const workRoot = options.workRoot ?? root;
|
|
224
|
+
const before = reportSourceSnapshot(workRoot);
|
|
214
225
|
// Validate sensitive arguments before executing or writing anything.
|
|
215
226
|
ReportCheckSchema.parse({ label, command, exit_code: null, output_hash: reportHash(""), before_snapshot: before.hash, after_snapshot: null, snapshot_limitations: before.limitations, timed_out: false, source: "local-command-runner" });
|
|
216
227
|
const checkId = beginReportCheck(root, taskId, label, timeoutMs);
|
|
@@ -228,7 +239,7 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
228
239
|
const started = (() => {
|
|
229
240
|
try {
|
|
230
241
|
const resolved = resolveSpawnCommand(command);
|
|
231
|
-
return spawn(resolved.file, resolved.args, { cwd:
|
|
242
|
+
return spawn(resolved.file, resolved.args, { cwd: workRoot, shell: false, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, detached: process.platform !== "win32", windowsVerbatimArguments: resolved.windowsVerbatimArguments === true });
|
|
232
243
|
}
|
|
233
244
|
catch (error) {
|
|
234
245
|
// An argument the launcher cannot carry, or a synchronous spawn refusal
|
|
@@ -319,9 +330,10 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = D
|
|
|
319
330
|
if (options.signal?.aborted)
|
|
320
331
|
cancel();
|
|
321
332
|
});
|
|
322
|
-
const after = reportSourceSnapshot(
|
|
333
|
+
const after = reportSourceSnapshot(workRoot);
|
|
323
334
|
const check = { check_id: checkId, label, command, exit_code: result.code, output_hash: result.hash, before_snapshot: before.hash, after_snapshot: after.hash,
|
|
324
|
-
snapshot_limitations: [...new Set([...before.limitations, ...after.limitations])], timed_out: result.timedOut, cancelled: result.cancelled,
|
|
335
|
+
snapshot_limitations: [...new Set([...before.limitations, ...after.limitations])], timed_out: result.timedOut, cancelled: result.cancelled,
|
|
336
|
+
...(workRoot !== root ? { work_root: workRoot } : {}), source: "local-command-runner" };
|
|
325
337
|
recordReportCheck(root, taskId, check);
|
|
326
338
|
return check;
|
|
327
339
|
}
|
|
@@ -6,7 +6,7 @@ import { findRoot } from "./paths.js";
|
|
|
6
6
|
import { canonicalReportRoot } from "./taskReportPaths.js";
|
|
7
7
|
import { isCredentialFreeText } from "./types.js";
|
|
8
8
|
import { aliasReportTask, continuationLinks, finishReportTask, isEmptyTaskReport, latestSessionTask, readTaskReport, recordReportRefusal, reportHash, reportPresentationEnabled, reportTaskExists, resolveReportTask, settleSessionTasks, startReportTask } from "./taskReport.js";
|
|
9
|
-
import {
|
|
9
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
10
10
|
import { renderTaskReport } from "./taskReportRender.js";
|
|
11
11
|
import { verificationLauncher } from "./verifyLauncher.js";
|
|
12
12
|
import { injectionMode } from "./hookcache.js";
|
|
@@ -290,7 +290,7 @@ export function stopHookReport(root, provider, event) {
|
|
|
290
290
|
if (id === "legacy")
|
|
291
291
|
return { systemMessage: "Hunch hook active. This host version does not provide an exact prompt identifier, so contribution for this response is unverified. Explicit task reports remain available with hunch report." };
|
|
292
292
|
try {
|
|
293
|
-
const report = readTaskReport(root, id,
|
|
293
|
+
const report = readTaskReport(root, id, currentSource(root));
|
|
294
294
|
if (isEmptyTaskReport(report))
|
|
295
295
|
return null;
|
|
296
296
|
// The HTML evidence view is a rendering of the local ledger, generated on
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { JsonStore } from "../store/jsonStore.js";
|
|
4
4
|
import { hunchPaths } from "./paths.js";
|
|
5
5
|
import { readTaskReport, reportHash } from "./taskReport.js";
|
|
6
|
-
import {
|
|
6
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
7
7
|
export function publicTaskReport(root, taskId) {
|
|
8
|
-
const report = readTaskReport(root, taskId,
|
|
8
|
+
const report = readTaskReport(root, taskId, currentSource(root));
|
|
9
9
|
const publicStore = new JsonStore(hunchPaths(root));
|
|
10
10
|
const records = new Map();
|
|
11
11
|
const deliveries = [];
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { writeFileAtomic } from "./io.js";
|
|
3
3
|
import { readTaskReport, readLessonHistory } from "./taskReport.js";
|
|
4
|
-
import {
|
|
4
|
+
import { currentSource } from "./taskReportEvidence.js";
|
|
5
5
|
import { assertReportPath } from "./taskReportPaths.js";
|
|
6
6
|
import { publicTaskReport, renderPublicTaskReportHtml } from "./taskReportPublic.js";
|
|
7
7
|
export function writeTaskReportHtml(root, taskId, publicOnly = false) {
|
|
8
|
-
const report = readTaskReport(root, taskId,
|
|
8
|
+
const report = readTaskReport(root, taskId, currentSource(root));
|
|
9
9
|
const directory = assertReportPath(root, ".hunch-cache", "reports");
|
|
10
10
|
mkdirSync(directory, { recursive: true });
|
|
11
11
|
const file = assertReportPath(root, ".hunch-cache", "reports", `${report.task.task_id}${publicOnly ? ".public" : ""}.html`);
|