@davesheffer/hunch 1.20.3 → 1.21.1
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 +3 -3
- package/dist/cli/dna.js +116 -0
- package/dist/cli/index.js +112 -1
- package/dist/cli/invocation.js +16 -3
- package/dist/core/projectDna.js +478 -0
- package/dist/core/projectDnaDelivery.js +54 -0
- package/dist/core/projectDnaDelta.js +159 -0
- package/dist/integrations/scaffold.js +18 -7
- package/dist/mcp/server.js +161 -8
- package/dist/projectDna.js +10 -0
- package/package.json +9 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ repository, separate from the code repository. Hunch does not host it. Create a
|
|
|
174
174
|
that every teammate can access, install Hunch on team machines and CI, then have one maintainer run:
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
|
-
npm i -g @davesheffer/hunch@1.
|
|
177
|
+
npm i -g @davesheffer/hunch@1.21.1
|
|
178
178
|
hunch shared --repo git@github.com:acme/project-hunch-memory.git
|
|
179
179
|
git add .gitignore .hunch/team.json
|
|
180
180
|
git commit -m "chore: connect shared Hunch memory"
|
|
@@ -189,7 +189,7 @@ printed by Hunch. Omit `--migrate` for a new setup.
|
|
|
189
189
|
After the pointer commit lands, teammates need Hunch installed and Git access to the memory repo:
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
|
-
npm i -g @davesheffer/hunch@1.
|
|
192
|
+
npm i -g @davesheffer/hunch@1.21.1
|
|
193
193
|
git pull
|
|
194
194
|
hunch init
|
|
195
195
|
hunch doctor
|
|
@@ -237,7 +237,7 @@ but stops automatic memory commits and pushes. As a team-coordinated rollback, r
|
|
|
237
237
|
commit to stop discovery after teammates pull the revert. Existing machines retain their ignored
|
|
238
238
|
local overlay until they are deliberately disconnected; do not delete the memory repo as part of a
|
|
239
239
|
rollback. For this rollout, reinstall the previous published package with
|
|
240
|
-
`npm i -g @davesheffer/hunch@1.20.
|
|
240
|
+
`npm i -g @davesheffer/hunch@1.20.3`; the release receipt resolves and records the verified rollback
|
|
241
241
|
target from the npm registry instead of trusting Git tags. Pause enforcement first as shown above,
|
|
242
242
|
and keep every team client on the same release before resuming Matrix policy workflows.
|
|
243
243
|
|
package/dist/cli/dna.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
|
|
5
|
+
import { diffProjectDna } from "../core/projectDnaDelta.js";
|
|
6
|
+
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
7
|
+
function gitRoot() {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12
|
+
timeout: 5_000,
|
|
13
|
+
}).trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error("Project DNA must run inside a Git repository");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function printProfile(root, revision, json) {
|
|
20
|
+
const profile = discoverProjectDna(root, revision);
|
|
21
|
+
if (json) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(`Project DNA ${profile.profile_id}\n`);
|
|
26
|
+
process.stdout.write(`revision: ${profile.repository_revision}\n`);
|
|
27
|
+
process.stdout.write(`history sample: ${profile.history_sample_count}\n`);
|
|
28
|
+
process.stdout.write(`committed convention files: ${profile.source_files.length ? profile.source_files.join(", ") : "none"}\n`);
|
|
29
|
+
if (!profile.traits.length) {
|
|
30
|
+
process.stdout.write("traits: none yet (insufficient evidence)\n");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const trait of profile.traits) {
|
|
34
|
+
process.stdout.write(`- [${trait.category}] ${trait.claim} (${trait.confidence.toFixed(2)}; ${trait.id})\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const program = new Command();
|
|
38
|
+
program
|
|
39
|
+
.name("hunch-dna")
|
|
40
|
+
.description("Derive and inspect Hunch Project DNA from exact committed repository evidence.")
|
|
41
|
+
.option("--revision <ref>", "Git revision to inspect", "HEAD")
|
|
42
|
+
.option("--json", "emit the sealed machine-readable profile")
|
|
43
|
+
.action((options) => {
|
|
44
|
+
printProfile(gitRoot(), options.revision, !!options.json);
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("match")
|
|
48
|
+
.description("Score a commit/PR/issue/message against deterministic applicable DNA traits.")
|
|
49
|
+
.requiredOption("--kind <kind>", "commit | pull_request | issue | message")
|
|
50
|
+
.requiredOption("--title <title>", "artifact title/subject")
|
|
51
|
+
.option("--body <body>", "artifact body")
|
|
52
|
+
.option("--revision <ref>", "Git revision whose DNA applies", "HEAD")
|
|
53
|
+
.option("--json", "emit the sealed match contract")
|
|
54
|
+
.action((options) => {
|
|
55
|
+
if (!(new Set(["commit", "pull_request", "issue", "message"])).has(options.kind)) {
|
|
56
|
+
throw new Error("--kind must be commit, pull_request, issue, or message");
|
|
57
|
+
}
|
|
58
|
+
const artifact = {
|
|
59
|
+
kind: options.kind,
|
|
60
|
+
title: options.title,
|
|
61
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
62
|
+
};
|
|
63
|
+
const globals = program.opts();
|
|
64
|
+
const revision = options.revision ?? globals.revision ?? "HEAD";
|
|
65
|
+
const json = options.json ?? globals.json ?? false;
|
|
66
|
+
const profile = discoverProjectDna(gitRoot(), revision);
|
|
67
|
+
const match = evaluateProjectDnaMatch(profile, artifact);
|
|
68
|
+
if (json)
|
|
69
|
+
process.stdout.write(`${JSON.stringify(match, null, 2)}\n`);
|
|
70
|
+
else {
|
|
71
|
+
process.stdout.write(`Project DNA match: ${match.score === null ? "n/a" : `${match.score}%`} (${match.applicable_checks} applicable checks)\n`);
|
|
72
|
+
for (const check of match.checks.filter((item) => item.applicable)) {
|
|
73
|
+
process.stdout.write(`- ${check.passed ? "PASS" : "FAIL"} ${check.key}: ${check.detail}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command("context")
|
|
79
|
+
.description("Render the bounded advisory DNA supplement intended for Hunch delivery/context assembly.")
|
|
80
|
+
.option("--revision <ref>", "Git revision to inspect", "HEAD")
|
|
81
|
+
.option("--traits <count>", "maximum DNA traits in the orientation slice", "8")
|
|
82
|
+
.option("--json", "emit supplement JSON")
|
|
83
|
+
.action((options) => {
|
|
84
|
+
const globals = program.opts();
|
|
85
|
+
const revision = options.revision ?? globals.revision ?? "HEAD";
|
|
86
|
+
const json = options.json ?? globals.json ?? false;
|
|
87
|
+
const cap = Number(options.traits);
|
|
88
|
+
const supplement = projectDnaDeliverySupplement(discoverProjectDna(gitRoot(), revision), cap);
|
|
89
|
+
if (json)
|
|
90
|
+
process.stdout.write(`${JSON.stringify(supplement, null, 2)}\n`);
|
|
91
|
+
else
|
|
92
|
+
process.stdout.write(`${supplement?.text ?? "No evidence-backed Project DNA traits yet."}\n`);
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command("diff")
|
|
96
|
+
.description("Compare DNA at two exact revisions; reports observation drift without inferring causality.")
|
|
97
|
+
.argument("<from>", "older Git revision")
|
|
98
|
+
.argument("<to>", "newer Git revision")
|
|
99
|
+
.option("--json", "emit the sealed delta contract")
|
|
100
|
+
.action((from, to, options) => {
|
|
101
|
+
const root = gitRoot();
|
|
102
|
+
const delta = diffProjectDna(discoverProjectDna(root, from), discoverProjectDna(root, to));
|
|
103
|
+
const json = options.json ?? program.opts().json ?? false;
|
|
104
|
+
if (json)
|
|
105
|
+
process.stdout.write(`${JSON.stringify(delta, null, 2)}\n`);
|
|
106
|
+
else {
|
|
107
|
+
process.stdout.write(`Project DNA drift ${delta.delta_id}: ${delta.changed ? `${delta.changes.length} change(s)` : "no observed change"}\n`);
|
|
108
|
+
for (const change of delta.changes)
|
|
109
|
+
process.stdout.write(`- ${change.kind}: ${change.key}\n`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
program.parseAsync().catch((error) => {
|
|
113
|
+
process.stderr.write(`hunch-dna: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=dna.js.map
|
package/dist/cli/index.js
CHANGED
|
@@ -57,6 +57,9 @@ import { compileVerifiedEvidenceMap, formatVerifiedEvidenceMap } from "../core/e
|
|
|
57
57
|
import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
|
|
58
58
|
import { buildDeliveryEnvelope, DELIVERY_PROFILES } from "../core/delivery.js";
|
|
59
59
|
import { deriveChangeIdentity } from "../core/changeIdentity.js";
|
|
60
|
+
import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
|
|
61
|
+
import { diffProjectDna } from "../core/projectDnaDelta.js";
|
|
62
|
+
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
60
63
|
import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness } from "../core/config.js";
|
|
61
64
|
import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
|
|
62
65
|
import { isHumanConfirmed } from "../core/strictgate.js";
|
|
@@ -306,7 +309,7 @@ program
|
|
|
306
309
|
// Claude's native hooks run alongside provider-specific hooks below. Every
|
|
307
310
|
// adapter reads firmness at run time, so changing it needs no config rewrite.
|
|
308
311
|
if (opts.agentHooks !== false) {
|
|
309
|
-
const a = installClaudeHooks(root, `${inv.
|
|
312
|
+
const a = installClaudeHooks(root, `${inv.agentHookShell} hook`);
|
|
310
313
|
console.log(` ✓ Claude Code agent hooks ${a.action} (firmness: ${firmness} — change with \`hunch firmness <level>\`)`);
|
|
311
314
|
}
|
|
312
315
|
// Multi-assistant compatibility: MCP + grounding + lifecycle adapters share
|
|
@@ -2903,6 +2906,114 @@ landscapeCmd
|
|
|
2903
2906
|
console.log(` hunch landscape adopt --ref ${discovery.sourceRevision} --expected ${discovery.discoveryHash} --all --reviewed-by <you>${discovery.issues.length ? " --acknowledge-issues" : ""}`);
|
|
2904
2907
|
console.log("Or pass --candidate <hash...> to adopt an explicit subset; relationships require both endpoint resources.");
|
|
2905
2908
|
});
|
|
2909
|
+
// ---- dna (read-only, exact-revision repository convention profile) -------
|
|
2910
|
+
const dnaCmd = program
|
|
2911
|
+
.command("dna")
|
|
2912
|
+
.description("Inspect evidence-backed Project DNA and evaluate repository-native artifacts without writing graph authority.");
|
|
2913
|
+
dnaCmd
|
|
2914
|
+
.command("inspect")
|
|
2915
|
+
.description("Derive a deterministic Project DNA profile from one exact Git revision. This command never writes memory.")
|
|
2916
|
+
.option("--ref <ref>", "Git commit/ref to inspect", "HEAD")
|
|
2917
|
+
.option("--json", "emit the complete machine-readable profile")
|
|
2918
|
+
.action((opts) => {
|
|
2919
|
+
try {
|
|
2920
|
+
const profile = discoverProjectDna(findRoot(), opts.ref);
|
|
2921
|
+
if (opts.json) {
|
|
2922
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
console.log(`Project DNA at ${profile.repository_revision}`);
|
|
2926
|
+
console.log(`Profile: ${profile.profile_id} (${profile.content_hash})`);
|
|
2927
|
+
console.log(`Evidence: ${profile.history_sample_count} commit subject(s), ${profile.source_files.length} convention file(s)`);
|
|
2928
|
+
if (profile.source_files.length)
|
|
2929
|
+
console.log(`Sources: ${profile.source_files.join(", ")}`);
|
|
2930
|
+
console.log(`\nTRAITS (${profile.traits.length})`);
|
|
2931
|
+
for (const trait of profile.traits) {
|
|
2932
|
+
console.log(` ${trait.id} [${trait.category}/${trait.confidence.toFixed(3)}] ${trait.claim}`);
|
|
2933
|
+
console.log(` ${trait.evidence.map((evidence) => `${evidence.ref} @ ${evidence.revision.slice(0, 12)} (${evidence.content_hash})`).join("; ")}`);
|
|
2934
|
+
}
|
|
2935
|
+
console.log("\nAdvisory observation only; no trait was adopted into graph authority.");
|
|
2936
|
+
}
|
|
2937
|
+
catch (error) {
|
|
2938
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
dnaCmd
|
|
2942
|
+
.command("match")
|
|
2943
|
+
.description("Explainably score a commit, PR, issue, or message against deterministic checks in an exact-revision DNA profile.")
|
|
2944
|
+
.requiredOption("--kind <kind>", "artifact kind: commit, pull_request, issue, or message")
|
|
2945
|
+
.requiredOption("--title <title>", "artifact title or commit subject")
|
|
2946
|
+
.option("--body <body>", "artifact body (for example a PR description)")
|
|
2947
|
+
.option("--ref <ref>", "Git commit/ref whose DNA should be used", "HEAD")
|
|
2948
|
+
.option("--json", "emit the complete machine-readable match envelope")
|
|
2949
|
+
.action((opts) => {
|
|
2950
|
+
try {
|
|
2951
|
+
if (!["commit", "pull_request", "issue", "message"].includes(opts.kind)) {
|
|
2952
|
+
return fail("--kind must be commit, pull_request, issue, or message");
|
|
2953
|
+
}
|
|
2954
|
+
const profile = discoverProjectDna(findRoot(), opts.ref);
|
|
2955
|
+
const match = evaluateProjectDnaMatch(profile, {
|
|
2956
|
+
kind: opts.kind,
|
|
2957
|
+
title: opts.title,
|
|
2958
|
+
body: opts.body,
|
|
2959
|
+
});
|
|
2960
|
+
if (opts.json) {
|
|
2961
|
+
console.log(JSON.stringify(match, null, 2));
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
console.log(`Project Match: ${match.score === null ? "not enough deterministic checks" : `${match.score.toFixed(1)}/100`}`);
|
|
2965
|
+
console.log(`Profile: ${match.profile_id} @ ${match.repository_revision}`);
|
|
2966
|
+
for (const check of match.checks.filter((candidate) => candidate.applicable)) {
|
|
2967
|
+
console.log(` ${check.passed ? "PASS" : "FAIL"} ${check.key} — ${check.detail}`);
|
|
2968
|
+
}
|
|
2969
|
+
console.log("Advisory only; the score does not grant or change enforcement authority.");
|
|
2970
|
+
}
|
|
2971
|
+
catch (error) {
|
|
2972
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
dnaCmd
|
|
2976
|
+
.command("context")
|
|
2977
|
+
.description("Render the bounded advisory Project DNA supplement used by normal Hunch context delivery.")
|
|
2978
|
+
.option("--ref <ref>", "Git commit/ref to inspect", "HEAD")
|
|
2979
|
+
.option("--traits <count>", "maximum traits in the orientation slice", "8")
|
|
2980
|
+
.option("--json", "emit the complete machine-readable supplement")
|
|
2981
|
+
.action((opts) => {
|
|
2982
|
+
try {
|
|
2983
|
+
const traitCap = Number(opts.traits);
|
|
2984
|
+
const supplement = projectDnaDeliverySupplement(discoverProjectDna(findRoot(), opts.ref), traitCap);
|
|
2985
|
+
if (opts.json)
|
|
2986
|
+
console.log(JSON.stringify(supplement, null, 2));
|
|
2987
|
+
else
|
|
2988
|
+
console.log(supplement?.text ?? "No evidence-backed Project DNA traits yet.");
|
|
2989
|
+
}
|
|
2990
|
+
catch (error) {
|
|
2991
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
dnaCmd
|
|
2995
|
+
.command("diff")
|
|
2996
|
+
.description("Compare two exact-revision DNA profiles without rewriting either snapshot.")
|
|
2997
|
+
.argument("<from>", "older Git revision")
|
|
2998
|
+
.argument("<to>", "newer Git revision")
|
|
2999
|
+
.option("--json", "emit the complete sealed delta")
|
|
3000
|
+
.action((from, to, opts) => {
|
|
3001
|
+
try {
|
|
3002
|
+
const root = findRoot();
|
|
3003
|
+
const delta = diffProjectDna(discoverProjectDna(root, from), discoverProjectDna(root, to));
|
|
3004
|
+
if (opts.json) {
|
|
3005
|
+
console.log(JSON.stringify(delta, null, 2));
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
console.log(`Project DNA drift ${delta.delta_id}: ${delta.changed ? `${delta.changes.length} change(s)` : "no observed change"}`);
|
|
3009
|
+
for (const change of delta.changes)
|
|
3010
|
+
console.log(` ${change.kind} ${change.key}`);
|
|
3011
|
+
console.log("Observation only; no Project DNA profile or graph authority was rewritten.");
|
|
3012
|
+
}
|
|
3013
|
+
catch (error) {
|
|
3014
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
2906
3017
|
landscapeCmd
|
|
2907
3018
|
.command("adopt")
|
|
2908
3019
|
.description("Human-confirm a reviewed candidate set and write only those exact records through Hunch's normal graph boundary.")
|
package/dist/cli/invocation.js
CHANGED
|
@@ -16,6 +16,13 @@ export function dim(s) {
|
|
|
16
16
|
export function publishedMcpInvocation() {
|
|
17
17
|
return { command: "npx", args: ["-y", `--package=${HUNCH_NPX_PACKAGE_SPEC}`, "hunch"] };
|
|
18
18
|
}
|
|
19
|
+
/** Render a structured invocation for the host shell. Safe tokens stay bare so
|
|
20
|
+
* PowerShell can execute the first token; paths and other unsafe tokens use
|
|
21
|
+
* JSON string quoting, which is accepted by PowerShell, cmd, and POSIX sh. */
|
|
22
|
+
export function shellInvocation(inv) {
|
|
23
|
+
const token = (part) => /^[A-Za-z0-9_@:=+.,/-]+$/.test(part) ? part : JSON.stringify(part);
|
|
24
|
+
return [inv.command, ...inv.args].map(token).join(" ");
|
|
25
|
+
}
|
|
19
26
|
/** The doctor command's synthesis-status line(s) for a resolved provider.
|
|
20
27
|
* Exported for testing — the previous version (a bare provider-name switch,
|
|
21
28
|
* before the resolveSynthesisProvider preference system existed) had zero
|
|
@@ -77,23 +84,29 @@ export function resolveInvocation() {
|
|
|
77
84
|
// absolute-node invocation below.
|
|
78
85
|
const installed = !isDev && entry.replace(/\\/g, "/").includes("/node_modules/");
|
|
79
86
|
if (installed) {
|
|
87
|
+
const mcp = publishedMcpInvocation();
|
|
80
88
|
return {
|
|
81
89
|
shell: `${q(process.execPath)} ${q(entry)}`,
|
|
82
|
-
|
|
90
|
+
agentHookShell: shellInvocation(mcp),
|
|
91
|
+
mcp,
|
|
83
92
|
};
|
|
84
93
|
}
|
|
85
94
|
if (isDev) {
|
|
95
|
+
const mcp = { command: "npx", args: ["tsx", entry] };
|
|
86
96
|
return {
|
|
87
97
|
shell: `npx tsx ${q(entry)}`,
|
|
88
|
-
|
|
98
|
+
agentHookShell: shellInvocation(mcp),
|
|
99
|
+
mcp,
|
|
89
100
|
};
|
|
90
101
|
}
|
|
91
102
|
// Source-checkout dist run (e.g. `node dist/cli/index.js`, npm link): inherently
|
|
92
103
|
// per-machine. Use the absolute node binary (process.execPath) rather than a bare
|
|
93
104
|
// `node`, so the hook works even when nvm's `node` isn't on the hook's PATH.
|
|
105
|
+
const mcp = { command: process.execPath, args: [entry] };
|
|
94
106
|
return {
|
|
95
107
|
shell: `${q(process.execPath)} ${q(entry)}`,
|
|
96
|
-
|
|
108
|
+
agentHookShell: shellInvocation(mcp),
|
|
109
|
+
mcp,
|
|
97
110
|
};
|
|
98
111
|
}
|
|
99
112
|
//# sourceMappingURL=invocation.js.map
|