@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,681 @@
|
|
|
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.parseKbAddArgs = parseKbAddArgs;
|
|
37
|
+
exports.gitRootForVault = gitRootForVault;
|
|
38
|
+
exports.resolveVaultRoot = resolveVaultRoot;
|
|
39
|
+
exports.vaultRelPath = vaultRelPath;
|
|
40
|
+
exports.readCorpusMarker = readCorpusMarker;
|
|
41
|
+
exports.globFiles = globFiles;
|
|
42
|
+
exports.enumerateDocuments = enumerateDocuments;
|
|
43
|
+
exports.pollReceiptsToTerminal = pollReceiptsToTerminal;
|
|
44
|
+
exports.runKbAdd = runKbAdd;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const crypto_1 = require("crypto");
|
|
49
|
+
const config_1 = require("../lib/config");
|
|
50
|
+
const http_1 = require("../lib/http");
|
|
51
|
+
const kb_acl_1 = require("../lib/kb_acl");
|
|
52
|
+
const render_1 = require("../lib/render");
|
|
53
|
+
const open_url_1 = require("../lib/open-url");
|
|
54
|
+
const workspace_1 = require("../lib/workspace");
|
|
55
|
+
const governed_path_cache_1 = require("../lib/governed-path-cache");
|
|
56
|
+
const failure_telemetry_1 = require("../lib/failure-telemetry");
|
|
57
|
+
const observability_1 = require("../lib/observability");
|
|
58
|
+
// `mla kb add <path> --mode file|corpus --provenance <kind> [flags]`
|
|
59
|
+
// (proposal §4.1).
|
|
60
|
+
//
|
|
61
|
+
// Remote-capable: this command POSTs the note bodies to the intel route
|
|
62
|
+
// `POST /internal/v1/kb/add`, which owns the governed ingestion front door
|
|
63
|
+
// (intake_delivery -> execute_run_set -> activation CAS head swap) and the
|
|
64
|
+
// server-authoritative canonical identity. The CLI no longer spawns a local
|
|
65
|
+
// python subprocess or needs an intel checkout on the operator's machine, so
|
|
66
|
+
// seeding works from any laptop against any backend (local dogfood or staging)
|
|
67
|
+
// the same way every other `mla` command does (INV-OSS-1).
|
|
68
|
+
//
|
|
69
|
+
// Split of responsibilities:
|
|
70
|
+
// - CLIENT (here, holds the filesystem): strict argv parsing, owner-only ACL
|
|
71
|
+
// pre-flight, path-existence + mode/dir guards, vault-root resolution
|
|
72
|
+
// (MEETLESS_NOTES_ROOT -> git-repo walk-up; corpus mode = the corpus
|
|
73
|
+
// folder), corpus-marker (`.meetless-kb-corpus.json`) read + glob
|
|
74
|
+
// enumeration, content reads, and the vault-relative POSIX path per doc.
|
|
75
|
+
// - SERVER (intel route): prefixes the single `notes/` root, runs the PURE
|
|
76
|
+
// canonicalizer (NFC + casefold) to reproduce exactly what the local
|
|
77
|
+
// `notes_external_object_id` computes (so HTTP-seeded and locally-seeded
|
|
78
|
+
// docs dedup against each other), mints/dedups the revision, runs the heavy
|
|
79
|
+
// LDM body inline, and returns the KbAddReceipt array `render.ts` consumes.
|
|
80
|
+
//
|
|
81
|
+
// The receipt tail (sync-extract poll, governed-path cache, Console URL stamp,
|
|
82
|
+
// `--open`) is unchanged: it operates on the receipts the route returns exactly
|
|
83
|
+
// as it did on the receipts the worker used to print.
|
|
84
|
+
const CORPUS_MARKER = ".meetless-kb-corpus.json";
|
|
85
|
+
const DEFAULT_GLOB = "*.md";
|
|
86
|
+
const DEFAULT_PROFILE = "markdown_atomic_v1";
|
|
87
|
+
const VALUE_FLAGS = new Set([
|
|
88
|
+
"--mode",
|
|
89
|
+
"--provenance",
|
|
90
|
+
"--workspace",
|
|
91
|
+
"--profile",
|
|
92
|
+
"--glob",
|
|
93
|
+
"--ingest-run-id",
|
|
94
|
+
"--agent-session",
|
|
95
|
+
]);
|
|
96
|
+
const BOOLEAN_FLAGS = new Set(["--allow-provenance-change", "--queue", "--open", "--reingest-if-active"]);
|
|
97
|
+
function parseKbAddArgs(argv) {
|
|
98
|
+
const out = {
|
|
99
|
+
allowProvenanceChange: false,
|
|
100
|
+
queue: false,
|
|
101
|
+
open: false,
|
|
102
|
+
reingestIfActive: false,
|
|
103
|
+
};
|
|
104
|
+
let positional = null;
|
|
105
|
+
for (let i = 0; i < argv.length; i++) {
|
|
106
|
+
const a = argv[i];
|
|
107
|
+
if (VALUE_FLAGS.has(a)) {
|
|
108
|
+
const v = argv[i + 1];
|
|
109
|
+
if (v === undefined) {
|
|
110
|
+
throw new Error(`Missing value for ${a}`);
|
|
111
|
+
}
|
|
112
|
+
if (v.startsWith("--") || v.startsWith("-")) {
|
|
113
|
+
throw new Error(`Missing value for ${a} (got the next flag ${v} instead)`);
|
|
114
|
+
}
|
|
115
|
+
switch (a) {
|
|
116
|
+
case "--mode":
|
|
117
|
+
if (v !== "file" && v !== "corpus") {
|
|
118
|
+
throw new Error(`--mode must be 'file' or 'corpus' (got '${v}')`);
|
|
119
|
+
}
|
|
120
|
+
out.mode = v;
|
|
121
|
+
break;
|
|
122
|
+
case "--provenance":
|
|
123
|
+
out.provenance = v;
|
|
124
|
+
break;
|
|
125
|
+
case "--workspace":
|
|
126
|
+
out.workspace = v;
|
|
127
|
+
break;
|
|
128
|
+
case "--profile":
|
|
129
|
+
out.profile = v;
|
|
130
|
+
break;
|
|
131
|
+
case "--glob":
|
|
132
|
+
out.glob = v;
|
|
133
|
+
break;
|
|
134
|
+
case "--ingest-run-id":
|
|
135
|
+
out.ingestRunId = v;
|
|
136
|
+
break;
|
|
137
|
+
case "--agent-session":
|
|
138
|
+
out.agentSession = v;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
i += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (BOOLEAN_FLAGS.has(a)) {
|
|
145
|
+
if (a === "--allow-provenance-change")
|
|
146
|
+
out.allowProvenanceChange = true;
|
|
147
|
+
else if (a === "--queue")
|
|
148
|
+
out.queue = true;
|
|
149
|
+
else if (a === "--open")
|
|
150
|
+
out.open = true;
|
|
151
|
+
else if (a === "--reingest-if-active")
|
|
152
|
+
out.reingestIfActive = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (a.startsWith("--") || a.startsWith("-")) {
|
|
156
|
+
throw new Error(`Unknown flag: ${a}. Supported flags: ${[...VALUE_FLAGS, ...BOOLEAN_FLAGS].sort().join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
if (positional !== null) {
|
|
159
|
+
throw new Error(`\`mla kb add\` takes exactly one positional path (got '${positional}' and '${a}')`);
|
|
160
|
+
}
|
|
161
|
+
positional = a;
|
|
162
|
+
}
|
|
163
|
+
if (positional === null) {
|
|
164
|
+
throw new Error("`mla kb add` requires a positional <path>");
|
|
165
|
+
}
|
|
166
|
+
if (!out.mode) {
|
|
167
|
+
throw new Error("--mode file|corpus is required");
|
|
168
|
+
}
|
|
169
|
+
if (!out.provenance) {
|
|
170
|
+
throw new Error("--provenance <kind> is required");
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
path: positional,
|
|
174
|
+
mode: out.mode,
|
|
175
|
+
provenance: out.provenance,
|
|
176
|
+
workspace: out.workspace,
|
|
177
|
+
profile: out.profile,
|
|
178
|
+
glob: out.glob,
|
|
179
|
+
ingestRunId: out.ingestRunId,
|
|
180
|
+
agentSession: out.agentSession,
|
|
181
|
+
allowProvenanceChange: !!out.allowProvenanceChange,
|
|
182
|
+
queue: !!out.queue,
|
|
183
|
+
open: !!out.open,
|
|
184
|
+
reingestIfActive: !!out.reingestIfActive,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Vault-root resolution + enumeration (client-side; was tools/mla_kb_add.py)
|
|
189
|
+
//
|
|
190
|
+
// The governed identity is the vault-relative POSIX path under a single
|
|
191
|
+
// `notes/` root. The CLIENT alone holds the filesystem, so it resolves the
|
|
192
|
+
// vault root and computes each file's relative path here, exactly mirroring the
|
|
193
|
+
// python worker's `_resolve_vault_root` / `_enumerate_files` /
|
|
194
|
+
// `notes_external_object_id`, then ships `{relPath, content}` to the route. The
|
|
195
|
+
// SERVER prefixes `notes/` and canonicalizes, so the externalObjectId matches
|
|
196
|
+
// the locally-seeded one byte-for-byte (dedup parity).
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
function expandHome(p) {
|
|
199
|
+
if (p === "~")
|
|
200
|
+
return os.homedir();
|
|
201
|
+
if (p.startsWith("~/"))
|
|
202
|
+
return path.join(os.homedir(), p.slice(2));
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
// Walk up from `start` looking for a `.git` entry; return the containing dir.
|
|
206
|
+
// Mirrors the worker's `_git_root_for` (first/closest match wins).
|
|
207
|
+
function gitRootForVault(start) {
|
|
208
|
+
let cur;
|
|
209
|
+
try {
|
|
210
|
+
cur = fs.realpathSync(start);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
cur = path.resolve(start);
|
|
214
|
+
}
|
|
215
|
+
for (;;) {
|
|
216
|
+
if (fs.existsSync(path.join(cur, ".git")))
|
|
217
|
+
return cur;
|
|
218
|
+
const parent = path.dirname(cur);
|
|
219
|
+
if (parent === cur)
|
|
220
|
+
return null;
|
|
221
|
+
cur = parent;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Resolve the notes vault root the governed identity is relative to.
|
|
225
|
+
// Order (mirrors the worker, minus the removed `--vault-root` flag): corpus
|
|
226
|
+
// folder (corpus mode), else MEETLESS_NOTES_ROOT, else a git-repo-root walk-up
|
|
227
|
+
// from the file's directory. `resolvedPath` is the absolute target.
|
|
228
|
+
function resolveVaultRoot(flags, resolvedPath) {
|
|
229
|
+
if (flags.mode === "corpus") {
|
|
230
|
+
return fs.realpathSync(resolvedPath);
|
|
231
|
+
}
|
|
232
|
+
const envRoot = process.env.MEETLESS_NOTES_ROOT;
|
|
233
|
+
if (envRoot) {
|
|
234
|
+
const expanded = path.resolve(expandHome(envRoot));
|
|
235
|
+
if (!fs.existsSync(expanded) || !fs.statSync(expanded).isDirectory()) {
|
|
236
|
+
throw new Error(`MEETLESS_NOTES_ROOT=${envRoot} is not a directory`);
|
|
237
|
+
}
|
|
238
|
+
return fs.realpathSync(expanded);
|
|
239
|
+
}
|
|
240
|
+
const anchor = path.dirname(resolvedPath);
|
|
241
|
+
const gitRoot = gitRootForVault(anchor);
|
|
242
|
+
if (gitRoot)
|
|
243
|
+
return gitRoot;
|
|
244
|
+
throw new Error("could not resolve a notes vault root for the governed identity; set MEETLESS_NOTES_ROOT or run inside a git repo");
|
|
245
|
+
}
|
|
246
|
+
// The vault-relative POSIX path for `file`, validated to live INSIDE the vault.
|
|
247
|
+
// The server prefixes `notes/` + canonicalizes this. Mirrors the worker's
|
|
248
|
+
// `notes_external_object_id`, which raises when the file escapes the vault.
|
|
249
|
+
function vaultRelPath(vaultRoot, file) {
|
|
250
|
+
const root = fs.realpathSync(vaultRoot);
|
|
251
|
+
const f = fs.realpathSync(file);
|
|
252
|
+
const rel = path.relative(root, f);
|
|
253
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
254
|
+
throw new Error(`file ${f} is not inside the notes vault root ${root}`);
|
|
255
|
+
}
|
|
256
|
+
return rel.split(path.sep).join("/");
|
|
257
|
+
}
|
|
258
|
+
// Read + validate `.meetless-kb-corpus.json`. Mirrors the worker's
|
|
259
|
+
// `_read_corpus_marker`: the marker pins the corpus to one workspace and may
|
|
260
|
+
// carry an allowedGlob / allowedProvenance guardrail.
|
|
261
|
+
function readCorpusMarker(folder, workspaceId) {
|
|
262
|
+
const markerPath = path.join(folder, CORPUS_MARKER);
|
|
263
|
+
if (!fs.existsSync(markerPath) || !fs.statSync(markerPath).isFile()) {
|
|
264
|
+
throw new Error(`corpus mode requires ${markerPath}; create one with the workspaceId and an optional allowedGlob / allowedProvenance guardrail`);
|
|
265
|
+
}
|
|
266
|
+
let raw;
|
|
267
|
+
try {
|
|
268
|
+
raw = JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
throw new Error(`${markerPath}: invalid JSON (${e.message})`);
|
|
272
|
+
}
|
|
273
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
274
|
+
throw new Error(`${markerPath}: marker must be a JSON object`);
|
|
275
|
+
}
|
|
276
|
+
const obj = raw;
|
|
277
|
+
if (obj.workspaceId !== workspaceId) {
|
|
278
|
+
throw new Error(`${markerPath}: workspaceId=${JSON.stringify(obj.workspaceId)} does NOT match --workspace (${JSON.stringify(workspaceId)}); the marker pins the corpus to one workspace`);
|
|
279
|
+
}
|
|
280
|
+
const allowedGlob = obj.allowedGlob ?? null;
|
|
281
|
+
if (allowedGlob !== null && typeof allowedGlob !== "string") {
|
|
282
|
+
throw new Error(`${markerPath}: allowedGlob must be a string`);
|
|
283
|
+
}
|
|
284
|
+
const allowedProvenance = obj.allowedProvenance ?? null;
|
|
285
|
+
if (allowedProvenance !== null && !Array.isArray(allowedProvenance)) {
|
|
286
|
+
throw new Error(`${markerPath}: allowedProvenance must be a list of strings`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
workspaceId,
|
|
290
|
+
corpusName: (typeof obj.corpusName === "string" && obj.corpusName) || path.basename(folder),
|
|
291
|
+
allowedGlob: allowedGlob,
|
|
292
|
+
allowedProvenance: allowedProvenance,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function segmentToRegex(seg) {
|
|
296
|
+
let re = "";
|
|
297
|
+
for (const ch of seg) {
|
|
298
|
+
if (ch === "*")
|
|
299
|
+
re += "[^/]*";
|
|
300
|
+
else if (ch === "?")
|
|
301
|
+
re += "[^/]";
|
|
302
|
+
else
|
|
303
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
304
|
+
}
|
|
305
|
+
return new RegExp(`^${re}$`);
|
|
306
|
+
}
|
|
307
|
+
// Enumerate files under `root` matching a glob, mirroring python `Path.glob`:
|
|
308
|
+
// `**` matches zero-or-more directory segments, `*`/`?` match within a single
|
|
309
|
+
// segment, and (Unix-glob convention) a `*` segment skips dotfiles. Returns
|
|
310
|
+
// absolute file paths, deduped + sorted (matching the worker's `sorted(...)`).
|
|
311
|
+
function globFiles(root, pattern) {
|
|
312
|
+
const parts = pattern.split("/").filter((p) => p.length > 0);
|
|
313
|
+
const results = [];
|
|
314
|
+
const walk = (dir, idx) => {
|
|
315
|
+
if (idx >= parts.length)
|
|
316
|
+
return;
|
|
317
|
+
const part = parts[idx];
|
|
318
|
+
const isLast = idx === parts.length - 1;
|
|
319
|
+
let entries;
|
|
320
|
+
try {
|
|
321
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (part === "**") {
|
|
327
|
+
// zero-directory case: try the rest of the pattern at this level...
|
|
328
|
+
walk(dir, idx + 1);
|
|
329
|
+
// ...then descend, keeping `**` active.
|
|
330
|
+
for (const e of entries) {
|
|
331
|
+
if (e.isDirectory())
|
|
332
|
+
walk(path.join(dir, e.name), idx);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const rx = segmentToRegex(part);
|
|
337
|
+
const skipDot = !part.startsWith(".");
|
|
338
|
+
for (const e of entries) {
|
|
339
|
+
if (skipDot && e.name.startsWith("."))
|
|
340
|
+
continue;
|
|
341
|
+
if (!rx.test(e.name))
|
|
342
|
+
continue;
|
|
343
|
+
const full = path.join(dir, e.name);
|
|
344
|
+
if (isLast) {
|
|
345
|
+
if (e.isFile())
|
|
346
|
+
results.push(full);
|
|
347
|
+
}
|
|
348
|
+
else if (e.isDirectory()) {
|
|
349
|
+
walk(full, idx + 1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
walk(root, 0);
|
|
354
|
+
return Array.from(new Set(results)).sort();
|
|
355
|
+
}
|
|
356
|
+
// Build the per-document upload list (relative path + body). File mode is the
|
|
357
|
+
// single target; corpus mode globs the marker-pinned set under the folder.
|
|
358
|
+
function enumerateDocuments(flags, resolvedPath, vaultRoot, marker) {
|
|
359
|
+
if (flags.mode === "file") {
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
relPath: vaultRelPath(vaultRoot, resolvedPath),
|
|
363
|
+
content: fs.readFileSync(resolvedPath, "utf8"),
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
// corpus
|
|
368
|
+
let effectiveGlob = flags.glob ?? DEFAULT_GLOB;
|
|
369
|
+
if (marker?.allowedGlob) {
|
|
370
|
+
if (flags.glob && flags.glob !== DEFAULT_GLOB && flags.glob !== marker.allowedGlob) {
|
|
371
|
+
throw new Error(`corpus marker pins allowedGlob=${JSON.stringify(marker.allowedGlob)} but --glob=${JSON.stringify(flags.glob)} was passed; the marker wins. Drop --glob or align it with the marker.`);
|
|
372
|
+
}
|
|
373
|
+
effectiveGlob = marker.allowedGlob;
|
|
374
|
+
}
|
|
375
|
+
const files = globFiles(vaultRoot, effectiveGlob);
|
|
376
|
+
if (files.length === 0) {
|
|
377
|
+
throw new Error(`--mode corpus: no files matched ${effectiveGlob} under ${resolvedPath}`);
|
|
378
|
+
}
|
|
379
|
+
return files.map((f) => ({
|
|
380
|
+
relPath: vaultRelPath(vaultRoot, f),
|
|
381
|
+
content: fs.readFileSync(f, "utf8"),
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
// Mirrors render.ts: only a body-changing FILE ingest (a minted, activated
|
|
385
|
+
// revision) enqueues a GRAPH_EXTRACT job worth polling. Corpus rollups are
|
|
386
|
+
// async-default; noop_unchanged and failed ingests enqueue nothing.
|
|
387
|
+
function receiptEnqueuesExtraction(r) {
|
|
388
|
+
return r.mode === "file" && r.outcome === "ingested";
|
|
389
|
+
}
|
|
390
|
+
async function pollReceiptsToTerminal(receipts, opts, deps) {
|
|
391
|
+
if (opts.queue)
|
|
392
|
+
return; // opt-out: leave the inferred async-queued state.
|
|
393
|
+
const deadline = deps.now() + opts.budgetMs;
|
|
394
|
+
for (const r of receipts) {
|
|
395
|
+
if (!receiptEnqueuesExtraction(r))
|
|
396
|
+
continue;
|
|
397
|
+
for (;;) {
|
|
398
|
+
let polled;
|
|
399
|
+
try {
|
|
400
|
+
polled = await deps.fetchExtraction(r.documentId);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// A transient read failure must NOT fail the ingest: the revision is
|
|
404
|
+
// already committed. Stop polling this receipt and let whatever state
|
|
405
|
+
// we last observed (or the inferred queued state) render.
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
if (!polled)
|
|
409
|
+
break; // pre-B2 intel: no job state to read.
|
|
410
|
+
r.extraction = {
|
|
411
|
+
state: polled.state,
|
|
412
|
+
candidateCount: polled.candidateCount ?? null,
|
|
413
|
+
conflictCount: polled.conflictCount ?? null,
|
|
414
|
+
jobId: polled.jobId ?? null,
|
|
415
|
+
};
|
|
416
|
+
if (polled.state === "completed" || polled.state === "failed")
|
|
417
|
+
break;
|
|
418
|
+
if (deps.now() >= deadline)
|
|
419
|
+
break; // timeout: render queued/running honestly.
|
|
420
|
+
await deps.sleep(opts.intervalMs);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Default budget mirrors the enrich-hook contract (deadline ~30s, budget ~25s;
|
|
425
|
+
// NT:20260528 §3.6). Interval keeps the poll count modest (~16 max) while still
|
|
426
|
+
// catching a fast worker within a couple seconds.
|
|
427
|
+
const EXTRACTION_POLL_BUDGET_MS = 25_000;
|
|
428
|
+
const EXTRACTION_POLL_INTERVAL_MS = 1_500;
|
|
429
|
+
const EXTRACTION_DETAIL_TIMEOUT_MS = 8_000;
|
|
430
|
+
// Build the real fetcher: GET the B2 detail route and project its `extraction`
|
|
431
|
+
// field into a PolledExtraction. When the job has COMPLETED we count the
|
|
432
|
+
// PENDING_REVIEW candidates on the doc (the ones `mla kb pending` will list) so
|
|
433
|
+
// the receipt summary lines up with the review command it points at; conflicts
|
|
434
|
+
// are the CONTRADICTS / SUPERSEDES subset.
|
|
435
|
+
function buildExtractionFetcher(cfg, workspaceId) {
|
|
436
|
+
return async (documentId) => {
|
|
437
|
+
const qs = new URLSearchParams({
|
|
438
|
+
workspaceId,
|
|
439
|
+
revisionLimit: "1",
|
|
440
|
+
auditLimit: "1",
|
|
441
|
+
}).toString();
|
|
442
|
+
const detail = await (0, http_1.intelGet)(cfg, `/internal/v1/kb/documents/${encodeURIComponent(documentId)}/detail?${qs}`, EXTRACTION_DETAIL_TIMEOUT_MS);
|
|
443
|
+
const ex = detail.extraction;
|
|
444
|
+
if (!ex)
|
|
445
|
+
return null;
|
|
446
|
+
let candidateCount = null;
|
|
447
|
+
let conflictCount = null;
|
|
448
|
+
if (ex.state === "completed") {
|
|
449
|
+
const pending = (detail.candidates ?? []).filter((c) => c.status === "PENDING_REVIEW");
|
|
450
|
+
candidateCount = pending.length;
|
|
451
|
+
conflictCount = pending.filter((c) => c.relationType === "CONTRADICTS" || c.relationType === "SUPERSEDES").length;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
state: ex.state,
|
|
455
|
+
jobId: ex.jobId ?? null,
|
|
456
|
+
candidateCount,
|
|
457
|
+
conflictCount,
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Local pre-flight echo so the operator sees what is about to happen before the
|
|
462
|
+
// (possibly slow) server-side per-file pipeline runs. Keeps the silent-shell-out
|
|
463
|
+
// feel of `mla session remember` but lets corpus mode announce its target.
|
|
464
|
+
function printPreflight(flags, cfg) {
|
|
465
|
+
const ws = flags.workspace || cfg.workspaceId;
|
|
466
|
+
const target = path.resolve(flags.path);
|
|
467
|
+
console.log(`mla kb add (${flags.mode}) workspace=${ws} provenance=${flags.provenance} path=${target}`);
|
|
468
|
+
}
|
|
469
|
+
// Scale the request timeout by document count: a single file is fast (one
|
|
470
|
+
// inline LDM body + embeds), but a corpus holds the connection while the server
|
|
471
|
+
// ingests every doc sequentially. 120s floor for the common single-file/seed
|
|
472
|
+
// case, ~20s/doc above that.
|
|
473
|
+
function ingestTimeoutMs(docCount) {
|
|
474
|
+
return Math.max(120_000, docCount * 20_000);
|
|
475
|
+
}
|
|
476
|
+
async function runKbAdd(argv) {
|
|
477
|
+
// Parse flags BEFORE loading config so `--workspace <id>` can override the
|
|
478
|
+
// marker-resolved workspace (T1.1 folder = workspace). Passing the override
|
|
479
|
+
// into readKbConfig short-circuits marker resolution, so an admin can curate
|
|
480
|
+
// another workspace without activating the current directory.
|
|
481
|
+
let flags;
|
|
482
|
+
try {
|
|
483
|
+
flags = parseKbAddArgs(argv);
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
console.error(e.message);
|
|
487
|
+
return 2;
|
|
488
|
+
}
|
|
489
|
+
let cfg;
|
|
490
|
+
try {
|
|
491
|
+
cfg = (0, config_1.readKbConfig)(flags.workspace);
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
console.error(e.message);
|
|
495
|
+
return 2;
|
|
496
|
+
}
|
|
497
|
+
// §13.14 owner-only ACL: verify the configured actor is a workspace OWNER
|
|
498
|
+
// before any side effect (ingest POST, outbox emit). v1 has no KB_CURATE
|
|
499
|
+
// scope per §11 Q8.
|
|
500
|
+
try {
|
|
501
|
+
await (0, kb_acl_1.verifyKbActorIsOwner)(cfg);
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
if (e instanceof kb_acl_1.KbOwnerCheckError) {
|
|
505
|
+
console.error(e.message);
|
|
506
|
+
// F5 (kb-write-blocked): the agent tried to write a lesson down and the
|
|
507
|
+
// owner-only ACL refused it. This is the canonical F5 signal. Records to
|
|
508
|
+
// the local deadletter only (never throws, respects the kill switch).
|
|
509
|
+
(0, failure_telemetry_1.recordKbWriteBlocked)({
|
|
510
|
+
traceId: (0, observability_1.getRunTraceId)(),
|
|
511
|
+
workspaceId: cfg.workspaceId,
|
|
512
|
+
reasonCode: "owner_gate",
|
|
513
|
+
status: 2,
|
|
514
|
+
});
|
|
515
|
+
return 2;
|
|
516
|
+
}
|
|
517
|
+
throw e;
|
|
518
|
+
}
|
|
519
|
+
// §4.1 explicit-path guards. The server also enforces these implicitly but
|
|
520
|
+
// surfacing them at the CLI boundary gives operators a faster, clearer error
|
|
521
|
+
// than waiting on a round trip.
|
|
522
|
+
const resolved = path.resolve(flags.path);
|
|
523
|
+
if (!fs.existsSync(resolved)) {
|
|
524
|
+
console.error(`path does not exist: ${resolved}`);
|
|
525
|
+
return 2;
|
|
526
|
+
}
|
|
527
|
+
const stat = fs.statSync(resolved);
|
|
528
|
+
if (flags.mode === "file" && stat.isDirectory()) {
|
|
529
|
+
console.error(`--mode file requires a file path, got directory: ${resolved}`);
|
|
530
|
+
return 2;
|
|
531
|
+
}
|
|
532
|
+
if (flags.mode === "corpus" && !stat.isDirectory()) {
|
|
533
|
+
console.error(`--mode corpus requires a directory path, got file: ${resolved}`);
|
|
534
|
+
return 2;
|
|
535
|
+
}
|
|
536
|
+
const workspaceId = flags.workspace || cfg.workspaceId;
|
|
537
|
+
// Resolve the vault root + assemble the upload list client-side (the CLI is
|
|
538
|
+
// the only side that holds the filesystem). Marker read (corpus) + the
|
|
539
|
+
// allowedProvenance guardrail run here, before the body bytes are read.
|
|
540
|
+
let documents;
|
|
541
|
+
let marker = null;
|
|
542
|
+
let corpusRootDisplay = null;
|
|
543
|
+
try {
|
|
544
|
+
if (flags.mode === "corpus") {
|
|
545
|
+
marker = readCorpusMarker(resolved, workspaceId);
|
|
546
|
+
// The corpus-marker provenance guardrail still applies to the operator's
|
|
547
|
+
// stated intent even though provenance is advisory at the governed layer.
|
|
548
|
+
if (marker.allowedProvenance && !marker.allowedProvenance.includes(flags.provenance)) {
|
|
549
|
+
throw new Error(`corpus marker allowedProvenance=${JSON.stringify(marker.allowedProvenance)} does NOT include --provenance=${flags.provenance}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const vaultRoot = resolveVaultRoot(flags, resolved);
|
|
553
|
+
if (flags.mode === "corpus")
|
|
554
|
+
corpusRootDisplay = vaultRoot;
|
|
555
|
+
documents = enumerateDocuments(flags, resolved, vaultRoot, marker);
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
console.error(e.message);
|
|
559
|
+
return 2;
|
|
560
|
+
}
|
|
561
|
+
printPreflight(flags, cfg);
|
|
562
|
+
// Relay the session UUID, canonicalized (defense in depth: a direct
|
|
563
|
+
// `mla kb add --agent-session X` may carry a non-canonical value). The server
|
|
564
|
+
// canonicalizes again and is the authoritative fail-closed gate; an invalid
|
|
565
|
+
// value yields no session here, never a composed or console value.
|
|
566
|
+
const agentSession = (0, observability_1.canonicalizeSessionId)(flags.agentSession ?? null);
|
|
567
|
+
const body = {
|
|
568
|
+
workspaceId,
|
|
569
|
+
actor: cfg.actorUserId,
|
|
570
|
+
documents,
|
|
571
|
+
provenance: flags.provenance, // advisory; the server derives the recorded value
|
|
572
|
+
profile: flags.profile || DEFAULT_PROFILE,
|
|
573
|
+
agentSession: agentSession ?? undefined,
|
|
574
|
+
mode: flags.mode,
|
|
575
|
+
corpusName: marker?.corpusName,
|
|
576
|
+
};
|
|
577
|
+
let receipts;
|
|
578
|
+
try {
|
|
579
|
+
const res = await (0, http_1.intelPost)(cfg, "/internal/v1/kb/add", body, ingestTimeoutMs(documents.length));
|
|
580
|
+
receipts = res.receipts ?? [];
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
console.error(`kb add failed: ${e.message}`);
|
|
584
|
+
// F5 (kb-write-blocked): the ingest POST did not land, so the lesson did not
|
|
585
|
+
// land. Record locally only; never throws, kill-switch aware.
|
|
586
|
+
(0, failure_telemetry_1.recordKbWriteBlocked)({
|
|
587
|
+
traceId: (0, observability_1.getRunTraceId)(),
|
|
588
|
+
workspaceId,
|
|
589
|
+
reasonCode: "ingest_post_failed",
|
|
590
|
+
status: 1,
|
|
591
|
+
});
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
if (receipts.length === 0) {
|
|
595
|
+
console.error("kb add: the ingest route returned no receipts.");
|
|
596
|
+
return 1;
|
|
597
|
+
}
|
|
598
|
+
// The server has no filesystem, so it cannot fill the corpus display root.
|
|
599
|
+
// Stamp the resolved corpus folder back onto the rollup the operator sees.
|
|
600
|
+
if (flags.mode === "corpus" && corpusRootDisplay && receipts[0].corpus) {
|
|
601
|
+
receipts[0].corpus.rootPath = corpusRootDisplay;
|
|
602
|
+
}
|
|
603
|
+
// A per-doc intake failure is reported in the receipt, not the HTTP status.
|
|
604
|
+
// Mirror the worker's exit semantics: any failed doc -> non-zero exit.
|
|
605
|
+
const anyFailed = receipts.some((r) => r.outcome === "failed");
|
|
606
|
+
if (anyFailed) {
|
|
607
|
+
(0, failure_telemetry_1.recordKbWriteBlocked)({
|
|
608
|
+
traceId: (0, observability_1.getRunTraceId)(),
|
|
609
|
+
workspaceId,
|
|
610
|
+
reasonCode: "ingest_doc_failed",
|
|
611
|
+
status: 1,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const exit = anyFailed ? 1 : 0;
|
|
615
|
+
// B3: sync-extract by default. Block on the worker-owned GRAPH_EXTRACT job by
|
|
616
|
+
// polling the B2 detail route to a terminal state or the latency budget.
|
|
617
|
+
// --queue opts out; corpus / failed / no-op-restore receipts are skipped by
|
|
618
|
+
// receiptEnqueuesExtraction so we never serialize on a bulk ingest.
|
|
619
|
+
const willPoll = !flags.queue && receipts.some(receiptEnqueuesExtraction);
|
|
620
|
+
if (willPoll) {
|
|
621
|
+
console.error("waiting for relationship extraction (up to 25s; pass --queue to skip and check `mla kb show` later)...");
|
|
622
|
+
}
|
|
623
|
+
await pollReceiptsToTerminal(receipts, {
|
|
624
|
+
queue: flags.queue,
|
|
625
|
+
budgetMs: EXTRACTION_POLL_BUDGET_MS,
|
|
626
|
+
intervalMs: EXTRACTION_POLL_INTERVAL_MS,
|
|
627
|
+
}, {
|
|
628
|
+
fetchExtraction: buildExtractionFetcher(cfg, workspaceId),
|
|
629
|
+
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
|
630
|
+
now: () => Date.now(),
|
|
631
|
+
});
|
|
632
|
+
// Task 3.4: owner-namespaced governed-path cache write-after-ingest. Record
|
|
633
|
+
// every produced doc as "this owner governed this exact (repo, path) as KB doc
|
|
634
|
+
// <id>" so a later turn can recognize a governed surface without a server round
|
|
635
|
+
// trip. This is the SAFE half only: we NEVER gate the POST on a cache hit (the
|
|
636
|
+
// server's resolve_by_canonical_path is the authoritative resolver, and a stale
|
|
637
|
+
// 3-day entry could point at a doc tombstoned server-side). The whole pass is
|
|
638
|
+
// best-effort: a cache write must never fail or interrupt the add.
|
|
639
|
+
try {
|
|
640
|
+
// repoRootHash: prefer the .meetless.json marker DIRECTORY (the governed repo
|
|
641
|
+
// root per the folder=workspace T1.1 binding), resolved from where the target
|
|
642
|
+
// file actually lives so it matches that file's repo. Fall back to process.cwd()
|
|
643
|
+
// only when the file's tree carries no usable marker.
|
|
644
|
+
const markerCtx = (0, workspace_1.findWorkspaceContext)(path.dirname(resolved));
|
|
645
|
+
const repoRootDir = markerCtx ? markerCtx.markerDir : process.cwd();
|
|
646
|
+
const repoRootHash = (0, crypto_1.createHash)("sha256").update(repoRootDir).digest("hex").slice(0, 24);
|
|
647
|
+
for (const receipt of receipts) {
|
|
648
|
+
const entry = (0, governed_path_cache_1.governedPathEntryForReceipt)(receipt, {
|
|
649
|
+
workspaceId,
|
|
650
|
+
ownerUserId: cfg.actorUserId,
|
|
651
|
+
repoRootHash,
|
|
652
|
+
});
|
|
653
|
+
if (entry)
|
|
654
|
+
(0, governed_path_cache_1.writeGovernedPath)(entry.key, entry.docId, config_1.HOME);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
// Advise, never block: the revision already committed. Swallow and continue.
|
|
659
|
+
console.error(`governed-path cache write skipped: ${e.message}`);
|
|
660
|
+
}
|
|
661
|
+
// B4a: stamp the Console review URL onto every receipt so `kb add` always
|
|
662
|
+
// surfaces the clickable human review surface. The server does not know the
|
|
663
|
+
// console base; the CLI owns it via getConsoleUrl(cfg).
|
|
664
|
+
const consoleUrl = `${(0, config_1.getConsoleUrl)(cfg)}/relationships`;
|
|
665
|
+
for (const receipt of receipts) {
|
|
666
|
+
receipt.consoleUrl = consoleUrl;
|
|
667
|
+
console.log((0, render_1.renderKbAddReceipt)(receipt));
|
|
668
|
+
console.log("");
|
|
669
|
+
}
|
|
670
|
+
// B4b: `--open` is opt-in (the URL is always printed in the receipt above; we
|
|
671
|
+
// NEVER auto-open, since the agent-proxy loop drives `kb add` headless). Launch
|
|
672
|
+
// once for the whole add, not per-receipt. Status note -> stderr.
|
|
673
|
+
if (flags.open) {
|
|
674
|
+
const res = (0, open_url_1.openUrl)(consoleUrl);
|
|
675
|
+
if (res.ok)
|
|
676
|
+
console.error(`opened ${consoleUrl} in your browser`);
|
|
677
|
+
else
|
|
678
|
+
console.error(`could not open a browser (${res.error}); the URL is in the receipt above`);
|
|
679
|
+
}
|
|
680
|
+
return exit;
|
|
681
|
+
}
|