@davesheffer/hunch 1.45.1 → 1.46.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.
Files changed (64) hide show
  1. package/dist/cli/index.js +165 -33
  2. package/dist/cli/invocation.d.ts +14 -0
  3. package/dist/cli/invocation.js +23 -0
  4. package/dist/cli/serve.d.ts +2 -0
  5. package/dist/cli/serve.js +2 -1
  6. package/dist/cli/verify.d.ts +28 -0
  7. package/dist/cli/verify.js +265 -0
  8. package/dist/constitution/experiment.d.ts +1 -1
  9. package/dist/core/agenthook.d.ts +1 -0
  10. package/dist/core/agenthook.js +82 -26
  11. package/dist/core/boundedSpawn.d.ts +57 -0
  12. package/dist/core/boundedSpawn.js +321 -0
  13. package/dist/core/config.d.ts +6 -0
  14. package/dist/core/config.js +1 -0
  15. package/dist/core/delivery.d.ts +10 -0
  16. package/dist/core/delivery.js +117 -9
  17. package/dist/core/migrate.d.ts +10 -1
  18. package/dist/core/migrate.js +17 -6
  19. package/dist/core/pipeline.d.ts +9 -2
  20. package/dist/core/pipeline.js +13 -3
  21. package/dist/core/procstart.d.ts +17 -3
  22. package/dist/core/procstart.js +26 -5
  23. package/dist/core/projectDna.js +11 -1
  24. package/dist/core/relativeImports.js +6 -2
  25. package/dist/core/taskSelection.d.ts +1 -0
  26. package/dist/core/taskSelection.js +132 -11
  27. package/dist/core/testSelection.d.ts +122 -0
  28. package/dist/core/testSelection.js +588 -0
  29. package/dist/core/whyRecord.d.ts +47 -0
  30. package/dist/core/whyRecord.js +105 -0
  31. package/dist/extractors/git.d.ts +32 -0
  32. package/dist/extractors/git.js +206 -94
  33. package/dist/extractors/indexer.js +42 -6
  34. package/dist/extractors/languages.d.ts +9 -0
  35. package/dist/extractors/languages.js +28 -0
  36. package/dist/extractors/parse.d.ts +20 -0
  37. package/dist/extractors/parse.js +81 -5
  38. package/dist/extractors/workspaces.js +3 -1
  39. package/dist/integrations/health.d.ts +6 -0
  40. package/dist/integrations/health.js +33 -12
  41. package/dist/integrations/hooks.d.ts +4 -2
  42. package/dist/integrations/hooks.js +12 -2
  43. package/dist/integrations/mergeDriver.js +5 -3
  44. package/dist/integrations/optout.d.ts +3 -0
  45. package/dist/integrations/optout.js +17 -0
  46. package/dist/integrations/providers.d.ts +6 -2
  47. package/dist/integrations/providers.js +19 -2
  48. package/dist/integrations/team.js +6 -4
  49. package/dist/integrations/uninstall.d.ts +17 -4
  50. package/dist/integrations/uninstall.js +35 -13
  51. package/dist/mcp/server.js +179 -30
  52. package/dist/serve/app.js +9 -1
  53. package/dist/serve/config.js +5 -0
  54. package/dist/serve/writelock.d.ts +6 -5
  55. package/dist/serve/writelock.js +26 -21
  56. package/dist/store/hunchStore.d.ts +12 -1
  57. package/dist/store/hunchStore.js +86 -4
  58. package/dist/store/jsonStore.d.ts +24 -4
  59. package/dist/store/jsonStore.js +75 -26
  60. package/dist/store/stateBinding.js +17 -4
  61. package/dist/synthesis/provider.js +26 -78
  62. package/dist/synthesis/synthesize.js +1 -1
  63. package/package.json +2 -2
  64. package/server.json +2 -2
