sneakoscope 0.6.66 → 0.6.69

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/README.md CHANGED
@@ -6,18 +6,21 @@ Sneakoscope Codex (`sks`, displayed as `ㅅㅋㅅ`) is a Codex CLI/App harness f
6
6
 
7
7
  ## Quick Start
8
8
 
9
- Install globally, initialize the current project, then open the cmux runtime:
9
+ Install globally, then run `sks` from either a project or any global shell location:
10
10
 
11
11
  ```sh
12
12
  npm i -g sneakoscope
13
+ sks root
13
14
  sks bootstrap
14
15
  sks
15
16
  ```
16
17
 
18
+ `sks root` tells you whether SKS found a project root or is using the per-user global runtime root. Outside a repo/project marker, runtime commands such as `sks`, `sks deps check`, `sks pipeline status`, and `sks team ...` use that global root instead of writing `.sneakoscope` into the random current directory.
19
+
17
20
  If you only want a one-shot run without keeping `sks` installed globally:
18
21
 
19
22
  ```sh
20
- npx -y -p sneakoscope sks bootstrap
23
+ npx -y -p sneakoscope sks root
21
24
  ```
22
25
 
23
26
  For a repo-local install:
@@ -89,10 +92,11 @@ Use this when you want `sks` available from any repo:
89
92
 
90
93
  ```sh
91
94
  npm i -g sneakoscope
95
+ sks root
92
96
  sks bootstrap
93
97
  ```
94
98
 
95
- `sks bootstrap` initializes the current project, installs Codex App skills/hooks/config, checks Context7/Codex App/cmux readiness, and prints a ready status.
99
+ `sks` commands work even when no project root is present. Project-aware commands use the nearest `.sneakoscope`, `.dcodex`, or `.git` root; if none exists, SKS uses a per-user global runtime root. `sks bootstrap` still initializes the current project when you want project-local hooks, skills, and TriWiki state.
96
100
 
97
101
  ### One-Shot Install
98
102
 
@@ -185,11 +189,12 @@ Answer `y` to install `sneakoscope@latest`, then rerun `sks --mad`. Answer `n` t
185
189
  ```sh
186
190
  sks team "implement this feature" executor:3 reviewer:1
187
191
  sks team watch latest
192
+ sks team lane latest --agent analysis_scout_1 --follow
188
193
  sks team status latest
189
194
  sks team log latest
190
195
  ```
191
196
 
192
- Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, and opens cmux live lanes when cmux is available.
197
+ Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, and opens cmux live lanes when cmux is available. `sks team lane` renders one agent's status, assigned runtime tasks, recent events, and a fallback global tail for multi-pane monitoring.
193
198
 
194
199
  ### QA, Ralph, Research, DB, Wiki, GX
195
200
 
@@ -374,6 +379,7 @@ OMX/DCodex conflicts intentionally block setup/doctor until the user approves cl
374
379
  ```sh
375
380
  sks pipeline status --json
376
381
  sks team watch latest
382
+ sks team lane latest --agent parent_orchestrator --follow
377
383
  sks wiki validate .sneakoscope/wiki/context-pack.json
378
384
  ```
379
385
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.66",
4
+ "version": "0.6.69",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Ralph, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
- import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION } from '../core/fsx.mjs';
6
+ import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot } from '../core/fsx.mjs';
7
7
  import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
8
8
  import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
9
9
  import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
@@ -28,7 +28,7 @@ import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/w
28
28
  import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
29
29
  import { context7Evidence, evaluateStop, recordContext7Evidence, recordSubagentEvidence } from '../core/pipeline.mjs';
