sneakoscope 1.18.3 → 1.18.4
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 +12 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/build-manifest.json +17 -9
- package/dist/cli/feature-commands.js +16 -5
- package/dist/commands/image-ux-review.d.ts +41 -7
- package/dist/commands/ppt.d.ts +41 -7
- package/dist/core/agents/agent-cleanup-executor.d.ts +47 -0
- package/dist/core/agents/agent-cleanup-executor.js +211 -0
- package/dist/core/agents/agent-command-surface.d.ts +3 -0
- package/dist/core/agents/agent-command-surface.js +4 -1
- package/dist/core/agents/agent-orchestrator.d.ts +41 -7
- package/dist/core/agents/agent-orchestrator.js +10 -2
- package/dist/core/agents/agent-proof-evidence.d.ts +19 -0
- package/dist/core/agents/agent-proof-evidence.js +27 -0
- package/dist/core/agents/agent-runner-codex-exec.js +10 -0
- package/dist/core/agents/agent-trust-report.d.ts +20 -0
- package/dist/core/agents/agent-trust-report.js +28 -2
- package/dist/core/agents/agent-work-partition.d.ts +3 -7
- package/dist/core/agents/agent-work-partition.js +11 -2
- package/dist/core/agents/intelligent-work-graph.d.ts +40 -0
- package/dist/core/agents/intelligent-work-graph.js +217 -0
- package/dist/core/agents/route-collaboration-ledger.d.ts +41 -7
- package/dist/core/agents/tmux-physical-proof.d.ts +184 -0
- package/dist/core/agents/tmux-physical-proof.js +269 -0
- package/dist/core/agents/work-partition/repo-inventory.js +1 -1
- package/dist/core/codex-hooks/codex-hook-actual-discovery.d.ts +1 -0
- package/dist/core/codex-hooks/codex-hook-actual-discovery.js +6 -2
- package/dist/core/codex-hooks/codex-hook-trust-doctor.d.ts +29 -0
- package/dist/core/codex-hooks/codex-hook-trust-doctor.js +12 -2
- package/dist/core/commands/agent-command.js +12 -5
- package/dist/core/commands/image-ux-review-command.d.ts +41 -7
- package/dist/core/commands/ppt-command.d.ts +41 -7
- package/dist/core/fsx.d.ts +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/proof/fake-real-proof-policy.d.ts +15 -0
- package/dist/core/proof/fake-real-proof-policy.js +52 -0
- package/dist/core/routes.js +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ SKS does not try to clone every other harness. It focuses on one thing: making C
|
|
|
10
10
|
|
|
11
11
|
## Current Release
|
|
12
12
|
|
|
13
|
-
SKS **1.18.
|
|
13
|
+
SKS **1.18.4** closes the runtime-truth gap left after the route-truth Dynamic Worker Pool work: real tmux mode now requires physical pane evidence from `tmux list-panes`, `tmux capture-pane`, pane-id reconciliation, and drain-close checks; real Codex dynamic smoke is opt-in through `SKS_TEST_REAL_DYNAMIC_AGENTS=1`; `sks agent close/cleanup` now writes executor proof for stale process, tmux, temp, and lock cleanup; and the task graph carries an intelligent work graph score, test ownership, critical path, and integration bottleneck artifacts. Fake fixture evidence remains useful for hermetic release gates, but it is explicitly separated from real runtime proof.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
sks mad-sks plan --target-root <path> --json
|
|
@@ -23,6 +23,11 @@ sks agent run "release review" --agents 8 --work-items 16 --concurrency 4 --mock
|
|
|
23
23
|
npm run source-intelligence:all-modes
|
|
24
24
|
npm run agent:background-terminals
|
|
25
25
|
npm run agent:tmux-lane-no-flicker
|
|
26
|
+
npm run agent:cleanup-executor
|
|
27
|
+
npm run agent:intelligent-work-graph
|
|
28
|
+
npm run proof:fake-vs-real-policy
|
|
29
|
+
npm run route:blackbox-realism
|
|
30
|
+
npm run release:real-check
|
|
26
31
|
npm run agent:backfill-route-blackbox
|
|
27
32
|
npm run team:actual-route-backfill
|
|
28
33
|
npm run release:readiness
|
|
@@ -58,6 +63,12 @@ Detailed release history lives in [CHANGELOG.md](CHANGELOG.md). Current release
|
|
|
58
63
|
- Main no-Scout / worker Scout policy: [docs/main-no-scout-worker-scout-policy.md](docs/main-no-scout-worker-scout-policy.md)
|
|
59
64
|
- Agent terminal lanes: [docs/agent-terminal-lanes.md](docs/agent-terminal-lanes.md)
|
|
60
65
|
- tmux right-lane cockpit: [docs/tmux-right-lane-cockpit.md](docs/tmux-right-lane-cockpit.md)
|
|
66
|
+
- Real tmux pane proof: [docs/real-tmux-pane-proof.md](docs/real-tmux-pane-proof.md)
|
|
67
|
+
- Real Codex dynamic smoke: [docs/real-codex-dynamic-smoke.md](docs/real-codex-dynamic-smoke.md)
|
|
68
|
+
- Agent cleanup executor: [docs/agent-cleanup-executor.md](docs/agent-cleanup-executor.md)
|
|
69
|
+
- Intelligent work graph: [docs/intelligent-work-graph.md](docs/intelligent-work-graph.md)
|
|
70
|
+
- Fake vs real proof policy: [docs/fake-vs-real-proof-policy.md](docs/fake-vs-real-proof-policy.md)
|
|
71
|
+
- Migration 1.18.3 to 1.18.4: [docs/migration-1.18.3-to-1.18.4.md](docs/migration-1.18.3-to-1.18.4.md)
|
|
61
72
|
- Codex official Goal mode: [docs/codex-official-goal-mode.md](docs/codex-official-goal-mode.md)
|
|
62
73
|
- Release parallel full coverage: [docs/release-parallel-full-coverage.md](docs/release-parallel-full-coverage.md)
|
|
63
74
|
- Priority closure P0-P4: [docs/priority-closure-p0-p4.md](docs/priority-closure-p0-p4.md)
|
|
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
|
|
|
4
4
|
fn main() {
|
|
5
5
|
let mut args = std::env::args().skip(1);
|
|
6
6
|
match args.next().as_deref() {
|
|
7
|
-
Some("--version") => println!("sks-rs 1.18.
|
|
7
|
+
Some("--version") => println!("sks-rs 1.18.4"),
|
|
8
8
|
Some("compact-info") => {
|
|
9
9
|
let mut input = String::new();
|
|
10
10
|
let _ = io::stdin().read_to_string(&mut input);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.dist-build-stamp.v1",
|
|
3
3
|
"package_name": "sneakoscope",
|
|
4
|
-
"package_version": "1.18.
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_file_count":
|
|
7
|
-
"built_at_source_time":
|
|
4
|
+
"package_version": "1.18.4",
|
|
5
|
+
"source_digest": "33109bbaf2af23979db84be40e35c9d47e2b35618270ab31547bfda9b59f590f",
|
|
6
|
+
"source_file_count": 1405,
|
|
7
|
+
"built_at_source_time": 1779777408842
|
|
8
8
|
}
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.dist-build.v2",
|
|
3
|
-
"version": "1.18.
|
|
4
|
-
"package_version": "1.18.
|
|
3
|
+
"version": "1.18.4",
|
|
4
|
+
"package_version": "1.18.4",
|
|
5
5
|
"typescript": true,
|
|
6
6
|
"mjs_runtime_files": 0,
|
|
7
|
-
"compiled_file_count":
|
|
8
|
-
"compiled_js_count":
|
|
9
|
-
"compiled_dts_count":
|
|
10
|
-
"source_digest": "
|
|
11
|
-
"source_file_count":
|
|
12
|
-
"source_files_hash": "
|
|
13
|
-
"source_list_hash": "
|
|
7
|
+
"compiled_file_count": 872,
|
|
8
|
+
"compiled_js_count": 436,
|
|
9
|
+
"compiled_dts_count": 436,
|
|
10
|
+
"source_digest": "33109bbaf2af23979db84be40e35c9d47e2b35618270ab31547bfda9b59f590f",
|
|
11
|
+
"source_file_count": 1405,
|
|
12
|
+
"source_files_hash": "eef20a281a31010636ae8b25a6e03acd3ac4564fb9870beed34653150250e3fe",
|
|
13
|
+
"source_list_hash": "eef20a281a31010636ae8b25a6e03acd3ac4564fb9870beed34653150250e3fe",
|
|
14
14
|
"src_mjs_runtime_files": 0,
|
|
15
15
|
"dist_stamp_schema": "sks.dist-build-stamp.v1",
|
|
16
16
|
"files": [
|
|
@@ -186,6 +186,8 @@
|
|
|
186
186
|
"commands/wiki.js",
|
|
187
187
|
"core/agents/agent-central-ledger.d.ts",
|
|
188
188
|
"core/agents/agent-central-ledger.js",
|
|
189
|
+
"core/agents/agent-cleanup-executor.d.ts",
|
|
190
|
+
"core/agents/agent-cleanup-executor.js",
|
|
189
191
|
"core/agents/agent-cleanup.d.ts",
|
|
190
192
|
"core/agents/agent-cleanup.js",
|
|
191
193
|
"core/agents/agent-codex-cockpit.d.ts",
|
|
@@ -262,12 +264,16 @@
|
|
|
262
264
|
"core/agents/agent-worker-slot.js",
|
|
263
265
|
"core/agents/agent-wrongness.d.ts",
|
|
264
266
|
"core/agents/agent-wrongness.js",
|
|
267
|
+
"core/agents/intelligent-work-graph.d.ts",
|
|
268
|
+
"core/agents/intelligent-work-graph.js",
|
|
265
269
|
"core/agents/route-collaboration-ledger.d.ts",
|
|
266
270
|
"core/agents/route-collaboration-ledger.js",
|
|
267
271
|
"core/agents/scout-policy.d.ts",
|
|
268
272
|
"core/agents/scout-policy.js",
|
|
269
273
|
"core/agents/tmux-lane-supervisor.d.ts",
|
|
270
274
|
"core/agents/tmux-lane-supervisor.js",
|
|
275
|
+
"core/agents/tmux-physical-proof.d.ts",
|
|
276
|
+
"core/agents/tmux-physical-proof.js",
|
|
271
277
|
"core/agents/tmux-right-lane-cockpit.d.ts",
|
|
272
278
|
"core/agents/tmux-right-lane-cockpit.js",
|
|
273
279
|
"core/agents/work-partition/conflict-detector.d.ts",
|
|
@@ -713,6 +719,8 @@
|
|
|
713
719
|
"core/proof/command-ledger.js",
|
|
714
720
|
"core/proof/evidence-collector.d.ts",
|
|
715
721
|
"core/proof/evidence-collector.js",
|
|
722
|
+
"core/proof/fake-real-proof-policy.d.ts",
|
|
723
|
+
"core/proof/fake-real-proof-policy.js",
|
|
716
724
|
"core/proof/file-change-ledger.d.ts",
|
|
717
725
|
"core/proof/file-change-ledger.js",
|
|
718
726
|
"core/proof/proof-reader.d.ts",
|
|
@@ -171,18 +171,29 @@ export async function hooksCommand(sub = 'explain', args = []) {
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
const
|
|
174
|
+
const projectReport = await installManagedCodexHooks(root);
|
|
175
|
+
const userReport = await installManagedCodexHooks(root, {
|
|
176
|
+
requirementsPath: path.join(os.homedir(), '.codex', 'requirements.toml'),
|
|
177
|
+
managedDir: path.join(os.homedir(), '.codex', 'managed-hooks')
|
|
178
|
+
});
|
|
179
|
+
const actual = await codexHookTrustDoctor(root, { actual: true });
|
|
175
180
|
const result = {
|
|
176
|
-
...report,
|
|
177
181
|
schema: 'sks.codex-hooks-repair.v1',
|
|
182
|
+
ok: Boolean(projectReport.ok && userReport.ok && actual.ok),
|
|
178
183
|
mode: 'managed',
|
|
184
|
+
root,
|
|
185
|
+
project_install: projectReport,
|
|
186
|
+
user_install: userReport,
|
|
187
|
+
actual_trust: actual.trust,
|
|
188
|
+
managed_dirs: actual.managed_dirs || [],
|
|
189
|
+
blockers: actual.blockers || [],
|
|
179
190
|
next_command: 'sks hooks trust-doctor --actual --json',
|
|
180
|
-
actions: ['
|
|
191
|
+
actions: ['project_requirements_toml_managed_install', 'user_requirements_toml_managed_install']
|
|
181
192
|
};
|
|
182
193
|
if (flag(args, '--json'))
|
|
183
194
|
return console.log(JSON.stringify(result, null, 2));
|
|
184
|
-
console.log(`Hooks managed repair: ${
|
|
185
|
-
if (!
|
|
195
|
+
console.log(`Hooks managed repair: ${result.ok ? 'ok' : 'blocked'}`);
|
|
196
|
+
if (!result.ok)
|
|
186
197
|
process.exitCode = 1;
|
|
187
198
|
return;
|
|
188
199
|
}
|
|
@@ -438,14 +438,9 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
438
438
|
lease_count: number;
|
|
439
439
|
blockers: string[];
|
|
440
440
|
};
|
|
441
|
-
task_graph:
|
|
442
|
-
target_active_slots: number;
|
|
443
|
-
total_work_items: number;
|
|
444
|
-
generated_from_route: string;
|
|
445
|
-
work_items_exceed_active_slots: boolean;
|
|
446
|
-
};
|
|
441
|
+
task_graph: any;
|
|
447
442
|
requested_work_items: number;
|
|
448
|
-
actual_total_work_items:
|
|
443
|
+
actual_total_work_items: any;
|
|
449
444
|
target_active_slots: number;
|
|
450
445
|
minimum_work_items: number;
|
|
451
446
|
scheduler: {
|
|
@@ -578,6 +573,26 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
578
573
|
pane_survival_checked: boolean;
|
|
579
574
|
unexpected_close_count: number;
|
|
580
575
|
lane_count: number;
|
|
576
|
+
physical_tmux_verified: boolean;
|
|
577
|
+
physical_proof_status: any;
|
|
578
|
+
list_panes_artifact: any;
|
|
579
|
+
capture_pane_artifacts: any;
|
|
580
|
+
};
|
|
581
|
+
cleanup_executor: {
|
|
582
|
+
status: string;
|
|
583
|
+
proof: string | null;
|
|
584
|
+
stale_processes_killed: any;
|
|
585
|
+
stale_tmux_panes_closed: any;
|
|
586
|
+
orphan_temp_dirs_removed: any;
|
|
587
|
+
stale_locks_removed: any;
|
|
588
|
+
skipped_active_sessions: any;
|
|
589
|
+
};
|
|
590
|
+
intelligent_work_graph: {
|
|
591
|
+
status: string;
|
|
592
|
+
score: any;
|
|
593
|
+
test_ownership: string;
|
|
594
|
+
critical_path: string;
|
|
595
|
+
integration_bottlenecks: string;
|
|
581
596
|
};
|
|
582
597
|
output_schema_ok: boolean;
|
|
583
598
|
output_tail_report: string;
|
|
@@ -660,6 +675,25 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
660
675
|
tmux_lane_auto_reopen_count: number;
|
|
661
676
|
tmux_pane_launch_ledger: string;
|
|
662
677
|
tmux_pane_launch_count: number;
|
|
678
|
+
physical_tmux_verified: boolean;
|
|
679
|
+
tmux_physical_proof: string;
|
|
680
|
+
tmux_list_panes_artifact: any;
|
|
681
|
+
tmux_capture_pane_artifacts: any;
|
|
682
|
+
tmux_pane_id_reconciled: boolean;
|
|
683
|
+
real_truth_summary: {
|
|
684
|
+
fake_backend: boolean;
|
|
685
|
+
physical_tmux_verified: boolean;
|
|
686
|
+
real_tmux_status: any;
|
|
687
|
+
cleanup_executor_status: string;
|
|
688
|
+
work_graph_quality_score: number;
|
|
689
|
+
fake_vs_real_policy: string;
|
|
690
|
+
};
|
|
691
|
+
intelligent_work_graph: string;
|
|
692
|
+
test_ownership_map: string;
|
|
693
|
+
critical_path: string;
|
|
694
|
+
integration_bottlenecks: string;
|
|
695
|
+
work_graph_quality_score: number;
|
|
696
|
+
work_graph_quality_partial: boolean;
|
|
663
697
|
terminal_reports_match_generations: boolean;
|
|
664
698
|
ledger_hash_chain_ok: boolean;
|
|
665
699
|
no_overlap_ok: boolean;
|
package/dist/commands/ppt.d.ts
CHANGED
|
@@ -290,14 +290,9 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
290
290
|
lease_count: number;
|
|
291
291
|
blockers: string[];
|
|
292
292
|
};
|
|
293
|
-
task_graph:
|
|
294
|
-
target_active_slots: number;
|
|
295
|
-
total_work_items: number;
|
|
296
|
-
generated_from_route: string;
|
|
297
|
-
work_items_exceed_active_slots: boolean;
|
|
298
|
-
};
|
|
293
|
+
task_graph: any;
|
|
299
294
|
requested_work_items: number;
|
|
300
|
-
actual_total_work_items:
|
|
295
|
+
actual_total_work_items: any;
|
|
301
296
|
target_active_slots: number;
|
|
302
297
|
minimum_work_items: number;
|
|
303
298
|
scheduler: {
|
|
@@ -430,6 +425,26 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
430
425
|
pane_survival_checked: boolean;
|
|
431
426
|
unexpected_close_count: number;
|
|
432
427
|
lane_count: number;
|
|
428
|
+
physical_tmux_verified: boolean;
|
|
429
|
+
physical_proof_status: any;
|
|
430
|
+
list_panes_artifact: any;
|
|
431
|
+
capture_pane_artifacts: any;
|
|
432
|
+
};
|
|
433
|
+
cleanup_executor: {
|
|
434
|
+
status: string;
|
|
435
|
+
proof: string | null;
|
|
436
|
+
stale_processes_killed: any;
|
|
437
|
+
stale_tmux_panes_closed: any;
|
|
438
|
+
orphan_temp_dirs_removed: any;
|
|
439
|
+
stale_locks_removed: any;
|
|
440
|
+
skipped_active_sessions: any;
|
|
441
|
+
};
|
|
442
|
+
intelligent_work_graph: {
|
|
443
|
+
status: string;
|
|
444
|
+
score: any;
|
|
445
|
+
test_ownership: string;
|
|
446
|
+
critical_path: string;
|
|
447
|
+
integration_bottlenecks: string;
|
|
433
448
|
};
|
|
434
449
|
output_schema_ok: boolean;
|
|
435
450
|
output_tail_report: string;
|
|
@@ -512,6 +527,25 @@ export declare function run(command: any, args?: any): Promise<void | {
|
|
|
512
527
|
tmux_lane_auto_reopen_count: number;
|
|
513
528
|
tmux_pane_launch_ledger: string;
|
|
514
529
|
tmux_pane_launch_count: number;
|
|
530
|
+
physical_tmux_verified: boolean;
|
|
531
|
+
tmux_physical_proof: string;
|
|
532
|
+
tmux_list_panes_artifact: any;
|
|
533
|
+
tmux_capture_pane_artifacts: any;
|
|
534
|
+
tmux_pane_id_reconciled: boolean;
|
|
535
|
+
real_truth_summary: {
|
|
536
|
+
fake_backend: boolean;
|
|
537
|
+
physical_tmux_verified: boolean;
|
|
538
|
+
real_tmux_status: any;
|
|
539
|
+
cleanup_executor_status: string;
|
|
540
|
+
work_graph_quality_score: number;
|
|
541
|
+
fake_vs_real_policy: string;
|
|
542
|
+
};
|
|
543
|
+
intelligent_work_graph: string;
|
|
544
|
+
test_ownership_map: string;
|
|
545
|
+
critical_path: string;
|
|
546
|
+
integration_bottlenecks: string;
|
|
547
|
+
work_graph_quality_score: number;
|
|
548
|
+
work_graph_quality_partial: boolean;
|
|
515
549
|
terminal_reports_match_generations: boolean;
|
|
516
550
|
ledger_hash_chain_ok: boolean;
|
|
517
551
|
no_overlap_ok: boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const AGENT_CLEANUP_PROOF_SCHEMA = "sks.agent-cleanup-proof.v1";
|
|
2
|
+
export declare const AGENT_CLEANUP_ACTION_LEDGER_SCHEMA = "sks.agent-cleanup-action-ledger.v1";
|
|
3
|
+
export interface AgentCleanupExecutorOptions {
|
|
4
|
+
missionDir: string;
|
|
5
|
+
missionId?: string | null;
|
|
6
|
+
action?: 'cleanup' | 'close';
|
|
7
|
+
apply?: boolean;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
drain?: boolean;
|
|
10
|
+
staleMs?: number;
|
|
11
|
+
}
|
|
12
|
+
type CleanupActionKind = 'terminate_process' | 'close_tmux_pane' | 'remove_temp_dir' | 'remove_lock' | 'skip_active_session' | 'skip_foreign_namespace' | 'archive_transcript_keep';
|
|
13
|
+
interface CleanupAction {
|
|
14
|
+
kind: CleanupActionKind;
|
|
15
|
+
target: string;
|
|
16
|
+
status: 'planned' | 'applied' | 'skipped' | 'failed';
|
|
17
|
+
reason: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function runAgentCleanupExecutor(opts: AgentCleanupExecutorOptions): Promise<{
|
|
21
|
+
schema: string;
|
|
22
|
+
generated_at: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
mission_id: string | null;
|
|
25
|
+
project_namespace: string | null;
|
|
26
|
+
action: string;
|
|
27
|
+
dry_run: boolean;
|
|
28
|
+
apply: boolean;
|
|
29
|
+
stale_processes_found: string[];
|
|
30
|
+
stale_processes_killed: string[];
|
|
31
|
+
stale_tmux_panes_found: string[];
|
|
32
|
+
stale_tmux_panes_closed: string[];
|
|
33
|
+
orphan_temp_dirs_found: string[];
|
|
34
|
+
orphan_temp_dirs_removed: string[];
|
|
35
|
+
stale_locks_found: string[];
|
|
36
|
+
stale_locks_removed: string[];
|
|
37
|
+
skipped_active_sessions: string[];
|
|
38
|
+
skipped_foreign_namespace: string[];
|
|
39
|
+
terminal_transcripts_preserved: string[];
|
|
40
|
+
action_count: number;
|
|
41
|
+
applied_count: number;
|
|
42
|
+
failed_count: number;
|
|
43
|
+
actions: CleanupAction[];
|
|
44
|
+
blockers: string[];
|
|
45
|
+
}>;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=agent-cleanup-executor.d.ts.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { appendJsonl, exists, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { drainTmuxLaneSupervisor } from './tmux-lane-supervisor.js';
|
|
5
|
+
import { normalizeAgentSessionRows } from './agent-session-rows.js';
|
|
6
|
+
export const AGENT_CLEANUP_PROOF_SCHEMA = 'sks.agent-cleanup-proof.v1';
|
|
7
|
+
export const AGENT_CLEANUP_ACTION_LEDGER_SCHEMA = 'sks.agent-cleanup-action-ledger.v1';
|
|
8
|
+
const TERMINAL_STATUSES = new Set(['closed', 'completed', 'done', 'failed', 'blocked', 'killed', 'timed_out']);
|
|
9
|
+
export async function runAgentCleanupExecutor(opts) {
|
|
10
|
+
const agentRoot = path.join(opts.missionDir, 'agents');
|
|
11
|
+
const action = opts.action || 'cleanup';
|
|
12
|
+
const apply = opts.apply === true && opts.dryRun !== true;
|
|
13
|
+
if (action === 'close' && opts.drain === true)
|
|
14
|
+
await drainTmuxLaneSupervisor(agentRoot).catch(() => null);
|
|
15
|
+
const namespace = await readJson(path.join(opts.missionDir, 'project-session-namespace.json'), null);
|
|
16
|
+
const sessionsRaw = await readJson(path.join(agentRoot, 'agent-sessions.json'), { sessions: {} });
|
|
17
|
+
const sessions = normalizeAgentSessionRows(sessionsRaw);
|
|
18
|
+
const activeSessionIds = new Set(sessions
|
|
19
|
+
.filter((row) => !TERMINAL_STATUSES.has(String(row.status || row.lifecycle_state || '')))
|
|
20
|
+
.map((row) => String(row.session_id || row.id || row.agent_id || ''))
|
|
21
|
+
.filter(Boolean));
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const staleMs = opts.staleMs ?? 30 * 60 * 1000;
|
|
24
|
+
const actions = [];
|
|
25
|
+
const processReports = await listNamedFiles(path.join(agentRoot, 'sessions'), 'agent-process-report.json');
|
|
26
|
+
for (const file of processReports) {
|
|
27
|
+
const report = await readJson(file, null);
|
|
28
|
+
const pid = Number(report?.pid || 0);
|
|
29
|
+
const sessionId = String(report?.session_id || '');
|
|
30
|
+
const status = String(sessions.find((row) => String(row.session_id || '') === sessionId)?.status || '');
|
|
31
|
+
const terminal = TERMINAL_STATUSES.has(status) || report?.exit_code !== null;
|
|
32
|
+
if (!pid || !processIsAlive(pid))
|
|
33
|
+
continue;
|
|
34
|
+
if (activeSessionIds.has(sessionId) && !terminal) {
|
|
35
|
+
actions.push({ kind: 'skip_active_session', target: sessionId || String(pid), status: 'skipped', reason: 'session_active' });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
actions.push(await applyAction({
|
|
39
|
+
kind: 'terminate_process',
|
|
40
|
+
target: String(pid),
|
|
41
|
+
reason: terminal ? 'terminal_session_process_alive' : 'stale_session_process',
|
|
42
|
+
apply,
|
|
43
|
+
run: async () => {
|
|
44
|
+
process.kill(pid, 'SIGTERM');
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
const tmuxReports = await listNamedFiles(path.join(agentRoot, 'sessions'), 'agent-tmux-report.json');
|
|
49
|
+
for (const file of tmuxReports) {
|
|
50
|
+
const report = await readJson(file, null);
|
|
51
|
+
const paneId = String(report?.pane_id || '');
|
|
52
|
+
const sessionId = String(report?.session_id || '');
|
|
53
|
+
if (!validTmuxPaneId(paneId))
|
|
54
|
+
continue;
|
|
55
|
+
if (activeSessionIds.has(sessionId)) {
|
|
56
|
+
actions.push({ kind: 'skip_active_session', target: sessionId || paneId, status: 'skipped', reason: 'tmux_session_active' });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
actions.push(await applyAction({
|
|
60
|
+
kind: 'close_tmux_pane',
|
|
61
|
+
target: paneId,
|
|
62
|
+
reason: 'stale_tmux_pane',
|
|
63
|
+
apply,
|
|
64
|
+
run: async () => {
|
|
65
|
+
const { runProcess } = await import('../fsx.js');
|
|
66
|
+
await runProcess('tmux', ['kill-pane', '-t', paneId], { timeoutMs: 3000, maxOutputBytes: 4096 });
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
const projectHash = String(namespace?.root_hash || '');
|
|
71
|
+
for (const dir of Array.isArray(namespace?.orphan_temp_dirs) ? namespace.orphan_temp_dirs.map(String) : []) {
|
|
72
|
+
if (!namespaceOwnsPath(dir, projectHash)) {
|
|
73
|
+
actions.push({ kind: 'skip_foreign_namespace', target: dir, status: 'skipped', reason: 'path_outside_project_namespace' });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!(await exists(dir)))
|
|
77
|
+
continue;
|
|
78
|
+
actions.push(await applyAction({
|
|
79
|
+
kind: 'remove_temp_dir',
|
|
80
|
+
target: dir,
|
|
81
|
+
reason: 'orphan_temp_dir',
|
|
82
|
+
apply,
|
|
83
|
+
run: async () => fsp.rm(dir, { recursive: true, force: true })
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
for (const lock of await staleLockFiles(String(namespace?.lock_dir || ''), projectHash, now, staleMs)) {
|
|
87
|
+
actions.push(await applyAction({
|
|
88
|
+
kind: 'remove_lock',
|
|
89
|
+
target: lock,
|
|
90
|
+
reason: 'stale_lock_file',
|
|
91
|
+
apply,
|
|
92
|
+
run: async () => fsp.rm(lock, { force: true })
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
for (const transcript of await listNamedFiles(path.join(agentRoot, 'sessions'), 'agent-terminal-session.json')) {
|
|
96
|
+
actions.push({ kind: 'archive_transcript_keep', target: transcript, status: 'skipped', reason: 'terminal_transcripts_are_preserved' });
|
|
97
|
+
}
|
|
98
|
+
const proof = buildCleanupProof({
|
|
99
|
+
generatedAt: nowIso(),
|
|
100
|
+
missionId: opts.missionId || namespace?.mission_id || null,
|
|
101
|
+
projectNamespace: projectHash || null,
|
|
102
|
+
action,
|
|
103
|
+
apply,
|
|
104
|
+
dryRun: !apply,
|
|
105
|
+
actions,
|
|
106
|
+
activeSessionIds: [...activeSessionIds]
|
|
107
|
+
});
|
|
108
|
+
await writeJsonAtomic(path.join(agentRoot, 'agent-cleanup-proof.json'), proof);
|
|
109
|
+
await writeJsonAtomic(path.join(agentRoot, 'agent-command-cleanup.json'), {
|
|
110
|
+
schema: 'sks.agent-command-cleanup.v2',
|
|
111
|
+
ok: proof.ok,
|
|
112
|
+
mission_id: proof.mission_id,
|
|
113
|
+
action,
|
|
114
|
+
cleanup_executor: 'agent-cleanup-proof.json',
|
|
115
|
+
cleanup_action_ledger: 'agent-cleanup-action-ledger.jsonl',
|
|
116
|
+
applied: apply,
|
|
117
|
+
blockers: proof.blockers
|
|
118
|
+
});
|
|
119
|
+
for (const row of actions) {
|
|
120
|
+
await appendJsonl(path.join(agentRoot, 'agent-cleanup-action-ledger.jsonl'), {
|
|
121
|
+
schema: AGENT_CLEANUP_ACTION_LEDGER_SCHEMA,
|
|
122
|
+
generated_at: proof.generated_at,
|
|
123
|
+
action: row
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return proof;
|
|
127
|
+
}
|
|
128
|
+
function buildCleanupProof(input) {
|
|
129
|
+
const byKind = (kind, status) => input.actions.filter((row) => row.kind === kind && (!status || row.status === status));
|
|
130
|
+
const failed = input.actions.filter((row) => row.status === 'failed');
|
|
131
|
+
return {
|
|
132
|
+
schema: AGENT_CLEANUP_PROOF_SCHEMA,
|
|
133
|
+
generated_at: input.generatedAt,
|
|
134
|
+
ok: failed.length === 0,
|
|
135
|
+
mission_id: input.missionId,
|
|
136
|
+
project_namespace: input.projectNamespace,
|
|
137
|
+
action: input.action,
|
|
138
|
+
dry_run: input.dryRun,
|
|
139
|
+
apply: input.apply,
|
|
140
|
+
stale_processes_found: byKind('terminate_process').map((row) => row.target),
|
|
141
|
+
stale_processes_killed: byKind('terminate_process', 'applied').map((row) => row.target),
|
|
142
|
+
stale_tmux_panes_found: byKind('close_tmux_pane').map((row) => row.target),
|
|
143
|
+
stale_tmux_panes_closed: byKind('close_tmux_pane', 'applied').map((row) => row.target),
|
|
144
|
+
orphan_temp_dirs_found: byKind('remove_temp_dir').map((row) => row.target),
|
|
145
|
+
orphan_temp_dirs_removed: byKind('remove_temp_dir', 'applied').map((row) => row.target),
|
|
146
|
+
stale_locks_found: byKind('remove_lock').map((row) => row.target),
|
|
147
|
+
stale_locks_removed: byKind('remove_lock', 'applied').map((row) => row.target),
|
|
148
|
+
skipped_active_sessions: [...new Set([...input.activeSessionIds, ...byKind('skip_active_session').map((row) => row.target)])].filter(Boolean),
|
|
149
|
+
skipped_foreign_namespace: byKind('skip_foreign_namespace').map((row) => row.target),
|
|
150
|
+
terminal_transcripts_preserved: byKind('archive_transcript_keep').map((row) => row.target),
|
|
151
|
+
action_count: input.actions.length,
|
|
152
|
+
applied_count: input.actions.filter((row) => row.status === 'applied').length,
|
|
153
|
+
failed_count: failed.length,
|
|
154
|
+
actions: input.actions,
|
|
155
|
+
blockers: failed.map((row) => `cleanup_action_failed:${row.kind}:${row.target}`)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function applyAction(input) {
|
|
159
|
+
if (!input.apply)
|
|
160
|
+
return { kind: input.kind, target: input.target, status: 'planned', reason: input.reason };
|
|
161
|
+
try {
|
|
162
|
+
await input.run();
|
|
163
|
+
return { kind: input.kind, target: input.target, status: 'applied', reason: input.reason };
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return { kind: input.kind, target: input.target, status: 'failed', reason: input.reason, error: err instanceof Error ? err.message : String(err) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function staleLockFiles(lockDir, projectHash, now, staleMs) {
|
|
170
|
+
const out = [];
|
|
171
|
+
if (!lockDir || !namespaceOwnsPath(lockDir, projectHash) || !(await exists(lockDir)))
|
|
172
|
+
return out;
|
|
173
|
+
for (const file of await listFiles(lockDir)) {
|
|
174
|
+
const stat = await fsp.stat(file).catch(() => null);
|
|
175
|
+
if (stat && now - stat.mtimeMs > staleMs)
|
|
176
|
+
out.push(file);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
async function listNamedFiles(dir, name) {
|
|
181
|
+
return (await listFiles(dir)).filter((file) => path.basename(file) === name);
|
|
182
|
+
}
|
|
183
|
+
async function listFiles(dir) {
|
|
184
|
+
const out = [];
|
|
185
|
+
if (!(await exists(dir)))
|
|
186
|
+
return out;
|
|
187
|
+
for (const entry of await fsp.readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
188
|
+
const file = path.join(dir, entry.name);
|
|
189
|
+
if (entry.isDirectory())
|
|
190
|
+
out.push(...await listFiles(file));
|
|
191
|
+
else if (entry.isFile())
|
|
192
|
+
out.push(file);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function namespaceOwnsPath(candidate, projectHash) {
|
|
197
|
+
return Boolean(candidate && (!projectHash || candidate.includes(projectHash)));
|
|
198
|
+
}
|
|
199
|
+
function processIsAlive(pid) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(pid, 0);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function validTmuxPaneId(value) {
|
|
209
|
+
return /^%\d+$/.test(value);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=agent-cleanup-executor.js.map
|
|
@@ -16,6 +16,9 @@ export function parseAgentCommandArgs(command, args = []) {
|
|
|
16
16
|
const mock = hasFlag(args, '--mock') || backend === 'fake';
|
|
17
17
|
const real = hasFlag(args, '--real');
|
|
18
18
|
const readonly = hasFlag(args, '--readonly') || hasFlag(args, '--read-only');
|
|
19
|
+
const apply = hasFlag(args, '--apply');
|
|
20
|
+
const dryRun = hasFlag(args, '--dry-run') || hasFlag(args, '--dryrun');
|
|
21
|
+
const drain = hasFlag(args, '--drain');
|
|
19
22
|
const codexApp = hasFlag(args, '--codex-app');
|
|
20
23
|
const positionals = positionalArgs(rest, new Set(['--agents', '--target-active-slots', '--work-items', '--minimum-work-items', '--max-queue-expansion', '--concurrency', '--backend', '--route', '--mission', '--mission-id', '--agent', '--lane']));
|
|
21
24
|
const missionDefault = action === 'run' || action === 'spawn' || action === 'plan' ? '' : 'latest';
|
|
@@ -24,7 +27,7 @@ export function parseAgentCommandArgs(command, args = []) {
|
|
|
24
27
|
const lane = String(readOption(args, '--agent', readOption(args, '--lane', '')));
|
|
25
28
|
const promptPositionals = positionalMission ? positionals.slice(1) : positionals;
|
|
26
29
|
const prompt = promptPositionals.join(' ').trim() || 'Native agent run';
|
|
27
|
-
return { command, action, prompt, route, agents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency, backend, mock, real, readonly, json, missionId, lane, codexApp };
|
|
30
|
+
return { command, action, prompt, route, agents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency, backend, mock, real, readonly, apply, dryRun, drain, json, missionId, lane, codexApp };
|
|
28
31
|
}
|
|
29
32
|
function hasFlag(args, flag) {
|
|
30
33
|
return args.includes(flag);
|