sneakoscope 0.7.75 → 0.7.78

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.
@@ -17,7 +17,7 @@ import { contextCapsule } from '../core/triwiki-attention.mjs';
17
17
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
18
18
  import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, RECOMMENDED_SKILLS, ROUTES, hasFromChatImgSignal, reflectionRequiredForRoute, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
19
19
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
20
- import { appendTeamEvent, formatAgentReasoning, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, readTeamControl, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamCleanupSummary, renderTeamWatch, requestTeamSessionCleanup, teamCleanupRequested, teamReasoningPolicy } from '../core/team-live.mjs';
20
+ import { appendTeamEvent, formatAgentReasoning, formatRoleCounts, initTeamLive, isTerminalTeamAgentStatus, normalizeTeamSpec, parseTeamSpecArgs, readTeamControl, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamCleanupSummary, renderTeamWatch, requestTeamSessionCleanup, teamCleanupRequested, teamReasoningPolicy } from '../core/team-live.mjs';
21
21
  import { evaluateTeamReviewPolicyGate, MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, teamReviewPolicy } from '../core/team-review-policy.mjs';
22
22
  import { ARTIFACT_FILES, writeValidationReport } from '../core/artifact-schemas.mjs';
23
23
  import { writeEffortDecision } from '../core/effort-orchestrator.mjs';
@@ -66,13 +66,20 @@ function readOption(args, name, fallback) {
66
66
  }
67
67
 
68
68
  function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
69
- if (!lb?.ok || lb.status !== 'configured') return opts;
70
- if (readOption(args, '--session', null) || readOption(args, '--workspace', null)) return opts;
71
69
  const root = readOption(args, '--root', process.cwd());
