@hecer/yoke 1.9.0 → 1.11.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 (153) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.codex-plugin/plugin.json +7 -7
  3. package/CHANGELOG.md +398 -358
  4. package/README.md +915 -913
  5. package/TODOS.md +5 -5
  6. package/agents/docs.toml +6 -6
  7. package/agents/implementer.toml +6 -6
  8. package/agents/reviewer.toml +6 -6
  9. package/agents/security.toml +6 -6
  10. package/bench/README.md +86 -86
  11. package/bench/RESULTS.md +35 -35
  12. package/bench/output-compaction.mjs +65 -65
  13. package/bench/result-schema.mjs +12 -12
  14. package/bench/results/claude-2026-07-27T18-03-26.json +50 -50
  15. package/bench/results/codex-unavailable-1785175418318.json +15 -15
  16. package/bench/results/gemini-2026-07-27T18-03-44.json +46 -46
  17. package/bench/run-matrix.mjs +26 -26
  18. package/bench/run.mjs +106 -106
  19. package/canon/AGENTS.md +30 -30
  20. package/canon/context/DECISIONS.md +4 -4
  21. package/canon/context/GLOSSARY.md +11 -11
  22. package/canon/context/KNOWLEDGE.md +4 -4
  23. package/canon/context/PROJECT.md +15 -15
  24. package/canon/loop/loop-spec.md +65 -65
  25. package/canon/loop/prd.schema.md +46 -40
  26. package/canon/manifest.yaml +59 -59
  27. package/canon/policy/gates.md +7 -7
  28. package/canon/policy/roles.md +9 -9
  29. package/canon/skills/ATTRIBUTION.md +99 -99
  30. package/canon/skills/authoring-prd/SKILL.md +56 -56
  31. package/canon/skills/brainstorming/SKILL.md +164 -164
  32. package/canon/skills/codebase-design/DEEPENING.md +15 -15
  33. package/canon/skills/codebase-design/DESIGN-IT-TWICE.md +12 -12
  34. package/canon/skills/codebase-design/SKILL.md +39 -39
  35. package/canon/skills/dispatching-parallel-agents/SKILL.md +182 -182
  36. package/canon/skills/document-release/SKILL.md +302 -302
  37. package/canon/skills/domain-modeling/ADR-FORMAT.md +19 -19
  38. package/canon/skills/domain-modeling/CONTEXT-FORMAT.md +39 -39
  39. package/canon/skills/domain-modeling/SKILL.md +35 -35
  40. package/canon/skills/executing-plans/SKILL.md +70 -70
  41. package/canon/skills/finishing-a-development-branch/SKILL.md +200 -200
  42. package/canon/skills/health/SKILL.md +177 -177
  43. package/canon/skills/maintaining-context/SKILL.md +34 -34
  44. package/canon/skills/minimal-code/SKILL.md +21 -21
  45. package/canon/skills/no-ai-slop/SKILL.md +103 -103
  46. package/canon/skills/no-ai-slop/eval.md +43 -43
  47. package/canon/skills/plan-ceo-review/SKILL.md +541 -541
  48. package/canon/skills/plan-eng-review/SKILL.md +362 -362
  49. package/canon/skills/receiving-code-review/SKILL.md +213 -213
  50. package/canon/skills/requesting-code-review/SKILL.md +105 -105
  51. package/canon/skills/resolving-merge-conflicts/SKILL.md +18 -18
  52. package/canon/skills/retro/SKILL.md +397 -397
  53. package/canon/skills/review/SKILL.md +246 -246
  54. package/canon/skills/ship/SKILL.md +691 -691
  55. package/canon/skills/subagent-driven-development/SKILL.md +277 -277
  56. package/canon/skills/systematic-debugging/SKILL.md +296 -296
  57. package/canon/skills/tdd/SKILL.md +371 -371
  58. package/canon/skills/unslop-ui/SKILL.md +34 -34
  59. package/canon/skills/using-git-worktrees/SKILL.md +218 -218
  60. package/canon/skills/verification-before-completion/SKILL.md +139 -139
  61. package/canon/skills/visual-verification/SKILL.md +54 -54
  62. package/canon/skills/workflow/SKILL.md +22 -22
  63. package/canon/skills/writing-for-agents/SKILL-MECHANICS.md +27 -27
  64. package/canon/skills/writing-for-agents/SKILL.md +42 -42
  65. package/canon/skills/writing-plans/SKILL.md +152 -152
  66. package/canon/skills/writing-skills/SKILL.md +655 -655
  67. package/canon/skills/yoke-retrofit/SKILL.md +26 -26
  68. package/canon/skills/yoke-workflow/SKILL.md +20 -20
  69. package/canon/tools/codex-rtk-hook.mjs +35 -35
  70. package/canon/tools/gemini-rtk-hook.mjs +25 -25
  71. package/canon/tools/graphify.md +3 -3
  72. package/canon/tools/playwright-mcp.md +3 -3
  73. package/canon/tools/rtk.md +7 -7
  74. package/canon/tools/serena.md +6 -6
  75. package/dist/agents/contracts.js +1 -1
  76. package/dist/agents/host.js +4 -0
  77. package/dist/agents/process-incarnation.js +1 -1
  78. package/dist/agents/process.js +74 -6
  79. package/dist/agents/providers.js +13 -0
  80. package/dist/agents/supervision.js +153 -0
  81. package/dist/agents/telemetry.js +33 -0
  82. package/dist/agents/windows-launch.js +80 -0
  83. package/dist/canon/manifest.js +1 -1
  84. package/dist/change/inbox.js +21 -5
  85. package/dist/cli.js +19 -10
  86. package/dist/dashboard/discovery.js +73 -0
  87. package/dist/dashboard/page.js +122 -28
  88. package/dist/dashboard/panels.js +91 -15
  89. package/dist/goals/command.js +4 -2
  90. package/dist/loop/claims.js +1 -1
  91. package/dist/loop/decision.js +2 -2
  92. package/dist/loop/git.js +12 -4
  93. package/dist/loop/loop.js +8 -4
  94. package/dist/loop/parallel-adapters.js +2 -3
  95. package/dist/loop/parallel-command.js +5 -0
  96. package/dist/loop/prd.js +3 -1
  97. package/dist/loop/reporter.js +4 -1
  98. package/dist/loop/run-command.js +11 -2
  99. package/dist/loop/runner.js +22 -26
  100. package/dist/loop/watchdog.js +87 -11
  101. package/dist/loop/worker.js +5 -3
  102. package/dist/prd/assess.js +145 -0
  103. package/dist/prd/command.js +76 -38
  104. package/dist/quality/types.js +1 -1
  105. package/dist/retrofit/config.js +11 -0
  106. package/dist/retrofit/plan.js +2 -0
  107. package/dist/retrofit/planners/claude.js +14 -14
  108. package/dist/retrofit/planners/qwen.js +73 -0
  109. package/dist/retrofit/preserve.js +2 -2
  110. package/dist/retrofit/skill-actions.js +1 -0
  111. package/dist/review/command.js +1 -1
  112. package/dist/routing/assessment.js +1 -1
  113. package/dist/routing/capability.js +25 -13
  114. package/dist/routing/contracts.js +60 -0
  115. package/dist/routing/planning.js +12 -0
  116. package/dist/routing/router.js +51 -16
  117. package/dist/setup/command.js +11 -3
  118. package/docs/BATCH-PLANNING-VALIDATION.md +67 -0
  119. package/docs/CAPABILITY-ROUTING.md +78 -50
  120. package/docs/DASHBOARD-EVOLUTION.md +33 -0
  121. package/docs/MIGRATING-TO-1.0.md +33 -33
  122. package/docs/MIGRATING-TO-1.1.md +27 -27
  123. package/docs/MIGRATING-TO-1.4.md +70 -70
  124. package/docs/PRODUCT-DIRECTION-2026-09-05.md +218 -200
  125. package/docs/PUBLISHING.md +114 -114
  126. package/docs/VERIFIED-PROJECTS-VALIDATION.md +29 -29
  127. package/docs/VERIFIED-PROJECTS.md +167 -167
  128. package/docs/WINDOWS-RUNNER-VALIDATION.md +104 -0
  129. package/docs/assets/yoke-logo.png +0 -0
  130. package/docs/community-outreach-2026-08-20.md +85 -0
  131. package/docs/launch-copy-2026-08-21.md +193 -0
  132. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -981
  133. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -258
  134. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -1006
  135. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -374
  136. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -450
  137. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -1024
  138. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -574
  139. package/docs/superpowers/plans/2026-08-13-gauntlet-quality-loop.md +537 -537
  140. package/docs/superpowers/plans/2026-08-16-artifact-backed-output-compaction.md +329 -329
  141. package/docs/superpowers/plans/2026-09-05-verified-projects.md +83 -83
  142. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -146
  143. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -106
  144. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -186
  145. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -113
  146. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -98
  147. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -200
  148. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -155
  149. package/docs/superpowers/specs/2026-08-13-gauntlet-quality-loop-design.md +422 -422
  150. package/docs/superpowers/specs/2026-08-16-artifact-backed-output-compaction-design.md +166 -166
  151. package/gemini-extension.json +6 -6
  152. package/hooks/hooks.json +19 -19
  153. package/package.json +87 -87
