@massu/core 2.5.2 → 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 +70 -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/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
@@ -36,6 +36,15 @@ var init_memory_path = __esm({
36
36
  }
37
37
  });
38
38
 
39
+ // src/lib/corrections-md.ts
40
+ var CORRECTIONS_FILENAME;
41
+ var init_corrections_md = __esm({
42
+ "src/lib/corrections-md.ts"() {
43
+ "use strict";
44
+ CORRECTIONS_FILENAME = "corrections.md";
45
+ }
46
+ });
47
+
39
48
  // src/credentials.ts
40
49
  import { homedir } from "os";
41
50
  import { resolve } from "path";
@@ -137,7 +146,7 @@ var init_config_memory_schema = __esm({
137
146
  message: "memory.recall.embedEndpoint must be an http(s) URL"
138
147
  }).optional(),
139
148
  embedModel: z.string().min(1).optional()
140
- }).default({}),
149
+ }).prefault({}),
141
150
  // --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
142
151
  // When a new high-value memory (decision/correction) is written, find
143
152
  // semantically-related existing records and, if the new one contradicts an
@@ -166,7 +175,7 @@ var init_config_memory_schema = __esm({
166
175
  annotateSuperseded: z.boolean().default(false),
167
176
  // Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
168
177
  budgetMs: z.number().int().positive().default(800)
169
- }).default({}),
178
+ }).prefault({}),
170
179
  // --- Background consolidation, the "sleep-time" pass
171
180
  // (plan-living-memory-slice-3-consolidation) ---
172
181
  // Keeps memory sharp over years: dedupes, distills dying sessions into
@@ -212,7 +221,7 @@ var init_config_memory_schema = __esm({
212
221
  // Surface optional upgrades (e.g. "a local model was detected") in chat.
213
222
  suggestUpgrades: z.boolean().default(true),
214
223
  suggestIntervalDays: z.number().int().positive().default(30)
215
- }).default({}),
224
+ }).prefault({}),
216
225
  // A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
217
226
  // Massu may ever WRITE into the user's memory directory.
218
227
  //
@@ -237,7 +246,7 @@ var init_config_memory_schema = __esm({
237
246
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
238
247
  // per-session cap bounds only the RATE, never the total.
239
248
  indexMaxLines: z.number().int().min(1).default(50)
240
- }).default({}),
249
+ }).prefault({}),
241
250
  // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
242
251
  // A decision made in one of your repos can surface in another — opt-in per
243
252
  // decision AND opt-in per repo, signed, verified, and materialized ONLY on
@@ -262,8 +271,8 @@ var init_config_memory_schema = __esm({
262
271
  enabled: z.boolean().default(true),
263
272
  maxCrossRepoItems: z.number().int().min(0).default(1),
264
273
  minScore: z.number().optional()
265
- }).default({})
266
- }).default({})
274
+ }).prefault({})
275
+ }).prefault({})
267
276
  }).optional();
268
277
  }
269
278
  });
@@ -443,13 +452,19 @@ function getResolvedPaths() {
443
452
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
444
453
  };
445
454
  }
