@massu/core 2.5.1 → 2.5.3

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 (92) hide show
  1. package/CHANGELOG.md +240 -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 +1731 -976
  53. package/dist/hooks/auto-learning-pipeline.js +745 -87
  54. package/dist/hooks/classify-failure.js +747 -45
  55. package/dist/hooks/cost-tracker.js +89 -28
  56. package/dist/hooks/fix-detector.js +536 -48
  57. package/dist/hooks/incident-pipeline.js +96 -29
  58. package/dist/hooks/intent-suggester.js +89 -28
  59. package/dist/hooks/memory-recall.js +89 -28
  60. package/dist/hooks/post-edit-context.js +137 -29
  61. package/dist/hooks/post-tool-use.js +95 -30
  62. package/dist/hooks/pre-compact.js +89 -28
  63. package/dist/hooks/pre-delete-check.js +137 -31
  64. package/dist/hooks/pre-tool-use-gate.js +271 -43
  65. package/dist/hooks/quality-event.js +89 -28
  66. package/dist/hooks/rule-enforcement-pipeline.js +89 -28
  67. package/dist/hooks/security-gate.js +219 -39
  68. package/dist/hooks/session-end.js +92 -28
  69. package/dist/hooks/session-start.js +140 -62
  70. package/dist/hooks/user-prompt.js +89 -28
  71. package/fragments/feature-components.md +15 -0
  72. package/fragments/feature-components.python.md +15 -0
  73. package/fragments/feature-components.typescript.md +15 -0
  74. package/package.json +6 -5
  75. package/patterns/testing-patterns.md +8 -8
  76. package/protocols/plan-implementation.md +2 -2
  77. package/protocols/verification.md +4 -4
  78. package/reference/lessons-learned.md +4 -4
  79. package/reference/vr-verification-reference.md +9 -9
  80. package/templates/aspnet/massu.config.yaml +2 -0
  81. package/templates/go-chi/massu.config.yaml +2 -0
  82. package/templates/multi-runtime/massu.config.yaml +4 -2
  83. package/templates/phoenix/massu.config.yaml +2 -0
  84. package/templates/python-django/massu.config.yaml +3 -1
  85. package/templates/python-fastapi/massu.config.yaml +3 -1
  86. package/templates/python-flask/massu.config.yaml +3 -1
  87. package/templates/rails/massu.config.yaml +4 -2
  88. package/templates/rust-actix/massu.config.yaml +2 -0
  89. package/templates/spring/massu.config.yaml +2 -0
  90. package/templates/swift-ios/massu.config.yaml +3 -1
  91. package/templates/ts-nestjs/massu.config.yaml +2 -0
  92. package/templates/ts-nextjs/massu.config.yaml +2 -0
@@ -597,6 +597,40 @@ var init_memory_path = __esm({
597
597
  }
598
598
  });
599
599
 
600
+ // src/lib/corrections-md.ts
601
+ function parseCorrectionRules(content) {
602
+ const rules = [];
603
+ for (const raw of content.split("\n")) {
604
+ const line = raw.trim();
605
+ const m = line.match(CORRECTION_RULE_BULLET);
606
+ if (m && m[1].trim()) {
607
+ rules.push(m[1].trim());
608
+ continue;
609
+ }
610
+ if (line.startsWith("|") && line.endsWith("|")) {
611
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
612
+ if (cells.length < 4) continue;
613
+ if (cells[0] === "Date" || cells[0].startsWith("-")) continue;
614
+ const prevention = cells[3];
615
+ if (prevention && !prevention.startsWith("-") && !prevention.startsWith("<!--")) {
616
+ rules.push(prevention);
617
+ }
618
+ }
619
+ }
620
+ return rules;
621
+ }
622
+ function correctionsPath(memoryDir) {
623
+ return memoryDir.endsWith("/") ? `${memoryDir}${CORRECTIONS_FILENAME}` : `${memoryDir}/${CORRECTIONS_FILENAME}`;
624
+ }
625
+ var CORRECTION_RULE_BULLET, CORRECTIONS_FILENAME;
626
+ var init_corrections_md = __esm({
627
+ "src/lib/corrections-md.ts"() {
628
+ "use strict";
629
+ CORRECTION_RULE_BULLET = /^-\s+\*\*Rule\*\*:\s*(.+)$/;
630
+ CORRECTIONS_FILENAME = "corrections.md";
631
+ }
632
+ });
633
+
600
634
  // src/config-memory-schema.ts
