@archme/cli 0.15.1 → 0.17.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.
- package/dist/cli.js +295 -141
- package/package.json +1 -1
- package/sandbox-image.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13691,6 +13691,89 @@ var init_repoContainer = __esm({
|
|
|
13691
13691
|
}
|
|
13692
13692
|
});
|
|
13693
13693
|
|
|
13694
|
+
// ../shared/seedArchitecture.ts
|
|
13695
|
+
function seedArchitectureFromRows(rows) {
|
|
13696
|
+
const components = [];
|
|
13697
|
+
const stories = [];
|
|
13698
|
+
for (const row of rows) {
|
|
13699
|
+
const compPath = componentPathFromFilePath(row.path);
|
|
13700
|
+
if (compPath !== null) {
|
|
13701
|
+
const parsed = tryParse(row.content);
|
|
13702
|
+
components.push({
|
|
13703
|
+
path: compPath,
|
|
13704
|
+
anchors: parsed?.anchors ?? [],
|
|
13705
|
+
unanchored: parsed?.unanchored === true ? true : void 0
|
|
13706
|
+
});
|
|
13707
|
+
continue;
|
|
13708
|
+
}
|
|
13709
|
+
const scope = storyScopeFromFilePath(row.path);
|
|
13710
|
+
if (scope !== null) {
|
|
13711
|
+
const parsed = tryParse(row.content);
|
|
13712
|
+
if (!parsed) continue;
|
|
13713
|
+
collectSeedStories(parsed, scope, row.path, stories);
|
|
13714
|
+
}
|
|
13715
|
+
}
|
|
13716
|
+
return { components, stories };
|
|
13717
|
+
}
|
|
13718
|
+
function tryParse(s) {
|
|
13719
|
+
try {
|
|
13720
|
+
return JSON.parse(s);
|
|
13721
|
+
} catch {
|
|
13722
|
+
return null;
|
|
13723
|
+
}
|
|
13724
|
+
}
|
|
13725
|
+
function componentPathFromFilePath(p) {
|
|
13726
|
+
if (!p.endsWith("/component.json")) return null;
|
|
13727
|
+
const inner = p.slice(0, -"/component.json".length);
|
|
13728
|
+
if (inner.length === 0) return null;
|
|
13729
|
+
const segments = inner.split("/");
|
|
13730
|
+
if (segments.length % 2 !== 0) return null;
|
|
13731
|
+
const ids = [];
|
|
13732
|
+
for (let i2 = 0; i2 < segments.length; i2 += 2) {
|
|
13733
|
+
if (segments[i2] !== "components") return null;
|
|
13734
|
+
const id = segments[i2 + 1];
|
|
13735
|
+
if (!id) return null;
|
|
13736
|
+
ids.push(id);
|
|
13737
|
+
}
|
|
13738
|
+
return `root.${ids.join(".")}`;
|
|
13739
|
+
}
|
|
13740
|
+
function storyScopeFromFilePath(p) {
|
|
13741
|
+
if (!STORY_PATH_RE.test(p)) return null;
|
|
13742
|
+
const prefix = p.slice(0, p.lastIndexOf("stories/"));
|
|
13743
|
+
if (prefix.length === 0) return [];
|
|
13744
|
+
const segments = prefix.split("/").filter(Boolean);
|
|
13745
|
+
const ids = [];
|
|
13746
|
+
for (let i2 = 0; i2 < segments.length; i2 += 2) {
|
|
13747
|
+
if (segments[i2] !== "components") return null;
|
|
13748
|
+
const id = segments[i2 + 1];
|
|
13749
|
+
if (!id) return null;
|
|
13750
|
+
ids.push(id);
|
|
13751
|
+
}
|
|
13752
|
+
return ids;
|
|
13753
|
+
}
|
|
13754
|
+
function collectSeedStories(raw, scope, fileFallbackId, out, parentId) {
|
|
13755
|
+
const id = raw.id ?? fileFallbackId;
|
|
13756
|
+
out.push({
|
|
13757
|
+
id,
|
|
13758
|
+
title: raw.title ?? id,
|
|
13759
|
+
sequence: raw.sequence ?? "",
|
|
13760
|
+
draft: raw.draft === true ? true : void 0,
|
|
13761
|
+
scope,
|
|
13762
|
+
parentId
|
|
13763
|
+
});
|
|
13764
|
+
if (Array.isArray(raw.userStories)) {
|
|
13765
|
+
for (const child of raw.userStories) {
|
|
13766
|
+
collectSeedStories(child, scope, fileFallbackId, out, id);
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13770
|
+
var STORY_PATH_RE;
|
|
13771
|
+
var init_seedArchitecture = __esm({
|
|
13772
|
+
"../shared/seedArchitecture.ts"() {
|
|
13773
|
+
STORY_PATH_RE = /^(?:components\/[^/]+\/)*stories\/[^/]+\.json$/;
|
|
13774
|
+
}
|
|
13775
|
+
});
|
|
13776
|
+
|
|
13694
13777
|
// src/scan/affected.ts
|
|
13695
13778
|
function normalizePath(p) {
|
|
13696
13779
|
return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
|
|
@@ -13904,42 +13987,6 @@ function formatAffectedSeed(result, removedSymbols = [], allStories) {
|
|
|
13904
13987
|
}
|
|
13905
13988
|
return lines.join("\n");
|
|
13906
13989
|
}
|
|
13907
|
-
function tryParse(s) {
|
|
13908
|
-
try {
|
|
13909
|
-
return JSON.parse(s);
|
|
13910
|
-
} catch {
|
|
13911
|
-
return null;
|
|
13912
|
-
}
|
|
13913
|
-
}
|
|
13914
|
-
function componentPathFromFilePath(p) {
|
|
13915
|
-
if (!p.endsWith("/component.json")) return null;
|
|
13916
|
-
const inner = p.slice(0, -"/component.json".length);
|
|
13917
|
-
if (inner.length === 0) return null;
|
|
13918
|
-
const segments = inner.split("/");
|
|
13919
|
-
if (segments.length % 2 !== 0) return null;
|
|
13920
|
-
const ids = [];
|
|
13921
|
-
for (let i2 = 0; i2 < segments.length; i2 += 2) {
|
|
13922
|
-
if (segments[i2] !== "components") return null;
|
|
13923
|
-
const id = segments[i2 + 1];
|
|
13924
|
-
if (!id) return null;
|
|
13925
|
-
ids.push(id);
|
|
13926
|
-
}
|
|
13927
|
-
return `root.${ids.join(".")}`;
|
|
13928
|
-
}
|
|
13929
|
-
function storyScopeFromFilePath(p) {
|
|
13930
|
-
if (!STORY_PATH_RE.test(p)) return null;
|
|
13931
|
-
const prefix = p.slice(0, p.lastIndexOf("stories/"));
|
|
13932
|
-
if (prefix.length === 0) return [];
|
|
13933
|
-
const segments = prefix.split("/").filter(Boolean);
|
|
13934
|
-
const ids = [];
|
|
13935
|
-
for (let i2 = 0; i2 < segments.length; i2 += 2) {
|
|
13936
|
-
if (segments[i2] !== "components") return null;
|
|
13937
|
-
const id = segments[i2 + 1];
|
|
13938
|
-
if (!id) return null;
|
|
13939
|
-
ids.push(id);
|
|
13940
|
-
}
|
|
13941
|
-
return ids;
|
|
13942
|
-
}
|
|
13943
13990
|
async function loadArchitectureForSeeding(supabase, projectId, branch) {
|
|
13944
13991
|
const PAGE_SIZE2 = 1e3;
|
|
13945
13992
|
const rows = [];
|
|
@@ -13950,54 +13997,18 @@ async function loadArchitectureForSeeding(supabase, projectId, branch) {
|
|
|
13950
13997
|
rows.push(...data);
|
|
13951
13998
|
if (data.length < PAGE_SIZE2) break;
|
|
13952
13999
|
}
|
|
13953
|
-
|
|
13954
|
-
const stories = [];
|
|
13955
|
-
for (const row of rows) {
|
|
13956
|
-
const compPath = componentPathFromFilePath(row.path);
|
|
13957
|
-
if (compPath !== null) {
|
|
13958
|
-
const parsed = tryParse(row.content);
|
|
13959
|
-
components.push({
|
|
13960
|
-
path: compPath,
|
|
13961
|
-
anchors: parsed?.anchors ?? [],
|
|
13962
|
-
unanchored: parsed?.unanchored === true ? true : void 0
|
|
13963
|
-
});
|
|
13964
|
-
continue;
|
|
13965
|
-
}
|
|
13966
|
-
const scope = storyScopeFromFilePath(row.path);
|
|
13967
|
-
if (scope !== null) {
|
|
13968
|
-
const parsed = tryParse(row.content);
|
|
13969
|
-
if (!parsed) continue;
|
|
13970
|
-
collectSeedStories(parsed, scope, row.path, stories);
|
|
13971
|
-
}
|
|
13972
|
-
}
|
|
13973
|
-
return { components, stories };
|
|
13974
|
-
}
|
|
13975
|
-
function collectSeedStories(raw, scope, fileFallbackId, out, parentId) {
|
|
13976
|
-
const id = raw.id ?? fileFallbackId;
|
|
13977
|
-
out.push({
|
|
13978
|
-
id,
|
|
13979
|
-
title: raw.title ?? id,
|
|
13980
|
-
sequence: raw.sequence ?? "",
|
|
13981
|
-
draft: raw.draft === true ? true : void 0,
|
|
13982
|
-
scope,
|
|
13983
|
-
parentId
|
|
13984
|
-
});
|
|
13985
|
-
if (Array.isArray(raw.userStories)) {
|
|
13986
|
-
for (const child of raw.userStories) {
|
|
13987
|
-
collectSeedStories(child, scope, fileFallbackId, out, id);
|
|
13988
|
-
}
|
|
13989
|
-
}
|
|
14000
|
+
return seedArchitectureFromRows(rows);
|
|
13990
14001
|
}
|
|
13991
|
-
var MESSAGE_RE, PARTICIPANT_RE, DISPLAY_FILE_CAP
|
|
14002
|
+
var MESSAGE_RE, PARTICIPANT_RE, DISPLAY_FILE_CAP;
|
|
13992
14003
|
var init_affected = __esm({
|
|
13993
14004
|
"src/scan/affected.ts"() {
|
|
13994
14005
|
"use strict";
|
|
13995
14006
|
init_esm3();
|
|
13996
14007
|
init_repoContainer();
|
|
14008
|
+
init_seedArchitecture();
|
|
13997
14009
|
MESSAGE_RE = /^\s*(\{[^}]+\}|\S+?)\s*(--?>>|--?>|->|-->|-x|-\))\s*(\{[^}]+\}|\S+?)\s*:\s*(.*)$/;
|
|
13998
14010
|
PARTICIPANT_RE = /^\s*participant\s+(\S+?)(?:\s+as\s+.+)?$/;
|
|
13999
14011
|
DISPLAY_FILE_CAP = 200;
|
|
14000
|
-
STORY_PATH_RE = /^(?:components\/[^/]+\/)*stories\/[^/]+\.json$/;
|
|
14001
14012
|
}
|
|
14002
14013
|
});
|
|
14003
14014
|
|
|
@@ -16251,10 +16262,11 @@ function buildBugReportGuidance(bugsFolderId) {
|
|
|
16251
16262
|
|
|
16252
16263
|
${folderMechanic}`;
|
|
16253
16264
|
}
|
|
16254
|
-
var BUGS_FOLDER_TITLE, BUG_REPORT_CORE;
|
|
16265
|
+
var BUGS_FOLDER_TITLE, BUGS_FOLDER_DESCRIPTION, BUG_REPORT_CORE;
|
|
16255
16266
|
var init_bugReport = __esm({
|
|
16256
16267
|
"../shared/bugReport.ts"() {
|
|
16257
16268
|
BUGS_FOLDER_TITLE = "Bugs found by ArchMe";
|
|
16269
|
+
BUGS_FOLDER_DESCRIPTION = "Bugs noticed by scan/diff agents in the code while updating the architecture. Prose-only proposals; humans triage.";
|
|
16258
16270
|
BUG_REPORT_CORE = "**Bugs you happen to notice.** Don't hunt for bugs \u2014 stay on the architecture task \u2014 but don't ignore an obvious, real one that surfaces while you read the code (a null-deref, a missing `await`, an unhandled error path, a broken invariant). Have any sub-agents you dispatch report a suspect back to you (a one-line `possible bug: <what> at <file:line>`) rather than filing it themselves. When suspects come back, dispatch ONE bug-validation sub-agent \u2014 it reads code and calls `create_proposal`, exactly like the build sub-agent, and never modifies the user's code \u2014 and have it: verify each suspect against the code, and for every one it confirms as a real defect, FILE it itself as a prose-only proposal (`create_proposal` with `kind: 'proposal'`, `stage: 'backlog'`, an empty graph delta, and a one-paragraph `body`: what is wrong, where by file:line or symbol, and why), `about` the component it lives in (`{ component: '<root.\u2026 path>' }` \u2014 find_components_for_paths maps the file to it), else the user story whose flow it breaks (`{ story: '<id>' }`). It skips anything it can't confirm, and searches the folder by title first to skip anything already logged. Keep the bar high \u2014 obvious, confirmed defects only, a few per run at most, never speculation, style, or a design opinion; record, never fix. Logging a bug is orthogonal to your architecture task and does NOT replace your completion signal \u2014 still call `update_architecture` or `report_no_change` exactly as you otherwise would.";
|
|
16259
16271
|
}
|
|
16260
16272
|
});
|
|
@@ -16685,7 +16697,11 @@ var init_validationCodes = __esm({
|
|
|
16685
16697
|
WALKTHROUGH_COVERAGE_CHAPTER_NOT_COVERED: "walkthrough.coverage.chapter-not-covered",
|
|
16686
16698
|
/** The delta added, removed or modified a component no step names. */
|
|
16687
16699
|
WALKTHROUGH_COVERAGE_COMPONENT_NOT_COVERED: "walkthrough.coverage.component-not-covered",
|
|
16688
|
-
/**
|
|
16700
|
+
/**
|
|
16701
|
+
* The delta changed a connection no step names, joins, or draws.
|
|
16702
|
+
* @deprecated Retired: connections show up through the stories that draw
|
|
16703
|
+
* them, so coverage no longer requires them.
|
|
16704
|
+
*/
|
|
16689
16705
|
WALKTHROUGH_COVERAGE_CONNECTION_NOT_COVERED: "walkthrough.coverage.connection-not-covered",
|
|
16690
16706
|
// ──────────────────────────────────────────────────────────────────────────
|
|
16691
16707
|
// mcp — request-level rules applied before any tool handler runs
|
|
@@ -16742,7 +16758,7 @@ var init_walkthroughGuidance = __esm({
|
|
|
16742
16758
|
init_walkthrough();
|
|
16743
16759
|
WALKTHROUGH_GUIDANCE = `A walkthrough breaks a change into bite-sized chunks a reviewer can step through. Each step names the elements it is about; the review surface filters the sequence diagram and the component graph down to just those, so the reader sees one chunk at a time instead of a forty-element diff at once.
|
|
16744
16760
|
|
|
16745
|
-
- **Every step must name what it is about** \u2014 the stories, chapters, components and/or connections it covers. A step with only prose is rejected: there is nothing to filter the diagrams by, and an explanation a reader cannot check against the change is exactly what this view exists to avoid.
|
|
16761
|
+
- **Every step must name what it is about** \u2014 the stories, chapters, components and/or connections it covers. A step with only prose is rejected: there is nothing to filter the diagrams by, and an explanation a reader cannot check against the change is exactly what this view exists to avoid. Name a connection inside a component as \`<component path>::<id>\` (\`daemon::session_auth->key_account\`); a root connection by its plain id.
|
|
16746
16762
|
- **Steps are in reading order, not build order.** This is not an implementation plan and not a restatement of your sequencing section \u2014 start where a reviewer should start, and put each step where it makes the next one make sense.
|
|
16747
16763
|
- **Group by idea.** One step is one coherent change, usually 3\u20138 elements. A one-element step is fine and often valuable: an independent fix that would otherwise be lost among the rest. Aim for a handful of steps; the cap is ${MAX_WALKTHROUGH_STEPS}.
|
|
16748
16764
|
- **Titles are terse** \u2014 a handle, not a summary. Under ${MAX_STEP_TITLE_CHARS} characters. \`Markers move per-repo\`, not \`This step explains how the source marker moves onto a per-repo table\`.
|
|
@@ -16754,7 +16770,7 @@ var init_walkthroughGuidance = __esm({
|
|
|
16754
16770
|
- Don't use the change's OWN new vocabulary undefined. If the step introduces a term, say what it means in the same breath or use plain words.
|
|
16755
16771
|
- Bad: \`A correctness item, not a phase-3 nicety: anchors are repo-relative paths, so two repos with the same conventional layout collide silently and seed stories from the wrong one.\`
|
|
16756
16772
|
- Good: \`With two repos in one project a path like src/index.ts exists in both, so looking up which component owns a changed file can return the wrong repo's. Each repo now records the container its code lives under, and lookups search only that subtree.\`
|
|
16757
|
-
- **Cover every change** \u2014 every story
|
|
16773
|
+
- **Cover every change** \u2014 every story and component the change touches \u2014 or \`set_walkthrough\` rejects the walkthrough and lists what is missing. Guided mode walks the steps and nothing after them, so anything no step reaches is a change the reviewer never sees. Connections need no step of their own: they show up through the stories that draw them.
|
|
16758
16774
|
- **Skip it for a small change.** A handful of elements needs no walkthrough; omit it rather than writing a step per element.
|
|
16759
16775
|
- **Prefer CHAPTERS to narrow a diagram.** If a story's sequence carries \`%% chapter: <id> | <Label>\` markers, name the ones your step covers as \`chapters: [{ story, chapter }]\`, using the **id**, never the label \u2014 ids are stable across a rename, labels are not. A chapter is a page break the story's author already wrote, so it is sharper than naming components, where one hub the step mentions decides the whole view.`;
|
|
16760
16776
|
}
|
|
@@ -16865,12 +16881,11 @@ async function ensureBugsFolder(client, projectId) {
|
|
|
16865
16881
|
return null;
|
|
16866
16882
|
}
|
|
16867
16883
|
}
|
|
16868
|
-
var
|
|
16884
|
+
var cache;
|
|
16869
16885
|
var init_bugsFolder = __esm({
|
|
16870
16886
|
"src/daemon/bugsFolder.ts"() {
|
|
16871
16887
|
"use strict";
|
|
16872
16888
|
init_bugReport();
|
|
16873
|
-
BUGS_FOLDER_DESCRIPTION = "Bugs noticed by scan/diff agents in the code while updating the architecture. Prose-only proposals; humans triage.";
|
|
16874
16889
|
cache = /* @__PURE__ */ new Map();
|
|
16875
16890
|
}
|
|
16876
16891
|
});
|
|
@@ -19496,7 +19511,7 @@ var init_setting = __esm({
|
|
|
19496
19511
|
}
|
|
19497
19512
|
});
|
|
19498
19513
|
|
|
19499
|
-
//
|
|
19514
|
+
// ../shared/architectureCounts.ts
|
|
19500
19515
|
function countFromPaths(paths) {
|
|
19501
19516
|
let components = 0;
|
|
19502
19517
|
let stories = 0;
|
|
@@ -19510,6 +19525,12 @@ function countFromPaths(paths) {
|
|
|
19510
19525
|
}
|
|
19511
19526
|
return { components, stories, rootStories };
|
|
19512
19527
|
}
|
|
19528
|
+
var init_architectureCounts = __esm({
|
|
19529
|
+
"../shared/architectureCounts.ts"() {
|
|
19530
|
+
}
|
|
19531
|
+
});
|
|
19532
|
+
|
|
19533
|
+
// src/scan/architectureCounts.ts
|
|
19513
19534
|
async function countArchitecture(supabase, projectId, branch) {
|
|
19514
19535
|
const PAGE_SIZE2 = 1e3;
|
|
19515
19536
|
const paths = [];
|
|
@@ -19529,9 +19550,10 @@ function firstScanGate(counts) {
|
|
|
19529
19550
|
reason: `first scan incomplete: the branch holds ${counts.components} component(s) and ${counts.rootStories} root story(ies); needs at least one of each`
|
|
19530
19551
|
};
|
|
19531
19552
|
}
|
|
19532
|
-
var
|
|
19553
|
+
var init_architectureCounts2 = __esm({
|
|
19533
19554
|
"src/scan/architectureCounts.ts"() {
|
|
19534
19555
|
"use strict";
|
|
19556
|
+
init_architectureCounts();
|
|
19535
19557
|
}
|
|
19536
19558
|
});
|
|
19537
19559
|
|
|
@@ -19607,6 +19629,142 @@ var init_claimStance = __esm({
|
|
|
19607
19629
|
}
|
|
19608
19630
|
});
|
|
19609
19631
|
|
|
19632
|
+
// src/daemon/platformDevices.ts
|
|
19633
|
+
async function readPlatformDevices(client, userId) {
|
|
19634
|
+
const { data, error } = await client.from("agent_devices").select("id, name").eq("user_id", userId).is("org_id", null).eq("is_platform", true).eq("status", "enabled");
|
|
19635
|
+
if (error) throw new Error(error.message);
|
|
19636
|
+
return data ?? [];
|
|
19637
|
+
}
|
|
19638
|
+
function withPlatformDevices(devices, platform3) {
|
|
19639
|
+
const out = [...devices];
|
|
19640
|
+
for (const p of platform3) {
|
|
19641
|
+
if (!out.some((d) => d.deviceId === p.id)) out.push({ orgId: null, deviceId: p.id });
|
|
19642
|
+
}
|
|
19643
|
+
return out;
|
|
19644
|
+
}
|
|
19645
|
+
function deviceForOrgLookup(devices) {
|
|
19646
|
+
const members = devices.filter((d) => d.orgId !== null);
|
|
19647
|
+
const byOrg = new Map(members.map((d) => [d.orgId, d.deviceId]));
|
|
19648
|
+
return (orgId) => byOrg.get(orgId) ?? members[0]?.deviceId ?? null;
|
|
19649
|
+
}
|
|
19650
|
+
function poolBootLine(platform3, mode) {
|
|
19651
|
+
if (platform3.length === 0) return "not in managed pool";
|
|
19652
|
+
const names = platform3.map((p) => `${p.name} (${p.id})`).join(", ");
|
|
19653
|
+
return `in managed pool: platform device ${names}, ${mode === "required" ? "active" : "inactive unless sandbox=required"}`;
|
|
19654
|
+
}
|
|
19655
|
+
async function discoverClaimDevices(client, userId, devices, mode, out = console) {
|
|
19656
|
+
let platform3 = [];
|
|
19657
|
+
try {
|
|
19658
|
+
platform3 = await readPlatformDevices(client, userId);
|
|
19659
|
+
} catch (e) {
|
|
19660
|
+
out.warn(`platform device read failed, starting member-only until restart: ${e.message}`);
|
|
19661
|
+
}
|
|
19662
|
+
out.log(` ${poolBootLine(platform3, mode)}`);
|
|
19663
|
+
return { devices: withPlatformDevices(devices, platform3), platform: platform3 };
|
|
19664
|
+
}
|
|
19665
|
+
var init_platformDevices = __esm({
|
|
19666
|
+
"src/daemon/platformDevices.ts"() {
|
|
19667
|
+
"use strict";
|
|
19668
|
+
}
|
|
19669
|
+
});
|
|
19670
|
+
|
|
19671
|
+
// src/daemon/jobReads.ts
|
|
19672
|
+
async function lookupProjectSlug(client, projectId) {
|
|
19673
|
+
const { data, error } = await client.from("projects").select("slug, organizations(slug)").eq("id", projectId).single();
|
|
19674
|
+
if (error || !data) throw new Error(`Project ${projectId} not found: ${error?.message}`);
|
|
19675
|
+
const orgSlug = data.organizations?.slug;
|
|
19676
|
+
if (!orgSlug) throw new Error(`Project ${projectId} has no org slug`);
|
|
19677
|
+
return `${orgSlug}/${data.slug}`;
|
|
19678
|
+
}
|
|
19679
|
+
async function detectWritesByPhase(client, projectId, archmeBranch, jobId) {
|
|
19680
|
+
const { data, error } = await client.from("commits").select("structural_changeset").eq("project_id", projectId).eq("branch", archmeBranch).eq("metadata->source->>job_id", jobId);
|
|
19681
|
+
if (error) {
|
|
19682
|
+
console.error("commits lookup error:", error.message);
|
|
19683
|
+
return { total: 0, diffWrites: 0, structuralWrites: 0 };
|
|
19684
|
+
}
|
|
19685
|
+
let diffWrites = 0;
|
|
19686
|
+
let structuralWrites = 0;
|
|
19687
|
+
for (const row of data ?? []) {
|
|
19688
|
+
if (row.structural_changeset == null) diffWrites++;
|
|
19689
|
+
else structuralWrites++;
|
|
19690
|
+
}
|
|
19691
|
+
return { total: diffWrites + structuralWrites, diffWrites, structuralWrites };
|
|
19692
|
+
}
|
|
19693
|
+
async function detectMcpActivity(client, jobId) {
|
|
19694
|
+
const { data, error } = await client.from("archme_jobs").select("meaningful_tool_calls, no_change_reason").eq("id", jobId).maybeSingle();
|
|
19695
|
+
if (error) {
|
|
19696
|
+
console.error("mcp_activity lookup error:", error.message);
|
|
19697
|
+
return { reads: void 0, noChange: false };
|
|
19698
|
+
}
|
|
19699
|
+
if (!data) return { reads: void 0, noChange: false };
|
|
19700
|
+
const row = data;
|
|
19701
|
+
return { reads: row.meaningful_tool_calls, noChange: row.no_change_reason != null };
|
|
19702
|
+
}
|
|
19703
|
+
function sessionJobReads(client, job) {
|
|
19704
|
+
return {
|
|
19705
|
+
projectSlug: () => lookupProjectSlug(client, job.project_id),
|
|
19706
|
+
marker: (repo) => readBranchMarker(client, job.project_id, job.archme_branch, repo),
|
|
19707
|
+
repoMappings: (log) => loadRepoMappings(client, job.project_id, log),
|
|
19708
|
+
seedArchitecture: () => loadArchitectureForSeeding(client, job.project_id, job.archme_branch),
|
|
19709
|
+
bugsFolderId: () => ensureBugsFolder(client, job.project_id),
|
|
19710
|
+
writesByPhase: () => detectWritesByPhase(client, job.project_id, job.archme_branch, job.id),
|
|
19711
|
+
mcpActivity: () => detectMcpActivity(client, job.id),
|
|
19712
|
+
architectureCounts: () => countArchitecture(client, job.project_id, job.archme_branch)
|
|
19713
|
+
};
|
|
19714
|
+
}
|
|
19715
|
+
function hostedJobReads(post) {
|
|
19716
|
+
let prefetch = null;
|
|
19717
|
+
let outcome = null;
|
|
19718
|
+
const pre = () => prefetch ??= post("prefetch");
|
|
19719
|
+
const out = () => outcome ??= post("outcome");
|
|
19720
|
+
return {
|
|
19721
|
+
projectSlug: async () => {
|
|
19722
|
+
const slug = (await pre()).project_slug;
|
|
19723
|
+
if (typeof slug !== "string") throw new Error("job-context returned no project slug");
|
|
19724
|
+
return slug;
|
|
19725
|
+
},
|
|
19726
|
+
marker: async () => (await pre()).marker,
|
|
19727
|
+
repoMappings: async (log) => {
|
|
19728
|
+
const mappings = (await pre()).repo_mappings;
|
|
19729
|
+
if (mappings === null) log("repo mappings unreadable (proceeding without repo scoping)");
|
|
19730
|
+
return mappings;
|
|
19731
|
+
},
|
|
19732
|
+
seedArchitecture: async () => {
|
|
19733
|
+
const p = await pre();
|
|
19734
|
+
if (!p.seed_architecture) throw new Error(p.seed_error ?? "seed architecture unavailable");
|
|
19735
|
+
return p.seed_architecture;
|
|
19736
|
+
},
|
|
19737
|
+
bugsFolderId: async () => (await pre()).bugs_folder_id,
|
|
19738
|
+
// Fails closed, unlike the session readers: a platform job has no session read to fall back on.
|
|
19739
|
+
writesByPhase: async () => {
|
|
19740
|
+
const { total, diff, structural } = (await out()).writes;
|
|
19741
|
+
return { total, diffWrites: diff, structuralWrites: structural };
|
|
19742
|
+
},
|
|
19743
|
+
mcpActivity: async () => {
|
|
19744
|
+
const { reads, no_change } = (await out()).mcp;
|
|
19745
|
+
return { reads: reads ?? void 0, noChange: no_change };
|
|
19746
|
+
},
|
|
19747
|
+
architectureCounts: async () => {
|
|
19748
|
+
const counts = (await out()).architecture_counts;
|
|
19749
|
+
if (!counts) throw new Error("job-context returned no architecture counts");
|
|
19750
|
+
return counts;
|
|
19751
|
+
}
|
|
19752
|
+
};
|
|
19753
|
+
}
|
|
19754
|
+
function jobReadsFor(deps, job) {
|
|
19755
|
+
return deps.isPlatformDevice ? hostedJobReads(deps.post) : sessionJobReads(deps.client, job);
|
|
19756
|
+
}
|
|
19757
|
+
var init_jobReads = __esm({
|
|
19758
|
+
"src/daemon/jobReads.ts"() {
|
|
19759
|
+
"use strict";
|
|
19760
|
+
init_architectureCounts2();
|
|
19761
|
+
init_affected();
|
|
19762
|
+
init_bugsFolder();
|
|
19763
|
+
init_branchMarker();
|
|
19764
|
+
init_repoMappings();
|
|
19765
|
+
}
|
|
19766
|
+
});
|
|
19767
|
+
|
|
19610
19768
|
// src/daemon/sandboxJob/result.ts
|
|
19611
19769
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
19612
19770
|
function isValidNonce(nonce) {
|
|
@@ -22209,9 +22367,9 @@ var init_run_scan = __esm({
|
|
|
22209
22367
|
init_transcript_signals();
|
|
22210
22368
|
init_report();
|
|
22211
22369
|
init_github_remote();
|
|
22212
|
-
|
|
22370
|
+
init_architectureCounts2();
|
|
22213
22371
|
init_claimDefaultProject();
|
|
22214
|
-
|
|
22372
|
+
init_architectureCounts2();
|
|
22215
22373
|
BOOTSTRAP_HEADLESS = `Run the \`scan\` MCP prompt now and follow it to completion. This is a non-interactive automated run \u2014 skip any steps that pause for user confirmation (e.g. "does this hierarchy look right?"). Proceed with your best-judgement decisions rather than waiting for human input.`;
|
|
22216
22374
|
BOOTSTRAP_INTERACTIVE = `Run the \`scan\` MCP prompt now and follow it to completion. The user is at an interactive terminal \u2014 ask them when the prompt says to, and wait for their answer.
|
|
22217
22375
|
|
|
@@ -22700,13 +22858,6 @@ async function routeShutdownInterrupt(id, cancelled2, fail2, exitCode) {
|
|
|
22700
22858
|
console.error(` shutdown retry-route failed for ${id}: ${e instanceof Error ? e.message : e}`);
|
|
22701
22859
|
}
|
|
22702
22860
|
}
|
|
22703
|
-
async function lookupProjectSlug(client, projectId) {
|
|
22704
|
-
const { data, error } = await client.from("projects").select("slug, organizations(slug)").eq("id", projectId).single();
|
|
22705
|
-
if (error || !data) throw new Error(`Project ${projectId} not found: ${error?.message}`);
|
|
22706
|
-
const orgSlug = data.organizations?.slug;
|
|
22707
|
-
if (!orgSlug) throw new Error(`Project ${projectId} has no org slug`);
|
|
22708
|
-
return `${orgSlug}/${data.slug}`;
|
|
22709
|
-
}
|
|
22710
22861
|
function cloneDirFor(baseDir, owner, name) {
|
|
22711
22862
|
return join25(baseDir, owner, name);
|
|
22712
22863
|
}
|
|
@@ -23182,30 +23333,6 @@ async function rpcComplete(ctx, jobId, deviceId, summary2, kind, changed, metric
|
|
|
23182
23333
|
function affectedFieldForComplete(sourceMode, affected) {
|
|
23183
23334
|
return sourceMode === "pr_update" && affected ? affected : void 0;
|
|
23184
23335
|
}
|
|
23185
|
-
async function detectWritesByPhase(client, projectId, archmeBranch, jobId) {
|
|
23186
|
-
const { data, error } = await client.from("commits").select("structural_changeset").eq("project_id", projectId).eq("branch", archmeBranch).eq("metadata->source->>job_id", jobId);
|
|
23187
|
-
if (error) {
|
|
23188
|
-
console.error("commits lookup error:", error.message);
|
|
23189
|
-
return { total: 0, diffWrites: 0, structuralWrites: 0 };
|
|
23190
|
-
}
|
|
23191
|
-
let diffWrites = 0;
|
|
23192
|
-
let structuralWrites = 0;
|
|
23193
|
-
for (const row of data ?? []) {
|
|
23194
|
-
if (row.structural_changeset == null) diffWrites++;
|
|
23195
|
-
else structuralWrites++;
|
|
23196
|
-
}
|
|
23197
|
-
return { total: diffWrites + structuralWrites, diffWrites, structuralWrites };
|
|
23198
|
-
}
|
|
23199
|
-
async function detectMcpActivity(client, jobId) {
|
|
23200
|
-
const { data, error } = await client.from("archme_jobs").select("meaningful_tool_calls, no_change_reason").eq("id", jobId).maybeSingle();
|
|
23201
|
-
if (error) {
|
|
23202
|
-
console.error("mcp_activity lookup error:", error.message);
|
|
23203
|
-
return { reads: void 0, noChange: false };
|
|
23204
|
-
}
|
|
23205
|
-
if (!data) return { reads: void 0, noChange: false };
|
|
23206
|
-
const row = data;
|
|
23207
|
-
return { reads: row.meaningful_tool_calls, noChange: row.no_change_reason != null };
|
|
23208
|
-
}
|
|
23209
23336
|
function setLocalPause(untilMs) {
|
|
23210
23337
|
if (!Number.isFinite(untilMs) || untilMs <= Date.now()) return;
|
|
23211
23338
|
if (pauseUntilMs != null && pauseUntilMs >= untilMs) return;
|
|
@@ -23236,14 +23363,16 @@ async function rpcSetDaemonPause(ctx, pauseUntil, reason, detail) {
|
|
|
23236
23363
|
console.error(`set_daemon_pause failed: ${error.message}`);
|
|
23237
23364
|
}
|
|
23238
23365
|
}
|
|
23239
|
-
async function rpcReportEvent(ctx,
|
|
23366
|
+
async function rpcReportEvent(ctx, job, deviceId, eventType, payload) {
|
|
23240
23367
|
try {
|
|
23241
23368
|
const { error } = await withAuthRetry(
|
|
23242
23369
|
"report_daemon_event",
|
|
23243
23370
|
() => ctx.supabase.rpc("report_daemon_event", {
|
|
23244
|
-
p_org_id:
|
|
23371
|
+
p_org_id: job.org_id,
|
|
23245
23372
|
p_event_type: eventType,
|
|
23246
|
-
p_payload: payload
|
|
23373
|
+
p_payload: payload,
|
|
23374
|
+
p_job_id: job.id,
|
|
23375
|
+
p_device_id: deviceId
|
|
23247
23376
|
}),
|
|
23248
23377
|
ctx.reLogin
|
|
23249
23378
|
);
|
|
@@ -23477,11 +23606,36 @@ async function setupDigestMcp(ctx, job, deviceId) {
|
|
|
23477
23606
|
return {};
|
|
23478
23607
|
}
|
|
23479
23608
|
}
|
|
23609
|
+
function readsFor(ctx, job, deviceId) {
|
|
23610
|
+
return jobReadsFor(
|
|
23611
|
+
{ client: ctx.supabase, post: (part) => postJobContext(ctx, job.id, deviceId, part), isPlatformDevice: platformDevices.has(deviceId) },
|
|
23612
|
+
job
|
|
23613
|
+
);
|
|
23614
|
+
}
|
|
23615
|
+
async function postJobContext(ctx, jobId, deviceId, part) {
|
|
23616
|
+
const response = await fetchWithAuthRetry(
|
|
23617
|
+
"job-context",
|
|
23618
|
+
() => {
|
|
23619
|
+
const accessToken = ctx.auth.getAccessToken();
|
|
23620
|
+
if (!accessToken) throw new Error("daemon is not signed in with a registered device");
|
|
23621
|
+
return fetch(`${ctx.supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/job-context`, {
|
|
23622
|
+
method: "POST",
|
|
23623
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
|
|
23624
|
+
body: JSON.stringify({ device_id: deviceId, job_id: jobId, part })
|
|
23625
|
+
});
|
|
23626
|
+
},
|
|
23627
|
+
ctx.reLogin
|
|
23628
|
+
);
|
|
23629
|
+
const body = await parseJsonResponse(response);
|
|
23630
|
+
if (!response.ok) throw new Error(`job-context ${part} failed: ${responseError(body, `status ${response.status}`)}`);
|
|
23631
|
+
return body;
|
|
23632
|
+
}
|
|
23480
23633
|
async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoCredentials) {
|
|
23481
23634
|
const log = (msg) => console.log(`[${job.id}] ${msg}`);
|
|
23482
23635
|
const firstScan = job.job_kind === "first_scan";
|
|
23483
23636
|
assertLocalJobShape(firstScan ? { ...job, after_sha: job.scan_ref ?? "" } : job);
|
|
23484
|
-
const
|
|
23637
|
+
const reads = readsFor(ctx, job, deviceId);
|
|
23638
|
+
const projectSlug = await reads.projectSlug();
|
|
23485
23639
|
const jobToken = await mintJobToken(ctx, job.id, deviceId);
|
|
23486
23640
|
const probe = await probeMcpAuth(
|
|
23487
23641
|
`${ctx.mcpBaseUrl}?project=${encodeURIComponent(projectSlug)}&branch=${encodeURIComponent(job.archme_branch)}`,
|
|
@@ -23492,15 +23646,15 @@ async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoC
|
|
|
23492
23646
|
`MCP auth preflight failed after ${probe.attempts} attempt(s) (last status ${probe.lastStatus ?? "n/a"}): ${probe.detail}. Not starting the sandbox \u2014 failing the job so it retries with a freshly minted token.`
|
|
23493
23647
|
);
|
|
23494
23648
|
}
|
|
23495
|
-
const marker = job.source_mode === "pr_update" || firstScan ? { sha: null, repo: null } : await
|
|
23649
|
+
const marker = job.source_mode === "pr_update" || firstScan ? { sha: null, repo: null } : await reads.marker(`${job.repo_owner}/${job.repo_name}`);
|
|
23496
23650
|
const jobRepo = `${job.repo_owner}/${job.repo_name}`;
|
|
23497
|
-
const mappings = await
|
|
23651
|
+
const mappings = await reads.repoMappings(log);
|
|
23498
23652
|
let jobScope = jobRepoScope(mappings, jobRepo);
|
|
23499
23653
|
if (!jobScope) log("repo mappings unreadable: proceeding unseeded");
|
|
23500
23654
|
let seedArch = null;
|
|
23501
23655
|
if (mappings && jobScope && !firstScan) {
|
|
23502
23656
|
try {
|
|
23503
|
-
seedArch = await
|
|
23657
|
+
seedArch = await reads.seedArchitecture();
|
|
23504
23658
|
if (job.source_mode === "pr_update") {
|
|
23505
23659
|
jobScope = forkRepoScope(mappings, jobRepo, new Set(seedArch.components.map((c3) => c3.path)));
|
|
23506
23660
|
}
|
|
@@ -23508,7 +23662,7 @@ async function prefetchJobContext(ctx, job, deviceId, repoCredentials = getRepoC
|
|
|
23508
23662
|
log(`seed architecture unavailable (the agent runs unseeded): ${e.message}`);
|
|
23509
23663
|
}
|
|
23510
23664
|
}
|
|
23511
|
-
const bugsFolderId = await
|
|
23665
|
+
const bugsFolderId = await reads.bugsFolderId();
|
|
23512
23666
|
const model = await resolveRunnableModel(ctx.supabase, { warn: log });
|
|
23513
23667
|
const credentials = await repoCredentials(ctx, job.id, deviceId);
|
|
23514
23668
|
return {
|
|
@@ -23562,8 +23716,9 @@ async function completeClaudeRun(ctx, job, deviceId, run) {
|
|
|
23562
23716
|
metrics.model_served = runResult.modelServed;
|
|
23563
23717
|
metrics.usage = runResult.usage;
|
|
23564
23718
|
metrics.cost_usd = runResult.costUsd;
|
|
23565
|
-
const
|
|
23566
|
-
const {
|
|
23719
|
+
const jobReads = run.reads ?? readsFor(ctx, job, deviceId);
|
|
23720
|
+
const { total: writes, diffWrites, structuralWrites } = await jobReads.writesByPhase();
|
|
23721
|
+
const { reads, noChange } = await jobReads.mcpActivity();
|
|
23567
23722
|
markPhase("write_detect", {
|
|
23568
23723
|
commits: writes,
|
|
23569
23724
|
diff_writes: diffWrites,
|
|
@@ -23752,7 +23907,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
23752
23907
|
Object.assign(timings, outcome.metrics.durations_ms);
|
|
23753
23908
|
Object.assign(metrics, outcome.metrics, { durations_ms: timings });
|
|
23754
23909
|
const sandboxMetrics = outcome.metrics.sandbox;
|
|
23755
|
-
|
|
23910
|
+
await rpcReportEvent(ctx, job, deviceId, "sandbox_job_metrics", {
|
|
23756
23911
|
job_id: job.id,
|
|
23757
23912
|
project_id: job.project_id,
|
|
23758
23913
|
outcome: outcome.kind,
|
|
@@ -23832,8 +23987,9 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
23832
23987
|
verdict
|
|
23833
23988
|
);
|
|
23834
23989
|
}
|
|
23990
|
+
const jobReads = readsFor(ctx, job, deviceId);
|
|
23835
23991
|
if (firstScan) {
|
|
23836
|
-
const gate = firstScanGate(await
|
|
23992
|
+
const gate = firstScanGate(await jobReads.architectureCounts());
|
|
23837
23993
|
if (!gate.ok) throw new Error(gate.reason);
|
|
23838
23994
|
}
|
|
23839
23995
|
await completeClaudeRun(ctx, job, deviceId, {
|
|
@@ -23844,7 +24000,8 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
23844
24000
|
affected: outcome.affected,
|
|
23845
24001
|
startedAt,
|
|
23846
24002
|
markPhase,
|
|
23847
|
-
log
|
|
24003
|
+
log,
|
|
24004
|
+
reads: jobReads
|
|
23848
24005
|
});
|
|
23849
24006
|
return;
|
|
23850
24007
|
}
|
|
@@ -24133,7 +24290,7 @@ ${logs.stderr}`, sentPrompt);
|
|
|
24133
24290
|
if (plan) {
|
|
24134
24291
|
const { parsed, pauseUntil, info } = plan;
|
|
24135
24292
|
if (!parsed) {
|
|
24136
|
-
|
|
24293
|
+
await rpcReportEvent(ctx, job, deviceId, "rate_limit_unmatched_exit", {
|
|
24137
24294
|
job_id: job.id,
|
|
24138
24295
|
excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
|
|
24139
24296
|
});
|
|
@@ -24476,7 +24633,9 @@ async function runDaemon() {
|
|
|
24476
24633
|
const via = usedFallback ? ` (via device ${active.cred.device_id})` : "";
|
|
24477
24634
|
const accountUserId = auth.getUserId() ?? "OAuth account";
|
|
24478
24635
|
console.log(` account ${accountUserId}: ${active.result.user.email ?? "OAuth user"} (${devices.length} org(s))${via}`);
|
|
24479
|
-
const
|
|
24636
|
+
const discovered = await discoverClaimDevices(client, auth.getUserId() ?? "", devices, sandboxSettings.mode);
|
|
24637
|
+
platformDevices = new Set(discovered.platform.map((p) => p.id));
|
|
24638
|
+
const claimDevices = discovered.devices;
|
|
24480
24639
|
const sessions = [{
|
|
24481
24640
|
supabase: client,
|
|
24482
24641
|
auth,
|
|
@@ -24486,16 +24645,9 @@ async function runDaemon() {
|
|
|
24486
24645
|
supabaseUrl: SUPABASE_URL,
|
|
24487
24646
|
cloneBaseDir: CLONE_BASE_DIR,
|
|
24488
24647
|
reLogin: () => auth.reauthenticate(),
|
|
24489
|
-
devices,
|
|
24490
|
-
deviceForOrg: (
|
|
24648
|
+
devices: claimDevices,
|
|
24649
|
+
deviceForOrg: deviceForOrgLookup(claimDevices)
|
|
24491
24650
|
}];
|
|
24492
|
-
try {
|
|
24493
|
-
const { data, error } = await client.from("agent_devices").select("id, is_platform").in("id", devices.map((d) => d.deviceId));
|
|
24494
|
-
if (error) throw new Error(error.message);
|
|
24495
|
-
platformDevices = new Set((data ?? []).filter((d) => d.is_platform).map((d) => d.id));
|
|
24496
|
-
} catch (e) {
|
|
24497
|
-
console.warn(`agent_devices.is_platform read failed (treating devices as not platform): ${e.message}`);
|
|
24498
|
-
}
|
|
24499
24651
|
if (sandboxSettings.mode !== "off") {
|
|
24500
24652
|
try {
|
|
24501
24653
|
const { createSandboxParts: createSandboxParts2 } = await Promise.resolve().then(() => (init_parts(), parts_exports));
|
|
@@ -24730,9 +24882,11 @@ var init_run = __esm({
|
|
|
24730
24882
|
init_localPrJob();
|
|
24731
24883
|
init_localPoller();
|
|
24732
24884
|
init_setting();
|
|
24733
|
-
|
|
24885
|
+
init_architectureCounts2();
|
|
24734
24886
|
init_imageManifest();
|
|
24735
24887
|
init_claimStance();
|
|
24888
|
+
init_platformDevices();
|
|
24889
|
+
init_jobReads();
|
|
24736
24890
|
init_claudeVersion();
|
|
24737
24891
|
init_host();
|
|
24738
24892
|
init_runSandboxedJob();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archme/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
package/sandbox-image.json
CHANGED