@davesheffer/hunch 1.37.1 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +12 -1
- package/dist/cli/taskReport.js +1 -1
- package/dist/core/taskRecord.d.ts +11 -2
- package/dist/core/taskRecord.js +94 -42
- package/dist/core/taskReport.d.ts +42 -3
- package/dist/core/taskReport.js +72 -10
- package/dist/core/taskReportEvidence.js +3 -3
- package/dist/core/taskReportHook.d.ts +11 -2
- package/dist/core/taskReportHook.js +51 -4
- package/dist/core/taskTouched.d.ts +5 -0
- package/dist/core/taskTouched.js +77 -0
- package/dist/mcp/taskReportTools.d.ts +8 -0
- package/dist/taskReports.d.ts +8 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -88,7 +88,8 @@ import { recordServed, servedSummary } from "../core/served.js";
|
|
|
88
88
|
import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
89
89
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
90
90
|
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
91
|
-
import { hookReportTaskId, nativeHookCwd, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
91
|
+
import { closeHookTask, hookReportTaskId, nativeHookCwd, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
92
|
+
import { persistTaskRecord } from "../core/taskRecord.js";
|
|
92
93
|
import { recordHookObservation } from "../core/hookObservations.js";
|
|
93
94
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
94
95
|
import { PIPELINE_LOOP, armExecutionObligations, beforeEditProbeVerdict, compileExecutableProbes, environmentExecutableProbes, environmentExecutionObligations, executionObligationBrief, isProductPath, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, proofCheckpoint, savePipelineState, stopVerdict, unverifiedNag, } from "../core/pipeline.js";
|
|
@@ -4548,6 +4549,16 @@ program
|
|
|
4548
4549
|
return;
|
|
4549
4550
|
}
|
|
4550
4551
|
}
|
|
4552
|
+
// The turn is over: close the prompt's task and keep its record, whether
|
|
4553
|
+
// or not the agent called finish. Fail-open: the card below still renders.
|
|
4554
|
+
try {
|
|
4555
|
+
const closed = closeHookTask(root, provider, evt);
|
|
4556
|
+
if (closed) {
|
|
4557
|
+
store ??= new HunchStore(paths);
|
|
4558
|
+
persistTaskRecord(root, store, closed);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
catch { /* the ledger and the card remain authoritative; the next finish retries */ }
|
|
4551
4562
|
const report = stopHookReport(root, provider, evt);
|
|
4552
4563
|
if (report)
|
|
4553
4564
|
console.log(JSON.stringify(report));
|
package/dist/cli/taskReport.js
CHANGED
|
@@ -90,7 +90,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
for (const s of summaries)
|
|
93
|
-
console.log(`${s.task.started_at.slice(0, 16).replace("T", " ")} ${s.task.task_id} ${s.task.state.padEnd(11)} ${renderTaskStatusLine(s) || "nothing observed"}${s.durable ? ` [graph: ${s.durable.home}]` : ""}`);
|
|
93
|
+
console.log(`${s.task.started_at.slice(0, 16).replace("T", " ")} ${s.task.task_id} ${s.task.state.padEnd(11)} ${renderTaskStatusLine(s) || "nothing observed"}${s.durable ? ` [graph: ${s.durable.home}${s.task.episode ? ` as ${s.task.episode}` : ""}]` : ""}${s.task.continues && !s.durable ? ` (continues ${s.task.continues})` : ""}`);
|
|
94
94
|
});
|
|
95
95
|
task.command("stats").description("Adherence over a window: how many prompts Hunch reached (delivery), checked, saved, or guarded — from the ledger, never from agent claims")
|
|
96
96
|
.option("--days <days>", "window in days", "7")
|
|
@@ -12,8 +12,17 @@ export declare function taskRecordFlushMode(root: string): "each" | "batch";
|
|
|
12
12
|
/** A delivery target that names code (a path or dotted symbol), not a task
|
|
13
13
|
* phrase like "fix the login redirect". Phrases never become file anchors. */
|
|
14
14
|
export declare function targetLooksLikePath(target: string): boolean;
|
|
15
|
-
/** The durable summary of a finished report, or null when there is nothing to keep.
|
|
16
|
-
|
|
15
|
+
/** The durable summary of a finished report, or null when there is nothing to keep.
|
|
16
|
+
* `touched` adds file anchors the report itself cannot know (git-side work while
|
|
17
|
+
* the task was open); report-derived files come first under the cap. */
|
|
18
|
+
export declare function taskRecordFromReport(report: TaskReport, touched?: readonly string[]): TaskRecord | null;
|
|
19
|
+
/** One record for an EPISODE: a prompt's report and the reports of the prompts
|
|
20
|
+
* that continued it in the same session (oldest first). Observations are the
|
|
21
|
+
* union in order, the id and start are the head's, the title is the first
|
|
22
|
+
* non-generic one, state and finish come from the latest closed member, and
|
|
23
|
+
* the report hash covers every member so a refresh is idempotent. Members
|
|
24
|
+
* still open contribute nothing yet. Null when nothing was observed at all. */
|
|
25
|
+
export declare function taskRecordFromReports(reports: readonly TaskReport[], touched?: readonly string[]): TaskRecord | null;
|
|
17
26
|
/** Where the record belongs. Anything that touched the private overlay — a
|
|
18
27
|
* private save, or a delivered lesson that lives only there — must not be named
|
|
19
28
|
* in a public record; the store's own routing (unified/shared mode) wins first. */
|
package/dist/core/taskRecord.js
CHANGED
|
@@ -14,10 +14,11 @@ import { readFileSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { flushCapture } from "../integrations/sync.js";
|
|
16
16
|
import { hunchPaths } from "./paths.js";
|
|
17
|
-
import { isEmptyTaskReport, readTaskReport, reportHash } from "./taskReport.js";
|
|
17
|
+
import { episodeTasks, isEmptyTaskReport, readTaskReport, reportHash } from "./taskReport.js";
|
|
18
18
|
import { reportSourceSnapshot } from "./taskReportEvidence.js";
|
|
19
19
|
import { ENTITY_KINDS, TaskRecordSchema } from "./types.js";
|
|
20
20
|
import { refreshRankEval } from "./taskRankingMode.js";
|
|
21
|
+
import { gitTouchedFiles } from "./taskTouched.js";
|
|
21
22
|
function localConfig(root) {
|
|
22
23
|
try {
|
|
23
24
|
return JSON.parse(readFileSync(join(root, ".hunch", "local.json"), "utf8"));
|
|
@@ -47,50 +48,76 @@ export function targetLooksLikePath(target) {
|
|
|
47
48
|
return false;
|
|
48
49
|
return /[./\\]/.test(t) && !/^\.+$/.test(t);
|
|
49
50
|
}
|
|
50
|
-
/** The durable summary of a finished report, or null when there is nothing to keep.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
/** The durable summary of a finished report, or null when there is nothing to keep.
|
|
52
|
+
* `touched` adds file anchors the report itself cannot know (git-side work while
|
|
53
|
+
* the task was open); report-derived files come first under the cap. */
|
|
54
|
+
export function taskRecordFromReport(report, touched = []) {
|
|
55
|
+
return taskRecordFromReports([report], touched);
|
|
56
|
+
}
|
|
57
|
+
const GENERIC_TASK_TITLES = new Set(["Assistant task", "Claude task"]);
|
|
58
|
+
const COVERAGE_RANK = { "no-delivery-observed": 0, "no-relevant-memory": 1, delivered: 2 };
|
|
59
|
+
/** One record for an EPISODE: a prompt's report and the reports of the prompts
|
|
60
|
+
* that continued it in the same session (oldest first). Observations are the
|
|
61
|
+
* union in order, the id and start are the head's, the title is the first
|
|
62
|
+
* non-generic one, state and finish come from the latest closed member, and
|
|
63
|
+
* the report hash covers every member so a refresh is idempotent. Members
|
|
64
|
+
* still open contribute nothing yet. Null when nothing was observed at all. */
|
|
65
|
+
export function taskRecordFromReports(reports, touched = []) {
|
|
66
|
+
const closed = reports.filter((r) => r.task.state !== "open" && r.task.finished_at);
|
|
67
|
+
if (!closed.length || closed.every((r) => isEmptyTaskReport(r)))
|
|
56
68
|
return null;
|
|
69
|
+
const head = reports[0].task;
|
|
70
|
+
const last = closed.reduce((a, b) => (b.task.finished_at >= a.task.finished_at ? b : a));
|
|
71
|
+
const title = reports.map((r) => r.task.title).find((t) => !GENERIC_TASK_TITLES.has(t)) ?? head.title;
|
|
57
72
|
const lessons = new Map();
|
|
58
|
-
for (const delivery of report.deliveries) {
|
|
59
|
-
for (const r of delivery.records) {
|
|
60
|
-
lessons.set(`${r.kind}:${r.record_id}:${r.content_hash}`, { kind: r.kind, record_id: r.record_id, content_hash: r.content_hash, title: r.title.slice(0, 200) });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
73
|
const files = new Set();
|
|
64
|
-
for (const d of report.deliveries)
|
|
65
|
-
if (d.target && targetLooksLikePath(d.target))
|
|
66
|
-
files.add(d.target.trim().replace(/\\/g, "/"));
|
|
67
|
-
for (const c of report.conformance)
|
|
68
|
-
for (const f of c.files)
|
|
69
|
-
files.add(f);
|
|
70
|
-
for (const r of report.refusals)
|
|
71
|
-
files.add(r.target);
|
|
72
74
|
const latestRule = new Map();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const applied = [], saved = [], checks = [];
|
|
76
|
+
let refusals = 0, coverage = "no-delivery-observed", sourceSnapshot = null;
|
|
77
|
+
for (const report of closed) {
|
|
78
|
+
for (const delivery of report.deliveries) {
|
|
79
|
+
if (delivery.target && targetLooksLikePath(delivery.target))
|
|
80
|
+
files.add(delivery.target.trim().replace(/\\/g, "/"));
|
|
81
|
+
for (const r of delivery.records)
|
|
82
|
+
lessons.set(`${r.kind}:${r.record_id}:${r.content_hash}`, { kind: r.kind, record_id: r.record_id, content_hash: r.content_hash, title: r.title.slice(0, 200) });
|
|
83
|
+
}
|
|
84
|
+
for (const c of report.conformance) {
|
|
85
|
+
for (const f of c.files)
|
|
86
|
+
files.add(f);
|
|
87
|
+
latestRule.set(`${c.kind}:${c.record_id}:${c.content_hash}`, { kind: c.kind, record_id: c.record_id, content_hash: c.content_hash, outcome: c.outcome });
|
|
88
|
+
}
|
|
89
|
+
for (const r of report.refusals)
|
|
90
|
+
files.add(r.target);
|
|
91
|
+
applied.push(...report.claims.map((c) => ({ record_id: c.record_id, content_hash: c.content_hash, action: c.action.slice(0, 300), supported_by: c.supported_by })));
|
|
92
|
+
saved.push(...report.saves.map((s) => ({ kind: s.record.kind, record_id: s.record.record_id, content_hash: s.record.content_hash, home: s.home, operation: s.operation, durability: s.durability })));
|
|
93
|
+
checks.push(...report.checks.map((c) => ({ label: c.label, state: (c.cancelled ? "cancelled" : c.timed_out ? "timed out" : c.exit_code === 0 ? "passed" : "failed"), exit_code: c.exit_code })));
|
|
94
|
+
refusals += report.refusals.length;
|
|
95
|
+
if (COVERAGE_RANK[report.coverage] > COVERAGE_RANK[coverage])
|
|
96
|
+
coverage = report.coverage;
|
|
97
|
+
const lastCheck = report.checks.at(-1);
|
|
98
|
+
if (lastCheck)
|
|
99
|
+
sourceSnapshot = lastCheck.after_snapshot ?? null;
|
|
75
100
|
}
|
|
76
|
-
const
|
|
101
|
+
const fromReport = [...files].sort();
|
|
102
|
+
const extra = [...new Set(touched.map((f) => f.trim().replace(/\\/g, "/")).filter((f) => f && !files.has(f)))].sort();
|
|
77
103
|
return TaskRecordSchema.parse({
|
|
78
|
-
id:
|
|
79
|
-
title
|
|
80
|
-
state: task.state,
|
|
81
|
-
started_at:
|
|
82
|
-
finished_at: task.finished_at,
|
|
83
|
-
coverage
|
|
104
|
+
id: head.task_id,
|
|
105
|
+
title,
|
|
106
|
+
state: last.task.state,
|
|
107
|
+
started_at: head.started_at,
|
|
108
|
+
finished_at: last.task.finished_at,
|
|
109
|
+
coverage,
|
|
84
110
|
lessons: [...lessons.values()],
|
|
85
|
-
applied
|
|
86
|
-
saved
|
|
87
|
-
checks:
|
|
111
|
+
applied,
|
|
112
|
+
saved,
|
|
113
|
+
checks: checks.slice(-64),
|
|
88
114
|
conformance: [...latestRule.values()],
|
|
89
|
-
refusals
|
|
90
|
-
files: [...
|
|
91
|
-
source_snapshot:
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
refusals,
|
|
116
|
+
files: [...fromReport, ...extra].slice(0, 64),
|
|
117
|
+
source_snapshot: sourceSnapshot,
|
|
118
|
+
// One member: its own hash, so records written before episodes existed do not all refresh.
|
|
119
|
+
report_hash: closed.length === 1 ? closed[0].content_hash : reportHash(closed.map((r) => r.content_hash)),
|
|
120
|
+
provenance: { source: "task_report", confidence: 1, evidence: closed.slice(0, 20).map((r) => `hunch report ${r.task.task_id}`), last_verified: last.task.finished_at },
|
|
94
121
|
});
|
|
95
122
|
}
|
|
96
123
|
/** Where the record belongs. Anything that touched the private overlay — a
|
|
@@ -145,13 +172,35 @@ export function computeSupersedes(record, others, limit = 20) {
|
|
|
145
172
|
export function persistTaskRecord(root, store, taskId, options = {}) {
|
|
146
173
|
if (!taskRecordsEnabled(root))
|
|
147
174
|
return null;
|
|
148
|
-
const
|
|
149
|
-
const
|
|
175
|
+
const snapshot = reportSourceSnapshot(root).hash;
|
|
176
|
+
const own = readTaskReport(root, taskId, snapshot);
|
|
177
|
+
if (own.task.state === "open")
|
|
178
|
+
return null;
|
|
179
|
+
// The record covers the whole episode: this prompt and the prompts of the
|
|
180
|
+
// same session it continued. Work done outside an instrumented editor (shell
|
|
181
|
+
// edits, rebases, release commits) still anchors it: git says what changed
|
|
182
|
+
// while the episode was open.
|
|
183
|
+
const headId = own.task.episode ?? own.task.task_id;
|
|
184
|
+
const members = headId === own.task.task_id && !own.task.episode ? [own.task] : episodeTasks(root, headId);
|
|
185
|
+
const reports = (members.length ? members : [own.task]).map((t) => (t.task_id === taskId ? own : readTaskReport(root, t.task_id, snapshot)));
|
|
186
|
+
const window = { from: reports[0].task.started_at, to: reports.reduce((max, r) => (r.task.finished_at && (!max || r.task.finished_at > max) ? r.task.finished_at : max), null) };
|
|
187
|
+
let built = taskRecordFromReports(reports, gitTouchedFiles(root, window.from, window.to));
|
|
150
188
|
if (!built)
|
|
151
189
|
return null;
|
|
190
|
+
let inPrivate = store.hasPrivate ? store.getPrivateRec("tasks", built.id) : undefined;
|
|
191
|
+
let inPublic = store.json.get("tasks", built.id);
|
|
192
|
+
// A record never changes home. If the episode's record already lives in the
|
|
193
|
+
// public store and this prompt brought private-only memory into it, the
|
|
194
|
+
// episode splits here: this prompt keeps its own record instead of naming
|
|
195
|
+
// private memory in a public one.
|
|
196
|
+
if (inPublic && !inPrivate && taskRecordHome(store, built) === "private" && built.id !== taskId) {
|
|
197
|
+
built = taskRecordFromReports([own], gitTouchedFiles(root, own.task.started_at, own.task.finished_at));
|
|
198
|
+
if (!built)
|
|
199
|
+
return null;
|
|
200
|
+
inPrivate = store.hasPrivate ? store.getPrivateRec("tasks", built.id) : undefined;
|
|
201
|
+
inPublic = store.json.get("tasks", built.id);
|
|
202
|
+
}
|
|
152
203
|
const record = { ...built, supersedes: computeSupersedes(built, store.recs("tasks")) };
|
|
153
|
-
const inPrivate = store.hasPrivate ? store.getPrivateRec("tasks", record.id) : undefined;
|
|
154
|
-
const inPublic = store.json.get("tasks", record.id);
|
|
155
204
|
const home = inPrivate ? "private" : inPublic ? "public" : taskRecordHome(store, record);
|
|
156
205
|
const existing = home === "private" ? inPrivate : inPublic;
|
|
157
206
|
if (existing && existing.report_hash === record.report_hash)
|
|
@@ -202,7 +251,10 @@ export function mergeDurableTaskSummaries(store, summaries, limit = 30) {
|
|
|
202
251
|
const seen = new Set();
|
|
203
252
|
const merged = summaries.map((s) => {
|
|
204
253
|
seen.add(s.task.task_id);
|
|
205
|
-
|
|
254
|
+
// A continued prompt's record lives under its episode head.
|
|
255
|
+
const home = homes.get(s.task.task_id) ?? homes.get(s.task.episode ?? "");
|
|
256
|
+
if (s.task.episode)
|
|
257
|
+
seen.add(s.task.episode);
|
|
206
258
|
return { ...s, durable: home ? { home } : null };
|
|
207
259
|
});
|
|
208
260
|
for (const r of records.values()) {
|
|
@@ -123,6 +123,13 @@ declare const TaskSchema: z.ZodObject<{
|
|
|
123
123
|
completed: "completed";
|
|
124
124
|
interrupted: "interrupted";
|
|
125
125
|
}>;
|
|
126
|
+
closed_by: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
host: "host";
|
|
128
|
+
agent: "agent";
|
|
129
|
+
}>>;
|
|
130
|
+
session_key: z.ZodOptional<z.ZodString>;
|
|
131
|
+
continues: z.ZodOptional<z.ZodString>;
|
|
132
|
+
episode: z.ZodOptional<z.ZodString>;
|
|
126
133
|
}, z.core.$strict>;
|
|
127
134
|
export type ReportTask = z.infer<typeof TaskSchema>;
|
|
128
135
|
export interface TaskDelivery {
|
|
@@ -200,7 +207,26 @@ export declare function readLessonHistory(root: string, reference: LessonReferen
|
|
|
200
207
|
limit?: number;
|
|
201
208
|
before?: number;
|
|
202
209
|
}): LessonHistory;
|
|
203
|
-
export
|
|
210
|
+
export interface TaskLinks {
|
|
211
|
+
session_key?: string;
|
|
212
|
+
continues?: string;
|
|
213
|
+
episode?: string;
|
|
214
|
+
}
|
|
215
|
+
export declare function startReportTask(root: string, title: string, taskId?: string, links?: TaskLinks): ReportTask;
|
|
216
|
+
/** A prompt that follows another in the same session within this window is the
|
|
217
|
+
* same work: its task continues the previous one and shares its episode. */
|
|
218
|
+
export declare const CONTINUATION_WINDOW_MS: number;
|
|
219
|
+
/** The links a new prompt's task takes from the latest task of its session, or
|
|
220
|
+
* null when that task is too old (measured from its close, or its start when it
|
|
221
|
+
* was never closed) to be the same work. */
|
|
222
|
+
export declare function continuationLinks(previous: ReportTask | null, nowMs?: number): {
|
|
223
|
+
continues: string;
|
|
224
|
+
episode: string;
|
|
225
|
+
} | null;
|
|
226
|
+
/** The most recent task of a session in this worktree, or null. */
|
|
227
|
+
export declare function latestSessionTask(root: string, sessionKey: string): ReportTask | null;
|
|
228
|
+
/** Every task of an episode, oldest first: the head and the prompts that continued it. */
|
|
229
|
+
export declare function episodeTasks(root: string, headId: string): ReportTask[];
|
|
204
230
|
/** The record revisions among `records` that this task has not received before.
|
|
205
231
|
* Powers the one-line "Hunch recalled …" indication on a lesson's FIRST delivery
|
|
206
232
|
* in a task; repeats of the same revision stay silent (deduplicated per task and
|
|
@@ -223,8 +249,21 @@ export declare function recordReportRefusal(root: string, taskId: string, refusa
|
|
|
223
249
|
export declare function recordReportConformance(root: string, taskId: string, conformance: ReportConformance): string;
|
|
224
250
|
/** Only the local runner calls this. MCP never accepts a claimed successful check. */
|
|
225
251
|
export declare function recordReportCheck(root: string, taskId: string, check: ReportCheck): string;
|
|
226
|
-
|
|
227
|
-
|
|
252
|
+
/** A start without a result blocks completion only while the runner could still
|
|
253
|
+
* deliver one: its own timeout plus a minute of grace. After that the runner is
|
|
254
|
+
* gone (a killed process, a closed laptop) and the report's unknowns already say
|
|
255
|
+
* the result was not retained; freezing the task forever would add nothing.
|
|
256
|
+
* Starts recorded before the timeout was retained use the verification ceiling. */
|
|
257
|
+
export declare const CHECK_RESULT_GRACE_MS = 60000;
|
|
258
|
+
export declare const MAX_PENDING_CHECK_MS: number;
|
|
259
|
+
export declare function beginReportCheck(root: string, taskId: string, label: string, timeoutMs?: number): string;
|
|
260
|
+
/** `by: "host"` is the lifecycle hook closing the prompt's task at Stop. It is
|
|
261
|
+
* provisional: a later observation reopens the task (see appendEvent) and an
|
|
262
|
+
* explicit agent finish, with any outcome, replaces it. Pending verification
|
|
263
|
+
* keeps the task open for either closer. */
|
|
264
|
+
export declare function finishReportTask(root: string, taskId: string, state?: "completed" | "interrupted", options?: {
|
|
265
|
+
by?: "agent" | "host";
|
|
266
|
+
}): ReportTask;
|
|
228
267
|
/** A report with no observation of any kind. Presentation surfaces may stay
|
|
229
268
|
* silent for it; the task row itself is retained so "never touched Hunch" is
|
|
230
269
|
* still countable (hunch report / the VS Code view / task list). */
|
package/dist/core/taskReport.js
CHANGED
|
@@ -82,6 +82,19 @@ const TaskSchema = z.object({
|
|
|
82
82
|
task_id: TaskIdSchema, scope: hashSchema, title: safeText(200),
|
|
83
83
|
started_at: z.string().datetime(), finished_at: z.string().datetime().nullable(),
|
|
84
84
|
state: z.enum(["open", "completed", "interrupted"]),
|
|
85
|
+
/** Who closed the task. "host": the lifecycle hook at Stop, a provisional
|
|
86
|
+
* close that a continuation reopens and an explicit agent finish overrides.
|
|
87
|
+
* Absent on rows written before this field existed (agent closes). */
|
|
88
|
+
closed_by: z.enum(["agent", "host"]).optional(),
|
|
89
|
+
/** Continuity across the prompts of one host session. `session_key` is a hash
|
|
90
|
+
* of (root, provider, session, agent), never the identifier itself; `continues`
|
|
91
|
+
* names the previous prompt's task when this prompt followed it within the
|
|
92
|
+
* continuation window; `episode` names the first task of that chain, the id
|
|
93
|
+
* the chain's graph record is written under. Absent on older rows and on
|
|
94
|
+
* tasks started without a host session (one task, one episode). */
|
|
95
|
+
session_key: hashSchema.optional(),
|
|
96
|
+
continues: TaskIdSchema.optional(),
|
|
97
|
+
episode: TaskIdSchema.optional(),
|
|
85
98
|
}).strict();
|
|
86
99
|
export const LessonReferenceSchema = z.object({
|
|
87
100
|
kind: safeText(64), record_id: safeText(512), content_hash: hashSchema.optional(),
|
|
@@ -227,9 +240,9 @@ function transaction(db, run, readOnly = false) {
|
|
|
227
240
|
throw error;
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
|
-
export function startReportTask(root, title, taskId) {
|
|
243
|
+
export function startReportTask(root, title, taskId, links = {}) {
|
|
231
244
|
const task = TaskSchema.parse({ task_id: taskId ?? `htask_${randomBytes(12).toString("hex")}`,
|
|
232
|
-
scope: scopeOf(root), title, started_at: new Date().toISOString(), finished_at: null, state: "open" });
|
|
245
|
+
scope: scopeOf(root), title, started_at: new Date().toISOString(), finished_at: null, state: "open", ...links });
|
|
233
246
|
// Local observations have a bounded lifetime; durable project memory is untouched.
|
|
234
247
|
pruneReportHistory(root);
|
|
235
248
|
return taskDb(root, db => transaction(db, () => {
|
|
@@ -244,6 +257,31 @@ export function startReportTask(root, title, taskId) {
|
|
|
244
257
|
return task;
|
|
245
258
|
}));
|
|
246
259
|
}
|
|
260
|
+
/** A prompt that follows another in the same session within this window is the
|
|
261
|
+
* same work: its task continues the previous one and shares its episode. */
|
|
262
|
+
export const CONTINUATION_WINDOW_MS = 30 * 60_000;
|
|
263
|
+
/** The links a new prompt's task takes from the latest task of its session, or
|
|
264
|
+
* null when that task is too old (measured from its close, or its start when it
|
|
265
|
+
* was never closed) to be the same work. */
|
|
266
|
+
export function continuationLinks(previous, nowMs = Date.now()) {
|
|
267
|
+
if (!previous)
|
|
268
|
+
return null;
|
|
269
|
+
const reference = Date.parse(previous.finished_at ?? previous.started_at);
|
|
270
|
+
if (!Number.isFinite(reference) || nowMs - reference > CONTINUATION_WINDOW_MS)
|
|
271
|
+
return null;
|
|
272
|
+
return { continues: previous.task_id, episode: previous.episode ?? previous.task_id };
|
|
273
|
+
}
|
|
274
|
+
/** The most recent task of a session in this worktree, or null. */
|
|
275
|
+
export function latestSessionTask(root, sessionKey) {
|
|
276
|
+
return taskDb(root, db => {
|
|
277
|
+
const row = db.prepare("SELECT body FROM report_tasks WHERE scope IN (?, ?, ?) AND json_extract(body, '$.session_key') = ? ORDER BY rowid DESC LIMIT 1").get(...scopePair(root), sessionKey);
|
|
278
|
+
return row ? TaskSchema.parse(JSON.parse(row.body)) : null;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/** Every task of an episode, oldest first: the head and the prompts that continued it. */
|
|
282
|
+
export function episodeTasks(root, headId) {
|
|
283
|
+
return taskDb(root, db => db.prepare("SELECT body FROM report_tasks WHERE task_id = ? OR json_extract(body, '$.episode') = ? ORDER BY rowid").all(headId, headId).map(r => TaskSchema.parse(JSON.parse(r.body))));
|
|
284
|
+
}
|
|
247
285
|
function appendEvent(root, taskId, kind, body, eventId) {
|
|
248
286
|
const encoded = JSON.stringify(body);
|
|
249
287
|
if (Buffer.byteLength(encoded) > MAX_EVENT_BYTES)
|
|
@@ -260,8 +298,14 @@ function appendEvent(root, taskId, kind, body, eventId) {
|
|
|
260
298
|
throw new Error("report event identity conflicts with existing evidence");
|
|
261
299
|
return id;
|
|
262
300
|
}
|
|
263
|
-
if (task.state !== "open" && !(kind === "check" && task.state === "interrupted"))
|
|
264
|
-
|
|
301
|
+
if (task.state !== "open" && !(kind === "check" && task.state === "interrupted")) {
|
|
302
|
+
if (task.closed_by !== "host")
|
|
303
|
+
throw new Error("task is already closed; start a new task for new work");
|
|
304
|
+
// The host closed this task at Stop, but the turn went on (another hook's
|
|
305
|
+
// block, a resumed prompt). Reopen it for the new observation; the next
|
|
306
|
+
// Stop closes it again and the graph record is refreshed from the report.
|
|
307
|
+
db.prepare("UPDATE report_tasks SET body = ? WHERE task_id = ?").run(JSON.stringify(TaskSchema.parse({ ...task, state: "open", finished_at: null, closed_by: undefined })), taskId);
|
|
308
|
+
}
|
|
265
309
|
if (kind === "check") {
|
|
266
310
|
const check = ReportCheckSchema.parse(body);
|
|
267
311
|
if (!check.check_id || !db.prepare("SELECT event_id FROM report_events WHERE event_id = ? AND task_id = ? AND kind = 'check-start'").get(check.check_id, taskId))
|
|
@@ -269,7 +313,7 @@ function appendEvent(root, taskId, kind, body, eventId) {
|
|
|
269
313
|
}
|
|
270
314
|
const { total, bytes, pending } = db.prepare(`SELECT COUNT(*) AS total,
|
|
271
315
|
COALESCE(SUM(length(CAST(body AS BLOB))), 0) AS bytes,
|
|
272
|
-
SUM(CASE WHEN kind = 'check-start' THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending
|
|
316
|
+
SUM(CASE WHEN kind = 'check-start' AND (julianday('now') - julianday(at)) * 86400000 < COALESCE(json_extract(body, '$.timeout_ms'), ${MAX_PENDING_CHECK_MS}) + ${CHECK_RESULT_GRACE_MS} THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending
|
|
273
317
|
FROM report_events WHERE task_id = ?`).get(taskId);
|
|
274
318
|
const reserved = Math.max(0, (pending ?? 0) + (kind === "check-start" ? 1 : kind === "check" ? -1 : 0));
|
|
275
319
|
if (total + 1 + reserved > MAX_EVENTS || bytes + Buffer.byteLength(encoded) + reserved * MAX_EVENT_BYTES > MAX_TASK_BYTES)
|
|
@@ -361,23 +405,41 @@ export function recordReportCheck(root, taskId, check) {
|
|
|
361
405
|
throw new Error("verification result requires a reserved check identity");
|
|
362
406
|
return appendEvent(root, taskId, "check", value, `hev_${reportHash({ taskId, check: value.check_id }).slice(7, 31)}`);
|
|
363
407
|
}
|
|
364
|
-
|
|
365
|
-
|
|
408
|
+
/** A start without a result blocks completion only while the runner could still
|
|
409
|
+
* deliver one: its own timeout plus a minute of grace. After that the runner is
|
|
410
|
+
* gone (a killed process, a closed laptop) and the report's unknowns already say
|
|
411
|
+
* the result was not retained; freezing the task forever would add nothing.
|
|
412
|
+
* Starts recorded before the timeout was retained use the verification ceiling. */
|
|
413
|
+
export const CHECK_RESULT_GRACE_MS = 60_000;
|
|
414
|
+
export const MAX_PENDING_CHECK_MS = 6 * 60 * 60_000;
|
|
415
|
+
export function beginReportCheck(root, taskId, label, timeoutMs) {
|
|
416
|
+
const timeout = Number.isInteger(timeoutMs) && timeoutMs > 0 ? { timeout_ms: timeoutMs } : {};
|
|
417
|
+
return appendEvent(root, taskId, "check-start", { label: safeText(200).parse(label), ...timeout });
|
|
366
418
|
}
|
|
367
|
-
|
|
419
|
+
/** `by: "host"` is the lifecycle hook closing the prompt's task at Stop. It is
|
|
420
|
+
* provisional: a later observation reopens the task (see appendEvent) and an
|
|
421
|
+
* explicit agent finish, with any outcome, replaces it. Pending verification
|
|
422
|
+
* keeps the task open for either closer. */
|
|
423
|
+
export function finishReportTask(root, taskId, state = "completed", options = {}) {
|
|
424
|
+
const by = options.by ?? "agent";
|
|
368
425
|
return taskDb(root, db => transaction(db, () => {
|
|
369
426
|
const task = readTask(db, root, taskId);
|
|
370
427
|
if (task.state !== "open") {
|
|
428
|
+
if (task.closed_by === "host" && by === "agent") {
|
|
429
|
+
const confirmed = TaskSchema.parse({ ...task, state, finished_at: task.state === state ? task.finished_at : new Date().toISOString(), closed_by: "agent" });
|
|
430
|
+
db.prepare("UPDATE report_tasks SET body = ? WHERE task_id = ?").run(JSON.stringify(confirmed), taskId);
|
|
431
|
+
return confirmed;
|
|
432
|
+
}
|
|
371
433
|
if (task.state !== state)
|
|
372
434
|
throw new Error("task already closed with a different outcome");
|
|
373
435
|
return task;
|
|
374
436
|
}
|
|
375
437
|
if (state === "completed") {
|
|
376
|
-
const { pending } = db.prepare(`SELECT SUM(CASE WHEN kind = 'check-start' THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending FROM report_events WHERE task_id = ?`).get(taskId);
|
|
438
|
+
const { pending } = db.prepare(`SELECT SUM(CASE WHEN kind = 'check-start' AND (julianday('now') - julianday(at)) * 86400000 < COALESCE(json_extract(body, '$.timeout_ms'), ${MAX_PENDING_CHECK_MS}) + ${CHECK_RESULT_GRACE_MS} THEN 1 WHEN kind = 'check' AND json_extract(body, '$.check_id') IS NOT NULL THEN -1 ELSE 0 END) AS pending FROM report_events WHERE task_id = ?`).get(taskId);
|
|
377
439
|
if ((pending ?? 0) > 0)
|
|
378
440
|
throw new Error("verification is still running or was interrupted; wait for its result or close the task as interrupted");
|
|
379
441
|
}
|
|
380
|
-
const finished = TaskSchema.parse({ ...task, state, finished_at: new Date().toISOString() });
|
|
442
|
+
const finished = TaskSchema.parse({ ...task, state, finished_at: new Date().toISOString(), closed_by: by });
|
|
381
443
|
db.prepare("UPDATE report_tasks SET body = ? WHERE task_id = ?").run(JSON.stringify(finished), taskId);
|
|
382
444
|
return finished;
|
|
383
445
|
}));
|
|
@@ -96,7 +96,7 @@ export function reportSourceSnapshot(root) {
|
|
|
96
96
|
* `not-exercised` or `unavailable` — never "satisfied" by file overlap. */
|
|
97
97
|
export function runReportConformance(root, store, taskId) {
|
|
98
98
|
const report = readTaskReport(root, taskId);
|
|
99
|
-
if (report.task.state !== "open")
|
|
99
|
+
if (report.task.state !== "open" && report.task.closed_by !== "host")
|
|
100
100
|
throw new Error("cannot evaluate rules for a closed task");
|
|
101
101
|
const delivered = [...new Map(report.deliveries.flatMap(d => d.records).filter(r => r.kind === "constraints" || r.kind === "decisions").map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
|
|
102
102
|
if (!delivered.length)
|
|
@@ -203,12 +203,12 @@ export async function runReportCheck(root, taskId, command, label, timeoutMs = 1
|
|
|
203
203
|
throw new Error(`verification timeout must be between 1 and ${MAX_CHECK_TIMEOUT_MS} ms`);
|
|
204
204
|
options.signal?.throwIfAborted();
|
|
205
205
|
const task = readTaskReport(root, taskId).task;
|
|
206
|
-
if (task.state !== "open")
|
|
206
|
+
if (task.state !== "open" && task.closed_by !== "host")
|
|
207
207
|
throw new Error("cannot verify a closed task");
|
|
208
208
|
const before = reportSourceSnapshot(root);
|
|
209
209
|
// Validate sensitive arguments before executing or writing anything.
|
|
210
210
|
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" });
|
|
211
|
-
const checkId = beginReportCheck(root, taskId, label);
|
|
211
|
+
const checkId = beginReportCheck(root, taskId, label, timeoutMs);
|
|
212
212
|
const result = await new Promise((resolveResult) => {
|
|
213
213
|
// Windows launchers (npx.cmd, npm.cmd, other .cmd/.bat shims) cannot be spawned
|
|
214
214
|
// without a shell; resolve them first so a check actually runs instead of
|
|
@@ -21,8 +21,17 @@ export declare function hookReportTaskId(root: string, provider: HookProvider, e
|
|
|
21
21
|
* No raw prompt, host session identifier, or transcript is retained; a repository
|
|
22
22
|
* that opts in (`taskTitles: "prompt"`) keeps only a bounded first-line title. */
|
|
23
23
|
export declare function startHookReport(root: string, provider: HookProvider, event: HunchHookInput): string | null;
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
24
|
+
/** Stop ends the turn, so the prompt's task closes here as a HOST close: the
|
|
25
|
+
* ledger says the task completed even when the agent never called finish, and
|
|
26
|
+
* a task with observations becomes a graph record without anyone's cooperation.
|
|
27
|
+
* The close is provisional because Stop can precede another hook's
|
|
28
|
+
* continuation: the next observation reopens the task and the following Stop
|
|
29
|
+
* closes it again (the record is refreshed from the report). An explicit agent
|
|
30
|
+
* finish with any outcome overrides a host close. Pending verification keeps
|
|
31
|
+
* the task open. Returns the task id when the task is closed after this call,
|
|
32
|
+
* so the caller can persist its record; null when nothing is closed. */
|
|
33
|
+
export declare function closeHookTask(root: string, provider: HookProvider, event: HunchHookInput): string | null;
|
|
34
|
+
/** A presentation notice never denies Stop or injects another model turn.
|
|
26
35
|
* A prompt with no observation at all prints nothing: the empty task row stays
|
|
27
36
|
* in the ledger (hunch task list, the VS Code Contribution view) so "never
|
|
28
37
|
* touched Hunch" remains countable without a five-line notice per prompt. */
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { findRoot } from "./paths.js";
|
|
6
6
|
import { canonicalReportRoot } from "./taskReportPaths.js";
|
|
7
7
|
import { isCredentialFreeText } from "./types.js";
|
|
8
|
-
import { isEmptyTaskReport, readTaskReport, recordReportRefusal, reportHash, reportPresentationEnabled, startReportTask } from "./taskReport.js";
|
|
8
|
+
import { continuationLinks, finishReportTask, isEmptyTaskReport, latestSessionTask, readTaskReport, recordReportRefusal, reportHash, reportPresentationEnabled, startReportTask } from "./taskReport.js";
|
|
9
9
|
import { reportSourceSnapshot } from "./taskReportEvidence.js";
|
|
10
10
|
import { renderTaskReport } from "./taskReportRender.js";
|
|
11
11
|
/** The exact task identity a native host prompt maps to. */
|
|
@@ -98,9 +98,25 @@ export function startHookReport(root, provider, event) {
|
|
|
98
98
|
return null;
|
|
99
99
|
const cwdLiteral = JSON.stringify(cwd);
|
|
100
100
|
const title = (promptTitlesEnabled(root) ? promptTaskTitle(event.prompt) : null) ?? NATIVE_TASK_TITLE;
|
|
101
|
+
// Continuity: a prompt that follows another of the same session within the
|
|
102
|
+
// window continues its task ("status", "next", "go" are the same work), and
|
|
103
|
+
// the episode's graph record is written under the first task's id. The key
|
|
104
|
+
// is a hash; the host session identifier itself is still never retained.
|
|
105
|
+
let links = {};
|
|
106
|
+
if (event.session_id) {
|
|
107
|
+
const sessionKey = reportHash([cwd, provider, event.session_id, event.agent_id ?? null]);
|
|
108
|
+
links = { session_key: sessionKey };
|
|
109
|
+
try {
|
|
110
|
+
const previous = latestSessionTask(root, sessionKey);
|
|
111
|
+
const continued = previous && previous.task_id !== id ? continuationLinks(previous) : null;
|
|
112
|
+
if (continued)
|
|
113
|
+
links = { ...links, ...continued };
|
|
114
|
+
}
|
|
115
|
+
catch { /* no continuity; still a task */ }
|
|
116
|
+
}
|
|
101
117
|
let task;
|
|
102
118
|
try {
|
|
103
|
-
task = startReportTask(root, title, id);
|
|
119
|
+
task = startReportTask(root, title, id, links);
|
|
104
120
|
}
|
|
105
121
|
catch (error) {
|
|
106
122
|
// The same prompt identity may already be open: a release that called every
|
|
@@ -113,8 +129,39 @@ export function startHookReport(root, provider, event) {
|
|
|
113
129
|
}
|
|
114
130
|
return `Hunch has opened this prompt's report: ${task.task_id}. Reuse this exact ID for this prompt. Call hunch_task(action: "start", task_id: "${task.task_id}", title: ${JSON.stringify(task.title)}, cwd: ${cwdLiteral}) to obtain verification_argv; do not create another report. Pass this task_id and cwd: ${cwdLiteral} to hunch_context and decision/correction/finding captures, and pass the same cwd when finishing with hunch_task before responding. A host Stop notice will show the evidence even if no task-linked memory was observed.`;
|
|
115
131
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
132
|
+
/** Stop ends the turn, so the prompt's task closes here as a HOST close: the
|
|
133
|
+
* ledger says the task completed even when the agent never called finish, and
|
|
134
|
+
* a task with observations becomes a graph record without anyone's cooperation.
|
|
135
|
+
* The close is provisional because Stop can precede another hook's
|
|
136
|
+
* continuation: the next observation reopens the task and the following Stop
|
|
137
|
+
* closes it again (the record is refreshed from the report). An explicit agent
|
|
138
|
+
* finish with any outcome overrides a host close. Pending verification keeps
|
|
139
|
+
* the task open. Returns the task id when the task is closed after this call,
|
|
140
|
+
* so the caller can persist its record; null when nothing is closed. */
|
|
141
|
+
export function closeHookTask(root, provider, event) {
|
|
142
|
+
let id;
|
|
143
|
+
try {
|
|
144
|
+
id = identity(root, provider, event);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (!id || id === "legacy")
|
|
150
|
+
return null;
|
|
151
|
+
try {
|
|
152
|
+
const task = readTaskReport(root, id).task;
|
|
153
|
+
if (task.state === "interrupted")
|
|
154
|
+
return null;
|
|
155
|
+
if (task.state === "open")
|
|
156
|
+
finishReportTask(root, id, "completed", { by: "host" });
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// No task for this prompt, or verification still running: leave it as it is.
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** A presentation notice never denies Stop or injects another model turn.
|
|
118
165
|
* A prompt with no observation at all prints nothing: the empty task row stays
|
|
119
166
|
* in the ledger (hunch task list, the VS Code Contribution view) so "never
|
|
120
167
|
* touched Hunch" remains countable without a five-line notice per prompt. */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Files the repository shows as worked on while a task was open.
|
|
2
|
+
*
|
|
3
|
+
* The pre-edit hook only sees edits made through an instrumented editor tool.
|
|
4
|
+
* Work done from a shell (patch scripts, rebases, release commits) never
|
|
5
|
+
* produced a delivery, so the task record had no file anchor for it and the
|
|
6
|
+
* ranking could not relate the task to later work on the same files. Two
|
|
7
|
+
* sources fill that gap, both bounded and fail-open (an error yields nothing,
|
|
8
|
+
* never a failed finish):
|
|
9
|
+
* - commits authored by the configured git user whose commit time falls in
|
|
10
|
+
* the task window (merges excluded);
|
|
11
|
+
* - working-tree changes (modified, added, untracked) whose mtime falls in it.
|
|
12
|
+
* Hunch's own memory and cache paths are excluded, so a capture commit made
|
|
13
|
+
* during the task does not count as work on a file. Deleted paths are skipped:
|
|
14
|
+
* nothing dates the deletion. Commit dates get one second of slack (git keeps
|
|
15
|
+
* seconds); working-tree mtimes get none before the start. */
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
const EXCLUDED_SEGMENTS = new Set([".hunch", ".hunch-cache", ".git"]);
|
|
20
|
+
function gitDate(ms) {
|
|
21
|
+
// Second resolution, a format every git accepts.
|
|
22
|
+
return `${new Date(ms).toISOString().slice(0, 19).replace("T", " ")} +0000`;
|
|
23
|
+
}
|
|
24
|
+
export function gitTouchedFiles(root, startedAt, finishedAt, options = {}) {
|
|
25
|
+
const limit = Math.max(1, options.limit ?? 64);
|
|
26
|
+
const timeout = options.timeoutMs ?? 3_000;
|
|
27
|
+
const since = Date.parse(startedAt);
|
|
28
|
+
if (!Number.isFinite(since))
|
|
29
|
+
return [];
|
|
30
|
+
const until = finishedAt ? Date.parse(finishedAt) : (options.now ?? Date.now());
|
|
31
|
+
if (!Number.isFinite(until) || until < since)
|
|
32
|
+
return [];
|
|
33
|
+
// One second of slack on each side: git dates and some filesystems are second-granular.
|
|
34
|
+
const from = since - 1_000, to = until + 1_000;
|
|
35
|
+
const env = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
36
|
+
for (const key of Object.keys(env))
|
|
37
|
+
if (key.startsWith("GIT_") && key !== "GIT_OPTIONAL_LOCKS")
|
|
38
|
+
delete env[key];
|
|
39
|
+
const run = (args) => execFileSync("git", ["-C", root, "-c", "core.quotePath=false", ...args], { env, encoding: "utf8", timeout, maxBuffer: 4_000_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
40
|
+
const out = new Set();
|
|
41
|
+
const keep = (raw) => {
|
|
42
|
+
const path = raw.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
43
|
+
if (!path || path.split("/").some((segment) => EXCLUDED_SEGMENTS.has(segment)))
|
|
44
|
+
return;
|
|
45
|
+
out.add(path);
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const email = run(["config", "--get", "user.email"]).trim();
|
|
49
|
+
if (email) {
|
|
50
|
+
const log = run(["log", "--no-merges", "-n", "50", `--since=${gitDate(from)}`, `--until=${gitDate(to)}`, `--author=${email}`, "--format=", "--name-only"]);
|
|
51
|
+
for (const line of log.split("\n"))
|
|
52
|
+
keep(line);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* no commits, no git user, or no git: the working tree may still say something */ }
|
|
56
|
+
try {
|
|
57
|
+
const entries = run(["status", "--porcelain=v1", "-z", "--untracked-files=all"]).split("\0").filter(Boolean);
|
|
58
|
+
for (let i = 0; i < entries.length; i++) {
|
|
59
|
+
const entry = entries[i];
|
|
60
|
+
const code = entry.slice(0, 2), path = entry.slice(3);
|
|
61
|
+
// A rename or copy is followed by its original path as a separate entry.
|
|
62
|
+
if (code[0] === "R" || code[0] === "C")
|
|
63
|
+
i++;
|
|
64
|
+
if (code.includes("D") || !path)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const mtime = statSync(join(root, path)).mtimeMs;
|
|
68
|
+
if (mtime >= since && mtime <= to)
|
|
69
|
+
keep(path);
|
|
70
|
+
}
|
|
71
|
+
catch { /* vanished between status and stat */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* not a git worktree or status failed: nothing to add */ }
|
|
75
|
+
return [...out].sort().slice(0, limit);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=taskTouched.js.map
|
|
@@ -16,6 +16,10 @@ export declare function boundedTaskReport(report: ReturnType<typeof readTaskRepo
|
|
|
16
16
|
started_at: string;
|
|
17
17
|
finished_at: string | null;
|
|
18
18
|
state: "open" | "completed" | "interrupted";
|
|
19
|
+
closed_by?: "host" | "agent" | undefined;
|
|
20
|
+
session_key?: string | undefined;
|
|
21
|
+
continues?: string | undefined;
|
|
22
|
+
episode?: string | undefined;
|
|
19
23
|
};
|
|
20
24
|
coverage: "no-delivery-observed" | "no-relevant-memory" | "delivered";
|
|
21
25
|
content_hash: string;
|
|
@@ -105,6 +109,10 @@ export declare function boundedTaskReportForHost(report: ReturnType<typeof readT
|
|
|
105
109
|
started_at: string;
|
|
106
110
|
finished_at: string | null;
|
|
107
111
|
state: "open" | "completed" | "interrupted";
|
|
112
|
+
closed_by?: "host" | "agent" | undefined;
|
|
113
|
+
session_key?: string | undefined;
|
|
114
|
+
continues?: string | undefined;
|
|
115
|
+
episode?: string | undefined;
|
|
108
116
|
};
|
|
109
117
|
coverage: "no-delivery-observed" | "no-relevant-memory" | "delivered";
|
|
110
118
|
content_hash: string;
|
package/dist/taskReports.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export declare function createTaskReporter(root: string): {
|
|
|
21
21
|
started_at: string;
|
|
22
22
|
finished_at: string | null;
|
|
23
23
|
state: "open" | "completed" | "interrupted";
|
|
24
|
+
closed_by?: "host" | "agent" | undefined;
|
|
25
|
+
session_key?: string | undefined;
|
|
26
|
+
continues?: string | undefined;
|
|
27
|
+
episode?: string | undefined;
|
|
24
28
|
};
|
|
25
29
|
/** The caller supplies the exact envelope it issued plus snapshots of the
|
|
26
30
|
* included revisions. Return the occurrence with the context to the agent.
|
|
@@ -69,6 +73,10 @@ export declare function createTaskReporter(root: string): {
|
|
|
69
73
|
started_at: string;
|
|
70
74
|
finished_at: string | null;
|
|
71
75
|
state: "open" | "completed" | "interrupted";
|
|
76
|
+
closed_by?: "host" | "agent" | undefined;
|
|
77
|
+
session_key?: string | undefined;
|
|
78
|
+
continues?: string | undefined;
|
|
79
|
+
episode?: string | undefined;
|
|
72
80
|
}[];
|
|
73
81
|
lesson: (reference: LessonReference, options?: {
|
|
74
82
|
limit?: number;
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.38.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.38.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|