@hybridlabor-api/aos 4.17.0 → 4.18.1
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/.claude/hooks/aos-bus.mjs +8 -4
- package/.claude/hooks/go-gate.mjs +1 -1
- package/.claude/hooks/go-token.mjs +17 -2
- package/.claude/hooks/memb-inject.mjs +61 -37
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/commands/bdb-aos-plan.md +1 -1
- package/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/bin/aos-acp.mjs +27 -1
- package/bin/aos-doctor.mjs +36 -1
- package/bin/aos-uninstall.mjs +16 -2
- package/bin/go-check.mjs +79 -0
- package/bin/guarded-patterns.json +106 -0
- package/commands/plan.md +1 -1
- package/docs/codenotch.md +44 -0
- package/docs/codex-gate-smoke.md +43 -0
- package/docs/delegation-routing.md +32 -0
- package/docs/go-check.md +60 -0
- package/docs/master-session-acp.md +2 -0
- package/docs/opencode-setup.md +18 -0
- package/installer.js +155 -70
- package/lib/codenotch.js +389 -0
- package/lib/retired-skills.js +101 -0
- package/mcps/mcsc/README.md +1 -1
- package/mcps/mcsc/packages/core/src/adapters/agy.js +3 -1
- package/mcps/mcsc/packages/core/src/adapters/codex.js +2 -1
- package/mcps/mcsc/packages/core/src/adapters/opencode.js +2 -1
- package/mcps/mcsc/packages/core/src/depth.js +16 -0
- package/mcps/mcsc/packages/mcp/server.js +15 -2
- package/package.json +2 -2
- package/plugin-commands.json +1 -2
- package/plugin.json +1 -4
- package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/bdb-aos-codex/skills/plan/SKILL.md +1 -1
- package/scripts/codex-gate-smoke.mjs +73 -0
- package/skills/basic/master-session/SKILL.md +11 -0
- package/skills/global_config/agenttrail/SKILL.md +3 -1
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +255 -117
- package/skills/global_config/agenttrail/bin/ensure.mjs +60 -40
- package/skills/global_config/agenttrail/bin/repoid.mjs +70 -0
- package/skills/global_config/agenttrail/public/index.html +9 -1
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
- package/skills/global_config/bdb-memb-mcp/SKILL.md +5 -4
- package/skills/global_config/bdb-visual-edit/SKILL.md +28 -32
- package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +2 -2
- package/skills/global_config/bdb-visual-edit/scripts/locate-source.mjs +135 -0
- package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +30 -2
- package/skills/global_config/mcsc/SKILL.md +9 -1
- package/skills/global_config/plan-arbiter/SKILL.md +1 -1
- package/skills/global_config/plan-canvas/SKILL.md +42 -4
- package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +1 -1
- package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +2 -2
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/geometry.js +76 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/index.js +596 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/model.js +192 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/toolbar.js +99 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-server.js +282 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotation-schema.js +210 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/route.js +10 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +6 -230
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +45 -4
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +60 -21
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/trail-on-approve.js +103 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +19 -7
- package/skills/global_config/plan-canvas/scripts/plan-canvas.js +118 -20
- package/skills/global_config/subagent-setup/SKILL.md +6 -0
- package/skills/global_config/subagent-setup/scripts/setup-subagents.mjs +20 -1
- package/skills/playbooks/pb-idea-to-launch/SKILL.md +2 -2
- package/skills/playbooks/pb-redesign-app/SKILL.md +3 -3
- package/skills/playbooks/pb-release-aos/SKILL.md +2 -2
- package/skills/playbooks/pb-ship/SKILL.md +2 -2
- package/skills/playbooks/pb-worktrees-land/SKILL.md +2 -2
- package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +0 -27
- package/skills/global_config/visual-edit/README.md +0 -96
- package/skills/global_config/visual-edit/SKILL.md +0 -615
- package/skills/global_config/visual-plan/README.md +0 -93
- package/skills/global_config/visual-plan/SKILL.md +0 -544
- package/skills/global_config/visual-plan/references/canvas.md +0 -139
- package/skills/global_config/visual-plan/references/connection.md +0 -51
- package/skills/global_config/visual-plan/references/document-quality.md +0 -186
- package/skills/global_config/visual-plan/references/exemplar.md +0 -62
- package/skills/global_config/visual-plan/references/local-files.md +0 -99
- package/skills/global_config/visual-plan/references/wireframe.md +0 -319
- package/skills/global_config/visual-recap/README.md +0 -103
- package/skills/global_config/visual-recap/SKILL.md +0 -560
- package/skills/global_config/visual-recap/references/connection.md +0 -51
- package/skills/global_config/visual-recap/references/local-files.md +0 -99
- package/skills/global_config/visual-recap/references/wireframe.md +0 -319
|
@@ -8,6 +8,7 @@ import path from 'node:path'
|
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import os from 'node:os'
|
|
10
10
|
import crypto from 'node:crypto'
|
|
11
|
+
import { norm, probePorts, mainRoot, gitRoots, listWorktrees, selectPlan, PROTOCOL } from './repoid.mjs'
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
15
16
|
const argv = process.argv.slice(2)
|
|
16
17
|
let cmd = null
|
|
17
18
|
let repo = process.cwd()
|
|
18
|
-
let port =
|
|
19
|
+
let port = probePorts()[0]
|
|
19
20
|
const spawningPaths = new Set()
|
|
20
21
|
let openBrowser = process.stdout.isTTY ? true : false
|
|
21
22
|
let noOpen = false
|
|
@@ -64,8 +65,10 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
64
65
|
else if (cmd === 'ask') (askWords = askWords || []).push(a)
|
|
65
66
|
else repo = path.resolve(a)
|
|
66
67
|
}
|
|
68
|
+
// one map per git repo: any worktree resolves to the main checkout (init keeps the folder it was given)
|
|
69
|
+
if (!ensureMode && (cmd === null || cmd === 'autostart')) repo = mainRoot(repo)
|
|
67
70
|
// AOS patch: --plan <path> overrides the default PLAN.md location
|
|
68
|
-
const planPath = planArg ? path.resolve(repo, planArg) : path.join(repo, 'PLAN.md')
|
|
71
|
+
const planPath = planArg ? (p => fs.existsSync(p) ? norm(p) : p)(path.resolve(repo, planArg)) : path.join(repo, 'PLAN.md')
|
|
69
72
|
const atDir = path.join(repo, '.agenttrail')
|
|
70
73
|
|
|
71
74
|
function realDirectory(value) {
|
|
@@ -160,7 +163,7 @@ async function relayHook() {
|
|
|
160
163
|
body = JSON.stringify(ev)
|
|
161
164
|
} catch {}
|
|
162
165
|
}
|
|
163
|
-
const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] :
|
|
166
|
+
const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] : probePorts()
|
|
164
167
|
await Promise.allSettled(ports.map(p => fetch(`http://127.0.0.1:${p}/hook`, {
|
|
165
168
|
method: 'POST', body, signal: AbortSignal.timeout(400),
|
|
166
169
|
}).catch(() => {})))
|
|
@@ -196,9 +199,8 @@ function parseTimeoutArg(v) {
|
|
|
196
199
|
// repoPath — same discovery as bootDedup; AGENTTRAIL_PORT wins when set. Both sides are
|
|
197
200
|
// symlink-normalized so a daemon started via /tmp matches a cwd of /private/tmp (macOS).
|
|
198
201
|
async function findDaemon() {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] : Array.from({ length: 15 }, (_, i) => 5330 + i)
|
|
202
|
+
const here = norm(mainRoot(repo))
|
|
203
|
+
const ports = process.env.AGENTTRAIL_PORT ? [parseInt(process.env.AGENTTRAIL_PORT, 10)] : probePorts()
|
|
202
204
|
const hits = await Promise.all(ports.map(p =>
|
|
203
205
|
fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(400) }).then(r => r.json()).then(w => (w && norm(w.repoPath) === here) ? p : null).catch(() => null)))
|
|
204
206
|
return hits.find(Boolean) || null
|
|
@@ -294,13 +296,171 @@ function parsePlan(text) {
|
|
|
294
296
|
|
|
295
297
|
// ---------- live state ----------
|
|
296
298
|
const session = { id: Math.random().toString(36).slice(2, 10), project: path.basename(repo), startedAt: new Date().toISOString() }
|
|
297
|
-
let planText =
|
|
298
|
-
let parsed =
|
|
299
|
+
let planText = ''
|
|
300
|
+
let parsed = { nodes: [], decisions: [], title: '' }
|
|
299
301
|
let activity = null // { file, at } — most recent non-plan repo write
|
|
300
302
|
let recentActivity = [] // last N writes, newest first — feeds the live-view drill-down
|
|
301
|
-
let planMtime =
|
|
303
|
+
let planMtime = null
|
|
302
304
|
const clients = new Set()
|
|
303
305
|
|
|
306
|
+
// ---------- lanes: the main checkout plus every linked worktree of the repo, one group each on the map ----------
|
|
307
|
+
const MAX_LANES = 12
|
|
308
|
+
const lanes = new Map() // key ('' = main checkout) -> { key, root, branch, label, watcher }
|
|
309
|
+
const laneList = () => [...lanes.values()]
|
|
310
|
+
const mainLane = () => lanes.get('')
|
|
311
|
+
const slug = s => String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'wt'
|
|
312
|
+
function laneAt(abs) {
|
|
313
|
+
let best = null
|
|
314
|
+
for (const l of lanes.values()) if (isWithin(l.root, abs) && (!best || l.root.length > best.root.length)) best = l
|
|
315
|
+
return best
|
|
316
|
+
}
|
|
317
|
+
// plan file of a lane: the CLI path, mapped to the same repo-relative path in every worktree, else that worktree's own plan
|
|
318
|
+
function planRel() {
|
|
319
|
+
if (!planArg) return 'PLAN.md'
|
|
320
|
+
if (!path.isAbsolute(planArg)) return planArg
|
|
321
|
+
const l = laneAt(norm(planArg))
|
|
322
|
+
return l ? path.relative(l.root, norm(planArg)) : null
|
|
323
|
+
}
|
|
324
|
+
function lanePlan(l) {
|
|
325
|
+
const rel = planRel()
|
|
326
|
+
if (!l.key && !(planArg && path.isAbsolute(planArg) && rel !== null)) return planPath
|
|
327
|
+
if (rel === null) return selectPlan(l.root).plan || null
|
|
328
|
+
const f = path.join(l.root, rel)
|
|
329
|
+
return fs.existsSync(f) || !planArg ? f : selectPlan(l.root).plan || null
|
|
330
|
+
}
|
|
331
|
+
// rebuilds the merged plan: linked-worktree ids get a `<lane>--` prefix so one repo's plan can sit in many lanes
|
|
332
|
+
function reloadPlans() {
|
|
333
|
+
const nodes = []
|
|
334
|
+
const matchers = []
|
|
335
|
+
const texts = []
|
|
336
|
+
let title = ''
|
|
337
|
+
let decisions = []
|
|
338
|
+
let mtime = null
|
|
339
|
+
const multi = lanes.size > 1
|
|
340
|
+
for (const l of laneList()) {
|
|
341
|
+
const file = lanePlan(l)
|
|
342
|
+
const text = file ? safeRead(file) : ''
|
|
343
|
+
texts.push(text)
|
|
344
|
+
const p = parsePlan(text)
|
|
345
|
+
const pre = l.key ? l.key + '--' : ''
|
|
346
|
+
for (const n of p.nodes) {
|
|
347
|
+
if (pre) { n.id = pre + n.id; n.parent = n.parent && pre + n.parent; n.needs = n.needs.map(x => pre + x); n.links = n.links.map(x => pre + x) }
|
|
348
|
+
if (multi) { n.lane = l.key; n.laneLabel = l.label }
|
|
349
|
+
nodes.push(n)
|
|
350
|
+
if (n.level === 'component' && n.files.length) matchers.push({ id: n.id, lane: l.key, res: n.files.map(globToRe) })
|
|
351
|
+
}
|
|
352
|
+
if (!l.key) { title = p.title; decisions = p.decisions }
|
|
353
|
+
const m = file ? statMtime(file) : null
|
|
354
|
+
if (m && (!mtime || m > mtime)) mtime = m
|
|
355
|
+
}
|
|
356
|
+
planText = texts.filter(Boolean).join('\n')
|
|
357
|
+
parsed = { nodes, decisions, title }
|
|
358
|
+
compMatchers = matchers
|
|
359
|
+
planMtime = mtime
|
|
360
|
+
}
|
|
361
|
+
function watchLane(l) {
|
|
362
|
+
const onPlan = () => {
|
|
363
|
+
clearTimeout(planDebounce)
|
|
364
|
+
planDebounce = setTimeout(() => { reloadPlans(); broadcast() }, 150)
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
l.watcher = fs.watch(l.root, { recursive: true }, (_ev, filename) => {
|
|
368
|
+
if (!filename) return
|
|
369
|
+
const f = filename.toString().split(path.sep).join('/')
|
|
370
|
+
if (IGNORE.test(f) || TMP_FILE.test(f)) return
|
|
371
|
+
const abs = path.resolve(l.root, f)
|
|
372
|
+
if (abs === lanePlan(l)) { onPlan(); return }
|
|
373
|
+
if (laneAt(abs) !== l) return // a nested worktree reports its own churn
|
|
374
|
+
const shown = l.key ? `${l.key}:${f}` : f
|
|
375
|
+
if (!l.key) treeDirty = true
|
|
376
|
+
stateDirty = true
|
|
377
|
+
activity = { file: shown, at: Date.now() }
|
|
378
|
+
heatFile(shown, activity.at)
|
|
379
|
+
touchComponents(f, activity.at, l.key)
|
|
380
|
+
if (!recentActivity.length || recentActivity[0].file !== shown) recentActivity.unshift(activity)
|
|
381
|
+
else recentActivity[0] = activity
|
|
382
|
+
recentActivity = recentActivity.slice(0, 12)
|
|
383
|
+
throttleBroadcast()
|
|
384
|
+
})
|
|
385
|
+
} catch (e) {
|
|
386
|
+
// recursive watch is unsupported on some platforms (older linux) — degrade:
|
|
387
|
+
// plan updates and top-level changes still tracked, deep activity limited.
|
|
388
|
+
try {
|
|
389
|
+
l.watcher = fs.watch(l.root, (_ev, filename) => {
|
|
390
|
+
if (filename && path.resolve(l.root, filename.toString()) === lanePlan(l)) onPlan()
|
|
391
|
+
})
|
|
392
|
+
if (!l.key) console.error('note: recursive file watching unavailable here — plan updates still live, deep file activity limited')
|
|
393
|
+
} catch (e2) {
|
|
394
|
+
console.error('watcher failed:', e2.message)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// re-reads `git worktree list`; returns true when the set of lanes changed
|
|
399
|
+
// main is always a lane; the other slots go to the most recently active worktrees, the rest share the 'other' lane
|
|
400
|
+
const OTHER = 'other'
|
|
401
|
+
const TTL = 30000
|
|
402
|
+
const active = new Map() // worktree root -> last event ts
|
|
403
|
+
const overCap = new Map() // worktree root without a lane -> cache expiry
|
|
404
|
+
const activeAt = p => { const t = active.get(p); if (t) return t; try { return fs.statSync(p).mtimeMs } catch { return 0 } }
|
|
405
|
+
const otherLane = root => ({ key: OTHER, root, label: 'other worktrees', branch: null, other: true })
|
|
406
|
+
const hasOther = () => Object.values(runs).some(r => r.lane === OTHER)
|
|
407
|
+
function refreshLanes() {
|
|
408
|
+
const all = listWorktrees(repo, Infinity)
|
|
409
|
+
const recent = p => Date.now() - (active.get(p) || 0) < 10 * 60e3 && laneList().some(l => l.root === p) // busy lanes keep their slot, so lanes do not thrash
|
|
410
|
+
const rank = p => recent(p) ? Infinity : activeAt(p)
|
|
411
|
+
const keep = new Set([all[0].path, ...all.slice(1).sort((a, b) => rank(b.path) - rank(a.path) || activeAt(b.path) - activeAt(a.path)).slice(0, MAX_LANES - 1).map(w => w.path)])
|
|
412
|
+
const wts = all.filter(w => keep.has(w.path))
|
|
413
|
+
const used = new Set(['', OTHER])
|
|
414
|
+
const next = new Map()
|
|
415
|
+
let changed = false
|
|
416
|
+
for (const w of wts) {
|
|
417
|
+
const old = laneList().find(l => l.root === w.path)
|
|
418
|
+
const main = w.path === repo
|
|
419
|
+
let key = main ? '' : old ? old.key : ''
|
|
420
|
+
if (!main && !old) { const base = slug(path.basename(w.path)); key = base; for (let i = 2; used.has(key) || (lanes.has(key) && lanes.get(key).root !== w.path); i++) key = `${base}-${i}` }
|
|
421
|
+
used.add(key)
|
|
422
|
+
const label = `${w.branch || 'no branch'} · ${path.basename(w.path)}`
|
|
423
|
+
const l = old || { key, root: w.path, watcher: null }
|
|
424
|
+
if (!old || l.branch !== w.branch) changed = true
|
|
425
|
+
l.branch = w.branch
|
|
426
|
+
l.label = label
|
|
427
|
+
next.set(key, l)
|
|
428
|
+
}
|
|
429
|
+
for (const [k, l] of lanes) if (!next.has(k)) { changed = true; try { l.watcher && l.watcher.close() } catch {} }
|
|
430
|
+
lanes.clear()
|
|
431
|
+
for (const [k, l] of next) lanes.set(k, l)
|
|
432
|
+
for (const l of laneList()) if (!l.watcher) watchLane(l)
|
|
433
|
+
for (const w of keep) overCap.delete(w)
|
|
434
|
+
if (changed) treeDirty = true
|
|
435
|
+
return changed
|
|
436
|
+
}
|
|
437
|
+
const missedCwd = new Map() // cwd -> expiry; keeps unrelated repos' events from costing a git call each
|
|
438
|
+
// the lane an event's cwd belongs to; git runs at most once per unknown cwd per TTL, over-cap worktrees are cached
|
|
439
|
+
function laneFor(cwd) {
|
|
440
|
+
if (!cwd) return null
|
|
441
|
+
const abs = path.resolve(cwd)
|
|
442
|
+
const now = Date.now()
|
|
443
|
+
let l = laneAt(abs) || laneAt(norm(abs))
|
|
444
|
+
if (l) { active.set(l.root, now); return l }
|
|
445
|
+
for (const [top, until] of overCap) if (until > now && isWithin(top, abs)) { active.set(top, now); return otherLane(top) }
|
|
446
|
+
const until = missedCwd.get(abs)
|
|
447
|
+
if (until && until > now) return null
|
|
448
|
+
const g = gitRoots(abs)
|
|
449
|
+
if (norm(g.main) === norm(repo)) {
|
|
450
|
+
const top = norm(g.top)
|
|
451
|
+
active.set(top, now)
|
|
452
|
+
if (refreshLanes()) { reloadPlans(); broadcast() }
|
|
453
|
+
l = laneAt(abs) || laneAt(norm(abs))
|
|
454
|
+
if (l) return l
|
|
455
|
+
if (top === norm(repo)) return mainLane()
|
|
456
|
+
overCap.set(top, now + TTL)
|
|
457
|
+
return otherLane(top)
|
|
458
|
+
}
|
|
459
|
+
if (missedCwd.size > 200) missedCwd.clear()
|
|
460
|
+
missedCwd.set(abs, now + TTL)
|
|
461
|
+
return null
|
|
462
|
+
}
|
|
463
|
+
|
|
304
464
|
// ---------- observed activity per component (files: globs) ----------
|
|
305
465
|
// Declared status ([~]/[x]) says what the agent claims; this says what the
|
|
306
466
|
// filesystem shows. A component whose files are being written is live no
|
|
@@ -319,12 +479,8 @@ function globToRe(g) {
|
|
|
319
479
|
return new RegExp('^' + esc + '$')
|
|
320
480
|
}
|
|
321
481
|
let compMatchers = []
|
|
322
|
-
function
|
|
323
|
-
compMatchers
|
|
324
|
-
.map(c => ({ id: c.id, res: c.files.map(globToRe) }))
|
|
325
|
-
}
|
|
326
|
-
function touchComponents(file, at) {
|
|
327
|
-
for (const m of compMatchers) if (m.res.some(re => re.test(file))) {
|
|
482
|
+
function touchComponents(file, at, laneKey = '') {
|
|
483
|
+
for (const m of compMatchers) if (m.lane === laneKey && m.res.some(re => re.test(file))) {
|
|
328
484
|
compTouched[m.id] = at
|
|
329
485
|
const arr = compRecent[m.id] || (compRecent[m.id] = [])
|
|
330
486
|
if (arr[0] && arr[0].file === file) arr[0] = { file, at, n: (arr[0].n || 1) + 1 }
|
|
@@ -342,15 +498,15 @@ const handoffs = [] // {c, from, to, at} — one session picks up where another
|
|
|
342
498
|
function runFor(id, cwd) {
|
|
343
499
|
return runs[id] || (runs[id] = { id, agent: 'claude', cwd, startedAt: Date.now(), lastEventAt: Date.now(), todos: [], currentTool: null, recentTools: [], componentId: null, ended: false, done: false, waiting: null })
|
|
344
500
|
}
|
|
345
|
-
function toolDetail(input = {}) {
|
|
501
|
+
function toolDetail(input = {}, root = repo) {
|
|
346
502
|
const p = input.file_path || input.notebook_path
|
|
347
|
-
const d = input.command || (p ? (relToRepo(p) ?? p) : '') || input.description || input.pattern || input.url || input.query || input.prompt || ''
|
|
503
|
+
const d = input.command || (p ? (relToRepo(p, root) ?? p) : '') || input.description || input.pattern || input.url || input.query || input.prompt || ''
|
|
348
504
|
return String(d).replace(/\s+/g, ' ').slice(0, 90)
|
|
349
505
|
}
|
|
350
|
-
function relToRepo(p) {
|
|
506
|
+
function relToRepo(p, root = repo) {
|
|
351
507
|
if (!p) return null
|
|
352
508
|
const r = path.resolve(String(p))
|
|
353
|
-
return r ===
|
|
509
|
+
return r === root ? '' : r.startsWith(root + path.sep) ? r.slice(root.length + 1) : null
|
|
354
510
|
}
|
|
355
511
|
// AOS patch: native plan/todo tools across harnesses, normalized to the
|
|
356
512
|
// {content, status} shape the frontend's run-todo rendering already expects.
|
|
@@ -376,8 +532,10 @@ function normalizeTodos(toolName, input) {
|
|
|
376
532
|
}
|
|
377
533
|
function handleHookEvent(ev) {
|
|
378
534
|
const cwd = ev.cwd || ''
|
|
379
|
-
|
|
535
|
+
const lane = laneFor(cwd)
|
|
536
|
+
if (!lane) return false
|
|
380
537
|
const run = runFor(ev.session_id || 'session', cwd)
|
|
538
|
+
run.lane = lane.key
|
|
381
539
|
if (ev.agent) run.agent = String(ev.agent).slice(0, 24).toLowerCase()
|
|
382
540
|
run.lastEventAt = Date.now()
|
|
383
541
|
stateDirty = true
|
|
@@ -393,7 +551,7 @@ function handleHookEvent(ev) {
|
|
|
393
551
|
run.ended = false
|
|
394
552
|
run.done = false // AOS patch: an ended run that resumes is no longer done
|
|
395
553
|
run.waiting = null // AOS patch: a resumed run is no longer waiting on the human
|
|
396
|
-
run.currentTool = { name: ev.tool_name, detail: toolDetail(ev.tool_input), at: Date.now() }
|
|
554
|
+
run.currentTool = { name: ev.tool_name, detail: toolDetail(ev.tool_input, lane.root), at: Date.now() }
|
|
397
555
|
if (ev.tool_name === 'Task' && ev.tool_input) {
|
|
398
556
|
const name = String(ev.tool_input.description || ev.tool_input.subagent_type || 'sub-agent').slice(0, 60)
|
|
399
557
|
;(run.subagents = run.subagents || []).push({ name, startedAt: Date.now(), ended: false })
|
|
@@ -407,17 +565,18 @@ function handleHookEvent(ev) {
|
|
|
407
565
|
run.done = false // AOS patch: keep done in sync when a run resumes
|
|
408
566
|
run.waiting = null // AOS patch: a resumed run is no longer waiting on the human
|
|
409
567
|
const started = run.currentTool && run.currentTool.name === ev.tool_name ? run.currentTool.at : Date.now()
|
|
410
|
-
run.recentTools.unshift({ name: ev.tool_name, detail: toolDetail(ev.tool_input), at: Date.now(), ms: Date.now() - started })
|
|
568
|
+
run.recentTools.unshift({ name: ev.tool_name, detail: toolDetail(ev.tool_input, lane.root), at: Date.now(), ms: Date.now() - started })
|
|
411
569
|
if (run.recentTools.length > 8) run.recentTools.length = 8
|
|
412
570
|
run.currentTool = null
|
|
413
571
|
const todos = normalizeTodos(ev.tool_name, ev.tool_input)
|
|
414
572
|
if (todos) run.todos = todos
|
|
415
|
-
const rel = relToRepo(ev.tool_input && (ev.tool_input.file_path || ev.tool_input.notebook_path))
|
|
573
|
+
const rel = relToRepo(ev.tool_input && (ev.tool_input.file_path || ev.tool_input.notebook_path), lane.root)
|
|
416
574
|
if (rel) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
575
|
+
const shown = lane.key ? `${lane.key}:${rel}` : rel
|
|
576
|
+
heatFile(shown, Date.now())
|
|
577
|
+
touchComponents(rel, Date.now(), lane.key)
|
|
578
|
+
activity = { file: shown, at: Date.now() }
|
|
579
|
+
for (const m of compMatchers) if (m.lane === lane.key && m.res.some(re => re.test(rel))) {
|
|
421
580
|
if (run.componentId !== m.id) {
|
|
422
581
|
(run.path = run.path || []).push({ c: m.id, at: Date.now() })
|
|
423
582
|
const prev = Object.values(runs).find(r2 => r2.id !== run.id && r2.ended && r2.componentId === m.id && Date.now() - r2.lastEventAt < 10 * 60e3)
|
|
@@ -520,6 +679,7 @@ function buildTree(rootDir, budgetN = 4000, perDir = 250) {
|
|
|
520
679
|
for (const e of entries) {
|
|
521
680
|
const r = rel ? rel + '/' + e.name : e.name
|
|
522
681
|
if (IGNORE.test(r) || TMP_FILE.test(r)) continue
|
|
682
|
+
if (e.isDirectory() && laneList().some(l => l.key && l.root === path.join(dir, e.name))) continue
|
|
523
683
|
if (taken >= perDir || budget <= 0) { treeTruncated = true; break }
|
|
524
684
|
taken++; budget--
|
|
525
685
|
if (e.isDirectory()) {
|
|
@@ -536,14 +696,13 @@ let treeTruncated = false
|
|
|
536
696
|
let tree = buildTree(repo)
|
|
537
697
|
let treeDirty = false
|
|
538
698
|
let lastTreeSent = Date.now()
|
|
539
|
-
rebuildMatchers()
|
|
540
699
|
|
|
541
700
|
function safeRead(p) { try { return fs.readFileSync(p, 'utf8') } catch { return '' } }
|
|
542
701
|
function statMtime(p) { try { return fs.statSync(p).mtimeMs } catch { return null } }
|
|
543
702
|
|
|
544
703
|
function compFilesFor(id) {
|
|
545
704
|
const m = compMatchers.find(x => x.id === id)
|
|
546
|
-
if (!m) return []
|
|
705
|
+
if (!m || m.lane) return [] // only the main checkout has a file tree
|
|
547
706
|
const out = []
|
|
548
707
|
const walk = t => { for (const n of t) { if (n.dir) walk(n.children || []); else if (m.res.some(re => re.test(n.path))) out.push(n.path) } }
|
|
549
708
|
walk(tree)
|
|
@@ -610,7 +769,7 @@ function syntheticPlanNodes() {
|
|
|
610
769
|
function model() {
|
|
611
770
|
if (treeDirty) { tree = buildTree(repo); treeDirty = false }
|
|
612
771
|
// AOS patch: include planFile (relative path) and aosPlan (boolean) for AOS UI customization
|
|
613
|
-
const planFile = planArg ? path.relative(repo, planPath).split(path.sep).join('/') : undefined
|
|
772
|
+
const planFile = planArg ? path.relative(repo, lanePlan(mainLane()) || planPath).split(path.sep).join('/') : undefined
|
|
614
773
|
const aosPlan = Boolean(planArg)
|
|
615
774
|
const realPlanNodes = parsed.nodes.map(n => n.level === 'component' ? { ...n, touchedAt: compTouched[n.id] || null, recent: compRecent[n.id] || [], filesList: compFilesFor(n.id) } : n)
|
|
616
775
|
const planNodes = realPlanNodes.length ? realPlanNodes : syntheticPlanNodes()
|
|
@@ -620,6 +779,7 @@ function model() {
|
|
|
620
779
|
session, plan: planNodes, tree,
|
|
621
780
|
planTitle: parsed.title,
|
|
622
781
|
hasPlan: planText.length > 0, treeTruncated,
|
|
782
|
+
lanes: lanes.size > 1 || hasOther() ? [...laneList(), ...(hasOther() ? [otherLane(null)] : [])].map(l => ({ key: l.key, label: l.label, branch: l.branch, root: l.root, main: !l.key, other: l.other || undefined })) : undefined,
|
|
623
783
|
activity, recentActivity, planMtime, handoffs, asks, hotFiles: hotFiles(), cycles, // AOS patch: asks ride along so the open map updates live
|
|
624
784
|
planStale: planStaleness(), lints: lintPlan(), hooksInstalled: hooksInstalled(),
|
|
625
785
|
planFile, aosPlan,
|
|
@@ -639,56 +799,9 @@ function broadcastTick() {
|
|
|
639
799
|
|
|
640
800
|
// ---------- watcher ----------
|
|
641
801
|
let planDebounce = null
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const f = filename.toString().split(path.sep).join('/')
|
|
646
|
-
if (IGNORE.test(f) || TMP_FILE.test(f)) return
|
|
647
|
-
if (path.resolve(repo, f) === planPath) {
|
|
648
|
-
clearTimeout(planDebounce)
|
|
649
|
-
planDebounce = setTimeout(() => {
|
|
650
|
-
planText = safeRead(planPath)
|
|
651
|
-
parsed = parsePlan(planText)
|
|
652
|
-
rebuildMatchers()
|
|
653
|
-
planMtime = statMtime(planPath)
|
|
654
|
-
broadcast()
|
|
655
|
-
}, 150)
|
|
656
|
-
return
|
|
657
|
-
}
|
|
658
|
-
// plain repo churn → liveness signal
|
|
659
|
-
treeDirty = true
|
|
660
|
-
stateDirty = true
|
|
661
|
-
activity = { file: f, at: Date.now() }
|
|
662
|
-
heatFile(f, activity.at)
|
|
663
|
-
touchComponents(f, activity.at)
|
|
664
|
-
if (!recentActivity.length || recentActivity[0].file !== f) recentActivity.unshift(activity)
|
|
665
|
-
else recentActivity[0] = activity
|
|
666
|
-
recentActivity = recentActivity.slice(0, 12)
|
|
667
|
-
throttleBroadcast()
|
|
668
|
-
})
|
|
669
|
-
} catch (e) {
|
|
670
|
-
// recursive watch is unsupported on some platforms (older linux) — degrade:
|
|
671
|
-
// PLAN.md and top-level changes still tracked, deep activity limited.
|
|
672
|
-
try {
|
|
673
|
-
fs.watch(repo, (_ev, filename) => {
|
|
674
|
-
if (!filename) return
|
|
675
|
-
const f = filename.toString()
|
|
676
|
-
if (path.resolve(repo, f) === planPath) {
|
|
677
|
-
clearTimeout(planDebounce)
|
|
678
|
-
planDebounce = setTimeout(() => {
|
|
679
|
-
planText = safeRead(planPath)
|
|
680
|
-
parsed = parsePlan(planText)
|
|
681
|
-
rebuildMatchers()
|
|
682
|
-
planMtime = statMtime(planPath)
|
|
683
|
-
broadcast()
|
|
684
|
-
}, 150)
|
|
685
|
-
}
|
|
686
|
-
})
|
|
687
|
-
console.error('note: recursive file watching unavailable here — plan updates still live, deep file activity limited')
|
|
688
|
-
} catch (e2) {
|
|
689
|
-
console.error('watcher failed:', e2.message)
|
|
690
|
-
}
|
|
691
|
-
}
|
|
802
|
+
refreshLanes()
|
|
803
|
+
reloadPlans()
|
|
804
|
+
setInterval(() => { if (refreshLanes()) { reloadPlans(); broadcast() } }, 30000).unref()
|
|
692
805
|
let lastActivityPush = 0
|
|
693
806
|
function throttleBroadcast() {
|
|
694
807
|
const now = Date.now()
|
|
@@ -702,7 +815,7 @@ function throttleBroadcast() {
|
|
|
702
815
|
let boards = [{ port, project: path.basename(repo), self: true, repoPath: repo }]
|
|
703
816
|
async function discoverBoards() {
|
|
704
817
|
const found = [{ port, project: session.project, self: true, repoPath: repo }]
|
|
705
|
-
await Promise.allSettled(
|
|
818
|
+
await Promise.allSettled(probePorts().filter(p => p !== port).map(async p => {
|
|
706
819
|
try {
|
|
707
820
|
const r = await fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(300) })
|
|
708
821
|
const j = await r.json()
|
|
@@ -770,20 +883,20 @@ const server = http.createServer((req, res) => {
|
|
|
770
883
|
// AOS patch: serve pipeline artifacts so a card's relative `url:` (e.g. an archify diagram) opens
|
|
771
884
|
let root
|
|
772
885
|
let f
|
|
773
|
-
try {
|
|
774
|
-
root = fs.realpathSync(path.join(repo, 'production_artifacts'))
|
|
775
|
-
f = fs.realpathSync(path.resolve(repo, '.' + decodeURIComponent(u.pathname)))
|
|
776
|
-
} catch {
|
|
777
|
-
res.writeHead(404).end()
|
|
778
|
-
return
|
|
779
|
-
}
|
|
780
886
|
let isFile = false
|
|
781
|
-
|
|
782
|
-
|
|
887
|
+
for (const l of laneList()) {
|
|
888
|
+
try {
|
|
889
|
+
root = fs.realpathSync(path.join(l.root, 'production_artifacts'))
|
|
890
|
+
f = fs.realpathSync(path.resolve(l.root, '.' + decodeURIComponent(u.pathname)))
|
|
891
|
+
isFile = fs.statSync(f).isFile() && isWithin(root, f)
|
|
892
|
+
} catch { isFile = false }
|
|
893
|
+
if (isFile) break
|
|
894
|
+
}
|
|
895
|
+
if (!isFile) { res.writeHead(404).end(); return }
|
|
783
896
|
const type = f.endsWith('.html') ? 'text/html' : f.endsWith('.svg') ? 'image/svg+xml' : f.endsWith('.json') ? 'application/json' : 'text/plain'
|
|
784
897
|
res.writeHead(200, { 'content-type': type + '; charset=utf-8' }).end(fs.readFileSync(f))
|
|
785
898
|
} else if (u.pathname === '/whoami') {
|
|
786
|
-
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ project: session.project, port, repoPath: repo }))
|
|
899
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ protocol: PROTOCOL, project: session.project, port, repoPath: repo, worktrees: laneList().map(l => ({ path: l.root, branch: l.branch, main: !l.key })) }))
|
|
787
900
|
} else if (u.pathname === '/board-lite') {
|
|
788
901
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(liteModel()))
|
|
789
902
|
} else if (u.pathname === '/world') {
|
|
@@ -810,18 +923,38 @@ const server = http.createServer((req, res) => {
|
|
|
810
923
|
} catch {}
|
|
811
924
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out.slice(0, 15)))
|
|
812
925
|
} else if (u.pathname === '/setup' && req.method === 'POST') {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
926
|
+
readCapped(req, res, body => {
|
|
927
|
+
const reply = o => res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(o))
|
|
928
|
+
let key = u.searchParams.get('lane')
|
|
929
|
+
try { const j = JSON.parse(body || '{}'); if (typeof j.lane === 'string') key = j.lane } catch {}
|
|
930
|
+
const lane = key === null ? (lanes.size > 1 ? null : mainLane()) : lanes.get(key)
|
|
931
|
+
if (!lane) { reply({ ok: false, error: key === null ? `this map has several lanes, so setup needs {"lane": "<key>"} (${laneList().map(l => l.key || '""=main').join(', ')}) to write into the right checkout` : 'unknown lane' }); return }
|
|
932
|
+
init(lane.root, lanePlan(lane) || path.join(lane.root, 'PLAN.md')).then(() => {
|
|
933
|
+
reloadPlans()
|
|
934
|
+
broadcast()
|
|
935
|
+
reply({ ok: true, prompt: BACKFILL_PROMPT })
|
|
936
|
+
}).catch(e => reply({ ok: false, error: String(e) }))
|
|
937
|
+
}, MAX_CONTROL_BODY_BYTES)
|
|
938
|
+
} else if (u.pathname === '/shutdown' && req.method === 'POST') {
|
|
939
|
+
// same CSRF guard as /answer, plus the caller must name this daemon's repo
|
|
940
|
+
const ct = req.headers['content-type']
|
|
941
|
+
const origin = req.headers['origin']
|
|
942
|
+
const ownOrigin = origin === `http://localhost:${port}` || origin === `http://127.0.0.1:${port}`
|
|
943
|
+
if (ct !== 'application/json' || (origin !== undefined && !ownOrigin)) { res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'forbidden' })); return }
|
|
944
|
+
readCapped(req, res, body => {
|
|
945
|
+
let named = null
|
|
946
|
+
try { named = JSON.parse(body || '{}').repoPath } catch {}
|
|
947
|
+
if (typeof named !== 'string' || norm(named) !== norm(repo)) { res.writeHead(409, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'repo mismatch' })); return }
|
|
948
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }), () => { stateDirty = true; saveState(); process.exit(0) })
|
|
949
|
+
}, MAX_CONTROL_BODY_BYTES)
|
|
818
950
|
} else if (u.pathname === '/setup-board' && req.method === 'POST') {
|
|
819
951
|
readCapped(req, res, async body => {
|
|
820
952
|
let out = { ok: false, error: 'no board there' }
|
|
821
953
|
try {
|
|
822
|
-
const
|
|
954
|
+
const j = JSON.parse(body || '{}')
|
|
955
|
+
const p = Number(j.port)
|
|
823
956
|
if (!Number.isInteger(p) || p < 1 || p > 65535) out = { ok: false, error: 'bad port' }
|
|
824
|
-
else out = await fetch(`http://127.0.0.1:${p}/setup`, { method: 'POST', signal: AbortSignal.timeout(5000) }).then(r => r.json())
|
|
957
|
+
else out = await fetch(`http://127.0.0.1:${p}/setup`, { method: 'POST', body: JSON.stringify(typeof j.lane === 'string' ? { lane: j.lane } : {}), signal: AbortSignal.timeout(5000) }).then(r => r.json())
|
|
825
958
|
} catch {}
|
|
826
959
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out))
|
|
827
960
|
}, MAX_CONTROL_BODY_BYTES)
|
|
@@ -833,16 +966,17 @@ const server = http.createServer((req, res) => {
|
|
|
833
966
|
if (!p) out = { ok: false, error: 'spawn target is outside the allowed workspace' }
|
|
834
967
|
else {
|
|
835
968
|
let already = null
|
|
969
|
+
const target = mainRoot(p)
|
|
836
970
|
for (const b of boards) {
|
|
837
971
|
try {
|
|
838
972
|
const w = await fetch(`http://127.0.0.1:${b.port}/whoami`, { signal: AbortSignal.timeout(300) }).then(r => r.json())
|
|
839
|
-
if (w.repoPath && realDirectory(w.repoPath) ===
|
|
973
|
+
if (w.repoPath && realDirectory(w.repoPath) === target) { already = b.port; break }
|
|
840
974
|
} catch {}
|
|
841
975
|
}
|
|
842
976
|
if (already) out = { ok: true, already }
|
|
843
|
-
else if (spawningPaths.has(
|
|
977
|
+
else if (spawningPaths.has(target)) out = { ok: true }
|
|
844
978
|
else {
|
|
845
|
-
spawningPaths.add(
|
|
979
|
+
spawningPaths.add(target); setTimeout(() => spawningPaths.delete(target), 30000)
|
|
846
980
|
const cp = await import('node:child_process')
|
|
847
981
|
const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), p, '--no-open'], { detached: true, stdio: 'ignore', shell: false })
|
|
848
982
|
child.unref()
|
|
@@ -858,8 +992,7 @@ const server = http.createServer((req, res) => {
|
|
|
858
992
|
const cwd = u.searchParams.get('cwd') || ''
|
|
859
993
|
const st = planStaleness()
|
|
860
994
|
const candidate = realDirectory(cwd)
|
|
861
|
-
const
|
|
862
|
-
const mine = !!(candidate && root && isWithin(root, candidate))
|
|
995
|
+
const mine = !!(candidate && laneAt(candidate))
|
|
863
996
|
res.writeHead(200, { 'content-type': 'text/plain' }).end(mine && st.stale
|
|
864
997
|
? `Note from agenttrail: PLAN.md in this repo is ${st.minutes} minutes behind the code. Re-verify task statuses against what you and other sessions actually changed, mark your current task [~], and keep files: globs current.`
|
|
865
998
|
: '')
|
|
@@ -987,7 +1120,7 @@ async function firstRunFlow() {
|
|
|
987
1120
|
console.log('\nno PLAN.md here yet — the live layer works now; the map needs a plan.')
|
|
988
1121
|
if (await askYesNo('set up the plan convention (PLAN.md skeleton + agent instructions + local hooks)? [Y/n] ')) {
|
|
989
1122
|
await init()
|
|
990
|
-
|
|
1123
|
+
reloadPlans()
|
|
991
1124
|
const copied = await copyToClipboard(BACKFILL_PROMPT)
|
|
992
1125
|
console.log(copied
|
|
993
1126
|
? 'backfill prompt is on your clipboard — paste it to your agent in this repo and the map draws itself.'
|
|
@@ -1014,8 +1147,8 @@ function onListen() {
|
|
|
1014
1147
|
}
|
|
1015
1148
|
async function bootDedup() {
|
|
1016
1149
|
const probes = []
|
|
1017
|
-
for (
|
|
1018
|
-
fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(400) }).then(r => r.json()).then(w => w.repoPath === repo ? p : null).catch(() => null))
|
|
1150
|
+
for (const p of probePorts()) probes.push(
|
|
1151
|
+
fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(400) }).then(r => r.json()).then(w => norm(w.repoPath) === norm(repo) && w.protocol >= PROTOCOL ? p : null).catch(() => null))
|
|
1019
1152
|
const hit = (await Promise.all(probes)).find(Boolean)
|
|
1020
1153
|
if (hit) { console.log(`agenttrail is already running for this repo · http://localhost:${hit}`); process.exit(0) }
|
|
1021
1154
|
listenWithFallback()
|
|
@@ -1023,10 +1156,10 @@ async function bootDedup() {
|
|
|
1023
1156
|
bootDedup()
|
|
1024
1157
|
|
|
1025
1158
|
// ---------- init ----------
|
|
1026
|
-
async function init() {
|
|
1027
|
-
fs.mkdirSync(
|
|
1028
|
-
if (!fs.existsSync(
|
|
1029
|
-
fs.writeFileSync(
|
|
1159
|
+
async function init(root = repo, plan = planPath) {
|
|
1160
|
+
fs.mkdirSync(path.join(root, '.agenttrail'), { recursive: true })
|
|
1161
|
+
if (!fs.existsSync(plan)) {
|
|
1162
|
+
fs.writeFileSync(plan, `# ${path.basename(root)}
|
|
1030
1163
|
|
|
1031
1164
|
## Set up the project {#setup}
|
|
1032
1165
|
tech: scaffolding
|
|
@@ -1041,25 +1174,25 @@ tech: scaffolding
|
|
|
1041
1174
|
// CLAUDE.md is read by Claude Code, AGENTS.md by Codex/Cursor and friends —
|
|
1042
1175
|
// the convention block goes in both so any agent maintains the same plan.
|
|
1043
1176
|
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
1044
|
-
const p = path.join(
|
|
1177
|
+
const p = path.join(root, name)
|
|
1045
1178
|
if (!safeRead(p).includes(marker)) {
|
|
1046
1179
|
fs.appendFileSync(p, snippet)
|
|
1047
1180
|
console.log(`appended agenttrail convention block to ${name}`)
|
|
1048
1181
|
}
|
|
1049
1182
|
}
|
|
1050
|
-
if (await askYesNo('wire Claude Code hooks into .claude/settings.local.json (gitignored, local only)? [Y/n] ')) installHooks()
|
|
1051
|
-
const gi = path.join(
|
|
1183
|
+
if (await askYesNo('wire Claude Code hooks into .claude/settings.local.json (gitignored, local only)? [Y/n] ')) installHooks(root)
|
|
1184
|
+
const gi = path.join(root, '.gitignore')
|
|
1052
1185
|
const giText = safeRead(gi)
|
|
1053
1186
|
if (!giText.includes('.agenttrail')) fs.appendFileSync(gi, (giText.endsWith('\n') || !giText ? '' : '\n') + '.agenttrail/\n')
|
|
1054
|
-
console.log('done — start the daemon with: agenttrail ' +
|
|
1187
|
+
console.log('done — start the daemon with: agenttrail ' + root)
|
|
1055
1188
|
console.log('\nnext: have your agent draw the real map. Paste this to Claude Code or Codex in this repo:\n')
|
|
1056
1189
|
console.log(' Read the "agenttrail plan convention" section in CLAUDE.md or AGENTS.md. Then study this repo in trust order — the code and directory layout first (what exists), git log for what is actually recent, decision logs and any in-progress build/handoff docs for what is in flight, and README/roadmap prose LAST and only for open intent (founding docs rot; cross-check their claims against git log) — and rewrite PLAN.md as the real map of this codebase: components with stable {#id}s, needs:/links: edges between them, files: globs for the paths each owns, and verb-led concrete titles with tech: sublines. Keep it to 5-9 components no matter how big the repo — a component is a part one agent could own for a session, with its own doneness and at least one edge; anything smaller is a task inside one. Verify every status against the CODE, not the docs — READMEs and roadmaps rot; mark [x] only after finding the implementing source or artifact, and cite that evidence on its tech: line (e.g. tech: TurnView.swift) so a human can audit every tick; if you cannot find evidence, leave it unchecked and say so in the decisions note. Statuses must be honest — [x] only for what verifiably exists, [~] only for what you are working on right now, everything else [ ]. Tag open tasks with an indented from: line naming provenance — from: agent when an in-progress build/handoff doc or your own declared intent claims it imminently, from: roadmap when sourced only from planning docs; omit when unsure. Record the backfill under ## decisions.')
|
|
1057
1190
|
}
|
|
1058
1191
|
|
|
1059
1192
|
// merge our hook relay into the repo's .claude/settings.json so Claude Code
|
|
1060
1193
|
// sessions stream tool calls + todos to the board. Additive and idempotent.
|
|
1061
|
-
function installHooks() {
|
|
1062
|
-
const dir = path.join(
|
|
1194
|
+
function installHooks(root = repo) {
|
|
1195
|
+
const dir = path.join(root, '.claude')
|
|
1063
1196
|
const sp = path.join(dir, 'settings.local.json')
|
|
1064
1197
|
let cfg = {}
|
|
1065
1198
|
try { cfg = JSON.parse(fs.readFileSync(sp, 'utf8')) } catch {}
|
|
@@ -1084,10 +1217,15 @@ async function upAll() {
|
|
|
1084
1217
|
let entries = []
|
|
1085
1218
|
try { entries = fs.readdirSync(dir).filter(f => f.endsWith('.json')) } catch {}
|
|
1086
1219
|
const known = []
|
|
1220
|
+
const seen = new Set()
|
|
1087
1221
|
for (const f of entries) {
|
|
1088
1222
|
try {
|
|
1089
1223
|
const st = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))
|
|
1090
|
-
if (st.repoPath
|
|
1224
|
+
if (!st.repoPath || !fs.existsSync(st.repoPath)) continue
|
|
1225
|
+
const main = mainRoot(st.repoPath) // state files of old per-worktree maps collapse into their repo's one map
|
|
1226
|
+
if (seen.has(main)) continue
|
|
1227
|
+
seen.add(main)
|
|
1228
|
+
known.push({ repo: main, port: st.port || 5330 })
|
|
1091
1229
|
} catch {}
|
|
1092
1230
|
}
|
|
1093
1231
|
if (!known.length) { console.log('no known repos yet — run agenttrail in a repo first'); return }
|