sneakoscope 2.0.13 → 2.0.14

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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/core/agents/agent-orchestrator.js +11 -4
  8. package/dist/core/agents/agent-output-validator.js +1 -1
  9. package/dist/core/codex-control/codex-fake-sdk-adapter.js +3 -3
  10. package/dist/core/codex-control/gpt-final-review-schema.js +61 -14
  11. package/dist/core/commands/naruto-command.js +1 -0
  12. package/dist/core/commands/research-command.js +71 -17
  13. package/dist/core/fsx.js +1 -1
  14. package/dist/core/naruto/naruto-real-worker-child.js +11 -3
  15. package/dist/core/naruto/naruto-real-worker-runtime.js +4 -0
  16. package/dist/core/pipeline/final-gpt-patch-stage.js +20 -3
  17. package/dist/core/research/implementation-blueprint-densifier.js +124 -0
  18. package/dist/core/research/research-claim-builder.js +114 -0
  19. package/dist/core/research/research-cycle-runner.js +115 -11
  20. package/dist/core/research/research-final-reviewer.js +155 -1
  21. package/dist/core/research/research-source-ledger-merge.js +186 -0
  22. package/dist/core/research/research-source-shards.js +176 -0
  23. package/dist/core/research/research-stage-runner.js +510 -11
  24. package/dist/core/research/research-work-graph.js +114 -23
  25. package/dist/core/research.js +12 -0
  26. package/dist/core/version.js +1 -1
  27. package/dist/scripts/codex-sdk-research-pipeline-check.js +40 -8
  28. package/dist/scripts/release-dag-full-coverage-check.js +14 -1
  29. package/dist/scripts/release-parallel-speed-budget-check.js +7 -2
  30. package/dist/scripts/research-blueprint-densifier-check.js +21 -0
  31. package/dist/scripts/research-claim-builder-check.js +19 -0
  32. package/dist/scripts/research-complete-package-fixture-check.js +23 -0
  33. package/dist/scripts/research-final-reviewer-blackbox.js +22 -0
  34. package/dist/scripts/research-parallel-source-shards-check.js +22 -0
  35. package/dist/scripts/research-quality-gate-check.js +28 -3
  36. package/dist/scripts/research-real-cycle-no-legacy-final-md-check.js +14 -0
  37. package/dist/scripts/research-short-report-rejection-check.js +46 -0
  38. package/dist/scripts/research-source-ledger-merge-check.js +26 -0
  39. package/dist/scripts/research-stage-cycle-runtime-blackbox.js +24 -0
  40. package/package.json +16 -1
  41. package/schemas/codex/agent-result.schema.json +1 -1
  42. package/schemas/research/research-source-shard.schema.json +46 -0
  43. package/dist/build-manifest.json +0 -1184
  44. package/dist/scripts/release-readiness-report.js +0 -1146
  45. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/permission-request.command.input.schema.json +0 -61
  46. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/permission-request.command.output.schema.json +0 -103
  47. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-compact.command.input.schema.json +0 -52
  48. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-compact.command.output.schema.json +0 -24
  49. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-tool-use.command.input.schema.json +0 -67
  50. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/post-tool-use.command.output.schema.json +0 -84
  51. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-compact.command.input.schema.json +0 -52
  52. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-compact.command.output.schema.json +0 -24
  53. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-tool-use.command.input.schema.json +0 -65
  54. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/pre-tool-use.command.output.schema.json +0 -105
  55. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/session-start.command.input.schema.json +0 -59
  56. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/session-start.command.output.schema.json +0 -63
  57. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/snapshot-metadata.json +0 -31
  58. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/stop.command.input.schema.json +0 -63
  59. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/stop.command.output.schema.json +0 -45
  60. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/user-prompt-submit.command.input.schema.json +0 -59
  61. package/dist/vendor/openai-codex/rust-v0.131.0/hooks/user-prompt-submit.command.output.schema.json +0 -81