@@ -0,0 +1,105 @@
1
+ /** Targets why() tries as a record id first. Generated ids are `<prefix>_<hex>`
2
+ * (shortHash), but hand-written/imported ids (`dec_session_transport`) are valid too. */
3
+ export const WHY_RECORD_ID = /^(dec|con|bug|cmp|fnd)_[A-Za-z0-9_-]+$/;
4
+ /** The generated id shape: only a miss on THIS shape is reported as "No record <id>";
5
+ * any other miss (e.g. a symbol named `bug_handler`) falls through to path/symbol lookup. */
6
+ export const WHY_GENERATED_ID = /^(dec|con|bug|cmp|fnd)_[0-9a-f]{6,}$/;
7
+ /** Per-list cap in hunch_why output, and the "what was withheld" note after a capped
8
+ * list — shared with the path/symbol view in the MCP server. */
9
+ export const WHY_CAP = 6;
10
+ export const more = (total, cap, hint = "") => total > cap ? `\n …(+${total - cap} more${hint ? ` — ${hint}` : ""})` : "";
11
+ const PROSE_CHARS = 600;
12
+ const ITEM_CHARS = 240;
13
+ const clip = (text, max = PROSE_CHARS) => {
14
+ const flat = text.replace(/\s+/g, " ").trim();
15
+ // By code point, so a cut never splits a surrogate pair (emoji, astral CJK).
16
+ const chars = Array.from(flat);
17
+ return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : flat;
18
+ };
19
+ const list = (label, items) => items.length ? [` ${label}:`, ...items.slice(0, WHY_CAP).map((i) => ` - ${clip(i, ITEM_CHARS)}`), ...(items.length > WHY_CAP ? [` ${more(items.length, WHY_CAP).trim()}`] : [])] : [];
20
+ const files = (label, items) => items.length ? [` ${label}: ${items.slice(0, WHY_CAP).join(", ")}${items.length > WHY_CAP ? ` …(+${items.length - WHY_CAP} more)` : ""}`] : [];
21
+ /** Plain-text detail lines for a record looked up by id. */
22
+ export function formatWhyRecord(byId) {
23
+ const hit = byId.record;
24
+ if (!hit) {
25
+ return [byId.asOf
26
+ ? `No record ${byId.id} as of ${byId.asOf} (it did not exist or was not in force then).`
27
+ : `No record ${byId.id} (no decision, constraint, bug, finding, or component has that id).`];
28
+ }
29
+ const out = [];
30
+ switch (hit.kind) {
31
+ case "decision": {
32
+ const d = hit.record;
33
+ out.push(`DECISION ${d.id} [${d.status}] ${d.title}`);
34
+ if (d.topic)
35
+ out.push(` topic: ${d.topic}`);
36
+ if (d.decision)
37
+ out.push(` decision: ${clip(d.decision)}`);
38
+ if (d.context)
39
+ out.push(` context: ${clip(d.context)}`);
40
+ out.push(...list("rejected alternatives", d.alternatives_rejected), ...list("consequences", d.consequences));
41
+ out.push(...files("files", d.related_files));
42
+ if (d.supersedes)
43
+ out.push(` supersedes: ${d.supersedes}`);
44
+ if (d.superseded_by)
45
+ out.push(` superseded by: ${d.superseded_by}`);
46
+ if (hit.chain.length > 1) {
47
+ // A WHY_CAP window that always holds the queried decision: the newest links
48
+ // when it is near the end, otherwise starting at it.
49
+ const at = Math.max(0, hit.chain.findIndex((c) => c.id === d.id));
50
+ const start = Math.max(0, Math.min(at, hit.chain.length - WHY_CAP));
51
+ const end = Math.min(hit.chain.length, start + WHY_CAP);
52
+ out.push(" chain (oldest first):");
53
+ if (start > 0)
54
+ out.push(` …(+${start} earlier)`);
55
+ out.push(...hit.chain.slice(start, end).map((c) => ` - ${c.id} [${c.status}] ${clip(c.title, ITEM_CHARS)}${c.id === d.id ? " ←" : ""}`));
56
+ if (end < hit.chain.length)
57
+ out.push(` …(+${hit.chain.length - end} later)`);
58
+ }
59
+ break;
60
+ }
61
+ case "constraint": {
62
+ const c = hit.record;
63
+ out.push(`CONSTRAINT ${c.id} [${c.severity}/${c.status}] ${clip(c.statement)}`);
64
+ out.push(...(c.scope.length ? files("scope", c.scope) : [" scope: repo"]));
65
+ if (c.rationale)
66
+ out.push(` rationale: ${clip(c.rationale)}`);
67
+ if (c.source_decision)
68
+ out.push(` source decision: ${c.source_decision}`);
69
+ break;
70
+ }
71
+ case "bug": {
72
+ const b = hit.record;
73
+ out.push(`BUG ${b.id} [${b.status}/${b.severity}] ${b.title}`);
74
+ if (b.symptom)
75
+ out.push(` symptom: ${clip(b.symptom)}`);
76
+ if (b.root_cause)
77
+ out.push(` root cause: ${clip(b.root_cause)}`);
78
+ if (b.lineage.fixed_commit)
79
+ out.push(` fixed in: ${b.lineage.fixed_commit}`);
80
+ out.push(...files("files", b.affected_files));
81
+ break;
82
+ }
83
+ case "finding": {
84
+ const f = hit.record;
85
+ out.push(`FINDING ${f.id} [${f.triage}/${f.severity}] ${f.title}`);
86
+ if (f.observation)
87
+ out.push(` observation: ${clip(f.observation)}`);
88
+ out.push(...list("evidence", f.evidence));
89
+ out.push(...files("files", f.affected_files));
90
+ break;
91
+ }
92
+ case "component": {
93
+ const c = hit.record;
94
+ out.push(`COMPONENT ${c.id} [${c.kind}/${c.status}] ${c.name}`);
95
+ if (c.responsibility)
96
+ out.push(` responsibility: ${clip(c.responsibility)}`);
97
+ out.push(...files("paths", c.paths));
98
+ break;
99
+ }
100
+ }
101
+ const p = hit.record.provenance;
102
+ out.push(` provenance: ${p.source}, confidence ${p.confidence}`);
103
+ return out;
104
+ }
105
+ //# sourceMappingURL=whyRecord.js.map
@@ -19,6 +19,29 @@ export declare function gitWorktreeRoot(cwd: string): string | null;
19
19
  * `.hunch-private/.hunch` can never stage or commit into the code repository. */
