@h-rig/server 0.0.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +14 -0
  2. package/dist/src/bootstrap.js +161 -0
  3. package/dist/src/index.js +13153 -0
  4. package/dist/src/inspector/agent-runtime.js +1077 -0
  5. package/dist/src/inspector/analysis.js +41 -0
  6. package/dist/src/inspector/discovery.js +137 -0
  7. package/dist/src/inspector/journal.js +518 -0
  8. package/dist/src/inspector/mission.js +562 -0
  9. package/dist/src/inspector/prompt.js +97 -0
  10. package/dist/src/inspector/provider-session.js +65 -0
  11. package/dist/src/inspector/reconcile.js +118 -0
  12. package/dist/src/inspector/review.js +13 -0
  13. package/dist/src/inspector/service.js +1759 -0
  14. package/dist/src/inspector/skills.js +155 -0
  15. package/dist/src/inspector/tools.js +1592 -0
  16. package/dist/src/inspector/types.js +1 -0
  17. package/dist/src/inspector/upstream-sync.js +479 -0
  18. package/dist/src/orchestration.js +402 -0
  19. package/dist/src/remote.js +123 -0
  20. package/dist/src/scheduler.js +84 -0
  21. package/dist/src/server-helpers/broadcasters.js +161 -0
  22. package/dist/src/server-helpers/conversation-snapshot.js +382 -0
  23. package/dist/src/server-helpers/event-emitter.js +41 -0
  24. package/dist/src/server-helpers/github-auth-store.js +155 -0
  25. package/dist/src/server-helpers/github-credentials.js +38 -0
  26. package/dist/src/server-helpers/github-project-status-sync.js +196 -0
  27. package/dist/src/server-helpers/github-projects.js +147 -0
  28. package/dist/src/server-helpers/github-reconciler.js +89 -0
  29. package/dist/src/server-helpers/http-router.js +3781 -0
  30. package/dist/src/server-helpers/http-utils.js +135 -0
  31. package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
  32. package/dist/src/server-helpers/inspector-jobs.js +4145 -0
  33. package/dist/src/server-helpers/issue-analysis.js +362 -0
  34. package/dist/src/server-helpers/normalizers.js +31 -0
  35. package/dist/src/server-helpers/notifications.js +96 -0
  36. package/dist/src/server-helpers/orchestration-ops.js +287 -0
  37. package/dist/src/server-helpers/orchestration.js +39 -0
  38. package/dist/src/server-helpers/plugin-host-cache.js +86 -0
  39. package/dist/src/server-helpers/project-fs-ops.js +194 -0
  40. package/dist/src/server-helpers/project-registry.js +124 -0
  41. package/dist/src/server-helpers/queue-state.js +78 -0
  42. package/dist/src/server-helpers/remote-checkout.js +140 -0
  43. package/dist/src/server-helpers/remote-snapshots.js +119 -0
  44. package/dist/src/server-helpers/run-io.js +262 -0
  45. package/dist/src/server-helpers/run-mutations.js +1784 -0
  46. package/dist/src/server-helpers/run-steering.js +176 -0
  47. package/dist/src/server-helpers/run-writers.js +75 -0
  48. package/dist/src/server-helpers/server-paths.js +27 -0
  49. package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
  50. package/dist/src/server-helpers/snapshot-service.js +1143 -0
  51. package/dist/src/server-helpers/summaries.js +126 -0
  52. package/dist/src/server-helpers/task-config.js +50 -0
  53. package/dist/src/server-helpers/task-projection.js +98 -0
  54. package/dist/src/server-helpers/terminal-runtime.js +156 -0
  55. package/dist/src/server-helpers/terminal-sessions.js +22 -0
  56. package/dist/src/server-helpers/validation-failure.js +31 -0
  57. package/dist/src/server-helpers/ws-router.js +1308 -0
  58. package/dist/src/server.js +12628 -0
  59. package/dist/src/websocket.js +63 -0
  60. package/package.json +33 -0
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,479 @@
1
+ // @bun
2
+ // packages/server/src/inspector/upstream-sync.ts
3
+ import { spawnSync } from "child_process";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
5
+ import { dirname as dirname2, resolve as resolve2 } from "path";
6
+ import { resolveMonorepoRoot as resolveMonorepoRoot2 } from "@rig/runtime/control-plane/native/utils";
7
+
8
+ // packages/server/src/bootstrap.ts
9
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
10
+ import { dirname, resolve } from "path";
11
+ import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
12
+ function normalizeOptionalString(value) {
13
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
14
+ }
15
+ function resolveRigServerPaths(projectRoot) {
16
+ const taskWorkspace = normalizeOptionalString(process.env.RIG_TASK_WORKSPACE);
17
+ const explicitStateDir = normalizeOptionalString(process.env.RIG_STATE_DIR);
18
+ const explicitLogsDir = normalizeOptionalString(process.env.RIG_LOGS_DIR);
19
+ const explicitSessionFile = normalizeOptionalString(process.env.RIG_SESSION_FILE);
20
+ const hostStateRoot = resolve(projectRoot, ".rig");
21
+ const monorepoRoot = resolveMonorepoRoot(projectRoot);
22
+ const monorepoStateRoot = resolve(monorepoRoot, ".rig");
23
+ const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : existsSync(hostStateRoot) ? hostStateRoot : monorepoStateRoot;
24
+ const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
25
+ const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
26
+ const taskConfigPath = taskWorkspace ? resolve(taskWorkspace, ".rig", "task-config.json") : existsSync(resolve(projectRoot, ".rig", "task-config.json")) ? resolve(projectRoot, ".rig", "task-config.json") : resolve(monorepoStateRoot, "task-config.json");
27
+ return {
28
+ stateRoot,
29
+ stateDir,
30
+ logsDir,
31
+ controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
32
+ taskConfigPath,
33
+ notificationsFile: resolve(projectRoot, "rig", "notifications", "targets.json"),
34
+ keybindingsPath: resolve(projectRoot, "rig", "keybindings.json")
35
+ };
36
+ }
37
+
38
+ // packages/server/src/inspector/upstream-sync.ts
39
+ var UPSTREAM_VALIDATION_DESCRIPTIONS = {
40
+ "integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
41
+ "integration:hg-core-security-backport": "Preserves the vendored core-app and backend security fixes for OTP validation, NoSQL/operator hardening, AES/encryption behavior, and rate-limit or redirect-state protections.",
42
+ "integration:hg-boundary-hardening": "Preserves boundary-safe CORS behavior and credential lookup error hygiene across the vendored backend and the extracted credentials-service analogue.",
43
+ "integration:hg-uudl-runtime": "Preserves the upstream uudl runtime/package fixes: production dependency placement, path-resolution/runtime bootstrap behavior, AWS/MSK environment detection, KMS coverage, and a compiling UUDL tree.",
44
+ "boundary:hg-auth-triage": "Requires a durable keep/adapt/reject triage record for the post-import auth policy and mobile-wallet SIWE commits so the watcher does not keep rediscovering the same ambiguity.",
45
+ "boundary:hg-upstream-triage": "Requires a durable keep/adapt/reject triage record for service-relevant upstream commits that do not match the current portable catalog, including commit hashes, touched paths, and follow-up routing decisions."
46
+ };
47
+ var CLUSTERS = {
48
+ "auth-hardening": {
49
+ classification: "portable-now",
50
+ title: "[HG-001] Preserve upstream auth and shared-token hardening across extracted auth work",
51
+ description: "Backport clearly portable auth changes introduced upstream after imported revision 58b56e15: 3196d5c (node-client login nonce), bb5b74f (JWT signature validation), and 92011ead (shared token verification for wallet signup onboarding). Apply them where they belong in this repo: hp-auth-service, hp-gateway if verification semantics surface there, and the upstream monorepo auth owner flows for the remaining wallet-login/shared-token work. This task must preserve the security behavior, not just copy code. It should explicitly reconcile with bd-k0y-10, bd-k0y-11, bd-k0y-12, and bd-k0y-13.",
52
+ acceptanceCriteria: "Map upstream commits 3196d5c, bb5b74f, and 92011ead to exact local targets; preserve nonce-backed node-client login, JWT signature validation, and shared-token verification behavior; add automated tests that fail without the hardening; document any deliberate non-ports.",
53
+ role: "extractor",
54
+ scope: [
55
+ "repos/spliter-monorepo/microservices/hp-auth-service/**",
56
+ "repos/spliter-monorepo/microservices/hp-gateway/**",
57
+ "repos/spliter-monorepo/humoongate/moongate/core-app/**"
58
+ ],
59
+ validation: ["integration:hg-auth-backport", "boundary:changed-files"],
60
+ validationDescriptions: {
61
+ "integration:hg-auth-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-auth-backport"]
62
+ },
63
+ labels: ["upstream:monorepo", "kind:backport", "cluster:auth"]
64
+ },
65
+ "core-security": {
66
+ classification: "portable-now",
67
+ title: "[HG-002] Backport core-app and backend security fixes from post-import upstream",
68
+ description: "Backport the clearly portable security fixes from efdae709 (OTP validation bypass), f63830d (NoSQL operator injection hardening), d7f6919 (AES encryption), and af86ac16 (three critical/high vulnerabilities). Preserve the behavior in the vendored auth owner and the touched backend query surfaces instead of treating these as optional cleanups. The high-value touched files from af86ac16 must be reviewed and ported or consciously waived with rationale.",
69
+ acceptanceCriteria: "Review and port the exact security behaviors from efdae709, f63830d, d7f6919, and af86ac16; cover the listed core-app and backend touched files; add regression tests for OTP bypass, NoSQL operator injection, AES/encryption behavior, and rate-limit or redirect-state hardening; document any consciously waived upstream changes.",
70
+ role: "mechanic",
71
+ scope: [
72
+ "repos/spliter-monorepo/humoongate/moongate/core-app/**",
73
+ "repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**"
74
+ ],
75
+ validation: ["integration:hg-core-security-backport", "boundary:changed-files"],
76
+ validationDescriptions: {
77
+ "integration:hg-core-security-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-core-security-backport"]
78
+ },
79
+ labels: ["upstream:monorepo", "kind:backport", "cluster:security"]
80
+ },
81
+ "boundary-hardening": {
82
+ classification: "portable-now",
83
+ title: "[HG-003] Backport request-boundary hardening for CORS and credential lookup flows",
84
+ description: "Backport the clearly portable boundary hardening from 083bbe4 (CORS config) and 36d52fba (credential lookup error hygiene). Preserve request-boundary behavior in the vendored backend and any analogous extracted credential-facing service seams so attackers cannot learn internals from credential lookup failures or exploit inconsistent CORS policy.",
85
+ acceptanceCriteria: "Port the exact request-boundary behavior from 083bbe4 and 36d52fba; preserve safe CORS policy and credential lookup error hygiene; add tests showing callers do not receive overly detailed credential lookup failures; document any extracted-service analogue that must mirror the behavior.",
86
+ role: "mechanic",
87
+ scope: [
88
+ "repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**",
89
+ "repos/spliter-monorepo/microservices/hp-credentials-service/**"
90
+ ],
91
+ validation: ["integration:hg-boundary-hardening", "boundary:changed-files"],
92
+ validationDescriptions: {
93
+ "integration:hg-boundary-hardening": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-boundary-hardening"]
94
+ },
95
+ labels: ["upstream:monorepo", "kind:backport", "cluster:boundary"]
96
+ },
97
+ "uudl-runtime": {
98
+ classification: "portable-now",
99
+ title: "[HG-004] Backport uudl runtime and packaging fixes from upstream",
100
+ description: "Backport the clearly portable uudl runtime fixes from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03. Preserve the runtime behavior rather than cherry-picking filenames: production dependency placement, tsconfig-paths resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS wiring all need to be reflected in whichever UUDL runtime this repo still owns.",
101
+ acceptanceCriteria: "Port the runtime behaviors from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03; preserve production dependency placement, tsconfig-paths runtime resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS configuration; add runtime-focused tests or smoke coverage for each preserved behavior.",
102
+ role: "extractor",
103
+ scope: ["repos/spliter-monorepo/humoongate/humanity/hp-uudl/**"],
104
+ validation: ["integration:hg-uudl-runtime", "boundary:changed-files"],
105
+ validationDescriptions: {
106
+ "integration:hg-uudl-runtime": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-uudl-runtime"]
107
+ },
108
+ labels: ["upstream:monorepo", "kind:backport", "cluster:uudl"]
109
+ },
110
+ "auth-triage": {
111
+ classification: "needs-human-triage",
112
+ title: "[HG-005] Triage post-import auth policy deltas and mobile-wallet SIWE changes",
113
+ description: "Review the upstream changes that are relevant but not safely auto-portable: 1262b75 (JWT_EXPIRES_IN 1d -> 7d), 15036424 (unlink-wallet endpoint and Wallet.default fix), and d4a47015 (unlink-wallet error handling). Produce an explicit keep/adapt/reject decision for each commit with rationale tied to the extracted auth plan, core-app ownership, and current threat model, and record the result in artifacts/bd-2ztk.5/decision-log.md so future agents do not silently re-litigate these decisions.",
114
+ acceptanceCriteria: "For 1262b75, 15036424, and d4a47015, produce explicit keep/adapt/reject decisions with rationale tied to auth ownership and threat model; record the decision in artifacts/bd-2ztk.5/decision-log.md; identify any follow-on implementation tasks if the answer is keep or adapt.",
115
+ role: "architect",
116
+ scope: ["artifacts/bd-2ztk.5/**", "docs/research/**"],
117
+ validation: ["boundary:hg-auth-triage"],
118
+ validationDescriptions: {
119
+ "boundary:hg-auth-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-auth-triage"]
120
+ },
121
+ labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
122
+ },
123
+ "generic-triage": {
124
+ classification: "needs-human-triage",
125
+ title: "[HG-007] Triage uncatalogued service-relevant upstream commits",
126
+ description: "Review future upstream commits that touch service-relevant paths but do not match the current portable-now catalog. For each commit or batch, produce keep/adapt/reject decisions with rationale, cite the touched paths and why they matter here, and either route them into an existing backport task or open a new follow-up task when they are worth preserving.",
127
+ acceptanceCriteria: "For each uncatalogued service-relevant upstream commit or batch, record keep/adapt/reject decisions with rationale in artifacts/bd-2ztk.7/decision-log.md; cite the commit hashes and touched paths; and either attach the work to an existing backport task or open a new follow-up task when preservation is warranted.",
128
+ role: "architect",
129
+ scope: ["artifacts/bd-2ztk.7/**", "docs/research/**"],
130
+ validation: ["boundary:hg-upstream-triage"],
131
+ validationDescriptions: {
132
+ "boundary:hg-upstream-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-upstream-triage"]
133
+ },
134
+ labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
135
+ }
136
+ };
137
+ var RELEVANT_PREFIXES = [
138
+ "humanity/hp-backend-ts/",
139
+ "humanity/hp-dev-api/",
140
+ "humanity/hp-uudl/",
141
+ "moongate/core-app/",
142
+ "moongate/models/",
143
+ "packages/",
144
+ "shared-migrations/"
145
+ ];
146
+ var IGNORE_PATTERNS = [
147
+ /hp-next|dashboard|frontend/i,
148
+ /CODEOWNERS|CodeRabbit|security-workflow|workflow housekeeping/i,
149
+ /npm registry|workspace packages to npm/i,
150
+ /bulk event|organizer/i
151
+ ];
152
+ function parseImportedUpstreamRevision(doc, upstreamName = "upstream") {
153
+ const sectionPattern = new RegExp(`##\\s+${upstreamName}\\b([\\s\\S]*?)(?:\\n##\\s+|$)`, "i");
154
+ const sectionMatch = doc.match(sectionPattern);
155
+ if (!sectionMatch) {
156
+ return null;
157
+ }
158
+ const revisionMatch = sectionMatch[1]?.match(/Imported revision:\s*`([0-9a-f]{40})`/i);
159
+ return revisionMatch?.[1] ?? null;
160
+ }
161
+ function normalizeGitResult(result) {
162
+ return {
163
+ exitCode: result.status ?? 1,
164
+ stdout: typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") ?? "",
165
+ stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString("utf8") ?? ""
166
+ };
167
+ }
168
+ function defaultGitRunner(repoRoot, args) {
169
+ return normalizeGitResult(spawnSync("git", ["-C", repoRoot, ...args], {
170
+ encoding: "utf8"
171
+ }));
172
+ }
173
+ function upstreamStatePath(projectRoot, override) {
174
+ if (override) {
175
+ return resolve2(override);
176
+ }
177
+ return resolve2(resolveRigServerPaths(projectRoot).stateDir, "inspector", "upstream-sync.json");
178
+ }
179
+ function readUpstreamState(projectRoot, statePath) {
180
+ const path = upstreamStatePath(projectRoot, statePath);
181
+ if (!existsSync2(path)) {
182
+ return null;
183
+ }
184
+ try {
185
+ return JSON.parse(readFileSync(path, "utf-8"));
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function writeUpstreamState(projectRoot, state, statePath) {
191
+ const path = upstreamStatePath(projectRoot, statePath);
192
+ mkdirSync2(dirname2(path), { recursive: true });
193
+ writeFileSync2(path, `${JSON.stringify(state, null, 2)}
194
+ `, "utf8");
195
+ }
196
+ function readImportedRevision(projectRoot, upstreamsDocPath) {
197
+ const monorepoRoot = resolveMonorepoRoot2(projectRoot);
198
+ const docPath = upstreamsDocPath ? resolve2(upstreamsDocPath) : resolve2(monorepoRoot, "docs", "UPSTREAMS.md");
199
+ if (!existsSync2(docPath)) {
200
+ throw new Error(`UPSTREAMS.md not found at ${docPath}`);
201
+ }
202
+ const docContent = readFileSync(docPath, "utf-8");
203
+ const revision = parseImportedUpstreamRevision(docContent, "upstream") ?? parseImportedUpstreamRevision(docContent, "humoongate");
204
+ if (!revision) {
205
+ throw new Error(`Failed to parse upstream imported revision from ${docPath}`);
206
+ }
207
+ return revision;
208
+ }
209
+ function resolveRemoteBranch(repoRoot, remote, gitRunner) {
210
+ const symbolic = gitRunner(repoRoot, ["symbolic-ref", `refs/remotes/${remote}/HEAD`]);
211
+ const symbolicRef = symbolic.stdout.trim();
212
+ if (symbolic.exitCode === 0 && symbolicRef.length > 0) {
213
+ return symbolicRef.replace(/^refs\/remotes\//, "");
214
+ }
215
+ for (const fallback of [`${remote}/main`, `${remote}/master`]) {
216
+ const showRef = gitRunner(repoRoot, ["show-ref", "--verify", `refs/remotes/${fallback}`]);
217
+ if (showRef.exitCode === 0) {
218
+ return fallback;
219
+ }
220
+ }
221
+ return null;
222
+ }
223
+ function isGitCheckout(path, gitRunner) {
224
+ if (!existsSync2(resolve2(path, ".git"))) {
225
+ return false;
226
+ }
227
+ const result = gitRunner(path, ["rev-parse", "--is-inside-work-tree"]);
228
+ return result.exitCode === 0 && result.stdout.trim() === "true";
229
+ }
230
+ function resolveUpstreamCheckout(projectRoot, explicitCheckout, gitRunner) {
231
+ const monorepoRoot = resolveMonorepoRoot2(projectRoot);
232
+ const candidates = [
233
+ explicitCheckout ? resolve2(explicitCheckout) : "",
234
+ process.env.UPSTREAM_CHECKOUT?.trim() ? resolve2(process.env.UPSTREAM_CHECKOUT.trim()) : "",
235
+ process.env.HUMOONGATE_UPSTREAM_CHECKOUT?.trim() ? resolve2(process.env.HUMOONGATE_UPSTREAM_CHECKOUT.trim()) : "",
236
+ resolve2(projectRoot, "..", "humoongate"),
237
+ resolve2(monorepoRoot, "..", "humoongate"),
238
+ resolve2(monorepoRoot, "humoongate")
239
+ ].filter(Boolean);
240
+ for (const candidate of candidates) {
241
+ if (isGitCheckout(candidate, gitRunner)) {
242
+ return candidate;
243
+ }
244
+ }
245
+ throw new Error(`Unable to locate an authoritative upstream checkout. Checked: ${candidates.join(", ")}`);
246
+ }
247
+ function parseCommitLog(output) {
248
+ const commits = [];
249
+ let current = null;
250
+ for (const rawLine of output.split(/\r?\n/)) {
251
+ const line = rawLine.trim();
252
+ if (line === "__COMMIT__") {
253
+ if (current) {
254
+ commits.push(current);
255
+ }
256
+ current = null;
257
+ continue;
258
+ }
259
+ if (!line) {
260
+ continue;
261
+ }
262
+ if (!current) {
263
+ const [sha, date, ...rest] = line.split("\t");
264
+ if (!sha || !date) {
265
+ continue;
266
+ }
267
+ current = {
268
+ sha,
269
+ date,
270
+ summary: rest.join("\t").trim(),
271
+ paths: []
272
+ };
273
+ continue;
274
+ }
275
+ current.paths.push(line);
276
+ }
277
+ if (current) {
278
+ commits.push(current);
279
+ }
280
+ return commits;
281
+ }
282
+ function relevantPaths(paths) {
283
+ return paths.filter((path) => RELEVANT_PREFIXES.some((prefix) => path.startsWith(prefix)));
284
+ }
285
+ function classifyCommit(commit) {
286
+ const summary = commit.summary.toLowerCase();
287
+ const paths = relevantPaths(commit.paths);
288
+ const pathText = paths.join(" ").toLowerCase();
289
+ const signature = `${summary}
290
+ ${pathText}`;
291
+ if (IGNORE_PATTERNS.some((pattern) => pattern.test(signature))) {
292
+ return { classification: "ignore", clusterKey: null };
293
+ }
294
+ if (/jwt_expires_in|unlink-wallet|mobile wallet|siwe endpoint/i.test(signature)) {
295
+ return { classification: "needs-human-triage", clusterKey: "auth-triage" };
296
+ }
297
+ if (/node-client login use nonce|jwt signature validation|shared token verification|wallet signup onboarding/i.test(signature)) {
298
+ return { classification: "portable-now", clusterKey: "auth-hardening" };
299
+ }
300
+ if (/otp validation bypass|otp queries|aes encryption|critical\/high vulnerabilities|redirect jwt steal/i.test(signature)) {
301
+ return { classification: "portable-now", clusterKey: "core-security" };
302
+ }
303
+ if (/cors config|credential lookup errors/i.test(signature)) {
304
+ return { classification: "portable-now", clusterKey: "boundary-hardening" };
305
+ }
306
+ if (paths.some((path) => path.startsWith("humanity/hp-uudl/")) && /tsconfig-paths|circular dependency|msk|kms port|runtime|production dependenc/i.test(signature)) {
307
+ return { classification: "portable-now", clusterKey: "uudl-runtime" };
308
+ }
309
+ if (paths.length > 0) {
310
+ return { classification: "needs-human-triage", clusterKey: "generic-triage" };
311
+ }
312
+ return { classification: "ignore", clusterKey: null };
313
+ }
314
+ function scanUpstream(options) {
315
+ const now = options.now ?? (() => new Date().toISOString());
316
+ const gitRunner = options.gitRunner ?? defaultGitRunner;
317
+ const remote = options.remote ?? "origin";
318
+ const baselineSha = options.baselineSha ?? readImportedRevision(options.projectRoot, options.upstreamsDocPath);
319
+ const checkoutPath = resolveUpstreamCheckout(options.projectRoot, options.explicitCheckout, gitRunner);
320
+ if (options.fetch !== false) {
321
+ const fetchResult = gitRunner(checkoutPath, ["fetch", remote]);
322
+ if (fetchResult.exitCode !== 0) {
323
+ throw new Error(`Failed to fetch ${remote} in ${checkoutPath}: ${fetchResult.stderr || fetchResult.stdout}`);
324
+ }
325
+ }
326
+ const branch = options.branch ?? resolveRemoteBranch(checkoutPath, remote, gitRunner);
327
+ if (!branch) {
328
+ throw new Error(`Failed to resolve tracked branch for remote ${remote} in ${checkoutPath}`);
329
+ }
330
+ const headResult = gitRunner(checkoutPath, ["rev-parse", branch]);
331
+ if (headResult.exitCode !== 0 || !headResult.stdout.trim()) {
332
+ throw new Error(`Failed to resolve latest upstream head for ${branch}: ${headResult.stderr || headResult.stdout}`);
333
+ }
334
+ const latestHeadSha = headResult.stdout.trim();
335
+ const state = readUpstreamState(options.projectRoot, options.statePath);
336
+ let sinceSha = state?.lastProcessedCommit || baselineSha;
337
+ const ancestorCheck = gitRunner(checkoutPath, ["merge-base", "--is-ancestor", sinceSha, branch]);
338
+ if (ancestorCheck.exitCode !== 0) {
339
+ sinceSha = baselineSha;
340
+ }
341
+ const logResult = gitRunner(checkoutPath, [
342
+ "log",
343
+ "--reverse",
344
+ "--date=short",
345
+ "--name-only",
346
+ "--pretty=format:__COMMIT__%n%H%x09%cs%x09%s",
347
+ `${sinceSha}..${branch}`
348
+ ]);
349
+ if (logResult.exitCode !== 0) {
350
+ throw new Error(`Failed to collect upstream commits: ${logResult.stderr || logResult.stdout}`);
351
+ }
352
+ const commits = parseCommitLog(logResult.stdout);
353
+ const grouped = new Map;
354
+ for (const commit of commits) {
355
+ const classification = classifyCommit(commit);
356
+ if (!classification.clusterKey) {
357
+ continue;
358
+ }
359
+ const existing = grouped.get(classification.clusterKey) ?? [];
360
+ existing.push(commit);
361
+ grouped.set(classification.clusterKey, existing);
362
+ }
363
+ const findings = Array.from(grouped.entries()).map(([clusterKey, clusterCommits]) => {
364
+ const definition = CLUSTERS[clusterKey];
365
+ const newestCommit = clusterCommits[clusterCommits.length - 1];
366
+ return {
367
+ key: `upstream:${definition.classification}:${clusterKey}:${newestCommit.sha}`,
368
+ summary: definition.title,
369
+ details: {
370
+ remote,
371
+ branch,
372
+ checkoutPath,
373
+ baselineSha,
374
+ sinceSha,
375
+ latestHeadSha,
376
+ classification: definition.classification,
377
+ clusterKey,
378
+ description: definition.description,
379
+ acceptanceCriteria: definition.acceptanceCriteria,
380
+ role: definition.role,
381
+ scope: definition.scope,
382
+ validation: definition.validation,
383
+ validationDescriptions: definition.validationDescriptions,
384
+ labels: definition.labels,
385
+ commits: clusterCommits.map((commit) => ({
386
+ sha: commit.sha,
387
+ date: commit.date,
388
+ summary: commit.summary,
389
+ paths: relevantPaths(commit.paths)
390
+ }))
391
+ }
392
+ };
393
+ });
394
+ const groupedCommitCount = Array.from(grouped.values()).reduce((sum, clusterCommits) => sum + clusterCommits.length, 0);
395
+ return {
396
+ baselineSha,
397
+ sinceSha,
398
+ latestHeadSha,
399
+ checkoutPath,
400
+ remote,
401
+ branch,
402
+ scannedCommitCount: commits.length,
403
+ ignoredCommitCount: Math.max(0, commits.length - groupedCommitCount),
404
+ findings,
405
+ statePath: upstreamStatePath(options.projectRoot, options.statePath)
406
+ };
407
+ }
408
+ async function runUpstreamSyncScan(options) {
409
+ const now = options.now ?? (() => new Date().toISOString());
410
+ const startedAt = now();
411
+ let followupCount = 0;
412
+ const createdTaskIds = [];
413
+ for (const finding of options.findings) {
414
+ const existingFollowup = options.journal.getFollowupByDedupeKey(finding.key);
415
+ if (!existingFollowup) {
416
+ const result = options.journal.appendFollowup({
417
+ id: `upstream-followup:${finding.key}`,
418
+ originRunId: null,
419
+ dedupeKey: finding.key,
420
+ taskId: null,
421
+ kind: "upstream-drift",
422
+ status: "proposed",
423
+ summary: finding.summary,
424
+ details: finding.details ?? null,
425
+ createdAt: startedAt
426
+ });
427
+ if (!result.ok) {
428
+ throw new Error(`Failed to record upstream follow-up ${finding.key}: ${result.error}`);
429
+ }
430
+ if (result.changed) {
431
+ followupCount += 1;
432
+ }
433
+ }
434
+ const followup = options.journal.getFollowupByDedupeKey(finding.key);
435
+ if (options.createTask && followup?.taskId == null) {
436
+ const created = await options.createTask(finding);
437
+ if (created.taskId) {
438
+ createdTaskIds.push(created.taskId);
439
+ const attachment = options.journal.attachFollowupTask({
440
+ dedupeKey: finding.key,
441
+ taskId: created.taskId,
442
+ status: "created",
443
+ details: finding.details && created.details ? { ...finding.details, followupTask: created.details } : created.details ?? finding.details ?? null
444
+ });
445
+ if (!attachment.ok) {
446
+ throw new Error(`Failed to attach task ${created.taskId} to upstream follow-up ${finding.key}: ${attachment.error}`);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ const summary = options.findings.length === 0 ? "No upstream drift findings" : `Detected ${options.findings.length} upstream drift finding(s)`;
452
+ options.journal.appendUpstreamSyncScan({
453
+ id: options.scanId,
454
+ dedupeKey: options.scanId,
455
+ startedAt,
456
+ completedAt: startedAt,
457
+ status: "completed",
458
+ summary,
459
+ details: {
460
+ findingKeys: options.findings.map((finding) => finding.key),
461
+ followupCount,
462
+ createdTaskIds
463
+ }
464
+ });
465
+ return {
466
+ status: "completed",
467
+ summary,
468
+ followupCount,
469
+ createdTaskIds
470
+ };
471
+ }
472
+ export {
473
+ writeUpstreamState,
474
+ scanUpstream,
475
+ runUpstreamSyncScan,
476
+ readUpstreamState,
477
+ parseImportedUpstreamRevision,
478
+ classifyCommit
479
+ };