instar 0.28.94 → 0.28.96

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 (59) hide show
  1. package/dashboard/index.html +249 -2
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +163 -1
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/core/MachineHeartbeat.d.ts +94 -0
  6. package/dist/core/MachineHeartbeat.d.ts.map +1 -0
  7. package/dist/core/MachineHeartbeat.js +155 -0
  8. package/dist/core/MachineHeartbeat.js.map +1 -0
  9. package/dist/core/ProjectAutoAdvancePoller.d.ts +107 -0
  10. package/dist/core/ProjectAutoAdvancePoller.d.ts.map +1 -0
  11. package/dist/core/ProjectAutoAdvancePoller.js +176 -0
  12. package/dist/core/ProjectAutoAdvancePoller.js.map +1 -0
  13. package/dist/core/ProjectRoundCompleteMessage.d.ts +158 -0
  14. package/dist/core/ProjectRoundCompleteMessage.d.ts.map +1 -0
  15. package/dist/core/ProjectRoundCompleteMessage.js +253 -0
  16. package/dist/core/ProjectRoundCompleteMessage.js.map +1 -0
  17. package/dist/core/ProjectRoundExecution.d.ts +111 -0
  18. package/dist/core/ProjectRoundExecution.d.ts.map +1 -0
  19. package/dist/core/ProjectRoundExecution.js +359 -0
  20. package/dist/core/ProjectRoundExecution.js.map +1 -0
  21. package/dist/core/ProjectRoundLock.d.ts +67 -0
  22. package/dist/core/ProjectRoundLock.d.ts.map +1 -0
  23. package/dist/core/ProjectRoundLock.js +161 -0
  24. package/dist/core/ProjectRoundLock.js.map +1 -0
  25. package/dist/core/ProjectRoundRunner.d.ts +146 -0
  26. package/dist/core/ProjectRoundRunner.d.ts.map +1 -0
  27. package/dist/core/ProjectRoundRunner.js +371 -0
  28. package/dist/core/ProjectRoundRunner.js.map +1 -0
  29. package/dist/core/ProjectRoundWorktrees.d.ts +57 -0
  30. package/dist/core/ProjectRoundWorktrees.d.ts.map +1 -0
  31. package/dist/core/ProjectRoundWorktrees.js +111 -0
  32. package/dist/core/ProjectRoundWorktrees.js.map +1 -0
  33. package/dist/monitoring/CommitmentTracker.d.ts +34 -0
  34. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  35. package/dist/monitoring/CommitmentTracker.js +28 -0
  36. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  37. package/dist/monitoring/PromiseBeacon.d.ts +15 -0
  38. package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
  39. package/dist/monitoring/PromiseBeacon.js +86 -6
  40. package/dist/monitoring/PromiseBeacon.js.map +1 -1
  41. package/dist/server/AgentServer.d.ts +12 -0
  42. package/dist/server/AgentServer.d.ts.map +1 -1
  43. package/dist/server/AgentServer.js +3 -0
  44. package/dist/server/AgentServer.js.map +1 -1
  45. package/dist/server/routes.d.ts +19 -0
  46. package/dist/server/routes.d.ts.map +1 -1
  47. package/dist/server/routes.js +610 -7
  48. package/dist/server/routes.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/data/builtin-manifest.json +47 -47
  51. package/upgrades/0.28.95.md +55 -0
  52. package/upgrades/0.28.96.md +347 -0
  53. package/upgrades/side-effects/project-scope-phase1b-connect-the-dots.md +184 -0
  54. package/upgrades/side-effects/project-scope-phase1b-pr3.md +228 -0
  55. package/upgrades/side-effects/project-scope-phase1b-pr4.md +198 -0
  56. package/upgrades/side-effects/project-scope-phase1b-pr5.md +150 -0
  57. package/upgrades/side-effects/project-scope-phase1b-pr6.md +136 -0
  58. package/upgrades/side-effects/project-scope-phase1b-pr7.md +175 -0
  59. package/upgrades/side-effects/promise-beacon-ux-fixes.md +146 -0
@@ -2523,6 +2523,7 @@
2523
2523
  <button class="tab" data-tab="systems" onclick="switchTab('systems')">Health</button>
