@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
@@ -42,6 +42,15 @@ var init_memory_path = __esm({
42
42
  }
43
43
  });
44
44
 
45
+ // src/lib/corrections-md.ts
46
+ var CORRECTIONS_FILENAME;
47
+ var init_corrections_md = __esm({
48
+ "src/lib/corrections-md.ts"() {
49
+ "use strict";
50
+ CORRECTIONS_FILENAME = "corrections.md";
51
+ }
52
+ });
53
+
45
54
  // src/credentials.ts
46
55
  import { homedir } from "os";
47
56
  import { resolve } from "path";
@@ -143,7 +152,7 @@ var init_config_memory_schema = __esm({
143
152
  message: "memory.recall.embedEndpoint must be an http(s) URL"
144
153
  }).optional(),
145
154
  embedModel: z.string().min(1).optional()
146
- }).default({}),
155
+ }).prefault({}),
147
156
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
148
157
  // When a new high-value memory (decision/correction) is written, find
149
158
  // semantically-related existing records and, if the new one contradicts an
@@ -172,7 +181,7 @@ var init_config_memory_schema = __esm({
172
181
  annotateSuperseded: z.boolean().default(false),
173
182
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
174
183
  budgetMs: z.number().int().positive().default(800)
175
- }).default({}),
184
+ }).prefault({}),
176
185
  // --- Background consolidation, the "sleep-time" pass
177
186
  // (plan-living-memory-slice-3-consolidation) ---
178
187
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -218,7 +227,7 @@ var init_config_memory_schema = __esm({
218
227
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
219
228
  suggestUpgrades: z.boolean().default(true),
220
229
  suggestIntervalDays: z.number().int().positive().default(30)
221
- }).default({}),
230
+ }).prefault({}),
222
231
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
223
232
  // Massu may ever WRITE into the user's memory directory.
224
233
  //
@@ -243,7 +252,7 @@ var init_config_memory_schema = __esm({
243
252
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
244
253
  // per-session cap bounds only the RATE, never the total.
245
254
  indexMaxLines: z.number().int().min(1).default(50)
246
- }).default({}),
255
+ }).prefault({}),
247
256
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
248
257
  // A decision made in one of your repos can surface in another — opt-in per
249
258
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -268,8 +277,8 @@ var init_config_memory_schema = __esm({
268
277
  enabled: z.boolean().default(true),
269
278
  maxCrossRepoItems: z.number().int().min(0).default(1),
270
279
  minScore: z.number().optional()
271
- }).default({})
272
- }).default({})
280
+ }).prefault({})
281
+ }).prefault({})
273
282
  }).optional();
274
283
  }
275
284
  });
@@ -449,13 +458,19 @@ function getResolvedPaths() {
449
458
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
450
459
  };
451
460
  }
