sneakoscope 0.7.66 → 0.7.67
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 +26 -85
- package/package.json +6 -5
- package/src/cli/install-helpers.mjs +46 -1
- package/src/cli/main.mjs +15 -11
- package/src/cli/maintenance-commands.mjs +2 -2
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +110 -1
- package/src/core/init.mjs +2 -1
- package/src/core/team-live.mjs +33 -10
- package/src/core/tmux-ui.mjs +34 -12
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Sneakoscope Codex
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
|
|
5
3
|
Sneakoscope Codex (`sks`) is a Codex CLI/App harness for repeatable workflows. It adds terminal commands, Codex App `$` commands, tmux workspaces, Team/QA/Research routes, pipeline plans, Computer Use, imagegen UI/UX review, Goal, Context7, DB safety, TriWiki, design-system routing, skill dreaming, Honest Mode.
|
|
6
4
|
|
|
7
5
|
## Quick Start
|
|
@@ -42,30 +40,30 @@ sks selftest --mock
|
|
|
42
40
|
|
|
43
41
|
| Area | What it does |
|
|
44
42
|
| --- | --- |
|
|
45
|
-
| CLI runtime | Bare `sks` opens
|
|
46
|
-
| Codex App commands | Installs generated skills
|
|
47
|
-
| OpenClaw agents | Generates an OpenClaw skill package so
|
|
48
|
-
| Pipeline plans | Writes `pipeline-plan.json`
|
|
49
|
-
| Team orchestration | Runs substantial work through
|
|
43
|
+
| CLI runtime | Bare `sks` opens/reuses the default tmux Codex CLI workspace. `sks tmux open` handles explicit session flags, and `sks --mad` launches a single-pane MAD session. |
|
|
44
|
+
| Codex App commands | Installs generated skills for `$Team`, `$DFix`, `$QA-LOOP`, `$PPT`, `$UX-Review`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes. |
|
|
45
|
+
| OpenClaw agents | Generates an OpenClaw skill package so agents can attach `sneakoscope-codex` and discover SKS commands from the repo root. |
|
|
46
|
+
| Pipeline plans | Writes `pipeline-plan.json` so runtime lanes, stages, verification, and no-fallback invariants are visible with `sks pipeline plan`. |
|
|
47
|
+
| Team orchestration | Runs substantial work through ambiguity handling, scouts, TriWiki, debate, runtime tasks, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence. |
|
|
50
48
|
| Skill dreaming | Records cheap generated-skill usage counters in JSON and only periodically scans `.agents/skills` for keep, merge, prune, and improvement candidates. Reports are recommendation-only and never delete skills automatically. |
|
|
51
49
|
| From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
|
|
52
50
|
| QA loop | Dogfoods UI/API behavior with safety gates, Codex Computer Use-only UI evidence, safe fixes, and rechecks. |
|
|
53
|
-
| PPT pipeline | Uses `$PPT` for restrained HTML/PDF
|
|
54
|
-
| Image UX Review | Uses `$Image-UX-Review` / `$UX-Review` for UI/UX audits that require generated annotated review images
|
|
55
|
-
| Computer Use fast lane | Uses `$Computer-Use` / `$CU` for UI/browser/visual work
|
|
51
|
+
| PPT pipeline | Uses `$PPT` for restrained HTML/PDF decks with sealed context, design SSOT, export QA, editable source HTML, and `$imagegen` assets when required. |
|
|
52
|
+
| Image UX Review | Uses `$Image-UX-Review` / `$UX-Review` for UI/UX audits that require generated annotated review images before issue extraction. |
|
|
53
|
+
| Computer Use fast lane | Uses `$Computer-Use` / `$CU` for fast UI/browser/visual work, then refreshes/validates TriWiki and runs Honest Mode. |
|
|
56
54
|
| Goal | Bridges Codex native `/goal` create, pause, resume, and clear controls while implementation continues through the selected SKS route. |
|
|
57
|
-
| TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata,
|
|
55
|
+
| TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata, attention hints, and mistake recall. |
|
|
58
56
|
| Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
|
|
59
|
-
| Design SSOT | Treats `design.md` as the only design decision source of truth
|
|
57
|
+
| Design SSOT | Treats `design.md` as the only design decision source of truth; getdesign and curated DESIGN.md examples are inputs, not parallel authorities. |
|
|
60
58
|
| DB safety | Treats SQL, migrations, Supabase, RLS, and destructive operations as high risk. |
|
|
61
|
-
| Release hygiene | Checks versioning, changelog,
|
|
59
|
+
| Release hygiene | Checks versioning, changelog, size, syntax. |
|
|
62
60
|
|
|
63
61
|
## Requirements
|
|
64
62
|
|
|
65
63
|
- Node.js `>=20.11`
|
|
66
64
|
- npm
|
|
67
65
|
- Codex CLI for terminal workflows
|
|
68
|
-
- Codex App for app-facing workflows,
|
|
66
|
+
- Codex App for app-facing workflows, including Codex Computer Use and `$imagegen`/`gpt-image-2` evidence when required
|
|
69
67
|
- tmux for the CLI-first runtime
|
|
70
68
|
- Context7 MCP for current-docs-gated routes
|
|
71
69
|
|
|
@@ -75,12 +73,7 @@ Install tmux from [tmux.dev/download](https://www.tmux.dev/download). On macOS,
|
|
|
75
73
|
brew install tmux
|
|
76
74
|
```
|
|
77
75
|
|
|
78
|
-
The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex` versions before opening tmux
|
|
79
|
-
|
|
80
|
-
- Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
|
|
81
|
-
- Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
|
|
82
|
-
- Requires tmux 3.x or newer before opening the session.
|
|
83
|
-
- Creates a named detached single-pane tmux session and prints only the session, gate, attach, and blocker details needed to act.
|
|
76
|
+
The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex` versions before opening tmux. `sks --mad` also checks dependencies, requires tmux 3.x, and prints only the session, gate, attach, and blocker details needed to act.
|
|
84
77
|
|
|
85
78
|
## Installation
|
|
86
79
|
|
|
@@ -97,7 +90,7 @@ sks root
|
|
|
97
90
|
|
|
98
91
|
Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Setup, doctor repair, and npm postinstall refreshes also compare the previous SKS generated-file manifest with the current package templates and prune stale SKS-generated legacy skills or agent files while preserving user-owned custom skills. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
|
|
99
92
|
|
|
100
|
-
During npm postinstall, SKS
|
|
93
|
+
During npm postinstall, SKS installs generated Codex App skills and tries `skills add MohtashamMurshid/getdesign` when the `skills` CLI is available. Design work still flows through one authority: `design.md`.
|
|
101
94
|
|
|
102
95
|
### One-Shot Install
|
|
103
96
|
|
|
@@ -190,17 +183,9 @@ sks --mad
|
|
|
190
183
|
sks --mad --yes
|
|
191
184
|
```
|
|
192
185
|
|
|
193
|
-
This syncs existing codex-lb/Codex CLI auth
|
|
194
|
-
|
|
195
|
-
MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
|
|
196
|
-
|
|
197
|
-
Before launching, SKS checks whether a newer `sneakoscope` exists on npm. In an interactive terminal it prompts:
|
|
198
|
-
|
|
199
|
-
```text
|
|
200
|
-
SKS 0.x.y -> 0.x.z update before MAD launch? [Y/n]
|
|
201
|
-
```
|
|
186
|
+
This syncs existing codex-lb/Codex CLI auth, creates/uses the `sks-mad-high` full-access profile, opens the MAD-SKS permission gate for that tmux run, and launches a single Codex CLI pane. The session recreates the named session so stale split-pane MAD sessions collapse back to one pane. Catastrophic database wipe/all-row/project-management safeguards remain active, and the pipeline contract still forbids unrequested fallback implementation code.
|
|
202
187
|
|
|
203
|
-
|
|
188
|
+
Before launching, SKS checks npm for a newer `sneakoscope`; answer `y` to update or `n` to continue. Use `--yes` to approve missing dependency installs automatically.
|
|
204
189
|
|
|
205
190
|
### Team Missions
|
|
206
191
|
|
|
@@ -216,17 +201,7 @@ sks team dashboard latest
|
|
|
216
201
|
sks team log latest
|
|
217
202
|
```
|
|
218
203
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and reconciles split live lanes inside the current SKS-owned tmux session when available. Outside an SKS tmux session, `sks team open-tmux --separate-session` keeps the named `sks-team-*` fallback view. Use `--no-open-tmux` for artifact-only mission creation. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
222
|
-
|
|
223
|
-
The tmux Team launch is a live orchestration screen in one tmux window: the main Codex pane stays alive, a managed overview pane follows `sks team watch <mission-id> --follow`, and neighboring managed split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS tags Team panes with tmux user options, closes only those managed panes when agent lanes complete or cleanup is requested, and recalculates the tiled layout after split/close operations. The separate `sks-team-*` session remains available as a fallback. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
|
|
224
|
-
|
|
225
|
-
Team roster and runtime artifacts now include per-agent Fast reasoning metadata. Simple bounded Team lanes can use low reasoning, tool-heavy runtime/CLI/tmux work uses medium, and knowledge, current-docs, safety, DB, release, commit, or research-heavy lanes use high or xhigh as appropriate instead of opening every scout at high.
|
|
226
|
-
|
|
227
|
-
Agent sessions communicate through the bounded Team transcript. Use `sks team message <mission-id|latest> --from <agent> --to <agent|all> --message "..."` to add direct or broadcast messages; lane panes show messages addressed to that agent plus the fallback global tail.
|
|
228
|
-
|
|
229
|
-
When the Team route reaches `session_cleanup`, SKS marks the tmux session record complete and asks `watch --follow` / `lane --follow` panes to show a cleanup summary and stop. You can also run `sks team cleanup-tmux <mission-id|latest>` manually, or `sks team cleanup-tmux latest --close` to kill the recorded tmux session.
|
|
204
|
+
Team missions keep at least five QA/reviewer lanes active, record live events, compile runtime tasks and worker inboxes, write schema-backed effort/work-order/dashboard artifacts, and reconcile split live lanes in tmux when available. Use `sks team watch`, `sks team lane`, `sks team message`, and `sks team cleanup-tmux` to inspect or close the live view.
|
|
230
205
|
|
|
231
206
|
### QA, Computer Use, Goal, Research, DB, Wiki, GX
|
|
232
207
|
|
|
@@ -252,21 +227,11 @@ sks skill-dream run --json
|
|
|
252
227
|
sks code-structure scan --json
|
|
253
228
|
```
|
|
254
229
|
|
|
255
|
-
`sks pipeline plan`
|
|
256
|
-
|
|
257
|
-
`sks proof-field scan` is SKS's lightweight outcome rubric: it maps the goal to proof cones, records unrelated work that can be skipped with evidence, reports a simplicity score, and names escalation triggers for when the route must return to the full Team/Honest proof path. The rubric embeds Hyperplan-style adversarial pressure as compact lenses instead of a new command: challenge framing, subtract surface, demand evidence, test integration risk, and consider one simpler alternative. When `execution_lane.lane` is `proof_field_fast_lane`, SKS can keep the parent-owned minimal patch plus listed verification and skip Team debate, fresh executor teams, broad route rework, and unrelated checks. Database, security, visual-forensic, unknown, broad, failed, or unsupported-claim signals fail closed to the normal Team/Honest path. Use `sks pipeline plan --proof-field` after changed files are known to bind that Proof Field decision to the mission plan.
|
|
230
|
+
`sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. `sks proof-field scan` is the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path.
|
|
258
231
|
|
|
259
232
|
### Ambiguity Questions
|
|
260
233
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
The design borrows two useful ideas from external planning systems without copying their route weight: Ouroboros-style ambiguity thresholds decide whether the prompt is clear enough to proceed, while Prometheus/Hyperplan-style adversarial lenses challenge framing, remove unnecessary surface, demand evidence, test integration risk, and consider a simpler alternative before Team work starts.
|
|
264
|
-
|
|
265
|
-
`sks skill-dream` keeps generated skill complexity bounded without doing a heavy evaluation on every prompt. Route use writes compact counters to `.sneakoscope/skills/dream-state.json`; after the configured 10-route-event threshold and cooldown, or when you run `sks skill-dream run`, SKS scans `.agents/skills` and writes `.sneakoscope/reports/skill-dream-latest.json` with keep, merge, prune, and improvement candidates. The report is intentionally advisory: deleting or merging skills requires explicit approval.
|
|
266
|
-
|
|
267
|
-
`sks goal` and `$Goal` only prepare/control the native `/goal` persistence bridge. They do not replace Team, QA, DB, or other implementation routes; use the selected execution route for the actual work and verification. Context7 is only needed for Goal when external API/library documentation becomes relevant.
|
|
268
|
-
|
|
269
|
-
Use `$Computer-Use` or `$CU` inside Codex App when the task specifically needs Codex Computer Use speed for UI/browser/visual work. This lane intentionally skips Team debate, QA-LOOP clarification, subagents, and upfront TriWiki refresh. It still requires Codex Computer Use as the evidence source, and it defers TriWiki refresh/validate plus Honest Mode to the final closeout. SKS does not install a generated skill named `computer-use`, because that name is reserved for the first-party Codex Computer Use plugin; use `$CU` or `$computer-use-fast` from the SKS picker for the SKS route, and use `@Computer` for the OpenAI plugin.
|
|
234
|
+
Clarification asks only for ambiguity that changes execution; predictable defaults are inferred and sealed. `sks skill-dream` records cheap counters and periodically writes advisory skill reports. `$Goal` controls native `/goal` persistence without replacing the selected execution route. `$Computer-Use` / `$CU` is the fast Codex Computer Use lane for UI/browser/visual work.
|
|
270
235
|
|
|
271
236
|
### Create A Presentation
|
|
272
237
|
|
|
@@ -274,9 +239,7 @@ Use `$Computer-Use` or `$CU` inside Codex App when the task specifically needs C
|
|
|
274
239
|
$PPT create a customer proposal deck as HTML/PDF
|
|
275
240
|
```
|
|
276
241
|
|
|
277
|
-
`$PPT` seals presentation
|
|
278
|
-
|
|
279
|
-
Design references do not compete with each other. `design.md` is the design decision SSOT; if it is missing, SKS uses `docs/Design-Sys-Prompt.md` to build or project the system. getdesign.md, official getdesign docs, and curated DESIGN.md examples from `VoltAgent/awesome-design-md` are source inputs that get fused into `design.md` or route-local `$PPT` style tokens. `$PPT` ignores installed design skills and MCP servers that are not in the route allowlist; generic design skills such as `design-artifact-expert`, `design-ui-editor`, and `design-system-builder` are not automatically used just because they are installed. This is an anti-AI-like-design guard: `$PPT` must ground visual choices in audience, source material, getdesign reference, and the design SSOT instead of freeform cards, gradients, and vague SaaS styling.
|
|
242
|
+
`$PPT` seals presentation context before artifact work and grounds design in `design.md`, getdesign inputs, and source material.
|
|
280
243
|
|
|
281
244
|
## Codex App Usage
|
|
282
245
|
|
|
@@ -514,7 +477,7 @@ node ./bin/sks.mjs --version
|
|
|
514
477
|
npm install -g .
|
|
515
478
|
```
|
|
516
479
|
|
|
517
|
-
If
|
|
480
|
+
If stale, reinstall globally from the repo or npm.
|
|
518
481
|
|
|
519
482
|
### tmux is missing
|
|
520
483
|
|
|
@@ -523,7 +486,7 @@ sks deps install tmux
|
|
|
523
486
|
sks tmux check
|
|
524
487
|
```
|
|
525
488
|
|
|
526
|
-
Install tmux from [tmux.dev/download](https://www.tmux.dev/download) or
|
|
489
|
+
Install tmux from [tmux.dev/download](https://www.tmux.dev/download) or `brew install tmux` on macOS, then re-run the check.
|
|
527
490
|
|
|
528
491
|
### Codex App tools are missing
|
|
529
492
|
|
|
@@ -532,9 +495,7 @@ sks codex-app check
|
|
|
532
495
|
codex mcp list
|
|
533
496
|
```
|
|
534
497
|
|
|
535
|
-
Codex App workflows need the app installed.
|
|
536
|
-
|
|
537
|
-
SKS setup removes old SKS-generated `computer-use` skills from `.agents/skills` so they cannot shadow the first-party Computer Use plugin. If a running Codex App thread was opened before setup or upgrade, start a fresh thread and invoke `@Computer` or Browser again so the host reloads plugin tools.
|
|
498
|
+
Codex App workflows need the app installed. UI/browser evidence requires first-party Codex Computer Use, and generated raster/image-review evidence requires real `$imagegen`/`gpt-image-2` output. After setup/upgrade, start a fresh thread so Codex reloads plugin tools.
|
|
538
499
|
|
|
539
500
|
### Setup is blocked by another harness
|
|
540
501
|
|
|
@@ -543,7 +504,7 @@ sks conflicts check
|
|
|
543
504
|
sks conflicts prompt
|
|
544
505
|
```
|
|
545
506
|
|
|
546
|
-
OMX/DCodex conflicts
|
|
507
|
+
OMX/DCodex conflicts block setup/doctor until the user approves cleanup.
|
|
547
508
|
|
|
548
509
|
### The route is stuck or a final hook keeps reopening
|
|
549
510
|
|
|
@@ -554,7 +515,7 @@ sks team lane latest --agent parent_orchestrator --follow
|
|
|
554
515
|
sks wiki validate .sneakoscope/wiki/context-pack.json
|
|
555
516
|
```
|
|
556
517
|
|
|
557
|
-
Finalization requires
|
|
518
|
+
Finalization requires evidence, valid Team cleanup artifacts, reflection when required, and Honest Mode.
|
|
558
519
|
|
|
559
520
|
## Development And Release
|
|
560
521
|
|
|
@@ -569,27 +530,7 @@ npm run sizecheck
|
|
|
569
530
|
npm run release:check
|
|
570
531
|
```
|
|
571
532
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
Dry-run publish:
|
|
575
|
-
|
|
576
|
-
```sh
|
|
577
|
-
npm run publish:dry
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
`publish:dry` proves the local package is packable. It does not prove npm ownership, OTP, or registry publish permission.
|
|
581
|
-
|
|
582
|
-
## Documentation Style
|
|
583
|
-
|
|
584
|
-
This README follows a common open-source CLI shape:
|
|
585
|
-
|
|
586
|
-
- quick start first
|
|
587
|
-
- explicit install paths
|
|
588
|
-
- separate CLI and app/plugin usage
|
|
589
|
-
- command examples before internal architecture
|
|
590
|
-
- troubleshooting and release checks near the end
|
|
591
|
-
|
|
592
|
-
That shape mirrors how projects such as `rdme` and Vite separate quick start, setup/configuration, and CLI usage while keeping copy-ready commands visible.
|
|
533
|
+
`release:check` runs audit, changelog, syntax, selftest, size, and registry checks.
|
|
593
534
|
|
|
594
535
|
## License
|
|
595
536
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.67",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"packcheck": "find bin src scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
|
|
38
38
|
"changelog:check": "node ./scripts/changelog-check.mjs",
|
|
39
39
|
"sizecheck": "node ./scripts/sizecheck.mjs",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"publish:
|
|
43
|
-
"
|
|
40
|
+
"registry:check": "node ./scripts/release-registry-check.mjs",
|
|
41
|
+
"release:check": "npm run repo-audit && npm run changelog:check && npm run packcheck && npm run selftest && npm run sizecheck && npm run registry:check",
|
|
42
|
+
"publish:dry": "npm run release:check && npm --cache /tmp/sks-npm-cache publish --dry-run --registry https://registry.npmjs.org/ --access public",
|
|
43
|
+
"publish:npm": "npm --cache /tmp/sks-npm-cache publish --registry https://registry.npmjs.org/ --access public",
|
|
44
|
+
"prepublishOnly": "npm run release:check && node ./scripts/release-registry-check.mjs --require-unpublished"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|
|
46
47
|
"sneakoscope",
|
|
@@ -17,6 +17,7 @@ export async function postinstall({ bootstrap }) {
|
|
|
17
17
|
await postinstallHarnessConflictNotice(conflictScan);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
const codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
|
|
20
21
|
console.log('\nSKS installed.');
|
|
21
22
|
const shim = await ensureSksCommandDuringInstall();
|
|
22
23
|
if (shim.status === 'present') console.log(`SKS command: available (${shim.command}).`);
|
|
@@ -54,9 +55,11 @@ export async function postinstall({ bootstrap }) {
|
|
|
54
55
|
if (bootstrapDecision.run) {
|
|
55
56
|
console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
|
|
56
57
|
await runPostinstallBootstrap(installRoot, bootstrap);
|
|
58
|
+
await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot);
|
|
57
59
|
await reportPostinstallCodexLbAuth();
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
62
|
+
await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot);
|
|
60
63
|
await reportPostinstallCodexLbAuth();
|
|
61
64
|
console.log('\nNext:');
|
|
62
65
|
console.log(' sks bootstrap');
|
|
@@ -141,6 +144,28 @@ export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
|
|
|
141
144
|
return path.join(home, '.codex', 'sks-codex-lb.env');
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
|
|
148
|
+
const configPath = codexLbConfigPath(home);
|
|
149
|
+
const envPath = codexLbEnvPath(home);
|
|
150
|
+
const config = await readText(configPath, '');
|
|
151
|
+
if (!hasTopLevelCodexLbSelected(config)) return null;
|
|
152
|
+
const baseUrl = codexLbProviderBaseUrl(config);
|
|
153
|
+
if (!baseUrl) return null;
|
|
154
|
+
if (!parseCodexLbEnvKey(await readText(envPath, ''))) return null;
|
|
155
|
+
return { config_path: configPath, env_path: envPath, base_url: baseUrl };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
|
|
159
|
+
if (!snapshot?.base_url) return { status: 'skipped', reason: 'no_snapshot' };
|
|
160
|
+
const current = await readText(snapshot.config_path, '');
|
|
161
|
+
if (hasTopLevelCodexLbSelected(current) && codexLbProviderBaseUrl(current)) {
|
|
162
|
+
return { status: 'present', config_path: snapshot.config_path };
|
|
163
|
+
}
|
|
164
|
+
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
|
|
165
|
+
await writeTextAtomic(snapshot.config_path, next);
|
|
166
|
+
return { status: 'restored', config_path: snapshot.config_path };
|
|
167
|
+
}
|
|
168
|
+
|
|
144
169
|
export function normalizeCodexLbBaseUrl(input = '') {
|
|
145
170
|
let host = String(input || '').trim();
|
|
146
171
|
if (!host) host = 'http://127.0.0.1:2455';
|
|
@@ -189,6 +214,16 @@ export async function codexLbStatus(opts = {}) {
|
|
|
189
214
|
};
|
|
190
215
|
}
|
|
191
216
|
|
|
217
|
+
function hasTopLevelCodexLbSelected(text = '') {
|
|
218
|
+
const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
|
|
219
|
+
return /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevel);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function codexLbProviderBaseUrl(text = '') {
|
|
223
|
+
const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
|
|
224
|
+
return block.match(/(^|\n)\s*base_url\s*=\s*"([^"]+)"/)?.[2] || '';
|
|
225
|
+
}
|
|
226
|
+
|
|
192
227
|
export async function repairCodexLbAuth(opts = {}) {
|
|
193
228
|
const status = await codexLbStatus(opts);
|
|
194
229
|
if (!status.ok) {
|
|
@@ -869,6 +904,9 @@ export async function selftestCodexLb(tmp) {
|
|
|
869
904
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
870
905
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
871
906
|
if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
|
|
907
|
+
await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
|
|
908
|
+
const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
909
|
+
if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest failed: initProject repair lost codex-lb provider config or exposed the stored key');
|
|
872
910
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
|
|
873
911
|
const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
|
|
874
912
|
try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
|
|
@@ -918,7 +956,12 @@ export async function selftestCodexLb(tmp) {
|
|
|
918
956
|
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
919
957
|
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
920
958
|
});
|
|
921
|
-
await postinstall({
|
|
959
|
+
await postinstall({
|
|
960
|
+
bootstrap: async () => {
|
|
961
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
962
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n\n[features]\nhooks = true\n');
|
|
963
|
+
}
|
|
964
|
+
});
|
|
922
965
|
} finally {
|
|
923
966
|
for (const key of postinstallEnvKeys) {
|
|
924
967
|
if (postinstallEnvBefore[key] === undefined) delete process.env[key];
|
|
@@ -926,8 +969,10 @@ export async function selftestCodexLb(tmp) {
|
|
|
926
969
|
}
|
|
927
970
|
}
|
|
928
971
|
const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
972
|
+
const codexLbPostBootstrapConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
929
973
|
const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
930
974
|
if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest failed: postinstall did not repair codex-lb auth after bootstrap drift');
|
|
975
|
+
if (!codexLbPostBootstrapConfig.includes('model_provider = "codex-lb"') || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest failed: postinstall did not restore codex-lb provider config after bootstrap drift');
|
|
931
976
|
const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
|
|
932
977
|
await ensureDir(codexLbContext7Bin);
|
|
933
978
|
await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
package/src/cli/main.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { sealContract, validateAnswers } from '../core/decision-contract.mjs';
|
|
|
12
12
|
import { buildQaLoopQuestionSchema, buildQaLoopPrompt, defaultQaGate, evaluateQaGate, isQaReportFilename, qaStatus, writeMockQaResult, writeQaLoopArtifacts } from '../core/qa-loop.mjs';
|
|
13
13
|
import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-question-guard.mjs';
|
|
14
14
|
import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
|
|
15
|
-
import { emitHook } from '../core/hooks-runtime.mjs';
|
|
15
|
+
import { emitHook, selftestCodexCommitHooks } from '../core/hooks-runtime.mjs';
|
|
16
16
|
import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
|
|
17
17
|
import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
|
|
18
18
|
import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
|
|
@@ -2603,15 +2603,16 @@ async function selftest() {
|
|
|
2603
2603
|
const codexAppQuickRefExists = await exists(path.join(tmp, '.codex', 'SNEAKOSCOPE.md'));
|
|
2604
2604
|
if (!codexAppQuickRefExists) throw new Error('selftest failed: Codex App quick reference missing');
|
|
2605
2605
|
const codexAppQuickRefText = await safeReadText(path.join(tmp, '.codex', 'SNEAKOSCOPE.md'));
|
|
2606
|
-
if (!codexAppQuickRefText.includes('dollar-commands')) throw new Error('selftest failed:
|
|
2607
|
-
if (!codexAppQuickRefText.includes('Context Tracking') || !codexAppQuickRefText.includes('TriWiki')) throw new Error('selftest failed:
|
|
2608
|
-
if (!codexAppQuickRefText.includes('Before each route phase') || !codexAppQuickRefText.includes('every stage')) throw new Error('selftest failed:
|
|
2606
|
+
if (!codexAppQuickRefText.includes('dollar-commands')) throw new Error('selftest failed: quickref commands');
|
|
2607
|
+
if (!codexAppQuickRefText.includes('Context Tracking') || !codexAppQuickRefText.includes('TriWiki')) throw new Error('selftest failed: quickref TriWiki');
|
|
2608
|
+
if (!codexAppQuickRefText.includes('Before each route phase') || !codexAppQuickRefText.includes('every stage')) throw new Error('selftest failed: quickref stage policy');
|
|
2609
2609
|
for (const { command } of DOLLAR_COMMANDS) {
|
|
2610
2610
|
if (!codexAppQuickRefText.includes(command)) throw new Error(`selftest failed: Codex App quick reference missing ${command}`);
|
|
2611
2611
|
}
|
|
2612
2612
|
const hookGoalTmp = tmpdir();
|
|
2613
2613
|
await initProject(hookGoalTmp, {});
|
|
2614
2614
|
const hookBin = path.join(packageRoot(), 'bin', 'sks.mjs');
|
|
2615
|
+
await selftestCodexCommitHooks();
|
|
2615
2616
|
const hookImageUxTmp = tmpdir();
|
|
2616
2617
|
await initProject(hookImageUxTmp, {});
|
|
2617
2618
|
const hookImageUxPayload = JSON.stringify({ cwd: hookImageUxTmp, prompt: '$Image-UX-Review localhost 화면을 gpt-image-2 콜아웃 리뷰로 검수해줘' });
|
|
@@ -3279,7 +3280,7 @@ async function selftest() {
|
|
|
3279
3280
|
if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
|
|
3280
3281
|
if (!roleTeamPlan.roster.analysis_team.every((agent) => tmuxTeam.agents.some((entry) => entry.agent === agent.id))) throw new Error('selftest failed: Team tmux view collapsed numbered analysis scout lanes');
|
|
3281
3282
|
if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
|
|
3282
|
-
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== '
|
|
3283
|
+
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'main-vertical' || tmuxTeam.split_ui?.right_side_only !== true || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: tmux UI');
|
|
3283
3284
|
if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
|
|
3284
3285
|
if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
|
|
3285
3286
|
if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
|
|
@@ -3288,7 +3289,7 @@ async function selftest() {
|
|
|
3288
3289
|
await ensureDir(fakeTmuxDir);
|
|
3289
3290
|
const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
|
|
3290
3291
|
const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
|
|
3291
|
-
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst
|
|
3292
|
+
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('node:fs'),e=process.env,c=process.argv[2];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,process.argv.slice(2).join(' ')+'\\n');if(['has-session','kill-session','kill-pane','set-option','select-pane','select-layout','resize-window','set-window-option','set-hook'].includes(c))process.exit(0);if(c==='new-session'){console.log('%1');process.exit(0)}if(c==='split-window'){console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');process.exit(0)}if(c==='list-windows'){console.log('@1');process.exit(0)}if(c==='display-message'){console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');process.exit(0)}if(c==='list-panes'){console.log(e.SKS_FAKE_TMUX_LIST||'');process.exit(0)}process.exit(0);\n`);
|
|
3292
3293
|
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3293
3294
|
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3294
3295
|
const previousPath = process.env.PATH;
|
|
@@ -3327,20 +3328,22 @@ async function selftest() {
|
|
|
3327
3328
|
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3328
3329
|
});
|
|
3329
3330
|
const cockpitOpenLog = await safeReadText(fakeTmuxLog);
|
|
3330
|
-
if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -t
|
|
3331
|
+
if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || cockpitOpen.main_pane_id !== '%1' || cockpitOpen.relayout?.layout_name !== 'main-vertical' || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -h -t %1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-pane -t %1') || !cockpitOpenLog.includes('select-layout -t @1 main-vertical')) throw new Error('selftest failed: split');
|
|
3331
3332
|
await writeTextAtomic(fakeTmuxLog, '');
|
|
3332
|
-
|
|
3333
|
+
const fakePanes = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
|
|
3334
|
+
process.env.SKS_FAKE_TMUX_LIST = fakePanes;
|
|
3333
3335
|
const cockpitClose = await reconcileTmuxTeamCockpit({
|
|
3334
3336
|
root: tmp,
|
|
3335
3337
|
missionId: teamId,
|
|
3336
3338
|
plan: roleTeamPlan,
|
|
3337
|
-
dashboard: { agents: {
|
|
3338
|
-
control: { status: '
|
|
3339
|
+
dashboard: { agents: { analysis_scout_2: { status: 'assigned' } } },
|
|
3340
|
+
control: { status: 'cleanup_requested' },
|
|
3341
|
+
close: true,
|
|
3339
3342
|
tmux: { bin: fakeTmuxBin },
|
|
3340
3343
|
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3341
3344
|
});
|
|
3342
3345
|
const cockpitCloseLog = await safeReadText(fakeTmuxLog);
|
|
3343
|
-
if (!cockpitClose.ok || cockpitClose.closed_lane_count !==
|
|
3346
|
+
if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 2 || !cockpitCloseLog.includes('kill-pane -t %81') || !cockpitCloseLog.includes('kill-pane -t %82') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest failed: cleanup');
|
|
3344
3347
|
delete process.env.SKS_FAKE_TMUX_DISPLAY;
|
|
3345
3348
|
delete process.env.SKS_FAKE_TMUX_LIST;
|
|
3346
3349
|
delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
|
|
@@ -3443,6 +3446,7 @@ async function selftest() {
|
|
|
3443
3446
|
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
|
|
3444
3447
|
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
|
|
3445
3448
|
if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest failed: team agent lane missing event context');
|
|
3449
|
+
if (!teamLane.includes('## Live Chat') || !teamLane.includes('selftest mapped repo slice') || teamLane.includes('## Global Tail')) throw new Error('selftest failed:cht');
|
|
3446
3450
|
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 });
|
|
3447
3451
|
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');
|
|
3448
3452
|
await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
|
|
@@ -2052,7 +2052,7 @@ async function teamCommand(sub, args) {
|
|
|
2052
2052
|
missionId: id,
|
|
2053
2053
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
|
2054
2054
|
reason: message,
|
|
2055
|
-
finalMessage: 'Team cleanup event received.
|
|
2055
|
+
finalMessage: 'Team cleanup event received. Follow loops stop and managed tmux Team panes are closed when reachable.'
|
|
2056
2056
|
}).then(() => cleanupTmuxTeamView({ root, missionId: id, closeSession: flag(args, '--close-session') || flag(args, '--close') })).catch((err) => ({ ok: false, reason: err.message || 'tmux cleanup failed' }))
|
|
2057
2057
|
: null;
|
|
2058
2058
|
if (flag(args, '--json')) return console.log(JSON.stringify(record, null, 2));
|
|
@@ -2091,7 +2091,7 @@ async function teamCommand(sub, args) {
|
|
|
2091
2091
|
missionId: id,
|
|
2092
2092
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
|
2093
2093
|
reason: readFlagValue(args, '--reason', 'Team session ended; clean up live follow panes.'),
|
|
2094
|
-
finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; managed tmux Team panes
|
|
2094
|
+
finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; managed tmux Team panes are closed when reachable.'
|
|
2095
2095
|
});
|
|
2096
2096
|
await appendTeamEvent(dir, {
|
|
2097
2097
|
agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
|
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.67';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256, packageRoot } from './fsx.mjs';
|
|
2
|
+
import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256, packageRoot, tmpdir } from './fsx.mjs';
|
|
3
3
|
import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.mjs';
|
|
4
4
|
import { missionDir, setCurrent, stateFile } from './mission.mjs';
|
|
5
5
|
import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.mjs';
|
|
@@ -15,9 +15,11 @@ const TEAM_DIGEST_CONTEXT_CHARS = 1600;
|
|
|
15
15
|
const TEAM_DIGEST_SYSTEM_CHARS = 260;
|
|
16
16
|
const STOP_REPEAT_GUARD_ARTIFACT = 'stop-hook-repeat-guard.json';
|
|
17
17
|
const LIGHT_ROUTE_STOP_ARTIFACT = 'light-route-stop.json';
|
|
18
|
+
const CODEX_GIT_ACTION_STOP_ARTIFACT = 'codex-git-action-stop-bypass.json';
|
|
18
19
|
const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
|
|
19
20
|
const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
|
|
20
21
|
const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
|
|
22
|
+
const CODEX_GIT_ACTION_STOP_TTL_MS = 5 * 60 * 1000;
|
|
21
23
|
|
|
22
24
|
async function loadHookPayload() {
|
|
23
25
|
const raw = await readStdin();
|
|
@@ -143,6 +145,13 @@ function clientModelCandidates(value, depth = 0) {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
async function hookUserPrompt(root, state, payload, noQuestion) {
|
|
148
|
+
if (looksLikeCodexGitCommitMessageGeneration(payload)) {
|
|
149
|
+
await armCodexGitActionStopBypass(root, payload).catch(() => null);
|
|
150
|
+
return {
|
|
151
|
+
continue: true,
|
|
152
|
+
systemMessage: 'SKS: Codex App git commit message generation bypassed route gates.'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
146
155
|
if (!noQuestion) {
|
|
147
156
|
const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
|
|
148
157
|
const madSksConfirmation = await handleMadSksUserConfirmation(root, state, prompt);
|
|
@@ -348,6 +357,12 @@ function clarificationPauseBlockReason(state = {}) {
|
|
|
348
357
|
|
|
349
358
|
async function hookStop(root, state, payload, noQuestion) {
|
|
350
359
|
const last = extractLastMessage(payload);
|
|
360
|
+
if (await consumeCodexGitActionStopBypass(root, payload)) {
|
|
361
|
+
return {
|
|
362
|
+
continue: true,
|
|
363
|
+
systemMessage: 'SKS: Codex App git commit message generation accepted without route finalization gates.'
|
|
364
|
+
};
|
|
365
|
+
}
|
|
351
366
|
if (!noQuestion && (hasDfixLightCompletion(last) || await consumeLightRouteStop(root, payload))) {
|
|
352
367
|
return {
|
|
353
368
|
continue: true,
|
|
@@ -422,6 +437,79 @@ function explicitConversationId(payload = {}) {
|
|
|
422
437
|
return payload.conversation_id || payload.thread_id || payload.session_id || payload.chat_id || null;
|
|
423
438
|
}
|
|
424
439
|
|
|
440
|
+
function looksLikeCodexGitCommitMessageGeneration(payload = {}) {
|
|
441
|
+
const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
|
|
442
|
+
const haystack = [
|
|
443
|
+
payload.action,
|
|
444
|
+
payload.intent,
|
|
445
|
+
payload.operation,
|
|
446
|
+
payload.permission,
|
|
447
|
+
payload.description,
|
|
448
|
+
payload.kind,
|
|
449
|
+
payload.type,
|
|
450
|
+
payload.feature,
|
|
451
|
+
payload.tool_name,
|
|
452
|
+
payload.toolName,
|
|
453
|
+
payload.source,
|
|
454
|
+
payload.event,
|
|
455
|
+
payload.hook,
|
|
456
|
+
payload.hook_name,
|
|
457
|
+
payload.input?.action,
|
|
458
|
+
payload.input?.intent,
|
|
459
|
+
payload.input?.operation,
|
|
460
|
+
payload.input?.feature,
|
|
461
|
+
payload.input?.source,
|
|
462
|
+
payload.metadata?.action,
|
|
463
|
+
payload.metadata?.intent,
|
|
464
|
+
payload.metadata?.operation,
|
|
465
|
+
payload.metadata?.feature,
|
|
466
|
+
payload.metadata?.source
|
|
467
|
+
].filter(Boolean).join(' ');
|
|
468
|
+
const appSignal = /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|codex_git_commit)\b/i.test(haystack)
|
|
469
|
+
|| /커밋\s*메시지\s*생성/i.test(haystack);
|
|
470
|
+
const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
|
|
471
|
+
|| /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
|
|
472
|
+
|| /커밋\s*메시지\s*생성/i.test(prompt);
|
|
473
|
+
if (!appSignal && !promptSignal) return false;
|
|
474
|
+
if (appSignal) return true;
|
|
475
|
+
return !looksLikeUserImplementationRequest(prompt);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function looksLikeUserImplementationRequest(text = '') {
|
|
479
|
+
return /(fix|bug|broken|error|issue|implement|change|update|repair|수정|버그|오류|에러|문제|고쳐|고치|해결|변경|수리|패치|안생기|안\s*생기)/i.test(String(text || ''));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function armCodexGitActionStopBypass(root, payload = {}) {
|
|
483
|
+
const nowMs = Date.now();
|
|
484
|
+
const record = {
|
|
485
|
+
schema_version: 1,
|
|
486
|
+
route: 'codex_git_commit',
|
|
487
|
+
pending_stop_bypass: true,
|
|
488
|
+
conversation_id: conversationId(payload),
|
|
489
|
+
created_at: nowIso(),
|
|
490
|
+
expires_at: new Date(nowMs + CODEX_GIT_ACTION_STOP_TTL_MS).toISOString()
|
|
491
|
+
};
|
|
492
|
+
await writeJsonAtomic(path.join(root, '.sneakoscope', 'state', CODEX_GIT_ACTION_STOP_ARTIFACT), record);
|
|
493
|
+
return record;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function consumeCodexGitActionStopBypass(root, payload = {}) {
|
|
497
|
+
const file = path.join(root, '.sneakoscope', 'state', CODEX_GIT_ACTION_STOP_ARTIFACT);
|
|
498
|
+
const record = await readJson(file, null).catch(() => null);
|
|
499
|
+
if (!record?.pending_stop_bypass) return false;
|
|
500
|
+
if (record.route !== 'codex_git_commit') return false;
|
|
501
|
+
const expiresMs = Date.parse(record.expires_at || '');
|
|
502
|
+
if (!Number.isFinite(expiresMs) || expiresMs < Date.now()) return false;
|
|
503
|
+
const currentConversation = conversationId(payload);
|
|
504
|
+
if (record.conversation_id && explicitConversationId(payload) && record.conversation_id !== currentConversation) return false;
|
|
505
|
+
await writeJsonAtomic(file, {
|
|
506
|
+
...record,
|
|
507
|
+
pending_stop_bypass: false,
|
|
508
|
+
consumed_at: nowIso()
|
|
509
|
+
}).catch(() => null);
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
425
513
|
async function finalizationRepeatDecision(root, state = {}, payload = {}, reason = '', kind = 'finalization') {
|
|
426
514
|
const now = nowIso();
|
|
427
515
|
const guardPath = path.join(root, '.sneakoscope', 'state', STOP_REPEAT_GUARD_ARTIFACT);
|
|
@@ -812,6 +900,27 @@ export async function emitHook(name) {
|
|
|
812
900
|
process.stdout.write(`${JSON.stringify(normalizeHookResult(name, result))}\n`);
|
|
813
901
|
}
|
|
814
902
|
|
|
903
|
+
export async function selftestCodexCommitHooks() {
|
|
904
|
+
const root = tmpdir();
|
|
905
|
+
const hookBin = path.join(packageRoot(), 'bin', 'sks.mjs');
|
|
906
|
+
const env = { SKS_DISABLE_UPDATE_CHECK: '1' };
|
|
907
|
+
const setup = await runProcess(process.execPath, [hookBin, 'setup', '--install-scope', 'project'], { cwd: root, env, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
908
|
+
if (setup.code !== 0) throw new Error(`selftest failed: commit setup ${setup.code}: ${setup.stderr}`);
|
|
909
|
+
const runHook = (name, payload) => runProcess(process.execPath, [hookBin, 'hook', name], { cwd: root, input: JSON.stringify({ cwd: root, ...payload }), env, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
910
|
+
const id = 'commit-selftest';
|
|
911
|
+
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
|
+
if (hook.code !== 0) throw new Error(`selftest failed: commit hook ${hook.code}: ${hook.stderr}`);
|
|
913
|
+
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');
|
|
915
|
+
const stop = await runHook('stop', { conversation_id: id, last_assistant_message: 'Fix Codex App commit message hook bypass' });
|
|
916
|
+
if (stop.code !== 0) throw new Error(`selftest failed: commit stop ${stop.code}: ${stop.stderr}`);
|
|
917
|
+
const stopJson = JSON.parse(stop.stdout);
|
|
918
|
+
if (stopJson.decision === 'block' || !String(stopJson.systemMessage || '').includes('accepted without route finalization')) throw new Error('selftest failed: commit stop bypass');
|
|
919
|
+
const userHook = await runHook('user-prompt-submit', { prompt: '[커밋 메시지를 생성하지 못했습니다.] 코덱스 앱에서 이 버그 수정해줘' });
|
|
920
|
+
if (userHook.code !== 0) throw new Error(`selftest failed: user commit hook ${userHook.code}: ${userHook.stderr}`);
|
|
921
|
+
if (!JSON.parse(userHook.stdout).hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: user prompt route');
|
|
922
|
+
}
|
|
923
|
+
|
|
815
924
|
function normalizeHookResult(name, result = {}) {
|
|
816
925
|
const eventName = codexHookEventName(name);
|
|
817
926
|
const out = { ...result };
|
package/src/core/init.mjs
CHANGED
|
@@ -112,6 +112,7 @@ export async function initProject(root, opts = {}) {
|
|
|
112
112
|
const sine = path.join(root, '.sneakoscope');
|
|
113
113
|
const manifestPath = path.join(sine, 'manifest.json');
|
|
114
114
|
const previousManifest = await readJson(manifestPath, null);
|
|
115
|
+
const preRepairCodexConfig = opts.repair ? await readText(path.join(root, '.codex', 'config.toml'), '') : '';
|
|
115
116
|
if (opts.repair) {
|
|
116
117
|
const repair = await repairSksGeneratedArtifacts(root, { resetState: Boolean(opts.resetState) });
|
|
117
118
|
if (repair.removed.length) created.push(`repaired generated SKS files (${repair.removed.length})`);
|
|
@@ -646,7 +647,7 @@ function upsertTomlTable(text, table, block) {
|
|
|
646
647
|
}
|
|
647
648
|
|
|
648
649
|
const generatedCodexConfigPath = path.join(root, '.codex', 'config.toml');
|
|
649
|
-
const existingCodexConfig = await readText(generatedCodexConfigPath, '');
|
|
650
|
+
const existingCodexConfig = await readText(generatedCodexConfigPath, '') || preRepairCodexConfig;
|
|
650
651
|
const managedCodexConfig = await mergeGlobalCodexConfigIfAvailable(
|
|
651
652
|
mergeManagedCodexConfigToml(existingCodexConfig),
|
|
652
653
|
generatedCodexConfigPath
|
package/src/core/team-live.mjs
CHANGED
|
@@ -498,7 +498,7 @@ export async function requestTeamSessionCleanup(dir, opts = {}) {
|
|
|
498
498
|
cleanup_requested_at: opts.ts || nowIso(),
|
|
499
499
|
cleanup_requested_by: opts.agent || 'parent_orchestrator',
|
|
500
500
|
cleanup_reason: opts.reason || 'Team session cleanup requested.',
|
|
501
|
-
final_message: opts.finalMessage || 'Team session ended. Lane follow loops
|
|
501
|
+
final_message: opts.finalMessage || 'Team session ended. Lane/watch follow loops stop after this summary; managed tmux Team panes are closed when reachable.'
|
|
502
502
|
};
|
|
503
503
|
await writeJsonAtomic(files.control, next);
|
|
504
504
|
return next;
|
|
@@ -518,7 +518,7 @@ export function renderTeamCleanupSummary(control = {}) {
|
|
|
518
518
|
`Requested by: ${control.cleanup_requested_by || 'unknown'}`,
|
|
519
519
|
`Reason: ${control.cleanup_reason || 'Team session cleanup requested.'}`,
|
|
520
520
|
'',
|
|
521
|
-
control.final_message || 'Team session ended. managed tmux Team panes
|
|
521
|
+
control.final_message || 'Team session ended. managed tmux Team panes are closed when reachable.'
|
|
522
522
|
].join('\n');
|
|
523
523
|
}
|
|
524
524
|
|
|
@@ -554,6 +554,9 @@ export async function renderTeamAgentLane(dir, opts = {}) {
|
|
|
554
554
|
const assignedTasks = runtimeTasks.filter((task) => aliasSet.has(task?.worker) || aliasSet.has(task?.agent_hint));
|
|
555
555
|
const agentEvents = parsedWindow.filter((event) => aliasSet.has(event?.agent) || aliases.some((id) => eventAddressedTo(event, id))).slice(-lines);
|
|
556
556
|
const directMessages = parsedWindow.filter((event) => event?.type === 'message' && aliases.some((id) => eventAddressedTo(event, id))).slice(-lines);
|
|
557
|
+
const chatEvents = uniqueTranscriptEvents([...agentEvents, ...directMessages])
|
|
558
|
+
.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || '')))
|
|
559
|
+
.slice(-lines);
|
|
557
560
|
const laneStyle = teamLaneTextStyle(agent);
|
|
558
561
|
return [
|
|
559
562
|
`# SKS Team Agent Lane`,
|
|
@@ -573,11 +576,8 @@ export async function renderTeamAgentLane(dir, opts = {}) {
|
|
|
573
576
|
`## Assigned Runtime Tasks`,
|
|
574
577
|
...(runtime ? formatRuntimeTasks(assignedTasks) : ['- team-runtime-tasks.json not available yet.']),
|
|
575
578
|
'',
|
|
576
|
-
`##
|
|
577
|
-
...(
|
|
578
|
-
'',
|
|
579
|
-
`## Direct Messages`,
|
|
580
|
-
...(directMessages.length ? directMessages.map(formatTranscriptEvent) : ['- No direct or broadcast messages in the bounded tail.']),
|
|
579
|
+
`## Live Chat`,
|
|
580
|
+
...(chatEvents.length ? chatEvents.map((event) => formatChatTranscriptEvent(event, aliases[0])) : ['- waiting for live agent messages...']),
|
|
581
581
|
opts.includeGlobalTail ? '' : null,
|
|
582
582
|
opts.includeGlobalTail ? `## Global Tail` : null,
|
|
583
583
|
...(opts.includeGlobalTail
|
|
@@ -608,11 +608,11 @@ export async function renderTeamWatch(dir, opts = {}) {
|
|
|
608
608
|
'## Split-Screen Map',
|
|
609
609
|
'- This overview pane follows the whole mission transcript.',
|
|
610
610
|
'- Run `sks team open-tmux ...` to materialize or reopen the split-pane Team tmux view for an existing mission.',
|
|
611
|
-
'- Inside an SKS-owned tmux session, Team panes are reconciled in the current window
|
|
612
|
-
'- Neighbor tmux panes follow individual `sks team lane ... --agent <name>` views.',
|
|
611
|
+
'- Inside an SKS-owned tmux session, Team panes are reconciled in the current window with the Codex pane on the left and Team lanes stacked on the right.',
|
|
612
|
+
'- Neighbor tmux panes follow individual `sks team lane ... --agent <name>` chat-style views.',
|
|
613
613
|
'- Use `sks team event ...` to mirror scout, debate, executor, review, and verification status into the live panes.',
|
|
614
614
|
'- Use `sks team message ... --from <agent> --to <agent|all>` for bounded inter-agent communication in transcript/lane views.',
|
|
615
|
-
'- Use `sks team cleanup-tmux ...` at session end; follow loops show cleanup and
|
|
615
|
+
'- Use `sks team cleanup-tmux ...` at session end; follow loops show cleanup and managed Team panes close when reachable.',
|
|
616
616
|
'',
|
|
617
617
|
'## Cockpit Views',
|
|
618
618
|
'- Mission / Goal | Agents | MultiAgentV2 | Work Orders | Skills | Memory Health | Forget Queue',
|
|
@@ -697,6 +697,29 @@ function formatTranscriptEvent(event = {}) {
|
|
|
697
697
|
return `- ${parts.join(' ')}: ${String(event.message || '').slice(0, 500)}${suffix}`;
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
function uniqueTranscriptEvents(events = []) {
|
|
701
|
+
const seen = new Set();
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const event of events) {
|
|
704
|
+
const key = event?.raw || [event?.ts, event?.agent, event?.to, event?.type, event?.message].map((value) => String(value || '')).join('\t');
|
|
705
|
+
if (seen.has(key)) continue;
|
|
706
|
+
seen.add(key);
|
|
707
|
+
out.push(event);
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function formatChatTranscriptEvent(event = {}, laneAgent = '') {
|
|
713
|
+
if (event.raw) return `- system: ${event.raw}`;
|
|
714
|
+
const from = event.agent || 'unknown';
|
|
715
|
+
const to = event.to ? ` -> ${event.to}` : '';
|
|
716
|
+
const kind = event.type && event.type !== 'message' ? ` [${event.type}]` : '';
|
|
717
|
+
const ts = event.ts ? `${event.ts} ` : '';
|
|
718
|
+
const artifact = event.artifact ? ` (${event.artifact})` : '';
|
|
719
|
+
const marker = String(from) === String(laneAgent) ? 'me' : from;
|
|
720
|
+
return `- ${ts}${marker}${to}${kind}: ${String(event.message || '').slice(0, 500)}${artifact}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
700
723
|
function eventAddressedTo(event = {}, agent = '') {
|
|
701
724
|
if (!event?.to) return false;
|
|
702
725
|
const target = String(event.to || '').trim().toLowerCase();
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -121,6 +121,7 @@ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
|
|
|
121
121
|
|
|
122
122
|
const LEGACY_TEAM_PANE_TITLE_RE = /^(?:overview: mission_overview|scout: analysis_scout|plan: (?:debate|consensus|planner|user)|exec: (?:executor|implementation|worker)|review: (?:reviewer|qa|validation)|safety:)/;
|
|
123
123
|
const GENERIC_TEAM_AGENT_IDS = new Set(['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer']);
|
|
124
|
+
const DYNAMIC_TEAM_TMUX_LAYOUT = 'main-vertical';
|
|
124
125
|
|
|
125
126
|
export function isTmuxShellSession(env = process.env) {
|
|
126
127
|
return Boolean(String(env.TMUX || '').trim());
|
|
@@ -582,14 +583,20 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
|
|
|
582
583
|
}
|
|
583
584
|
const first = normalizedPanes[0] || { cwd: root, command: 'pwd' };
|
|
584
585
|
const dimensions = currentTerminalDimensions(opts);
|
|
585
|
-
const
|
|
586
|
+
const rightSidePanes = Boolean(opts.rightSidePanes || opts.rightSideOnly);
|
|
587
|
+
const layout = tmuxLayoutName(opts.layout || (rightSidePanes ? DYNAMIC_TEAM_TMUX_LAYOUT : 'tiled'));
|
|
586
588
|
const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', dimensions.width, '-y', dimensions.height, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
|
|
587
589
|
if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
|
|
588
590
|
const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
|
|
591
|
+
let rightStackTarget = created[0].pane_id || session;
|
|
589
592
|
for (const pane of normalizedPanes.slice(1)) {
|
|
590
|
-
const
|
|
593
|
+
const direction = rightSidePanes ? (created.length === 1 ? '-h' : '-v') : (pane.vertical ? '-v' : '-h');
|
|
594
|
+
const splitTarget = rightSidePanes ? rightStackTarget : session;
|
|
595
|
+
const split = await tmuxRun(tmuxBin, ['split-window', '-t', splitTarget, direction, '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
|
|
591
596
|
if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
|
|
592
|
-
|
|
597
|
+
const newPaneId = paneId(split.stdout);
|
|
598
|
+
created.push({ pane_id: newPaneId, role: pane.role || 'lane', title: pane.title || null });
|
|
599
|
+
if (rightSidePanes && newPaneId) rightStackTarget = newPaneId;
|
|
593
600
|
await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
|
|
594
601
|
}
|
|
595
602
|
const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout });
|
|
@@ -732,6 +739,10 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
732
739
|
const desiredAgents = new Set(lanes.map((lane) => lane.agent));
|
|
733
740
|
const paneList = await listTmuxWindowPanes(tmuxBin, target.window_id);
|
|
734
741
|
if (!paneList.ok) return { ok: false, skipped: false, session: target.session, window_id: target.window_id, reason: paneList.stderr };
|
|
742
|
+
const cockpitState = await readJson(tmuxCockpitStatePath(resolvedRoot), {}).catch(() => ({}));
|
|
743
|
+
const previousCockpit = cockpitState?.missions?.[id] || {};
|
|
744
|
+
const currentPane = paneList.panes.find((pane) => pane.pane_id === target.pane_id);
|
|
745
|
+
const mainPaneId = previousCockpit.main_pane_id || (currentPane?.managed && currentPane?.mission_id === id ? null : target.pane_id);
|
|
735
746
|
const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
|
|
736
747
|
const byAgent = new Map();
|
|
737
748
|
for (const pane of managed) {
|
|
@@ -747,14 +758,19 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
747
758
|
else failed.push({ action: 'kill-pane', pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
748
759
|
}
|
|
749
760
|
}
|
|
761
|
+
const remainingManaged = managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id));
|
|
762
|
+
let rightStackTarget = remainingManaged.at(-1)?.pane_id || mainPaneId || target.window_id;
|
|
750
763
|
for (const lane of lanes) {
|
|
751
764
|
if (byAgent.has(lane.agent)) continue;
|
|
752
|
-
const
|
|
765
|
+
const firstRightPane = remainingManaged.length === 0 && opened.length === 0;
|
|
766
|
+
const direction = firstRightPane ? '-h' : '-v';
|
|
767
|
+
const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', rightStackTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
753
768
|
const pane_id = paneId(split.stdout);
|
|
754
769
|
if (split.code !== 0 || !pane_id) {
|
|
755
770
|
failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
|
|
756
771
|
continue;
|
|
757
772
|
}
|
|
773
|
+
rightStackTarget = pane_id;
|
|
758
774
|
const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
|
|
759
775
|
'@sks_team_managed': '1',
|
|
760
776
|
'@sks_mission_id': id,
|
|
@@ -766,9 +782,10 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
766
782
|
}
|
|
767
783
|
let relayout = null;
|
|
768
784
|
if (opened.length || closed.length) {
|
|
769
|
-
const
|
|
785
|
+
const selectedMain = mainPaneId ? await tmuxRun(tmuxBin, ['select-pane', '-t', mainPaneId], { timeoutMs: 5000 }) : { code: 0 };
|
|
786
|
+
const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 });
|
|
770
787
|
const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
|
|
771
|
-
relayout = { ok: tiled.code === 0 && even.code === 0,
|
|
788
|
+
relayout = { ok: selectedMain.code === 0 && tiled.code === 0 && even.code === 0, selected_main: selectedMain.code, layout: tiled.code, even: even.code, layout_name: DYNAMIC_TEAM_TMUX_LAYOUT };
|
|
772
789
|
}
|
|
773
790
|
const nextPanes = [
|
|
774
791
|
...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),
|
|
@@ -778,8 +795,10 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
778
795
|
mission_id: id,
|
|
779
796
|
session: target.session,
|
|
780
797
|
window_id: target.window_id,
|
|
781
|
-
main_pane_id: target.pane_id,
|
|
798
|
+
main_pane_id: mainPaneId || target.pane_id,
|
|
782
799
|
mode: 'current_session_dynamic_panes',
|
|
800
|
+
layout: DYNAMIC_TEAM_TMUX_LAYOUT,
|
|
801
|
+
right_side_only: true,
|
|
783
802
|
desired_lane_count: lanes.length,
|
|
784
803
|
panes: nextPanes,
|
|
785
804
|
opened,
|
|
@@ -791,7 +810,7 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
791
810
|
mode: 'current_session_dynamic_panes',
|
|
792
811
|
session: target.session,
|
|
793
812
|
window_id: target.window_id,
|
|
794
|
-
main_pane_id: target.pane_id,
|
|
813
|
+
main_pane_id: mainPaneId || target.pane_id,
|
|
795
814
|
desired_lane_count: lanes.length,
|
|
796
815
|
opened_lane_count: opened.length,
|
|
797
816
|
closed_lane_count: closed.length,
|
|
@@ -825,7 +844,8 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
825
844
|
const splitUi = {
|
|
826
845
|
mode: 'single_window_split_panes',
|
|
827
846
|
window: 'sks',
|
|
828
|
-
layout:
|
|
847
|
+
layout: DYNAMIC_TEAM_TMUX_LAYOUT,
|
|
848
|
+
right_side_only: true,
|
|
829
849
|
dynamic_resize: true,
|
|
830
850
|
window_size: 'latest',
|
|
831
851
|
resize_hooks: ['client-attached', 'client-resized'],
|
|
@@ -866,6 +886,8 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
866
886
|
mode: cockpit.mode,
|
|
867
887
|
current_session: true,
|
|
868
888
|
window_id: cockpit.window_id,
|
|
889
|
+
main_pane_id: cockpit.main_pane_id,
|
|
890
|
+
layout: cockpit.relayout?.layout_name || DYNAMIC_TEAM_TMUX_LAYOUT,
|
|
869
891
|
user_attach_command: cockpit.attach_command
|
|
870
892
|
};
|
|
871
893
|
await writeTmuxTeamRecord(launch.root, {
|
|
@@ -902,7 +924,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
902
924
|
}
|
|
903
925
|
}
|
|
904
926
|
const panes = lanes.map((lane, index) => ({ cwd: launch.root, command: lane.command, focused: index === 0, role: lane.role, title: lane.title, vertical: index > 1 }));
|
|
905
|
-
const created = await createTmuxSession(launch, panes, { layout:
|
|
927
|
+
const created = await createTmuxSession(launch, panes, { layout: DYNAMIC_TEAM_TMUX_LAYOUT, recreate: true, rightSidePanes: true });
|
|
906
928
|
result.created = Boolean(created.ok);
|
|
907
929
|
result.opened = created;
|
|
908
930
|
result.session = created.session || launch.session;
|
|
@@ -1077,7 +1099,7 @@ async function cleanupLegacyTmuxTeamSurfaces(root, missionId, opts = {}) {
|
|
|
1077
1099
|
else failed.push({ pane_id: pane.pane_id, title: pane.title, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
1078
1100
|
}
|
|
1079
1101
|
if (closed.length) {
|
|
1080
|
-
await tmuxRun(tmuxBin, ['select-layout', '-t', current.window_id,
|
|
1102
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', current.window_id, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 }).catch(() => null);
|
|
1081
1103
|
await tmuxRun(tmuxBin, ['select-layout', '-t', current.window_id, '-E'], { timeoutMs: 5000 }).catch(() => null);
|
|
1082
1104
|
}
|
|
1083
1105
|
}
|
|
@@ -1117,7 +1139,7 @@ async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
|
|
|
1117
1139
|
else failed.push({ pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
1118
1140
|
}
|
|
1119
1141
|
if (closed.length) {
|
|
1120
|
-
await tmuxRun(tmuxBin, ['select-layout', '-t', target,
|
|
1142
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', target, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 }).catch(() => null);
|
|
1121
1143
|
await tmuxRun(tmuxBin, ['select-layout', '-t', target, '-E'], { timeoutMs: 5000 }).catch(() => null);
|
|
1122
1144
|
}
|
|
1123
1145
|
return {
|