@massu/core 2.5.2 → 2.5.4

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 (93) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/agents/massu-output-scorer.md +2 -2
  3. package/agents/massu-plan-auditor.md +1 -1
  4. package/commands/_shared-references/test-first-protocol.md +4 -4
  5. package/commands/_shared-references/verification-table.md +5 -5
  6. package/commands/massu-audit-deps.md +2 -2
  7. package/commands/massu-batch.md +3 -3
  8. package/commands/massu-checkpoint.md +7 -7
  9. package/commands/massu-ci-fix.md +1 -1
  10. package/commands/massu-cleanup.md +16 -16
  11. package/commands/massu-commit.md +7 -7
  12. package/commands/massu-create-plan.md +7 -7
  13. package/commands/massu-dead-code.md +3 -3
  14. package/commands/massu-debug/references/common-shortcuts.md +1 -1
  15. package/commands/massu-debug/references/investigation-phases.md +4 -4
  16. package/commands/massu-debug/references/report-format.md +2 -2
  17. package/commands/massu-deploy.md +1 -1
  18. package/commands/massu-deps.md +5 -5
  19. package/commands/massu-doc-gen.md +3 -3
  20. package/commands/massu-docs.md +6 -6
  21. package/commands/massu-estimate.md +3 -3
  22. package/commands/massu-golden-path/references/competitive-mode.md +2 -2
  23. package/commands/massu-golden-path/references/phase-1-plan-creation.md +1 -1
  24. package/commands/massu-golden-path/references/phase-2-implementation.md +2 -2
  25. package/commands/massu-golden-path/references/phase-4-commit.md +3 -3
  26. package/commands/massu-golden-path/references/phase-5-push.md +5 -5
  27. package/commands/massu-guide.md +3 -3
  28. package/commands/massu-hotfix.md +14 -14
  29. package/commands/massu-loop/references/checkpoint-audit.md +3 -3
  30. package/commands/massu-loop/references/iteration-structure.md +3 -3
  31. package/commands/massu-loop/references/loop-controller.md +5 -5
  32. package/commands/massu-loop/references/plan-extraction.md +4 -4
  33. package/commands/massu-loop/references/vr-plan-spec.md +3 -3
  34. package/commands/massu-loop-playwright.md +5 -5
  35. package/commands/massu-loop.md +1 -1
  36. package/commands/massu-new-feature.md +6 -24
  37. package/commands/massu-perf.md +1 -1
  38. package/commands/massu-plan.md +21 -21
  39. package/commands/massu-push-light.md +9 -9
  40. package/commands/massu-recap.md +1 -1
  41. package/commands/massu-refactor.md +8 -8
  42. package/commands/massu-release.md +3 -3
  43. package/commands/massu-review.md +1 -1
  44. package/commands/massu-rollback.md +6 -6
  45. package/commands/massu-scaffold-page.md +1 -1
  46. package/commands/massu-scaffold-router.md +1 -1
  47. package/commands/massu-status.md +9 -9
  48. package/commands/massu-tdd.md +2 -2
  49. package/commands/massu-test.md +13 -13
  50. package/commands/massu-verify-playwright.md +1 -1
  51. package/commands/massu-verify.md +20 -20
  52. package/dist/cli.js +1265 -876
  53. package/dist/hooks/auto-learning-pipeline.js +517 -56
  54. package/dist/hooks/classify-failure.js +495 -47
  55. package/dist/hooks/cost-tracker.js +65 -27
  56. package/dist/hooks/fix-detector.js +496 -47
  57. package/dist/hooks/incident-pipeline.js +72 -28
  58. package/dist/hooks/intent-suggester.js +65 -27
  59. package/dist/hooks/memory-recall.js +65 -27
  60. package/dist/hooks/post-edit-context.js +65 -27
  61. package/dist/hooks/post-tool-use.js +70 -29
  62. package/dist/hooks/pre-compact.js +65 -27
  63. package/dist/hooks/pre-delete-check.js +65 -27
  64. package/dist/hooks/pre-tool-use-gate.js +193 -37
  65. package/dist/hooks/quality-event.js +65 -27
  66. package/dist/hooks/rule-enforcement-pipeline.js +65 -27
  67. package/dist/hooks/security-gate.js +197 -38
  68. package/dist/hooks/session-end.js +68 -27
  69. package/dist/hooks/session-start.js +116 -61
  70. package/dist/hooks/user-prompt.js +65 -27
  71. package/docs/SECURITY.md +1 -1
  72. package/fragments/feature-components.md +15 -0
  73. package/fragments/feature-components.python.md +15 -0
  74. package/fragments/feature-components.typescript.md +15 -0
  75. package/package.json +6 -5
  76. package/patterns/testing-patterns.md +8 -8
  77. package/protocols/plan-implementation.md +2 -2
  78. package/protocols/verification.md +4 -4
  79. package/reference/lessons-learned.md +4 -4
  80. package/reference/vr-verification-reference.md +9 -9
  81. package/templates/aspnet/massu.config.yaml +2 -0
  82. package/templates/go-chi/massu.config.yaml +2 -0
  83. package/templates/multi-runtime/massu.config.yaml +4 -2
  84. package/templates/phoenix/massu.config.yaml +2 -0
  85. package/templates/python-django/massu.config.yaml +3 -1
  86. package/templates/python-fastapi/massu.config.yaml +3 -1
  87. package/templates/python-flask/massu.config.yaml +3 -1
  88. package/templates/rails/massu.config.yaml +4 -2
  89. package/templates/rust-actix/massu.config.yaml +2 -0
  90. package/templates/spring/massu.config.yaml +2 -0
  91. package/templates/swift-ios/massu.config.yaml +3 -1
  92. package/templates/ts-nestjs/massu.config.yaml +2 -0
  93. package/templates/ts-nextjs/massu.config.yaml +2 -0
