@davesheffer/hunch 1.10.7 → 1.11.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 +11 -0
- package/dist/core/correction.js +23 -2
- package/dist/core/docanchors.js +35 -2
- package/dist/core/premises.js +29 -3
- package/dist/core/types.js +18 -1
- package/dist/mcp/server.js +28 -9
- package/dist/store/hunchStore.js +11 -1
- package/package.json +1 -1
- package/server.json +4 -4
package/dist/cli/index.js
CHANGED
|
@@ -4939,6 +4939,17 @@ program
|
|
|
4939
4939
|
console.log(`· ${f.id} — ${f.detail}`);
|
|
4940
4940
|
console.log(`\nHeal: run \`hunch wiki --heal\` — regenerates only the stale pages (the wiki is a derived view; never edit it by hand).\n`);
|
|
4941
4941
|
}
|
|
4942
|
+
// Every drift kind heals here — see bug_drift_heal_asymmetry above. premise-stale
|
|
4943
|
+
// shipped in the drift report without a section here, so a repo whose ONLY drift
|
|
4944
|
+
// was a dead premise got "N findings" from `hunch drift` and a bare closing line
|
|
4945
|
+
// from `hunch heal` — exactly the broken loop that bug is about.
|
|
4946
|
+
const premiseStale = kind("premise-stale");
|
|
4947
|
+
if (premiseStale.length) {
|
|
4948
|
+
console.log(`${premiseStale.length} decision(s) rest on a premise that no longer holds (world≠graph):\n`);
|
|
4949
|
+
for (const f of premiseStale)
|
|
4950
|
+
console.log(`· ${f.id} — ${f.detail}`);
|
|
4951
|
+
console.log(`\nHeal: this is a HUMAN call — the decision's authority is unchanged until you make it. Re-attest (update the premise's review_by/attested), supersede via /capture, or retire the decision. Keeping it for consistency is a valid answer.\n`);
|
|
4952
|
+
}
|
|
4942
4953
|
console.log(`Hunch never rewrites prose for you; this is a read-only reconciliation report.`);
|
|
4943
4954
|
}
|
|
4944
4955
|
finally {
|
package/dist/core/correction.js
CHANGED
|
@@ -88,6 +88,19 @@ export function buildCorrectionConstraint(input, now) {
|
|
|
88
88
|
let severity = input.severity ?? "warning";
|
|
89
89
|
if (severity === "blocking" && repoWide && !input.applies_to_all)
|
|
90
90
|
severity = "warning";
|
|
91
|
+
// AUTHORSHIP TIER. This function hardcoded provenance human_confirmed @1, and
|
|
92
|
+
// isStrictBlocker treats human_confirmed + blocking as a DENY — so an un-interviewed
|
|
93
|
+
// agent call could mint a repo-wide deny carrying a signature nobody gave. That made
|
|
94
|
+
// the highest-authority write path the least gated one, and strictly worse than
|
|
95
|
+
// hunch_record_decision, which only ever produced advisory memory and is now tiered.
|
|
96
|
+
//
|
|
97
|
+
// The token sets the TIER, never whether the write lands. An un-vouched correction is
|
|
98
|
+
// still recorded immediately and still held against every assistant at edit time and
|
|
99
|
+
// in CI — Never Twice keeps its promise. What waits for a countersign is only the
|
|
100
|
+
// authority to DENY.
|
|
101
|
+
const vouched = input.vouched !== false;
|
|
102
|
+
if (!vouched && severity === "blocking")
|
|
103
|
+
severity = "warning";
|
|
91
104
|
return {
|
|
92
105
|
id: constraintId(rule),
|
|
93
106
|
type: input.type ?? "correctness",
|
|
@@ -101,13 +114,21 @@ export function buildCorrectionConstraint(input, now) {
|
|
|
101
114
|
// rule that goes stale. Validated against the repo's real deps when supplied → never mints a
|
|
102
115
|
// never-firing rule for a non-dependency. null when nothing derivable → falls back to scope.
|
|
103
116
|
forbids: deriveForbids(rule, input.knownDeps),
|
|
104
|
-
rationale: input.rationale ??
|
|
117
|
+
rationale: input.rationale ?? (vouched
|
|
118
|
+
? "Captured from a human correction of the agent (Never Twice)."
|
|
119
|
+
: "Recorded by the agent as a correction, WITHOUT a capture interview — advisory testimony until a human countersigns it via /capture (Never Twice)."),
|
|
105
120
|
source_decision: input.source_decision ?? null,
|
|
106
121
|
violations: [],
|
|
107
122
|
status: "active",
|
|
108
123
|
valid_from: now,
|
|
109
124
|
valid_to: null,
|
|
110
|
-
|
|
125
|
+
// The signature is EARNED, not assumed. isStrictBlocker treats human_confirmed as
|
|
126
|
+
// authority to deny, so stamping it on an un-interviewed write forges the one thing
|
|
127
|
+
// the strict gate trusts. agent_recorded is the same tier hunch_record_decision uses
|
|
128
|
+
// for an un-token'd write — advisory, real, and honest about who wrote it.
|
|
129
|
+
provenance: vouched
|
|
130
|
+
? { source: "human_confirmed", confidence: 1, evidence: [], last_verified: now }
|
|
131
|
+
: { source: "agent_recorded", confidence: 0.75, evidence: [], last_verified: now },
|
|
111
132
|
};
|
|
112
133
|
}
|
|
113
134
|
//# sourceMappingURL=correction.js.map
|
package/dist/core/docanchors.js
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
import { currentForTopic, rejectedForTopic } from "./topics.js";
|
|
2
2
|
const MARKER = /<!--\s*hunch:topic\s+([A-Za-z0-9._/-]+)(?:\s+(dec_[A-Za-z0-9]+))?\s*-->/g;
|
|
3
|
-
/**
|
|
3
|
+
/** Character ranges covered by fenced code blocks (``` or ~~~), so a
|
|
4
|
+
* documentation EXAMPLE of a marker never registers as a live anchor.
|
|
5
|
+
* CommonMark-lite: a fence of N chars (≤3 leading spaces) closes only on a
|
|
6
|
+
* line of ≥N of the same char and nothing else; an unclosed fence runs to
|
|
7
|
+
* EOF; a backtick fence's info string may not itself contain a backtick. */
|
|
8
|
+
function fencedRanges(text) {
|
|
9
|
+
const ranges = [];
|
|
10
|
+
let open = null;
|
|
11
|
+
let offset = 0;
|
|
12
|
+
for (const line of text.split("\n")) {
|
|
13
|
+
const m = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
14
|
+
if (m) {
|
|
15
|
+
const ch = m[1][0];
|
|
16
|
+
if (!open) {
|
|
17
|
+
if (!(ch === "`" && m[2].includes("`")))
|
|
18
|
+
open = { ch, len: m[1].length, start: offset };
|
|
19
|
+
}
|
|
20
|
+
else if (ch === open.ch && m[1].length >= open.len && m[2].trim() === "") {
|
|
21
|
+
ranges.push([open.start, offset + line.length]);
|
|
22
|
+
open = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
offset += line.length + 1;
|
|
26
|
+
}
|
|
27
|
+
if (open)
|
|
28
|
+
ranges.push([open.start, text.length]);
|
|
29
|
+
return ranges;
|
|
30
|
+
}
|
|
31
|
+
/** Parse every hunch:topic marker out of a markdown document. Markers inside
|
|
32
|
+
* fenced code blocks are examples, not declarations, and are skipped. */
|
|
4
33
|
export function parseDocAnchors(text) {
|
|
5
34
|
const out = [];
|
|
35
|
+
const fences = fencedRanges(text);
|
|
6
36
|
MARKER.lastIndex = 0;
|
|
7
37
|
let m;
|
|
8
38
|
while ((m = MARKER.exec(text))) {
|
|
9
|
-
|
|
39
|
+
const at = m.index;
|
|
40
|
+
if (fences.some(([s, e]) => at >= s && at <= e))
|
|
41
|
+
continue;
|
|
42
|
+
out.push({ topic: m[1], pin: m[2] ?? null, line: text.slice(0, at).split("\n").length });
|
|
10
43
|
}
|
|
11
44
|
return out;
|
|
12
45
|
}
|
package/dist/core/premises.js
CHANGED
|
@@ -5,7 +5,7 @@ const clip = (s, n = 90) => (s.length > n ? s.slice(0, n - 1).trimEnd() + "…"
|
|
|
5
5
|
function validRel(p) {
|
|
6
6
|
return !!p && !p.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(p) && !p.split(/[\\/]/).includes("..");
|
|
7
7
|
}
|
|
8
|
-
function checkPath(claim, rel, wantExists, env) {
|
|
8
|
+
function checkPath(claim, rel, wantExists, env, under) {
|
|
9
9
|
if (!validRel(rel)) {
|
|
10
10
|
return { claim, holds: false, reason: `unevaluable: path must be repo-relative ("${rel}") — fix the premise record` };
|
|
11
11
|
}
|
|
@@ -15,13 +15,39 @@ function checkPath(claim, rel, wantExists, env) {
|
|
|
15
15
|
? { claim, holds: true, reason: `"${rel}" still exists` }
|
|
16
16
|
: { claim, holds: false, reason: `"${rel}" no longer exists` };
|
|
17
17
|
}
|
|
18
|
+
// NEGATIVE probe. A missing path proves nothing on its own: a deleted subtree, a
|
|
19
|
+
// renamed directory and a typo all read as "absent". The `under` anchor is what makes
|
|
20
|
+
// the answer mean something — when the ancestor is gone, the question no longer has a
|
|
21
|
+
// subject, so the premise is UNEVALUABLE rather than quietly satisfied. That is the
|
|
22
|
+
// decay this closes: it is what rotted five decisions pointing into vscode-extension/
|
|
23
|
+
// after that tree was cut.
|
|
24
|
+
if (under !== undefined) {
|
|
25
|
+
if (!validRel(under)) {
|
|
26
|
+
return { claim, holds: false, reason: `unevaluable: under must be repo-relative ("${under}") — fix the premise record` };
|
|
27
|
+
}
|
|
28
|
+
if (!env.exists(under)) {
|
|
29
|
+
return { claim, holds: false, reason: `unevaluable: the anchor "${under}" no longer exists, so "${rel}" being absent proves nothing — re-anchor or supersede` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
return exists
|
|
19
33
|
? { claim, holds: false, reason: `"${rel}" now exists` }
|
|
20
|
-
|
|
34
|
+
// The residual risk is stated, not hidden: a MISTYPED path is also absent, and it
|
|
35
|
+
// stays absent forever — the harm is a premise that silently never fires, which no
|
|
36
|
+
// existence probe can detect. Saying so is what keeps "holds" honest.
|
|
37
|
+
: { claim, holds: true, reason: `"${rel}" still absent${under ? ` under "${under}"` : ""} (a mistyped path also reads as absent — confirm it on re-attest)` };
|
|
21
38
|
}
|
|
22
39
|
function checkOne(p, env) {
|
|
40
|
+
// The clock is an INJECTED input, so it is an unevaluable case like any other.
|
|
41
|
+
// `Date.parse("nope")` is NaN, and `NaN > due` is false — which fell through to
|
|
42
|
+
// "attested until …", i.e. HOLDS. The module's hard rule ("cannot-evaluate is
|
|
43
|
+
// never holds") was enforced for a bad review_by but not for a bad now, and the
|
|
44
|
+
// unenforced half is the one a future caller can get wrong. Checked once, here,
|
|
45
|
+
// so it covers every check kind rather than only the dated one.
|
|
46
|
+
if (!Number.isFinite(Date.parse(env.now))) {
|
|
47
|
+
return { claim: p.claim, holds: false, reason: `unevaluable: caller supplied a non-ISO clock ("${env.now}")` };
|
|
48
|
+
}
|
|
23
49
|
if (p.path_absent !== undefined)
|
|
24
|
-
return checkPath(p.claim, p.path_absent, false, env);
|
|
50
|
+
return checkPath(p.claim, p.path_absent, false, env, p.under);
|
|
25
51
|
if (p.path_exists !== undefined)
|
|
26
52
|
return checkPath(p.claim, p.path_exists, true, env);
|
|
27
53
|
if (p.review_by !== undefined) {
|
package/dist/core/types.js
CHANGED
|
@@ -115,12 +115,29 @@ export const ConformancePredicateSchema = z.object({
|
|
|
115
115
|
// (the human renews, supersedes, or retires — same ethos as topic anchors).
|
|
116
116
|
export const PremiseSchema = z.object({
|
|
117
117
|
claim: z.string().min(1).describe("the human-readable reason this decision rests on"),
|
|
118
|
-
path_absent: z.string().optional().describe("premise holds while this repo-relative path does NOT exist"),
|
|
118
|
+
path_absent: z.string().optional().describe("premise holds while this repo-relative path does NOT exist. Requires `under`. PREFER path_exists where you can: a negative probe cannot tell 'verified absent' from 'wrong path', so it fails OPEN, while path_exists fails closed."),
|
|
119
|
+
under: z.string().optional().describe("required with path_absent: an EXISTING repo-relative ancestor of it. When this anchor disappears (a directory deleted or moved), the premise reads unevaluable instead of silently 'still absent'."),
|
|
119
120
|
path_exists: z.string().optional().describe("premise holds while this repo-relative path exists"),
|
|
120
121
|
review_by: z.string().optional().describe("dated attestation: premise holds until this ISO date, then needs re-attesting"),
|
|
121
122
|
attested: z.string().optional().describe("ISO date a human last attested the claim (informational)"),
|
|
122
123
|
}).refine((p) => [p.path_absent, p.path_exists, p.review_by].filter((x) => x !== undefined).length <= 1, {
|
|
123
124
|
message: "a premise carries at most one check (path_absent | path_exists | review_by)",
|
|
125
|
+
}).refine((p) => p.path_absent === undefined || (typeof p.under === "string" && p.under.trim().length > 0), {
|
|
126
|
+
message: "path_absent requires `under`: an existing ancestor path. Without an anchor, a deleted or renamed subtree reads as 'still absent' forever. Prefer path_exists where you can — it fails closed.",
|
|
127
|
+
path: ["under"],
|
|
128
|
+
}).refine((p) => {
|
|
129
|
+
if (p.path_absent === undefined || typeof p.under !== "string")
|
|
130
|
+
return true;
|
|
131
|
+
const norm = (s) => s.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
132
|
+
const target = norm(p.path_absent);
|
|
133
|
+
const anchor = norm(p.under);
|
|
134
|
+
// Must be a real ANCESTOR, not an arbitrary existing path: `under` is what makes
|
|
135
|
+
// "absent" meaningful ("nothing named gateway UNDER src"). An unrelated anchor
|
|
136
|
+
// would prove the premise still evaluable while telling you nothing about it.
|
|
137
|
+
return anchor !== "" && target !== anchor && target.startsWith(`${anchor}/`);
|
|
138
|
+
}, {
|
|
139
|
+
message: "`under` must be a proper ancestor of `path_absent` (e.g. path_absent 'src/gateway' with under 'src')",
|
|
140
|
+
path: ["under"],
|
|
124
141
|
});
|
|
125
142
|
export const DecisionSchema = z.object({
|
|
126
143
|
id: z.string().describe("dec_*"),
|
package/dist/mcp/server.js
CHANGED
|
@@ -731,14 +731,20 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
731
731
|
related_files: z.array(z.string()).optional(),
|
|
732
732
|
related_components: z.array(z.string()).optional(),
|
|
733
733
|
topic: z.string().optional().describe("decision-grounding anchor — one topic per decision; enables doc≠graph drift detection for it. Omit to leave un-anchored."),
|
|
734
|
+
// FLAT, matching PremiseSchema exactly. A nested { check: {...} } shape is
|
|
735
|
+
// silently STRIPPED by Zod, leaving a claim-only premise — and a claim-only
|
|
736
|
+
// premise is "documented only (no check attached)", which ALWAYS HOLDS. An
|
|
737
|
+
// agent following a wrong schema would record a premise that can never fire:
|
|
738
|
+
// the exact fail-open this feature exists to prevent. Keep in lockstep with
|
|
739
|
+
// PremiseSchema in src/core/types.ts.
|
|
734
740
|
premises: z.array(z.object({
|
|
735
|
-
claim: z.string().describe("the
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
})).optional().describe("the checkable reasons this decision rests on. A dead premise NEVER changes authority
|
|
741
|
+
claim: z.string().min(1).describe("the human-readable reason this decision rests on"),
|
|
742
|
+
path_absent: z.string().optional().describe("premise holds while this repo-relative path does NOT exist. REQUIRES `under`. Prefer path_exists where you can — a negative probe fails OPEN, a positive one fails closed."),
|
|
743
|
+
under: z.string().optional().describe("required with path_absent: an EXISTING repo-relative ANCESTOR of it (path_absent 'src/gateway' -> under 'src'). When the anchor disappears the premise reads unevaluable instead of silently 'still absent'."),
|
|
744
|
+
path_exists: z.string().optional().describe("premise holds while this repo-relative path exists"),
|
|
745
|
+
review_by: z.string().optional().describe("dated attestation: premise holds until this ISO date, then needs re-attesting"),
|
|
746
|
+
attested: z.string().optional().describe("ISO date a human last attested the claim (informational)"),
|
|
747
|
+
})).optional().describe("the checkable reasons this decision rests on — at most ONE check per premise (path_absent | path_exists | review_by). A dead premise NEVER changes authority; it raises an escalation for the human. Omit on re-record to keep the incumbent's premises."),
|
|
742
748
|
status: z.enum(["proposed", "accepted", "rejected", "superseded"]).optional(),
|
|
743
749
|
commit: z.string().optional(),
|
|
744
750
|
supersedes: z.string().optional().describe("id of a decision this one replaces — closes its valid-time window (invalidate, don't delete)"),
|
|
@@ -936,6 +942,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
936
942
|
rationale: z.string().optional().describe("Why it must hold."),
|
|
937
943
|
source_decision: z.string().optional().describe("id of a decision this correction derives from."),
|
|
938
944
|
private: z.boolean().optional().describe("write into the PRIVATE overlay store (HUNCH_PRIVATE_DIR) instead of the committed repo — a sensitive rule enforced locally (pre-edit hook + local check) but never exposed in a public PR comment. Errors if no private store is configured."),
|
|
945
|
+
capture_token: z.string().optional().describe("token from hunch_capture_decision. The rule is recorded and enforced either way — the token only decides whether it may DENY: without one it lands as advisory testimony capped at severity 'warning'."),
|
|
939
946
|
},
|
|
940
947
|
}, async (input) => {
|
|
941
948
|
try {
|
|
@@ -945,7 +952,12 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
945
952
|
// paths (edit-tool payloads and MCP roots are absolute) and every consumer matches
|
|
946
953
|
// repo-relative — without this the rule would be blocking-but-inert and would leak
|
|
947
954
|
// the local filesystem path into the committed graph.
|
|
948
|
-
|
|
955
|
+
// Same authorship tier as hunch_record_decision: a consumed token mints the
|
|
956
|
+
// signature, an un-token'd write is testimony. Here the stakes are HIGHER — a
|
|
957
|
+
// blocking constraint DENIES edits, so an un-vouched write is capped at
|
|
958
|
+
// "warning" rather than being refused. Never Twice still lands immediately.
|
|
959
|
+
const vouched = consumeCaptureToken(input.capture_token);
|
|
960
|
+
const rec = buildCorrectionConstraint({ ...input, knownDeps: knownRepoDeps(root), root, vouched }, new Date().toISOString());
|
|
949
961
|
// Private corrections go to the overlay (enforced locally via the merged read,
|
|
950
962
|
// never rendered into the public CI comment, which is public-only by construction).
|
|
951
963
|
const home = store.captureHome(!!input.private);
|
|
@@ -978,7 +990,14 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
978
990
|
// The Constraint itself is the durable retry queue. Normal `hunch index`
|
|
979
991
|
// and post-commit sync rescan it; no in-process timer can be lost on exit.
|
|
980
992
|
const reviewNote = "\n\nREVIEW PENDING: After the fix is committed, run hunch index; an installed post-commit hook retries this automatically on the fixing commit. Only the supported static ESM import-declaration package projection is eligible, and it remains activation-blocked; the immediate guard is already durable.";
|
|
981
|
-
|
|
993
|
+
// Say plainly which tier this landed in. A silent downgrade would be its own
|
|
994
|
+
// dishonesty: the caller asked for "blocking" and must be told it is not.
|
|
995
|
+
const tierNote = vouched
|
|
996
|
+
? ""
|
|
997
|
+
: `
|
|
998
|
+
|
|
999
|
+
⚠ Recorded WITHOUT a capture interview — this rule is agent_recorded TESTIMONY${input.severity === "blocking" ? ' and was capped from "blocking" to "warning"' : ""}. 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 countersigned may block. Countersign it by re-recording through hunch_capture_decision → hunch_record_correction(capture_token).`;
|
|
1000
|
+
return ok(`${existing ? "Updated" : "Recorded"} ${rec.severity} constraint ${rec.id}: "${rec.statement}" (scope: ${rec.scope.join(", ")}).${where} It now ${enforce}.${reviewNote}${tierNote}`);
|
|
982
1001
|
}
|
|
983
1002
|
catch (e) {
|
|
984
1003
|
return err(`Failed to record correction: ${e.message}`);
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -571,7 +571,17 @@ export class HunchStore {
|
|
|
571
571
|
if (m) {
|
|
572
572
|
if (m.dead)
|
|
573
573
|
w *= 0.6;
|
|
574
|
-
|
|
574
|
+
// agent_recorded sits BETWEEN human_confirmed and llm_draft. It is testimony —
|
|
575
|
+
// a human directed the capture but did not countersign it through /capture — so
|
|
576
|
+
// it must not carry human authority (strict/veto gates key on human_confirmed).
|
|
577
|
+
// But it is a deliberate, human-prompted write, and the unlabelled tier (0.75)
|
|
578
|
+
// is for extracted/inferred machine output. Without this it fell to 0.75 and
|
|
579
|
+
// ranked BELOW an llm_draft the model produced unprompted, which inverts what
|
|
580
|
+
// the authorship stamp is trying to express.
|
|
581
|
+
w *= m.provenance.includes("human_confirmed") ? 1
|
|
582
|
+
: m.provenance.includes("agent_recorded") ? 0.9
|
|
583
|
+
: m.provenance.includes("llm_draft") ? 0.85
|
|
584
|
+
: 0.75;
|
|
575
585
|
if (m.at) {
|
|
576
586
|
const ageDays = Math.max(0, now - Date.parse(m.at)) / 86400000;
|
|
577
587
|
if (Number.isFinite(ageDays))
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.davesheffer/hunch",
|
|
4
|
-
"description": "Engineering memory for AI-assisted codebases: decisions,
|
|
4
|
+
"description": "Engineering memory for AI-assisted codebases: decisions, bug lineage and invariants, over MCP.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/davesheffer/hunch",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://hunch-pi.vercel.app",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.11.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.11.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|