sneakoscope 8.4.0 → 8.6.0
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 +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/doctor.js +64 -15
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/architecture-map-pipeline.js +4 -3
- package/dist/core/codex/agent-config-file-repair.js +17 -3
- package/dist/core/codex/codex-config-guard.js +3 -1
- package/dist/core/codex/codex-feature-flags.js +10 -0
- package/dist/core/codex/codex-project-config-policy.js +54 -30
- package/dist/core/codex/codex-startup-config-postcheck.js +5 -0
- package/dist/core/codex-hooks/codex-hook-state-writer.js +2 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +11 -8
- package/dist/core/codex-lb/request-route-resolver.js +21 -18
- package/dist/core/doctor/doctor-dirty-planner.js +16 -1
- package/dist/core/doctor/doctor-idempotence.js +11 -0
- package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
- package/dist/core/doctor/doctor-transaction.js +47 -2
- package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -1
- package/dist/core/init.js +40 -17
- package/dist/core/pipeline-internals/runtime-core.js +8 -3
- package/dist/core/subagents/agent-catalog.js +4 -2
- package/dist/core/subagents/official-subagent-config.js +92 -15
- package/dist/core/subagents/official-subagent-preparation.js +1 -0
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
|
|
|
22
22
|
Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
|
|
23
23
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
24
24
|
|
|
25
|
-
This README documents package **SKS 8.
|
|
25
|
+
This README documents package **SKS 8.6.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
|
|
26
26
|
|
|
27
27
|
Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
|
|
28
28
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -825,6 +825,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
825
825
|
phases: [
|
|
826
826
|
{
|
|
827
827
|
id: 'setup',
|
|
828
|
+
report: () => setupRepair,
|
|
828
829
|
run: async () => ({
|
|
829
830
|
id: 'setup',
|
|
830
831
|
ok: setupRepair !== null,
|
|
@@ -835,6 +836,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
835
836
|
},
|
|
836
837
|
{
|
|
837
838
|
id: 'codex_startup_repair',
|
|
839
|
+
report: () => codexStartupRepair,
|
|
838
840
|
run: async () => ({
|
|
839
841
|
id: 'codex_startup_repair',
|
|
840
842
|
ok: codexStartupRepair?.ok !== false,
|
|
@@ -846,6 +848,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
846
848
|
},
|
|
847
849
|
{
|
|
848
850
|
id: 'startup_config_repair',
|
|
851
|
+
report: () => startupConfigRepair,
|
|
849
852
|
run: async () => ({
|
|
850
853
|
id: 'startup_config_repair',
|
|
851
854
|
ok: startupConfigRepair?.ok === true,
|
|
@@ -856,6 +859,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
856
859
|
},
|
|
857
860
|
{
|
|
858
861
|
id: 'codex_config_syntax_repair',
|
|
862
|
+
report: () => codexConfigSyntaxRepair,
|
|
859
863
|
run: async () => ({
|
|
860
864
|
id: 'codex_config_syntax_repair',
|
|
861
865
|
ok: codexConfigSyntaxRepair?.ok !== false,
|
|
@@ -867,6 +871,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
867
871
|
},
|
|
868
872
|
{
|
|
869
873
|
id: 'context7_repair',
|
|
874
|
+
report: () => context7Repair,
|
|
870
875
|
run: async () => ({
|
|
871
876
|
id: 'context7_repair',
|
|
872
877
|
ok: context7Repair?.ok !== false,
|
|
@@ -878,6 +883,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
878
883
|
},
|
|
879
884
|
{
|
|
880
885
|
id: 'context7_mcp_repair',
|
|
886
|
+
report: () => context7McpRepair,
|
|
881
887
|
run: async () => ({
|
|
882
888
|
id: 'context7_mcp_repair',
|
|
883
889
|
ok: context7McpRepair?.ok === true,
|
|
@@ -890,6 +896,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
890
896
|
},
|
|
891
897
|
{
|
|
892
898
|
id: 'supabase_mcp_repair',
|
|
899
|
+
report: () => supabaseMcpRepair,
|
|
893
900
|
required_for_ready: false,
|
|
894
901
|
run: async () => ({
|
|
895
902
|
id: 'supabase_mcp_repair',
|
|
@@ -904,6 +911,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
904
911
|
},
|
|
905
912
|
{
|
|
906
913
|
id: 'hook_trust_repair',
|
|
914
|
+
report: () => hookTrustRepair,
|
|
907
915
|
run: async () => ({
|
|
908
916
|
id: 'hook_trust_repair',
|
|
909
917
|
ok: hookTrustRepair?.ok !== false,
|
|
@@ -917,6 +925,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
917
925
|
},
|
|
918
926
|
{
|
|
919
927
|
id: 'sks_menubar',
|
|
928
|
+
report: () => sksMenuBar,
|
|
920
929
|
required_for_ready: false,
|
|
921
930
|
run: async () => ({
|
|
922
931
|
id: 'sks_menubar',
|
|
@@ -961,6 +970,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
961
970
|
},
|
|
962
971
|
{
|
|
963
972
|
id: 'command_alias_cleanup',
|
|
973
|
+
report: () => commandAliasCleanupBeforeReceipt,
|
|
964
974
|
run: async () => ({
|
|
965
975
|
id: 'command_alias_cleanup',
|
|
966
976
|
ok: commandAliasCleanupBeforeReceipt?.ok !== false,
|
|
@@ -972,6 +982,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
972
982
|
},
|
|
973
983
|
{
|
|
974
984
|
id: 'native_capability_repair',
|
|
985
|
+
report: () => doctorNativeCapabilityRepair,
|
|
975
986
|
required_for_ready: false,
|
|
976
987
|
run: async () => ({
|
|
977
988
|
id: 'native_capability_repair',
|
|
@@ -1214,6 +1225,20 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
1214
1225
|
warnings: []
|
|
1215
1226
|
}))
|
|
1216
1227
|
: null;
|
|
1228
|
+
const configDiskVerification = doctorFix
|
|
1229
|
+
? await (await import('../core/doctor/doctor-repair-postcheck.js')).verifyCodexConfigsOnDisk({ root })
|
|
1230
|
+
.catch((err) => ({
|
|
1231
|
+
schema: 'sks.doctor-config-disk-verification.v1',
|
|
1232
|
+
ok: false,
|
|
1233
|
+
project_config_path: '',
|
|
1234
|
+
codex_home_config_path: '',
|
|
1235
|
+
project_config_present: false,
|
|
1236
|
+
codex_home_config_present: false,
|
|
1237
|
+
multi_agent_v2_enabled: null,
|
|
1238
|
+
agents_enabled: null,
|
|
1239
|
+
blockers: [`config_disk_verification_failed:${err?.message || String(err)}`]
|
|
1240
|
+
}))
|
|
1241
|
+
: null;
|
|
1217
1242
|
const codexAppHarnessMatrix = deepDiagnostics
|
|
1218
1243
|
? await buildCodexAppHarnessMatrix({ root, mode: 'read-only' }).catch((err) => ({
|
|
1219
1244
|
schema: 'sks.codex-app-harness-matrix.v1',
|
|
@@ -1385,16 +1410,36 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
1385
1410
|
}
|
|
1386
1411
|
}
|
|
1387
1412
|
const runtimeReadiness = buildRuntimeReadiness(codexNativeFeatureMatrix);
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
&&
|
|
1395
|
-
&&
|
|
1396
|
-
|
|
1397
|
-
|
|
1413
|
+
const resultBlockers = [
|
|
1414
|
+
...(ready.ready
|
|
1415
|
+
? []
|
|
1416
|
+
: (Array.isArray(ready.blockers) && ready.blockers.length
|
|
1417
|
+
? ready.blockers.map((blocker) => `readiness:${String(blocker)}`)
|
|
1418
|
+
: ['readiness_not_ready'])),
|
|
1419
|
+
...(sksUpdate && sksUpdate.ok === false ? ['sks_update_blocked'] : []),
|
|
1420
|
+
...(doctorFix && !migrationReceiptOwnsReconcile && skillsReconcile?.convergence?.ok !== true
|
|
1421
|
+
? ['managed_skill_reconcile_not_converged']
|
|
1422
|
+
: []),
|
|
1423
|
+
...(commandAliasCleanup.ok === false ? ['command_alias_cleanup_blocked'] : []),
|
|
1424
|
+
...(codexStartupRepair.ok === false ? ['codex_startup_repair_blocked'] : []),
|
|
1425
|
+
...(codexConfigSyntaxRepair?.ok === false ? ['codex_config_syntax_repair_blocked'] : []),
|
|
1426
|
+
...(agentRoleConfigRepair.ok === false ? ['agent_role_config_repair_blocked'] : []),
|
|
1427
|
+
...(openRouterProviderRepair.ok === false ? ['openrouter_provider_repair_blocked'] : []),
|
|
1428
|
+
...((officialSubagentConfig.blockers || [])
|
|
1429
|
+
.map((blocker) => `official_subagent_config:${String(blocker)}`)),
|
|
1430
|
+
...(doctorProfileRequiresDesktopBridgeReadiness(doctorProfile) && desktopBridge.ok === false
|
|
1431
|
+
? ['desktop_bridge_not_ready']
|
|
1432
|
+
: []),
|
|
1433
|
+
...(mcpTransportCollisionRepair?.ok === false ? ['mcp_transport_collision_repair_blocked'] : []),
|
|
1434
|
+
...(codexNativeRepair?.ok === false ? ['codex_native_repair_blocked'] : []),
|
|
1435
|
+
...(configDiskVerification?.blockers || [])
|
|
1436
|
+
.map((blocker) => `config_disk_verification:${String(blocker)}`)
|
|
1437
|
+
];
|
|
1438
|
+
const resultOk = resultBlockers.length === 0;
|
|
1439
|
+
const repairOperatorActions = [
|
|
1440
|
+
...(codexStartupRepair?.config_file_repair?.operator_actions || []),
|
|
1441
|
+
...(agentRoleConfigRepair?.operator_actions || [])
|
|
1442
|
+
].map(String).filter(Boolean);
|
|
1398
1443
|
const result = {
|
|
1399
1444
|
schema: 'sks.doctor-status.v3',
|
|
1400
1445
|
elapsed_ms: Date.now() - startedAtMs,
|
|
@@ -1409,11 +1454,14 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
1409
1454
|
not_counted_as_full_doctor: !deepDiagnostics,
|
|
1410
1455
|
root,
|
|
1411
1456
|
arg_warnings: argWarnings,
|
|
1457
|
+
blockers: [...new Set(resultBlockers.map(String))],
|
|
1412
1458
|
warnings: [...oauthCallbackPortDiagnostic.warnings, ...(desktopBridge.warnings || [])],
|
|
1413
|
-
operator_actions: [
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1459
|
+
operator_actions: [...new Set([
|
|
1460
|
+
...oauthCallbackOperatorActions,
|
|
1461
|
+
...(desktopBridge.ok === false ? desktopBridge.recovery_actions || [] : []),
|
|
1462
|
+
...repairOperatorActions,
|
|
1463
|
+
...(resultOk ? [] : ['Each blocker above names a report under .sneakoscope/reports/; re-run `sks doctor --full --json` after resolving them.'])
|
|
1464
|
+
].map(String).filter(Boolean))],
|
|
1417
1465
|
node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.version },
|
|
1418
1466
|
codex,
|
|
1419
1467
|
oauth_callback_port_diagnostic: oauthCallbackPortDiagnostic,
|
|
@@ -1438,8 +1486,9 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
|
|
|
1438
1486
|
supabase_mcp_repair: supabaseMcpRepair,
|
|
1439
1487
|
doctor_fix_transaction: doctorFixTransaction,
|
|
1440
1488
|
doctor_fix_postcheck: doctorFixPostcheck,
|
|
1489
|
+
config_disk_verification: configDiskVerification,
|
|
1441
1490
|
postcheck: doctorFixPostcheck ? {
|
|
1442
|
-
ok: doctorFixPostcheck.ok === true,
|
|
1491
|
+
ok: doctorFixPostcheck.ok === true && configDiskVerification?.ok !== false,
|
|
1443
1492
|
pending_manual: doctorFixPostcheck.pending_manual || [],
|
|
1444
1493
|
required_blockers: doctorFixPostcheck.required_blockers || [],
|
|
1445
1494
|
optional_warnings: doctorFixPostcheck.optional_warnings || []
|
|
@@ -46,9 +46,10 @@ export async function seedArchitectureMapPlanBinding(dir, plan) {
|
|
|
46
46
|
return plan;
|
|
47
47
|
}
|
|
48
48
|
export async function maybeSeedArchitectureMapForPlan(input) {
|
|
49
|
-
if (!input.plan?.architecture_map || !input.missionId)
|
|
50
|
-
return;
|
|
51
|
-
|
|
49
|
+
if (!input.plan?.architecture_map || !input.missionId) {
|
|
50
|
+
return { ok: false, reason: 'architecture_map_not_planned' };
|
|
51
|
+
}
|
|
52
|
+
return seedArchitectureMapBaselineArtifacts({
|
|
52
53
|
root: input.root,
|
|
53
54
|
dir: input.dir,
|
|
54
55
|
missionId: String(input.missionId),
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
4
|
-
import { backupInvalidToml, inspectOfficialSubagentToml,
|
|
4
|
+
import { SKS_MANAGED_CODEX_CONFIG_MARKER, backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfigResult, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText } from '../subagents/official-subagent-config.js';
|
|
5
5
|
import { writeCodexConfigGuarded } from './codex-config-guard.js';
|
|
6
|
+
function unmanagedConfigOperatorAction(configPath) {
|
|
7
|
+
return `Refused to modify ${configPath}: no SKS ownership proof. `
|
|
8
|
+
+ `If SKS owns this file, add the line \`${SKS_MANAGED_CODEX_CONFIG_MARKER}\` as its first line `
|
|
9
|
+
+ 'and re-run `sks doctor --fix`. Otherwise run `sks setup` to regenerate a managed config.';
|
|
10
|
+
}
|
|
6
11
|
export async function repairAgentConfigFileReferences(input) {
|
|
7
12
|
const root = path.resolve(input.root);
|
|
8
13
|
const configPath = path.join(root, '.codex', 'config.toml');
|
|
@@ -37,6 +42,10 @@ export async function repairAgentConfigFileReferences(input) {
|
|
|
37
42
|
...(!ownershipProof.owned ? ['user_owned_file_without_sks_marker'] : [])
|
|
38
43
|
],
|
|
39
44
|
warnings: [],
|
|
45
|
+
operator_actions: [
|
|
46
|
+
`${configPath} is not valid TOML; SKS preserved it and wrote a backup. Fix the syntax, then re-run \`sks doctor --fix\`.`,
|
|
47
|
+
...(!ownershipProof.owned ? [unmanagedConfigOperatorAction(configPath)] : [])
|
|
48
|
+
],
|
|
40
49
|
ownership_proof: ownershipProof
|
|
41
50
|
});
|
|
42
51
|
}
|
|
@@ -55,6 +64,7 @@ export async function repairAgentConfigFileReferences(input) {
|
|
|
55
64
|
manual_required: true,
|
|
56
65
|
blockers: ['user_owned_file_without_sks_marker'],
|
|
57
66
|
warnings: [],
|
|
67
|
+
operator_actions: [unmanagedConfigOperatorAction(configPath)],
|
|
58
68
|
ownership_proof: ownershipProof
|
|
59
69
|
});
|
|
60
70
|
}
|
|
@@ -62,13 +72,14 @@ export async function repairAgentConfigFileReferences(input) {
|
|
|
62
72
|
...(input.home ? { home: input.home } : {}),
|
|
63
73
|
...(input.codexHome ? { codexHome: input.codexHome } : {})
|
|
64
74
|
});
|
|
65
|
-
const
|
|
75
|
+
const mergeResult = mergeOfficialSubagentConfigResult(original, {
|
|
66
76
|
sksOwned: ownershipProof.owned,
|
|
67
77
|
inheritedText
|
|
68
78
|
});
|
|
79
|
+
const merged = mergeResult.text;
|
|
69
80
|
const validation = inspectOfficialSubagentToml(merged);
|
|
70
81
|
const warnings = officialSubagentConfigWarnings(merged, inheritedText);
|
|
71
|
-
const blockers = [];
|
|
82
|
+
const blockers = [...mergeResult.blockers];
|
|
72
83
|
let changed = merged !== original;
|
|
73
84
|
let writeSucceeded = input.apply !== true;
|
|
74
85
|
let backupPath = null;
|
|
@@ -104,6 +115,9 @@ export async function repairAgentConfigFileReferences(input) {
|
|
|
104
115
|
manual_required: blockers.length > 0,
|
|
105
116
|
blockers,
|
|
106
117
|
warnings,
|
|
118
|
+
operator_actions: blockers.map((blocker) => blocker === 'user_owned_file_without_sks_marker'
|
|
119
|
+
? unmanagedConfigOperatorAction(configPath)
|
|
120
|
+
: `${configPath}: ${blocker}. Re-run \`sks doctor --fix\` after resolving it, or run \`sks setup\` to regenerate a managed config.`),
|
|
107
121
|
ownership_proof: ownershipProof
|
|
108
122
|
};
|
|
109
123
|
return writeReport(input.reportPath, root, report);
|
|
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
4
4
|
import { appendJsonl, ensureDir, nowIso, readText, sha256, writeTextAtomic } from '../fsx.js';
|
|
5
5
|
import { diffCodexAppUiSnapshots, snapshotCodexAppUiState } from '../codex-app/codex-app-ui-state-snapshot.js';
|
|
6
6
|
import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from './codex-config-toml.js';
|
|
7
|
+
import { hasManagedAgentsConfigFingerprint } from '../subagents/official-subagent-config.js';
|
|
7
8
|
import { escapeRegExp } from '../text/regex.js';
|
|
8
9
|
import { withFileLock } from '../locks/file-lock.js';
|
|
9
10
|
export const SKS_MANAGED_CODEX_CONFIG_MARKER = '# SKS-MANAGED-CODEX-CONFIG';
|
|
@@ -576,7 +577,8 @@ export function isProjectCodexConfig(root, configPath) {
|
|
|
576
577
|
export function hasSksManagedCodexConfigMarker(text) {
|
|
577
578
|
const source = String(text || '');
|
|
578
579
|
return hasExplicitSksManagedCodexConfigMarker(source)
|
|
579
|
-
|| /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker
|
|
580
|
+
|| /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker)/i.test(source)
|
|
581
|
+
|| hasManagedAgentsConfigFingerprint(source)
|
|
580
582
|
|| /^\s*model_provider\s*=\s*["']codex-lb["']\s*(?:#.*)?$/mi.test(source)
|
|
581
583
|
|| /^\s*default_profile\s*=\s*["']sks-fast-high["']\s*(?:#.*)?$/mi.test(source)
|
|
582
584
|
|| /^\s*\[(?:user\.fast_mode|model_providers\.(?:"codex-lb"|codex-lb)|profiles\.(?:"sks-fast-high"|sks-fast-high))\]\s*(?:#.*)?$/mi.test(source);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const MANAGED_CODEX_FEATURE_FLAGS = Object.freeze(['hooks', 'fast_mode', 'apps']);
|
|
2
|
+
export const REMOVED_CODEX_FEATURE_FLAGS = Object.freeze([
|
|
3
|
+
'fast_mode_ui',
|
|
4
|
+
'codex_hooks',
|
|
5
|
+
'remote_control',
|
|
6
|
+
'codex_git_commit',
|
|
7
|
+
'plugin_hooks',
|
|
8
|
+
'js_repl',
|
|
9
|
+
'multi_agent_mode'
|
|
10
|
+
]);
|
|
@@ -118,20 +118,8 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
|
|
|
118
118
|
let backupPath = null;
|
|
119
119
|
let userConfigPath = null;
|
|
120
120
|
let profileConfigPath = null;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await ensureDir(path.dirname(configPath));
|
|
124
|
-
await fsp.copyFile(configPath, backupPath);
|
|
125
|
-
await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
|
|
126
|
-
await writeCodexConfigGuarded({
|
|
127
|
-
root,
|
|
128
|
-
configPath,
|
|
129
|
-
before: String(original || ''),
|
|
130
|
-
cause: 'codex-project-config-policy',
|
|
131
|
-
mutate: () => projectText
|
|
132
|
-
});
|
|
133
|
-
actions.push('project_config_rewritten_with_backup');
|
|
134
|
-
}
|
|
121
|
+
const writeBlockers = [];
|
|
122
|
+
let machineLocalMoved = true;
|
|
135
123
|
if (opts.apply && split.machine_text.trim()) {
|
|
136
124
|
await ensureDir(codexHome);
|
|
137
125
|
userConfigPath = codexHomeConfigPath;
|
|
@@ -139,15 +127,39 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
|
|
|
139
127
|
const machineText = preserveExistingGlobalFastModeConfig(String(currentUser || ''), split.machine_text);
|
|
140
128
|
const dedupedUser = removeConfigIds(String(currentUser || ''), configIds(machineText));
|
|
141
129
|
const commentLine = `# SKS moved machine-local Codex config from ${path.relative(root, configPath) || configPath} at ${nowIso()}`;
|
|
142
|
-
const mergedUser = mergeMachineLocalIntoUserConfig(dedupedUser, machineText.trim(), commentLine);
|
|
143
|
-
await writeCodexConfigGuarded({
|
|
130
|
+
const mergedUser = mergeMachineLocalIntoUserConfig(stripSksMovedComments(dedupedUser), machineText.trim(), commentLine);
|
|
131
|
+
const homeWrite = await writeCodexConfigGuarded({
|
|
144
132
|
root,
|
|
145
133
|
configPath: userConfigPath,
|
|
146
134
|
before: String(currentUser || ''),
|
|
147
135
|
cause: 'codex-project-config-policy-machine-local',
|
|
148
136
|
mutate: () => mergedUser
|
|
149
137
|
});
|
|
150
|
-
|
|
138
|
+
machineLocalMoved = homeWrite.ok;
|
|
139
|
+
if (homeWrite.ok)
|
|
140
|
+
actions.push('machine_local_keys_moved_to_codex_home_config');
|
|
141
|
+
else
|
|
142
|
+
writeBlockers.push(`machine_local_move_refused:${homeWrite.status}`);
|
|
143
|
+
}
|
|
144
|
+
if (opts.apply && changed && machineLocalMoved) {
|
|
145
|
+
backupPath = `${configPath}.bak-${Date.now().toString(36)}`;
|
|
146
|
+
await ensureDir(path.dirname(configPath));
|
|
147
|
+
await fsp.copyFile(configPath, backupPath);
|
|
148
|
+
await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
|
|
149
|
+
const projectWrite = await writeCodexConfigGuarded({
|
|
150
|
+
root,
|
|
151
|
+
configPath,
|
|
152
|
+
before: String(original || ''),
|
|
153
|
+
cause: 'codex-project-config-policy',
|
|
154
|
+
mutate: () => projectText
|
|
155
|
+
});
|
|
156
|
+
if (projectWrite.ok)
|
|
157
|
+
actions.push('project_config_rewritten_with_backup');
|
|
158
|
+
else
|
|
159
|
+
writeBlockers.push(`project_config_write_refused:${projectWrite.status}`);
|
|
160
|
+
}
|
|
161
|
+
else if (opts.apply && changed) {
|
|
162
|
+
writeBlockers.push('project_config_rewrite_skipped_machine_local_move_refused');
|
|
151
163
|
}
|
|
152
164
|
if (profileName)
|
|
153
165
|
actions.push('legacy_project_profile_selector_removed');
|
|
@@ -157,7 +169,7 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
|
|
|
157
169
|
root,
|
|
158
170
|
config_path: configPath,
|
|
159
171
|
codex_home: codexHome,
|
|
160
|
-
ok: parseSmoke.ok && split.blockers.length === 0,
|
|
172
|
+
ok: parseSmoke.ok && split.blockers.length === 0 && writeBlockers.length === 0,
|
|
161
173
|
changed,
|
|
162
174
|
applied: opts.apply === true,
|
|
163
175
|
backup_path: backupPath,
|
|
@@ -175,7 +187,11 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
|
|
|
175
187
|
deprecated_approval_policy_fixed: split.deprecated_approval_policy_fixed,
|
|
176
188
|
actions,
|
|
177
189
|
parse_smoke: parseSmoke,
|
|
178
|
-
blockers: [
|
|
190
|
+
blockers: [
|
|
191
|
+
...split.blockers,
|
|
192
|
+
...writeBlockers,
|
|
193
|
+
...(parseSmoke.ok ? [] : ['project_config_rewrite_parse_smoke_failed'])
|
|
194
|
+
]
|
|
179
195
|
};
|
|
180
196
|
if (opts.writeReport !== false)
|
|
181
197
|
await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
|
|
@@ -201,28 +217,36 @@ export async function repairCodexConfigStructure(configPathInput, opts = {}) {
|
|
|
201
217
|
}
|
|
202
218
|
const hoist = hoistMisplacedMachineLocalKeys(String(original));
|
|
203
219
|
let backupPath = null;
|
|
220
|
+
let applied = false;
|
|
221
|
+
const writeBlockers = [];
|
|
204
222
|
if (opts.apply && hoist.changed) {
|
|
205
223
|
backupPath = `${configPath}.struct-bak-${Date.now().toString(36)}`;
|
|
206
224
|
await ensureDir(path.dirname(configPath));
|
|
207
225
|
await fsp.copyFile(configPath, backupPath);
|
|
208
226
|
await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
|
|
209
|
-
await writeCodexConfigGuarded({
|
|
227
|
+
const guarded = await writeCodexConfigGuarded({
|
|
210
228
|
configPath,
|
|
211
229
|
before: String(original || ''),
|
|
212
230
|
cause: 'codex-config-structure-repair',
|
|
213
231
|
mutate: () => hoist.text
|
|
214
232
|
});
|
|
233
|
+
applied = guarded.ok;
|
|
234
|
+
if (!guarded.ok)
|
|
235
|
+
writeBlockers.push(`config_write_guard:${guarded.status}`);
|
|
215
236
|
}
|
|
216
237
|
const parseSmoke = tomlRewriteSmoke(hoist.text);
|
|
217
238
|
return {
|
|
218
239
|
config_path: configPath,
|
|
219
|
-
ok: parseSmoke.ok,
|
|
220
|
-
status: hoist.changed
|
|
240
|
+
ok: parseSmoke.ok && writeBlockers.length === 0,
|
|
241
|
+
status: hoist.changed
|
|
242
|
+
? (applied ? 'structure_repaired' : opts.apply ? 'structure_repair_refused' : 'structure_repair_available')
|
|
243
|
+
: 'structure_ok',
|
|
221
244
|
changed: hoist.changed,
|
|
222
|
-
applied
|
|
245
|
+
applied,
|
|
223
246
|
hoisted_keys: hoist.hoisted_keys,
|
|
224
247
|
backup_path: backupPath,
|
|
225
|
-
parse_smoke: parseSmoke
|
|
248
|
+
parse_smoke: parseSmoke,
|
|
249
|
+
blockers: writeBlockers
|
|
226
250
|
};
|
|
227
251
|
}
|
|
228
252
|
function isCodexConfigToml(file) {
|
|
@@ -451,12 +475,6 @@ function tomlRewriteSmoke(text) {
|
|
|
451
475
|
invalid_table_header: badHeader || null
|
|
452
476
|
};
|
|
453
477
|
}
|
|
454
|
-
function profileTableBody(blocks, profile) {
|
|
455
|
-
const block = blocks.find((item) => item.table === `profiles.${profile}`);
|
|
456
|
-
if (!block)
|
|
457
|
-
return '';
|
|
458
|
-
return block.text.split('\n').filter((line) => !/^\s*\[/.test(line)).join('\n');
|
|
459
|
-
}
|
|
460
478
|
function configIds(text) {
|
|
461
479
|
const ids = { keys: new Set(), tables: new Set() };
|
|
462
480
|
for (const block of tomlBlocks(text)) {
|
|
@@ -512,6 +530,12 @@ function mergeMachineLocalIntoUserConfig(userText, machineText, commentLine) {
|
|
|
512
530
|
}
|
|
513
531
|
return normalizeProjectText(sections.join('\n\n'));
|
|
514
532
|
}
|
|
533
|
+
function stripSksMovedComments(text) {
|
|
534
|
+
return normalizeProjectText(String(text || '')
|
|
535
|
+
.split('\n')
|
|
536
|
+
.filter((line) => !/^\s*#\s*SKS moved machine-local Codex config\b/i.test(line))
|
|
537
|
+
.join('\n'));
|
|
538
|
+
}
|
|
515
539
|
function collectTomlSections(text, preamble, tables) {
|
|
516
540
|
for (const block of tomlBlocks(text)) {
|
|
517
541
|
if (block.table)
|
|
@@ -16,9 +16,14 @@ export async function postcheckCodexStartupConfig(input) {
|
|
|
16
16
|
const rolePlan = await installOfficialSubagentAgentConfigs(root, { apply: false });
|
|
17
17
|
const tomlSmoke = tomlSyntaxSmoke(text);
|
|
18
18
|
const orphanChildTables = orphanMcpChildTables(text);
|
|
19
|
+
const multiAgentV2Disabled = officialConfig.multiAgentV2.enabled === false;
|
|
19
20
|
const blockers = [
|
|
20
21
|
...(!configPresent ? ['project_official_subagent_config_missing'] : []),
|
|
21
22
|
...(!tomlValidation.ok ? ['project_official_subagent_config_toml_parse_failed'] : []),
|
|
23
|
+
...(multiAgentV2Disabled ? ['official_subagent_multi_agent_v2_disabled'] : []),
|
|
24
|
+
...(multiAgentV2Disabled && officialConfig.enabled === false
|
|
25
|
+
? ['official_subagent_agents_disabled']
|
|
26
|
+
: []),
|
|
22
27
|
...officialConfig.blockers.map((item) => `official_subagent_config:${item}`),
|
|
23
28
|
...rolePlan.missing.map((file) => `missing_official_subagent_agent:${file}`),
|
|
24
29
|
...rolePlan.stale.map((file) => `stale_official_subagent_agent:${file}`),
|
|
@@ -160,6 +160,8 @@ function upsertTomlTable(text, table, block) {
|
|
|
160
160
|
break;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
while (end > start + 1 && !String(lines[end - 1] || '').trim())
|
|
164
|
+
end -= 1;
|
|
163
165
|
lines.splice(start, end - start, ...blockLines);
|
|
164
166
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
165
167
|
}
|
|
@@ -158,22 +158,25 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
|
158
158
|
const sessionId = request.session_id ? canonicalSessionId(request.session_id) : null;
|
|
159
159
|
const pin = sessionId ? pins.find((entry) => entry.thread_id === sessionId) || null : null;
|
|
160
160
|
if (pin && pin.public_model === publicModel) {
|
|
161
|
-
if (pin.public_model !== publicModel
|
|
162
|
-
|| pin.catalog_generation !== policy.catalog_generation
|
|
163
|
-
|| pin.route_policy_generation !== policy.policy_generation) {
|
|
164
|
-
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
165
|
-
}
|
|
166
161
|
const current = policy.model_routes[publicModel];
|
|
167
162
|
if (!current || current.provider_id !== pin.provider_id || current.upstream_model !== pin.upstream_model) {
|
|
168
163
|
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
169
164
|
}
|
|
165
|
+
const aged = pin.catalog_generation !== policy.catalog_generation
|
|
166
|
+
|| pin.route_policy_generation !== policy.policy_generation;
|
|
170
167
|
return {
|
|
171
168
|
provider_id: pin.provider_id,
|
|
172
169
|
public_model: publicModel,
|
|
173
170
|
upstream_model: pin.upstream_model,
|
|
174
|
-
catalog_generation:
|
|
175
|
-
route_policy_generation:
|
|
176
|
-
session_pin:
|
|
171
|
+
catalog_generation: policy.catalog_generation,
|
|
172
|
+
route_policy_generation: policy.policy_generation,
|
|
173
|
+
session_pin: aged
|
|
174
|
+
? {
|
|
175
|
+
...pin,
|
|
176
|
+
catalog_generation: policy.catalog_generation,
|
|
177
|
+
route_policy_generation: policy.policy_generation,
|
|
178
|
+
}
|
|
179
|
+
: pin,
|
|
177
180
|
};
|
|
178
181
|
}
|
|
179
182
|
const route = policy.model_routes[publicModel];
|
|
@@ -22,11 +22,13 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
22
22
|
if (options.active_catalog_generation && options.active_catalog_generation !== policy.catalog_generation) {
|
|
23
23
|
return blocked(base, 'catalog_route_index_stale');
|
|
24
24
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const threadPin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
|
|
26
|
+
const pin = threadPin && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
|
|
27
|
+
const pinAffinity = pin ? sessionPinAffinity(pin, policy, options.route_index, options.registry) : null;
|
|
28
|
+
if (pinAffinity === 'unprovable')
|
|
29
|
+
return blocked(base, 'session_pin_route_unavailable');
|
|
30
|
+
const pinClaim = pinAffinity && pinAffinity !== 'honored' ? pinAffinity : null;
|
|
31
|
+
if (pin && pinAffinity === 'honored') {
|
|
30
32
|
const route = { provider_id: pin.provider_id, upstream_model: pin.upstream_model };
|
|
31
33
|
const providerBlocker = validateProviderRoute(route, request.requested_endpoint_origin, options.registry, options.route_index);
|
|
32
34
|
if (providerBlocker)
|
|
@@ -51,6 +53,8 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
51
53
|
return blocked(base, 'catalog_model_route_missing');
|
|
52
54
|
if (!sameTarget(indexed, policyTarget))
|
|
53
55
|
return blocked(base, 'bridge_route_policy_route_index_mismatch');
|
|
56
|
+
if (pinClaim && !sameTarget(indexed, pinClaim))
|
|
57
|
+
return blocked(base, 'session_pin_route_unavailable');
|
|
54
58
|
const providerBlocker = validateProviderRoute(indexed, request.requested_endpoint_origin, options.registry, options.route_index);
|
|
55
59
|
if (providerBlocker)
|
|
56
60
|
return blocked(base, providerBlocker);
|
|
@@ -76,23 +80,22 @@ export function resolveBridgeRequestRoute(request, policy, options) {
|
|
|
76
80
|
recovery_action: null
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
|
-
function
|
|
80
|
-
if (pin.
|
|
81
|
-
|
|
82
|
-
return 'session_pin_route_unavailable';
|
|
83
|
-
}
|
|
84
|
-
if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id]) {
|
|
85
|
-
return 'session_pin_route_unavailable';
|
|
86
|
-
}
|
|
83
|
+
function sessionPinAffinity(pin, policy, routeIndex, registry) {
|
|
84
|
+
if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id])
|
|
85
|
+
return 'unprovable';
|
|
87
86
|
const model = canonicalizeBridgeModelId(pin.public_model);
|
|
88
87
|
const upstream = normalizeBridgeUpstreamModelId(pin.upstream_model);
|
|
89
88
|
if (!model || !upstream)
|
|
90
|
-
return '
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
89
|
+
return 'unprovable';
|
|
90
|
+
const claim = { provider_id: pin.provider_id, upstream_model: upstream };
|
|
91
|
+
if (pin.catalog_generation !== policy.catalog_generation
|
|
92
|
+
|| pin.route_policy_generation !== policy.policy_generation) {
|
|
93
|
+
return claim;
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
|
|
96
|
+
if (!indexed || !sameTarget(indexed, claim))
|
|
97
|
+
return claim;
|
|
98
|
+
return 'honored';
|
|
96
99
|
}
|
|
97
100
|
function validateProviderRoute(route, requestedOrigin, registry, routeIndex) {
|
|
98
101
|
if (!isProviderId(route.provider_id))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
|
|
@@ -104,7 +105,21 @@ function phaseInputHash(root, id) {
|
|
|
104
105
|
return { rel, hash: 'missing' };
|
|
105
106
|
return hashSemanticPath(root, rel);
|
|
106
107
|
});
|
|
107
|
-
return hashJson({
|
|
108
|
+
return hashJson({
|
|
109
|
+
id,
|
|
110
|
+
package: packageIdentity(),
|
|
111
|
+
files,
|
|
112
|
+
env: phaseEnvPresence(id),
|
|
113
|
+
semantic_state: phaseSemanticState(root, id),
|
|
114
|
+
project_codex_config: hashSemanticPath(root, '.codex/config.toml').hash,
|
|
115
|
+
codex_home_config: hashFileIfExists(codexHomeConfigPath()),
|
|
116
|
+
phase_schema_version: 3
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function codexHomeConfigPath() {
|
|
120
|
+
const codexHome = process.env.CODEX_HOME
|
|
121
|
+
|| path.join(process.env.HOME || os.homedir(), '.codex');
|
|
122
|
+
return path.join(codexHome, 'config.toml');
|
|
108
123
|
}
|
|
109
124
|
function phaseInputFiles(id) {
|
|
110
125
|
if (id.includes('context7'))
|
|
@@ -17,6 +17,8 @@ export async function runDoctorIdempotence(root) {
|
|
|
17
17
|
blockers.push(`second_doctor_exit_${second.exit_code}`);
|
|
18
18
|
if (changedFilesSecond.length > 0)
|
|
19
19
|
blockers.push('second_doctor_not_noop');
|
|
20
|
+
if (!reportsChangedFiles(second))
|
|
21
|
+
blockers.push('second_doctor_changed_files_unavailable');
|
|
20
22
|
const report = {
|
|
21
23
|
schema: 'sks.doctor-idempotence.v1',
|
|
22
24
|
ok: blockers.length === 0,
|
|
@@ -60,6 +62,15 @@ async function runDoctor(root, home, codexHome) {
|
|
|
60
62
|
parsed
|
|
61
63
|
};
|
|
62
64
|
}
|
|
65
|
+
function reportsChangedFiles(run) {
|
|
66
|
+
if (!run.parsed)
|
|
67
|
+
return false;
|
|
68
|
+
return [
|
|
69
|
+
run.parsed?.repair?.doctor_transaction?.changed_files,
|
|
70
|
+
run.parsed?.doctor_fix_transaction?.changed_files,
|
|
71
|
+
run.parsed?.changed_files
|
|
72
|
+
].some((value) => Array.isArray(value));
|
|
73
|
+
}
|
|
63
74
|
function extractChangedFiles(run) {
|
|
64
75
|
const repair = run.parsed?.repair || {};
|
|
65
76
|
const candidates = [
|
|
@@ -1,4 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inspectOfficialSubagentToml, readOfficialSubagentConfig } from '../subagents/official-subagent-config.js';
|
|
1
5
|
import {} from './doctor-transaction.js';
|
|
6
|
+
export async function verifyCodexConfigsOnDisk(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const home = input.home || process.env.HOME || os.homedir();
|
|
9
|
+
const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(home, '.codex');
|
|
10
|
+
const projectConfigPath = path.join(root, '.codex', 'config.toml');
|
|
11
|
+
const codexHomeConfigPath = path.join(codexHome, 'config.toml');
|
|
12
|
+
const blockers = [];
|
|
13
|
+
const [projectText, homeText] = await Promise.all([
|
|
14
|
+
fs.readFile(projectConfigPath, 'utf8').catch(() => null),
|
|
15
|
+
fs.readFile(codexHomeConfigPath, 'utf8').catch(() => null)
|
|
16
|
+
]);
|
|
17
|
+
if (projectText !== null && !inspectOfficialSubagentToml(projectText).ok) {
|
|
18
|
+
blockers.push('project_codex_config_unparseable_after_repair');
|
|
19
|
+
}
|
|
20
|
+
if (homeText !== null && !inspectOfficialSubagentToml(homeText).ok) {
|
|
21
|
+
blockers.push('codex_home_config_unparseable_after_repair');
|
|
22
|
+
}
|
|
23
|
+
let multiAgentV2Enabled = null;
|
|
24
|
+
let agentsEnabled = null;
|
|
25
|
+
if (!blockers.length) {
|
|
26
|
+
const config = await readOfficialSubagentConfig(root, { home, codexHome }).catch(() => null);
|
|
27
|
+
if (config) {
|
|
28
|
+
multiAgentV2Enabled = config.multiAgentV2.enabled;
|
|
29
|
+
agentsEnabled = config.enabled;
|
|
30
|
+
for (const blocker of config.blockers)
|
|
31
|
+
blockers.push(`official_subagent_config:${blocker}`);
|
|
32
|
+
if (multiAgentV2Enabled === false) {
|
|
33
|
+
blockers.push('official_subagent_multi_agent_v2_disabled_after_repair');
|
|
34
|
+
if (agentsEnabled === false)
|
|
35
|
+
blockers.push('official_subagent_agents_disabled_after_repair');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
schema: 'sks.doctor-config-disk-verification.v1',
|
|
41
|
+
ok: blockers.length === 0,
|
|
42
|
+
project_config_path: projectConfigPath,
|
|
43
|
+
codex_home_config_path: codexHomeConfigPath,
|
|
44
|
+
project_config_present: projectText !== null,
|
|
45
|
+
codex_home_config_present: homeText !== null,
|
|
46
|
+
multi_agent_v2_enabled: multiAgentV2Enabled,
|
|
47
|
+
agents_enabled: agentsEnabled,
|
|
48
|
+
blockers: [...new Set(blockers)]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
2
51
|
export function doctorRepairPostcheck(transaction) {
|
|
3
52
|
const phases = transaction?.phases || [];
|
|
4
53
|
const requiredBlockers = phases
|
|
@@ -3,6 +3,47 @@ import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
|
3
3
|
import { isDoctorPhaseClean, markDoctorPhaseClean } from './doctor-dirty-planner.js';
|
|
4
4
|
import { ui as cliUi } from '../../cli/cli-theme.js';
|
|
5
5
|
import { messageOf } from '../errors/message.js';
|
|
6
|
+
const CHANGED_PATH_KEYS = Object.freeze([
|
|
7
|
+
'repaired_paths', 'created_files', 'created', 'updated', 'changed_files',
|
|
8
|
+
'written_files', 'removed_files', 'removed', 'quarantined', 'backups'
|
|
9
|
+
]);
|
|
10
|
+
export function changedFilesFromRepairReport(report) {
|
|
11
|
+
const found = new Set();
|
|
12
|
+
const walk = (value, depth) => {
|
|
13
|
+
if (depth > 6 || !value || typeof value !== 'object')
|
|
14
|
+
return;
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value)
|
|
17
|
+
walk(item, depth + 1);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const row = value;
|
|
21
|
+
for (const key of CHANGED_PATH_KEYS) {
|
|
22
|
+
const entries = row[key];
|
|
23
|
+
if (Array.isArray(entries)) {
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const text = String(entry || '').trim();
|
|
26
|
+
if (text)
|
|
27
|
+
found.add(text);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (typeof entries === 'string' && entries.trim()) {
|
|
31
|
+
found.add(entries.trim());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (row.changed === true || row.applied === true) {
|
|
35
|
+
for (const key of ['config_path', 'backup_path', 'path', 'file']) {
|
|
36
|
+
const text = String(row[key] || '').trim();
|
|
37
|
+
if (text)
|
|
38
|
+
found.add(text);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const nested of Object.values(row))
|
|
42
|
+
walk(nested, depth + 1);
|
|
43
|
+
};
|
|
44
|
+
walk(report, 0);
|
|
45
|
+
return [...found].sort();
|
|
46
|
+
}
|
|
6
47
|
export async function runDoctorFixTransaction(input) {
|
|
7
48
|
const startedAt = nowIso();
|
|
8
49
|
const phases = [];
|
|
@@ -110,7 +151,8 @@ export async function writeDoctorFixTransaction(input) {
|
|
|
110
151
|
started_at: phase.started_at || null,
|
|
111
152
|
completed_at: phase.completed_at || null,
|
|
112
153
|
duration_ms: Number.isFinite(phase.duration_ms) ? Number(phase.duration_ms) : null,
|
|
113
|
-
rollback_performed: phase.rollback_performed === true
|
|
154
|
+
rollback_performed: phase.rollback_performed === true,
|
|
155
|
+
changed_files: [...new Set(phase.changed_files || [])].sort()
|
|
114
156
|
}));
|
|
115
157
|
const postcheckOk = phases.every((phase) => phase.ok || phase.required_for_ready === false);
|
|
116
158
|
const mutationsWithoutRollback = phases.filter((phase) => phase.required_for_ready && phase.repaired && !phase.rollback_evidence).length;
|
|
@@ -124,6 +166,7 @@ export async function writeDoctorFixTransaction(input) {
|
|
|
124
166
|
postcheck_ok: postcheckOk && mutationsWithoutRollback === 0,
|
|
125
167
|
rollback_performed: input.rollbackPerformed === true,
|
|
126
168
|
mutations_without_rollback: mutationsWithoutRollback,
|
|
169
|
+
changed_files: [...new Set(phases.flatMap((phase) => phase.changed_files))].sort(),
|
|
127
170
|
raw_secret_values_recorded: false,
|
|
128
171
|
skipped_clean_phases: phases.filter((phase) => phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
|
|
129
172
|
dirty_phases: input.dirtyPlan?.phases.filter((phase) => phase.status === 'dirty').map((phase) => phase.id) || phases.filter((phase) => !phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
|
|
@@ -158,7 +201,9 @@ function normalizePhase(definition, result, fallback, startedMs) {
|
|
|
158
201
|
started_at: phase.started_at || fallback.started_at || nowIso(),
|
|
159
202
|
completed_at: phase.completed_at || nowIso(),
|
|
160
203
|
duration_ms: phase.duration_ms ?? Math.max(0, Date.now() - startedMs),
|
|
161
|
-
rollback_performed: phase.rollback_performed === true
|
|
204
|
+
rollback_performed: phase.rollback_performed === true,
|
|
205
|
+
changed_files: phase.changed_files
|
|
206
|
+
?? (definition.report ? changedFilesFromRepairReport(definition.report()) : [])
|
|
162
207
|
};
|
|
163
208
|
}
|
|
164
209
|
function mergePhase(phase, update) {
|
|
@@ -144,7 +144,7 @@ function projectSksHookCommand(value) {
|
|
|
144
144
|
const command = String(value || '').trim();
|
|
145
145
|
if (/[;&|`$<>\r\n]/.test(command))
|
|
146
146
|
return null;
|
|
147
|
-
return command.match(/^node\s
|
|
147
|
+
return command.match(/^(?:\S*[\\/])?(?:node\s+\S*sks\.js|sks)\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
|
|
148
148
|
}
|
|
149
149
|
function hookRef(event, matcher, command) {
|
|
150
150
|
return `${event}:${String(matcher ?? '')}:${command}`;
|
package/dist/core/init.js
CHANGED
|
@@ -11,13 +11,15 @@ import { OFFICIAL_SUBAGENT_REVIEW_POLICY_TEXT } from './official-subagent-review
|
|
|
11
11
|
import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, CODEX_WEB_VERIFICATION_POLICY, DEFAULT_CODEX_APP_PLUGINS, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, IMAGEGEN_SOCIAL_SOURCE_POLICY, LEGACY_DOLLAR_SKILL_NAMES, OPENAI_CHATGPT_IMAGES_2_DOC_URL, OPENAI_GPT_IMAGE_2_MODEL_DOC_URL, OPENAI_IMAGE_GENERATION_DOC_URL, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, prefixKnownSksDollarReferences, productDesignPluginPolicyText, sksPrefixedDollarCommand, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.js';
|
|
12
12
|
import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.js';
|
|
13
13
|
import { CODEX_HOOK_EVENT_STATE_KEYS } from './codex-compat/codex-hook-events.js';
|
|
14
|
+
import { MANAGED_CODEX_FEATURE_FLAGS, REMOVED_CODEX_FEATURE_FLAGS } from './codex/codex-feature-flags.js';
|
|
15
|
+
import { writeCodexConfigGuarded } from './codex/codex-config-guard.js';
|
|
14
16
|
import { codexCommandHookCurrentHash } from './codex-hooks/codex-hook-hash.js';
|
|
15
17
|
import { buildSksCoreSkillManifest, isCoreSkillName, legacyCoreSkillNames } from './codex-native/core-skill-manifest.js';
|
|
16
18
|
import { syncCoreSkillsIntegrity } from './codex-native/core-skill-integrity.js';
|
|
17
19
|
import { AUTHORITATIVE_SKS_SKILL_ROOT_REFERENCE } from './codex-native/sks-skill-paths.js';
|
|
18
20
|
import { currentGeneratedFileInventory, installCodexAgents, pruneStaleGeneratedFiles, REMOVED_SKS_SKILL_NAMES } from './init/skills.js';
|
|
19
21
|
import { reconcileManagedSkillInstallation } from './init/managed-skill-install.js';
|
|
20
|
-
import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText, resolveInheritedOfficialSubagentConfigPath } from './subagents/official-subagent-config.js';
|
|
22
|
+
import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText, resolveInheritedOfficialSubagentConfigPath, stampSksManagedCodexConfigMarker } from './subagents/official-subagent-config.js';
|
|
21
23
|
import { escapeRegExp } from './text/regex.js';
|
|
22
24
|
export { installGlobalSkills, installProjectSkills, installSkills } from './init/skills.js';
|
|
23
25
|
const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
|
|
@@ -206,6 +208,8 @@ function upsertCodexTrustTomlTable(text, table, block) {
|
|
|
206
208
|
break;
|
|
207
209
|
}
|
|
208
210
|
}
|
|
211
|
+
while (end > start + 1 && !String(lines[end - 1] || '').trim())
|
|
212
|
+
end -= 1;
|
|
209
213
|
lines.splice(start, end - start, ...blockLines);
|
|
210
214
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
211
215
|
}
|
|
@@ -631,14 +635,11 @@ export async function initProject(root, opts = {}) {
|
|
|
631
635
|
project_install: 'npm i -D sneakoscope && npx sks setup --install-scope project'
|
|
632
636
|
};
|
|
633
637
|
}
|
|
634
|
-
const MANAGED_CODEX_FEATURE_FLAGS = ['hooks', 'fast_mode', 'apps'];
|
|
635
|
-
const REMOVED_CODEX_FEATURE_FLAGS = [
|
|
636
|
-
'remote_control', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'browser_use',
|
|
637
|
-
'browser_use_external', 'image_generation', 'in_app_browser', 'guardian_approval',
|
|
638
|
-
'tool_suggest', 'plugins', 'codex_hooks', 'multi_agent'
|
|
639
|
-
];
|
|
640
638
|
function mergeManagedCodexConfigToml(existingContent = '', opts = {}) {
|
|
641
639
|
let next = String(existingContent || '').trimEnd();
|
|
640
|
+
if (opts.sksOwned === true || opts.configWasFresh === true) {
|
|
641
|
+
next = stampSksManagedCodexConfigMarker(next);
|
|
642
|
+
}
|
|
642
643
|
next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
|
|
643
644
|
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
644
645
|
next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
|
|
@@ -918,6 +919,7 @@ export async function initProject(root, opts = {}) {
|
|
|
918
919
|
else {
|
|
919
920
|
const managedCodexConfig = await mergeGlobalCodexConfigIfAvailable(mergeManagedCodexConfigToml(existingCodexConfig, {
|
|
920
921
|
sksOwned: configPreviouslySksOwned,
|
|
922
|
+
configWasFresh,
|
|
921
923
|
inheritedText: inheritedCodexConfig
|
|
922
924
|
}), generatedCodexConfigPath, { home: opts.home, codexHome: opts.codexHome });
|
|
923
925
|
const managedConfigValidation = inspectOfficialSubagentToml(managedCodexConfig);
|
|
@@ -934,16 +936,37 @@ export async function initProject(root, opts = {}) {
|
|
|
934
936
|
created.push('.codex/config.toml merge skipped (manual repair required)');
|
|
935
937
|
}
|
|
936
938
|
else {
|
|
937
|
-
await
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
};
|
|
946
|
-
|
|
939
|
+
const guarded = await writeCodexConfigGuarded({
|
|
940
|
+
root,
|
|
941
|
+
configPath: generatedCodexConfigPath,
|
|
942
|
+
before: existingCodexConfig,
|
|
943
|
+
cause: 'sks-init-managed-project-config',
|
|
944
|
+
ownershipVerified: configWasFresh || configPreviouslySksOwned,
|
|
945
|
+
preserveTextFormatting: true,
|
|
946
|
+
mutate: () => managedCodexConfig
|
|
947
|
+
});
|
|
948
|
+
codexConfigInstall = guarded.ok
|
|
949
|
+
? {
|
|
950
|
+
ok: true,
|
|
951
|
+
status: managedCodexConfig === existingCodexConfig ? 'present' : 'written',
|
|
952
|
+
config_path: generatedCodexConfigPath,
|
|
953
|
+
backup_path: guarded.backup_path,
|
|
954
|
+
manual_blockers: [],
|
|
955
|
+
warnings: officialSubagentConfigWarnings(managedCodexConfig, inheritedCodexConfig)
|
|
956
|
+
}
|
|
957
|
+
: {
|
|
958
|
+
ok: false,
|
|
959
|
+
status: guarded.status,
|
|
960
|
+
config_path: generatedCodexConfigPath,
|
|
961
|
+
backup_path: guarded.backup_path,
|
|
962
|
+
manual_blockers: [`manual_unmanaged_project_codex_config:${guarded.status}`],
|
|
963
|
+
warnings: [],
|
|
964
|
+
operator_actions: [
|
|
965
|
+
`${generatedCodexConfigPath} is not proven SKS-owned, so setup left it unchanged. `
|
|
966
|
+
+ 'Run `sks config adopt` to bring it under management, or delete it and re-run `sks setup`.'
|
|
967
|
+
]
|
|
968
|
+
};
|
|
969
|
+
created.push(guarded.ok ? '.codex/config.toml' : '.codex/config.toml preserved (user-owned)');
|
|
947
970
|
}
|
|
948
971
|
}
|
|
949
972
|
}
|
|
@@ -148,6 +148,7 @@ export function buildPipelinePlan(input = {}) {
|
|
|
148
148
|
verification,
|
|
149
149
|
engineering_sanity_review: null,
|
|
150
150
|
architecture_map: null,
|
|
151
|
+
architecture_map_unavailable: null,
|
|
151
152
|
invariants,
|
|
152
153
|
proof_field: proof,
|
|
153
154
|
ssot_guard: buildSsotGuard({ route: route?.id || 'SKS', mode: route?.mode || 'SKS', task }),
|
|
@@ -187,6 +188,13 @@ export async function writePipelinePlan(dir, input = {}) {
|
|
|
187
188
|
}
|
|
188
189
|
if (planStagesArchitectureMap(plan)) {
|
|
189
190
|
plan.architecture_map = createArchitectureMapPlanBinding();
|
|
191
|
+
const seeded = await maybeSeedArchitectureMapForPlan({
|
|
192
|
+
root, dir, plan, missionId: input.missionId, taskProfile
|
|
193
|
+
});
|
|
194
|
+
if (seeded && seeded.ok === false) {
|
|
195
|
+
plan.architecture_map = null;
|
|
196
|
+
plan.architecture_map_unavailable = { reason: seeded.reason || 'architecture_map_baseline_unavailable' };
|
|
197
|
+
}
|
|
190
198
|
}
|
|
191
199
|
await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
|
|
192
200
|
if (plan.engineering_sanity_review) {
|
|
@@ -201,9 +209,6 @@ export async function writePipelinePlan(dir, input = {}) {
|
|
|
201
209
|
await writeJsonAtomic(reviewFile, createEngineeringSanityReviewSeed(plan.route?.command || plan.route?.id || null, report, reportText));
|
|
202
210
|
}
|
|
203
211
|
}
|
|
204
|
-
await maybeSeedArchitectureMapForPlan({
|
|
205
|
-
root, dir, plan, missionId: input.missionId, taskProfile
|
|
206
|
-
});
|
|
207
212
|
return plan;
|
|
208
213
|
}
|
|
209
214
|
function planStagesEngineeringSanityReview(plan) {
|
|
@@ -100,7 +100,8 @@ export function officialSubagentFanoutPolicy(input = {}) {
|
|
|
100
100
|
...(input.suggestedRoles === undefined ? {} : { suggestedRoles: input.suggestedRoles }),
|
|
101
101
|
...(input.independentSliceCount === undefined ? {} : { independentSliceCount: input.independentSliceCount }),
|
|
102
102
|
...(input.maxThreads === undefined ? {} : { maxThreads: input.maxThreads }),
|
|
103
|
-
...(input.hardware === undefined ? {} : { hardware: input.hardware })
|
|
103
|
+
...(input.hardware === undefined ? {} : { hardware: input.hardware }),
|
|
104
|
+
...(input.requiresWrite === undefined ? {} : { requiresWrite: input.requiresWrite })
|
|
104
105
|
});
|
|
105
106
|
const requested = explicit ? explicitRequested : automatic.count;
|
|
106
107
|
const automaticReviewerCeiling = automatic.criticalMultiDomain
|
|
@@ -141,7 +142,8 @@ function automaticSubagentFanout(input) {
|
|
|
141
142
|
const critical = CRITICAL_RISK_RE.test(text);
|
|
142
143
|
const largeScale = LARGE_SCALE_WORK_RE.test(text);
|
|
143
144
|
const criticalMultiDomain = highRisk && critical && riskDomains.length >= 3;
|
|
144
|
-
const reviewerOnly = isReviewerOnlyFanout(input.suggestedRoles || [])
|
|
145
|
+
const reviewerOnly = isReviewerOnlyFanout(input.suggestedRoles || [])
|
|
146
|
+
&& input.requiresWrite !== true;
|
|
145
147
|
const massLane = decideSubagentModel({ description: text }).policy;
|
|
146
148
|
const massParallel = MASS_PARALLEL_WORK_RE.test(text)
|
|
147
149
|
&& !NON_CHEAP_MASS_ACTION_RE.test(text)
|
|
@@ -20,17 +20,23 @@ export const LEGACY_SKS_MAX_THREAD_VALUES = Object.freeze([4, 5, 6, 12]);
|
|
|
20
20
|
export const AGENTS_MAX_CONCURRENT_THREADS_KEY = 'max_concurrent_threads_per_session';
|
|
21
21
|
export const LEGACY_AGENTS_MAX_THREADS_KEY = 'max_threads';
|
|
22
22
|
export const LEGACY_AGENTS_JOB_MAX_RUNTIME_KEY = 'job_max_runtime_seconds';
|
|
23
|
+
export const SKS_MANAGED_CODEX_CONFIG_MARKER = '# SKS-MANAGED-CODEX-CONFIG';
|
|
23
24
|
export function mergeOfficialSubagentConfig(text = '', opts = {}) {
|
|
25
|
+
return mergeOfficialSubagentConfigResult(text, opts).text;
|
|
26
|
+
}
|
|
27
|
+
export function mergeOfficialSubagentConfigResult(text = '', opts = {}) {
|
|
24
28
|
const source = String(text || '');
|
|
25
|
-
if (!inspectToml(source).ok)
|
|
26
|
-
return source;
|
|
29
|
+
if (!inspectToml(source).ok) {
|
|
30
|
+
return { text: source, blockers: ['project_official_subagent_config_toml_parse_failed'] };
|
|
31
|
+
}
|
|
27
32
|
const inherited = parsedToml(opts.inheritedText || '');
|
|
28
33
|
const inheritedAgents = objectValue(inherited?.agents);
|
|
29
34
|
const inheritedFeatures = objectValue(inherited?.features);
|
|
30
35
|
let next = source.trimEnd();
|
|
31
36
|
if (!next.trim())
|
|
32
|
-
next =
|
|
37
|
+
next = SKS_MANAGED_CODEX_CONFIG_MARKER;
|
|
33
38
|
if (opts.sksOwned === true) {
|
|
39
|
+
next = stampManagedConfigMarker(next);
|
|
34
40
|
next = removeExactLegacyManagedAgentBlocks(next);
|
|
35
41
|
next = stripLegacyUnsupportedAgentKeys(next);
|
|
36
42
|
}
|
|
@@ -48,10 +54,12 @@ export function mergeOfficialSubagentConfig(text = '', opts = {}) {
|
|
|
48
54
|
next = mergeOfficialMultiAgentV2FeatureConfig(next, {
|
|
49
55
|
sksOwned: opts.sksOwned === true,
|
|
50
56
|
inheritedFeatures,
|
|
51
|
-
maxThreads: readAgentsMaxThreads(next) || targetMaxThreads
|
|
57
|
+
maxThreads: Math.min(readAgentsMaxThreads(next) || targetMaxThreads, HARD_NARUTO_MAX_THREADS)
|
|
52
58
|
});
|
|
53
59
|
const merged = ensureTrailingNewline(next);
|
|
54
|
-
|
|
60
|
+
if (inspectToml(merged).ok)
|
|
61
|
+
return { text: merged, blockers: [] };
|
|
62
|
+
return { text: source, blockers: ['official_subagent_config_merge_produced_invalid_toml'] };
|
|
55
63
|
}
|
|
56
64
|
export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
|
|
57
65
|
const source = String(text || '');
|
|
@@ -61,18 +69,21 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
|
|
|
61
69
|
const inheritedMaV2 = featureTomlObject(inheritedFeatures.multi_agent_v2);
|
|
62
70
|
let next = source.trimEnd();
|
|
63
71
|
if (!next.trim())
|
|
64
|
-
next =
|
|
65
|
-
if (
|
|
72
|
+
next = SKS_MANAGED_CODEX_CONFIG_MARKER;
|
|
73
|
+
if (hasUnmanageableMultiAgentV2Declaration(next))
|
|
74
|
+
return ensureTrailingNewline(next);
|
|
75
|
+
const booleanForm = readTomlTableBoolean(next, 'features', 'multi_agent_v2');
|
|
76
|
+
if (booleanForm !== null) {
|
|
66
77
|
next = removeTomlTableKey(next, 'features', 'multi_agent_v2');
|
|
67
78
|
}
|
|
68
79
|
const maxThreads = boundedManagedMaxThreads(opts.maxThreads, DEFAULT_OFFICIAL_SUBAGENT_MAX_THREADS);
|
|
69
80
|
const targetTotal = Math.max(1, maxThreads + 1);
|
|
70
81
|
if (!hasTomlTable(next, 'features.multi_agent_v2')) {
|
|
71
|
-
if (inheritedMaV2)
|
|
82
|
+
if (inheritedMaV2 && booleanForm === null)
|
|
72
83
|
return ensureTrailingNewline(next);
|
|
73
84
|
next = upsertTomlTable(next, 'features.multi_agent_v2', [
|
|
74
85
|
'[features.multi_agent_v2]',
|
|
75
|
-
|
|
86
|
+
`enabled = ${booleanForm === null ? true : booleanForm}`,
|
|
76
87
|
`max_concurrent_threads_per_session = ${targetTotal}`,
|
|
77
88
|
'expose_spawn_agent_model_overrides = true'
|
|
78
89
|
].join('\n'));
|
|
@@ -81,7 +92,7 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
|
|
|
81
92
|
if (opts.sksOwned === true) {
|
|
82
93
|
next = upsertTomlTableKey(next, 'features.multi_agent_v2', 'enabled = true');
|
|
83
94
|
const currentTotal = readTomlTableInteger(next, 'features.multi_agent_v2', 'max_concurrent_threads_per_session');
|
|
84
|
-
if (currentTotal ===
|
|
95
|
+
if (currentTotal === null || LEGACY_MANAGED_V2_TOTAL_THREADS.includes(currentTotal)) {
|
|
85
96
|
next = upsertTomlTableKey(next, 'features.multi_agent_v2', `max_concurrent_threads_per_session = ${targetTotal}`);
|
|
86
97
|
}
|
|
87
98
|
if (!hasTomlTableKey(next, 'features.multi_agent_v2', 'expose_spawn_agent_model_overrides')) {
|
|
@@ -105,6 +116,10 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
|
|
|
105
116
|
return ensureTrailingNewline(next);
|
|
106
117
|
}
|
|
107
118
|
const DEFAULT_NARUTO_LEGACY_TOTAL_THREADS = 13;
|
|
119
|
+
const LEGACY_MANAGED_V2_TOTAL_THREADS = Object.freeze([
|
|
120
|
+
...LEGACY_SKS_MAX_THREAD_VALUES.map((value) => value + 1),
|
|
121
|
+
DEFAULT_NARUTO_LEGACY_TOTAL_THREADS
|
|
122
|
+
]);
|
|
108
123
|
export async function readOfficialSubagentConfig(root, opts = {}) {
|
|
109
124
|
const projectConfigPath = path.resolve(opts.projectConfigPath || path.join(root, '.codex', 'config.toml'));
|
|
110
125
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
@@ -173,6 +188,9 @@ export function officialSubagentConfigWarnings(text = '', inheritedText = '') {
|
|
|
173
188
|
? [`official_subagent_max_depth_coerced_to_one:${maxDepth.value}:${maxDepth.source}`]
|
|
174
189
|
: []),
|
|
175
190
|
...capacityNormalizationWarnings(maxThreads, multiAgentV2),
|
|
191
|
+
...(hasUnmanageableMultiAgentV2Declaration(text)
|
|
192
|
+
? ['project_multi_agent_v2_declaration_form_unmanaged']
|
|
193
|
+
: []),
|
|
176
194
|
...project.legacyWarnings,
|
|
177
195
|
...inherited.legacyWarnings
|
|
178
196
|
];
|
|
@@ -315,6 +333,8 @@ export function officialSubagentConfigOwnershipProof(input = {}) {
|
|
|
315
333
|
}
|
|
316
334
|
if (hasExactManagedConfigMarker(text))
|
|
317
335
|
reasons.push('managed_marker_or_hash');
|
|
336
|
+
if (hasManagedAgentsConfigFingerprint(text))
|
|
337
|
+
reasons.push('managed_agents_config_fingerprint');
|
|
318
338
|
if (migrationReceiptProvesManagedMaxThreads(input.migrationReceipt)) {
|
|
319
339
|
reasons.push('migration_receipt:agents.max_concurrent_threads_per_session');
|
|
320
340
|
}
|
|
@@ -482,7 +502,38 @@ function featureTomlObject(value) {
|
|
|
482
502
|
return Object.keys(row).length ? row : null;
|
|
483
503
|
}
|
|
484
504
|
function hasExactManagedConfigMarker(text) {
|
|
485
|
-
return
|
|
505
|
+
return /^\s*(?:#\s*SKS-MANAGED-CODEX-CONFIG\b|#\s*SKS managed Codex config\b|#\s*sks_managed_(?:body_)?sha256\s*=\s*["'][a-f0-9]{64}["'])/mi.test(text);
|
|
506
|
+
}
|
|
507
|
+
export function stampSksManagedCodexConfigMarker(text) {
|
|
508
|
+
return stampManagedConfigMarker(text);
|
|
509
|
+
}
|
|
510
|
+
function stampManagedConfigMarker(text) {
|
|
511
|
+
const source = String(text || '');
|
|
512
|
+
if (hasExactManagedConfigMarker(source))
|
|
513
|
+
return source;
|
|
514
|
+
const body = source.replace(/^\n+/, '');
|
|
515
|
+
return body ? `${SKS_MANAGED_CODEX_CONFIG_MARKER}\n${body}` : SKS_MANAGED_CODEX_CONFIG_MARKER;
|
|
516
|
+
}
|
|
517
|
+
const MANAGED_AGENTS_FINGERPRINT_KEYS = Object.freeze([
|
|
518
|
+
'enabled',
|
|
519
|
+
'max_depth',
|
|
520
|
+
'interrupt_message',
|
|
521
|
+
'default_subagent_model',
|
|
522
|
+
'default_subagent_reasoning_effort'
|
|
523
|
+
]);
|
|
524
|
+
export function hasManagedAgentsConfigFingerprint(text) {
|
|
525
|
+
const parsed = parsedToml(text);
|
|
526
|
+
if (!parsed)
|
|
527
|
+
return false;
|
|
528
|
+
const agents = objectValue(parsed.agents);
|
|
529
|
+
if (!MANAGED_AGENTS_FINGERPRINT_KEYS.every((key) => hasOwn(agents, key)))
|
|
530
|
+
return false;
|
|
531
|
+
if (readAgentsMaxThreadsFromRecord(agents) === undefined)
|
|
532
|
+
return false;
|
|
533
|
+
if (agents.max_depth !== DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH)
|
|
534
|
+
return false;
|
|
535
|
+
const multiAgentV2 = featureTomlObject(objectValue(parsed.features).multi_agent_v2);
|
|
536
|
+
return multiAgentV2 !== null && hasOwn(multiAgentV2, 'expose_spawn_agent_model_overrides');
|
|
486
537
|
}
|
|
487
538
|
function migrationReceiptProvesManagedMaxThreads(value) {
|
|
488
539
|
const root = objectValue(value);
|
|
@@ -676,11 +727,37 @@ function upsertDefaultUnlessInherited(text, inheritedAgents, key, line) {
|
|
|
676
727
|
}
|
|
677
728
|
function readTomlTableInteger(text, table, key) {
|
|
678
729
|
const line = tomlTableKeyLine(text, table, key);
|
|
679
|
-
const
|
|
680
|
-
|
|
730
|
+
const raw = line?.match(/^\s*[^=]+\s*=\s*([^#]+?)\s*(?:#.*)?$/)?.[1];
|
|
731
|
+
return parseTomlInteger(raw);
|
|
732
|
+
}
|
|
733
|
+
function parseTomlInteger(raw) {
|
|
734
|
+
const value = String(raw ?? '').trim().replace(/_/g, '');
|
|
735
|
+
const match = value.match(/^([+-]?)(?:0x([0-9a-fA-F]+)|0o([0-7]+)|0b([01]+)|([0-9]+))$/);
|
|
736
|
+
if (!match)
|
|
681
737
|
return null;
|
|
682
|
-
const
|
|
683
|
-
|
|
738
|
+
const magnitude = match[2] !== undefined
|
|
739
|
+
? Number.parseInt(match[2], 16)
|
|
740
|
+
: match[3] !== undefined
|
|
741
|
+
? Number.parseInt(match[3], 8)
|
|
742
|
+
: match[4] !== undefined
|
|
743
|
+
? Number.parseInt(match[4], 2)
|
|
744
|
+
: Number(match[5]);
|
|
745
|
+
const parsed = (match[1] === '-' ? -1 : 1) * magnitude;
|
|
746
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
747
|
+
}
|
|
748
|
+
function hasUnmanageableMultiAgentV2Declaration(text) {
|
|
749
|
+
const parsed = parsedToml(text);
|
|
750
|
+
if (!parsed)
|
|
751
|
+
return false;
|
|
752
|
+
if (!hasOwn(objectValue(parsed.features), 'multi_agent_v2'))
|
|
753
|
+
return false;
|
|
754
|
+
return !hasTomlTable(text, 'features.multi_agent_v2')
|
|
755
|
+
&& readTomlTableBoolean(text, 'features', 'multi_agent_v2') === null;
|
|
756
|
+
}
|
|
757
|
+
function readTomlTableBoolean(text, table, key) {
|
|
758
|
+
const line = tomlTableKeyLine(text, table, key);
|
|
759
|
+
const match = line?.match(/^\s*[^=]+\s*=\s*(true|false)\s*(?:#.*)?$/);
|
|
760
|
+
return match?.[1] ? match[1] === 'true' : null;
|
|
684
761
|
}
|
|
685
762
|
function hasTomlTableKey(text, table, key) {
|
|
686
763
|
return tomlTableKeyLine(text, table, key) !== null;
|
|
@@ -107,6 +107,7 @@ async function prepareOfficialSubagentMissionLocked(input) {
|
|
|
107
107
|
taskProfile,
|
|
108
108
|
suggestedRoles: suggestedAgents,
|
|
109
109
|
goal,
|
|
110
|
+
requiresWrite: input.readOnly !== true,
|
|
110
111
|
maxThreads: input.maxThreads ?? officialConfig.maxThreads,
|
|
111
112
|
...(input.hardware ? { hardware: input.hardware } : {}),
|
|
112
113
|
...(slices.length > 0 ? { independentSliceCount: slices.length } : {})
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '8.
|
|
1
|
+
export const PACKAGE_VERSION = '8.6.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.6.0",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|