446
- 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;
455
+ 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;
447
456
  var init_config = __esm({
448
457
  "src/config.ts"() {
449
458
  "use strict";
450
459
  init_memory_path();
460
+ init_corrections_md();
451
461
  init_credentials();
452
462
  init_config_memory_schema();
463
+ DEFAULT_KNOWLEDGE_SOURCE_FILES = [
464
+ "CLAUDE.md",
465
+ "MEMORY.md",
466
+ CORRECTIONS_FILENAME
467
+ ];
453
468
  DomainConfigSchema = z2.object({
454
469
  name: z2.string().default("Unknown"),
455
470
  routers: z2.array(z2.string()).default([]),
@@ -578,34 +593,40 @@ var init_config = __esm({
578
593
  fixDetection: z2.object({
579
594
  enabled: z2.boolean().default(true),
580
595
  lookbackDays: z2.number().default(7),
581
- signals: z2.array(z2.string()).default([
582
- "removed_broken_code",
583
- "added_error_handling",
584
- "method_name_correction",
585
- "auth_fix",
586
- "nil_handling_fix",
587
- "concurrency_fix",
588
- "async_pattern_fix",
589
- "added_missing_import"
590
- ])
591
- }).default({}),
596
+ // ⛔ NO DEFAULT LIST HERE, DELIBERATELY — the heuristics are the SSOT.
597
+ //
598
+ // This carried a hand-typed copy of all eight signal names while
599
+ // `fix-detector.ts` already read `config…signals ?? FIX_HEURISTICS.map(h => h.name)`.
600
+ // Because zod ALWAYS materialised the default, that fallback — the correct expression
601
+ // — was unreachable, and the duplicate was the only thing in effect. Two authoring
602
+ // sites for one vocabulary, and the drift is SILENT: a name listed here that no longer
603
+ // exists simply never matches `enabledSignals.has(...)`, so a retired heuristic lingers
604
+ // as a dead entry with no error. Retiring `method_name_correction` would have landed at
605
+ // one site of two (CR-74).
606
+ //
607
+ // Optional, so an ABSENT value reaches the `??` and the heuristic list defines itself
608
+ // (Rule 25). An explicit list in massu.config.yaml still overrides, which is the only
609
+ // behaviour anyone actually configured — measured: `fix-detector.ts:267` is the sole
610
+ // consumer and no config file in the repo sets it.
611
+ signals: z2.array(z2.string()).optional()
612
+ }).prefault({}),
592
613
  failureClassification: z2.object({
593
614
  enabled: z2.boolean().default(true),
594
615
  thresholds: z2.object({
595
616
  known: z2.number().default(5),
596
617
  similar: z2.number().default(3)
597
- }).default({}),
618
+ }).prefault({}),
598
619
  scoring: z2.object({
599
620
  diffPatternWeight: z2.number().default(3),
600
621
  filePatternWeight: z2.number().default(2),
601
622
  promptKeywordWeight: z2.number().default(2)
602
- }).default({})
603
- }).default({}),
623
+ }).prefault({})
624
+ }).prefault({}),
604
625
  pipeline: z2.object({
605
626
  requireIncidentReport: z2.boolean().default(true),
606
627
  requirePreventionRule: z2.boolean().default(true),
607
628
  requireEnforcement: z2.boolean().default(true)
608
- }).default({}),
629
+ }).prefault({}),
609
630
  // plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
610
631
  // custom destinations for the rule-candidate funnel. The classifier matches
611
632
  // a candidate to one of these entries when none of the framework
@@ -638,7 +659,7 @@ var init_config = __esm({
638
659
  memory: z2.boolean().default(true),
639
660
  analytics: z2.boolean().default(true),
640
661
  audit: z2.boolean().default(true)
641
- }).default({ memory: true, analytics: true, audit: true })
662
+ }).prefault({ memory: true, analytics: true, audit: true })
642
663
  }).optional();