601
635
  import { z } from "zod";
602
636
  var MemoryConfigSchema;
@@ -630,7 +664,7 @@ var init_config_memory_schema = __esm({
630
664
  message: "memory.recall.embedEndpoint must be an http(s) URL"
631
665
  }).optional(),
632
666
  embedModel: z.string().min(1).optional()
633
- }).default({}),
667
+ }).prefault({}),
634
668
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
635
669
  // When a new high-value memory (decision/correction) is written, find
636
670
  // semantically-related existing records and, if the new one contradicts an
@@ -659,7 +693,7 @@ var init_config_memory_schema = __esm({
659
693
  annotateSuperseded: z.boolean().default(false),
660
694
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
661
695
  budgetMs: z.number().int().positive().default(800)
662
- }).default({}),
696
+ }).prefault({}),
663
697
  // --- Background consolidation, the "sleep-time" pass
664
698
  // (plan-living-memory-slice-3-consolidation) ---
665
699
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -705,7 +739,7 @@ var init_config_memory_schema = __esm({
705
739
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
706
740
  suggestUpgrades: z.boolean().default(true),
707
741
  suggestIntervalDays: z.number().int().positive().default(30)
708
- }).default({}),
742
+ }).prefault({}),
709
743
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
710
744
  // Massu may ever WRITE into the user's memory directory.
711
745
  //
@@ -730,7 +764,7 @@ var init_config_memory_schema = __esm({
730
764
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
731
765
  // per-session cap bounds only the RATE, never the total.
732
766
  indexMaxLines: z.number().int().min(1).default(50)
733
- }).default({}),
767
+ }).prefault({}),
734
768
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
735
769
  // A decision made in one of your repos can surface in another — opt-in per
736
770
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -755,8 +789,8 @@ var init_config_memory_schema = __esm({
755
789
  enabled: z.boolean().default(true),
756
790
  maxCrossRepoItems: z.number().int().min(0).default(1),
757
791
  minScore: z.number().optional()
758
- }).default({})
759
- }).default({})
792
+ }).prefault({})
793
+ }).prefault({})
760
794
  }).optional();
761
795
  }
762
796
  });
@@ -936,13 +970,19 @@ function getResolvedPaths() {
936
970
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
937
971
  };
938
972
  }
939
- 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;
973
+ 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;
940
974
  var init_config = __esm({
941
975
  "src/config.ts"() {
942
976
  "use strict";
943
977
  init_memory_path();
978
+ init_corrections_md();
944
979
  init_credentials();
945
980
  init_config_memory_schema();
981
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
982
+ "CLAUDE.md",
983
+ "MEMORY.md",
984
+ CORRECTIONS_FILENAME
985
+ ];
946
986
  DomainConfigSchema = z2.object({
947
987
  name: z2.string().default("Unknown"),
948
988
  routers: z2.array(z2.string()).default([]),
@@ -1071,34 +1111,40 @@ var init_config = __esm({
1071
1111
  fixDetection: z2.object({
1072
1112
  enabled: z2.boolean().default(true),
1073
1113
  lookbackDays: z2.number().default(7),
1074
- signals: z2.array(z2.string()).default([
1075
- "removed_broken_code",
1076
- "added_error_handling",
1077
- "method_name_correction",
1078
- "auth_fix",
1079
- "nil_handling_fix",
1080
- "concurrency_fix",
1081
- "async_pattern_fix",
1082
- "added_missing_import"
1083
- ])
1084
- }).default({}),
1114
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
1115
+ //
1116
+ // This carried a hand-typed copy of all eight signal names while
1117
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
1118
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
1119
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
1120
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
1121
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
1122
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
1123
+ // one site of two (CR-74).
1124
+ //
1125
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
1126
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
1127
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
1128
+ // consumer and no config file in the repo sets it.
1129
+ signals: z2.array(z2.string()).optional()
1130
+ }).prefault({}),
1085
1131
  failureClassification: z2.object({
1086
1132
  enabled: z2.boolean().default(true),
1087
1133
  thresholds: z2.object({
1088
1134
  known: z2.number().default(5),
1089
1135
  similar: z2.number().default(3)
1090
- }).default({}),
1136
+ }).prefault({}),
1091
1137
  scoring: z2.object({
1092
1138
  diffPatternWeight: z2.number().default(3),
1093
1139
  filePatternWeight: z2.number().default(2),
1094
1140
  promptKeywordWeight: z2.number().default(2)
1095
- }).default({})
1096
- }).default({}),
1141
+ }).prefault({})
1142
+ }).prefault({}),
1097
1143
  pipeline: z2.object({
1098
1144
  requireIncidentReport: z2.boolean().default(true),
1099
1145
  requirePreventionRule: z2.boolean().default(true),
1100
1146
  requireEnforcement: z2.boolean().default(true)
1101
- }).default({}),
1147
+ }).prefault({}),
1102
1148
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
1103
1149
  // custom destinations for the rule-candidate funnel. The classifier matches
