@davesheffer/hunch 1.32.7 → 1.33.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 (60) hide show
  1. package/README.md +102 -206
  2. package/dist/cli/index.js +2 -0
  3. package/dist/cli/integrations.js +4 -1
  4. package/dist/cli/serve.js +28 -2
  5. package/dist/cli/state.d.ts +3 -0
  6. package/dist/cli/state.js +150 -0
  7. package/dist/cli/update.js +6 -2
  8. package/dist/client/state.d.ts +82 -14
  9. package/dist/client/state.js +16 -2
  10. package/dist/client/stateProof.d.ts +4 -0
  11. package/dist/client/stateProof.js +17 -0
  12. package/dist/constitution/behaviorEvaluator.js +1 -1
  13. package/dist/constitution/schema.d.ts +2 -2
  14. package/dist/core/automaticReviewMemory.d.ts +5 -0
  15. package/dist/core/conventionDelivery.d.ts +8 -0
  16. package/dist/core/conventionDelivery.js +52 -0
  17. package/dist/core/fieldProvenance.d.ts +8 -0
  18. package/dist/core/fieldProvenance.js +72 -0
  19. package/dist/core/recordVisibility.d.ts +9 -0
  20. package/dist/core/recordVisibility.js +25 -0
  21. package/dist/core/stateCanonical.d.ts +3 -0
  22. package/dist/core/stateCanonical.js +34 -0
  23. package/dist/core/stateContract.d.ts +122 -7
  24. package/dist/core/stateContract.js +26 -31
  25. package/dist/core/stateDelivery.d.ts +3 -3
  26. package/dist/core/stateDelivery.js +10 -1
  27. package/dist/core/stateHttp.d.ts +280 -0
  28. package/dist/core/stateHttp.js +17 -0
  29. package/dist/core/stateProof.d.ts +13 -0
  30. package/dist/core/stateProof.js +34 -0
  31. package/dist/core/stateRecords.d.ts +127 -0
  32. package/dist/core/stateRecords.js +48 -0
  33. package/dist/core/types.d.ts +146 -4
  34. package/dist/core/types.js +8 -2
  35. package/dist/extractors/git.js +3 -10
  36. package/dist/integrations/health.d.ts +2 -1
  37. package/dist/integrations/health.js +53 -16
  38. package/dist/mcp/server.js +10 -4
  39. package/dist/serve/app.d.ts +2 -0
  40. package/dist/serve/app.js +71 -30
  41. package/dist/serve/config.d.ts +16 -0
  42. package/dist/serve/config.js +27 -7
  43. package/dist/serve/operator.d.ts +4 -0
  44. package/dist/serve/operator.js +223 -0
  45. package/dist/serve/stateProof.d.ts +15 -0
  46. package/dist/serve/stateProof.js +105 -0
  47. package/dist/store/changeLedger.d.ts +6 -0
  48. package/dist/store/hunchStore.d.ts +4 -2
  49. package/dist/store/hunchStore.js +18 -19
  50. package/dist/store/stateAccess.d.ts +13 -0
  51. package/dist/store/stateAccess.js +85 -0
  52. package/dist/store/stateBinding.d.ts +13 -18
  53. package/dist/store/stateBinding.js +161 -52
  54. package/dist/store/stateCapture.js +10 -2
  55. package/dist/store/stateError.d.ts +12 -0
  56. package/dist/store/stateError.js +12 -0
  57. package/dist/store/statePartition.d.ts +9 -0
  58. package/dist/store/statePartition.js +30 -0
  59. package/package.json +6 -2
  60. package/server.json +3 -3
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { ProvenanceSchema, isCredentialFreeText, type Provenance } from "./provenance.js";
10
- import { type ActionReceipt, type Commitment, type DerivedState, type ExternalEntity, type StateRelationship } from "./stateRecords.js";
10
+ import { type Convention, type ActionReceipt, type Commitment, type DerivedState, type ExternalEntity, type StateRelationship } from "./stateRecords.js";
11
11
  export { ProvenanceSchema, isCredentialFreeText };
12
12
  export type { Provenance };
