@jabworks/condux 0.19.0 → 0.21.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/index.js +144 -13
- package/package.json +1 -1
- package/skills/blueprint/SKILL.md +19 -18
- package/skills/condux-doctor/SKILL.md +4 -1
- package/skills/condux-doctor/doctor.mjs +19 -2
- package/skills/discovery/SKILL.md +22 -18
- package/skills/discovery/references/design-template.md +1 -1
- package/skills/plan-review/references/annotate-server.js +53 -1
- package/skills/plan-review/references/plan-review-template.html +32 -5
- package/skills/workflow/hooks/routing.md +4 -2
package/index.js
CHANGED
|
@@ -13,13 +13,18 @@
|
|
|
13
13
|
// separate `npx skills add` step for condux. This works because the config hook
|
|
14
14
|
// mutates the cached config before OpenCode lazily discovers skills.
|
|
15
15
|
//
|
|
16
|
-
// It also
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
16
|
+
// It also carries workflow's routing.md — the payload Claude Code and Codex
|
|
17
|
+
// get from their SessionStart hook — into the session as a `synthetic: true`
|
|
18
|
+
// `<system-reminder>` text part on the first user message of every *main*
|
|
19
|
+
// session (`chat.message`), re-injected after compaction. That is the shape
|
|
20
|
+
// OpenCode's own plan-mode reminders use (session/reminders.ts): it reaches
|
|
21
|
+
// the model in the user turn rather than mid-system-prompt, and it never
|
|
22
|
+
// reaches subagent sessions. Verified live on OpenCode 1.18.25 — the part
|
|
23
|
+
// persists (`chat.message` runs before the message is saved, and its parts
|
|
24
|
+
// array is the one written) and the model sees it. Per main-session turn it
|
|
25
|
+
// costs the same ~390 tokens as the `config.instructions` channel it replaces
|
|
26
|
+
// (docket #38 ratified that trade; docket #72 relocated the payload out of
|
|
27
|
+
// the system prompt and out of subagents).
|
|
23
28
|
//
|
|
24
29
|
// Optional plan-review listener (CONDUX_PLAN_REVIEW=1): when the primary
|
|
25
30
|
// `plan` agent finishes a turn (session.idle), spawns the plan-review annotate
|
|
@@ -29,6 +34,7 @@
|
|
|
29
34
|
|
|
30
35
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
31
36
|
import { spawn } from 'node:child_process';
|
|
37
|
+
import { randomBytes } from 'node:crypto';
|
|
32
38
|
import path from 'node:path';
|
|
33
39
|
import os from 'node:os';
|
|
34
40
|
import { fileURLToPath } from 'node:url';
|
|
@@ -62,6 +68,56 @@ function loadBundledAgents() {
|
|
|
62
68
|
return agents;
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
// The routing payload, framed the way OpenCode frames its own reminders. Null
|
|
72
|
+
// when routing.md is missing or empty — then the hook is a no-op, the same
|
|
73
|
+
// fail-open contract the Claude Code and Codex SessionStart scripts follow.
|
|
74
|
+
function loadRoutingReminder() {
|
|
75
|
+
if (!existsSync(ROUTING_MD)) return null;
|
|
76
|
+
const payload = readFileSync(ROUTING_MD, 'utf8').trim();
|
|
77
|
+
|
|
78
|
+
return payload ? `<system-reminder>\n${payload}\n</system-reminder>` : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// What the compaction summariser is asked to keep. The full reminder is
|
|
82
|
+
// re-injected on the next user message; this only stops the summary from
|
|
83
|
+
// describing a session in which the router never existed.
|
|
84
|
+
const COMPACTION_CONTEXT =
|
|
85
|
+
'Carry this rule into the summary: every implementation request in this session starts by loading the ' +
|
|
86
|
+
'`workflow` skill (`skill(name="workflow")`) before any other condux skill or any edit.';
|
|
87
|
+
|
|
88
|
+
// A part id in OpenCode's own ascending format — `prt_` + six big-endian bytes
|
|
89
|
+
// of (ms << 12 | counter) + fourteen base62 characters — so the reminder sorts
|
|
90
|
+
// among the parts OpenCode minted. The counter is pinned to its maximum: a
|
|
91
|
+
// part created in the same millisecond as the user's text must still order
|
|
92
|
+
// after it. The reminder follows the prompt, never precedes it.
|
|
93
|
+
const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
94
|
+
|
|
95
|
+
function partId() {
|
|
96
|
+
const stamp = BigInt(Date.now()) * 0x1000n + 0xfffn;
|
|
97
|
+
const bytes = Buffer.alloc(6);
|
|
98
|
+
for (let i = 0; i < 6; i++) bytes[i] = Number((stamp >> BigInt(40 - 8 * i)) & 0xffn);
|
|
99
|
+
let suffix = '';
|
|
100
|
+
for (const byte of randomBytes(14)) suffix += BASE62[byte % 62];
|
|
101
|
+
|
|
102
|
+
return `prt_${bytes.toString('hex')}${suffix}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// True when the session's stored history still shows the reminder to the
|
|
106
|
+
// model: a synthetic text part with exactly this text on a user message that
|
|
107
|
+
// comes after the last compaction. Compaction narrows the model's view to the
|
|
108
|
+
// summary plus a short tail, so a reminder on the far side of one counts as
|
|
109
|
+
// absent — the next user message gets it again.
|
|
110
|
+
export function historyIsRouted(messages, reminder) {
|
|
111
|
+
let routed = false;
|
|
112
|
+
for (const { info, parts = [] } of messages) {
|
|
113
|
+
if (parts.some((part) => part.type === 'compaction')) routed = false;
|
|
114
|
+
if (info?.role !== 'user') continue;
|
|
115
|
+
if (parts.some((part) => part.type === 'text' && part.synthetic === true && part.text === reminder)) routed = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return routed;
|
|
119
|
+
}
|
|
120
|
+
|
|
65
121
|
// The annotate server ships inside the plan-review skill; look for it in every
|
|
66
122
|
// skill tree OpenCode discovers (project then global).
|
|
67
123
|
function findAnnotateServer(worktree) {
|
|
@@ -82,11 +138,54 @@ function findAnnotateServer(worktree) {
|
|
|
82
138
|
return null;
|
|
83
139
|
}
|
|
84
140
|
|
|
85
|
-
export const ConduxPlugin = async ({ worktree }) => {
|
|
141
|
+
export const ConduxPlugin = async ({ worktree, client }) => {
|
|
86
142
|
const bundled = loadBundledAgents();
|
|
143
|
+
const reminder = loadRoutingReminder();
|
|
87
144
|
const activeAgent = new Map();
|
|
145
|
+
// Sessions whose live history carries the reminder, as far as this process
|
|
146
|
+
// knows. A compaction clears the entry so the next user message re-injects.
|
|
147
|
+
const routed = new Set();
|
|
148
|
+
const isChild = new Map();
|
|
88
149
|
let warnedPayloadShape = false;
|
|
89
150
|
|
|
151
|
+
// Subagent sessions never get the router: their brief is the task the parent
|
|
152
|
+
// handed them, and "every request starts at workflow" contradicts coder's.
|
|
153
|
+
// The bundled agent names are the free path; the parent lookup catches every
|
|
154
|
+
// other child (OpenCode's own `general`, user-defined subagents) — `task`
|
|
155
|
+
// creates them with parentID set, and `chat.message` carries their agent
|
|
156
|
+
// name. Unknown counts as main: the instructions channel this replaces
|
|
157
|
+
// reached subagents too, so leaning toward injection is the known state.
|
|
158
|
+
async function isSubagentSession(input) {
|
|
159
|
+
if (input.agent && bundled[input.agent]?.mode === 'subagent') return true;
|
|
160
|
+
if (typeof client?.session?.get !== 'function') return false;
|
|
161
|
+
if (!isChild.has(input.sessionID)) {
|
|
162
|
+
try {
|
|
163
|
+
const { data } = await client.session.get({ path: { id: input.sessionID } });
|
|
164
|
+
isChild.set(input.sessionID, Boolean(data?.parentID));
|
|
165
|
+
} catch {
|
|
166
|
+
isChild.set(input.sessionID, false);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return isChild.get(input.sessionID);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// On first sight of a session in this process — fresh, resumed, or after a
|
|
174
|
+
// compaction cleared it — ask the server whether the reminder is already in
|
|
175
|
+
// the model's view instead of trusting process memory: a resumed session
|
|
176
|
+
// would otherwise be injected twice, a compacted one never.
|
|
177
|
+
async function alreadyRouted(sessionID) {
|
|
178
|
+
if (routed.has(sessionID)) return true;
|
|
179
|
+
if (typeof client?.session?.messages !== 'function') return false;
|
|
180
|
+
try {
|
|
181
|
+
const { data } = await client.session.messages({ path: { id: sessionID } });
|
|
182
|
+
|
|
183
|
+
return Array.isArray(data) && historyIsRouted(data, reminder);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
90
189
|
return {
|
|
91
190
|
config: async (cfg) => {
|
|
92
191
|
cfg.agent ??= {};
|
|
@@ -116,13 +215,40 @@ export const ConduxPlugin = async ({ worktree }) => {
|
|
|
116
215
|
cfg.skills.paths ??= [];
|
|
117
216
|
if (!cfg.skills.paths.includes(SKILLS_DIR)) cfg.skills.paths.push(SKILLS_DIR);
|
|
118
217
|
}
|
|
218
|
+
// routing.md deliberately does NOT go on cfg.instructions any more: that
|
|
219
|
+
// channel is global (every subagent session read it) and lands mid
|
|
220
|
+
// system prompt. The reminder below is the replacement (docket #72).
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Route through workflow the way Claude Code and Codex do at SessionStart:
|
|
224
|
+
// one reminder on the first user message of a main session. Runs before
|
|
225
|
+
// the message is saved, so the pushed part is persisted with the rest.
|
|
226
|
+
'chat.message': async (input, output) => {
|
|
227
|
+
if (!reminder || !input?.sessionID || !output?.message?.id || !Array.isArray(output.parts)) return;
|
|
228
|
+
if (await isSubagentSession(input)) return;
|
|
229
|
+
if (await alreadyRouted(input.sessionID)) {
|
|
230
|
+
routed.add(input.sessionID);
|
|
119
231
|
|
|
120
|
-
|
|
121
|
-
// see the file-header comment for why `instructions` is the mechanism.
|
|
122
|
-
if (existsSync(ROUTING_MD)) {
|
|
123
|
-
cfg.instructions ??= [];
|
|
124
|
-
if (!cfg.instructions.includes(ROUTING_MD)) cfg.instructions.push(ROUTING_MD);
|
|
232
|
+
return;
|
|
125
233
|
}
|
|
234
|
+
|
|
235
|
+
output.parts.push({
|
|
236
|
+
id: partId(),
|
|
237
|
+
messageID: output.message.id,
|
|
238
|
+
sessionID: input.sessionID,
|
|
239
|
+
type: 'text',
|
|
240
|
+
synthetic: true,
|
|
241
|
+
text: reminder,
|
|
242
|
+
});
|
|
243
|
+
routed.add(input.sessionID);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Compaction narrows the model's view to a summary plus a short tail. Ask
|
|
247
|
+
// the summariser to keep the rule; the next user message re-injects the
|
|
248
|
+
// full reminder (the `session.compacted` event below clears the session).
|
|
249
|
+
'experimental.session.compacting': async (input, output) => {
|
|
250
|
+
if (!reminder || !Array.isArray(output?.context)) return;
|
|
251
|
+
if (input?.sessionID && routed.has(input.sessionID)) output.context.push(COMPACTION_CONTEXT);
|
|
126
252
|
},
|
|
127
253
|
|
|
128
254
|
'chat.params': async (input) => {
|
|
@@ -130,6 +256,11 @@ export const ConduxPlugin = async ({ worktree }) => {
|
|
|
130
256
|
},
|
|
131
257
|
|
|
132
258
|
event: async ({ event }) => {
|
|
259
|
+
if (event.type === 'session.compacted') {
|
|
260
|
+
routed.delete(event.properties?.sessionID);
|
|
261
|
+
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
133
264
|
if (process.env.CONDUX_PLAN_REVIEW !== '1') return;
|
|
134
265
|
if (event.type !== 'session.idle') return;
|
|
135
266
|
const sessionID = event.properties?.sessionID;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jabworks/condux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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": {
|
|
@@ -46,19 +46,19 @@ product family.
|
|
|
46
46
|
│ Standalone: open in the browser (xdg-open / open). If neither │
|
|
47
47
|
│ works (headless, SSH), print the absolute paths and continue — │
|
|
48
48
|
│ never fail on delivery. │
|
|
49
|
-
│ Inside /discovery: open NOTHING.
|
|
50
|
-
│
|
|
51
|
-
│ see Inside the Workflow.
|
|
49
|
+
│ Inside /discovery: open NOTHING. Link the file from the design │
|
|
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
|
-
│ Step 4:
|
|
58
|
-
│
|
|
59
|
-
│
|
|
60
|
-
│
|
|
61
|
-
│
|
|
57
|
+
│ Step 4: LINK │
|
|
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. │
|
|
62
62
|
└──────────────────────────────────────────────────────────────────┘
|
|
63
63
|
```
|
|
64
64
|
|
|
@@ -115,17 +115,18 @@ mockup into the spec directory first and cite the committed copy.
|
|
|
115
115
|
| What states are legal and what moves between them? | state machine |
|
|
116
116
|
| What goes where on a screen, and in what order does it read? | wireframe |
|
|
117
117
|
|
|
118
|
-
**Inside discovery, deliver by
|
|
118
|
+
**Inside discovery, deliver by linking — never by opening.** The reader
|
|
119
119
|
already has the design preview open (discovery launches it at §1 and it
|
|
120
|
-
live-reloads on every write). Add
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
129
130
|
- **/draft-plan** task cards may cite mockup paths so an implementer sees the
|
|
130
131
|
visual target for their task.
|
|
131
132
|
- Standalone asks ("mock this up", "visualize the data model") run the same
|
|
@@ -46,7 +46,10 @@ else on the machine competes with it. Only `broken` affects the exit code.
|
|
|
46
46
|
- **plan-review's Codex Stop hook** — resolved, never executed. It blocks a
|
|
47
47
|
turn for up to four days waiting on a human by design.
|
|
48
48
|
- **OpenCode** — `@jabworks/condux` in the plugin array, and when a local
|
|
49
|
-
copy exists, that it ships its bundled `agents/` and `skills
|
|
49
|
+
copy exists, that it ships its bundled `agents/` and `skills/` and that the
|
|
50
|
+
routing reminder it injects names the OpenCode verb, `skill(name="workflow")`
|
|
51
|
+
— a payload still saying `/condux:workflow` is the docket #72 under-firing
|
|
52
|
+
shape, reported broken with an upgrade as the fix.
|
|
50
53
|
- **The four specialist agents** — `coder`, `explorer`, `planner`,
|
|
51
54
|
`researcher`. They live in a plugin-level directory reached by its own sync
|
|
52
55
|
step, which is exactly the mirror that drifted once before.
|
|
@@ -338,12 +338,29 @@ function probeOpencode(hosts) {
|
|
|
338
338
|
return {
|
|
339
339
|
host: 'opencode',
|
|
340
340
|
status: 'broken',
|
|
341
|
-
detail: 'the installed package ships no workflow/hooks/routing.md —
|
|
341
|
+
detail: 'the installed package ships no workflow/hooks/routing.md — the plugin has no routing reminder to inject',
|
|
342
342
|
fix: `reinstall ${PACKAGE}`,
|
|
343
343
|
};
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
|
|
346
|
+
// Since @jabworks/condux 0.21.0 the payload is rewritten for OpenCode:
|
|
347
|
+
// `/condux:workflow` is a Claude Code command the host cannot run, and a
|
|
348
|
+
// payload still naming it is the docket #72 under-firing shape — installed,
|
|
349
|
+
// but asking for a verb the model has to translate on its own.
|
|
350
|
+
if (!fs.readFileSync(routing, 'utf8').includes('skill(name="workflow")')) {
|
|
351
|
+
return {
|
|
352
|
+
host: 'opencode',
|
|
353
|
+
status: 'broken',
|
|
354
|
+
detail: 'the installed routing payload names /condux:workflow, which OpenCode cannot run — the reminder under-fires',
|
|
355
|
+
fix: `upgrade ${PACKAGE} to 0.21.0 or later`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
host: 'opencode',
|
|
361
|
+
status: 'done',
|
|
362
|
+
detail: 'registered, and the installed package ships its agents, skills, and a routing reminder that names the OpenCode verb',
|
|
363
|
+
};
|
|
347
364
|
}
|
|
348
365
|
|
|
349
366
|
// The four specialist agents are plugin-level and reached by their own sync
|
|
@@ -59,8 +59,9 @@ Accept either answer, same as any other soft gate in this skill.
|
|
|
59
59
|
│ §1 is approaches-and-tradeoffs; the rest follow the design. │
|
|
60
60
|
│ Full contract: The Section Card, below. │
|
|
61
61
|
│ Does a section's decision turn on one of blueprint's five │
|
|
62
|
-
│ questions? Load `blueprint` — the artifact
|
|
63
|
-
│
|
|
62
|
+
│ questions? Load `blueprint` — the artifact is linked from │
|
|
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
|
|
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,12 +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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
210
213
|
|
|
211
214
|
## The Design File and the Live Preview (Step 3 onward)
|
|
212
215
|
|
|
@@ -287,15 +290,16 @@ Honour an `AGENTS.md` path override if the project defines one.
|
|
|
287
290
|
|
|
288
291
|
For full design mockups — UI wireframes and renders in the house token
|
|
289
292
|
language, data-model / flow / architecture diagrams — load the `blueprint`
|
|
290
|
-
skill; the design doc
|
|
291
|
-
|
|
292
|
-
opening. At sign-off, offer to
|
|
293
|
-
(a style-block swap). If the
|
|
294
|
-
the file too: copy it into
|
|
295
|
-
|
|
296
|
-
clone (workflow's
|
|
297
|
-
side-by-side options in the
|
|
298
|
-
can point at blueprint's
|
|
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).
|
|
299
303
|
|
|
300
304
|
## Design Review Loop
|
|
301
305
|
|
|
@@ -31,7 +31,7 @@ feature: <slug>
|
|
|
31
31
|
|---|---|
|
|
32
32
|
| <alternative> | <one line> |
|
|
33
33
|
|
|
34
|
-
<evidence — table, list, or a
|
|
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
|
|
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
|
|
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
|
-
|
|
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){
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<EXTREMELY_IMPORTANT>
|
|
2
2
|
You have condux — this session's agentic workflow system.
|
|
3
3
|
|
|
4
|
-
**Every implementation request starts at
|
|
4
|
+
**Every implementation request starts at the `workflow` skill — `skill(name="workflow")`.** Feature, bug fix,
|
|
5
5
|
refactor, new endpoint, UI change, "can you add…", "can you fix…" — all of it
|
|
6
6
|
routes there first. The router infers the tier (Small / Medium / Large), confirms
|
|
7
7
|
it with the user, then executes the matching flow, loading downstream skills only
|
|
@@ -13,7 +13,7 @@ when a step needs them.
|
|
|
13
13
|
`root-cause-analysis`, `live-verification`, `plan-review`, `technical-spec`.
|
|
14
14
|
|
|
15
15
|
If you find yourself reaching for one of those as the *first* skill of a task,
|
|
16
|
-
you have skipped the router — go back to
|
|
16
|
+
you have skipped the router — go back to the `workflow` skill — `skill(name="workflow")`. Debugging is the
|
|
17
17
|
most common miss: a crash report, a stack trace, or "why does this fail" is
|
|
18
18
|
still a dev task, so it routes through workflow, which loads
|
|
19
19
|
`root-cause-analysis` at the right moment.
|
|
@@ -25,4 +25,6 @@ user instructions — "skip workflow", "just do it", "I've already planned this"
|
|
|
25
25
|
**Not everything is a dev task.** Questions, explanations, code reading, research,
|
|
26
26
|
and one-line factual answers route nowhere — answer them directly. Routing a
|
|
27
27
|
conversation through a workflow is as wrong as skipping the router on a feature.
|
|
28
|
+
|
|
29
|
+
**On OpenCode, a `/name` mention of any condux skill means the `skill` tool:** `/workflow` → `skill(name="workflow")`, `/finalize` → `skill(name="finalize")`, and so on. None of them is a slash command here.
|
|
28
30
|
</EXTREMELY_IMPORTANT>
|