@dezycro-ai/agent-plugin 2.0.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 (55) hide show
  1. package/.claude-plugin/plugin.json +7 -0
  2. package/LICENSE +18 -0
  3. package/PERSONAS.md +309 -0
  4. package/README.md +125 -0
  5. package/bin/resolve-auth.js +112 -0
  6. package/dist/hooks/lib/authoring.d.ts +118 -0
  7. package/dist/hooks/lib/authoring.js +277 -0
  8. package/dist/hooks/lib/config.d.ts +28 -0
  9. package/dist/hooks/lib/config.js +175 -0
  10. package/dist/hooks/lib/controller-match.d.ts +16 -0
  11. package/dist/hooks/lib/controller-match.js +96 -0
  12. package/dist/hooks/lib/coverage.d.ts +17 -0
  13. package/dist/hooks/lib/coverage.js +66 -0
  14. package/dist/hooks/lib/hashing.d.ts +8 -0
  15. package/dist/hooks/lib/hashing.js +49 -0
  16. package/dist/hooks/lib/logging.d.ts +13 -0
  17. package/dist/hooks/lib/logging.js +72 -0
  18. package/dist/hooks/lib/publish-spec.d.ts +17 -0
  19. package/dist/hooks/lib/publish-spec.js +64 -0
  20. package/dist/hooks/lib/quality-gate.d.ts +67 -0
  21. package/dist/hooks/lib/quality-gate.js +187 -0
  22. package/dist/hooks/lib/router.d.ts +32 -0
  23. package/dist/hooks/lib/router.js +717 -0
  24. package/dist/hooks/lib/state.d.ts +34 -0
  25. package/dist/hooks/lib/state.js +238 -0
  26. package/dist/hooks/lib/undo.d.ts +11 -0
  27. package/dist/hooks/lib/undo.js +71 -0
  28. package/dist/hooks/lib/verify.d.ts +28 -0
  29. package/dist/hooks/lib/verify.js +94 -0
  30. package/dist/hooks/lib/workbook.d.ts +21 -0
  31. package/dist/hooks/lib/workbook.js +77 -0
  32. package/dist/hooks/types.d.ts +249 -0
  33. package/dist/hooks/types.js +14 -0
  34. package/hooks/companion-runner +108 -0
  35. package/hooks/hooks.json +74 -0
  36. package/install.js +84 -0
  37. package/package.json +50 -0
  38. package/prompts/existing-work-ranker.md +47 -0
  39. package/prompts/prd-drafter.md +53 -0
  40. package/prompts/prd-question.md +66 -0
  41. package/prompts/trd-context-gate.md +57 -0
  42. package/prompts/trd-discovery.md +71 -0
  43. package/prompts/trd-drafter.md +63 -0
  44. package/prompts/trd-question.md +61 -0
  45. package/prompts/trd-reviewer.md +74 -0
  46. package/prompts/workbook-sweep.md +89 -0
  47. package/setup.js +35 -0
  48. package/skills/author/SKILL.md +285 -0
  49. package/skills/import-features/SKILL.md +428 -0
  50. package/skills/init/SKILL.md +133 -0
  51. package/skills/publish-spec/SKILL.md +101 -0
  52. package/skills/status/SKILL.md +76 -0
  53. package/skills/sync/SKILL.md +76 -0
  54. package/skills/verify/SKILL.md +248 -0
  55. package/skills/work/SKILL.md +201 -0