452
- 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;
461
+ 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;
453
462
  var init_config = __esm({
454
463
  "src/config.ts"() {
455
464
  "use strict";
456
465
  init_memory_path();
466
+ init_corrections_md();
457
467
  init_credentials();
458
468
  init_config_memory_schema();
469
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
470
+ "CLAUDE.md",
471
+ "MEMORY.md",
472
+ CORRECTIONS_FILENAME
473
+ ];
459
474
  DomainConfigSchema = z2.object({
460
475
  name: z2.string().default("Unknown"),
461
476
  routers: z2.array(z2.string()).default([]),
@@ -584,34 +599,40 @@ var init_config = __esm({
584
599
  fixDetection: z2.object({
585
600
  enabled: z2.boolean().default(true),
586
601
  lookbackDays: z2.number().default(7),
587
- signals: z2.array(z2.string()).default([
588
- "removed_broken_code",
589
- "added_error_handling",
590
- "method_name_correction",
591
- "auth_fix",
592
- "nil_handling_fix",
593
- "concurrency_fix",
594
- "async_pattern_fix",
595
- "added_missing_import"
596
- ])
597
- }).default({}),
602
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
603
+ //
604
+ // This carried a hand-typed copy of all eight signal names while
605
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
606
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
607
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
608
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
609
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
610
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
611
+ // one site of two (CR-74).
612
+ //
613
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
614
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
615
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
616
+ // consumer and no config file in the repo sets it.
617
+ signals: z2.array(z2.string()).optional()
618
+ }).prefault({}),
598
619
  failureClassification: z2.object({
599
620
  enabled: z2.boolean().default(true),
600
621
  thresholds: z2.object({
601
622
  known: z2.number().default(5),
602
623
  similar: z2.number().default(3)
603
- }).default({}),
624
+ }).prefault({}),
604
625
  scoring: z2.object({
605
626
  diffPatternWeight: z2.number().default(3),
606
627
  filePatternWeight: z2.number().default(2),
607
628
  promptKeywordWeight: z2.number().default(2)
608
- }).default({})
609
- }).default({}),
629
+ }).prefault({})
630
+ }).prefault({}),
610
631
  pipeline: z2.object({
611
632
  requireIncidentReport: z2.boolean().default(true),
612
633
  requirePreventionRule: z2.boolean().default(true),
613
634
  requireEnforcement: z2.boolean().default(true)
614
- }).default({}),
635
+ }).prefault({}),
615
636
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
616
637
  // custom destinations for the rule-candidate funnel. The classifier matches
617
638
  // a candidate to one of these entries when none of the framework
@@ -644,7 +665,7 @@ var init_config = __esm({
644
665
  memory: z2.boolean().default(true),
645
666
  analytics: z2.boolean().default(true),
646
667
  audit: z2.boolean().default(true)
647
- }).default({ memory: true, analytics: true, audit: true })
668
+ }).prefault({ memory: true, analytics: true, audit: true })
648
669
  }).optional();
