@jabworks/condux 0.18.0 → 0.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jabworks/condux",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "OpenCode plugin for the condux workflow toolkit — bundles the condux skills and injects the specialist agents (coder, explorer, planner, researcher) plus an opt-in plan-review listener.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -47,16 +47,18 @@ product family.
47
47
  │ works (headless, SSH), print the absolute paths and continue — │
48
48
  │ never fail on delivery. │
49
49
  │ Inside /discovery: open NOTHING. Link the file from the design │
50
- │ doc and let the running preview reload — see Inside the │
51
- │ Workflow. │
50
+ │ doc (label = href = its served-root-relative path) and let the │
51
+ │ running preview reload — see Inside the Workflow. │
52
52
  │ Either way, always state which mode was produced │
53
53
  │ and offer the flip: "wireframe mode — say 'promote to render' │
54
54
  │ for the full house look" (or the reverse). The mode is never │
55
55
  │ chosen silently. │
56
56
  │ │
57
57
  │ Step 4: LINK │
58
- │ Reference the files from the design doc or task card that │
59
- │ motivated them, so the visual target travels with the design. │
58
+ │ Link the files from the design doc or task card that motivated │
59
+ │ them — a markdown link whose href and label are the same │
60
+ │ served-root-relative path — so the visual target travels with │
61
+ │ the design and the click opens it in the review preview. │
60
62
  └──────────────────────────────────────────────────────────────────┘
