sneakoscope 0.7.74 → 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.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +178 -3
- package/src/cli/main.mjs +166 -27
- package/src/cli/maintenance-commands.mjs +40 -8
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +24 -8
- package/src/core/init.mjs +12 -17
- package/src/core/pipeline.mjs +6 -1
- package/src/core/routes.mjs +3 -3
- package/src/core/team-live.mjs +81 -12
- package/src/core/tmux-ui.mjs +303 -61
- package/src/core/version-manager.mjs +61 -31
|
@@ -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
|
|
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 = {}) {
|
|
@@ -761,15 +768,32 @@ export async function validateArtifactsCommand(args = []) {
|
|
|
761
768
|
const root = await sksRoot();
|
|
762
769
|
const missionArg = args[0] && !String(args[0]).startsWith('--') ? args[0] : 'latest';
|
|
763
770
|
const id = await resolveMissionId(root, missionArg);
|
|
764
|
-
const
|
|
771
|
+
const loaded = id ? await loadMission(root, id) : null;
|
|
772
|
+
const targetDir = loaded ? loaded.dir : root;
|
|
765
773
|
const requiredRaw = readFlagValue(args, '--required', '');
|
|
766
774
|
const required = requiredRaw === 'all'
|
|
767
775
|
? Object.keys(ARTIFACT_FILES)
|
|
768
776
|
: String(requiredRaw || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
769
777
|
const report = await writeValidationReport(targetDir, { required });
|
|
778
|
+
const missionMode = String(loaded?.mission?.mode || '').toLowerCase();
|
|
779
|
+
if (missionMode === 'research' || await exists(path.join(targetDir, 'research-gate.json'))) {
|
|
780
|
+
const researchGate = await evaluateResearchGate(targetDir);
|
|
781
|
+
report.route_gate = {
|
|
782
|
+
route: 'Research',
|
|
783
|
+
ok: researchGate.passed === true,
|
|
784
|
+
gate_file: 'research-gate.evaluated.json',
|
|
785
|
+
reasons: researchGate.reasons || []
|
|
786
|
+
};
|
|
787
|
+
if (!report.route_gate.ok) {
|
|
788
|
+
report.ok = false;
|
|
789
|
+
report.errors = [...(report.errors || []), ...report.route_gate.reasons.map((reason) => `research-gate:${reason}`)];
|
|
790
|
+
}
|
|
791
|
+
await writeJsonAtomic(path.join(targetDir, 'artifact-validation.json'), report);
|
|
792
|
+
}
|
|
770
793
|
if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
|
|
771
794
|
console.log(`Artifact validation: ${report.ok ? 'pass' : 'fail'}`);
|
|
772
795
|
console.log(`Target: ${path.relative(root, targetDir) || '.'}`);
|
|
796
|
+
if (report.route_gate) console.log(`Route gate: ${report.route_gate.route} ${report.route_gate.ok ? 'pass' : `fail (${report.route_gate.reasons.join(', ')})`}`);
|
|
773
797
|
if (report.missing.length) console.log(`Missing: ${report.missing.join(', ')}`);
|
|
774
798
|
for (const [schema, result] of Object.entries(report.results)) console.log(`${schema}: ${result.ok ? 'pass' : `fail (${result.errors.join(', ')})`}`);
|
|
775
799
|
if (!report.ok) process.exitCode = 2;
|
|
@@ -1827,6 +1851,7 @@ export async function team(args) {
|
|
|
1827
1851
|
if (result.tmux.ready) {
|
|
1828
1852
|
const tmuxState = result.tmux.created ? 'opened' : 'not opened';
|
|
1829
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)`);
|
|
1830
1855
|
if (result.tmux.split_ui?.mode) console.log(`tmux UI: ${result.tmux.split_ui.mode} (${result.tmux.split_ui.layout})`);
|
|
1831
1856
|
}
|
|
1832
1857
|
else console.log(`tmux: blocked (${Array.from(new Set(result.tmux.blockers || [])).join('; ')})`);
|
|
@@ -2121,6 +2146,7 @@ async function teamCommand(sub, args) {
|
|
|
2121
2146
|
return;
|
|
2122
2147
|
}
|
|
2123
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)`);
|
|
2124
2150
|
if (tmux.split_ui?.mode) console.log(`tmux UI: ${tmux.split_ui.mode} (${tmux.split_ui.layout})`);
|
|
2125
2151
|
if (tmux.split_ui?.current_session) console.log('tmux cockpit: reconciled inside the current SKS tmux window');
|
|
2126
2152
|
console.log(`Attach: ${tmux.attach_command}`);
|
|
@@ -2215,6 +2241,7 @@ async function teamCommand(sub, args) {
|
|
|
2215
2241
|
}
|
|
2216
2242
|
if (cleanup.killed_session) console.log(`tmux cleanup: killed session ${cleanup.session}`);
|
|
2217
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)`);
|
|
2218
2245
|
console.log(renderTeamCleanupSummary(control));
|
|
2219
2246
|
return;
|
|
2220
2247
|
}
|
|
@@ -2245,6 +2272,11 @@ async function teamCommand(sub, args) {
|
|
|
2245
2272
|
const agent = readFlagValue(args, '--agent', 'parent_orchestrator');
|
|
2246
2273
|
const phase = readFlagValue(args, '--phase', '');
|
|
2247
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
|
+
};
|
|
2248
2280
|
const printLane = async () => {
|
|
2249
2281
|
const text = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
|
|
2250
2282
|
if (flag(args, '--json')) {
|
|
@@ -2256,7 +2288,7 @@ async function teamCommand(sub, args) {
|
|
|
2256
2288
|
return text;
|
|
2257
2289
|
};
|
|
2258
2290
|
let last = await printLane();
|
|
2259
|
-
if (flag(args, '--follow') && !
|
|
2291
|
+
if (flag(args, '--follow') && !(await shouldStopLaneFollow())) {
|
|
2260
2292
|
for (;;) {
|
|
2261
2293
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2262
2294
|
const next = await renderTeamAgentLane(dir, { missionId: id, agent, phase, lines });
|
|
@@ -2266,7 +2298,7 @@ async function teamCommand(sub, args) {
|
|
|
2266
2298
|
console.log(next);
|
|
2267
2299
|
last = next;
|
|
2268
2300
|
}
|
|
2269
|
-
if (
|
|
2301
|
+
if (await shouldStopLaneFollow()) return;
|
|
2270
2302
|
}
|
|
2271
2303
|
}
|
|
2272
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.
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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 (
|
|
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
|
|
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 {
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
296
|
+
enabled: false,
|
|
299
297
|
bump: policy.versioning?.bump || 'patch',
|
|
300
|
-
trigger: '
|
|
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:
|
|
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:
|
|
376
|
+
enabled: false,
|
|
380
377
|
bump: 'patch',
|
|
381
|
-
trigger: '
|
|
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: '
|
|
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
|
|
782
|
-
|
|
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
|
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { writeMemorySweepReport } from './memory-governor.mjs';
|
|
|
12
12
|
import { writeMistakeMemoryReport } from './mistake-memory.mjs';
|
|
13
13
|
import { MISTAKE_RECALL_ARTIFACT, mistakeRecallGateStatus } from './mistake-recall.mjs';
|
|
14
14
|
import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } from './skill-forge.mjs';
|
|
15
|
-
import { writeResearchPlan } from './research.mjs';
|
|
15
|
+
import { evaluateResearchGate, writeResearchPlan } from './research.mjs';
|
|
16
16
|
import { PPT_REQUIRED_GATE_FIELDS, writePptRouteArtifacts } from './ppt.mjs';
|
|
17
17
|
import { writeQaLoopArtifacts } from './qa-loop.mjs';
|
|
18
18
|
import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
|
|
@@ -1456,6 +1456,11 @@ function missingRequiredGateFields(file, state, gate = {}) {
|
|
|
1456
1456
|
|
|
1457
1457
|
async function missingRequiredGateArtifacts(root, file, state, gate = {}) {
|
|
1458
1458
|
const mode = String(state?.mode || '').toUpperCase();
|
|
1459
|
+
if (file === 'research-gate.json' || mode === 'RESEARCH') {
|
|
1460
|
+
const evaluated = await evaluateResearchGate(missionDir(root, state.mission_id));
|
|
1461
|
+
if (evaluated.passed === true) return [];
|
|
1462
|
+
return (evaluated.reasons || ['research_gate_blocked']).map((reason) => `research-gate:${reason}`);
|
|
1463
|
+
}
|
|
1459
1464
|
if (file === IMAGE_UX_REVIEW_GATE_ARTIFACT || mode === 'IMAGE_UX_REVIEW') return missingImageUxReviewArtifacts(root, state, gate);
|
|
1460
1465
|
if (file !== 'team-gate.json' && mode !== 'TEAM') return [];
|
|
1461
1466
|
const missing = [];
|
package/src/core/routes.mjs
CHANGED
|
@@ -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|
|
|
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.' },
|
package/src/core/team-live.mjs
CHANGED
|
@@ -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
|
|
762
|
+
const isLaneAgent = String(from) === String(laneAgent);
|
|
720
763
|
return codexChatBlock({
|
|
721
|
-
speaker:
|
|
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
|
|
729
|
-
const
|
|
730
|
-
|
|
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 = '') {
|