@volter/supercode-orchestrator 0.5.206 → 0.5.208

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/board/ztrack.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { fillReadableIds, validateReadableId } from './readable-id.mjs';
1
2
  // The document is card state. One SQLite file beside it keeps the runtime tables the engine speaks
2
3
  // and an index of the document's card spans. ADR 0001 and 0003.
3
4
  // All markdown interpretation belongs to ztrack's public codec and kanban preset.
@@ -15,10 +16,10 @@ import { current } from './call.mjs';
15
16
  import { fenced } from './fence.mjs';
16
17
  import { createRequire } from 'node:module';
17
18
  import { createHash, randomBytes } from 'node:crypto';
18
- import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
19
+ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
19
20
  import { basename, dirname, join, resolve } from 'node:path';
20
21
  import { DatabaseSync } from 'node:sqlite';
21
- import { actorOf, contention, event, json, now, principalOf, storageFault, STORAGE_TRIES, unreadable } from './store.mjs';
22
+ import { actorOf, boardKey, CARD_NUMBERS, contention, raiseCardNumbers, event, json, now, principalOf, storageFault, STORAGE_TRIES, unreadable } from './store.mjs';
22
23
  import { GitDocument } from './git-document.mjs';
23
24
  import { unimported } from './legacy-store.mjs';
24
25
  import { noteStep, recordSlow } from '@volter/supercode-harness-sdk/slow-log';
@@ -104,6 +105,10 @@ const ARCHIVE_LINES = 2000;
104
105
  // document changed while it was read (readSnapshot).
105
106
  const SNAPSHOT_TRIES = 8;
106
107
  const isArchive = (role) => role === 'archived' || String(role ?? '').startsWith('archived:');