13
13
  export declare const ComponentKind: z.ZodEnum<{
@@ -335,6 +335,11 @@ export declare const PremiseSchema: z.ZodObject<{
335
335
  }, z.core.$strip>;
336
336
  export type Premise = z.infer<typeof PremiseSchema>;
337
337
  export declare const DecisionSchema: z.ZodObject<{
338
+ visibility: z.ZodOptional<z.ZodObject<{
339
+ owner: z.ZodString;
340
+ readers: z.ZodArray<z.ZodString>;
341
+ writers: z.ZodArray<z.ZodString>;
342
+ }, z.core.$strict>>;
338
343
  id: z.ZodString;
339
344
  title: z.ZodString;
340
345
  topic: z.ZodDefault<z.ZodNullable<z.ZodString>>;
@@ -416,6 +421,11 @@ export declare const BugLineageSchema: z.ZodObject<{
416
421
  export type BugLineage = z.infer<typeof BugLineageSchema>;
417
422
  /** A bug with root cause and lineage (introduced → fixed → recurred). */
418
423
  export declare const BugSchema: z.ZodObject<{
424
+ visibility: z.ZodOptional<z.ZodObject<{
425
+ owner: z.ZodString;
426
+ readers: z.ZodArray<z.ZodString>;
427
+ writers: z.ZodArray<z.ZodString>;
428
+ }, z.core.$strict>>;
419
429
  id: z.ZodString;
420
430
  title: z.ZodString;
421
431
  symptom: z.ZodDefault<z.ZodString>;
@@ -452,6 +462,11 @@ export declare const BugSchema: z.ZodObject<{
452
462
  export type Bug = z.infer<typeof BugSchema>;
453
463
  /** An invariant the system must respect. */
454
464
  export declare const ConstraintSchema: z.ZodObject<{
465
+ visibility: z.ZodOptional<z.ZodObject<{
466
+ owner: z.ZodString;
467
+ readers: z.ZodArray<z.ZodString>;
468
+ writers: z.ZodArray<z.ZodString>;
469
+ }, z.core.$strict>>;
455
470
  id: z.ZodString;
456
471
  type: z.ZodDefault<z.ZodEnum<{
457
472
  security: "security";
@@ -526,6 +541,11 @@ export type Runbook = z.infer<typeof RunbookSchema>;
526
541
  * never enters any block path. Lifecycle is `triage`, not valid-time: a finding is
527
542
  * resolved/stale-marked, never superseded. */
528
543
  export declare const FindingSchema: z.ZodObject<{
544
+ visibility: z.ZodOptional<z.ZodObject<{
545
+ owner: z.ZodString;
546
+ readers: z.ZodArray<z.ZodString>;
547
+ writers: z.ZodArray<z.ZodString>;
548
+ }, z.core.$strict>>;
529
549
  id: z.ZodString;
530
550
  title: z.ZodString;
531
551
  observation: z.ZodDefault<z.ZodString>;
@@ -538,8 +558,8 @@ export declare const FindingSchema: z.ZodObject<{
538
558
  critical: "critical";
539
559
  }>>;
540
560
  triage: z.ZodDefault<z.ZodEnum<{
541
- open: "open";
542
561
  stale: "stale";
562
+ open: "open";
543
563
  "accepted-risk": "accepted-risk";
544
564
  scheduled: "scheduled";
545
565
  resolved: "resolved";
@@ -590,7 +610,7 @@ export declare function assertLandscapeDriftCandidate(value: unknown): asserts v
590
610
  /** Convert one valid external observation into advisory Hunch memory, never graph authority. */
591
611
  export declare function landscapeDriftCandidateFinding(value: unknown): Finding;
592
612
  /** The entity collections, keyed by their on-disk directory name. */
593
- export declare const ENTITY_KINDS: readonly ["components", "resources", "edges", "symbols", "decisions", "bugs", "constraints", "runbooks", "findings", "receipts", "commitments", "derived", "entities", "relationships"];
613
+ export declare const ENTITY_KINDS: readonly ["components", "resources", "edges", "symbols", "decisions", "bugs", "constraints", "runbooks", "findings", "receipts", "commitments", "derived", "entities", "relationships", "conventions"];
594
614
  export type EntityKind = (typeof ENTITY_KINDS)[number];
595
615
  export declare const SCHEMAS: {
596
616
  readonly components: z.ZodObject<{
@@ -753,6 +773,11 @@ export declare const SCHEMAS: {
753
773
  last_changed: z.ZodDefault<z.ZodString>;
754
774
  }, z.core.$strip>;
755
775
  readonly decisions: z.ZodObject<{
776
+ visibility: z.ZodOptional<z.ZodObject<{
777
+ owner: z.ZodString;
778
+ readers: z.ZodArray<z.ZodString>;
779
+ writers: z.ZodArray<z.ZodString>;
780
+ }, z.core.$strict>>;
756
781
  id: z.ZodString;
757
782
  title: z.ZodString;
758
783
  topic: z.ZodDefault<z.ZodNullable<z.ZodString>>;
@@ -823,6 +848,11 @@ export declare const SCHEMAS: {
823
848
  date: z.ZodString;
824
849
  }, z.core.$strip>;
825
850
  readonly bugs: z.ZodObject<{
851
+ visibility: z.ZodOptional<z.ZodObject<{
852
+ owner: z.ZodString;
853
+ readers: z.ZodArray<z.ZodString>;
854
+ writers: z.ZodArray<z.ZodString>;
855
+ }, z.core.$strict>>;
826
856
  id: z.ZodString;
827
857
  title: z.ZodString;
828
858
  symptom: z.ZodDefault<z.ZodString>;
@@ -857,6 +887,11 @@ export declare const SCHEMAS: {
857
887
  }, z.core.$strip>;
858
888
  }, z.core.$strip>;
859
889
  readonly constraints: z.ZodObject<{
890
+ visibility: z.ZodOptional<z.ZodObject<{
891
+ owner: z.ZodString;
892
+ readers: z.ZodArray<z.ZodString>;
893
+ writers: z.ZodArray<z.ZodString>;
894
+ }, z.core.$strict>>;
860
895
  id: z.ZodString;
861
896
  type: z.ZodDefault<z.ZodEnum<{
862
897
  security: "security";
@@ -919,6 +954,11 @@ export declare const SCHEMAS: {
919
954
  date: z.ZodString;
920
955
  }, z.core.$strip>;
921
956
  readonly findings: z.ZodObject<{
957
+ visibility: z.ZodOptional<z.ZodObject<{
958
+ owner: z.ZodString;
959
+ readers: z.ZodArray<z.ZodString>;
960
+ writers: z.ZodArray<z.ZodString>;
961
+ }, z.core.$strict>>;
922
962
  id: z.ZodString;
923
963
  title: z.ZodString;
924
964
  observation: z.ZodDefault<z.ZodString>;
@@ -931,8 +971,8 @@ export declare const SCHEMAS: {
931
971
  critical: "critical";
932
972
  }>>;
933
973
  triage: z.ZodDefault<z.ZodEnum<{
934
- open: "open";
935
974
  stale: "stale";
975
+ open: "open";
936
976
  "accepted-risk": "accepted-risk";
937
977
  scheduled: "scheduled";
938
978
  resolved: "resolved";
@@ -950,7 +990,76 @@ export declare const SCHEMAS: {
950
990
  last_verified: z.ZodOptional<z.ZodString>;
951
991
  }, z.core.$strip>;
952
992
  }, z.core.$strip>;
993
+ readonly conventions: z.ZodObject<{
994
+ schema: z.ZodLiteral<"nuryel.convention/1">;
995
+ id: z.ZodString;
996
+ scope: z.ZodObject<{
997
+ kind: z.ZodEnum<{
998
+ repository: "repository";
999
+ organization: "organization";
1000
+ team: "team";
1001
+ user: "user";
1002
+ }>;
1003
+ id: z.ZodString;
1004
+ }, z.core.$strict>;
1005
+ key: z.ZodString;
1006
+ value: z.ZodString;
1007
+ status: z.ZodEnum<{
1008
+ stale: "stale";
1009
+ proposed: "proposed";
1010
+ accepted: "accepted";
1011
+ withdrawn: "withdrawn";
1012
+ }>;
1013
+ sources: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1014
+ kind: z.ZodLiteral<"record">;
1015
+ id: z.ZodString;
1016
+ record_hash: z.ZodString;
1017
+ scope: z.ZodOptional<z.ZodObject<{
1018
+ kind: z.ZodEnum<{
1019
+ repository: "repository";
1020
+ organization: "organization";
1021
+ team: "team";
1022
+ user: "user";
1023
+ }>;
1024
+ id: z.ZodString;
1025
+ }, z.core.$strict>>;
1026
+ }, z.core.$strict>, z.ZodObject<{
1027
+ kind: z.ZodLiteral<"external">;
1028
+ ref: z.ZodObject<{
1029
+ system: z.ZodString;
1030
+ object_type: z.ZodString;
1031
+ object_key: z.ZodString;
1032
+ version: z.ZodOptional<z.ZodString>;
1033
+ content_hash: z.ZodOptional<z.ZodString>;
1034
+ observed_at: z.ZodString;
1035
+ locator: z.ZodOptional<z.ZodString>;
1036
+ }, z.core.$strict>;
1037
+ }, z.core.$strict>, z.ZodObject<{
1038
+ kind: z.ZodLiteral<"schema">;
1039
+ name: z.ZodString;
1040
+ fingerprint: z.ZodString;
1041
+ }, z.core.$strict>], "kind">>;
1042
+ valid_from: z.ZodString;
1043
+ valid_to: z.ZodDefault<z.ZodNullable<z.ZodString>>;
1044
+ review_by: z.ZodString;
1045
+ provenance: z.ZodObject<{
1046
+ source: z.ZodString;
1047
+ confidence: z.ZodNumber;
1048
+ evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
1049
+ last_verified: z.ZodOptional<z.ZodString>;
1050
+ }, z.core.$strip>;
1051
+ visibility: z.ZodOptional<z.ZodObject<{
1052
+ owner: z.ZodString;
1053
+ readers: z.ZodArray<z.ZodString>;
1054
+ writers: z.ZodArray<z.ZodString>;
1055
+ }, z.core.$strict>>;
1056
+ }, z.core.$strict>;
953
1057
  readonly receipts: z.ZodObject<{
1058
+ visibility: z.ZodOptional<z.ZodObject<{
1059
+ owner: z.ZodString;
1060
+ readers: z.ZodArray<z.ZodString>;
1061
+ writers: z.ZodArray<z.ZodString>;
1062
+ }, z.core.$strict>>;
954
1063
  schema: z.ZodLiteral<"nuryel.receipt/1">;
955
1064
  id: z.ZodString;
956
1065
  scope: z.ZodObject<{
@@ -1023,6 +1132,11 @@ export declare const SCHEMAS: {
1023
1132
  }, z.core.$strip>;
1024
1133
  }, z.core.$strict>;
1025
1134
  readonly commitments: z.ZodObject<{
1135
+ visibility: z.ZodOptional<z.ZodObject<{
1136
+ owner: z.ZodString;
1137
+ readers: z.ZodArray<z.ZodString>;
1138
+ writers: z.ZodArray<z.ZodString>;
1139
+ }, z.core.$strict>>;
1026
1140
  schema: z.ZodLiteral<"nuryel.commitment/1">;
1027
1141
  id: z.ZodString;
1028
1142
  scope: z.ZodObject<{
@@ -1065,6 +1179,11 @@ export declare const SCHEMAS: {
1065
1179
  }, z.core.$strip>;
1066
1180
  }, z.core.$strict>;
1067
1181
  readonly derived: z.ZodObject<{
1182
+ visibility: z.ZodOptional<z.ZodObject<{
1183
+ owner: z.ZodString;
1184
+ readers: z.ZodArray<z.ZodString>;
1185
+ writers: z.ZodArray<z.ZodString>;
1186
+ }, z.core.$strict>>;
1068
1187
  schema: z.ZodLiteral<"nuryel.derived/1">;
1069
1188
  id: z.ZodString;
1070
1189
  scope: z.ZodObject<{
@@ -1108,6 +1227,18 @@ export declare const SCHEMAS: {
1108
1227
  name: z.ZodString;
1109
1228
  fingerprint: z.ZodString;
1110
1229
  }, z.core.$strict>], "kind">>;
1230
+ field_provenance: z.ZodOptional<z.ZodArray<z.ZodObject<{
1231
+ selector: z.ZodDiscriminatedUnion<[z.ZodObject<{
1232
+ kind: z.ZodLiteral<"json_pointer">;
1233
+ path: z.ZodString;
1234
+ }, z.core.$strict>, z.ZodObject<{
1235
+ kind: z.ZodLiteral<"text">;
1236
+ start: z.ZodNumber;
1237
+ end: z.ZodNumber;
1238
+ }, z.core.$strict>], "kind">;
1239
+ value_hash: z.ZodString;
1240
+ dependency_hashes: z.ZodArray<z.ZodString>;
1241
+ }, z.core.$strict>>>;
1111
1242
  transform_version: z.ZodString;
1112
1243
  computed_at: z.ZodString;
1113
1244
  valid_to: z.ZodDefault<z.ZodNullable<z.ZodString>>;
@@ -1142,6 +1273,11 @@ export declare const SCHEMAS: {
1142
1273
  }, z.core.$strip>;
1143
1274
  }, z.core.$strict>;
1144
1275
  readonly entities: z.ZodObject<{
1276
+ visibility: z.ZodOptional<z.ZodObject<{
1277
+ owner: z.ZodString;
1278
+ readers: z.ZodArray<z.ZodString>;
1279
+ writers: z.ZodArray<z.ZodString>;
1280
+ }, z.core.$strict>>;
1145
1281
  schema: z.ZodLiteral<"nuryel.entity/1">;
1146
1282
  id: z.ZodString;
1147
1283
  kind: z.ZodString;
@@ -1181,6 +1317,11 @@ export declare const SCHEMAS: {
1181
1317
  updated_at: z.ZodString;
1182
1318
  }, z.core.$strict>;
1183
1319
  readonly relationships: z.ZodObject<{
1320
+ visibility: z.ZodOptional<z.ZodObject<{
1321
+ owner: z.ZodString;
1322
+ readers: z.ZodArray<z.ZodString>;
1323
+ writers: z.ZodArray<z.ZodString>;
1324
+ }, z.core.$strict>>;
1184
1325
  schema: z.ZodLiteral<"nuryel.relationship/1">;
1185
1326
  id: z.ZodString;
1186
1327
  from: z.ZodString;
@@ -1228,6 +1369,7 @@ export type EntityFor = {
1228
1369
  constraints: Constraint;
1229
1370
  runbooks: Runbook;
1230
1371
  findings: Finding;
1372
+ conventions: Convention;
1231
1373
  receipts: ActionReceipt;
1232
1374
  commitments: Commitment;
1233
1375
  derived: DerivedState;
@@ -1,3 +1,4 @@
1
+ import { RecordVisibilitySchema } from "./recordVisibility.js";
1
2
  /**
2
3
  * Core entity schema for the Project Hunch (DESIGN.md §3).
3
4
  *
@@ -9,7 +10,7 @@ import { z } from "zod";
9
10
  import { createHash } from "node:crypto";
10
11
  import { findingId, resourceId, resourceRelationshipId } from "./ids.js";
11
12
  import { ProvenanceSchema, SENSITIVE_METADATA_KEY, isCredentialFreeText } from "./provenance.js";
12
- import { ActionReceiptSchema, CommitmentSchema, DerivedStateSchema, ExternalEntitySchema, StateRelationshipSchema, } from "./stateRecords.js";
13
+ import { ConventionSchema, ActionReceiptSchema, CommitmentSchema, DerivedStateSchema, ExternalEntitySchema, StateRelationshipSchema, } from "./stateRecords.js";
13
14
  // Provenance and the credential-free text check live in the leaf module ./provenance.js so
14
15
  // record schemas registered below can import them without a cycle; re-exported unchanged.
15
16
  export { ProvenanceSchema, isCredentialFreeText };
@@ -323,6 +324,7 @@ export const PremiseSchema = z.object({
323
324
  path: ["under"],
324
325
  });
325
326
  export const DecisionSchema = z.object({
327
+ visibility: RecordVisibilitySchema.optional(),
326
328
  id: z.string().describe("dec_*"),
327
329
  title: z.string(),
328
330
  // Decision-grounding anchor: the join key that relates a doc section, a decision,
@@ -372,6 +374,7 @@ export const BugLineageSchema = z.object({
372
374
  });
373
375
  /** A bug with root cause and lineage (introduced → fixed → recurred). */
374
376
  export const BugSchema = z.object({
377
+ visibility: RecordVisibilitySchema.optional(),
375
378
  id: z.string().describe("bug_*"),
376
379
  title: z.string(),
377
380
  symptom: z.string().default(""),
@@ -388,6 +391,7 @@ export const BugSchema = z.object({
388
391
  });
389
392
  /** An invariant the system must respect. */
390
393
  export const ConstraintSchema = z.object({
394
+ visibility: RecordVisibilitySchema.optional(),
391
395
  id: z.string().describe("con_*"),
392
396
  type: z.enum(["security", "performance", "correctness", "architecture", "compliance"]).default("correctness"),
393
397
  statement: z.string(),
@@ -448,6 +452,7 @@ export const RunbookSchema = z.object({
448
452
  * never enters any block path. Lifecycle is `triage`, not valid-time: a finding is
449
453
  * resolved/stale-marked, never superseded. */
450
454
  export const FindingSchema = z.object({
455
+ visibility: RecordVisibilitySchema.optional(),
451
456
  id: z.string().describe("fnd_*"),
452
457
  title: z.string(),
453
458
  observation: z.string().default("").describe("what was observed, in plain words"),
@@ -594,7 +599,7 @@ export function landscapeDriftCandidateFinding(value) {
594
599
  // loads exactly as before, and an older build ignores directories it does not know.
595
600
  export const ENTITY_KINDS = [
596
601
  "components", "resources", "edges", "symbols", "decisions", "bugs", "constraints", "runbooks", "findings",
597
- "receipts", "commitments", "derived", "entities", "relationships",
602
+ "receipts", "commitments", "derived", "entities", "relationships", "conventions",
598
603
  ];
599
604
  export const SCHEMAS = {
600
605
  components: ComponentSchema,
@@ -606,6 +611,7 @@ export const SCHEMAS = {
606
611
  constraints: ConstraintSchema,
607
612
  runbooks: RunbookSchema,
608
613
  findings: FindingSchema,
614
+ conventions: ConventionSchema,
609
615
  receipts: ActionReceiptSchema,
610
616
  commitments: CommitmentSchema,
611
617
  derived: DerivedStateSchema,
@@ -1838,16 +1838,9 @@ export function isLinkedWorktree(cwd) {
1838
1838
  const own = gitSafe(["rev-parse", "--absolute-git-dir"], cwd);
1839
1839
  if (!common || !own)
1840
1840
  return false;
1841
- // realpath BOTH before comparing: `--absolute-git-dir` is symlink-resolved while
1842
- // gitCommonDir is not, so on macOS the main checkout would otherwise mismatch on
1843
- // /var vs /private/var and falsely read as "linked".
1844
- const norm = (p) => { try {
1845
- return realpathSync(p);
1846
- }
1847
- catch {
1848
- return resolve(p);
1849
- } };
1850
- return norm(own) !== norm(common);
1841
+ // Git can spell the same directory differently: /var vs /private/var on
1842
+ // macOS, or long vs 8.3/case variants on Windows. Compare physical identity.
1843
+ return !sameFilesystemEntry(own, common);
1851
1844
  }
1852
1845
  /** Current branch name (e.g. "main", "feat/x"), or "" in detached HEAD / non-repo.
1853
1846
  * Stamped onto auto-captured decisions so branch-scoped work stays filterable. */
@@ -75,7 +75,8 @@ export declare function inspectIntegrations(root: string, selected?: Harness): I
75
75
  * tooling/sync-version-pins.mjs) so hooks and MCP never point at a version npm
76
76
  * cannot serve. Unknown git state yields [] — callers then treat nothing as local. */
77
77
  export declare function machineLocalIntegrationFiles(root: string): string[];
78
- /** Repair only exact published pins. Preserve formatting and all other values.
78
+ /** Repair exact published pins and the known misplaced MCP hook subcommand.
79
+ * Preserve formatting and all other values, including disabled hook settings.
79
80
  * Preflight every affected file before writing any; reject malformed JSON/TOML.
80
81
  * `skip` leaves a file untouched (used to keep machine-local pins on a version
81
82
  * npm can actually serve while a release is still publishing). */
@@ -54,18 +54,33 @@ function strings(value) {
54
54
  return Object.values(value).flatMap(strings);
55
55
  return [];
56
56
  }
57
- function hookCommands(value) {
57
+ function hookCommands(value, includeDisabled = false) {
58
58
  if (Array.isArray(value))
59
- return value.flatMap(hookCommands);
59
+ return value.flatMap(v => hookCommands(v, includeDisabled));
60
60
  if (!value || typeof value !== "object")
61
61
  return [];
62
62
  const obj = value;
63
- if (obj.enabled === false || (obj.type !== undefined && obj.type !== "command"))
63
+ if ((!includeDisabled && obj.enabled === false) || (obj.type !== undefined && obj.type !== "command"))
64
64
  return [];
65
65
  const command = typeof obj.command === "string" ? obj.command : "";
66
- const own = /(?:@davesheffer\/hunch|(?:dist|src)[\\/]+cli[\\/]+index\.(?:js|ts))/.test(command)
66
+ const own = publishedHookCommand(command) !== undefined || /(?:dist|src)[\\/]+cli[\\/]+index\.(?:js|ts)/.test(command)
67
67
  && /\s"?hook"?(?:\s+"?--provider"?\s+"?[a-z]+"?)?\s*$/.test(command);
68
- return [...(own ? [command] : []), ...(obj.hooks ? hookCommands(obj.hooks) : [])];
68
+ return [...(own ? [command] : []), ...(obj.hooks ? hookCommands(obj.hooks, includeDisabled) : [])];
69
+ }
70
+ /** Recognize only generated npm commands, including their legacy quoted form.
71
+ * Never normalize a wrapper, shell expression, or another program's arguments. */
72
+ function publishedHookCommand(command) {
73
+ const parts = command.trim().match(/"[^"\\]*"|'[^']*'|[^\s"'\\]+/g);
74
+ if (!parts || parts.join(" ") !== command.trim().replace(/\s+/g, " "))
75
+ return undefined;
76
+ const tokens = parts.map(p => /^["']/.test(p) ? p.slice(1, -1) : p);
77
+ if (tokens.some(token => /\s/.test(token)))
78
+ return undefined;
79
+ const bare = tokens.join(" ");
80
+ return /^npx(?:\.cmd)? (?:-y|--yes) --package=(?:hunch-exact@npm:)?@davesheffer\/hunch@[0-9A-Za-z.+-]+ (?:-- )?hunch (?:mcp )?hook(?: --provider [a-z]+)?$/.test(bare) ? bare : undefined;
81
+ }
82
+ function misroutedHook(command) {
83
+ return publishedHookCommand(command)?.includes(" hunch mcp hook") ?? false;
69
84
  }
70
85
  /** Pin repair is restricted to Hunch's marker-owned TOML block. */
71
86
  function codexBlock(raw) {
@@ -169,7 +184,13 @@ export function inspectIntegrations(root, selected) {
169
184
  const config = object(parseJsonc(readFileSync(join(root, spec.hooks), "utf8")));
170
185
  disabled = config.disableAllHooks === true;
171
186
  events = object(harness === "antigravity" ? config.hunch : config.hooks);
172
- recordPins(spec.hooks, Object.values(events).flatMap(hookCommands));
187
+ const commands = Object.values(events).flatMap(v => hookCommands(v));
188
+ recordPins(spec.hooks, commands);
189
+ if (commands.some(misroutedHook))
190
+ report.issues.push({
191
+ file: spec.hooks, code: "hook-command",
192
+ detail: "Hunch hooks invoke the MCP subcommand instead of the hook handler; run hunch integrations repair-pins",
193
+ });
173
194
  }
174
195
  catch (e) {
175
196
  report.issues.push({ file: spec.hooks, code: "hook-config", detail: e.message });
@@ -178,6 +199,12 @@ export function inspectIntegrations(root, selected) {
178
199
  for (const [i, capability] of ["context", "edit-blocking", "failure-capture", "compaction"].entries()) {
179
200
  const event = spec.events[i];
180
201
  const status = capabilities[capability];
202
+ const matchesProvider = (command) => {
203
+ if (misroutedHook(command))
204
+ return false;
205
+ const dialect = (publishedHookCommand(command) ?? command).match(/"?--provider"?\s+"?([a-z]+)"?/i)?.[1]?.toLowerCase() ?? "claude";
206
+ return dialect === harness;
207
+ };
181
208
  if (!event) {
182
209
  status.status = capability === "context" ? "advisory-only" : "unsupported";
183
210
  status.detail = capability === "context" ? "Hunch relies on instructions and voluntary MCP calls on this adapter" : "No Hunch lifecycle adapter for this capability";
@@ -189,12 +216,15 @@ export function inspectIntegrations(root, selected) {
189
216
  status.status = "unsupported";
190
217
  status.detail = "Disabled by local hook settings, firmness, or HUNCH_PIPELINE";
191
218
  }
192
- else if (!hookCommands(events[event]).some(command => {
193
- const dialect = command.match(/"?--provider"?\s+"?([a-z]+)"?/i)?.[1]?.toLowerCase() ?? "claude";
194
- return dialect === harness;
195
- })) {
196
- status.detail = `Missing Hunch ${event} handler`;
197
- report.issues.push({ file: spec.hooks, code: "missing-hook", detail: status.detail });
219
+ else if (!hookCommands(events[event]).some(matchesProvider)) {
220
+ if (hookCommands(events[event], true).some(matchesProvider)) {
221
+ status.status = "unsupported";
222
+ status.detail = `Hunch ${event} handler disabled by local hook settings`;
223
+ }
224
+ else {
225
+ status.detail = `Missing Hunch ${event} handler`;
226
+ report.issues.push({ file: spec.hooks, code: "missing-hook", detail: status.detail });
227
+ }
198
228
  }
199
229
  else if (capability === "edit-blocking" && firmness !== "strict") {
200
230
  status.status = "advisory-only";
@@ -238,7 +268,8 @@ export function machineLocalIntegrationFiles(root) {
238
268
  return [];
239
269
  return (r.stdout ?? "").split(/\r?\n/).map(l => l.trim()).filter(Boolean);
240
270
  }
241
- /** Repair only exact published pins. Preserve formatting and all other values.
271
+ /** Repair exact published pins and the known misplaced MCP hook subcommand.
272
+ * Preserve formatting and all other values, including disabled hook settings.
242
273
  * Preflight every affected file before writing any; reject malformed JSON/TOML.
243
274
  * `skip` leaves a file untouched (used to keep machine-local pins on a version
244
275
  * npm can actually serve while a release is still publishing). */
@@ -262,7 +293,7 @@ export function repairIntegrationPins(root, opts = {}) {
262
293
  if (!exactVersion.test(old))
263
294
  throw new Error(`refusing non-exact Hunch pin in ${file}`);
264
295
  return `@davesheffer/hunch@${version}`;
265
- });
296
+ }).replace(/--package=@davesheffer\/hunch@/g, "--package=hunch-exact@npm:@davesheffer/hunch@");
266
297
  let after;
267
298
  if (name === "codex" && file === spec.mcp) {
268
299
  readLauncher(root, "codex");
@@ -282,8 +313,14 @@ export function repairIntegrationPins(root, opts = {}) {
282
313
  const config = object(parseJsonc(before));
283
314
  const values = file === spec.mcp
284
315
  ? strings(object(object(config[spec.key]).hunch).args)
285
- : Object.values(object(name === "antigravity" ? config.hunch : config.hooks)).flatMap(hookCommands);
286
- const replacements = new Map(values.map(v => [v, replace(v)]).filter(([a, b]) => a !== b));
316
+ : Object.values(object(name === "antigravity" ? config.hunch : config.hooks)).flatMap(v => hookCommands(v, true));
317
+ const replacements = new Map(values.map(v => {
318
+ // Every recognized npm token is shell-safe. Bare tokens also repair
319
+ // the legacy quoted executable, which PowerShell treats as a string.
320
+ const published = file === spec.hooks ? publishedHookCommand(v) : undefined;
321
+ const repaired = published?.replace(" hunch mcp hook", " hunch hook") ?? v;
322
+ return [v, replace(repaired)];
323
+ }).filter(([a, b]) => a !== b));
287
324
  const counts = new Map();
288
325
  // Tokenize comments too, so a quoted command in a comment is untouched.
289
326
  after = before.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:[^"\\]|\\.)*"/g, token => {
@@ -1,3 +1,5 @@
1
+ import { conventionSupplements } from '../core/conventionDelivery.js';
2
+ import { fieldCitationText } from "../core/fieldProvenance.js";
1
3
  /**
2
4
  * MCP server — the structured two-way API into the Hunch (DESIGN.md §7 / App. A).
3
5
  * Exposes read tools (query/why/bug_lineage/check_constraints/get_dependents) and
@@ -1102,7 +1104,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1102
1104
  decisionCorpus: store.recs("decisions"),
1103
1105
  historical: !!asOf,
1104
1106
  profile: profile ?? "builder",
1105
- supplements: [...(dnaSupplement ? [dnaSupplement] : []), ...stateGrounding],
1107
+ supplements: [...(dnaSupplement ? [dnaSupplement] : []), ...stateGrounding, ...(asOf ? [] : conventionSupplements(store.recs("conventions")))],
1106
1108
  };
1107
1109
  // Task-phrase input ("improve retrieval ranking") resolves no file/symbol and
1108
1110
  // used to return an empty brief while the graph held the answer — fall back to
@@ -1822,7 +1824,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1822
1824
  const r = (response.records ?? {})[ref.id] ?? {};
1823
1825
  const g = (k) => { const v = r[k]; return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v); };
1824
1826
  if (ref.facet === "derived")
1825
- return `- ${label} derived ${ref.id} · computed ${g("computed_at")} · ${r.dependencies?.length ?? 0} dependencies\n ${g("content").slice(0, 1200)}`;
1827
+ return `- ${label} derived ${ref.id} · computed ${g("computed_at")} · ${r.dependencies?.length ?? 0} dependencies\n ${g("content").slice(0, 1200)}${fieldCitationText(r)}`;
1826
1828
  if (ref.facet === "commitments")
1827
1829
  return `- ${label} commitment ${ref.id} · ${g("status")} · due ${g("due")} · owner ${g("owner")}: ${g("title")}${r.closed_by ? ` · closed by ${g("closed_by")}` : ""}`;
1828
1830
  if (ref.facet === "receipts") {
@@ -1858,7 +1860,11 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1858
1860
  : sor.observed_truncated ? ['- More observations exist; read this subject with observed_page:{} in one partition, then follow next_cursor.'] : []),
1859
1861
  ...(sor.invalidated_by.length ? [`- invalidated by: ${sor.invalidated_by.join(", ")}`] : [])].join("\n") || "(nothing on record for this subject)"
1860
1862
  : "";
1861
- return stateResult(`${response.receipt_id} · ${summary}${deniedNote}${stateText ? `\n\nState of record:\n${stateText}` : ""}\n\n${envelope.text}`, response);
1863
+ const conventionText = response.conventions ? '\n\nExplicit conventions (advisory; no scope takes precedence):\n' + response.conventions.items.map(item => {
1864
+ const record = response.records?.[item.ref.id];
1865
+ return `- ${item.ref.scope.kind}/${item.ref.scope.id} · ${item.key} · ${record?.status}/${item.currentness}${item.conflict ? ' · CONFLICT' : ''}: ${String(record?.value ?? '').slice(0, 300)} (${item.ref.id})`;
1866
+ }).join('\n') + (response.conventions.truncated ? '\nMore conventions exist; this view is incomplete.' : '') : '';
1867
+ return stateResult(`${response.receipt_id} · ${summary}${deniedNote}${stateText ? `\n\nState of record:\n${stateText}` : ""}\n\n${envelope.text}${conventionText}`, response);
1862
1868
  }
1863
1869
  catch (e) {
1864
1870
  return stateRefusal(e);
@@ -1866,7 +1872,7 @@ export function buildServerWithRootControl(initialRoot, options = {}) {
1866
1872
  });
1867
1873
  server.registerTool("nuryel_write", {
1868
1874
  title: "nuryel.state/1 write — provenance + idempotency in, durability out",
1869
- description: "Write one record into a facet (receipts, commitments, derived, entities, relationships, or the legacy decisions/constraints/bugs/findings). The record must carry provenance; the request must carry an idempotency_key — a replay returns the original, a reused key with a different payload is refused. Ids are derived from the record's facts, never chosen. A second live decision on a topic is refused with the incumbent named; pass supersedes to replace it explicitly. organization/team/user partitions never ride a repository: they require an overlay. To show an existing captured observation under another subject without copying it, write a relationship type observation_about with from=observation id, to=subject, observation_hash, lifecycle=active, reason and hashed external evidence of the explicit association. Retire the relationship to unlink; reactivation requires expected_version.",
1875
+ description: "Write one record into a facet (receipts, commitments, derived, entities, relationships, or the legacy decisions/constraints/bugs/findings). The record must carry provenance; the request must carry an idempotency_key — a replay returns the original, a reused key with a different payload is refused. Ids are derived from the record's facts, never chosen. A second live decision on a topic is refused with the incumbent named; pass supersedes to replace it explicitly. organization/team/user partitions never ride a repository: they require an overlay. To show an existing captured observation under another subject without copying it, write a relationship type observation_about with from=observation id, to=subject, observation_hash, lifecycle=active, reason and hashed external evidence of the explicit association. Retire the relationship to unlink; reactivation requires expected_version. With capability nuryel.field-provenance/1, a derived record may carry field_provenance: [{selector:{kind:json_pointer,path:/field} or {kind:text,start:0,end:10}, value_hash:stateHash(selected scalar or text), dependency_hashes:[stateHash(existing dependency)]}]. Text offsets count Unicode code points, end exclusive. Citations are writer-supplied traceability, not verified support; negotiate support in every shared reader before writing them. With nuryel.record-visibility/1, records may carry visibility:{owner:principal-id,readers:[ids],writers:[ids]}; the owner is implicit in both lists, and writers must be readers. Owner-only audience changes require expected_version, including supersession. Restricted records require a dedicated partition; local stdio principal assertions assume trusted callers.",
1870
1876
  inputSchema: { ...WriteRequestSchema.omit({ schema: true }).shape, cwd: cwdHintField },
1871
1877
  outputSchema: WriteResultSchema.shape,
1872
1878
  }, async ({ cwd: _cwd, ...input }) => {
@@ -27,6 +27,8 @@ export declare class HttpProblem extends Error {
27
27
  constructor(status: number, code: string, message: string, extra?: Record<string, unknown>);
28
28
  }
29
29
  export interface ServeOptions {
30
+ /** Required for key-bound credentials when supplying an in-memory configuration. */
31
+ authStateDir?: string;
30
32
  version?: string;
31
33
  /** Injectable for tests: how a partition's store is opened. */
32
34
  openStore?: (root: string) => HunchStore;