dsh-graft-plugin 0.2.1 → 0.3.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/LICENSE +21 -21
- package/README.md +24 -1
- package/package.json +13 -8
- package/scripts/test-autosync.mjs +66 -1
- package/scripts/test-guidance.mjs +160 -0
- package/scripts/test-harness.mjs +184 -2
- package/src/auto-sync.js +34 -2
- package/src/client.js +424 -59
- package/src/graft-tools.js +11 -0
- package/src/index.js +190 -36
package/src/graft-tools.js
CHANGED
|
@@ -81,6 +81,14 @@ export function resolveGraftCommand(env = process.env, exists = existsSync, exec
|
|
|
81
81
|
const cli = join(dir, 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
|
|
82
82
|
if (exists(cli)) return { command: execPath, prefix: [cli], shell: false }
|
|
83
83
|
}
|
|
84
|
+
// A bun global install puts only the shim on PATH; the package lives under
|
|
85
|
+
// $BUN_INSTALL/install/global. Missing it left prefix empty, which auto-sync
|
|
86
|
+
// used to turn into a fatal dirname(undefined).
|
|
87
|
+
const bunRoot = env.BUN_INSTALL ?? join(env.HOME ?? env.USERPROFILE ?? '.', '.bun')
|
|
88
|
+
if (typeof bunRoot === 'string' && bunRoot !== '') {
|
|
89
|
+
const bunCli = join(bunRoot, 'install', 'global', 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
|
|
90
|
+
if (exists(bunCli)) return { command: execPath, prefix: [bunCli], shell: false }
|
|
91
|
+
}
|
|
84
92
|
return { command: 'graft', prefix: [], shell: process.platform === 'win32' }
|
|
85
93
|
}
|
|
86
94
|
|
|
@@ -189,6 +197,9 @@ function graftTool(spec, deps) {
|
|
|
189
197
|
'Run `graft build` in it once — it needs no API key — and this tool will work from then on.',
|
|
190
198
|
}
|
|
191
199
|
}
|
|
200
|
+
// Announced BEFORE the run, so the chip's dot flickers while graft is
|
|
201
|
+
// actually working, not only after it has answered.
|
|
202
|
+
deps.noteUse?.(found)
|
|
192
203
|
const result = await runGraft([...spec.argv(args), found], found, deps)
|
|
193
204
|
const saved = savedTokensIn(result.text)
|
|
194
205
|
if (saved > 0) deps.recordSavings?.(found, saved)
|
package/src/index.js
CHANGED
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
45
45
|
import { spawn } from 'node:child_process'
|
|
46
46
|
import { access, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises'
|
|
47
|
-
import { dirname, join } from 'node:path'
|
|
47
|
+
import { dirname, join, resolve } from 'node:path'
|
|
48
48
|
|
|
49
49
|
import { createAutoSync } from './auto-sync.js'
|
|
50
50
|
import { createGraftCommand } from './command.js'
|
|
51
|
-
import { graftTools, resolveGraftCommand } from './graft-tools.js'
|
|
51
|
+
import { graftTools, resolveGraftCommand, runGraft } from './graft-tools.js'
|
|
52
52
|
|
|
53
53
|
export const name = 'graft-status'
|
|
54
54
|
|
|
@@ -70,6 +70,31 @@ export const inject = ['tools']
|
|
|
70
70
|
*/
|
|
71
71
|
const CACHE_MS = 5000
|
|
72
72
|
|
|
73
|
+
const readUtf8 = async (path) => {
|
|
74
|
+
try {
|
|
75
|
+
return await readFile(path, 'utf8')
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined // missing, or a directory (a main checkout's .git)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The main checkout's root when `dir` is a linked git worktree, else undefined.
|
|
83
|
+
*
|
|
84
|
+
* Pure reads, no `git` child: in a worktree `.git` is a FILE pointing at the
|
|
85
|
+
* worktree's admin dir, whose `commondir` names the shared `.git`.
|
|
86
|
+
*/
|
|
87
|
+
async function linkedWorktreeMain(dir, readText = readUtf8) {
|
|
88
|
+
const dotGit = await readText(join(dir, '.git'))
|
|
89
|
+
if (typeof dotGit !== 'string') return undefined
|
|
90
|
+
const match = /^\s*gitdir:\s*(.+?)\s*$/m.exec(dotGit)
|
|
91
|
+
if (match === null) return undefined
|
|
92
|
+
const gitdir = resolve(dir, match[1])
|
|
93
|
+
const common = await readText(join(gitdir, 'commondir'))
|
|
94
|
+
if (typeof common !== 'string' || common.trim() === '') return undefined
|
|
95
|
+
return dirname(resolve(gitdir, common.trim()))
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
/**
|
|
74
99
|
* The repository root graft would serve from `start`.
|
|
75
100
|
*
|
|
@@ -78,12 +103,18 @@ const CACHE_MS = 5000
|
|
|
78
103
|
* directory alone, because an empty `graft/` folder left behind by a removed
|
|
79
104
|
* index would otherwise shadow a real one higher up.
|
|
80
105
|
*/
|
|
81
|
-
export async function findGraftRoot(start, exists) {
|
|
106
|
+
export async function findGraftRoot(start, exists, readText = readUtf8) {
|
|
82
107
|
let dir = start
|
|
83
108
|
for (;;) {
|
|
84
109
|
for (const probe of ['.cache/stats.json', '.graph/wiring.json', 'INDEX.md']) {
|
|
85
110
|
if (await exists(join(dir, 'graft', probe))) return dir
|
|
86
111
|
}
|
|
112
|
+
if (await exists(join(dir, '.git'))) {
|
|
113
|
+
// Repo boundary. A linked worktree owns its graph: answer only from it,
|
|
114
|
+
// never from the main checkout above — graft seeds a worktree's own
|
|
115
|
+
// first graph from there, and main-branch paths are wrong for this one.
|
|
116
|
+
if ((await linkedWorktreeMain(dir, readText)) !== undefined) return undefined
|
|
117
|
+
}
|
|
87
118
|
const parent = dirname(dir)
|
|
88
119
|
// `dirname` of a filesystem root returns the root itself; that fixed point
|
|
89
120
|
// is the only reliable stop signal across platforms, and it is why this
|
|
@@ -214,14 +245,14 @@ const GRAFT_GUIDANCE = [
|
|
|
214
245
|
'This workspace has a graft index: a prebuilt graph of every symbol, its',
|
|
215
246
|
'file:line span, and who calls what.',
|
|
216
247
|
'',
|
|
217
|
-
'
|
|
248
|
+
'Must use the graft_* tools over reading or grepping files:',
|
|
218
249
|
'- graft_ask to find code or understand how something works — one call usually',
|
|
219
250
|
' replaces several file reads.',
|
|
220
251
|
'- graft_callers before renaming or changing a symbol, to see what depends on it.',
|
|
221
252
|
'- graft_skeleton instead of reading a whole file when signatures are enough.',
|
|
222
253
|
'- graft_map to orient in an unfamiliar part of the tree.',
|
|
223
254
|
'',
|
|
224
|
-
'Fall back to read/grep when graft returns nothing useful, and say so rather',
|
|
255
|
+
'Fall back to read/grep (or ast-grep, if it is installed) when graft returns nothing useful, and say so rather',
|
|
225
256
|
'than searching twice in silence. The graph rebuilds itself after edits, so it',
|
|
226
257
|
'is normally current; `/graft` rebuilds it on demand.',
|
|
227
258
|
].join('\n')
|
|
@@ -257,6 +288,30 @@ export function totalSaved(graftOwn, ours) {
|
|
|
257
288
|
return Math.round(a + b)
|
|
258
289
|
}
|
|
259
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Per-root tally of graft tool calls, for the chip's violet flicker.
|
|
293
|
+
*
|
|
294
|
+
* Every graft_* execute() notes its root here, and the next status poll for
|
|
295
|
+
* that repo carries the count; the client flickers the dot when it moves. The
|
|
296
|
+
* tally lives OUTSIDE the status cache on purpose: a pulse that waited out
|
|
297
|
+
* `cacheMs` would blink long after the tool call it describes had finished.
|
|
298
|
+
*/
|
|
299
|
+
export function createToolPulse() {
|
|
300
|
+
const counts = new Map()
|
|
301
|
+
return {
|
|
302
|
+
/** The model invoked a graft tool whose graph lives at `root`. */
|
|
303
|
+
note(root) {
|
|
304
|
+
if (typeof root !== 'string' || root === '') return
|
|
305
|
+
const hit = counts.get(root)
|
|
306
|
+
counts.set(root, { count: (hit?.count ?? 0) + 1, at: Date.now() })
|
|
307
|
+
},
|
|
308
|
+
/** undefined until the first use: no pulse, no flicker, no field. */
|
|
309
|
+
get(root) {
|
|
310
|
+
return counts.get(root)
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
260
315
|
/**
|
|
261
316
|
* The whole status for one repo root.
|
|
262
317
|
*
|
|
@@ -267,20 +322,22 @@ export function totalSaved(graftOwn, ours) {
|
|
|
267
322
|
* disagree about the same repo.
|
|
268
323
|
*/
|
|
269
324
|
export async function computeStatus(root, io) {
|
|
325
|
+
const worktreeOf = await linkedWorktreeMain(root, io.readText ?? readUtf8)
|
|
270
326
|
const stats = await io.readJson(join(root, 'graft', '.cache', 'stats.json'))
|
|
271
327
|
const base = stats !== undefined && typeof stats.nodeCount === 'number' && stats.nodeCount > 0
|
|
272
328
|
? { nodeCount: stats.nodeCount, edgeCount: stats.edgeCount ?? 0, freshness: freshnessOf(stats), syncedAt: stats.syncedAt ?? null }
|
|
273
329
|
: undefined
|
|
274
330
|
|
|
275
331
|
if (base !== undefined) {
|
|
276
|
-
return { ...base, root, savedTokens: totalSaved(await io.readSavedTokens(root), io.ourSavings?.(root)), source: 'cache' }
|
|
332
|
+
return { ...base, root, worktreeOf, savedTokens: totalSaved(await io.readSavedTokens(root), io.ourSavings?.(root)), source: 'cache' }
|
|
277
333
|
}
|
|
278
334
|
|
|
279
335
|
const counted = countWiring(await io.readJson(join(root, 'graft', '.graph', 'wiring.json')))
|
|
280
|
-
if (counted === undefined) return { root, ok: false, reason: 'no graph' }
|
|
336
|
+
if (counted === undefined) return { root, ok: false, reason: 'no graph', worktreeOf }
|
|
281
337
|
return {
|
|
282
338
|
...counted,
|
|
283
339
|
root,
|
|
340
|
+
worktreeOf,
|
|
284
341
|
// The graph carries no drift signal of its own, so this reads as synced
|
|
285
342
|
// until something repopulates the cache — the same concession graft's
|
|
286
343
|
// statusline documents for this exact fallback.
|
|
@@ -298,6 +355,15 @@ export async function computeStatus(root, io) {
|
|
|
298
355
|
/** graft viz's own default. Kept so a URL a user already has still works. */
|
|
299
356
|
const VIZ_PORT = 4400
|
|
300
357
|
|
|
358
|
+
/**
|
|
359
|
+
* How long an initialise from the popup may take before it gives up, in ms.
|
|
360
|
+
*
|
|
361
|
+
* The same bound as the bare /graft: a small repo finishes in about a
|
|
362
|
+
* second, a large one can take minutes, and a wedged graft must not hold
|
|
363
|
+
* the button busy forever.
|
|
364
|
+
*/
|
|
365
|
+
const BUILD_TIMEOUT_MS = 180_000
|
|
366
|
+
|
|
301
367
|
/** Is something already answering on this port? */
|
|
302
368
|
async function vizAlive(port) {
|
|
303
369
|
try {
|
|
@@ -382,6 +448,12 @@ class GraftStatusRemote extends TypertRemoteService {
|
|
|
382
448
|
if (liveOps === null) throw new Error('graft-status is not ready')
|
|
383
449
|
return liveOps.viz(sessionId)
|
|
384
450
|
}
|
|
451
|
+
|
|
452
|
+
/** Rebuild that session's repo, or initialise it when there is none. */
|
|
453
|
+
build(sessionId) {
|
|
454
|
+
if (liveOps === null) throw new Error('graft-status is not ready')
|
|
455
|
+
return liveOps.build(sessionId)
|
|
456
|
+
}
|
|
385
457
|
}
|
|
386
458
|
|
|
387
459
|
/** Emulate the @Remote decorator without decorator syntax. */
|
|
@@ -407,6 +479,7 @@ function markRemoteMethod(prototype, method) {
|
|
|
407
479
|
|
|
408
480
|
markRemoteMethod(GraftStatusRemote.prototype, 'status')
|
|
409
481
|
markRemoteMethod(GraftStatusRemote.prototype, 'viz')
|
|
482
|
+
markRemoteMethod(GraftStatusRemote.prototype, 'build')
|
|
410
483
|
|
|
411
484
|
export function apply(ctx, config = {}) {
|
|
412
485
|
const fallbackFrom = typeof config.cwd === 'string' && config.cwd.length > 0 ? config.cwd : process.cwd()
|
|
@@ -423,6 +496,9 @@ export function apply(ctx, config = {}) {
|
|
|
423
496
|
let savingsLoaded = false
|
|
424
497
|
let savingsDirty = false
|
|
425
498
|
|
|
499
|
+
/** Per-root graft tool-use pulses; the chip's dot blinks violet when one moves. */
|
|
500
|
+
const pulses = createToolPulse()
|
|
501
|
+
|
|
426
502
|
const loadSavings = async () => {
|
|
427
503
|
if (savingsLoaded) return
|
|
428
504
|
savingsLoaded = true
|
|
@@ -452,6 +528,7 @@ export function apply(ctx, config = {}) {
|
|
|
452
528
|
const io = {
|
|
453
529
|
readJson,
|
|
454
530
|
readSavedTokens,
|
|
531
|
+
readText: readUtf8,
|
|
455
532
|
ourSavings: (root) => savings.get(root) ?? 0,
|
|
456
533
|
exists: async (path) => {
|
|
457
534
|
try {
|
|
@@ -479,6 +556,17 @@ export function apply(ctx, config = {}) {
|
|
|
479
556
|
/** root -> in-flight start, so a double click cannot race two spawns. */
|
|
480
557
|
const vizPending = new Map()
|
|
481
558
|
|
|
559
|
+
/**
|
|
560
|
+
* The rebuild engine, shared by the automatic hooks below and the popup's
|
|
561
|
+
* Build button. Hoisted out of the `autoBuild` block on purpose: the button
|
|
562
|
+
* must rebuild even in a profile that turned the automatic hooks off.
|
|
563
|
+
*/
|
|
564
|
+
const autoSync = createAutoSync({
|
|
565
|
+
cliPath: resolveGraftCommand().prefix[0],
|
|
566
|
+
minIntervalMs: config.autoBuildMinIntervalMs,
|
|
567
|
+
writeTools: config.autoBuildTools,
|
|
568
|
+
})
|
|
569
|
+
|
|
482
570
|
const vizDeps = {
|
|
483
571
|
alive: vizAlive,
|
|
484
572
|
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -510,7 +598,7 @@ export function apply(ctx, config = {}) {
|
|
|
510
598
|
const from = cwdFor(sessionId)
|
|
511
599
|
let root
|
|
512
600
|
try {
|
|
513
|
-
root = await findGraftRoot(from, io.exists)
|
|
601
|
+
root = await findGraftRoot(from, io.exists, io.readText)
|
|
514
602
|
} catch (error) {
|
|
515
603
|
return { ok: false, reason: String(error?.message ?? error), from }
|
|
516
604
|
}
|
|
@@ -518,7 +606,9 @@ export function apply(ctx, config = {}) {
|
|
|
518
606
|
|
|
519
607
|
const now = Date.now()
|
|
520
608
|
const hit = cache.get(root)
|
|
521
|
-
|
|
609
|
+
// The pulse rides ALONGSIDE the cached body, outside it: a blink that
|
|
610
|
+
// waited out cacheMs would land after the tool call had already ended.
|
|
611
|
+
if (hit !== undefined && now - hit.at < cacheMs) return { ...hit.value, toolUse: pulses.get(root) }
|
|
522
612
|
|
|
523
613
|
let value
|
|
524
614
|
try {
|
|
@@ -528,12 +618,12 @@ export function apply(ctx, config = {}) {
|
|
|
528
618
|
value = { ok: false, reason: String(error?.message ?? error), from }
|
|
529
619
|
}
|
|
530
620
|
cache.set(root, { value, at: now })
|
|
531
|
-
return value
|
|
621
|
+
return { ...value, toolUse: pulses.get(root) }
|
|
532
622
|
},
|
|
533
623
|
|
|
534
624
|
viz: async (sessionId) => {
|
|
535
625
|
const from = cwdFor(sessionId)
|
|
536
|
-
const root = await findGraftRoot(from, io.exists)
|
|
626
|
+
const root = await findGraftRoot(from, io.exists, io.readText)
|
|
537
627
|
if (root === undefined) return { ok: false, reason: 'no graft index above ' + from, from }
|
|
538
628
|
|
|
539
629
|
// Already serving this repo: reuse it. Checked before the pending map so
|
|
@@ -567,6 +657,44 @@ export function apply(ctx, config = {}) {
|
|
|
567
657
|
vizPending.delete(root)
|
|
568
658
|
}
|
|
569
659
|
},
|
|
660
|
+
|
|
661
|
+
build: async (sessionId) => {
|
|
662
|
+
const from = cwdFor(sessionId)
|
|
663
|
+
let root
|
|
664
|
+
try {
|
|
665
|
+
root = await findGraftRoot(from, io.exists, io.readText)
|
|
666
|
+
} catch (error) {
|
|
667
|
+
return { ok: false, reason: String(error?.message ?? error), from }
|
|
668
|
+
}
|
|
669
|
+
// No index above the workspace: initialise the workspace itself, plain
|
|
670
|
+
// offline `graft build` — the same argv the bare /graft runs, $0, no
|
|
671
|
+
// API key. sync-run cannot do this job (no index yet to sync), so this
|
|
672
|
+
// is the one place a bare build is correct.
|
|
673
|
+
if (root === undefined) {
|
|
674
|
+
const result = await runGraft(['build', from], from, { timeoutMs: BUILD_TIMEOUT_MS })
|
|
675
|
+
const built = await findGraftRoot(from, io.exists, io.readText)
|
|
676
|
+
if (built === undefined) {
|
|
677
|
+
return { ok: false, reason: `graft build produced no index in ${from}.\n\n${result.text.slice(0, 1200)}`, root: from }
|
|
678
|
+
}
|
|
679
|
+
cache.delete(from)
|
|
680
|
+
return { ok: true, root: from, initialising: true }
|
|
681
|
+
}
|
|
682
|
+
// The SAME rebuild the automatic hooks run — graft's own sync-run.js,
|
|
683
|
+
// which patches stats.json on the way out (clearing `dirty`, stamping
|
|
684
|
+
// `syncedAt`, writing the new counts). A bare `graft build` was tried
|
|
685
|
+
// here first and shipped as a button that changed nothing: it writes the
|
|
686
|
+
// graph but NOT stats.json, so the pill kept reading the old `dirty`
|
|
687
|
+
// flag and never flipped to IN SYNC.
|
|
688
|
+
const outcome = await autoSync.rebuild(root)
|
|
689
|
+
if (outcome !== 'started') {
|
|
690
|
+
return { ok: false, reason: `graft rebuild did not start (${outcome})`, root }
|
|
691
|
+
}
|
|
692
|
+
// Dropped so the next status poll reads the rebuilding graph, not the
|
|
693
|
+
// pre-build counts for the rest of the cache window. sync-run sets
|
|
694
|
+
// `syncing` itself, so the pill shows syncing → in sync for free.
|
|
695
|
+
cache.delete(root)
|
|
696
|
+
return { ok: true, root }
|
|
697
|
+
},
|
|
570
698
|
}
|
|
571
699
|
|
|
572
700
|
new GraftStatusRemote(ctx)
|
|
@@ -577,7 +705,8 @@ export function apply(ctx, config = {}) {
|
|
|
577
705
|
if (config.tools !== false) {
|
|
578
706
|
const toolDeps = {
|
|
579
707
|
fallbackCwd: fallbackFrom,
|
|
580
|
-
findRoot: (start) => findGraftRoot(start, io.exists),
|
|
708
|
+
findRoot: (start) => findGraftRoot(start, io.exists, io.readText),
|
|
709
|
+
noteUse: (root) => pulses.note(root),
|
|
581
710
|
recordSavings: (root, saved) => {
|
|
582
711
|
savings.set(root, (savings.get(root) ?? 0) + saved)
|
|
583
712
|
savingsDirty = true
|
|
@@ -603,7 +732,7 @@ export function apply(ctx, config = {}) {
|
|
|
603
732
|
scope.commands.register(
|
|
604
733
|
createGraftCommand({
|
|
605
734
|
fallbackCwd: fallbackFrom,
|
|
606
|
-
findRoot: (start) => findGraftRoot(start, io.exists),
|
|
735
|
+
findRoot: (start) => findGraftRoot(start, io.exists, io.readText),
|
|
607
736
|
readJson,
|
|
608
737
|
countWiring,
|
|
609
738
|
}),
|
|
@@ -616,37 +745,59 @@ export function apply(ctx, config = {}) {
|
|
|
616
745
|
// The standing instruction, registered on each agent's OWN context so no
|
|
617
746
|
// preset can shadow it (see GRAFT_GUIDANCE for why the persona could not).
|
|
618
747
|
if (config.promptSection !== false) {
|
|
619
|
-
/** Agent contexts already carrying the section
|
|
620
|
-
const briefed = new
|
|
748
|
+
/** Agent contexts already carrying the section -> the registering fiber. */
|
|
749
|
+
const briefed = new WeakMap()
|
|
621
750
|
|
|
622
|
-
|
|
623
|
-
const agentCtx =
|
|
751
|
+
const installGuidance = (agent) => {
|
|
752
|
+
const agentCtx = agent?.ctx
|
|
624
753
|
if (agentCtx === undefined || agentCtx === null || briefed.has(agentCtx)) return
|
|
625
|
-
briefed.add(agentCtx)
|
|
626
754
|
try {
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
755
|
+
// dsh 0.1.7 removed the old session-start event; `agent/created` is
|
|
756
|
+
// the seam its own plugins use. `inject` rather than `agentCtx.get`:
|
|
757
|
+
// an agent scope without the prompt registry never calls this back —
|
|
758
|
+
// it loses the sentence, not the session.
|
|
759
|
+
const fiber = agentCtx.inject?.(['systemPrompt'], (scope) => {
|
|
760
|
+
try {
|
|
761
|
+
scope.systemPrompt?.section?.({
|
|
762
|
+
name: 'graft-status:usage',
|
|
763
|
+
order: GUIDANCE_ORDER,
|
|
764
|
+
text: GRAFT_GUIDANCE,
|
|
765
|
+
})
|
|
766
|
+
} catch {
|
|
767
|
+
// Already registered (e.g. after a plugin reload).
|
|
768
|
+
}
|
|
769
|
+
})
|
|
770
|
+
if (fiber !== undefined) briefed.set(agentCtx, fiber)
|
|
637
771
|
} catch {
|
|
638
772
|
// A section that fails to register costs a nudge, never the session.
|
|
639
773
|
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const uninstallGuidance = (agent) => {
|
|
777
|
+
const agentCtx = agent?.ctx
|
|
778
|
+
const fiber = agentCtx === undefined || agentCtx === null ? undefined : briefed.get(agentCtx)
|
|
779
|
+
if (fiber === undefined) return
|
|
780
|
+
briefed.delete(agentCtx)
|
|
781
|
+
try {
|
|
782
|
+
void fiber.dispose?.()
|
|
783
|
+
} catch {
|
|
784
|
+
// Teardown must not throw.
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Agents that already exist when this row mounts (plugin reload).
|
|
789
|
+
ctx.inject?.(['agents'], (scope) => {
|
|
790
|
+
for (const agent of scope.agents?.list?.() ?? []) installGuidance(agent)
|
|
640
791
|
})
|
|
792
|
+
ctx.on('agent/created', ({ agent }) => installGuidance(agent))
|
|
793
|
+
ctx.on('agent/disposed', ({ agent }) => uninstallGuidance(agent))
|
|
641
794
|
}
|
|
642
795
|
|
|
643
796
|
// Auto-rebuild: graft's two Claude Code hooks, on dsh's own seams.
|
|
797
|
+
// The autoSync instance itself is shared (see above): this block only wires
|
|
798
|
+
// the AUTOMATIC triggers, so `autoBuild: false` silences the hooks while
|
|
799
|
+
// the popup's manual Build keeps working.
|
|
644
800
|
if (config.autoBuild !== false) {
|
|
645
|
-
const autoSync = createAutoSync({
|
|
646
|
-
cliPath: resolveGraftCommand().prefix[0],
|
|
647
|
-
minIntervalMs: config.autoBuildMinIntervalMs,
|
|
648
|
-
writeTools: config.autoBuildTools,
|
|
649
|
-
})
|
|
650
801
|
|
|
651
802
|
ctx.on('tools/execute', async (exec, next) => {
|
|
652
803
|
const result = await next()
|
|
@@ -657,8 +808,11 @@ export function apply(ctx, config = {}) {
|
|
|
657
808
|
// Going via the sessions registry would be a lookup to reach a value
|
|
658
809
|
// already in hand, and would differ if the two ever disagreed.
|
|
659
810
|
const from = exec?.agent?.session?.header?.cwd ?? fallbackFrom
|
|
660
|
-
const root = await findGraftRoot(from, io.exists)
|
|
661
|
-
if (root !== undefined)
|
|
811
|
+
const root = await findGraftRoot(from, io.exists, io.readText)
|
|
812
|
+
if (root !== undefined) {
|
|
813
|
+
// A rejected promise here would be unhandled — fatal to dsh.
|
|
814
|
+
void autoSync.markDirty(root).catch(() => {})
|
|
815
|
+
}
|
|
662
816
|
}
|
|
663
817
|
return result
|
|
664
818
|
})
|
|
@@ -667,7 +821,7 @@ export function apply(ctx, config = {}) {
|
|
|
667
821
|
|
|
668
822
|
ctx.on('agent/turn-stopping', async (payload) => {
|
|
669
823
|
const cwd = payload?.agent?.session?.header?.cwd ?? fallbackFrom
|
|
670
|
-
const root = await findGraftRoot(cwd, io.exists)
|
|
824
|
+
const root = await findGraftRoot(cwd, io.exists, io.readText)
|
|
671
825
|
if (root === undefined) return
|
|
672
826
|
const outcome = await autoSync.syncIfDirty(root)
|
|
673
827
|
if (outcome === 'started') {
|