@dezycro-ai/agent-plugin 2.0.0 → 2.1.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 +1 -1
- package/dist/hooks/lib/command-bus.d.ts +53 -0
- package/dist/hooks/lib/command-bus.js +307 -0
- package/dist/hooks/lib/router.d.ts +4 -4
- package/dist/hooks/lib/router.js +69 -4
- package/dist/hooks/lib/state.js +6 -0
- package/dist/hooks/types.d.ts +29 -0
- package/package.json +1 -1
- package/skills/init/SKILL.md +42 -2
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ See **[PERSONAS.md](./PERSONAS.md)** for the full schema, all supported auth typ
|
|
|
116
116
|
|
|
117
117
|
For installation issues, MCP setup, updates, and uninstall instructions, see the [installation guide](https://docs.dezycro.ai/claude-code/install/).
|
|
118
118
|
|
|
119
|
-
For everything else,
|
|
119
|
+
For everything else, email [support@dezycro.io](mailto:support@dezycro.io).
|
|
120
120
|
|
|
121
121
|
---
|
|
122
122
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-bus.ts — pulls the Agent Command Bus inbox on hook boundaries and renders
|
|
3
|
+
* unseen events for the agent transcript.
|
|
4
|
+
*
|
|
5
|
+
* Polling model (per the Agent Command Bus TRD):
|
|
6
|
+
* * `session-start` always polls (no floor).
|
|
7
|
+
* * `user-prompt-submit`, `stop`, `post-tool-use` poll only if `minPollIntervalSeconds`
|
|
8
|
+
* has elapsed since the last successful poll.
|
|
9
|
+
* * On 2xx: advance cursor + lastPollTs, dedup by `eventId` (recent ring, cap 200),
|
|
10
|
+
* return new events for surfacing.
|
|
11
|
+
* * On non-2xx / network failure: cursor stays put, consecutiveFailures++.
|
|
12
|
+
*
|
|
13
|
+
* Auth: a bearer token from `.dezycro/companion-token.local.json` (`{"token": "dzy_…"}`)
|
|
14
|
+
* is sent on every request. Separate from the MCP server token (the user maintains both).
|
|
15
|
+
*
|
|
16
|
+
* URL: `<environments[default].apiUrl>/api/v1/workspaces/<wsId>/features/<fId>/inbox`.
|
|
17
|
+
* Active feature comes from `.dezycro/active-feature.json` written by `/dezycro:work`.
|
|
18
|
+
*/
|
|
19
|
+
import type { CommandBusConfig, CommandBusHook, CommandBusReactionMode, CompanionState, InboxEvent, StatePatch } from '../types';
|
|
20
|
+
export interface PollResult {
|
|
21
|
+
events: InboxEvent[];
|
|
22
|
+
statePatch: StatePatch | null;
|
|
23
|
+
reactionMode: CommandBusReactionMode;
|
|
24
|
+
}
|
|
25
|
+
interface ResolvedConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
hooks: ReadonlySet<CommandBusHook>;
|
|
28
|
+
minPollIntervalSeconds: number;
|
|
29
|
+
reactionMode: CommandBusReactionMode;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Entry point called from each hook handler. Returns the events that should be surfaced
|
|
33
|
+
* to the agent on this hook fire, plus a state patch the caller must apply.
|
|
34
|
+
*
|
|
35
|
+
* Never throws — failures are logged and result in `{events: [], statePatch: null}`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pollIfDue(repoRoot: string, hookEvent: CommandBusHook, config: CommandBusConfig | undefined, state: CompanionState): Promise<PollResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Render zero or more events as a chat-ready Markdown block. Returns the empty string
|
|
40
|
+
* when there is nothing to surface — callers can guard with the return value's length.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderForAgent(events: InboxEvent[], reactionMode: CommandBusReactionMode): string;
|
|
43
|
+
declare function renderEvent(e: InboxEvent): string;
|
|
44
|
+
declare function reactionGuidance(mode: CommandBusReactionMode): string;
|
|
45
|
+
declare function resolveConfig(c: CommandBusConfig | undefined): ResolvedConfig;
|
|
46
|
+
declare function trimRecent(ids: string[]): string[];
|
|
47
|
+
export declare const _internals: {
|
|
48
|
+
resolveConfig: typeof resolveConfig;
|
|
49
|
+
renderEvent: typeof renderEvent;
|
|
50
|
+
reactionGuidance: typeof reactionGuidance;
|
|
51
|
+
trimRecent: typeof trimRecent;
|
|
52
|
+
};
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-bus.ts — pulls the Agent Command Bus inbox on hook boundaries and renders
|
|
3
|
+
* unseen events for the agent transcript.
|
|
4
|
+
*
|
|
5
|
+
* Polling model (per the Agent Command Bus TRD):
|
|
6
|
+
* * `session-start` always polls (no floor).
|
|
7
|
+
* * `user-prompt-submit`, `stop`, `post-tool-use` poll only if `minPollIntervalSeconds`
|
|
8
|
+
* has elapsed since the last successful poll.
|
|
9
|
+
* * On 2xx: advance cursor + lastPollTs, dedup by `eventId` (recent ring, cap 200),
|
|
10
|
+
* return new events for surfacing.
|
|
11
|
+
* * On non-2xx / network failure: cursor stays put, consecutiveFailures++.
|
|
12
|
+
*
|
|
13
|
+
* Auth: a bearer token from `.dezycro/companion-token.local.json` (`{"token": "dzy_…"}`)
|
|
14
|
+
* is sent on every request. Separate from the MCP server token (the user maintains both).
|
|
15
|
+
*
|
|
16
|
+
* URL: `<environments[default].apiUrl>/api/v1/workspaces/<wsId>/features/<fId>/inbox`.
|
|
17
|
+
* Active feature comes from `.dezycro/active-feature.json` written by `/dezycro:work`.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports._internals = void 0;
|
|
55
|
+
exports.pollIfDue = pollIfDue;
|
|
56
|
+
exports.renderForAgent = renderForAgent;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const logging = __importStar(require("./logging"));
|
|
60
|
+
const DEFAULT_HOOKS = [
|
|
61
|
+
'sessionStart',
|
|
62
|
+
'userPromptSubmit',
|
|
63
|
+
'stop',
|
|
64
|
+
'postToolUse',
|
|
65
|
+
];
|
|
66
|
+
const DEFAULT_MIN_POLL_INTERVAL_SECONDS = 60;
|
|
67
|
+
const DEFAULT_REACTION_MODE = 'HALT_AND_REPLAN';
|
|
68
|
+
const RECENT_EVENT_IDS_CAP = 200;
|
|
69
|
+
const POLL_LIMIT = 100;
|
|
70
|
+
/**
|
|
71
|
+
* Entry point called from each hook handler. Returns the events that should be surfaced
|
|
72
|
+
* to the agent on this hook fire, plus a state patch the caller must apply.
|
|
73
|
+
*
|
|
74
|
+
* Never throws — failures are logged and result in `{events: [], statePatch: null}`.
|
|
75
|
+
*/
|
|
76
|
+
async function pollIfDue(repoRoot, hookEvent, config, state) {
|
|
77
|
+
const resolved = resolveConfig(config);
|
|
78
|
+
const reactionMode = resolved.reactionMode;
|
|
79
|
+
const empty = { events: [], statePatch: null, reactionMode };
|
|
80
|
+
if (!resolved.enabled)
|
|
81
|
+
return empty;
|
|
82
|
+
if (!resolved.hooks.has(hookEvent) && hookEvent !== 'sessionStart')
|
|
83
|
+
return empty;
|
|
84
|
+
// SessionStart always polls regardless of the configured hook set — bootstrap-on-start
|
|
85
|
+
// is non-negotiable for cursor recovery after compaction.
|
|
86
|
+
const cbState = state.commandBus;
|
|
87
|
+
if (hookEvent !== 'sessionStart' && cbState.lastPollTs) {
|
|
88
|
+
const lastMs = Date.parse(cbState.lastPollTs);
|
|
89
|
+
if (!Number.isNaN(lastMs)) {
|
|
90
|
+
const ageSeconds = (Date.now() - lastMs) / 1000;
|
|
91
|
+
if (ageSeconds < resolved.minPollIntervalSeconds)
|
|
92
|
+
return empty;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const feature = readActiveFeature(repoRoot);
|
|
96
|
+
if (!feature)
|
|
97
|
+
return empty;
|
|
98
|
+
const apiUrl = readApiUrl(repoRoot);
|
|
99
|
+
if (!apiUrl)
|
|
100
|
+
return empty;
|
|
101
|
+
const token = readToken(repoRoot);
|
|
102
|
+
if (!token) {
|
|
103
|
+
logging.info(repoRoot, 'command_bus_skip_no_token', { hookEvent });
|
|
104
|
+
return empty;
|
|
105
|
+
}
|
|
106
|
+
const since = hookEvent === 'sessionStart' ? null : cbState.cursor;
|
|
107
|
+
try {
|
|
108
|
+
const fetched = await fetchInbox(apiUrl, feature, token, since);
|
|
109
|
+
const seen = new Set(cbState.recentEventIds ?? []);
|
|
110
|
+
const fresh = fetched.events.filter((e) => !seen.has(e.eventId));
|
|
111
|
+
const newRing = trimRecent([...(cbState.recentEventIds ?? []), ...fresh.map((e) => e.eventId)]);
|
|
112
|
+
const patch = {
|
|
113
|
+
commandBus: {
|
|
114
|
+
cursor: fetched.nextCursor || cbState.cursor,
|
|
115
|
+
lastPollTs: fetched.serverTs,
|
|
116
|
+
consecutiveFailures: 0,
|
|
117
|
+
recentEventIds: newRing,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
if (fresh.length > 0) {
|
|
121
|
+
logging.info(repoRoot, 'command_bus_events_received', {
|
|
122
|
+
hookEvent,
|
|
123
|
+
count: fresh.length,
|
|
124
|
+
types: fresh.map((e) => e.eventType),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { events: fresh, statePatch: patch, reactionMode };
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
const patch = {
|
|
131
|
+
commandBus: {
|
|
132
|
+
...cbState,
|
|
133
|
+
consecutiveFailures: (cbState.consecutiveFailures ?? 0) + 1,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
logging.info(repoRoot, 'command_bus_poll_failed', {
|
|
137
|
+
hookEvent,
|
|
138
|
+
error: e instanceof Error ? e.message : String(e),
|
|
139
|
+
consecutiveFailures: patch.commandBus.consecutiveFailures,
|
|
140
|
+
});
|
|
141
|
+
return { events: [], statePatch: patch, reactionMode };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Render zero or more events as a chat-ready Markdown block. Returns the empty string
|
|
146
|
+
* when there is nothing to surface — callers can guard with the return value's length.
|
|
147
|
+
*/
|
|
148
|
+
function renderForAgent(events, reactionMode) {
|
|
149
|
+
if (events.length === 0)
|
|
150
|
+
return '';
|
|
151
|
+
const lines = [];
|
|
152
|
+
lines.push(`⚠️ Dezycro Command Bus: ${events.length} new event(s) on this feature.`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
for (const e of events) {
|
|
155
|
+
lines.push(renderEvent(e));
|
|
156
|
+
}
|
|
157
|
+
if (events.some((e) => e.urgency === 'BLOCKING')) {
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push(reactionGuidance(reactionMode));
|
|
160
|
+
}
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
function renderEvent(e) {
|
|
164
|
+
if (e.eventType === 'INVALIDATION') {
|
|
165
|
+
const summary = stringField(e.payload, 'artifactSummary') ?? '(no summary)';
|
|
166
|
+
const reason = stringField(e.payload, 'reason') ?? '(no reason)';
|
|
167
|
+
const prev = stringField(e.payload, 'previousStatus') ?? '?';
|
|
168
|
+
const sourceLabel = `${e.sourceType.toLowerCase()} ${e.sourceId ?? '?'}`;
|
|
169
|
+
return `- INVALIDATION (${e.urgency}): ${sourceLabel} ("${truncate(summary, 120)}") — reason: "${truncate(reason, 200)}" — was ${prev}.`;
|
|
170
|
+
}
|
|
171
|
+
return `- ${e.eventType} (${e.urgency}) source=${e.sourceType}/${e.sourceId ?? '-'}.`;
|
|
172
|
+
}
|
|
173
|
+
function reactionGuidance(mode) {
|
|
174
|
+
switch (mode) {
|
|
175
|
+
case 'HALT_AND_REPLAN':
|
|
176
|
+
return 'Reaction mode: HALT_AND_REPLAN — pause your current line of work, acknowledge in chat, and re-plan around these invalidations before touching code again.';
|
|
177
|
+
case 'ACKNOWLEDGE_AND_CONTINUE':
|
|
178
|
+
return 'Reaction mode: ACKNOWLEDGE_AND_CONTINUE — surface this in your reply but continue current work; do not silently reuse invalidated artifacts.';
|
|
179
|
+
case 'SILENT_LOG':
|
|
180
|
+
return 'Reaction mode: SILENT_LOG — recorded for the session transcript; no chat interruption.';
|
|
181
|
+
default:
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/* ─── Helpers ─────────────────────────────────────────────────────────── */
|
|
186
|
+
function resolveConfig(c) {
|
|
187
|
+
const enabled = c?.enabled !== false; // default true
|
|
188
|
+
const hooks = new Set(c?.pollHooks ?? DEFAULT_HOOKS);
|
|
189
|
+
hooks.add('sessionStart'); // bootstrap is non-removable
|
|
190
|
+
const minPollIntervalSeconds = Math.max(0, c?.minPollIntervalSeconds ?? DEFAULT_MIN_POLL_INTERVAL_SECONDS);
|
|
191
|
+
const reactionMode = c?.reactionMode ?? DEFAULT_REACTION_MODE;
|
|
192
|
+
return { enabled, hooks, minPollIntervalSeconds, reactionMode };
|
|
193
|
+
}
|
|
194
|
+
function readActiveFeature(repoRoot) {
|
|
195
|
+
try {
|
|
196
|
+
const p = path.join(repoRoot, '.dezycro', 'active-feature.json');
|
|
197
|
+
if (!fs.existsSync(p))
|
|
198
|
+
return null;
|
|
199
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
200
|
+
const featureId = typeof raw.featureId === 'string' ? raw.featureId : null;
|
|
201
|
+
const workspaceId = typeof raw.workspaceId === 'string' ? raw.workspaceId : null;
|
|
202
|
+
if (!featureId)
|
|
203
|
+
return null;
|
|
204
|
+
if (workspaceId)
|
|
205
|
+
return { workspaceId, featureId };
|
|
206
|
+
// Older active-feature.json shapes only carry featureId; fall back to config.
|
|
207
|
+
const wsFromConfig = readWorkspaceId(repoRoot);
|
|
208
|
+
if (!wsFromConfig)
|
|
209
|
+
return null;
|
|
210
|
+
return { workspaceId: wsFromConfig, featureId };
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function readApiUrl(repoRoot) {
|
|
217
|
+
const cfg = readDezycroConfig(repoRoot);
|
|
218
|
+
if (!cfg)
|
|
219
|
+
return null;
|
|
220
|
+
const environments = Array.isArray(cfg.environments) ? cfg.environments : [];
|
|
221
|
+
const defaultEnv = environments.find((e) => e && e.default === true) ?? environments[0];
|
|
222
|
+
if (!defaultEnv)
|
|
223
|
+
return null;
|
|
224
|
+
const apiUrl = defaultEnv.apiUrl;
|
|
225
|
+
return typeof apiUrl === 'string' && apiUrl.length > 0 ? apiUrl.replace(/\/$/, '') : null;
|
|
226
|
+
}
|
|
227
|
+
function readWorkspaceId(repoRoot) {
|
|
228
|
+
const cfg = readDezycroConfig(repoRoot);
|
|
229
|
+
if (!cfg)
|
|
230
|
+
return null;
|
|
231
|
+
const wsId = cfg.workspaceId;
|
|
232
|
+
return typeof wsId === 'string' ? wsId : null;
|
|
233
|
+
}
|
|
234
|
+
function readDezycroConfig(repoRoot) {
|
|
235
|
+
try {
|
|
236
|
+
const p = path.join(repoRoot, '.dezycro', 'config.json');
|
|
237
|
+
if (!fs.existsSync(p))
|
|
238
|
+
return null;
|
|
239
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function readToken(repoRoot) {
|
|
246
|
+
try {
|
|
247
|
+
const p = path.join(repoRoot, '.dezycro', 'companion-token.local.json');
|
|
248
|
+
if (!fs.existsSync(p))
|
|
249
|
+
return null;
|
|
250
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
251
|
+
const token = raw.token;
|
|
252
|
+
return typeof token === 'string' && token.length > 0 ? token : null;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function fetchInbox(apiUrl, feature, token, since) {
|
|
259
|
+
const url = new URL(`${apiUrl}/api/v1/workspaces/${feature.workspaceId}/features/${feature.featureId}/inbox`);
|
|
260
|
+
if (since)
|
|
261
|
+
url.searchParams.set('since', since);
|
|
262
|
+
url.searchParams.set('limit', String(POLL_LIMIT));
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch(url.toString(), {
|
|
267
|
+
method: 'GET',
|
|
268
|
+
headers: {
|
|
269
|
+
Authorization: `Bearer ${token}`,
|
|
270
|
+
Accept: 'application/json',
|
|
271
|
+
},
|
|
272
|
+
signal: controller.signal,
|
|
273
|
+
});
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
throw new Error(`inbox poll returned ${res.status}`);
|
|
276
|
+
}
|
|
277
|
+
const body = (await res.json());
|
|
278
|
+
if (!body || typeof body.serverTs !== 'string' || typeof body.nextCursor !== 'string') {
|
|
279
|
+
throw new Error('inbox poll response missing serverTs/nextCursor');
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
serverTs: body.serverTs,
|
|
283
|
+
nextCursor: body.nextCursor,
|
|
284
|
+
events: Array.isArray(body.events) ? body.events : [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
clearTimeout(timeoutId);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function trimRecent(ids) {
|
|
292
|
+
return ids.length <= RECENT_EVENT_IDS_CAP ? ids : ids.slice(ids.length - RECENT_EVENT_IDS_CAP);
|
|
293
|
+
}
|
|
294
|
+
function stringField(payload, key) {
|
|
295
|
+
const v = payload?.[key];
|
|
296
|
+
return typeof v === 'string' ? v : null;
|
|
297
|
+
}
|
|
298
|
+
function truncate(s, max) {
|
|
299
|
+
return s.length <= max ? s : s.slice(0, max - 1) + '…';
|
|
300
|
+
}
|
|
301
|
+
/* Exported for tests. */
|
|
302
|
+
exports._internals = {
|
|
303
|
+
resolveConfig,
|
|
304
|
+
renderEvent,
|
|
305
|
+
reactionGuidance,
|
|
306
|
+
trimRecent,
|
|
307
|
+
};
|
|
@@ -11,11 +11,11 @@ import type { PostToolUsePayload, PreToolUsePayload, RouterInvocation, StopPaylo
|
|
|
11
11
|
export declare const QUALITY_GATE_REASON_PREFIX = "TRD quality gate (deterministic layer) blocked APPROVED transition: ";
|
|
12
12
|
export declare function dispatch(invocation: RouterInvocation): Promise<number>;
|
|
13
13
|
declare function handleUpdateTaskGate(payload: PreToolUsePayload): number;
|
|
14
|
-
declare function handlePostEdit(payload: PostToolUsePayload): number
|
|
14
|
+
declare function handlePostEdit(payload: PostToolUsePayload): Promise<number>;
|
|
15
15
|
declare function extractEditedPaths(toolInput: Record<string, unknown> | null | undefined): string[];
|
|
16
|
-
declare function handleSessionStart(_payload: SessionStartPayload): number
|
|
17
|
-
declare function handleUserPromptSubmit(payload: UserPromptSubmitPayload): number
|
|
18
|
-
declare function handleStop(payload: StopPayload): number
|
|
16
|
+
declare function handleSessionStart(_payload: SessionStartPayload): Promise<number>;
|
|
17
|
+
declare function handleUserPromptSubmit(payload: UserPromptSubmitPayload): Promise<number>;
|
|
18
|
+
declare function handleStop(payload: StopPayload): Promise<number>;
|
|
19
19
|
declare function transcriptCharsSinceLastStop(payload: StopPayload): number | null;
|
|
20
20
|
declare function readActiveFeatureId(repoRoot: string): string | null;
|
|
21
21
|
declare function handleWorkbookMutation(payload: PostToolUsePayload): number;
|
package/dist/hooks/lib/router.js
CHANGED
|
@@ -57,6 +57,7 @@ const hashing = __importStar(require("./hashing"));
|
|
|
57
57
|
const workbook = __importStar(require("./workbook"));
|
|
58
58
|
const undo = __importStar(require("./undo"));
|
|
59
59
|
const coverage = __importStar(require("./coverage"));
|
|
60
|
+
const commandBus = __importStar(require("./command-bus"));
|
|
60
61
|
const DONE_STATUSES = new Set(['DONE', 'PUSHED']);
|
|
61
62
|
const WRAP_PHRASES = [
|
|
62
63
|
'/dezycro:sync',
|
|
@@ -239,10 +240,31 @@ function handleChangeDocumentStatus(payload) {
|
|
|
239
240
|
return 0;
|
|
240
241
|
}
|
|
241
242
|
/* ─── Pillars 1+2 — post-edit pulse + controller-match (TRD §5.1+§5.2) ─── */
|
|
242
|
-
function handlePostEdit(payload) {
|
|
243
|
+
async function handlePostEdit(payload) {
|
|
243
244
|
const repoRoot = config.findRepoRoot(process.cwd());
|
|
244
245
|
if (!repoRoot)
|
|
245
246
|
return 0;
|
|
247
|
+
// Autonomous-loop poll for the Agent Command Bus inbox (TRD §Pillar 1 PostToolUse).
|
|
248
|
+
// Subject to the floor, so most edits no-op. When events arrive, surface them as
|
|
249
|
+
// additionalContext and short-circuit — pulse/spec-publish logic skips this turn.
|
|
250
|
+
{
|
|
251
|
+
const cfg = safeLoadConfig(repoRoot);
|
|
252
|
+
const cur = state.read(repoRoot);
|
|
253
|
+
const result = await commandBus.pollIfDue(repoRoot, 'postToolUse', cfg?.companion?.commandBus, cur);
|
|
254
|
+
if (result.statePatch)
|
|
255
|
+
state.patch(repoRoot, result.statePatch);
|
|
256
|
+
const block = commandBus.renderForAgent(result.events, result.reactionMode);
|
|
257
|
+
if (block) {
|
|
258
|
+
const out = {
|
|
259
|
+
hookSpecificOutput: {
|
|
260
|
+
hookEventName: 'PostToolUse',
|
|
261
|
+
additionalContext: block,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
process.stdout.write(JSON.stringify(out));
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
246
268
|
const toolInput = (payload && payload.tool_input) || {};
|
|
247
269
|
const editedPaths = extractEditedPaths(toolInput);
|
|
248
270
|
if (editedPaths.length === 0)
|
|
@@ -406,7 +428,7 @@ function handleAuthoringState(flags) {
|
|
|
406
428
|
}
|
|
407
429
|
/* ─── Pillar 3 — Proactive Workbook Updates (TRD §5.3) ────────────────────── */
|
|
408
430
|
const TRIVIAL_TURN_CHAR_DEFAULT = 600;
|
|
409
|
-
function handleSessionStart(_payload) {
|
|
431
|
+
async function handleSessionStart(_payload) {
|
|
410
432
|
const repoRoot = config.findRepoRoot(process.cwd());
|
|
411
433
|
if (!repoRoot)
|
|
412
434
|
return 0;
|
|
@@ -418,9 +440,16 @@ function handleSessionStart(_payload) {
|
|
|
418
440
|
coverageFetchPending: false,
|
|
419
441
|
});
|
|
420
442
|
logging.info(repoRoot, 'session_start', {});
|
|
443
|
+
// Bootstrap-on-start poll for the Agent Command Bus inbox. Bypasses the floor.
|
|
444
|
+
// Failures swallowed; fetched events surface on the next UserPromptSubmit.
|
|
445
|
+
const cfg = safeLoadConfig(repoRoot);
|
|
446
|
+
const current = state.read(repoRoot);
|
|
447
|
+
const result = await commandBus.pollIfDue(repoRoot, 'sessionStart', cfg?.companion?.commandBus, current);
|
|
448
|
+
if (result.statePatch)
|
|
449
|
+
state.patch(repoRoot, result.statePatch);
|
|
421
450
|
return 0;
|
|
422
451
|
}
|
|
423
|
-
function handleUserPromptSubmit(payload) {
|
|
452
|
+
async function handleUserPromptSubmit(payload) {
|
|
424
453
|
const repoRoot = config.findRepoRoot(process.cwd());
|
|
425
454
|
if (!repoRoot)
|
|
426
455
|
return 0;
|
|
@@ -430,6 +459,27 @@ function handleUserPromptSubmit(payload) {
|
|
|
430
459
|
currentTurnId: nextTurnId,
|
|
431
460
|
turnAutoLogCount: 0,
|
|
432
461
|
});
|
|
462
|
+
// Pre-reasoning poll for the Agent Command Bus inbox. If new events arrived
|
|
463
|
+
// since the last poll, surface them as additionalContext so the agent factors
|
|
464
|
+
// them in before responding. Wins over the existing undo / coverage paths
|
|
465
|
+
// for this turn — invalidations are blocking by nature.
|
|
466
|
+
{
|
|
467
|
+
const cfg = safeLoadConfig(repoRoot);
|
|
468
|
+
const result = await commandBus.pollIfDue(repoRoot, 'userPromptSubmit', cfg?.companion?.commandBus, current);
|
|
469
|
+
if (result.statePatch)
|
|
470
|
+
state.patch(repoRoot, result.statePatch);
|
|
471
|
+
const block = commandBus.renderForAgent(result.events, result.reactionMode);
|
|
472
|
+
if (block) {
|
|
473
|
+
const out = {
|
|
474
|
+
hookSpecificOutput: {
|
|
475
|
+
hookEventName: 'UserPromptSubmit',
|
|
476
|
+
additionalContext: block,
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
process.stdout.write(JSON.stringify(out));
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
433
483
|
const userMessage = String(payload?.prompt ?? payload?.user_message ?? payload?.message ?? '').trim();
|
|
434
484
|
const wrap = detectWrapPhrase(userMessage);
|
|
435
485
|
if (wrap) {
|
|
@@ -487,11 +537,26 @@ function maybeSurfaceCoverage(repoRoot, current) {
|
|
|
487
537
|
};
|
|
488
538
|
process.stdout.write(JSON.stringify(out));
|
|
489
539
|
}
|
|
490
|
-
function handleStop(payload) {
|
|
540
|
+
async function handleStop(payload) {
|
|
491
541
|
const repoRoot = config.findRepoRoot(process.cwd());
|
|
492
542
|
if (!repoRoot)
|
|
493
543
|
return 0;
|
|
494
544
|
const cfg = safeLoadConfig(repoRoot);
|
|
545
|
+
// Between-turn poll for the Agent Command Bus inbox (TRD §Pillar 1 Stop). Subject to
|
|
546
|
+
// the floor. When events arrive, surface as a top-level systemMessage (Stop has no
|
|
547
|
+
// hookSpecificOutput) and short-circuit — workbook sweep skips this turn.
|
|
548
|
+
{
|
|
549
|
+
const cur0 = state.read(repoRoot);
|
|
550
|
+
const result = await commandBus.pollIfDue(repoRoot, 'stop', cfg?.companion?.commandBus, cur0);
|
|
551
|
+
if (result.statePatch)
|
|
552
|
+
state.patch(repoRoot, result.statePatch);
|
|
553
|
+
const block = commandBus.renderForAgent(result.events, result.reactionMode);
|
|
554
|
+
if (block) {
|
|
555
|
+
const out = { systemMessage: block };
|
|
556
|
+
process.stdout.write(JSON.stringify(out));
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
495
560
|
const ws = cfg?.companion?.autoWorkbookUpdates || {};
|
|
496
561
|
if (cfg?.companion?.safeMode === true)
|
|
497
562
|
return silentExit();
|
package/dist/hooks/lib/state.js
CHANGED
|
@@ -93,6 +93,12 @@ exports.DEFAULT_COMPANION_STATE = Object.freeze({
|
|
|
93
93
|
pendingWorkbookSweep: false,
|
|
94
94
|
pendingWorkbookSweepReason: null,
|
|
95
95
|
lastSeenVerifyCompletionTs: null,
|
|
96
|
+
commandBus: {
|
|
97
|
+
cursor: null,
|
|
98
|
+
lastPollTs: null,
|
|
99
|
+
consecutiveFailures: 0,
|
|
100
|
+
recentEventIds: [],
|
|
101
|
+
},
|
|
96
102
|
metrics: exports.DEFAULT_COMPANION_METRICS,
|
|
97
103
|
});
|
|
98
104
|
function statePath(repoRoot) {
|
package/dist/hooks/types.d.ts
CHANGED
|
@@ -139,6 +139,7 @@ export interface CompanionState {
|
|
|
139
139
|
pendingWorkbookSweep: boolean;
|
|
140
140
|
pendingWorkbookSweepReason: string | null;
|
|
141
141
|
lastSeenVerifyCompletionTs: string | null;
|
|
142
|
+
commandBus: CommandBusState;
|
|
142
143
|
metrics: CompanionMetrics;
|
|
143
144
|
}
|
|
144
145
|
/**
|
|
@@ -198,12 +199,40 @@ export interface AuthoringConfig {
|
|
|
198
199
|
enabled?: boolean;
|
|
199
200
|
};
|
|
200
201
|
}
|
|
202
|
+
export type CommandBusHook = 'sessionStart' | 'userPromptSubmit' | 'stop' | 'postToolUse';
|
|
203
|
+
export type CommandBusReactionMode = 'HALT_AND_REPLAN' | 'ACKNOWLEDGE_AND_CONTINUE' | 'SILENT_LOG';
|
|
204
|
+
export interface CommandBusConfig {
|
|
205
|
+
enabled?: boolean;
|
|
206
|
+
pollHooks?: CommandBusHook[];
|
|
207
|
+
minPollIntervalSeconds?: number;
|
|
208
|
+
reactionMode?: CommandBusReactionMode;
|
|
209
|
+
}
|
|
210
|
+
export interface CommandBusState {
|
|
211
|
+
cursor: string | null;
|
|
212
|
+
lastPollTs: string | null;
|
|
213
|
+
consecutiveFailures: number;
|
|
214
|
+
recentEventIds: string[];
|
|
215
|
+
}
|
|
216
|
+
export interface InboxEvent {
|
|
217
|
+
eventId: string;
|
|
218
|
+
featureId: string;
|
|
219
|
+
eventType: string;
|
|
220
|
+
sourceType: string;
|
|
221
|
+
sourceId: string | null;
|
|
222
|
+
urgency: 'BLOCKING' | 'INFORMATIONAL';
|
|
223
|
+
payload: Record<string, unknown>;
|
|
224
|
+
createdTs: string;
|
|
225
|
+
ttlAt: string;
|
|
226
|
+
issuerType: string;
|
|
227
|
+
issuerId: string | null;
|
|
228
|
+
}
|
|
201
229
|
export interface CompanionConfig {
|
|
202
230
|
companion?: {
|
|
203
231
|
autoVerify?: AutoVerifyConfig;
|
|
204
232
|
autoWorkbookUpdates?: AutoWorkbookUpdatesConfig;
|
|
205
233
|
coverageNudges?: CoverageNudgesConfig;
|
|
206
234
|
authoring?: AuthoringConfig;
|
|
235
|
+
commandBus?: CommandBusConfig;
|
|
207
236
|
safeMode?: boolean;
|
|
208
237
|
};
|
|
209
238
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dezycro-ai/agent-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Official Dezycro plugin for AI coding agents (Claude Code today, more to come) — reverse-engineer your codebase into features, PRDs, TRDs, and live verification.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dezycro-setup": "./setup.js",
|
package/skills/init/SKILL.md
CHANGED
|
@@ -86,7 +86,19 @@ If yes, ask for:
|
|
|
86
86
|
- API URL (e.g., `https://api.dev.example.com`)
|
|
87
87
|
- Health check endpoint (default: same as local)
|
|
88
88
|
|
|
89
|
-
## Step 6:
|
|
89
|
+
## Step 6: Agent Command Bus Reaction Mode
|
|
90
|
+
|
|
91
|
+
The Companion can poll Dezycro's per-feature Agent Command Bus inbox so the platform can deliver in-turn commands (today: invalidations from the workbook UI; future: recap, refocus, plan-check, …) to the agent. Ask the user how the agent should react when a **blocking** event arrives mid-feature:
|
|
92
|
+
|
|
93
|
+
> When the platform sends a blocking event (e.g., a user invalidates an assumption in the workbook), how should the agent react?
|
|
94
|
+
>
|
|
95
|
+
> 1. **Halt and re-plan** (recommended) — pause current work at the next hook boundary, acknowledge in chat, re-plan before touching code again.
|
|
96
|
+
> 2. **Acknowledge and keep going** — surface the event in the next reply, continue current work, do not silently reuse invalidated artifacts.
|
|
97
|
+
> 3. **Silent log only** — record in transcript without interrupting chat. Not recommended for v1 invalidations.
|
|
98
|
+
|
|
99
|
+
Capture the choice as one of `HALT_AND_REPLAN`, `ACKNOWLEDGE_AND_CONTINUE`, `SILENT_LOG`. If the user does not pick, default to `HALT_AND_REPLAN`.
|
|
100
|
+
|
|
101
|
+
## Step 7: Write Config
|
|
90
102
|
|
|
91
103
|
Create the `.dezycro/` directory and write `.dezycro/config.json`:
|
|
92
104
|
|
|
@@ -103,6 +115,14 @@ Create the `.dezycro/` directory and write `.dezycro/config.json`:
|
|
|
103
115
|
"healthCheck": "curl -sf <apiUrl><healthPath>",
|
|
104
116
|
"default": true
|
|
105
117
|
}
|
|
118
|
+
},
|
|
119
|
+
"companion": {
|
|
120
|
+
"commandBus": {
|
|
121
|
+
"enabled": true,
|
|
122
|
+
"pollHooks": ["sessionStart", "userPromptSubmit", "stop", "postToolUse"],
|
|
123
|
+
"minPollIntervalSeconds": 60,
|
|
124
|
+
"reactionMode": "<HALT_AND_REPLAN | ACKNOWLEDGE_AND_CONTINUE | SILENT_LOG>"
|
|
125
|
+
}
|
|
106
126
|
}
|
|
107
127
|
}
|
|
108
128
|
```
|
|
@@ -112,16 +132,36 @@ Omit `setup` and `teardown` keys if the user skipped environment setup. Include
|
|
|
112
132
|
Use the Bash tool to create the directory: `mkdir -p .dezycro`
|
|
113
133
|
Use the Write tool to write the config file.
|
|
114
134
|
|
|
115
|
-
## Step
|
|
135
|
+
## Step 8: Companion API Token
|
|
136
|
+
|
|
137
|
+
The Companion plugin needs a bearer token to read the Agent Command Bus inbox directly from the platform. This is **separate from the MCP server token** (the MCP token is configured in Claude Code's MCP settings; this token is read locally by the runner). Yes, that means setting the token in two places — by design, since the runner runs outside of MCP.
|
|
138
|
+
|
|
139
|
+
Tell the user:
|
|
140
|
+
|
|
141
|
+
> Create `.dezycro/companion-token.local.json` and paste your Dezycro API token. Get one from **Dezycro > Settings > API Keys** (same key you can use for MCP). Shape:
|
|
142
|
+
>
|
|
143
|
+
> ```json
|
|
144
|
+
> { "token": "dzy_…" }
|
|
145
|
+
> ```
|
|
146
|
+
>
|
|
147
|
+
> **Always gitignore this file** — it contains a real secret.
|
|
148
|
+
|
|
149
|
+
Without this file the Companion silently skips command-bus polling (no errors, no surfaced events). You can always add it later.
|
|
150
|
+
|
|
151
|
+
## Step 9: Gitignore Check
|
|
116
152
|
|
|
117
153
|
Read `.gitignore` (if it exists) and check if it already covers `.dezycro/`. If not, suggest adding:
|
|
118
154
|
|
|
119
155
|
```
|
|
120
156
|
# Dezycro local state
|
|
121
157
|
.dezycro/active-feature.json
|
|
158
|
+
.dezycro/companion-state.json
|
|
159
|
+
.dezycro/companion-token.local.json
|
|
122
160
|
.dezycro/bin/
|
|
123
161
|
```
|
|
124
162
|
|
|
163
|
+
`.dezycro/companion-token.local.json` MUST be gitignored — it holds a real API token.
|
|
164
|
+
|
|
125
165
|
Ask the user if they want `.dezycro/config.json` committed (recommended) or gitignored too. If they want it gitignored, add `.dezycro/` instead.
|
|
126
166
|
|
|
127
167
|
## Done
|