70
+ const explicitSession = readOption(args, '--session', null) || readOption(args, '--workspace', null);
71
+ if (lb?.bypass_codex_lb) {
72
+ const session = explicitSession || sanitizeTmuxSessionName(`sks-openai-fallback-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
73
+ console.log(`codex-lb bypass active for this launch: ${lb.chain_health?.status || lb.status}`);
74
+ console.log(`Using fresh OpenAI fallback tmux session: ${session}`);
75
+ return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
76
+ }
77
+ if (!lb?.ok) return opts;
78
+ if (explicitSession) return opts;
72
79
  const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
73
- console.log(`codex-lb key loaded for this launch: ${lb.env_path}`);
80
+ console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
74
81
  console.log(`Using fresh tmux session: ${session}`);
75
- return { ...opts, session };
82
+ return { ...opts, session, codexLbFreshSession: true };
76
83
  }
77
84
 
78
85
  export async function madHighCommand(args = [], deps = {}) {
@@ -1844,6 +1851,7 @@ export async function team(args) {
1844
1851
  if (result.tmux.ready) {
1845
1852
  const tmuxState = result.tmux.created ? 'opened' : 'not opened';
1846
1853
  console.log(`tmux: ${tmuxState} ${result.tmux.opened_lane_count || result.tmux.agents.length} agent lane(s) in ${result.tmux.session || result.tmux.workspace}`);
1854
+ if (result.tmux.preflight_cleanup?.closed_lane_count) console.log(`tmux cleanup preflight: closed ${result.tmux.preflight_cleanup.closed_lane_count} stale Team pane(s)`);
1847
1855
  if (result.tmux.split_ui?.mode) console.log(`tmux UI: ${result.tmux.split_ui.mode} (${result.tmux.split_ui.layout})`);
1848
1856
  }
1849
1857
  else console.log(`tmux: blocked (${Array.from(new Set(result.tmux.blockers || [])).join('; ')})`);
@@ -2138,6 +2146,7 @@ async function teamCommand(sub, args) {
2138
2146
  return;
2139
2147
  }
2140
2148
  console.log(`tmux: opened ${tmux.opened_lane_count || tmux.lanes?.length || 0} Team lane(s) in ${tmux.session}`);
2149
+ if (tmux.preflight_cleanup?.closed_lane_count) console.log(`tmux cleanup preflight: closed ${tmux.preflight_cleanup.closed_lane_count} stale Team pane(s)`);
2141
2150
  if (tmux.split_ui?.mode) console.log(`tmux UI: ${tmux.split_ui.mode} (${tmux.split_ui.layout})`);
2142
2151
  if (tmux.split_ui?.current_session) console.log('tmux cockpit: reconciled inside the current SKS tmux window');
2143
2152
  console.log(`Attach: ${tmux.attach_command}`);
@@ -2232,6 +2241,7 @@ async function teamCommand(sub, args) {
2232
2241
  }
2233
2242
  if (cleanup.killed_session) console.log(`tmux cleanup: killed session ${cleanup.session}`);
2234
2243
  else console.log(`tmux cleanup: marked complete (${cleanup.reason || 'record updated'})`);
2244
+ if (cleanup.sweep_cleanup?.closed_lane_count) console.log(`tmux sweep: closed ${cleanup.sweep_cleanup.closed_lane_count} stale recorded Team pane(s)`);
2235
2245
  console.log(renderTeamCleanupSummary(control));
2236
2246
  return;
2237
2247
  }
@@ -2262,6 +2272,11 @@ async function teamCommand(sub, args) {
2262
2272
  const agent = readFlagValue(args, '--agent', 'parent_orchestrator');
2263
2273
  const phase = readFlagValue(args, '--phase', '');
2264
2274
  const lines = Number(readFlagValue(args, '--lines', '12'));
2275
+ const shouldStopLaneFollow = async () => {
2276
+ if (teamCleanupRequested(await readTeamControl(dir))) return true;
2277
+ const dashboard = await readTeamDashboard(dir).catch(() => null);
2278
+ return isTerminalTeamAgentStatus(dashboard?.agents?.[agent]?.status || '');
2279
+ };
2265
2280
  const printLane = async () => {
2266
2281
  const text = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
2267
2282
  if (flag(args, '--json')) {
@@ -2273,7 +2288,7 @@ async function teamCommand(sub, args) {
2273
2288
  return text;
2274
2289
  };
2275
2290
  let last = await printLane();
2276
- if (flag(args, '--follow') && !teamCleanupRequested(await readTeamControl(dir))) {
2291
+ if (flag(args, '--follow') && !(await shouldStopLaneFollow())) {
2277
2292
  for (;;) {
2278
2293
  await new Promise((resolve) => setTimeout(resolve, 2000));
2279
2294
  const next = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
@@ -2283,7 +2298,7 @@ async function teamCommand(sub, args) {
2283
2298
  console.log(next);
2284
2299
  last = next;
2285
2300
  }
2286
- if (teamCleanupRequested(await readTeamControl(dir))) return;
2301
+ if (await shouldStopLaneFollow()) return;
2287
2302
  }
2288
2303
  }
2289
2304
  return;
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.7.75';
8
+ export const PACKAGE_VERSION = '0.7.78';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -140,11 +140,11 @@ function clientModelCandidates(value, depth = 0) {
140
140
  }
141
141
 
142
142
  async function hookUserPrompt(root, state, payload, noQuestion) {
143
- if (looksLikeCodexGitCommitMessageGeneration(payload)) {
143
+ if (looksLikeCodexGitAction(payload)) {
144
144
  await armCodexGitActionStopBypass(root, payload).catch(() => null);
145
145
  return {
146
146
  continue: true,
147
- systemMessage: 'SKS: Codex App git commit message generation bypassed route gates.'
147
+ systemMessage: 'SKS: Codex App git action bypassed route gates.'
148
148
  };
149
149
  }
150
150
  if (!noQuestion) {
@@ -355,7 +355,7 @@ async function hookStop(root, state, payload, noQuestion) {
355
355
  if (await consumeCodexGitActionStopBypass(root, payload)) {
356
356
  return {
357
357
  continue: true,
358
- systemMessage: 'SKS: Codex App git commit message generation accepted without route finalization gates.'
358
+ systemMessage: 'SKS: Codex App git action accepted without route finalization gates.'
359
359
  };
360
360
  }
361
361
  if (!noQuestion && (hasDfixLightCompletion(last) || await consumeLightRouteStop(root, payload))) {
@@ -432,7 +432,7 @@ function explicitConversationId(payload = {}) {
432
432
  return payload.conversation_id || payload.thread_id || payload.session_id || payload.chat_id || null;
433
433
  }
434
434
 
435
- function looksLikeCodexGitCommitMessageGeneration(payload = {}) {
435
+ function looksLikeCodexGitAction(payload = {}) {
436
436
  const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
437
437
  const haystack = [
438
438
  payload.action,
@@ -460,7 +460,11 @@ function looksLikeCodexGitCommitMessageGeneration(payload = {}) {
460
460
  payload.metadata?.feature,
461
461
  payload.metadata?.source
462
462
  ].filter(Boolean).join(' ');
463
- const appSignal = /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|codex_git_commit)\b/i.test(haystack)
463
+ const codexAppGitSignal = /\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|actions|commit|push|pr)\b/i.test(haystack);
464
+ const gitActionSignal = /\bgit[_\s-]*actions?\b[\s\S]{0,80}\b(?:commit|push|commit[\s_-]*(?:and|&)?[\s_-]*push)\b/i.test(haystack);
465
+ const appSignal = codexAppGitSignal
466
+ || gitActionSignal
467
+ || /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|codex_git_commit)\b/i.test(haystack)
464
468
  || /커밋\s*메시지\s*생성/i.test(haystack);
465
469
  const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
466
470
  || /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
@@ -478,7 +482,7 @@ async function armCodexGitActionStopBypass(root, payload = {}) {
478
482
  const nowMs = Date.now();
479
483
  const record = {
480
484
  schema_version: 1,
481
- route: 'codex_git_commit',
485
+ route: 'codex_git_action',
482
486
  pending_stop_bypass: true,
483
487
  conversation_id: conversationId(payload),
484
488
  created_at: nowIso(),
@@ -492,7 +496,7 @@ async function consumeCodexGitActionStopBypass(root, payload = {}) {
492
496
  const file = path.join(root, '.sneakoscope', 'state', CODEX_GIT_ACTION_STOP_ARTIFACT);
493
497
  const record = await readJson(file, null).catch(() => null);
494
498
  if (!record?.pending_stop_bypass) return false;
495
- if (record.route !== 'codex_git_commit') return false;
499
+ if (!['codex_git_action', 'codex_git_commit'].includes(record.route)) return false;
496
500
  const expiresMs = Date.parse(record.expires_at || '');
497
501
  if (!Number.isFinite(expiresMs) || expiresMs < Date.now()) return false;
498
502
  const currentConversation = conversationId(payload);
@@ -911,14 +915,26 @@ export async function selftestCodexCommitHooks() {
911
915
  const hook = await runHook('user-prompt-submit', { conversation_id: id, action: 'codex_git_commit', prompt: 'Generate a git commit message for the staged diff.' });
912
916
  if (hook.code !== 0) throw new Error(`selftest failed: commit hook ${hook.code}: ${hook.stderr}`);
913
917
  const hookJson = JSON.parse(hook.stdout);
914
- if (hookJson.decision === 'block' || hookJson.hookSpecificOutput?.additionalContext || !String(hookJson.systemMessage || '').includes('git commit message generation')) throw new Error('selftest failed: commit route bypass');
918
+ if (hookJson.decision === 'block' || hookJson.hookSpecificOutput?.additionalContext || !String(hookJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: commit route bypass');
915
919
  const stop = await runHook('stop', { conversation_id: id, last_assistant_message: 'Fix Codex App commit message hook bypass' });
916
920
  if (stop.code !== 0) throw new Error(`selftest failed: commit stop ${stop.code}: ${stop.stderr}`);
917
921
  const stopJson = JSON.parse(stop.stdout);
918
922
  if (stopJson.decision === 'block' || !String(stopJson.systemMessage || '').includes('accepted without route finalization')) throw new Error('selftest failed: commit stop bypass');
923
+ const commitPushId = 'commit-push-selftest';
924
+ const appCommitPushHook = await runHook('user-prompt-submit', { conversation_id: commitPushId, action: 'Codex App Git Actions Commit and Push', prompt: 'Commit and push changes.' });
925
+ if (appCommitPushHook.code !== 0) throw new Error(`selftest failed: app commit-push hook ${appCommitPushHook.code}: ${appCommitPushHook.stderr}`);
926
+ const appCommitPushJson = JSON.parse(appCommitPushHook.stdout);
927
+ if (appCommitPushJson.decision === 'block' || appCommitPushJson.hookSpecificOutput?.additionalContext || !String(appCommitPushJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: app commit-push route bypass');
928
+ const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
929
+ if (appCommitPushStop.code !== 0) throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
930
+ if (JSON.parse(appCommitPushStop.stdout).decision === 'block') throw new Error('selftest failed: app commit-push stop bypass');
919
931
  const userHook = await runHook('user-prompt-submit', { prompt: '[커밋 메시지를 생성하지 못했습니다.] 코덱스 앱에서 이 버그 수정해줘' });
920
932
  if (userHook.code !== 0) throw new Error(`selftest failed: user commit hook ${userHook.code}: ${userHook.stderr}`);
921
933
  if (!JSON.parse(userHook.stdout).hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: user prompt route');
934
+ const userCommitPushHook = await runHook('user-prompt-submit', { prompt: '배포하게 커밋하고 푸쉬해줘' });
935
+ if (userCommitPushHook.code !== 0) throw new Error(`selftest failed: user commit-push hook ${userCommitPushHook.code}: ${userCommitPushHook.stderr}`);
936
+ const userCommitPushJson = JSON.parse(userCommitPushHook.stdout);
937
+ if (String(userCommitPushJson.systemMessage || '').includes('git action') || !userCommitPushJson.hookSpecificOutput?.additionalContext) throw new Error('selftest failed: user commit-push prompt should stay on normal route');
922
938
  }
923
939
 
924
940
  function normalizeHookResult(name, result = {}) {
package/src/core/init.mjs CHANGED
@@ -5,7 +5,7 @@ import { DEFAULT_RETENTION_POLICY } from './retention.mjs';
5
5
  import { DEFAULT_DB_SAFETY_POLICY } from './db-safety.mjs';
6
6
  import { isHarnessSourceProject, writeHarnessGuardPolicy } from './harness-guard.mjs';
7
7
  import { repairSksGeneratedArtifacts } from './harness-conflicts.mjs';
8
- import { installVersionGitHook } from './version-manager.mjs';
8
+ import { disableVersionGitHook } from './version-manager.mjs';
9
9
  import { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT } from './team-review-policy.mjs';
10
10
  import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, 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, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, solutionScoutPolicyText, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
11
11
  import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.mjs';
@@ -142,7 +142,7 @@ function isSksManagedHook(hook) {
142
142
  return hook.type === 'command' && /\bhook\s+(?:user-prompt-submit|pre-tool|post-tool|permission-request|stop)\b/.test(command) && /\b(?:sks|sneakoscope|sks\.mjs)\b/.test(command);
143
143
  }
144
144
 
145
- const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Codex native `/goal` workflows are the persisted continuation surface; Ralph is removed from the user-facing SKS surface.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Before substantive work, SKS checks npm for a newer package. If newer, ask update-now vs skip-for-this-conversation.\n- Versioning is managed by the SKS pre-commit hook; check `sks versioning status`. Bypass only with `SKS_DISABLE_VERSIONING=1`.\n- Installed harness files are immutable to LLM edits: `.codex/*`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/*policy*.json`, `AGENTS.md`, and `node_modules/sneakoscope`. The Sneakoscope engine source repo is the only automatic exception.\n- OMX/DCodex conflicts block setup/doctor. Show `sks conflicts prompt`; cleanup requires explicit human approval.\n- Do not stop at a plan when implementation was requested. Finish, verify, or report the hard blocker.\n- Do not create unrequested fallback implementation code. If the requested path is impossible, block with evidence instead of inventing substitute behavior.\n\n## Routes\n\n- General execution/code-changing prompts default to `$Team`: analysis scouts, TriWiki refresh/validate, read-only debate, consensus, concrete runtime task graph/inboxes, fresh executor team, minimum five-lane Team review, integration, Honest Mode.\n- `$Computer-Use` / `$CU` is the maximum-speed Codex Computer Use lane for UI/browser/visual tasks: skip Team debate and upfront TriWiki loops, use Codex Computer Use directly, then refresh/validate TriWiki and run Honest Mode at final closeout.\n- `$Goal` is a fast bridge/overlay for Codex native `/goal` create/pause/resume/clear persistence controls; implementation continues through the selected SKS execution route.\n- TriWiki recall must stay bounded. Use `sks wiki sweep` to record demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim.\n- Team missions must keep schema-backed evidence current: `work-order-ledger.json`, `effort-decision.json`, `team-dashboard-state.json`, and route-specific visual/dogfood artifacts where applicable. Team completion requires at least five independent reviewer/QA validation lanes before integration or final, even when a prompt requests fewer reviewers. Use `sks validate-artifacts latest` before claiming those artifacts pass.\n- `$DFix` is Direct Fix: only tiny copy/config/docs/labels/spacing/translation/simple mechanical edits, bypassing the main pipeline, Team, TriWiki/TriFix/reflection recording, and persistent route state; it still uses a one-line DFix-specific Honest check before final. Broad implementation stays on `$Team`, while UI design specifics follow the relevant design/UI route rules. `$PPT` is the restrained, information-first HTML/PDF presentation route and must seal delivery context, audience profile, STP, decision context, and 3+ pain-point/solution/aha mappings before design/render work. It must avoid over-designed visuals, carry detail through hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents, preserve editable source HTML under `source-html/`, record `ppt-parallel-report.json`, and clean PPT-only temporary build files before completion. `$Answer`, `$Help`, and `$Wiki` stay lightweight.\n- For code work, surface route/guard/write scopes first, split independent worker scopes when available, and keep parent-owned integration and verification.\n- Design work reads `design.md` as the only design decision SSOT. If missing, create it through `design-system-builder` from `docs/Design-Sys-Prompt.md`; getdesign.md, getdesign-reference, and curated DESIGN.md examples from https://github.com/VoltAgent/awesome-design-md are source inputs to fuse into that SSOT or route-local style tokens, not parallel design authorities. Image/logo/raster assets use `imagegen`, which must prefer official Codex App built-in image generation via `$imagegen` / `gpt-image-2` before API generation and must not be replaced by placeholder SVG/HTML/CSS, prose-only reviews, or fabricated files when generated raster evidence is required.\n- Research, AutoResearch, performance, token, accuracy, SEO/GEO, or workflow-improvement claims need experiment/eval evidence. Do not claim live model accuracy without a scored dataset.\n- Treat handwritten files above 3000 lines as split-review risks. Run `sks code-structure scan` and prefer extraction before adding substantial logic.\n- Skill dreaming stays lightweight: route use records JSON counters in `.sneakoscope/skills/dream-state.json`, and full skill inventory/recommendation runs only after the configured 10-route-event threshold and cooldown. Reports are recommendation-only; deleting or merging skills needs explicit user approval.\n\n## Evidence And Context\n\n- Context7 is required for external libraries, APIs, MCPs, package managers, SDKs, and generated docs: resolve-library-id then query-docs.\n- When tech stack, framework, package, runtime, or deployment-platform versions change, use Context7 or official vendor web docs, record current syntax/security/limit guidance as high-priority TriWiki claims, then refresh and validate before coding.\n- TriWiki is the context-tracking SSOT for long-running missions, Team handoffs, and context-pressure recovery. Read `.sneakoscope/wiki/context-pack.json` before each stage, use `attention.use_first` for compact high-trust recall, hydrate `attention.hydrate_first` from source before risky or lower-trust decisions, refresh after findings or artifact changes, and validate before handoffs/final claims.\n- Source priority: current code/tests/config, decision contract, vgraph, beta, GX render/snapshot metadata, LLM Wiki coordinate index, then model knowledge only if allowed.\n- Final response before stop: summarize what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked; then run Honest Mode. Say what passed and what was not verified.\n- `$From-Chat-IMG` uses forensic visual effort, not ordinary Team effort. Completion is blocked until source inventory, visual mapping, work-order coverage, scoped dogfood/QA, and post-fix verification artifacts are present and valid.\n\n## Safety\n\n- Database access is high risk. Use read-only inspection by default; live data mutation is out of scope unless a sealed contract allows local or branch-only migration files.\n- MAD and MAD-SKS widen only explicit scoped permissions; they still do not authorize unrequested fallback implementation code.\n- Task completion requires relevant tests or justification, zero unsupported critical claims, accepted visual/wiki drift, and final evidence.\n\n## Codex App\n\nUse `.codex/SNEAKOSCOPE.md`, generated `.agents/skills`, `.codex/hooks.json`, and SKS dollar commands (`$sks`, `$team`, `$computer-use`, `$cu`, `$ppt`, `$goal`, `$dfix`, `$qa-loop`, etc.) as the app control surface.\n";
145
+ const AGENTS_BLOCK = "\n# Sneakoscope Codex Managed Rules\n\nThis repository uses Sneakoscope Codex.\n\n## Core Rules\n\n- Codex native `/goal` workflows are the persisted continuation surface; Ralph is removed from the user-facing SKS surface.\n- Keep runtime state bounded: raw logs go to files, prompts get tails/summaries, and `sks gc` may prune stale artifacts.\n- Before substantive work, SKS checks npm for a newer package. If newer, ask update-now vs skip-for-this-conversation.\n- Versioning is explicit: use `sks versioning bump` when preparing release metadata. SKS must not install Git pre-commit hooks.\n- Installed harness files are immutable to LLM edits: `.codex/*`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/*policy*.json`, `AGENTS.md`, and `node_modules/sneakoscope`. The Sneakoscope engine source repo is the only automatic exception.\n- OMX/DCodex conflicts block setup/doctor. Show `sks conflicts prompt`; cleanup requires explicit human approval.\n- Do not stop at a plan when implementation was requested. Finish, verify, or report the hard blocker.\n- Do not create unrequested fallback implementation code. If the requested path is impossible, block with evidence instead of inventing substitute behavior.\n\n## Routes\n\n- General execution/code-changing prompts default to `$Team`: analysis scouts, TriWiki refresh/validate, read-only debate, consensus, concrete runtime task graph/inboxes, fresh executor team, minimum five-lane Team review, integration, Honest Mode.\n- `$Computer-Use` / `$CU` is the maximum-speed Codex Computer Use lane for UI/browser/visual tasks: skip Team debate and upfront TriWiki loops, use Codex Computer Use directly, then refresh/validate TriWiki and run Honest Mode at final closeout.\n- `$Goal` is a fast bridge/overlay for Codex native `/goal` create/pause/resume/clear persistence controls; implementation continues through the selected SKS execution route.\n- TriWiki recall must stay bounded. Use `sks wiki sweep` to record demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim.\n- Team missions must keep schema-backed evidence current: `work-order-ledger.json`, `effort-decision.json`, `team-dashboard-state.json`, and route-specific visual/dogfood artifacts where applicable. Team completion requires at least five independent reviewer/QA validation lanes before integration or final, even when a prompt requests fewer reviewers. Use `sks validate-artifacts latest` before claiming those artifacts pass.\n- `$DFix` is Direct Fix: only tiny copy/config/docs/labels/spacing/translation/simple mechanical edits, bypassing the main pipeline, Team, TriWiki/TriFix/reflection recording, and persistent route state; it still uses a one-line DFix-specific Honest check before final. Broad implementation stays on `$Team`, while UI design specifics follow the relevant design/UI route rules. `$PPT` is the restrained, information-first HTML/PDF presentation route and must seal delivery context, audience profile, STP, decision context, and 3+ pain-point/solution/aha mappings before design/render work. It must avoid over-designed visuals, carry detail through hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents, preserve editable source HTML under `source-html/`, record `ppt-parallel-report.json`, and clean PPT-only temporary build files before completion. `$Answer`, `$Help`, and `$Wiki` stay lightweight.\n- For code work, surface route/guard/write scopes first, split independent worker scopes when available, and keep parent-owned integration and verification.\n- Design work reads `design.md` as the only design decision SSOT. If missing, create it through `design-system-builder` from `docs/Design-Sys-Prompt.md`; getdesign.md, getdesign-reference, and curated DESIGN.md examples from https://github.com/VoltAgent/awesome-design-md are source inputs to fuse into that SSOT or route-local style tokens, not parallel design authorities. Image/logo/raster assets use `imagegen`, which must prefer official Codex App built-in image generation via `$imagegen` / `gpt-image-2` before API generation and must not be replaced by placeholder SVG/HTML/CSS, prose-only reviews, or fabricated files when generated raster evidence is required.\n- Research, AutoResearch, performance, token, accuracy, SEO/GEO, or workflow-improvement claims need experiment/eval evidence. Do not claim live model accuracy without a scored dataset.\n- Treat handwritten files above 3000 lines as split-review risks. Run `sks code-structure scan` and prefer extraction before adding substantial logic.\n- Skill dreaming stays lightweight: route use records JSON counters in `.sneakoscope/skills/dream-state.json`, and full skill inventory/recommendation runs only after the configured 10-route-event threshold and cooldown. Reports are recommendation-only; deleting or merging skills needs explicit user approval.\n\n## Evidence And Context\n\n- Context7 is required for external libraries, APIs, MCPs, package managers, SDKs, and generated docs: resolve-library-id then query-docs.\n- When tech stack, framework, package, runtime, or deployment-platform versions change, use Context7 or official vendor web docs, record current syntax/security/limit guidance as high-priority TriWiki claims, then refresh and validate before coding.\n- TriWiki is the context-tracking SSOT for long-running missions, Team handoffs, and context-pressure recovery. Read `.sneakoscope/wiki/context-pack.json` before each stage, use `attention.use_first` for compact high-trust recall, hydrate `attention.hydrate_first` from source before risky or lower-trust decisions, refresh after findings or artifact changes, and validate before handoffs/final claims.\n- Source priority: current code/tests/config, decision contract, vgraph, beta, GX render/snapshot metadata, LLM Wiki coordinate index, then model knowledge only if allowed.\n- Final response before stop: summarize what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked; then run Honest Mode. Say what passed and what was not verified.\n- `$From-Chat-IMG` uses forensic visual effort, not ordinary Team effort. Completion is blocked until source inventory, visual mapping, work-order coverage, scoped dogfood/QA, and post-fix verification artifacts are present and valid.\n\n## Safety\n\n- Database access is high risk. Use read-only inspection by default; live data mutation is out of scope unless a sealed contract allows local or branch-only migration files.\n- MAD and MAD-SKS widen only explicit scoped permissions; they still do not authorize unrequested fallback implementation code.\n- Task completion requires relevant tests or justification, zero unsupported critical claims, accepted visual/wiki drift, and final evidence.\n\n## Codex App\n\nUse `.codex/SNEAKOSCOPE.md`, generated `.agents/skills`, `.codex/hooks.json`, and SKS dollar commands (`$sks`, `$team`, `$computer-use`, `$cu`, `$ppt`, `$goal`, `$dfix`, `$qa-loop`, etc.) as the app control surface.\n";
146
146
 
147
147
  function agentsBlockText() {
148
148
  return AGENTS_BLOCK
@@ -249,8 +249,7 @@ export async function initProject(root, opts = {}) {
249
249
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
250
250
  excluded_patterns: localExclude?.patterns || [],
251
251
  versioning: {
252
- enabled: true,
253
- hook: 'pre-commit',
252
+ enabled: false,
254
253
  bump: 'patch',
255
254
  lock: 'git-common-dir/sks-version.lock',
256
255
  state: 'git-common-dir/sks-version-state.json'
@@ -286,8 +285,7 @@ export async function initProject(root, opts = {}) {
286
285
  excluded_patterns: localExclude?.patterns || policy.git?.excluded_patterns || [],
287
286
  versioning: {
288
287
  ...(policy.git?.versioning || {}),
289
- enabled: true,
290
- hook: 'pre-commit',
288
+ enabled: false,
291
289
  bump: policy.git?.versioning?.bump || 'patch',
292
290
  lock: 'git-common-dir/sks-version.lock',
293
291
  state: 'git-common-dir/sks-version-state.json'
@@ -295,9 +293,9 @@ export async function initProject(root, opts = {}) {
295
293
  },
296
294
  versioning: {
297
295
  ...(policy.versioning || {}),
298
- enabled: true,
296
+ enabled: false,
299
297
  bump: policy.versioning?.bump || 'patch',
300
- trigger: 'git-pre-commit',
298
+ trigger: 'manual',
301
299
  lock_scope: 'git-common-dir',
302
300
  managed_files: ['package.json', 'package-lock.json', 'npm-shrinkwrap.json']
303
301
  },
@@ -361,8 +359,7 @@ export async function initProject(root, opts = {}) {
361
359
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
362
360
  excluded_patterns: localExclude?.patterns || [],
363
361
  versioning: {
364
- enabled: true,
365
- hook: 'pre-commit',
362
+ enabled: false,
366
363
  bump: 'patch',
367
364
  lock: 'git-common-dir/sks-version.lock',
368
365
  state: 'git-common-dir/sks-version-state.json'
@@ -376,12 +373,12 @@ export async function initProject(root, opts = {}) {
376
373
  skip_scope: 'conversation_only'
377
374
  },
378
375
  versioning: {
379
- enabled: true,
376
+ enabled: false,
380
377
  bump: 'patch',
381
- trigger: 'git-pre-commit',
378
+ trigger: 'manual',
382
379
  lock_scope: 'git-common-dir',
383
380
  managed_files: ['package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
384
- collision_policy: 'lock_then_bump_above_last_seen_version'
381
+ collision_policy: 'explicit_bump_only'
385
382
  },
386
383
  honest_mode: {
387
384
  required_before_final: true,
@@ -778,10 +775,8 @@ function upsertTomlTable(text, table, block) {
778
775
  await writeJsonAtomic(manifestPath, manifest);
779
776
  await writeHarnessGuardPolicy(root);
780
777
  created.push('.sneakoscope/harness-guard.json');
781
- const versionHookCommand = sourceProject ? 'node ./bin/sks.mjs' : hookCommandPrefix;
782
- const versionHook = await installVersionGitHook(root, versionHookCommand);
783
- if (versionHook.installed) created.push('.git/hooks/pre-commit SKS version guard');
784
- else created.push(`version guard skipped (${versionHook.reason})`);
778
+ const versionHookCleanup = await disableVersionGitHook(root);
779
+ created.push(versionHookCleanup.hook_removed ? '.git/hooks/pre-commit SKS version guard removed' : `version guard disabled (${versionHookCleanup.reason || 'policy updated'})`);
785
780
  return { created, generated_cleanup: generatedCleanup, skill_install: skillInstall };
786
781
  }
787
782
 
@@ -523,8 +523,8 @@ export const COMMAND_CATALOG = [
523
523
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
524
524
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, tmux, and Homebrew on macOS.' },
525
525
  { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
526
- { name: 'codex-lb', usage: 'sks codex-lb status|repair|setup --host <domain> --api-key <key>', description: 'Configure or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
527
- { name: 'auth', usage: 'sks auth status|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, repair, and setup commands.' },
526
+ { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
527
+ { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, health, repair, and setup commands.' },
528
528
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
529
529
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
530
530
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
@@ -538,7 +538,7 @@ export const COMMAND_CATALOG = [
538
538
  { name: 'pipeline', usage: 'sks pipeline status|resume|plan|answer ...', description: 'Inspect the active skill-first route, materialized execution plan, ambiguity gates, and completion gates.' },
539
539
  { name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
540
540
  { name: 'conflicts', usage: 'sks conflicts check|prompt [--json]', description: 'Detect other Codex harnesses such as OMX/DCodex and print the GPT-5.5 high cleanup prompt.' },
541
- { name: 'versioning', usage: 'sks versioning status|bump|pre-commit [--json]', description: 'Manage automatic project version bumps on every commit with a shared Git lock.' },
541
+ { name: 'versioning', usage: 'sks versioning status|bump|disable [--json]', description: 'Manage explicit project version syncs; SKS does not install Git pre-commit hooks.' },
542
542
  { name: 'aliases', usage: 'sks aliases', description: 'Show command aliases and npm binary names.' },
543
543
  { name: 'setup', usage: 'sks setup [--bootstrap] [--install-scope global|project] [--local-only] [--force] [--json]', description: 'Initialize SKS state, Codex App files, hooks, skills, and rules.' },
544
544
  { name: 'fix-path', usage: 'sks fix-path [--install-scope global|project] [--json]', description: 'Refresh hook commands with the resolved SKS binary path.' },
@@ -8,6 +8,32 @@ const MAX_LIVE_BYTES = 192 * 1024;
8
8
  const TEAM_RUNTIME_TASKS_ARTIFACT = 'team-runtime-tasks.json';
9
9
  const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
10
10
  const DEFAULT_AGENTS = ['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer'];
11
+ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
12
+ 'agent_closed',
13
+ 'agent_done',
14
+ 'cancelled',
15
+ 'canceled',
16
+ 'cleanup',
17
+ 'cleanup_requested',
18
+ 'closed',
19
+ 'complete',
20
+ 'completed',
21
+ 'done',
22
+ 'ended',
23
+ 'failed',
24
+ 'stopped',
25
+ 'terminal',
26
+ 'tmux_lane_closed'
27
+ ]);
28
+ const CHAT_COLOR_CODES = {
29
+ blue: '34',
30
+ cyan: '36',
31
+ yellow: '33',
32
+ magenta: '35',
33
+ red: '31',
34
+ green: '32',
35
+ gray: '90'
36
+ };
11
37
  export const DEFAULT_TEAM_ROLE_COUNTS = { user: 1, planner: 1, reviewer: MIN_TEAM_REVIEWER_LANES, executor: 3 };
12
38
  export const DEFAULT_MAX_TEAM_AGENT_SESSIONS = 6;
13
39
  const ROLE_ALIASES = {
@@ -440,11 +466,13 @@ export async function appendTeamEvent(dir, event) {
440
466
  dashboard.updated_at = record.ts;
441
467
  dashboard.latest_messages = [...(dashboard.latest_messages || []), record].slice(-20);
442
468
  const agent = record.agent || 'unknown';
469
+ const terminalStatus = terminalTeamAgentStatusFromEvent(record);
443
470
  dashboard.agents ||= {};
444
471
  dashboard.agents[agent] ||= {};
445
- dashboard.agents[agent].status = record.type || 'active';
472
+ dashboard.agents[agent].status = terminalStatus || record.type || 'active';
446
473
  dashboard.agents[agent].phase = record.phase || null;
447
474
  dashboard.agents[agent].last_seen = record.ts;
475
+ if (terminalStatus) dashboard.agents[agent].closed_at = record.ts;
448
476
  await writeJsonAtomic(files.dashboard, dashboard);
449
477
  }
450
478
  await reconcileTeamTmuxFromEvent(dir, record).catch(() => null);
@@ -508,6 +536,22 @@ export function teamCleanupRequested(control = {}) {
508
536
  return Boolean(control?.cleanup_requested || control?.status === 'cleanup_requested' || control?.status === 'ended');
509
537
  }
510
538
 
539
+ export function isTerminalTeamAgentStatus(status = '') {
540
+ const normalized = String(status || '').trim().toLowerCase();
541
+ return TERMINAL_TEAM_AGENT_STATUSES.has(normalized) || /(?:^|_)(?:done|complete|completed|closed|cleanup|cancelled|canceled|failed|ended|stopped)(?:_|$)/.test(normalized);
542
+ }
543
+
544
+ export function terminalTeamAgentStatusFromEvent(event = {}) {
545
+ const type = String(event.type || '').trim().toLowerCase();
546
+ if (isTerminalTeamAgentStatus(type)) return type;
547
+ const phase = String(event.phase || '').trim().toLowerCase();
548
+ if (isTerminalTeamAgentStatus(phase)) return phase;
549
+ const message = String(event.message || '').trim();
550
+ if (/^(?:done|complete|completed|finished|final|closed|agent_done|agent_closed)\b/i.test(message)) return 'completed';
551
+ if (/(?:작업|분석|구현|검토|리뷰|qa|lane|agent|에이전트).{0,40}(?:완료|종료|끝)/i.test(message)) return 'completed';
552
+ return '';
553
+ }
554
+
511
555
  export function renderTeamCleanupSummary(control = {}) {
512
556
  if (!teamCleanupRequested(control)) return '';
513
557
  return [
@@ -558,6 +602,7 @@ export async function renderTeamAgentLane(dir, opts = {}) {
558
602
  .sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || '')))
559
603
  .slice(-lines);
560
604
  const laneStyle = teamLaneTextStyle(agent);
605
+ const colorChat = terminalChatColorEnabled(opts);
561
606
  return [
562
607
  `# SKS Team Agent Lane`,
563
608
  '',
@@ -577,7 +622,7 @@ export async function renderTeamAgentLane(dir, opts = {}) {
577
622
  ...(runtime ? formatRuntimeTasks(assignedTasks) : ['- team-runtime-tasks.json not available yet.']),
578
623
  '',
579
624
  `## Codex Chat`,
580
- ...(chatEvents.length ? chatEvents.map((event) => formatChatTranscriptEvent(event, aliases[0])) : ['- waiting for live agent messages...']),
625
+ ...(chatEvents.length ? chatEvents.map((event) => formatChatTranscriptEvent(event, aliases[0], { color: colorChat })) : ['- waiting for live agent messages...']),
581
626
  opts.includeGlobalTail ? '' : null,
582
627
  opts.includeGlobalTail ? `## Global Tail` : null,
583
628
  ...(opts.includeGlobalTail
@@ -709,25 +754,49 @@ function uniqueTranscriptEvents(events = []) {
709
754
  return out;
710
755
  }
711
756
 
712
- function formatChatTranscriptEvent(event = {}, laneAgent = '') {
713
- if (event.raw) return codexChatBlock({ speaker: 'system', message: event.raw });
757
+ function formatChatTranscriptEvent(event = {}, laneAgent = '', opts = {}) {
758
+ if (event.raw) return codexChatBlock({ speaker: 'system', kind: 'raw', style: teamLaneTextStyle('overview'), color: opts.color, message: event.raw });
714
759
  const from = event.agent || 'unknown';
715
- const to = event.to ? ` -> ${event.to}` : '';
716
- const kind = event.type && event.type !== 'message' ? ` [${event.type}]` : '';
717
760
  const ts = event.ts ? `${event.ts} ` : '';
718
761
  const artifact = event.artifact ? ` (${event.artifact})` : '';
719
- const marker = String(from) === String(laneAgent) ? 'me' : from;
762
+ const isLaneAgent = String(from) === String(laneAgent);
720
763
  return codexChatBlock({
721
- speaker: `${marker}${to}${kind}`,
764
+ speaker: isLaneAgent ? `me (${from})` : from,
765
+ to: event.to || '',
766
+ kind: event.type || 'message',
722
767
  meta: ts.trim(),
768
+ style: teamLaneTextStyle(from),
769
+ color: opts.color,
723
770
  message: `${String(event.message || '').slice(0, 500)}${artifact}`
724
771
  });
725
772
  }
726
773
 
727
- function codexChatBlock({ speaker = 'agent', meta = '', message = '' } = {}) {
728
- const header = [speaker, meta].filter(Boolean).join(' | ');
729
- const body = String(message || '').split(/\r?\n/).map((line) => `| ${line}`).join('\n');
730
- return [`+-- ${header}`, body || '|', '+--'].join('\n');
774
+ function codexChatBlock({ speaker = 'agent', to = '', kind = '', meta = '', style = {}, color = false, message = '' } = {}) {
775
+ const role = style?.role || 'agent';
776
+ const roleKind = [kind, role].filter(Boolean).join('/');
777
+ const target = to ? ` -> ${to}` : '';
778
+ const header = [
779
+ colorizeChatText(`${speaker}${target}`, style, color, { bold: true }),
780
+ roleKind ? colorizeChatText(`[${roleKind}]`, style, color) : null,
781
+ meta ? colorizeChatText(`| ${meta}`, { color_name: 'Gray' }, color) : null
782
+ ].filter(Boolean).join(' ');
783
+ const border = (text) => colorizeChatText(text, style, color);
784
+ const body = String(message || '').split(/\r?\n/).map((line) => `${border('│')} ${colorizeChatText(line || ' ', style, color)}`).join('\n');
785
+ return [`${border('╭─')} ${header}`, body || `${border('│')} `, border('╰─')].join('\n');
786
+ }
787
+
788
+ function terminalChatColorEnabled(opts = {}) {
789
+ if (Object.prototype.hasOwnProperty.call(opts, 'color')) return Boolean(opts.color);
790
+ if (process.env.NO_COLOR) return false;
791
+ return Boolean(process.stdout?.isTTY);
792
+ }
793
+
794
+ function colorizeChatText(text, style = {}, enabled = false, opts = {}) {
795
+ if (!enabled) return text;
796
+ const colorName = String(style?.color_name || 'gray').toLowerCase();
797
+ const colorCode = CHAT_COLOR_CODES[colorName] || CHAT_COLOR_CODES.gray;
798
+ const code = opts.bold ? `1;${colorCode}` : colorCode;
799
+ return `\x1b[${code}m${text}\x1b[0m`;
731
800
  }
732
801
 
733
802
  function eventAddressedTo(event = {}, agent = '') {