@@ -0,0 +1,124 @@
1
+ import path from 'node:path';
2
+ import { nowIso, runProcess } from '../fsx.js';
3
+ import {} from '../codex-control/codex-control-plane.js';
4
+ import { defaultImplementationBlueprint } from './implementation-blueprint.js';
5
+ export async function densifyImplementationBlueprint(input) {
6
+ const fileMap = await repositoryFileMap(input.root);
7
+ const likelyFiles = likelyTargetFiles(fileMap, input.plan, input.claimMatrix);
8
+ const possibleNewFiles = possibleNewResearchFiles(fileMap);
9
+ const base = input.existingBlueprint || defaultImplementationBlueprint(input.plan);
10
+ const claims = Array.isArray(input.claimMatrix?.claims) ? input.claimMatrix.claims : [];
11
+ const keyClaimIds = Array.isArray(input.claimMatrix?.key_claim_ids) ? input.claimMatrix.key_claim_ids : claims.slice(0, 8).map((claim) => claim.id);
12
+ const sections = [
13
+ section('problem', 'Problem', `Research currently must prove it executes stage-aware source shard runtime instead of relying on summary-style final.md output. The handoff should preserve ${keyClaimIds.length} key claim ids and all source-ledger evidence before code work begins.`, keyClaimIds, likelyFiles.slice(0, 8), ['Confirm the follow-up route reads claim-evidence-matrix.json and source-ledger.json before implementation.']),
14
+ section('decision', 'Decision', 'Use a dependency-aware research cycle with source_shard, source_merge, claim_matrix_build, falsification, implementation_blueprint, experiment_plan, synthesis, final_review, and verification stages. Keep Research read-only against repository source and write only mission artifacts.', keyClaimIds, likelyFiles.slice(0, 8), ['Default research run calls runResearchCycle; legacy final.md loop is opt-in only.']),
15
+ section('architecture', 'Architecture', 'The runtime is split into source shard generation, source-ledger merge, claim builder, blueprint densifier, final reviewer, blackbox scripts, and CLI status output. Each stage writes a ResearchStageResult under research/cycle-N/stages.', keyClaimIds, likelyFiles, ['Stage result artifacts list concrete output_artifacts for every passed stage.']),
16
+ section('interfaces', 'API And Schema Changes', `Existing files should expose typed contracts for shard outputs, stage results, merged source ledgers, Codex final review outputs, and concrete blueprint fields. Possible new files: ${possibleNewFiles.join(', ')}.`, keyClaimIds, likelyFiles, ['Schemas exist for research-source-shard and research-final-review.']),
17
+ section('data_contracts', 'Data Contracts', 'Source rows must preserve id, layer, kind, title, locator, publisher_or_author, accessed_at, reliability, credibility, stance, and claim_ids. Claim rows must preserve source_ids, counterevidence_ids, triangulation layers, confidence, and test_or_probe.', keyClaimIds, ['schemas/research/research-source-shard.schema.json', 'schemas/research/claim-evidence-matrix.schema.json'], ['Source quality report returns ok only when source metadata and citation coverage are complete.']),
18
+ section('execution_plan', 'Step By Step Implementation', implementationSteps(likelyFiles, possibleNewFiles).join('\n'), keyClaimIds, likelyFiles, ['Run research stage runtime blackbox, short-report rejection, complete-package fixture, and codex-sdk research pipeline gates.']),
19
+ section('verification_plan', 'Verification Plan', 'Run the release truth, research quality, source shard, source merge, claim builder, blueprint densifier, final reviewer, codex-sdk research pipeline, release DAG, and release check commands listed in the directive.', keyClaimIds, ['package.json', 'release-gates.v2.json', 'src/scripts/release-dag-full-coverage-check.ts'], ['All directive final checklist commands either pass or have a documented blocker.']),
20
+ section('risks_and_rollbacks', 'Risks And Rollbacks', 'The main risk is accepting deterministic fixture text as public-ready proof. Roll back by disabling new release gates only if the gate itself is wrong, not if implementation is incomplete. Research must block when live Codex/GPT final review is unavailable outside mock fixtures.', keyClaimIds, likelyFiles, ['A rollback keeps source mutation outside Research and restores package version metadata consistently.'])
21
+ ];
22
+ return {
23
+ ...base,
24
+ schema: 'sks.research-implementation-blueprint.v1',
25
+ generated_at: nowIso(),
26
+ prompt: input.plan?.prompt || base.prompt || '',
27
+ implementation_allowed_in_research: false,
28
+ handoff_route: '$Team',
29
+ repository_aware: true,
30
+ existing_files: likelyFiles,
31
+ possible_new_files: possibleNewFiles,
32
+ api_schema_changes: [
33
+ 'ResearchStageResult contract for every executed stage.',
34
+ 'ResearchSourceShardOutput contract for source layer partials.',
35
+ 'Codex/GPT final reviewer merged with static review.'
36
+ ],
37
+ test_commands: [
38
+ 'npm run research:stage-cycle-runtime-blackbox',
39
+ 'npm run research:short-report-rejection',
40
+ 'npm run research:complete-package-fixture',
41
+ 'npm run codex-sdk:research-pipeline',
42
+ 'npm run release:check'
43
+ ],
44
+ rollback_steps: [
45
+ 'Revert only the files listed in the follow-up patch plan.',
46
+ 'Restore package version metadata with npm install --package-lock-only if package-lock drift occurs.',
47
+ 'Run npm run release:version-truth and the research blackbox gates after rollback.'
48
+ ],
49
+ parallel_work_decomposition: [
50
+ 'WS-A stage runtime and research run integration.',
51
+ 'WS-B source shards and ledger merge.',
52
+ 'WS-C claim matrix builder.',
53
+ 'WS-D blueprint and handoff densifier.',
54
+ 'WS-E final reviewer.',
55
+ 'WS-F blackbox gates and release DAG.',
56
+ 'WS-G CLI/docs.',
57
+ 'WS-H integration and verification.'
58
+ ],
59
+ sections,
60
+ dependencies: ['claim-evidence-matrix.json', 'source-ledger.json', 'falsification-ledger.json'],
61
+ out_of_scope: ['Repository source mutation during $Research runs.'],
62
+ open_questions: []
63
+ };
64
+ }
65
+ async function repositoryFileMap(root) {
66
+ const result = await runProcess('git', ['ls-files'], { cwd: root, timeoutMs: 15000, maxOutputBytes: 2 * 1024 * 1024 }).catch(() => ({ code: 1, stdout: '' }));
67
+ return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
68
+ }
69
+ function likelyTargetFiles(files, plan, claimMatrix) {
70
+ const promptTerms = new Set(String(plan?.prompt || '').toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 3));
71
+ const guidanceTerms = new Set((Array.isArray(claimMatrix?.claims) ? claimMatrix.claims : []).flatMap((claim) => String(claim?.claim || '').toLowerCase().split(/[^a-z0-9]+/)).filter((term) => term.length > 5));
72
+ const preferred = [
73
+ 'src/core/research/research-cycle-runner.ts',
74
+ 'src/core/research/research-stage-runner.ts',
75
+ 'src/core/research/research-work-graph.ts',
76
+ 'src/core/commands/research-command.ts',
77
+ 'src/core/research/claim-evidence-matrix.ts',
78
+ 'src/core/research/implementation-blueprint.ts',
79
+ 'src/core/research/research-final-reviewer.ts',
80
+ 'package.json',
81
+ 'release-gates.v2.json',
82
+ 'docs/research-pipeline.md',
83
+ 'docs/research-artifacts.md',
84
+ 'docs/research-implementation-handoff.md'
85
+ ].filter((file) => files.includes(file));
86
+ const matched = files.filter((file) => {
87
+ const lower = file.toLowerCase();
88
+ return lower.includes('research') || [...promptTerms, ...guidanceTerms].some((term) => lower.includes(term));
89
+ }).slice(0, 30);
90
+ return [...new Set([...preferred, ...matched])].slice(0, 40);
91
+ }
92
+ function possibleNewResearchFiles(files) {
93
+ return [
94
+ 'src/core/research/research-source-shards.ts',
95
+ 'src/core/research/research-source-ledger-merge.ts',
96
+ 'src/core/research/research-claim-builder.ts',
97
+ 'src/core/research/implementation-blueprint-densifier.ts',
98
+ 'src/scripts/research-stage-cycle-runtime-blackbox.ts',
99
+ 'src/scripts/research-short-report-rejection-check.ts',
100
+ 'schemas/research/research-source-shard.schema.json'
101
+ ].filter((file) => !files.includes(file));
102
+ }
103
+ function section(id, title, detail, claimIds, targetPaths, acceptanceChecks) {
104
+ return {
105
+ id,
106
+ title,
107
+ order: ['problem', 'decision', 'architecture', 'interfaces', 'data_contracts', 'execution_plan', 'verification_plan', 'risks_and_rollbacks'].indexOf(id) + 1,
108
+ detail,
109
+ evidence_claim_ids: claimIds.slice(0, 8),
110
+ target_paths: targetPaths,
111
+ acceptance_checks: acceptanceChecks
112
+ };
113
+ }
114
+ function implementationSteps(existingFiles, newFiles) {
115
+ return [
116
+ `1. Update runtime files: ${existingFiles.filter((file) => file.includes('research-cycle-runner') || file.includes('research-stage-runner') || file.includes('research-work-graph')).join(', ')}.`,
117
+ `2. Add or verify source/claim/blueprint helper files: ${newFiles.filter((file) => file.includes('research')).join(', ')}.`,
118
+ '3. Wire sks research run so default execution uses runResearchCycle and the final.md Codex exec loop is legacy-only.',
119
+ '4. Add blackbox scripts that create temporary missions and verify rejection/pass/runtime behavior.',
120
+ '5. Update package scripts, release-gates.v2.json, docs, changelog, and version metadata.',
121
+ '6. Run the directive final checklist and record any hard blocker instead of claiming completion.'
122
+ ];
123
+ }
124
+ //# sourceMappingURL=implementation-blueprint-densifier.js.map
@@ -0,0 +1,114 @@
1
+ import { normalizeClaimEvidenceMatrix } from './claim-evidence-matrix.js';
2
+ export async function buildClaimEvidenceMatrixFromSourceShards(input) {
3
+ const sources = [
4
+ ...(Array.isArray(input.sourceLedger?.sources) ? input.sourceLedger.sources : []),
5
+ ...(Array.isArray(input.sourceLedger?.counterevidence_sources) ? input.sourceLedger.counterevidence_sources : [])
6
+ ];
7
+ const noveltyEntries = Array.isArray(input.noveltyLedger?.entries) ? input.noveltyLedger.entries : [];
8
+ const candidates = new Map();
9
+ for (const entry of noveltyEntries) {
10
+ const id = String(entry?.id || '').trim();
11
+ if (!id)
12
+ continue;
13
+ candidates.set(id, {
14
+ id,
15
+ claim: String(entry?.claim || entry?.title || id),
16
+ claim_type: entry?.type === 'implementation_guidance' ? 'implementation_guidance' : 'hypothesis',
17
+ importance: candidates.size < 2 ? 'critical' : 'high',
18
+ source_ids: normalizeStringList(entry?.source_ids || entry?.evidence),
19
+ counterevidence_ids: normalizeStringList(entry?.counterevidence_ids || entry?.falsifiers),
20
+ test_or_probe: String(entry?.next_experiment || entry?.test_or_probe || '').trim()
21
+ });
22
+ }
23
+ for (const source of sources) {
24
+ const claimIds = normalizeStringList(source?.claim_ids);
25
+ if (claimIds.length) {
26
+ for (const claimId of claimIds) {
27
+ const existing = candidates.get(claimId) || {
28
+ id: claimId,
29
+ claim: claimTextFromSource(source, claimId),
30
+ claim_type: 'inference',
31
+ importance: candidates.size < 2 ? 'critical' : candidates.size < 8 ? 'high' : 'medium',
32
+ source_ids: [],
33
+ counterevidence_ids: [],
34
+ test_or_probe: ''
35
+ };
36
+ if (source?.stance === 'undermines')
37
+ existing.counterevidence_ids = [...new Set([...(existing.counterevidence_ids || []), source.id])];
38
+ else
39
+ existing.source_ids = [...new Set([...(existing.source_ids || []), source.id])];
40
+ if (!existing.test_or_probe)
41
+ existing.test_or_probe = `Probe ${claimId} against supporting and undermining source layers.`;
42
+ candidates.set(claimId, existing);
43
+ }
44
+ }
45
+ else if (String(source?.notes || '').trim()) {
46
+ const id = `hypothesis-${candidates.size + 1}`;
47
+ candidates.set(id, {
48
+ id,
49
+ claim: String(source.notes).slice(0, 240),
50
+ claim_type: 'hypothesis',
51
+ importance: 'medium',
52
+ source_ids: source?.stance === 'undermines' ? [] : [source.id].filter(Boolean),
53
+ counterevidence_ids: source?.stance === 'undermines' ? [source.id].filter(Boolean) : [],
54
+ test_or_probe: `Turn ${source.id || id} notes into a decisive source-backed probe.`
55
+ });
56
+ }
57
+ }
58
+ const falsificationCounterIds = new Set((Array.isArray(input.falsificationLedger?.cases) ? input.falsificationLedger.cases : [])
59
+ .flatMap((row) => [...normalizeStringList(row?.counterevidence_source_ids), ...normalizeStringList(row?.source_ids)]));
60
+ const claims = [...candidates.values()].slice(0, Math.max(8, candidates.size)).map((candidate, index) => {
61
+ const sourceIds = normalizeStringList(candidate.source_ids).filter((id) => sourceById(sources, id));
62
+ const counterIds = normalizeStringList(candidate.counterevidence_ids).filter((id) => sourceById(sources, id) || falsificationCounterIds.has(id));
63
+ const layers = sourceLayersForSourceIds(sources, [...sourceIds, ...counterIds]);
64
+ return {
65
+ id: candidate.id,
66
+ claim: candidate.claim,
67
+ claim_type: candidate.claim_type,
68
+ importance: candidate.importance || (index < 2 ? 'critical' : 'high'),
69
+ source_ids: sourceIds,
70
+ local_evidence_ids: sources.filter((source) => source.layer === 'local_project_evidence' && normalizeStringList(source.claim_ids).includes(candidate.id)).map((source) => source.id),
71
+ counterevidence_ids: counterIds,
72
+ triangulation: {
73
+ source_layers: layers,
74
+ independent_confirmation_count: layers.length,
75
+ conflicts: counterIds.length ? [`counterevidence:${counterIds.join(',')}`] : []
76
+ },
77
+ confidence: layers.length >= 3 && counterIds.length ? 'high' : layers.length >= 2 ? 'medium' : 'low',
78
+ falsifiable: true,
79
+ test_or_probe: candidate.test_or_probe || `Run a source-layer replication probe for ${candidate.id}.`
80
+ };
81
+ });
82
+ const unsupported = claims
83
+ .filter((claim) => {
84
+ const important = claim.importance === 'high' || claim.importance === 'critical';
85
+ return important && (claim.triangulation.source_layers.length < 2 || (claim.importance === 'critical' && claim.counterevidence_ids.length === 0));
86
+ })
87
+ .map((claim) => claim.id);
88
+ return normalizeClaimEvidenceMatrix({
89
+ schema: 'sks.claim-evidence-matrix.v1',
90
+ mission_id: input.plan?.mission_id || '',
91
+ claims,
92
+ key_claim_ids: claims.slice(0, 8).map((claim) => claim.id),
93
+ unsupported_claims: unsupported,
94
+ triangulated_claim_count: claims.filter((claim) => claim.triangulation.source_layers.length >= 2).length,
95
+ blockers: unsupported.map((id) => `unsupported_important_claim:${id}`)
96
+ });
97
+ }
98
+ function claimTextFromSource(source, claimId) {
99
+ const notes = String(source?.notes || '').trim();
100
+ if (notes)
101
+ return `${claimId}: ${notes.slice(0, 220)}`;
102
+ return `${claimId}: Evidence row ${source?.id || 'unknown'} contributes to this research claim.`;
103
+ }
104
+ function sourceById(sources, id) {
105
+ return sources.find((source) => String(source?.id || '') === id) || null;
106
+ }
107
+ function sourceLayersForSourceIds(sources, ids) {
108
+ const idSet = new Set(ids);
109
+ return [...new Set(sources.filter((source) => idSet.has(String(source?.id || ''))).map((source) => String(source?.layer || '')).filter(Boolean))];
110
+ }
111
+ function normalizeStringList(value) {
112
+ return [...new Set((Array.isArray(value) ? value : value == null ? [] : [value]).map((item) => String(item || '').trim()).filter(Boolean))];
113
+ }
114
+ //# sourceMappingURL=research-claim-builder.js.map
@@ -1,25 +1,129 @@
1
1
  import path from 'node:path';
