@davesheffer/hunch 1.33.0 → 1.36.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 +55 -7
- 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/spawnCommand.d.ts +14 -0
- package/dist/core/spawnCommand.js +61 -0
- package/dist/core/stateContract.d.ts +3 -3
- package/dist/core/taskDelivery.d.ts +15 -0
- package/dist/core/taskDelivery.js +38 -0
- 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 +18 -3
- package/dist/core/taskReportHook.d.ts +18 -3
- package/dist/core/taskReportHook.js +82 -17
- 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/claudemd.js +1 -1
- package/dist/integrations/gitignore.js +1 -0
- package/dist/integrations/health.js +27 -2
- package/dist/mcp/server.js +11 -2
- package/dist/mcp/taskReportTools.d.ts +4 -4
- package/dist/mcp/taskReportTools.js +34 -8
- 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
|
@@ -66,6 +66,7 @@ import { scaffoldProviders, regenerateGrounding, refreshExistingGrounding, refre
|
|
|
66
66
|
import { healClaudeConfigCaseSplit } from "../integrations/claudeConfig.js";
|
|
67
67
|
import { formatSearchHit, formatStructure } from "../core/format.js";
|
|
68
68
|
import { isStateKind, renderStateLine, stateSupplements } from "../core/stateDelivery.js";
|
|
69
|
+
import { taskSupplements } from "../core/taskDelivery.js";
|
|
69
70
|
import { diagnoseIssueCorrectionStage, formatCorrectionStageDiagnostic } from "../core/correctionStage.js";
|
|
70
71
|
import { compileVerifiedEvidenceMap, formatVerifiedEvidenceMap } from "../core/evidenceMap.js";
|
|
71
72
|
import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
|
|
@@ -85,7 +86,7 @@ import { recordServed, servedSummary } from "../core/served.js";
|
|
|
85
86
|
import { recordTaskDelivery, reportActivity, reportPresentationEnabled, unseenLessons } from "../core/taskReport.js";
|
|
86
87
|
import { snapshotDeliveredRecords } from "../core/taskReportEvidence.js";
|
|
87
88
|
import { renderRecalledLine } from "../core/taskReportRender.js";
|
|
88
|
-
import { hookReportTaskId, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
89
|
+
import { hookReportTaskId, nativeHookCwd, startHookReport, stopHookReport, observeHookDenial } from "../core/taskReportHook.js";
|
|
89
90
|
import { recordHookObservation } from "../core/hookObservations.js";
|
|
90
91
|
import { contextHookOutput, denyHookOutput, hookProvider, normalizeHookEvent, stopHookOutput } from "../core/agenthook.js";
|
|
91
92
|
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 +132,7 @@ import { ENTITY_KINDS } from "../core/types.js";
|
|
|
131
132
|
import { planCompaction } from "../store/compact.js";
|
|
132
133
|
import { repairDecisionReference } from "../core/refrepair.js";
|
|
133
134
|
import { resolveInvocation, dim, synthesisStatusLines, maybeWarnOllamaContext } from "./invocation.js";
|
|
135
|
+
import { formatUpdateNotice, scheduleUpdateCheck, shouldCheckForUpdate } from "../core/updatecheck.js";
|
|
134
136
|
const program = new Command();
|
|
135
137
|
program.name("hunch").description("Hunch — engineering memory and a deterministic Change Gate for AI-assisted codebases.").version(HUNCH_VERSION);
|
|
136
138
|
program.option("--initiator <name>", "bind agent launches to the originating CLI (Claude, Codex, Kimi, or a configured adapter)")
|
|
@@ -200,6 +202,29 @@ registerReviewMemoryCommands(program, (records, repository, privateOnly) => {
|
|
|
200
202
|
return { root, existing: store.captureHome(privateOnly) === "private"
|
|
201
203
|
? store.recs("constraints") : store.json.loadAll("constraints") };
|
|
202
204
|
});
|
|
205
|
+
// Read an already-known update and schedule any registry refresh in a detached
|
|
206
|
+
// worker. No network handle is opened in this command's process, so the
|
|
207
|
+
// advisory cannot delay command completion or process exit.
|
|
208
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
209
|
+
try {
|
|
210
|
+
const path = [actionCommand.name()];
|
|
211
|
+
for (let parent = actionCommand.parent; parent && parent !== program; parent = parent.parent)
|
|
212
|
+
path.unshift(parent.name());
|
|
213
|
+
const gate = {
|
|
214
|
+
commandName: path.join(" "),
|
|
215
|
+
isTTY: process.stderr.isTTY === true,
|
|
216
|
+
installed: resolveInvocation().installed,
|
|
217
|
+
};
|
|
218
|
+
if (!shouldCheckForUpdate(gate))
|
|
219
|
+
return;
|
|
220
|
+
const result = scheduleUpdateCheck();
|
|
221
|
+
if (result)
|
|
222
|
+
console.error(dim(formatUpdateNotice(result)));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Never let the update-check advisory abort the command it's piggybacking on.
|
|
226
|
+
}
|
|
227
|
+
});
|
|
203
228
|
let openStore = null;
|
|
204
229
|
function openTeamStore(root, opts = {}) {
|
|
205
230
|
// A committed team.json is an explicit declaration that this checkout belongs
|
|
@@ -4186,14 +4211,14 @@ program
|
|
|
4186
4211
|
decisionCorpus: store.recs("decisions"),
|
|
4187
4212
|
historical: !!asOf,
|
|
4188
4213
|
profile: opts.profile,
|
|
4189
|
-
supplements: stateGrounding,
|
|
4214
|
+
supplements: [...stateGrounding, ...(asOf ? [] : taskSupplements(store.tasksFor(target, 3), target))],
|
|
4190
4215
|
});
|
|
4191
4216
|
process.stdout.write(envelope.text);
|
|
4192
4217
|
if (opts.task) {
|
|
4193
4218
|
try {
|
|
4194
4219
|
const records = asOf ? [] : snapshotDeliveredRecords(store, envelope);
|
|
4195
4220
|
const recalled = renderRecalledLine(unseenLessons(root, opts.task, records));
|
|
4196
|
-
const occurrence = recordTaskDelivery(root, opts.task, envelope, records);
|
|
4221
|
+
const occurrence = recordTaskDelivery(root, opts.task, envelope, records, undefined, target);
|
|
4197
4222
|
console.log(`\n${recalled ? `${recalled}\n` : ""}Task evidence: ${opts.task} · occurrence ${occurrence}`);
|
|
4198
4223
|
}
|
|
4199
4224
|
catch {
|
|
@@ -4582,6 +4607,12 @@ program
|
|
|
4582
4607
|
// explorers get the indexed shape, planners get live decisions + what
|
|
4583
4608
|
// was already rejected, everyone else gets the invariant digest. Public
|
|
4584
4609
|
// store only; cheap reads.
|
|
4610
|
+
const routedCwd = nativeHookCwd(root, provider, evt);
|
|
4611
|
+
// A native host that supplied cwd made an explicit scope claim. If it is
|
|
4612
|
+
// malformed or names another checkout, serving this process root's memory
|
|
4613
|
+
// would cross worktrees; stay silent instead of guessing which side is right.
|
|
4614
|
+
if ((provider === "claude" || provider === "codex") && evt.cwd !== undefined && !routedCwd)
|
|
4615
|
+
return;
|
|
4585
4616
|
const s = new HunchStore(paths);
|
|
4586
4617
|
try {
|
|
4587
4618
|
const clip1 = (text, max) => {
|
|
@@ -4591,11 +4622,19 @@ program
|
|
|
4591
4622
|
const type = (evt.agent_type ?? "").toLowerCase();
|
|
4592
4623
|
const L = [];
|
|
4593
4624
|
const served = [];
|
|
4625
|
+
const route = routedCwd
|
|
4626
|
+
? `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.`
|
|
4627
|
+
: null;
|
|
4628
|
+
const activeProvider = provider;
|
|
4629
|
+
const emitRouteOnly = () => { if (route)
|
|
4630
|
+
emitContext(activeProvider, "SubagentStart", route); };
|
|
4594
4631
|
if (/explore|search|investigat/.test(type)) {
|
|
4595
4632
|
// Orient from the graph, not grep rounds: the component map IS the shape.
|
|
4596
4633
|
const components = s.advisoryRecs("components").filter((c) => c.status === "active");
|
|
4597
|
-
if (!components.length)
|
|
4634
|
+
if (!components.length) {
|
|
4635
|
+
emitRouteOnly();
|
|
4598
4636
|
return;
|
|
4637
|
+
}
|
|
4599
4638
|
L.push(`🧠 Hunch — repo shape for a delegated explorer: ${components.length} component(s).`);
|
|
4600
4639
|
for (const c of components.slice(0, 12)) {
|
|
4601
4640
|
const line = `- ${c.name}${c.paths.length ? ` (${c.paths.slice(0, 2).join(", ")})` : ""}${c.responsibility ? ` — ${clip1(c.responsibility, 90)}` : ""}`;
|
|
@@ -4611,8 +4650,10 @@ program
|
|
|
4611
4650
|
const decisions = s.advisoryRecs("decisions")
|
|
4612
4651
|
.filter((d) => d.status === "accepted")
|
|
4613
4652
|
.sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
4614
|
-
if (!decisions.length)
|
|
4653
|
+
if (!decisions.length) {
|
|
4654
|
+
emitRouteOnly();
|
|
4615
4655
|
return;
|
|
4656
|
+
}
|
|
4616
4657
|
L.push(`🧠 Hunch — live decisions for a delegated planner (${decisions.length} in force; plans must not re-propose the rejected).`);
|
|
4617
4658
|
for (const d of decisions.slice(0, 6)) {
|
|
4618
4659
|
const line = `- ${d.title} (${d.id})${d.alternatives_rejected.length ? ` — rejected: ${clip1(d.alternatives_rejected[0], 80)}` : ""}`;
|
|
@@ -4626,8 +4667,10 @@ program
|
|
|
4626
4667
|
const constraints = s.advisoryRecs("constraints")
|
|
4627
4668
|
.filter((c) => c.status === "active")
|
|
4628
4669
|
.sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
|
|
4629
|
-
if (!constraints.length)
|
|
4670
|
+
if (!constraints.length) {
|
|
4671
|
+
emitRouteOnly();
|
|
4630
4672
|
return;
|
|
4673
|
+
}
|
|
4631
4674
|
L.push(`🧠 Hunch — delegated agent grounding: ${constraints.length} invariant(s) in force in this repo.`);
|
|
4632
4675
|
for (const c of constraints.slice(0, 8)) {
|
|
4633
4676
|
const line = `- [${c.severity}] ${clip1(c.statement, 140)}${c.scope.length ? ` (scope: ${c.scope.slice(0, 3).join(", ")})` : ""}`;
|
|
@@ -4638,6 +4681,8 @@ program
|
|
|
4638
4681
|
L.push(`…and ${constraints.length - 8} more — hunch_check_constraints(scope) for your files.`);
|
|
4639
4682
|
L.push("Before editing: hunch_check_constraints(scope) · hunch_why(target). Orient: hunch_context(task).");
|
|
4640
4683
|
}
|
|
4684
|
+
if (route)
|
|
4685
|
+
L.push(route);
|
|
4641
4686
|
// No dedup here: the hook event carries the PARENT session id, but each
|
|
4642
4687
|
// spawned agent is a fresh empty context — deduping would ground the
|
|
4643
4688
|
// first Explore and silently starve every later one.
|
|
@@ -4836,6 +4881,7 @@ program
|
|
|
4836
4881
|
// from this file. No diff exists yet, so this is context — "don't re-add X" —
|
|
4837
4882
|
// not a block; the commit-time `hunch check` does the actual gating.
|
|
4838
4883
|
const retired = store.retiredForFile(target).filter((r) => r.symbols.length || r.deps.length);
|
|
4884
|
+
const recentTasks = taskSupplements(store.tasksFor(target, 3), target);
|
|
4839
4885
|
const hasContent = ctx.constraints.length ||
|
|
4840
4886
|
ctx.decisions.length ||
|
|
4841
4887
|
ctx.bugs.length ||
|
|
@@ -4844,6 +4890,7 @@ program
|
|
|
4844
4890
|
ctx.landscape?.resources.length ||
|
|
4845
4891
|
ctx.landscape?.relationships.length ||
|
|
4846
4892
|
retired.length ||
|
|
4893
|
+
recentTasks.length ||
|
|
4847
4894
|
docGround;
|
|
4848
4895
|
if (!hasContent)
|
|
4849
4896
|
return; // no noise on files Hunch hasn't learned yet
|
|
@@ -4861,6 +4908,7 @@ program
|
|
|
4861
4908
|
text: `⚠ Deliberately RETIRED from this file — do not re-introduce without cause: ${retired.map((r) => `${[...r.symbols, ...r.deps].join(", ")} (${r.decision})`).join("; ")}.`,
|
|
4862
4909
|
}] : []),
|
|
4863
4910
|
...(docGround ? [{ id: "doc-grounding", kind: "doc-grounding", priority: 100, text: docGround }] : []),
|
|
4911
|
+
...recentTasks,
|
|
4864
4912
|
],
|
|
4865
4913
|
});
|
|
4866
4914
|
const text = envelope.text.trim();
|
|
@@ -4900,7 +4948,7 @@ program
|
|
|
4900
4948
|
// The first time a lesson reaches this prompt's task, tell the USER in one
|
|
4901
4949
|
// line (systemMessage); repeats of the same revision stay silent.
|
|
4902
4950
|
recalled = reportPresentationEnabled(root) ? renderRecalledLine(unseenLessons(root, reportTaskId, snapshots)) : null;
|
|
4903
|
-
const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots);
|
|
4951
|
+
const occurrence = recordTaskDelivery(root, reportTaskId, envelope, snapshots, undefined, target);
|
|
4904
4952
|
reportNotice = `\n\nHunch task ${reportTaskId} · delivery ${occurrence}. Inspect exact application references with hunch_report(task_id).`;
|
|
4905
4953
|
}
|
|
4906
4954
|
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";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ResolvedSpawn {
|
|
2
|
+
file: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
/** Set when a batch launcher runs through cmd.exe and the line is pre-quoted. */
|
|
5
|
+
windowsVerbatimArguments?: boolean;
|
|
6
|
+
how: "direct" | "npm-cli" | "pathext" | "cmd-shim";
|
|
7
|
+
}
|
|
8
|
+
export interface SpawnResolveOptions {
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
execPath?: string;
|
|
12
|
+
exists?: (path: string) => boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveSpawnCommand(command: readonly string[], options?: SpawnResolveOptions): ResolvedSpawn;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Resolve a user-supplied argv into something `spawn` can run without a shell.
|
|
2
|
+
*
|
|
3
|
+
* On POSIX the argv is already right. On Windows, `spawn("npx", ...)` with
|
|
4
|
+
* `shell: false` fails: the launcher is `npx.cmd`, and Node refuses to run
|
|
5
|
+
* `.cmd`/`.bat` files directly. The verification runner used to swallow that
|
|
6
|
+
* as `exit_code: null`, so every contribution card on Windows said "no
|
|
7
|
+
* independent command result". This keeps `shell: false` for real
|
|
8
|
+
* executables and only routes batch launchers through `cmd.exe`, with the
|
|
9
|
+
* npm/npx launchers run as plain Node scripts (no shell at all). */
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { posix, win32 } from "node:path";
|
|
12
|
+
/** cmd.exe quoting for one argument: wrap when it has whitespace or shell
|
|
13
|
+
* metacharacters; double embedded quotes. Good for test/build commands; a
|
|
14
|
+
* deliberately hostile argument still cannot escape because the whole line is
|
|
15
|
+
* passed as one `/s /c "..."` token. */
|
|
16
|
+
function quoteForCmd(arg) {
|
|
17
|
+
if (arg === "")
|
|
18
|
+
return '""';
|
|
19
|
+
if (!/[\s"&|<>^()%!]/.test(arg))
|
|
20
|
+
return arg;
|
|
21
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
22
|
+
}
|
|
23
|
+
export function resolveSpawnCommand(command, options = {}) {
|
|
24
|
+
const platform = options.platform ?? process.platform;
|
|
25
|
+
const [cmd = "", ...args] = command;
|
|
26
|
+
if (platform !== "win32")
|
|
27
|
+
return { file: cmd, args, how: "direct" };
|
|
28
|
+
const env = options.env ?? process.env;
|
|
29
|
+
const exists = options.exists ?? existsSync;
|
|
30
|
+
const execPath = options.execPath ?? process.execPath;
|
|
31
|
+
// Resolve Windows paths with Windows semantics even when the resolution is
|
|
32
|
+
// exercised (tested) on another platform; the host's default `path` is POSIX there.
|
|
33
|
+
const { join, dirname } = platform === "win32" ? win32 : posix;
|
|
34
|
+
// npm / npx: run the CLI script with this same Node. No shim, no shell.
|
|
35
|
+
if (/^(npm|npx)$/i.test(cmd)) {
|
|
36
|
+
const script = join(dirname(execPath), "node_modules", "npm", "bin", `${cmd.toLowerCase()}-cli.js`);
|
|
37
|
+
if (exists(script))
|
|
38
|
+
return { file: execPath, args: [script, ...args], how: "npm-cli" };
|
|
39
|
+
}
|
|
40
|
+
// A path or an explicit executable extension: spawn as given.
|
|
41
|
+
if (/[\\/]/.test(cmd) || /\.(exe|com)$/i.test(cmd))
|
|
42
|
+
return { file: cmd, args, how: "direct" };
|
|
43
|
+
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
44
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(";").map((d) => d.trim()).filter(Boolean);
|
|
45
|
+
for (const dir of dirs) {
|
|
46
|
+
for (const ext of ["", ...pathExt]) {
|
|
47
|
+
const candidate = join(dir, cmd + ext);
|
|
48
|
+
if (!exists(candidate))
|
|
49
|
+
continue;
|
|
50
|
+
if (/\.(cmd|bat)$/i.test(candidate)) {
|
|
51
|
+
const line = [candidate, ...args].map(quoteForCmd).join(" ");
|
|
52
|
+
return { file: env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true, how: "cmd-shim" };
|
|
53
|
+
}
|
|
54
|
+
if (ext === "" && !/\.(exe|com)$/i.test(candidate))
|
|
55
|
+
continue; // an extensionless file is not runnable on Windows
|
|
56
|
+
return { file: candidate, args, how: "pathext" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { file: cmd, args, how: "direct" };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=spawnCommand.js.map
|
|
@@ -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,15 @@
|
|
|
1
|
+
/** Recent finished tasks as delivered context.
|
|
2
|
+
*
|
|
3
|
+
* Task records (`.hunch/tasks/`) say what earlier agent work did around a file:
|
|
4
|
+
* which lessons it received, what it applied, saved and checked, and whether a
|
|
5
|
+
* rule was violated. Delivering the newest few next to the invariants lets the
|
|
6
|
+
* next agent build on verified work instead of rediscovering it. Supplements
|
|
7
|
+
* share the brief's budget and are advisory: a task line is history, never a
|
|
8
|
+
* rule, and never an instruction to repeat or skip anything. */
|
|
9
|
+
import type { DeliverySupplement } from "./delivery.js";
|
|
10
|
+
import type { TaskRecord } from "./types.js";
|
|
11
|
+
export declare const TASK_SUPPLEMENT_LIMIT = 3;
|
|
12
|
+
/** One bounded line for a task: identity, when, what reached it, what it did. */
|
|
13
|
+
export declare function describeTaskRecord(t: TaskRecord): string;
|
|
14
|
+
/** Newest first, bounded. Empty input yields no supplement at all (no header noise). */
|
|
15
|
+
export declare function taskSupplements(tasks: readonly TaskRecord[], target: string, limit?: number): DeliverySupplement[];
|