@njeybe/nativ 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/setup/pm-playbook.d.ts +1 -1
  3. package/dist/core/setup/pm-playbook.js +1 -1
  4. package/dist/server/studio-ui/pages/benchmarks/script.js +9 -8
  5. package/dist/server/studio-ui/pages/benchmarks/styles.js +3 -1
  6. package/dist/server/studio-ui/pages/database/browser.js +1 -1
  7. package/dist/server/studio-ui/pages/database/connection.js +5 -4
  8. package/dist/server/studio-ui/pages/database/data-shell.js +8 -13
  9. package/dist/server/studio-ui/pages/database/drift.js +1 -1
  10. package/dist/server/studio-ui/pages/database/events.js +1 -2
  11. package/dist/server/studio-ui/pages/database/explorer.js +4 -4
  12. package/dist/server/studio-ui/pages/flow/layout.js +1 -1
  13. package/dist/server/studio-ui/pages/flow/styles.js +1 -1
  14. package/dist/server/studio-ui/pages/flow/timeline-view.js +1 -1
  15. package/dist/server/studio-ui/pages/home/script.js +26 -29
  16. package/dist/server/studio-ui/pages/home/styles.js +2 -6
  17. package/dist/server/studio-ui/pages/tasks/script.js +25 -40
  18. package/dist/server/studio-ui/pages/tasks/styles.js +9 -9
  19. package/dist/server/studio-ui/pages/team/script.js +27 -8
  20. package/dist/server/studio-ui/pages/team/styles.js +0 -1
  21. package/dist/server/studio-ui/shell/console.js +13 -7
  22. package/dist/server/studio-ui/shell/detail-view.js +3 -3
  23. package/dist/server/studio-ui/shell/dispatch.js +9 -8
  24. package/dist/server/studio-ui/shell/events-tools.js +11 -0
  25. package/dist/server/studio-ui/shell/format.js +31 -2
  26. package/dist/server/studio-ui/shell/markup-head.js +1 -1
  27. package/dist/server/studio-ui/shell/markup-tools.js +13 -8
  28. package/dist/server/studio-ui/shell/model.js +8 -2
  29. package/dist/server/studio-ui/shell/paint.js +1 -3
  30. package/dist/server/studio-ui/shell/pipeline-state.js +2 -11
  31. package/dist/server/studio-ui/shell/pulse.js +8 -7
  32. package/dist/server/studio-ui/shell/sidebar-view.js +2 -3
  33. package/dist/server/studio-ui/shell/triage.js +1 -1
  34. package/dist/server/studio-ui/styles/console.js +3 -0
  35. package/dist/server/studio-ui/styles/shell.js +4 -6
  36. package/package.json +1 -1
  37. package/plugin/.claude-plugin/plugin.json +1 -1
  38. package/plugin/agents/architect.md +2 -2
  39. package/plugin/agents/verifier.md +4 -1
  40. package/plugin/skills/nativ/SKILL.md +7 -7
  41. package/templates/AGENTS.md +7 -7
  42. package/templates/GEMINI.md +1 -1
  43. package/templates/claude-agents/architect.md +2 -2
  44. package/templates/claude-agents/verifier.md +4 -1
  45. package/templates/dot-ai/subagents/architect.md +2 -2
@@ -3,7 +3,7 @@ export const tasksCss = String.raw `#panel-tasks .board { display: grid; grid-te
3
3
  #panel-tasks .col { background: var(--surface-2); border-radius: 6px; padding: 8px; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
4
4
  #panel-tasks .col-head { display: flex; align-items: center; gap: 8px; padding: 4px 6px 2px; font-weight: 500; font-size: 13px; }
5
5
  #panel-tasks .col-head .count { margin-left: auto; background: var(--surface); border-radius: 999px; padding: 0 8px; font-size: 12px; color: var(--fg-2); }
6
- #panel-tasks .col-empty { font-size: 12.5px; color: var(--fg-3); padding: 14px 8px; text-align: center; border: 1px dashed var(--line-2); border-radius: 6px; }
6
+ #panel-tasks .col-empty { margin: 0; font-size: 12.5px; color: var(--fg-3); padding: 4px 2px; }
7
7
  #panel-tasks .tcard { display: flex; flex-direction: column; gap: 6px; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding: 10px; text-align: left; width: 100%; cursor: pointer; }
8
8
  #panel-tasks .tcard:hover { border-color: var(--line-2); }
9
9
  #panel-tasks .tcard.in_progress { border-color: color-mix(in srgb, var(--run) 45%, var(--line)); }
@@ -51,24 +51,24 @@ export const tasksCss = String.raw `#panel-tasks .board { display: grid; grid-te
51
51
  #panel-tasks .tg-head[aria-expanded="true"] .chev { transform: rotate(90deg); }
52
52
  #panel-tasks .trow {
53
53
  display: grid; grid-template-columns: 76px minmax(0, 1fr) 110px 150px 72px; gap: 12px;
54
- align-items: center; height: 36px; padding: 0 8px; border-bottom: 1px solid var(--line);
54
+ align-items: center; height: 36px; padding: 0 8px;
55
55
  font-size: 13px; cursor: pointer;
56
56
  }
57
57
  #panel-tasks .trow:hover, #panel-tasks .trow:focus-visible { background: var(--surface-2); }
58
58
  #panel-tasks .trow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
59
- #panel-tasks .trow.thead { cursor: default; color: var(--fg-3); font-size: 12px; background: none; }
59
+ #panel-tasks .trow.thead { border-bottom: 1px solid var(--line); cursor: default; color: var(--fg-3); font-size: 12px; background: none; }
60
60
  #panel-tasks .trow .mono { font-variant-numeric: tabular-nums; font-size: 12px; }
61
61
  #panel-tasks .c-id { color: var(--fg-3); }
62
62
  #panel-tasks .c-title, #panel-tasks .c-agent { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
63
63
  #panel-tasks .c-agent { color: var(--fg-2); }
64
64
  #panel-tasks .c-time { text-align: right; color: var(--fg-2); }