2
2
  import { appendJsonlBounded, nowIso, writeJsonAtomic } from '../fsx.js';
3
3
  import { runResearchStage } from './research-stage-runner.js';
4
- export async function runResearchCycle(dir, graph = null, opts = {}) {
4
+ export async function runResearchCycle(inputOrDir, legacyGraph = null, legacyOpts = {}) {
5
+ const input = typeof inputOrDir === 'string'
6
+ ? {
7
+ root: process.cwd(),
8
+ dir: inputOrDir,
9
+ plan: null,
10
+ graph: legacyGraph,
11
+ cycle: Number(legacyOpts.cycle || 0),
12
+ backend: legacyOpts.mock ? 'mock' : 'deterministic',
13
+ timeoutMs: Number(legacyOpts.timeoutMs || 120000),
14
+ maxParallelStages: Number(legacyOpts.maxParallelStages || legacyOpts.maxParallel || 4),
15
+ mock: legacyOpts.mock === true
16
+ }
17
+ : inputOrDir;
5
18
  const startedAt = nowIso();
6
- const stages = Array.isArray(graph?.work_items) ? graph.work_items : [];
7
- const stageResults = [];
8
- for (const stage of stages) {
9
- stageResults.push(await runResearchStage(dir, stage, { status: opts.status || 'planned', startedAt }));
19
+ const stages = normalizeStages(input.graph);
20
+ const pending = new Map(stages.map((stage) => [String(stage.id), stage]));
21
+ const completed = new Map();
22
+ const running = new Map();
23
+ const blockers = [];
24
+ const maxParallel = Math.max(1, Math.min(16, Number(input.maxParallelStages || 4)));
25
+ let maxObservedParallel = 0;
26
+ while (pending.size || running.size) {
27
+ const ready = readyStages([...pending.values()], completed);
28
+ while (running.size < maxParallel && ready.length) {
29
+ const stage = ready.shift();
30
+ if (!stage)
31
+ break;
32
+ pending.delete(String(stage.id));
33
+ const promise = runResearchStage({ ...input, stage }).catch((err) => failureStage(input, stage, err));
34
+ running.set(String(stage.id), promise);
35
+ maxObservedParallel = Math.max(maxObservedParallel, running.size);
36
+ }
37
+ if (!running.size) {
38
+ const blockedIds = [...pending.keys()];
39
+ blockers.push(...blockedIds.map((id) => `stage_dependencies_unresolved:${id}`));
40
+ for (const stage of pending.values()) {
41
+ const failed = failureStage(input, stage, new Error(`dependencies unresolved: ${(stage.dependencies || []).join(',')}`));
42
+ completed.set(String(stage.id), failed);
43
+ }
44
+ pending.clear();
45
+ break;
46
+ }
47
+ const done = await Promise.race([...running.entries()].map(async ([id, promise]) => ({ id, result: await promise })));
48
+ running.delete(done.id);
49
+ completed.set(done.id, done.result);
50
+ if (done.result.status !== 'passed' && pendingStageRequired(stages.find((stage) => String(stage.id) === done.id))) {
51
+ blockers.push(...(done.result.blockers.length ? done.result.blockers.map((blocker) => `${done.id}:${blocker}`) : [`${done.id}:stage_not_passed`]));
52
+ }
10
53
  }
54
+ const stageResults = [...completed.values()];
11
55
  const record = {
12
- schema: 'sks.research-cycle-runner.v1',
13
- cycle: opts.cycle || 0,
56
+ schema: 'sks.research-cycle-runner.v2',
57
+ cycle: input.cycle,
14
58
  readonly: true,
15
59
  started_at: startedAt,
16
60
  completed_at: nowIso(),
17
61
  stage_count: stageResults.length,
18
- status: opts.status || 'planned',
19
- stages: stageResults.map((stage) => stage.stage_id)
62
+ status: blockers.length ? 'blocked' : 'passed',
63
+ blockers: [...new Set(blockers)],
64
+ stages: stageResults.map((stage) => stage.stage_id),
65
+ stage_results: stageResults,
66
+ parallelism: {
67
+ max_parallel_stages: maxParallel,
68
+ max_observed_parallel: maxObservedParallel,
69
+ stage_count: stageResults.length,
70
+ critical_path_length: criticalPathLength(stages)
71
+ },
72
+ legacy_final_md_loop: false
20
73
  };
21
- await writeJsonAtomic(path.join(dir, 'research-cycle-runner.json'), record);
22
- await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle_runner.recorded', cycle: record.cycle, stage_count: record.stage_count });
74
+ await writeJsonAtomic(path.join(input.dir, 'research', `cycle-${input.cycle}`, 'research-cycle-runner.json'), record);
75
+ await writeJsonAtomic(path.join(input.dir, 'research-cycle-runner.json'), record);
76
+ await appendJsonlBounded(path.join(input.dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle_runner.completed', cycle: record.cycle, stage_count: record.stage_count, status: record.status, max_observed_parallel: maxObservedParallel });
23
77
  return record;
24
78
  }
79
+ export function readyStages(pending, completed) {
80
+ return pending.filter((stage) => (Array.isArray(stage.dependencies) ? stage.dependencies : []).every((id) => completed.has(String(id))));
81
+ }
82
+ function normalizeStages(graph) {
83
+ const stages = Array.isArray(graph?.work_items) ? graph.work_items : [];
84
+ return stages.map((stage, index) => ({
85
+ ...stage,
86
+ id: String(stage?.id || `research-stage-${index + 1}`),
87
+ dependencies: Array.isArray(stage?.dependencies) ? stage.dependencies.map(String) : []
88
+ }));
89
+ }
90
+ function pendingStageRequired(stage) {
91
+ return stage?.required !== false;
92
+ }
93
+ function failureStage(input, stage, err) {
94
+ const ts = nowIso();
95
+ return {
96
+ schema: 'sks.research-stage-result.v1',
97
+ mission_id: String(input.plan?.mission_id || ''),
98
+ cycle: input.cycle,
99
+ stage_id: String(stage?.id || 'unknown'),
100
+ stage_kind: 'verification',
101
+ status: 'failed',
102
+ started_at: ts,
103
+ completed_at: ts,
104
+ input_artifacts: [],
105
+ output_artifacts: [],
106
+ backend: input.backend,
107
+ worker_result_path: null,
108
+ blockers: [err instanceof Error ? err.message : String(err)],
109
+ metrics: {}
110
+ };
111
+ }
112
+ function criticalPathLength(stages) {
113
+ const byId = new Map(stages.map((stage) => [String(stage.id), stage]));
114
+ const memo = new Map();
115
+ const visit = (id, seen = new Set()) => {
116
+ if (memo.has(id))
117
+ return memo.get(id);
118
+ if (seen.has(id))
119
+ return 1;
120
+ seen.add(id);
121
+ const stage = byId.get(id);
122
+ const deps = Array.isArray(stage?.dependencies) ? stage.dependencies.map(String) : [];
123
+ const value = 1 + (deps.length ? Math.max(...deps.map((dep) => visit(dep, new Set(seen)))) : 0);
124
+ memo.set(id, value);
125
+ return value;
126
+ };
127
+ return stages.length ? Math.max(...stages.map((stage) => visit(String(stage.id)))) : 0;
128
+ }
25
129
  //# sourceMappingURL=research-cycle-runner.js.map
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
3
+ import { runCodexTask } from '../codex-control/codex-task-runner.js';
3
4
  import { analyzeResearchReportQuality } from './research-report-quality.js';
4
5
  import { validateClaimEvidenceMatrix } from './claim-evidence-matrix.js';
5
6
  import { validateImplementationBlueprint } from './implementation-blueprint.js';
@@ -7,10 +8,12 @@ import { validateExperimentPlan } from './experiment-plan.js';
7
8
  import { validateReplicationPack } from './replication-pack.js';
8
9
  import { validateFalsificationCoverage } from './falsification.js';
9
10
  export const RESEARCH_FINAL_REVIEW_ARTIFACT = 'research-final-review.json';
11
+ export const RESEARCH_STATIC_FINAL_REVIEW_ARTIFACT = 'research-final-review.static.json';
12
+ export const RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT = 'research-final-review.codex.json';
10
13
  export async function readResearchFinalReview(dir) {
11
14
  return readJson(path.join(dir, RESEARCH_FINAL_REVIEW_ARTIFACT), null);
12
15
  }
13
- export async function runResearchFinalReviewer(dir, input = {}) {
16
+ export async function runResearchStaticFinalReview(dir, input = {}) {
14
17
  const contract = input.contract || await readJson(path.join(dir, 'research-quality-contract.json'), null);
15
18
  const sourceLedger = input.sourceLedger || await readJson(path.join(dir, 'source-ledger.json'), null);
16
19
  const claimMatrix = input.claimMatrix || await readJson(path.join(dir, 'claim-evidence-matrix.json'), null);
@@ -52,7 +55,158 @@ export async function runResearchFinalReviewer(dir, input = {}) {
52
55
  },
53
56
  reviewer: 'research_final_reviewer_static_gate'
54
57
  };
58
+ await writeJsonAtomic(path.join(dir, RESEARCH_STATIC_FINAL_REVIEW_ARTIFACT), review);
59
+ return review;
60
+ }
61
+ export async function runResearchCodexFinalReviewer(input) {
62
+ if (input.staticReview?.approved !== true) {
63
+ const skipped = {
64
+ schema: 'sks.research-codex-final-review.v1',
65
+ reviewed_at: nowIso(),
66
+ verdict: 'revise',
67
+ unsupported_claim_ids: [],
68
+ missing_evidence: [],
69
+ blueprint_findings: [],
70
+ falsification_findings: [],
71
+ required_revisions: ['static_review_failed'],
72
+ confidence: 'low',
73
+ skipped: true,
74
+ skip_reason: 'static_review_failed'
75
+ };
76
+ await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), skipped);
77
+ return skipped;
78
+ }
79
+ if (input.mock === true) {
80
+ const approved = {
81
+ schema: 'sks.research-codex-final-review.v1',
82
+ reviewed_at: nowIso(),
83
+ verdict: 'approve',
84
+ unsupported_claim_ids: [],
85
+ missing_evidence: [],
86
+ blueprint_findings: ['mock final reviewer approves the complete package fixture'],
87
+ falsification_findings: ['mock counterevidence and falsification cases are present'],
88
+ required_revisions: [],
89
+ confidence: 'high',
90
+ mock: true
91
+ };
92
+ await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), approved);
93
+ return approved;
94
+ }
95
+ const result = await runCodexTask({
96
+ route: '$Research',
97
+ tier: 'worker',
98
+ missionId: String(input.plan?.mission_id || 'research-final-review'),
99
+ workItemId: 'research_final_review',
100
+ cwd: input.root,
101
+ prompt: buildResearchFinalReviewPrompt(input.plan, input.staticReview),
102
+ outputSchema: researchCodexFinalReviewSchema,
103
+ outputSchemaId: 'sks.research-codex-final-review.v1',
104
+ sandboxPolicy: 'read-only',
105
+ requestedScopeContract: {
106
+ id: 'research-final-review',
107
+ route: '$Research',
108
+ read_only: true,
109
+ allowed_paths: [`.sneakoscope/missions/${input.plan?.mission_id || ''}/`],
110
+ write_paths: [],
111
+ allowed_write_prefixes: [`.sneakoscope/missions/${input.plan?.mission_id || ''}/`],
112
+ source_mutation_allowed: false
113
+ },
114
+ backendPreference: input.backendPreference || ['codex-sdk', 'python-codex-sdk'],
115
+ localLlmPolicy: { mode: 'disabled', requiresGptFinal: true },
116
+ allowLocalLlm: false,
117
+ mutationLedgerRoot: path.join(input.dir, 'research', 'final-review-codex-control'),
118
+ reliabilityPolicy: {
119
+ timeoutClass: 'standard',
120
+ idleTimeoutMs: input.timeoutMs || 120000
121
+ }
122
+ });
123
+ const worker = await readJson(result.workerResultPath, null);
124
+ const review = normalizeCodexReview(worker, result);
125
+ await writeJsonAtomic(path.join(input.dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), review);
126
+ return review;
127
+ }
128
+ export async function runResearchFinalReviewer(dir, input = {}) {
129
+ const staticReview = await runResearchStaticFinalReview(dir, input);
130
+ const existingCodex = await readJson(path.join(dir, RESEARCH_CODEX_FINAL_REVIEW_ARTIFACT), null);
131
+ const codexReview = existingCodex || (input.mock === true ? await runResearchCodexFinalReviewer({
132
+ root: input.root || process.cwd(),
133
+ dir,
134
+ plan: input.plan || await readJson(path.join(dir, 'research-plan.json'), null),
135
+ staticReview,
136
+ mock: true
137
+ }) : null);
138
+ const codexApproved = codexReview?.verdict === 'approve';
139
+ const codexRequired = input.codexRequired !== false;
140
+ const blockers = [
141
+ ...(Array.isArray(staticReview?.blockers) ? staticReview.blockers : []),
142
+ ...(codexRequired && !codexReview ? ['research_codex_final_review_missing'] : []),
143
+ ...(codexReview && !codexApproved ? ['research_codex_final_review_not_approved'] : []),
144
+ ...(Array.isArray(codexReview?.required_revisions) ? codexReview.required_revisions.map((revision) => `codex_revision:${revision}`) : [])
145
+ ];
146
+ const review = {
147
+ schema: 'sks.research-final-reviewer.v2',
148
+ reviewed_at: nowIso(),
149
+ approved: staticReview?.approved === true && (!codexRequired || codexApproved) && blockers.length === 0,
150
+ blockers: [...new Set(blockers)],
151
+ static_review: staticReview,
152
+ codex_review: codexReview,
153
+ reviewer: 'research_final_reviewer_static_plus_codex_gate'
154
+ };
55
155
  await writeJsonAtomic(path.join(dir, RESEARCH_FINAL_REVIEW_ARTIFACT), review);
56
156
  return review;
57
157
  }
