@nurix/nustack 0.24.0 → 0.25.2

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 (48) hide show
  1. package/dist/commands/desktopAgent.js +1 -1
  2. package/dist/commands/doctor.js +8 -11
  3. package/dist/commands/obligationList.js +23 -49
  4. package/dist/commands/records.js +6 -88
  5. package/dist/commands/store.js +199 -0
  6. package/dist/commands/sync.js +2 -2
  7. package/dist/commands/toolkit.js +2 -2
  8. package/dist/lib/context/register.js +1 -1
  9. package/dist/lib/desktop-agents/checkout.js +1 -1
  10. package/dist/lib/desktop-agents/claudeRun.js +4 -4
  11. package/dist/lib/etna/reconcile.js +4 -4
  12. package/dist/lib/graph/library.js +1 -1
  13. package/dist/lib/harness/hookCheck.js +2 -8
  14. package/dist/lib/harness/storeWaivers.js +1 -4
  15. package/dist/lib/local_store/database.js +12 -92
  16. package/dist/lib/local_store/device_state.js +2 -2
  17. package/dist/lib/local_store/index.js +2 -2
  18. package/dist/lib/local_store/paths.js +0 -6
  19. package/dist/lib/local_store/routing.js +8 -44
  20. package/dist/lib/onboarding/folder_state.js +2 -2
  21. package/dist/lib/onboarding/init_fresh_folder.js +1 -1
  22. package/dist/lib/onboarding/manifest_writer.js +6 -6
  23. package/dist/lib/origin_guard.js +1 -1
  24. package/dist/lib/project_context.js +5 -5
  25. package/dist/lib/project_contract.js +2 -2
  26. package/dist/lib/records/drain.js +3 -153
  27. package/dist/lib/records/kinds.js +16 -0
  28. package/dist/lib/records/recordInput.js +1 -1
  29. package/dist/lib/records/recordsTable.js +119 -161
  30. package/dist/lib/records/refusalLog.js +7 -7
  31. package/dist/lib/sessions/brief.js +1 -1
  32. package/dist/lib/sessions/sessionEvents.js +5 -49
  33. package/dist/lib/store/open.js +136 -0
  34. package/dist/lib/store/wipe.js +34 -0
  35. package/dist/lib/sync/apply.js +113 -0
  36. package/dist/lib/sync/client.js +327 -0
  37. package/dist/lib/sync/endpoint.js +26 -0
  38. package/dist/lib/sync/manifest.js +80 -0
  39. package/dist/lib/sync/outbox.js +104 -0
  40. package/dist/machine.sql +934 -0
  41. package/dist/program.js +8 -6
  42. package/dist/records.schema.json +4 -0
  43. package/dist/spec_schema.json +5 -5
  44. package/dist/vendor/harness/rules/docsRoot.js +2 -2
  45. package/dist/vendor/harness/rules/index.js +1 -1
  46. package/package.json +2 -2
  47. package/dist/lib/local_store/migrations.js +0 -399
  48. package/dist/lib/records/door.js +0 -79