@@ -574,6 +574,15 @@ var init_memory_path = __esm({
574
574
  }
575
575
  });
576
576
 
577
+ // src/lib/corrections-md.ts
578
+ var CORRECTIONS_FILENAME;
579
+ var init_corrections_md = __esm({
580
+ "src/lib/corrections-md.ts"() {
581
+ "use strict";
582
+ CORRECTIONS_FILENAME = "corrections.md";
583
+ }
584
+ });
585
+
577
586
  // src/config-memory-schema.ts
578
587
  import { z } from "zod";
579
588
  var MemoryConfigSchema;
@@ -607,7 +616,7 @@ var init_config_memory_schema = __esm({
607
616
  message: "memory.recall.embedEndpoint must be an http(s) URL"
608
617
  }).optional(),
609
618
  embedModel: z.string().min(1).optional()
610
- }).default({}),
619
+ }).prefault({}),
611
620
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
612
621
  // When a new high-value memory (decision/correction) is written, find
613
622
  // semantically-related existing records and, if the new one contradicts an
@@ -636,7 +645,7 @@ var init_config_memory_schema = __esm({
636
645
  annotateSuperseded: z.boolean().default(false),
637
646
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
638
647
  budgetMs: z.number().int().positive().default(800)
639
- }).default({}),
648
+ }).prefault({}),
640
649
  // --- Background consolidation, the "sleep-time" pass
641
650
  // (plan-living-memory-slice-3-consolidation) ---
642
651
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -682,7 +691,7 @@ var init_config_memory_schema = __esm({
682
691
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
683
692
  suggestUpgrades: z.boolean().default(true),
684
693
  suggestIntervalDays: z.number().int().positive().default(30)
685
- }).default({}),
694
+ }).prefault({}),
686
695
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
687
696
  // Massu may ever WRITE into the user's memory directory.
688
697
  //
@@ -707,7 +716,7 @@ var init_config_memory_schema = __esm({
707
716
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
708
717
  // per-session cap bounds only the RATE, never the total.
709
718
  indexMaxLines: z.number().int().min(1).default(50)
710
- }).default({}),
719
+ }).prefault({}),
711
720
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
712
721
  // A decision made in one of your repos can surface in another — opt-in per