643
664
  ConventionsConfigSchema = z2.object({
644
665
  claudeDirName: z2.string().default(".claude").refine(
@@ -669,7 +690,7 @@ var init_config = __esm({
669
690
  "session-state",
670
691
  "agents"
671
692
  ]),
672
- knowledgeSourceFiles: z2.array(z2.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
693
+ knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
673
694
  excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
674
695
  }).optional();
675
696
  PythonDomainConfigSchema = z2.object({
@@ -685,6 +706,23 @@ var init_config = __esm({
685
706
  }).optional();
686
707
  PathsConfigSchema = z2.object({
687
708
  source: z2.string().default("src"),
709
+ /**
710
+ * Where this stack's tests live, relative to the project root.
711
+ *
712
+ * Added 2026-09-20 for P4.3 of docs/plans/2026-09-20-stack-conformant-asset-rendering.md.
713
+ * ~25 shipped assets told every customer their tests belong in
714
+ * `packages/core/src/__tests__/` — massu's OWN layout — and no vocabulary could express
715
+ * the alternative because none existed. A test directory is a first-class property of
716
+ * every stack (`tests/` on FastAPI, `test/` on Rails, `src/test/java` on Spring,
717
+ * `Tests/` on SwiftPM, and Go keeps `_test.go` beside the source), so it belongs here
718
+ * rather than in prose.
719
+ *
720
+ * OPTIONAL, so every config written before this key existed keeps loading unchanged; an
721
+ * asset that needs it uses `{{paths.tests | default("...")}}`. All 13 shipped templates
722
+ * declare it, each with the convention it follows recorded at the site (G32 option 2),
723
+ * and `stack-template-conformance.test.ts` asserts that.
724
+ */
725
+ tests: z2.string().optional(),
688
726
  aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
689
727
  monorepo_roots: z2.array(z2.string()).optional(),
690
728
  routers: z2.string().optional(),
@@ -794,14 +832,14 @@ var init_config = __esm({
794
832
  project: z2.object({
795
833
  name: z2.string().default("my-project"),
796
834
  root: z2.string().default("auto")
797
- }).default({ name: "my-project", root: "auto" }),
798
- framework: FrameworkConfigSchema.default({
835
+ }).prefault({ name: "my-project", root: "auto" }),
836
+ framework: FrameworkConfigSchema.prefault({
799
837
  type: "typescript",
800
838
  router: "none",
801
839
  orm: "none",
802
840
  ui: "none"
803
841
  }),
804
- paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
842
+ paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
805
843
  toolPrefix: z2.string().default("massu"),
806
844
  dbAccessPattern: z2.string().optional(),
807
845
  knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
@@ -3995,9 +4033,10 @@ var init_hook_failure_signal = __esm({
3995
4033
  // src/hooks/auto-learning-pipeline.ts
3996
4034
  init_config();
3997
4035
  import { execFileSync } from "child_process";
3998
- import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
4036
+ import { createHash } from "crypto";
4037
+ import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
3999
4038
  import { tmpdir as tmpdir2 } from "os";
4000
- import { join as join6 } from "path";
4039
+ import { join as join6, dirname as dirname8 } from "path";
4001
4040
 
4002
4041
  // src/hooks/lib/write-hook-message.ts
4003
4042
  var HOOK_EVENTS = [
@@ -4154,6 +4193,342 @@ function gitSafeEnv(extra = {}, source = process.env) {
4154
4193
  return e;
4155
4194
  }
4156
4195
 
4196
+ // src/hooks/lib/code-spans.ts
4197
+ import { extname } from "path";
4198
+ var DQ = { open: '"', close: '"', escapes: true, multiline: false };
4199
+ var SQ = { open: "'", close: "'", escapes: true, multiline: false };
4200
+ var C_COMMENTS = { lineComments: ["//"], blockComments: [["/*", "*/"]] };
4201
+ var JS = {
4202
+ id: "js",
4203
+ ...C_COMMENTS,
4204
+ strings: [DQ, SQ, { open: "`", close: "`", escapes: true, multiline: true, interpolation: true }],
4205
+ regexLiterals: true
4206
+ };
4207
+ var C = { id: "c", ...C_COMMENTS, strings: [DQ, SQ] };
4208
+ var GO = {
4209
+ id: "go",
4210
+ ...C_COMMENTS,
4211
+ strings: [DQ, SQ, { open: "`", close: "`", escapes: false, multiline: true }]
4212
+ };
4213
+ var JVM = {
4214
+ id: "jvm",
4215
+ ...C_COMMENTS,
4216
+ strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ, SQ]
4217
+ };
4218
+ var SWIFT = {
4219
+ id: "swift",
4220
+ ...C_COMMENTS,
4221
+ strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ]
4222
+ };
4223
+ var RUST = {
4224
+ id: "rust",
4225
+ ...C_COMMENTS,
4226
+ strings: [{ open: '"', close: '"', escapes: true, multiline: true }],
4227
+ rustChars: true
4228
+ };
4229
+ var PYTHON = {
4230
+ id: "python",
4231
+ lineComments: ["#"],
4232
+ blockComments: [],
4233
+ strings: [
4234
+ { open: '"""', close: '"""', escapes: true, multiline: true },
4235
+ { open: "'''", close: "'''", escapes: true, multiline: true },
4236
+ DQ,
4237
+ SQ
4238
+ ]
4239
+ };
4240
+ var SHELL = {
4241
+ id: "shell",
4242
+ lineComments: ["#"],
4243
+ blockComments: [],
4244
+ strings: [
4245
+ { open: '"', close: '"', escapes: true, multiline: true },
4246
+ { open: "'", close: "'", escapes: false, multiline: true }
4247
+ ],
4248
+ hashNeedsBoundary: true
4249
+ };
4250
+ var RUBY = {
4251
+ id: "ruby",
4252
+ lineComments: ["#"],
4253
+ blockComments: [],
4254
+ strings: [
4255
+ { open: '"', close: '"', escapes: true, multiline: true },
4256
+ { open: "'", close: "'", escapes: true, multiline: true }
4257
+ ]
4258
+ };
4259
+ var LEX_SPECS = Object.freeze({
4260
+ ".ts": JS,
4261
+ ".tsx": JS,
4262
+ ".mts": JS,
4263
+ ".cts": JS,
4264
+ ".js": JS,
4265
+ ".jsx": JS,
4266
+ ".mjs": JS,
4267
+ ".cjs": JS,
4268
+ ".c": C,
4269
+ ".h": C,
4270
+ ".cc": C,
4271
+ ".cpp": C,
4272
+ ".hpp": C,
4273
+ ".cs": C,
4274
+ ".dart": C,
4275
+ ".m": C,
4276
+ ".mm": C,
4277
+ ".go": GO,
4278
+ ".java": JVM,
4279
+ ".kt": JVM,
4280
+ ".kts": JVM,
4281
+ ".scala": JVM,
4282
+ ".groovy": JVM,
4283
+ ".swift": SWIFT,
4284
+ ".rs": RUST,
4285
+ ".py": PYTHON,
4286
+ ".pyi": PYTHON,
4287
+ ".sh": SHELL,
4288
+ ".bash": SHELL,
4289
+ ".zsh": SHELL,
4290
+ ".rb": RUBY
4291
+ });
4292
+ function lexSpecFor(path) {
4293
+ const ext = extname(path).toLowerCase();
4294
+ return Object.prototype.hasOwnProperty.call(LEX_SPECS, ext) ? LEX_SPECS[ext] : null;
4295
+ }
4296
+ var RUST_CHAR_RE = /^'(?:\\(?:u\{[0-9a-fA-F]{1,6}\}|x[0-9a-fA-F]{2}|.)|[^\\'\n])'/;
4297
+ var IDENT_CHAR_RE = /[A-Za-z0-9_$]/;
4298
+ var SHELL_WORD_BREAK = /* @__PURE__ */ new Set([" ", " ", ";", "|", "&", "(", ")"]);
4299
+ var REGEX_AFTER_KEYWORD = /* @__PURE__ */ new Set([
4300
+ "return",
4301
+ "typeof",
4302
+ "case",
4303
+ "in",
4304
+ "of",
4305
+ "delete",
4306
+ "void",
4307
+ "throw",
4308
+ "new",
4309
+ "yield",
4310
+ "await",
4311
+ "else",
4312
+ "do"
4313
+ ]);
4314
+ function regexLiteralLength(line, i, lastSig) {
4315
+ if (lastSig === ")" || lastSig === "]") return 0;
4316
+ if (lastSig !== "" && IDENT_CHAR_RE.test(lastSig)) {
4317
+ const m = /([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(line.slice(0, i));
4318
+ if (!m || !REGEX_AFTER_KEYWORD.has(m[1])) return 0;
4319
+ }
4320
+ let inClass = false;
4321
+ for (let j = i + 1; j < line.length; j++) {
4322
+ const ch = line[j];
4323
+ if (ch === "\\") {
4324
+ j++;
4325
+ continue;
4326
+ }
4327
+ if (inClass) {
4328
+ if (ch === "]") inClass = false;
4329
+ continue;
4330
+ }
4331
+ if (ch === "[") {
4332
+ inClass = true;
4333
+ continue;
4334
+ }
4335
+ if (ch === "/") return j - i + 1;
4336
+ }
4337
+ return 0;
4338
+ }
4339
+ function projectLine(line, spec, st) {
4340
+ const out = line.split("");
4341
+ const blank = (from, len) => {
4342
+ for (let k = from; k < from + len && k < out.length; k++) out[k] = " ";
4343
+ };
4344
+ let i = 0;
4345
+ while (i < line.length) {
4346
+ const mode = st.mode;
4347
+ if (mode.kind === "block") {
4348
+ if (line.startsWith(mode.close, i)) {
4349
+ blank(i, mode.close.length);
4350
+ i += mode.close.length;
4351
+ st.mode = { kind: "code" };
4352
+ } else {
4353
+ blank(i, 1);
4354
+ i++;
4355
+ }
4356
+ continue;
4357
+ }
4358
+ if (mode.kind === "string") {
4359
+ const r = mode.rule;
4360
+ if (r.escapes && line[i] === "\\") {
4361
+ blank(i, 2);
4362
+ i += 2;
4363
+ continue;
4364
+ }
4365
+ if (r.interpolation && line.startsWith("${", i)) {
4366
+ blank(i, 2);
4367
+ i += 2;
4368
+ st.holes.push(0);
4369
+ st.mode = { kind: "code" };
4370
+ st.lastSig = "";
4371
+ continue;
4372
+ }
4373
+ if (line.startsWith(r.close, i)) {
4374
+ blank(i, r.close.length);
4375
+ i += r.close.length;
4376
+ st.mode = { kind: "code" };
4377
+ st.lastSig = '"';
4378
+ continue;
4379
+ }
4380
+ blank(i, 1);
4381
+ i++;
4382
+ continue;
4383
+ }
4384
+ const ch = line[i];
4385
+ if (st.holes.length > 0) {
4386
+ const top = st.holes.length - 1;
4387
+ if (ch === "{") st.holes[top]++;
4388
+ else if (ch === "}") {
4389
+ if (st.holes[top] === 0) {
4390
+ st.holes.pop();
4391
+ blank(i, 1);
4392
+ i++;
4393
+ st.mode = { kind: "string", rule: spec.strings.find((s) => s.interpolation) };
4394
+ continue;
4395
+ }
4396
+ st.holes[top]--;
4397
+ }
4398
+ }
4399
+ const lc = spec.lineComments.find((m) => line.startsWith(m, i));
4400
+ if (lc && (!spec.hashNeedsBoundary || i === 0 || SHELL_WORD_BREAK.has(line[i - 1]))) {
4401
+ blank(i, line.length - i);
4402
+ break;
4403
+ }
4404
+ const bc = spec.blockComments.find(([open]) => line.startsWith(open, i));
4405
+ if (bc) {
4406
+ blank(i, bc[0].length);
4407
+ i += bc[0].length;
4408
+ st.mode = { kind: "block", close: bc[1] };
4409
+ continue;
4410
+ }
4411
+ if (spec.rustChars && ch === "'") {
4412
+ const m = RUST_CHAR_RE.exec(line.slice(i));
4413
+ if (m) {
4414
+ blank(i, m[0].length);
4415
+ i += m[0].length;
4416
+ st.lastSig = "'";
4417
+ continue;
4418
+ }
4419
+ i++;
4420
+ continue;
4421
+ }
4422
+ const sr = spec.strings.find((s) => line.startsWith(s.open, i));
4423
+ if (sr) {
4424
+ blank(i, sr.open.length);
4425
+ i += sr.open.length;
4426
+ st.mode = { kind: "string", rule: sr };
4427
+ continue;
4428
+ }
4429
+ if (spec.regexLiterals && ch === "/") {
4430
+ const len = regexLiteralLength(line, i, st.lastSig);
4431
+ if (len > 0) {
4432
+ blank(i, len);
4433
+ i += len;
4434
+ st.lastSig = "/";
4435
+ continue;
4436
+ }
4437
+ }
4438
+ if (ch !== " " && ch !== " ") st.lastSig = ch;
4439
+ i++;
4440
+ }
4441
+ if (st.mode.kind === "string" && !st.mode.rule.multiline) st.mode = { kind: "code" };
4442
+ return out.join("");
4443
+ }
4444
+ function projectCode(lines, spec) {
4445
+ const st = { mode: { kind: "code" }, holes: [], lastSig: "" };
4446
+ return lines.map((l) => projectLine(l, spec, st));
4447
+ }
4448
+ function unquoteGitPath(p) {
4449
+ const s = p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
4450
+ return s.startsWith("b/") ? s.slice(2) : s;
4451
+ }
4452
+ function diffCodeView(diff, opts = {}) {
4453
+ const files = [];
4454
+ let cur = null;
4455
+ let hunk = null;
4456
+ let newLine = 0;
4457
+ let inHeader = false;
4458
+ const startFile = (path) => {
4459
+ const f = { path, hunks: [], removed: [] };
4460
+ files.push(f);
4461
+ return f;
4462
+ };
4463
+ for (const line of diff.split("\n")) {
4464
+ if (line.startsWith("diff --git ")) {
4465
+ cur = startFile("");
4466
+ hunk = null;
4467
+ inHeader = true;
4468
+ continue;
4469
+ }
4470
+ if (inHeader && line.startsWith("+++ ")) {
4471
+ const p = line.slice(4).trim();
4472
+ if (cur) cur.path = p === "/dev/null" ? "" : unquoteGitPath(p);
4473
+ continue;
4474
+ }
4475
+ if (inHeader && (line.startsWith("--- ") || !line.startsWith("@@"))) continue;
4476
+ const hh = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
4477
+ if (hh) {
4478
+ inHeader = false;
4479
+ if (!cur) cur = startFile("");
4480
+ hunk = { newSide: [], addedAt: [] };
4481
+ cur.hunks.push(hunk);
4482
+ newLine = parseInt(hh[1], 10);
4483
+ continue;
4484
+ }
4485
+ if (!cur) cur = startFile("");
4486
+ if (!hunk) {
4487
+ hunk = { newSide: [], addedAt: [] };
4488
+ cur.hunks.push(hunk);
4489
+ newLine = 1;
4490
+ }
4491
+ if (line.startsWith("+")) {
4492
+ hunk.addedAt.push({ idx: hunk.newSide.length, lineNo: newLine });
4493
+ hunk.newSide.push(line.slice(1));
4494
+ newLine++;
4495
+ } else if (line.startsWith("-")) {
4496
+ cur.removed.push(line.slice(1));
4497
+ } else if (line.startsWith(" ")) {
4498
+ hunk.newSide.push(line.slice(1));
4499
+ newLine++;
4500
+ }
4501
+ }
4502
+ return files.map((f) => {
4503
+ const spec = f.path ? lexSpecFor(f.path) : null;
4504
+ const rawAdded = f.hunks.flatMap(
4505
+ (h) => h.addedAt.map((a) => ({ lineNo: a.lineNo, raw: h.newSide[a.idx] }))
4506
+ );
4507
+ if (!spec) {
4508
+ return { path: f.path, mode: "raw", added: rawAdded.map((a) => ({ ...a, code: a.raw })), removed: f.removed };
4509
+ }
4510
+ const text = opts.postImage ? opts.postImage(f.path) : null;
4511
+ if (text !== null && text !== void 0) {
4512
+ const post = text.split(/\r\n|\n|\r/);
4513
+ if (rawAdded.every((a) => post[a.lineNo - 1] === a.raw)) {
4514
+ const proj = projectCode(post, spec);
4515
+ return {
4516
+ path: f.path,
4517
+ mode: "post-image",
4518
+ added: rawAdded.map((a) => ({ ...a, code: proj[a.lineNo - 1] })),
4519
+ removed: f.removed
4520
+ };
4521
+ }
4522
+ }
4523
+ const added = [];
4524
+ for (const h of f.hunks) {
4525
+ const proj = projectCode(h.newSide, spec);
4526
+ for (const a of h.addedAt) added.push({ lineNo: a.lineNo, raw: h.newSide[a.idx], code: proj[a.idx] });
4527
+ }
4528
+ return { path: f.path, mode: "hunk", added, removed: f.removed };
4529
+ });
4530
+ }
4531
+
4157
4532
  // src/hooks/auto-learning-pipeline.ts
4158
4533
  var HOOK_EVENT = "Stop";
4159
4534
  var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
@@ -4177,6 +4552,27 @@ var REMOVED_BROKEN_RE = /^-.*\b(?:bug|broken|crash|wrong|incorrect|typo|fail|err
4177
4552
  function getSessionFlagPath(sessionId) {
4178
4553
  return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
4179
4554
  }
4555
+ function getSessionAckPath(sessionId) {
4556
+ return join6(tmpdir2(), "massu-auto-learning", `shown-${sessionId.slice(0, 12)}.json`);
4557
+ }
4558
+ function matchKey(file, side, line) {
4559
+ return createHash("sha256").update(`${file}\0${side}\0${line}`).digest("hex").slice(0, 32);
4560
+ }
4561
+ function readShownKeys(ackPath) {
4562
+ try {
4563
+ const parsed = JSON.parse(readFileSync7(ackPath, "utf-8"));
4564
+ return new Set(Array.isArray(parsed.shown) ? parsed.shown.filter((k) => typeof k === "string") : []);
4565
+ } catch {
4566
+ return /* @__PURE__ */ new Set();
4567
+ }
4568
+ }
4569
+ function writeShownKeys(ackPath, keys) {
4570
+ mkdirSync7(dirname8(ackPath), { recursive: true });
4571
+ writeFileSync3(ackPath, JSON.stringify({ shown: [...keys].sort() }), "utf-8");
4572
+ }
4573
+ function freshMatches(matchKeys, shown) {
4574
+ return [...new Set(matchKeys)].filter((k) => !shown.has(k));
4575
+ }
4180
4576
  var RECENT_INCIDENT_WINDOW_MS = 12 * 60 * 60 * 1e3;
4181
4577
  function recentIncidentFiles(dir, nowMs = Date.now()) {
4182
4578
  try {
@@ -4191,7 +4587,44 @@ function recentIncidentFiles(dir, nowMs = Date.now()) {
4191
4587
  return [];
4192
4588
  }
4193
4589
  }
4590
+ var MAX_POST_IMAGE_BYTES = MAX_FULL_DIFF_BYTES;
4591
+ function readPostImageFrom(root) {
4592
+ return (rel) => {
4593
+ try {
4594
+ const abs = join6(root, rel);
4595
+ if (statSync2(abs).size > MAX_POST_IMAGE_BYTES) return null;
4596
+ return readFileSync7(abs, "utf-8");
4597
+ } catch {
4598
+ return null;
4599
+ }
4600
+ };
4601
+ }
4602
+ function scoreDiff(diff, postImage) {
4603
+ const fixRe = new RegExp(FIX_PATTERN_RE.source);
4604
+ const brokenRe = new RegExp(REMOVED_BROKEN_RE.source);
4605
+ let fixPatterns = 0;
4606
+ let removedBroken = 0;
4607
+ const matchKeys = [];
4608
+ const lexModes = {};
4609
+ for (const view of diffCodeView(diff, { postImage })) {
4610
+ lexModes[view.path] = view.mode;
4611
+ for (const a of view.added) {
4612
+ if (fixRe.test(`+${a.code}`)) {
4613
+ fixPatterns++;
4614
+ matchKeys.push(matchKey(view.path, "+", a.raw));
4615
+ }
4616
+ }
4617
+ for (const r of view.removed) {
4618
+ if (brokenRe.test(`-${r}`)) {
4619
+ removedBroken++;
4620
+ matchKeys.push(matchKey(view.path, "-", r));
4621
+ }
4622
+ }
4623
+ }
4624
+ return { fixPatterns, removedBroken, matchKeys, lexModes };
4625
+ }
4194
4626
  function scanUncommittedForFix(opts) {
4627
+ const none = { fixPatterns: 0, removedBroken: 0, matchKeys: [], lexModes: {} };
4195
4628
  const git = opts.git ?? ((args, t, m) => execFileSync("git", [...args], {
4196
4629
  cwd: opts.root,
4197
4630
  // `cwd` does NOT scope git: GIT_DIR in the environment outranks it, and a leaked one
@@ -4204,40 +4637,58 @@ function scanUncommittedForFix(opts) {
4204
4637
  maxBuffer: m
4205
4638
  }));
4206
4639
  if (opts.sessionTouched === null) {
4207
- return { uncommittedFix: false, reason: "unattributable", filesScanned: 0 };
4640
+ return { uncommittedFix: false, reason: "unattributable", filesScanned: 0, ...none };
4208
4641
  }
4209
4642
  if (opts.sessionTouched.size === 0) {
4210
- return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0 };
4643
+ return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0, ...none };
4211
4644
  }
4212
4645
  try {
4213
4646
  const nameOnly = git(["diff", "--name-only", ...CODE_ONLY_PATHSPEC], 3e3, 1024 * 1024);
4214
4647
  const changed = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
4215
4648
  if (changed.length === 0) {
4216
- return { uncommittedFix: false, reason: "no-changes", filesScanned: 0 };
4649
+ return { uncommittedFix: false, reason: "no-changes", filesScanned: 0, ...none };
4217
4650
  }
4218
4651
  const mine = changed.filter((f) => opts.sessionTouched.has(f));
4219
4652
  if (mine.length === 0) {
4220
- return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0 };
4653
+ return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0, ...none };
4221
4654
  }
4222
4655
  const scoped = mine.length <= MAX_SESSION_PATHSPEC_FILES ? ["--", ...mine, ...DOC_EXCLUDE_PATHSPEC] : [...CODE_ONLY_PATHSPEC];
4223
4656
  const shortstat = git(["diff", "--shortstat", ...scoped], 2e3, 64 * 1024);
4224
4657
  const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
4225
4658
  const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
4226
4659
  if ((insertions + deletions) * 80 > MAX_FULL_DIFF_BYTES) {
4227
- return { uncommittedFix: false, reason: "over-cap", filesScanned: mine.length };
4660
+ return { uncommittedFix: false, reason: "over-cap", filesScanned: mine.length, ...none };
4228
4661
  }
4229
4662
  const fullDiff = git(["diff", ...scoped], 5e3, MAX_FULL_DIFF_BYTES);
4230
- const fixPatterns = (fullDiff.match(FIX_PATTERN_RE) || []).length;
4231
- const removedBroken = (fullDiff.match(REMOVED_BROKEN_RE) || []).length;
4232
- const hit = fixPatterns > 3 || removedBroken > 1;
4663
+ const scored = scoreDiff(fullDiff, opts.postImage ?? readPostImageFrom(opts.root));
4664
+ const hit = scored.fixPatterns > 3 || scored.removedBroken > 1;
4233
4665
  return {
4234
4666
  uncommittedFix: hit,
4235
4667
  reason: hit ? "fix-detected" : "no-signal",
4236
- filesScanned: mine.length
4668
+ filesScanned: mine.length,
4669
+ ...scored
4237
4670
  };
4238
4671
  } catch {
4239
- return { uncommittedFix: false, reason: "git-unavailable", filesScanned: 0 };
4672
+ return { uncommittedFix: false, reason: "git-unavailable", filesScanned: 0, ...none };
4673
+ }
4674
+ }
4675
+ function formatFixSummary(sessionFixes) {
4676
+ if (sessionFixes.length === 0) return [];
4677
+ const byFile = /* @__PURE__ */ new Map();
4678
+ for (const fix of sessionFixes) {
4679
+ const existing = byFile.get(fix.file) ?? [];
4680
+ existing.push(...fix.signals);
4681
+ byFile.set(fix.file, [...new Set(existing)]);
4682
+ }
4683
+ const lines = [
4684
+ "",
4685
+ ` ${sessionFixes.length} fix signal(s) across ${byFile.size} file(s) detected during this session:`,
4686
+ ""
4687
+ ];
4688
+ for (const [file, signals] of byFile) {
4689
+ lines.push(` - ${file} (${signals.join(", ")})`);
4240
4690
  }
4691
+ return lines;
4241
4692
  }
4242
4693
  async function main() {
4243
4694
  try {
@@ -4260,14 +4711,29 @@ async function main() {
4260
4711
  } catch {
4261
4712
  }
4262
4713
  }
4263
- const uncommittedFix = scanUncommittedForFix({
4714
+ const scan = scanUncommittedForFix({
4264
4715
  root,
4265
4716
  sessionTouched: sessionTouchedFiles({
4266
4717
  root,
4267
4718
  transcriptPath: hookInput.transcript_path,
4268
4719
  flagPath
4269
4720
  })
4270
- }).uncommittedFix;
4721
+ });
4722
+ const ackPath = getSessionAckPath(hookInput.session_id);
4723
+ const shown = readShownKeys(ackPath);
4724
+ const fresh = scan.uncommittedFix ? freshMatches(scan.matchKeys, shown) : [];
4725
+ const uncommittedFix = fresh.length > 0;
4726
+ if (scan.uncommittedFix) {
4727
+ try {
4728
+ writeShownKeys(ackPath, /* @__PURE__ */ new Set([...shown, ...scan.matchKeys]));
4729
+ } catch {
4730
+ }
4731
+ }
4732
+ const modes = Object.entries(scan.lexModes);
4733
+ const rawFallback = modes.filter(([, m]) => m === "raw").map(([f]) => f || "<headerless>");
4734
+ process.stderr.write(
4735
+ `[massu auto-learning] scan reason=${scan.reason} files=${scan.filesScanned} fixPatterns=${scan.fixPatterns} removedBroken=${scan.removedBroken} lexed=${modes.length - rawFallback.length}/${modes.length}` + (rawFallback.length ? ` raw-fallback=[${rawFallback.join(", ")}]` : "") + (scan.uncommittedFix ? ` new-lines=${fresh.length} (${fresh.length === 0 ? "already shown \u2014 silent" : "notice"})` : "") + "\n"
4736
+ );
4271
4737
  if (sessionFixes.length === 0 && !uncommittedFix) {
4272
4738
  cleanup(flagPath);
4273
4739
  process.exit(0);
@@ -4278,20 +4744,7 @@ async function main() {
4278
4744
  lines.push("============================================================================");
4279
4745
  lines.push(" MASSU AUTO-LEARNING PIPELINE \u2014 ACTION REQUIRED BEFORE SESSION END");
4280
4746
  lines.push("============================================================================");
4281
- if (sessionFixes.length > 0) {
4282
- lines.push("");
4283
- lines.push(` ${sessionFixes.length} bug fix(es) detected during this session:`);
4284
- lines.push("");
4285
- const byFile = /* @__PURE__ */ new Map();
4286
- for (const fix of sessionFixes) {
4287
- const existing = byFile.get(fix.file) ?? [];
4288
- existing.push(...fix.signals);
4289
- byFile.set(fix.file, [...new Set(existing)]);
4290
- }
4291
- for (const [file, signals] of byFile) {
4292
- lines.push(` - ${file} (${signals.join(", ")})`);
4293
- }
4294
- }
4747
+ lines.push(...formatFixSummary(sessionFixes));
4295
4748
  if (uncommittedFix) {
4296
4749
  lines.push("");
4297
4750
  lines.push(" Additional uncommitted fix patterns detected in git diff.");
@@ -4381,9 +4834,17 @@ export {
4381
4834
  DOC_EXCLUDE_PATHSPEC,
4382
4835
  FIX_PATTERN_RE,
4383
4836
  MAX_FULL_DIFF_BYTES,
4837
+ MAX_POST_IMAGE_BYTES,
4384
4838
  MAX_SESSION_PATHSPEC_FILES,
4385
4839
  RECENT_INCIDENT_WINDOW_MS,
4386
4840
  REMOVED_BROKEN_RE,
4841
+ formatFixSummary,
4842
+ freshMatches,
4843
+ getSessionAckPath,
4844
+ matchKey,
4845
+ readShownKeys,
4387
4846
  recentIncidentFiles,
4388
- scanUncommittedForFix
4847
+ scanUncommittedForFix,
4848
+ scoreDiff,
4849
+ writeShownKeys
4389
4850
  };