2524
2524
  <button class="tab" data-tab="integrated-being" onclick="switchTab('integrated-being')">Integrated-Being</button>
2525
2525
  <button class="tab" data-tab="pr-pipeline" onclick="switchTab('pr-pipeline')">PR Pipeline</button>
2526
+ <button class="tab" data-tab="projects" onclick="switchTab('projects')">Projects <span class="tab-count" id="tabProjectCount">0</span></button>
2526
2527
  <button class="tab" data-tab="initiatives" onclick="switchTab('initiatives')">Initiatives <span class="tab-count" id="tabInitiativeCount">0</span></button>
2527
2528
  <button class="tab" data-tab="commitments" onclick="switchTab('commitments')">Commitments <span class="tab-count" id="tabCommitmentCount">0</span></button>
2528
2529
  <button class="tab" data-tab="tokens" onclick="switchTab('tokens')">Tokens</button>
@@ -2892,6 +2893,21 @@
2892
2893
  <div id="prPipelineList" style="display:flex;flex-direction:column;gap:12px"></div>
2893
2894
  </div>
2894
2895
 
2896
+ <!-- Projects Tab — PROJECT-SCOPE-SPEC Phase 1.10 read-only view -->
2897
+ <div id="projectsPanel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
2898
+ <div style="display:flex;justify-content:space-between;align-items:center">
2899
+ <h2 style="margin:0">Projects</h2>
2900
+ <button onclick="loadProjects()" style="padding:6px 12px">Refresh</button>
2901
+ </div>
2902
+ <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
2903
+ Multi-spec project plans. Each project bundles related child initiatives across rounds with structurally-gated round advance.
2904
+ </div>
2905
+ <div id="projectsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
2906
+ No active projects. Create one via <code>POST /projects</code> with a plan-doc path.
2907
+ </div>
2908
+ <div id="projectsList" style="display:flex;flex-direction:column;gap:12px"></div>
2909
+ </div>
2910
+
2895
2911
  <!-- Initiatives Tab — multi-phase long-running work -->
2896
2912
  <div id="initiativesPanel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
2897
2913
  <div style="display:flex;justify-content:space-between;align-items:center">
@@ -2904,6 +2920,9 @@
2904
2920
  <option value="archived">Archived</option>
2905
2921
  <option value="abandoned">Abandoned</option>
2906
2922
  </select>
2923
+ <label style="font-size:12px;color:var(--text-dim);display:flex;align-items:center;gap:4px">
2924
+ <input type="checkbox" id="initiativesShowAll" onchange="loadInitiatives()"> Show projects + children
2925
+ </label>
2907
2926
  <button onclick="loadInitiatives()" style="padding:6px 12px">Refresh</button>
2908
2927
  </div>
2909
2928
  </div>
@@ -4185,6 +4204,13 @@
4185
4204
  display: ['flex'],
4186
4205
  onActivate: () => { if (typeof loadPrPipeline === 'function') loadPrPipeline(); },
4187
4206
  },
4207
+ {
4208
+ id: 'projects',
4209
+ panels: ['projectsPanel'],
4210
+ display: ['flex'],
4211
+ onActivate: () => { if (typeof loadProjects === 'function') loadProjects(); if (typeof startProjectsPoll === 'function') startProjectsPoll(); },
4212
+ onDeactivate: () => { if (typeof stopProjectsPoll === 'function') stopProjectsPoll(); },
4213
+ },
4188
4214
  {
4189
4215
  id: 'initiatives',
4190
4216
  panels: ['initiativesPanel'],
@@ -6682,6 +6708,216 @@
6682
6708
  }
6683
6709
  }
6684
6710
 