65
65
  #panel-tasks .c-status { display: flex; align-items: center; gap: 6px; }
66
- #panel-tasks .c-status .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
67
- #panel-tasks .st-ok { color: var(--ok); }
68
- #panel-tasks .st-run { color: var(--run); }
69
- #panel-tasks .st-bad { color: var(--bad); }
70
- #panel-tasks .st-fg { color: var(--fg); }
71
- #panel-tasks .st-faint { color: var(--fg-3); }
66
+ #panel-tasks .c-status { white-space: nowrap; overflow: hidden; }
67
+ #panel-tasks .st-done { color: var(--ok); }
68
+ #panel-tasks .st-running { color: var(--run); }
69
+ #panel-tasks .st-stuck { color: var(--bad); }
70
+ #panel-tasks .st-next { color: var(--fg); }
71
+ #panel-tasks .panel-title .lead { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
72
72
  #panel-tasks .tdispatch {
73
73
  display: none; margin-left: auto; border: 1px solid var(--line-2); background: var(--surface);
74
74
  color: var(--fg); border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer;
@@ -13,6 +13,7 @@ export const teamScript = String.raw ` var esc = Studio.ui.esc;
13
13
  var stamps = tasks.map(function (t) { return t.status === 'in_progress' ? (t.startT || 0) : (t.endT || 0); });
14
14
  return {
15
15
  agent: key || null,
16
+ tasks: tasks,
16
17
  total: tasks.length,
17
18
  done: done.length,
18
19
  work: timed.length ? timed.reduce(function (n, t) { return n + t.durationMs; }, 0) : null,
@@ -26,18 +27,38 @@ export const teamScript = String.raw ` var esc = Studio.ui.esc;
26
27
  function buildRows(model) {
27
28
  var groups = {};
28
29
  var order = [];
30
+ var ids = {};
29
31
  model.tasks.forEach(function (t) {
30
- var key = t.agent || '';
31
- if (!groups[key]) { groups[key] = []; order.push(key); }
32
+ var key = t.agent ? Studio.agentInfo(t.agent).label : '';
33
+ if (!groups[key]) { groups[key] = []; ids[key] = []; order.push(key); }
32
34
  groups[key].push(t);
35
+ if (ids[key].indexOf(t.agent) < 0) ids[key].push(t.agent);
33
36
  });
34
- return order.map(function (k) { return statsFor(k, groups[k]); })
37
+ return order.map(function (k) {
38
+ var plain = ids[k].filter(function (i) { return !/-agent$/.test(i || ''); });
39
+ var id = plain.length ? plain[0] : String(ids[k][0] || '').replace(/-agent$/, '');
40
+ var r = statsFor(k ? id : '', groups[k]);
41
+ r.label = k || 'Unassigned';
42
+ if (ids[k].length === 1) r.agent = ids[k][0] || null;
43
+ return r;
44
+ })
35
45
  .sort(function (a, b) { return b.total - a.total; });
36
46
  }
37
47
 
