@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
|
@@ -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
|
-
}).
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
266
|
-
}).
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
603
|
-
}).
|
|
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
|
-
}).
|
|
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
|
-
}).
|
|
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([
|
|
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(),
|
|
@@ -697,6 +735,13 @@ var init_config = __esm({
|
|
|
697
735
|
});
|
|
698
736
|
LanguageFrameworkEntrySchema = z2.object({
|
|
699
737
|
framework: z2.string().optional(),
|
|
738
|
+
// `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
|
|
739
|
+
// by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
|
|
740
|
+
// `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
|
|
741
|
+
// derives the index builders' candidate set from it, so it is typed here rather than
|
|
742
|
+
// read as an unknown: a consumer that has to shape-check its own SoT is one branch away
|
|
743
|
+
// from silently contributing `undefined` to a path predicate.
|
|
744
|
+
source_dirs: z2.array(z2.string()).optional(),
|
|
700
745
|
test_framework: z2.string().optional(),
|
|
701
746
|
test: z2.string().optional(),
|
|
702
747
|
runtime: z2.string().optional(),
|
|
@@ -787,14 +832,14 @@ var init_config = __esm({
|
|
|
787
832
|
project: z2.object({
|
|
788
833
|
name: z2.string().default("my-project"),
|
|
789
834
|
root: z2.string().default("auto")
|
|
790
|
-
}).
|
|
791
|
-
framework: FrameworkConfigSchema.
|
|
835
|
+
}).prefault({ name: "my-project", root: "auto" }),
|
|
836
|
+
framework: FrameworkConfigSchema.prefault({
|
|
792
837
|
type: "typescript",
|
|
793
838
|
router: "none",
|
|
794
839
|
orm: "none",
|
|
795
840
|
ui: "none"
|
|
796
841
|
}),
|
|
797
|
-
paths: PathsConfigSchema.
|
|
842
|
+
paths: PathsConfigSchema.prefault({ source: "src", aliases: { "@": "src" } }),
|
|
798
843
|
toolPrefix: z2.string().default("massu"),
|
|
799
844
|
dbAccessPattern: z2.string().optional(),
|
|
800
845
|
knownMismatches: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
|
|
@@ -3988,9 +4033,10 @@ var init_hook_failure_signal = __esm({
|
|
|
3988
4033
|
// src/hooks/auto-learning-pipeline.ts
|
|
3989
4034
|
init_config();
|
|
3990
4035
|
import { execFileSync } from "child_process";
|
|
3991
|
-
import {
|
|
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";
|
|
3992
4038
|
import { tmpdir as tmpdir2 } from "os";
|
|
3993
|
-
import { join as join6 } from "path";
|
|
4039
|
+
import { join as join6, dirname as dirname8 } from "path";
|
|
3994
4040
|
|
|
3995
4041
|
// src/hooks/lib/write-hook-message.ts
|
|
3996
4042
|
var HOOK_EVENTS = [
|
|
@@ -4029,17 +4075,627 @@ function writeHookContext(hookEventName, message) {
|
|
|
4029
4075
|
|
|
4030
4076
|
// src/hooks/auto-learning-pipeline.ts
|
|
4031
4077
|
init_hook_failure_signal();
|
|
4078
|
+
|
|
4079
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4080
|
+
import { pathToFileURL } from "url";
|
|
4081
|
+
function isDirectInvocation(moduleUrl) {
|
|
4082
|
+
const entry = process.argv[1];
|
|
4083
|
+
if (!entry) return false;
|
|
4084
|
+
try {
|
|
4085
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4086
|
+
} catch {
|
|
4087
|
+
return false;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
// src/hooks/lib/session-touched-files.ts
|
|
4092
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9, realpathSync } from "fs";
|
|
4093
|
+
import { relative, isAbsolute, resolve as resolve5 } from "path";
|
|
4094
|
+
var FILE_WRITING_TOOLS = Object.freeze({
|
|
4095
|
+
Edit: ["file_path"],
|
|
4096
|
+
Write: ["file_path"],
|
|
4097
|
+
MultiEdit: ["file_path"],
|
|
4098
|
+
NotebookEdit: ["notebook_path", "file_path"]
|
|
4099
|
+
});
|
|
4100
|
+
function canonical(p) {
|
|
4101
|
+
try {
|
|
4102
|
+
return realpathSync(p);
|
|
4103
|
+
} catch {
|
|
4104
|
+
return p;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
function toRepoRelative(root, filePath) {
|
|
4108
|
+
if (typeof filePath !== "string" || filePath === "") return null;
|
|
4109
|
+
const absRaw = isAbsolute(filePath) ? filePath : resolve5(root, filePath);
|
|
4110
|
+
const canonicalRoot = canonical(root);
|
|
4111
|
+
const abs = resolve5(canonical(resolve5(absRaw, "..")), absRaw.split("/").pop() ?? "");
|
|
4112
|
+
const rel = relative(canonicalRoot, abs).split("\\").join("/");
|
|
4113
|
+
if (rel === "" || rel.startsWith("../")) return null;
|
|
4114
|
+
return rel;
|
|
4115
|
+
}
|
|
4116
|
+
function transcriptTouchedFiles(root, transcriptPath) {
|
|
4117
|
+
let raw;
|
|
4118
|
+
try {
|
|
4119
|
+
raw = readFileSync6(transcriptPath, "utf-8");
|
|
4120
|
+
} catch {
|
|
4121
|
+
return null;
|
|
4122
|
+
}
|
|
4123
|
+
const out = /* @__PURE__ */ new Set();
|
|
4124
|
+
for (const line of raw.split("\n")) {
|
|
4125
|
+
if (!line.trim()) continue;
|
|
4126
|
+
let entry;
|
|
4127
|
+
try {
|
|
4128
|
+
entry = JSON.parse(line);
|
|
4129
|
+
} catch {
|
|
4130
|
+
continue;
|
|
4131
|
+
}
|
|
4132
|
+
const message = entry.message;
|
|
4133
|
+
const content = message?.content;
|
|
4134
|
+
if (!Array.isArray(content)) continue;
|
|
4135
|
+
for (const block of content) {
|
|
4136
|
+
if (!block || typeof block !== "object") continue;
|
|
4137
|
+
const b = block;
|
|
4138
|
+
if (b.type !== "tool_use") continue;
|
|
4139
|
+
const keys = b.name ? FILE_WRITING_TOOLS[b.name] : void 0;
|
|
4140
|
+
if (!keys || !b.input) continue;
|
|
4141
|
+
for (const key of keys) {
|
|
4142
|
+
const rel = toRepoRelative(root, b.input[key]);
|
|
4143
|
+
if (rel) out.add(rel);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
return out;
|
|
4148
|
+
}
|
|
4149
|
+
function flagFileTouchedFiles(root, flagPath) {
|
|
4150
|
+
if (!existsSync9(flagPath)) return null;
|
|
4151
|
+
let raw;
|
|
4152
|
+
try {
|
|
4153
|
+
raw = readFileSync6(flagPath, "utf-8");
|
|
4154
|
+
} catch {
|
|
4155
|
+
return null;
|
|
4156
|
+
}
|
|
4157
|
+
const out = /* @__PURE__ */ new Set();
|
|
4158
|
+
for (const line of raw.split("\n")) {
|
|
4159
|
+
if (!line.trim()) continue;
|
|
4160
|
+
try {
|
|
4161
|
+
const rel = toRepoRelative(root, JSON.parse(line).file ?? "");
|
|
4162
|
+
if (rel) out.add(rel);
|
|
4163
|
+
} catch {
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
return out;
|
|
4168
|
+
}
|
|
4169
|
+
function sessionTouchedFiles(opts) {
|
|
4170
|
+
const sources = [];
|
|
4171
|
+
if (opts.transcriptPath) sources.push(transcriptTouchedFiles(opts.root, opts.transcriptPath));
|
|
4172
|
+
if (opts.flagPath) sources.push(flagFileTouchedFiles(opts.root, opts.flagPath));
|
|
4173
|
+
const readable = sources.filter((s) => s !== null);
|
|
4174
|
+
if (readable.length === 0) return null;
|
|
4175
|
+
const union = /* @__PURE__ */ new Set();
|
|
4176
|
+
for (const s of readable) for (const f of s) union.add(f);
|
|
4177
|
+
return union;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
// src/hooks/lib/git-safe-env.ts
|
|
4181
|
+
var GIT_ENV_LEAKS = [
|
|
4182
|
+
"GIT_DIR",
|
|
4183
|
+
"GIT_WORK_TREE",
|
|
4184
|
+
"GIT_INDEX_FILE",
|
|
4185
|
+
"GIT_OBJECT_DIRECTORY",
|
|
4186
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
4187
|
+
"GIT_COMMON_DIR",
|
|
4188
|
+
"GIT_PREFIX"
|
|
4189
|
+
];
|
|
4190
|
+
function gitSafeEnv(extra = {}, source = process.env) {
|
|
4191
|
+
const e = { ...source, ...extra };
|
|
4192
|
+
for (const k of GIT_ENV_LEAKS) delete e[k];
|
|
4193
|
+
return e;
|
|
4194
|
+
}
|
|
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
|
+
|
|
4532
|
+
// src/hooks/auto-learning-pipeline.ts
|
|
4032
4533
|
var HOOK_EVENT = "Stop";
|
|
4033
4534
|
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4535
|
+
var DOC_EXCLUDE_PATHSPEC = [
|
|
4536
|
+
":(exclude)*.md",
|
|
4537
|
+
":(exclude)*.markdown",
|
|
4538
|
+
":(exclude)*.mdx",
|
|
4539
|
+
":(exclude)*.rst",
|
|
4540
|
+
":(exclude)*.adoc",
|
|
4541
|
+
":(exclude)*.txt",
|
|
4542
|
+
":(exclude)docs/**",
|
|
4543
|
+
// massu's OWN runtime artifacts: agent-result JSONL, workflow logs, rule candidates.
|
|
4544
|
+
// Audit findings quote source lines verbatim, so leaving these in scope re-creates the
|
|
4545
|
+
// prose-as-code false positive with the hook's own output as the input.
|
|
4546
|
+
":(exclude).massu/**"
|
|
4547
|
+
];
|
|
4548
|
+
var CODE_ONLY_PATHSPEC = ["--", ".", ...DOC_EXCLUDE_PATHSPEC];
|
|
4549
|
+
var MAX_SESSION_PATHSPEC_FILES = 500;
|
|
4550
|
+
var FIX_PATTERN_RE = /^\+.*(?:\b(?:try|except|catch|guard|throw|raise|assert|validate)\b|\bif\b.*\b(?:null|nil|None|undefined)\b)/gm;
|
|
4551
|
+
var REMOVED_BROKEN_RE = /^-.*\b(?:bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)\b/gm;
|
|
4034
4552
|
function getSessionFlagPath(sessionId) {
|
|
4035
4553
|
return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
4036
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
|
+
}
|
|
4576
|
+
var RECENT_INCIDENT_WINDOW_MS = 12 * 60 * 60 * 1e3;
|
|
4577
|
+
function recentIncidentFiles(dir, nowMs = Date.now()) {
|
|
4578
|
+
try {
|
|
4579
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).filter((f) => {
|
|
4580
|
+
try {
|
|
4581
|
+
return nowMs - statSync2(join6(dir, f)).mtimeMs <= RECENT_INCIDENT_WINDOW_MS;
|
|
4582
|
+
} catch {
|
|
4583
|
+
return false;
|
|
4584
|
+
}
|
|
4585
|
+
}).sort();
|
|
4586
|
+
} catch {
|
|
4587
|
+
return [];
|
|
4588
|
+
}
|
|
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
|
+
}
|
|
4626
|
+
function scanUncommittedForFix(opts) {
|
|
4627
|
+
const none = { fixPatterns: 0, removedBroken: 0, matchKeys: [], lexModes: {} };
|
|
4628
|
+
const git = opts.git ?? ((args, t, m) => execFileSync("git", [...args], {
|
|
4629
|
+
cwd: opts.root,
|
|
4630
|
+
// `cwd` does NOT scope git: GIT_DIR in the environment outranks it, and a leaked one
|
|
4631
|
+
// would make this read a different repository while looking correct. This hook runs
|
|
4632
|
+
// at Stop and is never reachable from a git hook, so nothing here depends on the
|
|
4633
|
+
// index a commit-time hook is handed — the variables are removed rather than asserted.
|
|
4634
|
+
env: gitSafeEnv(),
|
|
4635
|
+
timeout: t,
|
|
4636
|
+
encoding: "utf-8",
|
|
4637
|
+
maxBuffer: m
|
|
4638
|
+
}));
|
|
4639
|
+
if (opts.sessionTouched === null) {
|
|
4640
|
+
return { uncommittedFix: false, reason: "unattributable", filesScanned: 0, ...none };
|
|
4641
|
+
}
|
|
4642
|
+
if (opts.sessionTouched.size === 0) {
|
|
4643
|
+
return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0, ...none };
|
|
4644
|
+
}
|
|
4645
|
+
try {
|
|
4646
|
+
const nameOnly = git(["diff", "--name-only", ...CODE_ONLY_PATHSPEC], 3e3, 1024 * 1024);
|
|
4647
|
+
const changed = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
4648
|
+
if (changed.length === 0) {
|
|
4649
|
+
return { uncommittedFix: false, reason: "no-changes", filesScanned: 0, ...none };
|
|
4650
|
+
}
|
|
4651
|
+
const mine = changed.filter((f) => opts.sessionTouched.has(f));
|
|
4652
|
+
if (mine.length === 0) {
|
|
4653
|
+
return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0, ...none };
|
|
4654
|
+
}
|
|
4655
|
+
const scoped = mine.length <= MAX_SESSION_PATHSPEC_FILES ? ["--", ...mine, ...DOC_EXCLUDE_PATHSPEC] : [...CODE_ONLY_PATHSPEC];
|
|
4656
|
+
const shortstat = git(["diff", "--shortstat", ...scoped], 2e3, 64 * 1024);
|
|
4657
|
+
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
|
|
4658
|
+
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
4659
|
+
if ((insertions + deletions) * 80 > MAX_FULL_DIFF_BYTES) {
|
|
4660
|
+
return { uncommittedFix: false, reason: "over-cap", filesScanned: mine.length, ...none };
|
|
4661
|
+
}
|
|
4662
|
+
const fullDiff = git(["diff", ...scoped], 5e3, MAX_FULL_DIFF_BYTES);
|
|
4663
|
+
const scored = scoreDiff(fullDiff, opts.postImage ?? readPostImageFrom(opts.root));
|
|
4664
|
+
const hit = scored.fixPatterns > 3 || scored.removedBroken > 1;
|
|
4665
|
+
return {
|
|
4666
|
+
uncommittedFix: hit,
|
|
4667
|
+
reason: hit ? "fix-detected" : "no-signal",
|
|
4668
|
+
filesScanned: mine.length,
|
|
4669
|
+
...scored
|
|
4670
|
+
};
|
|
4671
|
+
} catch {
|
|
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(", ")})`);
|
|
4690
|
+
}
|
|
4691
|
+
return lines;
|
|
4692
|
+
}
|
|
4037
4693
|
async function main() {
|
|
4038
4694
|
try {
|
|
4039
4695
|
const input = await readStdin();
|
|
4040
4696
|
const hookInput = JSON.parse(input);
|
|
4041
4697
|
const config = getConfig();
|
|
4042
|
-
if (config.autoLearning?.enabled === false) {
|
|
4698
|
+
if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
|
|
4043
4699
|
process.exit(0);
|
|
4044
4700
|
return;
|
|
4045
4701
|
}
|
|
@@ -4049,46 +4705,35 @@ async function main() {
|
|
|
4049
4705
|
const autoLearn = config.autoLearning;
|
|
4050
4706
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4051
4707
|
let sessionFixes = [];
|
|
4052
|
-
if (
|
|
4708
|
+
if (existsSync10(flagPath)) {
|
|
4053
4709
|
try {
|
|
4054
|
-
sessionFixes =
|
|
4710
|
+
sessionFixes = readFileSync7(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4055
4711
|
} catch {
|
|
4056
4712
|
}
|
|
4057
4713
|
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
4075
|
-
const estimatedBytes = (insertions + deletions) * 80;
|
|
4076
|
-
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
4077
|
-
const fullDiff = execFileSync("git", ["diff"], {
|
|
4078
|
-
cwd: root,
|
|
4079
|
-
timeout: 5e3,
|
|
4080
|
-
encoding: "utf-8",
|
|
4081
|
-
maxBuffer: MAX_FULL_DIFF_BYTES
|
|
4082
|
-
});
|
|
4083
|
-
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
4084
|
-
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
4085
|
-
if (fixPatterns > 3 || removedBroken > 1) {
|
|
4086
|
-
uncommittedFix = true;
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4714
|
+
const scan = scanUncommittedForFix({
|
|
4715
|
+
root,
|
|
4716
|
+
sessionTouched: sessionTouchedFiles({
|
|
4717
|
+
root,
|
|
4718
|
+
transcriptPath: hookInput.transcript_path,
|
|
4719
|
+
flagPath
|
|
4720
|
+
})
|
|
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 {
|
|
4089
4730
|
}
|
|
4090
|
-
} catch {
|
|
4091
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
|
+
);
|
|
4092
4737
|
if (sessionFixes.length === 0 && !uncommittedFix) {
|
|
4093
4738
|
cleanup(flagPath);
|
|
4094
4739
|
process.exit(0);
|
|
@@ -4099,28 +4744,23 @@ async function main() {
|
|
|
4099
4744
|
lines.push("============================================================================");
|
|
4100
4745
|
lines.push(" MASSU AUTO-LEARNING PIPELINE \u2014 ACTION REQUIRED BEFORE SESSION END");
|
|
4101
4746
|
lines.push("============================================================================");
|
|
4102
|
-
|
|
4103
|
-
lines.push("");
|
|
4104
|
-
lines.push(` ${sessionFixes.length} bug fix(es) detected during this session:`);
|
|
4105
|
-
lines.push("");
|
|
4106
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
4107
|
-
for (const fix of sessionFixes) {
|
|
4108
|
-
const existing = byFile.get(fix.file) ?? [];
|
|
4109
|
-
existing.push(...fix.signals);
|
|
4110
|
-
byFile.set(fix.file, [...new Set(existing)]);
|
|
4111
|
-
}
|
|
4112
|
-
for (const [file, signals] of byFile) {
|
|
4113
|
-
lines.push(` - ${file} (${signals.join(", ")})`);
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4747
|
+
lines.push(...formatFixSummary(sessionFixes));
|
|
4116
4748
|
if (uncommittedFix) {
|
|
4117
4749
|
lines.push("");
|
|
4118
4750
|
lines.push(" Additional uncommitted fix patterns detected in git diff.");
|
|
4119
4751
|
}
|
|
4752
|
+
const todaysIncidents = recentIncidentFiles(join6(root, incidentDir));
|
|
4120
4753
|
lines.push("");
|
|
4121
|
-
|
|
4754
|
+
if (todaysIncidents.length > 0) {
|
|
4755
|
+
lines.push(" ALREADY SATISFIED TODAY (verify these cover the fixes above):");
|
|
4756
|
+
for (const f of todaysIncidents.slice(0, 5)) lines.push(` - ${incidentDir}/${f}`);
|
|
4757
|
+
lines.push("");
|
|
4758
|
+
lines.push(" If they do, nothing further is required. Otherwise:");
|
|
4759
|
+
} else {
|
|
4760
|
+
lines.push(" Complete these steps before this session ends:");
|
|
4761
|
+
}
|
|
4122
4762
|
lines.push("");
|
|
4123
|
-
if (autoLearn?.pipeline?.requireIncidentReport !== false) {
|
|
4763
|
+
if (autoLearn?.pipeline?.requireIncidentReport !== false && todaysIncidents.length === 0) {
|
|
4124
4764
|
lines.push(" STEP 1: INCIDENT REPORT");
|
|
4125
4765
|
lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
|
|
4126
4766
|
lines.push(" Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules");
|
|
@@ -4157,9 +4797,9 @@ async function main() {
|
|
|
4157
4797
|
}
|
|
4158
4798
|
function cleanup(flagPath) {
|
|
4159
4799
|
try {
|
|
4160
|
-
if (
|
|
4800
|
+
if (existsSync10(flagPath)) unlinkSync2(flagPath);
|
|
4161
4801
|
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4162
|
-
if (
|
|
4802
|
+
if (existsSync10(dir)) {
|
|
4163
4803
|
const now = Date.now();
|
|
4164
4804
|
for (const file of readdirSync2(dir)) {
|
|
4165
4805
|
const fullPath = join6(dir, file);
|
|
@@ -4176,17 +4816,35 @@ function cleanup(flagPath) {
|
|
|
4176
4816
|
}
|
|
4177
4817
|
}
|
|
4178
4818
|
function readStdin() {
|
|
4179
|
-
return new Promise((
|
|
4819
|
+
return new Promise((resolve6) => {
|
|
4180
4820
|
let data = "";
|
|
4181
4821
|
process.stdin.setEncoding("utf-8");
|
|
4182
4822
|
process.stdin.on("data", (chunk) => {
|
|
4183
4823
|
data += chunk;
|
|
4184
4824
|
});
|
|
4185
|
-
process.stdin.on("end", () =>
|
|
4186
|
-
setTimeout(() =>
|
|
4825
|
+
process.stdin.on("end", () => resolve6(data));
|
|
4826
|
+
setTimeout(() => resolve6(data), 5e3);
|
|
4187
4827
|
});
|
|
4188
4828
|
}
|
|
4189
|
-
|
|
4829
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4830
|
+
main();
|
|
4831
|
+
}
|
|
4190
4832
|
export {
|
|
4191
|
-
|
|
4833
|
+
CODE_ONLY_PATHSPEC,
|
|
4834
|
+
DOC_EXCLUDE_PATHSPEC,
|
|
4835
|
+
FIX_PATTERN_RE,
|
|
4836
|
+
MAX_FULL_DIFF_BYTES,
|
|
4837
|
+
MAX_POST_IMAGE_BYTES,
|
|
4838
|
+
MAX_SESSION_PATHSPEC_FILES,
|
|
4839
|
+
RECENT_INCIDENT_WINDOW_MS,
|
|
4840
|
+
REMOVED_BROKEN_RE,
|
|
4841
|
+
formatFixSummary,
|
|
4842
|
+
freshMatches,
|
|
4843
|
+
getSessionAckPath,
|
|
4844
|
+
matchKey,
|
|
4845
|
+
readShownKeys,
|
|
4846
|
+
recentIncidentFiles,
|
|
4847
|
+
scanUncommittedForFix,
|
|
4848
|
+
scoreDiff,
|
|
4849
|
+
writeShownKeys
|
|
4192
4850
|
};
|