713
722
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -732,8 +741,8 @@ var init_config_memory_schema = __esm({
732
741
  enabled: z.boolean().default(true),
733
742
  maxCrossRepoItems: z.number().int().min(0).default(1),
734
743
  minScore: z.number().optional()
735
- }).default({})
736
- }).default({})
744
+ }).prefault({})
745
+ }).prefault({})
737
746
  }).optional();
738
747
  }
739
748
  });
@@ -913,13 +922,19 @@ function getResolvedPaths() {
913
922
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
914
923
  };
915
924
  }
916
- var DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
925
+ var DEFAULT_KNOWLEDGE_SOURCE_FILES, DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
917
926
  var init_config = __esm({
918
927
  "src/config.ts"() {
919
928
  "use strict";
920
929
  init_memory_path();
930
+ init_corrections_md();
921
931
  init_credentials();
922
932
  init_config_memory_schema();
933
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
934
+ "CLAUDE.md",
935
+ "MEMORY.md",
936
+ CORRECTIONS_FILENAME
937
+ ];
923
938
  DomainConfigSchema = z2.object({
924
939
  name: z2.string().default("Unknown"),
925
940
  routers: z2.array(z2.string()).default([]),
@@ -1048,34 +1063,40 @@ var init_config = __esm({
1048
1063
  fixDetection: z2.object({
1049
1064
  enabled: z2.boolean().default(true),
1050
1065
  lookbackDays: z2.number().default(7),
1051
- signals: z2.array(z2.string()).default([
1052
- "removed_broken_code",
1053
- "added_error_handling",
1054
- "method_name_correction",
1055
- "auth_fix",
1056
- "nil_handling_fix",
1057
- "concurrency_fix",
1058
- "async_pattern_fix",
1059
- "added_missing_import"
1060
- ])
1061
- }).default({}),
1066
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
1067
+ //
1068
+ // This carried a hand-typed copy of all eight signal names while
1069
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
1070
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
1071
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
1072
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
1073
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
1074
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
1075
+ // one site of two (CR-74).
1076
+ //
1077
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
1078
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
1079
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
1080
+ // consumer and no config file in the repo sets it.
1081
+ signals: z2.array(z2.string()).optional()
1082
+ }).prefault({}),
1062
1083
  failureClassification: z2.object({
1063
1084
  enabled: z2.boolean().default(true),
1064
1085
  thresholds: z2.object({
1065
1086
  known: z2.number().default(5),
1066
1087
  similar: z2.number().default(3)
1067
- }).default({}),
1088
+ }).prefault({}),
1068
1089
  scoring: z2.object({
1069
1090
  diffPatternWeight: z2.number().default(3),
1070
1091
  filePatternWeight: z2.number().default(2),
1071
1092
  promptKeywordWeight: z2.number().default(2)
1072
- }).default({})
1073
- }).default({}),
1093
+ }).prefault({})
1094
+ }).prefault({}),
1074
1095
  pipeline: z2.object({
1075
1096
  requireIncidentReport: z2.boolean().default(true),
1076
1097
  requirePreventionRule: z2.boolean().default(true),
1077
1098
  requireEnforcement: z2.boolean().default(true)
1078
- }).default({}),
1099
+ }).prefault({}),
1079
1100
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
1080
1101
  // custom destinations for the rule-candidate funnel. The classifier matches
1081
1102
  // a candidate to one of these entries when none of the framework
@@ -1108,7 +1129,7 @@ var init_config = __esm({
1108
1129
  memory: z2.boolean().default(true),
1109
1130
  analytics: z2.boolean().default(true),
1110
1131
  audit: z2.boolean().default(true)
1111
- }).default({ memory: true, analytics: true, audit: true })
1132
+ }).prefault({ memory: true, analytics: true, audit: true })
1112
1133
  }).optional();