20
20
  export declare function isGitRepoRoot(cwd: string): boolean;
21
21
  export declare function gitNullDevice(): string;
22
+ export interface HunchPullFailureDetail {
23
+ /** The mergeRemote/pull step that failed (e.g. "ls-remote", "fetch", "worktree-clean"). */
24
+ step: string;
25
+ /** Node error code (ETIMEDOUT), killing signal, or `exit <status>`. */
26
+ code?: string;
27
+ /** Last 400 characters of git's stderr, when captured. */
28
+ stderr?: string;
29
+ }
30
+ /** Captured git stderr can echo a remote URL with embedded credentials; strip the
31
+ * userinfo (`://user:pass@`, `://token@`) before it is stored or printed. */
32
+ export declare function redactUrlUserinfo(text: string): string;
33
+ /** One-line rendering for logs and refusal messages: `step: fetch, ETIMEDOUT, fatal: ...`. */
34
+ export declare function describePullFailure(detail: HunchPullFailureDetail | undefined): string;
35
+ /** HUNCH_PULL_GIT_TIMEOUT_MS: per-process override of the read-side pull's DEFAULT
36
+ * remote budget (ls-remote/fetch/merge) for slow runners — a caller's explicit
37
+ * timeoutMs still wins; ignored unless a positive integer. */
38
+ export declare function pullGitTimeoutOverride(): number | undefined;
39
+ /** Remote budget for one read-side pull: an explicit caller budget always wins;
40
+ * HUNCH_PULL_GIT_TIMEOUT_MS only replaces the 5s default. */
41
+ export declare function pullRemoteTimeoutMs(callerMs?: number): number;
42
+ /** Budget for the bounded local git helpers (status, rev-parse, ls-tree): 2s by
43
+ * default, scaled with HUNCH_PULL_GIT_TIMEOUT_MS to max(2s, override / 2.5). */
44
+ export declare function pullLocalGitTimeoutMs(): number;
22
45
  /** Compare physical directory identity before path text. Git for Windows can
23
46
  * return an 8.3/short or differently-cased spelling for the same top-level
24
47
  * directory that Node reached through its long path. A nonzero file ID keeps
@@ -111,6 +134,7 @@ export interface HunchRemoteContract {
111
134
  ref: string;
112
135
  verify: () => boolean;
113
136
  }
137
+ export declare const DEFAULT_READ_REMOTE_TIMEOUT_MS = 5000;
114
138
  export declare function commitAndPushHunch(hunchDir: string, message: string, opts: HunchCommitOptions): "pushed" | "committed" | null;
115
139
  /** True when `rel` is tracked with no staged or unstaged changes (untracked counts as
116
140
  * dirty, so a doc the user never committed is never swept into a memory commit). */
@@ -138,6 +162,14 @@ export interface PullHunchOptions {
138
162
  allowUnrelatedHistories?: boolean;
139
163
  }
140
164
  export declare function pullHunchStatus(hunchDir: string, opts?: PullHunchOptions): HunchPullStatus;
165
+ export interface HunchPullResult {
166
+ status: HunchPullStatus;
167
+ /** Present on "failed": which step failed and git's own reason (issue #482). */
168
+ detail?: HunchPullFailureDetail;
169
+ }
170
+ /** pullHunchStatus plus a diagnosable reason for a "failed" pull. The remote budget is
171
+ * the caller's (default 5s) unless HUNCH_PULL_GIT_TIMEOUT_MS overrides it. */
172
+ export declare function pullHunchStatusDetailed(hunchDir: string, opts?: PullHunchOptions): HunchPullResult;
141
173
  export declare function pullHunch(hunchDir: string): boolean;
142
174
  /** Explicit retry path for a clean overlay that already has a local memory commit
143
175
  * stranded by an earlier offline/rejected push. Unlike commitAndPushHunch this