@davesheffer/hunch 1.33.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/cli/index.js +50 -6
- package/dist/cli/invocation.d.ts +8 -0
- package/dist/cli/invocation.js +17 -10
- package/dist/cli/taskReport.js +52 -4
- package/dist/cli/update.js +5 -5
- package/dist/client/state.d.ts +4 -4
- package/dist/constitution/schema.d.ts +12 -12
- package/dist/core/stateContract.d.ts +3 -3
- package/dist/core/taskRecord.d.ts +39 -0
- package/dist/core/taskRecord.js +185 -0
- package/dist/core/taskReport.d.ts +8 -1
- package/dist/core/taskReport.js +28 -14
- package/dist/core/taskReportEvidence.js +2 -1
- package/dist/core/taskReportHook.d.ts +18 -3
- package/dist/core/taskReportHook.js +77 -8
- package/dist/core/taskReportPaths.d.ts +6 -0
- package/dist/core/taskReportPaths.js +13 -0
- package/dist/core/types.d.ts +176 -1
- package/dist/core/types.js +40 -1
- package/dist/core/updatecheck.d.ts +51 -0
- package/dist/core/updatecheck.js +266 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.js +3 -1
- package/dist/integrations/gitignore.js +1 -0
- package/dist/integrations/health.js +27 -2
- package/dist/mcp/server.js +5 -1
- package/dist/mcp/taskReportTools.d.ts +4 -4
- package/dist/mcp/taskReportTools.js +32 -3
- package/dist/store/hunchStore.d.ts +5 -1
- package/dist/store/hunchStore.js +18 -0
- package/dist/taskReports.d.ts +1 -1
- package/dist/taskReports.js +16 -4
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ hunch init
|
|
|
23
23
|
hunch backfill --since 90d # optional: draft memory from recent history
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
If the shell reports that `hunch` is not found, initialize without a global binary from the
|
|
27
|
+
repository directory: `npx -y @davesheffer/hunch@latest init`.
|
|
28
|
+
|
|
26
29
|
Reload your assistant, then ask:
|
|
27
30
|
|
|
28
31
|
> Why is this built this way, and what should I preserve when changing it?
|
|
@@ -78,8 +81,13 @@ From each repository that uses Hunch:
|
|
|
78
81
|
hunch update
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
If the shell reports that `hunch` is not found, run
|
|
85
|
+
`npx -y @davesheffer/hunch@latest update` from the repository instead.
|
|
86
|
+
|
|
81
87
|
Or ask your agent to **“update Hunch.”** The command installs the latest release, aligns configured integration pins, repairs known legacy launch commands, and refreshes Hunch instructions. It preserves unrelated settings and intentionally disabled hooks.
|
|
82
88
|
|
|
89
|
+
Installed interactive CLI commands can also show a cached update notice. At most once every 24 hours, a detached worker asks npm for the package's public `latest` version; hooks, MCP, CI, servers, the updater, non-interactive commands, and source checkouts skip that request. Set `HUNCH_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1` to disable it.
|
|
90
|
+
|
|
83
91
|
- A standalone npm project keeps Hunch in its existing dependency section at an exact version. Without a repository dependency, the global CLI is updated. Add `--global` to update both.
|
|
84
92
|
- For other package managers or workspaces, update the dependency with that package manager, then run `hunch integrations repair-pins`.
|
|
85
93
|
- Restart or reconnect active assistants. In Codex, open `/hooks` to review and trust changed commands, then start a new session. A changed version pin changes the command and requires renewed trust.
|
|
@@ -178,7 +186,7 @@ Profiles retain their revision, sources, confidence, and freshness. [Project DNA
|
|
|
178
186
|
- [Review memory](docs/review-memory.md)
|
|
179
187
|
- [Agent-origin handling](docs/agent-origin.md)
|
|
180
188
|
- [Autonomy ladder](docs/autonomy-ladder.md)
|
|
181
|
-
- [Autonomous development](docs/autonomous-development.md)
|
|
189
|
+
- [Autonomous development](https://github.com/davesheffer/hunch-private/blob/main/projects/hunch/docs/autonomous-development.md)
|
|
182
190
|
- [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
|
|
183
191
|
- [VS Code extension](vscode-extension/README.md)
|
|
184
192
|
- [Architecture benchmark](bench/architectural-conformance.md)
|
package/dist/cli/index.js
CHANGED
|
@@ -85,7 +85,7 @@ import { recordServed, servedSummary } from "../core/served.js";
|
|
|
85
85
|
import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
86
86
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
87
87
|
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
88
|
-
import { hookReportTaskId, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
88
|
+
import { hookReportTaskId, nativeHookCwd, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
89
89
|
import { recordHookObservation } from "../core/hookObservations.js";
|
|
90
90
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
91
91
|
import { PIPELINE_LOOP, armExecutionObligations, beforeEditProbeVerdict, compileExecutableProbes, environmentExecutableProbes, environmentExecutionObligations, executionObligationBrief, isProductPath, loadPipelineState, onCommand, onEdit, onPrompt, onSkill, pipelineEnabled, proofCheckpoint, savePipelineState, stopVerdict, unverifiedNag, } from "../core/pipeline.js";
|
|
@@ -131,6 +131,7 @@ import { ENTITY_KINDS } from "../core/types.js";
|
|
|
131
131
|
import { planCompaction } from "../store/compact.js";
|
|
132
132
|
import { repairDecisionReference } from "../core/refrepair.js";
|
|
133
133
|
import { resolveInvocation, dim, synthesisStatusLines, maybeWarnOllamaContext } from "./invocation.js";
|
|
134
|
+
import { formatUpdateNotice, scheduleUpdateCheck, shouldCheckForUpdate } from "../core/updatecheck.js";
|
|
134
135
|
const program = new Command();
|
|
135
136
|
program.name("hunch").description("Hunch — engineering memory and a deterministic Change Gate for AI-assisted codebases.").version(HUNCH_VERSION);
|
|
136
137
|
program.option("--initiator <name>", "bind agent launches to the originating CLI (Claude, Codex, Kimi, or a configured adapter)")
|
|
@@ -200,6 +201,29 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
|
|
|
200
201
|
return { root, existing: store.captureHome(privateOnly) === "private"
|
|
201
202
|
? store.recs("constraints") : store.json.loadAll("constraints") };
|
|
202
203
|
});
|
|
204
|
+
// Read an already-known update and schedule any registry refresh in a detached
|
|
205
|
+
// worker. No network handle is opened in this command's process, so the
|
|
206
|
+
// advisory cannot delay command completion or process exit.
|
|
207
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
208
|
+
try {
|
|
209
|
+
const path = [actionCommand.name()];
|
|
210
|
+
for (let parent = actionCommand.parent; parent && parent !== program; parent = parent.parent)
|
|
211
|
+
path.unshift(parent.name());
|
|
212
|
+
const gate = {
|
|
213
|
+
commandName: path.join(" "),
|
|
214
|
+
isTTY: process.stderr.isTTY === true,
|
|
215
|
+
installed: resolveInvocation().installed,
|
|
216
|
+
};
|
|
217
|
+
if (!shouldCheckForUpdate(gate))
|
|
218
|
+
return;
|
|
219
|
+
const result = scheduleUpdateCheck();
|
|
220
|
+
if (result)
|
|
221
|
+
console.error(dim(formatUpdateNotice(result)));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Never let the update-check advisory abort the command it's piggybacking on.
|
|
225
|
+
}
|
|
226
|
+
});
|
|
203
227
|
let openStore = null;
|
|
204
228
|
function openTeamStore(root, opts = {}) {
|
|
205
229
|
// A committed team.json is an explicit declaration that this checkout belongs
|
|
@@ -4193,7 +4217,7 @@ program
|
|
|
4193
4217
|
try {
|
|
4194
4218
|
const records = asOf ? [] : snapshotDeliveredRecords(store, envelope);
|
|
4195
4219
|
const recalled = renderRecalledLine(unseenLessons(root, opts.task, records));
|
|
4196
|
-
const occurrence = recordTaskDelivery(root, opts.task, envelope, records);
|
|
4220
|
+
const occurrence = recordTaskDelivery(root, opts.task, envelope, records, undefined, target);
|
|
4197
4221
|
console.log(`\n${recalled ? `${recalled}\n` : ""}Task evidence: ${opts.task} · occurrence ${occurrence}`);
|
|
4198
4222
|
}
|
|
4199
4223
|
catch {
|
|
@@ -4582,6 +4606,12 @@ program
|
|
|
4582
4606
|
// explorers get the indexed shape, planners get live decisions + what
|
|
4583
4607
|
// was already rejected, everyone else gets the invariant digest. Public
|
|
4584
4608
|
// store only; cheap reads.
|
|
4609
|
+
const routedCwd = nativeHookCwd(root, provider, evt);
|
|
4610
|
+
// A native host that supplied cwd made an explicit scope claim. If it is
|
|
4611
|
+
// malformed or names another checkout, serving this process root's memory
|
|
4612
|
+
// would cross worktrees; stay silent instead of guessing which side is right.
|
|
4613
|
+
if ((provider === "claude" || provider === "codex") && evt.cwd !== undefined && !routedCwd)
|
|
4614
|
+
return;
|
|
4585
4615
|
const s = new HunchStore(paths);
|
|
4586
4616
|
try {
|
|
4587
4617
|
const clip1 = (text, max) => {
|
|
@@ -4591,11 +4621,19 @@ program
|
|
|
4591
4621
|
const type = (evt.agent_type ?? "").toLowerCase();
|
|
4592
4622
|
const L = [];
|
|
4593
4623
|
const served = [];
|
|
4624
|
+
const route = routedCwd
|
|
4625
|
+
? `Worktree routing: call hunch_context first with cwd: ${JSON.stringify(routedCwd)}, and pass the same cwd to hunch_task, hunch_report, and every Hunch capture/write call in this delegated task.`
|
|
4626
|
+
: null;
|
|
4627
|
+
const activeProvider = provider;
|
|
4628
|
+
const emitRouteOnly = () => { if (route)
|
|
4629
|
+
emitContext(activeProvider, "SubagentStart", route); };
|
|
4594
4630
|
if (/explore|search|investigat/.test(type)) {
|
|
4595
4631
|
// Orient from the graph, not grep rounds: the component map IS the shape.
|
|
4596
4632
|
const components = s.advisoryRecs("components").filter((c) => c.status === "active");
|
|
4597
|
-
if (!components.length)
|
|
4633
|
+
if (!components.length) {
|
|
4634
|
+
emitRouteOnly();
|
|
4598
4635
|
return;
|
|
4636
|
+
}
|
|
4599
4637
|
L.push(`🧠 Hunch — repo shape for a delegated explorer: ${components.length} component(s).`);
|
|
4600
4638
|
for (const c of components.slice(0, 12)) {
|
|
4601
4639
|
const line = `- ${c.name}${c.paths.length ? ` (${c.paths.slice(0, 2).join(", ")})` : ""}${c.responsibility ? ` — ${clip1(c.responsibility, 90)}` : ""}`;
|
|
@@ -4611,8 +4649,10 @@ program
|
|
|
4611
4649
|
const decisions = s.advisoryRecs("decisions")
|
|
4612
4650
|
.filter((d) => d.status === "accepted")
|
|
4613
4651
|
.sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
4614
|
-
if (!decisions.length)
|
|
4652
|
+
if (!decisions.length) {
|
|
4653
|
+
emitRouteOnly();
|
|
4615
4654
|
return;
|
|
4655
|
+
}
|
|
4616
4656
|
L.push(`🧠 Hunch — live decisions for a delegated planner (${decisions.length} in force; plans must not re-propose the rejected).`);
|
|
4617
4657
|
for (const d of decisions.slice(0, 6)) {
|
|
4618
4658
|
const line = `- ${d.title} (${d.id})${d.alternatives_rejected.length ? ` — rejected: ${clip1(d.alternatives_rejected[0], 80)}` : ""}`;
|
|
@@ -4626,8 +4666,10 @@ program
|
|
|
4626
4666
|
const constraints = s.advisoryRecs("constraints")
|
|
4627
4667
|
.filter((c) => c.status === "active")
|
|
4628
4668
|
.sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
|
|
4629
|
-
if (!constraints.length)
|
|
4669
|
+
if (!constraints.length) {
|
|
4670
|
+
emitRouteOnly();
|
|
4630
4671
|
return;
|
|
4672
|
+
}
|
|
4631
4673
|
L.push(`🧠 Hunch — delegated agent grounding: ${constraints.length} invariant(s) in force in this repo.`);
|
|
4632
4674
|
for (const c of constraints.slice(0, 8)) {
|
|
4633
4675
|
const line = `- [${c.severity}] ${clip1(c.statement, 140)}${c.scope.length ? ` (scope: ${c.scope.slice(0, 3).join(", ")})` : ""}`;
|
|
@@ -4638,6 +4680,8 @@ program
|
|
|
4638
4680
|
L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
|
|
4639
4681
|
L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
|
|
4640
4682
|
}
|
|
4683
|
+
if (route)
|
|
4684
|
+
L.push(route);
|
|
4641
4685
|
// No dedup here: the hook event carries the PARENT session id, but each
|
|
4642
4686
|
// spawned agent is a fresh empty context — deduping would ground the
|
|
4643
4687
|
// first Explore and silently starve every later one.
|
|
@@ -4900,7 +4944,7 @@ program
|
|
|
4900
4944
|
// The first time a lesson reaches this prompt's task, tell the USER in one
|
|
4901
4945
|
// line (systemMessage); repeats of the same revision stay silent.
|
|
4902
4946
|
recalled = reportPresentationEnabled(root) ? renderRecalledLine(unseenLessons(root, reportTaskId, snapshots)) : null;
|
|
4903
|
-
const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots);
|
|
4947
|
+
const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots, undefined, target);
|
|
4904
4948
|
reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
|
|
4905
4949
|
}
|
|
4906
4950
|
catch {
|
package/dist/cli/invocation.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface ResolvedInvocation {
|
|
|
7
7
|
agentHookShell: string;
|
|
8
8
|
/** Structured command/args for .mcp.json (subcommand appended by the writer). */
|
|
9
9
|
mcp: Invocation;
|
|
10
|
+
/** True only when running from an installed/published copy (global, local,
|
|
11
|
+
* or npx cache), never from a source checkout. */
|
|
12
|
+
installed: boolean;
|
|
10
13
|
}
|
|
11
14
|
export declare function dim(s: string): string;
|
|
12
15
|
/** Portable invocation written into committed MCP/provider configuration.
|
|
@@ -32,4 +35,9 @@ export declare function synthesisStatusLines(resolution: ProviderResolution, env
|
|
|
32
35
|
* separate from synthesisStatusLines (sync, already fully covered) because
|
|
33
36
|
* this one makes a best-effort network call. */
|
|
34
37
|
export declare function maybeWarnOllamaContext(providerName: string, env: NodeJS.ProcessEnv): Promise<string | null>;
|
|
38
|
+
/** Classify the entry path without faking import.meta.url in tests. */
|
|
39
|
+
export declare function classifyEntry(entry: string): {
|
|
40
|
+
isDev: boolean;
|
|
41
|
+
installed: boolean;
|
|
42
|
+
};
|
|
35
43
|
export declare function resolveInvocation(): ResolvedInvocation;
|
package/dist/cli/invocation.js
CHANGED
|
@@ -77,27 +77,32 @@ export async function maybeWarnOllamaContext(providerName, env) {
|
|
|
77
77
|
return null;
|
|
78
78
|
return probeOllamaNumCtx(env.HUNCH_SYNTH_BASE_URL ?? "", env.HUNCH_SYNTH_MODEL ?? "");
|
|
79
79
|
}
|
|
80
|
+
/** Classify the entry path without faking import.meta.url in tests. */
|
|
81
|
+
export function classifyEntry(entry) {
|
|
82
|
+
const isDev = entry.endsWith(".ts");
|
|
83
|
+
const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
|
|
84
|
+
return { isDev, installed };
|
|
85
|
+
}
|
|
80
86
|
export function resolveInvocation() {
|
|
81
87
|
const entry = fileURLToPath(import.meta.url).replace(/invocation\.(js|ts)$/, "index.$1");
|
|
82
|
-
const isDev = entry
|
|
88
|
+
const { isDev, installed } = classifyEntry(entry);
|
|
83
89
|
// JSON.stringify yields a double-quoted, backslash-escaped token /bin/sh
|
|
84
90
|
// accepts — so install paths with spaces don't break the hook command.
|
|
85
91
|
const q = (s) => JSON.stringify(s);
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// absolute-node invocation below.
|
|
94
|
-
const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
|
|
92
|
+
// The MCP/provider config files we write are committed and shared across a
|
|
93
|
+
// team via git, so they must NOT embed this machine's absolute path or
|
|
94
|
+
// OS-specific separators. Reference the exact published Hunch package
|
|
95
|
+
// instead, which `npx` resolves the same on any OS and any clone without
|
|
96
|
+
// floating to a newer release. The git hook lives in per-machine
|
|
97
|
+
// .git/hooks (never committed), so it keeps the PATH-robust absolute-node
|
|
98
|
+
// invocation below.
|
|
95
99
|
if (installed) {
|
|
96
100
|
const mcp = publishedMcpInvocation();
|
|
97
101
|
return {
|
|
98
102
|
shell: `${q(process.execPath)} ${q(entry)}`,
|
|
99
103
|
agentHookShell: shellInvocation(mcp),
|
|
100
104
|
mcp,
|
|
105
|
+
installed,
|
|
101
106
|
};
|
|
102
107
|
}
|
|
103
108
|
if (isDev) {
|
|
@@ -106,6 +111,7 @@ export function resolveInvocation() {
|
|
|
106
111
|
shell: `npx tsx ${q(entry)}`,
|
|
107
112
|
agentHookShell: shellInvocation(mcp),
|
|
108
113
|
mcp,
|
|
114
|
+
installed,
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
// Source-checkout dist run (e.g. `node dist/cli/index.js`, npm link): inherently
|
|
@@ -116,6 +122,7 @@ export function resolveInvocation() {
|
|
|
116
122
|
shell: `${q(process.execPath)} ${q(entry)}`,
|
|
117
123
|
agentHookShell: shellInvocation(mcp),
|
|
118
124
|
mcp,
|
|
125
|
+
installed,
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
//# sourceMappingURL=invocation.js.map
|
package/dist/cli/taskReport.js
CHANGED
|
@@ -7,6 +7,7 @@ import { DEFAULT_CHECK_TIMEOUT_MS, MAX_CHECK_TIMEOUT_MS, reportSourceSnapshot, r
|
|
|
7
7
|
import { renderTaskReport, writeTaskReportHtml } from "../core/taskReportRender.js";
|
|
8
8
|
import { assertReportPath } from "../core/taskReportPaths.js";
|
|
9
9
|
import { publicTaskReport } from "../core/taskReportPublic.js";
|
|
10
|
+
import { mergeDurableTaskSummaries, persistTaskRecord } from "../core/taskRecord.js";
|
|
10
11
|
export function registerTaskReportCommands(program, openStore) {
|
|
11
12
|
const task = program.command("task").description("Record an explicit task lifecycle for Hunch contribution reports");
|
|
12
13
|
task.command("start <title>").option("--id <id>", "retry an exact existing task identity")
|
|
@@ -39,14 +40,44 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
39
40
|
catch { /* disclosed as unverified */ }
|
|
40
41
|
}
|
|
41
42
|
finishReportTask(root, id, opts.interrupted ? "interrupted" : "completed");
|
|
42
|
-
|
|
43
|
+
// The finished task becomes graph memory (.hunch/tasks/) through the normal
|
|
44
|
+
// capture path. A failed write is disclosed, never a reason to lose the card.
|
|
45
|
+
let graph = "";
|
|
46
|
+
try {
|
|
47
|
+
const opened = openStore();
|
|
48
|
+
try {
|
|
49
|
+
const saved = persistTaskRecord(opened.root, opened.store, id);
|
|
50
|
+
graph = saved
|
|
51
|
+
? `\nGraph ${saved.changed ? "saved" : "already saved"} as ${saved.record.id} (${saved.home}${saved.flushed ? `, ${saved.flushed}` : ""})`
|
|
52
|
+
: "\nGraph nothing to keep (no observation, or task records disabled)";
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
opened.store.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
graph = `\nGraph not saved: ${error.message}`;
|
|
60
|
+
}
|
|
61
|
+
console.log(renderTaskReport(readTaskReport(root, id, reportSourceSnapshot(root).hash)) + graph);
|
|
43
62
|
});
|
|
44
63
|
task.command("list").description("Recent tasks observed in this repository with what Hunch delivered, saved, guarded, and checked")
|
|
45
64
|
.option("--limit <n>", "how many recent tasks (max 30)", "30")
|
|
46
65
|
.option("--json", "machine-readable summaries (consumed by the VS Code Contribution view)")
|
|
47
66
|
.action((opts) => {
|
|
48
67
|
const root = findRoot();
|
|
49
|
-
const
|
|
68
|
+
const limit = Number(opts.limit) || 30;
|
|
69
|
+
let summaries = listTaskSummaries(root, limit, reportSourceSnapshot(root).hash);
|
|
70
|
+
// Graph records (this machine's or a teammate's) join the local ledger view.
|
|
71
|
+
try {
|
|
72
|
+
const opened = openStore();
|
|
73
|
+
try {
|
|
74
|
+
summaries = mergeDurableTaskSummaries(opened.store, summaries, limit);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
opened.store.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* ledger-only view when the store is unavailable */ }
|
|
50
81
|
if (opts.json) {
|
|
51
82
|
console.log(JSON.stringify(summaries, null, 2));
|
|
52
83
|
return;
|
|
@@ -56,7 +87,7 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
56
87
|
return;
|
|
57
88
|
}
|
|
58
89
|
for (const s of summaries)
|
|
59
|
-
console.log(`${s.task.started_at.slice(0, 16).replace("T", " ")} ${s.task.task_id} ${s.task.state.padEnd(11)} ${renderTaskStatusLine(s) || "nothing observed"}`);
|
|
90
|
+
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}]` : ""}`);
|
|
60
91
|
});
|
|
61
92
|
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")
|
|
62
93
|
.option("--days <days>", "window in days", "7")
|
|
@@ -188,7 +219,24 @@ export function registerTaskReportCommands(program, openStore) {
|
|
|
188
219
|
console.log(JSON.stringify(publicTaskReport(root, id), null, 2));
|
|
189
220
|
return;
|
|
190
221
|
}
|
|
191
|
-
|
|
222
|
+
let report;
|
|
223
|
+
try {
|
|
224
|
+
report = readTaskReport(root, id, reportSourceSnapshot(root).hash);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
// Not in this machine's ledger: the graph record (if any) is what remains.
|
|
228
|
+
const opened = openStore();
|
|
229
|
+
try {
|
|
230
|
+
const record = opened.store.getRec("tasks", id);
|
|
231
|
+
if (!record)
|
|
232
|
+
throw error;
|
|
233
|
+
console.log(opts.json ? JSON.stringify(record, null, 2) : `Task ${record.id} · ${record.state} · ${record.title}\nGraph record only (no local observation ledger for it here): ${record.lessons.length} lesson(s), ${record.applied.length} applied, ${record.saved.length} saved, ${record.checks.length} check(s), ${record.refusals} denied. Files: ${record.files.join(", ") || "none recorded"}.`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
opened.store.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
192
240
|
console.log(opts.json ? JSON.stringify(report, null, 2) : renderTaskReport(report));
|
|
193
241
|
});
|
|
194
242
|
}
|
package/dist/cli/update.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { findRoot } from "../core/paths.js";
|
|
5
|
-
|
|
5
|
+
import { HUNCH_PACKAGE_NAME } from "../core/version.js";
|
|
6
6
|
/** Arguments come only from fixed commands and a validated registry version.
|
|
7
7
|
* Windows needs the shell to resolve npm.cmd; cwd is never interpolated. */
|
|
8
8
|
export function runNpm(root, args, capture = false) {
|
|
@@ -26,24 +26,24 @@ export function updateHunch(root, opts = {}, run = (args, capture) => runNpm(roo
|
|
|
26
26
|
const manifest = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
27
27
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
28
28
|
throw new Error("package.json must contain an object");
|
|
29
|
-
if (manifest.name ===
|
|
29
|
+
if (manifest.name === HUNCH_PACKAGE_NAME)
|
|
30
30
|
throw new Error("Run hunch update in a consumer repository, not Hunch's own source checkout.");
|
|
31
31
|
const sections = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
32
32
|
const declared = sections.filter(section => {
|
|
33
33
|
const deps = manifest[section];
|
|
34
34
|
if (deps !== undefined && (!deps || typeof deps !== "object" || Array.isArray(deps)))
|
|
35
35
|
throw new Error(`invalid ${section} in package.json`);
|
|
36
|
-
return deps && Object.hasOwn(deps,
|
|
36
|
+
return deps && Object.hasOwn(deps, HUNCH_PACKAGE_NAME);
|
|
37
37
|
});
|
|
38
38
|
if (declared.length > 1)
|
|
39
39
|
throw new Error("Hunch is declared in multiple dependency sections; resolve the duplicate before updating.");
|
|
40
40
|
if (declared.length && (manifest.workspaces || (manifest.packageManager && !/^npm@/.test(manifest.packageManager)) || ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"].some(name => existsSync(join(root, name))))) {
|
|
41
41
|
throw new Error("Automatic dependency updates currently support standalone npm projects. Update Hunch to an exact version with your package manager, then run hunch integrations repair-pins.");
|
|
42
42
|
}
|
|
43
|
-
const version = JSON.parse(run(["view", `${
|
|
43
|
+
const version = JSON.parse(run(["view", `${HUNCH_PACKAGE_NAME}@latest`, "version", "--json"], true));
|
|
44
44
|
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version))
|
|
45
45
|
throw new Error("npm returned an invalid Hunch version");
|
|
46
|
-
const spec = `${
|
|
46
|
+
const spec = `${HUNCH_PACKAGE_NAME}@${version}`;
|
|
47
47
|
const commands = [];
|
|
48
48
|
if (declared.length) {
|
|
49
49
|
const flag = { dependencies: "--save-prod", devDependencies: "--save-dev", optionalDependencies: "--save-optional" }[declared[0]];
|
package/dist/client/state.d.ts
CHANGED
|
@@ -178,7 +178,7 @@ export declare function createStateClient(opts: StateClientOptions): {
|
|
|
178
178
|
schema: "nuryel.state.write/1";
|
|
179
179
|
record_id: string;
|
|
180
180
|
record_hash: string;
|
|
181
|
-
durability: "
|
|
181
|
+
durability: "local" | "committed" | "pushed";
|
|
182
182
|
outcome: "updated" | "superseded" | "created" | "replayed";
|
|
183
183
|
conflict: {
|
|
184
184
|
incumbent_id: string;
|
|
@@ -190,7 +190,7 @@ export declare function createStateClient(opts: StateClientOptions): {
|
|
|
190
190
|
schema: "nuryel.state.write/1";
|
|
191
191
|
record_id: string;
|
|
192
192
|
record_hash: string;
|
|
193
|
-
durability: "
|
|
193
|
+
durability: "local" | "committed" | "pushed";
|
|
194
194
|
outcome: "updated" | "superseded" | "created" | "replayed";
|
|
195
195
|
conflict: {
|
|
196
196
|
incumbent_id: string;
|
|
@@ -207,7 +207,7 @@ export declare function createStateClient(opts: StateClientOptions): {
|
|
|
207
207
|
schema: "nuryel.state.write/1";
|
|
208
208
|
record_id: string;
|
|
209
209
|
record_hash: string;
|
|
210
|
-
durability: "
|
|
210
|
+
durability: "local" | "committed" | "pushed";
|
|
211
211
|
outcome: "updated" | "superseded" | "created" | "replayed";
|
|
212
212
|
conflict: {
|
|
213
213
|
incumbent_id: string;
|
|
@@ -228,7 +228,7 @@ export declare function createStateClient(opts: StateClientOptions): {
|
|
|
228
228
|
schema: "nuryel.state.write/1";
|
|
229
229
|
record_id: string;
|
|
230
230
|
record_hash: string;
|
|
231
|
-
durability: "
|
|
231
|
+
durability: "local" | "committed" | "pushed";
|
|
232
232
|
outcome: "updated" | "superseded" | "created" | "replayed";
|
|
233
233
|
conflict: {
|
|
234
234
|
incumbent_id: string;
|
|
@@ -370,13 +370,13 @@ export declare const PolicyStateSchema: z.ZodEnum<{
|
|
|
370
370
|
proposed: "proposed";
|
|
371
371
|
rejected: "rejected";
|
|
372
372
|
superseded: "superseded";
|
|
373
|
+
repaired: "repaired";
|
|
374
|
+
active_advisory: "active_advisory";
|
|
375
|
+
active_blocking: "active_blocking";
|
|
373
376
|
compiled: "compiled";
|
|
377
|
+
validating: "validating";
|
|
374
378
|
uncompilable: "uncompilable";
|
|
375
379
|
drafted: "drafted";
|
|
376
|
-
validating: "validating";
|
|
377
|
-
active_advisory: "active_advisory";
|
|
378
|
-
active_blocking: "active_blocking";
|
|
379
|
-
repaired: "repaired";
|
|
380
380
|
}>;
|
|
381
381
|
export type PolicyState = z.infer<typeof PolicyStateSchema>;
|
|
382
382
|
export declare const PolicySelectorSchema: z.ZodObject<{
|
|
@@ -470,8 +470,8 @@ export declare const PolicyAuditEventSchema: z.ZodObject<{
|
|
|
470
470
|
retired: "retired";
|
|
471
471
|
withdrawn: "withdrawn";
|
|
472
472
|
rejected: "rejected";
|
|
473
|
-
compiled: "compiled";
|
|
474
473
|
repaired: "repaired";
|
|
474
|
+
compiled: "compiled";
|
|
475
475
|
enriched: "enriched";
|
|
476
476
|
linked_exception: "linked_exception";
|
|
477
477
|
proved: "proved";
|
|
@@ -518,13 +518,13 @@ export declare const PolicySpecSchema: z.ZodObject<{
|
|
|
518
518
|
proposed: "proposed";
|
|
519
519
|
rejected: "rejected";
|
|
520
520
|
superseded: "superseded";
|
|
521
|
+
repaired: "repaired";
|
|
522
|
+
active_advisory: "active_advisory";
|
|
523
|
+
active_blocking: "active_blocking";
|
|
521
524
|
compiled: "compiled";
|
|
525
|
+
validating: "validating";
|
|
522
526
|
uncompilable: "uncompilable";
|
|
523
527
|
drafted: "drafted";
|
|
524
|
-
validating: "validating";
|
|
525
|
-
active_advisory: "active_advisory";
|
|
526
|
-
active_blocking: "active_blocking";
|
|
527
|
-
repaired: "repaired";
|
|
528
528
|
}>;
|
|
529
529
|
statement: z.ZodString;
|
|
530
530
|
rationale: z.ZodDefault<z.ZodString>;
|
|
@@ -610,10 +610,10 @@ export declare const PolicySpecSchema: z.ZodObject<{
|
|
|
610
610
|
}>>;
|
|
611
611
|
surfaces: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
612
612
|
ci: "ci";
|
|
613
|
-
pre_edit: "pre_edit";
|
|
614
|
-
pre_commit: "pre_commit";
|
|
615
613
|
mcp: "mcp";
|
|
616
614
|
cli: "cli";
|
|
615
|
+
pre_edit: "pre_edit";
|
|
616
|
+
pre_commit: "pre_commit";
|
|
617
617
|
}>>>;
|
|
618
618
|
authority: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
619
619
|
kind: z.ZodLiteral<"human">;
|
|
@@ -663,8 +663,8 @@ export declare const PolicySpecSchema: z.ZodObject<{
|
|
|
663
663
|
retired: "retired";
|
|
664
664
|
withdrawn: "withdrawn";
|
|
665
665
|
rejected: "rejected";
|
|
666
|
-
compiled: "compiled";
|
|
667
666
|
repaired: "repaired";
|
|
667
|
+
compiled: "compiled";
|
|
668
668
|
enriched: "enriched";
|
|
669
669
|
linked_exception: "linked_exception";
|
|
670
670
|
proved: "proved";
|
|
@@ -780,9 +780,9 @@ export declare const WriteResultSchema: z.ZodObject<{
|
|
|
780
780
|
record_id: z.ZodString;
|
|
781
781
|
record_hash: z.ZodString;
|
|
782
782
|
durability: z.ZodEnum<{
|
|
783
|
+
local: "local";
|
|
783
784
|
committed: "committed";
|
|
784
785
|
pushed: "pushed";
|
|
785
|
-
local: "local";
|
|
786
786
|
}>;
|
|
787
787
|
outcome: z.ZodEnum<{
|
|
788
788
|
updated: "updated";
|
|
@@ -807,9 +807,9 @@ export declare const CaptureBatchResultSchema: z.ZodObject<{
|
|
|
807
807
|
record_id: z.ZodString;
|
|
808
808
|
record_hash: z.ZodString;
|
|
809
809
|
durability: z.ZodEnum<{
|
|
810
|
+
local: "local";
|
|
810
811
|
committed: "committed";
|
|
811
812
|
pushed: "pushed";
|
|
812
|
-
local: "local";
|
|
813
813
|
}>;
|
|
814
814
|
outcome: z.ZodEnum<{
|
|
815
815
|
updated: "updated";
|
|
@@ -837,9 +837,9 @@ export declare const CaptureBatchResultSchema: z.ZodObject<{
|
|
|
837
837
|
record_id: z.ZodString;
|
|
838
838
|
record_hash: z.ZodString;
|
|
839
839
|
durability: z.ZodEnum<{
|
|
840
|
+
local: "local";
|
|
840
841
|
committed: "committed";
|
|
841
842
|
pushed: "pushed";
|
|
842
|
-
local: "local";
|
|
843
843
|
}>;
|
|
844
844
|
outcome: z.ZodEnum<{
|
|
845
845
|
updated: "updated";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HunchStore } from "../store/hunchStore.js";
|
|
2
|
+
import { type TaskReport, type TaskSummary } from "./taskReport.js";
|
|
3
|
+
import { type TaskRecord } from "./types.js";
|
|
4
|
+
export type TaskRecordHome = "public" | "private";
|
|
5
|
+
/** `taskRecords: false` in `.hunch/local.json` keeps tasks ledger-only. */
|
|
6
|
+
export declare function taskRecordsEnabled(root: string): boolean;
|
|
7
|
+
/** `taskRecordsFlush: "batch"` writes the record but leaves the commit to the
|
|
8
|
+
* next capture flush (decision, finding, correction), so a busy repository
|
|
9
|
+
* gets one memory commit per real capture instead of one per prompt. Default
|
|
10
|
+
* "each": every finished task commits like any other capture. */
|
|
11
|
+
export declare function taskRecordFlushMode(root: string): "each" | "batch";
|
|
12
|
+
/** A delivery target that names code (a path or dotted symbol), not a task
|
|
13
|
+
* phrase like "fix the login redirect". Phrases never become file anchors. */
|
|
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
|
+
export declare function taskRecordFromReport(report: TaskReport): TaskRecord | null;
|
|
17
|
+
/** Where the record belongs. Anything that touched the private overlay — a
|
|
18
|
+
* private save, or a delivered lesson that lives only there — must not be named
|
|
19
|
+
* in a public record; the store's own routing (unified/shared mode) wins first. */
|
|
20
|
+
export declare function taskRecordHome(store: HunchStore, record: TaskRecord): TaskRecordHome;
|
|
21
|
+
export interface PersistedTaskRecord {
|
|
22
|
+
record: TaskRecord;
|
|
23
|
+
home: TaskRecordHome;
|
|
24
|
+
flushed: "pushed" | "committed" | null;
|
|
25
|
+
/** False when the same report revision was already in the graph. */
|
|
26
|
+
changed: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Write (or refresh) the graph record for a finished task. Idempotent on the
|
|
29
|
+
* report hash. A record never changes home once written. Returns null for an
|
|
30
|
+
* open task, an empty report, or when task records are disabled locally. */
|
|
31
|
+
export declare function persistTaskRecord(root: string, store: HunchStore, taskId: string, options?: {
|
|
32
|
+
flush?: boolean;
|
|
33
|
+
}): PersistedTaskRecord | null;
|
|
34
|
+
/** A ledger-shaped summary for a task known only from the graph (another
|
|
35
|
+
* machine, a teammate, or a pruned local ledger). */
|
|
36
|
+
export declare function summaryFromTaskRecord(record: TaskRecord, home: TaskRecordHome): TaskSummary;
|
|
37
|
+
/** Ledger summaries annotated with their graph home, plus graph-only tasks the
|
|
38
|
+
* local ledger never saw. Newest first, bounded. */
|
|
39
|
+
export declare function mergeDurableTaskSummaries(store: HunchStore, summaries: TaskSummary[], limit?: number): TaskSummary[];
|