@davesheffer/hunch 1.46.0 → 1.47.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 +151 -321
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.js +9 -9
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +8 -23
- package/dist/extractors/git.js +45 -64
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +60 -73
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -111,20 +111,15 @@ export declare function installPostCommitHook(root: string, invocation: string,
|
|
|
111
111
|
* blocking invariant (see strictgate.ts), so it's safe on a shared repo.
|
|
112
112
|
* Preserves any existing pre-commit hook. */
|
|
113
113
|
export declare function installPreCommitHook(root: string, invocation: string, strict?: boolean): HookInstall;
|
|
114
|
-
/** Install
|
|
115
|
-
* re-
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* no commit of its own and so can't reuse HUNCH_SYNC's re-trigger guard).
|
|
124
|
-
* Each block is keyed by its own marker pair (installManagedBlock), so
|
|
125
|
-
* re-running updates only its own block, preserves the other untouched, and
|
|
126
|
-
* a repo carrying only one half (an older install, or a hand-edited hook)
|
|
127
|
-
* gets the other appended rather than clobbered. */
|
|
114
|
+
/** Install the post-merge hook. It carries two independently managed blocks:
|
|
115
|
+
* - grounding: re-syncs committed grounding docs when a merge brought in memory.
|
|
116
|
+
* Guarded by HUNCH_SYNC, runs in the foreground, never fails the merge.
|
|
117
|
+
* - repair: detects a decision's commit provenance being orphaned by a
|
|
118
|
+
* squash-merge (including a fast-forward from `git pull`) and queues the match
|
|
119
|
+
* for `hunch repair-provenance --apply`. Guarded by HUNCH_MERGE_SYNC, runs in
|
|
120
|
+
* the background.
|
|
121
|
+
* Each block is keyed by its own marker pair, so a re-run updates only its own
|
|
122
|
+
* block, and a hook that has only one half gets the other appended, not clobbered. */
|
|
128
123
|
export declare function installPostMergeHook(root: string, invocation: string): HookInstall;
|
|
129
124
|
export declare function installPostCheckoutHook(root: string, invocation: string): HookInstall;
|
|
130
125
|
export type HookState = BlockState;
|
|
@@ -793,14 +793,12 @@ export function installPreCommitHook(root, invocation, strict = false) {
|
|
|
793
793
|
};
|
|
794
794
|
return installManagedBlock(root, "pre-commit", PRE_MARK, PRE_END, build, invocation);
|
|
795
795
|
}
|
|
796
|
-
//
|
|
797
|
-
// grounding
|
|
798
|
-
// exact marker text (fnd_c402046ac7).
|
|
796
|
+
// The original grounding marker is kept unchanged for backward compatibility, so an
|
|
797
|
+
// existing grounding block is still found and updated in place.
|
|
799
798
|
const GROUNDING_MERGE_MARK = "# >>> hunch post-merge >>>";
|
|
800
799
|
const GROUNDING_MERGE_END = "# <<< hunch post-merge <<<";
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
// independently created/updated/removed without touching the other.
|
|
800
|
+
// The repair-provenance block gets a SEPARATE marker so the two blocks never collide
|
|
801
|
+
// in one post-merge file and each can be created, updated or removed on its own.
|
|
804
802
|
const REPAIR_MERGE_MARK = "# >>> hunch post-merge (repair-provenance) >>>";
|
|
805
803
|
const REPAIR_MERGE_END = "# <<< hunch post-merge (repair-provenance) <<<";
|
|
806
804
|
function groundingMergeBlock(invocation) {
|
|
@@ -819,37 +817,30 @@ function repairProvenanceMergeBlock(invocation) {
|
|
|
819
817
|
REPAIR_MERGE_MARK,
|
|
820
818
|
'if [ -z "$HUNCH_MERGE_SYNC" ]; then',
|
|
821
819
|
" export HUNCH_MERGE_SYNC=1",
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
// (file-set overlap, not git's own rename detection) isn't strong enough to
|
|
826
|
-
// trust an unattended, backgrounded write into shared team memory.
|
|
820
|
+
// Detect-and-queue only, never --apply: the file-overlap match is not strong
|
|
821
|
+
// enough to trust an unattended write to shared memory. A human confirms the
|
|
822
|
+
// queue (.hunch/pending-commit-repairs.json) with `repair-provenance --apply`.
|
|
827
823
|
` ( ${invocation} repair-provenance --from-hook --quiet >/dev/null 2>&1 || true ) &`,
|
|
828
824
|
"fi",
|
|
829
825
|
REPAIR_MERGE_END,
|
|
830
826
|
].join("\n");
|
|
831
827
|
}
|
|
832
|
-
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
828
|
+
/** Rank used to pick ONE HookInstall action out of two independent sub-installs
|
|
829
|
+
* into the same file. "created" (the file itself is new) outranks "appended" /
|
|
830
|
+
* "updated" (an existing file
|
|
835
831
|
* changed), which outrank "unchanged". Non-writing results are handled before
|
|
836
832
|
* ranking (they must never be masked by a sibling's success). */
|
|
837
833
|
const ACTION_RANK = { created: 3, appended: 2, updated: 2, unchanged: 1, "kept-shared": 1, "managed-elsewhere": 0, unreachable: 0, "opted-out": 0 };
|
|
838
834
|
const writes = (h) => h.action !== "managed-elsewhere" && h.action !== "unreachable" && h.action !== "opted-out";
|
|
839
|
-
/** Install
|
|
840
|
-
* re-
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
* no commit of its own and so can't reuse HUNCH_SYNC's re-trigger guard).
|
|
849
|
-
* Each block is keyed by its own marker pair (installManagedBlock), so
|
|
850
|
-
* re-running updates only its own block, preserves the other untouched, and
|
|
851
|
-
* a repo carrying only one half (an older install, or a hand-edited hook)
|
|
852
|
-
* gets the other appended rather than clobbered. */
|
|
835
|
+
/** Install the post-merge hook. It carries two independently managed blocks:
|
|
836
|
+
* - grounding: re-syncs committed grounding docs when a merge brought in memory.
|
|
837
|
+
* Guarded by HUNCH_SYNC, runs in the foreground, never fails the merge.
|
|
838
|
+
* - repair: detects a decision's commit provenance being orphaned by a
|
|
839
|
+
* squash-merge (including a fast-forward from `git pull`) and queues the match
|
|
840
|
+
* for `hunch repair-provenance --apply`. Guarded by HUNCH_MERGE_SYNC, runs in
|
|
841
|
+
* the background.
|
|
842
|
+
* Each block is keyed by its own marker pair, so a re-run updates only its own
|
|
843
|
+
* block, and a hook that has only one half gets the other appended, not clobbered. */
|
|
853
844
|
export function installPostMergeHook(root, invocation) {
|
|
854
845
|
// Only the grounding half regenerates committed docs; repair-provenance writes
|
|
855
846
|
// no docs (it only queues a match for a human to confirm), so it keeps the
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wires up two independent git merge drivers:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Both routes live in
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* - `merge=hunch`: the structured `.hunch/` JSON driver (store/merge.ts), which
|
|
4
|
+
* resolves concurrent edits by record id;
|
|
5
|
+
* - `merge=hunch-grounding`: the generated grounding docs driver
|
|
6
|
+
* (core/groundingMerge.ts), which auto-resolves only a hard conflict confined
|
|
7
|
+
* to the record-counts sentence.
|
|
8
|
+
* Both routes live in the committed `.gitattributes`. Both drivers are
|
|
9
|
+
* registered only in local, per-clone git config, because each references this
|
|
10
|
+
* machine's node and CLI path; teammates re-run `hunch init`.
|
|
11
11
|
*/
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { readFileSync, existsSync } from "node:fs";
|
|
@@ -18,13 +18,12 @@ import { mergeDriverShell } from "../cli/invocation.js";
|
|
|
18
18
|
// Route the .hunch JSON records through the structured driver — but NOT the
|
|
19
19
|
// manifest (an id-less `{schema_version}` object the driver can't merge by id; a
|
|
20
20
|
// normal text merge with conflict markers is the right behavior for it).
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// (it only ever touches a hard conflict confined to the counts sentence).
|
|
21
|
+
// The five generated grounding docs go to the other driver, which is narrower:
|
|
22
|
+
// it only ever touches a hard conflict confined to the counts sentence.
|
|
24
23
|
export const ATTR_LINES = [
|
|
25
24
|
".hunch/**/*.json merge=hunch",
|
|
26
25
|
".hunch/manifest.json merge=text",
|
|
27
|
-
...GROUNDING_DOC_PATHS.map((
|
|
26
|
+
...GROUNDING_DOC_PATHS.map((p) => `${p} merge=hunch-grounding`),
|
|
28
27
|
];
|
|
29
28
|
export function targetRepositoryEnv() {
|
|
30
29
|
const env = { ...process.env };
|
|
@@ -57,8 +56,8 @@ export function installMergeDriver(root, invShell) {
|
|
|
57
56
|
assertSafeTopLevelConfigFile(root, ".gitattributes");
|
|
58
57
|
writeFileAtomic(attrPath, text);
|
|
59
58
|
}
|
|
60
|
-
// 2.
|
|
61
|
-
// this machine's node
|
|
59
|
+
// 2. The driver definitions go into local git config. They are per-clone (they
|
|
60
|
+
// reference this machine's node and CLI path), so they are never committed; teammates
|
|
62
61
|
// re-run init. Dev mode swaps `npx tsx` for Hunch's own loader (#492).
|
|
63
62
|
const shell = mergeDriverShell(invShell);
|
|
64
63
|
const driver = `${shell} merge-driver "%O" "%A" "%B" "%P"`;
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -359,12 +359,12 @@ declare const DELIVERY_OUTPUT_SCHEMA: z.ZodObject<{
|
|
|
359
359
|
type: z.ZodEnum<{
|
|
360
360
|
depends_on: "depends_on";
|
|
361
361
|
calls: "calls";
|
|
362
|
+
references: "references";
|
|
362
363
|
imports: "imports";
|
|
363
364
|
contains: "contains";
|
|
364
365
|
implements: "implements";
|
|
365
366
|
supersedes: "supersedes";
|
|
366
367
|
related_to: "related_to";
|
|
367
|
-
references: "references";
|
|
368
368
|
provides: "provides";
|
|
369
369
|
belongs_to: "belongs_to";
|
|
370
370
|
implemented_by: "implemented_by";
|
|
@@ -624,10 +624,9 @@ export declare function compactEnvelope(envelope: z.infer<typeof DELIVERY_OUTPUT
|
|
|
624
624
|
* receipts and receipt_id are unchanged. */
|
|
625
625
|
export declare function compactOmissions(envelope: z.infer<typeof DELIVERY_OUTPUT_SCHEMA>): z.infer<typeof DELIVERY_ADVERTISED_OUTPUT_SCHEMA>;
|
|
626
626
|
export declare function decisionSubstanceChanged(before: Decision, after: Decision): boolean;
|
|
627
|
-
/** Resolve a free-form target (symbol id
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* apart on the same question. */
|
|
627
|
+
/** Resolve a free-form target (symbol id, name, or file path) to symbol records using
|
|
628
|
+
* tiered matching — exact id, then exact name, then exact file, then a path-segment-anchored
|
|
629
|
+
* suffix. Shared with HunchStore.why() through matchSymbolsTiered so the two never disagree. */
|
|
631
630
|
export declare function resolveSymbols(store: HunchStore, target: string): Symbol[];
|
|
632
631
|
/** Resolve a target to canonical indexed file path(s) (for file-granular blast
|
|
633
632
|
* radius). Falls back to the literal target so direct-scope checks still run. */
|
package/dist/mcp/server.js
CHANGED
|
@@ -90,24 +90,25 @@ const err = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
|
90
90
|
* Transient states that say "retry" in their own words stay unprefixed. */
|
|
91
91
|
const refused = (text) => err(`Refused: ${text}`);
|
|
92
92
|
const invalid = (text) => err(`Invalid: ${text}`);
|
|
93
|
-
/** Shared by every auto-committing write tool
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
/** Shared by every auto-committing write tool. The MCP roots protocol cannot observe an
|
|
94
|
+
* agent-driven directory change or worktree entry, so a stdio server's cached root never
|
|
95
|
+
* moves by itself; this field is the client-agnostic fallback, resolved fresh on every
|
|
96
|
+
* call by the generic tool wrapper. */
|
|
97
97
|
const cwdHintField = z.string().optional().describe("Your current directory, when it differs from where this session started (e.g. a worktree); " +
|
|
98
98
|
"writes then commit there, not to the original root.");
|
|
99
99
|
/** Shared by hunch_record_decision/correction/finding — identical text repeated 3x
|
|
100
100
|
* otherwise. hunch_context's task_id field differs (delivery vs. save) and keeps its
|
|
101
101
|
* own description. */
|
|
102
102
|
const saveTaskIdField = TaskIdSchema.optional().describe("Task ID, to attribute this save to it; never affects capture authority.");
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
/** Pulls the cwd hint out of a tool call's already-parsed first argument. The wrapper
|
|
104
|
+
* runs for EVERY tool (read and write), so this assumes nothing about any one tool's
|
|
105
|
+
* input shape: anything that is not an object with a non-blank string `cwd` yields
|
|
106
|
+
* undefined. The original (untrimmed) string is returned. */
|
|
107
|
+
function cwdHintOf(input) {
|
|
108
|
+
if (typeof input !== "object" || input === null)
|
|
108
109
|
return undefined;
|
|
109
110
|
const cwd = input.cwd;
|
|
110
|
-
return typeof cwd === "string" && cwd.trim() ? cwd : undefined;
|
|
111
|
+
return typeof cwd === "string" && cwd.trim() !== "" ? cwd : undefined;
|
|
111
112
|
}
|
|
112
113
|
/** Honest auto-commit suffix: reports only what flushCapture ACTUALLY did. A skipped
|
|
113
114
|
* commit (backstop/lock/nothing staged) says nothing — the record is on disk and the
|
|
@@ -150,11 +151,9 @@ const publicHomeNote = (home, hasPrivate, record, hunchDir) => {
|
|
|
150
151
|
return risk;
|
|
151
152
|
return "\nℹ Landed in the COMMITTED PUBLIC store (publishes with the repo). For sensitive/strategy content, re-record with private:true — the overlay store." + risk;
|
|
152
153
|
};
|
|
153
|
-
/** Self-diagnosing destination
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* appends this so the destination is always visible in the response, whether or
|
|
157
|
-
* not a cwd hint was involved in choosing it. */
|
|
154
|
+
/** Self-diagnosing destination suffix for every auto-committing write tool. A capture
|
|
155
|
+
* silently landing in the wrong repo or branch is only discoverable by auditing git log
|
|
156
|
+
* by hand, so the destination is ALWAYS shown, whether or not a cwd hint chose it. */
|
|
158
157
|
const destinationNote = (destRoot) => {
|
|
159
158
|
const branch = currentBranch(destRoot);
|
|
160
159
|
return ` [captured${branch ? ` on branch ${branch}` : ""} in ${destRoot}]`;
|
|
@@ -586,12 +585,10 @@ function nuryelWriteSubject(facet, record) {
|
|
|
586
585
|
const kind = facet.endsWith("s") ? facet.slice(0, -1) : facet;
|
|
587
586
|
return `${kind}${typeof label === "string" ? ` "${label.slice(0, 60)}"` : ""} (nuryel_write)`;
|
|
588
587
|
}
|
|
589
|
-
/** Where a capture keyed to
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
|
|
593
|
-
* regardless), silently misreporting the destination for a private-homed proof. */
|
|
594
|
-
const resolveDestRoot = (home, store, root) => home === "private" && store.privateDir ? store.privateDir : root;
|
|
588
|
+
/** Where a capture keyed to a storage home actually lands. Centralized so every
|
|
589
|
+
* destination-reporting call site agrees: hunch_policy_upgrade_correction once computed
|
|
590
|
+
* its own home but reported the public root regardless, misreporting a private-homed proof. */
|
|
591
|
+
const destinationRoot = (home, store, root) => home === "private" && store.privateDir ? store.privateDir : root;
|
|
595
592
|
// Read-side token budgets: every tool result is injected into a Claude Code
|
|
596
593
|
// session, so an uncapped list pollutes the context window. Cap each list to its
|
|
597
594
|
// highest-signal head (records are pre-sorted by severity/confidence) and tell the
|
|
@@ -993,13 +990,12 @@ function qualityNudge(rec) {
|
|
|
993
990
|
}
|
|
994
991
|
return "";
|
|
995
992
|
}
|
|
996
|
-
/** Resolve a free-form target (symbol id
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
* apart on the same question. */
|
|
993
|
+
/** Resolve a free-form target (symbol id, name, or file path) to symbol records using
|
|
994
|
+
* tiered matching — exact id, then exact name, then exact file, then a path-segment-anchored
|
|
995
|
+
* suffix. Shared with HunchStore.why() through matchSymbolsTiered so the two never disagree. */
|
|
1000
996
|
export function resolveSymbols(store, target) {
|
|
1001
|
-
// An absolute
|
|
1002
|
-
//
|
|
997
|
+
// An absolute path (an agent's verbatim edit-payload path) must match the repo-relative
|
|
998
|
+
// stored file paths (issue #296).
|
|
1003
999
|
target = repoRelativeTarget(target, store.publicRoot);
|
|
1004
1000
|
const syms = store.json.loadAll("symbols");
|
|
1005
1001
|
const components = store.json.loadAll("components");
|
|
@@ -1372,23 +1368,20 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1372
1368
|
};
|
|
1373
1369
|
const registerTool = server.registerTool.bind(server);
|
|
1374
1370
|
server.registerTool = ((name, config, callback) => hiddenTools.has(name) ? undefined : registerTool(name, config, async (...args) => {
|
|
1375
|
-
// Claude Code CLI never advertises
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
|
|
1382
|
-
// root/store out from under it, so that case is refused rather than risked.
|
|
1383
|
-
const cwdHint = extractCwdHint(args[0]);
|
|
1371
|
+
// The Claude Code CLI never advertises roots/list_changed for an agent `cd` or worktree
|
|
1372
|
+
// entry, so the cached root never moves and writes land where the process was spawned.
|
|
1373
|
+
// Write tools therefore accept an optional `cwd` (see cwdHintField) as a client-agnostic
|
|
1374
|
+
// fallback, resolved fresh on every call rather than cached, re-homing this stdio
|
|
1375
|
+
// process the way a roots notification would. That is only safe when no other request
|
|
1376
|
+
// is in flight, so that case is refused instead of risked.
|
|
1377
|
+
const cwdHint = cwdHintOf(args[0]);
|
|
1384
1378
|
// A pinned root is the whole point of `hunch mcp --root`: a served partition must not
|
|
1385
1379
|
// follow the caller's working directory into some other checkout.
|
|
1386
1380
|
if (cwdHint !== undefined && !pinned) {
|
|
1387
1381
|
const target = canonicalRootPath(findRoot(cwdHint));
|
|
1388
1382
|
if (target !== canonicalRootPath(root)) {
|
|
1389
|
-
if (activeRequests) {
|
|
1390
|
-
return err(`Hunch is
|
|
1391
|
-
`(resolves to ${target}) while another call is in flight. Retry this call once the other one completes.`);
|
|
1383
|
+
if (activeRequests !== 0) {
|
|
1384
|
+
return err(`Hunch is serving ${root} and cannot safely switch to your working directory (${target}) while another call is in flight. Retry once the other call completes.`);
|
|
1392
1385
|
}
|
|
1393
1386
|
try {
|
|
1394
1387
|
setRoot(cwdHint);
|
|
@@ -1923,13 +1916,13 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1923
1916
|
// -- hunch_now (the hot view: recent activity + roadmap) --------------------
|
|
1924
1917
|
// PUBLIC store only, per dec_29eff08c69's jurisdiction rule: an assistant may
|
|
1925
1918
|
// paste this anywhere, so it must be publishable by construction. Union view
|
|
1926
|
-
//
|
|
1927
|
-
// queued commit-repair's liveness is checked against the full store
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1919
|
+
// is available only through the local terminal command `hunch now --private`.
|
|
1920
|
+
// Exception: a queued commit-repair's liveness is checked against the full store, so a
|
|
1921
|
+
// private-overlay decision's id and commit shas (never its title) may appear in the
|
|
1922
|
+
// escalation line.
|
|
1930
1923
|
server.registerTool("hunch_now", {
|
|
1931
1924
|
title: "Recent activity + the roadmap (the hot view)",
|
|
1932
|
-
description: "What just happened and what
|
|
1925
|
+
description: "What just happened and what is next, straight from the graph: the last N decisions, the ROADMAP, and any inline human question such as an imported ADR awaiting explicit approve/decline. Call at session start to orient, or before planning. Same data as the wiki's now.md. Public store only — except that a queued commit-repair's liveness is checked against the full store, so a private-overlay decision's answerable repair does not go silently unanswerable; only its id and commit shas ever surface, never its title.",
|
|
1933
1926
|
inputSchema: {
|
|
1934
1927
|
recent_limit: z.number().optional().describe("How many recent decisions to include (default 5)."),
|
|
1935
1928
|
roadmap_limit: z.number().optional().describe("How many roadmap entries to include (default 2); the rest are counted."),
|
|
@@ -1962,18 +1955,15 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
1962
1955
|
L.push("", ws);
|
|
1963
1956
|
const escalations = pendingEscalations(store.advisoryRecs("decisions"));
|
|
1964
1957
|
escalations.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
const actionableNow = actionableEscalations(escalations);
|
|
1975
|
-
if (actionableNow.length) {
|
|
1976
|
-
L.push("", `⚖ ${actionableNow.length} decision(s) need the human's call — ASK inline (never queue): ${actionableNow.map((e) => e.question).join(" · ")}`);
|
|
1958
|
+
// Liveness is checked against the FULL store (the title stays scoped to advisory
|
|
1959
|
+
// records), so an overlay decision's repair does not go silent.
|
|
1960
|
+
const repairQueue = readActivePendingRepairs(root);
|
|
1961
|
+
escalations.push(...commitRepairEscalations(repairQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, repairQueue)));
|
|
1962
|
+
// A duplicate-id commit-repair follower whose own answer is "act on a different
|
|
1963
|
+
// entry first" is not a question to raise (#61).
|
|
1964
|
+
const actionable = actionableEscalations(escalations);
|
|
1965
|
+
if (actionable.length) {
|
|
1966
|
+
L.push("", `⚖ ${actionable.length} decision(s) need the human's call — ASK inline (never queue): ${actionable.map((e) => e.question).join(" · ")}`);
|
|
1977
1967
|
}
|
|
1978
1968
|
return ok(L.join("\n"));
|
|
1979
1969
|
});
|
|
@@ -2039,24 +2029,20 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2039
2029
|
// Premise decay: a live decision whose recorded reason died. Question-framed
|
|
2040
2030
|
// like every entry here — authority never changes until the human answers.
|
|
2041
2031
|
items.push(...premiseEscalations(store.advisoryRecs("decisions"), { now: new Date().toISOString(), exists: (p) => existsSync(join(root, p)) }));
|
|
2042
|
-
//
|
|
2043
|
-
const
|
|
2044
|
-
items.push(...commitRepairEscalations(
|
|
2032
|
+
// Live set is the full store, for the reason given in the sibling hunch_now handler.
|
|
2033
|
+
const repairQueue = readActivePendingRepairs(root);
|
|
2034
|
+
items.push(...commitRepairEscalations(repairQueue, store.advisoryRecs("decisions"), store.recs("decisions"), withheldRewrites(root, repairQueue)));
|
|
2045
2035
|
try {
|
|
2046
2036
|
items.push(...policyEscalations(new ConstitutionService(store, root).list({ publicOnly: true }).map((p) => ({ ...p, last_action: p.audit.at(-1)?.action ?? null }))));
|
|
2047
2037
|
}
|
|
2048
2038
|
catch { /* constitution unavailable — memory escalations still surface */ }
|
|
2049
2039
|
if (!items.length)
|
|
2050
2040
|
return ok("✓ Nothing needs a human decision — memory is auto-trusted and self-consistent.");
|
|
2051
|
-
// Only
|
|
2052
|
-
//
|
|
2053
|
-
// a different entry first" still surfaces below for transparency, but
|
|
2054
|
-
// isn't itself something to raise as a decision (#61).
|
|
2041
|
+
// Only actionable entries are questions to raise; non-actionable followers still
|
|
2042
|
+
// appear below for transparency (#61).
|
|
2055
2043
|
const L = [escalationHeadline(items, "mcp"), ""];
|
|
2056
2044
|
for (const e of items) {
|
|
2057
|
-
//
|
|
2058
|
-
// itself something to raise — an entry whose own answer is "act on a
|
|
2059
|
-
// different entry first" reads as informational, not as a question.
|
|
2045
|
+
// Non-actionable entries are marked distinctly so they read as informational, not as a question.
|
|
2060
2046
|
L.push(`${e.actionable === false ? "·" : "⚖"} ${e.question}`);
|
|
2061
2047
|
L.push(` ${e.detail}`);
|
|
2062
2048
|
L.push(` → ${e.resolution}`, "");
|
|
@@ -2474,7 +2460,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2474
2460
|
const where = decision.private
|
|
2475
2461
|
? ` [PRIVATE overlay — not committed to this repo]${flushed}`
|
|
2476
2462
|
: home === "private" ? ` [SHARED store — one source of truth for the whole team]${flushed}` : flushed;
|
|
2477
|
-
const dest = destinationNote(
|
|
2463
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2478
2464
|
// The chain (nuryel.state/1): a `shipped` receipt in an organization drawer rests on
|
|
2479
2465
|
// this decision by id + the hash ON FILE + this repository's partition. Hand the ref
|
|
2480
2466
|
// over now so the agent never rests on a pre-store hash or re-derives the scope.
|
|
@@ -2585,7 +2571,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2585
2571
|
const tierNote = `
|
|
2586
2572
|
|
|
2587
2573
|
⚠ ${why} — this rule is agent_recorded TESTIMONY${capped ? ` and was capped from "${requested.severity}" to "${rec.severity}"` : ""}. It IS enforced: the pre-edit hook and CI surface it on every matching edit from now on. What it cannot do is DENY an edit — only a rule a human confirmed outside the agent channel may block. The human confirms it by running: ${confirmCommand(rec.id, { private: home === "private", severity: requested.severity })}`;
|
|
2588
|
-
const dest = destinationNote(
|
|
2574
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2589
2575
|
return ok(`${existing ? "Updated" : "Recorded"} ${rec.severity} constraint ${rec.id}: "${rec.statement}" (scope: ${rec.scope.join(", ")}).${where}${dest} It now ${enforce}.${reviewNote}${tierNote}`);
|
|
2590
2576
|
}
|
|
2591
2577
|
catch (e) {
|
|
@@ -2666,8 +2652,9 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2666
2652
|
? `\n\n△ violates_constraint ${rec.violates_constraint} resolves to no known constraint — if the rule isn't recorded yet, hunch_record_correction it and re-record this finding with the real id.`
|
|
2667
2653
|
: "";
|
|
2668
2654
|
const noEvidence = rec.evidence.length ? "" : "\n\n△ No evidence attached — a finding without the query/output that produced it is an opinion. Re-record with evidence when you have it.";
|
|
2669
|
-
const dest = destinationNote(
|
|
2670
|
-
|
|
2655
|
+
const dest = destinationNote(destinationRoot(home, store, root));
|
|
2656
|
+
const grounds = [...rec.affected_files, ...rec.affected_symbols].join(", ") || "(nothing — add affected_files/symbols so it surfaces at edit time)";
|
|
2657
|
+
return ok(`${existing ? "Updated" : "Recorded"} finding ${id}: "${rec.title}" (${rec.triage}/${rec.severity}, observed ${rec.observed_at.slice(0, 10)}).${where}${dest} It now grounds edits to: ${grounds}.${danglingCon}${noEvidence}`);
|
|
2671
2658
|
}
|
|
2672
2659
|
catch (e) {
|
|
2673
2660
|
return err(`Failed to record finding: ${e.message}`);
|
|
@@ -2950,7 +2937,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
2950
2937
|
if (artifactHome === "private") {
|
|
2951
2938
|
flushMemoryHome(store, hunchPaths(root).hunch, "private", `hunch: prove correction ${constraint_id}`, startupTeamRoute ?? undefined);
|
|
2952
2939
|
}
|
|
2953
|
-
const destRoot =
|
|
2940
|
+
const destRoot = destinationRoot(artifactHome, store, root);
|
|
2954
2941
|
const destination = { root: destRoot, branch: currentBranch(destRoot) };
|
|
2955
2942
|
if (include_artifacts)
|
|
2956
2943
|
return ok(JSON.stringify({ ...upgrade, destination }, null, 2));
|
|
@@ -3139,7 +3126,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
3139
3126
|
if (!home)
|
|
3140
3127
|
throw new Error(`policy ${policy_id} has no exact storage home`);
|
|
3141
3128
|
flushMemoryHome(store, hunchPaths(root).hunch, home, `hunch: plan policy ${policy_id}`, startupTeamRoute ?? undefined);
|
|
3142
|
-
const destRoot =
|
|
3129
|
+
const destRoot = destinationRoot(home, store, root);
|
|
3143
3130
|
return ok(JSON.stringify({ ...plan, destination: { root: destRoot, branch: currentBranch(destRoot) } }, null, 2));
|
|
3144
3131
|
}
|
|
3145
3132
|
catch (e) {
|
|
@@ -3437,7 +3424,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
|
|
|
3437
3424
|
dependencyTimeoutMs: dependency_timeout_ms ?? 300_000,
|
|
3438
3425
|
});
|
|
3439
3426
|
flushMemoryHome(store, hunchPaths(root).hunch, "private", "hunch: materialize G2 behavior policies", startupTeamRoute ?? undefined);
|
|
3440
|
-
const destRoot =
|
|
3427
|
+
const destRoot = destinationRoot("private", store, root);
|
|
3441
3428
|
return ok(JSON.stringify({ ...materialized, destination: { root: destRoot, branch: currentBranch(destRoot) } }, null, 2));
|
|
3442
3429
|
}
|
|
3443
3430
|
catch (e) {
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -1341,8 +1341,8 @@ export class HunchStore {
|
|
|
1341
1341
|
* instant — "what did we believe as of commit X?". Omit `asOf` for the full,
|
|
1342
1342
|
* history-inclusive view (backward-compatible default). */
|
|
1343
1343
|
why(target, opts = {}) {
|
|
1344
|
-
// An
|
|
1345
|
-
//
|
|
1344
|
+
// An agent's verbatim edit-payload path is absolute; stored paths are
|
|
1345
|
+
// repo-relative, so rewrite before any matching (issue #299).
|
|
1346
1346
|
target = repoRelativeTarget(target, this.paths.root);
|
|
1347
1347
|
// A record id (every delivered brief line ends in `hunch_why("dec_…")`) resolves
|
|
1348
1348
|
// to that record directly (issue #474). With `asOf`, a record not in force then
|
|
@@ -1364,11 +1364,11 @@ export class HunchStore {
|
|
|
1364
1364
|
// so an unanchored suffix pulled unrelated files' records into why()/the
|
|
1365
1365
|
// pre-edit grounding block (issue #32). Segment-anchored matching only.
|
|
1366
1366
|
//
|
|
1367
|
-
// A target that
|
|
1368
|
-
// vscode-extension/index.ts
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1367
|
+
// A target that is a path the index already knows (README.md,
|
|
1368
|
+
// `vscode-extension/index.ts`) names exactly ONE file. Suffix-matching it too
|
|
1369
|
+
// would pull in every other file sharing its basename (issue #299: root
|
|
1370
|
+
// `index.ts` received `vscode-extension/index.ts`'s rules and vice versa). "Known
|
|
1371
|
+
// to the index" is answered from already-loaded graph data, looked up
|
|
1372
1372
|
// first (a symbol's exact file, or an indexed component's path glob — so a
|
|
1373
1373
|
// symbol-less indexed file still counts as real), and only then from the
|
|
1374
1374
|
// working tree, for a real file the index cannot see at all (issue #334).
|
|
@@ -1696,9 +1696,8 @@ export class HunchStore {
|
|
|
1696
1696
|
* longer enforced. Pass `{ asOf }` to instead return the invariants in force at
|
|
1697
1697
|
* that instant (time-travel: "what must I not have broken as of commit X?"). */
|
|
1698
1698
|
checkConstraints(scope, opts = {}) {
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
// blocking rule plainly applied (issue #296).
|
|
1699
|
+
// Scopes are repo-relative; an absolute target matched nothing even when a
|
|
1700
|
+
// blocking rule applied (issue #296).
|
|
1702
1701
|
scope = repoRelativeTarget(scope, this.paths.root);
|
|
1703
1702
|
const all = this.recs("constraints");
|
|
1704
1703
|
const asOf = opts.asOf;
|
|
@@ -1713,11 +1712,8 @@ export class HunchStore {
|
|
|
1713
1712
|
* glob, and the queried scope may be either too. Advisory only — findings never
|
|
1714
1713
|
* enter any block path. Sorted worst-first, then id for stable output. */
|
|
1715
1714
|
liveFindingsFor(scope) {
|
|
1716
|
-
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1717
|
-
//
|
|
1718
|
-
// below, and an un-guarded suffix fallback could pull in an unrelated file
|
|
1719
|
-
// that merely shares a basename. Normalize once and reuse the same "is this
|
|
1720
|
-
// path known to the index" guard.
|
|
1715
|
+
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1716
|
+
// normalize once and reuse the known-path guard.
|
|
1721
1717
|
const t = repoRelativeTarget(scope, this.paths.root);
|
|
1722
1718
|
const indexed = this.isKnownPath(t);
|
|
1723
1719
|
const live = (f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled";
|
|
@@ -2340,9 +2336,7 @@ export class HunchStore {
|
|
|
2340
2336
|
* a task on `target`, ordered by what matters most — invariants first, then the
|
|
2341
2337
|
* why, then blast radius and bug history — trimmed to a rough token budget. */
|
|
2342
2338
|
assembleContext(target, budget = 1500, opts = {}) {
|
|
2343
|
-
//
|
|
2344
|
-
// repo-relative BEFORE it reaches why()/liveFindingsFor() below, or an
|
|
2345
|
-
// absolute path never matches the repo-relative stored data (issue #299).
|
|
2339
|
+
// Normalize before it reaches why()/liveFindingsFor() below (issue #299).
|
|
2346
2340
|
target = repoRelativeTarget(target, this.paths.root);
|
|
2347
2341
|
const w = this.why(target, opts);
|
|
2348
2342
|
const symIds = w.symbols.map((s) => s.id);
|