38
48
  function currentTask(r) {
39
49
  var t = r.running;
40
- if (!t) return '<span class="idle">Idle</span>';
50
+ if (!t) return waiting(r);
51
+ return taskButton(t);
52
+ }
53
+
54
+ function waiting(r) {
55
+ var stuck = r.tasks.filter(function (t) { return Studio.status.bucket(t) === 'stuck'; })[0];
56
+ if (stuck) return '<span class="idle">Stuck</span> ' + taskButton(stuck);
57
+ var next = r.tasks.some(function (t) { return Studio.status.bucket(t) === 'next'; });
58
+ return '<span class="idle">' + (next ? 'Up next' : 'Done') + '</span>';
59
+ }
60
+
61
+ function taskButton(t) {
41
62
  return '<button type="button" class="cur" data-act="team-open" data-tid="' + esc(t.id) +
42
63
  '" title="' + esc(t.title || t.id) + '"><span class="mono">' + esc(t.id) + '</span>' +
43
64
  '<span class="ttl">' + esc(t.title || '') + '</span></button>';
@@ -53,9 +74,7 @@ export const teamScript = String.raw ` var esc = Studio.ui.esc;
53
74
  }
54
75
 
55
76
  function row(r) {
56
- var dot = r.running ? '<i class="run-dot"></i>' : '';
57
- return '<tr data-agent="' + esc(r.agent || 'unassigned') + '"><td class="who">' + dot +
58
- esc(r.agent || 'unassigned') + '</td>' +
77
+ return '<tr data-agent="' + esc(r.agent || 'unassigned') + '"><td class="who">' + esc(r.label) + '</td>' +
59
78
  '<td>' + currentTask(r) + '</td>' +
60
79
  '<td class="mono r">' + r.done + '</td>' +
61
80
  '<td class="mono r hide-sm">' + (r.work == null ? DASH : esc(Studio.fmt.dur(r.work))) + '</td>' +
@@ -102,7 +121,7 @@ export const teamScript = String.raw ` var esc = Studio.ui.esc;
102
121
 
103
122
  Studio.registerPage('team', {
104
123
  deps: ['tasks', 'telemetry', 'runs'],
105
- actions: { 'team-open': function (el) { Studio.taskAction('logs', el.getAttribute('data-tid')); } },
124
+ actions: { 'team-open': function (el) { Studio.openTask(el.getAttribute('data-tid')); } },
106
125
  render: render,
107
126
  skeleton: skeleton
108
127
  });
@@ -8,7 +8,6 @@ export const teamCss = String.raw `#panel-team .page-head h1 { font-size: 22px;
8
8
  #panel-team .r { text-align: right; }
9
9
  #panel-team .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
10
10
  #panel-team .who { font-weight: 500; }
11
- #panel-team .run-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--run); margin-right: 8px; }
12
11
  #panel-team .idle { color: var(--fg-3); }
13
12
  #panel-team .cur { display: inline-flex; gap: 8px; max-width: 360px; padding: 0; border: 0; background: none; color: var(--fg); font: inherit; cursor: pointer; }
14
13
  #panel-team .cur:hover .ttl { text-decoration: underline; }
@@ -38,6 +38,7 @@ export const consoleScript = String.raw ` // ─── Live runner console draw
38
38
 
39
39
  function rcStepper(run) {
40
40
  var status = run ? run.status : null;
41
+ if (!run) return '';
41
42
  var stage = RC_STAGE[status] != null ? RC_STAGE[status] : rc.stage;
42
43
  var settled = !!status && !RUN_ACTIVE[status];
43
44
  var failed = status === 'failed' || status === 'aborted';
@@ -60,15 +61,19 @@ export const consoleScript = String.raw ` // ─── Live runner console draw
60
61
  $('rc-task').textContent = rc.taskId || '';
61
62
  $('rc-title').textContent = task ? task.title : '';
62
63
  $('rc-steps').innerHTML = rcStepper(run);
63
- $('rc-abort').disabled = !isRunActive(run);
64
+ $('rc-abort').hidden = !isRunActive(run);
65
+ $('rc-steps').hidden = !run;
66
+ $('rc-foot').hidden = !run;
64
67
  $('rc-elapsed').textContent = mmss(elapsedOf(run) || 0);
65
68
  $('rc-clock').textContent = $('rc-elapsed').textContent;
66
69
  syncInspectorStatus(run);
67
70
  $('rc-bytes').textContent = formatBytes((run && run.logBytes) || 0);
68
- var gate = 'Idle';
71
+ $('rc-bytes-wrap').hidden = !(run && run.logBytes > 0);
72
+ var gate = '';
69
73
  if (run && run.verification) gate = run.verification.success ? 'Passed' : 'Failed';
70
74
  else if (run && run.status === 'verifying') gate = 'Running';
71
75
  $('rc-gate').textContent = gate;
76
+ $('rc-gate-wrap').hidden = !gate;
72
77
  // Native runs report grounded usage; cli runs have none to show.
73
78
  var usage = run && run.usage;
74
79
  $('rc-spend-wrap').hidden = !usage;
@@ -81,7 +86,7 @@ export const consoleScript = String.raw ` // ─── Live runner console draw
81
86
  if (run && !isRunActive(run)) {
82
87
  exit = RUN_LABEL[run.status] || run.status;
83
88
  if (typeof run.exitCode === 'number') exit += ' (exit ' + run.exitCode + ')';
84
- if (run.error) exit += ' · ' + run.error;
89
+ if (run.error) exit += ', ' + run.error;
85
90
  }
86
91
  $('rc-exit').textContent = exit;
87
92
  }
@@ -148,7 +153,7 @@ export const consoleScript = String.raw ` // ─── Live runner console draw
148
153
  return api('/api/pipeline/worktrees/diff?taskId=' + encodeURIComponent(taskId)).then(function (body) {
149
154
  if (seq !== rc.diffSeq || rc.taskId !== taskId) return;
150
155
  var files = Array.isArray(body.filesChanged) ? body.filesChanged : [];
151
- var summary = '<code>' + esc(body.branch || '') + '</code> · ' + (body.hasChanges ? files.length + ' uncommitted file' + (files.length === 1 ? '' : 's') : 'working tree clean');
156
+ var summary = '<code>' + esc(body.branch || '') + '</code>, ' + (body.hasChanges ? files.length + ' uncommitted file' + (files.length === 1 ? '' : 's') : 'working tree clean');
152
157
  setDiffState(summary, files, body.diff ? diffToHtml(body.diff) : '<span class="rc-empty">No uncommitted changes in this worktree yet.</span>', files.length);
153
158
  }, function (e) {
154
159
  if (seq !== rc.diffSeq || rc.taskId !== taskId) return;
@@ -196,10 +201,11 @@ export const consoleScript = String.raw ` // ─── Live runner console draw
196
201
 
197
202
  // Inspector header status: dot color follows the run state only.
198
203
  function syncInspectorStatus(run) {
199
- var st = run ? run.status : '';
200
- var tone = !st ? 'idle' : st === 'completed' ? 'ok' : (st === 'failed' || st === 'aborted') ? 'err' : 'run';
204
+ var task = rc.taskId ? taskById(rc.taskId) : null;
205
+ var bucket = Studio.status.runBucket(run) || (task ? Studio.status.bucket(task) : 'next');
206
+ var tone = { done: 'ok', stuck: 'err', running: 'run' }[bucket] || 'idle';
201
207
  $('rc-status').setAttribute('data-tone', tone);
202
- $('rc-status-text').textContent = st ? (RUN_LABEL[st] || st) : 'Idle';
208
+ $('rc-status-text').textContent = Studio.status.word(bucket);
203
209
  }
204
210
 
205
211
  // Below 1024px the inspector is a modal sheet over the content.
@@ -28,8 +28,8 @@ export const detailViewScript = String.raw ` // ─── Task detail panel (pu
28
28
  }
29
29
  if (t.status === 'in_progress') {
30
30
  var live = t.run && isRunActive(t.run);
31
- return dBtn('task-logs', 'Open live logs', { busy: b }) +
32
- dBtn('task-abort', 'Stop', { danger: true, busy: b, disabled: !live, title: live ? '' : 'No runner is attached to this task' });
31
+ return (t.run ? dBtn('task-logs', 'Watch live', { busy: b }) : '') +
32
+ (live ? dBtn('task-abort', 'Stop', { danger: true, busy: b }) : '');
33
33
  }
34
34
  if (t.status === 'pending' && !t.waitingOn.length) {
35
35
  return dBtn('task-start', 'Start', { primary: true, busy: b }) + dBtn('task-dispatch', 'Dispatch', { busy: b });
@@ -73,7 +73,7 @@ export const detailViewScript = String.raw ` // ─── Task detail panel (pu
73
73
  chipList('Needs first', t.deps.length ? t.deps.map(function (d) { return depChip(model, d); }).join('') : '<span class="faint">Nothing. It can start right away.</span>') +
74
74
  chipList('Unblocks', t.children.length ? t.children.map(function (d) { return depChip(model, d); }).join('') : '<span class="faint">No later task depends on this.</span>') +
75
75
  chipList('Files it may change', files) +
76
- '<div><div class="dlabel">How it is checked</div><div class="code">' + esc(t.verify || 'No check command.') + '</div></div>' +
76
+ '<div><div class="dlabel">How it is checked</div><div class="code wrap">' + esc(t.verify || 'No check command.') + '</div></div>' +
77
77
  '<div class="chips"><button type="button" class="btn" data-act="show-flow">' + ui.icon('flow') + 'Show in Flow</button></div></div>';
78
78
  };
