@volter/supercode-orchestrator 0.4.154 → 0.4.156

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/ci.mjs CHANGED
@@ -180,6 +180,31 @@ export function openReviewPullRequests(board, id) {
180
180
  return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
181
181
  }
182
182
 
183
+ /** An arc that changed no code (a disposition, such as closing a PR with its reason): its work branch holds no commit
184
+ * beyond its repository's default branch, locally or on origin, its checkout holds no uncommitted work, no PR was
185
+ * ever opened from it, and it registered no other branch. Answers what its review reads instead of a diff, or null when the arc carries (or may carry) code. */
186
+ export function noCodeChange(board, id) {
187
+ const own = repositoryWorkspace(board, id);
188
+ if (!own) return null;
189
+ try {
190
+ const repository = originOf(own);
191
+ const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
192
+ const branch = task?.branch_name ?? (run('git', ['branch', '--show-current'], own) || null);
193
+ const base = run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own);
194
+ if (!repository || !branch || !base || branch === base.replace(/^origin\//, '')) return null;
195
+ if (registeredBranches(board, id).some((r) => r.repository !== repository || r.branch !== branch)) return null;
196
+ // Uncommitted work in the checkout is code not yet committed: a dirty worktree is never no-code.
197
+ if (run('git', ['status', '--porcelain', '--untracked-files=normal'], own)) return null;
198
+ // An unknown local or remote commit is code this checkout cannot see: run() throws and the arc waits.
199
+ if (run('git', ['for-each-ref', '--format=%(refname)', `refs/heads/${branch}`], own)) run('git', ['merge-base', '--is-ancestor', `refs/heads/${branch}`, base], own);
200
+ const remote = run('git', ['ls-remote', 'origin', `refs/heads/${branch}`], own).split(/\s+/)[0];
201
+ if (remote) run('git', ['merge-base', '--is-ancestor', remote, base], own);
202
+ const owner = repository.split('/')[0];
203
+ if (github(`/repos/${repository}/pulls?state=all&head=${encodeURIComponent(`${owner}:${branch}`)}&per_page=1`).length) return null;
204
+ return { pr: [], pr_head: [], branch, no_code: { repository, branch, base, branch_head: remote || null } };
205
+ } catch { return null; }
206
+ }
207
+
183
208
  /** The repositories this machine keeps a checkout of. */
184
209
  function knownRepositories() {
185
210
  try { return [...new Set(branchCheckouts(null, '*').map((row) => row.repository))]; } catch { return []; }
package/board/cli.mjs CHANGED
@@ -756,8 +756,11 @@ export async function runWorkflow(argv) {
756
756
  const params = { ...(args['failure-limit'] ? { failure_limit: Number(args['failure-limit']) } : {}), ...(args.max ? { max_spawn: Number(args.max) } : {}) };
757
757
  if (args['dry-run']) {
758
758
  // planned through the workflow's own dispatch (its start event, eligible, order and limits), writing nothing
759
- const would = (await plan({ root, lanes, settings, params, slugs: [slug] })).flatMap((r) => r.would);
760
- say(args, would.length ? would.map((w) => `would start ${w.id} (${w.status}${w.role ? `, its ${w.role}` : ''}) as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`).join('\n') : 'nothing would start', would);
759
+ const reports = await plan({ root, lanes, settings, params, slugs: [slug] });
760
+ const would = reports.flatMap((r) => r.would), reviews = reports.flatMap((r) => r.would_review ?? []);
761
+ const lines = [...reviews.map((r) => `would request review of ${r.id}: ${r.no_code ? `no code changed (${r.no_code.repository} ${r.no_code.branch} holds no commit beyond ${r.no_code.base}, no PR from it)` : r.prs.map((url, i) => `${url} at ${r.heads[i]}`).join(', ')}`),
762
+ ...would.map((w) => `would start ${w.id} (${w.status}${w.role ? `, its ${w.role}` : ''}) as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`)];
763
+ say(args, lines.length ? lines.join('\n') : 'nothing would start', args.json ? { would, would_review: reviews } : would);
761
764
  return 0;
762
765
  }
763
766
  const reports = await tick({ root, lanes, settings, params });
@@ -15,7 +15,7 @@
15
15
  // Every side effect follows a committed transition: a card is claimed before its session is launched, and a
16
16
  // subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
17
17
  // launch and a crash between the two steps is answered by the next tick.
18
- import { inspectCompletionEvidence, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
18
+ import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
19
19
  import { createRequire } from 'node:module';
20
20
  import { spawn as spawnChild, spawnSync } from 'node:child_process';
21
21
  import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
@@ -1848,15 +1848,30 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
1848
1848
  for (const slug of slugs ?? listBoards(root)) {
1849
1849
  const report = { board: slug, would: [], queued: [], unassigned: [], nonspawnable: [] };
1850
1850
  const board = openBoard(root, slug);
1851
- try { board.label = boardLabel(root, slug); startCards(board, fleet, lanes, settings, report, entry, [], { dry: true }); } finally { board.close(); }
1851
+ try {
1852
+ board.label = boardLabel(root, slug);
1853
+ await activatePullRequestReviews(board, entry, report, [], { dry: true });
1854
+ startCards(board, fleet, lanes, settings, report, entry, [], { dry: true });
1855
+ } finally { board.close(); }
1852
1856
  out.push(report);
1853
1857
  }
1854
1858
  return out;
1855
1859
  }
1856
1860
 
1861
+ /** Whether an arc with no open PR may be one that changed no code and is due its review: it filed and closed tasks
1862
+ * (a fresh arc with none has not started), and no review of it is pending or passed (after changes are requested,
1863
+ * closing the tasks that own the findings makes it due again). */
1864
+ function noCodeReviewDue(board, id) {
1865
+ return view(board, (db) => {
1866
+ if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
1867
+ const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass') ORDER BY id DESC LIMIT 1").get(id);
1868
+ return !last || last.kind === 'changes_requested';
1869
+ });
1870
+ }
1871
+
1857
1872
  /** An opened PR asks for the declared review transition once the arc's tasks are closed. */
1858
1873
  // A task written in the card's own Tasks section that did not become a subtask holds its review too.
1859
- async function activatePullRequestReviews(board, entry, report, effects, { only = null } = {}) {
1874
+ async function activatePullRequestReviews(board, entry, report, effects, { only = null, dry = false } = {}) {
1860
1875
  const candidates = view(board, db => db.prepare(`SELECT t.id FROM tasks t JOIN supercode_cards c ON c.task_id=t.id
1861
1876
  WHERE c.subtask_of IS NULL AND c.every IS NULL
1862
1877
  AND t.workspace_kind!='scratch' ${only ? 'AND t.id=?' : ''}`).all(...(only ? [only] : [])));
@@ -1879,20 +1894,26 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
1879
1894
  payload = openReviewPullRequests(board, id);
1880
1895
  // The arc's open PRs ask for one review only while some head has no verdict: after changes are
1881
1896
  // requested the next run is the implementer's, until a new head is pushed.
1897
+ // An arc that closed its tasks having changed no code gets its one review of the act itself (ADR 0003).
1898
+ if (!payload && noCodeReviewDue(board, id)) payload = noCodeChange(board, id);
1882
1899
  if (!payload) { report.queued.push([id, 'review waits: no open PR on its branch, a wt/<card> branch or a registered checkout']); continue; }
1883
1900
  if (payload.waits) { report.queued.push([id, `review waits: ${payload.waits}`]); payload = null; continue; }
1884
- if (!reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
1901
+ if (!payload.no_code && !reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
1885
1902
  report.queued.push([id, `review waits: every open PR head (${payload.pr.join(', ')}) has a verdict or predates the last one; push a commit that answers it`]);
1886
1903
  continue;
1887
1904
  }
1888
- const evidence = inspectCompletionEvidence(board, id, entry, payload, 'request_review');
1905
+ const evidence = payload.no_code ? { required: false, ready: true, reason: null, phase: 'source', no_code: payload.no_code } : inspectCompletionEvidence(board, id, entry, payload, 'request_review');
1889
1906
  if (!evidence.ready) throw new Error(evidence.reason);
1907
+ // a dry run names the review a tick would request and writes nothing
1908
+ if (dry) { (report.would_review ??= []).push({ id, prs: payload.pr, heads: payload.pr_head, no_code: payload.no_code ?? null }); continue; }
1890
1909
  refreshCardInputs(board, id, { commits: true });
1891
1910
  const moved = tx(board, db => {
1892
1911
  const task = getTask(db, id);
1893
1912
  if (eligible(db) !== true) return null;
1894
1913
  return withCompletionEvidence(board, id, evidence, () => fire(db, board, entry, id, 'request_review', {
1895
- roles: ['platform'], payload: { ...payload, run_id: task.current_run_id, summary: `Opened ${payload.pr.map((url, index) => `${url} at ${payload.pr_head[index]}`).join(', ')}`, ci: evidence }
1914
+ roles: ['platform'], payload: { ...payload, run_id: task.current_run_id, ci: evidence, summary: payload.no_code
1915
+ ? `No code changed: ${payload.no_code.repository} ${payload.branch} holds no commit beyond ${payload.no_code.base} and no PR was opened from it. Review the arc's act from the card: its comments, its tasks' results and what they point at.`
1916
+ : `Opened ${payload.pr.map((url, index) => `${url} at ${payload.pr_head[index]}`).join(', ')}` }
1896
1917
  }));
1897
1918
  });
1898
1919
  if (moved) {
package/board/ztrack.mjs CHANGED
@@ -78,7 +78,7 @@ class ZtrackBoard {
78
78
  this.conn = new DatabaseSync(this.file);
79
79
  this.refuseUnimported(legacy);
80
80
  this.errors = new Map(); this.statements = new Map(); this.hashes = new Map();
81
- this.active = false; this.overlay = false; this.seen = null;
81
+ this.active = false; this.overlay = false; this.seen = null; this.refused = new Set();
82
82
  this.prepareConnection();
83
83
  this.loadErrors();
84
84
  const self = this;
@@ -330,6 +330,7 @@ class ZtrackBoard {
330
330
  this.conn.exec('BEGIN');
331
331
  this.active = true;
332
332
  this.conn.exec('DELETE FROM temp.board_dirty; DELETE FROM temp.board_touched');
333
+ this.refused = new Set();
333
334
  this.loadErrors();
334
335
  this.recover();
335
336
  if (this.seed) this.indexForSeed();
@@ -339,6 +340,7 @@ class ZtrackBoard {
339
340
  }
340
341
  if (hermes && hermes.cursor !== this.get('hermes_cursor', 0)) throw RETRY;
341
342
  if (this.seed) this.seedRows(); else this.lift(hermes);
343
+ this.snapshotHeld();
342
344
  const result = fn(this.db);
343
345
  const files = this.render();
344
346
  this.statement('INSERT INTO main.board_export (name, key) SELECT DISTINCT name, key FROM temp.board_dirty WHERE true ON CONFLICT(name, key) DO UPDATE SET rev = rev + 1').run();
@@ -417,8 +419,9 @@ class ZtrackBoard {
417
419
 
418
420
  // What the edited documents now say, as a plan of rows; applied to the tables by a write, to
419
421
  // connection-local copies by a read. Only the spans the edit touched are parsed.
420
- observe(stale) {
421
- 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() };
422
+ // `held` names cards read again from their indexed span with no edit (see render).
423
+ observe(stale, held = []) {
424
+ 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() };
422
425
  const changed = new Set();
423
426
  for (const doc of this.docs) {
424
427
  const entries = this.entries(doc.role);
@@ -436,9 +439,11 @@ class ZtrackBoard {
436
439
  plan.dropped.push(...replaced.map((e) => e.cid));
437
440
  plan.published[doc.role] = edit.hash;
438
441
  for (const c of [...added, ...replaced]) if (c.id) changed.add(nativeId(c.id));
442
+ for (const c of replaced) if (c.id) plan.before.set(nativeId(c.id), { role: doc.role, entry: c });
439
443
  }
440
444
  const places = new Map();
441
445
  for (const [role, entries] of plan.layout) for (const entry of entries) if (entry.id) places.set(nativeId(entry.id), [...(places.get(nativeId(entry.id)) ?? []), { role, entry }]);
446
+ for (const id of held) { changed.add(id); const at = places.get(id); if (at?.length === 1) plan.before.set(id, at[0]); }
442
447
  for (const id of changed) {
443
448
  plan.checked.add(id);
444
449
  const at = places.get(id) ?? [];
@@ -469,7 +474,8 @@ class ZtrackBoard {
469
474
  const keyed = new Map([...plan.tasks].map(([tid, { key }]) => [key, tid]));
470
475
  const resolveRef = (ref) => keyed.get(ref) ?? this.one('SELECT id FROM main.board_tasks WHERE key = ?', ref)?.id ?? ref;
471
476
  for (const rows of plan.rows.values()) rows.state.blocked_by = JSON.stringify(json(rows.state.blocked_by, []).map((b) => ({ ...b, id: resolveRef(b.id) })));
472
- this.checkControls(plan);
477
+ if (held.length) this.keepMoved(plan);
478
+ this.checkControls(plan, resolveRef);
473
479
  // A refused task line refuses its card's edit: the hand text stays in the document with the reason, never
474
480
  // re-rendered without the line.
475
481
  for (const [id, subtasks] of plan.subtasks) {
@@ -485,6 +491,35 @@ class ZtrackBoard {
485
491
  return plan;
486
492
  }
487
493
 
494
+ // The rows of every card held by a recorded refusal (with its subtasks) before this write's verb runs, so a held
495
+ // card read again after it keeps what the verb moved (keepMoved).
496
+ snapshotHeld() {
497
+ this.heldRows = new Map();
498
+ const cards = new Set();
499
+ for (const id of this.errors.keys()) cards.add(this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of ?? id);
500
+ for (const card of cards) {
501
+ for (const { task_id: id } of [{ task_id: card }, ...this.statement('SELECT task_id FROM main.supercode_cards WHERE subtask_of = ?').all(card)]) {
502
+ this.heldRows.set(id, { task: this.one('SELECT * FROM main.tasks WHERE id = ?', id) ?? null, state: this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id) ?? null });
503
+ }
504
+ }
505
+ }
506
+
507
+ // A held card's span is older than this write: every column the write's verb moved keeps the store's value
508
+ // (title, body, assignee, status, ...); the span's text lands only where the store did not move.
509
+ keepMoved(plan) {
510
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
511
+ for (const [id, rows] of plan.rows) {
512
+ const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
513
+ if (!task) continue;
514
+ const was = this.heldRows?.get(id);
515
+ for (const [side, now, then] of [['task', task, was?.task], ['state', state, was?.state]]) {
516
+ if (!now) continue;
517
+ // A row the write created has nothing held to keep: all of it is the store's.
518
+ for (const key of Object.keys(now)) if (!then || !same(now[key], then[key])) rows[side][key] = now[key];
519
+ }
520
+ }
521
+ }
522
+
488
523
  planRemoval(plan, id) {
489
524
  const task = this.one('SELECT current_run_id FROM main.tasks WHERE id = ?', id);
490
525
  if (!task) return;
@@ -495,7 +530,10 @@ class ZtrackBoard {
495
530
 
496
531
  // A raw edit cannot authenticate its author: the workflow's controlled fields change only through
497
532
  // role-checked verbs. The first import of a document has nothing to compare with.
498
- checkControls(plan) {
533
+ // A controlled value the edit left as the replaced span said it is no edit but a stale line (the store moved
534
+ // while its card was not rendered, as under a recorded refusal): it takes the store's value, and the card's
535
+ // render writes that back into its lines (t_f3f102db).
536
+ checkControls(plan, resolveRef) {
499
537
  if (!this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
500
538
  let fields = [];
501
539
  try { fields = workflowFor(this.root).params.document_control_fields ?? []; } catch { /* no home policy */ }
@@ -504,13 +542,46 @@ class ZtrackBoard {
504
542
  : field === 'blocked_by' ? json(state?.blocked_by, []).map((b) => [b.kind, b.id]).sort()
505
543
  : field === 'paused' ? json(state?.paused, null)
506
544
  : field === 'archived' ? Boolean(state?.archived) : task?.[field] ?? null;
545
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
546
+ // The rows a card's span read as before this edit, with its subtasks', parsed only when a field differs.
547
+ const before = new Map();
548
+ const rowsBefore = (card) => {
549
+ if (before.has(card)) return before.get(card);
550
+ const rows = new Map(); before.set(card, rows);
551
+ const at = plan.before.get(card);
552
+ if (!at) return rows;
553
+ try {
554
+ const { records, errors } = this.codec.inspectBoardDocument(this.texts([at.entry])[0].text, this.docOf(at.role).path);
555
+ if (errors.length || records.length !== 1 || nativeId(records[0].id) !== card) return rows;
556
+ const parsed = this.preset.KanbanRootSchema.parse(this.preset.parseKanban([records[0]])).issues[0];
557
+ rows.set(card, this.cardRows(card, parsed, null));
558
+ for (const [tid, taskCard] of this.taskCards(card, parsed)) rows.set(tid, this.cardRows(tid, taskCard, card));
559
+ for (const r of rows.values()) r.state.blocked_by = JSON.stringify(json(r.state.blocked_by, []).map((b) => ({ ...b, id: resolveRef(b.id) })));
560
+ } catch { rows.clear(); } // an unreadable span says nothing of what the lines said
561
+ return rows;
562
+ };
563
+ const adopt = (rows, task, state, field) => {
564
+ if (field === 'status') { rows.task.status = task.status; rows.state.stage = state?.stage ?? null; }
565
+ else if (field === 'blocked_by') rows.state.blocked_by = state?.blocked_by ?? '[]';
566
+ else if (field === 'paused') rows.state.paused = state?.paused ?? null;
567
+ else if (field === 'archived') rows.state.archived = state?.archived ?? 0;
568
+ else rows.task[field] = task[field] ?? null;
569
+ };
507
570
  for (const [id, rows] of plan.rows) {
508
571
  const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
509
572
  // A new display author is not an authenticated creator.
510
- const forbidden = fields.filter((field) => (task || field !== 'created_by') && JSON.stringify(value(rows.task, rows.state, field)) !== JSON.stringify(value(task, state, field)));
573
+ const differ = fields.filter((field) => (task || field !== 'created_by') && !same(value(rows.task, rows.state, field), value(task, state, field)));
574
+ const prior = task && differ.length ? rowsBefore(rows.state.subtask_of ?? id).get(id) : null;
575
+ const stale = prior ? differ.filter((field) => same(value(prior.task, prior.state, field), value(rows.task, rows.state, field))) : [];
576
+ for (const field of stale) adopt(rows, task, state, field);
577
+ const forbidden = differ.filter((field) => !stale.includes(field));
511
578
  if (forbidden.length) plan.errors.set(id, `Document edit cannot change ${forbidden.join(', ')}; use this card's role-checked workflow verbs or request-state.`);
512
579
  }
513
580
  for (const id of plan.remove) {
581
+ // A task line already absent from the replaced span was not removed by this edit: the store keeps it.
582
+ const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
583
+ const prior = parent && !plan.remove.has(parent) ? rowsBefore(parent) : null;
584
+ if (prior?.size && !prior.has(id)) { plan.remove.delete(id); plan.checked.add(id); continue; }
514
585
  plan.errors.set(id, "Document edit cannot change card removal; use this card's role-checked workflow verbs or request-state.");
515
586
  }
516
587
  for (const id of plan.errors.keys()) plan.remove.delete(id);
@@ -553,6 +624,7 @@ class ZtrackBoard {
553
624
  for (const id of plan.checked) this.errors.delete(id);
554
625
  for (const [id, reason] of plan.errors) this.errors.set(id, reason);
555
626
  if (overlay) return;
627
+ 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);
556
628
  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);
557
629
  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);
558
630
  for (const [id, reason] of plan.errors) this.statement('INSERT OR REPLACE INTO main.board_errors VALUES (?, ?)').run(id, reason);
@@ -629,21 +701,30 @@ class ZtrackBoard {
629
701
  // into their documents; a card whose record is unchanged keeps its exact bytes. Answers the
630
702
  // documents to publish.
631
703
  render() {
632
- const touched = new Set(this.statement('SELECT DISTINCT id FROM temp.board_touched WHERE id IS NOT NULL').all().map((r) => r.id));
633
- if (!touched.size) return [];
704
+ const cardsTouched = () => {
705
+ const parents = new Set();
706
+ for (const { id } of this.statement('SELECT DISTINCT id FROM temp.board_touched WHERE id IS NOT NULL').all()) {
707
+ const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
708
+ const key = parent ? null : this.one('SELECT key FROM main.board_tasks WHERE id = ?', id)?.key;
709
+ parents.add(parent ?? (key && !this.one('SELECT 1 FROM main.tasks WHERE id = ?', id) ? key.slice(0, key.lastIndexOf(':')) : id));
710
+ }
711
+ return parents;
712
+ };
713
+ let parents = cardsTouched();
714
+ if (!parents.size) return [];
715
+ 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);
716
+ // A card held by a recorded refusal is not rendered, so the store's verbs move its tasks past its lines. When
717
+ // the store moves it, its span is read again: what the store moved past is stale and takes the store's value
718
+ // (checkControls), and the card renders. A card refused by this write's own edit stays as it was written.
719
+ const held = [...parents].filter((id) => !this.refused.has(id) && heldBy(id));
720
+ if (held.length) { this.applyPlan(this.observe([], held), { overlay: false }); parents = cardsTouched(); }
721
+ if (this.seedIds) for (const id of this.seedIds) parents.add(id);
634
722
  const layout = { open: this.entries('open'), archived: this.entries('archived') };
635
723
  const where = new Map();
636
724
  for (const role of ['open', 'archived']) layout[role].forEach((entry, index) => { if (entry.id) where.set(nativeId(entry.id), { role, index, entry }); });
637
- const parents = new Set();
638
- for (const id of touched) {
639
- const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
640
- const key = parent ? null : this.one('SELECT key FROM main.board_tasks WHERE id = ?', id)?.key;
641
- parents.add(parent ?? (key && !this.one('SELECT 1 FROM main.tasks WHERE id = ?', id) ? key.slice(0, key.lastIndexOf(':')) : id));
642
- }
643
- if (this.seedIds) for (const id of this.seedIds) parents.add(id);
644
725
  const edits = { open: [], archived: [] };
645
726
  for (const id of parents) {
646
- if (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)) continue;
727
+ if (heldBy(id)) continue;
647
728
  const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id);
648
729
  const state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id) ?? {};
649
730
  if (task && state.subtask_of) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.154",
3
+ "version": "0.4.156",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {