@meetless/mla 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Agent-failure telemetry: the central sanitizer, the local deadletter, and the
|
|
3
|
+
// F8 (telemetry-upload-failed) detector. The TS half of the
|
|
4
|
+
// notes/20260608-agent-failure-telemetry-sentry-proposal.md §8 contracts.
|
|
5
|
+
//
|
|
6
|
+
// Three §8 invariants live here:
|
|
7
|
+
// INV-TELEMETRY-METADATA-CLASSIFICATION -> sanitizeTelemetry (allowlist, fail closed)
|
|
8
|
+
// INV-DEADLETTER-SAFETY -> appendDeadletter / flushDeadletter (0600, bounded, TTL, backoff)
|
|
9
|
+
// F8 detector -> recordTelemetryUploadFailure
|
|
10
|
+
//
|
|
11
|
+
// The Python twin is intel app/core/telemetry_sanitizer.py + failure_telemetry.py.
|
|
12
|
+
// The two MUST classify the same field names the same way; a field that hashes on
|
|
13
|
+
// one plane and ships raw on the other defeats the whole contract.
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.FAILURE_KB_WRITE_BLOCKED = exports.FAILURE_TELEMETRY_UPLOAD_FAILED = exports.DEADLETTER_MAX_ATTEMPTS = exports.DEADLETTER_TTL_MS = exports.DEADLETTER_MAX_BYTES = exports.DEADLETTER_MAX_RECORDS = exports.TELEMETRY_SCHEMA_VERSION = void 0;
|
|
49
|
+
exports.hashBasename = hashBasename;
|
|
50
|
+
exports.sanitizeTelemetry = sanitizeTelemetry;
|
|
51
|
+
exports.deadletterPath = deadletterPath;
|
|
52
|
+
exports.appendDeadletter = appendDeadletter;
|
|
53
|
+
exports.loadDeadletter = loadDeadletter;
|
|
54
|
+
exports.isAttemptDue = isAttemptDue;
|
|
55
|
+
exports.flushDeadletter = flushDeadletter;
|
|
56
|
+
exports.recordTelemetryUploadFailure = recordTelemetryUploadFailure;
|
|
57
|
+
exports.recordKbWriteBlocked = recordKbWriteBlocked;
|
|
58
|
+
const crypto = __importStar(require("crypto"));
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const os = __importStar(require("os"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const config_1 = require("./config");
|
|
63
|
+
// Schema version stamped on every deadletter record. Bump when the record shape
|
|
64
|
+
// changes so a post-upgrade replay can drop records it no longer understands
|
|
65
|
+
// (INV-DEADLETTER-SAFETY: schema-versioned).
|
|
66
|
+
exports.TELEMETRY_SCHEMA_VERSION = 1;
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// INV-TELEMETRY-METADATA-CLASSIFICATION: the single central sanitizer.
|
|
69
|
+
//
|
|
70
|
+
// One choke point. Every telemetry / deadletter payload passes through this
|
|
71
|
+
// before it leaves the process. The table is an ALLOWLIST: any field not
|
|
72
|
+
// classified here is dropped, so an unclassified field fails closed to
|
|
73
|
+
// "not sent" rather than open to "sent raw" (An, 2026-06-08). The next detector
|
|
74
|
+
// author who adds a field MUST add a row, which is the point.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Always sent verbatim: low-cardinality enums and the cross-system join keys.
|
|
77
|
+
// workspace_id / session_id are server-resolved identifiers, not free text.
|
|
78
|
+
const ALWAYS_SEND = new Set([
|
|
79
|
+
"failure_class",
|
|
80
|
+
"severity",
|
|
81
|
+
"trace_id",
|
|
82
|
+
"surface",
|
|
83
|
+
"workspace_id",
|
|
84
|
+
"session_id",
|
|
85
|
+
"schema_version",
|
|
86
|
+
"release",
|
|
87
|
+
"mla_version",
|
|
88
|
+
"platform",
|
|
89
|
+
"environment",
|
|
90
|
+
"window",
|
|
91
|
+
"tool", // tool NAME (e.g. "Bash", "Edit") is low-cardinality, not content
|
|
92
|
+
"posture",
|
|
93
|
+
"reason_code",
|
|
94
|
+
"status", // HTTP status code: low-cardinality integer, never content
|
|
95
|
+
"http_status",
|
|
96
|
+
"status_code",
|
|
97
|
+
]);
|
|
98
|
+
// Never sent: content and anything that can carry customer / security context.
|
|
99
|
+
// Full paths and raw text are the canonical leak vectors (§8: a path like
|
|
100
|
+
// customers/acme/security-audit/oauth-migration.ts is itself sensitive).
|
|
101
|
+
const NEVER_SEND = new Set([
|
|
102
|
+
"full_path",
|
|
103
|
+
"path",
|
|
104
|
+
"file_path",
|
|
105
|
+
"query",
|
|
106
|
+
"query_text",
|
|
107
|
+
"answer",
|
|
108
|
+
"answer_text",
|
|
109
|
+
"prompt",
|
|
110
|
+
"prompt_text",
|
|
111
|
+
"tool_output",
|
|
112
|
+
"tool_input",
|
|
113
|
+
"doc_body",
|
|
114
|
+
"content",
|
|
115
|
+
"code",
|
|
116
|
+
"stdout",
|
|
117
|
+
"stderr",
|
|
118
|
+
"message_text",
|
|
119
|
+
]);
|
|
120
|
+
// File basenames are hashed (not sent raw): keeps the path out of the clear but
|
|
121
|
+
// preserves cross-event correlation via a stable digest. A full path smuggled
|
|
122
|
+
// under one of these keys is reduced to its basename first, then hashed.
|
|
123
|
+
const BASENAME_KEYS = new Set([
|
|
124
|
+
"file_basename",
|
|
125
|
+
"basename",
|
|
126
|
+
"file_name",
|
|
127
|
+
"filename",
|
|
128
|
+
"target_basename",
|
|
129
|
+
]);
|
|
130
|
+
// Nested metadata bags that are themselves sanitized field-by-field. Lets a
|
|
131
|
+
// detector pass a `metadata_only_context: { candidate_count: 3 }` without the
|
|
132
|
+
// whole object being dropped, while every field inside it still runs the table.
|
|
133
|
+
const CONTAINER_KEYS = new Set(["context", "metadata_only_context", "metadata"]);
|
|
134
|
+
// Numeric metadata: counts, durations, lengths, byte sizes, attempt counters.
|
|
135
|
+
// Only sent when the value is actually a number, so a string smuggled under a
|
|
136
|
+
// "*_count" key still drops.
|
|
137
|
+
const NUMERIC_METADATA_RE = /(?:_count|_ms|_seconds|_secs|_duration|_len|_length|_bytes|_attempts?|_size|_total|_index|_n)$|^(?:count|attempts|duration_ms|n|index|total)$/i;
|
|
138
|
+
// Hash a basename into a stable, non-reversible short digest. A full path is
|
|
139
|
+
// reduced to its last segment first (split on both separators so a Windows path
|
|
140
|
+
// is handled), so even a misclassified full path cannot leak its parent dirs.
|
|
141
|
+
function hashBasename(value) {
|
|
142
|
+
const base = value.split(/[\\/]/).filter(Boolean).pop() ?? value;
|
|
143
|
+
const digest = crypto.createHash("sha256").update(base).digest("hex").slice(0, 16);
|
|
144
|
+
return `b_${digest}`;
|
|
145
|
+
}
|
|
146
|
+
function isScalar(v) {
|
|
147
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
148
|
+
}
|
|
149
|
+
function isPlainObject(v) {
|
|
150
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
151
|
+
}
|
|
152
|
+
// The central sanitizer. Drop-by-default. Returns a new object; the input is
|
|
153
|
+
// never mutated. Recurses one level into known container keys so a detector's
|
|
154
|
+
// metadata bag is classified field-by-field, not waved through.
|
|
155
|
+
function sanitizeTelemetry(event) {
|
|
156
|
+
const out = {};
|
|
157
|
+
for (const [key, value] of Object.entries(event)) {
|
|
158
|
+
if (NEVER_SEND.has(key))
|
|
159
|
+
continue;
|
|
160
|
+
if (CONTAINER_KEYS.has(key) && isPlainObject(value)) {
|
|
161
|
+
out[key] = sanitizeTelemetry(value);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (BASENAME_KEYS.has(key) && typeof value === "string") {
|
|
165
|
+
out[`${key}_hash`] = hashBasename(value);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ALWAYS_SEND.has(key) && isScalar(value)) {
|
|
169
|
+
out[key] = value;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (NUMERIC_METADATA_RE.test(key) && typeof value === "number") {
|
|
173
|
+
out[key] = value;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Allowlist fail-closed: anything not classified above is dropped.
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// INV-DEADLETTER-SAFETY: the bounded, mode-0600, TTL-governed local store.
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
exports.DEADLETTER_MAX_RECORDS = 500;
|
|
184
|
+
exports.DEADLETTER_MAX_BYTES = 1_000_000; // 1 MB ceiling
|
|
185
|
+
exports.DEADLETTER_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
186
|
+
exports.DEADLETTER_MAX_ATTEMPTS = 5;
|
|
187
|
+
const DEADLETTER_BACKOFF_BASE_MS = 60_000; // 1 minute
|
|
188
|
+
const DEADLETTER_BACKOFF_CAP_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
189
|
+
const DEADLETTER_FILE_MODE = 0o600;
|
|
190
|
+
const DEADLETTER_DIR_MODE = 0o700;
|
|
191
|
+
// Mirror config.ts HOME resolution but compute per-call so a test can redirect
|
|
192
|
+
// the whole store to a temp dir via MEETLESS_HOME without module-cache games.
|
|
193
|
+
function homeDir(env) {
|
|
194
|
+
return env.MEETLESS_HOME || path.join(os.homedir(), ".meetless");
|
|
195
|
+
}
|
|
196
|
+
function deadletterPath(env = process.env) {
|
|
197
|
+
return path.join(homeDir(env), "telemetry-deadletter.jsonl");
|
|
198
|
+
}
|
|
199
|
+
function readRecords(file) {
|
|
200
|
+
let raw;
|
|
201
|
+
try {
|
|
202
|
+
raw = fs.readFileSync(file, "utf8");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const out = [];
|
|
208
|
+
for (const line of raw.split("\n")) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (!trimmed)
|
|
211
|
+
continue;
|
|
212
|
+
try {
|
|
213
|
+
const rec = JSON.parse(trimmed);
|
|
214
|
+
// Drop records from an older/newer schema we cannot safely replay.
|
|
215
|
+
if (rec && rec.schema_version === exports.TELEMETRY_SCHEMA_VERSION)
|
|
216
|
+
out.push(rec);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// A torn line (partial write) is skipped, not fatal.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function notExpired(rec, now) {
|
|
225
|
+
const exp = Date.parse(rec.expires_at);
|
|
226
|
+
return Number.isNaN(exp) ? true : now < exp;
|
|
227
|
+
}
|
|
228
|
+
// Enforce the count + byte ceilings by dropping OLDEST records first. The file
|
|
229
|
+
// is append-mostly, so position == age; keeping the tail keeps the freshest.
|
|
230
|
+
function enforceBounds(records) {
|
|
231
|
+
let kept = records;
|
|
232
|
+
if (kept.length > exports.DEADLETTER_MAX_RECORDS) {
|
|
233
|
+
kept = kept.slice(kept.length - exports.DEADLETTER_MAX_RECORDS);
|
|
234
|
+
}
|
|
235
|
+
let serialized = kept.map((r) => JSON.stringify(r)).join("\n");
|
|
236
|
+
while (kept.length > 0 && Buffer.byteLength(serialized, "utf8") > exports.DEADLETTER_MAX_BYTES) {
|
|
237
|
+
kept = kept.slice(1);
|
|
238
|
+
serialized = kept.map((r) => JSON.stringify(r)).join("\n");
|
|
239
|
+
}
|
|
240
|
+
return kept;
|
|
241
|
+
}
|
|
242
|
+
// Atomic-ish rewrite with locked-down perms. Writes the whole set (the store is
|
|
243
|
+
// low-volume: it only grows on an upload failure), then chmods to 0600 even when
|
|
244
|
+
// the file pre-existed with looser perms.
|
|
245
|
+
function writeRecords(file, records) {
|
|
246
|
+
const dir = path.dirname(file);
|
|
247
|
+
fs.mkdirSync(dir, { recursive: true, mode: DEADLETTER_DIR_MODE });
|
|
248
|
+
const body = records.length > 0 ? records.map((r) => JSON.stringify(r)).join("\n") + "\n" : "";
|
|
249
|
+
fs.writeFileSync(file, body, { mode: DEADLETTER_FILE_MODE });
|
|
250
|
+
try {
|
|
251
|
+
fs.chmodSync(file, DEADLETTER_FILE_MODE);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// best-effort on platforms without POSIX perms
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Append one failure event. Sanitizes BEFORE write so a replayed record can
|
|
258
|
+
// never carry content the live path would have stripped (§8, An Decision 9).
|
|
259
|
+
// Never throws: a telemetry write must not break the user's command.
|
|
260
|
+
function appendDeadletter(event, opts = {}) {
|
|
261
|
+
const env = opts.env ?? process.env;
|
|
262
|
+
const now = opts.now ?? Date.now();
|
|
263
|
+
try {
|
|
264
|
+
const sanitized = sanitizeTelemetry(event);
|
|
265
|
+
const record = {
|
|
266
|
+
schema_version: exports.TELEMETRY_SCHEMA_VERSION,
|
|
267
|
+
created_at: new Date(now).toISOString(),
|
|
268
|
+
expires_at: new Date(now + exports.DEADLETTER_TTL_MS).toISOString(),
|
|
269
|
+
attempts: 0,
|
|
270
|
+
last_attempt_at: null,
|
|
271
|
+
failure_class: String(sanitized.failure_class ?? "unknown"),
|
|
272
|
+
event: sanitized,
|
|
273
|
+
};
|
|
274
|
+
const file = deadletterPath(env);
|
|
275
|
+
const existing = readRecords(file).filter((r) => notExpired(r, now));
|
|
276
|
+
const next = enforceBounds([...existing, record]);
|
|
277
|
+
writeRecords(file, next);
|
|
278
|
+
return record;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Non-expired records, oldest first. Drops expired records as a side effect
|
|
285
|
+
// (rewrites the file) so a stale record cannot be retried forever.
|
|
286
|
+
function loadDeadletter(opts = {}) {
|
|
287
|
+
const env = opts.env ?? process.env;
|
|
288
|
+
const now = opts.now ?? Date.now();
|
|
289
|
+
const file = deadletterPath(env);
|
|
290
|
+
const all = readRecords(file);
|
|
291
|
+
const live = all.filter((r) => notExpired(r, now));
|
|
292
|
+
if (live.length !== all.length) {
|
|
293
|
+
try {
|
|
294
|
+
writeRecords(file, live);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// best effort
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return live;
|
|
301
|
+
}
|
|
302
|
+
// Exponential backoff gate. A fresh record (0 attempts) is due immediately;
|
|
303
|
+
// thereafter the next attempt waits base * 2^(attempts-1), capped, measured from
|
|
304
|
+
// the last attempt. Keeps a down backend from being hammered on every CLI run.
|
|
305
|
+
function isAttemptDue(rec, now) {
|
|
306
|
+
if (rec.attempts <= 0 || !rec.last_attempt_at)
|
|
307
|
+
return true;
|
|
308
|
+
const last = Date.parse(rec.last_attempt_at);
|
|
309
|
+
if (Number.isNaN(last))
|
|
310
|
+
return true;
|
|
311
|
+
const delay = Math.min(DEADLETTER_BACKOFF_BASE_MS * 2 ** (rec.attempts - 1), DEADLETTER_BACKOFF_CAP_MS);
|
|
312
|
+
return now >= last + delay;
|
|
313
|
+
}
|
|
314
|
+
// Replay the deadletter through `upload`. Per record:
|
|
315
|
+
// - expired -> dropped
|
|
316
|
+
// - not yet due (backoff) -> kept as-is
|
|
317
|
+
// - upload ok -> dropped (sent)
|
|
318
|
+
// - upload fails -> attempts++, last_attempt_at=now; at MAX_ATTEMPTS dropped
|
|
319
|
+
// Respects the telemetry kill switch (nothing leaves the machine when off) and
|
|
320
|
+
// never throws.
|
|
321
|
+
async function flushDeadletter(opts) {
|
|
322
|
+
const env = opts.env ?? process.env;
|
|
323
|
+
const now = opts.now ?? Date.now();
|
|
324
|
+
const result = { sent: 0, dropped: 0, kept: 0 };
|
|
325
|
+
if ((0, config_1.telemetryDisabled)(env)) {
|
|
326
|
+
// Kill switch on: do not forward anything. Leave the local store intact.
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
const file = deadletterPath(env);
|
|
330
|
+
const all = readRecords(file);
|
|
331
|
+
const survivors = [];
|
|
332
|
+
for (const rec of all) {
|
|
333
|
+
if (!notExpired(rec, now)) {
|
|
334
|
+
result.dropped++;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (!isAttemptDue(rec, now)) {
|
|
338
|
+
survivors.push(rec);
|
|
339
|
+
result.kept++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
await opts.upload(rec);
|
|
344
|
+
result.sent++;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
const attempts = rec.attempts + 1;
|
|
348
|
+
if (attempts >= exports.DEADLETTER_MAX_ATTEMPTS) {
|
|
349
|
+
result.dropped++;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
survivors.push({
|
|
353
|
+
...rec,
|
|
354
|
+
attempts,
|
|
355
|
+
last_attempt_at: new Date(now).toISOString(),
|
|
356
|
+
});
|
|
357
|
+
result.kept++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
writeRecords(file, survivors);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// best effort
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// F8 detector: telemetry upload itself failed.
|
|
371
|
+
//
|
|
372
|
+
// Per INV-SENTRY-NOISE-BUDGET, F8 routes to the LOCAL deadletter + a local
|
|
373
|
+
// warning, NOT to Sentry (unless the backend side later observes it). The CLI
|
|
374
|
+
// has no PostHog key by design, so an F8 on the user's machine is recorded
|
|
375
|
+
// locally; the deadletter IS the store. This is also the self-host posture: on a
|
|
376
|
+
// user-owned backend nothing leaves the machine.
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
exports.FAILURE_TELEMETRY_UPLOAD_FAILED = "telemetry_upload_failed";
|
|
379
|
+
// Record an F8 failure to the deadletter. Returns the record (for tests/logging)
|
|
380
|
+
// or null when the kill switch is on or the write failed. Never throws.
|
|
381
|
+
function recordTelemetryUploadFailure(ctx, opts = {}) {
|
|
382
|
+
const env = opts.env ?? process.env;
|
|
383
|
+
if ((0, config_1.telemetryDisabled)(env))
|
|
384
|
+
return null;
|
|
385
|
+
const context = {};
|
|
386
|
+
if (typeof ctx.status === "number")
|
|
387
|
+
context.status = ctx.status;
|
|
388
|
+
if (ctx.reasonCode)
|
|
389
|
+
context.reason_code = ctx.reasonCode;
|
|
390
|
+
const event = {
|
|
391
|
+
failure_class: exports.FAILURE_TELEMETRY_UPLOAD_FAILED,
|
|
392
|
+
severity: "warning",
|
|
393
|
+
surface: ctx.surface ?? "mla-cli",
|
|
394
|
+
metadata_only_context: context,
|
|
395
|
+
};
|
|
396
|
+
if (ctx.traceId)
|
|
397
|
+
event.trace_id = ctx.traceId;
|
|
398
|
+
if (ctx.workspaceId)
|
|
399
|
+
event.workspace_id = ctx.workspaceId;
|
|
400
|
+
if (ctx.sessionId)
|
|
401
|
+
event.session_id = ctx.sessionId;
|
|
402
|
+
return appendDeadletter(event, opts);
|
|
403
|
+
}
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// F5 detector: a KB write the agent attempted was blocked.
|
|
406
|
+
//
|
|
407
|
+
// The canonical signal is `mla kb add` exiting non-zero because the actor is not
|
|
408
|
+
// the workspace owner (the §13.14 owner-only ACL): the agent tried to write a
|
|
409
|
+
// lesson down and could not. Like F8 this routes to the LOCAL deadletter and a
|
|
410
|
+
// local warning, NOT to Sentry directly (intel SENTRY_ROUTE[F5] = IF_REPEATED;
|
|
411
|
+
// a one-off block is expected friction, a recurrence is the alert). The CLI has
|
|
412
|
+
// no PostHog key by design, so the deadletter IS the store on the user's machine.
|
|
413
|
+
//
|
|
414
|
+
// The failure_class string and "warning" severity MUST match the intel twin
|
|
415
|
+
// (app/core/failure_telemetry.py: FailureClass.KB_WRITE_BLOCKED = "kb_write_blocked",
|
|
416
|
+
// _DEFAULT_SEVERITY[KB_WRITE_BLOCKED] = WARNING); they are the cross-plane key.
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
exports.FAILURE_KB_WRITE_BLOCKED = "kb_write_blocked";
|
|
419
|
+
// Record an F5 (kb-write-blocked) failure to the deadletter. Returns the record
|
|
420
|
+
// (for tests/logging) or null when the kill switch is on or the write failed.
|
|
421
|
+
// Never throws: detecting a blocked write must not itself break the command.
|
|
422
|
+
function recordKbWriteBlocked(ctx, opts = {}) {
|
|
423
|
+
const env = opts.env ?? process.env;
|
|
424
|
+
if ((0, config_1.telemetryDisabled)(env))
|
|
425
|
+
return null;
|
|
426
|
+
const context = {};
|
|
427
|
+
if (typeof ctx.status === "number")
|
|
428
|
+
context.status = ctx.status;
|
|
429
|
+
if (ctx.reasonCode)
|
|
430
|
+
context.reason_code = ctx.reasonCode;
|
|
431
|
+
const event = {
|
|
432
|
+
failure_class: exports.FAILURE_KB_WRITE_BLOCKED,
|
|
433
|
+
severity: "warning",
|
|
434
|
+
surface: ctx.surface ?? "mla-cli",
|
|
435
|
+
metadata_only_context: context,
|
|
436
|
+
};
|
|
437
|
+
if (ctx.traceId)
|
|
438
|
+
event.trace_id = ctx.traceId;
|
|
439
|
+
if (ctx.workspaceId)
|
|
440
|
+
event.workspace_id = ctx.workspaceId;
|
|
441
|
+
if (ctx.sessionId)
|
|
442
|
+
event.session_id = ctx.sessionId;
|
|
443
|
+
return appendDeadletter(event, opts);
|
|
444
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.computeRepoFingerprint = computeRepoFingerprint;
|
|
37
|
+
exports.captureGitEvidence = captureGitEvidence;
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
function gitRun(repo, args) {
|
|
41
|
+
const r = (0, child_process_1.spawnSync)("git", ["-C", repo, ...args], { encoding: "utf8" });
|
|
42
|
+
return {
|
|
43
|
+
ok: r.status === 0,
|
|
44
|
+
stdout: r.stdout || "",
|
|
45
|
+
stderr: r.stderr || "",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// A NON-identifying, one-way fingerprint of the repository a run executed in,
|
|
49
|
+
// for analytics attribution (spec section 3.7 / T1.10). Hashes the git remote
|
|
50
|
+
// URL (the stable repo identity) when present, else the repo top-level path, so
|
|
51
|
+
// two runs in the same checkout share a fingerprint WITHOUT the raw remote URL
|
|
52
|
+
// or absolute path ever leaving the machine (INV-POSTHOG-PII-1 forbids a raw
|
|
53
|
+
// repoPath). The "r_" prefix + sha256-slice mirror store.ts machineId(). Returns
|
|
54
|
+
// null outside a git repo or when git is unavailable; attribution then carries a
|
|
55
|
+
// null repoFingerprint rather than a fabricated value. Computed once per run at
|
|
56
|
+
// bootstrap, never per event (it shells out to git).
|
|
57
|
+
function computeRepoFingerprint(repo = process.cwd()) {
|
|
58
|
+
const remote = gitRun(repo, ["config", "--get", "remote.origin.url"]);
|
|
59
|
+
let seed = remote.ok ? remote.stdout.trim() : "";
|
|
60
|
+
if (!seed) {
|
|
61
|
+
const top = gitRun(repo, ["rev-parse", "--show-toplevel"]);
|
|
62
|
+
seed = top.ok ? top.stdout.trim() : "";
|
|
63
|
+
}
|
|
64
|
+
if (!seed)
|
|
65
|
+
return null;
|
|
66
|
+
return "r_" + crypto.createHash("sha256").update(seed).digest("hex").slice(0, 24);
|
|
67
|
+
}
|
|
68
|
+
function parseDiffStat(out) {
|
|
69
|
+
let filesChanged = 0;
|
|
70
|
+
let insertions = 0;
|
|
71
|
+
let deletions = 0;
|
|
72
|
+
const m = out.match(/(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/);
|
|
73
|
+
if (m) {
|
|
74
|
+
filesChanged = parseInt(m[1] || "0", 10);
|
|
75
|
+
insertions = parseInt(m[2] || "0", 10);
|
|
76
|
+
deletions = parseInt(m[3] || "0", 10);
|
|
77
|
+
}
|
|
78
|
+
return { filesChanged, insertions, deletions };
|
|
79
|
+
}
|
|
80
|
+
function parsePorcelain(out) {
|
|
81
|
+
const trackedModified = [];
|
|
82
|
+
const staged = [];
|
|
83
|
+
const untracked = [];
|
|
84
|
+
const deleted = [];
|
|
85
|
+
const renamed = [];
|
|
86
|
+
const lines = out.split("\n");
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line)
|
|
89
|
+
continue;
|
|
90
|
+
const xy = line.slice(0, 2);
|
|
91
|
+
const rest = line.slice(3);
|
|
92
|
+
const X = xy[0];
|
|
93
|
+
const Y = xy[1];
|
|
94
|
+
if (X === "?" && Y === "?") {
|
|
95
|
+
untracked.push(rest);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (X === "R" || Y === "R") {
|
|
99
|
+
const parts = rest.split(" -> ");
|
|
100
|
+
if (parts.length === 2) {
|
|
101
|
+
renamed.push({ from: parts[0], to: parts[1] });
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (X === "D" || Y === "D") {
|
|
106
|
+
deleted.push(rest);
|
|
107
|
+
}
|
|
108
|
+
if (X !== " " && X !== "?") {
|
|
109
|
+
staged.push(rest);
|
|
110
|
+
}
|
|
111
|
+
if (Y !== " " && Y !== "?") {
|
|
112
|
+
trackedModified.push(rest);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { trackedModified, staged, untracked, deleted, renamed };
|
|
116
|
+
}
|
|
117
|
+
// Subtract the session-start baseline from the finalize-time porcelain so the
|
|
118
|
+
// review attributes only what the SESSION touched, not ambient dirty state the
|
|
119
|
+
// working tree already carried before the agent started (the 2026-05-31 dogfood
|
|
120
|
+
// bug: a pre-existing `.claude/scheduled_tasks.lock` deletion was blamed on the
|
|
121
|
+
// run). Comparison is exact-line (porcelain `XY␣path`): a line byte-identical to
|
|
122
|
+
// the baseline is ambient and dropped; a line whose status code CHANGED since
|
|
123
|
+
// the baseline means the session acted on it, so it survives.
|
|
124
|
+
//
|
|
125
|
+
// Known, accepted limitation: a file already dirty at session start that the
|
|
126
|
+
// session edits FURTHER with the SAME status code (e.g. " M" -> " M") is treated
|
|
127
|
+
// as ambient. Closing that needs per-path content hashing; the dominant bug
|
|
128
|
+
// (files the session never touched at all) is what this fixes.
|
|
129
|
+
function subtractBaseline(currentPorcelain, baselinePorcelain) {
|
|
130
|
+
const baselineLines = new Set(baselinePorcelain.split("\n").filter((l) => l.length > 0));
|
|
131
|
+
return currentPorcelain
|
|
132
|
+
.split("\n")
|
|
133
|
+
.filter((l) => l.length > 0 && !baselineLines.has(l))
|
|
134
|
+
.join("\n");
|
|
135
|
+
}
|
|
136
|
+
function captureGitEvidence(repo, baselinePorcelain) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
const branchRes = gitRun(repo, ["branch", "--show-current"]);
|
|
139
|
+
const branch = branchRes.ok ? branchRes.stdout.trim() : "";
|
|
140
|
+
if (!branchRes.ok)
|
|
141
|
+
errors.push("branch:" + branchRes.stderr.trim().slice(0, 100));
|
|
142
|
+
const topRes = gitRun(repo, ["rev-parse", "--show-toplevel"]);
|
|
143
|
+
const topLevel = topRes.ok ? topRes.stdout.trim() : "";
|
|
144
|
+
if (!topRes.ok)
|
|
145
|
+
errors.push("toplevel:" + topRes.stderr.trim().slice(0, 100));
|
|
146
|
+
const logRes = gitRun(repo, ["log", "-1", "--oneline"]);
|
|
147
|
+
const lastCommit = logRes.ok ? logRes.stdout.trim() : "";
|
|
148
|
+
// -c core.quotePath=false keeps non-ASCII paths in raw UTF-8 instead of the
|
|
149
|
+
// default C-style octal-quoted form. Without this, a Vietnamese filename
|
|
150
|
+
// like `Lỗi-không-tìm-thấy.ts` comes back as
|
|
151
|
+
// `"L\341\273\227i-kh\303\264ng-t\303\254m-th\341\272\245y.ts"`, which then
|
|
152
|
+
// flows into trackedModified / staged / untracked verbatim, kills the
|
|
153
|
+
// worker's classifier match, and renders as garbled text in `mla review`.
|
|
154
|
+
const porcelainRes = gitRun(repo, [
|
|
155
|
+
"-c",
|
|
156
|
+
"core.quotePath=false",
|
|
157
|
+
"status",
|
|
158
|
+
"--porcelain=v1",
|
|
159
|
+
]);
|
|
160
|
+
// A non-null baseline (even an empty string) switches us to session-scoped
|
|
161
|
+
// attribution; `undefined`/`null` preserves the original whole-tree behavior.
|
|
162
|
+
const scoped = baselinePorcelain !== undefined && baselinePorcelain !== null;
|
|
163
|
+
const effectivePorcelain = scoped && porcelainRes.ok ? subtractBaseline(porcelainRes.stdout, baselinePorcelain) : porcelainRes.stdout;
|
|
164
|
+
const { trackedModified, staged, untracked, deleted, renamed } = porcelainRes.ok
|
|
165
|
+
? parsePorcelain(effectivePorcelain)
|
|
166
|
+
: { trackedModified: [], staged: [], untracked: [], deleted: [], renamed: [] };
|
|
167
|
+
if (!porcelainRes.ok)
|
|
168
|
+
errors.push("status:" + porcelainRes.stderr.trim().slice(0, 100));
|
|
169
|
+
// When scoped, recompute the diff stats over only the session-attributed
|
|
170
|
+
// paths so insertion/deletion counts and filesChanged exclude ambient churn.
|
|
171
|
+
// `git diff --stat -- <paths>` with an empty pathspec would list everything,
|
|
172
|
+
// so an empty session set is reported as a zeroed stat (the honest answer).
|
|
173
|
+
const ZERO_STAT = { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
174
|
+
const renamedPaths = renamed.flatMap((r) => [r.from, r.to]);
|
|
175
|
+
const unstagedPaths = Array.from(new Set([...trackedModified, ...renamedPaths]));
|
|
176
|
+
const stagedPaths = Array.from(new Set([...staged, ...renamedPaths]));
|
|
177
|
+
let diffStat;
|
|
178
|
+
let diffStatCached;
|
|
179
|
+
if (scoped) {
|
|
180
|
+
diffStat = unstagedPaths.length > 0 ? parseDiffStat(gitRun(repo, ["diff", "--stat", "--", ...unstagedPaths]).stdout) : { ...ZERO_STAT };
|
|
181
|
+
diffStatCached = stagedPaths.length > 0 ? parseDiffStat(gitRun(repo, ["diff", "--cached", "--stat", "--", ...stagedPaths]).stdout) : { ...ZERO_STAT };
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
diffStat = parseDiffStat(gitRun(repo, ["diff", "--stat"]).stdout);
|
|
185
|
+
diffStatCached = parseDiffStat(gitRun(repo, ["diff", "--cached", "--stat"]).stdout);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
branch,
|
|
189
|
+
topLevel,
|
|
190
|
+
lastCommit,
|
|
191
|
+
trackedModified,
|
|
192
|
+
staged,
|
|
193
|
+
untracked,
|
|
194
|
+
deleted,
|
|
195
|
+
renamed,
|
|
196
|
+
diffStat,
|
|
197
|
+
diffStatCached,
|
|
198
|
+
errors,
|
|
199
|
+
};
|
|
200
|
+
}
|