@@ -2,33 +2,127 @@ import { dashboardPanels } from './panels.js';
2
2
  export function dashboardPage(token, nonce) {
3
3
  if (!/^[a-f0-9]{64}$/u.test(token) || !/^[A-Za-z0-9+/=]+$/u.test(nonce))
4
4
  throw new Error('Invalid dashboard session');
5
- return `<!doctype html>
6
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Yoke · Project workspace</title>
7
- <style nonce="${nonce}">
8
- :root{color-scheme:light;--ink:#15242d;--muted:#647077;--line:#dce1df;--paper:#f4f5f0;--accent:#c34c27;--green:#167452}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,sans-serif}button{font:inherit;cursor:pointer}button:focus-visible,a:focus-visible{outline:3px solid #dc734a;outline-offset:3px}.layout{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}aside{background:#142b32;color:#eef5f1;padding:34px 22px;display:flex;flex-direction:column;gap:30px}.brand{font-size:31px;font-weight:780;letter-spacing:-1.5px}.brand span{color:#ffb185}aside p{color:#afc3c6;font-size:13px}.label{text-transform:uppercase;font-size:11px;letter-spacing:1.7px;font-weight:750;color:var(--muted)}aside .label{color:#91a9ad}nav{display:grid;gap:7px}.nav-button{border:0;background:transparent;color:#cad8d9;text-align:left;padding:11px 12px;border-radius:8px;overflow-wrap:anywhere}.nav-button[aria-current=true]{background:#29444b;color:#fff}.aside-foot{margin-top:auto;border-top:1px solid #355057;padding-top:20px}main{max-width:1500px;width:100%;padding:40px 5vw 70px}.top{display:flex;justify-content:space-between;align-items:center;gap:20px}.local{font-size:12px;color:var(--green);background:#e2ece1;border-radius:30px;padding:6px 12px}.button{border:1px solid #bcc7c5;background:#fff;border-radius:7px;padding:9px 15px;color:var(--ink)}.button.primary{background:var(--ink);color:#fff;border-color:var(--ink)}h1{font-size:clamp(27px,3vw,40px);line-height:1.2;margin:17px 0 8px;letter-spacing:-1.4px}h2{font-size:18px;margin:0 0 18px}h3{font-size:16px;margin:0 0 8px}p{margin:6px 0}.muted{color:var(--muted)}.summary{max-width:850px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin:28px 0}.metric,.panel,.project-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px}.metric strong{display:block;font-size:28px;letter-spacing:-.8px;margin:8px 0 4px}.metric p{font-size:12px;color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;margin:28px 0}.project-card{display:flex;flex-direction:column;align-items:flex-start;gap:12px}.project-card p{color:var(--muted);overflow-wrap:anywhere}.project-card button{margin-top:auto}.columns{display:grid;grid-template-columns:1.2fr 1fr;gap:22px}.panel{margin-bottom:22px}.row{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;padding:13px 0;border-top:1px solid #edf0eb}.row:first-child{border-top:0}.row-main{min-width:0;overflow-wrap:anywhere}.row small{display:block;color:var(--muted);margin-top:4px}.badge{font-size:11px;font-weight:650;background:#edf0ed;padding:4px 9px;border-radius:20px;white-space:nowrap}.badge.good{color:#166343;background:#e4f0e7}.badge.attention{color:#a43e22;background:#fff0e7}.notice{padding:13px 16px;margin:18px 0;border-left:3px solid var(--accent);background:#fff2e9;overflow-wrap:anywhere}.actions{display:flex;align-items:center;gap:12px;margin:22px 0}.path{font:12px/1.5 ui-monospace,monospace;word-break:break-all}.empty{padding:26px 0;color:var(--muted)}details{font-size:12px;margin-top:8px}details p{white-space:pre-wrap;max-height:220px;overflow:auto}.timeline .row{font-size:13px}.loading{padding:50px;color:var(--muted)}@media(max-width:900px){.layout{grid-template-columns:1fr}aside{padding:16px 24px;gap:12px}.brand{font-size:25px}aside>p,.aside-foot,aside>.label{display:none}nav{display:flex;overflow:auto}.nav-button{white-space:nowrap}.metrics{gap:9px}.metric{padding:15px}.columns{grid-template-columns:1fr}main{padding:25px 22px}.metric strong{font-size:23px}}@media(max-width:520px){.metrics{grid-template-columns:1fr}.top{align-items:flex-start}.local{white-space:nowrap}.metric strong{font-size:26px}.metric p{font-size:13px}}
9
- main,aside,.panel{min-width:0}.actions{flex-wrap:wrap}.table-scroll{overflow-x:auto;max-width:100%}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--line)}select,input{font:inherit;padding:8px;margin:4px;border:1px solid var(--line);border-radius:6px;max-width:100%}</style></head><body><div class="layout"><aside><div class="brand">yoke<span>.</span></div><p>Work you can verify.<br>Projects you can pick up again.</p><div class="label">Your workspace</div><nav id="navigation" aria-label="Projects"></nav><div class="aside-foot"><div class="label">Local workspace</div><p>Project data stays on this machine.</p></div></aside><main><div class="top"><div class="label">Project workspace</div><div><span class="local">● Local session</span> <button id="refresh" class="button">Refresh</button></div></div><div id="content" aria-live="polite"><p class="loading">Loading your projects…</p></div></main></div>
10
- <script nonce="${nonce}">
11
- const sessionToken = ${JSON.stringify(token)};
12
- const content=document.getElementById('content'), navigation=document.getElementById('navigation');
13
- let selected=null, projects=[];
14
- function el(tag,text,cls){const node=document.createElement(tag);if(text!==undefined)node.textContent=String(text);if(cls)node.className=cls;return node}
15
- function append(parent,...children){for(const child of children)parent.append(child);return parent}
16
- function badge(state){return el('span',state||'unknown','badge '+(['complete','passed'].includes(state)?'good':['blocked','failed','paused','unverified'].includes(state)?'attention':''))}
17
- function button(label,action,primary=false){const node=el('button',label,'button'+(primary?' primary':''));node.addEventListener('click',()=>Promise.resolve().then(action).catch(error=>content.append(el('p',error.message,'notice'))));return node}
18
- function duration(ms){if(!Number.isFinite(ms))return 'Unknown';if(ms<60000)return Math.round(ms/1000)+'s';if(ms<3600000)return Math.round(ms/60000)+'m';return (ms/3600000).toFixed(1)+'h'}
19
- function tokenText(value){return Number.isFinite(value)?value.toLocaleString('en-US'):'Unknown'}
20
- function taskTiming(task,estimate){const planned=estimate?.available?estimate.tasks.find(item=>item.storyId===task.id):undefined;return planned?'Predicted '+duration(planned.endMs-planned.startMs)+' · planned start +'+duration(planned.startMs):task.passes?'Completed · timing unknown':'Duration and planned start unknown'}
21
- function unknownCallCount(status){return Number.isFinite(status?.measurement?.unknownCalls)?status.measurement.unknownCalls:Array.isArray(status?.tokens?.calls)?status.tokens.calls.filter(call=>call.usageAvailable!==true).length:undefined}
22
- function renderUsage(project){const status=project.status,usage=status?.tokens,measurement=status?.measurement;const panel=append(el('section',undefined,'panel'),el('h2','Token usage'));panel.append(el('p','Recorded input: '+tokenText(usage?.inputTokens)+' · recorded output: '+tokenText(usage?.outputTokens)));panel.append(el('p','Calls with unknown usage: '+tokenText(unknownCallCount(status))+' · unmeasured attempts: '+tokenText(measurement?.unmeasuredAttempts),'muted'));if(usage?.measurementComplete===false)panel.append(el('p','Partial measurement. These counts are the reported portion; total usage is unknown.','muted'));if(!usage)panel.append(el('p','No token totals have been reported.','muted'));content.append(panel)}
23
- function metric(title,value,note){return append(el('div',undefined,'metric'),el('div',title,'label'),el('strong',value),el('p',note))}
24
- async function api(path,options){const response=await fetch(path,options);const data=await response.json();if(!response.ok)throw Error(data.error||'Request failed');return data}
25
- function nav(){navigation.replaceChildren();const all=el('button','All projects','nav-button');all.setAttribute('aria-current',String(selected===null));all.onclick=()=>{selected=null;renderOverview()};navigation.append(all);for(const project of projects){const item=el('button',project.name,'nav-button');item.setAttribute('aria-current',String(selected===project.id));item.onclick=()=>showProject(project.id);navigation.append(item)}}
26
- function heading(title,subtitle){content.replaceChildren(el('h1',title),el('p',subtitle,'summary muted'))}
27
- function renderOverview(){nav();heading('A clear view of the work.','Goals, independent checks and the next thing that needs your attention.');const blocked=projects.filter(p=>p.errors.length||['blocked','paused'].includes(p.goal?.status||p.status?.state)).length;const active=projects.filter(p=>['active','running'].includes(p.goal?.status||p.status?.state)).length;content.append(append(el('div',undefined,'metrics'),metric('Projects',projects.length,'Registered on this machine'),metric('In progress',active,'Active goals and story loops'),metric('Needs attention',blocked,'Blocked, paused or unavailable')));content.append(button('Compare consumption',showWorkspaceUsage));const cards=el('div',undefined,'cards');for(const p of projects){const card=append(el('article',undefined,'project-card'),badge(p.goal?.status||p.status?.state||(p.errors.length?'unavailable':'ready')),el('h2',p.name),el('p',p.goal?.objective||'No active objective yet.'),el('p',p.root,'path'));if(p.errors.length)card.append(el('p',p.errors.join('; '),'notice'));card.append(button('Open project →',()=>showProject(p.id)));cards.append(card)}if(!projects.length)cards.append(el('p','No registered projects. Run yoke dashboard from a project directory.','empty'));content.append(cards)}
28
- function row(title,note,state){return append(el('div',undefined,'row'),append(el('div',undefined,'row-main'),el('div',title),el('small',note)),badge(state))}
29
-
30
- ${dashboardPanels()}
31
- async function refresh(){try{projects=await api('/api/projects');if(selected)await showProject(selected);else renderOverview()}catch(error){heading('Workspace unavailable',error.message)}}
32
- document.getElementById('refresh').onclick=refresh;refresh();
5
+ return `<!doctype html>
6
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Yoke · Project workspace</title>
7
+ <style nonce="${nonce}">
8
+ :root{color-scheme:light;--ink:#15242d;--muted:#647077;--line:#dce1df;--paper:#f4f5f0;--accent:#c34c27;--green:#167452}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,sans-serif}button{font:inherit;cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #dc734a;outline-offset:3px}.layout{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}aside{background:#142b32;color:#eef5f1;padding:34px 22px;display:flex;flex-direction:column;gap:30px}.brand{font-size:31px;font-weight:780;letter-spacing:-1.5px}.brand span{color:#ffb185}aside p{color:#afc3c6;font-size:13px}.label{text-transform:uppercase;font-size:11px;letter-spacing:1.7px;font-weight:750;color:var(--muted)}aside .label{color:#91a9ad}nav{display:grid;gap:7px}.nav-button{border:0;background:transparent;color:#cad8d9;text-align:left;padding:11px 12px;border-radius:8px;overflow-wrap:anywhere}.nav-button[aria-current=true]{background:#29444b;color:#fff}.aside-foot{margin-top:auto;border-top:1px solid #355057;padding-top:20px}main{max-width:1500px;width:100%;padding:40px 5vw 70px}.top{display:flex;justify-content:space-between;align-items:center;gap:20px}.local{font-size:12px;color:var(--green);background:#e2ece1;border-radius:30px;padding:6px 12px}.button{border:1px solid #bcc7c5;background:#fff;border-radius:7px;padding:9px 15px;color:var(--ink)}.button.primary{background:var(--ink);color:#fff;border-color:var(--ink)}h1{font-size:clamp(27px,3vw,40px);line-height:1.2;margin:17px 0 8px;letter-spacing:-1.4px}h2{font-size:18px;margin:0 0 18px}h3{font-size:16px;margin:0 0 8px}p{margin:6px 0}.muted{color:var(--muted)}.summary{max-width:850px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin:28px 0}.metric,.panel,.project-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px}.metric strong{display:block;font-size:28px;letter-spacing:-.8px;margin:8px 0 4px}.metric p{font-size:12px;color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;margin:28px 0}.project-card{display:flex;flex-direction:column;align-items:flex-start;gap:12px}.project-card p{color:var(--muted);overflow-wrap:anywhere}.project-card button{margin-top:auto}.columns{display:grid;grid-template-columns:1.2fr 1fr;gap:22px}.panel{margin-bottom:22px}.row{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;padding:13px 0;border-top:1px solid #edf0eb}.row:first-child{border-top:0}.row-main{min-width:0;overflow-wrap:anywhere}.row small{display:block;color:var(--muted);margin-top:4px}.badge{font-size:11px;font-weight:650;background:#edf0ed;padding:4px 9px;border-radius:20px;white-space:nowrap}.badge.good{color:#166343;background:#e4f0e7}.badge.attention{color:#a43e22;background:#fff0e7}.notice{padding:13px 16px;margin:18px 0;border-left:3px solid var(--accent);background:#fff2e9;overflow-wrap:anywhere}.actions{display:flex;align-items:center;gap:12px;margin:22px 0}.path{font:12px/1.5 ui-monospace,monospace;word-break:break-all}.empty{padding:26px 0;color:var(--muted)}details{font-size:12px;margin-top:8px}details p{white-space:pre-wrap;max-height:220px;overflow:auto}.timeline .row{font-size:13px}.loading{padding:50px;color:var(--muted)}.overview-tools{display:flex;flex-wrap:wrap;align-items:end;gap:14px;margin:24px 0}.field{display:grid;gap:5px;flex:1 1 260px}.field input{margin:0;width:100%}.filter-group{display:flex;flex-wrap:wrap;gap:7px}.filter-group .button[aria-pressed=true]{background:var(--ink);color:#fff}.state-line{font-size:13px}.comparison{font-weight:650}.chart-gap-note{margin-top:12px}@media(max-width:900px){.layout{grid-template-columns:1fr}aside{padding:16px 24px;gap:12px}.brand{font-size:25px}aside>p,.aside-foot,aside>.label{display:none}nav{display:flex;overflow:auto}.nav-button{white-space:nowrap}.metrics{gap:9px}.metric{padding:15px}.columns{grid-template-columns:1fr}main{padding:25px 22px}.metric strong{font-size:23px}}@media(max-width:520px){.metrics{grid-template-columns:1fr}.top{align-items:flex-start;flex-direction:column}.local{white-space:nowrap}.metric strong{font-size:26px}.metric p{font-size:13px}.overview-tools{align-items:stretch}.filter-group{display:grid;grid-template-columns:1fr}}
9
+ main,aside,.panel{min-width:0}.actions{flex-wrap:wrap}.table-scroll{overflow-x:auto;max-width:100%}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--line)}select,input{font:inherit;padding:8px;margin:4px;border:1px solid var(--line);border-radius:6px;max-width:100%}</style></head><body><div class="layout"><aside><div class="brand">yoke<span>.</span></div><p>Work you can verify.<br>Projects you can pick up again.</p><div class="label">Your workspace</div><nav id="navigation" aria-label="Projects"></nav><div class="aside-foot"><div class="label">Local workspace</div><p>Project data stays on this machine.</p></div></aside><main><div class="top"><div class="label">Project workspace</div><div><span class="local">● Local session</span> <button id="refresh" class="button">Refresh</button></div></div><div id="content" aria-live="polite"><p class="loading">Loading your projects…</p></div></main></div>
10
+ <script nonce="${nonce}">
11
+ const sessionToken = ${JSON.stringify(token)};
12
+ const content=document.getElementById('content'), navigation=document.getElementById('navigation');
13
+ const defaultNavigation={screen:'overview',project:null,view:'now',period:30,group:'day',from:'',to:''};
14
+ let navigationState=parseNavigationHash(typeof location==='undefined'?'':location.hash);
15
+ let selected=navigationState.screen==='project'?navigationState.project:null, projects=[];
16
+ let projectView=navigationState.view,periodDays=navigationState.period,bucket=navigationState.group,customFrom=navigationState.from,customTo=navigationState.to;
17
+ let overviewQuery='',overviewFilter='all',requestVersion=0,activeController=null;
18
+ function el(tag,text,cls){const node=document.createElement(tag);if(text!==undefined)node.textContent=String(text);if(cls)node.className=cls;return node}
19
+ function append(parent,...children){for(const child of children)parent.append(child);return parent}
20
+ function badge(state){return el('span',state||'unknown','badge '+(['complete','passed'].includes(state)?'good':['blocked','failed','paused','unverified','unconfirmed','unavailable'].includes(state)?'attention':''))}
21
+ function button(label,action,primary=false){const node=el('button',label,'button'+(primary?' primary':''));node.addEventListener('click',()=>Promise.resolve().then(action).catch(error=>content.append(el('p',error.message,'notice'))));return node}
22
+ function duration(ms){if(!Number.isFinite(ms))return 'Unknown';if(ms<60000)return Math.round(ms/1000)+'s';if(ms<3600000)return Math.round(ms/60000)+'m';return (ms/3600000).toFixed(1)+'h'}
23
+ function tokenText(value){return Number.isFinite(value)?value.toLocaleString('en-US'):'Unknown'}
24
+ function taskTiming(task,estimate){const planned=estimate?.available?estimate.tasks.find(item=>item.storyId===task.id):undefined;return planned?'Predicted '+duration(planned.endMs-planned.startMs)+' · planned start +'+duration(planned.startMs):task.passes?'Completed · timing unknown':'Duration and planned start unknown'}
25
+ function unknownCallCount(status){return Number.isFinite(status?.measurement?.unknownCalls)?status.measurement.unknownCalls:Array.isArray(status?.tokens?.calls)?status.tokens.calls.filter(call=>call.usageAvailable!==true).length:undefined}
26
+ function renderUsage(project){const status=project.status,usage=status?.tokens,measurement=status?.measurement;const panel=append(el('section',undefined,'panel'),el('h2','Token usage'));panel.append(el('p','Recorded input: '+tokenText(usage?.inputTokens)+' · recorded output: '+tokenText(usage?.outputTokens)));panel.append(el('p','Calls with unknown usage: '+tokenText(unknownCallCount(status))+' · unmeasured attempts: '+tokenText(measurement?.unmeasuredAttempts),'muted'));if(usage?.measurementComplete===false)panel.append(el('p','Partial measurement. These counts are the reported portion; total usage is unknown.','muted'));if(!usage)panel.append(el('p','No token totals have been reported.','muted'));content.append(panel)}
27
+ function metric(title,value,note){return append(el('div',undefined,'metric'),el('div',title,'label'),el('strong',value),el('p',note))}
28
+ function validDate(value){if(!/^\\d{4}-\\d{2}-\\d{2}$/u.test(value))return false;const time=Date.parse(value+'T00:00:00Z');return Number.isFinite(time)&&new Date(time).toISOString().slice(0,10)===value}
29
+ function parseNavigationHash(hash){
30
+ const params=new URLSearchParams(String(hash||'').replace(/^#/u,''));
31
+ const screen=params.get('screen')||'overview',project=params.get('project'),view=params.get('view')||'now';
32
+ const period=Number(params.get('period')||30),group=params.get('group')||'day',from=params.get('from')||'',to=params.get('to')||'';
33
+ const datesValid=(!from&&!to)||(validDate(from)&&validDate(to)&&from<=to&&Date.parse(to+'T00:00:00Z')-Date.parse(from+'T00:00:00Z')<=365*86400000);
34
+ if(!['overview','workspace','project'].includes(screen)||!['now','usage','results'].includes(view)||![1,7,30,90,365].includes(period)||!['day','week','month'].includes(group)||!datesValid||(screen==='project'&&!/^[a-f0-9]{32}$/u.test(project||'')))return {...defaultNavigation};
35
+ return {screen,project:screen==='project'?project:null,view,period,group,from,to};
36
+ }
37
+ function serializeNavigationHash(state){
38
+ const params=new URLSearchParams({screen:state.screen,view:state.view,period:String(state.period),group:state.group});
39
+ if(state.project)params.set('project',state.project);
40
+ if(state.from&&state.to){params.set('from',state.from);params.set('to',state.to)}
41
+ return '#'+params.toString();
42
+ }
43
+ function setNavigation(change,replace=false){
44
+ const next=parseNavigationHash(serializeNavigationHash({...navigationState,...change}));
45
+ navigationState=next;selected=next.screen==='project'?next.project:null;projectView=next.view;periodDays=next.period;bucket=next.group;customFrom=next.from;customTo=next.to;
46
+ if(typeof history!=='undefined'&&typeof location!=='undefined')history[replace?'replaceState':'pushState'](null,'',serializeNavigationHash(next));
47
+ renderRoute();
48
+ }
49
+ function restoreNavigation(){
50
+ const restored=parseNavigationHash(location.hash);
51
+ // Back/forward can emit both popstate and hashchange for one transition.
52
+ if(serializeNavigationHash(restored)===serializeNavigationHash(navigationState))return;
53
+ navigationState=restored;selected=navigationState.screen==='project'?navigationState.project:null;projectView=navigationState.view;periodDays=navigationState.period;bucket=navigationState.group;customFrom=navigationState.from;customTo=navigationState.to;
54
+ if(selected&&!projects.some(project=>project.id===selected)){navigationState={...navigationState,screen:'overview',project:null};selected=null}
55
+ if(typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));
56
+ renderRoute();
57
+ }
58
+ function beginRequest(){requestVersion++;if(activeController)activeController.abort();activeController=new AbortController();return {version:requestVersion,signal:activeController.signal}}
59
+ function currentRequest(version){return version===requestVersion}
60
+ function isAbort(error){return error&&error.name==='AbortError'}
61
+ async function api(path,options={},signal){const response=await fetch(path,{...options,signal});const data=await response.json();if(!response.ok)throw Error(data.error||'Request failed');return data}
62
+ function projectStatus(project,now=Date.now()){
63
+ const goal=project.goal?.status||null,loop=project.status?.state||null,attentionStates=['blocked','failed','paused'],activeStates=['active','running'];
64
+ const observed=(project.status?.supervision||[]).filter(p=>p.liveness==='alive'&&now-Date.parse(p.heartbeatAt)<15000).map(p=>Date.parse(p.lastProgressAt||'')).filter(Number.isFinite);
65
+ const updated=Math.max(Date.parse(project.status?.updatedAt||''),...observed),stale=activeStates.includes(loop)&&Number.isFinite(updated)&&now-updated>20*60000;
66
+ const attention=Boolean(project.errors?.length)||attentionStates.includes(loop)||attentionStates.includes(goal)||stale;
67
+ const active=activeStates.includes(loop)||activeStates.includes(goal);
68
+ let primary=project.errors?.length?'unavailable':attentionStates.includes(loop)?loop:attentionStates.includes(goal)?goal:activeStates.includes(loop)?loop:activeStates.includes(goal)?goal:goal||loop||'ready';
69
+ if(stale&&!project.errors?.length&&!attentionStates.includes(goal))primary='unconfirmed';
70
+ return {primary,goal,loop,attention,active,stale};
71
+ }
72
+ function visibleProjects(values,query,filter,now=Date.now()){
73
+ const needle=query.trim().toLowerCase();
74
+ return values.filter(project=>{
75
+ const status=projectStatus(project,now),matches=!needle||[project.name,project.root,project.goal?.objective].some(value=>String(value||'').toLowerCase().includes(needle));
76
+ return matches&&(filter==='all'||filter==='active'&&status.active||filter==='attention'&&status.attention);
77
+ }).map((project,index)=>({project,index,status:projectStatus(project,now)})).sort((a,b)=>Number(b.status.attention)-Number(a.status.attention)||Number(b.status.active)-Number(a.status.active)||a.index-b.index).map(item=>item.project);
78
+ }
79
+ function nav(){
80
+ navigation.replaceChildren();
81
+ const all=el('button','All projects','nav-button');all.setAttribute('aria-current',String(navigationState.screen==='overview'));all.onclick=()=>setNavigation({screen:'overview',project:null});navigation.append(all);
82
+ const compare=el('button','Compare consumption','nav-button');compare.setAttribute('aria-current',String(navigationState.screen==='workspace'));compare.onclick=()=>setNavigation({screen:'workspace',project:null});navigation.append(compare);
83
+ for(const project of projects){const item=el('button',project.name,'nav-button');item.setAttribute('aria-current',String(navigationState.screen==='project'&&selected===project.id));item.onclick=()=>setNavigation({screen:'project',project:project.id});navigation.append(item)}
84
+ }
85
+ function heading(title,subtitle){content.replaceChildren(el('h1',title),el('p',subtitle,'summary muted'))}
86
+ function renderOverview(){
87
+ beginRequest();nav();heading('A clear view of the work.','Goals, loops, independent checks and the next thing that needs your attention.');
88
+ const statuses=projects.map(project=>projectStatus(project)),attention=statuses.filter(status=>status.attention).length,active=statuses.filter(status=>status.active).length;
89
+ content.append(append(el('div',undefined,'metrics'),metric('Projects',projects.length,'Registered on this machine'),metric('In progress',active,'Active goals or reported loops'),metric('Needs attention',attention,'Blocked, paused, stale or unavailable')));
90
+ const tools=el('div',undefined,'overview-tools'),field=el('label',undefined,'field'),searchLabel=el('span','Search projects','label'),search=el('input');search.type='search';search.value=overviewQuery;search.placeholder='Name, path or objective';field.append(searchLabel,search);tools.append(field);
91
+ const filters=el('div',undefined,'filter-group');filters.setAttribute('role','group');filters.setAttribute('aria-label','Project status filter');
92
+ const cards=el('div',undefined,'cards');
93
+ function draw(){
94
+ cards.replaceChildren();
95
+ const visible=visibleProjects(projects,overviewQuery,overviewFilter);
96
+ for(const project of visible){const status=projectStatus(project),card=append(el('article',undefined,'project-card'),badge(status.primary),el('h2',project.name),el('p',project.goal?.objective||'No active objective yet.'),el('p',project.root,'path'));
97
+ if(status.goal&&status.loop&&status.goal!==status.loop)card.append(el('p','Goal: '+status.goal+' · Loop: '+(status.stale?'unconfirmed (last reported '+status.loop+')':status.loop),'state-line'));
98
+ else if(status.stale)card.append(el('p','Loop activity is unconfirmed; last report was '+status.loop+'.','state-line notice'));
99
+ if(['active','running'].includes(status.loop)&&project.status?.storyTitle)card.append(el('p','Reported task: '+project.status.storyTitle,'state-line'));
100
+ const reason=['blocked','failed','paused'].includes(status.loop)?project.status?.reason:['blocked','paused'].includes(status.goal)?project.goal?.reason:null;
101
+ if(reason)card.append(el('p',reason,'notice'));
102
+ if(project.errors.length)card.append(el('p',project.errors.join('; '),'notice'));
103
+ card.append(button('Open project →',()=>setNavigation({screen:'project',project:project.id})));cards.append(card);
104
+ }
105
+ if(!visible.length){const empty=append(el('div',undefined,'empty'),el('p',projects.length?'No projects match this search and filter.':'No registered projects. Run yoke dashboard from a project directory.'));if(projects.length)empty.append(button('Clear search and filters',()=>{overviewQuery='';overviewFilter='all';renderOverview()}));cards.append(empty)}
106
+ for(const child of filters.children)child.setAttribute('aria-pressed',String(child.dataset.filter===overviewFilter));
107
+ }
108
+ for(const [value,label] of [['all','All'],['active','Active'],['attention','Needs attention']]){const control=button(label,()=>{overviewFilter=value;draw()});control.dataset.filter=value;filters.append(control)}
109
+ search.oninput=()=>{overviewQuery=search.value;draw()};tools.append(filters);content.append(tools,cards);draw();
110
+ }
111
+ function row(title,note,state){return append(el('div',undefined,'row'),append(el('div',undefined,'row-main'),el('div',title),el('small',note)),badge(state))}
112
+
113
+ ${dashboardPanels()}
114
+ function renderRoute(){if(navigationState.screen==='workspace')showWorkspaceUsage();else if(navigationState.screen==='project'&&selected&&projects.some(project=>project.id===selected))showProject(selected);else renderOverview()}
115
+ async function refresh(){
116
+ const request=beginRequest();
117
+ try{
118
+ const loaded=await api('/api/projects',{},request.signal);if(!currentRequest(request.version))return;projects=loaded;
119
+ if(selected&&!projects.some(project=>project.id===selected)){navigationState={...navigationState,screen:'overview',project:null};selected=null}
120
+ if(typeof location!=='undefined'&&typeof history!=='undefined'&&location.hash!==serializeNavigationHash(navigationState))history.replaceState(null,'',serializeNavigationHash(navigationState));
121
+ renderRoute();
122
+ }catch(error){if(currentRequest(request.version)&&!isAbort(error))heading('Workspace unavailable',error.message)}
123
+ }
124
+ document.getElementById('refresh').onclick=refresh;
125
+ if(typeof window!=='undefined'){window.addEventListener('popstate',restoreNavigation);window.addEventListener('hashchange',restoreNavigation)}
126
+ refresh();
33
127
  </script></body></html>`;
34
128
  }
@@ -1,19 +1,95 @@
1
1
  /** Static script; all project-controlled values are inserted through textContent. */
2
2
  export function dashboardPanels() {
3
- return String.raw `
4
- let projectView='now', periodDays=30, bucket='day', customFrom='', customTo='', requestVersion=0;
5
- function panel(title){return append(el('section',undefined,'panel'),el('h2',title))}
6
- function table(headers,rows){const wrap=el('div',undefined,'table-scroll'),t=el('table'),head=el('thead'),hr=el('tr');for(const title of headers)hr.append(el('th',title));head.append(hr);t.append(head);const body=el('tbody');for(const values of rows){const r=el('tr');for(const value of values)r.append(el('td',String(value)));body.append(r)}t.append(body);wrap.append(t);return wrap}
7
- function costText(t){return t.costState==='unknown'?'Unknown':'$'+t.reportedCostUsd.toFixed(3)+(t.costState==='partial'?' reported (partial)':' reported')}
8
- function rate(value){return Number.isFinite(value)?value.toLocaleString('en-US',{maximumFractionDigits:1}):'Unknown'}
9
- function usageChart(buckets){const data=buckets.slice(-31);const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 800 220');svg.setAttribute('role','img');svg.setAttribute('aria-label','Recorded input and output tokens over the latest '+data.length+' periods; exact values in the following table');const max=Math.max(1,...data.map(d=>d.inputTokens+d.outputTokens));const step=760/Math.max(1,data.length);data.forEach((d,i)=>{let y=180;for(const [value,color] of [[d.inputTokens,'#29444b'],[d.outputTokens,'#c34c27']]){const h=value/max*160;y-=h;const r=document.createElementNS(svg.namespaceURI,'rect');for(const [k,v] of Object.entries({x:20+i*step,y,width:Math.max(1,step-4),height:h,fill:color}))r.setAttribute(k,String(v));const title=document.createElementNS(svg.namespaceURI,'title');title.textContent=d.label+': input '+d.inputTokens+', output '+d.outputTokens;r.append(title);svg.append(r)}if(i===0||i===data.length-1){const label=document.createElementNS(svg.namespaceURI,'text');label.setAttribute('x',String(i===0?20:780));label.setAttribute('y','205');label.setAttribute('text-anchor',i===0?'start':'end');label.setAttribute('font-size','12');label.textContent=d.label;svg.append(label)}});return svg}
10
- async function showWorkspaceUsage(){selected=null;const version=++requestVersion;nav();heading('Compare project consumption','Recorded usage in the selected period; missing measurements remain unknown.');periodControls(null);const p=panel('Projects');p.append(el('p','Loading…','muted'));content.append(p);const rows=[];const query=queryPeriod();for(const project of projects){try{const a=await api('/api/projects/'+project.id+'/analytics?'+query);rows.push([project.name,tokenText(a.total.inputTokens),tokenText(a.total.outputTokens),costText(a.total),a.total.accepted,a.total.unknownCalls,a.errors.length?'Partial history':'Recorded portion'])}catch(error){rows.push([project.name,'Unknown','Unknown','Unknown','Unknown','Unknown',error.message])}if(version!==requestVersion||selected!==null)return}p.replaceChildren(el('h2','Projects'),table(['Project','Input','Output','Cost','Accepted','Unknown calls','Coverage'],rows))}
11
- function queryPeriod(){const end=customTo?new Date(customTo+'T00:00:00Z').getTime()+86400000:Date.now();const start=customFrom?Date.parse(customFrom+'T00:00:00Z'):end-periodDays*86400000;return new URLSearchParams({from:new Date(start).toISOString(),to:new Date(end).toISOString(),bucket}).toString()}
12
- function periodControls(id){const p=panel('Period · UTC');const select=el('select');select.setAttribute('aria-label','Period');for(const [value,label] of [[1,'Last 24 hours'],[7,'Last 7 days'],[30,'Last 30 days'],[90,'Last 90 days'],[365,'Last 365 days']]){const o=el('option',label);o.value=String(value);o.selected=value===periodDays;select.append(o)}select.onchange=()=>{periodDays=Number(select.value);customFrom='';customTo='';(id?showProject(id):showWorkspaceUsage())};p.append(select);const grouping=el('select');grouping.setAttribute('aria-label','Group by');for(const value of ['day','week','month']){const o=el('option',value);o.value=value;o.selected=value===bucket;grouping.append(o)}grouping.onchange=()=>{bucket=grouping.value;(id?showProject(id):showWorkspaceUsage())};p.append(grouping);const from=el('input'),to=el('input');from.type=to.type='date';from.value=customFrom;to.value=customTo;from.setAttribute('aria-label','From date UTC');to.setAttribute('aria-label','Through date UTC');p.append(from,to,button('Apply dates',()=>{if(!from.value||!to.value||from.value>to.value){p.append(el('p','Choose both dates in chronological order.','notice'));return}customFrom=from.value;customTo=to.value;(id?showProject(id):showWorkspaceUsage())}));p.append(el('p','Calendar dates include the entire end date. Maximum range: 366 days.','muted'));content.append(p)}
13
- function renderNow(p){const status=p.status||{},goal=p.goal||{},workers=status.parallel?.workers||[];const age=status.updatedAt?Date.now()-Date.parse(status.updatedAt):undefined;const stale=Number.isFinite(age)&&age>20*60000;const summary=panel('Now');summary.append(badge(goal.status||status.state||'unknown'));summary.append(el('p',goal.reason||status.reason||'No reported blocker.'));summary.append(el('p','Last status: '+(status.updatedAt||'Unknown')+(stale?' · stale; activity is not confirmed':''),'muted'));summary.append(el('p','Reported workers: '+workers.length+' / '+(status.parallel?.maxConcurrency||1)+' · queue: '+tokenText(status.parallel?.queuedCandidates),'muted'));if(goal.pendingAttempt)summary.append(row('Goal attempt',goal.pendingAttempt.provider+' · requested model '+(goal.pendingAttempt.model||'provider default')+' · started '+goal.pendingAttempt.startedAt,'running'));if(status.execution&&status.state==='running')summary.append(el('p',status.execution.provider+' · requested model '+(status.execution.requestedModel||'provider default')+' · elapsed '+duration(Date.now()-Date.parse(status.execution.startedAt))));if(status.story&&!workers.length)summary.append(row(status.storyTitle||status.story,status.phase||'Phase unknown',status.state));for(const w of workers)summary.append(row(w.storyTitle||w.story,[w.selectedProvider||w.provider,'requested model '+(w.selectedModel||w.model||'provider default'),w.phase||w.lifecycle||'working',w.startedAt?'elapsed '+duration(Date.now()-Date.parse(w.startedAt)):null,w.quality?'repair '+w.quality.usedRepairs:null].filter(Boolean).join(' · '),w.lifecycle||'running'));if(status.parallel?.integrator){const w=status.parallel.integrator;summary.append(row('Integration: '+(w.storyTitle||w.story),w.phase||'checking','integrating'))}if(!workers.length&&!status.story&&!goal.pendingAttempt)summary.append(el('p','No task currently reported.','empty'));summary.append(el('p','Status refreshes every 5 seconds. Requested models are not proof of the model actually used; reported model identities appear under Usage & time.','muted'));content.append(summary);const tasks=panel('Tasks');for(const task of p.stories||[])tasks.append(row(task.title,task.id+(task.needs?.length?' · after '+task.needs.join(', '):'')+' · '+taskTiming(task,p.estimate),task.passes?'passed':workers.some(w=>w.story===task.id)?'running':'open'));content.append(tasks);const routing=panel('Model selection');for(const [id,d] of Object.entries(status.routingDecisions||{})){routing.append(row(id,d.provider+' / '+(d.model||'provider default')+(d.reasoningEffort?' · '+d.reasoningEffort:''),d.profile));routing.append(el('p',d.reason));routing.append(el('p','Next escalation: '+d.next,'muted'))}if(!Object.keys(status.routingDecisions||{}).length)routing.append(el('p','No recorded model selection for this run.','empty'));content.append(routing);const activity=panel('Recent activity');for(const e of (p.events||[]).slice(-16).reverse())activity.append(row(e.storyId||e.type,e.timestamp+(e.phase?' · '+e.phase:'')+(Number.isFinite(e.durationMs)?' · '+duration(e.durationMs):''),e.outcome||e.type));content.append(activity)}
14
- function renderUsageHistory(a){const t=a.total;content.append(append(el('div',undefined,'metrics'),metric('Recorded input',tokenText(t.inputTokens),'Output: '+tokenText(t.outputTokens)),metric('Reported cost',costText(t),'Missing charges are not estimated'),metric('Tokens / elapsed minute',rate(t.tokensPerElapsedMinute),'Input + output / entire selected period')));const notes=panel('Measurement coverage');notes.append(el('p',a.coverage));notes.append(el('p','First record in period: '+(a.earliest||'none')+' · latest: '+(a.latest||'none'),'muted'));notes.append(el('p','Measured calls: '+t.measuredCalls+' · calls with unknown usage: '+t.unknownCalls+' · unmeasured attempts: '+t.unmeasuredAttempts));notes.append(el('p','Tokens / recorded call minute: '+rate(t.tokensPerCallMinute)+' · summed call time: '+duration(t.callDurationMs)+'. Parallel calls overlap; this is consumption intensity, not generation speed.'));notes.append(el('p','Cache reads: '+tokenText(t.cachedInputTokens)+' · cache writes: '+tokenText(t.cacheWriteInputTokens)+'. These are reported categories and are not added again to input totals.','muted'));for(const error of a.errors)notes.append(el('p',error,'notice'));content.append(notes);const series=panel('Consumption over time');series.append(usageChart(a.buckets));series.append(el('p','Dark: input · orange: output. Chart shows up to 31 latest periods; table includes all measured periods.','muted'));series.append(table(['UTC '+a.bucket,'Input','Output','Cache reads','Reported cost'],a.buckets.map(b=>[b.label,tokenText(b.inputTokens),tokenText(b.outputTokens),tokenText(b.cachedInputTokens),costText(b)])));if(!a.buckets.length)series.append(el('p','No measurements in this period.','empty'));content.append(series);const models=panel('Reported models in this period');models.append(table(['Provider','Actual model','Role','Input','Output','Call time','Cost'],a.models.map(m=>[m.provider,m.model,m.role,tokenText(m.inputTokens),tokenText(m.outputTokens),duration(m.callDurationMs),costText(m)])));content.append(models);const modelTimeline=panel('Models over time');modelTimeline.append(table(['UTC '+a.bucket,'Provider','Actual model','Role','Input','Output'],a.modelBuckets.map(m=>[m.label,m.provider,m.model,m.role,tokenText(m.inputTokens),tokenText(m.outputTokens)])));content.append(modelTimeline);const tasks=panel('Consumption by task');tasks.append(table(['Task','Input','Output','Attempt time','Cost'],a.tasks.map(t=>[t.storyId,tokenText(t.inputTokens),tokenText(t.outputTokens),duration(t.attemptDurationMs),costText(t)])));content.append(tasks);const phases=panel('Recorded phase time');phases.append(table(['Phase','Summed duration'],a.phases.map(p=>[p.phase,duration(p.durationMs)])));phases.append(el('p','Overlapping worker time is summed. Unrecorded queue time and human waiting time remain unknown.','muted'));content.append(phases)}
15
- function renderResults(p,a){const t=a.total;content.append(append(el('div',undefined,'metrics'),metric('Accepted in period',t.accepted,'Recorded acceptance events'),metric('Repair phases',t.repairs,'Routing escalations: '+t.escalations),metric('Tokens / acceptance',rate(t.tokensPerAccepted),'Reported input + output, including failed work')));const attempts=panel('Measured outcomes');attempts.append(el('p','Ended attempts: '+t.attempts+' · explicitly successful attempts: '+t.successfulAttempts+' · average summed attempt time per acceptance: '+duration(t.timePerAcceptedMs)));attempts.append(el('p','Worker termination alone is not counted as successful acceptance. Rates only describe recorded activity in the selected period.','muted'));attempts.append(table(['Task','Attempts','Accepted','Repairs','Escalations','Tokens / acceptance'],a.tasks.map(v=>[v.storyId,v.attempts,v.accepted,v.repairs,v.escalations,rate(v.tokensPerAccepted)])));content.append(attempts);const checks=panel('Latest saved acceptance evidence');if(p.check){checks.append(el('p',p.check.summary));for(const c of p.check.criteria){const item=row(c.text,c.id,c.status);item.firstChild.append(append(el('details'),el('summary','View evidence'),el('p',c.summary)));checks.append(item)}checks.append(el('p','Checked '+p.check.generatedAt+' · historical evidence; independent of the selected statistics period.','muted'))}else checks.append(el('p','No saved independent check.','empty'));content.append(checks);const goals=panel('Saved goal attempts');for(const attempt of p.goal?.attempts||[])goals.append(row(attempt.provider||'Agent',(attempt.summary||'')+' · '+duration(attempt.durationMs)+' · input '+tokenText(attempt.inputTokens)+' / output '+tokenText(attempt.outputTokens),attempt.success?'finished':'failed'));content.append(goals)}
16
- async function showProject(id){const version=++requestVersion;selected=id;nav();try{const p=await api('/api/projects/'+id);const a=projectView==='now'?null:await api('/api/projects/'+id+'/analytics?'+queryPeriod());if(version!==requestVersion||selected!==id)return;heading(p.name,p.goal?.objective||'Saved project work');content.append(el('p',p.root,'path'));for(const error of p.errors)content.append(el('p',error,'notice'));const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results']]){const b=button(label,()=>{projectView=view;showProject(id)},projectView===view);b.setAttribute('aria-pressed',String(projectView===view));tabs.append(b)}if(p.goal&&!['complete','paused'].includes(p.goal.status))tabs.append(button('Request pause',async()=>{await api('/api/projects/'+id+'/pause',{method:'POST',headers:{'x-yoke-token':sessionToken}});showProject(id)}));content.append(tabs);if(projectView==='now'){const tasks=p.stories||[];content.append(append(el('div',undefined,'metrics'),metric('Accepted tasks',tasks.filter(t=>t.passes).length+' / '+tasks.length,'Saved acceptance state'),metric('Remaining time',p.estimate?.available?duration(p.estimate.lowerMs)+' – '+duration(p.estimate.upperMs):'Unknown',p.estimate?.available?p.estimate.sampleCount+' samples · empirical range':'No reliable duration history'),metric('Project status',p.goal?.status||p.status?.state||'unknown','Latest reported state')));renderNow(p)}else{periodControls(id);if(projectView==='usage')renderUsageHistory(a);else renderResults(p,a)}}catch(error){if(version===requestVersion&&selected===id)heading('Project unavailable',error.message)}}
17
- if(typeof setInterval==='function')setInterval(()=>{if(selected&&projectView==='now'&&typeof document.hidden!=='undefined'&&!document.hidden&&!['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName)&&!document.activeElement?.matches?.(':focus-visible'))showProject(selected)},5000);
3
+ return String.raw `
4
+ function panel(title){return append(el('section',undefined,'panel'),el('h2',title))}
5
+ function table(headers,rows){const wrap=el('div',undefined,'table-scroll'),t=el('table'),head=el('thead'),hr=el('tr');for(const title of headers)hr.append(el('th',title));head.append(hr);t.append(head);const body=el('tbody');for(const values of rows){const r=el('tr');for(const value of values)r.append(el('td',String(value)));body.append(r)}t.append(body);wrap.append(t);return wrap}
6
+ function costText(t){return t.costState==='unknown'?'Unknown':'$'+t.reportedCostUsd.toFixed(3)+(t.costState==='partial'?' reported (partial)':' reported')}
7
+ function historyCost(total,errors){return errors.length&&total.costState==='measured'?{...total,costState:'partial'}:total}
8
+ function rate(value){return Number.isFinite(value)?value.toLocaleString('en-US',{maximumFractionDigits:1}):'Unknown'}
9
+ function comparisonText(current,previous){if(previous===0)return current===0?'No change; both periods recorded 0':'New recorded activity; previous period recorded 0';const change=(current-previous)/previous*100;return (change>0?'+':'')+change.toFixed(1)+'% vs previous period'}
10
+ function costComparisonText(current,previous){if(current.costState!=='measured'||previous.costState!=='measured')return 'Percentage unavailable because cost coverage is incomplete in one or both periods';return comparisonText(current.reportedCostUsd,previous.reportedCostUsd)}
11
+ function usageCoverage(total){const missing=total.unknownCalls+total.unmeasuredAttempts;return missing?missing+' call'+(missing===1?'':'s')+' or attempt'+(missing===1?'':'s')+' missing usage; totals are recorded portions':'No missing usage recorded in this period'}
12
+ function periodWindows(now=Date.now(),days=periodDays,from=customFrom,to=customTo){const end=to?Date.parse(to+'T00:00:00Z')+86400000:now,start=from?Date.parse(from+'T00:00:00Z'):end-days*86400000,duration=end-start;return {current:{from:start,to:end},previous:{from:start-duration,to:start}}}
13
+ function queryPeriod(range){return new URLSearchParams({from:new Date(range.from).toISOString(),to:new Date(range.to).toISOString(),bucket}).toString()}
14
+ async function mapLimited(items,limit,shouldContinue,worker){
15
+ const results=[],next={value:0};
16
+ async function run(){while(shouldContinue()&&next.value<items.length){const index=next.value++;const value=await worker(items[index],index);results[index]=value}}
17
+ await Promise.all(Array.from({length:Math.min(limit,items.length)},run));
18
+ return results.filter(value=>value!==undefined);
19
+ }
20
+ function usageChart(buckets){const data=buckets.slice(-31);const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 800 220');svg.setAttribute('role','img');svg.setAttribute('aria-label','Recorded input and output tokens over the latest '+data.length+' non-empty periods; omitted periods mean no recorded activity, not measured zero');const max=Math.max(1,...data.map(d=>d.inputTokens+d.outputTokens));const step=760/Math.max(1,data.length);data.forEach((d,i)=>{let y=180;for(const [value,color] of [[d.inputTokens,'#29444b'],[d.outputTokens,'#c34c27']]){const h=value/max*160;y-=h;const r=document.createElementNS(svg.namespaceURI,'rect');for(const [k,v] of Object.entries({x:20+i*step,y,width:Math.max(1,step-4),height:h,fill:color}))r.setAttribute(k,String(v));const title=document.createElementNS(svg.namespaceURI,'title');title.textContent=d.label+': input '+d.inputTokens+', output '+d.outputTokens;r.append(title);svg.append(r)}if(i===0||i===data.length-1){const label=document.createElementNS(svg.namespaceURI,'text');label.setAttribute('x',String(i===0?20:780));label.setAttribute('y','205');label.setAttribute('text-anchor',i===0?'start':'end');label.setAttribute('font-size','12');label.textContent=d.label;svg.append(label)}});return svg}
21
+ async function showWorkspaceUsage(){
22
+ const request=beginRequest();selected=null;nav();heading('Compare project consumption','Recorded usage in the selected period; missing measurements remain unknown.');periodControls(null);const p=panel('Projects');p.append(el('p','Loading…','muted'));content.append(p);
23
+ const bounds=periodWindows(Date.now()),query=queryPeriod(bounds.current),valid=()=>currentRequest(request.version)&&navigationState.screen==='workspace'&&!request.signal.aborted;
24
+ const rows=await mapLimited(projects,3,valid,async project=>{try{const a=await api('/api/projects/'+project.id+'/analytics?'+query,{},request.signal);return [project.name,tokenText(a.total.inputTokens),tokenText(a.total.outputTokens),costText(a.total),a.total.accepted,a.total.unknownCalls+a.total.unmeasuredAttempts,a.errors.length?'Partial history':usageCoverage(a.total)]}catch(error){if(isAbort(error))return undefined;return [project.name,'Unknown','Unknown','Unknown','Unknown','Unknown',error.message]}});
25
+ if(valid())p.replaceChildren(el('h2','Projects'),table(['Project','Input','Output','Cost','Accepted','Missing usage','Coverage'],rows));
26
+ }
27
+ function periodControls(id){
28
+ const p=panel('Period · UTC'),select=el('select');select.setAttribute('aria-label','Period');
29
+ for(const [value,label] of [[1,'Last 24 hours'],[7,'Last 7 days'],[30,'Last 30 days'],[90,'Last 90 days'],[365,'Last 365 days']]){const option=el('option',label);option.value=String(value);option.selected=value===periodDays;select.append(option)}
30
+ select.onchange=()=>setNavigation({period:Number(select.value),from:'',to:''});p.append(select);
31
+ const grouping=el('select');grouping.setAttribute('aria-label','Group by');for(const value of ['day','week','month']){const option=el('option',value);option.value=value;option.selected=value===bucket;grouping.append(option)}grouping.onchange=()=>setNavigation({group:grouping.value});p.append(grouping);
32
+ const from=el('input'),to=el('input');from.type=to.type='date';from.value=customFrom;to.value=customTo;from.setAttribute('aria-label','From date UTC');to.setAttribute('aria-label','Through date UTC');
33
+ p.append(from,to,button('Apply dates',()=>{if(!validDate(from.value)||!validDate(to.value)||from.value>to.value||Date.parse(to.value+'T00:00:00Z')-Date.parse(from.value+'T00:00:00Z')>365*86400000){p.append(el('p','Choose both dates in chronological order within 366 days.','notice'));return}setNavigation({from:from.value,to:to.value})}));
34
+ p.append(el('p','Calendar dates include the entire end date. Maximum range: 366 days.','muted'));content.append(p);
35
+ }
36
+ function renderNow(p){
37
+ const status=p.status||{},goal=p.goal||{},workers=status.parallel?.workers||[],state=projectStatus(p),reportedState=state.stale?'unconfirmed':status.state;
38
+ const summary=panel('Now');summary.append(badge(state.primary));
39
+ if(state.goal&&state.loop&&state.goal!==state.loop)summary.append(el('p','Goal: '+state.goal+' · Loop: '+(state.stale?'unconfirmed (last reported '+state.loop+')':state.loop),'state-line'));
40
+ summary.append(el('p',status.reason||goal.reason||'No reported blocker.'));
41
+ summary.append(el('p','Last status: '+(status.updatedAt||'Unknown')+(state.stale?' · stale; activity is not confirmed':''),'muted'));
42
+ if(status.progress)summary.append(el('p','Backlog: '+status.progress.passed+'/'+status.progress.total+' tasks accepted · not overall product completion','muted'));
43
+ for(const proc of status.supervision||[])summary.append(row('Provider PID '+(proc.childPid||'not started'),['Attempt: '+((proc.retry||0)+1),'Supervisor heartbeat: '+proc.heartbeatAt,'Last output: '+(proc.lastOutputAt||'none'),'Last successful tool/edit: '+(proc.lastProgressAt||'none'),proc.reason].filter(Boolean).join(' · '),proc.state+' · '+(proc.liveness||'identity unverified')));
44
+ summary.append(el('p','Reported workers: '+workers.length+' / '+(status.parallel?.maxConcurrency||1)+' · queue: '+tokenText(status.parallel?.queuedCandidates),'muted'));
45
+ if(goal.pendingAttempt)summary.append(row('Goal attempt',goal.pendingAttempt.provider+' · requested model '+(goal.pendingAttempt.model||'provider default')+' · started '+goal.pendingAttempt.startedAt,state.stale?'unconfirmed':'running'));
46
+ if(status.execution&&status.state==='running')summary.append(el('p',state.stale?'Execution was reported but is now unconfirmed.':status.execution.provider+' · requested model '+(status.execution.requestedModel||'provider default')+' · elapsed '+duration(Date.now()-Date.parse(status.execution.startedAt))));
47
+ if(status.story&&!workers.length)summary.append(row(status.storyTitle||status.story,status.phase||'Phase unknown',reportedState));
48
+ for(const worker of workers)summary.append(row(worker.storyTitle||worker.story,[worker.selectedProvider||worker.provider,'requested model '+(worker.selectedModel||worker.model||'provider default'),worker.phase||worker.lifecycle||'working',worker.startedAt?(state.stale?'last started '+worker.startedAt:'elapsed '+duration(Date.now()-Date.parse(worker.startedAt))):null,worker.quality?'repair '+worker.quality.usedRepairs:null].filter(Boolean).join(' · '),state.stale?'unconfirmed':worker.lifecycle||'running'));
49
+ if(status.parallel?.integrator){const worker=status.parallel.integrator;summary.append(row('Integration: '+(worker.storyTitle||worker.story),worker.phase||'checking',state.stale?'unconfirmed':'integrating'))}
50
+ if(!workers.length&&!status.story&&!goal.pendingAttempt)summary.append(el('p','No task currently reported.','empty'));
51
+ summary.append(el('p','Status refreshes every 5 seconds. Requested models are not proof of the model actually used; reported model identities appear under Usage & time.','muted'));content.append(summary);
52
+ const tasks=panel('Tasks');for(const task of p.stories||[])tasks.append(row(task.title,task.id+(task.needs?.length?' · after '+task.needs.join(', '):'')+' · '+taskTiming(task,p.estimate),task.passes?'passed':workers.some(worker=>worker.story===task.id)?state.stale?'unconfirmed':'running':'open'));content.append(tasks);
53
+ const routing=panel('Model selection');for(const [id,decision] of Object.entries(status.routingDecisions||{})){routing.append(row(id,decision.provider+' / '+(decision.model||'provider default')+(decision.reasoningEffort?' · '+decision.reasoningEffort:''),decision.profile));routing.append(el('p',decision.reason));routing.append(el('p','Next escalation: '+decision.next,'muted'))}if(!Object.keys(status.routingDecisions||{}).length)routing.append(el('p','No recorded model selection for this run.','empty'));content.append(routing);
54
+ const activity=panel('Recent activity');for(const event of (p.events||[]).slice(-16).reverse())activity.append(row(event.storyId||event.type,event.timestamp+(event.phase?' · '+event.phase:'')+(Number.isFinite(event.durationMs)?' · '+duration(event.durationMs):''),event.outcome||event.type));content.append(activity);
55
+ }
56
+ function renderUsageHistory(current,previous){
57
+ const total=current.total,prior=previous.total,currentTokens=total.inputTokens+total.outputTokens,previousTokens=prior.inputTokens+prior.outputTokens;
58
+ const currentCoverage=usageCoverage(total)+(current.errors.length?' · measurement history is incomplete':''),previousCoverage=usageCoverage(prior)+(previous.errors.length?' · measurement history is incomplete':'');
59
+ const currentCost=historyCost(total,current.errors),previousCost=historyCost(prior,previous.errors);
60
+ const comparison=panel('Compared with the immediately preceding equal period');
61
+ const boundary=value=>new Date(value).toISOString().slice(0,16).replace('T',' ');
62
+ comparison.append(el('p','Selected: '+boundary(current.from)+' → '+boundary(current.to)+' UTC','muted'),el('p','Previous: '+boundary(previous.from)+' → '+boundary(previous.to)+' UTC · end boundaries excluded','muted'));
63
+ comparison.append(append(el('div',undefined,'metrics'),metric('Recorded tokens',tokenText(currentTokens),'Previous: '+tokenText(previousTokens)+' · '+comparisonText(currentTokens,previousTokens)+' · '+currentCoverage),metric('Accepted tasks',tokenText(total.accepted),'Previous: '+tokenText(prior.accepted)+' · '+comparisonText(total.accepted,prior.accepted)+' · recorded acceptance events'),metric('Reported cost',costText(currentCost),'Previous: '+costText(previousCost)+' · '+costComparisonText(currentCost,previousCost))));
64
+ comparison.append(el('p','Current coverage: '+currentCoverage+'. Previous coverage: '+previousCoverage+'. Missing measurements are not estimated and recorded totals are not treated as complete totals.','muted'));for(const error of previous.errors)comparison.append(el('p','Previous period: '+error,'notice'));content.append(comparison);
65
+ content.append(append(el('div',undefined,'metrics'),metric('Recorded input',tokenText(total.inputTokens),'Output: '+tokenText(total.outputTokens)),metric('Reported cost',costText(currentCost),'Missing charges are not estimated'),metric('Tokens / elapsed minute',rate(total.tokensPerElapsedMinute),'Input + output / entire selected period')));
66
+ const notes=panel('Measurement coverage');notes.append(el('p',current.coverage));notes.append(el('p','First record in period: '+(current.earliest||'none')+' · latest: '+(current.latest||'none'),'muted'));notes.append(el('p','Measured calls: '+total.measuredCalls+' · calls with unknown usage: '+total.unknownCalls+' · unmeasured attempts: '+total.unmeasuredAttempts));notes.append(el('p','Tokens / recorded call minute: '+rate(total.tokensPerCallMinute)+' · summed call time: '+duration(total.callDurationMs)+'. Parallel calls overlap; this is consumption intensity, not generation speed.'));notes.append(el('p','Cache reads: '+tokenText(total.cachedInputTokens)+' · cache writes: '+tokenText(total.cacheWriteInputTokens)+'. These are reported categories and are not added again to input totals.','muted'));for(const error of current.errors)notes.append(el('p',error,'notice'));content.append(notes);
67
+ const series=panel('Consumption over time');series.append(usageChart(current.buckets));series.append(el('p','Dark: input · orange: output. Chart shows up to 31 latest non-empty periods; the table includes every recorded period. Periods with no recorded events are omitted, so calendar gaps mean no recorded activity and do not assert a measured zero.','muted chart-gap-note'));series.append(table(['UTC '+current.bucket,'Input','Output','Cache reads','Reported cost'],current.buckets.map(value=>[value.label,tokenText(value.inputTokens),tokenText(value.outputTokens),tokenText(value.cachedInputTokens),costText(value)])));if(!current.buckets.length)series.append(el('p','No recorded activity in this period; measurement coverage may still be incomplete.','empty'));content.append(series);
68
+ const models=panel('Reported models in this period');models.append(table(['Provider','Actual model','Role','Input','Output','Call time','Cost'],current.models.map(value=>[value.provider,value.model,value.role,tokenText(value.inputTokens),tokenText(value.outputTokens),duration(value.callDurationMs),costText(value)])));content.append(models);
69
+ const modelTimeline=panel('Models over time');modelTimeline.append(table(['UTC '+current.bucket,'Provider','Actual model','Role','Input','Output'],current.modelBuckets.map(value=>[value.label,value.provider,value.model,value.role,tokenText(value.inputTokens),tokenText(value.outputTokens)])));content.append(modelTimeline);
70
+ const tasks=panel('Consumption by task');tasks.append(table(['Task','Input','Output','Attempt time','Cost'],current.tasks.map(value=>[value.storyId,tokenText(value.inputTokens),tokenText(value.outputTokens),duration(value.attemptDurationMs),costText(value)])));content.append(tasks);
71
+ const phases=panel('Recorded phase time');phases.append(table(['Phase','Summed duration'],current.phases.map(value=>[value.phase,duration(value.durationMs)])));phases.append(el('p','Overlapping worker time is summed. Unrecorded queue time and human waiting time remain unknown.','muted'));content.append(phases);
72
+ }
73
+ function renderResults(p,a){const t=a.total;content.append(append(el('div',undefined,'metrics'),metric('Accepted in period',t.accepted,'Recorded acceptance events'),metric('Repair phases',t.repairs,'Routing escalations: '+t.escalations),metric('Tokens / acceptance',rate(t.tokensPerAccepted),'Reported input + output, including failed work')));const attempts=panel('Measured outcomes');attempts.append(el('p','Ended attempts: '+t.attempts+' · explicitly successful attempts: '+t.successfulAttempts+' · average summed attempt time per acceptance: '+duration(t.timePerAcceptedMs)));attempts.append(el('p','Worker termination alone is not counted as successful acceptance. Rates only describe recorded activity in the selected period.','muted'));attempts.append(table(['Task','Attempts','Accepted','Repairs','Escalations','Tokens / acceptance'],a.tasks.map(v=>[v.storyId,v.attempts,v.accepted,v.repairs,v.escalations,rate(v.tokensPerAccepted)])));content.append(attempts);const checks=panel('Latest saved acceptance evidence');if(p.check){checks.append(el('p',p.check.summary));for(const c of p.check.criteria){const item=row(c.text,c.id,c.status);item.firstChild.append(append(el('details'),el('summary','View evidence'),el('p',c.summary)));checks.append(item)}checks.append(el('p','Checked '+p.check.generatedAt+' · historical evidence; independent of the selected statistics period.','muted'))}else checks.append(el('p','No saved independent check.','empty'));content.append(checks);const goals=panel('Saved goal attempts');for(const attempt of p.goal?.attempts||[])goals.append(row(attempt.provider||'Agent',(attempt.summary||'')+' · '+duration(attempt.durationMs)+' · input '+tokenText(attempt.inputTokens)+' / output '+tokenText(attempt.outputTokens),attempt.success?'finished':'failed'));content.append(goals)}
74
+ async function showProject(id){
75
+ const request=beginRequest();selected=id;nav();
76
+ try{
77
+ const bounds=periodWindows(Date.now()),detailRequest=api('/api/projects/'+id,{},request.signal);
78
+ let project,current=null,previous=null;
79
+ if(projectView==='usage'){
80
+ [project,current,previous]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal),api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.previous),{},request.signal)]);
81
+ }else if(projectView==='results'){
82
+ [project,current]=await Promise.all([detailRequest,api('/api/projects/'+id+'/analytics?'+queryPeriod(bounds.current),{},request.signal)]);
83
+ }else project=await detailRequest;
84
+ if(!currentRequest(request.version)||navigationState.screen!=='project'||selected!==id)return;
85
+ heading(project.name,project.goal?.objective||'Saved project work');content.append(el('p',project.root,'path'));for(const error of project.errors)content.append(el('p',error,'notice'));
86
+ const tabs=el('div',undefined,'actions');for(const [view,label] of [['now','Now'],['usage','Usage & time'],['results','Results']]){const control=button(label,()=>setNavigation({view}),projectView===view);control.setAttribute('aria-pressed',String(projectView===view));tabs.append(control)}
87
+ if(project.goal&&!['complete','paused'].includes(project.goal.status))tabs.append(button('Request pause',async()=>{await api('/api/projects/'+id+'/pause',{method:'POST',headers:{'x-yoke-token':sessionToken}},request.signal);await refresh()}));content.append(tabs);
88
+ if(projectView==='now'){
89
+ const tasks=project.stories||[],state=projectStatus(project);content.append(append(el('div',undefined,'metrics'),metric('Accepted tasks',tasks.filter(task=>task.passes).length+' / '+tasks.length,'Saved acceptance state'),metric('Remaining time',project.estimate?.available?duration(project.estimate.lowerMs)+' – '+duration(project.estimate.upperMs):'Unknown',project.estimate?.available?project.estimate.sampleCount+' samples · empirical range':'No reliable duration history'),metric('Project status',state.primary,state.goal&&state.loop&&state.goal!==state.loop?'Goal '+state.goal+' · loop '+(state.stale?'unconfirmed':state.loop):'Latest reported state')));renderNow(project);
90
+ }else{periodControls(id);if(projectView==='usage')renderUsageHistory(current,previous);else renderResults(project,current)}
91
+ }catch(error){if(currentRequest(request.version)&&navigationState.screen==='project'&&selected===id&&!isAbort(error))heading('Project unavailable',error.message)}
92
+ }
93
+ if(typeof setInterval==='function')setInterval(()=>{if(selected&&projectView==='now'&&typeof document.hidden!=='undefined'&&!document.hidden&&!['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName)&&!document.activeElement?.matches?.(':focus-visible'))showProject(selected)},5000);
18
94
  `;
19
95
  }
@@ -1,5 +1,6 @@
1
1
  import { loadConfig } from "../retrofit/config.js";
2
2
  import { makeAsyncAdaptiveRunner } from "../routing/router.js";
3
+ import { resolvePlanner } from '../routing/planning.js';
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
5
6
  import { join } from 'node:path';
@@ -11,8 +12,8 @@ import { killProcessTreeForCleanup } from '../loop/watchdog.js';
11
12
  import { buildProviderInvocation, startProviderProcess } from '../agents/providers.js';
12
13
  import { appendEvent } from '../observability/events.js';
13
14
  import { statePath } from '../workspace/state.js';
14
- const Attempt = z.object({ provider: z.enum(['codex', 'claude', 'gemini']), model: z.string().optional(), startedAt: z.string(), durationMs: z.number().nonnegative(), success: z.boolean(), summary: z.string(), checkId: z.string(), inputTokens: z.number().nonnegative().optional(), outputTokens: z.number().nonnegative().optional() });
15
- const Goal = z.object({ version: z.literal(1), id: z.string().uuid(), objective: z.string().trim().min(1).max(16000), status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), createdAt: z.string(), updatedAt: z.string(), maxAttempts: z.number().int().min(1).max(20), maxMinutes: z.number().positive().max(1440), tokenBudget: z.number().int().positive().optional(), attempts: z.array(Attempt), reason: z.string().optional(), lastCheck: z.string().optional(), pendingAttempt: z.object({ provider: z.enum(['codex', 'claude', 'gemini']), model: z.string().optional(), startedAt: z.string().datetime() }).optional() }).strict();
15
+ const Attempt = z.object({ provider: z.enum(['codex', 'claude', 'gemini', 'qwen']), model: z.string().optional(), startedAt: z.string(), durationMs: z.number().nonnegative(), success: z.boolean(), summary: z.string(), checkId: z.string(), inputTokens: z.number().nonnegative().optional(), outputTokens: z.number().nonnegative().optional() });
16
+ const Goal = z.object({ version: z.literal(1), id: z.string().uuid(), objective: z.string().trim().min(1).max(16000), status: z.enum(['active', 'running', 'paused', 'blocked', 'complete']), createdAt: z.string(), updatedAt: z.string(), maxAttempts: z.number().int().min(1).max(20), maxMinutes: z.number().positive().max(1440), tokenBudget: z.number().int().positive().optional(), attempts: z.array(Attempt), reason: z.string().optional(), lastCheck: z.string().optional(), pendingAttempt: z.object({ provider: z.enum(['codex', 'claude', 'gemini', 'qwen']), model: z.string().optional(), startedAt: z.string().datetime() }).optional() }).strict();
16
17
  const goalPath = (root) => statePath(root, 'goal.json');
17
18
  function save(root, goal) {
18
19
  const parsed = Goal.parse({ ...goal, updatedAt: new Date().toISOString() });
@@ -138,6 +139,7 @@ export async function runProjectGoal(root, options = {}) {
138
139
  if (!config?.routing?.enabled || config.routing.strategy !== "capability")
139
140
  return rawExecute(input);
140
141
  const routed = makeAsyncAdaptiveRunner({ parent: provider, parentSelection: input.selection, projectRoot: root, workers: config.routing.workers, strategy: "capability", maxCandidates: config.routing.maxCandidates, maxAttempts: Math.min(goal.maxAttempts, config.routing.maxAttempts ?? 5),
142
+ planner: resolvePlanner(config, provider, input.selection), fallback: config.routing.fallback, maxTier: config.routing.maxTier,
141
143
  captureRoute: async (agent, context, prompt, selection) => {
142
144
  const run = await startProviderProcess(agent, buildProviderInvocation(agent, prompt, root, "read-only", selection), { signal: input.signal, idleTimeoutMs: 20 * 60_000 }).completion;
143
145
  return { success: run.kind === "succeeded", summary: run.kind, output: run.stdout, tokens: run.telemetry.tokens };
@@ -18,7 +18,7 @@ const RichClaimSchema = LegacyClaimSchema.extend({
18
18
  heartbeatAt: z.string().min(1),
19
19
  baseCommit: z.string().min(1).optional(),
20
20
  worktree: z.string().min(1).optional(),
21
- provider: z.enum(['claude', 'codex', 'gemini']).optional(),
21
+ provider: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
22
22
  model: z.string().min(1).optional(),
23
23
  role: z.enum(['implementation', 'quality', 'repair', 'review']).optional(),
24
24
  cancellation: z.object({
@@ -60,9 +60,9 @@ export const DecisionResumeSchema = z.object({
60
60
  answered: z.boolean().optional(),
61
61
  /** Absent preserves the default unlimited run across a critical-decision resume. */
62
62
  maxIterations: z.number().int().positive().optional(),
63
- agent: z.enum(['claude', 'codex', 'gemini']).optional(),
63
+ agent: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
64
64
  isolate: z.boolean().optional(),
65
- reviewer: z.enum(['claude', 'codex', 'gemini']).optional(),
65
+ reviewer: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
66
66
  review: z.boolean().optional(),
67
67
  allowSelfReview: z.boolean().optional(),
68
68
  timeoutMinutes: z.number().nonnegative().optional(),
package/dist/loop/git.js CHANGED
@@ -1,15 +1,23 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { sanitizeCommitMessage } from './identity.js';
3
- export const RUNTIME_PATHS = ['.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause'];
4
- export const RUNTIME_EXCLUDES = RUNTIME_PATHS.map(path => `:(exclude)${path}${path.endsWith('.json') || path.endsWith('.pause') ? '' : '/**'}`);
3
+ export const RUNTIME_PATHS = [".yoke/supervision", ".yoke/provider-processes", '.yoke/artifacts', '.yoke/events', '.yoke/history', '.yoke/routing', '.yoke/checks', '.yoke/goal.json', '.yoke/goal.pause', '.yoke/loop-status.json', '.yoke/loop.lock', '.yoke/loop.lock.takeover', '.yoke/loop.lock.takeover.recovery', '.yoke/runner.pid', '.yoke/story-durations.json'];
4
+ // Directory pathspecs include descendants and also exclude the directory entry.
5
+ export const RUNTIME_EXCLUDES = RUNTIME_PATHS.map(path => `:(exclude)${path}`);
6
+ export function stageImplementation(dir) {
7
+ execFileSync('git', ['reset', '--quiet', '--', ...RUNTIME_PATHS], { cwd: dir, stdio: 'pipe' });
8
+ // Negative ignored pathspecs can make git add fail. Enumerate eligible files
9
+ // first, including tracked deletions, and preserve literal names with NULs.
10
+ const paths = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.', ...RUNTIME_EXCLUDES], { cwd: dir, maxBuffer: 16 * 1024 * 1024 });
11
+ if (paths.length)
12
+ execFileSync('git', ['--literal-pathspecs', 'add', '-A', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd: dir, input: paths, stdio: ['pipe', 'pipe', 'pipe'] });
13
+ }
5
14
  export const realGitOps = {
6
15
  isClean(dir) {
7
16
  const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all', '--', '.', ...RUNTIME_EXCLUDES], { cwd: dir }).toString();
8
17
  return out.trim() === '';
9
18
  },
10
19
  commitAll(dir, message, identity) {
11
- execFileSync('git', ['reset', '--quiet', '--', ...RUNTIME_PATHS], { cwd: dir, stdio: 'pipe' });
12
- execFileSync('git', ['add', '-A', '--', '.', ...RUNTIME_EXCLUDES], { cwd: dir, stdio: 'pipe' });
20
+ stageImplementation(dir);
13
21
  const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: dir }).toString().trim();
14
22
  if (staged === '') {
15
23
  throw new Error('nothing to commit after agent run');
package/dist/loop/loop.js CHANGED
@@ -307,7 +307,9 @@ export function runLoop(opts) {
307
307
  iterations++;
308
308
  if (result.tokens)
309
309
  reporter.addTokens(result.tokens);
310
- if (result.routing?.blocked) {
310
+ if (result.infrastructureFailure || result.routing?.blocked) {
311
+ if (result.infrastructureFailure)
312
+ result.routing?.recordOutcome(false, "infrastructure");
311
313
  reporter.blocked(result.summary);
312
314
  return { status: "blocked", iterations, reason: result.summary, finalProgress: progress(stories) };
313
315
  }
@@ -442,7 +444,9 @@ export function runLoop(opts) {
442
444
  iterations++;
443
445
  if (result.tokens)
444
446
  reporter.addTokens(result.tokens);
445
- if (result.routing?.blocked) {
447
+ if (result.infrastructureFailure || result.routing?.blocked) {
448
+ if (result.infrastructureFailure)
449
+ result.routing?.recordOutcome(false, "infrastructure");
446
450
  reporter.blocked(result.summary);
447
451
  return { status: "blocked", iterations, reason: result.summary, finalProgress: progress(stories) };
448
452
  }
@@ -602,8 +606,8 @@ function runImplementation(opts, dir, story, reporter) {
602
606
  if (verdict.passed)
603
607
  return result;
604
608
  if (knownInfrastructureFailure(verdict.summary)) {
605
- result.routing.recordOutcome(false, "infrastructure");
606
- return result;
609
+ result.routing.recordOutcome(false, 'infrastructure');
610
+ return { ...result, success: false, infrastructureFailure: true, summary: verdict.summary, routing: { ...result.routing, blocked: true, canRetry: false } };
607
611
  }
608
612
  result.routing.recordOutcome(false);
609
613
  if (result.tokens)
@@ -2,7 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { isProviderTreeAlive, reapProviderProcesses } from './cleanup.js';
5
- import { realGitOps, RUNTIME_EXCLUDES, RUNTIME_PATHS } from './git.js';
5
+ import { realGitOps, stageImplementation } from './git.js';
6
6
  import { isPidAlive } from './lock.js';
7
7
  import { storyPathSegment } from './prd.js';
8
8
  import { killProcessTreeForCleanup } from './watchdog.js';
@@ -130,8 +130,7 @@ function rebaseCandidate(targetDir, input) {
130
130
  }
131
131
  let candidateTree;
132
132
  try {
133
- execFileSync('git', ['reset', '--quiet', '--', ...RUNTIME_PATHS], { cwd: input.worktree.path, stdio: 'pipe' });
134
- execFileSync('git', ['add', '-A', '--', '.', ...RUNTIME_EXCLUDES], { cwd: input.worktree.path, stdio: 'pipe' });
133
+ stageImplementation(input.worktree.path);
135
134
  const unstaged = spawnSync('git', ['diff', '--quiet', '--ignore-submodules=none'], { cwd: input.worktree.path, stdio: 'pipe' });
136
135
  if (unstaged.error || unstaged.status !== 0)
137
136
  return { kind: 'reopen', reason: 'candidate contains changes Git cannot snapshot' };
@@ -1,6 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { acceptanceProtectionProblem } from '../check/command.js';
3
3
  import { makeAsyncAdaptiveRunner } from '../routing/router.js';
4
+ import { resolvePlanner } from '../routing/planning.js';
4
5
  import { startProviderProcess } from '../agents/process.js';
5
6
  import { runnerInvocation, isAgentAvailable } from './runner.js';
6
7
  import { createDispatcher } from './dispatcher.js';
@@ -261,6 +262,10 @@ function asyncRunner(input, provider, signal, workerId) {
261
262
  strategy: input.routing.strategy,
262
263
  maxCandidates: input.routing.maxCandidates,
263
264
  maxAttempts: input.routing.maxAttempts,
265
+ planner: resolvePlanner({ planning: input.planning }, input.runnerAgent, input.selection),
266
+ assessmentPolicy: input.routing.assessmentPolicy,
267
+ fallback: input.routing.fallback,
268
+ maxTier: input.routing.maxTier,
264
269
  onDecision: (id, decision) => input.reporter.routingDecision?.(id, decision),
265
270
  orchestratorSelection: input.routing.orchestrator,
266
271
  isAvailable: input.isAvailable ?? isAgentAvailable,
package/dist/loop/prd.js CHANGED
@@ -42,11 +42,13 @@ export const StorySchema = z.object({
42
42
  area: z.string().min(1).optional(),
43
43
  /** Advisory relative file/directory scopes; not a filesystem permission boundary. */
44
44
  writes: z.array(z.string().min(1).max(500).refine(validWriteScope, 'Write scopes must be relative non-glob paths without traversal')).max(100).optional(),
45
- agent: z.enum(['claude', 'codex', 'gemini']).optional(),
45
+ agent: z.enum(['claude', 'codex', 'gemini', 'qwen']).optional(),
46
46
  /** Inbox request that created this story. Used for idempotent append-only intake. */
47
47
  sourceChange: z.string().min(1).optional(),
48
48
  quality: StoryQualityDeclarationSchema.optional(),
49
49
  assessment: AssessmentSchema.optional(),
50
+ /** Binding to the task, upstream contracts and approved planning brief. */
51
+ assessmentFor: z.string().regex(/^[a-f0-9]{64}$/).optional(),
50
52
  }).superRefine((story, ctx) => {
51
53
  const structured = story.acceptance.filter(isAcceptanceCriterion);
52
54
  const ids = structured.map(criterion => criterion.id);
@@ -1,3 +1,4 @@
1
+ import { readSupervision } from "../agents/supervision.js";
1
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, appendFileSync, statSync, rmSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
@@ -76,7 +77,9 @@ export function readStatus(dir) {
76
77
  if (!existsSync(file))
77
78
  return null;
78
79
  try {
79
- return JSON.parse(readFileSync(file, 'utf8'));
80
+ const status = JSON.parse(readFileSync(file, 'utf8'));
81
+ const supervision = readSupervision(dir, status.startedAt);
82
+ return supervision.length ? { ...status, supervision } : status;
80
83
  }
81
84
  catch {
82
85
  return null;