@dezycro-ai/agent-plugin 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +14 -98
  2. package/dist/cli/dezycro.js +50 -0
  3. package/dist/cli/install.js +7 -0
  4. package/dist/dezycro-config.d.ts +52 -0
  5. package/dist/dezycro-config.js +167 -0
  6. package/dist/gateway/compact-cli.js +8 -0
  7. package/dist/gateway/proxy.js +35 -0
  8. package/dist/hooks/lib/router.js +14 -782
  9. package/dist/schemas/anthropic.d.ts +119 -0
  10. package/dist/schemas/anthropic.js +43 -0
  11. package/dist/schemas/dezycro.d.ts +19 -0
  12. package/dist/schemas/dezycro.js +22 -0
  13. package/dist/schemas/gateway.d.ts +40 -0
  14. package/dist/schemas/gateway.js +30 -0
  15. package/dist/schemas/gating.d.ts +32 -0
  16. package/dist/schemas/gating.js +15 -0
  17. package/dist/schemas/index.d.ts +14 -0
  18. package/dist/schemas/index.js +29 -0
  19. package/package.json +21 -13
  20. package/LICENSE +0 -18
  21. package/bin/resolve-auth.js +0 -112
  22. package/dist/hooks/lib/authoring.d.ts +0 -118
  23. package/dist/hooks/lib/authoring.js +0 -277
  24. package/dist/hooks/lib/command-bus.d.ts +0 -53
  25. package/dist/hooks/lib/command-bus.js +0 -318
  26. package/dist/hooks/lib/config.d.ts +0 -28
  27. package/dist/hooks/lib/config.js +0 -175
  28. package/dist/hooks/lib/controller-match.d.ts +0 -16
  29. package/dist/hooks/lib/controller-match.js +0 -96
  30. package/dist/hooks/lib/coverage.d.ts +0 -17
  31. package/dist/hooks/lib/coverage.js +0 -66
  32. package/dist/hooks/lib/hashing.d.ts +0 -8
  33. package/dist/hooks/lib/hashing.js +0 -49
  34. package/dist/hooks/lib/logging.d.ts +0 -13
  35. package/dist/hooks/lib/logging.js +0 -72
  36. package/dist/hooks/lib/publish-spec.d.ts +0 -17
  37. package/dist/hooks/lib/publish-spec.js +0 -64
  38. package/dist/hooks/lib/quality-gate.d.ts +0 -67
  39. package/dist/hooks/lib/quality-gate.js +0 -187
  40. package/dist/hooks/lib/router.d.ts +0 -32
  41. package/dist/hooks/lib/state.d.ts +0 -34
  42. package/dist/hooks/lib/state.js +0 -244
  43. package/dist/hooks/lib/undo.d.ts +0 -11
  44. package/dist/hooks/lib/undo.js +0 -71
  45. package/dist/hooks/lib/verify.d.ts +0 -28
  46. package/dist/hooks/lib/verify.js +0 -94
  47. package/dist/hooks/lib/workbook.d.ts +0 -21
  48. package/dist/hooks/lib/workbook.js +0 -77
  49. package/dist/hooks/types.d.ts +0 -278
  50. package/dist/hooks/types.js +0 -14
  51. package/install.js +0 -84
  52. package/setup.js +0 -53