@@ -86,7 +86,7 @@ async function runAccepted(request) {
86
86
  const outcome = await runClaude(request, claudeArgs(request, inputs, modelEnv), modelEnv, worktreePath, composePrompt(request));
87
87
  if (request.sessionId !== null) {
88
88
  const synced = await syncRecords(env, worktreePath);
89
- log(`records sync exited ${synced.code ?? "unspawned"}`);
89
+ log(`store sync exited ${synced.code ?? "unspawned"}`);
90
90
  }
91
91
  const shared = { vendorSessionId: outcome.vendorSessionId, costCents: outcome.costCents, engineCheck: outcome.engineCheck, mount: mounted.mount };
92
92
  if (outcome.reason) {
@@ -14,7 +14,6 @@ import { readDeviceState } from "../lib/local_store/device_state.js";
14
14
  import { getBindingByRepositoryRoot, listActiveBindings } from "../lib/local_store/bindings.js";
15
15
  import { listApplicationsForBinding } from "../lib/local_store/handoff_applications.js";
16
16
  import { readFilebeatState, listActivityBindings } from "../lib/local_store/filebeat_state.js";
17
- import { LATEST_SCHEMA_VERSION } from "../lib/local_store/migrations.js";
18
17
  import { resolveRepository, readSymbolicBranch, readHeadCommit } from "../lib/git-sync/core/repository.js";
19
18
  import { readGitOperationState } from "../lib/git-sync/core/operation_state.js";
20
19
  import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
@@ -72,23 +71,21 @@ export async function runDoctor(options) {
72
71
  add("identity", row.check, row.status, row.detail);
73
72
  add("api", "NuStack reachability", ...(await probeReachability(baseUrl)));
74
73
  let store = null;
74
+ let storeError = "the Device state database could not be opened";
75
75
  try {
76
76
  store = await openLocalStore();
77
77
  }
78
- catch {
78
+ catch (error) {
79
79
  store = null;
80
+ if (error instanceof Error)
81
+ storeError = error.message;
80
82
  }
81
83
  if (!store) {
82
- add("state", "Local store", "error", "the Device state database could not be opened");
84
+ add("state", "Local store", "error", storeError);
83
85
  }
84
86
  else {
85
- const schema = readDeviceState(store)?.schemaVersion ?? null;
86
- if (schema === null)
87
- add("state", "Local store schema", "info", "no Device state yet — sign in to initialize");
88
- else if (schema >= LATEST_SCHEMA_VERSION)
89
- add("state", "Local store schema", "ok", `v${schema}`);
90
- else
91
- add("state", "Local store schema", "warn", `v${schema} behind v${LATEST_SCHEMA_VERSION} — the next command migrates it`);
87
+ const stamp = store.db.prepare("PRAGMA user_version").get().user_version;
88
+ add("state", "Local store schema", "ok", `v${stamp}`);
92
89
  }
93
90
  const repository = resolveRepositoryOrNull(cwd);
94
91
  if (!repository) {
@@ -176,7 +173,7 @@ async function fetchCloudProfile(baseUrl, accessToken, projectId) {
176
173
  async function addManifestRows(add, root, cloud) {
177
174
  const manifests = await readProjectManifests(root);
178
175
  if (!manifests.projectManifest && !manifests.serviceToolkit) {
179
- add("manifests", "Project binding markers", "info", "no `.nustack/app.json` or Service toolkit block — repository not attached");
176
+ add("manifests", "Project binding markers", "info", "no `.nustack/project.json` or Service toolkit block — repository not attached");
180
177
  return;
181
178
  }
182
179
  const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
@@ -1,42 +1,19 @@
1
- import { obligationsCachePath } from "../lib/local_store/paths.js";
1
+ import { openLocalStore } from "../lib/local_store/database.js";
2
2
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
3
3
  import { resolveProjectId } from "../lib/project_context.js";
4
- import { getFromDoor, readCache, writeCache } from "../lib/sessions/brief.js";
5
- const MAX_PAGES = 100;
6
- const PAGE_TIMEOUT_MS = 10_000;
7
- function lineOf(item) {
8
- if (typeof item.naturalKey !== "string")
9
- return null;
10
- const payload = (item.payload ?? {});
4
+ import { listRecords } from "../lib/records/recordsTable.js";
5
+ import { pullAll } from "../lib/sync/client.js";
6
+ const PULL_TIMEOUT_MS = 10_000;
7
+ function lineOf(naturalKey, payload) {
8
+ const fields = (payload ?? {});
11
9
  return {
12
- naturalKey: item.naturalKey,
13
- state: typeof payload.state === "string" ? payload.state : "unknown",
14
- verdict: typeof payload.outcome?.verdict === "string" ? payload.outcome.verdict : null,
10
+ naturalKey,
11
+ state: typeof fields.state === "string" ? fields.state : "unknown",
12
+ verdict: typeof fields.outcome?.verdict === "string" ? fields.outcome.verdict : null,
15
13
  };
16
14
  }
17
- async function fetchAll(root, globals, projectId, plan) {
18
- const lines = [];
19
- let cursor = null;
20
- for (let page = 0; page < MAX_PAGES; page += 1) {
21
- const query = `/records?kind=obligation&planAnchor=${encodeURIComponent(plan)}&limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
22
- const answered = await getFromDoor(root, globals, projectId, query, PAGE_TIMEOUT_MS);
23
- const items = answered?.data?.items;
24
- if (!answered || !Array.isArray(items))
25
- return null;
26
- for (const item of items) {
27
- const line = lineOf(item);
28
- if (line)
29
- lines.push(line);
30
- }
31
- cursor = answered.meta?.nextCursor ?? null;
32
- if (!cursor)
33
- return lines;
34
- }
35
- return lines;
36
- }
37
15
  export async function runObligationList(options, globals) {
38
- const root = process.cwd();
39
- const project = await resolveProjectId(root, options.project ? { project: options.project } : {});
16
+ const project = await resolveProjectId(process.cwd(), options.project ? { project: options.project } : {});
40
17
  if (!project.ok) {
41
18
  if (options.json)
42
19
  printJsonError("obligation list", { code: "NO_PROJECT", message: project.message });
@@ -46,28 +23,25 @@ export async function runObligationList(options, globals) {
46
23
  }
47
24
  return;
48
25
  }
49
- const cacheFile = obligationsCachePath(project.projectId, options.plan);
50
- const fetched = await fetchAll(root, globals, project.projectId, options.plan);
26
+ const store = await openLocalStore();
51
27
  let obligations;
52
- let source;
53
- let fetchedAt = null;
54
- if (fetched) {
55
- writeCache(cacheFile, fetched);
56
- obligations = fetched;
57
- source = "server";
28
+ let pullError;
29
+ try {
30
+ pullError = (await pullAll(store, globals, { timeoutMs: PULL_TIMEOUT_MS })).lastError;
31
+ obligations = listRecords(store, { projectId: project.projectId, kind: "obligation", planAnchor: options.plan })
32
+ .map((record) => lineOf(record.naturalKey, record.payload))
33
+ .sort((a, b) => a.naturalKey.localeCompare(b.naturalKey));
58
34
  }
59
- else {
60
- const cached = readCache(cacheFile);
61
- obligations = cached?.value ?? [];
62
- fetchedAt = cached?.fetchedAt ?? null;
63
- source = "cache";
35
+ finally {
36
+ store.close();
64
37
  }
38
+ const source = pullError === null ? "synced" : "store";
65
39
  if (options.json) {
66
- printJsonSuccess("obligation list", { plan: options.plan, projectId: project.projectId, source, fetchedAt, obligations });
40
+ printJsonSuccess("obligation list", { plan: options.plan, projectId: project.projectId, source, pullError, obligations });
67
41
  return;
68
42
  }
69
- if (source === "cache")
70
- process.stderr.write(`obligation list: offline; the cached list${fetchedAt ? ` from ${fetchedAt}` : " is empty"}\n`);
43
+ if (pullError !== null)
44
+ process.stderr.write(`obligation list: could not sync (${pullError}); showing the rows as last synced\n`);
71
45
  for (const line of obligations)
72
46
  process.stdout.write(`${line.naturalKey} ${line.state}${line.verdict ? ` ${line.verdict}` : ""}\n`);
73
47
  if (obligations.length === 0)
@@ -1,17 +1,13 @@
1
- import { existsSync } from "node:fs";
2
1
  import { readDesktopSettings } from "../lib/desktop_settings.js";
3
2
  import { openLocalStore } from "../lib/local_store/database.js";
4
- import { stateDbPath } from "../lib/local_store/paths.js";
5
3
  import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
6
- import { HARNESS_CODES } from "../lib/harness/codes.js";
7
4
  import { resolveProjectId } from "../lib/project_context.js";
8
- import { isEmitKind } from "../lib/records/door.js";
9
- import { acquireDrainLock, drainRecords, refreshDrainLock, releaseDrainLock, startDetachedDrain } from "../lib/records/drain.js";
10
- import { DEVICE_TOKEN_VARIABLE, DRAIN_CHILD_VARIABLE, SESSION_ID_VARIABLE, isFlagSet, readVariable, recordCredentialOf, } from "../lib/records/environment.js";
5
+ import { isEmitKind } from "../lib/records/kinds.js";
6
+ import { startDetachedDrain } from "../lib/records/drain.js";
7
+ import { SESSION_ID_VARIABLE, readVariable, recordCredentialOf } from "../lib/records/environment.js";
11
8
  import { UNKNOWN_KIND_MESSAGE, assembleRecord, parseRowsText, translateRow, } from "../lib/records/recordInput.js";
12
- import { AgentOwnedError, OBLIGATION_AGENT_OWNED, countRecords, hasDueRecord, listRecords, releaseUnboundRows, writeRecords, } from "../lib/records/recordsTable.js";
9
+ import { AgentOwnedError, OBLIGATION_AGENT_OWNED, countRecords, listRecords, releaseUnboundRows, writeRecords, } from "../lib/records/recordsTable.js";
13
10
  import { logRefusals } from "../lib/records/refusalLog.js";
14
- import { drainSessionLegs } from "../lib/sessions/sessionEvents.js";
15
11
  async function readStdin() {
16
12
  if (process.stdin.isTTY)
17
13
  return "";
@@ -175,65 +171,10 @@ export async function runRecordsEmit(options, globals) {
175
171
  json: options.json === true,
176
172
  }, globals);
177
173
  }
178
- async function drainAs(store, globals, isChild) {
179
- if (!isChild) {
180
- const drained = await drainRecords(store, globals, { isForeground: true });
181
- await drainSessionLegs(store, globals);
182
- return drained;
183
- }
184
- if (!acquireDrainLock())
185
- return null;
186
- try {
187
- const drained = await drainRecords(store, globals, { onPass: () => refreshDrainLock() });
188
- refreshDrainLock();
189
- await drainSessionLegs(store, globals);
190
- return drained;
191
- }
192
- finally {
193
- releaseDrainLock();
194
- }
195
- }
196
- export async function runRecordsSync(options, globals) {
197
- const store = await openLocalStore();
198
- try {
199
- const root = process.cwd();
200
- const project = await resolveProjectId(root, {});
201
- if (project.ok)
202
- releaseUnboundRows(store, root);
203
- const drained = await drainAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
204
- if (drained?.originRefusal) {
205
- if (options.json)
206
- printJsonError("records sync", { code: HARNESS_CODES.ORIGIN_MISMATCH, message: drained.originRefusal });
207
- else {
208
- process.stderr.write(`${drained.originRefusal}\n`);
209
- process.exitCode = 1;
210
- }
211
- return;
212
- }
213
- if (options.quiet)
214
- return;
215
- const counts = await countRecords(store);
216
- const result = {
217
- sent: drained?.sent ?? 0,
218
- waiting: counts.waitingCount,
219
- stored: counts.storeCount,
220
- refused: counts.refusedCount,
221
- lastError: drained?.lastError ?? counts.lastError,
222
- };
223
- if (options.json)
224
- printJsonSuccess("records sync", result);
225
- else {
226
- process.stdout.write(`records sync: sent ${result.sent}; ${result.waiting} waiting, ${result.stored} kept locally, ${result.refused} refused\n`);
227
- }
228
- }
229
- finally {
230
- store.close();
231
- }
232
- }
233
174
  export async function runRecordsList(options) {
234
175
  const store = await openLocalStore();
235
176
  try {
236
- const counts = await countRecords(store, options.project);
177
+ const counts = countRecords(store, options.project);
237
178
  const records = listRecords(store, {
238
179
  ...(options.project ? { projectId: options.project } : {}),
239
180
  ...(options.kind ? { kind: options.kind } : {}),
@@ -260,27 +201,10 @@ export async function runRecordsList(options) {
260
201
  store.close();
261
202
  }
262
203
  }
263
- export async function startDrainIfDue(globals) {
264
- try {
265
- if (isFlagSet(DRAIN_CHILD_VARIABLE) || !existsSync(stateDbPath()))
266
- return;
267
- const store = await openLocalStore();
268
- try {
269
- if (hasDueRecord(store.db, Date.now(), readVariable(DEVICE_TOKEN_VARIABLE) !== null)) {
270
- startDetachedDrain({ nustackUrl: globals.nustackUrl });
271
- }
272
- }
273
- finally {
274
- store.close();
275
- }
276
- }
277
- catch {
278
- }
279
- }
280
204
  export function registerRecordsCommands(recordsCommand, getGlobalOptions) {
281
205
  recordsCommand
282
206
  .command("emit")
283
- .description("Write records locally and return; a background drain sends them to NuStack")
207
+ .description("Write records locally and return; a background store sync sends them to NuStack")
284
208
  .option("--kind <kind>", "obligation, gate_event, changelog, memory, backlog, proposal or triage")
285
209
  .option("--row <json>", "one row, an array of rows, or {kind, rows}, as JSON (default: read from stdin)")
286
210
  .option("--root <dir>", "the folder the records are about (default: the current directory)")
@@ -289,12 +213,6 @@ export function registerRecordsCommands(recordsCommand, getGlobalOptions) {
289
213
  .option("--plan <anchor>", "the plan anchor, when a row names none")
290
214
  .option("--json", "emit the versioned machine envelope")
291
215
  .action(async (options) => runRecordsEmit(options, getGlobalOptions()));
292
- recordsCommand
293
- .command("sync")
294
- .description("Send every due record this device holds to NuStack")
295
- .option("--quiet", "print nothing")
296
- .option("--json", "emit the versioned machine envelope")
297
- .action(async (options) => runRecordsSync(options, getGlobalOptions()));
298
216
  recordsCommand
299
217
  .command("list")
300
218
  .description("Show this device's records: what waits, what was refused, what stays local")
@@ -0,0 +1,199 @@
1
+ import { existsSync } from "node:fs";
2
+ import { HARNESS_CODES } from "../lib/harness/codes.js";
3
+ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
4
+ import { openLocalStore } from "../lib/local_store/database.js";
5
+ import { resolveProjectId } from "../lib/project_context.js";
6
+ import { acquireDrainLock, refreshDrainLock, releaseDrainLock, startDetachedDrain } from "../lib/records/drain.js";
7
+ import { DEVICE_TOKEN_VARIABLE, DRAIN_CHILD_VARIABLE, isFlagSet, readVariable } from "../lib/records/environment.js";
8
+ import { countRecords, releaseUnboundRows } from "../lib/records/recordsTable.js";
9
+ import { drainSessionLegs } from "../lib/sessions/sessionEvents.js";
10
+ import { pullAll, pullCursor, pushOutbox } from "../lib/sync/client.js";
11
+ import { countOutbox, hasDueEntry, newestOutboxError } from "../lib/sync/outbox.js";
12
+ import { cliVersion, machineStorePath, storeStampOf } from "../lib/store/open.js";
13
+ const CLIENT_OUTDATED = "CLIENT_OUTDATED";
14
+ function answeredRefusal(command, options, summaries) {
15
+ for (const summary of summaries) {
16
+ if (!summary)
17
+ continue;
18
+ const refusal = summary.originRefusal
19
+ ? { code: HARNESS_CODES.ORIGIN_MISMATCH, message: summary.originRefusal }
20
+ : summary.outdated
21
+ ? { code: CLIENT_OUTDATED, message: summary.outdated }
22
+ : null;
23
+ if (!refusal)
24
+ continue;
25
+ if (options.json)
26
+ printJsonError(command, refusal);
27
+ else {
28
+ process.stderr.write(`${refusal.message}\n`);
29
+ process.exitCode = 1;
30
+ }
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ async function releaseHere(store) {
36
+ const root = process.cwd();
37
+ if ((await resolveProjectId(root, {})).ok)
38
+ releaseUnboundRows(store, root);
39
+ }
40
+ async function syncAs(store, globals, isChild) {
41
+ if (!isChild) {
42
+ const push = await pushOutbox(store, globals, { isForeground: true });
43
+ const pull = push.originRefusal || push.outdated ? null : await pullAll(store, globals);
44
+ await drainSessionLegs(globals);
45
+ return { push, pull };
46
+ }
47
+ if (!acquireDrainLock())
48
+ return null;
49
+ try {
50
+ const push = await pushOutbox(store, globals, { onPass: () => refreshDrainLock() });
51
+ refreshDrainLock();
52
+ const pull = push.originRefusal || push.outdated ? null : await pullAll(store, globals);
53
+ refreshDrainLock();
54
+ await drainSessionLegs(globals);
55
+ return { push, pull };
56
+ }
57
+ finally {
58
+ releaseDrainLock();
59
+ }
60
+ }
61
+ export async function runStoreSync(options, globals) {
62
+ const store = await openLocalStore();
63
+ try {
64
+ await releaseHere(store);
65
+ const synced = await syncAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
66
+ if (answeredRefusal("store sync", options, [synced?.push ?? null, synced?.pull ?? null]))
67
+ return;
68
+ if (options.quiet)
69
+ return;
70
+ const counts = countRecords(store);
71
+ const result = {
72
+ pushed: synced?.push.sent ?? 0,
73
+ refused: synced?.push.refused ?? 0,
74
+ pulled: synced?.pull?.rows ?? 0,
75
+ tombstones: synced?.pull?.tombstones ?? 0,
76
+ cursor: pullCursor(store),
77
+ waiting: counts.waitingCount,
78
+ stored: counts.storeCount,
79
+ lastError: synced?.push.lastError ?? synced?.pull?.lastError ?? newestOutboxError(store.db),
80
+ };
81
+ if (options.json)
82
+ printJsonSuccess("store sync", result);
83
+ else {
84
+ process.stdout.write(`store sync: pushed ${result.pushed}, pulled ${result.pulled}; ${result.waiting} waiting, ${result.stored} kept locally, ${result.refused} refused\n`);
85
+ if (result.lastError)
86
+ process.stderr.write(` last error: ${result.lastError}\n`);
87
+ }
88
+ }
89
+ finally {
90
+ store.close();
91
+ }
92
+ }
93
+ export async function runStorePush(options, globals) {
94
+ const store = await openLocalStore();
95
+ try {
96
+ await releaseHere(store);
97
+ const push = await pushOutbox(store, globals, { isForeground: true });
98
+ if (answeredRefusal("store push", options, [push]))
99
+ return;
100
+ const result = { pushed: push.sent, refused: push.refused, lastError: push.lastError };
101
+ if (options.json)
102
+ printJsonSuccess("store push", result);
103
+ else
104
+ process.stdout.write(`store push: pushed ${result.pushed}, ${result.refused} refused\n`);
105
+ }
106
+ finally {
107
+ store.close();
108
+ }
109
+ }
110
+ export async function runStorePull(options, globals) {
111
+ const store = await openLocalStore();
112
+ try {
113
+ const pull = await pullAll(store, globals);
114
+ if (answeredRefusal("store pull", options, [pull]))
115
+ return;
116
+ const result = { pulled: pull.rows, tombstones: pull.tombstones, cursor: pull.cursor, isReset: pull.isReset, lastError: pull.lastError };
117
+ if (options.json)
118
+ printJsonSuccess("store pull", result);
119
+ else {
120
+ process.stdout.write(`store pull: ${result.pulled} row(s), ${result.tombstones} tombstone(s); cursor ${result.cursor}\n`);
121
+ if (result.lastError)
122
+ process.stderr.write(` last error: ${result.lastError}\n`);
123
+ }
124
+ }
125
+ finally {
126
+ store.close();
127
+ }
128
+ }
129
+ export async function runStoreStatus(options) {
130
+ const store = await openLocalStore();
131
+ try {
132
+ const tableCount = (name) => Number(store.db.prepare(`SELECT count(*) AS n FROM "${name}"`).get().n);
133
+ const result = {
134
+ path: machineStorePath(),
135
+ version: cliVersion(),
136
+ stamp: storeStampOf(cliVersion()),
137
+ cursor: pullCursor(store),
138
+ outbox: countOutbox(store.db, Date.now()),
139
+ records: tableCount("harness_records"),
140
+ labelRows: tableCount("session_label_rows"),
141
+ lastError: newestOutboxError(store.db),
142
+ };
143
+ if (options.json)
144
+ printJsonSuccess("store status", result);
145
+ else {
146
+ process.stdout.write(`store status: ${result.path} (NuStack ${result.version}, stamp ${result.stamp}); cursor ${result.cursor}\n`);
147
+ process.stdout.write(` ${result.records} record(s), ${result.labelRows} label row(s)\n`);
148
+ if (result.outbox.length === 0)
149
+ process.stdout.write(" outbox: empty\n");
150
+ for (const entry of result.outbox) {
151
+ process.stdout.write(` outbox ${entry.table}: ${entry.pending} pending, ${entry.due} due, ${entry.failing} failing\n`);
152
+ }
153
+ if (result.lastError)
154
+ process.stdout.write(` last error: ${result.lastError}\n`);
155
+ }
156
+ }
157
+ finally {
158
+ store.close();
159
+ }
160
+ }
161
+ export async function startDrainIfDue(globals) {
162
+ try {
163
+ if (isFlagSet(DRAIN_CHILD_VARIABLE) || !existsSync(machineStorePath()))
164
+ return;
165
+ const store = await openLocalStore();
166
+ try {
167
+ if (hasDueEntry(store.db, Date.now(), readVariable(DEVICE_TOKEN_VARIABLE) !== null))
168
+ startDetachedDrain({ nustackUrl: globals.nustackUrl });
169
+ }
170
+ finally {
171
+ store.close();
172
+ }
173
+ }
174
+ catch {
175
+ }
176
+ }
177
+ export function registerStoreCommands(storeCommand, getGlobalOptions) {
178
+ storeCommand
179
+ .command("sync")
180
+ .description("Push this machine's changes to NuStack, then pull what changed there")
181
+ .option("--quiet", "print nothing")
182
+ .option("--json", "emit the versioned machine envelope")
183
+ .action(async (options) => runStoreSync(options, getGlobalOptions()));
184
+ storeCommand
185
+ .command("push")
186
+ .description("Push this machine's pending changes to NuStack")
187
+ .option("--json", "emit the versioned machine envelope")
188
+ .action(async (options) => runStorePush(options, getGlobalOptions()));
189
+ storeCommand
190
+ .command("pull")
191
+ .description("Pull every row that changed on NuStack since the last pull")
192
+ .option("--json", "emit the versioned machine envelope")
193
+ .action(async (options) => runStorePull(options, getGlobalOptions()));
194
+ storeCommand
195
+ .command("status")
196
+ .description("Show the machine database: its release, the pull cursor and the outbox")
197
+ .option("--json", "emit the versioned machine envelope")
198
+ .action(async (options) => runStoreStatus(options));
199
+ }
@@ -45,8 +45,8 @@ export async function runSync(options) {
45
45
  spinner?.stop(`${projections.length} service credential(s) projected`);
46
46
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
47
47
  const nextValues = {};
48
- if (managedValues.NUSTACK_WORKSPACE_ID)
49
- nextValues.NUSTACK_WORKSPACE_ID = managedValues.NUSTACK_WORKSPACE_ID;
48
+ if (managedValues.NUSTACK_PROJECT_ID)
49
+ nextValues.NUSTACK_PROJECT_ID = managedValues.NUSTACK_PROJECT_ID;
50
50
  for (const credential of [...projections].sort((a, b) => a.service.localeCompare(b.service))) {
51
51
  const prefix = toEnvPrefix(credential.service);
52
52
  nextValues[`${prefix}_ENDPOINT`] = credential.endpoint;
@@ -101,10 +101,10 @@ export async function runToolkitReconcile(options, globals) {
101
101
  const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
102
102
  const manifests = await readProjectManifests(ctx.root);
103
103
  const identity = manifests.projectManifest
104
- ? { projectId: manifests.projectManifest.workspaceId, organizationId: manifests.projectManifest.organizationId }
104
+ ? { projectId: manifests.projectManifest.projectId, organizationId: manifests.projectManifest.organizationId }
105
105
  : manifests.serviceProjectBinding;
106
106
  if (!identity) {
107
- emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/app.json` binding to reconcile — attach the repository first.");
107
+ emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/project.json` binding to reconcile — attach the repository first.");
108
108
  return;
109
109
  }
110
110
  const plan = await buildReconcilePlan(ctx.root, binding ?? { etnaProfileJson: null }, cloud);
@@ -8,7 +8,7 @@ const SCALAR_FIELDS = new Set(["kind", "plane", "projects", "one-line"]);
8
8
  const SEQUENCE_FIELDS = new Set(["aliases", "tables"]);
9
9
  export function docsRootOf(root) {
10
10
  try {
11
- const parsed = JSON.parse(readFileSync(path.join(root, ".nustack", "workspace.json"), "utf8"));
11
+ const parsed = JSON.parse(readFileSync(path.join(root, ".nustack", "project.json"), "utf8"));
12
12
  if (typeof parsed.docsRoot === "string" && parsed.docsRoot.trim() !== "")
13
13
  return parsed.docsRoot.trim();
14
14
  }
@@ -12,7 +12,7 @@ export const DESKTOP_AGENT_BIN_DIR = ".bin";
12
12
  export const DESKTOP_AGENT_PROPOSAL_BRANCH_PREFIX = "nustack/proposal/";
13
13
  export const DESKTOP_AGENT_EXCLUDE_START = "# nustack:desktop-agent:start";
14
14
  export const DESKTOP_AGENT_EXCLUDE_END = "# nustack:desktop-agent:end";
15
- const MARKER_RELATIVE = ".nustack/workspace.json";
15
+ const MARKER_RELATIVE = ".nustack/project.json";
16
16
  const FETCH_TIMEOUT_MS = 10 * 60_000;
17
17
  export function agentCheckoutsRoot(env = process.env) {
18
18
  return path.join(sessionsHome(env), DESKTOP_AGENT_CHECKOUTS_DIR);
@@ -101,9 +101,9 @@ export function sandboxSettings(inputs) {
101
101
  path.join(inputs.gitCommonDir, "refs"),
102
102
  path.join(inputs.gitCommonDir, "logs"),
103
103
  inputs.worktreeGitDir,
104
- path.join(nustackHome, "state.db"),
105
- path.join(nustackHome, "state.db-wal"),
106
- path.join(nustackHome, "state.db-shm"),
104
+ path.join(nustackHome, "nustack.db"),
105
+ path.join(nustackHome, "nustack.db-wal"),
106
+ path.join(nustackHome, "nustack.db-shm"),
107
107
  path.join(nustackHome, "locks"),
108
108
  path.join(nustackHome, "session-events"),
109
109
  ],
@@ -253,7 +253,7 @@ export function gitDirsOf(gitCommonDirOutput, gitDirOutput, worktree) {
253
253
  }
254
254
  export function syncRecords(env, cwd) {
255
255
  return new Promise((resolve) => {
256
- const child = spawn(process.execPath, [cliEntry(), "records", "sync", "--json"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
256
+ const child = spawn(process.execPath, [cliEntry(), "store", "sync", "--json"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
257
257
  let output = "";
258
258
  child.stdout.on("data", (chunk) => (output += chunk.toString("utf8")));
259
259
  child.on("error", () => resolve({ code: null, output }));
@@ -41,10 +41,10 @@ export async function buildReconcilePlan(root, binding, cloud) {
41
41
  const changes = [];
42
42
  const project = manifests.projectManifest?.toolkit;
43
43
  if (project) {
44
- pushIf(changes, ".nustack/workspace.json", "toolkit.name", project.name, cloud.name);
45
- pushIf(changes, ".nustack/workspace.json", "toolkit.version", project.version, cloud.version);
46
- pushIf(changes, ".nustack/workspace.json", "toolkit.rendererVersion", project.rendererVersion, cloud.rendererVersion);
47
- pushIf(changes, ".nustack/workspace.json", "toolkit.placementMapVersion", project.placementMapVersion, cloud.placementMapVersion);
44
+ pushIf(changes, ".nustack/project.json", "toolkit.name", project.name, cloud.name);
45
+ pushIf(changes, ".nustack/project.json", "toolkit.version", project.version, cloud.version);
46
+ pushIf(changes, ".nustack/project.json", "toolkit.rendererVersion", project.rendererVersion, cloud.rendererVersion);
47
+ pushIf(changes, ".nustack/project.json", "toolkit.placementMapVersion", project.placementMapVersion, cloud.placementMapVersion);
48
48
  }
49
49
  const svc = manifests.serviceToolkit;
50
50
  if (svc) {
@@ -1,4 +1,4 @@
1
- import { withSuppressedSqliteWarning } from "../local_store/database.js";
1
+ import { withSuppressedSqliteWarning } from "../store/open.js";
2
2
  export class GraphLibraryMissing extends Error {
3
3
  code = "GRAPH_LIBRARY_MISSING";
4
4
  constructor() {
@@ -2,9 +2,9 @@ import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { openLocalStore, openLocalStoreReadOnly } from "../local_store/database.js";
5
+ import { openLocalStore } from "../local_store/database.js";
6
6
  import { writeRecords } from "../records/recordsTable.js";
7
- import { RECORD_SCHEMA_VERSION } from "../records/door.js";
7
+ import { RECORD_SCHEMA_VERSION } from "../records/kinds.js";
8
8
  import { idempotencyKeyOf } from "../records/naturalKey.js";
9
9
  import { recordCredentialOf } from "../records/environment.js";
10
10
  import { readProjectContract, CONTRACT_PATH } from "../project_contract.js";
@@ -17,7 +17,6 @@ export const HOOK_EVENTS = ["pre-tool-use", "stop"];
17
17
  export const RULE_UNAVAILABLE_GATE = "hook.rule_unavailable";
18
18
  export const DEADLINE_GATE = "hook.deadline";
19
19
  const EVENT_NAMESPACE = "3f0c9a52-6d1e-4b7a-9c2f-8e5d4a1b7c60";
20
- const RECORDS_STORE_VERSION = 9;
21
20
  const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
22
21
  export const COMMIT_RULES = new Set([
23
22
  "changelog",
@@ -163,11 +162,6 @@ async function writeVerdicts(root, projectId, payload, target, verdicts) {
163
162
  if (verdicts.length === 0)
164
163
  return null;
165
164
  try {
166
- const held = await openLocalStoreReadOnly();
167
- const version = held?.version ?? RECORDS_STORE_VERSION;
168
- held?.close();
169
- if (version < RECORDS_STORE_VERSION)
170
- return null;
171
165
  const store = await openLocalStore();
172
166
  try {
173
167
  const now = Date.now();
@@ -1,5 +1,4 @@
1
1
  import { openLocalStoreReadOnly } from "../local_store/database.js";
2
- const RECORDS_STORE_VERSION = 9;
3
2
  const NEVER_EXPIRES = "9999-12-31";
4
3
  const DISCHARGED_VERDICTS = new Set(["updated", "owes_nothing"]);
5
4
  export async function readStoreDocumentationWaivers(projectId, dbPath) {
@@ -9,10 +8,8 @@ export async function readStoreDocumentationWaivers(projectId, dbPath) {
9
8
  if (!store)
10
9
  return [];
11
10
  try {
12
- if (store.version < RECORDS_STORE_VERSION)
13
- return [];
14
11
  const rows = store.db
15
- .prepare("SELECT payload_json FROM records WHERE kind = 'obligation' AND project_id = ?")
12
+ .prepare("SELECT payload_json FROM harness_records WHERE kind = 'obligation' AND project_id = ?")
16
13
  .all(projectId);
17
14
  const subjects = new Set();
18
15
  for (const row of rows) {