@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.
- package/CHANGELOG.md +240 -0
- package/agents/massu-output-scorer.md +2 -2
- package/agents/massu-plan-auditor.md +1 -1
- package/commands/_shared-references/test-first-protocol.md +4 -4
- package/commands/_shared-references/verification-table.md +5 -5
- package/commands/massu-audit-deps.md +2 -2
- package/commands/massu-batch.md +3 -3
- package/commands/massu-checkpoint.md +7 -7
- package/commands/massu-ci-fix.md +1 -1
- package/commands/massu-cleanup.md +16 -16
- package/commands/massu-commit.md +7 -7
- package/commands/massu-create-plan.md +7 -7
- package/commands/massu-dead-code.md +3 -3
- package/commands/massu-debug/references/common-shortcuts.md +1 -1
- package/commands/massu-debug/references/investigation-phases.md +4 -4
- package/commands/massu-debug/references/report-format.md +2 -2
- package/commands/massu-deploy.md +1 -1
- package/commands/massu-deps.md +5 -5
- package/commands/massu-doc-gen.md +3 -3
- package/commands/massu-docs.md +6 -6
- package/commands/massu-estimate.md +3 -3
- package/commands/massu-golden-path/references/competitive-mode.md +2 -2
- package/commands/massu-golden-path/references/phase-1-plan-creation.md +1 -1
- package/commands/massu-golden-path/references/phase-2-implementation.md +2 -2
- package/commands/massu-golden-path/references/phase-4-commit.md +3 -3
- package/commands/massu-golden-path/references/phase-5-push.md +5 -5
- package/commands/massu-guide.md +3 -3
- package/commands/massu-hotfix.md +14 -14
- package/commands/massu-loop/references/checkpoint-audit.md +3 -3
- package/commands/massu-loop/references/iteration-structure.md +3 -3
- package/commands/massu-loop/references/loop-controller.md +5 -5
- package/commands/massu-loop/references/plan-extraction.md +4 -4
- package/commands/massu-loop/references/vr-plan-spec.md +3 -3
- package/commands/massu-loop-playwright.md +5 -5
- package/commands/massu-loop.md +1 -1
- package/commands/massu-new-feature.md +6 -24
- package/commands/massu-perf.md +1 -1
- package/commands/massu-plan.md +21 -21
- package/commands/massu-push-light.md +9 -9
- package/commands/massu-recap.md +1 -1
- package/commands/massu-refactor.md +8 -8
- package/commands/massu-release.md +3 -3
- package/commands/massu-review.md +1 -1
- package/commands/massu-rollback.md +6 -6
- package/commands/massu-scaffold-page.md +1 -1
- package/commands/massu-scaffold-router.md +1 -1
- package/commands/massu-status.md +9 -9
- package/commands/massu-tdd.md +2 -2
- package/commands/massu-test.md +13 -13
- package/commands/massu-verify-playwright.md +1 -1
- package/commands/massu-verify.md +20 -20
- package/dist/cli.js +1731 -976
- package/dist/hooks/auto-learning-pipeline.js +745 -87
- package/dist/hooks/classify-failure.js +747 -45
- package/dist/hooks/cost-tracker.js +89 -28
- package/dist/hooks/fix-detector.js +536 -48
- package/dist/hooks/incident-pipeline.js +96 -29
- package/dist/hooks/intent-suggester.js +89 -28
- package/dist/hooks/memory-recall.js +89 -28
- package/dist/hooks/post-edit-context.js +137 -29
- package/dist/hooks/post-tool-use.js +95 -30
- package/dist/hooks/pre-compact.js +89 -28
- package/dist/hooks/pre-delete-check.js +137 -31
- package/dist/hooks/pre-tool-use-gate.js +271 -43
- package/dist/hooks/quality-event.js +89 -28
- package/dist/hooks/rule-enforcement-pipeline.js +89 -28
- package/dist/hooks/security-gate.js +219 -39
- package/dist/hooks/session-end.js +92 -28
- package/dist/hooks/session-start.js +140 -62
- package/dist/hooks/user-prompt.js +89 -28
- package/fragments/feature-components.md +15 -0
- package/fragments/feature-components.python.md +15 -0
- package/fragments/feature-components.typescript.md +15 -0
- package/package.json +6 -5
- package/patterns/testing-patterns.md +8 -8
- package/protocols/plan-implementation.md +2 -2
- package/protocols/verification.md +4 -4
- package/reference/lessons-learned.md +4 -4
- package/reference/vr-verification-reference.md +9 -9
- package/templates/aspnet/massu.config.yaml +2 -0
- package/templates/go-chi/massu.config.yaml +2 -0
- package/templates/multi-runtime/massu.config.yaml +4 -2
- package/templates/phoenix/massu.config.yaml +2 -0
- package/templates/python-django/massu.config.yaml +3 -1
- package/templates/python-fastapi/massu.config.yaml +3 -1
- package/templates/python-flask/massu.config.yaml +3 -1
- package/templates/rails/massu.config.yaml +4 -2
- package/templates/rust-actix/massu.config.yaml +2 -0
- package/templates/spring/massu.config.yaml +2 -0
- package/templates/swift-ios/massu.config.yaml +3 -1
- package/templates/ts-nestjs/massu.config.yaml +2 -0
- package/templates/ts-nextjs/massu.config.yaml +2 -0
|
@@ -4,6 +4,12 @@ var __defProp = Object.defineProperty;
|
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
7
13
|
var __esm = (fn, res, err) => function __init() {
|
|
8
14
|
if (err) throw err[0];
|
|
9
15
|
try {
|
|
@@ -36,6 +42,15 @@ var init_memory_path = __esm({
|
|
|
36
42
|
}
|
|
37
43
|
});
|
|
38
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
|
+
|
|
39
54
|
// src/credentials.ts
|
|
40
55
|
import { homedir } from "os";
|
|
41
56
|
import { resolve } from "path";
|
|
@@ -137,7 +152,7 @@ var init_config_memory_schema = __esm({
|
|
|
137
152
|
message: "memory.recall.embedEndpoint must be an http(s) URL"
|
|
138
153
|
}).optional(),
|
|
139
154
|
embedModel: z.string().min(1).optional()
|
|
140
|
-
}).
|
|
155
|
+
}).prefault({}),
|
|
141
156
|
// --- Contradiction / supersede gate (plan-living-memory-slice-2-temporal-model, P5-001) ---
|
|
142
157
|
// When a new high-value memory (decision/correction) is written, find
|
|
143
158
|
// semantically-related existing records and, if the new one contradicts an
|
|
@@ -166,7 +181,7 @@ var init_config_memory_schema = __esm({
|
|
|
166
181
|
annotateSuperseded: z.boolean().default(false),
|
|
167
182
|
// Time budget (ms) for the contradiction check; exceeded → fail-open (ADD).
|
|
168
183
|
budgetMs: z.number().int().positive().default(800)
|
|
169
|
-
}).
|
|
184
|
+
}).prefault({}),
|
|
170
185
|
// --- Background consolidation, the "sleep-time" pass
|
|
171
186
|
// (plan-living-memory-slice-3-consolidation) ---
|
|
172
187
|
// Keeps memory sharp over years: dedupes, distills dying sessions into
|
|
@@ -212,7 +227,7 @@ var init_config_memory_schema = __esm({
|
|
|
212
227
|
// Surface optional upgrades (e.g. "a local model was detected") in chat.
|
|
213
228
|
suggestUpgrades: z.boolean().default(true),
|
|
214
229
|
suggestIntervalDays: z.number().int().positive().default(30)
|
|
215
|
-
}).
|
|
230
|
+
}).prefault({}),
|
|
216
231
|
// A-20 (Slice 4) — memory FILES: the mirror, and the one switch that decides whether
|
|
217
232
|
// Massu may ever WRITE into the user's memory directory.
|
|
218
233
|
//
|
|
@@ -237,7 +252,7 @@ var init_config_memory_schema = __esm({
|
|
|
237
252
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
238
253
|
// per-session cap bounds only the RATE, never the total.
|
|
239
254
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
240
|
-
}).
|
|
255
|
+
}).prefault({}),
|
|
241
256
|
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
242
257
|
// A decision made in one of your repos can surface in another — opt-in per
|
|
243
258
|
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
@@ -262,8 +277,8 @@ var init_config_memory_schema = __esm({
|
|
|
262
277
|
enabled: z.boolean().default(true),
|
|
263
278
|
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
264
279
|
minScore: z.number().optional()
|
|
265
|
-
}).
|
|
266
|
-
}).
|
|
280
|
+
}).prefault({})
|
|
281
|
+
}).prefault({})
|
|
267
282
|
}).optional();
|
|
268
283
|
}
|
|
269
284
|
});
|
|
@@ -443,13 +458,19 @@ function getResolvedPaths() {
|
|
|
443
458
|
settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
|
|
444
459
|
};
|
|
445
460
|
}
|
|
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;
|
|
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;
|
|
447
462
|
var init_config = __esm({
|
|
448
463
|
"src/config.ts"() {
|
|
449
464
|
"use strict";
|
|
450
465
|
init_memory_path();
|
|
466
|
+
init_corrections_md();
|
|
451
467
|
init_credentials();
|
|
452
468
|
init_config_memory_schema();
|
|
469
|
+
DEFAULT_KNOWLEDGE_SOURCE_FILES = [
|
|
470
|
+
"CLAUDE.md",
|
|
471
|
+
"MEMORY.md",
|
|
472
|
+
CORRECTIONS_FILENAME
|
|
473
|
+
];
|
|
453
474
|
DomainConfigSchema = z2.object({
|
|
454
475
|
name: z2.string().default("Unknown"),
|
|
455
476
|
routers: z2.array(z2.string()).default([]),
|
|
@@ -578,34 +599,40 @@ var init_config = __esm({
|
|
|
578
599
|
fixDetection: z2.object({
|
|
579
600
|
enabled: z2.boolean().default(true),
|
|
580
601
|
lookbackDays: z2.number().default(7),
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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({}),
|
|
592
619
|
failureClassification: z2.object({
|
|
593
620
|
enabled: z2.boolean().default(true),
|
|
594
621
|
thresholds: z2.object({
|
|
595
622
|
known: z2.number().default(5),
|
|
596
623
|
similar: z2.number().default(3)
|
|
597
|
-
}).
|
|
624
|
+
}).prefault({}),
|
|
598
625
|
scoring: z2.object({
|
|
599
626
|
diffPatternWeight: z2.number().default(3),
|
|
600
627
|
filePatternWeight: z2.number().default(2),
|
|
601
628
|
promptKeywordWeight: z2.number().default(2)
|
|
602
|
-
}).
|
|
603
|
-
}).
|
|
629
|
+
}).prefault({})
|
|
630
|
+
}).prefault({}),
|
|
604
631
|
pipeline: z2.object({
|
|
605
632
|
requireIncidentReport: z2.boolean().default(true),
|
|
606
633
|
requirePreventionRule: z2.boolean().default(true),
|
|
607
634
|
requireEnforcement: z2.boolean().default(true)
|
|
608
|
-
}).
|
|
635
|
+
}).prefault({}),
|
|
609
636
|
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
610
637
|
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
611
638
|
// a candidate to one of these entries when none of the framework
|
|
@@ -638,7 +665,7 @@ var init_config = __esm({
|
|
|
638
665
|
memory: z2.boolean().default(true),
|
|
639
666
|
analytics: z2.boolean().default(true),
|
|
640
667
|
audit: z2.boolean().default(true)
|
|
641
|
-
}).
|
|
668
|
+
}).prefault({ memory: true, analytics: true, audit: true })
|
|
642
669
|
}).optional();
|
|
643
670
|
ConventionsConfigSchema = z2.object({
|
|
644
671
|
claudeDirName: z2.string().default(".claude").refine(
|
|
@@ -669,7 +696,7 @@ var init_config = __esm({
|
|
|
669
696
|
"session-state",
|
|
670
697
|
"agents"
|
|
671
698
|
]),
|
|
672
|
-
knowledgeSourceFiles: z2.array(z2.string()).default([
|
|
699
|
+
knowledgeSourceFiles: z2.array(z2.string()).default([...DEFAULT_KNOWLEDGE_SOURCE_FILES]),
|
|
673
700
|
excludePatterns: z2.array(z2.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
|
|
674
701
|
}).optional();
|
|
675
702
|
PythonDomainConfigSchema = z2.object({
|
|
@@ -685,6 +712,23 @@ var init_config = __esm({
|
|
|
685
712
|
}).optional();
|
|
686
713
|
PathsConfigSchema = z2.object({
|
|
687
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(),
|
|
688
732
|
aliases: z2.record(z2.string(), z2.string()).default({ "@": "src" }),
|
|
689
733
|
monorepo_roots: z2.array(z2.string()).optional(),
|
|
690
734
|
routers: z2.string().optional(),
|
|
@@ -697,6 +741,13 @@ var init_config = __esm({
|
|
|
697
741
|
});
|
|
698
742
|
LanguageFrameworkEntrySchema = z2.object({
|
|
699
743
|
framework: z2.string().optional(),
|
|
744
|
+
// `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
|
|
745
|
+
// by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
|
|
746
|
+
// `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
|
|
747
|
+
// derives the index builders' candidate set from it, so it is typed here rather than
|
|
748
|
+
// read as an unknown: a consumer that has to shape-check its own SoT is one branch away
|
|
749
|
+
// from silently contributing `undefined` to a path predicate.
|
|
750
|
+
source_dirs: z2.array(z2.string()).optional(),
|
|
700
751
|
test_framework: z2.string().optional(),
|
|
701
752
|
test: z2.string().optional(),
|
|
702
753
|
runtime: z2.string().optional(),
|
|
@@ -787,14 +838,14 @@ var init_config = __esm({
|
|
|
787
838
|
project: z2.object({
|
|
788
839
|
name: z2.string().default("my-project"),
|
|
789
840
|
root: z2.string().default("auto")
|
|
790
|
-
}).
|
|
791
|
-
framework: FrameworkConfigSchema.
|
|
841
|
+
}).prefault({ name: "my-project", root: "auto" }),
|
|
842
|
+
framework: FrameworkConfigSchema.prefault({
|
|
792
843
|
type: "typescript",
|
|
793
844
|
router: "none",
|
|
794
845
|
orm: "none",
|
|
795
846
|
ui: "none"
|
|
796
847
|
}),
|
|
797
|
-
paths: PathsConfigSchema.
|
|
848
|
+
paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
|
|
798
849
|
toolPrefix: z2.string().default("massu"),
|
|
799
850
|
dbAccessPattern: z2.string().optional(),
|
|
800
851
|
knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
|
|
@@ -1814,8 +1865,8 @@ function resolveModelDir() {
|
|
|
1814
1865
|
function resolveWasmDir() {
|
|
1815
1866
|
try {
|
|
1816
1867
|
const req3 = createRequire3(import.meta.url);
|
|
1817
|
-
const
|
|
1818
|
-
return dirname6(
|
|
1868
|
+
const main3 = req3.resolve(ORT_PKG);
|
|
1869
|
+
return dirname6(main3);
|
|
1819
1870
|
} catch {
|
|
1820
1871
|
return null;
|
|
1821
1872
|
}
|
|
@@ -3988,9 +4039,352 @@ var init_memory_db = __esm({
|
|
|
3988
4039
|
// src/hooks/classify-failure.ts
|
|
3989
4040
|
init_config();
|
|
3990
4041
|
init_memory_db();
|
|
3991
|
-
import { existsSync as
|
|
4042
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4043
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
4044
|
+
import { join as join7, basename as basename3 } from "path";
|
|
4045
|
+
|
|
4046
|
+
// src/hooks/fix-detector.ts
|
|
4047
|
+
init_config();
|
|
4048
|
+
import { execFileSync } from "child_process";
|
|
4049
|
+
import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
3992
4050
|
import { tmpdir as tmpdir2 } from "os";
|
|
3993
|
-
import { join as join6
|
|
4051
|
+
import { join as join6 } from "path";
|
|
4052
|
+
|
|
4053
|
+
// src/hooks/lib/code-spans.ts
|
|
4054
|
+
import { extname } from "path";
|
|
4055
|
+
var DQ = { open: '"', close: '"', escapes: true, multiline: false };
|
|
4056
|
+
var SQ = { open: "'", close: "'", escapes: true, multiline: false };
|
|
4057
|
+
var C_COMMENTS = { lineComments: ["//"], blockComments: [["/*", "*/"]] };
|
|
4058
|
+
var JS = {
|
|
4059
|
+
id: "js",
|
|
4060
|
+
...C_COMMENTS,
|
|
4061
|
+
strings: [DQ, SQ, { open: "`", close: "`", escapes: true, multiline: true, interpolation: true }],
|
|
4062
|
+
regexLiterals: true
|
|
4063
|
+
};
|
|
4064
|
+
var C = { id: "c", ...C_COMMENTS, strings: [DQ, SQ] };
|
|
4065
|
+
var GO = {
|
|
4066
|
+
id: "go",
|
|
4067
|
+
...C_COMMENTS,
|
|
4068
|
+
strings: [DQ, SQ, { open: "`", close: "`", escapes: false, multiline: true }]
|
|
4069
|
+
};
|
|
4070
|
+
var JVM = {
|
|
4071
|
+
id: "jvm",
|
|
4072
|
+
...C_COMMENTS,
|
|
4073
|
+
strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ, SQ]
|
|
4074
|
+
};
|
|
4075
|
+
var SWIFT = {
|
|
4076
|
+
id: "swift",
|
|
4077
|
+
...C_COMMENTS,
|
|
4078
|
+
strings: [{ open: '"""', close: '"""', escapes: true, multiline: true }, DQ]
|
|
4079
|
+
};
|
|
4080
|
+
var RUST = {
|
|
4081
|
+
id: "rust",
|
|
4082
|
+
...C_COMMENTS,
|
|
4083
|
+
strings: [{ open: '"', close: '"', escapes: true, multiline: true }],
|
|
4084
|
+
rustChars: true
|
|
4085
|
+
};
|
|
4086
|
+
var PYTHON = {
|
|
4087
|
+
id: "python",
|
|
4088
|
+
lineComments: ["#"],
|
|
4089
|
+
blockComments: [],
|
|
4090
|
+
strings: [
|
|
4091
|
+
{ open: '"""', close: '"""', escapes: true, multiline: true },
|
|
4092
|
+
{ open: "'''", close: "'''", escapes: true, multiline: true },
|
|
4093
|
+
DQ,
|
|
4094
|
+
SQ
|
|
4095
|
+
]
|
|
4096
|
+
};
|
|
4097
|
+
var SHELL = {
|
|
4098
|
+
id: "shell",
|
|
4099
|
+
lineComments: ["#"],
|
|
4100
|
+
blockComments: [],
|
|
4101
|
+
strings: [
|
|
4102
|
+
{ open: '"', close: '"', escapes: true, multiline: true },
|
|
4103
|
+
{ open: "'", close: "'", escapes: false, multiline: true }
|
|
4104
|
+
],
|
|
4105
|
+
hashNeedsBoundary: true
|
|
4106
|
+
};
|
|
4107
|
+
var RUBY = {
|
|
4108
|
+
id: "ruby",
|
|
4109
|
+
lineComments: ["#"],
|
|
4110
|
+
blockComments: [],
|
|
4111
|
+
strings: [
|
|
4112
|
+
{ open: '"', close: '"', escapes: true, multiline: true },
|
|
4113
|
+
{ open: "'", close: "'", escapes: true, multiline: true }
|
|
4114
|
+
]
|
|
4115
|
+
};
|
|
4116
|
+
var LEX_SPECS = Object.freeze({
|
|
4117
|
+
".ts": JS,
|
|
4118
|
+
".tsx": JS,
|
|
4119
|
+
".mts": JS,
|
|
4120
|
+
".cts": JS,
|
|
4121
|
+
".js": JS,
|
|
4122
|
+
".jsx": JS,
|
|
4123
|
+
".mjs": JS,
|
|
4124
|
+
".cjs": JS,
|
|
4125
|
+
".c": C,
|
|
4126
|
+
".h": C,
|
|
4127
|
+
".cc": C,
|
|
4128
|
+
".cpp": C,
|
|
4129
|
+
".hpp": C,
|
|
4130
|
+
".cs": C,
|
|
4131
|
+
".dart": C,
|
|
4132
|
+
".m": C,
|
|
4133
|
+
".mm": C,
|
|
4134
|
+
".go": GO,
|
|
4135
|
+
".java": JVM,
|
|
4136
|
+
".kt": JVM,
|
|
4137
|
+
".kts": JVM,
|
|
4138
|
+
".scala": JVM,
|
|
4139
|
+
".groovy": JVM,
|
|
4140
|
+
".swift": SWIFT,
|
|
4141
|
+
".rs": RUST,
|
|
4142
|
+
".py": PYTHON,
|
|
4143
|
+
".pyi": PYTHON,
|
|
4144
|
+
".sh": SHELL,
|
|
4145
|
+
".bash": SHELL,
|
|
4146
|
+
".zsh": SHELL,
|
|
4147
|
+
".rb": RUBY
|
|
4148
|
+
});
|
|
4149
|
+
function lexSpecFor(path) {
|
|
4150
|
+
const ext = extname(path).toLowerCase();
|
|
4151
|
+
return Object.prototype.hasOwnProperty.call(LEX_SPECS, ext) ? LEX_SPECS[ext] : null;
|
|
4152
|
+
}
|
|
4153
|
+
var RUST_CHAR_RE = /^'(?:\\(?:u\{[0-9a-fA-F]{1,6}\}|x[0-9a-fA-F]{2}|.)|[^\\'\n])'/;
|
|
4154
|
+
var IDENT_CHAR_RE = /[A-Za-z0-9_$]/;
|
|
4155
|
+
var SHELL_WORD_BREAK = /* @__PURE__ */ new Set([" ", " ", ";", "|", "&", "(", ")"]);
|
|
4156
|
+
var REGEX_AFTER_KEYWORD = /* @__PURE__ */ new Set([
|
|
4157
|
+
"return",
|
|
4158
|
+
"typeof",
|
|
4159
|
+
"case",
|
|
4160
|
+
"in",
|
|
4161
|
+
"of",
|
|
4162
|
+
"delete",
|
|
4163
|
+
"void",
|
|
4164
|
+
"throw",
|
|
4165
|
+
"new",
|
|
4166
|
+
"yield",
|
|
4167
|
+
"await",
|
|
4168
|
+
"else",
|
|
4169
|
+
"do"
|
|
4170
|
+
]);
|
|
4171
|
+
function regexLiteralLength(line, i, lastSig) {
|
|
4172
|
+
if (lastSig === ")" || lastSig === "]") return 0;
|
|
4173
|
+
if (lastSig !== "" && IDENT_CHAR_RE.test(lastSig)) {
|
|
4174
|
+
const m = /([A-Za-z_$][A-Za-z0-9_$]*)\s*$/.exec(line.slice(0, i));
|
|
4175
|
+
if (!m || !REGEX_AFTER_KEYWORD.has(m[1])) return 0;
|
|
4176
|
+
}
|
|
4177
|
+
let inClass = false;
|
|
4178
|
+
for (let j = i + 1; j < line.length; j++) {
|
|
4179
|
+
const ch = line[j];
|
|
4180
|
+
if (ch === "\\") {
|
|
4181
|
+
j++;
|
|
4182
|
+
continue;
|
|
4183
|
+
}
|
|
4184
|
+
if (inClass) {
|
|
4185
|
+
if (ch === "]") inClass = false;
|
|
4186
|
+
continue;
|
|
4187
|
+
}
|
|
4188
|
+
if (ch === "[") {
|
|
4189
|
+
inClass = true;
|
|
4190
|
+
continue;
|
|
4191
|
+
}
|
|
4192
|
+
if (ch === "/") return j - i + 1;
|
|
4193
|
+
}
|
|
4194
|
+
return 0;
|
|
4195
|
+
}
|
|
4196
|
+
function projectLine(line, spec, st) {
|
|
4197
|
+
const out = line.split("");
|
|
4198
|
+
const blank = (from, len) => {
|
|
4199
|
+
for (let k = from; k < from + len && k < out.length; k++) out[k] = " ";
|
|
4200
|
+
};
|
|
4201
|
+
let i = 0;
|
|
4202
|
+
while (i < line.length) {
|
|
4203
|
+
const mode = st.mode;
|
|
4204
|
+
if (mode.kind === "block") {
|
|
4205
|
+
if (line.startsWith(mode.close, i)) {
|
|
4206
|
+
blank(i, mode.close.length);
|
|
4207
|
+
i += mode.close.length;
|
|
4208
|
+
st.mode = { kind: "code" };
|
|
4209
|
+
} else {
|
|
4210
|
+
blank(i, 1);
|
|
4211
|
+
i++;
|
|
4212
|
+
}
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
if (mode.kind === "string") {
|
|
4216
|
+
const r = mode.rule;
|
|
4217
|
+
if (r.escapes && line[i] === "\\") {
|
|
4218
|
+
blank(i, 2);
|
|
4219
|
+
i += 2;
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
if (r.interpolation && line.startsWith("${", i)) {
|
|
4223
|
+
blank(i, 2);
|
|
4224
|
+
i += 2;
|
|
4225
|
+
st.holes.push(0);
|
|
4226
|
+
st.mode = { kind: "code" };
|
|
4227
|
+
st.lastSig = "";
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
if (line.startsWith(r.close, i)) {
|
|
4231
|
+
blank(i, r.close.length);
|
|
4232
|
+
i += r.close.length;
|
|
4233
|
+
st.mode = { kind: "code" };
|
|
4234
|
+
st.lastSig = '"';
|
|
4235
|
+
continue;
|
|
4236
|
+
}
|
|
4237
|
+
blank(i, 1);
|
|
4238
|
+
i++;
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
const ch = line[i];
|
|
4242
|
+
if (st.holes.length > 0) {
|
|
4243
|
+
const top = st.holes.length - 1;
|
|
4244
|
+
if (ch === "{") st.holes[top]++;
|
|
4245
|
+
else if (ch === "}") {
|
|
4246
|
+
if (st.holes[top] === 0) {
|
|
4247
|
+
st.holes.pop();
|
|
4248
|
+
blank(i, 1);
|
|
4249
|
+
i++;
|
|
4250
|
+
st.mode = { kind: "string", rule: spec.strings.find((s) => s.interpolation) };
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
st.holes[top]--;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
const lc = spec.lineComments.find((m) => line.startsWith(m, i));
|
|
4257
|
+
if (lc && (!spec.hashNeedsBoundary || i === 0 || SHELL_WORD_BREAK.has(line[i - 1]))) {
|
|
4258
|
+
blank(i, line.length - i);
|
|
4259
|
+
break;
|
|
4260
|
+
}
|
|
4261
|
+
const bc = spec.blockComments.find(([open]) => line.startsWith(open, i));
|
|
4262
|
+
if (bc) {
|
|
4263
|
+
blank(i, bc[0].length);
|
|
4264
|
+
i += bc[0].length;
|
|
4265
|
+
st.mode = { kind: "block", close: bc[1] };
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
if (spec.rustChars && ch === "'") {
|
|
4269
|
+
const m = RUST_CHAR_RE.exec(line.slice(i));
|
|
4270
|
+
if (m) {
|
|
4271
|
+
blank(i, m[0].length);
|
|
4272
|
+
i += m[0].length;
|
|
4273
|
+
st.lastSig = "'";
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4276
|
+
i++;
|
|
4277
|
+
continue;
|
|
4278
|
+
}
|
|
4279
|
+
const sr = spec.strings.find((s) => line.startsWith(s.open, i));
|
|
4280
|
+
if (sr) {
|
|
4281
|
+
blank(i, sr.open.length);
|
|
4282
|
+
i += sr.open.length;
|
|
4283
|
+
st.mode = { kind: "string", rule: sr };
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
if (spec.regexLiterals && ch === "/") {
|
|
4287
|
+
const len = regexLiteralLength(line, i, st.lastSig);
|
|
4288
|
+
if (len > 0) {
|
|
4289
|
+
blank(i, len);
|
|
4290
|
+
i += len;
|
|
4291
|
+
st.lastSig = "/";
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
if (ch !== " " && ch !== " ") st.lastSig = ch;
|
|
4296
|
+
i++;
|
|
4297
|
+
}
|
|
4298
|
+
if (st.mode.kind === "string" && !st.mode.rule.multiline) st.mode = { kind: "code" };
|
|
4299
|
+
return out.join("");
|
|
4300
|
+
}
|
|
4301
|
+
function projectCode(lines, spec) {
|
|
4302
|
+
const st = { mode: { kind: "code" }, holes: [], lastSig: "" };
|
|
4303
|
+
return lines.map((l) => projectLine(l, spec, st));
|
|
4304
|
+
}
|
|
4305
|
+
function unquoteGitPath(p) {
|
|
4306
|
+
const s = p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
4307
|
+
return s.startsWith("b/") ? s.slice(2) : s;
|
|
4308
|
+
}
|
|
4309
|
+
function diffCodeView(diff, opts = {}) {
|
|
4310
|
+
const files = [];
|
|
4311
|
+
let cur = null;
|
|
4312
|
+
let hunk = null;
|
|
4313
|
+
let newLine = 0;
|
|
4314
|
+
let inHeader = false;
|
|
4315
|
+
const startFile = (path) => {
|
|
4316
|
+
const f = { path, hunks: [], removed: [] };
|
|
4317
|
+
files.push(f);
|
|
4318
|
+
return f;
|
|
4319
|
+
};
|
|
4320
|
+
for (const line of diff.split("\n")) {
|
|
4321
|
+
if (line.startsWith("diff --git ")) {
|
|
4322
|
+
cur = startFile("");
|
|
4323
|
+
hunk = null;
|
|
4324
|
+
inHeader = true;
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
4327
|
+
if (inHeader && line.startsWith("+++ ")) {
|
|
4328
|
+
const p = line.slice(4).trim();
|
|
4329
|
+
if (cur) cur.path = p === "/dev/null" ? "" : unquoteGitPath(p);
|
|
4330
|
+
continue;
|
|
4331
|
+
}
|
|
4332
|
+
if (inHeader && (line.startsWith("--- ") || !line.startsWith("@@"))) continue;
|
|
4333
|
+
const hh = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
4334
|
+
if (hh) {
|
|
4335
|
+
inHeader = false;
|
|
4336
|
+
if (!cur) cur = startFile("");
|
|
4337
|
+
hunk = { newSide: [], addedAt: [] };
|
|
4338
|
+
cur.hunks.push(hunk);
|
|
4339
|
+
newLine = parseInt(hh[1], 10);
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
if (!cur) cur = startFile("");
|
|
4343
|
+
if (!hunk) {
|
|
4344
|
+
hunk = { newSide: [], addedAt: [] };
|
|
4345
|
+
cur.hunks.push(hunk);
|
|
4346
|
+
newLine = 1;
|
|
4347
|
+
}
|
|
4348
|
+
if (line.startsWith("+")) {
|
|
4349
|
+
hunk.addedAt.push({ idx: hunk.newSide.length, lineNo: newLine });
|
|
4350
|
+
hunk.newSide.push(line.slice(1));
|
|
4351
|
+
newLine++;
|
|
4352
|
+
} else if (line.startsWith("-")) {
|
|
4353
|
+
cur.removed.push(line.slice(1));
|
|
4354
|
+
} else if (line.startsWith(" ")) {
|
|
4355
|
+
hunk.newSide.push(line.slice(1));
|
|
4356
|
+
newLine++;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
return files.map((f) => {
|
|
4360
|
+
const spec = f.path ? lexSpecFor(f.path) : null;
|
|
4361
|
+
const rawAdded = f.hunks.flatMap(
|
|
4362
|
+
(h) => h.addedAt.map((a) => ({ lineNo: a.lineNo, raw: h.newSide[a.idx] }))
|
|
4363
|
+
);
|
|
4364
|
+
if (!spec) {
|
|
4365
|
+
return { path: f.path, mode: "raw", added: rawAdded.map((a) => ({ ...a, code: a.raw })), removed: f.removed };
|
|
4366
|
+
}
|
|
4367
|
+
const text = opts.postImage ? opts.postImage(f.path) : null;
|
|
4368
|
+
if (text !== null && text !== void 0) {
|
|
4369
|
+
const post = text.split(/\r\n|\n|\r/);
|
|
4370
|
+
if (rawAdded.every((a) => post[a.lineNo - 1] === a.raw)) {
|
|
4371
|
+
const proj = projectCode(post, spec);
|
|
4372
|
+
return {
|
|
4373
|
+
path: f.path,
|
|
4374
|
+
mode: "post-image",
|
|
4375
|
+
added: rawAdded.map((a) => ({ ...a, code: proj[a.lineNo - 1] })),
|
|
4376
|
+
removed: f.removed
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
const added = [];
|
|
4381
|
+
for (const h of f.hunks) {
|
|
4382
|
+
const proj = projectCode(h.newSide, spec);
|
|
4383
|
+
for (const a of h.addedAt) added.push({ lineNo: a.lineNo, raw: h.newSide[a.idx], code: proj[a.idx] });
|
|
4384
|
+
}
|
|
4385
|
+
return { path: f.path, mode: "hunk", added, removed: f.removed };
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
3994
4388
|
|
|
3995
4389
|
// src/hooks/lib/write-hook-message.ts
|
|
3996
4390
|
var HOOK_EVENTS = [
|
|
@@ -4027,9 +4421,311 @@ function writeHookContext(hookEventName, message) {
|
|
|
4027
4421
|
);
|
|
4028
4422
|
}
|
|
4029
4423
|
|
|
4030
|
-
// src/hooks/
|
|
4424
|
+
// src/hooks/fix-detector.ts
|
|
4031
4425
|
init_hook_failure_signal();
|
|
4426
|
+
|
|
4427
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4428
|
+
import { pathToFileURL } from "url";
|
|
4429
|
+
function isDirectInvocation(moduleUrl) {
|
|
4430
|
+
const entry = process.argv[1];
|
|
4431
|
+
if (!entry) return false;
|
|
4432
|
+
try {
|
|
4433
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4434
|
+
} catch {
|
|
4435
|
+
return false;
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
// src/hooks/fix-detector.ts
|
|
4032
4440
|
var HOOK_EVENT = "PostToolUse";
|
|
4441
|
+
var TEST_OR_FIXTURE_PATH_PATTERNS = [
|
|
4442
|
+
/(^|\/)(__tests__|__fixtures__|tests?|fixtures?|spec)\//,
|
|
4443
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/,
|
|
4444
|
+
/(^|\/)(test_[^/]+|[^/]+_test)\.py$/
|
|
4445
|
+
];
|
|
4446
|
+
function isTestOrFixturePath(filePath) {
|
|
4447
|
+
return TEST_OR_FIXTURE_PATH_PATTERNS.some((re) => re.test(filePath));
|
|
4448
|
+
}
|
|
4449
|
+
var FIX_HEURISTICS = [
|
|
4450
|
+
{
|
|
4451
|
+
name: "removed_broken_code",
|
|
4452
|
+
test: (diff) => /^-.*\b(bug|broken|wrong|incorrect|typo|crash|error|fail|miss|stale)\b/m.test(diff)
|
|
4453
|
+
},
|
|
4454
|
+
{
|
|
4455
|
+
name: "added_error_handling",
|
|
4456
|
+
// `if.*nil` / `if.*None` were DROPPED here, and that is deduplication rather than a silent
|
|
4457
|
+
// narrowing: `nil_handling_fix` below already matches exactly that shape
|
|
4458
|
+
// (`= nil|= None|is None|!= nil|is not None`), so a nil check scored twice under one banner.
|
|
4459
|
+
// Scored on each added line's CODE projection (`lib/code-spans.ts`), the same lexer the
|
|
4460
|
+
// Stop hook uses: a keyword in a string literal or a comment is not error handling. The
|
|
4461
|
+
// per-edit diff carries its `diff --git` header, so the file's language is known here
|
|
4462
|
+
// without threading a path through this table. A language with no lexer is judged raw.
|
|
4463
|
+
test: (diff) => {
|
|
4464
|
+
const re = /^\+.*\b(try|except|catch|guard|validate|assert|raise|throw)\b/;
|
|
4465
|
+
let added = 0;
|
|
4466
|
+
for (const view of diffCodeView(diff)) {
|
|
4467
|
+
for (const a of view.added) if (re.test(`+${a.code}`)) added++;
|
|
4468
|
+
}
|
|
4469
|
+
return added > 2;
|
|
4470
|
+
}
|
|
4471
|
+
},
|
|
4472
|
+
// ⛔ `method_name_correction` WAS RETIRED HERE 2026-08-20, and the measurement is why.
|
|
4473
|
+
//
|
|
4474
|
+
// test: (diff) => {
|
|
4475
|
+
// const removed = diff.match(/^-.*\.([a-z_]+)\(/m); // FIRST removed method call
|
|
4476
|
+
// const added = diff.match(/^\+.*\.([a-z_]+)\(/m); // FIRST added method call
|
|
4477
|
+
// return !!(removed && added && removed[1] !== added[1]);
|
|
4478
|
+
// }
|
|
4479
|
+
//
|
|
4480
|
+
// It compared the FIRST removed call against the FIRST added call, anywhere in the diff.
|
|
4481
|
+
// The two need no relationship to each other, so it fired whenever a diff contained any
|
|
4482
|
+
// two differently-named method calls — which is nearly every diff that touches code.
|
|
4483
|
+
//
|
|
4484
|
+
// Measured over the last 250 commits repo-wide:
|
|
4485
|
+
// shipped predicate 29 firings (12%)
|
|
4486
|
+
// one requiring the two lines to be identical but for the name 0 firings
|
|
4487
|
+
// Eleven firings read by hand over packages/core/src: `.push()`->`.includes()`,
|
|
4488
|
+
// `.call()`->`.system()`, `.trim()`->`.stringify()`, `.all()`->`.cwd()`, `.now()`->`.push()`,
|
|
4489
|
+
// `.on()`->`.sort()`, `.test()`->`.split()` … NOT ONE was a rename. Precision 0 of 29.
|
|
4490
|
+
//
|
|
4491
|
+
// RETIRED RATHER THAN REPAIRED, on the same numbers: a correct implementation fires ZERO
|
|
4492
|
+
// times in 250 commits, so repairing it buys a branch that never runs — dead weight with a
|
|
4493
|
+
// maintenance cost (CR-71 in spirit: a detector that cannot fire is not a detector). It was
|
|
4494
|
+
// not free while it lived: signals feed `sessionFixes`, which drives the ACTION REQUIRED
|
|
4495
|
+
// banner demanding incidents, so this put noise directly on the retro pipeline — the one
|
|
4496
|
+
// banner that must not cry wolf (CR-83).
|
|
4497
|
+
//
|
|
4498
|
+
// Kept as a comment, not deleted outright, so the next author who reaches for
|
|
4499
|
+
// "detect a method rename from a diff" finds the measurement instead of rebuilding it.
|
|
4500
|
+
//
|
|
4501
|
+
// ⛔ THE FULL WRITE-UP IS DELIBERATELY NOT LINKED FROM HERE. This file SYNCS TO THE PUBLIC
|
|
4502
|
+
// MIRROR and the incident corpus does not, so a path citation would point at something
|
|
4503
|
+
// that does not exist there — and the leak guard refuses it, correctly. It caught exactly
|
|
4504
|
+
// this line. The measurement above is self-contained for that reason; search the internal
|
|
4505
|
+
// incident corpus for "a summary counted signals and listed files" if you need the rest.
|
|
4506
|
+
// (Memory: a-retro-citation-is-publishable-content.)
|
|
4507
|
+
{
|
|
4508
|
+
name: "auth_fix",
|
|
4509
|
+
// `authn`/`authz` are spelled out because `\bauth\b` no longer reaches them, and the
|
|
4510
|
+
// trailing `s?` keeps plurals (`tokens`, `credential`+`s`) that the boundary would drop.
|
|
4511
|
+
//
|
|
4512
|
+
// ⚠️ Backticks, not double quotes, and the plural is split — deliberately. CR-59's
|
|
4513
|
+
// drift-guard forbids a quoted `credentials` segment literal outside credentials.ts via
|
|
4514
|
+
// /['"]credentials['"]/, which cannot tell a string literal in CODE from quotation marks
|
|
4515
|
+
// in PROSE. Writing the word normally here turns that guard red. That is the SAME defect
|
|
4516
|
+
// class this file's own boundary fix addresses, in a second detector — recorded in the
|
|
4517
|
+
// incident rather than fixed here, because widening a security-adjacent guard is its own
|
|
4518
|
+
// change with its own blast radius.
|
|
4519
|
+
test: (diff) => /^\+.*\b(token|auth|authn|authz|header|X-Service|Bearer|credential)s?\b/im.test(diff)
|
|
4520
|
+
},
|
|
4521
|
+
{
|
|
4522
|
+
name: "nil_handling_fix",
|
|
4523
|
+
test: (diff) => /^\+.*(= nil|= None|\.isNil|is None|!= nil|is not None|guard let|if let|optional)/m.test(diff) && /^-/m.test(diff)
|
|
4524
|
+
},
|
|
4525
|
+
{
|
|
4526
|
+
name: "concurrency_fix",
|
|
4527
|
+
// `rate[-_. ]limit` — the original `rate.limit` used an UNESCAPED dot, so `.` matched any
|
|
4528
|
+
// character and the alternative fired on "corporate_limits". The class is now explicit
|
|
4529
|
+
// about which separators are intended.
|
|
4530
|
+
test: (diff) => /^\+.*\b(timeout|semaphore|lock|mutex|throttle|rate[-_. ]limit|max_conn)\b/im.test(diff)
|
|
4531
|
+
}
|
|
4532
|
+
// ⛔ `async_pattern_fix` AND `added_missing_import` WERE RETIRED HERE 2026-08-20.
|
|
4533
|
+
//
|
|
4534
|
+
// async_pattern_fix:
|
|
4535
|
+
// test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff)
|
|
4536
|
+
// && /^-/m.test(diff),
|
|
4537
|
+
// added_missing_import:
|
|
4538
|
+
// test: (diff) => /^\+.*(import|from.*import|require)/.test(diff)
|
|
4539
|
+
// && !/^-.*(import|from.*import|require)/m.test(diff),
|
|
4540
|
+
//
|
|
4541
|
+
// Both fire ZERO times across the last 250 commits — measured on the same run that produced
|
|
4542
|
+
// the boundary numbers above, over the 87 commits touching non-test source:
|
|
4543
|
+
//
|
|
4544
|
+
// async_pattern_fix 0 / 87
|
|
4545
|
+
// added_missing_import 0 / 87
|
|
4546
|
+
//
|
|
4547
|
+
// This is the identical finding that retired `method_name_correction` below, and it is
|
|
4548
|
+
// retired for the identical reason: a detector that never fires is not a detector, and it
|
|
4549
|
+
// still costs review attention and maintenance on every pass over this table (CR-71 in
|
|
4550
|
+
// spirit — a zero-caller chokepoint is a dead feature).
|
|
4551
|
+
//
|
|
4552
|
+
// WHY THEY CANNOT FIRE HERE, so the next author does not "fix" them back in: their tokens are
|
|
4553
|
+
// largely foreign to this codebase. `@MainActor` is Swift; `async with` is Python. Neither
|
|
4554
|
+
// appears in a TypeScript monorepo. `added_missing_import` requires added imports with NO
|
|
4555
|
+
// removed import anywhere in the same diff, which any edit touching an import block violates.
|
|
4556
|
+
// If this table is ever pointed at a Swift or Python corpus, re-measure before reinstating —
|
|
4557
|
+
// do not reinstate on the assumption that the language mix changed.
|
|
4558
|
+
//
|
|
4559
|
+
// Kept as comments rather than deleted so the measurement travels with the code, and so a
|
|
4560
|
+
// future author reaching for "detect an async-pattern fix" finds the number first.
|
|
4561
|
+
// (Same reasoning, and same publication constraint, as the block below: this file syncs to
|
|
4562
|
+
// the public mirror, so no internal path citations.)
|
|
4563
|
+
];
|
|
4564
|
+
function getSessionFlagPath(sessionId) {
|
|
4565
|
+
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4566
|
+
if (!existsSync9(dir)) {
|
|
4567
|
+
mkdirSync7(dir, { recursive: true });
|
|
4568
|
+
}
|
|
4569
|
+
return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
4570
|
+
}
|
|
4571
|
+
function _stateDir() {
|
|
4572
|
+
const dir = join6(tmpdir2(), "massu-fix-detector-state");
|
|
4573
|
+
if (!existsSync9(dir)) {
|
|
4574
|
+
mkdirSync7(dir, { recursive: true });
|
|
4575
|
+
}
|
|
4576
|
+
return dir;
|
|
4577
|
+
}
|
|
4578
|
+
function _cwdHash(cwd) {
|
|
4579
|
+
let h = 5381;
|
|
4580
|
+
for (let i = 0; i < cwd.length; i++) {
|
|
4581
|
+
h = (h << 5) + h + cwd.charCodeAt(i) | 0;
|
|
4582
|
+
}
|
|
4583
|
+
return Math.abs(h).toString(36);
|
|
4584
|
+
}
|
|
4585
|
+
function isGitWorkTreeFast(cwd) {
|
|
4586
|
+
try {
|
|
4587
|
+
const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
|
|
4588
|
+
if (existsSync9(cachePath)) {
|
|
4589
|
+
const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
4590
|
+
const ageMs = Date.now() - cached.ts;
|
|
4591
|
+
if (ageMs < 36e5) return cached.isWorkTree;
|
|
4592
|
+
}
|
|
4593
|
+
let isWorkTree = false;
|
|
4594
|
+
try {
|
|
4595
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 500, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4596
|
+
isWorkTree = true;
|
|
4597
|
+
} catch {
|
|
4598
|
+
isWorkTree = false;
|
|
4599
|
+
}
|
|
4600
|
+
try {
|
|
4601
|
+
appendFileSync3(cachePath, "");
|
|
4602
|
+
const fs = __require("fs");
|
|
4603
|
+
fs.writeFileSync(cachePath, JSON.stringify({ isWorkTree, ts: Date.now() }));
|
|
4604
|
+
} catch {
|
|
4605
|
+
}
|
|
4606
|
+
return isWorkTree;
|
|
4607
|
+
} catch {
|
|
4608
|
+
return false;
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
function _disabledFlagPath(sessionId) {
|
|
4612
|
+
return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
|
|
4613
|
+
}
|
|
4614
|
+
function isSessionAutoDisabled(sessionId) {
|
|
4615
|
+
try {
|
|
4616
|
+
return existsSync9(_disabledFlagPath(sessionId));
|
|
4617
|
+
} catch {
|
|
4618
|
+
return false;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
function markSessionAutoDisabled(sessionId, elapsedMs) {
|
|
4622
|
+
try {
|
|
4623
|
+
const fs = __require("fs");
|
|
4624
|
+
fs.writeFileSync(_disabledFlagPath(sessionId), `disabled: previous git diff took ${elapsedMs}ms
|
|
4625
|
+
`);
|
|
4626
|
+
} catch {
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
async function main() {
|
|
4630
|
+
try {
|
|
4631
|
+
const input = await readStdin();
|
|
4632
|
+
const hookInput = JSON.parse(input);
|
|
4633
|
+
const filePath = hookInput.tool_input?.file_path;
|
|
4634
|
+
if (!filePath || !existsSync9(filePath)) {
|
|
4635
|
+
process.exit(0);
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
if (!/\.(py|swift|ts|tsx|js|jsx|rs|go|rb|sh)$/.test(filePath)) {
|
|
4639
|
+
process.exit(0);
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
const config = getConfig();
|
|
4643
|
+
const incidentDir = config.autoLearning?.incidentDir ?? "docs/incidents";
|
|
4644
|
+
const memoryDir = config.autoLearning?.memoryDir ?? "memory";
|
|
4645
|
+
if (filePath.includes(incidentDir) || filePath.includes(memoryDir) || filePath.includes("MEMORY.md")) {
|
|
4646
|
+
process.exit(0);
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
if (isTestOrFixturePath(filePath)) {
|
|
4650
|
+
process.exit(0);
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
|
|
4654
|
+
process.exit(0);
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
const root = getProjectRoot();
|
|
4658
|
+
if (!isGitWorkTreeFast(root) || isSessionAutoDisabled(hookInput.session_id)) {
|
|
4659
|
+
process.exit(0);
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
let diff = "";
|
|
4663
|
+
try {
|
|
4664
|
+
const startMs = Date.now();
|
|
4665
|
+
diff = execFileSync("git", ["diff", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
4666
|
+
if (!diff) {
|
|
4667
|
+
diff = execFileSync("git", ["diff", "HEAD", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
4668
|
+
}
|
|
4669
|
+
const elapsedMs = Date.now() - startMs;
|
|
4670
|
+
if (elapsedMs > 2e3) {
|
|
4671
|
+
markSessionAutoDisabled(hookInput.session_id, elapsedMs);
|
|
4672
|
+
}
|
|
4673
|
+
} catch {
|
|
4674
|
+
process.exit(0);
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
if (!diff) {
|
|
4678
|
+
process.exit(0);
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
const enabledSignals = new Set(config.autoLearning?.fixDetection?.signals ?? FIX_HEURISTICS.map((h) => h.name));
|
|
4682
|
+
const detected = [];
|
|
4683
|
+
for (const heuristic of FIX_HEURISTICS) {
|
|
4684
|
+
if (enabledSignals.has(heuristic.name) && heuristic.test(diff)) {
|
|
4685
|
+
detected.push(heuristic.name);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
if (detected.length === 0) {
|
|
4689
|
+
process.exit(0);
|
|
4690
|
+
return;
|
|
4691
|
+
}
|
|
4692
|
+
const signal = {
|
|
4693
|
+
file: filePath,
|
|
4694
|
+
signals: detected,
|
|
4695
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4696
|
+
};
|
|
4697
|
+
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4698
|
+
appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
|
|
4699
|
+
const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
|
|
4700
|
+
if (lines.length === 1) {
|
|
4701
|
+
writeHookContext(
|
|
4702
|
+
HOOK_EVENT,
|
|
4703
|
+
`[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
} catch (err) {
|
|
4707
|
+
recordHookFailure("fix-detector", err);
|
|
4708
|
+
}
|
|
4709
|
+
process.exit(0);
|
|
4710
|
+
}
|
|
4711
|
+
function readStdin() {
|
|
4712
|
+
return new Promise((resolve5) => {
|
|
4713
|
+
let data = "";
|
|
4714
|
+
process.stdin.setEncoding("utf-8");
|
|
4715
|
+
process.stdin.on("data", (chunk) => {
|
|
4716
|
+
data += chunk;
|
|
4717
|
+
});
|
|
4718
|
+
process.stdin.on("end", () => resolve5(data));
|
|
4719
|
+
setTimeout(() => resolve5(data), 3e3);
|
|
4720
|
+
});
|
|
4721
|
+
}
|
|
4722
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4723
|
+
main();
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
// src/hooks/classify-failure.ts
|
|
4727
|
+
init_hook_failure_signal();
|
|
4728
|
+
var HOOK_EVENT2 = "PostToolUse";
|
|
4033
4729
|
var BUG_FIX_INDICATORS = [
|
|
4034
4730
|
/\b(catch|error|throw|fail|fix|bug|broken|missing|crash|wrong|typo|incorrect)\b/i
|
|
4035
4731
|
];
|
|
@@ -4037,7 +4733,7 @@ var CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
|
|
|
4037
4733
|
function getDedupeMarkerPath(sessionId, filePath) {
|
|
4038
4734
|
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4039
4735
|
const hash = simpleHash(filePath);
|
|
4040
|
-
return
|
|
4736
|
+
return join7(tmpdir3(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
|
|
4041
4737
|
}
|
|
4042
4738
|
function simpleHash(str) {
|
|
4043
4739
|
let h = 0;
|
|
@@ -4047,13 +4743,13 @@ function simpleHash(str) {
|
|
|
4047
4743
|
return Math.abs(h).toString(36);
|
|
4048
4744
|
}
|
|
4049
4745
|
function readFailureContextFiles() {
|
|
4050
|
-
const dir =
|
|
4746
|
+
const dir = tmpdir3();
|
|
4051
4747
|
let context = "";
|
|
4052
4748
|
try {
|
|
4053
4749
|
const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
|
|
4054
4750
|
for (const file of files) {
|
|
4055
4751
|
try {
|
|
4056
|
-
context += " " +
|
|
4752
|
+
context += " " + readFileSync7(join7(dir, file), "utf-8");
|
|
4057
4753
|
} catch {
|
|
4058
4754
|
}
|
|
4059
4755
|
}
|
|
@@ -4062,21 +4758,21 @@ function readFailureContextFiles() {
|
|
|
4062
4758
|
return context.trim();
|
|
4063
4759
|
}
|
|
4064
4760
|
function cleanupFailureContextFiles() {
|
|
4065
|
-
const dir =
|
|
4761
|
+
const dir = tmpdir3();
|
|
4066
4762
|
try {
|
|
4067
4763
|
const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
|
|
4068
4764
|
for (const file of files) {
|
|
4069
4765
|
try {
|
|
4070
|
-
unlinkSync2(
|
|
4766
|
+
unlinkSync2(join7(dir, file));
|
|
4071
4767
|
} catch {
|
|
4072
4768
|
}
|
|
4073
4769
|
}
|
|
4074
4770
|
} catch {
|
|
4075
4771
|
}
|
|
4076
4772
|
}
|
|
4077
|
-
async function
|
|
4773
|
+
async function main2() {
|
|
4078
4774
|
try {
|
|
4079
|
-
const input = await
|
|
4775
|
+
const input = await readStdin2();
|
|
4080
4776
|
const hookInput = JSON.parse(input);
|
|
4081
4777
|
const filePath = hookInput.tool_input?.file_path;
|
|
4082
4778
|
if (!filePath) {
|
|
@@ -4100,6 +4796,10 @@ async function main() {
|
|
|
4100
4796
|
process.exit(0);
|
|
4101
4797
|
return;
|
|
4102
4798
|
}
|
|
4799
|
+
if (isTestOrFixturePath(relPath) || isTestOrFixturePath(filePath)) {
|
|
4800
|
+
process.exit(0);
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4103
4803
|
const oldString = hookInput.tool_input?.old_string ?? "";
|
|
4104
4804
|
const newString = hookInput.tool_input?.new_string ?? "";
|
|
4105
4805
|
const content = hookInput.tool_input?.content ?? "";
|
|
@@ -4129,7 +4829,7 @@ async function main() {
|
|
|
4129
4829
|
return;
|
|
4130
4830
|
}
|
|
4131
4831
|
const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
|
|
4132
|
-
if (
|
|
4832
|
+
if (existsSync10(dedupeMarker)) {
|
|
4133
4833
|
process.exit(0);
|
|
4134
4834
|
return;
|
|
4135
4835
|
}
|
|
@@ -4171,7 +4871,7 @@ function outputKnownPattern(match) {
|
|
|
4171
4871
|
lines.push(` Covered by: ${match.rules.join(", ")}`);
|
|
4172
4872
|
}
|
|
4173
4873
|
lines.push(" No new rules needed. Reference existing incident if logging.");
|
|
4174
|
-
writeHookContext(
|
|
4874
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4175
4875
|
}
|
|
4176
4876
|
function outputSimilarPattern(match) {
|
|
4177
4877
|
const lines = [];
|
|
@@ -4181,7 +4881,7 @@ function outputSimilarPattern(match) {
|
|
|
4181
4881
|
lines.push(` Check if existing rules cover this case: ${match.rules.join(", ")}`);
|
|
4182
4882
|
}
|
|
4183
4883
|
lines.push(" If genuinely new: create incident + prevention rule + enforcement.");
|
|
4184
|
-
writeHookContext(
|
|
4884
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4185
4885
|
}
|
|
4186
4886
|
function outputNewPattern(fileName, match) {
|
|
4187
4887
|
const lines = [];
|
|
@@ -4195,9 +4895,9 @@ function outputNewPattern(fileName, match) {
|
|
|
4195
4895
|
lines.push(" 1. INCIDENT REPORT");
|
|
4196
4896
|
lines.push(" 2. PREVENTION RULE (if new failure pattern)");
|
|
4197
4897
|
lines.push(" 3. ENFORCEMENT (hook or static check)");
|
|
4198
|
-
writeHookContext(
|
|
4898
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4199
4899
|
}
|
|
4200
|
-
function
|
|
4900
|
+
function readStdin2() {
|
|
4201
4901
|
return new Promise((resolve5) => {
|
|
4202
4902
|
let data = "";
|
|
4203
4903
|
process.stdin.setEncoding("utf-8");
|
|
@@ -4208,4 +4908,6 @@ function readStdin() {
|
|
|
4208
4908
|
setTimeout(() => resolve5(data), 3e3);
|
|
4209
4909
|
});
|
|
4210
4910
|
}
|
|
4211
|
-
|
|
4911
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4912
|
+
main2();
|
|
4913
|
+
}
|