sneakoscope 2.0.17 → 2.0.18
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 +19 -28
- 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/commands/doctor.js +39 -1
- package/dist/core/agents/agent-effort-policy.js +7 -1
- package/dist/core/codex-app/codex-app-handoff.js +77 -0
- package/dist/core/codex-control/codex-0138-capability.js +64 -0
- package/dist/core/codex-control/codex-model-capabilities.js +41 -0
- package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
- package/dist/core/codex-control/codex-task-runner.js +1 -1
- package/dist/core/codex-plugins/codex-plugin-json.js +152 -0
- package/dist/core/commands/mad-sks-command.js +4 -0
- package/dist/core/commands/naruto-command.js +3 -1
- package/dist/core/commands/qa-loop-command.js +111 -4
- package/dist/core/doctor/codex-0138-doctor.js +104 -0
- package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
- package/dist/core/effort-orchestrator.js +9 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime.js +6 -9
- package/dist/core/image/image-artifact-path-contract.js +99 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +24 -3
- package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
- package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
- package/dist/core/mcp/mcp-server-policy.js +24 -0
- package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
- package/dist/core/qa-loop.js +28 -2
- package/dist/core/usage/codex-account-usage.js +78 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
- package/dist/core/zellij/zellij-slot-pane-renderer.js +18 -0
- package/dist/scripts/release-gate-existence-audit.js +5 -1
- package/package.json +25 -2
- package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
- package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
- package/schemas/image/image-artifact-path-contract.schema.json +32 -0
- package/schemas/usage/codex-account-usage.schema.json +27 -0
|
@@ -74,6 +74,24 @@ export async function recordMadDbToolResult(input) {
|
|
|
74
74
|
event
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
export async function maybeRecordMadDbToolResultFromToolUse(input) {
|
|
78
|
+
const payload = input.toolResult ?? input.toolCallPayload ?? {};
|
|
79
|
+
const hook = lifecycleHookFromUnknown(input.decision)
|
|
80
|
+
|| lifecycleHookFromUnknown(input.toolCallPayload)
|
|
81
|
+
|| lifecycleHookFromUnknown(input.toolResult)
|
|
82
|
+
|| await readLatestPendingMadDbLifecycleHook(input.root, input.missionId, input.toolCallPayload || payload);
|
|
83
|
+
if (!hook)
|
|
84
|
+
return null;
|
|
85
|
+
const ok = !madDbToolUseFailed(payload);
|
|
86
|
+
return recordMadDbToolResult({
|
|
87
|
+
root: input.root,
|
|
88
|
+
missionId: input.missionId,
|
|
89
|
+
hook,
|
|
90
|
+
ok,
|
|
91
|
+
rowCount: extractRowCount(payload),
|
|
92
|
+
error: ok ? null : extractToolError(payload)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
77
95
|
export function lifecycleHookFromUnknown(value) {
|
|
78
96
|
const candidate = value?.ledger_result_hook || value?.mad_db?.ledger_result_hook || value;
|
|
79
97
|
const missionId = stringOrNull(candidate?.mission_id || candidate?.missionId);
|
|
@@ -107,6 +125,59 @@ function hookMatchesPayload(hook, payload) {
|
|
|
107
125
|
return true;
|
|
108
126
|
return toolText.includes(String(hook.tool_name).toLowerCase()) || String(hook.tool_name).toLowerCase().includes(toolText);
|
|
109
127
|
}
|
|
128
|
+
function madDbToolUseFailed(payload = {}) {
|
|
129
|
+
if (payload?.isError === true || payload?.tool_response?.isError === true || payload?.toolResponse?.isError === true || payload?.result?.isError === true)
|
|
130
|
+
return true;
|
|
131
|
+
const candidates = [
|
|
132
|
+
payload.exit_code,
|
|
133
|
+
payload.exitCode,
|
|
134
|
+
payload.tool_response?.exit_code,
|
|
135
|
+
payload.toolResponse?.exitCode,
|
|
136
|
+
payload.result?.exit_code,
|
|
137
|
+
payload.result?.exitCode
|
|
138
|
+
];
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
if (candidate === undefined || candidate === null || candidate === '')
|
|
141
|
+
continue;
|
|
142
|
+
const n = Number(candidate);
|
|
143
|
+
if (Number.isFinite(n))
|
|
144
|
+
return n !== 0;
|
|
145
|
+
}
|
|
146
|
+
if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false)
|
|
147
|
+
return true;
|
|
148
|
+
if (payload.executed === false)
|
|
149
|
+
return true;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function extractRowCount(payload = {}) {
|
|
153
|
+
const candidates = [
|
|
154
|
+
payload.row_count,
|
|
155
|
+
payload.rowCount,
|
|
156
|
+
payload.tool_response?.row_count,
|
|
157
|
+
payload.tool_response?.rowCount,
|
|
158
|
+
payload.toolResponse?.rowCount,
|
|
159
|
+
payload.result?.row_count,
|
|
160
|
+
payload.result?.rowCount,
|
|
161
|
+
payload.result?.rows_affected,
|
|
162
|
+
payload.tool_response?.rows_affected
|
|
163
|
+
];
|
|
164
|
+
for (const candidate of candidates) {
|
|
165
|
+
if (candidate === undefined || candidate === null || candidate === '')
|
|
166
|
+
continue;
|
|
167
|
+
const parsed = Number(candidate);
|
|
168
|
+
if (Number.isFinite(parsed))
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function extractToolError(payload = {}) {
|
|
174
|
+
if (payload?.result?.isError === true && Array.isArray(payload.result.content)) {
|
|
175
|
+
const text = payload.result.content.map((entry) => entry?.text || entry?.message || '').filter(Boolean).join('\n');
|
|
176
|
+
if (text.trim())
|
|
177
|
+
return text.trim();
|
|
178
|
+
}
|
|
179
|
+
return String(payload.error || payload.message || payload.stderr || payload.tool_response?.stderr || payload.toolResponse?.stderr || payload.result?.stderr || payload.result?.error || 'tool_failed');
|
|
180
|
+
}
|
|
110
181
|
async function hasTerminalLifecycleEvent(root, missionId, operationId) {
|
|
111
182
|
const ledger = path.join(missionDir(root, missionId), 'mad-db-ledger.jsonl');
|
|
112
183
|
const text = await readText(ledger, '').catch(() => '');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildCodexPluginInventory } from '../codex-plugins/codex-plugin-json.js';
|
|
3
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { policyForPluginMcpServer } from './mcp-server-policy.js';
|
|
5
|
+
export function buildMcpPluginServerCandidates(inventory) {
|
|
6
|
+
const candidates = inventory.plugins.flatMap((plugin) => plugin.remote_mcp_servers.map((server) => policyForPluginMcpServer({
|
|
7
|
+
pluginId: plugin.id,
|
|
8
|
+
name: server.name,
|
|
9
|
+
url: server.url,
|
|
10
|
+
authType: server.auth_type
|
|
11
|
+
})));
|
|
12
|
+
return {
|
|
13
|
+
schema: 'sks.mcp-plugin-server-candidates.v1',
|
|
14
|
+
generated_at: nowIso(),
|
|
15
|
+
candidates,
|
|
16
|
+
candidate_only: true,
|
|
17
|
+
blockers: []
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function writeMcpPluginInventoryArtifacts(root, input = {}) {
|
|
21
|
+
const inventory = input.inventory || await buildCodexPluginInventory();
|
|
22
|
+
const candidates = buildMcpPluginServerCandidates(inventory);
|
|
23
|
+
const pluginArtifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.json');
|
|
24
|
+
const candidateArtifact = path.join(root, '.sneakoscope', 'mcp-plugin-server-candidates.json');
|
|
25
|
+
await writeJsonAtomic(pluginArtifact, inventory);
|
|
26
|
+
await writeJsonAtomic(candidateArtifact, candidates);
|
|
27
|
+
return { inventory, candidates, plugin_artifact: pluginArtifact, candidate_artifact: candidateArtifact };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mcp-plugin-inventory.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function policyForPluginMcpServer(input) {
|
|
2
|
+
const haystack = `${input.name} ${input.url || ''}`.toLowerCase();
|
|
3
|
+
const dbRelated = /supabase|postgres|database|sql|db\b/.test(haystack);
|
|
4
|
+
const oauth = /oauth/i.test(String(input.authType || ''));
|
|
5
|
+
return {
|
|
6
|
+
name: input.name,
|
|
7
|
+
plugin_id: input.pluginId,
|
|
8
|
+
url: input.url || null,
|
|
9
|
+
auth_type: input.authType || null,
|
|
10
|
+
candidate_only: true,
|
|
11
|
+
auto_enable: false,
|
|
12
|
+
destructive_tools_auto_enabled: false,
|
|
13
|
+
db_safety_required: dbRelated,
|
|
14
|
+
mad_db_required_for_destructive: dbRelated,
|
|
15
|
+
oauth_prerefresh_recommended: oauth,
|
|
16
|
+
policy_notes: [
|
|
17
|
+
'Remote MCP servers from plugin detail are candidate only.',
|
|
18
|
+
'Do not auto-enable destructive MCP tools.',
|
|
19
|
+
dbRelated ? 'DB MCP servers require DB safety and Mad-DB for destructive operations.' : 'Non-DB MCP candidate still requires explicit operator enablement.',
|
|
20
|
+
oauth ? 'OAuth-backed MCP should trigger pre-refresh doctor check.' : ''
|
|
21
|
+
].filter(Boolean)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=mcp-server-policy.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defaultModelCallBudget } from '../codex-control/model-call-concurrency.js';
|
|
2
|
+
import { codexModelEffortCapability, nextAdvertisedEffort } from '../codex-control/codex-model-capabilities.js';
|
|
3
|
+
export function buildQaLoopBudgetPolicy(input = {}) {
|
|
4
|
+
const usage = input.usage || null;
|
|
5
|
+
const available = Boolean(usage?.token_usage);
|
|
6
|
+
const limit = Number(usage?.usage_limit_tokens || 0);
|
|
7
|
+
const total = Number(usage?.token_usage?.total_tokens || 0);
|
|
8
|
+
const nearLimit = Boolean(limit > 0 && total / limit >= 0.9);
|
|
9
|
+
const baseBudget = defaultModelCallBudget(String(input.provider || 'codex-sdk'));
|
|
10
|
+
return {
|
|
11
|
+
schema: 'sks.qa-loop-budget-policy.v1',
|
|
12
|
+
ok: true,
|
|
13
|
+
account_usage_source: usage?.source || 'unavailable',
|
|
14
|
+
token_usage_available: available,
|
|
15
|
+
near_limit: nearLimit,
|
|
16
|
+
remote_model_call_concurrency: nearLimit ? Math.max(1, Math.min(2, baseBudget)) : baseBudget,
|
|
17
|
+
local_llm_draft_preferred: nearLimit,
|
|
18
|
+
final_reviewer_gpt_backed: true,
|
|
19
|
+
warnings: available ? [] : ['codex_account_usage_unavailable_no_hard_block']
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function selectQaLoopEscalatedEffort(input = {}) {
|
|
23
|
+
const capability = input.capability || codexModelEffortCapability();
|
|
24
|
+
const current = input.currentEffort || capability.default_effort;
|
|
25
|
+
const failureCount = Number(input.failureCount || 0);
|
|
26
|
+
return {
|
|
27
|
+
schema: 'sks.qa-loop-effort-escalation.v1',
|
|
28
|
+
model: capability.model,
|
|
29
|
+
advertised_efforts: capability.advertised_efforts,
|
|
30
|
+
order_source: capability.order_source,
|
|
31
|
+
failure_count: failureCount,
|
|
32
|
+
current_effort: current,
|
|
33
|
+
next_effort: failureCount >= 2 ? nextAdvertisedEffort(current, capability) : current,
|
|
34
|
+
escalated: failureCount >= 2 && nextAdvertisedEffort(current, capability) !== current
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=qa-loop-budget-policy.js.map
|
package/dist/core/qa-loop.js
CHANGED
|
@@ -309,6 +309,14 @@ export function defaultQaGate(contract = {}, opts = {}) {
|
|
|
309
309
|
ui_chrome_extension_evidence: !uiRequired,
|
|
310
310
|
ui_computer_use_evidence: false,
|
|
311
311
|
ui_evidence_source: uiRequired ? null : 'not_required',
|
|
312
|
+
desktop_app_handoff_required: false,
|
|
313
|
+
desktop_app_handoff_status: 'not_requested',
|
|
314
|
+
desktop_app_handoff_artifact: null,
|
|
315
|
+
desktop_app_handoff_supported: false,
|
|
316
|
+
desktop_app_handoff_is_web_ui_evidence: false,
|
|
317
|
+
image_artifact_path_contract_present: false,
|
|
318
|
+
image_artifact_path_contract_artifact: null,
|
|
319
|
+
image_artifact_path_contract_blockers: [],
|
|
312
320
|
api_e2e_required: apiRequired,
|
|
313
321
|
unsafe_external_side_effects: false,
|
|
314
322
|
corrective_loop_enabled: corrective,
|
|
@@ -374,6 +382,15 @@ export async function evaluateQaGate(dir) {
|
|
|
374
382
|
if (evidenceMentionsForbiddenWebComputerUseEvidence({ evidence: gate.evidence, ui_evidence_source: gate.ui_evidence_source }))
|
|
375
383
|
reasons.push('computer_use_web_evidence_forbidden');
|
|
376
384
|
}
|
|
385
|
+
if (gate.desktop_app_handoff_required === true) {
|
|
386
|
+
if (gate.desktop_app_handoff_status !== 'pending' && gate.desktop_app_handoff_status !== 'completed')
|
|
387
|
+
reasons.push('desktop_app_handoff_missing');
|
|
388
|
+
if (gate.desktop_app_handoff_is_web_ui_evidence === true)
|
|
389
|
+
reasons.push('desktop_app_handoff_misused_as_web_evidence');
|
|
390
|
+
}
|
|
391
|
+
const imageBlockers = Array.isArray(gate.image_artifact_path_contract_blockers) ? gate.image_artifact_path_contract_blockers : [];
|
|
392
|
+
if (imageBlockers.includes('image_generated_file_path_missing'))
|
|
393
|
+
reasons.push('image_generated_file_path_missing');
|
|
377
394
|
if (!reportFile)
|
|
378
395
|
reasons.push('qa_report_file_missing');
|
|
379
396
|
else if (!isQaReportFilename(reportFile))
|
|
@@ -396,8 +413,14 @@ export async function writeMockQaResult(dir, mission, contract) {
|
|
|
396
413
|
await writeJsonAtomic(path.join(dir, 'qa-gate.json'), { ...defaultQaGate(contract, { reportFile }), passed: !uiRequired, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, chrome_extension_preflight_passed: !uiRequired, ui_chrome_extension_evidence: !uiRequired, ui_computer_use_evidence: false, ui_evidence_source: uiRequired ? null : 'not_required', unresolved_findings: 0, unresolved_fixable_findings: 0, unsafe_or_deferred_findings: 0, post_fix_verification_complete: true, honest_mode_complete: true, evidence: ['mock QA-LOOP smoke completed'], notes: ['No live UI/API verification was claimed.'] });
|
|
397
414
|
return evaluateQaGate(dir);
|
|
398
415
|
}
|
|
399
|
-
export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile }) {
|
|
416
|
+
export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
|
|
400
417
|
const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
|
|
418
|
+
const imageContractText = imagePathContract
|
|
419
|
+
? `\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`
|
|
420
|
+
: '';
|
|
421
|
+
const appHandoffText = appHandoff
|
|
422
|
+
? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
|
|
423
|
+
: '';
|
|
401
424
|
return `SKS QA-LOOP
|
|
402
425
|
MISSION: ${id}
|
|
403
426
|
TASK: ${mission.prompt}
|
|
@@ -410,6 +433,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
|
|
|
410
433
|
ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
|
|
411
434
|
CONTRACT:
|
|
412
435
|
${JSON.stringify(contract, null, 2)}
|
|
436
|
+
${imageContractText}${appHandoffText}
|
|
413
437
|
Previous tail:
|
|
414
438
|
${String(previous || '').slice(-2500)}
|
|
415
439
|
`;
|
|
@@ -417,9 +441,11 @@ ${String(previous || '').slice(-2500)}
|
|
|
417
441
|
export async function qaStatus(dir) {
|
|
418
442
|
const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
|
|
419
443
|
const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
|
|
444
|
+
const appHandoff = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
|
|
445
|
+
const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
|
|
420
446
|
const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
|
|
421
447
|
const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
|
|
422
|
-
return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()) };
|
|
448
|
+
return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, image_path_contract: imagePathContract };
|
|
423
449
|
}
|
|
424
450
|
function qaChecklist(a) {
|
|
425
451
|
const cases = [
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
export async function collectCodexAccountUsage() {
|
|
4
|
+
if (process.env.SKS_CODEX_ACCOUNT_USAGE_FAKE === '1') {
|
|
5
|
+
return {
|
|
6
|
+
schema: 'sks.codex-account-usage.v1',
|
|
7
|
+
generated_at: nowIso(),
|
|
8
|
+
ok: true,
|
|
9
|
+
source: 'fake',
|
|
10
|
+
account_id: 'fake-account',
|
|
11
|
+
token_usage: {
|
|
12
|
+
input_tokens: 1000,
|
|
13
|
+
output_tokens: 500,
|
|
14
|
+
total_tokens: 1500,
|
|
15
|
+
reset_at: null
|
|
16
|
+
},
|
|
17
|
+
usage_limit_tokens: 100000,
|
|
18
|
+
blockers: []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const url = String(process.env.SKS_CODEX_APP_SERVER_USAGE_URL || process.env.CODEX_APP_SERVER_USAGE_URL || '').trim();
|
|
22
|
+
if (!url)
|
|
23
|
+
return unavailable(['codex_app_server_usage_endpoint_unavailable']);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
return unavailable([`codex_app_server_usage_http_${response.status}`]);
|
|
28
|
+
const payload = await response.json();
|
|
29
|
+
return normalizeUsagePayload(payload, 'app-server');
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return unavailable([`codex_app_server_usage_fetch_failed:${err?.message || String(err)}`]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function writeCodexAccountUsageArtifacts(root, input = {}) {
|
|
36
|
+
const snapshot = await collectCodexAccountUsage();
|
|
37
|
+
const rootArtifact = path.join(root, '.sneakoscope', 'codex-account-usage.json');
|
|
38
|
+
await writeJsonAtomic(rootArtifact, snapshot);
|
|
39
|
+
let missionArtifact = null;
|
|
40
|
+
if (input.missionId) {
|
|
41
|
+
missionArtifact = path.join(root, '.sneakoscope', 'missions', input.missionId, 'codex-account-usage.json');
|
|
42
|
+
await writeJsonAtomic(missionArtifact, snapshot);
|
|
43
|
+
}
|
|
44
|
+
return { snapshot, root_artifact: rootArtifact, mission_artifact: missionArtifact };
|
|
45
|
+
}
|
|
46
|
+
function normalizeUsagePayload(payload, source) {
|
|
47
|
+
const usage = payload?.token_usage || payload?.usage || payload;
|
|
48
|
+
const input = Number(usage?.input_tokens || usage?.inputTokens || 0);
|
|
49
|
+
const output = Number(usage?.output_tokens || usage?.outputTokens || 0);
|
|
50
|
+
const total = Number(usage?.total_tokens || usage?.totalTokens || input + output);
|
|
51
|
+
return {
|
|
52
|
+
schema: 'sks.codex-account-usage.v1',
|
|
53
|
+
generated_at: nowIso(),
|
|
54
|
+
ok: true,
|
|
55
|
+
source,
|
|
56
|
+
account_id: payload?.account_id || payload?.accountId || null,
|
|
57
|
+
token_usage: {
|
|
58
|
+
input_tokens: Number.isFinite(input) ? input : 0,
|
|
59
|
+
output_tokens: Number.isFinite(output) ? output : 0,
|
|
60
|
+
total_tokens: Number.isFinite(total) ? total : 0,
|
|
61
|
+
reset_at: usage?.reset_at || usage?.resetAt || null
|
|
62
|
+
},
|
|
63
|
+
usage_limit_tokens: Number.isFinite(Number(payload?.usage_limit_tokens || payload?.usageLimitTokens)) ? Number(payload?.usage_limit_tokens || payload?.usageLimitTokens) : null,
|
|
64
|
+
blockers: []
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function unavailable(blockers) {
|
|
68
|
+
return {
|
|
69
|
+
schema: 'sks.codex-account-usage.v1',
|
|
70
|
+
generated_at: nowIso(),
|
|
71
|
+
ok: true,
|
|
72
|
+
source: 'unavailable',
|
|
73
|
+
token_usage: null,
|
|
74
|
+
usage_limit_tokens: null,
|
|
75
|
+
blockers
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=codex-account-usage.js.map
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '2.0.
|
|
1
|
+
export const PACKAGE_VERSION = '2.0.18';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -10,18 +10,21 @@ export function renderZellijSlotColumnAnchor(input = {}) {
|
|
|
10
10
|
const fail = nonNegativeInt(input.failedWorkers, 0);
|
|
11
11
|
const update = input.updateAvailableVersion ? ` · update ${trimInline(input.updateAvailableVersion, 18)} available` : '';
|
|
12
12
|
const madDb = input.madDbActive ? ' · MAD-DB ACTIVE' : '';
|
|
13
|
+
const appHandoff = input.qaAppHandoffPending ? ' · QA /app handoff pending' : '';
|
|
13
14
|
const header = done || fail
|
|
14
|
-
? `SLOTS active ${active} · headless ${headless} · done ${done} · fail ${fail} · q ${queue}${update}${madDb}`
|
|
15
|
-
: `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}`;
|
|
15
|
+
? `SLOTS active ${active} · headless ${headless} · done ${done} · fail ${fail} · q ${queue}${update}${madDb}${appHandoff}`
|
|
16
|
+
: `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}${appHandoff}`;
|
|
16
17
|
const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
|
|
18
|
+
const handoffLine = input.qaAppHandoffPending ? `QA app handoff pending · ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 64)}` : null;
|
|
17
19
|
if (!workers.length)
|
|
18
|
-
return
|
|
20
|
+
return [header, handoffLine, 'visible slot panes stack below this anchor'].filter(Boolean).join('\n');
|
|
19
21
|
const maxRows = Math.max(1, nonNegativeInt(input.maxWorkerRows, input.mode === 'full-debug' ? 24 : 12));
|
|
20
22
|
const overflowRows = workers.filter((row) => row.placement === 'headless').slice(0, maxRows);
|
|
21
23
|
const visibleRows = overflowRows.length ? overflowRows : workers.filter((row) => row.placement !== 'zellij-pane').slice(0, maxRows);
|
|
22
24
|
const hidden = Math.max(0, workers.length - visibleRows.length);
|
|
23
25
|
return [
|
|
24
26
|
header,
|
|
27
|
+
handoffLine,
|
|
25
28
|
`visible slot panes stack below this anchor`,
|
|
26
29
|
...visibleRows.map((row, index) => renderWorkerRow(row, index + 1)),
|
|
27
30
|
...(hidden && visibleRows.length ? [`+${hidden} worker${hidden === 1 ? '' : 's'} in dedicated panes or overflow`] : [])
|
|
@@ -33,8 +36,9 @@ export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
|
|
|
33
36
|
const telemetry = await readZellijSlotTelemetrySnapshot(root, input.missionId).catch(() => null);
|
|
34
37
|
const updateNotice = await readJson(path.join(missionDir, 'update-notice.json'));
|
|
35
38
|
const madDb = await readJson(path.join(missionDir, 'mad-db-capability.json'));
|
|
39
|
+
const appHandoff = await readJson(path.join(missionDir, 'qa-loop', 'app-handoff.json'));
|
|
36
40
|
if (telemetry && Object.keys(telemetry.slots || {}).length) {
|
|
37
|
-
return renderTelemetryAnchor(telemetry, updateNotice, madDb);
|
|
41
|
+
return renderTelemetryAnchor(telemetry, updateNotice, madDb, appHandoff);
|
|
38
42
|
}
|
|
39
43
|
const snapshot = await readJson(path.join(missionDir, 'zellij-dashboard-snapshot.json'));
|
|
40
44
|
const rightColumn = await readJson(path.join(missionDir, 'zellij-right-column-state.json'));
|
|
@@ -50,20 +54,25 @@ export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
|
|
|
50
54
|
anchorInput.updateAvailableVersion = String(updateNotice.latest_version);
|
|
51
55
|
if (isMadDbActive(madDb))
|
|
52
56
|
anchorInput.madDbActive = true;
|
|
57
|
+
if (['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || ''))) {
|
|
58
|
+
anchorInput.qaAppHandoffPending = true;
|
|
59
|
+
anchorInput.qaAppHandoffArtifact = appHandoff?.artifact_path || 'qa-loop/app-handoff.json';
|
|
60
|
+
}
|
|
53
61
|
if (input.mode !== undefined)
|
|
54
62
|
anchorInput.mode = input.mode;
|
|
55
63
|
return renderZellijSlotColumnAnchor(anchorInput);
|
|
56
64
|
}
|
|
57
|
-
function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability = null) {
|
|
65
|
+
function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability = null, appHandoff = null) {
|
|
58
66
|
const updatedAt = Date.parse(snapshot.updated_at || '');
|
|
59
67
|
const staleSeconds = Number.isFinite(updatedAt) ? Math.max(0, Math.round((Date.now() - updatedAt) / 1000)) : null;
|
|
60
68
|
const counts = snapshot.counts || {};
|
|
61
69
|
const active = Number(counts.running || 0) + Number(counts.verifying || 0);
|
|
62
70
|
const update = updateNotice?.update_available && updateNotice?.latest_version ? ` · update ${trimInline(String(updateNotice.latest_version), 18)} available` : '';
|
|
63
71
|
const madDb = isMadDbActive(madDbCapability) ? ' · MAD-DB ACTIVE' : '';
|
|
72
|
+
const qaHandoff = ['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || '')) ? ' · QA /app handoff pending' : '';
|
|
64
73
|
if (staleSeconds != null && staleSeconds > 10)
|
|
65
|
-
return `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}`;
|
|
66
|
-
return `SLOTS active ${active} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}`;
|
|
74
|
+
return `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}${qaHandoff}`;
|
|
75
|
+
return `SLOTS active ${active} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}${qaHandoff}`;
|
|
67
76
|
}
|
|
68
77
|
function isMadDbActive(capability) {
|
|
69
78
|
if (!capability || capability.enabled !== true || capability.consumed === true)
|
|
@@ -24,6 +24,7 @@ export function renderZellijSlotPane(input) {
|
|
|
24
24
|
`doing: ${task}`,
|
|
25
25
|
`files: ${trimInline(files.length ? files.join(', ') : 'no changed file yet', 78)}`,
|
|
26
26
|
`patch: ${trimInline(input.patchStatus || 'queued', 24)} verify: ${trimInline(input.verifyStatus || 'queued', 24)}`,
|
|
27
|
+
input.qaAppHandoffPending ? `QA app handoff pending: ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 55)}` : null,
|
|
27
28
|
...events.map((event) => `event: ${trimInline(event, 78)}`),
|
|
28
29
|
...stdout.map((line) => `out: ${trimInline(line, 79)}`),
|
|
29
30
|
...stderr.map((line) => `err: ${trimInline(line, 79)}`)
|
|
@@ -97,6 +98,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
|
|
|
97
98
|
...(Array.isArray(intake?.input_files) ? intake.input_files : [])
|
|
98
99
|
]);
|
|
99
100
|
const now = Date.now();
|
|
101
|
+
const qaAppHandoff = await readQaAppHandoffNearArtifactDir(artifactDir);
|
|
100
102
|
if (!result && !intake && !backendReport && !fastReport && !paneReport && !codexProof && !localProof && !heartbeatMtime && !eventRows.length)
|
|
101
103
|
return null;
|
|
102
104
|
return renderZellijSlotPane({
|
|
@@ -155,9 +157,25 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
|
|
|
155
157
|
eventLines: eventRows.map(formatArtifactEvent).filter(Boolean),
|
|
156
158
|
stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'), 2),
|
|
157
159
|
stderrTail: await readTextTailLines(path.join(artifactDir, 'worker.stderr.log'), 1),
|
|
160
|
+
qaAppHandoffPending: ['pending', 'blocked_for_desktop_review'].includes(String(qaAppHandoff?.status || '')),
|
|
161
|
+
qaAppHandoffArtifact: qaAppHandoff?.artifact_path || null,
|
|
158
162
|
mode: input.mode || 'compact-slots'
|
|
159
163
|
});
|
|
160
164
|
}
|
|
165
|
+
async function readQaAppHandoffNearArtifactDir(artifactDir) {
|
|
166
|
+
let current = path.resolve(artifactDir);
|
|
167
|
+
for (let i = 0; i < 8; i += 1) {
|
|
168
|
+
const candidate = path.join(current, 'qa-loop', 'app-handoff.json');
|
|
169
|
+
const json = await readJson(candidate);
|
|
170
|
+
if (json)
|
|
171
|
+
return json;
|
|
172
|
+
const next = path.dirname(current);
|
|
173
|
+
if (next === current)
|
|
174
|
+
break;
|
|
175
|
+
current = next;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
161
179
|
export async function renderZellijSlotPaneStatusFromArtifacts(input) {
|
|
162
180
|
const snapshot = input.missionId && input.missionId !== 'latest'
|
|
163
181
|
? await readZellijSlotTelemetrySnapshot(path.resolve(input.artifactRoot || input.artifactDir), input.missionId).catch(() => null)
|
|
@@ -84,7 +84,11 @@ const required = [
|
|
|
84
84
|
'git-collaboration:e2e'
|
|
85
85
|
];
|
|
86
86
|
assertGate(releaseManifest.schema === 'sks.release-gates.v2', 'release gate manifest schema mismatch', { schema: releaseManifest.schema });
|
|
87
|
-
|
|
87
|
+
const releaseCheck = String(scripts['release:check'] || '');
|
|
88
|
+
const releaseCheckTarget = releaseCheck.includes('release:check:affected')
|
|
89
|
+
? String(scripts['release:check:affected'] || '')
|
|
90
|
+
: releaseCheck;
|
|
91
|
+
assertGate(releaseCheckTarget.includes('release-gate-dag-runner') && /--preset\s+(?:release|affected)/.test(releaseCheckTarget), 'release:check must use the v2 DAG release/affected preset', { release_check: scripts['release:check'], resolved_release_check: releaseCheckTarget });
|
|
88
92
|
assertGate(releaseGates.length > 0, 'release v2 manifest must include release preset gates', { gate_count: releaseGates.length });
|
|
89
93
|
for (const name of required) {
|
|
90
94
|
assertGate(Boolean(scripts[name]), `missing release gate script: ${name}`, { required });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.18",
|
|
5
5
|
"description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -557,6 +557,29 @@
|
|
|
557
557
|
"release:check:dag:explain": "node ./dist/scripts/release-gate-dag-runner.js --preset release --explain",
|
|
558
558
|
"release:check:dag:no-cache": "node ./dist/scripts/release-gate-dag-runner.js --preset release --no-cache",
|
|
559
559
|
"release:check:dag:fail-fast": "node ./dist/scripts/release-gate-dag-runner.js --preset release --fail-fast",
|
|
560
|
+
"codex:0138-capability": "node ./dist/scripts/codex-0138-capability-check.js",
|
|
561
|
+
"codex:0138-capability-artifact": "node ./dist/scripts/codex-0138-capability-artifact-check.js",
|
|
562
|
+
"codex-sdk:version-compat": "node ./dist/scripts/codex-sdk-version-compat-check.js",
|
|
563
|
+
"codex-app:handoff": "node ./dist/scripts/codex-app-handoff-check.js",
|
|
564
|
+
"qa-loop:app-handoff": "node ./dist/scripts/qa-loop-app-handoff-check.js",
|
|
565
|
+
"qa-loop:app-handoff-capability": "node ./dist/scripts/qa-loop-app-handoff-capability-check.js",
|
|
566
|
+
"qa-loop:app-handoff-cli": "node ./dist/scripts/qa-loop-app-handoff-cli-check.js",
|
|
567
|
+
"zellij:qa-app-handoff-status": "node ./dist/scripts/zellij-qa-app-handoff-status-check.js",
|
|
568
|
+
"codex-plugin:json": "node ./dist/scripts/codex-plugin-json-check.js",
|
|
569
|
+
"codex-plugin:inventory": "node ./dist/scripts/codex-plugin-inventory-check.js",
|
|
570
|
+
"mcp:plugin-inventory": "node ./dist/scripts/mcp-plugin-inventory-check.js",
|
|
571
|
+
"codex-plugin:app-template-policy": "node ./dist/scripts/codex-plugin-app-template-policy-check.js",
|
|
572
|
+
"image:artifact-path-contract": "node ./dist/scripts/image-artifact-path-contract-check.js",
|
|
573
|
+
"qa-loop:image-path-exposure": "node ./dist/scripts/qa-loop-image-path-exposure-check.js",
|
|
574
|
+
"image:generation-path-handoff": "node ./dist/scripts/image-generation-path-handoff-check.js",
|
|
575
|
+
"image:followup-edit-path": "node ./dist/scripts/image-followup-edit-path-check.js",
|
|
576
|
+
"codex:effort-order": "node ./dist/scripts/codex-effort-order-check.js",
|
|
577
|
+
"qa-loop:effort-escalation": "node ./dist/scripts/qa-loop-effort-escalation-check.js",
|
|
578
|
+
"codex:account-usage": "node ./dist/scripts/codex-account-usage-check.js",
|
|
579
|
+
"qa-loop:budget-policy": "node ./dist/scripts/qa-loop-budget-policy-check.js",
|
|
580
|
+
"naruto:parallel-gate-consistency": "node ./dist/scripts/naruto-parallel-gate-consistency-check.js",
|
|
581
|
+
"codex:0138-doctor": "node ./dist/scripts/codex-0138-doctor-check.js",
|
|
582
|
+
"doctor:codex-0138-fix": "node ./dist/scripts/doctor-codex-0138-fix-check.js",
|
|
560
583
|
"release:dag-runner": "node ./dist/scripts/release-gate-dag-runner-check.js",
|
|
561
584
|
"release:parallel-speed-budget": "node ./dist/scripts/release-parallel-speed-budget-check.js",
|
|
562
585
|
"release:stability-report": "node ./dist/scripts/release-stability-report-check.js",
|
|
@@ -711,7 +734,7 @@
|
|
|
711
734
|
"license": "MIT",
|
|
712
735
|
"dependencies": {
|
|
713
736
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
714
|
-
"@openai/codex-sdk": "0.
|
|
737
|
+
"@openai/codex-sdk": "^0.138.0",
|
|
715
738
|
"figlet": "^1.11.0",
|
|
716
739
|
"typescript": "^5.9.3"
|
|
717
740
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sneakoscope.dev/schemas/codex-app/codex-app-handoff.schema.json",
|
|
4
|
+
"title": "SKS Codex App Handoff",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema", "ok", "status", "desktop_handoff_supported", "artifact_path", "prompt_artifact_path"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema": { "const": "sks.codex-app-handoff-result.v1" },
|
|
9
|
+
"ok": { "type": "boolean" },
|
|
10
|
+
"attempted": { "type": "boolean" },
|
|
11
|
+
"launched": { "type": "boolean" },
|
|
12
|
+
"status": { "enum": ["pending", "skipped", "blocked_for_desktop_review"] },
|
|
13
|
+
"desktop_handoff_supported": { "type": "boolean" },
|
|
14
|
+
"artifact_path": { "type": "string", "minLength": 1 },
|
|
15
|
+
"prompt_artifact_path": { "type": "string", "minLength": 1 },
|
|
16
|
+
"blockers": { "type": "array", "items": { "type": "string" } },
|
|
17
|
+
"request": { "type": "object" }
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": true
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sneakoscope.dev/schemas/codex-plugins/codex-plugin-inventory.schema.json",
|
|
4
|
+
"title": "SKS Codex Plugin Inventory",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema", "generated_at", "plugins", "marketplace_available", "blockers"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema": { "const": "sks.codex-plugin-inventory.v1" },
|
|
9
|
+
"generated_at": { "type": "string" },
|
|
10
|
+
"marketplace_available": { "type": "boolean" },
|
|
11
|
+
"plugins": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["id", "name", "source", "installed", "enabled", "default_prompts", "remote_mcp_servers", "unavailable_app_templates"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": { "type": "string", "minLength": 1 },
|
|
18
|
+
"name": { "type": "string" },
|
|
19
|
+
"source": { "enum": ["marketplace", "local", "remote", "unknown"] },
|
|
20
|
+
"installed": { "type": "boolean" },
|
|
21
|
+
"enabled": { "type": "boolean" },
|
|
22
|
+
"default_prompts": { "type": "array", "items": { "type": "string" } },
|
|
23
|
+
"remote_mcp_servers": { "type": "array", "items": { "type": "object" } },
|
|
24
|
+
"unavailable_app_templates": { "type": "array", "items": { "type": "string" } }
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"blockers": { "type": "array", "items": { "type": "string" } }
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": true
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sneakoscope.dev/schemas/image/image-artifact-path-contract.schema.json",
|
|
4
|
+
"title": "SKS Image Artifact Path Contract",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema", "mission_id", "generated_at", "images", "blockers"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema": { "const": "sks.image-artifact-path-contract.v1" },
|
|
9
|
+
"mission_id": { "type": "string", "minLength": 1 },
|
|
10
|
+
"generated_at": { "type": "string" },
|
|
11
|
+
"images": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["id", "kind", "file_path", "relative_path", "exists", "model_visible_path", "followup_edit_hint"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": { "type": "string", "minLength": 1 },
|
|
18
|
+
"kind": { "enum": ["input_attachment", "generated_image", "edited_image", "visual_qa_snapshot"] },
|
|
19
|
+
"file_path": { "type": "string", "minLength": 1 },
|
|
20
|
+
"relative_path": { "type": "string" },
|
|
21
|
+
"exists": { "type": "boolean" },
|
|
22
|
+
"mime_type": { "type": ["string", "null"] },
|
|
23
|
+
"model_visible_path": { "type": "string", "minLength": 1 },
|
|
24
|
+
"followup_edit_hint": { "type": "string", "minLength": 1 }
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"blockers": { "type": "array", "items": { "type": "string" } }
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": true
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sneakoscope.dev/schemas/usage/codex-account-usage.schema.json",
|
|
4
|
+
"title": "SKS Codex Account Usage",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["schema", "generated_at", "ok", "source", "blockers"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema": { "const": "sks.codex-account-usage.v1" },
|
|
9
|
+
"generated_at": { "type": "string" },
|
|
10
|
+
"ok": { "type": "boolean" },
|
|
11
|
+
"source": { "enum": ["app-server", "unavailable", "fake"] },
|
|
12
|
+
"account_id": { "type": ["string", "null"] },
|
|
13
|
+
"token_usage": {
|
|
14
|
+
"type": ["object", "null"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"input_tokens": { "type": "number" },
|
|
17
|
+
"output_tokens": { "type": "number" },
|
|
18
|
+
"total_tokens": { "type": "number" },
|
|
19
|
+
"reset_at": { "type": ["string", "null"] }
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": true
|
|
22
|
+
},
|
|
23
|
+
"usage_limit_tokens": { "type": ["number", "null"] },
|
|
24
|
+
"blockers": { "type": "array", "items": { "type": "string" } }
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": true
|
|
27
|
+
}
|