30
30
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
31
- import { appendTeamEvent, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail } from '../core/team-live.mjs';
31
+ import { appendTeamEvent, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
32
32
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
33
33
  import { CMUX_BREW_COMMAND, CMUX_BREW_UPGRADE_COMMAND, buildCmuxLaunchPlan, defaultCmuxWorkspaceName, ensureCmuxInstalled, formatCmuxBanner, launchCmuxTeamView, launchCmuxUi, platformCmuxInstallHint, runCmuxStatus, sanitizeCmuxWorkspaceName, cmuxAvailable } from '../core/cmux-ui.mjs';
34
34
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
@@ -63,6 +63,7 @@ export async function main(args) {
63
63
  if (cmd === 'help') return help(tail);
64
64
  if (cmd === 'commands') return commands(tail);
65
65
  if (cmd === 'usage') return usage(tail);
66
+ if (cmd === 'root') return rootCommand(tail);
66
67
  if (cmd === 'quickstart') return quickstart();
67
68
  if (cmd === 'codex-app') return codexAppHelp(tail);
68
69
  if (cmd === 'bootstrap') return bootstrap(tail);
@@ -112,6 +113,7 @@ Usage:
112
113
  sks wizard
113
114
  sks commands [--json]
114
115
  sks usage [${USAGE_TOPICS}]
116
+ sks root [--json]
115
117
  sks quickstart
116
118
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
117
119
  sks deps check|install [cmux|codex|context7|all] [--yes] [--json]
@@ -145,7 +147,7 @@ Usage:
145
147
  sks ralph run <mission-id|latest> [--mock] [--max-cycles N]
146
148
  sks ralph status <mission-id|latest>
147
149
  sks team "task" [executor:5 reviewer:2 user:1] [--json]
148
- sks team log|tail|watch|status [mission-id|latest]
150
+ sks team log|tail|watch|lane|status [mission-id|latest]
149
151
  sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
150
152
  sks research prepare "topic" [--depth frontier]
151
153
  sks research run <mission-id|latest> [--mock] [--max-cycles N]
@@ -504,6 +506,27 @@ function commands(args = []) {
504
506
  console.log(`\nCanonical Codex App picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}`);
505
507
  }
506
508
 
509
+ async function rootCommand(args = []) {
510
+ const project = await findProjectRoot();
511
+ const global = globalSksRoot();
512
+ const active = await sksRoot();
513
+ const result = {
514
+ cwd: process.cwd(),
515
+ mode: project ? 'project' : 'global',
516
+ active_root: active,
517
+ project_root: project,
518
+ global_root: global,
519
+ using_global_root: !project
520
+ };
521
+ if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
522
+ console.log('SKS Root\n');
523
+ console.log(`Mode: ${result.mode}`);
524
+ console.log(`Active root: ${active}`);
525
+ console.log(`Project: ${project || 'none'}`);
526
+ console.log(`Global root: ${global}`);
527
+ if (!project) console.log('\nNo project marker was found here, so SKS will use the per-user global runtime root. Run `sks bootstrap` to initialize the current directory as a project.');
528
+ }
529
+
507
530
  function dollarCommands(args = []) {
508
531
  if (flag(args, '--json')) return console.log(JSON.stringify({ dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES }, null, 2));
509
532
  console.log('ㅅㅋㅅ $ Commands\n');
@@ -549,8 +572,9 @@ Rules:
549
572
  }
550
573
 
551
574
  async function context7(sub = 'check', args = []) {
552
- const root = await projectRoot();
553
575
  const action = sub || 'check';
576
+ const setupScope = action === 'setup' ? readOption(args, '--scope', flag(args, '--global') ? 'global' : 'project') : null;
577
+ const root = action === 'setup' && setupScope === 'project' ? await projectRoot() : await sksRoot();
554
578
  if (action === 'check') {
555
579
  const result = await checkContext7(root);
556
580
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
@@ -636,7 +660,7 @@ async function context7(sub = 'check', args = []) {
636
660
  return;
637
661
  }
638
662
  if (action === 'setup') {
639
- const scope = readOption(args, '--scope', flag(args, '--global') ? 'global' : 'project');
663
+ const scope = setupScope;
640
664
  const transport = readOption(args, '--transport', flag(args, '--remote') ? 'remote' : 'local');
641
665
  if (!['project', 'global'].includes(scope)) throw new Error('Invalid Context7 scope. Use project or global.');
642
666
  if (!['local', 'remote'].includes(transport)) throw new Error('Invalid Context7 transport. Use local or remote.');
@@ -673,7 +697,7 @@ function printContext7DocsResult(result, opts = {}) {
673
697
  }
674
698
 
675
699
  async function pipeline(sub = 'status', args = []) {
676
- const root = await projectRoot();
700
+ const root = await sksRoot();
677
701
  const action = sub || 'status';
678
702
  if (action === 'answer') return pipelineAnswer(root, args);
679
703
  const state = await readJson(stateFile(root), {});
@@ -1022,7 +1046,7 @@ async function cmuxCommand(sub = 'start', args = []) {
1022
1046
  return;
1023
1047
  }
1024
1048
  if (action === 'check') {
1025
- const root = await projectRoot();
1049
+ const root = await sksRoot();
1026
1050
  const plan = await buildCmuxLaunchPlan({ root, session: readOption(args, '--session', null) });
1027
1051
  if (flag(args, '--json')) return console.log(JSON.stringify(plan, null, 2));
1028
1052
  console.log(formatCmuxBanner(plan.app));
@@ -1111,14 +1135,14 @@ async function ensureMadLaunchDependencies(args = []) {
1111
1135
  const cmux = await cmuxAvailable().catch(() => ({ ok: false }));
1112
1136
  if (!cmux.ok && !cmux.bin) actions.push(await installCmuxDependency(args));
1113
1137
  }
1114
- const status = await depsStatus(await projectRoot());
1138
+ const status = await depsStatus(await sksRoot());
1115
1139
  return { ready: Boolean(status.codex_cli.ok && (status.cmux.ok || status.cmux.bin)), actions, status };
1116
1140
  }
1117
1141
 
1118
1142
  async function deps(sub = 'check', args = []) {
1119
1143
  const action = sub || 'check';
1120
1144
  if (action === 'check' || action === 'status') {
1121
- const root = await projectRoot();
1145
+ const root = await sksRoot();
1122
1146
  const status = await depsStatus(root);
1123
1147
  if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
1124
1148
  printDepsStatus(status);
@@ -1131,7 +1155,7 @@ async function deps(sub = 'check', args = []) {
1131
1155
  }
1132
1156
 
1133
1157
  async function depsStatus(root = null, opts = {}) {
1134
- root ||= await projectRoot();
1158
+ root ||= await sksRoot();
1135
1159
  const npmBin = await which('npm').catch(() => null);
1136
1160
  const codex = opts.codex || await getCodexInfo().catch(() => ({}));
1137
1161
  const app = opts.codexApp || await codexAppIntegrationStatus({ codex });
@@ -1195,7 +1219,7 @@ function printDepsStatus(status) {
1195
1219
  }
1196
1220
 
1197
1221
  async function depsInstall(args = []) {
1198
- const root = await projectRoot();
1222
+ const root = await sksRoot();
1199
1223
  const target = positionalArgs(args)[0] || 'all';
1200
1224
  const wants = target === 'all' ? ['codex', 'context7', 'cmux'] : [target];
1201
1225
  const actions = [];
@@ -1308,9 +1332,15 @@ function quickstart() {
1308
1332
 
1309
1333
  First install and bootstrap this project:
1310
1334
  npm i -g sneakoscope
1335
+ sks root
1311
1336
  sks bootstrap
1312
1337
  sks
1313
1338
 
1339
+ Use outside a project:
1340
+ sks root
1341
+ sks deps check
1342
+ sks team "global mission"
1343
+
1314
1344
  If cmux is missing:
1315
1345
  sks deps install cmux
1316
1346
 
@@ -1411,12 +1441,13 @@ Examples:
1411
1441
  function usage(args = []) {
1412
1442
  const topic = String(args[0] || 'overview').toLowerCase();
1413
1443
  const blocks = {
1414
- overview: ['ㅅㅋㅅ Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks cmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1415
- install: ['Install', '', ' npm i -g sneakoscope', ' sks bootstrap', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks bootstrap', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1444
+ overview: ['ㅅㅋㅅ Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks cmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1445
+ install: ['Install', '', ' npm i -g sneakoscope', ' sks root', ' sks', '', 'Project bootstrap:', ' sks bootstrap', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1416
1446
  bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and cmux.'],
1447
+ root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
1417
1448
  deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [cmux|codex|context7|all] [--yes]', '', 'cmux on macOS uses Homebrew only after approval.'],
1418
1449
  cmux: ['cmux', '', ' sks', ' sks cmux check', ' sks cmux status --once', ' sks deps install cmux'],
1419
- team: ['Team', '', ' sks team "task" executor:5 reviewer:2 user:1', ' sks team watch latest', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1450
+ team: ['Team', '', ' sks team "task" executor:5 reviewer:2 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1420
1451
  'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
1421
1452
  ralph: ['Ralph', '', ' sks ralph prepare "task"', ' sks ralph answer <MISSION_ID> answers.json', ' sks ralph run <MISSION_ID> --max-cycles 8'],
1422
1453
  'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
@@ -1608,7 +1639,7 @@ async function fixPath(args) {
1608
1639
  }
1609
1640
 
1610
1641
  async function doctor(args) {
1611
- const root = await projectRoot();
1642
+ const root = await sksRoot();
1612
1643
  const requestedScope = args.includes('--install-scope') || flag(args, '--project') || flag(args, '--global')
1613
1644
  ? installScopeFromArgs(args)
1614
1645
  : null;
@@ -1731,7 +1762,7 @@ async function checkRequiredSkills(root, skillRoot = path.join(root, '.agents',
1731
1762
  }
1732
1763
 
1733
1764
  async function codexAppSkillReadiness(root = null) {
1734
- root ||= await projectRoot();
1765
+ root ||= await sksRoot();
1735
1766
  const project = await checkRequiredSkills(root);
1736
1767
  const global = await checkRequiredSkills(null, globalCodexSkillsRoot());
1737
1768
  return { ok: project.ok || global.ok, project, global };
@@ -1892,7 +1923,7 @@ function qaRoute() {
1892
1923
  }
1893
1924
 
1894
1925
  async function qaLoopPrepare(args) {
1895
- const root = await projectRoot();
1926
+ const root = await sksRoot();
1896
1927
  if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
1897
1928
  const prompt = promptOf(args);
1898
1929
  if (!prompt) throw new Error('Missing QA target prompt.');
@@ -1912,7 +1943,7 @@ async function qaLoopPrepare(args) {
1912
1943
  }
1913
1944
 
1914
1945
  async function qaLoopAnswer(args) {
1915
- const root = await projectRoot();
1946
+ const root = await sksRoot();
1916
1947
  const [missionArg, answerFile] = args;
1917
1948
  const id = await resolveMissionId(root, missionArg);
1918
1949
  if (!id || !answerFile) throw new Error('Usage: sks qa-loop answer <mission-id|latest> <answers.json>');
@@ -1937,7 +1968,7 @@ async function qaLoopAnswer(args) {
1937
1968
  }
1938
1969
 
1939
1970
  async function qaLoopRun(args) {
1940
- const root = await projectRoot();
1971
+ const root = await sksRoot();
1941
1972
  const id = await resolveMissionId(root, args[0]);
1942
1973
  if (!id) throw new Error('Usage: sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]');
1943
1974
  const { dir, mission } = await loadMission(root, id);
@@ -2002,7 +2033,7 @@ async function qaLoopRun(args) {
2002
2033
  }
2003
2034
 
2004
2035
  async function qaLoopStatus(args) {
2005
- const root = await projectRoot();
2036
+ const root = await sksRoot();
2006
2037
  const id = await resolveMissionId(root, args[0]);
2007
2038
  if (!id) throw new Error('Usage: sks qa-loop status <mission-id|latest>');
2008
2039
  const { dir, mission } = await loadMission(root, id);
@@ -2019,7 +2050,7 @@ async function qaLoopStatus(args) {
2019
2050
  }
2020
2051
 
2021
2052
  async function researchPrepare(args) {
2022
- const root = await projectRoot();
2053
+ const root = await sksRoot();
2023
2054
  if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
2024
2055
  const prompt = positionalArgs(args).join(' ').trim();
2025
2056
  if (!prompt) throw new Error('Missing research topic.');
@@ -2033,7 +2064,7 @@ async function researchPrepare(args) {
2033
2064
  }
2034
2065
 
2035
2066
  async function researchRun(args) {
2036
- const root = await projectRoot();
2067
+ const root = await sksRoot();
2037
2068
  const id = await resolveMissionId(root, args[0]);
2038
2069
  if (!id) throw new Error('Usage: sks research run <mission-id|latest> [--mock] [--max-cycles N]');
2039
2070
  const { dir, mission } = await loadMission(root, id);
@@ -2095,7 +2126,7 @@ async function researchRun(args) {
2095
2126
  }
2096
2127
 
2097
2128
  async function researchStatus(args) {
2098
- const root = await projectRoot();
2129
+ const root = await sksRoot();
2099
2130
  const id = await resolveMissionId(root, args[0]);
2100
2131
  if (!id) throw new Error('Usage: sks research status <mission-id|latest>');
2101
2132
  const { dir, mission } = await loadMission(root, id);
@@ -2106,7 +2137,7 @@ async function researchStatus(args) {
2106
2137
  }
2107
2138
 
2108
2139
  async function ralphPrepare(args) {
2109
- const root = await projectRoot();
2140
+ const root = await sksRoot();
2110
2141
  if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
2111
2142
  const prompt = promptOf(args);
2112
2143
  if (!prompt) throw new Error('Missing task prompt.');
@@ -2124,7 +2155,7 @@ async function ralphPrepare(args) {
2124
2155
  }
2125
2156
 
2126
2157
  async function ralphAnswer(args) {
2127
- const root = await projectRoot();
2158
+ const root = await sksRoot();
2128
2159
  const [missionArg, answerFile] = args;
2129
2160
  const id = await resolveMissionId(root, missionArg);
2130
2161
  if (!id || !answerFile) throw new Error('Usage: sks ralph answer <mission-id|latest> <answers.json>');
@@ -2145,7 +2176,7 @@ async function ralphAnswer(args) {
2145
2176
  }
2146
2177
 
2147
2178
  async function ralphRun(args) {
2148
- const root = await projectRoot();
2179
+ const root = await sksRoot();
2149
2180
  const id = await resolveMissionId(root, args[0]);
2150
2181
  if (!id) throw new Error('Usage: sks ralph run <mission-id|latest> [--mock]');
2151
2182
  const { dir, mission } = await loadMission(root, id);
@@ -2229,7 +2260,7 @@ async function ralphRunMock(root, id, dir) {
2229
2260
  }
2230
2261
 
2231
2262
  async function ralphStatus(args) {
2232
- const root = await projectRoot();
2263
+ const root = await sksRoot();
2233
2264
  const id = await resolveMissionId(root, args[0]);
2234
2265
  if (!id) throw new Error('Usage: sks ralph status <mission-id|latest>');
2235
2266
  const { dir, mission } = await loadMission(root, id);
@@ -2321,6 +2352,18 @@ async function selftest() {
2321
2352
  const depsCheck = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'deps', 'check', '--json'], { cwd: bootstrapJsonTmp, env: { HOME: path.join(bootstrapJsonTmp, 'home') }, timeoutMs: 20000, maxOutputBytes: 256 * 1024 });
2322
2353
  const depsResult = JSON.parse(depsCheck.stdout);
2323
2354
  if (!depsResult.node?.ok || !('cmux' in depsResult) || !('homebrew' in depsResult)) throw new Error('selftest failed: deps check json missing expected fields');
2355
+ const globalCwd = tmpdir();
2356
+ const globalRuntimeRoot = path.join(tmpdir(), 'sks-global-root');
2357
+ const globalRootProbe = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'root', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2358
+ const globalRootResult = JSON.parse(globalRootProbe.stdout);
2359
+ if (globalRootResult.mode !== 'global' || globalRootResult.active_root !== globalRuntimeRoot || globalRootResult.project_root !== null) throw new Error('selftest failed: global root probe did not use SKS_GLOBAL_ROOT outside a project');
2360
+ const globalPipeline = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'pipeline', 'status', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2361
+ const globalPipelineResult = JSON.parse(globalPipeline.stdout);
2362
+ if (globalPipelineResult.root !== globalRuntimeRoot) throw new Error('selftest failed: pipeline status did not use global runtime root outside a project');
2363
+ const globalTeam = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'team', 'global path smoke', '--json'], { cwd: globalCwd, env: { SKS_GLOBAL_ROOT: globalRuntimeRoot }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2364
+ const globalTeamResult = JSON.parse(globalTeam.stdout);
2365
+ if (!String(globalTeamResult.mission_dir || '').startsWith(path.join(globalRuntimeRoot, '.sneakoscope', 'missions')) || !(await exists(path.join(globalRuntimeRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest failed: team mission did not materialize under global runtime root');
2366
+ if (await exists(path.join(globalCwd, '.sneakoscope'))) throw new Error('selftest failed: global runtime command polluted the caller cwd with .sneakoscope');
2324
2367
  const madProfilePath = path.join(tmp, 'mad-codex-config.toml');
2325
2368
  const madProfile = await enableMadHighProfile({ configPath: madProfilePath });
2326
2369
  const madProfileText = await safeReadText(madProfilePath);
@@ -2464,6 +2507,8 @@ async function selftest() {
2464
2507
  if (!promptPipelineText.includes('From-Chat-IMG') || !promptPipelineText.includes('Do not assume ordinary image prompts are chat captures')) throw new Error('selftest failed: prompt pipeline missing explicit From-Chat-IMG gating');
2465
2508
  const fromChatImgSkillText = await safeReadText(path.join(tmp, '.agents', 'skills', 'from-chat-img', 'SKILL.md'));
2466
2509
  if (!fromChatImgSkillText.includes('normal Team pipeline') || !fromChatImgSkillText.includes('Computer Use/browser visual inspection') || !fromChatImgSkillText.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT) || !fromChatImgSkillText.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT) || !fromChatImgSkillText.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT)) throw new Error('selftest failed: from-chat-img skill missing Team/browser inspection checklist guidance');
2510
+ const fromChatImgSkillMeta = await safeReadText(path.join(tmp, '.agents', 'skills', 'from-chat-img', 'agents', 'openai.yaml'));
2511
+ if (!fromChatImgSkillMeta.includes('model_reasoning_effort: xhigh')) throw new Error('selftest failed: from-chat-img skill metadata is not xhigh');
2467
2512
  for (const supportSkill of ['reasoning-router', 'pipeline-runner', 'context7-docs', 'seo-geo-optimizer', 'reflection', 'design-system-builder', 'design-ui-editor', 'imagegen']) {
2468
2513
  if (!(await exists(path.join(tmp, '.agents', 'skills', supportSkill, 'SKILL.md')))) throw new Error(`selftest failed: ${supportSkill} skill not installed`);
2469
2514
  }
@@ -2498,7 +2543,7 @@ async function selftest() {
2498
2543
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Team')) throw new Error('selftest failed: dollar-commands missing Team default routing guidance');
2499
2544
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$From-Chat-IMG')) throw new Error('selftest failed: dollar-commands missing From-Chat-IMG guidance');
2500
2545
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$MAD-SKS')) throw new Error('selftest failed: dollar-commands missing MAD-SKS scoped override guidance');
2501
- if (!COMMAND_CATALOG.some((c) => c.name === 'context7') || !COMMAND_CATALOG.some((c) => c.name === 'pipeline') || !COMMAND_CATALOG.some((c) => c.name === 'qa-loop')) throw new Error('selftest failed: context7/pipeline/qa-loop commands missing from catalog');
2546
+ if (!COMMAND_CATALOG.some((c) => c.name === 'context7') || !COMMAND_CATALOG.some((c) => c.name === 'pipeline') || !COMMAND_CATALOG.some((c) => c.name === 'qa-loop') || !COMMAND_CATALOG.some((c) => c.name === 'root')) throw new Error('selftest failed: context7/pipeline/qa-loop/root commands missing from catalog');
2502
2547
  const registryDollarCommands = DOLLAR_COMMANDS.map((c) => c.command);
2503
2548
  const manifest = await readJson(path.join(tmp, '.sneakoscope', 'manifest.json'));
2504
2549
  const policy = await readJson(path.join(tmp, '.sneakoscope', 'policy.json'));
@@ -2967,8 +3012,9 @@ async function selftest() {
2967
3012
  if (roleTeamPlan.roster.development_team.filter((agent) => agent.role === 'executor').length !== 5) throw new Error('selftest failed: executor role count not reflected in development team');
2968
3013
  if (!roleTeamPlan.roster.debate_team.some((agent) => /inconvenience/.test(agent.persona))) throw new Error('selftest failed: user friction persona missing from debate team');
2969
3014
  const cmuxTeam = await launchCmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
2970
- if (!cmuxTeam.agents?.length || !cmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !cmuxTeam.agents.every((entry) => String(entry.command || '').includes('team watch'))) throw new Error('selftest failed: Team cmux view did not expose agent live lanes');
3015
+ if (!cmuxTeam.agents?.length || !cmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !cmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team cmux view did not expose agent live lanes');
2971
3016
  if (routeReasoning(routePrompt('$Research frontier idea'), '$Research frontier idea').effort !== 'xhigh') throw new Error('selftest failed: research reasoning not xhigh');
3017
+ if (routeReasoning(routePrompt('$From-Chat-IMG 채팅 이미지 작업'), '$From-Chat-IMG 채팅 이미지 작업').effort !== 'xhigh') throw new Error('selftest failed: From-Chat-IMG reasoning not xhigh');
2972
3018
  if (routeReasoning(routePrompt('$DB migration'), '$DB migration').effort !== 'high') throw new Error('selftest failed: logical reasoning not high');
2973
3019
  if (routeReasoning(routePrompt('$DFix button label'), '$DFix button label').effort !== 'medium') throw new Error('selftest failed: simple reasoning not medium');
2974
3020
  if (routePrompt('이 파이프라인은 왜 이렇게 동작해?')?.id !== 'Answer') throw new Error('selftest failed: question prompt did not route to Answer');
@@ -3004,6 +3050,10 @@ async function selftest() {
3004
3050
  if (!teamLive.includes('selftest mapped options')) throw new Error('selftest failed: team live transcript missing event');
3005
3051
  if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest failed: team live transcript missing TriWiki context tracking');
3006
3052
  if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
3053
+ const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
3054
+ if (!teamLane.includes('SKS Team Agent Lane') || !teamLane.includes('analysis_scout_1') || !teamLane.includes('selftest mapped repo slice')) throw new Error('selftest failed: team agent lane missing agent event context');
3055
+ const teamLaneCli = await runProcess(process.execPath, [hookBin, 'team', 'lane', teamId, '--agent', 'analysis_scout_1', '--lines', '4'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
3056
+ if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest failed: sks team lane CLI did not render an agent lane');
3007
3057
  await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
3008
3058
  const installUxSchema = buildQuestionSchema('SKS first install/bootstrap UX and Context7 MCP setup improvement');
3009
3059
  const installUxSlotIds = installUxSchema.slots.map((s) => s.id);
@@ -3118,7 +3168,7 @@ async function selftest() {
3118
3168
  }
3119
3169
 
3120
3170
  async function profile(sub, args) {
3121
- const root = await projectRoot();
3171
+ const root = await sksRoot();
3122
3172
  if (sub === 'show') return console.log(JSON.stringify(await readJson(path.join(root, '.sneakoscope', 'model', 'current.json'), { model: 'gpt-5.5' }), null, 2));
3123
3173
  if (sub === 'set') { await writeJsonAtomic(path.join(root, '.sneakoscope', 'model', 'current.json'), { model: args[0] || 'gpt-5.5', set_at: nowIso() }); return console.log(`Model profile set: ${args[0] || 'gpt-5.5'}`); }
3124
3174
  console.error('Usage: sks profile show|set <model>');
@@ -3126,7 +3176,7 @@ async function profile(sub, args) {
3126
3176
 
3127
3177
  async function hproof(sub, args) {
3128
3178
  if (sub !== 'check') return console.error('Usage: sks hproof check [mission-id]');
3129
- const root = await projectRoot();
3179
+ const root = await sksRoot();
3130
3180
  const id = await resolveMissionId(root, args[0]);
3131
3181
  if (!id) throw new Error('No mission found.');
3132
3182
  console.log(JSON.stringify(await evaluateDoneGate(root, id), null, 2));
@@ -3138,7 +3188,7 @@ async function evalCommand(sub, args) {
3138
3188
  return;
3139
3189
  }
3140
3190
  if (sub === 'thresholds') return console.log(JSON.stringify(DEFAULT_EVAL_THRESHOLDS, null, 2));
3141
- const root = await projectRoot();
3191
+ const root = await sksRoot();
3142
3192
  if (sub === 'run') {
3143
3193
  const iterations = Number(readFlagValue(args, '--iterations', 200));
3144
3194
  const report = runEvaluationBenchmark({ iterations });
@@ -3176,14 +3226,14 @@ async function wiki(sub, args = []) {
3176
3226
  return;
3177
3227
  }
3178
3228
  if (sub === 'pack') {
3179
- const root = await projectRoot();
3229
+ const root = await sksRoot();
3180
3230
  const { pack, file } = await writeWikiContextPack(root, args);
3181
3231
  if (flag(args, '--json')) return console.log(JSON.stringify({ ...pack, path: file }, null, 2));
3182
3232
  printWikiPackSummary(root, file, pack);
3183
3233
  return;
3184
3234
  }
3185
3235
  if (sub === 'refresh') {
3186
- const root = await projectRoot();
3236
+ const root = await sksRoot();
3187
3237
  const dryRun = flag(args, '--dry-run');
3188
3238
  const { pack, file } = await writeWikiContextPack(root, args, { dryRun });
3189
3239
  const validation = wikiValidationResult(pack);
@@ -3220,7 +3270,7 @@ async function wiki(sub, args = []) {
3220
3270
  return;
3221
3271
  }
3222
3272
  if (sub === 'prune') {
3223
- const root = await projectRoot();
3273
+ const root = await sksRoot();
3224
3274
  const pruneResult = await pruneWikiArtifacts(root, { dryRun: flag(args, '--dry-run') });
3225
3275
  if (flag(args, '--json')) {
3226
3276
  return console.log(JSON.stringify({
@@ -3237,7 +3287,7 @@ async function wiki(sub, args = []) {
3237
3287
  return;
3238
3288
  }
3239
3289
  if (sub === 'validate') {
3240
- const root = await projectRoot();
3290
+ const root = await sksRoot();
3241
3291
  const target = positionalArgs(args)[0] || path.join(root, '.sneakoscope', 'wiki', 'context-pack.json');
3242
3292
  const pack = await readJson(path.resolve(target));
3243
3293
  const { result, trustAnchors } = wikiValidationResult(pack);
@@ -3721,7 +3771,7 @@ function printEvalCompare(report, saved) {
3721
3771
  async function memory(sub, args) { return gc(args || []); }
3722
3772
 
3723
3773
  async function gc(args) {
3724
- const root = await projectRoot();
3774
+ const root = await sksRoot();
3725
3775
  const res = await enforceRetention(root, { dryRun: flag(args, '--dry-run') });
3726
3776
  if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
3727
3777
  console.log(flag(args, '--dry-run') ? 'ㅅㅋㅅ GC dry run' : 'ㅅㅋㅅ GC completed');
@@ -3731,7 +3781,7 @@ async function gc(args) {
3731
3781
  }
3732
3782
 
3733
3783
  async function stats(args) {
3734
- const root = await projectRoot();
3784
+ const root = await sksRoot();
3735
3785
  const report = await storageReport(root);
3736
3786
  const pkgBytes = await dirSize(packageRoot()).catch(() => 0);
3737
3787
  const out = { package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }, storage: report };
@@ -3744,7 +3794,7 @@ async function stats(args) {
3744
3794
 
3745
3795
  function positionalArgs(args = []) {
3746
3796
  const out = [];
3747
- const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors']);
3797
+ const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines']);
3748
3798
  for (let i = 0; i < args.length; i++) {
3749
3799
  const arg = String(args[i]);
3750
3800
  if (valueFlags.has(arg)) {
@@ -3806,7 +3856,7 @@ function defaultBeta(name) {
3806
3856
  }
3807
3857
 
3808
3858
  async function gx(sub, args) {
3809
- const root = await projectRoot();
3859
+ const root = await sksRoot();
3810
3860
  const name = cartridgeName(args);
3811
3861
  const dir = cartridgeDir(root, name);
3812
3862
  if (sub === 'init') {
@@ -3854,18 +3904,19 @@ async function gx(sub, args) {
3854
3904
  }
3855
3905
 
3856
3906
  async function team(args) {
3857
- const teamSubcommands = new Set(['log', 'tail', 'watch', 'status', 'event']);
3907
+ const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'event']);
3858
3908
  if (teamSubcommands.has(args[0])) return teamCommand(args[0], args.slice(1));
3859
3909
  const opts = parseTeamCreateArgs(args);
3860
3910
  const { prompt, agentSessions, roleCounts, roster } = opts;
3861
3911
  if (!prompt) {
3862
3912
  console.error('Usage: sks team "task" [executor:5 reviewer:2 user:1] [--agents N] [--json]');
3863
- console.error(' sks team log|tail|watch|status [mission-id|latest]');
3913
+ console.error(' sks team log|tail|watch|lane|status [mission-id|latest]');
3864
3914
  console.error(' sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."');
3865
3915
  process.exitCode = 1;
3866
3916
  return;
3867
3917
  }
3868
- const root = await projectRoot();
3918
+ const root = await sksRoot();
3919
+ if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
3869
3920
  const { id, dir } = await createMission(root, { mode: 'team', prompt });
3870
3921
  const schema = buildQuestionSchema(prompt);
3871
3922
  await writeQuestions(dir, schema);
@@ -4061,6 +4112,7 @@ function buildTeamPlan(id, prompt, opts = {}) {
4061
4112
  'sks team log <mission-id>',
4062
4113
  'sks team tail <mission-id>',
4063
4114
  'sks team watch <mission-id>',
4115
+ 'sks team lane <mission-id> --agent <name> --follow',
4064
4116
  'sks team event <mission-id> --agent <name> --phase <phase> --message "..."'
4065
4117
  ]
4066
4118
  },
@@ -4135,7 +4187,7 @@ ${plan.roster.validation_team.map((agent) => `- ${agent.id}: ${agent.persona}`).
4135
4187
  - Keep team-live.md readable for the user inside Codex App.
4136
4188
  - Mirror every useful subagent status, debate result, handoff, review finding, and integration decision to team-transcript.jsonl.
4137
4189
  - Use \`sks team event ${plan.mission_id} --agent <name> --phase <phase> --message "..."\` when recording a live event from the parent thread.
4138
- - The user can inspect the flow with \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, or \`sks team watch ${plan.mission_id}\`.
4190
+ - The user can inspect the flow with \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, \`sks team watch ${plan.mission_id}\`, or \`sks team lane ${plan.mission_id} --agent analysis_scout_1 --follow\`.
4139
4191
 
4140
4192
  ## Phases
4141
4193
 
@@ -4148,7 +4200,7 @@ ${plan.invariants.map((x) => `- ${x}`).join('\n')}
4148
4200
  }
4149
4201
 
4150
4202
  async function teamCommand(sub, args) {
4151
- const root = await projectRoot();
4203
+ const root = await sksRoot();
4152
4204
  const missionArg = args[0] && !String(args[0]).startsWith('--') ? args[0] : 'latest';
4153
4205
  const id = await resolveMissionId(root, missionArg);
4154
4206
  if (!id) {
@@ -4191,6 +4243,35 @@ async function teamCommand(sub, args) {
4191
4243
  return;
4192
4244
  }
4193
4245
  if (sub === 'log') return console.log(await readTeamLive(dir));
4246
+ if (sub === 'lane') {
4247
+ const agent = readFlagValue(args, '--agent', 'parent_orchestrator');
4248
+ const phase = readFlagValue(args, '--phase', '');
4249
+ const lines = Number(readFlagValue(args, '--lines', '12'));
4250
+ const printLane = async () => {
4251
+ const text = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
4252
+ if (flag(args, '--json')) {
4253
+ console.log(JSON.stringify({ mission_id: id, agent, phase, lane: text }, null, 2));
4254
+ } else {
4255
+ if (flag(args, '--follow') && process.stdout.isTTY) console.clear();
4256
+ console.log(text);
4257
+ }
4258
+ return text;
4259
+ };
4260
+ let last = await printLane();
4261
+ if (flag(args, '--follow')) {
4262
+ for (;;) {
4263
+ await new Promise((resolve) => setTimeout(resolve, 2000));
4264
+ const next = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
4265
+ if (next !== last) {
4266
+ if (process.stdout.isTTY) console.clear();
4267
+ else console.log('\n--- team lane update ---\n');
4268
+ console.log(next);
4269
+ last = next;
4270
+ }
4271
+ }
4272
+ }
4273
+ return;
4274
+ }
4194
4275
  if (sub === 'tail' || sub === 'watch') {
4195
4276
  const lines = readFlagValue(args, '--lines', '20');
4196
4277
  const printTail = async () => {
@@ -4213,7 +4294,7 @@ async function teamCommand(sub, args) {
4213
4294
  }
4214
4295
 
4215
4296
  async function db(sub, args) {
4216
- const root = await projectRoot();
4297
+ const root = await sksRoot();
4217
4298
  if (sub === 'policy') {
4218
4299
  console.log(JSON.stringify(await loadDbSafetyPolicy(root), null, 2));
4219
4300
  return;
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { exists, packageRoot, projectRoot, runProcess, sha256, which } from './fsx.mjs';
4
+ import { exists, packageRoot, runProcess, sha256, sksRoot, which } from './fsx.mjs';
5
5
  import { getCodexInfo } from './codex-adapter.mjs';
6
6
  import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
7
7
 
@@ -137,12 +137,12 @@ export function teamAgentCommand(root, missionId, agentId, phase) {
137
137
  return [
138
138
  `printf '%s\\n' ${shellEscape(`${SKS_CMUX_LOGO}\n\nTeam mission: ${missionId}\nAgent: ${agentId}\nPhase: ${phase}\n`)}`,
139
139
  `cd ${shellEscape(root)}`,
140
- `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 12`
140
+ `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
141
141
  ].join('; ');
142
142
  }
143
143
 
144
144
  export async function buildCmuxLaunchPlan(opts = {}) {
145
- const root = path.resolve(opts.root || await projectRoot());
145
+ const root = path.resolve(opts.root || await sksRoot());
146
146
  const workspace = sanitizeCmuxWorkspaceName(opts.workspace || opts.session || defaultCmuxWorkspaceName(root));
147
147
  const sksBin = opts.sksBin || path.join(packageRoot(), 'bin', 'sks.mjs');
148
148
  const codex = opts.codex || await getCodexInfo().catch(() => ({}));
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.6.66';
8
+ export const PACKAGE_VERSION = '0.6.69';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -145,7 +145,7 @@ export async function findUp(start, names) {
145
145
  }
146
146
  }
147
147
 
148
- export async function projectRoot(start = process.cwd()) {
148
+ export async function findProjectRoot(start = process.cwd()) {
149
149
  const resolved = path.resolve(start);
150
150
  const sine = await findUp(resolved, ['.sneakoscope', '.dcodex']);
151
151
  if (sine) {
@@ -157,6 +157,22 @@ export async function projectRoot(start = process.cwd()) {
157
157
  const root = path.dirname(git);
158
158
  if (root !== path.parse(root).root) return root;
159
159
  }
160
+ return null;
161
+ }
162
+
163
+ export function globalSksRoot() {
164
+ if (process.env.SKS_GLOBAL_ROOT) return path.resolve(process.env.SKS_GLOBAL_ROOT);
165
+ return path.join(process.env.HOME || os.homedir(), '.sneakoscope-global');
166
+ }
167
+
168
+ export async function sksRoot(start = process.cwd()) {
169
+ return await findProjectRoot(start) || globalSksRoot();
170
+ }
171
+
172
+ export async function projectRoot(start = process.cwd()) {
173
+ const resolved = path.resolve(start);
174
+ const root = await findProjectRoot(resolved);
175
+ if (root) return root;
160
176
  return resolved;
161
177
  }
162
178
 
package/src/core/init.mjs CHANGED
@@ -474,8 +474,10 @@ function codexAppQuickReference(scope, commandPrefix) {
474
474
  `Picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}.`,
475
475
  'Routing: Answer direct, DFix ultralight, execution routes ask only scope/safety/behavior/acceptance-changing questions before sealing answers.',
476
476
  `Full routes write reflection.md, record lessons to ${REFLECTION_MEMORY_PATH}, refresh/pack TriWiki, validate, then final-answer with a user-visible completion summary plus Honest Mode.`,
477
+ `Runtime root: ${commandPrefix} root shows whether SKS is using the nearest project root or the per-user global SKS runtime root; outside any project marker, runtime commands use the global root instead of writing .sneakoscope into the current random directory.`,
477
478
  `Context Tracking: TriWiki SSOT. Before each route phase read only the latest coordinate+voxel overlay pack at .sneakoscope/wiki/context-pack.json; coordinate-only legacy packs are invalid. Use attention.use_first for compact high-trust recall and hydrate attention.hydrate_first from source before risky/lower-trust decisions. During every stage hydrate low-trust claims from source/hash/RGBA anchors; after changes run ${commandPrefix} wiki refresh or pack; before handoff/final run ${commandPrefix} wiki validate .sneakoscope/wiki/context-pack.json.`,
478
479
  stackCurrentDocsPolicyText(commandPrefix),
480
+ `Team lanes: ${commandPrefix} team lane latest --agent analysis_scout_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, and fallback global tail.`,
479
481
  `Runtime: open Codex App once, then run ${commandPrefix} bootstrap, ${commandPrefix} deps check, or ${commandPrefix} deps install cmux.`,
480
482
  `Guard: generated harness files are immutable outside the engine source repo; check ${commandPrefix} guard check; conflicts use ${commandPrefix} conflicts prompt with human approval.`
481
483
  ].join('\n') + '\n';
@@ -498,7 +500,7 @@ export async function installSkills(root) {
498
500
  'gx': `---\nname: gx\ndescription: Dollar-command route for $GX or $gx deterministic GX visual context cartridges.\n---\n\nUse when the user invokes $GX/$gx or asks for architecture/context visualization through SKS. Prefer sks gx init, render, validate, drift, and snapshot. vgraph.json remains the source of truth.\n`,
499
501
  'help': `---\nname: help\ndescription: Dollar-command route for $Help or $help explaining installed SKS commands and workflows.\n---\n\nUse when the user invokes $Help/$help or asks what commands exist. Prefer concise output from sks commands, sks usage <topic>, sks quickstart, sks aliases, and sks codex-app.\n`,
500
502
  'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Ask only scope/safety/behavior/acceptance-changing questions; otherwise seal inferred answers. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode.\n\n${chatCaptureIntakeText()}\n\nDesign: read design.md; if missing use design-system-builder; use imagegen for image/logo/raster. TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
501
- 'reasoning-router': `---\nname: reasoning-router\ndescription: Temporary SKS reasoning-effort routing for every command and pipeline route.\n---\n\nmedium: simple copy/color/discovery/setup/mechanical edits. high: logic, safety, architecture, DB, orchestration, refactor, multi-file work. xhigh: research, AutoResearch, falsification, benchmarks, SEO/GEO, open-ended discovery. Routing is temporary; return to default after the gate. Inspect with sks reasoning and sks pipeline status.\n`,
503
+ 'reasoning-router': `---\nname: reasoning-router\ndescription: Temporary SKS reasoning-effort routing for every command and pipeline route.\n---\n\nmedium: simple copy/color/discovery/setup/mechanical edits. high: logic, safety, architecture, DB, orchestration, refactor, multi-file work. xhigh: research, AutoResearch, falsification, benchmarks, SEO/GEO, open-ended discovery, and From-Chat-IMG image work-order analysis. Routing is temporary; return to default after the gate. Inspect with sks reasoning and sks pipeline status.\n`,
502
504
  'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS dollar-command routes as stateful pipelines with mission artifacts, route gates, Context7 evidence, temporary reasoning routing, reflection, and Honest Mode.\n---\n\nEvery $ command is a route. Use current.json and mission artifacts, temporary reasoning, TriWiki before stages, source hydration, Context7 when required, Team cleanup before reflection, reflection for full routes, and completion summary plus Honest Mode before final. Surface guard/scopes, record evidence, refresh/pack/validate TriWiki, and check sks pipeline status/resume.\n`,
503
505
  'context7-docs': `---\nname: context7-docs\ndescription: Enforce Context7 MCP documentation evidence for SKS routes that depend on external libraries, frameworks, APIs, MCPs, package managers, DB SDKs, or generated docs.\n---\n\nWhen required, resolve-library-id, then query-docs for the resolved id. Legacy get-library-docs evidence is accepted. Prefer sks context7 tools/resolve/docs/evidence and finish only after both evidence stages exist. Check setup with sks context7 check.\n`,
504
506
  'seo-geo-optimizer': `---\nname: seo-geo-optimizer\ndescription: SEO/GEO support for README, npm, GitHub, keywords, snippets, schema, and AI-search visibility.\n---\n\nUse for SEO/GEO, package metadata, README ranking, snippets, schema, and AI search. Optimize README, package.json, docs, badges, topics, quickstart, examples, command discovery, exact names, keywords, and AI Answer Snapshot. Do not invent metrics; use $AutoResearch unless it is a tiny copy edit.\n`,
@@ -552,7 +554,7 @@ Context tracking:
552
554
  }
553
555
 
554
556
  async function writeSkillMetadata(dir, name) {
555
- const effort = ['research', 'autoresearch', 'research-discovery', 'autoresearch-loop'].includes(name)
557
+ const effort = ['research', 'autoresearch', 'research-discovery', 'autoresearch-loop', 'from-chat-img'].includes(name)
556
558
  ? 'xhigh'
557
559
  : (['dfix', 'sks', 'help'].includes(name) ? 'medium' : 'high');
558
560
  await ensureDir(path.join(dir, 'agents'));
@@ -305,14 +305,14 @@ async function prepareTeam(root, route, task, required) {
305
305
  transcript: 'team-transcript.jsonl',
306
306
  dashboard: 'team-dashboard.json',
307
307
  cmux: 'CLI Team entrypoints open cmux live lanes for the visible Team agent budget when cmux is available.',
308
- commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team event latest --agent <name> --phase <phase> --message "..."']
308
+ commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow', 'sks team event latest --agent <name> --phase <phase> --message "..."']
309
309
  },
310
310
  required_artifacts: ['team-roster.json', 'team-analysis.md', ...(fromChatImgRequired ? [FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT] : []), 'team-consensus.md', ...teamRuntimeRequiredArtifacts(), 'team-review.md', 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'reflection.md', 'reflection-gate.json', 'team-live.md', 'team-transcript.jsonl', 'team-dashboard.json', '.sneakoscope/wiki/context-pack.json', 'context7-evidence.jsonl']
311
311
  };
312
312
  await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
313
313
  await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: roleCounts, agent_sessions: agentSessions, bundle_size: roster.bundle_size, roster, confirmed: true, source: 'default_or_prompt_team_spec' });
314
314
  const contextTracking = triwikiContextTracking();
315
- await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Review, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
315
+ await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Review, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team lane ${id} --agent analysis_scout_1 --follow\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
316
316
  await initTeamLive(id, dir, cleanTask, { agentSessions, roleCounts, roster });
317
317
  const runtime = await writeTeamRuntimeArtifacts(dir, plan, {});
318
318
  await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
@@ -4,7 +4,7 @@ export const FROM_CHAT_IMG_CHECKLIST_ARTIFACT = 'from-chat-img-checklist.md';
4
4
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT = 'from-chat-img-temp-triwiki.json';
5
5
  export const FROM_CHAT_IMG_QA_LOOP_ARTIFACT = 'from-chat-img-qa-loop.json';
6
6
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS = 5;
7
- export const USAGE_TOPICS = 'install|setup|bootstrap|deps|cmux|auto-review|team|qa-loop|ralph|research|db|codex-app|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|hproof|gx|wiki';
7
+ export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|cmux|auto-review|team|qa-loop|ralph|research|db|codex-app|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|hproof|gx|wiki';
8
8
 
9
9
  export const RECOMMENDED_MCP_SERVERS = [
10
10
  {
@@ -331,6 +331,7 @@ export const COMMAND_CATALOG = [
331
331
  { name: 'usage', usage: `sks usage [${USAGE_TOPICS}]`, description: 'Print copy-ready workflows for common tasks.' },
332
332
  { name: 'quickstart', usage: 'sks quickstart', description: 'Show the shortest safe setup and verification flow.' },
333
333
  { name: 'bootstrap', usage: 'sks bootstrap [--install-scope global|project] [--local-only] [--json]', description: 'Initialize the current project, install SKS Codex App files/skills, check Context7/Codex App/cmux, and print ready true/false.' },
334
+ { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
334
335
  { name: 'deps', usage: 'sks deps check|install [cmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, cmux, and Homebrew on macOS.' },
335
336
  { name: 'codex-app', usage: 'sks codex-app [check|open]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files and examples.' },
336
337
  { name: 'cmux', usage: 'sks cmux [check|status] [--workspace name]', description: 'Open the SKS cmux runtime with the ㅅㅋㅅ ASCII status pane and Codex CLI.' },
@@ -356,7 +357,7 @@ export const COMMAND_CATALOG = [
356
357
  { name: 'eval', usage: 'sks eval run|compare|thresholds ...', description: 'Run deterministic context-quality and performance evidence checks.' },
357
358
  { name: 'wiki', usage: 'sks wiki coords|pack|refresh|prune|validate ...', description: 'Build, refresh, prune, and validate RGBA/trig LLM Wiki context packs with attention.use_first and attention.hydrate_first for compact recall plus source hydration.' },
358
359
  { name: 'hproof', usage: 'sks hproof check [mission-id|latest]', description: 'Evaluate the H-Proof done gate for a mission.' },
359
- { name: 'team', usage: 'sks team "task" [executor:5 reviewer:2 user:1]|log|tail|watch|status|event ...', description: 'Create and observe a scout-first Team mission: parallel analysis, TriWiki attention, role debate, runtime graph/inbox handoff, then executor parallel development.' },
360
+ { name: 'team', usage: 'sks team "task" [executor:5 reviewer:2 user:1]|log|tail|watch|lane|status|event ...', description: 'Create and observe a scout-first Team mission: parallel analysis, TriWiki attention, role debate, runtime graph/inbox handoff, then executor parallel development.' },
360
361
  { name: 'reasoning', usage: 'sks reasoning ["prompt"] [--json]', description: 'Show SKS temporary reasoning-effort routing: medium for simple tasks, high for logic, xhigh for research.' },
361
362
  { name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
362
363
  { name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
@@ -527,6 +528,7 @@ export function subagentExecutionPolicyText(route, prompt = '') {
527
528
  export function routeReasoning(route, prompt = '') {
528
529
  const text = String(prompt || '');
529
530
  const base = route?.reasoningPolicy || 'medium';
531
+ if (hasFromChatImgSignal(text)) return reasoning('xhigh', 'from_chat_img_image_work_order_analysis');
530
532
  if (route?.id === 'Research' || route?.id === 'AutoResearch') return reasoning('xhigh', 'research_or_experiment_route');
531
533
  if (/\b(research|autoresearch|hypothesis|falsify|novelty|frontier|benchmark|experiment|SEO|GEO|ranking|연구|실험|가설|검증)\b/i.test(text)) return reasoning('xhigh', 'research_level_prompt');
532
534
  if (base === 'high' || /\b(architecture|design|migration|database|security|parallel|orchestrat|refactor|algorithm|logic|tradeoff|검토|설계|마이그레이션|보안|병렬|팀|논리)\b/i.test(text)) return reasoning('high', 'logical_or_safety_work');
@@ -3,6 +3,7 @@ import { appendJsonlBounded, nowIso, readJson, readText, writeJsonAtomic, writeT
3
3
  import { triwikiContextTracking, triwikiContextTrackingText } from './routes.mjs';
4
4
 
5
5
  const MAX_LIVE_BYTES = 192 * 1024;
6
+ const TEAM_RUNTIME_TASKS_ARTIFACT = 'team-runtime-tasks.json';
6
7
  const DEFAULT_AGENTS = ['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer'];
7
8
  export const DEFAULT_TEAM_ROLE_COUNTS = { user: 1, planner: 1, reviewer: 1, executor: 3 };
8
9
  export const DEFAULT_MAX_TEAM_AGENT_SESSIONS = 6;
@@ -67,6 +68,7 @@ export function defaultTeamDashboard(id, prompt, opts = {}) {
67
68
  log: `sks team log ${id}`,
68
69
  tail: `sks team tail ${id}`,
69
70
  watch: `sks team watch ${id}`,
71
+ lane: `sks team lane ${id} --agent <agent> --follow`,
70
72
  event: `sks team event ${id} --agent <agent> --phase <phase> --message "..."`
71
73
  },
72
74
  agents: Object.fromEntries([...new Set([...DEFAULT_AGENTS, ...spec.roster.all_agents.map((agent) => agent.id)])].map((name) => [name, { status: 'pending', phase: null, last_seen: null }])),
@@ -115,6 +117,7 @@ sks team status ${id}
115
117
  sks team log ${id}
116
118
  sks team tail ${id}
117
119
  sks team watch ${id}
120
+ sks team lane ${id} --agent analysis_scout_1 --follow
118
121
  sks team event ${id} --agent analysis_scout_1 --phase parallel_analysis_scouting --message "mapped repo slice"
119
122
  \`\`\`
120
123
 
@@ -337,6 +340,42 @@ export async function readTeamTranscriptTail(dir, count = 20) {
337
340
  return text.split(/\n/).filter(Boolean).slice(-Math.max(1, Number(count) || 20));
338
341
  }
339
342
 
343
+ export async function renderTeamAgentLane(dir, opts = {}) {
344
+ const agent = String(opts.agent || opts.agentId || 'parent_orchestrator');
345
+ const phase = opts.phase ? String(opts.phase) : null;
346
+ const lines = Math.max(1, Number(opts.lines) || 12);
347
+ const dashboard = await readTeamDashboard(dir);
348
+ const runtime = await readJson(path.join(dir, TEAM_RUNTIME_TASKS_ARTIFACT), null);
349
+ const missionId = opts.missionId || dashboard?.mission_id || runtime?.mission_id || path.basename(dir);
350
+ const status = dashboard?.agents?.[agent] || {};
351
+ const runtimeTasks = Array.isArray(runtime?.tasks) ? runtime.tasks : Array.isArray(runtime) ? runtime : [];
352
+ const assignedTasks = runtimeTasks.filter((task) => task?.worker === agent || task?.agent_hint === agent);
353
+ const eventWindow = await readTeamTranscriptTail(dir, Math.max(lines * 8, 80));
354
+ const agentEvents = eventWindow.map(parseTranscriptLine).filter((event) => event?.agent === agent).slice(-lines);
355
+ const globalTail = (await readTeamTranscriptTail(dir, lines)).map(parseTranscriptLine).filter(Boolean);
356
+ return [
357
+ `# SKS Team Agent Lane`,
358
+ '',
359
+ `Mission: ${missionId}`,
360
+ `Agent: ${agent}`,
361
+ `Requested phase: ${phase || 'any'}`,
362
+ '',
363
+ `## Agent Status`,
364
+ `- status: ${status.status || 'pending'}`,
365
+ `- phase: ${status.phase || 'unknown'}`,
366
+ `- last_seen: ${status.last_seen || 'never'}`,
367
+ '',
368
+ `## Assigned Runtime Tasks`,
369
+ ...(runtime ? formatRuntimeTasks(assignedTasks) : ['- team-runtime-tasks.json not available yet.']),
370
+ '',
371
+ `## Recent Agent Events`,
372
+ ...(agentEvents.length ? agentEvents.map(formatTranscriptEvent) : ['- No recent agent-specific events in the bounded tail.']),
373
+ '',
374
+ `## Fallback Global Tail`,
375
+ ...(globalTail.length ? globalTail.map(formatTranscriptEvent) : ['- No transcript events yet.'])
376
+ ].join('\n');
377
+ }
378
+
340
379
  function normalizeEvent(event = {}) {
341
380
  return {
342
381
  ts: event.ts || nowIso(),
@@ -348,6 +387,39 @@ function normalizeEvent(event = {}) {
348
387
  };
349
388
  }
350
389
 
390
+ function parseTranscriptLine(line) {
391
+ try {
392
+ return JSON.parse(line);
393
+ } catch {
394
+ return { raw: String(line || '').slice(0, 1000) };
395
+ }
396
+ }
397
+
398
+ function formatTranscriptEvent(event = {}) {
399
+ if (event.raw) return `- ${event.raw}`;
400
+ const parts = [
401
+ event.ts || 'no-ts',
402
+ `[${event.phase || 'general'}]`,
403
+ event.agent || 'unknown',
404
+ event.type ? `(${event.type})` : null
405
+ ].filter(Boolean);
406
+ const suffix = event.artifact ? ` (${event.artifact})` : '';
407
+ return `- ${parts.join(' ')}: ${String(event.message || '').slice(0, 500)}${suffix}`;
408
+ }
409
+
410
+ function formatRuntimeTasks(tasks = []) {
411
+ if (!tasks.length) return ['- No assigned runtime tasks found.'];
412
+ return tasks.slice(0, 12).map((task) => {
413
+ const details = [
414
+ task.status || 'pending',
415
+ task.phase || task.role || 'team',
416
+ task.depends_on?.length ? `deps:${task.depends_on.join(',')}` : null,
417
+ task.file_paths?.length ? `files:${task.file_paths.slice(0, 3).join(',')}` : null
418
+ ].filter(Boolean).join(' | ');
419
+ return `- ${task.task_id || 'task'} ${task.subject || task.symbolic_id || 'untitled'} (${details})`;
420
+ });
421
+ }
422
+
351
423
  function trimLiveMarkdown(text) {
352
424
  if (Buffer.byteLength(text) <= MAX_LIVE_BYTES) return text.endsWith('\n') ? text : `${text}\n`;
353
425
  const marker = '## Live Events\n';