@hybridlabor-api/aos 4.7.0 → 4.7.2
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/.agents/agents.md +8 -3
- package/.agents/nodes.json +2 -1
- package/.agents/state.schema.json +24 -0
- package/.agents/vendor-manifest.json +6 -0
- package/.agents/workflows/startcycle-graph.md +2 -2
- package/.claude/agents/architect.md +5 -4
- package/.claude/agents/techlead.md +2 -0
- package/.claude/workflows/startcycle-dispatch.mjs +175 -17
- package/.opencode/agents/architect.md +5 -4
- package/.opencode/agents/techlead.md +2 -0
- package/THIRD_PARTY_NOTICES.md +9 -1
- package/bin/aos-doctor.mjs +386 -0
- package/bin/aos-store.mjs +171 -0
- package/docs/AUDIT_BRIEF_ECC_STORE.md +122 -0
- package/docs/PLAN_AOS_STORE_PHASE1.md +190 -0
- package/installer.js +68 -15
- package/lib/aos-archify-contract.mjs +277 -0
- package/lib/ecc-store-index.json +1 -0
- package/mcp_config.json +4 -0
- package/mcps/mcsc/packages/core/src/adapters/codex.js +160 -37
- package/package.json +7 -2
- package/scripts/build-ecc-store-index.mjs +147 -0
- package/skills/basic/startcycle-graph/SKILL.md +13 -0
- package/skills/global_config/agenttrail/bin/agenttrail.mjs +153 -42
- package/skills/global_config/agenttrail/public/index.html +5 -4
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +8 -2
- package/skills/global_config/archify/examples/dataflow-product-analytics.html +15048 -0
- package/skills/global_config/archify/examples/lifecycle-agent-run.html +14983 -0
- package/skills/global_config/archify/examples/sequence-cache-miss-request.html +15063 -0
- package/skills/global_config/archify/examples/web-app-rendered.html +15012 -0
- package/skills/global_config/archify/examples/workflow-agent-tool-call-rendered.html +15054 -0
- package/skills/global_config/ask-tim/SKILL.md +10 -0
- package/skills/global_config/mcsc/SKILL.md +60 -0
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +31 -6
- package/skills/global_config/plan-canvas/scripts/plan-canvas.js +4 -2
- package/docs/handover/agenttrail-for-ao.md +0 -58
- package/docs/handover/deja-for-ao.md +0 -51
|
@@ -20,6 +20,8 @@ const spawningPaths = new Set()
|
|
|
20
20
|
let openBrowser = process.stdout.isTTY ? true : false
|
|
21
21
|
let noOpen = false
|
|
22
22
|
let hooksOnly = false
|
|
23
|
+
const MAX_BODY_BYTES = 64 * 1024
|
|
24
|
+
const MAX_CONTROL_BODY_BYTES = 4 * 1024
|
|
23
25
|
let assumeYes = false
|
|
24
26
|
let removeFlag = false
|
|
25
27
|
let printFlag = false
|
|
@@ -57,6 +59,42 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
57
59
|
const planPath = planArg ? path.resolve(repo, planArg) : path.join(repo, 'PLAN.md')
|
|
58
60
|
const atDir = path.join(repo, '.agenttrail')
|
|
59
61
|
|
|
62
|
+
function realDirectory(value) {
|
|
63
|
+
if (typeof value !== 'string' || value.length === 0) return null
|
|
64
|
+
try {
|
|
65
|
+
const resolved = fs.realpathSync(path.resolve(value))
|
|
66
|
+
return fs.statSync(resolved).isDirectory() ? resolved : null
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isWithin(root, candidate) {
|
|
73
|
+
const relativePath = path.relative(root, candidate)
|
|
74
|
+
return relativePath === '' || (relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function allowedSpawnDirectory(value) {
|
|
78
|
+
const target = realDirectory(value)
|
|
79
|
+
if (!target) return null
|
|
80
|
+
const roots = [repo]
|
|
81
|
+
for (const board of Array.isArray(boards) ? boards : []) if (board && board.repoPath) roots.push(board.repoPath)
|
|
82
|
+
try {
|
|
83
|
+
for (const file of fs.readdirSync(path.join(os.homedir(), '.agenttrail'))) {
|
|
84
|
+
if (!file.endsWith('.json')) continue
|
|
85
|
+
try {
|
|
86
|
+
const state = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.agenttrail', file), 'utf8'))
|
|
87
|
+
if (state && typeof state.repoPath === 'string') roots.push(state.repoPath)
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
for (const root of roots) {
|
|
92
|
+
const realRoot = realDirectory(root)
|
|
93
|
+
if (realRoot && isWithin(realRoot, target)) return target
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
async function askYesNo(q) {
|
|
61
99
|
if (assumeYes || !process.stdin.isTTY) return true
|
|
62
100
|
const rl = (await import('node:readline')).createInterface({ input: process.stdin, output: process.stdout })
|
|
@@ -71,7 +109,7 @@ function copyToClipboard(text) {
|
|
|
71
109
|
return import('node:child_process').then(cp => new Promise(res => {
|
|
72
110
|
const cmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip'
|
|
73
111
|
const args = process.platform === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
74
|
-
const p = cp.spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] })
|
|
112
|
+
const p = cp.spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'], shell: false })
|
|
75
113
|
p.on('error', () => res(false)); p.on('close', () => res(true))
|
|
76
114
|
p.stdin.end(text)
|
|
77
115
|
})).catch(() => false)
|
|
@@ -87,8 +125,17 @@ if (cmd === 'ask') { process.exit(await askFlow()) }
|
|
|
87
125
|
// each daemon keeps only events whose cwd lives inside its repo. Never blocks
|
|
88
126
|
// the agent: 400ms cap, failures are silent.
|
|
89
127
|
async function relayHook() {
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
const chunks = []
|
|
129
|
+
let size = 0
|
|
130
|
+
try {
|
|
131
|
+
for await (const chunk of process.stdin) {
|
|
132
|
+
const buffer = Buffer.from(chunk)
|
|
133
|
+
size += buffer.length
|
|
134
|
+
if (size > MAX_BODY_BYTES) return
|
|
135
|
+
chunks.push(buffer)
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
92
139
|
if (!raw) return
|
|
93
140
|
// AOS patch: --agent <name> tags the hook event with the calling harness
|
|
94
141
|
let body = raw
|
|
@@ -291,6 +338,28 @@ function relToRepo(p) {
|
|
|
291
338
|
const r = path.resolve(String(p))
|
|
292
339
|
return r === repo ? '' : r.startsWith(repo + path.sep) ? r.slice(repo.length + 1) : null
|
|
293
340
|
}
|
|
341
|
+
// AOS patch: native plan/todo tools across harnesses, normalized to the
|
|
342
|
+
// {content, status} shape the frontend's run-todo rendering already expects.
|
|
343
|
+
// - Claude: `TodoWrite`, input {todos:[{content,status}]}
|
|
344
|
+
// - OpenCode: its real tool id is `todowrite` (verified live via `opencode
|
|
345
|
+
// run --format json`), but mcsc's opencode.js adapter title-cases the raw
|
|
346
|
+
// tool name before it reaches this hook (`part.tool.charAt(0).toUpperCase()
|
|
347
|
+
// + part.tool.slice(1)`), so it arrives here as `Todowrite`. Same input
|
|
348
|
+
// shape as Claude's, plus an extra `priority` field we ignore.
|
|
349
|
+
// - Codex: `update_plan`, input {explanation?, plan:[{step,status}]}. NOT
|
|
350
|
+
// verified live (Codex CLI's `--json` stream errored on auth in the
|
|
351
|
+
// environment this was built in, see report.md) — mapped on Codex's
|
|
352
|
+
// publicly documented tool contract; re-verify once Codex is authenticated.
|
|
353
|
+
function normalizeTodos(toolName, input) {
|
|
354
|
+
if (!input) return null
|
|
355
|
+
if ((toolName === 'TodoWrite' || toolName === 'Todowrite') && Array.isArray(input.todos)) {
|
|
356
|
+
return input.todos.map(t => ({ content: t.content, status: t.status }))
|
|
357
|
+
}
|
|
358
|
+
if (toolName === 'update_plan' && Array.isArray(input.plan)) {
|
|
359
|
+
return input.plan.map(t => ({ content: t.step, status: t.status }))
|
|
360
|
+
}
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
294
363
|
function handleHookEvent(ev) {
|
|
295
364
|
const cwd = ev.cwd || ''
|
|
296
365
|
if (!(cwd === repo || cwd.startsWith(repo + path.sep))) return false
|
|
@@ -327,9 +396,8 @@ function handleHookEvent(ev) {
|
|
|
327
396
|
run.recentTools.unshift({ name: ev.tool_name, detail: toolDetail(ev.tool_input), at: Date.now(), ms: Date.now() - started })
|
|
328
397
|
if (run.recentTools.length > 8) run.recentTools.length = 8
|
|
329
398
|
run.currentTool = null
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
399
|
+
const todos = normalizeTodos(ev.tool_name, ev.tool_input)
|
|
400
|
+
if (todos) run.todos = todos
|
|
333
401
|
const rel = relToRepo(ev.tool_input && (ev.tool_input.file_path || ev.tool_input.notebook_path))
|
|
334
402
|
if (rel) {
|
|
335
403
|
heatFile(rel, Date.now())
|
|
@@ -503,15 +571,39 @@ function planStaleness() {
|
|
|
503
571
|
const fresh = Date.now() - activity.at < 15 * 60e3
|
|
504
572
|
return gap > 20 * 60e3 && fresh ? { stale: true, minutes: Math.round(gap / 60e3) } : { stale: false }
|
|
505
573
|
}
|
|
574
|
+
// AOS patch: when no plan file exists yet, synthesize one synthetic
|
|
575
|
+
// component card per live run that has native todos (any harness — see
|
|
576
|
+
// normalizeTodos above) so the board renders instead of the empty
|
|
577
|
+
// "Waiting for the plan" state. A real plan file always wins outright: this
|
|
578
|
+
// only runs when planText is empty, and never mixes with parsed.nodes.
|
|
579
|
+
function syntheticPlanNodes() {
|
|
580
|
+
if (planText.length) return []
|
|
581
|
+
const out = []
|
|
582
|
+
for (const r of liveRuns()) {
|
|
583
|
+
if (!r.todos || !r.todos.length) continue
|
|
584
|
+
const id = 'live-' + r.id.replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 20)
|
|
585
|
+
const done = r.todos.filter(t => t.status === 'completed').length
|
|
586
|
+
const status = r.todos.some(t => t.status === 'in_progress') ? 'active' : done === r.todos.length ? 'done' : 'pending'
|
|
587
|
+
out.push({
|
|
588
|
+
id, title: `${r.agent}'s live todos`, level: 'component', parent: null,
|
|
589
|
+
needs: [], links: [], files: [], tech: '', by: r.agent, kind: '', url: '',
|
|
590
|
+
status, synthetic: true,
|
|
591
|
+
touchedAt: r.lastEventAt, recent: [], filesList: [],
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
return out
|
|
595
|
+
}
|
|
506
596
|
function model() {
|
|
507
597
|
if (treeDirty) { tree = buildTree(repo); treeDirty = false }
|
|
508
598
|
// AOS patch: include planFile (relative path) and aosPlan (boolean) for AOS UI customization
|
|
509
599
|
const planFile = planArg ? path.relative(repo, planPath).split(path.sep).join('/') : undefined
|
|
510
600
|
const aosPlan = Boolean(planArg)
|
|
601
|
+
const realPlanNodes = parsed.nodes.map(n => n.level === 'component' ? { ...n, touchedAt: compTouched[n.id] || null, recent: compRecent[n.id] || [], filesList: compFilesFor(n.id) } : n)
|
|
602
|
+
const planNodes = realPlanNodes.length ? realPlanNodes : syntheticPlanNodes()
|
|
511
603
|
return {
|
|
512
604
|
boards, port,
|
|
513
605
|
runs: liveRuns(),
|
|
514
|
-
session, plan:
|
|
606
|
+
session, plan: planNodes, tree,
|
|
515
607
|
planTitle: parsed.title,
|
|
516
608
|
hasPlan: planText.length > 0, treeTruncated,
|
|
517
609
|
activity, recentActivity, planMtime, handoffs, asks, hotFiles: hotFiles(), cycles, // AOS patch: asks ride along so the open map updates live
|
|
@@ -593,14 +685,14 @@ function throttleBroadcast() {
|
|
|
593
685
|
}
|
|
594
686
|
|
|
595
687
|
// ---------- sibling boards (multi-repo: one daemon per repo, boards link to each other) ----------
|
|
596
|
-
let boards = [{ port, project: path.basename(repo), self: true }]
|
|
688
|
+
let boards = [{ port, project: path.basename(repo), self: true, repoPath: repo }]
|
|
597
689
|
async function discoverBoards() {
|
|
598
|
-
const found = [{ port, project: session.project, self: true }]
|
|
690
|
+
const found = [{ port, project: session.project, self: true, repoPath: repo }]
|
|
599
691
|
await Promise.allSettled(Array.from({ length: 15 }, (_, i) => 5330 + i).filter(p => p !== port).map(async p => {
|
|
600
692
|
try {
|
|
601
693
|
const r = await fetch(`http://127.0.0.1:${p}/whoami`, { signal: AbortSignal.timeout(300) })
|
|
602
694
|
const j = await r.json()
|
|
603
|
-
if (j && j.project) found.push({ port: p, project: j.project, self: false })
|
|
695
|
+
if (j && j.project) found.push({ port: p, project: j.project, self: false, repoPath: j.repoPath })
|
|
604
696
|
} catch {}
|
|
605
697
|
}))
|
|
606
698
|
boards = found.sort((a, b) => a.port - b.port)
|
|
@@ -662,9 +754,18 @@ const server = http.createServer((req, res) => {
|
|
|
662
754
|
res.writeHead(200, { 'content-type': 'text/html' }).end(fs.readFileSync(indexPath, 'utf8'))
|
|
663
755
|
} else if (u.pathname.startsWith('/production_artifacts/')) {
|
|
664
756
|
// AOS patch: serve pipeline artifacts so a card's relative `url:` (e.g. an archify diagram) opens
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
757
|
+
let root
|
|
758
|
+
let f
|
|
759
|
+
try {
|
|
760
|
+
root = fs.realpathSync(path.join(repo, 'production_artifacts'))
|
|
761
|
+
f = fs.realpathSync(path.resolve(repo, '.' + decodeURIComponent(u.pathname)))
|
|
762
|
+
} catch {
|
|
763
|
+
res.writeHead(404).end()
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
let isFile = false
|
|
767
|
+
try { isFile = fs.statSync(f).isFile() } catch {}
|
|
768
|
+
if (!isWithin(root, f) || !isFile) { res.writeHead(404).end(); return }
|
|
668
769
|
const type = f.endsWith('.html') ? 'text/html' : f.endsWith('.svg') ? 'image/svg+xml' : f.endsWith('.json') ? 'application/json' : 'text/plain'
|
|
669
770
|
res.writeHead(200, { 'content-type': type + '; charset=utf-8' }).end(fs.readFileSync(f))
|
|
670
771
|
} else if (u.pathname === '/whoami') {
|
|
@@ -701,30 +802,27 @@ const server = http.createServer((req, res) => {
|
|
|
701
802
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, prompt: BACKFILL_PROMPT }))
|
|
702
803
|
}).catch(e => res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: String(e) })))
|
|
703
804
|
} else if (u.pathname === '/setup-board' && req.method === 'POST') {
|
|
704
|
-
|
|
705
|
-
req.on('data', c => body += c)
|
|
706
|
-
req.on('end', async () => {
|
|
805
|
+
readCapped(req, res, async body => {
|
|
707
806
|
let out = { ok: false, error: 'no board there' }
|
|
708
807
|
try {
|
|
709
|
-
const p = Number(JSON.parse(body).port)
|
|
710
|
-
|
|
808
|
+
const p = Number(JSON.parse(body || '{}').port)
|
|
809
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) out = { ok: false, error: 'bad port' }
|
|
810
|
+
else out = await fetch(`http://127.0.0.1:${p}/setup`, { method: 'POST', signal: AbortSignal.timeout(5000) }).then(r => r.json())
|
|
711
811
|
} catch {}
|
|
712
812
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out))
|
|
713
|
-
})
|
|
813
|
+
}, MAX_CONTROL_BODY_BYTES)
|
|
714
814
|
} else if (u.pathname === '/spawn' && req.method === 'POST') {
|
|
715
|
-
|
|
716
|
-
req.on('data', c => body += c)
|
|
717
|
-
req.on('end', async () => {
|
|
815
|
+
readCapped(req, res, async body => {
|
|
718
816
|
let out = { ok: false, error: 'bad request' }
|
|
719
817
|
try {
|
|
720
|
-
const p =
|
|
721
|
-
if (!
|
|
818
|
+
const p = allowedSpawnDirectory(JSON.parse(body || '{}').path)
|
|
819
|
+
if (!p) out = { ok: false, error: 'spawn target is outside the allowed workspace' }
|
|
722
820
|
else {
|
|
723
821
|
let already = null
|
|
724
822
|
for (const b of boards) {
|
|
725
823
|
try {
|
|
726
824
|
const w = await fetch(`http://127.0.0.1:${b.port}/whoami`, { signal: AbortSignal.timeout(300) }).then(r => r.json())
|
|
727
|
-
if (w.repoPath === p) { already = b.port; break }
|
|
825
|
+
if (w.repoPath && realDirectory(w.repoPath) === p) { already = b.port; break }
|
|
728
826
|
} catch {}
|
|
729
827
|
}
|
|
730
828
|
if (already) out = { ok: true, already }
|
|
@@ -732,7 +830,7 @@ const server = http.createServer((req, res) => {
|
|
|
732
830
|
else {
|
|
733
831
|
spawningPaths.add(p); setTimeout(() => spawningPaths.delete(p), 30000)
|
|
734
832
|
const cp = await import('node:child_process')
|
|
735
|
-
const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), p, '--no-open'], { detached: true, stdio: 'ignore' })
|
|
833
|
+
const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), p, '--no-open'], { detached: true, stdio: 'ignore', shell: false })
|
|
736
834
|
child.unref()
|
|
737
835
|
setTimeout(discoverBoards, 2500)
|
|
738
836
|
setTimeout(discoverBoards, 6000)
|
|
@@ -741,11 +839,13 @@ const server = http.createServer((req, res) => {
|
|
|
741
839
|
}
|
|
742
840
|
} catch {}
|
|
743
841
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(out))
|
|
744
|
-
})
|
|
842
|
+
}, MAX_CONTROL_BODY_BYTES)
|
|
745
843
|
} else if (u.pathname === '/nudge') {
|
|
746
844
|
const cwd = u.searchParams.get('cwd') || ''
|
|
747
845
|
const st = planStaleness()
|
|
748
|
-
const
|
|
846
|
+
const candidate = realDirectory(cwd)
|
|
847
|
+
const root = realDirectory(repo)
|
|
848
|
+
const mine = !!(candidate && root && isWithin(root, candidate))
|
|
749
849
|
res.writeHead(200, { 'content-type': 'text/plain' }).end(mine && st.stale
|
|
750
850
|
? `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.`
|
|
751
851
|
: '')
|
|
@@ -766,12 +866,10 @@ const server = http.createServer((req, res) => {
|
|
|
766
866
|
clients.add(res)
|
|
767
867
|
req.on('close', () => clients.delete(res))
|
|
768
868
|
} else if (u.pathname === '/hook' && req.method === 'POST') {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
req.on('end', () => {
|
|
772
|
-
try { if (handleHookEvent(JSON.parse(body))) hookTick() } catch {}
|
|
869
|
+
readCapped(req, res, body => {
|
|
870
|
+
try { if (handleHookEvent(JSON.parse(body || '{}'))) hookTick() } catch {}
|
|
773
871
|
res.writeHead(200).end()
|
|
774
|
-
})
|
|
872
|
+
}, MAX_CONTROL_BODY_BYTES)
|
|
775
873
|
} else if (u.pathname === '/ask' && req.method === 'POST') {
|
|
776
874
|
// AOS patch: ask engine — register a question the human answers on the map.
|
|
777
875
|
// Pure relay: an answer here never runs anything (go-gate keeps sole authority).
|
|
@@ -833,16 +931,29 @@ const server = http.createServer((req, res) => {
|
|
|
833
931
|
} else res.writeHead(404).end()
|
|
834
932
|
})
|
|
835
933
|
// AOS patch: body reader for the ask/answer routes, capped at 4 KB
|
|
836
|
-
function readCapped(req, res, cb) {
|
|
837
|
-
|
|
934
|
+
function readCapped(req, res, cb, maxBytes = MAX_CONTROL_BODY_BYTES) {
|
|
935
|
+
const chunks = []
|
|
936
|
+
let size = 0
|
|
838
937
|
let done = false
|
|
839
|
-
req.on('data',
|
|
938
|
+
req.on('data', chunk => {
|
|
939
|
+
if (done) return
|
|
940
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
|
|
941
|
+
size += buffer.length
|
|
942
|
+
if (size > maxBytes) {
|
|
943
|
+
done = true
|
|
944
|
+
chunks.length = 0
|
|
945
|
+
res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: false, error: 'body too large' }))
|
|
946
|
+
req.destroy()
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
chunks.push(buffer)
|
|
950
|
+
})
|
|
951
|
+
req.on('end', () => {
|
|
840
952
|
if (done) return
|
|
841
|
-
|
|
842
|
-
|
|
953
|
+
done = true
|
|
954
|
+
cb(Buffer.concat(chunks).toString('utf8'))
|
|
843
955
|
})
|
|
844
|
-
req.on('
|
|
845
|
-
req.on('error', () => {})
|
|
956
|
+
req.on('error', () => { done = true })
|
|
846
957
|
}
|
|
847
958
|
let lastHookTick = 0
|
|
848
959
|
function hookTick() {
|
|
@@ -883,7 +994,7 @@ function onListen() {
|
|
|
883
994
|
}
|
|
884
995
|
if (openBrowser && !noOpen) {
|
|
885
996
|
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
|
|
886
|
-
import('node:child_process').then(cp => cp.spawn(opener, [`http://localhost:${port}`], { stdio: 'ignore', detached: true }))
|
|
997
|
+
import('node:child_process').then(cp => cp.spawn(opener, [`http://localhost:${port}`], { stdio: 'ignore', detached: true, shell: false }))
|
|
887
998
|
}
|
|
888
999
|
firstRunFlow()
|
|
889
1000
|
}
|
|
@@ -970,7 +1081,7 @@ async function upAll() {
|
|
|
970
1081
|
for (const k of known) {
|
|
971
1082
|
const live = await fetch(`http://127.0.0.1:${k.port}/whoami`, { signal: AbortSignal.timeout(300) }).then(r => r.json()).catch(() => null)
|
|
972
1083
|
if (live) { console.log(`already up: ${k.repo} (:${k.port})`); continue }
|
|
973
|
-
const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), k.repo, '--port', String(k.port), '--no-open'], { detached: true, stdio: 'ignore' })
|
|
1084
|
+
const child = cp.spawn(process.execPath, [fileURLToPath(import.meta.url), k.repo, '--port', String(k.port), '--no-open'], { detached: true, stdio: 'ignore', shell: false })
|
|
974
1085
|
child.unref()
|
|
975
1086
|
console.log(`started: ${k.repo} (:${k.port})`)
|
|
976
1087
|
}
|
|
@@ -318,7 +318,8 @@ function renderZero(){
|
|
|
318
318
|
${emptyStateBlock}
|
|
319
319
|
</div>`
|
|
320
320
|
}
|
|
321
|
-
function isSkeletonPlan(){return M.plan.filter(n=>n.level==='task').length<=1}
|
|
321
|
+
function isSkeletonPlan(){return M.plan.filter(n=>n.level==='task').length<=1&&!hasSyntheticPlan(M)}
|
|
322
|
+
function hasSyntheticPlan(b){return(b.plan||[]).some(n=>n.synthetic)}
|
|
322
323
|
function boardLayout(b){
|
|
323
324
|
const phases=b.plan.filter(n=>n.level==='component')
|
|
324
325
|
const byId=Object.fromEntries(phases.map(p=>[p.id,p])),depth={},visiting=new Set()
|
|
@@ -357,7 +358,7 @@ function regionCardHtml(b,L){
|
|
|
357
358
|
const live=(b.runs||[]).filter(r=>!r.ended)
|
|
358
359
|
const sq=b.plan.filter(n=>n.level==='component').map((c,i)=>`<rect x="${40+i*30}" y="${h/2+4}" width="20" height="20" rx="5" fill="${c.status==='done'?'#3f7f57':c.status==='blocked'?'var(--danger)':'none'}" stroke="${c.status==='active'?'var(--accent)':'var(--faint)'}" stroke-width="2.5"/>`).join('')
|
|
359
360
|
const marks=live.map((r,i)=>{const ic=agentIcon(r.agent);return ic?`<g transform="translate(${40+i*34} ${h/2+44}) scale(1.1)"><path fill="${agentColor(r.agent)}" d="${ic}"/></g>`:''}).join('')
|
|
360
|
-
return`<text x="40" y="${h/2-36}" fill="var(--text)" font-size="44" font-weight="650" font-family="var(--font)" letter-spacing="-1">${esc(b.project)}</text>${sq}${marks}${!b.hasPlan?`<text x="40" y="${h/2+44}" fill="var(--faint)" font-size="16" font-family="var(--font)">nothing mapped yet</text><g class="setup-btn" onclick="setupBoard(${b.port})" style="cursor:pointer"><rect x="40" y="${h/2+64}" width="172" height="40" rx="11" fill="var(--accent-bg)" stroke="var(--accent)" stroke-width="1.2"/><text x="126" y="${h/2+89}" text-anchor="middle" fill="var(--accent)" font-size="15.5" font-weight="600" font-family="var(--font)">Set up the map</text></g>`:''}${live.length?`<text x="${w-40}" y="${h/2-36}" text-anchor="end" fill="var(--dim)" font-size="16" font-family="var(--font)">${live.length} running</text>`:''}`
|
|
361
|
+
return`<text x="40" y="${h/2-36}" fill="var(--text)" font-size="44" font-weight="650" font-family="var(--font)" letter-spacing="-1">${esc(b.project)}</text>${sq}${marks}${(!b.hasPlan&&!hasSyntheticPlan(b))?`<text x="40" y="${h/2+44}" fill="var(--faint)" font-size="16" font-family="var(--font)">nothing mapped yet</text><g class="setup-btn" onclick="setupBoard(${b.port})" style="cursor:pointer"><rect x="40" y="${h/2+64}" width="172" height="40" rx="11" fill="var(--accent-bg)" stroke="var(--accent)" stroke-width="1.2"/><text x="126" y="${h/2+89}" text-anchor="middle" fill="var(--accent)" font-size="15.5" font-weight="600" font-family="var(--font)">Set up the map</text></g>`:(!b.hasPlan&&hasSyntheticPlan(b))?`<text x="40" y="${h/2+44}" fill="var(--faint)" font-size="16" font-family="var(--font)">live from todos, no plan file yet</text>`:''}${live.length?`<text x="${w-40}" y="${h/2-36}" text-anchor="end" fill="var(--dim)" font-size="16" font-family="var(--font)">${live.length} running</text>`:''}`
|
|
361
362
|
}
|
|
362
363
|
const CARD_LOD=0.3
|
|
363
364
|
let _saved=null
|
|
@@ -366,7 +367,7 @@ function restoreBoardGlobals(){if(_saved){M=_saved.M;BP=_saved.BP;_saved=null}}
|
|
|
366
367
|
function renderGraph(){
|
|
367
368
|
const boards=boardsAll()
|
|
368
369
|
if(!boards.length)return
|
|
369
|
-
if(boards.length===1&&M&&(!M.hasPlan||isSkeletonPlan())){renderZero();return}
|
|
370
|
+
if(boards.length===1&&M&&((!M.hasPlan&&!hasSyntheticPlan(M))||isSkeletonPlan())){renderZero();return}
|
|
370
371
|
regions={};graphPos={}
|
|
371
372
|
const GAP=160,ROWMAX=3600;let cx=80,cy=80,rowH=0
|
|
372
373
|
let content=''
|
|
@@ -381,7 +382,7 @@ function renderGraph(){
|
|
|
381
382
|
for(const[id,q]of Object.entries(L.pos))graphPos[BP+':'+id]={...q,nodeX:q.nodeX+ox,nodeY:q.nodeY+56+oy}
|
|
382
383
|
const hot=eff.activity&&Date.now()-eff.activity.at<60000
|
|
383
384
|
const cardMode=graphZoom<CARD_LOD||!eff.hasPlan
|
|
384
|
-
content+=`<g transform="translate(${ox} ${oy})"><rect width="${L.width}" height="${L.height+56}" rx="26" fill="var(--region)" stroke="${hot?'rgba(233,162,59,.5)':'var(--line)'}" stroke-width="1.5"/><text x="28" y="38" fill="var(--faint)" font-size="15" font-weight="600" font-family="var(--mono)" letter-spacing=".08em">${esc((eff.project||'').toUpperCase())}${eff.planStale&&eff.planStale.stale?`<tspan fill="var(--accent)"> · PLAN BEHIND</tspan>`:''}</text>${cardMode?regionCardHtml(eff,L):`<g transform="translate(0 56)">${boardInnerHtml(eff,L)}</g>`}</g>`
|
|
385
|
+
content+=`<g transform="translate(${ox} ${oy})"><rect width="${L.width}" height="${L.height+56}" rx="26" fill="var(--region)" stroke="${hot?'rgba(233,162,59,.5)':'var(--line)'}" stroke-width="1.5"/><text x="28" y="38" fill="var(--faint)" font-size="15" font-weight="600" font-family="var(--mono)" letter-spacing=".08em">${esc((eff.project||'').toUpperCase())}${eff.planStale&&eff.planStale.stale?`<tspan fill="var(--accent)"> · PLAN BEHIND</tspan>`:''}${hasSyntheticPlan(eff)?`<tspan fill="var(--dim)"> · LIVE TODOS, NO PLAN FILE</tspan>`:''}</text>${cardMode?regionCardHtml(eff,L):`<g transform="translate(0 56)">${boardInnerHtml(eff,L)}</g>`}</g>`
|
|
385
386
|
cx+=L.width+GAP;rowH=Math.max(rowH,L.height+56)
|
|
386
387
|
}
|
|
387
388
|
M=realM;BP=realBP
|
|
@@ -52,7 +52,7 @@ const tilde = (p) => (p || '').replace(HOME, '~');
|
|
|
52
52
|
// `which` does not exist on native Windows; cmd ships `where`, which prints one
|
|
53
53
|
// path per line.
|
|
54
54
|
const which = (bin) => {
|
|
55
|
-
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
55
|
+
const probe = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
56
56
|
try { return tilde(execFileSync(probe, [bin], { encoding: 'utf8' }).trim().split(/\r?\n/)[0]); } catch { return null; }
|
|
57
57
|
};
|
|
58
58
|
const dirCount = (p) => { try { return readdirSync(p, { withFileTypes: true }).filter(d => d.isDirectory()).length; } catch { return 0; } };
|
|
@@ -100,7 +100,13 @@ function checkAos() {
|
|
|
100
100
|
|
|
101
101
|
if (manifest && NET) {
|
|
102
102
|
try {
|
|
103
|
-
const
|
|
103
|
+
const npmBin = IS_WIN ? 'npm.cmd' : 'npm';
|
|
104
|
+
const latest = execFileSync(npmBin, ['view', '@hybridlabor-api/aos', 'version'], {
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
timeout: 8000,
|
|
107
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
108
|
+
shell: IS_WIN
|
|
109
|
+
}).trim();
|
|
104
110
|
add('aos', 'version vs npm', manifest.version === latest, `local v${manifest.version} · npm v${latest}`,
|
|
105
111
|
'npx -y @hybridlabor-api/aos@latest');
|
|
106
112
|
} catch {
|