1113
1134
  ConventionsConfigSchema = z2.object({
1114
1135
  claudeDirName: z2.string().default(".claude").refine(
@@ -1139,7 +1160,7 @@ var init_config = __esm({
1139
1160
  "session-state",
1140
1161
  "agents"
1141
1162
  ]),
1142
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
1163
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
1143
1164
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
1144
1165
  }).optional();
1145
1166
  PythonDomainConfigSchema = z2.object({
@@ -1155,6 +1176,23 @@ var init_config = __esm({
1155
1176
  }).optional();
1156
1177
  PathsConfigSchema = z2.object({
1157
1178
  source: z2.string().default("src"),
1179
+ /**
1180
+ * Where this stack's tests live, relative to the project root.
1181
+ *
1182
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
1183
+ * ~25 shipped assets told every customer their tests belong in
1184
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
1185
+ * the alternative because none existed. A test directory is a first-class property of
1186
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
1187
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
1188
+ * rather than in prose.
1189
+ *
1190
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
1191
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
1192
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
1193
+ * and `stack-template-conformance.test.ts` asserts that.
1194
+ */
1195
+ tests: z2.string().optional(),
1158
1196
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
1159
1197
  monorepo_roots: z2.array(z2.string()).optional(),
1160
1198
  routers: z2.string().optional(),
@@ -1264,14 +1302,14 @@ var init_config = __esm({
1264
1302
  project: z2.object({
1265
1303
  name: z2.string().default("my-project"),
1266
1304
  root: z2.string().default("auto")
1267
- }).default({ name: "my-project", root: "auto" }),
1268
- framework: FrameworkConfigSchema.default({
1305
+ }).prefault({ name: "my-project", root: "auto" }),
1306
+ framework: FrameworkConfigSchema.prefault({
1269
1307
  type: "typescript",
1270
1308
  router: "none",
1271
1309
  orm: "none",
1272
1310
  ui: "none"
1273
1311
  }),
1274
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
1312
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
1275
1313
  toolPrefix: z2.string().default("massu"),
1276
1314
  dbAccessPattern: z2.string().optional(),
1277
1315
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -580,6 +580,15 @@ var init_memory_path = __esm({
580
580
  }
581
581
  });
582
582
 
583
+ // src/lib/corrections-md.ts
584
+ var CORRECTIONS_FILENAME;
585
+ var init_corrections_md = __esm({
586
+ "src/lib/corrections-md.ts"() {
587
+ "use strict";
588
+ CORRECTIONS_FILENAME = "corrections.md";
589
+ }
590
+ });
591
+
583
592
  // src/config-memory-schema.ts
584
593
  import { z } from "zod";
585
594
  var MemoryConfigSchema;
@@ -613,7 +622,7 @@ var init_config_memory_schema = __esm({
613
622
  message: "memory.recall.embedEndpoint must be an http(s) URL"
614
623
  }).optional(),
615
624
  embedModel: z.string().min(1).optional()
616
- }).default({}),
625
+ }).prefault({}),
617
626
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
618
627
  // When a new high-value memory (decision/correction) is written, find
619
628
  // semantically-related existing records and, if the new one contradicts an
@@ -642,7 +651,7 @@ var init_config_memory_schema = __esm({
642
651
  annotateSuperseded: z.boolean().default(false),
643
652
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
644
653
  budgetMs: z.number().int().positive().default(800)
645
- }).default({}),
654
+ }).prefault({}),
646
655
  // --- Background consolidation, the "sleep-time" pass
647
656
  // (plan-living-memory-slice-3-consolidation) ---
648
657
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -688,7 +697,7 @@ var init_config_memory_schema = __esm({
688
697
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
689
698
  suggestUpgrades: z.boolean().default(true),
690
699
  suggestIntervalDays: z.number().int().positive().default(30)
691
- }).default({}),
700
+ }).prefault({}),
692
701
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
693
702
  // Massu may ever WRITE into the user's memory directory.
694
703
  //
@@ -713,7 +722,7 @@ var init_config_memory_schema = __esm({
713
722
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
714
723
  // per-session cap bounds only the RATE, never the total.
715
724
  indexMaxLines: z.number().int().min(1).default(50)
716
- }).default({}),
725
+ }).prefault({}),
717
726
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
718
727
  // A decision made in one of your repos can surface in another — opt-in per
719
728
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -738,8 +747,8 @@ var init_config_memory_schema = __esm({
738
747
  enabled: z.boolean().default(true),
739
748
  maxCrossRepoItems: z.number().int().min(0).default(1),
740
749
  minScore: z.number().optional()
741
- }).default({})
742
- }).default({})
750
+ }).prefault({})
751
+ }).prefault({})
743
752
  }).optional();