@@ -0,0 +1,717 @@
1
+ /**
2
+ * router.ts — dispatches hook invocations to the right handler.
3
+ *
4
+ * The companion-runner shim parses argv and stdin payloads into a
5
+ * RouterInvocation and calls dispatch(). Per-handler boundaries try/catch
6
+ * via the runner entry point: any exception inside this module is logged
7
+ * by the runner and converts to silent exit so a hook never breaks a
8
+ * user session.
9
+ */
10
+ 'use strict';
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.TRIVIAL_TURN_CHAR_DEFAULT_EXPORT = exports._transcriptCharsSinceLastStop = exports._readActiveFeatureId = exports._handleSessionStart = exports._handleWorkbookMutation = exports._handleUserPromptSubmit = exports._handleStop = exports._handleUpdateTaskGate = exports._handlePostEdit = exports._extractEditedPaths = exports.QUALITY_GATE_REASON_PREFIX = void 0;
46
+ exports.dispatch = dispatch;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const config = __importStar(require("./config"));
50
+ const state = __importStar(require("./state"));
51
+ const logging = __importStar(require("./logging"));
52
+ const verify = __importStar(require("./verify"));
53
+ const controllerMatch = __importStar(require("./controller-match"));
54
+ const authoring = __importStar(require("./authoring"));
55
+ const qualityGate = __importStar(require("./quality-gate"));
56
+ const hashing = __importStar(require("./hashing"));
57
+ const workbook = __importStar(require("./workbook"));
58
+ const undo = __importStar(require("./undo"));
59
+ const coverage = __importStar(require("./coverage"));
60
+ const DONE_STATUSES = new Set(['DONE', 'PUSHED']);
61
+ const WRAP_PHRASES = [
62
+ '/dezycro:sync',
63
+ 'log what we did',
64
+ 'log this',
65
+ 'save progress',
66
+ 'save what we did',
67
+ 'wrap up',
68
+ "we're done",
69
+ 'we are done',
70
+ 'end of session',
71
+ 'sweep the workbook',
72
+ ];
73
+ function detectWrapPhrase(userMessage) {
74
+ if (typeof userMessage !== 'string')
75
+ return null;
76
+ const m = userMessage.toLowerCase().trim();
77
+ if (!m)
78
+ return null;
79
+ for (const p of WRAP_PHRASES) {
80
+ if (m.includes(p))
81
+ return p;
82
+ }
83
+ return null;
84
+ }
85
+ exports.QUALITY_GATE_REASON_PREFIX = 'TRD quality gate (deterministic layer) blocked APPROVED transition: ';
86
+ async function dispatch(invocation) {
87
+ const { event, route, payload, flags } = invocation;
88
+ if (event === 'state') {
89
+ return handleStateSubcommand(flags || {});
90
+ }
91
+ if (event === 'authoring-state') {
92
+ return handleAuthoringState(flags || {});
93
+ }
94
+ if (event === 'pre-tool-use' && route === 'update_task') {
95
+ return handleUpdateTaskGate(payload || {});
96
+ }
97
+ if (event === 'pre-tool-use' && route === 'change_document_status') {
98
+ return handleChangeDocumentStatus(payload || {});
99
+ }
100
+ if (event === 'post-tool-use' && route === 'edit') {
101
+ return handlePostEdit(payload || {});
102
+ }
103
+ if (event === 'post-tool-use' && route === 'workbook_mutation') {
104
+ return handleWorkbookMutation(payload || {});
105
+ }
106
+ if (event === 'session-start') {
107
+ return handleSessionStart(payload || {});
108
+ }
109
+ if (event === 'user-prompt-submit') {
110
+ return handleUserPromptSubmit(payload || {});
111
+ }
112
+ if (event === 'stop') {
113
+ return handleStop(payload || {});
114
+ }
115
+ return 0;
116
+ }
117
+ /* ─── Pillar 1 — pre-DONE verify gate (TRD §5.1) ─────────────────────────── */
118
+ function handleUpdateTaskGate(payload) {
119
+ const repoRoot = config.findRepoRoot(process.cwd());
120
+ if (!repoRoot) {
121
+ process.stdout.write('{}');
122
+ return 0;
123
+ }
124
+ const toolInput = (payload && payload.tool_input) || {};
125
+ const status = String(toolInput.status || '').toUpperCase();
126
+ if (!DONE_STATUSES.has(status)) {
127
+ process.stdout.write('{}');
128
+ return 0;
129
+ }
130
+ const pathRefsRaw = toolInput.pathRefs;
131
+ const pathRefs = Array.isArray(pathRefsRaw) ? pathRefsRaw.filter(Boolean) : [];
132
+ if (pathRefs.length === 0) {
133
+ process.stdout.write('{}');
134
+ return 0;
135
+ }
136
+ const cfg = safeLoadConfig(repoRoot);
137
+ const av = (cfg && cfg.companion && cfg.companion.autoVerify) || {};
138
+ if (av.preTaskDone === false) {
139
+ process.stdout.write('{}');
140
+ return 0;
141
+ }
142
+ const passed = verify.hasRecentPassingVerify({ repoRoot, paths: pathRefs });
143
+ if (passed) {
144
+ logging.info(repoRoot, 'pre_done_gate_pass', { status, pathRefs });
145
+ // Pillar 4 pre-PUSH coverage nudge.
146
+ if (status === 'PUSHED') {
147
+ const nudge = buildPrePushCoverageNudge(repoRoot, cfg);
148
+ if (nudge) {
149
+ logging.info(repoRoot, 'pre_push_coverage_nudge', { line: nudge });
150
+ const out = { systemMessage: nudge };
151
+ process.stdout.write(JSON.stringify(out));
152
+ return 0;
153
+ }
154
+ }
155
+ process.stdout.write('{}');
156
+ return 0;
157
+ }
158
+ state.incrementMetric(repoRoot, 'verifyAutoRuns');
159
+ state.incrementMetric(repoRoot, 'verifyAutoFailures');
160
+ logging.warn(repoRoot, 'pre_done_gate_block', { status, pathRefs });
161
+ const reason = verify.buildBlockReason({ paths: pathRefs, status }).reason;
162
+ const out = {
163
+ decision: 'block',
164
+ reason,
165
+ hookSpecificOutput: {
166
+ hookEventName: 'PreToolUse',
167
+ permissionDecision: 'deny',
168
+ permissionDecisionReason: reason,
169
+ },
170
+ };
171
+ process.stdout.write(JSON.stringify(out));
172
+ return 0;
173
+ }
174
+ function buildPrePushCoverageNudge(repoRoot, cfg) {
175
+ const cv = (cfg && cfg.companion && cfg.companion.coverageNudges) || {};
176
+ if (cv.enabled === false)
177
+ return null;
178
+ const snap = state.read(repoRoot).coverageSnapshot;
179
+ const line = coverage.formatOneLiner(snap);
180
+ if (!line)
181
+ return null;
182
+ return `[Companion] ${line} — verify passed, but consider checking these before push.`;
183
+ }
184
+ /* ─── Pillar 5 — TRD quality gate on change_document_status (TRD §5.5.3) ── */
185
+ function handleChangeDocumentStatus(payload) {
186
+ const toolInput = (payload && payload.tool_input) || {};
187
+ const ti = toolInput;
188
+ const status = ti.status || ti.newStatus || null;
189
+ const docId = ti.docId || ti.documentId || ti.id || null;
190
+ if (status !== 'APPROVED') {
191
+ process.stdout.write('{}');
192
+ return 0;
193
+ }
194
+ const repoRoot = config.findRepoRoot(process.cwd());
195
+ if (!repoRoot) {
196
+ process.stdout.write('{}');
197
+ return 0;
198
+ }
199
+ const sidecar = authoring.readDraftSidecar(repoRoot);
200
+ if (!sidecar || (docId && sidecar.docId && sidecar.docId !== docId)) {
201
+ logging.warn(repoRoot, 'quality_gate_degraded_permit', {
202
+ reason: sidecar ? 'docId_mismatch' : 'sidecar_missing',
203
+ docId,
204
+ sidecarDocId: sidecar ? sidecar.docId : null,
205
+ });
206
+ process.stdout.write('{}');
207
+ return 0;
208
+ }
209
+ if (sidecar.docType !== 'TRD') {
210
+ process.stdout.write('{}');
211
+ return 0;
212
+ }
213
+ const result = qualityGate.deterministicTrdGate({
214
+ trdContent: sidecar.content,
215
+ prdContent: sidecar.prdContent || '',
216
+ });
217
+ if (result.passed) {
218
+ logging.info(repoRoot, 'quality_gate_deterministic_pass', { docId: sidecar.docId });
219
+ process.stdout.write('{}');
220
+ return 0;
221
+ }
222
+ state.incrementMetric(repoRoot, 'qualityGateFailures');
223
+ const summary = result.blockingIssues
224
+ .map((b) => `[${b.section}] ${b.issue}`)
225
+ .slice(0, 5)
226
+ .join(' | ');
227
+ const reason = `${exports.QUALITY_GATE_REASON_PREFIX}${summary || 'unspecified failure'}. Fix and try again.`;
228
+ logging.warn(repoRoot, 'quality_gate_blocked', { docId: sidecar.docId, issues: result.blockingIssues });
229
+ const out = {
230
+ decision: 'block',
231
+ reason,
232
+ hookSpecificOutput: {
233
+ hookEventName: 'PreToolUse',
234
+ permissionDecision: 'deny',
235
+ permissionDecisionReason: reason,
236
+ },
237
+ };
238
+ process.stdout.write(JSON.stringify(out));
239
+ return 0;
240
+ }
241
+ /* ─── Pillars 1+2 — post-edit pulse + controller-match (TRD §5.1+§5.2) ─── */
242
+ function handlePostEdit(payload) {
243
+ const repoRoot = config.findRepoRoot(process.cwd());
244
+ if (!repoRoot)
245
+ return 0;
246
+ const toolInput = (payload && payload.tool_input) || {};
247
+ const editedPaths = extractEditedPaths(toolInput);
248
+ if (editedPaths.length === 0)
249
+ return 0;
250
+ const cfg = safeLoadConfig(repoRoot);
251
+ const av = (cfg && cfg.companion && cfg.companion.autoVerify) || {};
252
+ const globs = Array.isArray(av.controllerPaths) ? av.controllerPaths : [];
253
+ const pulseEnabled = av.postEditPulse && av.postEditPulse.enabled !== false;
254
+ const pulseThreshold = (av.postEditPulse && Number(av.postEditPulse.threshold)) || 5;
255
+ const matchedControllers = controllerMatch.selectControllers(editedPaths, globs);
256
+ const editedTs = new Date().toISOString();
257
+ const current = state.read(repoRoot);
258
+ const nextPulseCount = (current.pulseCount || 0) + 1;
259
+ const shouldNudge = pulseEnabled &&
260
+ nextPulseCount >= pulseThreshold &&
261
+ matchedControllers.length > 0;
262
+ if (matchedControllers.length === 0) {
263
+ state.patch(repoRoot, {
264
+ pulseCount: nextPulseCount,
265
+ lastEditTs: editedTs,
266
+ });
267
+ }
268
+ else {
269
+ for (let i = 0; i < matchedControllers.length; i += 1) {
270
+ const isLast = i === matchedControllers.length - 1;
271
+ const editPatch = {
272
+ pushControllerEdit: { path: matchedControllers[i], editedTs },
273
+ };
274
+ if (isLast) {
275
+ editPatch.needsSpecPublish = true;
276
+ editPatch.lastEditTs = editedTs;
277
+ editPatch.pulseCount = shouldNudge ? 0 : nextPulseCount;
278
+ }
279
+ state.patch(repoRoot, editPatch);
280
+ }
281
+ }
282
+ logging.info(repoRoot, 'post_edit', {
283
+ edited: editedPaths.length,
284
+ matchedControllers: matchedControllers.length,
285
+ pulseCount: shouldNudge ? 0 : nextPulseCount,
286
+ nudged: shouldNudge,
287
+ });
288
+ if (shouldNudge) {
289
+ const nudge = '[Companion] ' + nextPulseCount + ' edits since last verify and controller paths were touched ' +
290
+ '(' + matchedControllers.join(', ') + '). Consider running `/dezycro:verify` to catch regressions early.';
291
+ process.stdout.write(JSON.stringify({
292
+ hookSpecificOutput: {
293
+ hookEventName: 'PostToolUse',
294
+ additionalContext: nudge,
295
+ },
296
+ }));
297
+ }
298
+ return 0;
299
+ }
300
+ function extractEditedPaths(toolInput) {
301
+ if (!toolInput || typeof toolInput !== 'object')
302
+ return [];
303
+ const fp = toolInput.file_path;
304
+ if (typeof fp === 'string')
305
+ return [fp];
306
+ const p = toolInput.path;
307
+ if (typeof p === 'string')
308
+ return [p];
309
+ const edits = toolInput.edits;
310
+ if (Array.isArray(edits)) {
311
+ const seen = new Set();
312
+ for (const e of edits) {
313
+ if (e && typeof e.file_path === 'string') {
314
+ seen.add(e.file_path);
315
+ }
316
+ }
317
+ return Array.from(seen);
318
+ }
319
+ return [];
320
+ }
321
+ /* ─── Skill-facing subcommands (state / authoring-state) ─────────────────── */
322
+ function handleStateSubcommand(flags) {
323
+ const repoRoot = config.findRepoRoot(process.cwd()) || process.cwd();
324
+ if (flags.get) {
325
+ const snap = state.read(repoRoot);
326
+ process.stdout.write(JSON.stringify(snap, null, 2));
327
+ return 0;
328
+ }
329
+ if (typeof flags.patch === 'string') {
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(flags.patch);
333
+ }
334
+ catch (err) {
335
+ const msg = err.message;
336
+ process.stdout.write(JSON.stringify({ ok: false, error: 'invalid JSON for --patch: ' + msg }));
337
+ return 1;
338
+ }
339
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
340
+ process.stdout.write(JSON.stringify({ ok: false, error: '--patch must be a JSON object' }));
341
+ return 1;
342
+ }
343
+ const next = state.patch(repoRoot, parsed);
344
+ if (next === null) {
345
+ process.stdout.write(JSON.stringify({ ok: false, error: 'lock contention — state not patched' }));
346
+ return 1;
347
+ }
348
+ process.stdout.write(JSON.stringify({ ok: true }));
349
+ return 0;
350
+ }
351
+ process.stdout.write(JSON.stringify({
352
+ ok: false,
353
+ error: 'state requires one of --get or --patch=<json>',
354
+ }));
355
+ return 1;
356
+ }
357
+ function handleAuthoringState(flags) {
358
+ const repoRoot = config.findRepoRoot(process.cwd()) || process.cwd();
359
+ const action = flags.set || (flags.get ? 'get' : null);
360
+ if (action === 'enter') {
361
+ const docId = flags['doc-id'];
362
+ const docType = flags['doc-type'];
363
+ if (!docId || !docType) {
364
+ process.stdout.write(JSON.stringify({
365
+ ok: false,
366
+ error: '--doc-id and --doc-type are required for --set=enter',
367
+ }));
368
+ return 1;
369
+ }
370
+ try {
371
+ const transcript = authoring.enter(repoRoot, {
372
+ docId,
373
+ docType: docType,
374
+ featureId: flags['feature-id'] || null,
375
+ seed: flags.seed || null,
376
+ });
377
+ process.stdout.write(JSON.stringify({ ok: true, transcript }));
378
+ return 0;
379
+ }
380
+ catch (err) {
381
+ process.stdout.write(JSON.stringify({ ok: false, error: err.message }));
382
+ return 1;
383
+ }
384
+ }
385
+ if (action === 'exit') {
386
+ try {
387
+ authoring.exit(repoRoot);
388
+ process.stdout.write(JSON.stringify({ ok: true }));
389
+ return 0;
390
+ }
391
+ catch (err) {
392
+ process.stdout.write(JSON.stringify({ ok: false, error: err.message }));
393
+ return 1;
394
+ }
395
+ }
396
+ if (action === 'get') {
397
+ const snap = authoring.getState(repoRoot);
398
+ process.stdout.write(JSON.stringify({ ok: true, ...snap }));
399
+ return 0;
400
+ }
401
+ process.stdout.write(JSON.stringify({
402
+ ok: false,
403
+ error: 'authoring-state requires one of --set=enter, --set=exit, --get',
404
+ }));
405
+ return 1;
406
+ }
407
+ /* ─── Pillar 3 — Proactive Workbook Updates (TRD §5.3) ────────────────────── */
408
+ const TRIVIAL_TURN_CHAR_DEFAULT = 600;
409
+ function handleSessionStart(_payload) {
410
+ const repoRoot = config.findRepoRoot(process.cwd());
411
+ if (!repoRoot)
412
+ return 0;
413
+ state.patch(repoRoot, {
414
+ sessionAutoLogCount: 0,
415
+ turnAutoLogCount: 0,
416
+ currentTurnId: 0,
417
+ lastAutoWorkbookMutation: null,
418
+ coverageFetchPending: false,
419
+ });
420
+ logging.info(repoRoot, 'session_start', {});
421
+ return 0;
422
+ }
423
+ function handleUserPromptSubmit(payload) {
424
+ const repoRoot = config.findRepoRoot(process.cwd());
425
+ if (!repoRoot)
426
+ return 0;
427
+ const current = state.read(repoRoot);
428
+ const nextTurnId = (current.currentTurnId || 0) + 1;
429
+ state.patch(repoRoot, {
430
+ currentTurnId: nextTurnId,
431
+ turnAutoLogCount: 0,
432
+ });
433
+ const userMessage = String(payload?.prompt ?? payload?.user_message ?? payload?.message ?? '').trim();
434
+ const wrap = detectWrapPhrase(userMessage);
435
+ if (wrap) {
436
+ state.patch(repoRoot, { pendingWorkbookSweep: true, pendingWorkbookSweepReason: 'wrap_phrase:' + wrap });
437
+ logging.info(repoRoot, 'wrap_phrase_detected', { phrase: wrap });
438
+ }
439
+ if (userMessage && undo.isUndoMessage(userMessage)) {
440
+ const mut = current.lastAutoWorkbookMutation;
441
+ if (mut && undo.isInUndoWindow(mut, nextTurnId)) {
442
+ const directive = undo.buildReversalDirective(mut);
443
+ if (directive) {
444
+ state.incrementMetric(repoRoot, 'undoCount');
445
+ state.patch(repoRoot, { lastAutoWorkbookMutation: null });
446
+ logging.info(repoRoot, 'undo_directive', { tool: directive.tool, entityId: mut.entityId });
447
+ const reason = '[Dezycro Companion — undo] ' + directive.humanLine + '\n\n' +
448
+ 'Call ' + directive.tool + ' with arguments:\n' +
449
+ JSON.stringify(directive.args, null, 2) + '\n\n' +
450
+ 'Then confirm the reversal in one short sentence.';
451
+ const out = {
452
+ hookSpecificOutput: {
453
+ hookEventName: 'UserPromptSubmit',
454
+ additionalContext: reason,
455
+ },
456
+ };
457
+ process.stdout.write(JSON.stringify(out));
458
+ return 0;
459
+ }
460
+ }
461
+ }
462
+ maybeSurfaceCoverage(repoRoot, current);
463
+ process.stdout.write('{}');
464
+ return 0;
465
+ }
466
+ function maybeSurfaceCoverage(repoRoot, current) {
467
+ const cfg = safeLoadConfig(repoRoot);
468
+ if (cfg?.companion?.coverageNudges?.enabled === false)
469
+ return;
470
+ const snap = current.coverageSnapshot;
471
+ if (!snap)
472
+ return;
473
+ if (snap.surfacedTs)
474
+ return;
475
+ const line = coverage.formatOneLiner(snap);
476
+ if (!line)
477
+ return;
478
+ state.patch(repoRoot, {
479
+ coverageSnapshot: { ...snap, surfacedTs: new Date().toISOString() },
480
+ });
481
+ logging.info(repoRoot, 'coverage_one_liner', { line });
482
+ const out = {
483
+ hookSpecificOutput: {
484
+ hookEventName: 'UserPromptSubmit',
485
+ additionalContext: '[Companion] ' + line,
486
+ },
487
+ };
488
+ process.stdout.write(JSON.stringify(out));
489
+ }
490
+ function handleStop(payload) {
491
+ const repoRoot = config.findRepoRoot(process.cwd());
492
+ if (!repoRoot)
493
+ return 0;
494
+ const cfg = safeLoadConfig(repoRoot);
495
+ const ws = cfg?.companion?.autoWorkbookUpdates || {};
496
+ if (cfg?.companion?.safeMode === true)
497
+ return silentExit();
498
+ if (ws.enabled === false)
499
+ return silentExit();
500
+ const cur = state.read(repoRoot);
501
+ const verifyAdvanced = Boolean(cur.lastVerifyCompletionTs &&
502
+ cur.lastVerifyCompletionTs !== cur.lastSeenVerifyCompletionTs);
503
+ const hasTrigger = Boolean(cur.pendingWorkbookSweep || verifyAdvanced);
504
+ if (!hasTrigger) {
505
+ logging.info(repoRoot, 'stop_skip', { reason: 'no_trigger' });
506
+ if (verifyAdvanced) {
507
+ state.patch(repoRoot, { lastSeenVerifyCompletionTs: cur.lastVerifyCompletionTs });
508
+ }
509
+ return maybeStopCoverageDelta(repoRoot, cur);
510
+ }
511
+ if (cur.authoringMode) {
512
+ logging.info(repoRoot, 'stop_skip', { reason: 'authoring_mode' });
513
+ state.patch(repoRoot, {
514
+ pendingWorkbookSweep: false,
515
+ pendingWorkbookSweepReason: null,
516
+ lastSeenVerifyCompletionTs: cur.lastVerifyCompletionTs || cur.lastSeenVerifyCompletionTs,
517
+ });
518
+ return maybeStopCoverageDelta(repoRoot, cur);
519
+ }
520
+ const maxTurn = Number(ws.maxAutoLogsPerTurn ?? 2);
521
+ const maxSession = Number(ws.maxAutoLogsPerSession ?? 10);
522
+ const perTurnSlotsLeft = Math.max(0, maxTurn - (cur.turnAutoLogCount || 0));
523
+ const perSessionSlotsLeft = Math.max(0, maxSession - (cur.sessionAutoLogCount || 0));
524
+ if (perTurnSlotsLeft === 0 || perSessionSlotsLeft === 0) {
525
+ logging.info(repoRoot, 'stop_skip', {
526
+ reason: 'caps_exhausted',
527
+ perTurnSlotsLeft, perSessionSlotsLeft,
528
+ });
529
+ state.patch(repoRoot, {
530
+ pendingWorkbookSweep: false,
531
+ pendingWorkbookSweepReason: null,
532
+ lastSeenVerifyCompletionTs: cur.lastVerifyCompletionTs || cur.lastSeenVerifyCompletionTs,
533
+ });
534
+ return maybeStopCoverageDelta(repoRoot, cur);
535
+ }
536
+ if (cur.suppressWorkbookUpdatesUntil && Date.now() < new Date(cur.suppressWorkbookUpdatesUntil).getTime()) {
537
+ logging.info(repoRoot, 'stop_skip', { reason: 'suppressed' });
538
+ state.patch(repoRoot, {
539
+ pendingWorkbookSweep: false,
540
+ pendingWorkbookSweepReason: null,
541
+ lastSeenVerifyCompletionTs: cur.lastVerifyCompletionTs || cur.lastSeenVerifyCompletionTs,
542
+ });
543
+ return maybeStopCoverageDelta(repoRoot, cur);
544
+ }
545
+ state.patch(repoRoot, {
546
+ pendingWorkbookSweep: false,
547
+ pendingWorkbookSweepReason: null,
548
+ lastSeenVerifyCompletionTs: cur.lastVerifyCompletionTs || cur.lastSeenVerifyCompletionTs,
549
+ });
550
+ const thresholds = config.resolveThresholds(cfg);
551
+ const recentHashes = [
552
+ ...(cur.recentDecisionHashes || []),
553
+ ...(cur.recentIssueHashes || []),
554
+ ...(cur.recentAssumptionHashes || []),
555
+ ];
556
+ const activeFeature = readActiveFeature(repoRoot);
557
+ let directive = workbook.buildSweepDirective({
558
+ thresholds,
559
+ perTurnSlotsLeft,
560
+ perSessionSlotsLeft,
561
+ recentHashes,
562
+ activeFeatureId: activeFeature.featureId,
563
+ activeFeatureName: activeFeature.featureName,
564
+ });
565
+ logging.info(repoRoot, 'stop_sweep_directive', {
566
+ perTurnSlotsLeft, perSessionSlotsLeft, recentHashCount: recentHashes.length,
567
+ });
568
+ const delta = computeCoverageDeltaForStop(repoRoot, cur);
569
+ if (delta)
570
+ directive += '\n\n' + delta;
571
+ const out = { decision: 'block', reason: directive };
572
+ process.stdout.write(JSON.stringify(out));
573
+ return 0;
574
+ }
575
+ function maybeStopCoverageDelta(repoRoot, cur) {
576
+ const delta = computeCoverageDeltaForStop(repoRoot, cur);
577
+ if (!delta)
578
+ return silentExit();
579
+ logging.info(repoRoot, 'stop_coverage_delta', { delta });
580
+ const out = { systemMessage: delta };
581
+ process.stdout.write(JSON.stringify(out));
582
+ return 0;
583
+ }
584
+ function computeCoverageDeltaForStop(repoRoot, cur) {
585
+ const lastVerify = cur.lastVerifyCompletionTs;
586
+ const snap = cur.coverageSnapshot;
587
+ if (!lastVerify || !snap)
588
+ return null;
589
+ const surfaced = snap.surfacedTs ? new Date(snap.surfacedTs).getTime() : 0;
590
+ const verifyTs = new Date(lastVerify).getTime();
591
+ if (verifyTs <= surfaced)
592
+ return null;
593
+ const line = coverage.formatDelta(snap.previous || null, snap);
594
+ if (!line)
595
+ return null;
596
+ state.patch(repoRoot, { coverageSnapshot: { ...snap, surfacedTs: new Date().toISOString() } });
597
+ return '[Companion] ' + line;
598
+ }
599
+ function silentExit() { process.stdout.write('{}'); return 0; }
600
+ function transcriptCharsSinceLastStop(payload) {
601
+ try {
602
+ const tp = payload?.transcript_path;
603
+ if (!tp || typeof tp !== 'string' || !fs.existsSync(tp))
604
+ return null;
605
+ const raw = fs.readFileSync(tp, 'utf8');
606
+ const lines = raw.split('\n').filter(Boolean);
607
+ let chars = 0;
608
+ let foundPrev = false;
609
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
610
+ let evt;
611
+ try {
612
+ evt = JSON.parse(lines[i]);
613
+ }
614
+ catch {
615
+ continue;
616
+ }
617
+ const role = evt?.message?.role ?? evt?.role;
618
+ if (role === 'assistant') {
619
+ const txt = JSON.stringify(evt.message?.content ?? evt.content ?? '');
620
+ chars += txt.length;
621
+ }
622
+ else if (role === 'user') {
623
+ if (foundPrev)
624
+ break;
625
+ foundPrev = true;
626
+ const txt = JSON.stringify(evt.message?.content ?? evt.content ?? '');
627
+ chars += txt.length;
628
+ }
629
+ if (chars > 1000000)
630
+ break;
631
+ }
632
+ return chars;
633
+ }
634
+ catch {
635
+ return null;
636
+ }
637
+ }
638
+ function readActiveFeatureId(repoRoot) {
639
+ return readActiveFeature(repoRoot).featureId;
640
+ }
641
+ function readActiveFeature(repoRoot) {
642
+ try {
643
+ const raw = fs.readFileSync(path.join(repoRoot, '.dezycro', 'active-feature.json'), 'utf8');
644
+ const p = JSON.parse(raw);
645
+ return { featureId: p?.featureId || null, featureName: p?.featureName || null };
646
+ }
647
+ catch {
648
+ return { featureId: null, featureName: null };
649
+ }
650
+ }
651
+ function safeLoadConfig(repoRoot) {
652
+ try {
653
+ return config.load(repoRoot);
654
+ }
655
+ catch {
656
+ return JSON.parse(JSON.stringify(config.DEFAULT_CONFIG));
657
+ }
658
+ }
659
+ function handleWorkbookMutation(payload) {
660
+ const repoRoot = config.findRepoRoot(process.cwd());
661
+ if (!repoRoot)
662
+ return 0;
663
+ const toolName = payload?.tool_name || '';
664
+ const kind = workbook.entityKindForTool(toolName);
665
+ if (!kind)
666
+ return 0;
667
+ const toolInput = (payload?.tool_input || {});
668
+ const toolResponse = (payload?.tool_response || payload?.response || {});
669
+ const summary = String(toolInput.summary || toolInput.title || toolInput.description || toolInput.statement || '');
670
+ const linkedTaskId = toolInput.taskId || toolInput.linkedTaskId || null;
671
+ const hash = hashing.hashCandidate(kind, summary, linkedTaskId);
672
+ const respData = toolResponse.data;
673
+ const entityId = toolResponse.id ||
674
+ (respData ? respData.id : undefined) ||
675
+ toolInput.taskId ||
676
+ null;
677
+ const cur = state.read(repoRoot);
678
+ const turnAutoLogCount = (cur.turnAutoLogCount || 0) + 1;
679
+ const sessionAutoLogCount = (cur.sessionAutoLogCount || 0) + 1;
680
+ const patch = {
681
+ turnAutoLogCount,
682
+ sessionAutoLogCount,
683
+ lastAutoWorkbookMutation: {
684
+ tool: toolName.split('__').pop() || '',
685
+ entityId,
686
+ createdInTurnId: cur.currentTurnId || 0,
687
+ createdTs: new Date().toISOString(),
688
+ priorStatus: toolInput.priorStatus || null,
689
+ },
690
+ };
691
+ if (kind === 'decision')
692
+ patch.pushDecisionHash = hash;
693
+ if (kind === 'issue')
694
+ patch.pushIssueHash = hash;
695
+ if (kind === 'assumption')
696
+ patch.pushAssumptionHash = hash;
697
+ state.patch(repoRoot, patch);
698
+ const metricKey = kind === 'decision' ? 'autoDecisionLogs' :
699
+ kind === 'issue' ? 'autoIssueLogs' :
700
+ kind === 'assumption' ? 'autoAssumptionLogs' :
701
+ 'autoTaskStatusLogs';
702
+ state.incrementMetric(repoRoot, metricKey);
703
+ logging.info(repoRoot, 'workbook_mutation', { kind, entityId, turnAutoLogCount, sessionAutoLogCount });
704
+ process.stdout.write('{}');
705
+ return 0;
706
+ }
707
+ // Exposed for tests:
708
+ exports._extractEditedPaths = extractEditedPaths;
709
+ exports._handlePostEdit = handlePostEdit;
710
+ exports._handleUpdateTaskGate = handleUpdateTaskGate;
711
+ exports._handleStop = handleStop;
712
+ exports._handleUserPromptSubmit = handleUserPromptSubmit;
713
+ exports._handleWorkbookMutation = handleWorkbookMutation;
714
+ exports._handleSessionStart = handleSessionStart;
715
+ exports._readActiveFeatureId = readActiveFeatureId;
716
+ exports._transcriptCharsSinceLastStop = transcriptCharsSinceLastStop;
717
+ exports.TRIVIAL_TURN_CHAR_DEFAULT_EXPORT = TRIVIAL_TURN_CHAR_DEFAULT;