@@ -1,277 +0,0 @@
1
- /**
2
- * authoring.ts — state machine helpers for /dezycro:author (TRD §5.5.1).
3
- *
4
- * The skill markdown drives the agent turn-by-turn; this module owns the
5
- * persisted side effects (authoring mode in companion-state.json, the
6
- * transcript at .dezycro/authoring-transcript.json, and the most-recent-draft
7
- * sidecar at .dezycro/authoring-current-draft.json).
8
- *
9
- * No LLM calls live here.
10
- */
11
- 'use strict';
12
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- var desc = Object.getOwnPropertyDescriptor(m, k);
15
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
- desc = { enumerable: true, get: function() { return m[k]; } };
17
- }
18
- Object.defineProperty(o, k2, desc);
19
- }) : (function(o, m, k, k2) {
20
- if (k2 === undefined) k2 = k;
21
- o[k2] = m[k];
22
- }));
23
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
- Object.defineProperty(o, "default", { enumerable: true, value: v });
25
- }) : function(o, v) {
26
- o["default"] = v;
27
- });
28
- var __importStar = (this && this.__importStar) || (function () {
29
- var ownKeys = function(o) {
30
- ownKeys = Object.getOwnPropertyNames || function (o) {
31
- var ar = [];
32
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
- return ar;
34
- };
35
- return ownKeys(o);
36
- };
37
- return function (mod) {
38
- if (mod && mod.__esModule) return mod;
39
- var result = {};
40
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
- __setModuleDefault(result, mod);
42
- return result;
43
- };
44
- })();
45
- Object.defineProperty(exports, "__esModule", { value: true });
46
- exports._draftSidecarPath = exports._transcriptPath = exports.DRAFT_SIDECAR_FILENAME = exports.TRANSCRIPT_FILENAME = exports.NON_SKIPPABLE_STATES = exports.SKIP_PHRASES = exports.DOC_TYPES = exports.STATES = void 0;
47
- exports.enter = enter;
48
- exports.exit = exit;
49
- exports.isActive = isActive;
50
- exports.getState = getState;
51
- exports.recordAnswer = recordAnswer;
52
- exports.recordDiscoverySummary = recordDiscoverySummary;
53
- exports.recordDraft = recordDraft;
54
- exports.recordGateResult = recordGateResult;
55
- exports.readTranscript = readTranscript;
56
- exports.writeDraftSidecar = writeDraftSidecar;
57
- exports.readDraftSidecar = readDraftSidecar;
58
- exports.isSkipPhrase = isSkipPhrase;
59
- exports.isNonSkippableState = isNonSkippableState;
60
- const fs = __importStar(require("fs"));
61
- const path = __importStar(require("path"));
62
- const state = __importStar(require("./state"));
63
- exports.STATES = Object.freeze({
64
- IDLE: 'IDLE',
65
- DETECT_INTENT: 'DETECT_INTENT',
66
- SCAN_EXISTING_WORK: 'SCAN_EXISTING_WORK',
67
- CLARIFY: 'CLARIFY',
68
- PROPOSE_PLACEMENT: 'PROPOSE_PLACEMENT',
69
- CREATE_FEATURE: 'CREATE_FEATURE',
70
- TRD_DISCOVERY: 'TRD_DISCOVERY',
71
- MIN_CONTEXT_GATE: 'MIN_CONTEXT_GATE',
72
- DRAFT: 'DRAFT',
73
- ITERATE: 'ITERATE',
74
- QUALITY_GATE: 'QUALITY_GATE',
75
- FINALIZE: 'FINALIZE',
76
- });
77
- exports.DOC_TYPES = Object.freeze({ PRD: 'PRD', TRD: 'TRD' });
78
- exports.SKIP_PHRASES = Object.freeze(['skip', 'draft it', 'just write it', 'next']);
79
- exports.NON_SKIPPABLE_STATES = Object.freeze([
80
- exports.STATES.MIN_CONTEXT_GATE,
81
- exports.STATES.QUALITY_GATE,
82
- ]);
83
- exports.TRANSCRIPT_FILENAME = 'authoring-transcript.json';
84
- exports.DRAFT_SIDECAR_FILENAME = 'authoring-current-draft.json';
85
- function transcriptPath(repoRoot) {
86
- return path.join(repoRoot, '.dezycro', exports.TRANSCRIPT_FILENAME);
87
- }
88
- function draftSidecarPath(repoRoot) {
89
- return path.join(repoRoot, '.dezycro', exports.DRAFT_SIDECAR_FILENAME);
90
- }
91
- function ensureDezycroDir(repoRoot) {
92
- fs.mkdirSync(path.join(repoRoot, '.dezycro'), { recursive: true });
93
- }
94
- function writeJsonAtomic(filePath, data) {
95
- const tmp = `${filePath}.tmp`;
96
- const fd = fs.openSync(tmp, 'w');
97
- try {
98
- fs.writeSync(fd, JSON.stringify(data, null, 2));
99
- fs.fsyncSync(fd);
100
- }
101
- finally {
102
- fs.closeSync(fd);
103
- }
104
- fs.renameSync(tmp, filePath);
105
- }
106
- function readJson(filePath) {
107
- try {
108
- const raw = fs.readFileSync(filePath, 'utf8');
109
- return JSON.parse(raw);
110
- }
111
- catch {
112
- return null;
113
- }
114
- }
115
- function enter(repoRoot, opts) {
116
- if (!opts || !opts.docId || !opts.docType) {
117
- throw new Error('authoring.enter: docId and docType required');
118
- }
119
- if (opts.docType !== exports.DOC_TYPES.PRD && opts.docType !== exports.DOC_TYPES.TRD) {
120
- throw new Error(`authoring.enter: invalid docType ${opts.docType}`);
121
- }
122
- ensureDezycroDir(repoRoot);
123
- const tp = transcriptPath(repoRoot);
124
- const now = new Date().toISOString();
125
- const existing = readJson(tp);
126
- const transcript = existing && existing.docId === opts.docId
127
- ? existing
128
- : {
129
- docId: opts.docId,
130
- docType: opts.docType,
131
- featureId: opts.featureId || null,
132
- seed: opts.seed || null,
133
- startedTs: now,
134
- discoverySummary: null,
135
- qaTurns: [],
136
- draftHistory: [],
137
- qualityGateHistory: [],
138
- };
139
- writeJsonAtomic(tp, transcript);
140
- state.patch(repoRoot, {
141
- authoringMode: true,
142
- authoringDocId: opts.docId,
143
- authoringDocType: opts.docType,
144
- authoringTranscriptPath: tp,
145
- authoringStartedTs: now,
146
- });
147
- return transcript;
148
- }
149
- function exit(repoRoot) {
150
- state.patch(repoRoot, {
151
- authoringMode: false,
152
- authoringDocId: null,
153
- authoringDocType: null,
154
- authoringTranscriptPath: null,
155
- authoringStartedTs: null,
156
- });
157
- for (const p of [transcriptPath(repoRoot), draftSidecarPath(repoRoot)]) {
158
- try {
159
- fs.unlinkSync(p);
160
- }
161
- catch { /* ignore */ }
162
- }
163
- }
164
- function isActive(repoRoot) {
165
- const s = state.read(repoRoot);
166
- return Boolean(s && s.authoringMode);
167
- }
168
- function getState(repoRoot) {
169
- const s = state.read(repoRoot);
170
- return {
171
- active: Boolean(s && s.authoringMode),
172
- docId: s ? s.authoringDocId : null,
173
- docType: s ? s.authoringDocType : null,
174
- transcriptPath: s ? s.authoringTranscriptPath : null,
175
- startedTs: s ? s.authoringStartedTs : null,
176
- };
177
- }
178
- function recordAnswer(repoRoot, opts) {
179
- if (!opts || typeof opts.question !== 'string' || typeof opts.answer !== 'string') {
180
- throw new Error('authoring.recordAnswer: question and answer required');
181
- }
182
- const tp = transcriptPath(repoRoot);
183
- const t = readJson(tp);
184
- if (!t)
185
- return null;
186
- t.qaTurns = (t.qaTurns || []).concat([{
187
- ts: new Date().toISOString(),
188
- question: opts.question,
189
- answer: opts.answer,
190
- category: opts.category || null,
191
- }]);
192
- writeJsonAtomic(tp, t);
193
- return t;
194
- }
195
- function recordDiscoverySummary(repoRoot, discoverySummary) {
196
- const tp = transcriptPath(repoRoot);
197
- const t = readJson(tp);
198
- if (!t)
199
- return null;
200
- t.discoverySummary = discoverySummary || null;
201
- writeJsonAtomic(tp, t);
202
- return t;
203
- }
204
- function recordDraft(repoRoot, opts) {
205
- if (!opts || typeof opts.contentHash !== 'string') {
206
- throw new Error('authoring.recordDraft: contentHash required');
207
- }
208
- const tp = transcriptPath(repoRoot);
209
- const t = readJson(tp);
210
- if (!t)
211
- return null;
212
- t.draftHistory = (t.draftHistory || []).concat([{
213
- ts: new Date().toISOString(),
214
- contentHash: opts.contentHash,
215
- }]);
216
- writeJsonAtomic(tp, t);
217
- return t;
218
- }
219
- function recordGateResult(repoRoot, opts) {
220
- if (!opts || typeof opts.passed !== 'boolean') {
221
- throw new Error('authoring.recordGateResult: passed required');
222
- }
223
- const tp = transcriptPath(repoRoot);
224
- const t = readJson(tp);
225
- if (!t)
226
- return null;
227
- t.qualityGateHistory = (t.qualityGateHistory || []).concat([{
228
- ts: new Date().toISOString(),
229
- passed: opts.passed,
230
- blockingIssues: opts.blockingIssues || [],
231
- score: typeof opts.score === 'number' ? opts.score : null,
232
- deterministic: opts.deterministic === true,
233
- }]);
234
- writeJsonAtomic(tp, t);
235
- return t;
236
- }
237
- function readTranscript(repoRoot) {
238
- return readJson(transcriptPath(repoRoot));
239
- }
240
- function writeDraftSidecar(repoRoot, opts) {
241
- if (!opts || !opts.docId || !opts.docType || typeof opts.content !== 'string') {
242
- throw new Error('authoring.writeDraftSidecar: docId, docType, content required');
243
- }
244
- ensureDezycroDir(repoRoot);
245
- writeJsonAtomic(draftSidecarPath(repoRoot), {
246
- docId: opts.docId,
247
- docType: opts.docType,
248
- content: opts.content,
249
- prdDocId: opts.prdDocId || null,
250
- prdContent: opts.prdContent || null,
251
- updatedTs: new Date().toISOString(),
252
- });
253
- }
254
- function readDraftSidecar(repoRoot) {
255
- return readJson(draftSidecarPath(repoRoot));
256
- }
257
- function isSkipPhrase(userMessage) {
258
- if (typeof userMessage !== 'string')
259
- return false;
260
- const trimmed = userMessage.trim().toLowerCase();
261
- if (!trimmed)
262
- return false;
263
- const stripped = trimmed.replace(/[.!?,;:]+$/g, '').trim();
264
- for (const phrase of exports.SKIP_PHRASES) {
265
- if (stripped === phrase)
266
- return true;
267
- if (stripped.startsWith(`${phrase} `) || stripped.startsWith(`${phrase},`))
268
- return true;
269
- }
270
- return false;
271
- }
272
- function isNonSkippableState(stateName) {
273
- return exports.NON_SKIPPABLE_STATES.includes(stateName);
274
- }
275
- // Path helpers exposed for tests and the router.
276
- exports._transcriptPath = transcriptPath;
277
- exports._draftSidecarPath = draftSidecarPath;
@@ -1,53 +0,0 @@
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 {};
@@ -1,318 +0,0 @@
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 os = __importStar(require("os"));
59
- const path = __importStar(require("path"));
60
- const logging = __importStar(require("./logging"));
61
- const DEFAULT_HOOKS = [
62
- 'sessionStart',
63
- 'userPromptSubmit',
64
- 'stop',
65
- 'postToolUse',
66
- ];
67
- const DEFAULT_MIN_POLL_INTERVAL_SECONDS = 60;
68
- const DEFAULT_REACTION_MODE = 'HALT_AND_REPLAN';
69
- const RECENT_EVENT_IDS_CAP = 200;
70
- const POLL_LIMIT = 100;
71
- /**
72
- * Entry point called from each hook handler. Returns the events that should be surfaced
73
- * to the agent on this hook fire, plus a state patch the caller must apply.
74
- *
75
- * Never throws — failures are logged and result in `{events: [], statePatch: null}`.
76
- */
77
- async function pollIfDue(repoRoot, hookEvent, config, state) {
78
- const resolved = resolveConfig(config);
79
- const reactionMode = resolved.reactionMode;
80
- const empty = { events: [], statePatch: null, reactionMode };
81
- if (!resolved.enabled)
82
- return empty;
83
- if (!resolved.hooks.has(hookEvent) && hookEvent !== 'sessionStart')
84
- return empty;
85
- // SessionStart always polls regardless of the configured hook set — bootstrap-on-start
86
- // is non-negotiable for cursor recovery after compaction.
87
- const cbState = state.commandBus;
88
- if (hookEvent !== 'sessionStart' && cbState.lastPollTs) {
89
- const lastMs = Date.parse(cbState.lastPollTs);
90
- if (!Number.isNaN(lastMs)) {
91
- const ageSeconds = (Date.now() - lastMs) / 1000;
92
- if (ageSeconds < resolved.minPollIntervalSeconds)
93
- return empty;
94
- }
95
- }
96
- const feature = readActiveFeature(repoRoot);
97
- if (!feature)
98
- return empty;
99
- const apiUrl = readApiUrl(repoRoot);
100
- if (!apiUrl)
101
- return empty;
102
- const token = readToken(repoRoot);
103
- if (!token) {
104
- logging.info(repoRoot, 'command_bus_skip_no_token', { hookEvent });
105
- return empty;
106
- }
107
- const since = hookEvent === 'sessionStart' ? null : cbState.cursor;
108
- try {
109
- const fetched = await fetchInbox(apiUrl, feature, token, since);
110
- const seen = new Set(cbState.recentEventIds ?? []);
111
- const fresh = fetched.events.filter((e) => !seen.has(e.eventId));
112
- const newRing = trimRecent([...(cbState.recentEventIds ?? []), ...fresh.map((e) => e.eventId)]);
113
- const patch = {
114
- commandBus: {
115
- cursor: fetched.nextCursor || cbState.cursor,
116
- lastPollTs: fetched.serverTs,
117
- consecutiveFailures: 0,
118
- recentEventIds: newRing,
119
- },
120
- };
121
- if (fresh.length > 0) {
122
- logging.info(repoRoot, 'command_bus_events_received', {
123
- hookEvent,
124
- count: fresh.length,
125
- types: fresh.map((e) => e.eventType),
126
- });
127
- }
128
- return { events: fresh, statePatch: patch, reactionMode };
129
- }
130
- catch (e) {
131
- const patch = {
132
- commandBus: {
133
- ...cbState,
134
- consecutiveFailures: (cbState.consecutiveFailures ?? 0) + 1,
135
- },
136
- };
137
- logging.info(repoRoot, 'command_bus_poll_failed', {
138
- hookEvent,
139
- error: e instanceof Error ? e.message : String(e),
140
- consecutiveFailures: patch.commandBus.consecutiveFailures,
141
- });
142
- return { events: [], statePatch: patch, reactionMode };
143
- }
144
- }
145
- /**
146
- * Render zero or more events as a chat-ready Markdown block. Returns the empty string
147
- * when there is nothing to surface — callers can guard with the return value's length.
148
- */
149
- function renderForAgent(events, reactionMode) {
150
- if (events.length === 0)
151
- return '';
152
- const lines = [];
153
- lines.push(`⚠️ Dezycro Command Bus: ${events.length} new event(s) on this feature.`);
154
- lines.push('');
155
- for (const e of events) {
156
- lines.push(renderEvent(e));
157
- }
158
- if (events.some((e) => e.urgency === 'BLOCKING')) {
159
- lines.push('');
160
- lines.push(reactionGuidance(reactionMode));
161
- }
162
- return lines.join('\n');
163
- }
164
- function renderEvent(e) {
165
- if (e.eventType === 'INVALIDATION') {
166
- const summary = stringField(e.payload, 'artifactSummary') ?? '(no summary)';
167
- const reason = stringField(e.payload, 'reason') ?? '(no reason)';
168
- const prev = stringField(e.payload, 'previousStatus') ?? '?';
169
- const sourceLabel = `${e.sourceType.toLowerCase()} ${e.sourceId ?? '?'}`;
170
- return `- INVALIDATION (${e.urgency}): ${sourceLabel} ("${truncate(summary, 120)}") — reason: "${truncate(reason, 200)}" — was ${prev}.`;
171
- }
172
- return `- ${e.eventType} (${e.urgency}) source=${e.sourceType}/${e.sourceId ?? '-'}.`;
173
- }
174
- function reactionGuidance(mode) {
175
- switch (mode) {
176
- case 'HALT_AND_REPLAN':
177
- 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.';
178
- case 'ACKNOWLEDGE_AND_CONTINUE':
179
- return 'Reaction mode: ACKNOWLEDGE_AND_CONTINUE — surface this in your reply but continue current work; do not silently reuse invalidated artifacts.';
180
- case 'SILENT_LOG':
181
- return 'Reaction mode: SILENT_LOG — recorded for the session transcript; no chat interruption.';
182
- default:
183
- return '';
184
- }
185
- }
186
- /* ─── Helpers ─────────────────────────────────────────────────────────── */
187
- function resolveConfig(c) {
188
- const enabled = c?.enabled !== false; // default true
189
- const hooks = new Set(c?.pollHooks ?? DEFAULT_HOOKS);
190
- hooks.add('sessionStart'); // bootstrap is non-removable
191
- const minPollIntervalSeconds = Math.max(0, c?.minPollIntervalSeconds ?? DEFAULT_MIN_POLL_INTERVAL_SECONDS);
192
- const reactionMode = c?.reactionMode ?? DEFAULT_REACTION_MODE;
193
- return { enabled, hooks, minPollIntervalSeconds, reactionMode };
194
- }
195
- function readActiveFeature(repoRoot) {
196
- try {
197
- const p = path.join(repoRoot, '.dezycro', 'active-feature.json');
198
- if (!fs.existsSync(p))
199
- return null;
200
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
201
- const featureId = typeof raw.featureId === 'string' ? raw.featureId : null;
202
- const workspaceId = typeof raw.workspaceId === 'string' ? raw.workspaceId : null;
203
- if (!featureId)
204
- return null;
205
- if (workspaceId)
206
- return { workspaceId, featureId };
207
- // Older active-feature.json shapes only carry featureId; fall back to config.
208
- const wsFromConfig = readWorkspaceId(repoRoot);
209
- if (!wsFromConfig)
210
- return null;
211
- return { workspaceId: wsFromConfig, featureId };
212
- }
213
- catch {
214
- return null;
215
- }
216
- }
217
- function readApiUrl(repoRoot) {
218
- const cfg = readDezycroConfig(repoRoot);
219
- if (!cfg)
220
- return null;
221
- const environments = Array.isArray(cfg.environments) ? cfg.environments : [];
222
- const defaultEnv = environments.find((e) => e && e.default === true) ?? environments[0];
223
- if (!defaultEnv)
224
- return null;
225
- const apiUrl = defaultEnv.apiUrl;
226
- return typeof apiUrl === 'string' && apiUrl.length > 0 ? apiUrl.replace(/\/$/, '') : null;
227
- }
228
- function readWorkspaceId(repoRoot) {
229
- const cfg = readDezycroConfig(repoRoot);
230
- if (!cfg)
231
- return null;
232
- const wsId = cfg.workspaceId;
233
- return typeof wsId === 'string' ? wsId : null;
234
- }
235
- function readDezycroConfig(repoRoot) {
236
- try {
237
- const p = path.join(repoRoot, '.dezycro', 'config.json');
238
- if (!fs.existsSync(p))
239
- return null;
240
- return JSON.parse(fs.readFileSync(p, 'utf8'));
241
- }
242
- catch {
243
- return null;
244
- }
245
- }
246
- function readToken(repoRoot) {
247
- // Project-local override first, then the user-global token written by
248
- // `dezycro-setup` (`~/.dezycro/token.json`). Typical install needs no
249
- // per-repo file at all.
250
- const candidates = [
251
- path.join(repoRoot, '.dezycro', 'companion-token.local.json'),
252
- path.join(os.homedir(), '.dezycro', 'token.json'),
253
- ];
254
- for (const p of candidates) {
255
- try {
256
- if (!fs.existsSync(p))
257
- continue;
258
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
259
- const token = raw.token;
260
- if (typeof token === 'string' && token.length > 0)
261
- return token;
262
- }
263
- catch {
264
- // Try the next candidate.
265
- }
266
- }
267
- return null;
268
- }
269
- async function fetchInbox(apiUrl, feature, token, since) {
270
- const url = new URL(`${apiUrl}/api/v1/workspaces/${feature.workspaceId}/features/${feature.featureId}/inbox`);
271
- if (since)
272
- url.searchParams.set('since', since);
273
- url.searchParams.set('limit', String(POLL_LIMIT));
274
- const controller = new AbortController();
275
- const timeoutId = setTimeout(() => controller.abort(), 5000);
276
- try {
277
- const res = await fetch(url.toString(), {
278
- method: 'GET',
279
- headers: {
280
- Authorization: `Bearer ${token}`,
281
- Accept: 'application/json',
282
- },
283
- signal: controller.signal,
284
- });
285
- if (!res.ok) {
286
- throw new Error(`inbox poll returned ${res.status}`);
287
- }
288
- const body = (await res.json());
289
- if (!body || typeof body.serverTs !== 'string' || typeof body.nextCursor !== 'string') {
290
- throw new Error('inbox poll response missing serverTs/nextCursor');
291
- }
292
- return {
293
- serverTs: body.serverTs,
294
- nextCursor: body.nextCursor,
295
- events: Array.isArray(body.events) ? body.events : [],
296
- };
297
- }
298
- finally {
299
- clearTimeout(timeoutId);
300
- }
301
- }
302
- function trimRecent(ids) {
303
- return ids.length <= RECENT_EVENT_IDS_CAP ? ids : ids.slice(ids.length - RECENT_EVENT_IDS_CAP);
304
- }
305
- function stringField(payload, key) {
306
- const v = payload?.[key];
307
- return typeof v === 'string' ? v : null;
308
- }
309
- function truncate(s, max) {
310
- return s.length <= max ? s : s.slice(0, max - 1) + '…';
311
- }
312
- /* Exported for tests. */
313
- exports._internals = {
314
- resolveConfig,
315
- renderEvent,
316
- reactionGuidance,
317
- trimRecent,
318
- };
@@ -1,28 +0,0 @@
1
- /**
2
- * config.ts — reads `.dezycro/config.json` and resolves Companion settings.
3
- *
4
- * Owns the named-preset constant table (TRD §7.2) for the workbook sweep
5
- * confidence-threshold logic.
6
- */
7
- import type { CompanionConfig, PresetName, Thresholds } from '../types';
8
- export declare const PRESETS: Readonly<Record<PresetName, Thresholds>>;
9
- export declare const DEFAULT_CONFIG: Readonly<CompanionConfig>;
10
- export declare function deepMerge<T>(base: T, override: unknown): T;
11
- /**
12
- * Load `.dezycro/config.json` from the given repo root, deep-merged with
13
- * DEFAULT_CONFIG. Returns DEFAULT_CONFIG if the file is missing or unparseable.
14
- */
15
- export declare function load(repoRoot: string): CompanionConfig;
16
- /**
17
- * Resolve per-type confidence thresholds for the workbook sweep, honoring
18
- * `companion.autoWorkbookUpdates.preset` and any numeric overrides under
19
- * `companion.autoWorkbookUpdates.thresholds.*`.
20
- */
21
- export declare function resolveThresholds(config: CompanionConfig): Thresholds;
22
- /**
23
- * Find the nearest ancestor of `startDir` that contains a `.dezycro/`
24
- * directory, returning that ancestor as the repo root. Returns null if not
25
- * found before reaching the filesystem root.
26
- */
27
- export declare function findRepoRoot(startDir: string | null | undefined): string | null;
28
- export declare const _deepMerge: typeof deepMerge;