744
753
  }
745
754
  });
@@ -919,13 +928,19 @@ function getResolvedPaths() {
919
928
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
920
929
  };
921
930
  }
922
- var DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
931
+ var DEFAULT_KNOWLEDGE_SOURCE_FILES, DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
923
932
  var init_config = __esm({
924
933
  "src/config.ts"() {
925
934
  "use strict";
926
935
  init_memory_path();
936
+ init_corrections_md();
927
937
  init_credentials();
928
938
  init_config_memory_schema();
939
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
940
+ "CLAUDE.md",
941
+ "MEMORY.md",
942
+ CORRECTIONS_FILENAME
943
+ ];
929
944
  DomainConfigSchema = z2.object({
930
945
  name: z2.string().default("Unknown"),
931
946
  routers: z2.array(z2.string()).default([]),
@@ -1054,34 +1069,40 @@ var init_config = __esm({
1054
1069
  fixDetection: z2.object({
1055
1070
  enabled: z2.boolean().default(true),
1056
1071
  lookbackDays: z2.number().default(7),
1057
- signals: z2.array(z2.string()).default([
1058
- "removed_broken_code",
1059
- "added_error_handling",
1060
- "method_name_correction",
1061
- "auth_fix",
1062
- "nil_handling_fix",
1063
- "concurrency_fix",
1064
- "async_pattern_fix",
1065
- "added_missing_import"
1066
- ])
1067
- }).default({}),
1072
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
1073
+ //
1074
+ // This carried a hand-typed copy of all eight signal names while
1075
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
1076
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
1077
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
1078
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
1079
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
1080
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
1081
+ // one site of two (CR-74).
1082
+ //
1083
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
1084
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
1085
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
1086
+ // consumer and no config file in the repo sets it.
1087
+ signals: z2.array(z2.string()).optional()
1088
+ }).prefault({}),
1068
1089
  failureClassification: z2.object({
1069
1090
  enabled: z2.boolean().default(true),
1070
1091
  thresholds: z2.object({
1071
1092
  known: z2.number().default(5),
1072
1093
  similar: z2.number().default(3)
1073
- }).default({}),
1094
+ }).prefault({}),
1074
1095
  scoring: z2.object({
1075
1096
  diffPatternWeight: z2.number().default(3),
1076
1097
  filePatternWeight: z2.number().default(2),
1077
1098
  promptKeywordWeight: z2.number().default(2)
1078
- }).default({})
1079
- }).default({}),
1099
+ }).prefault({})
1100
+ }).prefault({}),
1080
1101
  pipeline: z2.object({
1081
1102
  requireIncidentReport: z2.boolean().default(true),
1082
1103
  requirePreventionRule: z2.boolean().default(true),
1083
1104
  requireEnforcement: z2.boolean().default(true)
1084
- }).default({}),
1105
+ }).prefault({}),
1085
1106
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
1086
1107
  // custom destinations for the rule-candidate funnel. The classifier matches
1087
1108
  // a candidate to one of these entries when none of the framework
@@ -1114,7 +1135,7 @@ var init_config = __esm({
1114
1135
  memory: z2.boolean().default(true),
1115
1136
  analytics: z2.boolean().default(true),
1116
1137
  audit: z2.boolean().default(true)
1117
- }).default({ memory: true, analytics: true, audit: true })
1138
+ }).prefault({ memory: true, analytics: true, audit: true })
1118
1139
  }).optional();