1104
1150
  // a candidate to one of these entries when none of the framework
@@ -1131,7 +1177,7 @@ var init_config = __esm({
1131
1177
  memory: z2.boolean().default(true),
1132
1178
  analytics: z2.boolean().default(true),
1133
1179
  audit: z2.boolean().default(true)
1134
- }).default({ memory: true, analytics: true, audit: true })
1180
+ }).prefault({ memory: true, analytics: true, audit: true })
1135
1181
  }).optional();
1136
1182
  ConventionsConfigSchema = z2.object({
1137
1183
  claudeDirName: z2.string().default(".claude").refine(
@@ -1162,7 +1208,7 @@ var init_config = __esm({
1162
1208
  "session-state",
1163
1209
  "agents"
1164
1210
  ]),
1165
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
1211
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
1166
1212
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
1167
1213
  }).optional();
1168
1214
  PythonDomainConfigSchema = z2.object({
@@ -1178,6 +1224,23 @@ var init_config = __esm({
1178
1224
  }).optional();
1179
1225
  PathsConfigSchema = z2.object({
1180
1226
  source: z2.string().default("src"),
1227
+ /**
1228
+ * Where this stack's tests live, relative to the project root.
1229
+ *
1230
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
1231
+ * ~25 shipped assets told every customer their tests belong in
1232
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
1233
+ * the alternative because none existed. A test directory is a first-class property of
1234
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
1235
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
1236
+ * rather than in prose.
1237
+ *
1238
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
1239
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
1240
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
1241
+ * and `stack-template-conformance.test.ts` asserts that.
1242
+ */
1243
+ tests: z2.string().optional(),
1181
1244
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
1182
1245
  monorepo_roots: z2.array(z2.string()).optional(),
1183
1246
  routers: z2.string().optional(),
@@ -1190,6 +1253,13 @@ var init_config = __esm({
1190
1253
  });
1191
1254
  LanguageFrameworkEntrySchema = z2.object({
1192
1255
  framework: z2.string().optional(),
1256
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1257
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1258
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1259
+ // derives the index builders' candidate set from it, so it is typed here rather than
1260
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1261
+ // from silently contributing `undefined` to a path predicate.
1262
+ source_dirs: z2.array(z2.string()).optional(),
1193
1263
  test_framework: z2.string().optional(),
1194
1264
  test: z2.string().optional(),
1195
1265
  runtime: z2.string().optional(),
@@ -1280,14 +1350,14 @@ var init_config = __esm({
1280
1350
  project: z2.object({
1281
1351
  name: z2.string().default("my-project"),
1282
1352
  root: z2.string().default("auto")
1283
- }).default({ name: "my-project", root: "auto" }),
1284
- framework: FrameworkConfigSchema.default({
1353
+ }).prefault({ name: "my-project", root: "auto" }),
1354
+ framework: FrameworkConfigSchema.prefault({
1285
1355
  type: "typescript",
1286
1356
  router: "none",
1287
1357
  orm: "none",
1288
1358
  ui: "none"
1289
1359
  }),
1290
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
1360
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
1291
1361
  toolPrefix: z2.string().default("massu"),
1292
1362
  dbAccessPattern: z2.string().optional(),
1293
1363
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -12515,7 +12585,11 @@ function defaultsFor(language, fw, dir) {
12515
12585
  test: testFw === "unittest" ? prefix(dir, "python3 -m unittest") : prefix(dir, "python3 -m pytest -q"),
12516
12586
  type: prefix(dir, "python3 -m mypy ."),
12517
12587
  build: null,
12518
- syntax: prefix(dir, "python3 -m py_compile"),
12588
+ // `python3 -m py_compile` with NO filenames exits 2 ('the following arguments are
12589
+ // required: filenames') — a check that can never pass, i.e. a brick, and a brick
12590
+ // gets bypassed. `compileall -q .` measured exit 1 on a tree with a syntax error
12591
+ // and exit 0 on a clean tree (2026-09-20).
12592
+ syntax: prefix(dir, "python3 -m compileall -q ."),
12519
12593
  lint: prefix(dir, "python3 -m ruff check .")
12520
12594
  };
12521
12595
  }
@@ -12553,7 +12627,12 @@ function defaultsFor(language, fw, dir) {
12553
12627
  return {
12554
12628
  test: prefix(dir, "swift test"),
12555
12629
  type: prefix(dir, "swift build"),
12556
- build: prefix(dir, "xcodebuild build"),
12630
+ // `xcodebuild build` requires an .xcodeproj/.xcworkspace, but this block's other
12631
+ // commands (`swift test`, `swift build`) and the swift-ios template's declared
12632
+ // layout (Sources/) are SwiftPM. One stack cannot be SwiftPM for two keys and
12633
+ // Xcode for a third. Operator-approved 2026-09-20; the three anti-vacuity plants
12634
+ // that target this literal were re-pointed in the same change.
12635
+ build: prefix(dir, "swift build -c release"),
12557
12636
  syntax: null,
12558
12637
  lint: prefix(dir, "swiftlint")
12559
12638
  };
@@ -12578,10 +12657,17 @@ function defaultsFor(language, fw, dir) {
12578
12657
  }
12579
12658
  case "ruby": {
12580
12659
  return {
12581
- test: prefix(dir, "bundle exec rspec"),
12660
+ // Minitest is Rails' default; `bin/rails test` works on a fresh `rails new`.
12661
+ // `bundle exec rspec` requires adding rspec-rails to the Gemfile first.
12662
+ // https://guides.rubyonrails.org/testing.html
12663
+ test: prefix(dir, "bin/rails test"),
12582
12664
  type: null,
12583
12665
  build: null,
12584
- syntax: prefix(dir, "ruby -c"),
12666
+ // `ruby -c` with NO file argument reads STDIN: on empty stdin it prints
12667
+ // 'Syntax OK' and exits 0 without inspecting one project file, and it BLOCKS on a
12668
+ // TTY. A check that cannot fail is a blind gate. Measured 2026-09-20; the
12669
+ // replacement exits 1 on a tree with a syntax error and 0 on a clean one.
12670
+ syntax: prefix(dir, "find app lib -name '*.rb' -print0 | xargs -0 -n1 ruby -c > /dev/null"),
12585
12671
  lint: prefix(dir, "bundle exec rubocop")
12586
12672
  };
12587
12673
  }
@@ -13573,32 +13659,7 @@ function verifyPromotionEnvelope(payload) {
13573
13659
 
13574
13660
  // src/rule-candidate-applier.ts
13575
13661
  init_safe_write();
13576
-
13577
- // src/lib/corrections-md.ts
13578
- var CORRECTION_RULE_BULLET = /^-\s+\*\*Rule\*\*:\s*(.+)$/;
13579
- function parseCorrectionRules(content) {
13580
- const rules = [];
13581
- for (const raw of content.split("\n")) {
13582
- const line = raw.trim();
13583
- const m = line.match(CORRECTION_RULE_BULLET);
13584
- if (m && m[1].trim()) {
13585
- rules.push(m[1].trim());
13586
- continue;
13587
- }
13588
- if (line.startsWith("|") && line.endsWith("|")) {
13589
- const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
13590
- if (cells.length < 4) continue;
13591
- if (cells[0] === "Date" || cells[0].startsWith("-")) continue;
13592
- const prevention = cells[3];
13593
- if (prevention && !prevention.startsWith("-") && !prevention.startsWith("<!--")) {
13594
- rules.push(prevention);
13595
- }
13596
- }
13597
- }
13598
- return rules;
13599
- }
13600
-
13601
- // src/rule-candidate-applier.ts
13662
+ init_corrections_md();
13602
13663
  init_memory_authorship();
13603
13664
  init_memory_index_region();
13604
13665
  init_config();
@@ -13896,9 +13957,10 @@ function failSync(db, reason) {
13896
13957
  // src/hooks/session-start.ts
13897
13958
  init_memory_file_ingest();
13898
13959
  init_config();
13960
+ init_corrections_md();
13899
13961
  init_db_driver();
13900
13962
  import { readFileSync as readFileSync19, existsSync as existsSync24 } from "fs";
13901
- import { join as join18, resolve as resolve9 } from "path";
13963
+ import { resolve as resolve9 } from "path";
13902
13964
  import { parse as parseYaml4 } from "yaml";
13903
13965
 
13904
13966
  // src/capability-advisor.ts
@@ -14188,6 +14250,20 @@ function checkWindows(pid) {
14188
14250
 
14189
14251
  // src/hooks/session-start.ts
14190
14252
  init_hook_failure_signal();
14253
+
14254
+ // src/hooks/lib/is-direct-invocation.ts
14255
+ import { pathToFileURL } from "url";
14256
+ function isDirectInvocation(moduleUrl) {
14257
+ const entry = process.argv[1];
14258
+ if (!entry) return false;
14259
+ try {
14260
+ return moduleUrl === pathToFileURL(entry).href;
14261
+ } catch {
14262
+ return false;
14263
+ }
14264
+ }
14265
+
14266
+ // src/hooks/session-start.ts
14191
14267
  async function main() {
14192
14268
  try {
14193
14269
  const input = await readStdin();
@@ -14492,9 +14568,9 @@ function getMemoryDir() {
14492
14568
  }
14493
14569
  function loadCorrectionsPreventionRules() {
14494
14570
  try {
14495
- const correctionsPath = join18(getResolvedPaths().memoryDir, "corrections.md");
14496
- if (!existsSync24(correctionsPath)) return [];
14497
- return parseCorrectionRules(readFileSync19(correctionsPath, "utf-8"));
14571
+ const correctionsFile = correctionsPath(getResolvedPaths().memoryDir);
14572
+ if (!existsSync24(correctionsFile)) return [];
14573
+ return parseCorrectionRules(readFileSync19(correctionsFile, "utf-8"));
14498
14574
  } catch (_e) {
14499
14575
  return [];
14500
14576
  }
@@ -14546,7 +14622,9 @@ function formatAge(ms) {
14546
14622
  const hr = Math.round(min / 60);
14547
14623
  return `${hr}h`;
14548
14624
  }
14549
- main();
14625
+ if (isDirectInvocation(import.meta.url)) {
14626
+ main();
14627
+ }
14550
14628
  /*! Bundled license information:
14551
14629
 
14552
14630
  is-extglob/index.js:
@@ -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(),
@@ -1167,6 +1205,13 @@ var init_config = __esm({
1167
1205
  });
1168
1206
  LanguageFrameworkEntrySchema = z2.object({
1169
1207
  framework: z2.string().optional(),
1208
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1209
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1210
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1211
+ // derives the index builders' candidate set from it, so it is typed here rather than
1212
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1213
+ // from silently contributing `undefined` to a path predicate.
1214
+ source_dirs: z2.array(z2.string()).optional(),
1170
1215
  test_framework: z2.string().optional(),
1171
1216
  test: z2.string().optional(),
1172
1217
  runtime: z2.string().optional(),
@@ -1257,14 +1302,14 @@ var init_config = __esm({
1257
1302
  project: z2.object({
1258
1303
  name: z2.string().default("my-project"),
1259
1304
  root: z2.string().default("auto")
1260
- }).default({ name: "my-project", root: "auto" }),
1261
- framework: FrameworkConfigSchema.default({
1305
+ }).prefault({ name: "my-project", root: "auto" }),
1306
+ framework: FrameworkConfigSchema.prefault({
1262
1307
  type: "typescript",
1263
1308
  router: "none",
1264
1309
  orm: "none",
1265
1310
  ui: "none"
1266
1311
  }),
1267
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
1312
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
1268
1313
  toolPrefix: z2.string().default("massu"),
1269
1314
  dbAccessPattern: z2.string().optional(),
1270
1315
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -4325,6 +4370,20 @@ function entitledForTeamSharedPromotion(tier) {
4325
4370
  // src/hooks/user-prompt.ts
4326
4371
  init_rule_candidate_store();
4327
4372
  init_hook_failure_signal();
4373
+
4374
+ // src/hooks/lib/is-direct-invocation.ts
4375
+ import { pathToFileURL } from "url";
4376
+ function isDirectInvocation(moduleUrl) {
4377
+ const entry = process.argv[1];
4378
+ if (!entry) return false;
4379
+ try {
4380
+ return moduleUrl === pathToFileURL(entry).href;
4381
+ } catch {
4382
+ return false;
4383
+ }
4384
+ }
4385
+
4386
+ // src/hooks/user-prompt.ts
4328
4387
  async function main() {
4329
4388
  try {
4330
4389
  const input = await readStdin();
@@ -4601,4 +4660,6 @@ function readStdin() {
4601
4660
  setTimeout(() => resolve5(data), 3e3);
4602
4661
  });
4603
4662
  }
4604
- main();
4663
+ if (isDirectInvocation(import.meta.url)) {
4664
+ main();
4665
+ }
@@ -0,0 +1,15 @@
1
+ #### Components Needed
2
+ - [ ] The module that implements the feature, under `{{paths.source}}`
3
+ - [ ] Its public entry point, exported where this project's other modules are
4
+ - [ ] Registration with whatever dispatches to it (router, CLI, event handler, DI container)
5
+ - [ ] Input validation at the boundary
6
+ - [ ] Configuration, if the feature needs any
7
+ - [ ] Tests under `{{paths.tests | default("tests")}}`
8
+ - [ ] Documentation
9
+
10
+ #### Patterns to Apply (MANDATORY)
11
+ - [ ] Follow the registration pattern this project already uses — find the nearest
12
+ existing feature and mirror it rather than inventing a second shape
13
+ - [ ] Resolve configuration through this project's config loader, not by reading files ad hoc
14
+ - [ ] Release every resource you acquire (connections, handles, locks)
15
+ - [ ] Every error path either handles the error or propagates it — never swallows it
@@ -0,0 +1,15 @@
1
+ #### Components Needed
2
+ - [ ] The module that implements the feature, under `{{paths.source}}`
3
+ - [ ] Its public names exported in the package `__init__.py`
4
+ - [ ] Registration with the application (router include, CLI entry point, task registry)
5
+ - [ ] Request/response models for the boundary
6
+ - [ ] Settings, if the feature needs configuration
7
+ - [ ] Tests under `{{paths.tests | default("tests")}}`
8
+ - [ ] Docstrings on every public function
9
+
10
+ #### Patterns to Apply (MANDATORY)
11
+ - [ ] Follow the registration pattern this project already uses — mirror the nearest
12
+ existing feature rather than inventing a second shape
13
+ - [ ] Type hints on every public signature; `{{verify.type | default("declare verification.LANG.type in massu.config.yaml")}}` must pass
14
+ - [ ] Context managers for anything that must be closed
15
+ - [ ] Never bare `except:` — catch the exception you mean
@@ -0,0 +1,15 @@
1
+ #### Components Needed
2
+ - [ ] The module that implements the feature, under `{{paths.source}}`
3
+ - [ ] Its exported surface (types and functions) declared explicitly
4
+ - [ ] Registration with whatever dispatches to it (router, command table, DI container)
5
+ - [ ] Input schemas at the boundary
6
+ - [ ] Configuration, if the feature needs any
7
+ - [ ] Tests under `{{paths.tests | default("tests")}}`
8
+ - [ ] Documentation
9
+
10
+ #### Patterns to Apply (MANDATORY)
11
+ - [ ] Follow the registration pattern this project already uses — mirror the nearest
12
+ existing feature rather than inventing a second shape
13
+ - [ ] Resolve configuration through this project's config loader
14
+ - [ ] `{{verify.type | default("declare verification.LANG.type in massu.config.yaml")}}` must pass with no new errors
15
+ - [ ] Release every resource you acquire — close what you open, in a `finally`