6711
+ // ── Projects Tab (PROJECT-SCOPE-SPEC Phase 1.10) ─────────────
6712
+ //
6713
+ // Read-only project view. Per spec: progress bar per round +
6714
+ // pipelineStage histogram, drift status badge per pending item
6715
+ // (cached verdict only; clicking does NOT trigger a new check),
6716
+ // halt + ack buttons, 15s poll, textContent rendering on every
6717
+ // user/agent-authored string, 409 response silently refreshes.
6718
+ let __projectsPollTimer = null;
6719
+ function startProjectsPoll() {
6720
+ if (__projectsPollTimer) return;
6721
+ __projectsPollTimer = setInterval(() => { loadProjects().catch(() => {}); }, 15000);
6722
+ }
6723
+ function stopProjectsPoll() {
6724
+ if (__projectsPollTimer) { clearInterval(__projectsPollTimer); __projectsPollTimer = null; }
6725
+ }
6726
+
6727
+ function escapeText(s) {
6728
+ // textContent assignment handles XSS for us; this is a no-op
6729
+ // helper that documents the intent for future authors.
6730
+ return typeof s === 'string' ? s : '';
6731
+ }
6732
+
6733
+ async function loadProjects() {
6734
+ const list = document.getElementById('projectsList');
6735
+ const empty = document.getElementById('projectsEmpty');
6736
+ const countBadge = document.getElementById('tabProjectCount');
6737
+ if (!list || !empty) return;
6738
+
6739
+ let projRes = null;
6740
+ try {
6741
+ projRes = await apiFetch('/projects').catch(() => null);
6742
+ } catch { /* handled by null check below */ }
6743
+
6744
+ while (list.firstChild) list.removeChild(list.firstChild);
6745
+ empty.style.display = 'none';
6746
+
6747
+ if (!projRes || !Array.isArray(projRes.items)) {
6748
+ empty.style.display = 'block';
6749
+ empty.textContent = 'Projects endpoint unavailable.';
6750
+ if (countBadge) countBadge.textContent = '0';
6751
+ return;
6752
+ }
6753
+
6754
+ const items = projRes.items.filter((p) => p.status === 'active');
6755
+ if (countBadge) countBadge.textContent = String(items.length);
6756
+ if (items.length === 0) {
6757
+ empty.style.display = 'block';
6758
+ return;
6759
+ }
6760
+
6761
+ // We need each project's children for the progress bar; fetch
6762
+ // them in parallel via /projects/:id (server returns children
6763
+ // alongside the project record). Cap at 25 to avoid a flood.
6764
+ const detailed = await Promise.all(items.slice(0, 25).map((p) =>
6765
+ apiFetch('/projects/' + encodeURIComponent(p.id) + '?reconcile=false').catch(() => null)
6766
+ ));
6767
+
6768
+ for (const detail of detailed) {
6769
+ if (!detail || !detail.project) continue;
6770
+ list.appendChild(renderProjectCard(detail.project, detail.children || []));
6771
+ }
6772
+ }
6773
+
6774
+ function renderProjectCard(project, children) {
6775
+ const card = document.createElement('div');
6776
+ card.style.padding = '14px';
6777
+ card.style.border = '1px solid var(--border)';
6778
+ card.style.borderRadius = '6px';
6779
+ card.style.background = 'var(--bg-dim)';
6780
+
6781
+ const titleRow = document.createElement('div');
6782
+ titleRow.style.display = 'flex';
6783
+ titleRow.style.justifyContent = 'space-between';
6784
+ titleRow.style.alignItems = 'baseline';
6785
+ titleRow.style.gap = '12px';
6786
+
6787
+ const title = document.createElement('div');
6788
+ title.style.fontWeight = '600';
6789
+ title.style.fontSize = '15px';
6790
+ title.textContent = project.title || project.id;
6791
+ titleRow.appendChild(title);
6792
+
6793
+ const idEl = document.createElement('div');
6794
+ idEl.style.fontSize = '11px';
6795
+ idEl.style.color = 'var(--text-dim)';
6796
+ idEl.style.fontFamily = 'monospace';
6797
+ idEl.textContent = project.id + ' v' + project.version;
6798
+ titleRow.appendChild(idEl);
6799
+ card.appendChild(titleRow);
6800
+
6801
+ // Drift status badge.
6802
+ const drift = project.rounds && project.rounds[0] ? project.rounds[0].lastDriftVerdict : null;
6803
+ if (drift && drift.verdict) {
6804
+ const badge = document.createElement('span');
6805
+ badge.style.display = 'inline-block';
6806
+ badge.style.marginTop = '4px';
6807
+ badge.style.padding = '2px 8px';
6808
+ badge.style.fontSize = '11px';
6809
+ badge.style.borderRadius = '10px';
6810
+ badge.style.border = '1px solid var(--border)';
6811
+ const colors = {
6812
+ 'no-drift': '#4a9a4a',
6813
+ 'minor-drift': '#e2b94a',
6814
+ 'premise-violated': '#d65f5f',
6815
+ 'manual-review-required': '#888',
6816
+ };
6817
+ badge.style.color = colors[drift.verdict] || '#888';
6818
+ badge.textContent = 'drift: ' + drift.verdict;
6819
+ card.appendChild(badge);
6820
+ }
6821
+
6822
+ // Rounds progress bars.
6823
+ const rounds = Array.isArray(project.rounds) ? project.rounds : [];
6824
+ const childById = new Map(children.map((c) => [c.id, c]));
6825
+ for (let i = 0; i < rounds.length; i++) {
6826
+ const r = rounds[i];
6827
+ const wrap = document.createElement('div');
6828
+ wrap.style.marginTop = '10px';
6829
+
6830
+ const rTitle = document.createElement('div');
6831
+ rTitle.style.fontSize = '12px';
6832
+ rTitle.style.color = 'var(--text-dim)';
6833
+ rTitle.textContent = `Round ${i}: ${r.name} — ${r.status || 'pending'}`;
6834
+ wrap.appendChild(rTitle);
6835
+
6836
+ // pipelineStage histogram counts.
6837
+ const stages = {};
6838
+ let total = 0;
6839
+ for (const id of (r.itemIds || [])) {
6840
+ const child = childById.get(id);
6841
+ const stage = (child && child.pipelineStage) || 'outline';
6842
+ stages[stage] = (stages[stage] || 0) + 1;
6843
+ total++;
6844
+ }
6845
+ const merged = stages['merged'] || 0;
6846
+ const bar = document.createElement('div');
6847
+ bar.style.marginTop = '4px';
6848
+ bar.style.height = '6px';
6849
+ bar.style.borderRadius = '3px';
6850
+ bar.style.background = 'var(--bg)';
6851
+ bar.style.overflow = 'hidden';
6852
+ const fill = document.createElement('div');
6853
+ fill.style.height = '100%';
6854
+ fill.style.background = '#4a9a4a';
6855
+ fill.style.width = total > 0 ? `${Math.round((merged / total) * 100)}%` : '0%';
6856
+ bar.appendChild(fill);
6857
+ wrap.appendChild(bar);
6858
+
6859
+ const histo = document.createElement('div');
6860
+ histo.style.fontSize = '11px';
6861
+ histo.style.color = 'var(--text-dim)';
6862
+ histo.style.marginTop = '4px';
6863
+ histo.textContent = Object.entries(stages).map(([k, v]) => `${k}: ${v}`).join(' • ') || 'no items';
6864
+ wrap.appendChild(histo);
6865
+ card.appendChild(wrap);
6866
+ }
6867
+
6868
+ // Halt + ack buttons.
6869
+ const actions = document.createElement('div');
6870
+ actions.style.display = 'flex';
6871
+ actions.style.gap = '8px';
6872
+ actions.style.marginTop = '12px';
6873
+
6874
+ const haltBtn = document.createElement('button');
6875
+ haltBtn.textContent = 'Halt';
6876
+ haltBtn.style.padding = '4px 10px';
6877
+ haltBtn.style.fontSize = '12px';
6878
+ haltBtn.addEventListener('click', () => projectHalt(project.id));
6879
+ actions.appendChild(haltBtn);
6880
+
6881
+ const ackBtn = document.createElement('button');
6882
+ ackBtn.textContent = 'Ack round 0';
6883
+ ackBtn.style.padding = '4px 10px';
6884
+ ackBtn.style.fontSize = '12px';
6885
+ ackBtn.addEventListener('click', () => projectAck(project.id, 0));
6886
+ actions.appendChild(ackBtn);
6887
+ card.appendChild(actions);
6888
+
6889
+ return card;
6890
+ }
6891
+
6892
+ async function projectHalt(projectId) {
6893
+ try {
6894
+ const res = await apiFetch('/projects/' + encodeURIComponent(projectId) + '/halt', {
6895
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
6896
+ body: JSON.stringify({ reason: 'user-halt-from-dashboard' }),
6897
+ });
6898
+ // 409 from a stale read → silent refresh, no error toast.
6899
+ if (res && res.error && /version mismatch/.test(res.error)) {
6900
+ await loadProjects();
6901
+ return;
6902
+ }
6903
+ await loadProjects();
6904
+ } catch { /* ignore — refresh on next poll */ }
6905
+ }
6906
+
6907
+ async function projectAck(projectId, forRoundIndex) {
6908
+ try {
6909
+ const res = await apiFetch('/projects/' + encodeURIComponent(projectId) + '/ack', {
6910
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
6911
+ body: JSON.stringify({ forRoundIndex }),
6912
+ });
6913
+ if (res && res.error && /version mismatch/.test(res.error)) {
6914
+ await loadProjects();
6915
+ return;
6916
+ }
6917
+ await loadProjects();
6918
+ } catch { /* ignore — refresh on next poll */ }
6919
+ }
6920
+
6685
6921
  // ── Initiatives Tab (INITIATIVE-TRACKER-SPEC v1) ─────────────