1119
1140
  ConventionsConfigSchema = z2.object({
1120
1141
  claudeDirName: z2.string().default(".claude").refine(
@@ -1145,7 +1166,7 @@ var init_config = __esm({
1145
1166
  "session-state",
1146
1167
  "agents"
1147
1168
  ]),
1148
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
1169
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
1149
1170
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
1150
1171
  }).optional();
1151
1172
  PythonDomainConfigSchema = z2.object({
@@ -1161,6 +1182,23 @@ var init_config = __esm({
1161
1182
  }).optional();
1162
1183
  PathsConfigSchema = z2.object({
1163
1184
  source: z2.string().default("src"),
1185
+ /**
1186
+ * Where this stack's tests live, relative to the project root.
1187
+ *
1188
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
1189
+ * ~25 shipped assets told every customer their tests belong in
1190
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
1191
+ * the alternative because none existed. A test directory is a first-class property of
1192
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
1193
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
1194
+ * rather than in prose.
1195
+ *
1196
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
1197
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
1198
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
1199
+ * and `stack-template-conformance.test.ts` asserts that.
1200
+ */
1201
+ tests: z2.string().optional(),
1164
1202
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
1165
1203
  monorepo_roots: z2.array(z2.string()).optional(),
1166
1204
  routers: z2.string().optional(),
@@ -1270,14 +1308,14 @@ var init_config = __esm({
1270
1308
  project: z2.object({
1271
1309
  name: z2.string().default("my-project"),
1272
1310
  root: z2.string().default("auto")
1273
- }).default({ name: "my-project", root: "auto" }),
1274
- framework: FrameworkConfigSchema.default({
1311
+ }).prefault({ name: "my-project", root: "auto" }),
1312
+ framework: FrameworkConfigSchema.prefault({
1275
1313
  type: "typescript",
1276
1314
  router: "none",
1277
1315
  orm: "none",
1278
1316
  ui: "none"
1279
1317
  }),
1280
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
1318
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
1281
1319
  toolPrefix: z2.string().default("massu"),
1282
1320
  dbAccessPattern: z2.string().optional(),
1283
1321
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -4182,7 +4220,7 @@ function classifyToolCall(tc) {
4182
4220
  }
4183
4221
  case "Read": {
4184
4222
  const filePath = tc.input.file_path ?? "unknown";
4185
- const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? ["CLAUDE.md", "MEMORY.md", "corrections.md"];
4223
+ const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? DEFAULT_KNOWLEDGE_SOURCE_FILES;
4186
4224
  const plansDir = getResolvedPaths().plansDir;
4187
4225
  if (filePath.includes(plansDir) || knowledgeSourceFiles.some((f) => filePath.includes(f))) {
4188
4226
  const title = `Read: ${shortenPath(filePath)}`;
@@ -4323,6 +4361,9 @@ function detectPlanProgress(rawToolResponse) {
4323
4361
  return results;
4324
4362
  }
4325
4363
 
4364
+ // src/hooks/post-tool-use.ts
4365
+ init_config();
4366
+
4326
4367
  // src/audit-trail.ts
4327
4368
  init_config();
4328
4369
  function logAuditEntry(db, entry) {
@@ -5341,7 +5382,7 @@ var _cachedConventions = null;
5341
5382
  var _cachedConventionsPath = null;
5342
5383
  var _cachedConventionsMtimeMs = 0;
5343
5384
  var _conventionDefaults = {
5344
- knowledgeSourceFiles: ["CLAUDE.md", "MEMORY.md", "corrections.md"],
5385
+ knowledgeSourceFiles: [...DEFAULT_KNOWLEDGE_SOURCE_FILES],
5345
5386
  claudeDirName: ".claude"
5346
5387
  };
5347
5388
  function readConventions(cwd) {
@@ -574,6 +574,15 @@ var init_memory_path = __esm({
574
574
  }
575
575
  });
576
576
 
577
+ // src/lib/corrections-md.ts
578
+ var CORRECTIONS_FILENAME;
579
+ var init_corrections_md = __esm({
580
+ "src/lib/corrections-md.ts"() {
581
+ "use strict";
582
+ CORRECTIONS_FILENAME = "corrections.md";
583
+ }
584
+ });
585
+
577
586
  // src/config-memory-schema.ts
578
587
  import { z } from "zod";
579
588
  var MemoryConfigSchema;
@@ -607,7 +616,7 @@ var init_config_memory_schema = __esm({
607
616
  message: "memory.recall.embedEndpoint must be an http(s) URL"
608
617
  }).optional(),
609
618
  embedModel: z.string().min(1).optional()
610
- }).default({}),
619
+ }).prefault({}),
611
620
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
612
621
  // When a new high-value memory (decision/correction) is written, find
613
622
  // semantically-related existing records and, if the new one contradicts an
@@ -636,7 +645,7 @@ var init_config_memory_schema = __esm({
636
645
  annotateSuperseded: z.boolean().default(false),
637
646
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
638
647
  budgetMs: z.number().int().positive().default(800)
639
- }).default({}),
648
+ }).prefault({}),
640
649
  // --- Background consolidation, the "sleep-time" pass
641
650
  // (plan-living-memory-slice-3-consolidation) ---
642
651
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -682,7 +691,7 @@ var init_config_memory_schema = __esm({
682
691
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
683
692
  suggestUpgrades: z.boolean().default(true),
684
693
  suggestIntervalDays: z.number().int().positive().default(30)
685
- }).default({}),
694
+ }).prefault({}),
686
695
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
687
696
  // Massu may ever WRITE into the user's memory directory.
688
697
  //
@@ -707,7 +716,7 @@ var init_config_memory_schema = __esm({
707
716
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
708
717
  // per-session cap bounds only the RATE, never the total.
709
718
  indexMaxLines: z.number().int().min(1).default(50)
710
- }).default({}),
719
+ }).prefault({}),
711
720
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
712
721
  // A decision made in one of your repos can surface in another — opt-in per