158
+ function buildResearchFinalReviewPrompt(plan, staticReview) {
159
+ return [
160
+ 'You are the Codex/GPT final reviewer for an SKS Research package.',
161
+ `Mission: ${plan?.mission_id || 'unknown'}`,
162
+ `Prompt: ${plan?.prompt || ''}`,
163
+ '',
164
+ 'Review the mission artifacts read-only. Reject if claims lack evidence, blueprint steps are template-like, falsification is missing, or the package is only a short summary.',
165
+ 'Return only JSON matching sks.research-codex-final-review.v1 with verdict approve, revise, or reject.',
166
+ '',
167
+ `Static review summary:\n${JSON.stringify(staticReview, null, 2).slice(0, 12000)}`
168
+ ].join('\n');
169
+ }
170
+ function normalizeCodexReview(worker, result) {
171
+ if (!result?.ok) {
172
+ return {
173
+ schema: 'sks.research-codex-final-review.v1',
174
+ reviewed_at: nowIso(),
175
+ verdict: 'revise',
176
+ unsupported_claim_ids: [],
177
+ missing_evidence: [],
178
+ blueprint_findings: [],
179
+ falsification_findings: [],
180
+ required_revisions: Array.isArray(result?.blockers) ? result.blockers : ['codex_final_reviewer_unavailable'],
181
+ confidence: 'low',
182
+ worker_result_path: result?.workerResultPath || null
183
+ };
184
+ }
185
+ return {
186
+ schema: 'sks.research-codex-final-review.v1',
187
+ reviewed_at: nowIso(),
188
+ verdict: ['approve', 'revise', 'reject'].includes(worker?.verdict) ? worker.verdict : 'revise',
189
+ unsupported_claim_ids: Array.isArray(worker?.unsupported_claim_ids) ? worker.unsupported_claim_ids.map(String) : [],
190
+ missing_evidence: Array.isArray(worker?.missing_evidence) ? worker.missing_evidence.map(String) : [],
191
+ blueprint_findings: Array.isArray(worker?.blueprint_findings) ? worker.blueprint_findings.map(String) : [],
192
+ falsification_findings: Array.isArray(worker?.falsification_findings) ? worker.falsification_findings.map(String) : [],
193
+ required_revisions: Array.isArray(worker?.required_revisions) ? worker.required_revisions.map(String) : [],
194
+ confidence: ['low', 'medium', 'high'].includes(worker?.confidence) ? worker.confidence : 'medium',
195
+ worker_result_path: result.workerResultPath
196
+ };
197
+ }
198
+ export const researchCodexFinalReviewSchema = {
199
+ type: 'object',
200
+ required: ['schema', 'verdict', 'unsupported_claim_ids', 'missing_evidence', 'blueprint_findings', 'falsification_findings', 'required_revisions', 'confidence'],
201
+ properties: {
202
+ schema: { const: 'sks.research-codex-final-review.v1' },
203
+ verdict: { enum: ['approve', 'revise', 'reject'] },
204
+ unsupported_claim_ids: { type: 'array' },
205
+ missing_evidence: { type: 'array' },
206
+ blueprint_findings: { type: 'array' },
207
+ falsification_findings: { type: 'array' },
208
+ required_revisions: { type: 'array' },
209
+ confidence: { enum: ['low', 'medium', 'high'] }
210
+ }
211
+ };
58
212
  //# sourceMappingURL=research-final-reviewer.js.map