6686
6922
  //
6687
6923
  // Fetches /initiatives and /initiatives/digest on panel activation.
@@ -6703,9 +6939,20 @@
6703
6939
  let itemsRes = null;
6704
6940
  let digestRes = null;
6705
6941
  try {
6706
- const statusQ = filter && filter.value ? `?status=${encodeURIComponent(filter.value)}` : '';
6942
+ // PROJECT-SCOPE-SPEC Phase 1.10 — by default hide project-kind
6943
+ // records (shown in the Projects tab) and parented children
6944
+ // (shown inside their parent's card). Server-side filter so
6945
+ // we don't ship them over the wire.
6946
+ const showAll = document.getElementById('initiativesShowAll');
6947
+ const params = [];
6948
+ if (filter && filter.value) params.push(`status=${encodeURIComponent(filter.value)}`);
6949
+ if (!showAll || !showAll.checked) {
6950
+ params.push('excludeKind=project');
6951
+ params.push('excludeParented=true');
6952
+ }
6953
+ const q = params.length ? '?' + params.join('&') : '';
6707
6954
  const [a, b] = await Promise.all([
6708
- apiFetch('/initiatives' + statusQ).catch(() => null),
6955
+ apiFetch('/initiatives' + q).catch(() => null),
6709
6956
  apiFetch('/initiatives/digest').catch(() => null),
6710
6957
  ]);
6711
6958
  itemsRes = a;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkQH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAw4CD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAs9JtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkQH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAw4CD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAknKtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -4549,6 +4549,79 @@ export async function startServer(options) {
4549
4549
  });
