sneakoscope 9.1.0 → 9.1.1

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 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 9.1.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 9.1.1** — 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
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "9.1.0"
262
+ version = "9.1.1"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "9.1.0"
3
+ version = "9.1.1"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "9.1.0",
3
+ "package_version": "9.1.1",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -148,6 +148,34 @@ function safeUpstreamErrorId(value) {
148
148
  const text = String(value ?? '').trim();
149
149
  return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
150
150
  }
151
+ const TRANSIENT_UPSTREAM_STATUSES = new Set([500, 502, 503, 524]);
152
+ const TRANSIENT_UPSTREAM_IDENTIFIERS = new Set(['upstream_error', 'upstream_request_timeout']);
153
+ export const TRANSIENT_UPSTREAM_REPLAY_LIMIT = 3;
154
+ const TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS = [200, 400, 800];
155
+ function isTransientUpstreamIdentifier(value) {
156
+ return Boolean(value && TRANSIENT_UPSTREAM_IDENTIFIERS.has(value));
157
+ }
158
+ function delay(ms) {
159
+ return new Promise((resolve) => {
160
+ const timer = setTimeout(resolve, ms);
161
+ timer.unref();
162
+ });
163
+ }
164
+ function isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode) {
165
+ if (statusCode === 429)
166
+ return false;
167
+ if (statusCode === 404) {
168
+ return isTransientUpstreamIdentifier(upstreamType) || isTransientUpstreamIdentifier(upstreamCode);
169
+ }
170
+ return TRANSIENT_UPSTREAM_STATUSES.has(statusCode);
171
+ }
172
+ function redactedUpstreamClientMessage(statusCode, transient) {
173
+ if (statusCode === 429)
174
+ return 'rate_limited';
175
+ if (transient)
176
+ return 'temporary_upstream_failure';
177
+ return 'bridge_upstream_request_failed';
178
+ }
151
179
  async function readRedactedUpstreamError(response) {
152
180
  let total = 0;
153
181
  const chunks = [];
@@ -169,19 +197,18 @@ async function readRedactedUpstreamError(response) {
169
197
  }
170
198
  catch {
171
199
  }
172
- return {
173
- upstreamCode,
174
- upstreamType,
175
- body: Buffer.from(JSON.stringify({
176
- error: {
177
- type: 'upstream_error',
178
- code: 'bridge_upstream_request_failed',
179
- message: 'Upstream request failed',
180
- ...(upstreamType ? { upstream_type: upstreamType } : {}),
181
- ...(upstreamCode ? { upstream_code: upstreamCode } : {})
182
- }
183
- }))
184
- };
200
+ return { upstreamCode, upstreamType };
201
+ }
202
+ function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
203
+ return Buffer.from(JSON.stringify({
204
+ error: {
205
+ type: 'upstream_error',
206
+ code: 'bridge_upstream_request_failed',
207
+ message: redactedUpstreamClientMessage(statusCode, transient),
208
+ ...(upstreamType ? { upstream_type: upstreamType } : {}),
209
+ ...(upstreamCode ? { upstream_code: upstreamCode } : {}),
210
+ },
211
+ }));
185
212
  }
186
213
  const upstreamAgents = new Map();