79
79
 
@@ -25,9 +25,9 @@ export const dispatchScript = String.raw ` // ─── Autonomous dispatch: PO
25
25
  pipe.busy[key] = 'abort';
26
26
  refreshUi([]);
27
27
  postJson('/api/pipeline/tasks/abort', { taskId: taskId, reason: 'Aborted from Nativ Studio' }).then(function () {
28
- toast('Abort signal sent to ' + taskId, 'ok');
28
+ toast('Stop signal sent to ' + taskId, 'ok');
29
29
  }, function (e) {
30
- toast('Could not abort ' + taskId + ': ' + e.message);
30
+ toast('Could not stop ' + taskId + ': ' + e.message);
31
31
  }).then(function () {
32
32
  delete pipe.busy[key];
33
33
  return fetchPipeline(['status', 'tasks', 'runs']);
@@ -47,9 +47,9 @@ export const dispatchScript = String.raw ` // ─── Autonomous dispatch: PO
47
47
 
48
48
  function confirmAbort(taskId) {
49
49
  confirmDialog({
50
- title: 'Abort agent run',
50
+ title: 'Stop agent run',
51
51
  body: 'Terminate the Claude process tree for ' + taskId + '? The isolated worktree and its branch are left in place so the partial work can be inspected.',
52
- confirmLabel: 'Abort Run',
52
+ confirmLabel: 'Stop',
53
53
  danger: true
54
54
  }, function () { runAbort(taskId); });
55
55
  }
@@ -152,14 +152,15 @@ export const dispatchScript = String.raw ` // ─── Autonomous dispatch: PO
152
152
  function syncDispatchNext() {
153
153
  var m = Studio.model;
154
154
  if (!m) return;
155
- var t = m.tasks.filter(function (x) { return x.status === 'pending' && x.isAvailable; })[0];
155
+ var t = m.tasks.filter(function (x) { return Studio.status.isReady(x); })[0];
156
156
  var key = t ? t.id : '';
157
157
  if (key === nextSeen) return;
158
158
  nextSeen = key;
159
159
  nextId = t ? t.id : null;
160
- nextBtn.disabled = !t;
161
- $('dispatch-next-label').textContent = t ? 'Dispatch ' + t.id : 'Nothing ready';
162
- nextBtn.title = t ? 'Open the dispatch dialog for ' + t.id : 'No pending task has its dependencies met';
160
+ nextBtn.hidden = !t;
161
+ if (!t) return;
162
+ $('dispatch-next-label').textContent = 'Dispatch ' + t.id;
163
+ nextBtn.title = 'Open the dispatch dialog for ' + t.id;
163
164
  }
164
165
  nextBtn.addEventListener('click', function () { if (nextId) openDispatchDialog(nextId); });
165
166
  Studio.onPaint(syncDispatchNext);
@@ -8,6 +8,17 @@ export const eventsToolsScript = String.raw ` // ─── Events: live runner
8
8
  $('rc-clear').addEventListener('click', function () {
9
9
  $('rc-body').innerHTML = '<span class="rc-empty">Console cleared. New output will stream in here.</span>';
10
10
  });
11
+ function setMoreMenu(open) {
12
+ $('rc-more-menu').hidden = !open;
13
+ $('rc-more').setAttribute('aria-expanded', String(open));
14
+ }
15
+ $('rc-more').addEventListener('click', function () { setMoreMenu($('rc-more-menu').hidden); });
16
+ $('rc-more-menu').addEventListener('click', function (e) {
17
+ if (e.target.closest('#rc-open-diff')) setMoreMenu(false);
18
+ });
19
+ document.addEventListener('click', function (e) {
20
+ if (!$('rc-more-menu').hidden && !e.target.closest('.rc-more')) setMoreMenu(false);
21
+ });
11
22
  $('rc-abort').addEventListener('click', function () { if (rc.taskId) confirmAbort(rc.taskId); });