4550
4550
  promiseBeacon.start();
4551
4551
  globalThis.__instarPromiseBeacon = promiseBeacon;
4552
+ // ── "keep watching" resume detector ─────────────────────────────
4553
+ // When a user replies on a topic that has any auto-paused beacons,
4554
+ // a literal "keep watching" match resumes them. Brittle detector
4555
+ // (regex) with no blocking authority — it only triggers the
4556
+ // structurally-symmetric /resume endpoint. False-positive cost:
4557
+ // one extra heartbeat that re-pauses on next idle cycle.
4558
+ const KEEP_WATCHING_RE = /\bkeep[\s-]?watching\b/i;
4559
+ const previousTelegramHook = telegram.onMessageLogged;
4560
+ telegram.onMessageLogged = (entry) => {
4561
+ if (previousTelegramHook)
4562
+ previousTelegramHook(entry);
4563
+ if (!entry.fromUser)
4564
+ return;
4565
+ if (!entry.topicId)
4566
+ return;
4567
+ if (!entry.text || !KEEP_WATCHING_RE.test(entry.text))
4568
+ return;
4569
+ const topicId = entry.topicId;
4570
+ const paused = commitmentTracker
4571
+ .getActive()
4572
+ .filter(c => c.topicId === topicId && c.beaconPaused);
4573
+ if (paused.length === 0)
4574
+ return;
4575
+ (async () => {
4576
+ let resumed = 0;
4577
+ for (const c of paused) {
4578
+ try {
4579
+ const url = `http://localhost:${config.port}/commitments/${c.id}/resume`;
4580
+ const resp = await fetch(url, {
4581
+ method: 'POST',
4582
+ headers: {
4583
+ 'Content-Type': 'application/json',
4584
+ 'Authorization': `Bearer ${config.authToken}`,
4585
+ },
4586
+ });
4587
+ if (resp.ok)
4588
+ resumed += 1;
4589
+ else
4590
+ console.warn(`[PromiseBeacon] resume call returned ${resp.status} for ${c.id}`);
4591
+ }
4592
+ catch (err) {
4593
+ console.warn(`[PromiseBeacon] resume call failed for ${c.id}:`, err.message);
4594
+ }
4595
+ }
4596
+ let ackText;
4597
+ if (resumed === paused.length && resumed > 0) {
4598
+ ackText = `⏳ resumed ${resumed === 1 ? 'watcher' : `${resumed} watchers`} on this topic.`;
4599
+ }
4600
+ else if (resumed > 0) {
4601
+ ackText = `⏳ resumed ${resumed} of ${paused.length} watchers — ${paused.length - resumed} didn't resume. Try again or open the dashboard.`;
4602
+ }
4603
+ else {
4604
+ ackText = `⚠️ couldn't resume the watcher${paused.length === 1 ? '' : 's'} on this topic — try again or open the dashboard.`;
4605
+ }
4606
+ try {
4607
+ const ackUrl = `http://localhost:${config.port}/telegram/reply/${topicId}`;
4608
+ await fetch(ackUrl, {
4609
+ method: 'POST',
4610
+ headers: {
4611
+ 'Content-Type': 'application/json',
4612
+ 'Authorization': `Bearer ${config.authToken}`,
4613
+ },
4614
+ body: JSON.stringify({
4615
+ text: ackText,
4616
+ metadata: { source: 'promise-beacon', isProxy: true, tier: 1 },
4617
+ }),
4618
+ });
4619
+ }
4620
+ catch (err) {
4621
+ console.warn('[PromiseBeacon] resume ack send failed:', err.message);
4622
+ }
4623
+ })();
4624
+ };
4552
4625
  }