61
63
  ```
62
64
 
@@ -115,10 +117,16 @@ mockup into the spec directory first and cite the committed copy.
115
117
 
116
118
  **Inside discovery, deliver by linking — never by opening.** The reader
117
119
  already has the design preview open (discovery launches it at §1 and it
118
- live-reloads on every write). Add the mockup's path to the design doc and the
119
- link appears there. A tab per section is the interruption this avoids;
120
- standalone invocations keep opening on deliver. Side-by-side option *picking*
121
- stays with discovery's own mockup-picker.
120
+ live-reloads on every write). Add a markdown link whose href and label are
121
+ the same served-root-relative path — `[mockups/flow.html](mockups/flow.html)`
122
+ — and the link appears in the doc; the preview serves enumerated sibling
123
+ `.html`/`.svg` files script-dead, so clicking opens the artifact. An older
124
+ preview shows the path as the label and loses nothing. Never a
125
+ git-root-relative href (it cannot resolve against the served root), and
126
+ never an inlined copy of the diagram beside the artifact: one fact, two
127
+ homes is how the copies drift. A tab per section is the interruption this
128
+ avoids; standalone invocations keep opening on deliver. Side-by-side option
129
+ *picking* stays with discovery's own mockup-picker.
122
130
  - **/draft-plan** task cards may cite mockup paths so an implementer sees the
123
131
  visual target for their task.
124
132
  - Standalone asks ("mock this up", "visualize the data model") run the same
@@ -60,7 +60,8 @@ Accept either answer, same as any other soft gate in this skill.
60
60
  │ Full contract: The Section Card, below. │
61
61
  │ Does a section's decision turn on one of blueprint's five │
62
62
  │ questions? Load `blueprint` — the artifact is linked from │
63
- │ the design doc, not opened in a new tab. │
63
+ │ the design doc (label = href = the served-root-relative │
64
+ │ path), not opened in a new tab. │
64
65
  │ │
65
66
  │ Step 4: DETAIL ROUND — feed the spec │
66
67
  │ With the approach chosen, ask ONE more batch, grouped by │
@@ -180,7 +181,7 @@ before they are inside it. Then create the design file and open the preview
180
181
  ## §n of N · <name> ← position, always
181
182
  <one line: what this section decides>
182
183
 
183
- <the evidence — table, list, or a linked artifact. Never a wall.>
184
+ <the evidence — table, list, or a linked artifact path. Never a wall.>
184
185
 
185
186
  **Recommendation:** <X>, because <one sentence>.
186
187
  <the decision, as named options>
@@ -201,8 +202,14 @@ the design existed only as scrollback, so the thread was lost between them. The
201
202
  **Visuals.** Load `blueprint` when a section's decision turns on one of its five
202
203
  questions — what entities exist · what happens in what order · what talks to
203
204
  what · what states are legal · what goes where on a screen. Inside discovery
204
- the artifact is *linked from the design doc*, never opened in its own tab: the
205
- running preview reloads and shows the link, so the reader stays where they are.
205
+ the artifact is *linked from the design doc*, never opened in its own tab: a
206
+ markdown link whose href and label are the same served-root-relative path —
207
+ `[mockups/checkout-flow.html](mockups/checkout-flow.html)` — one string, no
208
+ drift. The preview serves enumerated sibling `.html`/`.svg` files
209
+ script-dead, so the click opens the artifact; an older preview simply shows
210
+ the path as the label, and nothing degrades without a server. Never write a
211
+ git-root-relative href (it cannot resolve against the served root), and
212
+ never inline a copy of the diagram beside the artifact.
206
213
 
207
214
  ## The Design File and the Live Preview (Step 3 onward)
208
215
 
@@ -283,11 +290,16 @@ Honour an `AGENTS.md` path override if the project defines one.
283
290
 
284
291
  For full design mockups — UI wireframes and renders in the house token
285
292
  language, data-model / flow / architecture diagrams — load the `blueprint`
286
- skill; the design doc links the files it produces, and the running preview
287
- shows the link rather than a new tab opening. At sign-off, offer to
288
- promote the chosen wireframes to render mode (a style-block swap). For
289
- *picking between* side-by-side options in the browser, see
290
- `references/mockup-picker.md` (it can point at blueprint's files).
293
+ skill; the design doc links the files it produces (served-root-relative
294
+ markdown links, label = href), and the running preview serves the artifact
295
+ on click rather than a new tab opening on deliver. At sign-off, offer to
296
+ promote the chosen wireframes to render mode (a style-block swap). If the
297
+ spec references a blueprint artifact, promote the file too: copy it into
298
+ the spec directory and link the committed path — a committed spec may never
299
+ point into `.condux/`, which is gone on any other clone (workflow's
300
+ citation-direction rule). For *picking between* side-by-side options in the
301
+ browser, see `references/mockup-picker.md` (it can point at blueprint's
302
+ files).
291
303
 
292
304
  ## Design Review Loop
293
305
 
@@ -31,7 +31,7 @@ feature: <slug>
31
31
  |---|---|
32
32
  | <alternative> | <one line> |
33
33
 
34
- <evidence — table, list, or a linked blueprint artifact. Never a wall.>
34
+ <evidence — table, list, or a linked blueprint artifact (served-root-relative, label = href). Never a wall.>
35
35
 
36
36
  ## Constraints & out of scope [accumulates — rows added any time]
37
37
 
@@ -131,6 +131,35 @@ function readPlan(doc) {
131
131
  } catch (e) { return ''; }
132
132
  }
133
133
 
134
+ // Artifact serving: sibling *.html / *.svg files (blueprint mockups and
135
+ // diagrams), enumerated with the same walk rules as listDocs(). Membership in
136
+ // this list is the only door — the request URL is never path-resolved, so
137
+ // traversal shapes can't reach the filesystem. Stdin-mode plans live in
138
+ // os.tmpdir() (see main), where a sibling walk would enumerate unrelated
139
+ // files; artifact serving is off there.
140
+ function artifactRoot() {
141
+ if (tmpPlanFile) return null;
142
+ return DIR_MODE ? planFile : path.dirname(planFile);
143
+ }
144
+
145
+ function listArtifacts() {
146
+ const root = artifactRoot();
147
+ if (!root) return [];
148
+ const out = [];
149
+ (function walk(rel) {
150
+ let entries;
151
+ try { entries = fs.readdirSync(path.join(root, rel || '.'), { withFileTypes: true }); }
152
+ catch (e) { return; }
153
+ entries.forEach(function (e) {
154
+ if (e.name[0] === '.') return;
155
+ const r = rel ? rel + '/' + e.name : e.name;
156
+ if (e.isDirectory()) walk(r);
157
+ else if (/\.(html|svg)$/i.test(e.name) && !/\.feedback\./i.test(e.name)) out.push(r);
158
+ });
159
+ })('');
160
+ return out;
161
+ }
162
+
134
163
  function watchPlan() {
135
164
  try {
136
165
  if (DIR_MODE) {
@@ -318,7 +347,13 @@ function handler(req, res) {
318
347
 
319
348
  if (req.method === 'GET' && p === '/') {
320
349
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
321
- return res.end(TEMPLATE.replace(/\{\{PLAN_NAME\}\}/g, escapeHtml(path.basename(planFile))));
350
+ return res.end(TEMPLATE
351
+ .replace(/\{\{PLAN_NAME\}\}/g, escapeHtml(path.basename(planFile)))
352
+ // Decision-bar affordance: manual mode defers the decision to a file the
353
+ // agent reads at its next checkpoint; every other mode (steer, hook,
354
+ // codex) has a live listener blocked on the decision, so only true
355
+ // manual may carry the deferred copy.
356
+ .replace(/\{\{MODE\}\}/g, CODEX_STOP_MODE ? 'codex' : HOOK_MODE ? 'hook' : STEER_MODE ? 'steer' : 'manual'));
322
357
  }
323
358
  if (req.method === 'GET' && p === '/api/plan') {
324
359
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
@@ -383,6 +418,23 @@ function handler(req, res) {
383
418
  });
384
419
  return;
385
420
  }
421
+ if (req.method === 'GET' && p !== '/' && p.indexOf('/api/') !== 0) {
422
+ // Enumerated artifacts only (see listArtifacts). Served script-dead:
423
+ // CSP sandbox gives the document an opaque origin and disables script
424
+ // execution, so an agent-authored artifact can never reach /api/*.
425
+ let rel = null;
426
+ try { rel = decodeURIComponent(p.slice(1)); } catch (e) { /* malformed → 404 */ }
427
+ if (rel && listArtifacts().indexOf(rel) >= 0) {
428
+ try {
429
+ const body = fs.readFileSync(path.join.apply(path, [artifactRoot()].concat(rel.split('/'))));
430
+ res.writeHead(200, {
431
+ 'Content-Type': /\.svg$/i.test(rel) ? 'image/svg+xml' : 'text/html; charset=utf-8',
432
+ 'Content-Security-Policy': 'sandbox',
433
+ });
434
+ return res.end(body);
435
+ } catch (e) { /* deleted between walk and read → 404 */ }
436
+ }
437
+ }
386
438
  res.writeHead(404);
387
439
  res.end();
388
440
  }
@@ -1648,14 +1648,21 @@
1648
1648
  });
1649
1649
  updateBadges(); updateSummary();
1650
1650
  }
1651
+ // Server mode, injected at render. 'manual' = no live listener: the
1652
+ // decision lands in the feedback file and the agent reads it at its next
1653
+ // checkpoint. Anything else (steer, or an unreplaced literal from an
1654
+ // older server) keeps the live-submission copy.
1655
+ const SERVER_MODE='{{MODE}}';
1656
+ const MANUAL=SERVER_MODE==='manual';
1651
1657
  // Live summary in the decision bar — what rides along with the decision.
1652
1658
  function updateSummary(){
1653
1659
  const n=annotations.filter(function(a){return a.kind==='note';}).length;
1654
1660
  const m=annotations.filter(function(a){return a.kind==='chat';}).length;
1655
1661
  const parts=[];
1656
1662
  if(n)parts.push(n+' note'+(n>1?'s':'')); if(m)parts.push(m+' message'+(m>1?'s':''));
1657
- document.getElementById('dbar-summary').textContent=
1658
- parts.length?parts.join(' · ')+' will be sent with your decision':'No notes or messages yet — a decision sends on its own';
1663
+ document.getElementById('dbar-summary').textContent=MANUAL
1664
+ ?(parts.length?parts.join(' · ')+' will be recorded with your decision — the agent reads it at its next checkpoint':'Decisions are recorded for the agent — it reads them at its next checkpoint')
1665
+ :(parts.length?parts.join(' · ')+' will be sent with your decision':'No notes or messages yet — a decision sends on its own');
1659
1666
  }
1660
1667
  function updateBadges(){
1661
1668
  const counts={}, docCounts={};
@@ -1680,18 +1687,29 @@
1680
1687
  }
1681
1688
 
1682
1689
  // Per-decision confirmation copy — what the agent does with each verdict.
1690
+ // Two tables: live (steer) and deferred (manual). Manual copy never
1691
+ // implies a listener, and never pulses a waiting dot — nothing is waiting.
1683
1692
  const SENT={
1684
1693
  'Approve': {cls:'approve',ico:'✓',wait:false,title:'Approved',sub:'Sent to the agent — it will proceed and implement the plan.'},
1685
1694
  'Request Revisions':{cls:'revise', ico:'',wait:true, title:'Revisions requested',sub:'Your notes are on their way. The agent will revise the plan; the updated version reloads here when it’s ready.'},
1686
1695
  'Reject': {cls:'deny', ico:'✕',wait:false,title:'Rejected',sub:'Sent to the agent — it will not implement this. It steps back to reconsider whether the feature should be built at all.'},
1687
1696
  };
1697
+ const SENT_MANUAL={
1698
+ 'Approve': {cls:'approve',ico:'✓',wait:false,title:'Approval recorded',sub:'Saved to the feedback file — the agent reads it at its next checkpoint. Nothing is watching live.'},
1699
+ 'Request Revisions':{cls:'revise', ico:'',wait:false,title:'Revisions recorded',sub:'Saved to the feedback file — the agent picks your notes up at its next checkpoint. Nothing is watching live.'},
1700
+ 'Reject': {cls:'deny', ico:'✕',wait:false,title:'Rejection recorded',sub:'Saved to the feedback file — the agent reads it at its next checkpoint. Nothing is watching live.'},
1701
+ };
1688
1702
  let submitting=false, decisionSent=false;
1689
1703
  function clearAllNotes(){
1690
1704
  annotations.slice().forEach(function(a){ if(a.kind==='note') unhighlight(a.id); });
1691
1705
  annotations.length=0; nextId=1; paint();
1692
1706
  }
1693
- function showSent(decision){
1694
- const cfg=SENT[decision]||SENT['Request Revisions'];
1707
+ function showSent(decision,mode){
1708
+ // The toast follows the mode /api/feedback reports; the injected
1709
+ // constant is the fallback when the response carried none.
1710
+ const manual=(mode||SERVER_MODE)==='manual';
1711
+ const table=manual?SENT_MANUAL:SENT;
1712
+ const cfg=table[decision]||table['Request Revisions'];
1695
1713
  document.getElementById('dbar-live').style.display='none';
1696
1714
  const ico=document.getElementById('sent-ico');
1697
1715
  ico.className='ico'+(cfg.wait?' dot':''); ico.textContent=cfg.wait?'':cfg.ico;
@@ -1715,11 +1733,13 @@
1715
1733
  // conversation would mean two different notes.
1716
1734
  const r=await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:threadOrder()})});
1717
1735
  if(!r.ok) throw new Error('status '+r.status);
1736
+ let mode=SERVER_MODE;
1737
+ try{ mode=(await r.json()).mode||mode; }catch(e){}
1718
1738
  hideApiError();
1719
1739
  decisionSent=true; // a terminal decision makes the server exit —
1720
1740
  // the SSE drop that follows is expected, not a fault
1721
1741
  clearAllNotes(); // notes clear the moment the decision is sent
1722
- showSent(decision);
1742
+ showSent(decision,mode);
1723
1743
  }catch(e){
1724
1744
  showApiError('Could not send your decision — the review server may be unreachable. Nothing was sent; try again.');
1725
1745
  document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=false;});
@@ -1729,6 +1749,13 @@
1729
1749
  document.getElementById('send').onclick=sendChat;
1730
1750
  document.getElementById('input').addEventListener('keydown',function(e){if((e.metaKey||e.ctrlKey)&&e.key==='Enter')sendChat();});
1731
1751
  document.querySelectorAll('.dbar button[data-decision]').forEach(function(b){b.onclick=function(){submit(b.dataset.decision);};});
1752
+ // Manual mode: the buttons record, they don't transmit — say so.
1753
+ if(MANUAL){
1754
+ const RECORD={'Approve':'Record approval','Request Revisions':'Record revisions','Reject':'Record rejection'};
1755
+ document.querySelectorAll('.dbar button[data-decision]').forEach(function(b){
1756
+ if(RECORD[b.dataset.decision]) b.textContent=RECORD[b.dataset.decision];
1757
+ });
1758
+ }
1732
1759
 
1733
1760
  // ---- Revision diff (self-contained line-level LCS, no deps) ----
1734
1761
  function lineDiff(a,b){