713
722
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -732,8 +741,8 @@ var init_config_memory_schema = __esm({
732
741
  enabled: z.boolean().default(true),
733
742
  maxCrossRepoItems: z.number().int().min(0).default(1),
734
743
  minScore: z.number().optional()
735
- }).default({})
736
- }).default({})
744
+ }).prefault({})
745
+ }).prefault({})
737
746
  }).optional();
738
747
  }
739
748
  });
@@ -913,13 +922,19 @@ function getResolvedPaths() {
913
922
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
914
923
  };
915
924
  }
916
- var DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
925
+ var DEFAULT_KNOWLEDGE_SOURCE_FILES, DomainConfigSchema, PatternRuleConfigSchema, CostModelSchema, AnalyticsConfigSchema, CustomPatternSchema, GovernanceConfigSchema, SecurityPatternSchema, SecurityConfigSchema, TeamConfigSchema, RegressionConfigSchema, AutoLearningConfigSchema, CloudConfigSchema, ConventionsConfigSchema, PythonDomainConfigSchema, PythonConfigSchema, PathsConfigSchema, LanguageFrameworkEntrySchema, FrameworkConfigSchema, DetectedConfigSchema, VerificationEntrySchema, VerificationConfigSchema, CanonicalPathsSchema, VerificationTypesSchema, DetectionRuleEntrySchema, DetectionConfigSchema, WatchConfigSchema, AdapterLocalPathSchema, AdaptersConfigSchema, TelemetryConfigSchema, LSPConfigSchema, RawConfigSchema, _config, _resolvedApiKeySource, _projectRoot;
917
926
  var init_config = __esm({
918
927
  "src/config.ts"() {
919
928
  "use strict";
920
929
  init_memory_path();
930
+ init_corrections_md();
921
931
  init_credentials();
922
932
  init_config_memory_schema();
933
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
934
+ "CLAUDE.md",
935
+ "MEMORY.md",
936
+ CORRECTIONS_FILENAME
937
+ ];
923
938
  DomainConfigSchema = z2.object({
924
939
  name: z2.string().default("Unknown"),
925
940
  routers: z2.array(z2.string()).default([]),
@@ -1048,34 +1063,40 @@ var init_config = __esm({
1048
1063
  fixDetection: z2.object({
1049
1064
  enabled: z2.boolean().default(true),
1050
1065
  lookbackDays: z2.number().default(7),
1051
- signals: z2.array(z2.string()).default([
1052
- "removed_broken_code",
1053
- "added_error_handling",
1054
- "method_name_correction",
1055
- "auth_fix",
1056
- "nil_handling_fix",
1057
- "concurrency_fix",
1058
- "async_pattern_fix",
1059
- "added_missing_import"
1060
- ])
1061
- }).default({}),
1066
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
1067
+ //
1068
+ // This carried a hand-typed copy of all eight signal names while
1069
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
1070
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
1071
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
1072
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
1073
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
1074
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
1075
+ // one site of two (CR-74).
1076
+ //
1077
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
1078
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
1079
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
1080
+ // consumer and no config file in the repo sets it.
1081
+ signals: z2.array(z2.string()).optional()
1082
+ }).prefault({}),
1062
1083
  failureClassification: z2.object({
1063
1084
  enabled: z2.boolean().default(true),
1064
1085
  thresholds: z2.object({
1065
1086
  known: z2.number().default(5),
1066
1087
  similar: z2.number().default(3)
1067
- }).default({}),
1088
+ }).prefault({}),
1068
1089
  scoring: z2.object({
1069
1090
  diffPatternWeight: z2.number().default(3),
1070
1091
  filePatternWeight: z2.number().default(2),
1071
1092
  promptKeywordWeight: z2.number().default(2)
1072
- }).default({})
1073
- }).default({}),
1093
+ }).prefault({})
1094
+ }).prefault({}),
1074
1095
  pipeline: z2.object({
1075
1096
  requireIncidentReport: z2.boolean().default(true),
1076
1097
  requirePreventionRule: z2.boolean().default(true),
1077
1098
  requireEnforcement: z2.boolean().default(true)
1078
- }).default({}),
1099
+ }).prefault({}),
1079
1100
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
1080
1101
  // custom destinations for the rule-candidate funnel. The classifier matches
