sneakoscope 3.1.4 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/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/install-helpers.js +56 -4
- package/dist/commands/codex-lb.js +12 -9
- package/dist/core/codex-app/codex-agent-role-sync.js +63 -11
- package/dist/core/codex-app/codex-agent-type-probe.js +202 -0
- package/dist/core/codex-app/codex-app-execution-profile.js +15 -8
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -4
- package/dist/core/codex-app/codex-app-harness-matrix.js +78 -26
- package/dist/core/codex-app/codex-app-types.js +21 -0
- package/dist/core/codex-app/codex-hook-approval-probe.js +188 -0
- package/dist/core/codex-app/codex-hook-lifecycle.js +31 -10
- package/dist/core/codex-app/codex-init-deep.js +97 -4
- package/dist/core/codex-app/codex-skill-sync.js +75 -3
- package/dist/core/codex-app/lazycodex-analysis.js +31 -10
- package/dist/core/codex-app/lazycodex-interop-policy.js +13 -2
- package/dist/core/codex-app/lazycodex-live-analyzer.js +98 -0
- package/dist/core/commands/mad-sks-command.js +37 -2
- package/dist/core/commands/qa-loop-command.js +3 -2
- package/dist/core/commands/research-command.js +2 -2
- package/dist/core/doctor/doctor-zellij-repair.js +5 -1
- package/dist/core/feature-fixtures.js +1 -0
- package/dist/core/feature-registry.js +4 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +4 -1
- package/dist/core/loops/loop-continuation-enforcer.js +11 -3
- package/dist/core/loops/loop-planner.js +21 -4
- package/dist/core/loops/loop-worker-runtime.js +23 -7
- package/dist/core/naruto/naruto-loop-worker-router.js +11 -2
- package/dist/core/qa-loop.js +39 -4
- package/dist/core/research/research-cycle-runner.js +1 -0
- package/dist/core/research/research-stage-runner.js +9 -2
- package/dist/core/research.js +35 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/homebrew-policy.js +0 -1
- package/dist/core/zellij/zellij-self-heal-types.js +45 -0
- package/dist/core/zellij/zellij-self-heal.js +69 -8
- package/dist/core/zellij/zellij-update.js +9 -1
- package/dist/scripts/sks-3-1-5-directive-check-lib.js +347 -0
- package/package.json +26 -2
|
@@ -43,7 +43,8 @@ async function runLoopWorkerNative(input) {
|
|
|
43
43
|
? buildLoopMakerPrompt({ plan: input.plan, node: input.node, worktreePath: input.worktree?.path || null })
|
|
44
44
|
: buildLoopCheckerPrompt({ plan: input.plan, node: input.node, makerArtifacts: input.makerArtifacts || [] });
|
|
45
45
|
const workerCount = effectiveLoopWorkerCount(input);
|
|
46
|
-
const
|
|
46
|
+
const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
|
|
47
|
+
const workGraph = buildLoopNarutoWorkGraph(input, workerCount, executionProfile);
|
|
47
48
|
// Root-cause-1 fix: keep the ORCHESTRATOR root on the MAIN repo (input.root), not the
|
|
48
49
|
// loop worktree. All zellij/right-column/slot-telemetry state derives from the orchestrator
|
|
49
50
|
// root, so anchoring it on input.root makes the SLOTS snapshot land under
|
|
@@ -52,7 +53,6 @@ async function runLoopWorkerNative(input) {
|
|
|
52
53
|
// The loop worktree is still where workers cwd + write: it is threaded through the per-worker
|
|
53
54
|
// `worktree` opt below, which launchWorker reads as ctx.opts.worktree -> workerCwd.
|
|
54
55
|
const insideZellij = Boolean(process.env.SKS_ZELLIJ_SESSION_NAME || process.env.ZELLIJ);
|
|
55
|
-
const executionProfile = await resolveCodexAppExecutionProfile({ root: input.root }).catch(() => null);
|
|
56
56
|
const visiblePaneCap = Math.min(resolveLoopVisiblePaneCap(workerCount), Math.max(1, workerCount));
|
|
57
57
|
const zellijPlacementOpts = insideZellij ? {
|
|
58
58
|
workerPlacement: 'zellij-pane',
|
|
@@ -99,9 +99,9 @@ async function runLoopWorkerNative(input) {
|
|
|
99
99
|
fallback_reason: null
|
|
100
100
|
} : null
|
|
101
101
|
});
|
|
102
|
-
return normalizeNativeResult(input, orchestrator);
|
|
102
|
+
return normalizeNativeResult(input, orchestrator, executionProfile);
|
|
103
103
|
}
|
|
104
|
-
async function normalizeNativeResult(input, result) {
|
|
104
|
+
async function normalizeNativeResult(input, result, executionProfile) {
|
|
105
105
|
const artifacts = collectArtifactPaths(result);
|
|
106
106
|
const changedFiles = stringArray(result?.changed_files || result?.proof?.changed_files || result?.results?.flatMap?.((row) => row?.changed_files || []));
|
|
107
107
|
const blockers = [
|
|
@@ -124,7 +124,15 @@ async function normalizeNativeResult(input, result) {
|
|
|
124
124
|
blockers: [...new Set(blockers)],
|
|
125
125
|
runtime_proof_path: proofPath,
|
|
126
126
|
worker_ids: stringArray(result?.results?.map?.((row) => row?.agent_id || row?.id)),
|
|
127
|
-
session_ids: stringArray(result?.results?.map?.((row) => row?.session_id))
|
|
127
|
+
session_ids: stringArray(result?.results?.map?.((row) => row?.session_id)),
|
|
128
|
+
...(executionProfile ? {
|
|
129
|
+
codex_app_execution_profile: {
|
|
130
|
+
mode: executionProfile.mode,
|
|
131
|
+
agent_role_strategy: executionProfile.agent_role_strategy,
|
|
132
|
+
artifact_path: executionProfile.artifact_path,
|
|
133
|
+
agent_type_probe_artifact_path: executionProfile.agent_type_probe_artifact_path
|
|
134
|
+
}
|
|
135
|
+
} : {})
|
|
128
136
|
};
|
|
129
137
|
await writeJsonAtomic(proofPath, { ...normalized, native_result_summary: summarizeNativeResult(result), generated_at: nowIso() });
|
|
130
138
|
return normalized;
|
|
@@ -189,7 +197,13 @@ async function runLoopWorkerFixture(input) {
|
|
|
189
197
|
fixture_allowed_reason: fixturePolicy.allowed ? fixturePolicy.reason : null
|
|
190
198
|
};
|
|
191
199
|
}
|
|
192
|
-
function buildLoopNarutoWorkGraph(input, workerCount) {
|
|
200
|
+
function buildLoopNarutoWorkGraph(input, workerCount, executionProfile) {
|
|
201
|
+
const profilePayload = executionProfile ? {
|
|
202
|
+
mode: executionProfile.mode,
|
|
203
|
+
agent_role_strategy: executionProfile.agent_role_strategy,
|
|
204
|
+
artifact_path: executionProfile.artifact_path,
|
|
205
|
+
agent_type_probe_artifact_path: executionProfile.agent_type_probe_artifact_path
|
|
206
|
+
} : undefined;
|
|
193
207
|
const workItems = Array.from({ length: Math.max(1, workerCount) }, (_, index) => {
|
|
194
208
|
const id = `${input.node.loop_id}-${input.phase}-${index + 1}`;
|
|
195
209
|
const writeAllowed = input.phase === 'maker';
|
|
@@ -218,7 +232,8 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
|
|
|
218
232
|
mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
|
|
219
233
|
required: input.node.worktree.required,
|
|
220
234
|
allocation_required: false
|
|
221
|
-
}
|
|
235
|
+
},
|
|
236
|
+
...(profilePayload ? { codex_app_execution_profile: profilePayload } : {})
|
|
222
237
|
};
|
|
223
238
|
});
|
|
224
239
|
return {
|
|
@@ -239,6 +254,7 @@ function buildLoopNarutoWorkGraph(input, workerCount) {
|
|
|
239
254
|
worktree_root: null,
|
|
240
255
|
fallback_reason: input.worktree?.path ? 'loop_worktree_already_allocated' : null
|
|
241
256
|
},
|
|
257
|
+
...(profilePayload ? { codex_app_execution_profile: profilePayload } : {}),
|
|
242
258
|
blockers: [],
|
|
243
259
|
ok: true
|
|
244
260
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export function routeNarutoLoopWorker(node, role) {
|
|
1
|
+
export function routeNarutoLoopWorker(node, role, profile) {
|
|
2
2
|
const domain = node.loop_id.replace(/^loop-/, '');
|
|
3
3
|
const roles = roleLabels(domain);
|
|
4
4
|
const gates = [...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final];
|
|
5
|
+
const roleName = role === 'maker' ? roles.maker : roles.checker;
|
|
6
|
+
const strategy = profile?.agent_role_strategy || 'message-role';
|
|
5
7
|
return {
|
|
6
8
|
schema: 'sks.naruto-loop-worker-route.v1',
|
|
7
9
|
loop_id: node.loop_id,
|
|
@@ -9,7 +11,10 @@ export function routeNarutoLoopWorker(node, role) {
|
|
|
9
11
|
checker_role: roles.checker,
|
|
10
12
|
prompt: [
|
|
11
13
|
`loop purpose: ${node.purpose}`,
|
|
12
|
-
`role: ${
|
|
14
|
+
`role: ${roleName}`,
|
|
15
|
+
`agent role strategy: ${strategy}`,
|
|
16
|
+
`agent_type: ${strategy === 'agent_type' ? roleName.replace(/\s+/g, '-') : '-'}`,
|
|
17
|
+
`message role prefix: ${strategy === 'message-role' ? `Role: ${roleName}.` : '-'}`,
|
|
13
18
|
`owner files: ${node.owner_scope.files.join(', ') || '-'}`,
|
|
14
19
|
`owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
|
|
15
20
|
`gates: ${gates.join(', ') || '-'}`,
|
|
@@ -21,6 +26,10 @@ export function routeNarutoLoopWorker(node, role) {
|
|
|
21
26
|
allowed_files: node.owner_scope.files,
|
|
22
27
|
allowed_directories: node.owner_scope.directories,
|
|
23
28
|
gates,
|
|
29
|
+
agent_role_strategy: strategy,
|
|
30
|
+
agent_type: strategy === 'agent_type' ? roleName.replace(/\s+/g, '-') : null,
|
|
31
|
+
message_role_prefix: strategy === 'message-role' ? `Role: ${roleName}.` : null,
|
|
32
|
+
execution_profile_artifact: profile?.artifact_path || null,
|
|
24
33
|
mutation_outside_owner_scope_allowed: false
|
|
25
34
|
};
|
|
26
35
|
}
|
package/dist/core/qa-loop.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { exists, nowIso, readJson, readText, writeJsonAtomic, writeTextAtomic, PACKAGE_VERSION } from './fsx.js';
|
|
3
3
|
import { CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE, CODEX_WEB_VERIFICATION_POLICY, evidenceMentionsForbiddenBrowserAutomation, evidenceMentionsForbiddenWebComputerUseEvidence } from './routes.js';
|
|
4
4
|
import { appendAgentLedgerEvent, initializeAgentCentralLedger } from './agents/agent-central-ledger.js';
|
|
5
|
+
import { resolveCodexAppExecutionProfile } from './codex-app/codex-app-execution-profile.js';
|
|
5
6
|
export const QA_LOOP_ROUTE = 'QALoop';
|
|
6
7
|
const QA_REPORT_SUFFIX = 'qa-report.md';
|
|
7
8
|
const UI_CHROME_EXTENSION_FIRST_ACK = 'use_codex_chrome_extension_first_no_computer_use_for_web_ui_or_mark_unverified';
|
|
@@ -320,6 +321,10 @@ export function defaultQaGate(contract = {}, opts = {}) {
|
|
|
320
321
|
image_artifact_path_contract_present: false,
|
|
321
322
|
image_artifact_path_contract_artifact: null,
|
|
322
323
|
image_artifact_path_contract_blockers: [],
|
|
324
|
+
codex_app_execution_profile: opts.executionProfile ? compactExecutionProfile(opts.executionProfile) : null,
|
|
325
|
+
codex_app_execution_profile_artifact: opts.executionProfile ? 'qa-loop/execution-profile.json' : null,
|
|
326
|
+
codex_app_hooks_approval_required: opts.executionProfile?.hooks_approval_required === true,
|
|
327
|
+
codex_app_agent_role_strategy: opts.executionProfile?.agent_role_strategy || null,
|
|
323
328
|
api_e2e_required: apiRequired,
|
|
324
329
|
unsafe_external_side_effects: false,
|
|
325
330
|
corrective_loop_enabled: corrective,
|
|
@@ -338,16 +343,21 @@ export async function writeQaLoopArtifacts(dir, mission, contract) {
|
|
|
338
343
|
const a = contract.answers || {};
|
|
339
344
|
const checklist = qaChecklist(a);
|
|
340
345
|
const reportFile = qaReportFilename();
|
|
346
|
+
const root = missionRootFromDir(dir);
|
|
347
|
+
const executionProfile = root ? await resolveCodexAppExecutionProfile({ root }).catch(() => null) : null;
|
|
348
|
+
if (executionProfile)
|
|
349
|
+
await writeJsonAtomic(path.join(dir, 'qa-loop', 'execution-profile.json'), executionProfile).catch(() => undefined);
|
|
341
350
|
await writeJsonAtomic(path.join(dir, 'qa-ledger.json'), {
|
|
342
351
|
schema_version: 1,
|
|
343
352
|
generated_at: nowIso(),
|
|
344
353
|
mission_id: mission.id,
|
|
345
354
|
qa_report_file: reportFile,
|
|
355
|
+
codex_app_execution_profile: executionProfile ? compactExecutionProfile(executionProfile) : null,
|
|
346
356
|
target: { scope: a.QA_SCOPE, environment: a.TARGET_ENVIRONMENT, base_url: a.TARGET_BASE_URL, api_base_url: a.API_BASE_URL },
|
|
347
357
|
safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: 'codex_chrome_extension_first_required_for_web_ui_e2e' },
|
|
348
358
|
checklist
|
|
349
359
|
});
|
|
350
|
-
await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile }));
|
|
360
|
+
await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile, executionProfile }));
|
|
351
361
|
await writeTextAtomic(path.join(dir, reportFile), qaReportTemplate(mission, contract, checklist));
|
|
352
362
|
return { checklist_count: checklist.length, report_file: reportFile };
|
|
353
363
|
}
|
|
@@ -432,6 +442,10 @@ export async function writeMockQaResult(dir, mission, contract) {
|
|
|
432
442
|
image_artifact_path_contract_present: previousGate.image_artifact_path_contract_present === true,
|
|
433
443
|
image_artifact_path_contract_artifact: previousGate.image_artifact_path_contract_artifact || null,
|
|
434
444
|
image_artifact_path_contract_blockers: previousGate.image_artifact_path_contract_blockers || [],
|
|
445
|
+
codex_app_execution_profile: previousGate.codex_app_execution_profile || null,
|
|
446
|
+
codex_app_execution_profile_artifact: previousGate.codex_app_execution_profile_artifact || null,
|
|
447
|
+
codex_app_hooks_approval_required: previousGate.codex_app_hooks_approval_required === true,
|
|
448
|
+
codex_app_agent_role_strategy: previousGate.codex_app_agent_role_strategy || null,
|
|
435
449
|
blockers: previousGate.blockers || [],
|
|
436
450
|
passed: !uiRequired,
|
|
437
451
|
qa_report_written: true,
|
|
@@ -453,7 +467,7 @@ export async function writeMockQaResult(dir, mission, contract) {
|
|
|
453
467
|
});
|
|
454
468
|
return evaluateQaGate(dir);
|
|
455
469
|
}
|
|
456
|
-
export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
|
|
470
|
+
export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff, executionProfile }) {
|
|
457
471
|
const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
|
|
458
472
|
const imageContractText = imagePathContract
|
|
459
473
|
? `\nIMAGE PATH CONTRACT:\n${JSON.stringify(imagePathContract, null, 2)}\nUse model_visible_path values for follow-up image edits; do not invent generated image paths.\n`
|
|
@@ -461,6 +475,9 @@ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, repo
|
|
|
461
475
|
const appHandoffText = appHandoff
|
|
462
476
|
? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
|
|
463
477
|
: '';
|
|
478
|
+
const executionProfileText = executionProfile
|
|
479
|
+
? `\nCODEX APP EXECUTION PROFILE:\n${JSON.stringify(compactExecutionProfile(executionProfile), null, 2)}\nUse this routing profile for agent role strategy and app/headless assumptions.\n`
|
|
480
|
+
: '';
|
|
464
481
|
return `SKS QA-LOOP
|
|
465
482
|
MISSION: ${id}
|
|
466
483
|
TASK: ${mission.prompt}
|
|
@@ -473,7 +490,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
|
|
|
473
490
|
ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
|
|
474
491
|
CONTRACT:
|
|
475
492
|
${JSON.stringify(contract, null, 2)}
|
|
476
|
-
${imageContractText}${appHandoffText}
|
|
493
|
+
${imageContractText}${appHandoffText}${executionProfileText}
|
|
477
494
|
Previous tail:
|
|
478
495
|
${String(previous || '').slice(-2500)}
|
|
479
496
|
`;
|
|
@@ -486,7 +503,8 @@ export async function qaStatus(dir) {
|
|
|
486
503
|
const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
|
|
487
504
|
const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
|
|
488
505
|
const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
|
|
489
|
-
|
|
506
|
+
const executionProfile = await readJson(path.join(dir, 'qa-loop', 'execution-profile.json'), null);
|
|
507
|
+
return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, desktop_app_confirmation: appConfirmation, desktop_review_complete: appConfirmation?.verdict === 'pass', image_path_contract: imagePathContract, codex_app_execution_profile: executionProfile };
|
|
490
508
|
}
|
|
491
509
|
function qaChecklist(a) {
|
|
492
510
|
const cases = [
|
|
@@ -504,6 +522,23 @@ function qaChecklist(a) {
|
|
|
504
522
|
cases.push(['report.evidence', 'Record pass/fail/blocked/skipped with evidence.'], ['report.corrective_loop', 'Record fixes, rechecks, unresolved findings, deferred blockers.'], ['report.honest', 'Run Honest Mode.']);
|
|
505
523
|
return cases.map(([id, title]) => ({ id, title, status: 'pending', evidence: [] }));
|
|
506
524
|
}
|
|
525
|
+
function missionRootFromDir(dir) {
|
|
526
|
+
const normalized = path.resolve(String(dir || ''));
|
|
527
|
+
const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
|
|
528
|
+
const idx = normalized.indexOf(marker);
|
|
529
|
+
return idx > 0 ? normalized.slice(0, idx) : null;
|
|
530
|
+
}
|
|
531
|
+
function compactExecutionProfile(profile) {
|
|
532
|
+
return profile ? {
|
|
533
|
+
mode: profile.mode || 'unknown',
|
|
534
|
+
agent_role_strategy: profile.agent_role_strategy || 'message-role',
|
|
535
|
+
hooks_approval_required: profile.hooks_approval_required === true,
|
|
536
|
+
hook_approval_state: profile.hook_approval_state || 'unknown',
|
|
537
|
+
app_handoff_ready: profile.app_handoff_ready === true,
|
|
538
|
+
plugin_mcp_inventory_ready: profile.plugin_mcp_inventory_ready === true,
|
|
539
|
+
artifact_path: profile.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json'
|
|
540
|
+
} : null;
|
|
541
|
+
}
|
|
507
542
|
function qaReportTemplate(mission, contract, checklist) {
|
|
508
543
|
const a = contract.answers || {};
|
|
509
544
|
return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence: ${CODEX_WEB_VERIFICATION_POLICY}\n\n## Checklist\n\n${checklist.map((item) => `- [ ] ${item.id}: ${item.title}`).join('\n')}\n\n## Findings\n\nTBD\n\n## Corrections And Rechecks\n\nTBD\n\n## Honest Mode\n\nTBD\n`;
|
|
@@ -63,6 +63,7 @@ export async function runResearchCycle(inputOrDir, legacyGraph = null, legacyOpt
|
|
|
63
63
|
blockers: [...new Set(blockers)],
|
|
64
64
|
stages: stageResults.map((stage) => stage.stage_id),
|
|
65
65
|
stage_results: stageResults,
|
|
66
|
+
codex_app_execution_profile: input.plan?.codex_app_execution_profile || null,
|
|
66
67
|
parallelism: {
|
|
67
68
|
max_parallel_stages: maxParallel,
|
|
68
69
|
max_observed_parallel: maxObservedParallel,
|
|
@@ -91,7 +91,7 @@ async function runSourceShardStage(input, startedAt) {
|
|
|
91
91
|
const validation = validateResearchSourceShardOutput(output);
|
|
92
92
|
await writeJsonAtomic(path.join(input.dir, artifact), output);
|
|
93
93
|
await writeTextAtomic(path.join(input.dir, 'research', `cycle-${input.cycle}`, 'source-notes', `${layer.id}.md`), `# Source shard: ${layer.label}\n\n${output.sources.map((source) => `- ${source.id}: ${source.title}`).join('\n')}\n`);
|
|
94
|
-
return baseResult(input, startedAt, 'source_shard', validation.ok ? 'passed' : 'blocked', [artifact, `research/cycle-${input.cycle}/source-notes/${layer.id}.md`], validation.blockers, { layer_id: layer.id, source_count: output.sources.length });
|
|
94
|
+
return baseResult(input, startedAt, 'source_shard', validation.ok ? 'passed' : 'blocked', [artifact, `research/cycle-${input.cycle}/source-notes/${layer.id}.md`], validation.blockers, { layer_id: layer.id, source_count: output.sources.length, source_tool_route: researchSourceToolRoute(input.plan) });
|
|
95
95
|
}
|
|
96
96
|
const codex = await runResearchCodexStage({
|
|
97
97
|
root: input.root,
|
|
@@ -333,9 +333,16 @@ function baseResult(input, startedAt, stageKind, status, outputArtifacts, blocke
|
|
|
333
333
|
backend: input.backend,
|
|
334
334
|
worker_result_path: typeof metrics.worker_result_path === 'string' ? metrics.worker_result_path : null,
|
|
335
335
|
blockers: [...new Set(blockers.map(String).filter(Boolean))],
|
|
336
|
-
metrics
|
|
336
|
+
metrics: {
|
|
337
|
+
...metrics,
|
|
338
|
+
codex_app_execution_profile: input.plan?.codex_app_execution_profile || null,
|
|
339
|
+
source_tool_route: metrics.source_tool_route || researchSourceToolRoute(input.plan)
|
|
340
|
+
}
|
|
337
341
|
};
|
|
338
342
|
}
|
|
343
|
+
function researchSourceToolRoute(plan) {
|
|
344
|
+
return plan?.web_research_policy?.source_tool_routing?.mode || (plan?.codex_app_execution_profile?.plugin_mcp_inventory_ready ? 'plugin-mcp-inventory-first' : 'codex-cli-or-web-fallback');
|
|
345
|
+
}
|
|
339
346
|
async function buildResearchGateSeed(dir, plan) {
|
|
340
347
|
const sourceLedger = await readJson(path.join(dir, 'source-ledger.json'), null);
|
|
341
348
|
const claimMatrix = await readJson(path.join(dir, 'claim-evidence-matrix.json'), null);
|
package/dist/core/research.js
CHANGED
|
@@ -17,6 +17,7 @@ import { writeResearchHandoffArtifacts } from './research/research-handoff.js';
|
|
|
17
17
|
import { RESEARCH_WORK_GRAPH_ARTIFACT, writeResearchWorkGraph } from './research/research-work-graph.js';
|
|
18
18
|
import { researchPromptContractText, validateResearchPromptContract } from './research/research-prompt-contract.js';
|
|
19
19
|
import { buildRealisticResearchPaper, buildRealisticResearchReport } from './research/research-realistic-report.js';
|
|
20
|
+
import { resolveCodexAppExecutionProfile } from './codex-app/codex-app-execution-profile.js';
|
|
20
21
|
export const RESEARCH_PAPER_ARTIFACT = 'research-paper.md';
|
|
21
22
|
export const RESEARCH_SOURCE_SKILL_ARTIFACT = 'research-source-skill.md';
|
|
22
23
|
export const RESEARCH_GENIUS_SUMMARY_ARTIFACT = 'genius-opinion-summary.md';
|
|
@@ -252,6 +253,7 @@ export function createResearchPlan(prompt, opts = {}) {
|
|
|
252
253
|
const createdAt = nowIso();
|
|
253
254
|
const paperArtifact = researchPaperArtifactName(prompt, createdAt, opts);
|
|
254
255
|
const nativeAgentPlan = researchNativeAgentPlan(prompt, { paperArtifact, missionId: opts.missionId });
|
|
256
|
+
const executionProfile = opts.executionProfile || null;
|
|
255
257
|
return {
|
|
256
258
|
schema_version: 1,
|
|
257
259
|
mission_id: opts.missionId || null,
|
|
@@ -262,6 +264,7 @@ export function createResearchPlan(prompt, opts = {}) {
|
|
|
262
264
|
paper_artifact: paperArtifact,
|
|
263
265
|
quality_contract: DEFAULT_RESEARCH_QUALITY_CONTRACT,
|
|
264
266
|
native_agent_plan: nativeAgentPlan,
|
|
267
|
+
codex_app_execution_profile: executionProfile ? compactExecutionProfile(executionProfile) : null,
|
|
265
268
|
agent_sessions: nativeAgentPlan.personas,
|
|
266
269
|
agent_batches: nativeAgentPlan.batches,
|
|
267
270
|
autoresearch_cycle_policy: nativeAgentPlan.autoresearch_cycle_policy,
|
|
@@ -318,6 +321,12 @@ export function createResearchPlan(prompt, opts = {}) {
|
|
|
318
321
|
web_research_policy: {
|
|
319
322
|
mode: 'layered_source_retrieval_and_triangulation',
|
|
320
323
|
requirement: 'Use every safely available public web/source route before synthesis, separated into source layers so the final claim is not dominated by one corpus or platform.',
|
|
324
|
+
source_tool_routing: {
|
|
325
|
+
mode: executionProfile?.plugin_mcp_inventory_ready ? 'plugin-mcp-inventory-first' : 'codex-cli-or-web-fallback',
|
|
326
|
+
plugin_mcp_inventory_ready: executionProfile?.plugin_mcp_inventory_ready === true,
|
|
327
|
+
execution_profile_artifact: executionProfile?.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json',
|
|
328
|
+
rule: 'Prefer verified plugin/MCP inventory when available; otherwise record source-tool blockers instead of assuming live search coverage.'
|
|
329
|
+
},
|
|
321
330
|
query_sets: [
|
|
322
331
|
'first-principles and theory sources',
|
|
323
332
|
'plain-language explanations and empirical examples',
|
|
@@ -417,6 +426,9 @@ export function researchPlanMarkdown(plan) {
|
|
|
417
426
|
lines.push(`Depth: ${plan.depth}`);
|
|
418
427
|
lines.push(`Methodology: ${plan.methodology}`);
|
|
419
428
|
lines.push(`Research paper: ${researchPaperArtifactForPlan(plan)}`);
|
|
429
|
+
if (plan.codex_app_execution_profile) {
|
|
430
|
+
lines.push(`Execution profile: ${plan.codex_app_execution_profile.mode}; agent role strategy ${plan.codex_app_execution_profile.agent_role_strategy}`);
|
|
431
|
+
}
|
|
420
432
|
if (plan.execution_policy) {
|
|
421
433
|
lines.push(`Execution: ${plan.execution_policy.normal_run}; default cycle timeout ${plan.execution_policy.default_cycle_timeout_minutes} minutes`);
|
|
422
434
|
if (plan.execution_policy.default_max_cycles)
|
|
@@ -468,6 +480,8 @@ export function researchPlanMarkdown(plan) {
|
|
|
468
480
|
lines.push('## Web Research Policy');
|
|
469
481
|
lines.push(`Mode: ${plan.web_research_policy.mode}`);
|
|
470
482
|
lines.push(`Requirement: ${plan.web_research_policy.requirement}`);
|
|
483
|
+
if (plan.web_research_policy.source_tool_routing)
|
|
484
|
+
lines.push(`Source tool routing: ${plan.web_research_policy.source_tool_routing.mode}`);
|
|
471
485
|
for (const querySet of plan.web_research_policy.query_sets || [])
|
|
472
486
|
lines.push(`- query set: ${querySet}`);
|
|
473
487
|
if (plan.web_research_policy.skill_creator?.artifact)
|
|
@@ -537,7 +551,9 @@ export function countGeniusOpinionSummaries(text = '') {
|
|
|
537
551
|
}).length;
|
|
538
552
|
}
|
|
539
553
|
export async function writeResearchPlan(dir, prompt, opts = {}) {
|
|
540
|
-
const
|
|
554
|
+
const root = opts.root || missionRootFromDir(String(dir || ''));
|
|
555
|
+
const executionProfile = opts.executionProfile || (root ? await resolveCodexAppExecutionProfile({ root }).catch(() => null) : null);
|
|
556
|
+
const plan = createResearchPlan(prompt, { ...opts, executionProfile });
|
|
541
557
|
const noveltyLedger = {
|
|
542
558
|
schema_version: 1,
|
|
543
559
|
entries: [],
|
|
@@ -553,6 +569,8 @@ export async function writeResearchPlan(dir, prompt, opts = {}) {
|
|
|
553
569
|
const experimentPlan = defaultExperimentPlan(plan);
|
|
554
570
|
const replicationPack = defaultReplicationPack(plan);
|
|
555
571
|
await writeJsonAtomic(path.join(dir, 'research-plan.json'), plan);
|
|
572
|
+
if (executionProfile)
|
|
573
|
+
await writeJsonAtomic(path.join(dir, 'research', 'execution-profile.json'), executionProfile).catch(() => undefined);
|
|
556
574
|
await writeTextAtomic(path.join(dir, 'research-plan.md'), researchPlanMarkdown(plan));
|
|
557
575
|
await writeTextAtomic(path.join(dir, RESEARCH_SOURCE_SKILL_ARTIFACT), researchSourceSkillMarkdown(plan));
|
|
558
576
|
await writeResearchQualityContract(dir, plan.quality_contract);
|
|
@@ -575,6 +593,22 @@ export async function writeResearchPlan(dir, prompt, opts = {}) {
|
|
|
575
593
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.plan.created', depth: plan.depth });
|
|
576
594
|
return plan;
|
|
577
595
|
}
|
|
596
|
+
function missionRootFromDir(dir) {
|
|
597
|
+
const normalized = path.resolve(String(dir || ''));
|
|
598
|
+
const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
|
|
599
|
+
const idx = normalized.indexOf(marker);
|
|
600
|
+
return idx > 0 ? normalized.slice(0, idx) : null;
|
|
601
|
+
}
|
|
602
|
+
function compactExecutionProfile(profile) {
|
|
603
|
+
return profile ? {
|
|
604
|
+
mode: profile.mode || 'unknown',
|
|
605
|
+
agent_role_strategy: profile.agent_role_strategy || 'message-role',
|
|
606
|
+
hooks_approval_required: profile.hooks_approval_required === true,
|
|
607
|
+
hook_approval_state: profile.hook_approval_state || 'unknown',
|
|
608
|
+
plugin_mcp_inventory_ready: profile.plugin_mcp_inventory_ready === true,
|
|
609
|
+
artifact_path: profile.artifact_path || '.sneakoscope/reports/codex-app-execution-profile.json'
|
|
610
|
+
} : null;
|
|
611
|
+
}
|
|
578
612
|
export async function writeResearchNativeAgentLedger(dir, plan, opts = {}) {
|
|
579
613
|
const missionId = opts.missionId || plan?.mission_id;
|
|
580
614
|
if (!missionId)
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.1.
|
|
1
|
+
export const PACKAGE_VERSION = '3.1.5';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { nowIso } from '../fsx.js';
|
|
2
|
+
export function isZellijSelfHealResult(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4
|
+
return false;
|
|
5
|
+
const row = value;
|
|
6
|
+
return row.schema === 'sks.zellij-self-heal.v1'
|
|
7
|
+
&& typeof row.ok === 'boolean'
|
|
8
|
+
&& typeof row.requested_by === 'string'
|
|
9
|
+
&& typeof row.strategy === 'string'
|
|
10
|
+
&& row.before !== null
|
|
11
|
+
&& typeof row.before === 'object'
|
|
12
|
+
&& row.after !== null
|
|
13
|
+
&& typeof row.after === 'object'
|
|
14
|
+
&& Array.isArray(row.blockers)
|
|
15
|
+
&& Array.isArray(row.warnings);
|
|
16
|
+
}
|
|
17
|
+
export function normalizeZellijSelfHealResult(value) {
|
|
18
|
+
if (isZellijSelfHealResult(value)) {
|
|
19
|
+
return {
|
|
20
|
+
...value,
|
|
21
|
+
dry_run: value.dry_run === true,
|
|
22
|
+
planned_mutations: Array.isArray(value.planned_mutations) ? value.planned_mutations : []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
schema: 'sks.zellij-self-heal.v1',
|
|
27
|
+
ok: false,
|
|
28
|
+
requested_by: 'setup',
|
|
29
|
+
fix_requested: false,
|
|
30
|
+
auto_approved: false,
|
|
31
|
+
install_homebrew_allowed: false,
|
|
32
|
+
dry_run: false,
|
|
33
|
+
planned_mutations: [],
|
|
34
|
+
before: { status: 'unknown', version: null, bin: null },
|
|
35
|
+
latest_version: null,
|
|
36
|
+
strategy: 'failed',
|
|
37
|
+
command: null,
|
|
38
|
+
after: { status: 'unknown', version: null, bin: null },
|
|
39
|
+
mutation_guard_artifact: null,
|
|
40
|
+
homebrew: { present: false, bin: null, install_attempted: false, install_allowed: false },
|
|
41
|
+
blockers: ['invalid_zellij_self_heal_result'],
|
|
42
|
+
warnings: [`normalized_at:${nowIso()}`]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=zellij-self-heal-types.js.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import fs from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { ensureDir, nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
@@ -12,6 +11,7 @@ export async function repairZellijForSks(input) {
|
|
|
12
11
|
const root = path.resolve(input.root || process.cwd());
|
|
13
12
|
const env = input.env || process.env;
|
|
14
13
|
const autoApproved = input.autoApprove === true;
|
|
14
|
+
const dryRun = input.dryRun === true;
|
|
15
15
|
const beforeReport = await capabilitySnapshot(root, env, 'before');
|
|
16
16
|
const before = compactCapability(beforeReport);
|
|
17
17
|
const latest = await latestZellijVersion(env);
|
|
@@ -41,6 +41,9 @@ export async function repairZellijForSks(input) {
|
|
|
41
41
|
if (input.fixRequested !== true) {
|
|
42
42
|
return manualResult(root, input, env, before, latest, brew, 'fix_not_requested');
|
|
43
43
|
}
|
|
44
|
+
if (dryRun) {
|
|
45
|
+
return dryRunResult(root, input, env, before, latest, brew, mutationArtifact);
|
|
46
|
+
}
|
|
44
47
|
if (!autoApproved && input.interactive !== true) {
|
|
45
48
|
return input.allowHeadlessFallback === true
|
|
46
49
|
? headlessResult(root, input, before, latest, brew, 'noninteractive_without_auto_approval')
|
|
@@ -183,10 +186,10 @@ async function capabilitySnapshot(root, env, phase, fallbackVersion) {
|
|
|
183
186
|
require_zellij: false,
|
|
184
187
|
min_version: ZELLIJ_MIN_VERSION,
|
|
185
188
|
version: null,
|
|
186
|
-
bin:
|
|
189
|
+
bin: 'zellij',
|
|
187
190
|
command: ['zellij', '--version'],
|
|
188
191
|
docs_evidence: [],
|
|
189
|
-
blockers: [`zellij_capability_check_failed:${tail(
|
|
192
|
+
blockers: [`zellij_capability_check_failed:${tail(errorMessage(err))}`],
|
|
190
193
|
warnings: [],
|
|
191
194
|
operator_actions: ['Resolve the Zellij capability check failure, then rerun `sks doctor --fix --yes`.']
|
|
192
195
|
}));
|
|
@@ -261,7 +264,7 @@ async function runZellijBrew(root, env, brewBin, args, command) {
|
|
|
261
264
|
env,
|
|
262
265
|
timeoutMs: 180000,
|
|
263
266
|
maxOutputBytes: 256 * 1024
|
|
264
|
-
}).catch((err) => ({ code: 1, stdout: '', stderr:
|
|
267
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: errorMessage(err) }));
|
|
265
268
|
}
|
|
266
269
|
async function runHomebrewInstall(root, env) {
|
|
267
270
|
if (env.SKS_ZELLIJ_SELF_HEAL_FAKE_RUN === '1') {
|
|
@@ -281,7 +284,7 @@ async function runHomebrewInstall(root, env) {
|
|
|
281
284
|
env,
|
|
282
285
|
timeoutMs: 600000,
|
|
283
286
|
maxOutputBytes: 256 * 1024
|
|
284
|
-
}).catch((err) => ({ code: 1, stdout: '', stderr:
|
|
287
|
+
}).catch((err) => ({ code: 1, stdout: '', stderr: errorMessage(err) }));
|
|
285
288
|
}
|
|
286
289
|
async function appendFakeBrewLog(env, args) {
|
|
287
290
|
if (!env.SKS_FAKE_BREW_LOG)
|
|
@@ -289,6 +292,56 @@ async function appendFakeBrewLog(env, args) {
|
|
|
289
292
|
await ensureDir(path.dirname(env.SKS_FAKE_BREW_LOG));
|
|
290
293
|
await fs.appendFile(env.SKS_FAKE_BREW_LOG, `${args.join(' ')}\n`, 'utf8');
|
|
291
294
|
}
|
|
295
|
+
async function dryRunResult(root, input, env, before, latest, brew, mutationArtifact) {
|
|
296
|
+
const policy = !brew.present
|
|
297
|
+
? resolveHomebrewInstallPolicy({
|
|
298
|
+
env,
|
|
299
|
+
installHomebrew: input.installHomebrew === true,
|
|
300
|
+
autoApprove: input.autoApprove === true,
|
|
301
|
+
interactiveAccepted: false
|
|
302
|
+
})
|
|
303
|
+
: null;
|
|
304
|
+
if (!brew.present && policy?.allowed !== true) {
|
|
305
|
+
return input.allowHeadlessFallback === true
|
|
306
|
+
? headlessResult(root, input, before, latest, brew, policy?.blockers[0] || 'homebrew_missing')
|
|
307
|
+
: manualResult(root, input, env, before, latest, brew, policy?.blockers[0] || 'homebrew_missing');
|
|
308
|
+
}
|
|
309
|
+
const planned = [];
|
|
310
|
+
if (!brew.present) {
|
|
311
|
+
planned.push({
|
|
312
|
+
command: HOMEBREW_INSTALL_COMMAND,
|
|
313
|
+
reason: 'homebrew_missing_for_zellij_repair'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const install = before.status === 'missing';
|
|
317
|
+
const zellijCommand = install ? 'brew install zellij' : 'brew upgrade zellij';
|
|
318
|
+
planned.push({
|
|
319
|
+
command: zellijCommand,
|
|
320
|
+
reason: install ? 'zellij_missing' : 'zellij_too_old_or_stale'
|
|
321
|
+
});
|
|
322
|
+
const strategy = !brew.present ? 'brew-install-homebrew-then-zellij'
|
|
323
|
+
: install ? 'brew-install-zellij'
|
|
324
|
+
: 'brew-upgrade-zellij';
|
|
325
|
+
return persistSelfHeal(root, input.missionDir, {
|
|
326
|
+
schema: 'sks.zellij-self-heal.v1',
|
|
327
|
+
ok: true,
|
|
328
|
+
requested_by: input.requestedBy,
|
|
329
|
+
fix_requested: true,
|
|
330
|
+
auto_approved: input.autoApprove === true,
|
|
331
|
+
install_homebrew_allowed: policy?.allowed === true,
|
|
332
|
+
dry_run: true,
|
|
333
|
+
planned_mutations: planned,
|
|
334
|
+
before,
|
|
335
|
+
latest_version: latest,
|
|
336
|
+
strategy,
|
|
337
|
+
command: planned.map((row) => row.command).join(' && '),
|
|
338
|
+
after: before,
|
|
339
|
+
mutation_guard_artifact: `${mutationArtifact}#planned`,
|
|
340
|
+
homebrew: { present: brew.present, bin: brew.bin, install_attempted: false, install_allowed: policy?.allowed === true },
|
|
341
|
+
blockers: [],
|
|
342
|
+
warnings: ['dry_run_no_mutation_performed']
|
|
343
|
+
});
|
|
344
|
+
}
|
|
292
345
|
async function manualResult(root, input, env, before, latest, brew, reason) {
|
|
293
346
|
const command = brew.present ? 'sks doctor --fix --yes' : 'sks doctor --fix --install-homebrew --yes';
|
|
294
347
|
return persistSelfHeal(root, input.missionDir, {
|
|
@@ -329,10 +382,15 @@ async function headlessResult(root, input, before, latest, brew, reason) {
|
|
|
329
382
|
});
|
|
330
383
|
}
|
|
331
384
|
async function persistSelfHeal(root, missionDir, result) {
|
|
332
|
-
|
|
385
|
+
const normalized = {
|
|
386
|
+
...result,
|
|
387
|
+
dry_run: result.dry_run === true,
|
|
388
|
+
planned_mutations: result.planned_mutations || []
|
|
389
|
+
};
|
|
390
|
+
await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'zellij-self-heal.json'), normalized).catch(() => undefined);
|
|
333
391
|
if (missionDir)
|
|
334
|
-
await writeJsonAtomic(path.join(missionDir, 'zellij-self-heal.json'),
|
|
335
|
-
return
|
|
392
|
+
await writeJsonAtomic(path.join(missionDir, 'zellij-self-heal.json'), normalized).catch(() => undefined);
|
|
393
|
+
return normalized;
|
|
336
394
|
}
|
|
337
395
|
async function askZellijRepairAllowed(question) {
|
|
338
396
|
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
@@ -347,6 +405,9 @@ async function askZellijRepairAllowed(question) {
|
|
|
347
405
|
rl.close();
|
|
348
406
|
}
|
|
349
407
|
}
|
|
408
|
+
function errorMessage(err) {
|
|
409
|
+
return err instanceof Error ? err.message : String(err);
|
|
410
|
+
}
|
|
350
411
|
function tail(value, limit = 1000) {
|
|
351
412
|
return String(value || '').replace(/\s+/g, ' ').trim().slice(-limit);
|
|
352
413
|
}
|
|
@@ -232,17 +232,25 @@ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
|
|
|
232
232
|
interactive: mode === 'interactive-prompt',
|
|
233
233
|
installHomebrew: opts.installHomebrew === true || list.includes('--install-homebrew'),
|
|
234
234
|
allowHeadlessFallback: opts.allowHeadlessFallback === true,
|
|
235
|
+
dryRun: opts.dryRun === true || list.includes('--dry-run'),
|
|
235
236
|
missionDir: opts.missionDir || null,
|
|
236
237
|
env
|
|
237
238
|
});
|
|
238
239
|
if (repaired.strategy === 'headless-fallback')
|
|
239
240
|
console.log('Zellij repair: headless fallback selected (live_panes=false).');
|
|
241
|
+
else if (repaired.dry_run)
|
|
242
|
+
console.log(`Zellij repair: dry_run planned ${repaired.command || 'none'}`);
|
|
240
243
|
else if (repaired.ok && repaired.command)
|
|
241
244
|
console.log(`Zellij repair: ${repaired.strategy} via ${repaired.command}`);
|
|
242
245
|
else if (!repaired.ok)
|
|
243
246
|
console.log(`Zellij repair required. Run: ${repaired.command || notice.upgrade_command}`);
|
|
247
|
+
const repairedStatus = repaired.strategy === 'headless-fallback' ? 'headless_fallback'
|
|
248
|
+
: !repaired.ok || repaired.strategy === 'manual-required' ? 'repair_required'
|
|
249
|
+
: repaired.strategy === 'brew-upgrade-zellij' ? 'upgraded'
|
|
250
|
+
: repaired.strategy === 'none-current' ? 'noop'
|
|
251
|
+
: 'installed';
|
|
244
252
|
return {
|
|
245
|
-
status:
|
|
253
|
+
status: repairedStatus,
|
|
246
254
|
current: repaired.after.version || repaired.before.version,
|
|
247
255
|
latest: repaired.latest_version,
|
|
248
256
|
command: repaired.command,
|