@chorus-aidlc/chorus-openclaw-plugin 0.17.2 → 0.18.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/README.md +3 -2
- package/bin/resolve-spec-mode.sh +107 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +49 -7
- package/dist/commands.js.map +1 -1
- package/dist/event-router.d.ts +16 -5
- package/dist/event-router.d.ts.map +1 -1
- package/dist/event-router.js +33 -6
- package/dist/event-router.js.map +1 -1
- package/dist/spec-mode.d.ts +81 -0
- package/dist/spec-mode.d.ts.map +1 -0
- package/dist/spec-mode.js +153 -0
- package/dist/spec-mode.js.map +1 -0
- package/package.json +2 -1
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/chorus/SKILL.md +9 -6
- package/skills/chorus-cli/SKILL.md +1 -1
- package/skills/code-reviewer/SKILL.md +2 -1
- package/skills/develop/SKILL.md +11 -9
- package/skills/docs/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/openspec-aware/SKILL.md +46 -34
- package/skills/orchestrate/SKILL.md +9 -1
- package/skills/proposal/SKILL.md +13 -10
- package/skills/proposal-reviewer/SKILL.md +4 -2
- package/skills/quick-dev/SKILL.md +3 -3
- package/skills/review/SKILL.md +5 -5
- package/skills/spec-lite/SKILL.md +159 -0
- package/skills/task-reviewer/SKILL.md +5 -1
- package/skills/yolo/SKILL.md +37 -22
- package/src/commands.ts +58 -7
- package/src/event-router.ts +43 -6
- package/src/spec-mode.ts +208 -0
package/src/event-router.ts
CHANGED
|
@@ -71,6 +71,16 @@ interface NotificationDetail {
|
|
|
71
71
|
* inject the same handback instruction the daemon does.
|
|
72
72
|
*/
|
|
73
73
|
orchestrator?: { type: string; uuid: string; name: string } | null;
|
|
74
|
+
/**
|
|
75
|
+
* Derived, NON-persisted waker-session anchor (wake-carry-waker-session-anchor, T1).
|
|
76
|
+
* Present only for an agent-caused wake on an idea/task-anchored resource whose waking
|
|
77
|
+
* agent has a live, ONLINE-origin session for that idea — it tells the woken peer that
|
|
78
|
+
* replying on this resource reaches the waker's existing live session. A SIBLING of
|
|
79
|
+
* `orchestrator` (actor-scoped vs assignment-scoped): either, both, or neither may be
|
|
80
|
+
* present. Mirrors the daemon's cli/prompts.mjs `wakerSession` field so
|
|
81
|
+
* buildWakerSessionGuidance can inject the same advisory the daemon does.
|
|
82
|
+
*/
|
|
83
|
+
wakerSession?: { agentUuid: string; agentName: string; ideaUuid: string } | null;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
export class ChorusEventRouter {
|
|
@@ -255,11 +265,34 @@ export class ChorusEventRouter {
|
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
268
|
+
* Waker-session advisory for a wake — the OpenClaw twin of the daemon's
|
|
269
|
+
* wakerSessionGuidance in cli/prompts.mjs. KEEP THE TWO WORDINGS IN SYNC. Returns null
|
|
270
|
+
* unless the notification carries a `wakerSession` anchor (surfaced by the server only when
|
|
271
|
+
* the waking agent has a live, ONLINE-origin session for this resource's idea). It tells the
|
|
272
|
+
* woken peer that replying on this resource reaches the waker's existing live session — an
|
|
273
|
+
* ADVISORY only, not an enforced server route. A SIBLING of buildOrchestratorGuidance:
|
|
274
|
+
* independent, so either/both/neither may render on one wake.
|
|
275
|
+
*/
|
|
276
|
+
private buildWakerSessionGuidance(n: NotificationDetail): string | null {
|
|
277
|
+
if (!n.wakerSession) return null;
|
|
278
|
+
const { agentName, agentUuid } = n.wakerSession;
|
|
279
|
+
return (
|
|
280
|
+
`@[${agentName}](agent:${agentUuid}) woke you and has a live session open on this ` +
|
|
281
|
+
`resource. If you reply by commenting on this same resource, your reply reaches that ` +
|
|
282
|
+
`agent's live session, keeping the exchange on one thread. This is advisory, not an ` +
|
|
283
|
+
`enforced server route — there is no automatic subscription and nothing is force-delivered; ` +
|
|
284
|
+
`replying here is simply where a reply lands via the normal return path. Prefer replying ` +
|
|
285
|
+
`on this resource over opening a new session.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Append orchestrator-handoff guidance (when the resource has an agent orchestrator) and the
|
|
291
|
+
* waker-session advisory (when the wake carries an online waker anchor) to a wake message and
|
|
292
|
+
* dispatch it. Every handler routes its wake through this so both instructions ride EVERY
|
|
293
|
+
* action — parity with the daemon, which appends orchestratorGuidance + wakerSessionGuidance
|
|
294
|
+
* in buildPrompt (cli/prompts.mjs). The two blocks are independent: either, both, or neither
|
|
295
|
+
* may append; with neither the message is dispatched unchanged.
|
|
263
296
|
*/
|
|
264
297
|
private wakeWithHandoff(
|
|
265
298
|
message: string,
|
|
@@ -268,7 +301,11 @@ export class ChorusEventRouter {
|
|
|
268
301
|
attr: WakeAttribution,
|
|
269
302
|
): void {
|
|
270
303
|
const handoff = this.buildOrchestratorGuidance(n);
|
|
271
|
-
this.
|
|
304
|
+
const anchor = this.buildWakerSessionGuidance(n);
|
|
305
|
+
let msg = message;
|
|
306
|
+
if (handoff) msg += `\n\n${handoff}`;
|
|
307
|
+
if (anchor) msg += `\n\n${anchor}`;
|
|
308
|
+
this.wake(msg, contextKey, attr);
|
|
272
309
|
}
|
|
273
310
|
|
|
274
311
|
private handleTaskAssigned(n: NotificationDetail, attr: WakeAttribution): void {
|
package/src/spec-mode.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-mode resolver for the Chorus OpenClaw plugin.
|
|
3
|
+
*
|
|
4
|
+
* This is the TypeScript reimplementation of the canonical bash resolver
|
|
5
|
+
* `public/chorus-plugin/bin/resolve-spec-mode.sh` (which the bash ports copy
|
|
6
|
+
* byte-identically; the TS ports reimplement + ship a same-contract test). It
|
|
7
|
+
* mirrors the chorus-pi port's `resolveSpecMode` in
|
|
8
|
+
* `packages/chorus-pi/lib/lib.ts` and is the **single source of truth** for the
|
|
9
|
+
* spec mode on OpenClaw.
|
|
10
|
+
*
|
|
11
|
+
* OpenClaw has no SessionStart hook and no per-session context-injection
|
|
12
|
+
* channel, so nothing precomputes the mode into the agent's context. Instead:
|
|
13
|
+
* - the `/chorus` command calls `resolveSpecModeFromEnv` so the resolved mode
|
|
14
|
+
* is a user-visible surface (its one real runtime caller), and
|
|
15
|
+
* - the stage skills (proposal / develop / yolo / openspec-aware) resolve the
|
|
16
|
+
* SAME contract inline, pointing back at this file as the authoritative rule.
|
|
17
|
+
*
|
|
18
|
+
* `resolveSpecMode` itself is pure given injectable fs + execSync, so it can be
|
|
19
|
+
* unit-tested without touching the disk or PATH.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { execSync } from "node:child_process";
|
|
24
|
+
|
|
25
|
+
/** Minimal fs surface needed by the resolver (injectable for tests). */
|
|
26
|
+
export interface FsLike {
|
|
27
|
+
existsSync(p: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Minimal execSync surface — used only to probe `command -v openspec`. */
|
|
31
|
+
export type ExecSync = (cmd: string, opts: { stdio: "ignore" }) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The resolved spec mode surfaced to the agent. `openspec` = the OpenSpec
|
|
35
|
+
* (openspec-aware) path; `lite` = Chorus-native lightweight specs
|
|
36
|
+
* (`.chorus/specs/<slug>/`); `off` = free-form, no spec artifact.
|
|
37
|
+
*/
|
|
38
|
+
export type SpecMode = "lite" | "openspec" | "off";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Inputs to the spec-mode resolver (env values + repo root). Mirrors the
|
|
42
|
+
* canonical bash resolver `public/chorus-plugin/bin/resolve-spec-mode.sh`.
|
|
43
|
+
*/
|
|
44
|
+
export interface SpecModeInputs {
|
|
45
|
+
/** CHORUS_SPEC_MODE — explicit override: "lite" | "openspec" | "off" (else unset/""). */
|
|
46
|
+
specMode?: string;
|
|
47
|
+
/** CHORUS_OPENSPEC_MODE — legacy opt-out: "off" disables OpenSpec. */
|
|
48
|
+
openspecMode?: string;
|
|
49
|
+
/** CLAUDE_PLUGIN_OPTION_ENABLEOPENSPEC — plugin toggle: "false" disables OpenSpec (default "true"). */
|
|
50
|
+
enableOpenSpec?: string;
|
|
51
|
+
/** Repo root to probe for openspec/. */
|
|
52
|
+
projectRoot: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolved spec mode for a repo — the TS mirror of the bash resolver's output
|
|
57
|
+
* vars. `specFail` non-empty ⇒ a stage skill MUST halt (an explicit
|
|
58
|
+
* `CHORUS_SPEC_MODE=openspec` that cannot be honored); `chorusOpenspecActive`
|
|
59
|
+
* is true only when the resolved mode is a USABLE openspec.
|
|
60
|
+
*/
|
|
61
|
+
export interface SpecModeResult {
|
|
62
|
+
specMode: SpecMode;
|
|
63
|
+
specReason: string;
|
|
64
|
+
specFail: string;
|
|
65
|
+
openspecUsable: boolean;
|
|
66
|
+
openspecUsableReason: string;
|
|
67
|
+
openspecHint: string;
|
|
68
|
+
chorusOpenspecActive: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the active Chorus spec mode for a repo. Pure given injectable fs +
|
|
73
|
+
* execSync.
|
|
74
|
+
*
|
|
75
|
+
* Rule (per owner): an explicit `CHORUS_SPEC_MODE` wins; when unset, OpenSpec
|
|
76
|
+
* stays the default whenever it is usable (openspec/ dir + CLI, not disabled),
|
|
77
|
+
* and lite is the fallback only when OpenSpec is absent or disabled. An explicit
|
|
78
|
+
* `=openspec` that isn't usable fails fast (`specFail`).
|
|
79
|
+
*/
|
|
80
|
+
export function resolveSpecMode(
|
|
81
|
+
inputs: SpecModeInputs,
|
|
82
|
+
fs: FsLike,
|
|
83
|
+
execSync: ExecSync,
|
|
84
|
+
): SpecModeResult {
|
|
85
|
+
const projectRoot = inputs.projectRoot || "";
|
|
86
|
+
|
|
87
|
+
// --- Is OpenSpec usable? (needs openspec/ dir + CLI on PATH + not disabled) ---
|
|
88
|
+
// enableOpenSpec toggle is checked BEFORE the legacy CHORUS_OPENSPEC_MODE, so a
|
|
89
|
+
// plugin-level opt-out wins the reason string (matches the bash resolver order).
|
|
90
|
+
let openspecDisabled = false;
|
|
91
|
+
let disabledReason = "";
|
|
92
|
+
if ((inputs.enableOpenSpec ?? "true") !== "true") {
|
|
93
|
+
openspecDisabled = true;
|
|
94
|
+
disabledReason = "enableOpenSpec userConfig=false (plugin-level opt-out)";
|
|
95
|
+
} else if (inputs.openspecMode === "off") {
|
|
96
|
+
openspecDisabled = true;
|
|
97
|
+
disabledReason = "CHORUS_OPENSPEC_MODE=off (legacy opt-out)";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let openspecUsable = false;
|
|
101
|
+
let openspecUsableReason = "";
|
|
102
|
+
let openspecHint = "";
|
|
103
|
+
if (openspecDisabled) {
|
|
104
|
+
openspecUsableReason = disabledReason;
|
|
105
|
+
} else if (!fs.existsSync(`${projectRoot}/openspec`)) {
|
|
106
|
+
openspecUsableReason = `no openspec/ directory at ${projectRoot}/openspec`;
|
|
107
|
+
openspecHint = "npm i -g @fission-ai/openspec && openspec init";
|
|
108
|
+
} else if (!openspecCliPresent(execSync)) {
|
|
109
|
+
openspecUsableReason = "openspec/ directory present but `openspec` CLI not on PATH";
|
|
110
|
+
openspecHint = "npm i -g @fission-ai/openspec";
|
|
111
|
+
} else {
|
|
112
|
+
openspecUsable = true;
|
|
113
|
+
openspecUsableReason = "openspec/ directory + openspec CLI both present";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Resolve CHORUS_SPEC_MODE (unset and "" are treated the same, as in bash) ---
|
|
117
|
+
let specMode: SpecMode;
|
|
118
|
+
let specReason: string;
|
|
119
|
+
let specFail = "";
|
|
120
|
+
const raw = inputs.specMode ?? "";
|
|
121
|
+
switch (raw) {
|
|
122
|
+
case "lite":
|
|
123
|
+
specMode = "lite";
|
|
124
|
+
specReason = "explicit — Chorus-native lightweight specs in .chorus/specs/<slug>/";
|
|
125
|
+
break;
|
|
126
|
+
case "off":
|
|
127
|
+
specMode = "off";
|
|
128
|
+
specReason = "explicit — free-form, no spec artifact";
|
|
129
|
+
break;
|
|
130
|
+
case "openspec":
|
|
131
|
+
specMode = "openspec";
|
|
132
|
+
if (openspecUsable) {
|
|
133
|
+
specReason = `explicit; ${openspecUsableReason}`;
|
|
134
|
+
} else if (openspecDisabled) {
|
|
135
|
+
specReason = `explicit, but OpenSpec is disabled: ${openspecUsableReason}`;
|
|
136
|
+
specFail = `config conflict — CHORUS_SPEC_MODE=openspec vs OpenSpec disabled (${openspecUsableReason}); re-enable OpenSpec or set CHORUS_SPEC_MODE=lite`;
|
|
137
|
+
} else {
|
|
138
|
+
specReason = `explicit, but OpenSpec is not installed: ${openspecUsableReason}`;
|
|
139
|
+
specFail = `OpenSpec not usable (${openspecUsableReason})`;
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
case "":
|
|
143
|
+
// Unset: OpenSpec is the default when usable; lite is the fallback otherwise.
|
|
144
|
+
if (openspecUsable) {
|
|
145
|
+
specMode = "openspec";
|
|
146
|
+
specReason = `default — ${openspecUsableReason}; set CHORUS_SPEC_MODE=lite for Chorus-native specs, =off to disable`;
|
|
147
|
+
} else {
|
|
148
|
+
specMode = "lite";
|
|
149
|
+
specReason = `default — OpenSpec not usable (${openspecUsableReason}); using Chorus-native lightweight specs in .chorus/specs/<slug>/`;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
// Unrecognized value: treat like unset (OpenSpec-if-usable, else lite).
|
|
154
|
+
if (openspecUsable) {
|
|
155
|
+
specMode = "openspec";
|
|
156
|
+
specReason = `CHORUS_SPEC_MODE='${raw}' unrecognized; falling back to default (${openspecUsableReason})`;
|
|
157
|
+
} else {
|
|
158
|
+
specMode = "lite";
|
|
159
|
+
specReason = `CHORUS_SPEC_MODE='${raw}' unrecognized; OpenSpec not usable, defaulting to lite`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const chorusOpenspecActive = specMode === "openspec" && specFail === "";
|
|
164
|
+
return {
|
|
165
|
+
specMode,
|
|
166
|
+
specReason,
|
|
167
|
+
specFail,
|
|
168
|
+
openspecUsable,
|
|
169
|
+
openspecUsableReason,
|
|
170
|
+
openspecHint,
|
|
171
|
+
chorusOpenspecActive,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function openspecCliPresent(execSync: ExecSync): boolean {
|
|
176
|
+
try {
|
|
177
|
+
execSync("command -v openspec", { stdio: "ignore" });
|
|
178
|
+
return true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convenience wrapper that wires the real Node `fs.existsSync` + `child_process.execSync`
|
|
186
|
+
* and reads the spec-mode env vars off a process-env-shaped bag. This is the
|
|
187
|
+
* `/chorus` command's real runtime caller. Kept out of `resolveSpecMode` so the
|
|
188
|
+
* core stays pure/injectable for tests.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveSpecModeFromEnv(
|
|
191
|
+
env: NodeJS.ProcessEnv,
|
|
192
|
+
projectRoot: string,
|
|
193
|
+
): SpecModeResult {
|
|
194
|
+
const fs: FsLike = { existsSync: (p: string) => existsSync(p) };
|
|
195
|
+
const exec: ExecSync = (cmd, opts) => {
|
|
196
|
+
execSync(cmd, opts);
|
|
197
|
+
};
|
|
198
|
+
return resolveSpecMode(
|
|
199
|
+
{
|
|
200
|
+
specMode: env.CHORUS_SPEC_MODE,
|
|
201
|
+
openspecMode: env.CHORUS_OPENSPEC_MODE,
|
|
202
|
+
enableOpenSpec: env.CLAUDE_PLUGIN_OPTION_ENABLEOPENSPEC,
|
|
203
|
+
projectRoot,
|
|
204
|
+
},
|
|
205
|
+
fs,
|
|
206
|
+
exec,
|
|
207
|
+
);
|
|
208
|
+
}
|