649
670
  ConventionsConfigSchema = z2.object({
650
671
  claudeDirName: z2.string().default(".claude").refine(
@@ -675,7 +696,7 @@ var init_config = __esm({
675
696
  "session-state",
676
697
  "agents"
677
698
  ]),
678
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
699
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
679
700
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
680
701
  }).optional();
681
702
  PythonDomainConfigSchema = z2.object({
@@ -691,6 +712,23 @@ var init_config = __esm({
691
712
  }).optional();
692
713
  PathsConfigSchema = z2.object({
693
714
  source: z2.string().default("src"),
715
+ /**
716
+ * Where this stack's tests live, relative to the project root.
717
+ *
718
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
719
+ * ~25 shipped assets told every customer their tests belong in
720
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
721
+ * the alternative because none existed. A test directory is a first-class property of
722
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
723
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
724
+ * rather than in prose.
725
+ *
726
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
727
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
728
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
729
+ * and `stack-template-conformance.test.ts` asserts that.
730
+ */
731
+ tests: z2.string().optional(),
694
732
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
695
733
  monorepo_roots: z2.array(z2.string()).optional(),
696
734
  routers: z2.string().optional(),
@@ -800,14 +838,14 @@ var init_config = __esm({
800
838
  project: z2.object({
801
839
  name: z2.string().default("my-project"),
802
840
  root: z2.string().default("auto")
803
- }).default({ name: "my-project", root: "auto" }),
804
- framework: FrameworkConfigSchema.default({
841
+ }).prefault({ name: "my-project", root: "auto" }),
842
+ framework: FrameworkConfigSchema.prefault({
805
843
  type: "typescript",
806
844
  router: "none",
807
845
  orm: "none",
808
846
  ui: "none"
809
847
  }),
810
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
848
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
811
849
  toolPrefix: z2.string().default("massu"),
812
850
  dbAccessPattern: z2.string().optional(),
813
851
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -4005,6 +4043,342 @@ import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync
4005
4043
  import { tmpdir as tmpdir2 } from "os";
4006
4044
  import { join as join6 } from "path";
4007
4045
 
4046
+ // src/hooks/lib/code-spans.ts
4047
+ import { extname } from "path";
4048
+ var DQ = { open: '"', close: '"', escapes: true, multiline: false };
4049
+ var SQ = { open: "'", close: "'", escapes: true, multiline: false };
4050
+ var C_COMMENTS = { lineComments: ["//"], blockComments: [["/*", "*/"]] };
4051
+ var JS = {
4052
+ id: "js",
4053
+ ...C_COMMENTS,
4054
+ strings: [DQ, SQ, { open: "`", close: "`", escapes: true, multiline: true, interpolation: true }],
4055
+ regexLiterals: true
4056
+ };
4057
+ var C = { id: "c", ...C_COMMENTS, strings: [DQ, SQ] };
4058
+ var GO = {
4059
+ id: "go",
4060
+ ...C_COMMENTS,
4061
+ strings: [DQ, SQ, { open: "`", close: "`", escapes: false, multiline: true }]
4062
+ };
4063
+ var JVM = {
4064
+ id: "jvm",
4065
+ ...C_COMMENTS,
4066
+ strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ, SQ]
4067
+ };
4068
+ var SWIFT = {
4069
+ id: "swift",
4070
+ ...C_COMMENTS,
4071
+ strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ]
4072
+ };
4073
+ var RUST = {
4074
+ id: "rust",
4075
+ ...C_COMMENTS,
4076
+ strings: [{ open: '"', close: '"', escapes: true, multiline: true }],
4077
+ rustChars: true
4078
+ };
4079
+ var PYTHON = {
4080
+ id: "python",
4081
+ lineComments: ["#"],
4082
+ blockComments: [],
4083
+ strings: [
4084
+ { open: '"""', close: '"""', escapes: true, multiline: true },
4085
+ { open: "'''", close: "'''", escapes: true, multiline: true },
4086
+ DQ,
4087
+ SQ
4088
+ ]
4089
+ };
4090
+ var SHELL = {
4091
+ id: "shell",
4092
+ lineComments: ["#"],
4093
+ blockComments: [],
4094
+ strings: [
4095
+ { open: '"', close: '"', escapes: true, multiline: true },
4096
+ { open: "'", close: "'", escapes: false, multiline: true }
4097
+ ],
4098
+ hashNeedsBoundary: true
4099
+ };
4100
+ var RUBY = {
4101
+ id: "ruby",
4102
+ lineComments: ["#"],
4103
+ blockComments: [],
4104
+ strings: [
4105
+ { open: '"', close: '"', escapes: true, multiline: true },
4106
+ { open: "'", close: "'", escapes: true, multiline: true }
4107
+ ]
4108
+ };
4109
+ var LEX_SPECS = Object.freeze({
4110
+ ".ts": JS,
4111
+ ".tsx": JS,
4112
+ ".mts": JS,
4113
+ ".cts": JS,
4114
+ ".js": JS,
4115
+ ".jsx": JS,
4116
+ ".mjs": JS,
4117
+ ".cjs": JS,
4118
+ ".c": C,
4119
+ ".h": C,
4120
+ ".cc": C,
4121
+ ".cpp": C,
4122
+ ".hpp": C,
4123
+ ".cs": C,
4124
+ ".dart": C,
4125
+ ".m": C,
4126
+ ".mm": C,
4127
+ ".go": GO,
4128
+ ".java": JVM,
4129
+ ".kt": JVM,
4130
+ ".kts": JVM,
4131
+ ".scala": JVM,
4132
+ ".groovy": JVM,
4133
+ ".swift": SWIFT,
4134
+ ".rs": RUST,
4135
+ ".py": PYTHON,
4136
+ ".pyi": PYTHON,
4137
+ ".sh": SHELL,
4138
+ ".bash": SHELL,
4139
+ ".zsh": SHELL,
4140
+ ".rb": RUBY
4141
+ });
4142
+ function lexSpecFor(path) {
4143
+ const ext = extname(path).toLowerCase();
4144
+ return Object.prototype.hasOwnProperty.call(LEX_SPECS, ext) ? LEX_SPECS[ext] : null;
4145
+ }
4146
+ var RUST_CHAR_RE = /^'(?:\\(?:u\{[0-9a-fA-F]{1,6}\}|x[0-9a-fA-F]{2}|.)|[^\\'\n])'/;
4147
+ var IDENT_CHAR_RE = /[A-Za-z0-9_$]/;
4148
+ var SHELL_WORD_BREAK = /* @__PURE__ */ new Set([" ", " ", ";", "|", "&", "(", ")"]);
4149
+ var REGEX_AFTER_KEYWORD = /* @__PURE__ */ new Set([
4150
+ "return",
4151
+ "typeof",
4152
+ "case",
4153
+ "in",
4154
+ "of",
4155
+ "delete",
4156
+ "void",
4157
+ "throw",
4158
+ "new",
4159
+ "yield",
4160
+ "await",
4161
+ "else",
4162
+ "do"
4163
+ ]);
4164
+ function regexLiteralLength(line, i, lastSig) {
4165
+ if (lastSig === ")" || lastSig === "]") return 0;
4166
+ if (lastSig !== "" && IDENT_CHAR_RE.test(lastSig)) {
4167
+ const m = /([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(line.slice(0, i));
4168
+ if (!m || !REGEX_AFTER_KEYWORD.has(m[1])) return 0;
4169
+ }
4170
+ let inClass = false;
4171
+ for (let j = i + 1; j < line.length; j++) {
4172
+ const ch = line[j];
4173
+ if (ch === "\\") {
4174
+ j++;
4175
+ continue;
4176
+ }
4177
+ if (inClass) {
4178
+ if (ch === "]") inClass = false;
4179
+ continue;
4180
+ }
4181
+ if (ch === "[") {
4182
+ inClass = true;
4183
+ continue;
4184
+ }
4185
+ if (ch === "/") return j - i + 1;
4186
+ }
4187
+ return 0;
4188
+ }
4189
+ function projectLine(line, spec, st) {
4190
+ const out = line.split("");
4191
+ const blank = (from, len) => {
4192
+ for (let k = from; k < from + len && k < out.length; k++) out[k] = " ";
4193
+ };
4194
+ let i = 0;
4195
+ while (i < line.length) {
4196
+ const mode = st.mode;
4197
+ if (mode.kind === "block") {
4198
+ if (line.startsWith(mode.close, i)) {
4199
+ blank(i, mode.close.length);
4200
+ i += mode.close.length;
4201
+ st.mode = { kind: "code" };
4202
+ } else {
4203
+ blank(i, 1);
4204
+ i++;
4205
+ }
4206
+ continue;
4207
+ }
4208
+ if (mode.kind === "string") {
4209
+ const r = mode.rule;
4210
+ if (r.escapes && line[i] === "\\") {
4211
+ blank(i, 2);
4212
+ i += 2;
4213
+ continue;
4214
+ }
4215
+ if (r.interpolation && line.startsWith("${", i)) {
4216
+ blank(i, 2);
4217
+ i += 2;
4218
+ st.holes.push(0);
4219
+ st.mode = { kind: "code" };
4220
+ st.lastSig = "";
4221
+ continue;
4222
+ }
4223
+ if (line.startsWith(r.close, i)) {
4224
+ blank(i, r.close.length);
4225
+ i += r.close.length;
4226
+ st.mode = { kind: "code" };
4227
+ st.lastSig = '"';
4228
+ continue;
4229
+ }
4230
+ blank(i, 1);
4231
+ i++;
4232
+ continue;
4233
+ }
4234
+ const ch = line[i];
4235
+ if (st.holes.length > 0) {
4236
+ const top = st.holes.length - 1;
4237
+ if (ch === "{") st.holes[top]++;
4238
+ else if (ch === "}") {
4239
+ if (st.holes[top] === 0) {
4240
+ st.holes.pop();
4241
+ blank(i, 1);
4242
+ i++;
4243
+ st.mode = { kind: "string", rule: spec.strings.find((s) => s.interpolation) };
4244
+ continue;
4245
+ }
4246
+ st.holes[top]--;
4247
+ }
4248
+ }
4249
+ const lc = spec.lineComments.find((m) => line.startsWith(m, i));
4250
+ if (lc && (!spec.hashNeedsBoundary || i === 0 || SHELL_WORD_BREAK.has(line[i - 1]))) {
4251
+ blank(i, line.length - i);
4252
+ break;
4253
+ }
4254
+ const bc = spec.blockComments.find(([open]) => line.startsWith(open, i));
4255
+ if (bc) {
4256
+ blank(i, bc[0].length);
4257
+ i += bc[0].length;
4258
+ st.mode = { kind: "block", close: bc[1] };
4259
+ continue;
4260
+ }
4261
+ if (spec.rustChars && ch === "'") {
4262
+ const m = RUST_CHAR_RE.exec(line.slice(i));
4263
+ if (m) {
4264
+ blank(i, m[0].length);
4265
+ i += m[0].length;
4266
+ st.lastSig = "'";
4267
+ continue;
4268
+ }
4269
+ i++;
4270
+ continue;
4271
+ }
4272
+ const sr = spec.strings.find((s) => line.startsWith(s.open, i));
4273
+ if (sr) {
4274
+ blank(i, sr.open.length);
4275
+ i += sr.open.length;
4276
+ st.mode = { kind: "string", rule: sr };
4277
+ continue;
4278
+ }
4279
+ if (spec.regexLiterals && ch === "/") {
4280
+ const len = regexLiteralLength(line, i, st.lastSig);
4281
+ if (len > 0) {
4282
+ blank(i, len);
4283
+ i += len;
4284
+ st.lastSig = "/";
4285
+ continue;
4286
+ }
4287
+ }
4288
+ if (ch !== " " && ch !== " ") st.lastSig = ch;
4289
+ i++;
4290
+ }
4291
+ if (st.mode.kind === "string" && !st.mode.rule.multiline) st.mode = { kind: "code" };
4292
+ return out.join("");
4293
+ }
4294
+ function projectCode(lines, spec) {
4295
+ const st = { mode: { kind: "code" }, holes: [], lastSig: "" };
4296
+ return lines.map((l) => projectLine(l, spec, st));
4297
+ }
4298
+ function unquoteGitPath(p) {
4299
+ const s = p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
4300
+ return s.startsWith("b/") ? s.slice(2) : s;
4301
+ }
4302
+ function diffCodeView(diff, opts = {}) {
4303
+ const files = [];
4304
+ let cur = null;
4305
+ let hunk = null;
4306
+ let newLine = 0;
4307
+ let inHeader = false;
4308
+ const startFile = (path) => {
4309
+ const f = { path, hunks: [], removed: [] };
4310
+ files.push(f);
4311
+ return f;
4312
+ };
4313
+ for (const line of diff.split("\n")) {
4314
+ if (line.startsWith("diff --git ")) {
4315
+ cur = startFile("");
4316
+ hunk = null;
4317
+ inHeader = true;
4318
+ continue;
4319
+ }
4320
+ if (inHeader && line.startsWith("+++ ")) {
4321
+ const p = line.slice(4).trim();
4322
+ if (cur) cur.path = p === "/dev/null" ? "" : unquoteGitPath(p);
4323
+ continue;
4324
+ }
4325
+ if (inHeader && (line.startsWith("--- ") || !line.startsWith("@@"))) continue;
4326
+ const hh = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
4327
+ if (hh) {
4328
+ inHeader = false;
4329
+ if (!cur) cur = startFile("");
4330
+ hunk = { newSide: [], addedAt: [] };
4331
+ cur.hunks.push(hunk);
4332
+ newLine = parseInt(hh[1], 10);
4333
+ continue;
4334
+ }
4335
+ if (!cur) cur = startFile("");
4336
+ if (!hunk) {
4337
+ hunk = { newSide: [], addedAt: [] };
4338
+ cur.hunks.push(hunk);
4339
+ newLine = 1;
4340
+ }
4341
+ if (line.startsWith("+")) {
4342
+ hunk.addedAt.push({ idx: hunk.newSide.length, lineNo: newLine });
4343
+ hunk.newSide.push(line.slice(1));
4344
+ newLine++;
4345
+ } else if (line.startsWith("-")) {
4346
+ cur.removed.push(line.slice(1));
4347
+ } else if (line.startsWith(" ")) {
4348
+ hunk.newSide.push(line.slice(1));
4349
+ newLine++;
4350
+ }
4351
+ }
4352
+ return files.map((f) => {
4353
+ const spec = f.path ? lexSpecFor(f.path) : null;
4354
+ const rawAdded = f.hunks.flatMap(
4355
+ (h) => h.addedAt.map((a) => ({ lineNo: a.lineNo, raw: h.newSide[a.idx] }))
4356
+ );
4357
+ if (!spec) {
4358
+ return { path: f.path, mode: "raw", added: rawAdded.map((a) => ({ ...a, code: a.raw })), removed: f.removed };
4359
+ }
4360
+ const text = opts.postImage ? opts.postImage(f.path) : null;
4361
+ if (text !== null && text !== void 0) {
4362
+ const post = text.split(/\r\n|\n|\r/);
4363
+ if (rawAdded.every((a) => post[a.lineNo - 1] === a.raw)) {
4364
+ const proj = projectCode(post, spec);
4365
+ return {
4366
+ path: f.path,
4367
+ mode: "post-image",
4368
+ added: rawAdded.map((a) => ({ ...a, code: proj[a.lineNo - 1] })),
4369
+ removed: f.removed
4370
+ };
4371
+ }
4372
+ }
4373
+ const added = [];
4374
+ for (const h of f.hunks) {
4375
+ const proj = projectCode(h.newSide, spec);
4376
+ for (const a of h.addedAt) added.push({ lineNo: a.lineNo, raw: h.newSide[a.idx], code: proj[a.idx] });
4377
+ }
4378
+ return { path: f.path, mode: "hunk", added, removed: f.removed };
4379
+ });
4380
+ }
4381
+
4008
4382
  // src/hooks/lib/write-hook-message.ts
4009
4383
  var HOOK_EVENTS = [
4010
4384
  "UserPromptSubmit",
@@ -4072,22 +4446,70 @@ var FIX_HEURISTICS = [
4072
4446
  },
4073
4447
  {
4074
4448
  name: "added_error_handling",
4449
+ // `if.*nil` / `if.*None` were DROPPED here, and that is deduplication rather than a silent
4450
+ // narrowing: `nil_handling_fix` below already matches exactly that shape
4451
+ // (`= nil|= None|is None|!= nil|is not None`), so a nil check scored twice under one banner.
4452
+ // Scored on each added line's CODE projection (`lib/code-spans.ts`), the same lexer the
4453
+ // Stop hook uses: a keyword in a string literal or a comment is not error handling. The
4454
+ // per-edit diff carries its `diff --git` header, so the file's language is known here
4455
+ // without threading a path through this table. A language with no lexer is judged raw.
4075
4456
  test: (diff) => {
4076
- const added = (diff.match(/^\+.*(try|except|catch|guard|if.*nil|if.*None|validate|assert|raise|throw)/gm) || []).length;
4457
+ const re = /^\+.*\b(try|except|catch|guard|validate|assert|raise|throw)\b/;
4458
+ let added = 0;
4459
+ for (const view of diffCodeView(diff)) {
4460
+ for (const a of view.added) if (re.test(`+${a.code}`)) added++;
4461
+ }
4077
4462
  return added > 2;
4078
4463
  }
4079
4464
  },
4080
- {
4081
- name: "method_name_correction",
4082
- test: (diff) => {
4083
- const removed = diff.match(/^-.*\.([a-z_]+)\(/m);
4084
- const added = diff.match(/^\+.*\.([a-z_]+)\(/m);
4085
- return !!(removed && added && removed[1] !== added[1]);
4086
- }
4087
- },
4465
+ // ⛔ `method_name_correction` WAS RETIRED HERE 2026-08-20, and the measurement is why.
4466
+ //
4467
+ // test: (diff) => {
4468
+ // const removed = diff.match(/^-.*\.([a-z_]+)\(/m); // FIRST removed method call
4469
+ // const added = diff.match(/^\+.*\.([a-z_]+)\(/m); // FIRST added method call
4470
+ // return !!(removed && added && removed[1] !== added[1]);
4471
+ // }
4472
+ //
4473
+ // It compared the FIRST removed call against the FIRST added call, anywhere in the diff.
4474
+ // The two need no relationship to each other, so it fired whenever a diff contained any
4475
+ // two differently-named method calls — which is nearly every diff that touches code.
4476
+ //
4477
+ // Measured over the last 250 commits repo-wide:
4478
+ // shipped predicate 29 firings (12%)
4479
+ // one requiring the two lines to be identical but for the name 0 firings
4480
+ // Eleven firings read by hand over packages/core/src: `.push()`->`.includes()`,
4481
+ // `.call()`->`.system()`, `.trim()`->`.stringify()`, `.all()`->`.cwd()`, `.now()`->`.push()`,
4482
+ // `.on()`->`.sort()`, `.test()`->`.split()` … NOT ONE was a rename. Precision 0 of 29.
4483
+ //
4484
+ // RETIRED RATHER THAN REPAIRED, on the same numbers: a correct implementation fires ZERO
4485
+ // times in 250 commits, so repairing it buys a branch that never runs — dead weight with a
4486
+ // maintenance cost (CR-71 in spirit: a detector that cannot fire is not a detector). It was
4487
+ // not free while it lived: signals feed `sessionFixes`, which drives the ACTION REQUIRED
4488
+ // banner demanding incidents, so this put noise directly on the retro pipeline — the one
4489
+ // banner that must not cry wolf (CR-83).
4490
+ //
4491
+ // Kept as a comment, not deleted outright, so the next author who reaches for
4492
+ // "detect a method rename from a diff" finds the measurement instead of rebuilding it.
4493
+ //
4494
+ // ⛔ THE FULL WRITE-UP IS DELIBERATELY NOT LINKED FROM HERE. This file SYNCS TO THE PUBLIC
4495
+ // MIRROR and the incident corpus does not, so a path citation would point at something
4496
+ // that does not exist there — and the leak guard refuses it, correctly. It caught exactly
4497
+ // this line. The measurement above is self-contained for that reason; search the internal
4498
+ // incident corpus for "a summary counted signals and listed files" if you need the rest.
4499
+ // (Memory: a-retro-citation-is-publishable-content.)
4088
4500
  {
4089
4501
  name: "auth_fix",
4090
- test: (diff) => /^\+.*(token|auth|header|X-Service|Bearer|credential)/im.test(diff)
4502
+ // `authn`/`authz` are spelled out because `\bauth\b` no longer reaches them, and the
4503
+ // trailing `s?` keeps plurals (`tokens`, `credential`+`s`) that the boundary would drop.
4504
+ //
4505
+ // ⚠️ Backticks, not double quotes, and the plural is split — deliberately. CR-59's
4506
+ // drift-guard forbids a quoted `credentials` segment literal outside credentials.ts via
4507
+ // /['"]credentials['"]/, which cannot tell a string literal in CODE from quotation marks
4508
+ // in PROSE. Writing the word normally here turns that guard red. That is the SAME defect
4509
+ // class this file's own boundary fix addresses, in a second detector — recorded in the
4510
+ // incident rather than fixed here, because widening a security-adjacent guard is its own
4511
+ // change with its own blast radius.
4512
+ test: (diff) => /^\+.*\b(token|auth|authn|authz|header|X-Service|Bearer|credential)s?\b/im.test(diff)
4091
4513
  },
4092
4514
  {
4093
4515
  name: "nil_handling_fix",
@@ -4095,16 +4517,42 @@ var FIX_HEURISTICS = [
4095
4517
  },
4096
4518
  {
4097
4519
  name: "concurrency_fix",
4098
- test: (diff) => /^\+.*(timeout|semaphore|lock|mutex|throttle|rate.limit|max_conn)/im.test(diff)
4099
- },
4100
- {
4101
- name: "async_pattern_fix",
4102
- test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff) && /^-/m.test(diff)
4103
- },
4104
- {
4105
- name: "added_missing_import",
4106
- test: (diff) => /^\+.*(import|from.*import|require)/.test(diff) && !/^-.*(import|from.*import|require)/m.test(diff)
4107
- }
4520
+ // `rate[-_. ]limit` — the original `rate.limit` used an UNESCAPED dot, so `.` matched any
4521
+ // character and the alternative fired on "corporate_limits". The class is now explicit
4522
+ // about which separators are intended.
4523
+ test: (diff) => /^\+.*\b(timeout|semaphore|lock|mutex|throttle|rate[-_. ]limit|max_conn)\b/im.test(diff)
4524
+ }
4525
+ // ⛔ `async_pattern_fix` AND `added_missing_import` WERE RETIRED HERE 2026-08-20.
4526
+ //
4527
+ // async_pattern_fix:
4528
+ // test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff)
4529
+ // && /^-/m.test(diff),
4530
+ // added_missing_import:
4531
+ // test: (diff) => /^\+.*(import|from.*import|require)/.test(diff)
4532
+ // && !/^-.*(import|from.*import|require)/m.test(diff),
4533
+ //
4534
+ // Both fire ZERO times across the last 250 commits — measured on the same run that produced
4535
+ // the boundary numbers above, over the 87 commits touching non-test source:
4536
+ //
4537
+ // async_pattern_fix 0 / 87
4538
+ // added_missing_import 0 / 87
4539
+ //
4540
+ // This is the identical finding that retired `method_name_correction` below, and it is
4541
+ // retired for the identical reason: a detector that never fires is not a detector, and it
4542
+ // still costs review attention and maintenance on every pass over this table (CR-71 in
4543
+ // spirit — a zero-caller chokepoint is a dead feature).
4544
+ //
4545
+ // WHY THEY CANNOT FIRE HERE, so the next author does not "fix" them back in: their tokens are
4546
+ // largely foreign to this codebase. `@MainActor` is Swift; `async with` is Python. Neither
4547
+ // appears in a TypeScript monorepo. `added_missing_import` requires added imports with NO
4548
+ // removed import anywhere in the same diff, which any edit touching an import block violates.
4549
+ // If this table is ever pointed at a Swift or Python corpus, re-measure before reinstating —
4550
+ // do not reinstate on the assumption that the language mix changed.
4551
+ //
4552
+ // Kept as comments rather than deleted so the measurement travels with the code, and so a
4553
+ // future author reaching for "detect an async-pattern fix" finds the number first.
4554
+ // (Same reasoning, and same publication constraint, as the block below: this file syncs to
4555
+ // the public mirror, so no internal path citations.)
4108
4556
  ];
4109
4557
  function getSessionFlagPath(sessionId) {
4110
4558
  const dir = join6(tmpdir2(), "massu-auto-learning");
@@ -4268,6 +4716,7 @@ if (isDirectInvocation(import.meta.url)) {
4268
4716
  main();
4269
4717
  }
4270
4718
  export {
4719
+ FIX_HEURISTICS,
4271
4720
  TEST_OR_FIXTURE_PATH_PATTERNS,
4272
4721
  isTestOrFixturePath
4273
4722
  };