@davesheffer/hunch 1.47.0 → 1.47.2
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 +146 -46
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- package/dist/core/correction.d.ts +2 -0
- package/dist/core/correction.js +15 -1
- package/dist/core/subagentrole.d.ts +6 -0
- package/dist/core/subagentrole.js +32 -0
- 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/extractors/git.d.ts +10 -0
- package/dist/extractors/git.js +27 -0
- package/dist/integrations/health.js +1 -1
- 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.js +11 -10
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- package/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Whether a delegated agent can call Hunch's MCP tools. A named role defined in
|
|
5
|
+
* `.claude/agents/<type>.md` (project first, then user) with a `tools:` list that
|
|
6
|
+
* names no `mcp__hunch` tool and no wildcard cannot: telling it to call
|
|
7
|
+
* hunch_why only costs it a failed lookup. Built-in and unknown types inherit
|
|
8
|
+
* the parent's tools. */
|
|
9
|
+
export function roleHasHunchTools(root, agentType, home = homedir()) {
|
|
10
|
+
const type = agentType.trim();
|
|
11
|
+
if (!type || !/^[\w.-]+$/.test(type))
|
|
12
|
+
return true;
|
|
13
|
+
for (const dir of [join(root, ".claude", "agents"), join(home, ".claude", "agents")]) {
|
|
14
|
+
let text;
|
|
15
|
+
try {
|
|
16
|
+
text = readFileSync(join(dir, `${type}.md`), "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const front = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
22
|
+
if (!front)
|
|
23
|
+
return true;
|
|
24
|
+
// A block list continues on indented "- x" lines; a flow list runs to its "]".
|
|
25
|
+
const tools = /^tools:[ \t]*(\[[^\]]*\]?|.*(?:\r?\n[ \t]+-.*)*)/m.exec(front)?.[1];
|
|
26
|
+
if (tools === undefined || !tools.trim())
|
|
27
|
+
return true;
|
|
28
|
+
return /\*|mcp__hunch/.test(tools);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=subagentrole.js.map
|
|
@@ -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`);
|
package/dist/extractors/git.d.ts
CHANGED
|
@@ -266,6 +266,16 @@ export declare function pathKnownToHistory(root: string, file: string): boolean;
|
|
|
266
266
|
/** Current branch name (e.g. "main", "feat/x"), or "" in detached HEAD / non-repo.
|
|
267
267
|
* Stamped onto auto-captured decisions so branch-scoped work stays filterable. */
|
|
268
268
|
export declare function currentBranch(cwd: string): string;
|
|
269
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
270
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
271
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
272
|
+
export declare function onDefaultBranch(cwd: string): boolean;
|
|
273
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
274
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
275
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
276
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
277
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
278
|
+
export declare function hunchDirHasChanges(hunchDir: string): boolean;
|
|
269
279
|
/** Whether HEAD is somewhere a write must not land: mid rebase/merge/cherry-pick/revert/bisect
|
|
270
280
|
* ("git-operation-in-progress", which wins when both hold), or detached ("detached-head").
|
|
271
281
|
* null when HEAD is a settled branch — and also when git cannot answer at all (not a repo,
|
package/dist/extractors/git.js
CHANGED
|
@@ -2283,6 +2283,33 @@ export function currentBranch(cwd) {
|
|
|
2283
2283
|
const b = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2284
2284
|
return b === "HEAD" ? "" : b; // detached HEAD reports "HEAD" — treat as no branch
|
|
2285
2285
|
}
|
|
2286
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
2287
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
2288
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
2289
|
+
export function onDefaultBranch(cwd) {
|
|
2290
|
+
const branch = currentBranch(cwd);
|
|
2291
|
+
if (!branch)
|
|
2292
|
+
return false;
|
|
2293
|
+
const remoteHead = gitSafe(["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
2294
|
+
if (remoteHead)
|
|
2295
|
+
return branch === remoteHead.replace(/^origin\//, "");
|
|
2296
|
+
return branch === "main" || branch === "master";
|
|
2297
|
+
}
|
|
2298
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
2299
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
2300
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
2301
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
2302
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
2303
|
+
export function hunchDirHasChanges(hunchDir) {
|
|
2304
|
+
const changed = [
|
|
2305
|
+
...gitSafeIsolated(["diff", "--name-only", "--relative", "--no-renames", "HEAD", "--", "."], hunchDir).split("\n"),
|
|
2306
|
+
...gitSafeIsolated(["ls-files", "--others", "--exclude-standard", "--", "."], hunchDir).split("\n"),
|
|
2307
|
+
];
|
|
2308
|
+
return changed.some((raw) => {
|
|
2309
|
+
const path = raw.trim().replace(/\\/g, "/");
|
|
2310
|
+
return path !== "" && path !== "local.json" && !isDerivedStoreArtifact(path);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2286
2313
|
/** The state files git writes while it is replaying history. `git rev-parse --git-path`
|
|
2287
2314
|
* resolves each against THIS worktree's private git dir (a linked worktree keeps its own
|
|
2288
2315
|
* rebase-merge/MERGE_HEAD), so a rebase in one worktree never reads as one in another. */
|
|
@@ -197,7 +197,7 @@ export function inspectIntegrations(root, selected) {
|
|
|
197
197
|
try {
|
|
198
198
|
const launcher = readLauncher(root, harness);
|
|
199
199
|
recordPins(spec.mcp, [launcher.command, ...launcher.args]);
|
|
200
|
-
capabilities.mcp.detail =
|
|
200
|
+
capabilities.mcp.detail = `Configured locally; run \`hunch integrations check --probe --harness ${harness}\` to verify a fresh server, then reconnect the host`;
|
|
201
201
|
}
|
|
202
202
|
catch (e) {
|
|
203
203
|
// A harness can be detected here via its hooks file alone —
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type Harness, type IntegrationHealth } from "./health.js";
|
|
2
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
3
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
4
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
5
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
6
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
7
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
8
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
9
|
+
export declare function isSelfLauncher(launcher: {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}, entry?: string): boolean;
|
|
2
13
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
3
14
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
4
15
|
export declare function probeIntegration(root: string, harness: Harness, report: IntegrationHealth, timeoutMs?: number): Promise<void>;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { sameFilesystemEntry } from "../extractors/git.js";
|
|
3
7
|
import { HARNESSES, readLauncher } from "./health.js";
|
|
8
|
+
/** A launcher that runs THIS Hunch build directly (`node <build>/cli/index.js mcp`) —
|
|
9
|
+
* a local checkout or an unpacked test build. It is exactly the code already
|
|
10
|
+
* running the probe, so starting it executes nothing new; any other custom
|
|
11
|
+
* command stays inspect-only. The server identity/version check still applies.
|
|
12
|
+
* The command is a bare `node` (PATH lookup) or this exact Node binary — a
|
|
13
|
+
* directory-qualified `./tools/node` in a cloned repo is not, whatever its name —
|
|
14
|
+
* and the probe spawns process.execPath itself, never the configured name. */
|
|
15
|
+
export function isSelfLauncher(launcher, entry = fileURLToPath(new URL("../cli/index.js", import.meta.url))) {
|
|
16
|
+
const bareNode = /^node(?:\.exe)?$/i.test(launcher.command);
|
|
17
|
+
const node = bareNode || (isAbsolute(launcher.command) && existsSync(launcher.command) && sameFilesystemEntry(launcher.command, process.execPath));
|
|
18
|
+
if (!node || launcher.args.length !== 2 || launcher.args[1] !== "mcp")
|
|
19
|
+
return false;
|
|
20
|
+
const script = launcher.args[0];
|
|
21
|
+
// Generated launchers name the build entry absolutely; a relative script would resolve
|
|
22
|
+
// against whatever cwd the host spawns in.
|
|
23
|
+
return isAbsolute(script) && existsSync(script) && existsSync(entry) && sameFilesystemEntry(script, entry);
|
|
24
|
+
}
|
|
4
25
|
/** Opt-in: starts a fresh configured MCP process, never claims the current
|
|
5
26
|
* harness connection or hook delivery has been tested. No model is invoked. */
|
|
6
27
|
export async function probeIntegration(root, harness, report, timeoutMs = 15_000) {
|
|
@@ -16,11 +37,15 @@ export async function probeIntegration(root, harness, report, timeoutMs = 15_000
|
|
|
16
37
|
throw new Error("custom MCP environment or working directory requires a host-side probe; no settings were ignored");
|
|
17
38
|
// Only execute the exact published launcher Hunch generates. Custom
|
|
18
39
|
// commands may need credentials or have unrelated effects; inspect only.
|
|
19
|
-
|
|
40
|
+
const generated = /^npx(?:\.cmd)?$/.test(launcher.command) && JSON.stringify(launcher.args) === JSON.stringify([
|
|
20
41
|
"-y", `--package=hunch-exact@npm:@davesheffer/hunch@${report.expectedVersion}`, "hunch", "mcp",
|
|
21
|
-
])
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
]);
|
|
43
|
+
const self = !generated && isSelfLauncher(launcher);
|
|
44
|
+
if (!generated && !self)
|
|
45
|
+
throw new Error("probe requires the generated exact-version npx launcher, or a `node <build>/cli/index.js mcp` launcher probed by that same build; repair stale pins first; other custom launchers remain untested");
|
|
46
|
+
// A self launcher runs this exact Node binary: no PATH/cwd lookup of `node` can
|
|
47
|
+
// resolve to an executable planted in the repository.
|
|
48
|
+
transport = new StdioClientTransport({ ...launcher, ...(self ? { command: process.execPath } : {}), cwd: root, stderr: "pipe" });
|
|
24
49
|
transport.stderr?.on("data", () => { });
|
|
25
50
|
await Promise.race([
|
|
26
51
|
(async () => {
|
|
@@ -11,11 +11,11 @@ export type MemoryHome = "public" | "private";
|
|
|
11
11
|
/** Flush one exact artifact home. Constitution repositories can explicitly
|
|
12
12
|
* choose public even in unified mode, so this must not infer routing through
|
|
13
13
|
* captureHome(isPrivate). The caller supplies the repository's actual home. */
|
|
14
|
-
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
14
|
+
export declare function flushMemoryHome(store: HunchStore, publicHunchDir: string, home: MemoryHome, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | "held" | null;
|
|
15
15
|
/** One completion flush per touched home. A mixed ingest/bootstrap can write
|
|
16
16
|
* both homes; unchanged homes are cheap no-ops after the memory-only stage
|
|
17
17
|
* check, while each real home becomes durable exactly once. */
|
|
18
|
-
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | null>>;
|
|
18
|
+
export declare function flushMemoryHomes(store: HunchStore, publicHunchDir: string, homes: Iterable<MemoryHome>, message: string, remoteOverride?: HunchRemoteContract): Partial<Record<MemoryHome, "pushed" | "committed" | "held" | null>>;
|
|
19
19
|
/** Auto-commit + push the overlay after a private write, when auto-commit is on. No-op
|
|
20
20
|
* otherwise (manual `hunch private --sync` still works). Never throws. */
|
|
21
21
|
export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
@@ -24,7 +24,19 @@ export declare function flushPrivate(store: HunchStore, message: string): void;
|
|
|
24
24
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
25
25
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
26
26
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
27
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
27
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
28
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
29
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
28
30
|
export declare function flushCapture(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string,
|
|
29
31
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
30
|
-
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | null;
|
|
32
|
+
remoteOverride?: HunchRemoteContract, observe?: GitMemoryObserver): "pushed" | "committed" | "held" | null;
|
|
33
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
34
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
35
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
36
|
+
export declare function flushStateWrite(store: HunchStore, publicHunchDir: string, isPrivate: boolean, message: string, remoteOverride?: HunchRemoteContract): "pushed" | "committed" | null;
|
|
37
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
38
|
+
* wondering why the capture commit stopped appearing. */
|
|
39
|
+
export declare const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out \u2014 your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
40
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
41
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
42
|
+
export declare function holdsOnDefaultBranch(publicHunchDir: string): boolean;
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* commits (bug_overlay_clobber); the memory commit rides their next push instead.
|
|
8
8
|
* Used by every CLI/MCP path that writes a record.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { commitAndPushHunch, hunchDirHasChanges, onDefaultBranch } from "../extractors/git.js";
|
|
12
13
|
import { refreshCommittableGrounding } from "./providers.js";
|
|
13
14
|
import { advertisedTeamRemoteContract } from "./team.js";
|
|
14
15
|
const pinnedSharedRoutes = new WeakMap();
|
|
@@ -41,6 +42,10 @@ export function flushMemoryHome(store, publicHunchDir, home, message, remoteOver
|
|
|
41
42
|
}
|
|
42
43
|
if (!store.autoCommit)
|
|
43
44
|
return null;
|
|
45
|
+
// Same hold as flushCapture: the public commit stages all of .hunch/, so it would
|
|
46
|
+
// otherwise sweep a capture that was just reported as held onto the default branch.
|
|
47
|
+
if (holdsOnDefaultBranch(publicHunchDir))
|
|
48
|
+
return heldOrNothing(publicHunchDir);
|
|
44
49
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
45
50
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding });
|
|
46
51
|
}
|
|
@@ -70,10 +75,21 @@ export function flushPrivate(store, message) {
|
|
|
70
75
|
* "committed" (commit created but not pushed — public .hunch/ rides the next push; an
|
|
71
76
|
* overlay commit whose merge/push failed retries on the next flush), or null (auto-commit
|
|
72
77
|
* off, no overlay for a private record, or the commit was skipped — lock held, safety
|
|
73
|
-
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up)
|
|
78
|
+
* backstop, nothing staged; the record stays on disk and the next flush sweeps it up),
|
|
79
|
+
* or "held" (a public capture left uncommitted because the default branch is checked
|
|
80
|
+
* out; callers say so with HELD_ON_DEFAULT_BRANCH). */
|
|
74
81
|
export function flushCapture(store, publicHunchDir, isPrivate, message,
|
|
75
82
|
/** Long-lived callers can pin the route snapshot that admitted the write. */
|
|
76
83
|
remoteOverride, observe) {
|
|
84
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, true);
|
|
85
|
+
}
|
|
86
|
+
/** Auto-commit a state-contract write (`nuryel_write`, `hunch serve`). A state root is a
|
|
87
|
+
* dedicated data repository whose writes commit on whatever branch it serves, so the
|
|
88
|
+
* default-branch hold for captures never applies; outcomes as flushCapture otherwise. */
|
|
89
|
+
export function flushStateWrite(store, publicHunchDir, isPrivate, message, remoteOverride) {
|
|
90
|
+
return flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, undefined, false);
|
|
91
|
+
}
|
|
92
|
+
function flushStore(store, publicHunchDir, isPrivate, message, remoteOverride, observe, holdOnDefaultBranch) {
|
|
77
93
|
// Follow the same routing as HunchStore.captureHome: unified ("shared") mode homes
|
|
78
94
|
// EVERY capture in the overlay, so the flush must go there too — one source of truth.
|
|
79
95
|
if (store.captureHome(isPrivate) === "private") {
|
|
@@ -89,10 +105,45 @@ remoteOverride, observe) {
|
|
|
89
105
|
}
|
|
90
106
|
if (!store.autoCommit)
|
|
91
107
|
return null;
|
|
108
|
+
// A capture never commits onto the default branch by itself: task and capture commits
|
|
109
|
+
// landed on main between the user's own. The record stays on disk; the user's next
|
|
110
|
+
// commit carries it, or the next capture on a feature branch sweeps it up. Held before
|
|
111
|
+
// the grounding refresh, so an automatic capture (hook sync, MCP) leaves tracked docs at
|
|
112
|
+
// HEAD instead of counting a record that `git commit -a` would leave behind; by-hand CLI
|
|
113
|
+
// commands refresh them on disk themselves (publicFlushCommitsGrounding in the CLI).
|
|
114
|
+
if (holdOnDefaultBranch && holdsOnDefaultBranch(publicHunchDir))
|
|
115
|
+
return heldOrNothing(publicHunchDir);
|
|
92
116
|
// A public capture changes record counts, so refresh git-clean grounding docs and fold
|
|
93
117
|
// them into the SAME memory commit — otherwise every capture re-stales the committed
|
|
94
118
|
// counts and the release gate's clean-tree check fails on the next CI index.
|
|
95
119
|
const grounding = refreshCommittableGrounding(dirname(publicHunchDir), store);
|
|
96
120
|
return commitAndPushHunch(publicHunchDir, message, { push: false, alsoStage: grounding, observe });
|
|
97
121
|
}
|
|
122
|
+
/** What a caller prints for a "held" flush, so a solo user on main is never left
|
|
123
|
+
* wondering why the capture commit stopped appearing. */
|
|
124
|
+
export const HELD_ON_DEFAULT_BRANCH = "not auto-committed: the default branch is checked out — your next commit that adds .hunch/ carries it (to restore auto-commit there, set \"autoCommitDefaultBranch\": true in .hunch/local.json)";
|
|
125
|
+
/** A hold reports "held" only when there is something left for the next commit; a clean
|
|
126
|
+
* .hunch/ is null, as an unheld flush with nothing staged would be. */
|
|
127
|
+
function heldOrNothing(publicHunchDir) {
|
|
128
|
+
return hunchDirHasChanges(publicHunchDir) ? "held" : null;
|
|
129
|
+
}
|
|
130
|
+
/** Whether an automatic public memory commit must be held: the default branch is checked
|
|
131
|
+
* out and the user has not opted back in. Shared by flushStore and `hunch sync`. */
|
|
132
|
+
export function holdsOnDefaultBranch(publicHunchDir) {
|
|
133
|
+
return !defaultBranchCommitsAllowed(publicHunchDir) && onDefaultBranch(dirname(publicHunchDir));
|
|
134
|
+
}
|
|
135
|
+
/** Opt back in to capture commits on the default branch: `autoCommitDefaultBranch: true`
|
|
136
|
+
* in the per-machine .hunch/local.json, or HUNCH_AUTO_COMMIT_DEFAULT_BRANCH=1. */
|
|
137
|
+
function defaultBranchCommitsAllowed(publicHunchDir) {
|
|
138
|
+
if (process.env.HUNCH_AUTO_COMMIT_DEFAULT_BRANCH === "1")
|
|
139
|
+
return true;
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(join(publicHunchDir, "local.json"), "utf8");
|
|
142
|
+
const local = JSON.parse(raw.startsWith("\uFEFF") ? raw.slice(1) : raw);
|
|
143
|
+
return !!local && typeof local === "object" && local.autoCommitDefaultBranch === true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
98
149
|
//# sourceMappingURL=sync.js.map
|
|
@@ -47,7 +47,7 @@ export type SnapshotOutcome = {
|
|
|
47
47
|
status: "written";
|
|
48
48
|
record: Workspace;
|
|
49
49
|
home: "private" | "public";
|
|
50
|
-
flushed: "pushed" | "committed" | null;
|
|
50
|
+
flushed: "pushed" | "committed" | "held" | null;
|
|
51
51
|
};
|
|
52
52
|
/** Record this machine's snapshot. Honors `workspaces.publish`, skips a write when the
|
|
53
53
|
* content is unchanged and the stored record is under a day old (an idle machine's hooks
|
package/dist/mcp/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { refreshExistingGrounding } from "../integrations/providers.js";
|
|
|
34
34
|
import { workspaceLedgerView, renderWorktreeTable, renderBranchTable, workspaceSummaryLine, snapshotHasHome, recordWorkspaceSnapshot, branchRows, worktreeRows } from "../integrations/workspaceLedger.js";
|
|
35
35
|
import { workspacesConfig } from "../core/config.js";
|
|
36
36
|
import { revParse, asOfDate, revExists, lastChangeDate, checkFreshnessRevision, teamPullIsFresh, rangeFiles, rangeGateDiff, commitFiles, commitGateDiff, stagedFiles, stagedGateDiff, workingFiles, workingGateDiff, pullHunchStatusDetailed, describePullFailure, sameRemoteUrl, currentBranch, worktreePaths, pathKnownToHistory } from "../extractors/git.js";
|
|
37
|
-
import { flushCapture, flushMemoryHome, pinSharedRemote } from "../integrations/sync.js";
|
|
37
|
+
import { flushCapture, flushStateWrite, flushMemoryHome, pinSharedRemote, HELD_ON_DEFAULT_BRANCH } from "../integrations/sync.js";
|
|
38
38
|
import { withWriteLock } from "../serve/writelock.js";
|
|
39
39
|
import { advertisedTeamRemoteContract, ensureTeamOverlay, isTeamStoreTrusted, overlayMatchesTeamRemote, teamWiringConsented, untrustedTeamStoreMessage, readTeamConfig, teamRemoteContract, teamSharedRef } from "../integrations/team.js";
|
|
40
40
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
@@ -113,12 +113,13 @@ function cwdHintOf(input) {
|
|
|
113
113
|
/** Honest auto-commit suffix: reports only what flushCapture ACTUALLY did. A skipped
|
|
114
114
|
* commit (backstop/lock/nothing staged) says nothing — the record is on disk and the
|
|
115
115
|
* next flush sweeps it up; claiming "auto-committed" there would be a lie. */
|
|
116
|
-
const flushNote = (flush, home, mode) => flush === "
|
|
117
|
-
: flush === "committed"
|
|
118
|
-
|
|
119
|
-
?
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
const flushNote = (flush, home, mode) => flush === "held" ? ` (${HELD_ON_DEFAULT_BRANCH})`
|
|
117
|
+
: flush === "pushed" ? ` (committed + pushed to the ${mode === "shared" ? "shared team store" : "private repo"})`
|
|
118
|
+
: flush === "committed"
|
|
119
|
+
? home === "private"
|
|
120
|
+
? " (committed to the overlay repo — push deferred: offline, no upstream, or merge conflict; the next capture or `hunch private --sync` retries)"
|
|
121
|
+
: " (auto-committed to .hunch/ — rides your next push)"
|
|
122
|
+
: "";
|
|
122
123
|
/** When an overlay exists, a PUBLIC write deserves one visible line: a record that
|
|
123
124
|
* lands in the committed store publishes on the next push, and an agent writing
|
|
124
125
|
* strategy/competitive content there is a leak nobody notices until it ships
|
|
@@ -2767,7 +2768,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2767
2768
|
// not race the HTTP server between the ledger read and the record write.
|
|
2768
2769
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2769
2770
|
const result = await withWriteLock(hunchDir, () => writeState(store, { schema: STATE_WRITE_VERSION, ...input }, {
|
|
2770
|
-
flush: (isPrivate, message) =>
|
|
2771
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2771
2772
|
}));
|
|
2772
2773
|
return stateResult(`${result.outcome} ${result.record_id} (${result.durability}) ${result.record_hash}`, result);
|
|
2773
2774
|
}
|
|
@@ -2784,7 +2785,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2784
2785
|
try {
|
|
2785
2786
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2786
2787
|
const result = await withWriteLock(hunchDir, () => captureState(store, { schema: STATE_CAPTURE_VERSION, ...input }, {
|
|
2787
|
-
flush: (isPrivate, message) =>
|
|
2788
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2788
2789
|
}));
|
|
2789
2790
|
return stateResult(`${result.outcome} observation ${result.record_id} (${result.durability}); this does not assert currentness. ${result.record_hash}`, result);
|
|
2790
2791
|
}
|
|
@@ -2801,7 +2802,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2801
2802
|
try {
|
|
2802
2803
|
const { hunchDir } = stateHomeFor(store, input.scope);
|
|
2803
2804
|
const result = await withWriteLock(hunchDir, () => captureBatchState(store, { schema: STATE_CAPTURE_BATCH_VERSION, ...input }, {
|
|
2804
|
-
flush: (isPrivate, message) =>
|
|
2805
|
+
flush: (isPrivate, message) => flushStateWrite(store, hunchPaths(root).hunch, isPrivate, message, startupTeamRoute ?? undefined),
|
|
2805
2806
|
}));
|
|
2806
2807
|
return stateResult(`Capture batch: ${result.results.filter(r => r.status === "saved").length} saved/replayed, ${result.results.filter(r => r.status === "refused").length} refused.${result.reviews ? ` Reviews: ${result.reviews.filter(r => r.status === "saved").length} withdrawn/replayed, ${result.reviews.filter(r => r.status === "refused").length} refused.` : ''} Inspect each indexed result.`, result);
|
|
2807
2808
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { verificationLauncherFor } from "../core/verifyLauncher.js";
|
|
3
3
|
import { TaskIdSchema, ReportClaimSchema, LessonReferenceSchema, finishReportTask, listReportTasks, readTaskReport, readLessonHistory, recordReportClaim, reportPresentationEnabled, startReportTask } from "../core/taskReport.js";
|
|
4
4
|
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
5
|
-
import {
|
|
5
|
+
import { currentSource, runReportConformance } from "../core/taskReportEvidence.js";
|
|
6
6
|
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
7
7
|
function applicationReferences(report) {
|
|
8
8
|
return report.deliveries.flatMap(d => d.records.map(r => ({ occurrence_id: d.occurrence_id, record_id: r.record_id, content_hash: r.content_hash, title: r.title }))).slice(-100);
|
|
@@ -85,7 +85,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
85
85
|
// from the model must not fork a second report or fail the start.
|
|
86
86
|
if (!task_id || !/different title/.test(error.message))
|
|
87
87
|
throw error;
|
|
88
|
-
task = readTaskReport(root, task_id,
|
|
88
|
+
task = readTaskReport(root, task_id, currentSource(root)).task;
|
|
89
89
|
}
|
|
90
90
|
const launcher = verificationLauncher();
|
|
91
91
|
// Compact by design (#370): the rules an agent needs to act, and exact
|
|
@@ -120,7 +120,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
120
120
|
catch (error) {
|
|
121
121
|
graphNote = `\nGraph not saved: ${error.message}`;
|
|
122
122
|
}
|
|
123
|
-
const report = readTaskReport(root, task_id,
|
|
123
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
124
124
|
const show = reportPresentationEnabled(root);
|
|
125
125
|
// The HTML evidence view is rendered on demand (hunch_report(html: true),
|
|
126
126
|
// `hunch report <id> --html`, or the VS Code view); finish writes no file.
|
|
@@ -165,7 +165,7 @@ export function registerTaskReportTools(server, getRoot, getStore) {
|
|
|
165
165
|
const tasks = listReportTasks(root);
|
|
166
166
|
return { content: [{ type: "text", text: tasks.length ? tasks.map(t => `${t.task_id} · ${t.state} · ${t.title}`).join("\n") : "No task reports yet. Start with hunch_task." }], structuredContent: { tasks } };
|
|
167
167
|
}
|
|
168
|
-
const report = readTaskReport(root, task_id,
|
|
168
|
+
const report = readTaskReport(root, task_id, currentSource(root));
|
|
169
169
|
const file = html ? writeTaskReportHtml(root, task_id) : null;
|
|
170
170
|
return { content: [{ type: "text", text: `${renderTaskReport(report)}${file ? `\nLocal evidence view: ${file}` : ""}` }], structuredContent: { ...boundedTaskReportForHost(report), application_references: applicationReferences(report), contribution_card: renderTaskReport(report), report_path: file } };
|
|
171
171
|
}
|