12
23
  document.querySelectorAll('.rc-tab').forEach(function (btn) {
13
24
  btn.addEventListener('click', function () { setConsoleTab(btn.getAttribute('data-rc-tab')); });
@@ -24,7 +24,34 @@ export const formatScript = String.raw ` // ─── Studio client core: state
24
24
  });
25
25
  }
26
26
 
27
- var STATUS_LABEL = { completed: 'Done', in_progress: 'Running', pending: 'Up next', blocked: 'Stuck' };
27
+ var BUCKETS = [['next', 'Up next'], ['running', 'Running'], ['done', 'Done'], ['stuck', 'Stuck']];
28
+ var BUCKET_WORD = {};
29
+ BUCKETS.forEach(function (b) { BUCKET_WORD[b[0]] = b[1]; });
30
+ var STATUS_BUCKET = { completed: 'done', in_progress: 'running', pending: 'next', blocked: 'stuck' };
31
+ var STATUS_LABEL = {};
32
+ Object.keys(STATUS_BUCKET).forEach(function (k) { STATUS_LABEL[k] = BUCKET_WORD[STATUS_BUCKET[k]]; });
33
+ var RUN_BUCKET = { spawning_worktree: 'running', running: 'running', verifying: 'running',
34
+ merging: 'running', completed: 'done', failed: 'stuck', aborted: 'next' };
35
+
36
+ var status = {
37
+ buckets: BUCKETS,
38
+ isReady: function (t) {
39
+ return t.status === 'pending' && (!!t.isAvailable || !(t.waitingOn || []).length);
40
+ },
41
+ bucket: function (t) {
42
+ if (t.status === 'completed') return 'done';
43
+ if (t.status === 'in_progress') return t.stale ? 'next' : 'running';
44
+ if (t.status === 'blocked') return 'stuck';
45
+ return 'next';
46
+ },
47
+ runBucket: function (run) { return run ? (RUN_BUCKET[run.status] || null) : null; },
48
+ word: function (key) { return BUCKET_WORD[key] || STATUS_LABEL[key] || 'Unknown'; },
49
+ waitText: function (t) {
50
+ if (t.status !== 'pending' || status.isReady(t)) return '';
51
+ var n = (t.waitingOn || []).length;
52
+ return n === 1 ? 'Waits for 1 task' : 'Waits for ' + n + ' tasks';
53
+ }
54
+ };
28
55
  var AGENTS = {
29
56
  'backend': ['Backend', '--ag-backend'],
30
57
  'frontend': ['Frontend', '--ag-frontend'],
@@ -118,7 +145,8 @@ export const formatScript = String.raw ` // ─── Studio client core: state
118
145
  if (!model) return '';
119
146
  var current = Studio.state.milestoneId;
120
147
  var opts = model.milestones.slice().reverse().map(function (m) {
121
- var text = m.id + ' · ' + m.short + ' (' + m.done + '/' + m.total + ')' + (m.id === model.activeMilestoneId ? ' · active' : '');
148
+ var count = m.done + '/' + m.total + (m.id === model.activeMilestoneId ? ', active' : '');
149
+ var text = m.id + ' ' + m.short + ' (' + count + ')';
122
150
  return '<option value="' + esc(m.id) + '"' + (m.id === current ? ' selected' : '') + '>' + esc(text) + '</option>';
123
151
  }).join('');
124
152
  return '<select class="sel" data-ms-select aria-label="Milestone">' + opts + '</select>';
@@ -136,6 +164,7 @@ export const formatScript = String.raw ` // ─── Studio client core: state
136
164
 
137
165
  Studio.fmt = fmt;
138
166
  Studio.ui = ui;
167
+ Studio.status = status;
139
168
  Studio.agentInfo = agentInfo;
140
169
  Studio.statusLabel = function (status) { return STATUS_LABEL[status] || 'Unknown'; };
141
170
 
@@ -25,7 +25,7 @@ export function markupHead(version, projectName, projectHidden) {
25
25
  <div class="top-right">
26
26
  <button class="btn" id="btn-find" type="button" aria-keyshortcuts="Control+K Meta+K">${svg('search')}<span>Find task</span><span class="kbd">Ctrl+K</span></button>
27
27
  <button class="btn ghost" id="btn-theme" type="button" title="Switch light or dark" aria-label="Switch light or dark"><span class="ic-sun">${svg('sun')}</span><span class="ic-moon">${svg('moon')}</span></button>
28
- <button class="btn primary" id="btn-dispatch-next" type="button" disabled title="Loading tasks">${svg('play')}<span id="dispatch-next-label">Nothing ready</span></button>
28
+ <button class="btn primary" id="btn-dispatch-next" type="button" hidden>${svg('play')}<span id="dispatch-next-label">Dispatch</span></button>
29
29
  <div class="t1" id="t1">
30
30
  <button class="t1-chip" id="t1-chip" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="t1-menu" data-state="idle" title="Tier 1 AI Strategist"><svg class="icon t1-spark" width="14" height="14" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M197.58,129.06,146,110l-19-51.62a15.92,15.92,0,0,0-29.88,0L78,110l-51.62,19a15.92,15.92,0,0,0,0,29.88L78,178l19,51.62a15.92,15.92,0,0,0,29.88,0L146,178l51.62-19a15.92,15.92,0,0,0,0-29.88ZM137,164.22a8,8,0,0,0-4.74,4.74L112,223.85,91.78,169A8,8,0,0,0,87,164.22L32.15,144,87,123.78A8,8,0,0,0,91.78,119L112,64.15,132.22,119a8,8,0,0,0,4.74,4.74L191.85,144ZM144,40a8,8,0,0,1,8-8h16V16a8,8,0,0,1,16,0V32h16a8,8,0,0,1,0,16H184V64a8,8,0,0,1-16,0V48H152A8,8,0,0,1,144,40ZM248,88a8,8,0,0,1-8,8h-8v8a8,8,0,0,1-16,0V96h-8a8,8,0,0,1,0-16h8V72a8,8,0,0,1,16,0v8h8A8,8,0,0,1,248,88Z"/></svg><span class="t1-chip-label">Tier 1 AI:</span><span class="t1-chip-model" id="t1-chip-model">Gemini 3.8 Flash</span><svg class="icon t1-caret" width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></svg></button>
31
31
  <div class="t1-menu" id="t1-menu" role="dialog" aria-label="Tier 1 AI Strategist" tabindex="-1" hidden>
@@ -14,16 +14,21 @@ export const markupTools = String.raw `</main>
14
14
  <header class="rc-head">
15
15
  <div class="rc-id-row">
16
16
  <code class="rc-task" id="rc-task"></code>
17
- <span class="rc-status" id="rc-status"><span class="rc-dot" aria-hidden="true"></span><span id="rc-status-text">Idle</span></span>
17
+ <span class="rc-status" id="rc-status"><span class="rc-dot" aria-hidden="true"></span><span id="rc-status-text">Up next</span></span>
18
18
  <code class="rc-clock" id="rc-clock">00:00</code>
19
19
  </div>
20
20
  <span class="rc-title" id="rc-title"></span>
21
21
  <ol class="rc-steps" id="rc-steps" aria-label="Runner lifecycle"></ol>
22
22
  <div class="rc-controls">
23
- <button class="rc-btn danger" type="button" id="rc-abort" aria-label="Abort the running agent">Abort</button>
24
- <button class="rc-btn" type="button" id="rc-open-diff" aria-label="Open the worktree diff">Open diff</button>
25
- <button class="rc-btn" type="button" id="rc-autoscroll" aria-pressed="true" aria-label="Toggle console auto-scroll">Auto-scroll</button>
26
- <button class="rc-btn" type="button" id="rc-clear" aria-label="Clear the console buffer">Clear</button>
23
+ <button class="rc-btn danger" type="button" id="rc-abort" aria-label="Stop the running agent" hidden>Stop</button>
24
+ <span class="rc-more">
25
+ <button class="rc-btn" type="button" id="rc-more" aria-haspopup="true" aria-expanded="false" aria-controls="rc-more-menu">More</button>
26
+ <div class="rc-menu" id="rc-more-menu" hidden>
27
+ <button class="rc-btn" type="button" id="rc-open-diff" aria-label="Open the worktree diff">Open diff</button>
28
+ <button class="rc-btn" type="button" id="rc-clear" aria-label="Clear the console buffer">Clear</button>
29
+ <button class="rc-btn" type="button" id="rc-autoscroll" aria-pressed="true" aria-label="Toggle console auto-scroll">Auto-scroll</button>
30
+ </div>
31
+ </span>
27
32
  <button class="rc-btn" type="button" id="rc-minimize" aria-expanded="true" aria-controls="rc-body" aria-label="Collapse the inspector">Collapse</button>
28
33
  <button class="rc-btn" type="button" id="rc-close" aria-label="Close the runner console">Close</button>
29
34
  </div>
@@ -43,12 +48,12 @@ export const markupTools = String.raw `</main>
43
48
  <pre class="rc-diff-body" id="rc-diff-body" aria-label="Unified diff" tabindex="0"></pre>
44
49
  </div>
45
50
  <div class="rc-heal" id="rc-heal" role="tabpanel" aria-labelledby="rc-tab-heal" tabindex="0" hidden></div>
46
- <footer class="rc-foot">
51
+ <footer class="rc-foot" id="rc-foot" hidden>
47
52
  <span>Elapsed <strong id="rc-elapsed">00:00</strong></span>
48
- <span>Output <strong id="rc-bytes">0 B</strong></span>
53
+ <span id="rc-bytes-wrap" hidden>Output <strong id="rc-bytes">0 B</strong></span>
49
54
  <span id="rc-spend-wrap" hidden>Spend <strong id="rc-spend">$0.0000</strong></span>
50
55
  <span id="rc-cache-wrap" hidden>Cache hit <strong id="rc-cache">0%</strong></span>
51
- <span>Gatekeeper <strong id="rc-gate">Idle</strong></span>
56
+ <span id="rc-gate-wrap" hidden>Gatekeeper <strong id="rc-gate"></strong></span>
52
57
  <span id="rc-exit"></span>
53
58
  </footer>
54
59
  </section>
@@ -2,6 +2,7 @@
2
2
  export const modelScript = String.raw ` // ─── Studio client model: buildModel(api) is pure ──────────────────────────
3
3
  var RUN_ACTIVE = { spawning_worktree: 1, running: 1, verifying: 1, merging: 1 };
4
4
  function isRunActive(run) { return !!run && !!RUN_ACTIVE[run.status]; }
5
+ var STALE_MS = 4 * 3600 * 1000;
5
6
 
6
7
  /** Text before the first colon, at most 46 characters. */
7
8
  function shortName(name) {
@@ -46,11 +47,14 @@ export const modelScript = String.raw ` // ─── Studio client model: build
46
47
  if (startT == null && run && isRunActive(run)) startT = toTime(run.startedAt);
47
48
  var cost = b && b.actual ? positive(b.actual.costUsd) : null;
48
49
  if (cost == null && b && b.estimated) cost = positive(b.estimated.costUsd);
50
+ var claimT = toTime(t.claimedAt);
51
+ var stale = running && claimT != null && Studio.now() - claimT > STALE_MS && !isRunActive(run);
49
52
  return {
50
53
  id: t.id,
51
54
  title: t.title || t.id,
52
55
  description: t.description || '',
53
- status: t.status,
56
+ status: stale ? 'pending' : t.status,
57
+ claimedStatus: t.status,
54
58
  agent: t.assignedSubagent || null,
55
59
  deps: t.dependencies || [],
56
60
  files: t.targetFiles || [],
@@ -62,6 +66,8 @@ export const modelScript = String.raw ` // ─── Studio client model: build
62
66
  reason: t.status === 'blocked' && t.notes ? String(t.notes).replace(/^\s+|\s+$/g, '') || null : null,
63
67
  attempt: attemptsOf(t, running),
64
68
  maxAttempts: (t.circuitBreaker && t.circuitBreaker.maxThreshold) || 3,
69
+ claimT: claimT,
70
+ stale: stale,
65
71
  startT: startT,
66
72
  endT: t.status === 'completed' && b ? toTime(b.completedAt) : null,
67
73
  durationMs: b ? positive(b.durationMs) : null,
@@ -175,7 +181,7 @@ export const modelScript = String.raw ` // ─── Studio client model: build
175
181
  tasks: tasks,
176
182
  byId: byId,
177
183
  activeMilestoneId: active,
178
- running: tasks.filter(function (t) { return t.status === 'in_progress'; }),
184
+ running: tasks.filter(function (t) { return t.status === 'in_progress' && !t.stale; }),
179
185
  stuck: tasks.filter(function (t) { return t.status === 'blocked'; }),
180
186
  needsYou: needsYouItems(tasks, byId, api.escalations),
181
187
  summary: summaryOf(api, milestones, tasks)
@@ -67,11 +67,9 @@ export const paintScript = String.raw ` // ─── Painting ─────
67
67
  var side = $('side-nav');
68
68
  var main = mainWorktree();
69
69
  var agents = pipe.worktrees.filter(function (w) { return w.isAgentWorktree; }).length;
70
- var s = pipe.report && pipe.report.summary;
71
70
  var navHtml = Studio.sidebarHtml({
72
71
  model: Studio.model, view: Studio.state.view,
73
- worktreeCount: pipe.loaded.worktrees && !pipe.errors.worktrees ? agents : null,
74
- benchmarkText: s ? compactNum(Number(s.averageThroughputOpsPerSec) || 0) : ''
72
+ worktreeCount: pipe.loaded.worktrees && !pipe.errors.worktrees ? agents : null
75
73
  });
76
74
  if (side.__html !== navHtml) { side.innerHTML = navHtml; side.__html = navHtml; }
77
75
  var foot = $('side-foot');
@@ -117,18 +117,9 @@ export const pipelineStateScript = String.raw ` // ─── Pipeline state ─
117
117
  return main;
118
118
  }
119
119
 
120
- /** 1234 becomes "1.2k" so the sidebar badge stays small. */
121
- function compactNum(n) {
122
- if (!isFinite(n)) return '-';
123
- if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
124
- if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k';
125
- return String(Math.round(n));
126
- }
127
-
128
- /** Newest Just-finished style helpers live in pages; here only what the runner tools need. */
129
120
  var RUN_LABEL = {
130
- spawning_worktree: 'Worktree', running: 'Claude Running', verifying: 'Verifying', merging: 'Merging',
131
- completed: 'Run completed', failed: 'Run failed', aborted: 'Run aborted'
121
+ spawning_worktree: 'Running', running: 'Running', verifying: 'Running', merging: 'Running',
122
+ completed: 'Done', failed: 'Stuck', aborted: 'Up next'
132
123
  };
133
124
  function runFor(taskId) {
134
125
  var found = null;
@@ -6,10 +6,11 @@ export const pulseScript = String.raw ` // ─── Pulse strip ────
6
6
  if (!s.loaded) return { kind: 'loading', text: '-- -- -- --' };
7
7
  var counts = [
8
8
  { key: 'running', label: 'running', n: s.running, tone: 'run' },
9
- { key: 'blocked', label: 'blocked', n: s.blocked, tone: 'bad' },
10
- { key: 'decisions', label: 'decisions', n: s.decisions, tone: 'warn' }
9
+ { key: 'stuck', label: 'stuck', n: s.blocked, tone: 'bad' },
10
+ { key: 'decisions', label: s.decisions === 1 ? 'decision' : 'decisions', n: s.decisions, tone: 'warn' }
11
11
  ];
12
- counts.forEach(function (c) { c.zero = !c.n; });
12
+ counts = counts.filter(function (c) { return c.n > 0; });
13
+ counts.forEach(function (c) { c.zero = false; });
13
14
  return { kind: 'ready', ms: s.ms, counts: counts };
14
15
  }
15
16
 
@@ -17,11 +18,10 @@ export const pulseScript = String.raw ` // ─── Pulse strip ────
17
18
  if (p.kind !== 'ready') {
18
19
  return '<span class="pulse-note ' + p.kind + '">' + esc(p.text) + '</span>';
19
20
  }
21
+ if (!p.counts.length) return '';
20
22
  var out = '<span class="pulse-ms num">' + esc(p.ms) + '</span>';
21
23
  p.counts.forEach(function (c) {
22
- var tone = c.zero ? 'zero' : c.tone;
23
- out += '<button type="button" class="pulse-item ' + tone + '" data-pulse="' + c.key + '">' +
24
- (c.zero ? '' : '<span class="pdot"></span>') +
24
+ out += '<button type="button" class="pulse-item ' + c.tone + '" data-pulse="' + c.key + '">' +
25
25
  '<span class="num">' + c.n + '</span> ' + c.label + '</button>';
26
26
  });
27
27
  return out;
@@ -49,10 +49,11 @@ export const pulseScript = String.raw ` // ─── Pulse strip ────
49
49
  var html = pulseHtml(pulseParts(pulseInput()));
50
50
  if (pulseEl.__html === html) return;
51
51
  pulseEl.innerHTML = html;
52
+ pulseEl.hidden = !html;
52
53
  pulseEl.__html = html;
53
54
  }
54
55
 
55
- // Studio.taskFilter ('running' or 'blocked') is read by the Tasks page when it opens.
56
+ // Studio.taskFilter ('running' or 'stuck') is read by the Tasks page when it opens.
56
57
  if (pulseEl) pulseEl.addEventListener('click', function (e) {
57
58
  var b = e.target.closest ? e.target.closest('[data-pulse]') : null;
58
59
  if (!b) return;
@@ -5,15 +5,14 @@ export const sidebarViewScript = String.raw ` // ─── Sidebar markup (pure
5
5
  function navBadges(env) {
6
6
  var badges = {}, model = env.model;
7
7
  if (model && model.needsYou.length) badges.home = ['warn', model.needsYou.length];
8
- var ready = model ? model.tasks.filter(function (t) { return t.status === 'pending' && t.isAvailable; }).length : 0;
8
+ var ready = model ? model.tasks.filter(Studio.status.isReady).length : 0;
9
9
  var active = model ? model.running.length + ready : 0;
10
10
  if (active) badges.tasks = [model.running.length ? 'run' : '', active];
11
11
  if (env.worktreeCount != null) badges.worktrees = ['', env.worktreeCount];
12
- if (env.benchmarkText) badges.benchmarks = ['', env.benchmarkText];
13
12
  return badges;
14
13
  }
15
14
 
16
- /** env = { model, view, worktreeCount, benchmarkText }. */
15
+ /** env = { model, view, worktreeCount }. */
17
16
  Studio.sidebarHtml = function (env) {
18
17
  var badges = navBadges(env);
19
18
  var item = function (p) {
@@ -35,7 +35,7 @@ export const triageScript = String.raw ` // ─── Tier 1 AI Strategist (/ap
35
35
  $('t1-chip-model').textContent = modelName(model);
36
36
  $('t1-model').textContent = model;
37
37
  var last = lastTriage();
38
- $('t1-latency').textContent = last ? fmtMs(last.latencyMs) + ' · ' + modelName(last.model || model) : 'No evaluations yet';
38
+ $('t1-latency').textContent = last ? fmtMs(last.latencyMs) + ', ' + modelName(last.model || model) : 'No evaluations yet';
39
39
  $('t1-key').textContent = pipe.errors.triage && !t ? 'Status unavailable' : !t ? 'Checking' : t.hasApiKey ? 'Configured' : 'Not set (offline rules)';
40
40
  var sw = $('t1-auto');
41
41
  var on = !!(t && t.autoTriageEnabled);
@@ -32,6 +32,9 @@ body.has-console-min main { margin-right: 56px; }
32
32
  .rc-step.is-active { color: var(--term-text); background: color-mix(in srgb, var(--primary) 32%, transparent); border-color: var(--primary); }
33
33
  .rc-step.is-failed { color: var(--term-text); background: color-mix(in srgb, var(--danger) 32%, transparent); border-color: var(--danger); }
34
34
  .rc-controls { display: flex; gap: 6px; flex-wrap: wrap; }
35
+ .rc-more { position: relative; }
36
+ .rc-menu { position: absolute; top: calc(100% + 4px); left: 0; z-index: 2; display: flex; flex-direction: column; gap: 4px; padding: 6px; background: var(--term-bg); border: 1px solid var(--term-line); border-radius: var(--r-control); }
37
+ .rc-menu[hidden], .rc-btn[hidden], .rc-steps[hidden], .rc-foot[hidden] { display: none; }
35
38
  .rc-btn { padding: 5px 9px; font: 500 11px/1 var(--font); color: var(--term-text); background: transparent; border: 1px solid var(--term-line); border-radius: var(--r-control); cursor: pointer; }
36
39
  .rc-btn:hover { background: color-mix(in srgb, var(--term-muted) 22%, transparent); }
37
40
  .rc-btn:disabled { opacity: .45; cursor: not-allowed; }
@@ -19,7 +19,7 @@ export const shellCss = String.raw `/* Layout */
19
19
  .nav-badge.run { background: var(--run-soft); color: var(--run); }
20
20
  .side-foot { margin-top: auto; font-size: 12px; color: var(--fg-3); padding: 0 8px; display: flex; flex-direction: column; gap: 4px; }
21
21
  .side-foot > div { display: flex; flex-direction: column; gap: 4px; }
22
- .side-foot .mono { color: var(--fg-2); font-size: 11px; word-break: break-all; }
22
+ .side-foot .mono { color: var(--fg-2); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
23
23
  .foot-project { color: var(--fg); font-weight: 500; }
24
24
  .main { min-width: 0; display: flex; flex-direction: column; }
25
25
  .top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 20; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 24px; }
@@ -28,21 +28,19 @@ export const shellCss = String.raw `/* Layout */
28
28
  .top-right { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
29
29
  .btn.ghost { background: none; border-color: transparent; color: var(--fg-2); box-shadow: none; }
30
30
  .btn.ghost:hover { background: var(--surface-2); }
31
- #btn-dispatch-next[disabled] { opacity: .55; cursor: not-allowed; }
32
31
  .spacer { flex: 1; }
33
32
  .ic-sun, .ic-moon { display: inline-flex; }
34
33
  .ic-moon { display: none; }
35
34
  :root[data-theme="light"] .ic-sun { display: none; }
36
35
  :root[data-theme="light"] .ic-moon { display: inline-flex; }
37
36
  @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .ic-sun { display: none; } :root:not([data-theme="dark"]) .ic-moon { display: inline-flex; } }
37
+ #pulse[hidden] { display: none; }
38
38
  .pulse-strip { min-height: 32px; height: auto; width: auto; border-radius: 0; background: none; box-shadow: none; animation: none; display: flex; flex-wrap: wrap; align-items: center; gap: 0 20px; padding: 0 24px; border-bottom: 1px solid var(--line); font: 12.5px var(--mono); color: var(--fg-2); }
39
39
  #pulse .num { font-variant-numeric: tabular-nums; }
40
40
  #pulse .pulse-ms { color: var(--fg); padding-right: 20px; border-right: 1px solid var(--line); }
41
41
  #pulse .pulse-item { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; cursor: pointer; }
42
- #pulse .pulse-item.zero, #pulse .pulse-note.loading { color: var(--fg-3); }
42
+ #pulse .pulse-note.loading { color: var(--fg-3); }
43
43
  #pulse .pulse-item.run { color: var(--run); } #pulse .pulse-item.bad { color: var(--bad); } #pulse .pulse-item.warn, #pulse .pulse-note.offline { color: var(--warn); }
44
- #pulse .pdot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
45
- #pulse .pulse-item.run .pdot { animation: blink 1.6s infinite; }
46
44
  @media (max-width: 1023px) { .pulse-strip { padding-top: 4px; padding-bottom: 4px; gap: 0 16px; } #pulse .pulse-ms { flex-basis: 100%; border-right: 0; padding-right: 0; } }
47
45
  .wordmark.is-rail { display: none; }
48
46
  @media (max-width: 1023px) { .wordmark { display: none; } .wordmark.is-rail { display: block; } }
@@ -132,5 +130,5 @@ export const shellCss = String.raw `/* Layout */
132
130
  .drawer { width: 100%; }
133
131
  #runner-console-drawer { inset: auto 0 0 0; }
134
132
  }
135
- @media (prefers-reduced-motion: reduce) { .pulse, .live-dot.flash, #pulse .pdot { animation: none !important; } * { transition: none !important; } }
133
+ @media (prefers-reduced-motion: reduce) { .pulse, .live-dot.flash { animation: none !important; } * { transition: none !important; } }
136
134
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@njeybe/nativ",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Role-based multi-agent workflow harness: Claude Code by default, other AI agents pluggable",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nativ",
3
3
  "displayName": "nativ",
4
- "version": "2.7.0",
4
+ "version": "2.8.0",
5
5
  "description": "Role-based multi-agent workflow for Claude Code: architect, worker, verifier and explorer agents, task-scope enforcement, and the nativ MCP server.",
6
6
  "author": {
7
7
  "name": "JB Natividad"