187
214
  function upstreamAgent(secure, key, idleTimeoutMs) {
@@ -240,7 +267,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
240
267
  delete headers['content-encoding'];
241
268
  const agent = upstreamAgent(provider.remote.secure, `${provider.remote.address}:${provider.remote.port}`, config.idleTimeoutMs);
242
269
  const replayable = Buffer.isBuffer(request.body);
243
- const attempt = (useFreshConnection) => new Promise((resolve, reject) => {
270
+ const attempt = (useFreshConnection, canReplay) => new Promise((resolve, reject) => {
244
271
  let responseStarted = false;
245
272
  let settled = false;
246
273
  const abort = () => { if (!res.writableEnded)
@@ -273,15 +300,17 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
273
300
  upstream.once('response', (response) => {
274
301
  const statusCode = response.statusCode || 502;
275
302
  if (statusCode >= 400) {
276
- void readRedactedUpstreamError(response).then(({ body, upstreamCode, upstreamType }) => {
277
- const transientMislabel = statusCode === 404
278
- && (upstreamType === 'upstream_error' || upstreamCode === 'upstream_error');
279
- if (transientMislabel && replayable && !useFreshConnection) {
303
+ void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType }) => {
304
+ const transientMislabel = isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode);
305
+ if (transientMislabel && replayable && canReplay) {
280
306
  responseStarted = true;
281
- finish(new StalePooledSocketFailure(new DesktopBridgeError('bridge_upstream_transient_mislabel')));
307
+ finish(new StalePooledSocketFailure(new DesktopBridgeError(upstreamCode === 'upstream_request_timeout'
308
+ ? 'bridge_upstream_request_timeout'
309
+ : 'bridge_upstream_transient_mislabel')));
282
310
  return;
283
311
  }
284
312
  const clientStatus = transientMislabel ? 503 : statusCode;
313
+ const body = buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
285
314
  logHttpRejection({
286
315
  code: transientMislabel
287
316
  ? `bridge_upstream_status_${statusCode}_translated_503`
@@ -298,6 +327,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
298
327
  responseHeaders['content-length'] = String(body.length);
299
328
  delete responseHeaders['transfer-encoding'];
300
329
  if (transientMislabel)
330
+ responseHeaders['retry-after'] = String(responseHeaders['retry-after'] || '10');
331
+ if (statusCode === 429 && !responseHeaders['retry-after'])
301
332
  responseHeaders['retry-after'] = '10';
302
333
  res.writeHead(clientStatus, responseHeaders);
303
334
  res.end(body, () => finish());
@@ -321,21 +352,34 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
321
352
  void pipeline(req, upstream).catch((error) => { if (!responseStarted)
322
353
  finish(error); });
323
354
  });
324
- try {
325
- await attempt(false);
326
- }
327
- catch (error) {
328
- if (!(error instanceof StalePooledSocketFailure))
329
- throw error;
330
- agent.destroy();
331
- upstreamAgents.delete(`${provider.remote.secure ? 'https' : 'http'}:${provider.remote.address}:${provider.remote.port}`);
332
- logHttpRejection({
333
- code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
334
- transport: 'http',
335
- ...(req.method === undefined ? {} : { method: req.method }),
336
- ...(req.url === undefined ? {} : { url: req.url }),
337
- });
338
- await attempt(true);
355
+ let remainingReplays = replayable ? TRANSIENT_UPSTREAM_REPLAY_LIMIT : 0;
356
+ let useFreshConnection = false;
357
+ for (;;) {
358
+ try {
359
+ await attempt(useFreshConnection, remainingReplays > 0);
360
+ return;
361
+ }
362
+ catch (error) {
363
+ if (!(error instanceof StalePooledSocketFailure) || remainingReplays <= 0)
364
+ throw error;
365
+ remainingReplays -= 1;
366
+ if (!useFreshConnection) {
367
+ agent.destroy();
368
+ upstreamAgents.delete(`${provider.remote.secure ? 'https' : 'http'}:${provider.remote.address}:${provider.remote.port}`);
369
+ }
370
+ logHttpRejection({
371
+ code: `bridge_upstream_socket_stale_replayed:${underlyingErrorCode(error.reason) || 'unknown'}`,
372
+ transport: 'http',
373
+ ...(req.method === undefined ? {} : { method: req.method }),
374
+ ...(req.url === undefined ? {} : { url: req.url }),
375
+ });
376
+ useFreshConnection = true;
377
+ const backoffIndex = TRANSIENT_UPSTREAM_REPLAY_LIMIT - remainingReplays - 1;
378
+ const backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[Math.max(0, backoffIndex)]
379
+ ?? TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS.length - 1];
380
+ if (backoffMs)
381
+ await delay(backoffMs);
382
+ }
339
383
  }
340
384
  }
341
385
  catch (error) {
@@ -8,7 +8,7 @@ import { createDesktopBridgePublicState, desktopBridgeListenOrigin, desktopBridg
8
8
  import { DesktopBridgeError } from './types.js';
9
9
  import { PACKAGE_VERSION } from '../../version.js';
10
10
  import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DESKTOP_BRIDGE_DIAGNOSTIC_HEALTH_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL } from './types.js';
11
- import { forwardWebSocket } from './websocket-forward.js';
11
+ import { forwardWebSocket, safeEndUpgradeSocket } from './websocket-forward.js';
12
12
  function authenticateDesktopBridgeClient(req, input) {
13
13
  const raw = String(req.url || '/');
14
14
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.startsWith('//') || /[\r\n\0]/.test(raw)) {
@@ -135,22 +135,14 @@ function writeUpgradeRejection(socket, error, req) {
135
135
  url: req?.url,
136
136
  status: rejectionStatus(safeBridgeErrorCode(error)),
137
137
  });
138
- socket.on('error', () => undefined);
139
- if (socket.destroyed || socket.writableEnded)
140
- return;
141
138
  const code = safeBridgeErrorCode(error);
142
139
  const status = rejectionStatus(code);
143
- try {
144
- socket.end(`HTTP/1.1 ${status} ${rejectionStatusText(status)}\r\n`
145
- + 'Content-Type: application/json\r\n'
146
- + 'Cache-Control: no-store\r\n'
147
- + 'Connection: close\r\n'
148
- + '\r\n'
149
- + JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
150
- }
151
- catch {
152
- socket.destroy();
153
- }
140
+ safeEndUpgradeSocket(socket, `HTTP/1.1 ${status} ${rejectionStatusText(status)}\r\n`
141
+ + 'Content-Type: application/json\r\n'
142
+ + 'Cache-Control: no-store\r\n'
143
+ + 'Connection: close\r\n'
144
+ + '\r\n'
145
+ + JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
154
146
  }
155
147
  function writeDiagnosticHealth(req, res, input) {
156
148
  if (req.method !== 'GET')
@@ -107,6 +107,17 @@ const PERMANENT_UPGRADE_REFUSALS = new Set([
107
107
  'catalog_model_route_missing',
108
108
  'bridge_provider_route_unavailable',
109
109
  ]);
110
+ export function safeEndUpgradeSocket(socket, payload) {
111
+ socket.on('error', () => undefined);
112
+ if (socket.destroyed || socket.writableEnded)
113
+ return;
114
+ try {
115
+ socket.end(payload);
116
+ }
117
+ catch {
118
+ socket.destroy();
119
+ }
120
+ }
110
121
  function writeUpgradeFailure(client, error, req) {
111
122
  const code = safeBridgeErrorCode(error) || 'bridge_websocket_upstream_unavailable';
112
123
  const permanent = PERMANENT_UPGRADE_REFUSALS.has(code);
@@ -119,9 +130,7 @@ function writeUpgradeFailure(client, error, req) {
119
130
  ...(req?.url === undefined ? {} : { url: req.url }),
120
131
  status,
121
132
  });
122
- if (client.destroyed)
123
- return;
124
- client.end(`HTTP/1.1 ${status} ${reason}\r\nContent-Type: application/json\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n`
133
+ safeEndUpgradeSocket(client, `HTTP/1.1 ${status} ${reason}\r\nContent-Type: application/json\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n`
125
134
  + JSON.stringify({ error: { type: 'sks_bridge_error', code, retryable: !permanent } }));
126
135
  }
127
136
  export async function forwardWebSocket(req, client, head, config, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
@@ -18,6 +18,28 @@ import { MAX_LIFECYCLE_THREADS } from './subagent-skill-availability-contract.js
18
18
  import { officialSubagentEvidenceReady, terminalBlockedNarutoGate } from '../subagents/terminal-subagent-state.js';
19
19
  const SUBAGENT_LIFECYCLE_CAPTURE_FAILURE_SCHEMA = 'sks.subagent-lifecycle-capture-failure.v1';
20
20
  const MAX_SUBAGENT_LIFECYCLE_CAPTURE_FAILURES = 528;
21
+ export const ACTIVE_OFFICIAL_WORKFLOW_IDLE_MS = 2 * 60 * 60 * 1000;
22
+ function parseIsoMs(value) {
23
+ const parsed = Date.parse(String(value || ''));
24
+ return Number.isFinite(parsed) ? parsed : 0;
25
+ }
26
+ export function officialWorkflowLastActivityMs(input) {
27
+ const times = [
28
+ parseIsoMs(input.plan?.created_at),
29
+ parseIsoMs(input.plan?.updated_at),
30
+ parseIsoMs(input.plan?.wave_lifecycle?.updated_at),
31
+ parseIsoMs(input.plan?.wave_lifecycle?.created_at),
32
+ parseIsoMs(input.gate?.updated_at),
33
+ parseIsoMs(input.gate?.created_at),
34
+ ...(input.events || []).map((event) => parseIsoMs(event.occurred_at ?? event.ts))
35
+ ];
36
+ return times.reduce((max, value) => (value > max ? value : max), 0);
37
+ }
38
+ export function officialWorkflowIsIdle(activityMs, nowMs = Date.now()) {
39
+ if (activityMs <= 0)
40
+ return true;
41
+ return nowMs - activityMs > ACTIVE_OFFICIAL_WORKFLOW_IDLE_MS;
42
+ }
21
43
  export async function inspectActiveOfficialSubagentWorkflow(root, state, sessionKey = null) {
22
44
  const missionId = String(state?.mission_id || '').trim();
23
45
  const workflowRunId = String(state?.official_subagent_run_id || '').trim();
@@ -71,8 +93,14 @@ export async function inspectActiveOfficialSubagentWorkflow(root, state, session
71
93
  if (events.length > 0 && openThreads !== liveThreads.size) {
72
94
  return { status: 'invalid', missionId, workflowRunId, reason: 'active_lifecycle_event_mismatch' };
73
95
  }
96
+ if (officialWorkflowIsIdle(officialWorkflowLastActivityMs({ plan, gate, events }))) {
97
+ return { status: 'inactive' };
98
+ }
74
99
  return { status: 'active', missionId, workflowRunId, openThreads };
75
100
  }
101
+ if (officialWorkflowIsIdle(officialWorkflowLastActivityMs({ plan, gate, events }))) {
102
+ return { status: 'inactive' };
103
+ }
76
104
  return { status: 'active', missionId, workflowRunId, openThreads: liveThreads.size };
77
105
  }
78
106
  catch {
@@ -2,7 +2,7 @@ import fsp from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonl, nowIso, sha256, packageRoot } from './fsx.js';
4
4
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.js';
5
- import { loadStateForSession, missionDir, sessionStateKey, setCurrent, stateFileForSession } from './mission.js';
5
+ import { loadOwnedRouteState, missionDir, sessionStateKey, setCurrent, stateFileForSession } from './mission.js';
6
6
  import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.js';
7
7
  import { maybeRecordMadSksSqlPlaneToolResultFromToolUse } from './mad-sks/sql-plane/result-lifecycle.js';
8
8
  import { checkHarnessModification, harnessGuardBlockReason, isHarnessSourceProject } from './harness-guard.js';
@@ -53,7 +53,7 @@ export { selftestCodexCommitHooks } from './hooks-runtime/codex-commit-hooks-sel
53
53
  async function loadState(root, payload = {}) {
54
54
  const sessionKey = conversationId(payload);
55
55
  if (!explicitConversationId(payload))
56
- return loadStateForSession(root, sessionKey);
56
+ return loadOwnedRouteState(root);
57
57
  const hashed = sessionStateKey(sessionKey);
58
58
  const sessionState = await readJson(stateFileForSession(root, sessionKey), null).catch(() => null);
59
59
  return sessionState ? { ...sessionState, _session_key: sessionState._session_key || hashed } : {};
@@ -228,6 +228,12 @@ export async function installProjectSkills(root) {
228
228
  export async function installSkills(root) {
229
229
  return installGlobalSkills(root);
230
230
  }
231
+ function hostExtraRemovedSkillTargets(home) {
232
+ return [
233
+ { scope: 'global-host-extra', ownerRoot: home, targetDir: path.join(home, '.cursor', 'skills'), managedOnly: true },
234
+ { scope: 'global-host-extra', ownerRoot: home, targetDir: path.join(home, '.claude', 'skills'), managedOnly: true },
235
+ ];
236
+ }
231
237
  export async function cleanupRemovedSksSkillResidue(opts) {
232
238
  const projectRoot = path.resolve(opts.root);
233
239
  const home = path.resolve(opts.home || os.homedir());
@@ -238,7 +244,8 @@ export async function cleanupRemovedSksSkillResidue(opts) {
238
244
  { scope: 'project', ownerRoot: projectRoot, targetDir: path.join(projectRoot, '.agents', 'skills') },
239
245
  { scope: 'project-codex', ownerRoot: projectRoot, targetDir: path.join(projectRoot, '.codex', 'skills') },
240
246
  { scope: 'global-runtime', ownerRoot: globalRuntimeRoot, targetDir: path.join(globalRuntimeRoot, '.agents', 'skills') },
241
- { scope: 'global-runtime-codex', ownerRoot: globalRuntimeRoot, targetDir: path.join(globalRuntimeRoot, '.codex', 'skills') }
247
+ { scope: 'global-runtime-codex', ownerRoot: globalRuntimeRoot, targetDir: path.join(globalRuntimeRoot, '.codex', 'skills') },
248
+ ...hostExtraRemovedSkillTargets(home),
242
249
  ];
243
250
  if (projectRoot !== home && projectRoot !== globalRuntimeRoot) {
244
251
  const scan = await collectNestedProjectRoots(projectRoot, new Set([home, globalRuntimeRoot]));
@@ -365,6 +372,8 @@ async function reconcileRemovedSkillTargets(targets, fix) {
365
372
  remaining.push(displayPath);
366
373
  continue;
367
374
  }
375
+ if (!managed && target.managedOnly)
376
+ continue;
368
377
  detected.push({
369
378
  scope: target.scope,
370
379
  name,
@@ -394,7 +403,7 @@ async function reconcileRemovedSkillTargets(targets, fix) {
394
403
  quarantinedManifestCollisions,
395
404
  remaining,
396
405
  errors
397
- });
406
+ }, { managedOnly: target.managedOnly === true });
398
407
  if (fix)
399
408
  await removeEmptySkillParents(target, errors);
400
409
  }
@@ -448,7 +457,7 @@ async function removeEmptySkillParents(target, errors) {
448
457
  }
449
458
  }
450
459
  }
451
- async function reconcileRemovedSkillManifests(target, rowByName, fix, report) {
460
+ async function reconcileRemovedSkillManifests(target, rowByName, fix, report, opts = {}) {
452
461
  for (const fileName of [SKS_SKILL_MANIFEST_FILE, 'skills-manifest.json']) {
453
462
  if (!rowByName.has(fileName))
454
463
  continue;
@@ -466,6 +475,8 @@ async function reconcileRemovedSkillManifests(target, rowByName, fix, report) {
466
475
  if (!inspection.exists)
467
476
  continue;
468
477
  if (inspection.leafSymlink || !inspection.stat?.isFile()) {
478
+ if (opts.managedOnly)
479
+ continue;
469
480
  if (fix) {
470
481
  try {
471
482
  await quarantineSkillDir(target.ownerRoot, manifestPath, fileName, 'removed-skill-manifest-collision');
@@ -496,6 +507,8 @@ async function reconcileRemovedSkillManifests(target, rowByName, fix, report) {
496
507
  catch {
497
508
  if (!manifestTextContainsRetiredJsonValue(text))
498
509
  continue;
510
+ if (opts.managedOnly)
511
+ continue;
499
512
  if (fix) {
500
513
  try {
501
514
  await quarantineSkillDir(target.ownerRoot, manifestPath, fileName, 'unparseable-removed-skill-manifest-collision');
@@ -514,6 +527,8 @@ async function reconcileRemovedSkillManifests(target, rowByName, fix, report) {
514
527
  if (!scrubbed.valid) {
515
528
  if (!scrubbed.hasRetiredResidue)
516
529
  continue;
530
+ if (opts.managedOnly)
531
+ continue;
517
532
  if (fix) {
518
533
  try {
519
534
  await quarantineSkillDir(target.ownerRoot, manifestPath, fileName, 'unmanaged-removed-skill-manifest-collision');
@@ -726,7 +741,7 @@ async function reconcileSkillsUnlocked(opts) {
726
741
  scope: 'global-runtime-codex',
727
742
  ownerRoot: globalRuntimeRoot,
728
743
  targetDir: path.join(globalRuntimeRoot, '.codex', 'skills')
729
- });
744
+ }, ...hostExtraRemovedSkillTargets(root));
730
745
  }
731
746
  const removedResidue = await reconcileRemovedSkillTargets(removedResidueTargets, opts.fix);
732
747
  report.retired_residue = {
@@ -1,32 +1,38 @@
1
1
  import { codexHookTrustDoctor } from '../../codex-hooks/codex-hook-trust-doctor.js';
2
2
  export async function runOtherHarnessCleanupStage(root) {
3
- const { scanHarnessConflicts } = await import('../../harness-conflicts.js');
3
+ const { cleanupOtherHarnessConflicts, scanHarnessConflicts } = await import('../../harness-conflicts.js');
4
4
  const scan = await scanHarnessConflicts(root);
5
- if (scan.hard_block) {
5
+ if (!scan.hard_block) {
6
6
  return {
7
- ok: false,
8
- status: 'failed',
9
- actions: ['other_harness_conflict_detected'],
10
- blockers: scan.hard.map((row) => `other_harness_conflict:${row.path}`),
7
+ ok: true,
8
+ status: 'ok',
9
+ actions: ['other_harness_conflict_check_clean'],
10
+ blockers: [],
11
11
  warnings: [],
12
12
  detail: {
13
13
  cleaned_count: 0,
14
- remaining_count: scan.hard.length,
15
- error_count: 0,
16
- cleanup_prompt_command: 'sks conflicts cleanup --yes'
14
+ remaining_count: 0,
15
+ error_count: 0
17
16
  }
18
17
  };
19
18
  }
19
+ const cleanup = await cleanupOtherHarnessConflicts(root);
20
+ const remaining = Array.isArray(cleanup.remaining) ? cleanup.remaining : [];
21
+ const errors = Array.isArray(cleanup.errors) ? cleanup.errors : [];
22
+ const blockers = [
23
+ ...remaining.map((row) => `other_harness_conflict:${row.path || 'unknown'}`),
24
+ ...errors.map((row) => `other_harness_cleanup_failed:${row.path || 'unknown'}:${row.error || 'error'}`),
25
+ ];
20
26
  return {
21
- ok: true,
22
- status: 'ok',
23
- actions: ['other_harness_conflict_check_clean'],
24
- blockers: [],
27
+ ok: blockers.length === 0,
28
+ status: blockers.length ? 'failed' : 'ok',
29
+ actions: ['other_harness_conflicts_quarantined'],
30
+ blockers,
25
31
  warnings: [],
26
32
  detail: {
27
- cleaned_count: 0,
28
- remaining_count: 0,
29
- error_count: 0
33
+ cleaned_count: Array.isArray(cleanup.cleaned) ? cleanup.cleaned.length : 0,
34
+ remaining_count: remaining.length,
35
+ error_count: errors.length
30
36
  }
31
37
  };
32
38
  }
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '9.1.0';
1
+ export const PACKAGE_VERSION = '9.1.1';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "9.1.0",
4
+ "version": "9.1.1",
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",