108
+ // The open document's record of the numbers the board has given (numberMark: its key, its number), and a task line's
109
+ // number (`- [ ] VO-2836 c3`); either written keyed, or with `#` as before a board read its numbers with a key.
110
+ const NUMBER_MARK = /^The board has given card numbers up to (?:#|([A-Z][A-Z0-9]+)-)(\d+)\.[ \t]*$/m;
111
+ const TASK_NUMBER = /^\s*[-*] \[[ xX]\]\s+(?:#|[A-Z][A-Z0-9]+-)(\d+)\s+[a-z]+\d+\s/gm;
107
112
 
108
113
  const BOARD_TABLES = `
109
114
  CREATE TABLE IF NOT EXISTS board_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
@@ -133,7 +138,7 @@ export function openZtrackBoard(marker, { create = false, document = null, decla
133
138
  }
134
139
  const config = JSON.parse(read(marker));
135
140
  const backing = new ZtrackBoard({ ...config, seed, marker, init, root, slug, held, readOnly });
136
- return { db: backing.db, tx: (fn) => backing.tx(fn), publishPending: () => backing.git?.publishPending(), unimported: () => backing.unimported(), close: () => backing.close(), document: backing.path,
141
+ return { db: backing.db, snapshot: (path) => backing.snapshot(path), tx: (fn) => backing.tx(fn), publishPending: () => backing.git?.publishPending(), unimported: () => backing.unimported(), close: () => backing.close(), document: backing.path,
137
142
  documentsChanged: () => backing.documentsChanged(), documentsUnwatched: (unwatched) => { backing.unwatched = unwatched; }, saidOnOpen: () => backing.sayLate() };
138
143
  }
139
144
 
@@ -210,7 +215,7 @@ class ZtrackBoard {
210
215
  }
211
216
  // A board's first open in this process recovers what its store accepted and derives what git owes for it (tx), so
212
217
  // a board that is only read, or whose owner runs no round, still finishes an earlier process's commit.
213
- if (seed || this.git?.underived) this.tx(() => {});
218
+ if (seed || this.git?.underived || this.get('rebuild_from_documents') || !this.get('task_identities_written') || !this.get('readable_ids_written')) this.tx(() => {});
214
219
  }
215
220
 
216
221
  // A listing's view, taken once at its open and kept until its close: the store's rows as of one read transaction, and
@@ -264,7 +269,7 @@ class ZtrackBoard {
264
269
  // state, so a rotation committed but not yet written is still a document of the board).
265
270
  get docs() {
266
271
  let rotated = [];
267
- try { rotated = this.conn ? this.get('archives', []) : []; } catch { rotated = []; }
272
+ try { rotated = this.conn ? this.get('archives', []) : this.rebuildArchives ?? []; } catch { rotated = []; }
268
273
  // recorded by file name, beside the document, so a board moved with its documents still finds them
269
274
  return [...this.baseDocs, ...rotated.map(({ role, name }) => ({ role, path: resolve(dirname(this.path), name) }))];
270
275
  }
@@ -273,6 +278,14 @@ class ZtrackBoard {
273
278
  board() { return { root: this.root, slug: this.slug, label: this.slug, db: this.db }; }
274
279
  close() { this.closed = true; this.git?.release(); this.dropOverlay(); this.conn.close(); }
275
280
 
281
+ // Import pending document edits through the backing, then copy the committed
282
+ // projection outside its write wrapper: SQLite forbids VACUUM inside a tx.
283
+ snapshot(path) {
284
+ if (this.active || this.readOnly) throw new Error('board export needs a writable backing outside a transaction');
285
+ this.tx(() => {});
286
+ this.conn.prepare('VACUUM INTO ?').run(path);
287
+ }
288
+
276
289
  statement(sql) {
277
290
  let statement = this.statements.get(sql);
278
291
  if (!statement) {
@@ -348,6 +361,8 @@ class ZtrackBoard {
348
361
 
349
362
  // A board created from the previous runtime store imports its rows once; the old file stays as it was.
350
363
  create(legacy) {
364
+ const named = `${basename(this.path)}.archive-`;
365
+ this.rebuildArchives = existsSync(dirname(this.path)) ? readdirSync(dirname(this.path)).filter(name => name.startsWith(named) && name.endsWith('.md')).sort().map(name => ({ role: `archived:${name.slice(named.length, -3)}`, name })) : [];
351
366
  const tmp = `${this.file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
352
367
  const db = new DatabaseSync(tmp);
353
368
  try {
@@ -359,6 +374,13 @@ class ZtrackBoard {
359
374
  this.init(db, { fresh: true });
360
375
  db.exec(BOARD_TABLES);
361
376
  if (has('runtime_rows')) this.importRuntime(db, old, legacy);
377
+ // Runtime history is retained, but an old cache is never the current document state. Persist the recovery
378
+ // marker before installing the store so a crash/reopen also finishes this first full reconciliation.
379
+ db.prepare('INSERT OR REPLACE INTO board_state VALUES (?, ?)').run('archives', JSON.stringify(this.rebuildArchives));
380
+ if (!this.seed) {
381
+ db.prepare('INSERT OR REPLACE INTO board_state VALUES (?, ?)').run('rebuild_from_documents', 'true');
382
+ db.prepare('INSERT OR REPLACE INTO board_state VALUES (?, ?)').run('published', '{}');
383
+ }
362
384
  } finally { old?.close(); }
363
385
  db.close();
364
386
  } catch (error) {
@@ -440,6 +462,13 @@ class ZtrackBoard {
440
462
  put(`chunks:${role}`, entries);
441
463
  }
442
464
  put('published', published);
465
+ // The earlier store held no numbers: its cards take the numbers their documents' headings give them, and the rest
466
+ // are numbered in the order they were made, after every number the documents name (documentNumbers).
467
+ db.exec(`DELETE FROM ${CARD_NUMBERS}; DELETE FROM sqlite_sequence WHERE name = '${CARD_NUMBERS}'`);
468
+ const named = this.documentNumbers();
469
+ this.numberFromHeadings([...named.headings].filter(([id]) => db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(id)), db);
470
+ raiseCardNumbers(db, named.highest);
471
+ db.exec(`INSERT INTO ${CARD_NUMBERS} (task_id) SELECT id FROM tasks WHERE id NOT IN (SELECT task_id FROM ${CARD_NUMBERS}) ORDER BY created_at, id`);
443
472
  db.exec('COMMIT');
444
473
  }
445
474
 
@@ -554,8 +583,26 @@ class ZtrackBoard {
554
583
  }
555
584
  if (this.seed) this.seedRows();
556
585
  timedStep('board tx snapshot', () => this.snapshotHeld());
586
+ fillReadableIds(this.db);
557
587
  const result = timedStep('board tx body', () => fn(this.db));
588
+ fillReadableIds(this.db);
558
589
  if (this.contended) throw RETRY;
590
+ // The first write made with a codec that renders keyed numbers (numbersRendered) once the store numbered its
591
+ // cards (store.mjs layOutNumbers) writes every card of the open document again, with its keyed number in its
592
+ // heading and each open subtask's on its task line, and again whenever the board's key changes (a renamed home);
593
+ // a card made since is written with its number when it is made. `numbers_written` is the key last written.
594
+ if (this.numbersRendered && (this.numberForm === 'keyed' ? this.get('numbers_written') !== this.cardKey : !this.get('numbers_written'))) {
595
+ this.statement('INSERT INTO temp.board_touched SELECT value FROM json_each(?)').run(JSON.stringify(this.entries('open').filter((e) => e.id).map((e) => nativeId(e.id))));
596
+ this.put('numbers_written', this.numberForm === 'keyed' ? this.cardKey : true);
597
+ }
598
+ if (!this.get('readable_ids_written')) {
599
+ this.conn.exec('INSERT INTO temp.board_touched SELECT id FROM main.tasks');
600
+ this.put('readable_ids_written', true);
601
+ }
602
+ if (!this.get('task_identities_written')) {
603
+ this.conn.exec('INSERT INTO temp.board_touched SELECT DISTINCT subtask_of FROM main.supercode_cards WHERE subtask_of IS NOT NULL');
604
+ this.put('task_identities_written', true);
605
+ }
559
606
  const files = timedStep('board tx render', () => this.render());
560
607
  const intent = files.length ? randomBytes(16).toString('hex') : null;
561
608
  if (intent) {
@@ -658,7 +705,7 @@ class ZtrackBoard {
658
705
  // connection-local copies by a read. Only the spans the edit touched are parsed.
659
706
  // `held` names cards read again from their indexed span with no edit (see render).
660
707
  observe(stale, held = []) {
661
- const plan = { layout: new Map(), dropped: [], published: {}, cards: new Map(), rows: new Map(), subtasks: new Map(), tasks: new Map(), remove: new Set(), errors: new Map(), checked: new Set(), before: new Map() };
708
+ const plan = { layout: new Map(), dropped: [], published: {}, cards: new Map(), numbers: new Map(), rows: new Map(), subtasks: new Map(), tasks: new Map(), remove: new Set(), errors: new Map(), checked: new Set(), before: new Map() };
662
709
  const changed = new Set();
663
710
  for (const doc of this.docs) {
664
711
  const entries = this.entries(doc.role);
@@ -699,11 +746,13 @@ class ZtrackBoard {
699
746
  catch (error) { plan.errors.set(id, error.message); continue; }
700
747
  entry.rec = hash(JSON.stringify(records[0]));
701
748
  plan.cards.set(id, card);
749
+ if (records[0].number) plan.numbers.set(id, records[0].number);
702
750
  }
703
751
  for (const [id, card] of plan.cards) {
704
752
  plan.rows.set(id, this.cardRows(id, card, null));
705
753
  const subtasks = [];
706
- for (const [tid, taskCard, key, sources] of this.taskCards(id, card)) {
754
+ for (const [tid, taskCard, key, sources, number] of this.taskCards(id, card)) {
755
+ if (number) plan.numbers.set(tid, number);
707
756
  plan.rows.set(tid, this.cardRows(tid, taskCard, id));
708
757
  plan.tasks.set(tid, { key, sources });
709
758
  subtasks.push(tid);
@@ -778,7 +827,7 @@ class ZtrackBoard {
778
827
  // while its card was not rendered, as under a recorded refusal): it takes the store's value, and the card's
779
828
  // render writes that back into its lines (t_f3f102db).
780
829
  checkControls(plan, resolveRef) {
781
- if (!this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
830
+ if (this.get('rebuild_from_documents') || !this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
782
831
  let fields = [];
783
832
  try { fields = workflowFor(this.root).params.document_control_fields ?? []; } catch { /* no home policy */ }
784
833
  if (!fields.length) return;
@@ -837,9 +886,17 @@ class ZtrackBoard {
837
886
  this.dropOverlay();
838
887
  this.conn.exec('CREATE TEMP TABLE tasks AS SELECT * FROM main.tasks; CREATE TEMP TABLE supercode_cards AS SELECT * FROM main.supercode_cards; CREATE TEMP TABLE task_links AS SELECT * FROM main.task_links');
839
888
  this.conn.exec(TASK_SUMMARY_INDEX_SQL.replace(TASK_SUMMARY_INDEX, `temp.${TASK_SUMMARY_INDEX}`));
889
+ this.conn.exec("CREATE UNIQUE INDEX temp.supercode_open_readable_ids ON tasks(readable_id) WHERE status NOT IN ('done','cancelled','archived')");
840
890
  this.overlay = true; this.statements.clear();
841
891
  }
842
- const initial = !overlay && !this.one('SELECT 1 FROM main.tasks LIMIT 1');
892
+ const rebuilding = !overlay && this.get('rebuild_from_documents');
893
+ const initial = !overlay && (rebuilding || !this.one('SELECT 1 FROM main.tasks LIMIT 1'));
894
+ if (rebuilding) {
895
+ if (plan.errors.size) throw new Error(`cannot rebuild board from invalid documents: ${[...plan.errors].map(([id, why]) => `${id}: ${why}`).join('; ')}`);
896
+ // The imported cache can predate cards added, edited or removed in the documents. Keep its history, not its
897
+ // obsolete card state. Every document was re-read, including unchanged archive spans.
898
+ for (const { id } of this.statement('SELECT id FROM main.tasks').all()) if (!plan.rows.has(id)) plan.remove.add(id);
899
+ }
843
900
  const by = 'document-editor';
844
901
  const events = [];
845
902
  const fingerprintOf = (id) => {
@@ -852,12 +909,24 @@ class ZtrackBoard {
852
909
  this.statement(`DELETE FROM ${schema}.task_links WHERE child_id = ?`).run(id);
853
910
  if (!overlay) { this.statement('DELETE FROM main.board_tasks WHERE id = ?').run(id); if (!initial) events.push({ id, kind: 'removed', payload: { by, source: 'document' } }); }
854
911
  }
912
+ // A store made again from its documents (an empty one reading them whole) gives each card and subtask the number its
913
+ // heading or task line says, and hands out none at or below any number the documents name. Any other import keeps
914
+ // the store's numbers: a number typed into a heading or pasted with a section changes nothing.
915
+ if (initial) {
916
+ if (rebuilding) this.conn.exec(`DELETE FROM ${CARD_NUMBERS}`);
917
+ this.numberFromHeadings([...plan.numbers].filter(([id]) => plan.rows.has(id)));
918
+ raiseCardNumbers(this.conn, this.documentNumbers().highest);
919
+ }
855
920
  for (const [id, rows] of plan.rows) {
856
921
  const before = fingerprintOf(id);
857
922
  const oldTask = this.one(`SELECT * FROM ${schema}.tasks WHERE id = ?`, id) ?? {};
858
923
  const oldState = this.one(`SELECT * FROM ${schema}.supercode_cards WHERE task_id = ?`, id) ?? {};
859
924
  this.replace(schema, 'tasks', { ...oldTask, ...rows.task }, 'id');
860
925
  this.replace(schema, 'supercode_cards', { ...oldState, ...rows.state }, 'task_id');
926
+ if (rebuilding && ENDED.has(rows.state.stage ?? rows.task.status) && oldTask.current_run_id) {
927
+ this.statement("UPDATE main.task_runs SET ended_at = COALESCE(ended_at, ?), status = 'reclaimed', outcome = COALESCE(outcome, 'reclaimed') WHERE id = ? AND ended_at IS NULL").run(now(), oldTask.current_run_id);
928
+ this.statement('UPDATE main.tasks SET current_run_id = NULL, claim_lock = NULL, claim_expires = NULL, worker_pid = NULL WHERE id = ?').run(id);
929
+ }
861
930
  this.statement(`DELETE FROM ${schema}.task_links WHERE child_id = ?`).run(id);
862
931
  for (const b of json(rows.state.blocked_by, []).filter((b) => b.kind === 'card')) this.statement(`INSERT OR IGNORE INTO ${schema}.task_links (parent_id, child_id) VALUES (?, ?)`).run(b.id, id);
863
932
  const after = fingerprintOf(id);
@@ -868,7 +937,11 @@ class ZtrackBoard {
868
937
  }
869
938
  for (const id of plan.checked) this.errors.delete(id);
870
939
  for (const [id, reason] of plan.errors) this.errors.set(id, reason);
940
+ // A read overlays new document cards before the owner commits them. Give that
941
+ // connection-local view the same names as the subsequent write.
942
+ fillReadableIds(this.conn);
871
943
  if (overlay) return;
944
+ if (rebuilding) this.drop('rebuild_from_documents');
872
945
  for (const id of plan.errors.keys()) this.refused.add(this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of ?? id);
873
946
  for (const [tid, { key, sources }] of plan.tasks) this.statement('INSERT OR REPLACE INTO main.board_tasks (id, key, sources) VALUES (?, ?, ?)').run(tid, key, sources?.length ? JSON.stringify(sources) : null);
874
947
  for (const id of [...plan.checked, ...plan.rows.keys()]) if (!plan.errors.has(id)) this.statement('DELETE FROM main.board_errors WHERE id = ?').run(id);
@@ -915,7 +988,7 @@ class ZtrackBoard {
915
988
  const workspace = /^(scratch|dir|worktree)(?::(.*))?$/.exec(card.workspace ?? 'scratch');
916
989
  const old = this.one('SELECT * FROM main.tasks WHERE id = ?', id) ?? {};
917
990
  const runtime = Object.fromEntries(Object.entries(old).filter(([k]) => RUNTIME.has(k) || RUN_OUTPUT.has(k)));
918
- const task = { ...meta, ...runtime, id, title: card.title, body: stored.body,
991
+ const task = { ...meta, ...runtime, id, title: card.title, readable_id: meta.readable_id == null ? old.readable_id ?? null : validateReadableId(meta.readable_id), body: stored.body,
919
992
  status: card.status, assignee: card.assignee ?? null, created_at: meta.created_at ?? old.created_at ?? now(), priority: card.priority ?? 0,
920
993
  workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: runtime.workspace_path ?? (/^(\/|~|[A-Za-z]:[\\/])/.test(workspace?.[2] ?? '') ? workspace[2] : null), branch_name: card.branch ?? null };
921
994
  const state = { task_id: id, workspace_anchor: workspace?.[2] ?? null, stage: card.status, machine: card.machine ?? null, subtask_of: parent,
@@ -924,28 +997,29 @@ class ZtrackBoard {
924
997
  // carried it (or edited without it) is not an unarchive. Saying `archived: false` of an archived card is.
925
998
  paused: card.paused ? JSON.stringify(card.paused) : null, every: card.every ?? null,
926
999
  archived: (meta.archived ?? card.archived) === undefined ? Number(this.one('SELECT archived FROM main.supercode_cards WHERE task_id = ?', id)?.archived ?? 0) : ((meta.archived ?? card.archived) ? 1 : 0) };
1000
+ if (!state.archived && !ENDED.has(card.status)) task.status = card.paused ? 'paused' : card.relations?.length ? 'blocked' : card.status;
927
1001
  return { task, state };
928
1002
  }
929
1003
 
930
- // A card's Tasks as subtask cards: [id, card, key, sources].
1004
+ // A card's Tasks as subtask cards: [id, card, key, sources, the number its line gives].
931
1005
  taskCards(id, card) {
932
1006
  return card.acceptanceCriteria.map((task) => {
933
1007
  const key = `${id}:${task.id}`;
934
1008
  const tid = task.metadata?.id ?? this.one('SELECT id FROM main.board_tasks WHERE key = ?', key)?.id ?? `t_${randomBytes(4).toString('hex')}`;
935
- // A task line carries its brief and its tick; what else the subtask holds is the store's (an older document's
936
- // `metadata:` line is still read).
937
- const stored = task.metadata ? null : this.one('SELECT * FROM main.tasks WHERE id = ?', tid);
938
- const storedState = task.metadata ? null : this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', tid);
1009
+ // The native identity and declared state travel with the line. Runtime history stays in the store, joined by
1010
+ // this same identity, including when that store was rebuilt from a cache older than the subtask.
1011
+ const stored = this.one('SELECT * FROM main.tasks WHERE id = ?', tid);
1012
+ const storedState = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', tid);
939
1013
  const was = task.metadata?.status ?? storedState?.stage ?? stored?.status;
940
- const taskCard = { ...task.metadata?.card, ...(storedState ? { paused: json(storedState.paused, null) ?? undefined, every: storedState.every || undefined } : {}),
1014
+ const taskCard = { ...(storedState ? { paused: json(storedState.paused, null) ?? undefined, every: storedState.every || undefined } : {}), ...task.metadata?.card,
941
1015
  id: tid, title: `${task.id} ${task.text}`, body: task.metadata?.body ?? task.brief ?? '',
942
1016
  status: task.status === 'passed' ? (was === 'cancelled' ? 'cancelled' : 'done') : (ENDED.has(was) ? 'todo' : was ?? 'todo'),
943
1017
  metadata: task.metadata?.properties ?? Object.fromEntries(Object.entries(stored ?? {}).filter(([k, v]) => declared(k, v))),
944
- assignee: task.metadata?.assignee ?? stored?.assignee ?? card.assignee,
945
- machine: task.metadata?.machine ?? storedState?.machine ?? card.machine, acceptanceCriteria: [], acceptance: task.lines ?? [],
1018
+ assignee: task.metadata && 'assignee' in task.metadata ? task.metadata.assignee : stored?.assignee ?? card.assignee,
1019
+ machine: task.metadata && 'machine' in task.metadata ? task.metadata.machine : storedState?.machine ?? card.machine, acceptanceCriteria: [], acceptance: task.lines ?? [],
946
1020
  relations: (task.blockedBy ?? []).map((b) => ({ type: 'blocked-by', issueId: b.ac && /^(?:[ma]-[0-9a-f]{6,}|t_[0-9a-f]+)$/.test(b.ac) ? b.ac : b.ac ? `${nativeId(b.issue)}:${b.ac}` : nativeId(b.issue) })) };
947
1021
  if (task.waitingOn) taskCard.relations.push({ type: 'blocked-by', issueId: task.waitingOn });
948
- return [tid, taskCard, key, task.sources];
1022
+ return [tid, taskCard, key, task.sources, task.number];
949
1023
  });
950
1024
  }
951
1025
 
@@ -965,7 +1039,8 @@ class ZtrackBoard {
965
1039
  return parents;
966
1040
  };
967
1041
  let parents = cardsTouched();
968
- if (!parents.size) return [];
1042
+ const mark = this.numbersRendered ? this.numberMark() : null;
1043
+ if (!parents.size && !mark) return [];
969
1044
  const heldBy = (id) => this.errors.has(id) || this.one('SELECT 1 FROM main.supercode_cards WHERE subtask_of = ? AND task_id IN (SELECT id FROM main.board_errors)', id);
970
1045
  // A card held by a recorded refusal is not rendered, so the store's verbs move its tasks past its lines. When
971
1046
  // the store moves it, its span is read again: what the store moved past is stale and takes the store's value
@@ -978,6 +1053,7 @@ class ZtrackBoard {
978
1053
  const where = new Map();
979
1054
  for (const role of roles) layout[role].forEach((entry, index) => { if (entry.id) where.set(nativeId(entry.id), { role, index, entry }); });
980
1055
  const edits = Object.fromEntries(roles.map((role) => [role, []]));
1056
+ if (mark) edits.open.push(mark);
981
1057
  for (const id of parents) {
982
1058
  if (heldBy(id)) continue;
983
1059
  const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id);
@@ -1041,7 +1117,8 @@ class ZtrackBoard {
1041
1117
  dropped.push(old.cid);
1042
1118
  if (edit.remove) entries.splice(edit.index, 1); else entries[edit.index] = edit.entry;
1043
1119
  }
1044
- for (const edit of edits[role].filter((e) => e.index === undefined)) {
1120
+ for (const edit of edits[role].filter((e) => e.prepend)) { text = edit.entry.text + text; entries.unshift(edit.entry); }
1121
+ for (const edit of edits[role].filter((e) => e.index === undefined && !e.prepend)) {
1045
1122
  if (text && !text.endsWith('\n\n')) {
1046
1123
  // the blank line before a new section belongs to the span above it
1047
1124
  const last = entries.at(-1), separator = text.endsWith('\n') ? '\n' : '\n\n';
@@ -1127,9 +1204,13 @@ class ZtrackBoard {
1127
1204
  const task = parse({ id: r.id, title: r.title, status, body: r.body ?? '' });
1128
1205
  const blocks = json(s.blocked_by, []);
1129
1206
  const messages = blocks.filter((block) => block.kind === 'message');
1130
- // A subtask is its line and its brief (its body, as Markdown under it); its id, status words, assignee and run
1131
- // output stay in the store (company RFC 0025 decision 4).
1132
- card.acceptanceCriteria.push({ sources: json(stored?.sources, undefined) ?? undefined, id: key, status: ENDED.has(status) ? 'passed' : 'pending', evidence: [], text: match?.[2] ?? r.title,
1207
+ // Keep the identity-to-number binding and declared state durable with its line; a stale runtime cache cannot
1208
+ // supply these for a newer subtask. Run output, clocks and active claims remain the runtime store's.
1209
+ card.acceptanceCriteria.push({ sources: json(stored?.sources, undefined) ?? undefined, id: key, ...this.numberOf(r.id), status: ENDED.has(status) ? 'passed' : 'pending', evidence: [], text: match?.[2] ?? r.title,
1210
+ metadata: { id: r.id, status, assignee: r.assignee, machine: s.machine,
1211
+ properties: Object.fromEntries(Object.entries(r).filter(([k, v]) => declared(k, v))),
1212
+ card: { priority: r.priority ?? 0, workspace: r.workspace_kind + (r.workspace_kind !== 'scratch' && (s.workspace_anchor ?? r.workspace_path) ? `:${s.workspace_anchor ?? r.workspace_path}` : ''),
1213
+ branch: r.branch_name || undefined, paused: json(s.paused, null) ?? undefined, every: s.every || undefined } },
1133
1214
  ...(task.body ? { brief: task.body } : {}), lines: task.acceptance,
1134
1215
  waitingOn: messages[0]?.id,
1135
1216
  blockedBy: blocks.filter((block) => block !== messages[0]).map((block) => {
@@ -1140,7 +1221,97 @@ class ZtrackBoard {
1140
1221
  }) });
1141
1222
  }
1142
1223
  const serialized = this.preset.serializeKanbanCard(card);
1143
- return { id: card.id, title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
1224
+ return { id: card.id, ...this.numberOf(id), title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
1225
+ }
1226
+
1227
+ // A card's number, shown first in its heading with its board's key (`## VO-42 t_… — …`): the store's (store.mjs
1228
+ // layOutNumbers, boardKey). An edit to a heading's number changes nothing; a store made again from its documents takes
1229
+ // it back (numberFromHeadings). A card's record keeps the number and key where the document codec reads them, after
1230
+ // the id.
1231
+ numberOf(id) {
1232
+ if (!this.numbersRendered) return {};
1233
+ const number = this.one(`SELECT number FROM main.${CARD_NUMBERS} WHERE task_id = ?`, id)?.number;
1234
+ return number ? { number, ...(this.numberForm === 'keyed' ? { key: this.cardKey } : {}) } : {};
1235
+ }
1236
+
1237
+ /** The board's key (store.mjs boardKey). */
1238
+ get cardKey() { return this._cardKey ??= boardKey(this.root, this.slug); }
1239
+
1240
+ // How the document codec (@volter/ztrack) writes a card's number in its heading and a subtask's on its task line:
1241
+ // `keyed` (`VO-42`), `plain` (`#42`, a codec from before numbers were keyed), or null (none). One that writes none
1242
+ // renders, reads and indexes cards as it always did; the open document is written with the numbers in the codec's form
1243
+ // by the first write made with it (tx), whichever was installed first.
1244
+ get numberForm() {
1245
+ if (this._numberForm === undefined) {
1246
+ const writes = (key, mark) => {
1247
+ try {
1248
+ const task = this.preset.serializeKanbanCard({ id: 't-00000000', title: 'number', summary: '', status: 'todo', body: '', acceptanceCriteria: [{ id: 'c1', number: 2, ...(key ? { key } : {}), status: 'pending', evidence: [], text: 'number' }] }).body;
1249
+ return this.codec.renderBoardCard({ id: 't_00000000', number: 1, ...(key ? { key } : {}), title: 'number', status: 'todo', body: '' }, 2).startsWith(`## ${mark}1 t_00000000 `) && task.includes(`- [ ] ${mark}2 c1 number`);
1250
+ } catch { return false; }
1251
+ };
1252
+ this._numberForm = writes('KB', 'KB-') ? 'keyed' : writes(null, '#') ? 'plain' : null;
1253
+ }
1254
+ return this._numberForm;
1255
+ }
1256
+ get numbersRendered() { return this.numberForm !== null; }
1257
+
1258
+ // Give each card the number its document gives it (its heading, or its task line), where no other card holds that
1259
+ // number and the card has none yet; the numbers' high-water mark rises with them, so none is handed out again. Only
1260
+ // for a store made again from its documents (applyPlan, importRuntime).
1261
+ numberFromHeadings(numbers, db = this.conn) {
1262
+ if (!numbers.length) return;
1263
+ const give = db.prepare(`INSERT INTO ${CARD_NUMBERS} (number, task_id) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM ${CARD_NUMBERS} WHERE task_id = ? OR number = ?)`);
1264
+ const holder = db.prepare(`SELECT task_id FROM ${CARD_NUMBERS} WHERE number = ?`);
1265
+ for (const [id, number] of numbers) {
1266
+ const other = holder.get(number)?.task_id;
1267
+ if (other && other !== id) throw new Error(`cannot rebuild card numbers: ${number} names both ${other} and ${id}`);
1268
+ give.run(number, id, id, number);
1269
+ }
1270
+ raiseCardNumbers(db);
1271
+ }
1272
+
1273
+ // What the board's documents (the open one, the current archive and each rotated one beside it) say of numbers:
1274
+ // `headings`, card id → the number its heading gives, and `highest`, the highest number any of them names (a
1275
+ // heading, a task line, or the open document's record of the numbers given, numberMark).
1276
+ documentNumbers() {
1277
+ const headings = new Map();
1278
+ let highest = 0;
1279
+ const dir = dirname(this.path), named = basename(this.path);
1280
+ let rotated = [];
1281
+ try { rotated = readdirSync(dir).filter((name) => name.startsWith(`${named}.archive-`) && name.endsWith('.md')).map((name) => join(dir, name)); } catch { /* none */ }
1282
+ for (const path of [this.path, this.archive, ...rotated]) {
1283
+ const text = lf(read(path));
1284
+ for (const line of text.matchAll(TASK_NUMBER)) highest = Math.max(highest, Number(line[1]));
1285
+ if (path === this.path) highest = Math.max(highest, Number(NUMBER_MARK.exec(text)?.[2] ?? 0));
1286
+ let records = [];
1287
+ try { ({ records } = this.codec.inspectBoardDocument(text, path)); } catch { continue; }
1288
+ for (const record of records) {
1289
+ if (!record.number) continue;
1290
+ highest = Math.max(highest, record.number);
1291
+ if (!headings.has(nativeId(record.id))) headings.set(nativeId(record.id), record.number);
1292
+ }
1293
+ }
1294
+ return { headings, highest };
1295
+ }
1296
+
1297
+ // The open document records the highest number the store has given (a line of its preamble, NUMBER_MARK), so a store
1298
+ // made again from the documents hands out none at or below it, whatever card held it: a subtask filed and gone, or
1299
+ // a card the documents no longer show. The edit that brings the line up to the store's count, or null.
1300
+ numberMark() {
1301
+ const given = Number(this.one('SELECT seq FROM main.sqlite_sequence WHERE name = ?', CARD_NUMBERS)?.seq ?? 0);
1302
+ if (!given) return null;
1303
+ const keyed = this.numberForm === 'keyed';
1304
+ const line = (top) => `The board has given card numbers up to ${keyed ? `${this.cardKey}-` : '#'}${top}.`;
1305
+ const first = this.entries('open')[0];
1306
+ if (first && first.id === null) {
1307
+ const text = this.texts([first])[0].text;
1308
+ const said = NUMBER_MARK.exec(text);
1309
+ if (said && (keyed ? said[1] === this.cardKey : said[1] === undefined) && Number(said[2]) >= given) return null;
1310
+ const next = said ? text.replace(NUMBER_MARK, line(Math.max(given, Number(said[2])))) : `${text.replace(/\n*$/, '')}\n\n${line(given)}\n\n`;
1311
+ return { index: 0, entry: { cid: null, id: null, level: 0, len: next.length, rec: null, text: next } };
1312
+ }
1313
+ const text = `${line(given)}\n\n`;
1314
+ return { prepend: true, entry: { cid: null, id: null, level: 0, len: text.length, rec: null, text } };
1144
1315
  }
1145
1316
 
1146
1317
  // An ended subtask's own record in the archive document: its title and brief, and the card it belongs to.
@@ -1150,7 +1321,7 @@ class ZtrackBoard {
1150
1321
  const metadata = { subtask_of: fileId(parentId), ...Object.fromEntries(Object.entries(task).filter(([k, v]) => declared(k, v))) };
1151
1322
  const card = { ...parsed, metadata, machine: state.machine || undefined, relations: [] };
1152
1323
  const serialized = this.preset.serializeKanbanCard(card);
1153
- return { id: card.id, title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
1324
+ return { id: card.id, ...this.numberOf(id), title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
1154
1325
  }
1155
1326
 
1156
1327
  // ------------------------------------------------------------------ migration from a SQLite board
@@ -1176,10 +1347,15 @@ class ZtrackBoard {
1176
1347
  if (keys.length) this.statement(`INSERT OR REPLACE INTO main.${quote(table)} (${keys.map(quote).join(',')}) VALUES (${keys.map(() => '?').join(',')})`).run(...keys.map((k) => row[k] ?? null));
1177
1348
  };
1178
1349
  for (const table of this.conn.prepare("SELECT name FROM main.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'board_%'").all()) this.conn.exec(`DELETE FROM main.${quote(table.name)}`);
1179
- for (const table of source.db.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all()) {
1350
+ // a card's number is copied before its row, which keeps it; the numbers' high-water mark comes with them, so no
1351
+ // number the old board handed out is handed out again
1352
+ const sourceTables = source.db.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all()
1353
+ .sort((a, b) => Number(b.name === CARD_NUMBERS) - Number(a.name === CARD_NUMBERS));
1354
+ for (const table of sourceTables) {
1180
1355
  if (!this.tables.has(table.name)) continue;
1181
1356
  for (const row of source.db.prepare(`SELECT * FROM ${quote(table.name)}`).all()) insert(table.name, row);
1182
1357
  }
1358
+ raiseCardNumbers(this.conn, Number(source.db.prepare('SELECT seq FROM sqlite_sequence WHERE name = ?').get(CARD_NUMBERS)?.seq ?? 0));
1183
1359
  for (const task of this.conn.prepare('SELECT * FROM main.tasks').all()) {
1184
1360
  const old = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', task.id) ?? {};
1185
1361
  if (old.subtask_of) { const key = /^([a-z]+\d+)\s/.exec(task.title)?.[1]; if (key) this.statement('INSERT OR REPLACE INTO main.board_tasks (id, key) VALUES (?, ?)').run(task.id, `${old.subtask_of}:${key}`); }
@@ -0,0 +1 @@
1
+ export { projectCodexPaneResume } from '@volter/supercode-teams/codex-pane-resume';