sneakoscope 6.1.2 → 6.2.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 +11 -4
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers.js +8 -3
- package/dist/config/skills-manifest.json +58 -58
- package/dist/core/agents/agent-effort-policy.js +28 -17
- package/dist/core/agents/agent-schema.js +1 -1
- package/dist/core/codex/codex-config-guard.js +178 -7
- package/dist/core/codex/codex-config-readability.js +21 -8
- package/dist/core/codex/codex-config-toml.js +14 -11
- package/dist/core/codex-app/mcp-manager.js +679 -0
- package/dist/core/codex-app/sks-menubar.js +405 -6
- package/dist/core/codex-control/codex-lb-launch-recovery.js +15 -0
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/commands/mad-sks-command.js +44 -3
- package/dist/core/commands/menubar-command.js +94 -0
- package/dist/core/commands/naruto-command.js +3 -1
- package/dist/core/commands/wiki-command.js +11 -5
- package/dist/core/fsx.js +1 -0
- package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +14 -8
- package/dist/core/hooks-runtime/naruto-decision-gate.js +184 -0
- package/dist/core/hooks-runtime/stop-repeat-guard.js +89 -0
- package/dist/core/hooks-runtime.js +36 -105
- package/dist/core/init/skills.js +4 -4
- package/dist/core/init.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +178 -29
- package/dist/core/preflight/parallel-preflight-engine.js +16 -3
- package/dist/core/proof/route-adapter.js +1 -1
- package/dist/core/proof/selftest-proof-fixtures.js +3 -5
- package/dist/core/provider/model-router.js +15 -6
- package/dist/core/release/package-size-budget.js +4 -6
- package/dist/core/research/research-plan-markdown.js +123 -0
- package/dist/core/research/research-super-search.js +8 -4
- package/dist/core/research.js +4 -119
- package/dist/core/retention.js +70 -2
- package/dist/core/routes/dollar-manifest-lite.js +1 -1
- package/dist/core/routes.js +75 -22
- package/dist/core/subagents/agent-catalog.js +87 -15
- package/dist/core/subagents/model-policy.js +189 -48
- package/dist/core/subagents/naruto-help-contract.js +11 -4
- package/dist/core/subagents/official-subagent-preparation.js +55 -9
- package/dist/core/subagents/official-subagent-prompt.js +45 -23
- package/dist/core/subagents/thread-budget.js +1 -1
- package/dist/core/subagents/triwiki-attention.js +117 -14
- package/dist/core/triwiki/code-pack-head-freshness.js +291 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-launcher.js +12 -2
- package/dist/core/zellij/zellij-pane-proof.js +9 -1
- package/dist/core/zellij/zellij-update.js +14 -1
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +11 -1
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +9 -2
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +13 -2
- package/dist/scripts/codex-native-agent-role-content-check.js +13 -1
- package/dist/scripts/codex-sdk-backend-router-check.js +2 -0
- package/dist/scripts/lib/codex-sdk-gate-lib.js +4 -0
- package/dist/scripts/official-subagent-workflow-check.js +1 -1
- package/dist/scripts/packlist-performance-check.js +1 -18
- package/dist/scripts/python-codex-sdk-all-pipelines-check.js +8 -0
- package/dist/scripts/sks-menubar-install-check.js +6 -1
- package/dist/scripts/super-search-provider-interface-check.js +31 -18
- package/package.json +3 -1
- package/dist/scripts/codex-0139-feature-probes-check.js +0 -30
- package/dist/scripts/codex-0139-marketplace-source-check.js +0 -13
|
@@ -2,7 +2,10 @@ import path from 'node:path';
|
|
|
2
2
|
import { ui as cliUi } from '../../cli/cli-theme.js';
|
|
3
3
|
import { projectRoot } from '../fsx.js';
|
|
4
4
|
import { inspectSksMenuBarStatus, installSksMenuBar, restartSksMenuBar, uninstallSksMenuBar } from '../codex-app/sks-menubar.js';
|
|
5
|
+
import { addCodexMcpServer, listCodexMcpServers, removeCodexMcpServer, setCodexMcpServerEnabled } from '../codex-app/mcp-manager.js';
|
|
5
6
|
export async function menubarCommand(subcommand = 'status', args = []) {
|
|
7
|
+
if (String(subcommand || '').toLowerCase() === 'mcp')
|
|
8
|
+
return menubarMcpCommand(args);
|
|
6
9
|
const action = normalizeAction(subcommand);
|
|
7
10
|
const root = path.resolve(String(readOption(args, '--root', '') || await projectRoot()));
|
|
8
11
|
const home = stringOption(args, '--home');
|
|
@@ -79,6 +82,38 @@ export async function menubarCommand(subcommand = 'status', args = []) {
|
|
|
79
82
|
printUsage();
|
|
80
83
|
process.exitCode = 2;
|
|
81
84
|
}
|
|
85
|
+
async function menubarMcpCommand(args = []) {
|
|
86
|
+
const action = String(args[0] || 'list').toLowerCase();
|
|
87
|
+
const rest = args.slice(1);
|
|
88
|
+
const home = stringOption(rest, '--home');
|
|
89
|
+
const options = home ? { home } : {};
|
|
90
|
+
let result;
|
|
91
|
+
if (['list', 'status', 'refresh'].includes(action)) {
|
|
92
|
+
result = await listCodexMcpServers(options);
|
|
93
|
+
}
|
|
94
|
+
else if (action === 'add') {
|
|
95
|
+
const payload = flag(rest, '--stdin-json') ? await readStdinJson() : null;
|
|
96
|
+
result = await addCodexMcpServer(payload, options);
|
|
97
|
+
}
|
|
98
|
+
else if (action === 'remove') {
|
|
99
|
+
result = await removeCodexMcpServer(stringOption(rest, '--name') || positional(rest), options);
|
|
100
|
+
}
|
|
101
|
+
else if (action === 'enable' || action === 'disable') {
|
|
102
|
+
result = await setCodexMcpServerEnabled(stringOption(rest, '--name') || positional(rest), action === 'enable', options);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
printMcpUsage();
|
|
106
|
+
process.exitCode = 2;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (flag(rest, '--json') || flag(args, '--json'))
|
|
110
|
+
printJson(result);
|
|
111
|
+
else
|
|
112
|
+
printMcpResult(result);
|
|
113
|
+
if (result?.ok !== true)
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
82
117
|
function normalizeAction(value) {
|
|
83
118
|
const text = String(value || 'status').toLowerCase();
|
|
84
119
|
if (['status', 'inspect', 'doctor'].includes(text))
|
|
@@ -124,8 +159,67 @@ Usage:
|
|
|
124
159
|
sks menubar install [--no-launch] [--json]
|
|
125
160
|
sks menubar restart [--json]
|
|
126
161
|
sks menubar uninstall [--json]
|
|
162
|
+
sks menubar mcp list [--json]
|
|
163
|
+
sks menubar mcp add --stdin-json [--json]
|
|
164
|
+
sks menubar mcp enable|disable|remove <name> [--json]
|
|
165
|
+
`);
|
|
166
|
+
}
|
|
167
|
+
function printMcpUsage() {
|
|
168
|
+
console.log(`SKS Menu Bar MCP Manager
|
|
169
|
+
|
|
170
|
+
Usage:
|
|
171
|
+
sks menubar mcp list [--json]
|
|
172
|
+
sks menubar mcp add --stdin-json [--json]
|
|
173
|
+
sks menubar mcp enable <name> [--json]
|
|
174
|
+
sks menubar mcp disable <name> [--json]
|
|
175
|
+
sks menubar mcp remove <name> [--json]
|
|
127
176
|
`);
|
|
128
177
|
}
|
|
178
|
+
function printMcpResult(result) {
|
|
179
|
+
if (result?.schema === 'sks.menubar-mcp-list.v1') {
|
|
180
|
+
console.log(`Codex MCP servers (${result.server_count || 0})`);
|
|
181
|
+
for (const server of result.servers || [])
|
|
182
|
+
console.log(`- ${server.enabled ? 'on ' : 'off'} ${server.name}: ${server.summary}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log(`Codex MCP ${result?.action || 'mutation'}: ${result?.ok ? 'ok' : 'failed'}`);
|
|
186
|
+
for (const blocker of result?.blockers || [])
|
|
187
|
+
console.log(`blocker: ${blocker}`);
|
|
188
|
+
}
|
|
189
|
+
async function readStdinJson() {
|
|
190
|
+
if (process.stdin.isTTY)
|
|
191
|
+
return null;
|
|
192
|
+
const chunks = [];
|
|
193
|
+
let bytes = 0;
|
|
194
|
+
for await (const chunk of process.stdin) {
|
|
195
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
196
|
+
bytes += buffer.length;
|
|
197
|
+
if (bytes > 1024 * 1024)
|
|
198
|
+
throw new Error('menubar_mcp_stdin_payload_too_large');
|
|
199
|
+
chunks.push(buffer);
|
|
200
|
+
}
|
|
201
|
+
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
202
|
+
if (!text)
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(text);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function positional(args) {
|
|
212
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
213
|
+
const arg = String(args[index] || '');
|
|
214
|
+
if (arg === '--home' || arg === '--name') {
|
|
215
|
+
index += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!arg.startsWith('--'))
|
|
219
|
+
return arg;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
129
223
|
function printJson(value) {
|
|
130
224
|
console.log(JSON.stringify(value, null, 2));
|
|
131
225
|
}
|
|
@@ -196,7 +196,8 @@ async function narutoRunTransaction(parsed, root, appSession, sessionKey) {
|
|
|
196
196
|
parentSummary: effectiveParentSummary,
|
|
197
197
|
blockers,
|
|
198
198
|
appSession,
|
|
199
|
-
sessionKey
|
|
199
|
+
sessionKey,
|
|
200
|
+
suggestedAgents: Array.isArray(completedPlan?.suggested_agents) ? completedPlan.suggested_agents : []
|
|
200
201
|
});
|
|
201
202
|
await writeJsonAtomic(path.join(dir, NARUTO_SUMMARY_FILENAME), summary);
|
|
202
203
|
await writeNarutoGate(dir, { missionId: id, workflowRunId, evidence, passed, blockers });
|
|
@@ -364,6 +365,7 @@ async function narutoProof(parsed) {
|
|
|
364
365
|
function narutoHelp(parsed) {
|
|
365
366
|
const result = buildNarutoHelpResult();
|
|
366
367
|
return emit(parsed, result, () => {
|
|
368
|
+
cliUi.ok('official subagent workflow help available');
|
|
367
369
|
console.log('$Naruto — Codex official subagent workflow');
|
|
368
370
|
for (const line of result.usage)
|
|
369
371
|
console.log(` ${line}`);
|
|
@@ -22,6 +22,7 @@ import { recordImageWrongnessFromValidation } from '../triwiki-wrongness/image-w
|
|
|
22
22
|
import { publishSharedMemory, rebuildSharedIndexes, sharedMemorySummary, validateSharedMemory } from '../git-hygiene/shared-memory-publish.js';
|
|
23
23
|
import { scanCodebaseIndex } from '../triwiki/code-index-scanner.js';
|
|
24
24
|
import { buildCodePack, validateCodePack, writeCodePackAtomic } from '../triwiki/code-pack.js';
|
|
25
|
+
import { inspectCodePackHeadFreshness } from '../triwiki/code-pack-head-freshness.js';
|
|
25
26
|
import { sealTriWikiContextPack, validateTriWikiContextPackProvenance } from '../triwiki-provenance.js';
|
|
26
27
|
import { flag, positionalArgs, readFlagValue, readOption, resolveMissionId } from './command-utils.js';
|
|
27
28
|
export async function wikiCommand(sub, args = []) {
|
|
@@ -251,8 +252,10 @@ async function wikiRefreshCode(args = []) {
|
|
|
251
252
|
console.log(`- ${issue}`);
|
|
252
253
|
}
|
|
253
254
|
/** Cheap freshness check: compares the code pack's recorded git HEAD sha (at
|
|
254
|
-
* generation time) against the current HEAD.
|
|
255
|
-
*
|
|
255
|
+
* generation time) against the current HEAD. A commit containing only the two
|
|
256
|
+
* tracked code-pack metadata files is equivalent to the generating HEAD, which
|
|
257
|
+
* avoids an impossible self-referential commit hash. Any other uncertainty
|
|
258
|
+
* resolves to 'stale' rather than 'fresh', never overclaiming. */
|
|
256
259
|
/** Active-wrongness counts per TriWiki module id (from wrongness records' module_ids),
|
|
257
260
|
* so attention can hydrate frequently-wrong modules' code entries first. */
|
|
258
261
|
async function buildWrongnessByModule(root) {
|
|
@@ -276,9 +279,12 @@ async function codePackFreshness(root) {
|
|
|
276
279
|
const packSha = pack?.git_head_sha || null;
|
|
277
280
|
if (!packSha)
|
|
278
281
|
return { status: 'missing', git_head_sha: null, pack_sha: null };
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
+
const freshness = await inspectCodePackHeadFreshness(root, packSha, { timeoutMs: 5_000 });
|
|
283
|
+
return {
|
|
284
|
+
status: freshness.fresh ? 'fresh' : 'stale',
|
|
285
|
+
git_head_sha: freshness.current_sha,
|
|
286
|
+
pack_sha: freshness.pack_sha
|
|
287
|
+
};
|
|
282
288
|
}
|
|
283
289
|
function wikiRefreshFailureAnalysis(validationResult, gate = {}) {
|
|
284
290
|
if (validationResult?.ok) {
|
package/dist/core/fsx.js
CHANGED
|
@@ -11,6 +11,7 @@ import { PACKAGE_VERSION } from './version.js';
|
|
|
11
11
|
export { PACKAGE_VERSION };
|
|
12
12
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
13
13
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14
|
+
export const SKS_TEMP_LEASE_FILE = '.sks-temp-lease.json';
|
|
14
15
|
const gunzipAsync = promisify(gunzipCallback);
|
|
15
16
|
export function nowIso() {
|
|
16
17
|
return new Date().toISOString();
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { exists, readJson
|
|
2
|
+
import { exists, readJson } from '../fsx.js';
|
|
3
|
+
import { inspectCodePackHeadFreshness } from '../triwiki/code-pack-head-freshness.js';
|
|
3
4
|
/** Bounded, non-blocking code-pack staleness nudge for the user-prompt-submit hook.
|
|
4
5
|
*
|
|
5
6
|
* Returns a one-line context note when a published code pack
|
|
6
7
|
* (.sneakoscope/wiki/code-pack.json) exists but was generated against a different
|
|
7
8
|
* git HEAD than the current one — i.e. the codebase moved and the LLM-facing code
|
|
8
|
-
* summaries are now out of date.
|
|
9
|
+
* summaries are now out of date. A follow-up commit that changes only the two
|
|
10
|
+
* tracked code-pack metadata files remains fresh; otherwise committing the generated
|
|
11
|
+
* pack would make itself stale forever. Returns null (silent) when there is no pack at all
|
|
9
12
|
* (repos that never opted into `sks wiki refresh --code` must not be nagged) or when
|
|
10
13
|
* the check can't complete cheaply. It NEVER regenerates the pack and NEVER blocks:
|
|
11
|
-
* the
|
|
12
|
-
* timeout so it cannot blow the hook latency budget. Any
|
|
14
|
+
* the common path is one JSON read plus one bounded `git log` history check,
|
|
15
|
+
* wrapped in a hard timeout so it cannot blow the hook latency budget. Any
|
|
16
|
+
* failure resolves to null. */
|
|
13
17
|
export async function codePackFreshnessNote(root, opts = {}) {
|
|
14
|
-
const budgetMs = Math.max(1, opts.budgetMs ??
|
|
18
|
+
const budgetMs = Math.max(1, opts.budgetMs ?? 750);
|
|
15
19
|
const gitTimeoutMs = Math.max(1, budgetMs - 50);
|
|
16
20
|
return raceWithTimeout(computeNote(root, gitTimeoutMs), budgetMs).catch(() => null);
|
|
17
21
|
}
|
|
@@ -25,9 +29,11 @@ async function computeNote(root, gitTimeoutMs) {
|
|
|
25
29
|
// rather than nag with a comparison we can't actually make.
|
|
26
30
|
if (!packSha)
|
|
27
31
|
return null;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const freshness = await inspectCodePackHeadFreshness(root, packSha, {
|
|
33
|
+
timeoutMs: gitTimeoutMs,
|
|
34
|
+
advisoryCache: true,
|
|
35
|
+
});
|
|
36
|
+
if (!freshness.conclusive || !freshness.current_sha || freshness.fresh)
|
|
31
37
|
return null;
|
|
32
38
|
return 'SKS note: the codebase code pack is stale (HEAD moved since it was built). Run `sks wiki refresh --code` to refresh source-cited code context.';
|
|
33
39
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { appendJsonlBounded, nowIso, sha256 } from '../fsx.js';
|
|
3
|
+
import { codexHookEventName } from '../codex-compat/codex-hook-events.js';
|
|
4
|
+
import { narutoDecisionForRoute, routeByDollarCommand, routeById, routePrompt, stripVisibleDecisionAnswerBlocks } from '../routes.js';
|
|
5
|
+
import { classifyTaskProfile, isTaskProfile } from '../runtime/task-profile.js';
|
|
6
|
+
export const HOOK_NARUTO_DECISION_LOG = 'hook-naruto-decision-gate.jsonl';
|
|
7
|
+
const HOOK_NARUTO_DECISION_LOG_MAX_BYTES = 256 * 1024;
|
|
8
|
+
export function hookNarutoDecisionLogPath(root) {
|
|
9
|
+
return path.join(root, '.sneakoscope', 'state', HOOK_NARUTO_DECISION_LOG);
|
|
10
|
+
}
|
|
11
|
+
export async function evaluateHookNarutoDecisionGate(input) {
|
|
12
|
+
const decision = decideHookNaruto(input);
|
|
13
|
+
const prompt = decision.event === 'UserPromptSubmit'
|
|
14
|
+
? stripVisibleDecisionAnswerBlocks(extractPrompt(input.payload))
|
|
15
|
+
: '';
|
|
16
|
+
const row = {
|
|
17
|
+
ts: nowIso(),
|
|
18
|
+
schema: decision.schema,
|
|
19
|
+
event: decision.event,
|
|
20
|
+
mode: decision.mode,
|
|
21
|
+
required: decision.required,
|
|
22
|
+
action: decision.action,
|
|
23
|
+
route_id: decision.route_id,
|
|
24
|
+
task_profile: decision.task_profile,
|
|
25
|
+
reason: decision.reason,
|
|
26
|
+
source: decision.source,
|
|
27
|
+
trivial: decision.trivial,
|
|
28
|
+
default_parallel: decision.default_parallel,
|
|
29
|
+
session_hash: shortHash(input.sessionKey),
|
|
30
|
+
turn_hash: shortHash(input.payload?.turn_id || input.payload?.turnId || ''),
|
|
31
|
+
prompt_hash: prompt ? shortHash(prompt) : null
|
|
32
|
+
};
|
|
33
|
+
let recorded = true;
|
|
34
|
+
try {
|
|
35
|
+
await appendJsonlBounded(hookNarutoDecisionLogPath(input.root), row, HOOK_NARUTO_DECISION_LOG_MAX_BYTES);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
recorded = false;
|
|
39
|
+
}
|
|
40
|
+
return { ...decision, recorded };
|
|
41
|
+
}
|
|
42
|
+
export function decideHookNaruto(input) {
|
|
43
|
+
const event = codexHookEventName(input.name) || String(input.name || 'unknown');
|
|
44
|
+
const state = input.state || {};
|
|
45
|
+
if (event !== 'UserPromptSubmit') {
|
|
46
|
+
return decisionFromState(event, state);
|
|
47
|
+
}
|
|
48
|
+
const prompt = stripVisibleDecisionAnswerBlocks(extractPrompt(input.payload));
|
|
49
|
+
const profile = classifyTaskProfile(prompt);
|
|
50
|
+
if (input.parentLaunchMissionId) {
|
|
51
|
+
return decision({
|
|
52
|
+
event,
|
|
53
|
+
mode: 'generic_naruto',
|
|
54
|
+
required: true,
|
|
55
|
+
action: 'prepare_naruto',
|
|
56
|
+
routeId: 'Naruto',
|
|
57
|
+
profile,
|
|
58
|
+
reason: 'attached_parent_naruto_launch',
|
|
59
|
+
source: 'parent_launch',
|
|
60
|
+
trivial: false
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (input.noQuestion || shouldInheritActiveRoute(prompt, state)) {
|
|
64
|
+
return decisionFromState(event, state);
|
|
65
|
+
}
|
|
66
|
+
const route = routePrompt(prompt);
|
|
67
|
+
const routeDecision = narutoDecisionForRoute(route, prompt, profile);
|
|
68
|
+
return decision({
|
|
69
|
+
event,
|
|
70
|
+
mode: routeDecision.mode,
|
|
71
|
+
required: routeDecision.required,
|
|
72
|
+
action: routeDecision.mode === 'generic_naruto'
|
|
73
|
+
? 'prepare_naruto'
|
|
74
|
+
: routeDecision.mode === 'route_owned'
|
|
75
|
+
? 'route_owned'
|
|
76
|
+
: 'bypass',
|
|
77
|
+
routeId: routeDecision.route_id,
|
|
78
|
+
profile: routeDecision.task_profile,
|
|
79
|
+
reason: routeDecision.reason,
|
|
80
|
+
source: 'user_prompt',
|
|
81
|
+
trivial: routeDecision.trivial
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function looksLikeActiveContinuationPrompt(prompt = '') {
|
|
85
|
+
const text = stripVisibleDecisionAnswerBlocks(String(prompt || ''))
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/[.!?。!?…,:;]+$/u, '')
|
|
88
|
+
.trim();
|
|
89
|
+
if (!text)
|
|
90
|
+
return false;
|
|
91
|
+
return /^(?:(?:please\s+)?(?:keep\s+going|continue|resume|go\s+on|proceed|carry\s+on)(?:\s+please)?|계속(?:\s*진행)?(?:\s*해\s*줘|\s*해주세요|\s*해)?|이어\s*서(?:\s*해\s*줘|\s*해주세요|\s*진행해)?|이어서(?:\s*해\s*줘|\s*해주세요|\s*진행해)?|진행(?:\s*해\s*줘|\s*해주세요|\s*해)?|마저\s*해(?:\s*줘|\s*주세요)?|다음|next)$/i.test(text);
|
|
92
|
+
}
|
|
93
|
+
function decisionFromState(event, state) {
|
|
94
|
+
const route = routeFromState(state);
|
|
95
|
+
const routePolicy = narutoDecisionForRoute(route, '', 'passthrough');
|
|
96
|
+
if (state?.route_closed !== true && routePolicy.mode === 'route_owned') {
|
|
97
|
+
return decision({
|
|
98
|
+
event,
|
|
99
|
+
mode: 'route_owned',
|
|
100
|
+
required: false,
|
|
101
|
+
action: 'route_owned',
|
|
102
|
+
routeId: state?.route_command || state?.route || state?.mode || routePolicy.route_id,
|
|
103
|
+
profile: isTaskProfile(state?.task_profile) ? state.task_profile : routePolicy.task_profile,
|
|
104
|
+
reason: routePolicy.reason,
|
|
105
|
+
source: state?.mission_id ? 'active_route' : 'no_task_context',
|
|
106
|
+
trivial: false
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const required = state?.route_closed === true
|
|
110
|
+
? false
|
|
111
|
+
: state?.subagents_required === true
|
|
112
|
+
|| (/^(?:NARUTO|TEAM)$/i.test(String(state?.mode || state?.route || '')) && state?.subagents_required !== false);
|
|
113
|
+
const profile = isTaskProfile(state?.task_profile)
|
|
114
|
+
? state.task_profile
|
|
115
|
+
: required
|
|
116
|
+
? 'bounded-work'
|
|
117
|
+
: 'passthrough';
|
|
118
|
+
return decision({
|
|
119
|
+
event,
|
|
120
|
+
mode: required ? 'generic_naruto' : 'none',
|
|
121
|
+
required,
|
|
122
|
+
action: required ? 'observe_required' : 'observe_bypass',
|
|
123
|
+
routeId: state?.route_command || state?.route || state?.mode || null,
|
|
124
|
+
profile,
|
|
125
|
+
reason: state?.route_closed === true
|
|
126
|
+
? 'active_route_closed'
|
|
127
|
+
: required
|
|
128
|
+
? 'active_route_requires_official_subagents'
|
|
129
|
+
: 'no_active_naruto_requirement',
|
|
130
|
+
source: state?.mission_id ? 'active_route' : 'no_task_context',
|
|
131
|
+
trivial: !required
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function routeFromState(state) {
|
|
135
|
+
for (const candidate of [state?.route, state?.mode, state?.route_command]) {
|
|
136
|
+
const route = routeById(candidate) || routeByDollarCommand(candidate);
|
|
137
|
+
if (route)
|
|
138
|
+
return route;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function shouldInheritActiveRoute(prompt, state) {
|
|
143
|
+
if (!state?.mission_id || state?.route_closed === true)
|
|
144
|
+
return false;
|
|
145
|
+
if (looksLikeActiveContinuationPrompt(prompt))
|
|
146
|
+
return true;
|
|
147
|
+
const phase = String(state?.phase || '');
|
|
148
|
+
const clarificationAwaiting = phase.includes('CLARIFICATION_AWAITING_ANSWERS')
|
|
149
|
+
|| String(state?.stop_gate || '') === 'clarification-gate';
|
|
150
|
+
return clarificationAwaiting
|
|
151
|
+
&& state?.ambiguity_gate_required === true
|
|
152
|
+
&& state?.ambiguity_gate_passed !== true;
|
|
153
|
+
}
|
|
154
|
+
function decision(input) {
|
|
155
|
+
return {
|
|
156
|
+
schema: 'sks.hook-naruto-decision.v1',
|
|
157
|
+
event: input.event,
|
|
158
|
+
mode: input.mode,
|
|
159
|
+
required: input.required,
|
|
160
|
+
action: input.action,
|
|
161
|
+
route_id: input.routeId,
|
|
162
|
+
task_profile: input.profile,
|
|
163
|
+
reason: input.reason,
|
|
164
|
+
source: input.source,
|
|
165
|
+
trivial: input.trivial,
|
|
166
|
+
default_parallel: input.required,
|
|
167
|
+
recorded: false
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function extractPrompt(payload = {}) {
|
|
171
|
+
return String(payload.prompt
|
|
172
|
+
|| payload.user_prompt
|
|
173
|
+
|| payload.userPrompt
|
|
174
|
+
|| payload.message
|
|
175
|
+
|| payload.input?.prompt
|
|
176
|
+
|| payload.input?.message
|
|
177
|
+
|| payload.raw
|
|
178
|
+
|| '');
|
|
179
|
+
}
|
|
180
|
+
function shortHash(value) {
|
|
181
|
+
const text = String(value || '').trim();
|
|
182
|
+
return text ? sha256(text).slice(0, 16) : null;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=naruto-decision-gate.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, readJson, sha256, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { appendMissionStatus } from '../recallpulse.js';
|
|
4
|
+
import { conversationId } from './payload-signals.js';
|
|
5
|
+
const STOP_REPEAT_GUARD_ARTIFACT = 'stop-hook-repeat-guard.json';
|
|
6
|
+
const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
|
|
7
|
+
const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
|
|
8
|
+
const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
|
|
9
|
+
export async function finalizationRepeatDecision(root, state = {}, payload = {}, reason = '', kind = 'finalization') {
|
|
10
|
+
const now = nowIso();
|
|
11
|
+
const guardPath = path.join(root, '.sneakoscope', 'state', STOP_REPEAT_GUARD_ARTIFACT);
|
|
12
|
+
const previous = await readJson(guardPath, {}).catch(() => ({}));
|
|
13
|
+
const limit = stopRepeatGuardLimit();
|
|
14
|
+
const entries = pruneStopRepeatEntries(previous.entries || {}, now);
|
|
15
|
+
const key = stopRepeatKey(state, payload, reason, kind);
|
|
16
|
+
const prior = entries[key] || {};
|
|
17
|
+
const repeatCount = stopRepeatInWindow(prior, now)
|
|
18
|
+
? Number(prior.repeat_count || 0) + 1
|
|
19
|
+
: 1;
|
|
20
|
+
const record = {
|
|
21
|
+
schema_version: 1,
|
|
22
|
+
updated_at: now,
|
|
23
|
+
window_ms: STOP_REPEAT_GUARD_WINDOW_MS,
|
|
24
|
+
limit,
|
|
25
|
+
entries: {
|
|
26
|
+
...entries,
|
|
27
|
+
[key]: {
|
|
28
|
+
kind,
|
|
29
|
+
route: state.route_command || state.route || state.mode || null,
|
|
30
|
+
mission_id: state.mission_id || null,
|
|
31
|
+
conversation_id: conversationId(payload),
|
|
32
|
+
first_seen: stopRepeatInWindow(prior, now) ? (prior.first_seen || now) : now,
|
|
33
|
+
last_seen: now,
|
|
34
|
+
repeat_count: repeatCount,
|
|
35
|
+
tripped: repeatCount >= limit,
|
|
36
|
+
reason
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
await writeJsonAtomic(guardPath, record).catch(() => null);
|
|
41
|
+
if (state.mission_id) {
|
|
42
|
+
await appendMissionStatus(root, state.mission_id, {
|
|
43
|
+
category: repeatCount >= limit ? 'warning' : 'blocker',
|
|
44
|
+
audience: ['user', 'route', 'final-summary'],
|
|
45
|
+
stage_id: 'before_final',
|
|
46
|
+
message: repeatCount >= limit
|
|
47
|
+
? `Repeated ${kind} stop prompt was suppressed; route completion is still unclaimed until evidence passes.`
|
|
48
|
+
: reason,
|
|
49
|
+
dedupe_key: key,
|
|
50
|
+
evidence: [STOP_REPEAT_GUARD_ARTIFACT]
|
|
51
|
+
}).catch(() => null);
|
|
52
|
+
}
|
|
53
|
+
if (repeatCount < limit)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
continue: true,
|
|
57
|
+
systemMessage: `SKS stop hook repeat guard suppressed repeated ${kind} prompt after ${repeatCount} identical block(s). No completion success is claimed by the hook.`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function stopRepeatKey(state = {}, payload = {}, reason = '', kind = '') {
|
|
61
|
+
return sha256(JSON.stringify({
|
|
62
|
+
kind,
|
|
63
|
+
reason,
|
|
64
|
+
conversation_id: conversationId(payload),
|
|
65
|
+
mission_id: state.mission_id || null,
|
|
66
|
+
route: state.route_command || state.route || state.mode || null,
|
|
67
|
+
gate: state.stop_gate || null
|
|
68
|
+
})).slice(0, 24);
|
|
69
|
+
}
|
|
70
|
+
function stopRepeatGuardLimit() {
|
|
71
|
+
const raw = Number.parseInt(process.env.SKS_STOP_REPEAT_GUARD_LIMIT || '', 10);
|
|
72
|
+
if (!Number.isFinite(raw))
|
|
73
|
+
return DEFAULT_STOP_REPEAT_GUARD_LIMIT;
|
|
74
|
+
return Math.max(1, Math.min(20, raw));
|
|
75
|
+
}
|
|
76
|
+
function stopRepeatInWindow(entry = {}, now = nowIso()) {
|
|
77
|
+
const last = Date.parse(entry.last_seen || '');
|
|
78
|
+
const current = Date.parse(now);
|
|
79
|
+
if (!Number.isFinite(last) || !Number.isFinite(current))
|
|
80
|
+
return false;
|
|
81
|
+
return current - last <= STOP_REPEAT_GUARD_WINDOW_MS;
|
|
82
|
+
}
|
|
83
|
+
function pruneStopRepeatEntries(entries = {}, now = nowIso()) {
|
|
84
|
+
return Object.fromEntries(Object.entries(entries)
|
|
85
|
+
.filter(([, entry]) => stopRepeatInWindow(entry, now))
|
|
86
|
+
.sort((a, b) => Date.parse(b[1]?.last_seen || '') - Date.parse(a[1]?.last_seen || ''))
|
|
87
|
+
.slice(0, STOP_REPEAT_GUARD_MAX_ENTRIES));
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=stop-repeat-guard.js.map
|