1081
1102
  // a candidate to one of these entries when none of the framework
@@ -1108,7 +1129,7 @@ var init_config = __esm({
1108
1129
  memory: z2.boolean().default(true),
1109
1130
  analytics: z2.boolean().default(true),
1110
1131
  audit: z2.boolean().default(true)
1111
- }).default({ memory: true, analytics: true, audit: true })
1132
+ }).prefault({ memory: true, analytics: true, audit: true })
1112
1133
  }).optional();
1113
1134
  ConventionsConfigSchema = z2.object({
1114
1135
  claudeDirName: z2.string().default(".claude").refine(
@@ -1139,7 +1160,7 @@ var init_config = __esm({
1139
1160
  "session-state",
1140
1161
  "agents"
1141
1162
  ]),
1142
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
1163
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
1143
1164
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
1144
1165
  }).optional();
1145
1166
  PythonDomainConfigSchema = z2.object({
@@ -1155,6 +1176,23 @@ var init_config = __esm({
1155
1176
  }).optional();
1156
1177
  PathsConfigSchema = z2.object({
1157
1178
  source: z2.string().default("src"),
1179
+ /**
1180
+ * Where this stack's tests live, relative to the project root.
1181
+ *
1182
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
1183
+ * ~25 shipped assets told every customer their tests belong in
1184
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
1185
+ * the alternative because none existed. A test directory is a first-class property of
1186
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
1187
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
1188
+ * rather than in prose.
1189
+ *
1190
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
1191
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
1192
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
1193
+ * and `stack-template-conformance.test.ts` asserts that.
1194
+ */
1195
+ tests: z2.string().optional(),
1158
1196
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
1159
1197
  monorepo_roots: z2.array(z2.string()).optional(),
1160
1198
  routers: z2.string().optional(),
@@ -1264,14 +1302,14 @@ var init_config = __esm({
1264
1302
  project: z2.object({
1265
1303
  name: z2.string().default("my-project"),
1266
1304
  root: z2.string().default("auto")
1267
- }).default({ name: "my-project", root: "auto" }),
1268
- framework: FrameworkConfigSchema.default({
1305
+ }).prefault({ name: "my-project", root: "auto" }),
1306
+ framework: FrameworkConfigSchema.prefault({
1269
1307
  type: "typescript",
1270
1308
  router: "none",
1271
1309
  orm: "none",
1272
1310
  ui: "none"
1273
1311
  }),
1274
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
1312
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
1275
1313
  toolPrefix: z2.string().default("massu"),
1276
1314
  dbAccessPattern: z2.string().optional(),
1277
1315
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),