sneakoscope 3.1.8 → 3.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- 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/bin/sks.js +1 -1
- package/dist/cli/args.js +17 -0
- package/dist/cli/command-registry.js +16 -13
- package/dist/cli/router.js +8 -5
- package/dist/commands/doctor.js +54 -4
- package/dist/core/codex-app/codex-skill-sync.js +37 -2
- package/dist/core/codex-native/core-skill-integrity.js +6 -1
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/codex-native/native-capability-postcheck.js +143 -15
- package/dist/core/codex-native/native-capability-repair-matrix.js +1 -1
- package/dist/core/codex-native/project-skill-dedupe.js +18 -3
- package/dist/core/codex-native/skill-registry-ledger.js +9 -2
- package/dist/core/commands/basic-cli.js +7 -2
- package/dist/core/commands/mad-sks-command.js +36 -13
- package/dist/core/commands/naruto-command.js +4 -1
- package/dist/core/commands/pipeline-command.js +3 -4
- package/dist/core/commands/qa-loop-command.js +36 -1
- package/dist/core/commands/research-command.js +61 -1
- package/dist/core/commands/team-command.js +63 -3
- package/dist/core/config/managed-config-merge.js +59 -10
- package/dist/core/config/secret-preservation.js +145 -37
- package/dist/core/decision-contract.js +28 -4
- package/dist/core/doctor/command-alias-cleanup.js +64 -0
- package/dist/core/feature-fixtures.js +2 -0
- package/dist/core/feature-registry.js +2 -2
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +31 -6
- package/dist/core/naruto/naruto-work-graph.js +4 -1
- package/dist/core/pipeline-internals/runtime-core.js +50 -4
- package/dist/core/pipeline-internals/runtime-gates.js +10 -1
- package/dist/core/proof/route-proof-gate.js +1 -1
- package/dist/core/qa-loop.js +227 -11
- package/dist/core/questions.js +239 -2
- package/dist/core/routes.js +3 -4
- package/dist/core/version.js +1 -1
- package/dist/scripts/agent-native-release-gate.js +13 -4
- package/package.json +5 -1
|
@@ -17,8 +17,16 @@ export async function dedupeProjectSkills(input) {
|
|
|
17
17
|
const userEntries = group.filter((entry) => !entry.managed_by_sks);
|
|
18
18
|
const managedEntries = group.filter((entry) => entry.managed_by_sks);
|
|
19
19
|
if (userEntries.length > 0 && managedEntries.length > 0) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const keepUser = userEntries[0];
|
|
21
|
+
if (keepUser)
|
|
22
|
+
actions.push(actionRow(canonical, 'kept', keepUser, null, 'user-authored skill preserved'));
|
|
23
|
+
const shouldMoveUserDuplicates = fix && yes && input.quarantineUserDuplicates === true;
|
|
24
|
+
for (const duplicateUser of userEntries.slice(1)) {
|
|
25
|
+
const quarantine = shouldMoveUserDuplicates ? await quarantineSkill(root, canonical, duplicateUser, 'user-authored duplicate skill') : null;
|
|
26
|
+
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicateUser, quarantine, 'user-authored duplicate skill requires --quarantine-user-duplicates --yes'));
|
|
27
|
+
}
|
|
28
|
+
if (userEntries.length > 1 && !shouldMoveUserDuplicates)
|
|
29
|
+
unresolvedUserDuplicates.push(canonical);
|
|
22
30
|
for (const managed of managedEntries) {
|
|
23
31
|
const quarantine = await maybeQuarantine(root, canonical, managed, fix, 'managed collision with user-authored skill');
|
|
24
32
|
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', managed, quarantine, 'managed collision with user-authored skill'));
|
|
@@ -49,7 +57,11 @@ export async function dedupeProjectSkills(input) {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
const duplicateNames = [...new Set(actions.filter((action) => action.action !== 'kept').map((action) => action.canonical_name))].sort();
|
|
52
|
-
const
|
|
60
|
+
const afterLedger = await buildSkillRegistryLedger({ root, reportPath: null });
|
|
61
|
+
const blockers = [
|
|
62
|
+
...unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`),
|
|
63
|
+
...afterLedger.duplicate_active_canonical_names.map((name) => `duplicate_active_skill_name:${name}`)
|
|
64
|
+
];
|
|
53
65
|
const report = {
|
|
54
66
|
schema: 'sks.project-skill-dedupe.v1',
|
|
55
67
|
generated_at: nowIso(),
|
|
@@ -58,6 +70,9 @@ export async function dedupeProjectSkills(input) {
|
|
|
58
70
|
fix,
|
|
59
71
|
yes,
|
|
60
72
|
actions,
|
|
73
|
+
active_unique_by_canonical_name: afterLedger.active_unique_by_canonical_name,
|
|
74
|
+
active_entries: afterLedger.active_entries,
|
|
75
|
+
duplicate_active_canonical_names: afterLedger.duplicate_active_canonical_names,
|
|
61
76
|
duplicate_canonical_names: duplicateNames,
|
|
62
77
|
unresolved_user_duplicates: unresolvedUserDuplicates,
|
|
63
78
|
blockers
|
|
@@ -56,13 +56,20 @@ export async function buildSkillRegistryLedger(input) {
|
|
|
56
56
|
entry.status = entry.status === 'user-owned' ? 'duplicate' : 'duplicate';
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
const
|
|
59
|
+
const activeEntries = entries.filter((entry) => entry.status !== 'quarantined');
|
|
60
|
+
const activeGrouped = groupByCanonical(activeEntries);
|
|
61
|
+
const duplicateActiveNames = [...activeGrouped.entries()].filter(([, group]) => group.length > 1).map(([name]) => name).sort();
|
|
62
|
+
const activeUnique = duplicateActiveNames.length === 0;
|
|
63
|
+
const blockers = duplicateActiveNames.map((name) => `duplicate_active_skill_name:${name}`);
|
|
60
64
|
const ledger = {
|
|
61
65
|
schema: 'sks.skill-registry-ledger.v1',
|
|
62
66
|
generated_at: nowIso(),
|
|
63
|
-
ok:
|
|
67
|
+
ok: activeUnique,
|
|
64
68
|
root,
|
|
65
69
|
entries,
|
|
70
|
+
active_unique_by_canonical_name: activeUnique,
|
|
71
|
+
active_entries: activeEntries,
|
|
72
|
+
duplicate_active_canonical_names: duplicateActiveNames,
|
|
66
73
|
duplicate_canonical_names: duplicates,
|
|
67
74
|
blockers
|
|
68
75
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { COMMANDS } from '../../cli/command-registry.js';
|
|
3
|
+
import { COMMANDS, LEGACY_COMMAND_ALIASES } from '../../cli/command-registry.js';
|
|
4
4
|
import { flag } from '../../cli/args.js';
|
|
5
5
|
import { printJson, sksTextLogo } from '../../cli/output.js';
|
|
6
6
|
import { PACKAGE_VERSION, ensureDir, exists, nowIso, projectRoot, readJson, sksRoot, tmpdir, writeJsonAtomic } from '../fsx.js';
|
|
@@ -55,6 +55,9 @@ export function dollarCommandsCommand(args = []) {
|
|
|
55
55
|
export function aliasesCommand() {
|
|
56
56
|
console.log('Aliases');
|
|
57
57
|
console.log('- sks, sneakoscope');
|
|
58
|
+
console.log('- CLI compatibility aliases:');
|
|
59
|
+
for (const [alias, canonical] of Object.entries(LEGACY_COMMAND_ALIASES))
|
|
60
|
+
console.log(` sks ${alias} -> sks ${canonical}`);
|
|
58
61
|
console.log('- $ aliases:');
|
|
59
62
|
for (const entry of DOLLAR_COMMAND_ALIASES)
|
|
60
63
|
console.log(` ${entry.app_skill} -> ${entry.canonical}`);
|
|
@@ -182,7 +185,9 @@ export async function initCommand(args = []) {
|
|
|
182
185
|
export async function fixPathCommand(args = []) {
|
|
183
186
|
const root = await projectRoot();
|
|
184
187
|
const installScope = installScopeFromArgs(args);
|
|
185
|
-
await
|
|
188
|
+
await withSecretPreservationGuard(root, 'fix-path-command', async () => {
|
|
189
|
+
await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
|
|
190
|
+
});
|
|
186
191
|
const result = {
|
|
187
192
|
schema: 'sks.fix-path.v1',
|
|
188
193
|
ok: true,
|
|
@@ -30,6 +30,7 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
30
30
|
return madSksSubcommand(subcommand, args.filter((arg) => String(arg) !== subcommand));
|
|
31
31
|
const cleanArgs = stripMadLaunchOnlyArgs(args);
|
|
32
32
|
const rawArgs = (args || []).map((arg) => String(arg));
|
|
33
|
+
const madDbGrant = resolveMadLaunchMadDbGrant(rawArgs);
|
|
33
34
|
const dryRun = rawArgs.includes('--dry-run');
|
|
34
35
|
if (args.includes('--json') && !dryRun) {
|
|
35
36
|
const profile = buildMadHighLaunchProfileNoWrite();
|
|
@@ -133,13 +134,6 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
133
134
|
// later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
|
|
134
135
|
// readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
|
|
135
136
|
// old behavior.
|
|
136
|
-
const madDbRequested = rawArgs.includes('--mad-db');
|
|
137
|
-
const madDbAck = readOption(rawArgs, '--ack', '');
|
|
138
|
-
if (madDbRequested && madDbAck !== MAD_DB_ACK) {
|
|
139
|
-
console.error(`SKS MAD-DB launch blocked. Required --ack ${JSON.stringify(MAD_DB_ACK)}`);
|
|
140
|
-
process.exitCode = 2;
|
|
141
|
-
return { ok: false, status: 'blocked', reason: 'mad_db_ack_phrase_required', required_ack: MAD_DB_ACK };
|
|
142
|
-
}
|
|
143
137
|
const allowMadRepair = rawArgs.includes('--repair-config') || rawArgs.includes('--fix') || rawArgs.includes('--yes-repair');
|
|
144
138
|
const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: allowMadRepair, launchFast: process.env.SKS_LAUNCH_FULL_CODEX_PROBE !== '1', profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
|
|
145
139
|
const afterPreflightUi = beforeUi ? await writeCodexAppUiSnapshot(launchRoot, `mad-after-preflight-${uiSnapshotId}`).catch(() => null) : null;
|
|
@@ -160,18 +154,38 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
160
154
|
return launchPreflight;
|
|
161
155
|
}
|
|
162
156
|
const madLaunch = await activateMadZellijPermissionState(process.cwd(), args);
|
|
163
|
-
const madDbCapability =
|
|
164
|
-
? await createMadDbCapability(madLaunch.root, { missionId: madLaunch.mission_id, ack:
|
|
157
|
+
const madDbCapability = madDbGrant.enabled
|
|
158
|
+
? await createMadDbCapability(madLaunch.root, { missionId: madLaunch.mission_id, ack: madDbGrant.ack, cwd: process.cwd() })
|
|
165
159
|
: null;
|
|
166
160
|
if (madDbCapability) {
|
|
161
|
+
const grantReport = {
|
|
162
|
+
schema: 'sks.mad-sks-launch-grants.v1',
|
|
163
|
+
generated_at: nowIso(),
|
|
164
|
+
mission_id: madLaunch.mission_id,
|
|
165
|
+
mad_sks_active: true,
|
|
166
|
+
mad_db_active: true,
|
|
167
|
+
mad_db_default_grant: madDbGrant.source === 'sks_mad_default',
|
|
168
|
+
mad_db_grant_source: madDbGrant.source,
|
|
169
|
+
mad_db_one_cycle_only: true,
|
|
170
|
+
mad_db_capability_file: 'mad-db-capability.json',
|
|
171
|
+
mad_db_cycle_id: madDbCapability.cycle_id,
|
|
172
|
+
mad_db_expires_at: madDbCapability.expires_at,
|
|
173
|
+
standalone_mad_db_enable_still_requires_ack: true
|
|
174
|
+
};
|
|
175
|
+
await writeJsonAtomic(path.join(madLaunch.dir, 'mad-sks-launch-grants.json'), grantReport);
|
|
167
176
|
await setCurrent(madLaunch.root, {
|
|
168
177
|
mission_id: madLaunch.mission_id,
|
|
169
178
|
mad_db_active: true,
|
|
170
179
|
mad_db_cycle_id: madDbCapability.cycle_id,
|
|
171
180
|
mad_db_capability_file: 'mad-db-capability.json',
|
|
172
|
-
mad_db_break_glass: true
|
|
181
|
+
mad_db_break_glass: true,
|
|
182
|
+
mad_db_default_grant: madDbGrant.source === 'sks_mad_default',
|
|
183
|
+
mad_db_grant_source: madDbGrant.source,
|
|
184
|
+
mad_db_one_cycle_only: true,
|
|
185
|
+
mad_db_priority_override_active: true,
|
|
186
|
+
mad_sks_launch_grants_file: 'mad-sks-launch-grants.json'
|
|
173
187
|
});
|
|
174
|
-
await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_db.capability_created', cycle_id: madDbCapability.cycle_id, expires_at: madDbCapability.expires_at });
|
|
188
|
+
await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_db.capability_created', grant_source: madDbGrant.source, cycle_id: madDbCapability.cycle_id, expires_at: madDbCapability.expires_at });
|
|
175
189
|
}
|
|
176
190
|
const updateNotice = await checkSksUpdateNotice({
|
|
177
191
|
packageName: deps.packageName || 'sneakoscope',
|
|
@@ -192,10 +206,10 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
192
206
|
await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), { ts: nowIso(), type: 'mad_sks.update_notice_checked', non_blocking: true, update_available: updateNotice.update_available === true, source: updateNotice.source });
|
|
193
207
|
console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
|
|
194
208
|
if (madDbCapability)
|
|
195
|
-
console.log(`MAD-DB one-cycle capability active; expires ${madDbCapability.expires_at}.`);
|
|
209
|
+
console.log(`MAD-DB one-cycle capability active (${madDbGrant.source}); expires ${madDbCapability.expires_at}.`);
|
|
196
210
|
if (updateNotice.update_available === true)
|
|
197
211
|
console.log(`SKS update notice: ${updateNotice.latest_version} available (non-blocking).`);
|
|
198
|
-
console.log('Scoped high-power maintenance authority active; add explicit --allow-* flags for packages, services, network, browser/Computer Use, generated assets, file permissions,
|
|
212
|
+
console.log('Scoped high-power maintenance authority active; add explicit --allow-* flags for packages, services, network, browser/Computer Use, generated assets, file permissions, or system/admin scopes. MAD-DB one-cycle DB break-glass is already active for this launch; protected-core, audit, and one-cycle bounds remain.');
|
|
199
213
|
const launchLb = lb.status === 'present' ? { ...lb, status: 'configured' } : lb;
|
|
200
214
|
const madSksEnv = {
|
|
201
215
|
SKS_PROTECTED_CORE_POLICY: madLaunch.gate.protected_core_policy,
|
|
@@ -260,6 +274,15 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
260
274
|
console.log('MAD launch running headless: live_panes=false.');
|
|
261
275
|
return launch;
|
|
262
276
|
}
|
|
277
|
+
export function resolveMadLaunchMadDbGrant(args = []) {
|
|
278
|
+
const list = (args || []).map((arg) => String(arg));
|
|
279
|
+
return {
|
|
280
|
+
enabled: true,
|
|
281
|
+
source: list.includes('--mad-db') ? 'sks_mad_explicit_redundant_flag' : 'sks_mad_default',
|
|
282
|
+
ack: MAD_DB_ACK,
|
|
283
|
+
one_cycle_only: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
263
286
|
export async function startMadNativeSwarm(root, madLaunch, args = [], profile = {}, opts = {}) {
|
|
264
287
|
const swarm = resolveMadNativeSwarmOptions(args, profile, opts);
|
|
265
288
|
const dir = madLaunch.dir || missionDirLike(root, madLaunch.mission_id);
|
|
@@ -114,6 +114,7 @@ async function narutoRun(parsed) {
|
|
|
114
114
|
prompt: parsed.prompt,
|
|
115
115
|
requestedClones: roster.agent_count,
|
|
116
116
|
totalWorkItems: parsed.workItems,
|
|
117
|
+
honorExplicitTotalWorkItems: parsed.workItemsExplicit,
|
|
117
118
|
readonly: parsed.readonly,
|
|
118
119
|
writeCapable,
|
|
119
120
|
leaseBasePath: patchEnvelopeBasePath,
|
|
@@ -127,6 +128,7 @@ async function narutoRun(parsed) {
|
|
|
127
128
|
prompt: parsed.prompt,
|
|
128
129
|
requestedClones: roster.agent_count,
|
|
129
130
|
totalWorkItems: parsed.workItems,
|
|
131
|
+
honorExplicitTotalWorkItems: parsed.workItemsExplicit,
|
|
130
132
|
readonly: parsed.readonly,
|
|
131
133
|
writeCapable,
|
|
132
134
|
leaseBasePath: patchEnvelopeBasePath,
|
|
@@ -791,6 +793,7 @@ function parseNarutoArgs(args = []) {
|
|
|
791
793
|
const json = hasFlag(args, '--json');
|
|
792
794
|
const requestedClones = Number(readOption(args, '--clones', readOption(args, '--agents', DEFAULT_NARUTO_CLONES)));
|
|
793
795
|
const clones = clampClones(requestedClones);
|
|
796
|
+
const workItemsExplicit = hasOption(args, '--work-items');
|
|
794
797
|
const workItems = clampWorkItems(Number(readOption(args, '--work-items', clones * 2)), clones);
|
|
795
798
|
const concurrency = normalizeConcurrency(readOption(args, '--concurrency', readOption(args, '--target-active-slots', null)), clones);
|
|
796
799
|
const useOllama = hasFlag(args, '--ollama') || hasFlag(args, '--local-model');
|
|
@@ -831,7 +834,7 @@ function parseNarutoArgs(args = []) {
|
|
|
831
834
|
const messages = normalizeMessages(readOption(args, '--messages', '8'));
|
|
832
835
|
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--service-tier', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
|
|
833
836
|
const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
|
|
834
|
-
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
837
|
+
return { action, prompt, clones, workItems, workItemsExplicit, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
835
838
|
}
|
|
836
839
|
function normalizeParallelism(value) {
|
|
837
840
|
const text = String(value || 'extreme').toLowerCase();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { projectRoot, readJson
|
|
2
|
+
import { projectRoot, readJson } from '../fsx.js';
|
|
3
3
|
import { missionDir, stateFile } from '../mission.js';
|
|
4
|
-
import {
|
|
4
|
+
import { PIPELINE_PLAN_ARTIFACT, projectGateStatus, writePipelinePlan } from '../pipeline.js';
|
|
5
5
|
import { routePrompt } from '../routes.js';
|
|
6
6
|
import { flag, positionalArgs, readFlagValue, resolveMissionId } from './command-utils.js';
|
|
7
7
|
export async function pipelineCommand(args = []) {
|
|
@@ -33,14 +33,13 @@ export async function pipelineCommand(args = []) {
|
|
|
33
33
|
force: flag(args, '--force-agents'),
|
|
34
34
|
noAgents: flag(args, '--no-agents')
|
|
35
35
|
};
|
|
36
|
-
const plan =
|
|
36
|
+
const plan = await writePipelinePlan(dir, {
|
|
37
37
|
missionId: id,
|
|
38
38
|
route,
|
|
39
39
|
task: routeContext.task || mission.prompt || state.prompt || '',
|
|
40
40
|
required: Boolean(routeContext.context7_required || state.context7_required),
|
|
41
41
|
agents
|
|
42
42
|
});
|
|
43
|
-
await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
|
|
44
43
|
if (flag(args, '--json'))
|
|
45
44
|
return console.log(JSON.stringify({ schema: 'sks.pipeline-plan.v1', ok: true, mission_id: id, plan }, null, 2));
|
|
46
45
|
console.log(`Pipeline plan written: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}`);
|
|
@@ -5,7 +5,7 @@ import { getCodexInfo, runCodexExec } from '../codex-adapter.js';
|
|
|
5
5
|
import { createMission, loadMission, setCurrent, stateFile } from '../mission.js';
|
|
6
6
|
import { writeQuestions } from '../questions.js';
|
|
7
7
|
import { sealContract } from '../decision-contract.js';
|
|
8
|
-
import { buildQaLoopQuestionSchema, buildQaLoopPrompt, evaluateQaGate, qaStatus, qaUiRequired, writeMockQaResult, writeQaLoopArtifacts, writeQaNativeAgentLedger } from '../qa-loop.js';
|
|
8
|
+
import { buildQaLoopQuestionSchema, buildQaLoopPrompt, ensureQaLoopVisualEvidenceContract, evaluateQaGate, qaGptImage2AnnotatedReviewRequired, qaStatus, qaUiRequired, writeMockQaResult, writeQaLoopArtifacts, writeQaNativeAgentLedger } from '../qa-loop.js';
|
|
9
9
|
import { containsUserQuestion, noQuestionContinuationReason } from '../no-question-guard.js';
|
|
10
10
|
import { ROUTES, routePrompt, stripVisibleDecisionAnswerBlocks } from '../routes.js';
|
|
11
11
|
import { codexChromeExtensionStatus } from '../codex-app.js';
|
|
@@ -124,6 +124,8 @@ async function qaLoopRun(args) {
|
|
|
124
124
|
const contract = await readJson(contractPath, {});
|
|
125
125
|
if (!(await exists(path.join(dir, 'qa-ledger.json'))))
|
|
126
126
|
await writeQaLoopArtifacts(dir, mission, contract);
|
|
127
|
+
else
|
|
128
|
+
await ensureQaLoopVisualEvidenceContract(dir, mission, contract);
|
|
127
129
|
const safetyScan = await scanDbSafety(root);
|
|
128
130
|
if (!safetyScan.ok) {
|
|
129
131
|
console.error('QA-LOOP cannot run: SKS safety scan found unsafe project data-tool configuration.');
|
|
@@ -148,6 +150,7 @@ async function qaLoopRun(args) {
|
|
|
148
150
|
const reportFile = qaGate.qa_report_file;
|
|
149
151
|
const executionProfile = await readJson(path.join(dir, 'qa-loop', 'execution-profile.json'), null);
|
|
150
152
|
const uiRequired = qaUiRequired(contract.answers || {});
|
|
153
|
+
const gptImage2ReviewRequired = qaGptImage2AnnotatedReviewRequired(contract, mission.prompt);
|
|
151
154
|
const capabilityArtifact = await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), report: null }));
|
|
152
155
|
const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
|
|
153
156
|
const budgetPolicy = buildQaLoopBudgetPolicy({ usage: usageArtifact?.snapshot || null, provider: 'codex-sdk' });
|
|
@@ -253,6 +256,16 @@ async function qaLoopRun(args) {
|
|
|
253
256
|
ui_chrome_extension_evidence: false,
|
|
254
257
|
ui_computer_use_evidence: false,
|
|
255
258
|
ui_evidence_source: 'blocked_chrome_extension_setup_required',
|
|
259
|
+
ui_chrome_extension_screenshot_required: true,
|
|
260
|
+
ui_chrome_extension_screenshot_captured: false,
|
|
261
|
+
ui_chrome_extension_screenshot_artifact: null,
|
|
262
|
+
ui_chrome_extension_screenshot_sha256: null,
|
|
263
|
+
gpt_image_2_annotated_review_required: gptImage2ReviewRequired,
|
|
264
|
+
gpt_image_2_annotated_review_generated: false,
|
|
265
|
+
gpt_image_2_annotated_review_artifact: null,
|
|
266
|
+
gpt_image_2_annotated_review_sha256: null,
|
|
267
|
+
gpt_image_2_annotated_review_model: gptImage2ReviewRequired ? null : 'not_required',
|
|
268
|
+
gpt_image_2_annotated_review_provider: gptImage2ReviewRequired ? null : 'not_required',
|
|
256
269
|
blocker: 'codex_chrome_extension_setup_required',
|
|
257
270
|
blockers: Array.from(new Set([...(qaGate.blockers || []), 'codex_chrome_extension_setup_required', ...(chrome.blockers || [])])),
|
|
258
271
|
evidence: [...(qaGate.evidence || []), 'Codex Chrome Extension preflight failed before web QA execution.'],
|
|
@@ -277,6 +290,28 @@ async function qaLoopRun(args) {
|
|
|
277
290
|
const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: '$QA-LOOP', prompt: mission.prompt || 'QA-LOOP run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount, minimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: !(applyPatches && writeMode !== 'off'), profile, writeMode: writeMode, applyPatches, dryRunPatches, maxWriteAgents, roster: nativeRoster, routeCommand: 'sks qa-loop run', routeBlackboxKind: 'actual_qa_command', env: { SKS_CODEX_APP_EXECUTION_PROFILE: executionProfile?.mode || 'unknown', SKS_CODEX_AGENT_ROLE_STRATEGY: executionProfile?.agent_role_strategy || 'message-role' } });
|
|
278
291
|
await writeJsonAtomic(path.join(dir, 'qa-native-agent-run.json'), nativeAgentRun);
|
|
279
292
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
|
|
293
|
+
if (flag(args, '--native-proof-only')) {
|
|
294
|
+
const proofOnlyGate = {
|
|
295
|
+
schema: 'sks.qa-native-proof-only-gate.v1',
|
|
296
|
+
ok: nativeAgentRun.proof?.ok === true,
|
|
297
|
+
native_agent_proof: nativeAgentRun.proof?.ok === true,
|
|
298
|
+
proof_status: nativeAgentRun.proof?.status || null,
|
|
299
|
+
blockers: nativeAgentRun.proof?.blockers || []
|
|
300
|
+
};
|
|
301
|
+
if (flag(args, '--json'))
|
|
302
|
+
return console.log(JSON.stringify({
|
|
303
|
+
schema: 'sks.qa-loop-run.v1',
|
|
304
|
+
ok: proofOnlyGate.ok,
|
|
305
|
+
status: proofOnlyGate.ok ? 'native_proof_ready' : 'blocked',
|
|
306
|
+
mission_id: id,
|
|
307
|
+
gate: proofOnlyGate,
|
|
308
|
+
proof: nativeAgentRun.proof,
|
|
309
|
+
native_agent_run: nativeAgentRun,
|
|
310
|
+
native_proof_only: true
|
|
311
|
+
}, null, 2));
|
|
312
|
+
console.log(`QA-LOOP native proof ready: ${id}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
280
315
|
if (mock) {
|
|
281
316
|
let gate = await writeMockQaResult(dir, mission, contract);
|
|
282
317
|
const needsVisual = uiRequired;
|
|
@@ -40,6 +40,38 @@ export async function researchCommand(sub, args = []) {
|
|
|
40
40
|
export async function autoresearchCommand(sub, args = []) {
|
|
41
41
|
return researchCommand(sub || 'status', args);
|
|
42
42
|
}
|
|
43
|
+
function hasFlagOption(args = [], name) {
|
|
44
|
+
return args.includes(name) || args.some((arg) => String(arg).startsWith(`${name}=`));
|
|
45
|
+
}
|
|
46
|
+
function limitResearchNativeWorkGraph(graph, limit) {
|
|
47
|
+
const count = Math.max(1, Math.min(Number(graph?.work_items?.length || 0) || 1, Math.floor(Number(limit) || 1)));
|
|
48
|
+
const workItems = (graph.work_items || []).slice(0, count).map((item) => {
|
|
49
|
+
const selectedIds = new Set((graph.work_items || []).slice(0, count).map((row) => String(row.id || '')));
|
|
50
|
+
return {
|
|
51
|
+
...item,
|
|
52
|
+
dependencies: (item.dependencies || []).map(String).filter((id) => selectedIds.has(id)),
|
|
53
|
+
can_run_in_parallel_with: (item.can_run_in_parallel_with || []).map(String).filter((id) => selectedIds.has(id))
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
const selectedIds = new Set(workItems.map((item) => String(item.id || '')));
|
|
57
|
+
const activeWaves = (graph.active_waves || [])
|
|
58
|
+
.map((wave) => ({
|
|
59
|
+
...wave,
|
|
60
|
+
work_item_ids: (wave.work_item_ids || []).map(String).filter((id) => selectedIds.has(id)),
|
|
61
|
+
write_paths: (wave.write_paths || []).map(String),
|
|
62
|
+
conflict_count: Number(wave.conflict_count || 0)
|
|
63
|
+
}))
|
|
64
|
+
.filter((wave) => wave.work_item_ids.length > 0);
|
|
65
|
+
return {
|
|
66
|
+
...graph,
|
|
67
|
+
requested_clones: Math.min(Number(graph.requested_clones || count), count),
|
|
68
|
+
total_work_items: workItems.length,
|
|
69
|
+
work_items: workItems,
|
|
70
|
+
active_waves: activeWaves,
|
|
71
|
+
mixed_work_kinds: [...new Set(workItems.map((item) => item.kind))],
|
|
72
|
+
write_allowed_count: workItems.filter((item) => item.write_allowed === true).length
|
|
73
|
+
};
|
|
74
|
+
}
|
|
43
75
|
async function researchPrepare(args) {
|
|
44
76
|
const root = await sksRoot();
|
|
45
77
|
if (!(await exists(path.join(root, '.sneakoscope'))))
|
|
@@ -139,10 +171,16 @@ async function researchRun(args) {
|
|
|
139
171
|
const mock = flag(args, '--mock');
|
|
140
172
|
const researchWorkGraph = await writeResearchWorkGraph(dir, plan);
|
|
141
173
|
const graphWorkItemCount = Math.max(1, Number(researchWorkGraph.total_work_items || researchWorkGraph.work_items?.length || 0));
|
|
174
|
+
const explicitWorkItems = hasFlagOption(args, '--work-items');
|
|
175
|
+
const effectiveDesiredWorkItemCount = explicitWorkItems ? desiredWorkItemCount : Math.max(desiredWorkItemCount, graphWorkItemCount);
|
|
176
|
+
const effectiveMinimumWorkItems = Math.min(effectiveDesiredWorkItemCount, explicitWorkItems ? minimumWorkItems : Math.max(minimumWorkItems, Math.min(graphWorkItemCount, targetActiveSlots)));
|
|
177
|
+
const nativeResearchWorkGraph = explicitWorkItems
|
|
178
|
+
? limitResearchNativeWorkGraph(researchWorkGraph, effectiveDesiredWorkItemCount)
|
|
179
|
+
: researchWorkGraph;
|
|
142
180
|
await runResearchCycle(dir, researchWorkGraph, { cycle: 0, status: mock ? 'mock_native_orchestrator_planned' : 'native_orchestrator_planned' });
|
|
143
181
|
await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
|
|
144
182
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
|
|
145
|
-
const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount:
|
|
183
|
+
const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
|
|
146
184
|
await writeJsonAtomic(path.join(dir, 'research-native-agent-run.json'), nativeAgentRun);
|
|
147
185
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
|
|
148
186
|
if (!nativeAgentRun.ok) {
|
|
@@ -151,6 +189,28 @@ async function researchRun(args) {
|
|
|
151
189
|
process.exitCode = 2;
|
|
152
190
|
return;
|
|
153
191
|
}
|
|
192
|
+
if (flag(args, '--native-proof-only')) {
|
|
193
|
+
const proofOnlyGate = {
|
|
194
|
+
schema: 'sks.research-native-proof-only-gate.v1',
|
|
195
|
+
ok: nativeAgentRun.proof?.ok === true,
|
|
196
|
+
native_agent_proof: nativeAgentRun.proof?.ok === true,
|
|
197
|
+
proof_status: nativeAgentRun.proof?.status || null,
|
|
198
|
+
blockers: nativeAgentRun.proof?.blockers || []
|
|
199
|
+
};
|
|
200
|
+
if (flag(args, '--json'))
|
|
201
|
+
return console.log(JSON.stringify({
|
|
202
|
+
schema: flag(args, '--autoresearch') ? 'sks.autoresearch-run.v1' : 'sks.research-run.v1',
|
|
203
|
+
ok: proofOnlyGate.ok,
|
|
204
|
+
mission_id: id,
|
|
205
|
+
gate: proofOnlyGate,
|
|
206
|
+
proof: nativeAgentRun.proof,
|
|
207
|
+
native_agent_run: nativeAgentRun,
|
|
208
|
+
research_work_graph: nativeResearchWorkGraph,
|
|
209
|
+
native_proof_only: true
|
|
210
|
+
}, null, 2));
|
|
211
|
+
console.log(`Research native proof ready: ${id}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
154
214
|
const legacyResearchCycle = flag(args, '--legacy-research-cycle') || process.env.SKS_RESEARCH_LEGACY_CYCLE === '1';
|
|
155
215
|
const sourceMutationBaseline = await researchCodeMutationSnapshot(root, id);
|
|
156
216
|
if (!legacyResearchCycle) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { nowIso, sksRoot, writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
|
|
3
3
|
import { findLatestMission } from '../mission.js';
|
|
4
4
|
import { narutoCommand } from './naruto-command.js';
|
|
5
5
|
import { teamLegacyObserveCommand, teamLegacySubcommands } from './team-legacy-observe-command.js';
|
|
6
|
+
import { SSOT_GUARD_ARTIFACT } from '../safety/ssot-guard.js';
|
|
6
7
|
export async function team(args = []) {
|
|
7
8
|
if (teamLegacySubcommands.has(String(args[0] || ''))) {
|
|
8
9
|
return teamLegacyObserveCommand(String(args[0]), args.slice(1));
|
|
@@ -13,9 +14,13 @@ async function redirectTeamCreateToNaruto(args = []) {
|
|
|
13
14
|
const root = await sksRoot();
|
|
14
15
|
const list = (args || []).map((arg) => String(arg));
|
|
15
16
|
const narutoArgs = list[0] === 'run' ? list : ['run', ...list];
|
|
17
|
+
const jsonRequested = list.includes('--json');
|
|
16
18
|
console.warn('SKS Team is deprecated for new execution missions; redirecting to $Naruto.');
|
|
17
|
-
const result =
|
|
19
|
+
const result = jsonRequested
|
|
20
|
+
? await withSuppressedConsoleLog(() => narutoCommand(narutoArgs))
|
|
21
|
+
: await narutoCommand(narutoArgs);
|
|
18
22
|
const missionId = result?.mission_id || await findLatestMission(root);
|
|
23
|
+
const nativeAgentRun = missionId ? await buildTeamNativeAgentCompatibility(root, missionId, result) : null;
|
|
19
24
|
if (missionId) {
|
|
20
25
|
await writeJsonAtomic(path.join(root, '.sneakoscope', 'missions', missionId, 'team-alias-to-naruto.json'), {
|
|
21
26
|
schema: 'sks.team-alias-to-naruto.v1',
|
|
@@ -26,10 +31,65 @@ async function redirectTeamCreateToNaruto(args = []) {
|
|
|
26
31
|
route_command: '$Naruto',
|
|
27
32
|
deprecated_route: '$Team',
|
|
28
33
|
parallel_write_policy: result?.parallel_write_policy || result?.run?.parallel_write_policy || null,
|
|
34
|
+
ssot_guard_artifact: SSOT_GUARD_ARTIFACT,
|
|
29
35
|
created_at: nowIso(),
|
|
30
36
|
args: list
|
|
31
37
|
});
|
|
32
38
|
}
|
|
33
|
-
|
|
39
|
+
const finalResult = {
|
|
40
|
+
...result,
|
|
41
|
+
mock: result?.mock === true || result?.backend === 'fake',
|
|
42
|
+
...(nativeAgentRun ? { native_agent_run: nativeAgentRun } : {})
|
|
43
|
+
};
|
|
44
|
+
if (jsonRequested)
|
|
45
|
+
console.log(JSON.stringify(finalResult, null, 2));
|
|
46
|
+
return finalResult;
|
|
47
|
+
}
|
|
48
|
+
async function withSuppressedConsoleLog(fn) {
|
|
49
|
+
const originalLog = console.log;
|
|
50
|
+
console.log = () => undefined;
|
|
51
|
+
try {
|
|
52
|
+
return await fn();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
console.log = originalLog;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function buildTeamNativeAgentCompatibility(root, missionId, result) {
|
|
59
|
+
const ledgerRoot = path.join(root, '.sneakoscope', 'missions', missionId, 'agents');
|
|
60
|
+
const [schedulerState, proof, parallelWritePolicy] = await Promise.all([
|
|
61
|
+
readJson(path.join(ledgerRoot, 'agent-scheduler-state.json'), null),
|
|
62
|
+
readJson(path.join(ledgerRoot, 'agent-proof-evidence.json'), null),
|
|
63
|
+
readJson(path.join(ledgerRoot, 'agent-parallel-write-policy.json'), null)
|
|
64
|
+
]);
|
|
65
|
+
if (!schedulerState || !proof)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
schema: result?.run?.schema || 'sks.agent-run.v1',
|
|
69
|
+
ok: result?.run?.ok === true || result?.ok === true,
|
|
70
|
+
mission_id: missionId,
|
|
71
|
+
route: '$Team',
|
|
72
|
+
backend: result?.backend || result?.run?.backend || proof.backend || null,
|
|
73
|
+
ledger_root: path.relative(root, ledgerRoot),
|
|
74
|
+
target_active_slots: schedulerState.target_active_slots ?? result?.target_active_slots ?? null,
|
|
75
|
+
scheduler: {
|
|
76
|
+
state: schedulerState
|
|
77
|
+
},
|
|
78
|
+
proof: {
|
|
79
|
+
...proof,
|
|
80
|
+
route: '$Team',
|
|
81
|
+
route_command: 'sks team',
|
|
82
|
+
route_blackbox_kind: 'actual_team_command',
|
|
83
|
+
real_route_command_used: true
|
|
84
|
+
},
|
|
85
|
+
parallel_write_policy: parallelWritePolicy || result?.parallel_write_policy || result?.run?.parallel_write_policy || null,
|
|
86
|
+
redirected_to: '$Naruto',
|
|
87
|
+
compatibility: {
|
|
88
|
+
schema: 'sks.team-native-agent-compatibility.v1',
|
|
89
|
+
ok: true,
|
|
90
|
+
source: 'team-alias-to-naruto',
|
|
91
|
+
ledger_root: path.relative(root, ledgerRoot)
|
|
92
|
+
}
|
|
93
|
+
};
|
|
34
94
|
}
|
|
35
95
|
//# sourceMappingURL=team-command.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
3
|
+
import { ensureDir, nowIso, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
4
4
|
import { isProtectedSecretKey, PROTECTED_SECRET_KEYS } from './supabase-secret-preservation.js';
|
|
5
5
|
export async function writeManagedJsonConfig(file, current, managed) {
|
|
6
6
|
const next = safeMergeObject(current, managed);
|
|
@@ -23,13 +23,14 @@ export async function writeManagedEnvConfig(file, currentText, managedLines) {
|
|
|
23
23
|
const next = additions.length ? `${String(currentText || '').replace(/\s*$/, '\n')}${additions.join('\n')}\n` : String(currentText || '');
|
|
24
24
|
return writeMergedText(file, currentText, next, 'env', protectedKeysInText(currentText));
|
|
25
25
|
}
|
|
26
|
-
export function safeMergeObject(current, managed) {
|
|
26
|
+
export function safeMergeObject(current, managed, prefix = '') {
|
|
27
27
|
const out = { ...current };
|
|
28
28
|
for (const [key, value] of Object.entries(managed)) {
|
|
29
|
-
|
|
29
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
if (isProtectedSecretKey(dotted) && current[key] != null)
|
|
30
31
|
continue;
|
|
31
32
|
if (isPlainObject(value) && isPlainObject(current[key]))
|
|
32
|
-
out[key] = safeMergeObject(current[key], value);
|
|
33
|
+
out[key] = safeMergeObject(current[key], value, dotted);
|
|
33
34
|
else
|
|
34
35
|
out[key] = value;
|
|
35
36
|
}
|
|
@@ -51,11 +52,23 @@ function upsertTomlBlockPreservingSecrets(text, block) {
|
|
|
51
52
|
break;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const existingBody = lines.slice(start + 1, end);
|
|
56
|
+
const nextBody = [...existingBody];
|
|
57
|
+
for (const managedLine of blockLines.slice(1)) {
|
|
58
|
+
const managedKey = managedLine.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
59
|
+
if (!managedKey) {
|
|
60
|
+
if (!nextBody.includes(managedLine))
|
|
61
|
+
nextBody.push(managedLine);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const existingIndex = nextBody.findIndex((line) => (line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '') === managedKey);
|
|
65
|
+
const protectedLine = isProtectedSecretKey(`${header}.${managedKey}`) || isProtectedSecretKey(managedKey);
|
|
66
|
+
if (existingIndex === -1)
|
|
67
|
+
nextBody.push(managedLine);
|
|
68
|
+
else if (!protectedLine)
|
|
69
|
+
nextBody[existingIndex] = managedLine;
|
|
70
|
+
}
|
|
71
|
+
lines.splice(start, end - start, blockLines[0] || `[${header}]`, ...nextBody);
|
|
59
72
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
60
73
|
}
|
|
61
74
|
async function writeMergedText(file, before, after, format, preserved) {
|
|
@@ -68,6 +81,7 @@ async function writeMergedText(file, before, after, format, preserved) {
|
|
|
68
81
|
}
|
|
69
82
|
await writeTextAtomic(file, after);
|
|
70
83
|
}
|
|
84
|
+
const preservedSecretLineHashes = protectedSecretLineHashes(before);
|
|
71
85
|
return {
|
|
72
86
|
schema: 'sks.managed-config-merge.v1',
|
|
73
87
|
generated_at: nowIso(),
|
|
@@ -77,6 +91,8 @@ async function writeMergedText(file, before, after, format, preserved) {
|
|
|
77
91
|
changed: before !== after,
|
|
78
92
|
backup_path: backupPath,
|
|
79
93
|
protected_keys_preserved: preserved,
|
|
94
|
+
preserved_secret_lines_sha256: preservedSecretLineHashes,
|
|
95
|
+
idempotent: before === after || protectedSecretLineHashes(after).every((hash) => preservedSecretLineHashes.includes(hash)),
|
|
80
96
|
blockers: []
|
|
81
97
|
};
|
|
82
98
|
}
|
|
@@ -88,7 +104,40 @@ function protectedKeysPresent(value) {
|
|
|
88
104
|
return found;
|
|
89
105
|
}
|
|
90
106
|
function protectedKeysInText(text) {
|
|
91
|
-
|
|
107
|
+
const found = new Set();
|
|
108
|
+
for (const key of PROTECTED_SECRET_KEYS) {
|
|
109
|
+
if (new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text))
|
|
110
|
+
found.add(String(key));
|
|
111
|
+
}
|
|
112
|
+
let section = '';
|
|
113
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
114
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
115
|
+
if (sectionMatch) {
|
|
116
|
+
section = String(sectionMatch[1] || '');
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
120
|
+
if (key && section && isProtectedSecretKey(`${section}.${key}`))
|
|
121
|
+
found.add(`${section}.${key}`);
|
|
122
|
+
}
|
|
123
|
+
return [...found].sort();
|
|
124
|
+
}
|
|
125
|
+
function protectedSecretLineHashes(text) {
|
|
126
|
+
const hashes = [];
|
|
127
|
+
let section = '';
|
|
128
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
129
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
130
|
+
if (sectionMatch) {
|
|
131
|
+
section = String(sectionMatch[1] || '');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const key = line.match(/^\s*(?:export\s+)?([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
135
|
+
if (!key)
|
|
136
|
+
continue;
|
|
137
|
+
if (isProtectedSecretKey(key) || (section && isProtectedSecretKey(`${section}.${key}`)))
|
|
138
|
+
hashes.push(sha256(line));
|
|
139
|
+
}
|
|
140
|
+
return hashes.sort();
|
|
92
141
|
}
|
|
93
142
|
function lookupPath(value, dotted) {
|
|
94
143
|
let current = value;
|