4553
4626
  catch (err) {
4554
4627
  console.warn('[PromiseBeacon] init failed:', err.message);
@@ -5787,6 +5860,95 @@ export async function startServer(options) {
5787
5860
  const projectDigestCache = new ProjectDigestCache(config.stateDir, initiativeTracker);
5788
5861
  initiativeTracker.setDigestCacheInvalidator(() => projectDigestCache.writeDigestCache());
5789
5862
  projectDigestCache.writeDigestCache();
5863
+ // Project-scope Phase 1b PR 3 — round runner (single chokepoint for
5864
+ // /advance, /halt, /ack, /accept-partial; lock-protected; future
5865
+ // autonomous-delegating run loop).
5866
+ const machineIdForProjects = coordinator.identity?.machineId ?? os.hostname();
5867
+ const { ProjectRoundRunner } = await import('../core/ProjectRoundRunner.js');
5868
+ const projectRoundRunner = new ProjectRoundRunner({
5869
+ tracker: initiativeTracker,
5870
+ stateDir: config.stateDir,
5871
+ machineId: machineIdForProjects,
5872
+ });
5873
+ // Project-scope Phase 1b PR 4 — machine heartbeat + auto-advance poller.
5874
+ // Heartbeat writes .instar/machine-health/<machineId>.json every 30 min
5875
+ // (git-synced); the claim-ownership endpoint queries it for the >48h
5876
+ // staleness check that spec § P5 requires. Auto-advance poller scans
5877
+ // for project rounds whose autoAdvanceAt has elapsed and bookkeeps the
5878
+ // next round.
5879
+ const { MachineHeartbeat } = await import('../core/MachineHeartbeat.js');
5880
+ const machineHeartbeatApi = new MachineHeartbeat({
5881
+ stateDir: config.stateDir,
5882
+ machineId: machineIdForProjects,
5883
+ });
5884
+ machineHeartbeatApi.start();
5885
+ const machineHeartbeat = { api: machineHeartbeatApi, config: { machineId: machineIdForProjects } };
5886
+ // Wire the run-round executor: fire-and-forget launch of
5887
+ // ProjectRoundExecution.runRound() after a successful auto-advance.
5888
+ // The executor itself acquires the lock + spawns + polls + cleans up.
5889
+ // Errors are surfaced via the poller's result.executorErrors and
5890
+ // logged to stderr; they don't take the server down.
5891
+ const { runRound } = await import('../core/ProjectRoundExecution.js');
5892
+ const { ProjectAutoAdvancePoller } = await import('../core/ProjectAutoAdvancePoller.js');
5893
+ const projectAutoAdvancePoller = new ProjectAutoAdvancePoller({
5894
+ tracker: initiativeTracker,
5895
+ runner: projectRoundRunner,
5896
+ machineId: machineIdForProjects,
5897
+ executor: async ({ projectId, roundIndex }) => {
5898
+ const proj = initiativeTracker.get(projectId);
5899
+ if (!proj || !proj.targetRepoPath)
5900
+ return;
5901
+ await runRound({
5902
+ tracker: initiativeTracker,
5903
+ projectId,
5904
+ roundIndex,
5905
+ targetRepoPath: proj.targetRepoPath,
5906
+ }, { stateDir: config.stateDir });
5907
+ },
5908
+ });
5909
+ // Tick once a minute; .unref() so the timer never keeps the process alive.
5910
+ const projectAutoAdvanceTimer = setInterval(() => {
5911
+ projectAutoAdvancePoller.tick().catch((err) => {
5912
+ console.error('[ProjectAutoAdvancePoller] tick error:', err);
5913
+ });
5914
+ }, 60_000);
5915
+ if (typeof projectAutoAdvanceTimer.unref === 'function')
5916
+ projectAutoAdvanceTimer.unref();
5917
+ // Post-restore reconciler — downgrade any round still flagged
5918
+ // in-progress to pending. The previous owner may have crashed or
5919
+ // migrated, and no TaskFlow exists yet to detect an actually-live
5920
+ // run. One-shot at startup.
5921
+ try {
5922
+ for (const proj of initiativeTracker.list({ kind: 'project', status: 'active' })) {
5923
+ const rounds = proj.rounds ?? [];
5924
+ let any = false;
5925
+ const nextRounds = rounds.map((r) => {
5926
+ if (r.status === 'in-progress') {
5927
+ any = true;
5928
+ return { ...r, status: 'pending' };
5929
+ }
5930
+ return r;
5931
+ });
5932
+ if (any) {
5933
+ await initiativeTracker.update(proj.id, {
5934
+ rounds: nextRounds,
5935
+ ifMatch: proj.version,
5936
+ }).catch(() => { });
5937
+ }
5938
+ }
5939
+ }
5940
+ catch (err) {
5941
+ console.error('[ProjectAutoAdvancePoller] post-restore reconciler error:', err);
5942
+ }
5943
+ // Project drift checker — used by POST /projects/:id/drift-check.
5944
+ // Reuses the shared IntelligenceProvider. Cache + ledger are optional
5945
+ // and not wired here yet (the dashboard caller can pass per-call
5946
+ // overrides); a follow-up can wire ProjectDriftCheckerCache + the
5947
+ // DriftSpendLedger once spend telemetry is needed in prod.
5948
+ const { ProjectDriftChecker } = await import('../core/ProjectDriftChecker.js');
5949
+ const projectDriftChecker = new ProjectDriftChecker({
5950
+ intelligence: sharedIntelligence,
5951
+ });
5790
5952
  // TaskFlow registry — opt-in via config.taskFlow.enabled (default: off in v1).
5791
5953
  // Owns its own SQLite file under .instar/task-flows.db. The maintenance
5792
5954
  // sweeper and due-waker start with the registry; both .unref() their timers
@@ -5891,7 +6053,7 @@ export async function startServer(options) {
5891
6053
  },
5892
6054
  });
5893
6055
  }
5894
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, proxyCoordinator, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, workingMemory, taskFlowRegistry, threadlineFlowBridge });
6056
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, proxyCoordinator, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, workingMemory, taskFlowRegistry, threadlineFlowBridge });
5895
6057
  await server.start();
5896
6058
  void taskFlowSweeper;
5897
6059
  void taskFlowDueWaker;