sneakoscope 2.0.2 → 2.0.4
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/build-manifest.json +21 -8
- package/dist/core/agents/agent-orchestrator.js +70 -4
- package/dist/core/agents/agent-patch-proof.js +5 -0
- package/dist/core/agents/agent-proof-evidence.js +26 -0
- package/dist/core/codex-control/codex-fake-sdk-adapter.js +20 -0
- package/dist/core/codex-control/codex-output-schemas.js +5 -1
- package/dist/core/codex-control/gpt-final-arbiter.js +160 -0
- package/dist/core/codex-control/gpt-final-context-compressor.js +17 -0
- package/dist/core/codex-control/gpt-final-proof-pack.js +120 -0
- package/dist/core/codex-control/gpt-final-review-schema.js +71 -0
- package/dist/core/commands/local-model-command.js +16 -1
- package/dist/core/commands/naruto-command.js +77 -5
- package/dist/core/commands/run-command.js +5 -1
- package/dist/core/doctor/doctor-readiness-matrix.js +15 -0
- package/dist/core/feature-fixtures.js +4 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/git-simple.js +143 -4
- package/dist/core/local-llm/local-collaboration-policy.js +93 -0
- package/dist/core/local-llm/local-llm-config.js +15 -0
- package/dist/core/pipeline/final-gpt-patch-stage.js +31 -0
- package/dist/core/pipeline/final-gpt-review-stage.js +5 -0
- package/dist/core/safety/mutation-guard.js +2 -0
- package/dist/core/update-check.js +60 -25
- package/dist/core/version.js +1 -1
- package/dist/scripts/codex-sdk-team-naruto-agent-pipeline-check.js +2 -1
- package/dist/scripts/gpt-final-arbiter-check.js +63 -0
- package/dist/scripts/gpt-final-arbiter-performance-check.js +36 -0
- package/dist/scripts/local-collab-gpt-final-availability-check.js +58 -0
- package/dist/scripts/local-collab-no-local-only-final-check.js +27 -0
- package/dist/scripts/local-collab-policy-check.js +17 -0
- package/package.json +8 -3
- package/schemas/local-llm/local-collaboration-policy.schema.json +57 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { nowIso, sha256 } from '../fsx.js';
|
|
2
|
+
export const GPT_FINAL_PROOF_PACK_SCHEMA = 'sks.gpt-final-proof-pack.v1';
|
|
3
|
+
export function buildGptFinalProofPack(input, opts = {}) {
|
|
4
|
+
const maxCriticalLogChars = Math.max(1000, Number(opts.maxCriticalLogChars || 6000));
|
|
5
|
+
const localOutputs = Array.isArray(input.local_outputs) ? input.local_outputs : [];
|
|
6
|
+
const candidatePatchEnvelopes = Array.isArray(input.candidate_patch_envelopes) ? input.candidate_patch_envelopes : [];
|
|
7
|
+
const verificationResults = Array.isArray(input.verification_results) ? input.verification_results : [];
|
|
8
|
+
const pack = {
|
|
9
|
+
schema: GPT_FINAL_PROOF_PACK_SCHEMA,
|
|
10
|
+
generated_at: nowIso(),
|
|
11
|
+
route: String(input.route || ''),
|
|
12
|
+
mission_id: String(input.mission_id || ''),
|
|
13
|
+
local_mode: String(input.local_mode || ''),
|
|
14
|
+
worker_count: localOutputs.length,
|
|
15
|
+
changed_files: sortedUnique([
|
|
16
|
+
...candidatePatchEnvelopes.flatMap((envelope) => envelopeChangedFiles(envelope)),
|
|
17
|
+
...localOutputs.flatMap((output) => Array.isArray(output?.changed_files) ? output.changed_files.map(String) : [])
|
|
18
|
+
]),
|
|
19
|
+
local_output_summaries: localOutputs.map((output, index) => ({
|
|
20
|
+
worker_id: String(output?.worker_id || output?.agent_id || output?.id || `worker-${index + 1}`),
|
|
21
|
+
backend: String(output?.backend || 'local-llm'),
|
|
22
|
+
status: String(output?.status || 'unknown'),
|
|
23
|
+
summary: trim(String(output?.summary || ''), 600),
|
|
24
|
+
proof: trim(String(output?.proof || output?.verification?.status || ''), 600),
|
|
25
|
+
blocker_count: Array.isArray(output?.blockers) ? output.blockers.length : 0,
|
|
26
|
+
patch_envelope_count: Array.isArray(output?.patch_envelopes) ? output.patch_envelopes.length : output?.patch_envelope ? 1 : 0
|
|
27
|
+
})),
|
|
28
|
+
candidate_diff_sha256: sha256(String(input.candidate_diff || '')),
|
|
29
|
+
candidate_diff_tail: tail(String(input.candidate_diff || ''), maxCriticalLogChars),
|
|
30
|
+
candidate_patch_envelopes: candidatePatchEnvelopes.map((envelope, index) => summarizeEnvelope(envelope, index)),
|
|
31
|
+
verification_failures: verificationResults.filter((result) => result?.ok === false || result?.status === 'failed').map((result) => ({
|
|
32
|
+
id: String(result?.id || result?.patch_entry_id || result?.name || 'verification'),
|
|
33
|
+
status: String(result?.status || (result?.ok === false ? 'failed' : 'unknown')),
|
|
34
|
+
blockers: stringArray(result?.blockers || result?.violations)
|
|
35
|
+
})),
|
|
36
|
+
side_effect_ledger_summary: summarizeObject(input.side_effect_report),
|
|
37
|
+
mutation_ledger_summary: summarizeObject(input.mutation_ledger),
|
|
38
|
+
rollback_plan_summary: summarizeObject(input.rollback_plan),
|
|
39
|
+
conflict_map: buildConflictMap(candidatePatchEnvelopes),
|
|
40
|
+
critical_logs_tail: tail(JSON.stringify({
|
|
41
|
+
local_outputs: localOutputs.map((output) => ({
|
|
42
|
+
id: output?.worker_id || output?.agent_id || output?.id,
|
|
43
|
+
blockers: output?.blockers || [],
|
|
44
|
+
unverified: output?.unverified || []
|
|
45
|
+
})),
|
|
46
|
+
verification_results: verificationResults
|
|
47
|
+
}), maxCriticalLogChars),
|
|
48
|
+
token_budget_estimate: 0
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
...pack,
|
|
52
|
+
token_budget_estimate: estimateTokens(pack)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function buildGptFinalLatencyBudgetReport(input) {
|
|
56
|
+
const workerCount = Math.max(0, Number(input.workerCount || 0));
|
|
57
|
+
const tokenBudgetEstimate = Math.max(0, Number(input.tokenBudgetEstimate || 0));
|
|
58
|
+
const capAdjustment = tokenBudgetEstimate > 8000 || workerCount > 20 ? 'reduce_local_parallelism_or_pack_size' : 'within_budget';
|
|
59
|
+
return {
|
|
60
|
+
schema: 'sks.gpt-final-latency-budget.v1',
|
|
61
|
+
generated_at: nowIso(),
|
|
62
|
+
worker_count: workerCount,
|
|
63
|
+
token_budget_estimate: tokenBudgetEstimate,
|
|
64
|
+
latency_ms: input.latencyMs ?? null,
|
|
65
|
+
cap_adjustment: capAdjustment,
|
|
66
|
+
ok: capAdjustment === 'within_budget'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function summarizeEnvelope(envelope, index) {
|
|
70
|
+
return {
|
|
71
|
+
id: String(envelope?.id || envelope?.patch_id || `patch-${index + 1}`),
|
|
72
|
+
agent_id: String(envelope?.agent_id || 'unknown'),
|
|
73
|
+
source: String(envelope?.source || 'local-llm'),
|
|
74
|
+
operations: Array.isArray(envelope?.operations) ? envelope.operations.length : 0,
|
|
75
|
+
write_paths: envelopeChangedFiles(envelope),
|
|
76
|
+
lease_id: envelope?.lease_id || envelope?.lease_proof?.lease_id || null,
|
|
77
|
+
rollback_ready: Boolean(envelope?.rollback_hint || envelope?.lease_proof?.rollback_node_id)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function envelopeChangedFiles(envelope) {
|
|
81
|
+
return Array.isArray(envelope?.operations) ? envelope.operations.map((operation) => String(operation?.path || '')).filter(Boolean) : [];
|
|
82
|
+
}
|
|
83
|
+
function buildConflictMap(envelopes) {
|
|
84
|
+
const owners = new Map();
|
|
85
|
+
for (const envelope of envelopes) {
|
|
86
|
+
const agent = String(envelope?.agent_id || 'unknown');
|
|
87
|
+
for (const file of envelopeChangedFiles(envelope))
|
|
88
|
+
owners.set(file, [...(owners.get(file) || []), agent]);
|
|
89
|
+
}
|
|
90
|
+
return [...owners.entries()]
|
|
91
|
+
.filter(([, agents]) => new Set(agents).size > 1)
|
|
92
|
+
.map(([file, agents]) => ({ file, agents: sortedUnique(agents) }));
|
|
93
|
+
}
|
|
94
|
+
function summarizeObject(value) {
|
|
95
|
+
if (!value || typeof value !== 'object')
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
schema: value.schema || null,
|
|
99
|
+
ok: typeof value.ok === 'boolean' ? value.ok : null,
|
|
100
|
+
status: value.status || null,
|
|
101
|
+
blockers: stringArray(value.blockers).slice(0, 20),
|
|
102
|
+
keys: Object.keys(value).slice(0, 30)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function estimateTokens(value) {
|
|
106
|
+
return Math.ceil(JSON.stringify(value).length / 4);
|
|
107
|
+
}
|
|
108
|
+
function tail(value, max) {
|
|
109
|
+
return value.length <= max ? value : value.slice(value.length - max);
|
|
110
|
+
}
|
|
111
|
+
function trim(value, max) {
|
|
112
|
+
return value.length <= max ? value : value.slice(0, max);
|
|
113
|
+
}
|
|
114
|
+
function sortedUnique(values) {
|
|
115
|
+
return [...new Set(values.map(String).filter(Boolean))].sort();
|
|
116
|
+
}
|
|
117
|
+
function stringArray(value) {
|
|
118
|
+
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=gpt-final-proof-pack.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const GPT_FINAL_ARBITER_RESULT_SCHEMA_ID = 'sks.gpt-final-arbiter-result.v1';
|
|
2
|
+
export const GPT_FINAL_ARBITER_INPUT_SCHEMA = 'sks.gpt-final-arbiter-input.v1';
|
|
3
|
+
export const gptFinalArbiterResultSchema = {
|
|
4
|
+
type: 'object',
|
|
5
|
+
required: [
|
|
6
|
+
'schema',
|
|
7
|
+
'status',
|
|
8
|
+
'summary',
|
|
9
|
+
'gpt_review_findings',
|
|
10
|
+
'accepted_patch_envelopes',
|
|
11
|
+
'modified_patch_envelopes',
|
|
12
|
+
'rejected_patch_envelopes',
|
|
13
|
+
'required_followup_work',
|
|
14
|
+
'verification_plan',
|
|
15
|
+
'rollback_notes',
|
|
16
|
+
'blockers',
|
|
17
|
+
'confidence'
|
|
18
|
+
],
|
|
19
|
+
properties: {
|
|
20
|
+
schema: { const: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID },
|
|
21
|
+
status: { enum: ['approved', 'modified', 'rejected', 'needs_more_work'] },
|
|
22
|
+
summary: { type: 'string' },
|
|
23
|
+
gpt_review_findings: { type: 'array', items: { type: 'object' } },
|
|
24
|
+
accepted_patch_envelopes: { type: 'array', items: { type: 'object' } },
|
|
25
|
+
modified_patch_envelopes: { type: 'array', items: { type: 'object' } },
|
|
26
|
+
rejected_patch_envelopes: { type: 'array', items: { type: 'object' } },
|
|
27
|
+
required_followup_work: { type: 'array', items: { type: 'object' } },
|
|
28
|
+
verification_plan: { type: 'array', items: { type: 'string' } },
|
|
29
|
+
rollback_notes: { type: 'array', items: { type: 'string' } },
|
|
30
|
+
blockers: { type: 'array', items: { type: 'string' } },
|
|
31
|
+
confidence: { enum: ['low', 'medium', 'high'] }
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: true
|
|
34
|
+
};
|
|
35
|
+
export function normalizeGptFinalArbiterResult(value) {
|
|
36
|
+
const status = normalizeStatus(value?.status);
|
|
37
|
+
return {
|
|
38
|
+
schema: GPT_FINAL_ARBITER_RESULT_SCHEMA_ID,
|
|
39
|
+
status,
|
|
40
|
+
summary: String(value?.summary || defaultSummary(status)),
|
|
41
|
+
gpt_review_findings: array(value?.gpt_review_findings),
|
|
42
|
+
accepted_patch_envelopes: array(value?.accepted_patch_envelopes),
|
|
43
|
+
modified_patch_envelopes: array(value?.modified_patch_envelopes),
|
|
44
|
+
rejected_patch_envelopes: array(value?.rejected_patch_envelopes),
|
|
45
|
+
required_followup_work: array(value?.required_followup_work),
|
|
46
|
+
verification_plan: stringArray(value?.verification_plan),
|
|
47
|
+
rollback_notes: stringArray(value?.rollback_notes),
|
|
48
|
+
blockers: stringArray(value?.blockers),
|
|
49
|
+
confidence: normalizeConfidence(value?.confidence)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function normalizeStatus(value) {
|
|
53
|
+
return value === 'approved' || value === 'modified' || value === 'rejected' || value === 'needs_more_work'
|
|
54
|
+
? value
|
|
55
|
+
: 'needs_more_work';
|
|
56
|
+
}
|
|
57
|
+
function normalizeConfidence(value) {
|
|
58
|
+
return value === 'low' || value === 'medium' || value === 'high' ? value : 'medium';
|
|
59
|
+
}
|
|
60
|
+
function array(value) {
|
|
61
|
+
return Array.isArray(value) ? value : [];
|
|
62
|
+
}
|
|
63
|
+
function stringArray(value) {
|
|
64
|
+
return Array.isArray(value) ? value.map((entry) => String(entry || '').trim()).filter(Boolean) : [];
|
|
65
|
+
}
|
|
66
|
+
function defaultSummary(status) {
|
|
67
|
+
return status === 'approved' || status === 'modified'
|
|
68
|
+
? 'GPT final arbiter accepted the candidate result.'
|
|
69
|
+
: 'GPT final arbiter did not approve the candidate result.';
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=gpt-final-review-schema.js.map
|
|
@@ -26,7 +26,7 @@ function normalizeLocalModelAction(value) {
|
|
|
26
26
|
return text;
|
|
27
27
|
}
|
|
28
28
|
async function enable(args) {
|
|
29
|
-
const model = readOption(args, '--model', args
|
|
29
|
+
const model = readOption(args, '--model', firstPositional(args) || '');
|
|
30
30
|
const baseUrl = readOption(args, '--base-url', '');
|
|
31
31
|
const think = readBoolFlag(args, '--think', '--no-think');
|
|
32
32
|
const patch = { enabled: true, provider: 'ollama' };
|
|
@@ -102,4 +102,19 @@ function readBoolFlag(args, trueName, falseName) {
|
|
|
102
102
|
return false;
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
|
+
function firstPositional(args = []) {
|
|
106
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
107
|
+
const arg = String(args[i] || '');
|
|
108
|
+
if (arg === '--model' || arg === '--base-url') {
|
|
109
|
+
if (args[i + 1] && !String(args[i + 1]).startsWith('--'))
|
|
110
|
+
i += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (arg.startsWith('--model=') || arg.startsWith('--base-url='))
|
|
114
|
+
continue;
|
|
115
|
+
if (!arg.startsWith('--'))
|
|
116
|
+
return arg;
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
105
120
|
//# sourceMappingURL=local-model-command.js.map
|
|
@@ -2,8 +2,10 @@ import path from 'node:path';
|
|
|
2
2
|
import { createMission, findLatestMission, loadMission } from '../mission.js';
|
|
3
3
|
import { readJson, sksRoot } from '../fsx.js';
|
|
4
4
|
import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
|
|
5
|
+
import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
|
|
5
6
|
import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
|
|
6
7
|
import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
|
|
8
|
+
import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
|
|
7
9
|
import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/zellij-launcher.js';
|
|
8
10
|
const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
|
|
9
11
|
const NARUTO_ROUTE = '$Naruto';
|
|
@@ -32,7 +34,9 @@ async function narutoRun(parsed) {
|
|
|
32
34
|
// The clone roster is the full work fan-out; live concurrency is throttled to a
|
|
33
35
|
// system-safe number so naruto never spawns the whole count at once unless an
|
|
34
36
|
// explicit operator override asks for a higher target.
|
|
35
|
-
const
|
|
37
|
+
const localWorker = await resolveNarutoLocalWorkerMode(parsed);
|
|
38
|
+
const schedulerBackend = localWorker.auto_select_eligible ? 'ollama' : parsed.backend;
|
|
39
|
+
const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
|
|
36
40
|
const activeSlots = Math.max(1, Math.min(roster.agent_count, parsed.concurrency || safe.cap));
|
|
37
41
|
const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
|
|
38
42
|
const ledgerRoot = path.join(mission.dir, 'agents');
|
|
@@ -75,6 +79,11 @@ async function narutoRun(parsed) {
|
|
|
75
79
|
narutoMode: true,
|
|
76
80
|
clones: roster.agent_count,
|
|
77
81
|
backend: parsed.backend,
|
|
82
|
+
backendExplicit: parsed.backendExplicit,
|
|
83
|
+
noOllama: parsed.noOllama,
|
|
84
|
+
ollamaEnabled: parsed.ollamaEnabled,
|
|
85
|
+
ollamaModel: parsed.ollamaModel,
|
|
86
|
+
ollamaBaseUrl: parsed.ollamaBaseUrl,
|
|
78
87
|
mock: parsed.mock,
|
|
79
88
|
real: parsed.real,
|
|
80
89
|
readonly: parsed.readonly,
|
|
@@ -86,6 +95,7 @@ async function narutoRun(parsed) {
|
|
|
86
95
|
json: parsed.json
|
|
87
96
|
});
|
|
88
97
|
const clones = result.roster?.agent_count ?? roster.agent_count;
|
|
98
|
+
const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
|
|
89
99
|
const summary = {
|
|
90
100
|
schema: NARUTO_RESULT_SCHEMA,
|
|
91
101
|
ok: result.ok === true,
|
|
@@ -99,6 +109,7 @@ async function narutoRun(parsed) {
|
|
|
99
109
|
target_active_slots: result.target_active_slots ?? activeSlots,
|
|
100
110
|
concurrency_capped: clones > (result.target_active_slots ?? activeSlots),
|
|
101
111
|
system: { cores: safe.cores, free_gb: safe.free_gb, safe_concurrency: safe.cap, heavy_backend: safe.heavy },
|
|
112
|
+
local_worker: localWorkerSummary,
|
|
102
113
|
proof: result.proof?.status || 'missing',
|
|
103
114
|
run: result,
|
|
104
115
|
zellij: null
|
|
@@ -116,6 +127,19 @@ async function narutoRun(parsed) {
|
|
|
116
127
|
console.log('Zellij: optional live panes unavailable (' + ((summary.zellij.warnings || []).join('; ') || summary.zellij.capability?.status || 'unknown') + ')');
|
|
117
128
|
});
|
|
118
129
|
}
|
|
130
|
+
function summarizeNarutoLocalWorkerResult(localWorker, result) {
|
|
131
|
+
const backendCounts = {};
|
|
132
|
+
const rows = Array.isArray(result?.results) ? result.results : [];
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
const selected = String(row?.backend_router_report?.selected_backend || row?.backend || 'unknown');
|
|
135
|
+
backendCounts[selected] = (backendCounts[selected] || 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...localWorker,
|
|
139
|
+
selected_worker_count: backendCounts.ollama || 0,
|
|
140
|
+
backend_counts: backendCounts
|
|
141
|
+
};
|
|
142
|
+
}
|
|
119
143
|
async function narutoStatus(parsed) {
|
|
120
144
|
const root = await sksRoot();
|
|
121
145
|
const id = parsed.missionId && parsed.missionId !== 'latest' ? parsed.missionId : await findLatestMission(root);
|
|
@@ -149,7 +173,7 @@ async function narutoHelp(parsed) {
|
|
|
149
173
|
mode: 'NARUTO',
|
|
150
174
|
description: 'Shadow Clone Swarm: fan out up to ' + MAX_NARUTO_AGENT_COUNT + ' parallel clone sessions.',
|
|
151
175
|
usage: [
|
|
152
|
-
'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake] [--work-items N] [--real] [--readonly] [--json]',
|
|
176
|
+
'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake|ollama] [--local-model|--no-ollama] [--work-items N] [--real] [--readonly] [--json]',
|
|
153
177
|
'sks naruto status [--mission <id>] [--json]'
|
|
154
178
|
],
|
|
155
179
|
defaults: { clones: DEFAULT_NARUTO_CLONES, max_clones: MAX_NARUTO_AGENT_COUNT, backend: 'codex-sdk' }
|
|
@@ -171,18 +195,23 @@ function parseNarutoArgs(args = []) {
|
|
|
171
195
|
const clones = clampClones(requestedClones);
|
|
172
196
|
const workItems = clampWorkItems(Number(readOption(args, '--work-items', clones)), clones);
|
|
173
197
|
const concurrency = normalizeConcurrency(readOption(args, '--concurrency', readOption(args, '--target-active-slots', null)), clones);
|
|
174
|
-
const
|
|
198
|
+
const useOllama = hasFlag(args, '--ollama') || hasFlag(args, '--local-model');
|
|
199
|
+
const noOllama = hasFlag(args, '--no-ollama') || hasFlag(args, '--no-local-model');
|
|
200
|
+
const backendExplicit = hasOption(args, '--backend') || useOllama || noOllama;
|
|
201
|
+
const backend = String(readOption(args, '--backend', hasFlag(args, '--mock') ? 'fake' : useOllama && !noOllama ? 'ollama' : 'codex-sdk'));
|
|
175
202
|
const mock = hasFlag(args, '--mock') || backend === 'fake';
|
|
176
203
|
const real = hasFlag(args, '--real');
|
|
177
204
|
const readonly = hasFlag(args, '--readonly') || hasFlag(args, '--read-only');
|
|
178
205
|
const writeModeRaw = String(readOption(args, '--write-mode', hasFlag(args, '--parallel-write') ? 'parallel' : '') || '');
|
|
179
206
|
const writeMode = (['proof-safe', 'parallel', 'serial', 'off'].includes(writeModeRaw) ? writeModeRaw : null);
|
|
180
207
|
const missionId = String(readOption(args, '--mission', readOption(args, '--mission-id', 'latest')));
|
|
208
|
+
const ollamaModel = String(readOption(args, '--ollama-model', readOption(args, '--local-model-model', '')) || '') || null;
|
|
209
|
+
const ollamaBaseUrl = String(readOption(args, '--ollama-base-url', readOption(args, '--local-model-base-url', '')) || '') || null;
|
|
181
210
|
const noOpenZellij = hasFlag(args, '--no-open-zellij') || hasFlag(args, '--no-zellij');
|
|
182
211
|
const attach = hasFlag(args, '--attach');
|
|
183
|
-
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id']);
|
|
212
|
+
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url']);
|
|
184
213
|
const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
|
|
185
|
-
return { action, prompt, clones, workItems, concurrency, backend, mock, real, readonly, writeMode, json, missionId, noOpenZellij, attach };
|
|
214
|
+
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach };
|
|
186
215
|
}
|
|
187
216
|
function clampClones(value) {
|
|
188
217
|
if (!Number.isFinite(value) || value < 1)
|
|
@@ -212,6 +241,9 @@ function readOption(args, name, fallback) {
|
|
|
212
241
|
const prefixed = args.find((arg) => String(arg).startsWith(name + '='));
|
|
213
242
|
return prefixed ? prefixed.slice(name.length + 1) : fallback;
|
|
214
243
|
}
|
|
244
|
+
function hasOption(args, name) {
|
|
245
|
+
return args.includes(name) || args.some((arg) => String(arg).startsWith(name + '='));
|
|
246
|
+
}
|
|
215
247
|
function positionalArgs(args, valueFlags) {
|
|
216
248
|
const out = [];
|
|
217
249
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -233,4 +265,44 @@ function emit(parsed, result, text) {
|
|
|
233
265
|
text();
|
|
234
266
|
return result;
|
|
235
267
|
}
|
|
268
|
+
async function resolveNarutoLocalWorkerMode(parsed) {
|
|
269
|
+
const configInput = {
|
|
270
|
+
ollamaEnabled: parsed.ollamaEnabled,
|
|
271
|
+
model: parsed.ollamaModel,
|
|
272
|
+
baseUrl: parsed.ollamaBaseUrl
|
|
273
|
+
};
|
|
274
|
+
if (parsed.backend === 'ollama')
|
|
275
|
+
configInput.backend = 'ollama';
|
|
276
|
+
const config = await resolveOllamaWorkerConfig(configInput).catch(() => null);
|
|
277
|
+
const policy = classifyOllamaWorkerSlice({
|
|
278
|
+
id: 'naruto-local-worker-probe',
|
|
279
|
+
role: parsed.readonly ? 'collector' : 'implementer',
|
|
280
|
+
description: parsed.prompt,
|
|
281
|
+
write_paths: parsed.readonly ? [] : ['<lease-scoped-worker-path>']
|
|
282
|
+
}, { route: NARUTO_ROUTE, agent: { role: parsed.readonly ? 'collector' : 'implementer' } });
|
|
283
|
+
const autoSelectEligible = parsed.backend === 'codex-sdk'
|
|
284
|
+
&& parsed.backendExplicit !== true
|
|
285
|
+
&& parsed.noOllama !== true
|
|
286
|
+
&& config?.ok === true
|
|
287
|
+
&& config.enabled === true
|
|
288
|
+
&& policy.ok === true;
|
|
289
|
+
return {
|
|
290
|
+
schema: 'sks.naruto-local-worker-mode.v1',
|
|
291
|
+
enabled: config?.enabled === true,
|
|
292
|
+
provider: config?.provider || 'ollama',
|
|
293
|
+
model: config?.model || null,
|
|
294
|
+
requested_backend: parsed.backend,
|
|
295
|
+
backend_explicit: parsed.backendExplicit,
|
|
296
|
+
auto_select_eligible: autoSelectEligible,
|
|
297
|
+
worker_only: true,
|
|
298
|
+
no_strategy_planning_design: true,
|
|
299
|
+
policy,
|
|
300
|
+
blockers: [
|
|
301
|
+
...(config?.blockers || (config ? [] : ['ollama_worker_config_unavailable'])),
|
|
302
|
+
...(policy.blockers || []),
|
|
303
|
+
...(parsed.backendExplicit ? ['backend_explicit'] : []),
|
|
304
|
+
...(parsed.noOllama ? ['no_ollama_requested'] : [])
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
236
308
|
//# sourceMappingURL=naruto-command.js.map
|
|
@@ -231,7 +231,7 @@ async function executeRouteCommand(root, route, prompt, { auto = false } = {}) {
|
|
|
231
231
|
return routeExecutionResult(route, ['sks', ...commandArgs].join(' '), result, {
|
|
232
232
|
okStatus: 'completed',
|
|
233
233
|
trustStatus: 'verified_partial',
|
|
234
|
-
executionKind: route.command === '$DB' || route.command === '$Wiki' || route.command === '$Fast-Mode' || route.command === '$with-local-llm-on' ? 'safe_deterministic' : 'mock_safe',
|
|
234
|
+
executionKind: route.command === '$DB' || route.command === '$Wiki' || route.command === '$Fast-Mode' || route.command === '$with-local-llm-on' || route.command === '$Commit' || route.command === '$Commit-And-Push' ? 'safe_deterministic' : 'mock_safe',
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
async function runAutoVerification(root, missionId) {
|
|
@@ -348,6 +348,10 @@ function safeRouteExecutionArgs(route, prompt, { auto = false } = {}) {
|
|
|
348
348
|
return ['fast-mode', fastModeActionFromPrompt(prompt), '--json'];
|
|
349
349
|
if (route.command === '$with-local-llm-on')
|
|
350
350
|
return ['with-local-llm', localModelActionFromPrompt(prompt), '--json'];
|
|
351
|
+
if (route.command === '$Commit')
|
|
352
|
+
return ['commit', '--json'];
|
|
353
|
+
if (route.command === '$Commit-And-Push')
|
|
354
|
+
return ['commit-and-push', '--json'];
|
|
351
355
|
return ['team', prompt, '--mock', '--json', ...(auto ? ['--no-open-zellij'] : [])];
|
|
352
356
|
}
|
|
353
357
|
function fastModeActionFromPrompt(prompt = '') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { resolveLocalCollaborationPolicy } from '../local-llm/local-collaboration-policy.js';
|
|
3
4
|
export const DOCTOR_READINESS_MATRIX_SCHEMA = 'sks.doctor-readiness-matrix.v1';
|
|
4
5
|
export async function writeDoctorReadinessMatrix(root, input = {}) {
|
|
5
6
|
const matrix = buildDoctorReadinessMatrix(input);
|
|
@@ -53,6 +54,12 @@ export function buildDoctorReadinessMatrix(input = {}) {
|
|
|
53
54
|
warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
|
|
54
55
|
if (input.codex_lb?.ok === false)
|
|
55
56
|
warnings.add(`codex_lb_${input.codex_lb?.circuit?.state || 'blocked'}`);
|
|
57
|
+
const localCollaborationPolicy = resolveLocalCollaborationPolicy({ mode: input.local_collaboration?.mode || null });
|
|
58
|
+
const gptFinalAvailable = input.local_collaboration?.gpt_final_arbiter_available === undefined
|
|
59
|
+
? codexBinOk
|
|
60
|
+
: input.local_collaboration.gpt_final_arbiter_available === true;
|
|
61
|
+
if (localCollaborationPolicy.gpt_final_required && !gptFinalAvailable)
|
|
62
|
+
blockers.add('gpt_final_arbiter_unavailable');
|
|
56
63
|
const codexConfigNode = nodeRead.ok !== false && codexConfig.ok !== false;
|
|
57
64
|
const codexConfigChild = childRead.ok !== false && codexConfig.ok !== false;
|
|
58
65
|
const cliReady = codexBinOk && codexConfigNode && codexConfigChild && cliConfigOk;
|
|
@@ -92,6 +99,14 @@ export function buildDoctorReadinessMatrix(input = {}) {
|
|
|
92
99
|
hooks_ready: input.hooks_ready !== false,
|
|
93
100
|
codex_app_ready: input.codex_app?.ok === true,
|
|
94
101
|
codex_app_required_for_cli: false,
|
|
102
|
+
local_collaboration: {
|
|
103
|
+
mode: localCollaborationPolicy.mode,
|
|
104
|
+
local_backend: input.local_collaboration?.local_backend || input.local_model?.provider || 'ollama',
|
|
105
|
+
local_model: input.local_collaboration?.local_model || input.local_model?.model || null,
|
|
106
|
+
final_arbiter: gptFinalAvailable ? 'GPT available' : 'missing',
|
|
107
|
+
final_apply_allowed: localCollaborationPolicy.gpt_final_required ? gptFinalAvailable : localCollaborationPolicy.mode === 'disabled',
|
|
108
|
+
blockers: localCollaborationPolicy.gpt_final_required && !gptFinalAvailable ? ['gpt_final_arbiter_unavailable'] : localCollaborationPolicy.blockers
|
|
109
|
+
},
|
|
95
110
|
ready: blockers.size === 0 && cliReady,
|
|
96
111
|
primary_blocker: [...blockers][0] || null,
|
|
97
112
|
blockers: [...blockers],
|
|
@@ -25,6 +25,7 @@ const FIXTURES = Object.freeze({
|
|
|
25
25
|
'cli-status': fixture('execute', 'sks status --json', [], 'pass'),
|
|
26
26
|
'cli-usage': fixture('execute', 'sks usage overview', [], 'pass'),
|
|
27
27
|
'cli-quickstart': fixture('execute', 'sks quickstart', [], 'pass'),
|
|
28
|
+
'cli-update': fixture('mock', 'sks update now --dry-run --json', [], 'pass'),
|
|
28
29
|
'cli-update-check': fixture('static', 'sks update-check --json', [], 'pass'),
|
|
29
30
|
'cli-guard': fixture('execute', 'sks guard check --json', [], 'pass'),
|
|
30
31
|
'cli-conflicts': fixture('execute', 'sks conflicts check --json', [], 'pass'),
|
|
@@ -55,6 +56,7 @@ const FIXTURES = Object.freeze({
|
|
|
55
56
|
'cli-stats': fixture('execute', 'sks stats --json', [], 'pass'),
|
|
56
57
|
'cli-dollar-commands': fixture('execute', 'sks dollar-commands --json', [], 'pass'),
|
|
57
58
|
'cli-fast-mode': fixture('execute', 'sks fast-mode status --json', [], 'pass'),
|
|
59
|
+
'cli-with-local-llm': fixture('execute', 'sks with-local-llm status --json', [], 'pass'),
|
|
58
60
|
'cli-dfix': fixture('execute_and_validate_artifacts', 'sks dfix fixture --json', ['completion-proof.json', 'dfix-gate.json', 'dfix-verification.json'], 'pass'),
|
|
59
61
|
'cli-wiki': fixture('execute_and_validate_artifacts', 'sks wiki image-ingest test/fixtures/images/one-by-one.png --json', [{ path: '.sneakoscope/wiki/image-voxel-ledger.json', schema: 'sks.image-voxel-ledger.v1', require_anchors: false }], 'pass'),
|
|
60
62
|
'cli-db': fixture('execute', 'sks db policy', [], 'pass'),
|
|
@@ -112,6 +114,8 @@ const FIXTURES = Object.freeze({
|
|
|
112
114
|
'route-fast-on': fixture('mock', '$Fast-On covered by hermetic fast-mode blackbox toggle test', [], 'pass'),
|
|
113
115
|
'route-fast-off': fixture('mock', '$Fast-Off covered by hermetic fast-mode blackbox toggle test', [], 'pass'),
|
|
114
116
|
'route-local-model': fixture('execute', 'sks with-local-llm status --json', [], 'pass'),
|
|
117
|
+
'route-with-local-llm-on': fixture('mock', '$with-local-llm-on covered by hermetic local-model dollar-command blackbox toggle test', [], 'pass'),
|
|
118
|
+
'route-with-local-llm-off': fixture('mock', '$with-local-llm-off covered by hermetic local-model dollar-command blackbox toggle test', [], 'pass'),
|
|
115
119
|
'route-help': fixture('mock', '$Help lightweight route', [], 'pass'),
|
|
116
120
|
'route-commit': fixture('mock', '$Commit git route', ['completion-proof.json'], 'pass'),
|
|
117
121
|
'route-commit-and-push': fixture('mock', '$Commit-And-Push git route', ['completion-proof.json'], 'pass'),
|
package/dist/core/fsx.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
export const PACKAGE_VERSION = '2.0.
|
|
8
|
+
export const PACKAGE_VERSION = '2.0.4';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
export function nowIso() {
|
package/dist/core/git-simple.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runProcess, projectRoot, isGitRepo, nowIso, sha256 } from './fsx.js';
|
|
2
3
|
import { redactSecrets } from './secret-redaction.js';
|
|
4
|
+
import { runOllamaAgent } from './agents/agent-runner-ollama.js';
|
|
5
|
+
import { resolveOllamaWorkerConfig } from './agents/ollama-worker-config.js';
|
|
3
6
|
const TRAILER = 'Co-authored-by: Codex <noreply@openai.com>';
|
|
4
7
|
export async function simpleGitCommitCommand(args = [], opts = {}) {
|
|
5
8
|
const root = await projectRoot();
|
|
@@ -16,17 +19,24 @@ export async function simpleGitCommitCommand(args = [], opts = {}) {
|
|
|
16
19
|
export async function simpleGitCommit(root, { message = null, push = false } = {}) {
|
|
17
20
|
if (!await isGitRepo(root))
|
|
18
21
|
return { schema: 'sks.simple-git.v1', ok: false, reason: 'not_git_repo', root };
|
|
19
|
-
const before = await
|
|
22
|
+
const [before, branch, head] = await Promise.all([
|
|
23
|
+
git(root, ['status', '--short']),
|
|
24
|
+
git(root, ['branch', '--show-current']),
|
|
25
|
+
git(root, ['rev-parse', '--short', 'HEAD'])
|
|
26
|
+
]);
|
|
20
27
|
const changed = statusLines(before.stdout);
|
|
21
28
|
if (!changed.length)
|
|
22
29
|
return { schema: 'sks.simple-git.v1', ok: false, reason: 'no_changes', root };
|
|
30
|
+
const localWorker = message
|
|
31
|
+
? localWorkerSkipped('message_provided')
|
|
32
|
+
: await draftCommitMessageWithLocalWorker(root, changed, { push, branch: branch.stdout.trim(), head: head.stdout.trim() });
|
|
23
33
|
const add = await git(root, ['add', '-A']);
|
|
24
34
|
if (add.code !== 0)
|
|
25
35
|
return failure(root, 'git_add_failed', before, add);
|
|
26
36
|
const stagedCheck = await git(root, ['diff', '--cached', '--quiet']);
|
|
27
37
|
if (stagedCheck.code === 0)
|
|
28
38
|
return { schema: 'sks.simple-git.v1', ok: false, reason: 'no_staged_changes', root, changed };
|
|
29
|
-
const commitMessage = ensureCodexTrailer(message || buildCommitMessage(changed));
|
|
39
|
+
const commitMessage = ensureCodexTrailer(message || localWorker.message || buildCommitMessage(changed));
|
|
30
40
|
const commit = await git(root, ['commit', '-m', commitTitle(commitMessage), '-m', commitBody(commitMessage)]);
|
|
31
41
|
if (commit.code !== 0)
|
|
32
42
|
return failure(root, 'git_commit_failed', before, commit);
|
|
@@ -46,7 +56,8 @@ export async function simpleGitCommit(root, { message = null, push = false } = {
|
|
|
46
56
|
commit: commitSummary(commit),
|
|
47
57
|
hash: hash.stdout.trim(),
|
|
48
58
|
pushed: Boolean(push && pushResult?.code === 0),
|
|
49
|
-
push: pushResult ? { ok: pushResult.code === 0, stdout: pushResult.stdout.trim(), stderr: pushResult.stderr.trim() } : null
|
|
59
|
+
push: pushResult ? { ok: pushResult.code === 0, stdout: pushResult.stdout.trim(), stderr: pushResult.stderr.trim() } : null,
|
|
60
|
+
local_worker: localWorker.report
|
|
50
61
|
});
|
|
51
62
|
}
|
|
52
63
|
function failure(root, reason, before, failed, extra = {}) {
|
|
@@ -77,6 +88,134 @@ function buildCommitMessage(changed = []) {
|
|
|
77
88
|
const more = changed.length > 12 ? `\n- ...and ${changed.length - 12} more` : '';
|
|
78
89
|
return `chore: update project changes\n\nSummary: ${summary}\n\nChanged files:\n${files}${more}`;
|
|
79
90
|
}
|
|
91
|
+
async function draftCommitMessageWithLocalWorker(root, changed, context) {
|
|
92
|
+
const disabled = String(process.env.SKS_SIMPLE_GIT_LOCAL_LLM || '').trim() === '0';
|
|
93
|
+
if (disabled)
|
|
94
|
+
return { message: null, report: localWorkerSkipped('disabled_by_SKS_SIMPLE_GIT_LOCAL_LLM') };
|
|
95
|
+
const config = await resolveOllamaWorkerConfig().catch((error) => null);
|
|
96
|
+
if (!config?.ok || config.enabled !== true) {
|
|
97
|
+
return {
|
|
98
|
+
message: null,
|
|
99
|
+
report: {
|
|
100
|
+
schema: 'sks.simple-git-local-worker.v1',
|
|
101
|
+
generated_at: nowIso(),
|
|
102
|
+
ok: false,
|
|
103
|
+
used: false,
|
|
104
|
+
enabled: config?.enabled === true,
|
|
105
|
+
provider: config?.provider || 'ollama',
|
|
106
|
+
model: config?.model || null,
|
|
107
|
+
worker_only: true,
|
|
108
|
+
parent_owned_git_mutation: true,
|
|
109
|
+
task: context.push ? 'commit-and-push-message-draft' : 'commit-message-draft',
|
|
110
|
+
blockers: config?.blockers || ['ollama_worker_config_unavailable']
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const runId = sha256(`${nowIso()}:${context.branch}:${context.head}:${changed.join('\n')}`).slice(0, 12);
|
|
115
|
+
const workerDirRel = path.join('.git', 'sks-local-workers', 'simple-git', runId);
|
|
116
|
+
const slice = {
|
|
117
|
+
id: `simple-git-message-${runId}`,
|
|
118
|
+
role: 'collector',
|
|
119
|
+
domain: 'git',
|
|
120
|
+
description: [
|
|
121
|
+
'simple collect summarize git status for a commit message draft only',
|
|
122
|
+
'Do not run git commands. Do not choose whether to commit or push.',
|
|
123
|
+
'Return summary as a conventional commit title and proposed_changes as short body bullets.',
|
|
124
|
+
`Action: ${context.push ? 'commit-and-push' : 'commit'}`,
|
|
125
|
+
`Branch: ${context.branch || 'unknown'}`,
|
|
126
|
+
`HEAD before commit: ${context.head || 'unknown'}`,
|
|
127
|
+
'Changed status lines:',
|
|
128
|
+
...changed.slice(0, 80)
|
|
129
|
+
].join('\n')
|
|
130
|
+
};
|
|
131
|
+
const agent = {
|
|
132
|
+
id: 'simple_git_local_worker',
|
|
133
|
+
session_id: `simple-git-${runId}`,
|
|
134
|
+
slot_id: 'local-worker',
|
|
135
|
+
generation_index: 1,
|
|
136
|
+
persona_id: 'local_git_summarizer',
|
|
137
|
+
role: 'collector'
|
|
138
|
+
};
|
|
139
|
+
const result = await runOllamaAgent(agent, slice, {
|
|
140
|
+
missionId: `simple-git-${runId}`,
|
|
141
|
+
agentRoot: root,
|
|
142
|
+
cwd: root,
|
|
143
|
+
workerDirRel,
|
|
144
|
+
route: context.push ? '$Commit-And-Push' : '$Commit',
|
|
145
|
+
fastMode: true,
|
|
146
|
+
serviceTier: 'fast',
|
|
147
|
+
ollamaTimeoutMs: Number(process.env.SKS_SIMPLE_GIT_OLLAMA_TIMEOUT_MS || 15000)
|
|
148
|
+
}).catch((error) => ({
|
|
149
|
+
status: 'blocked',
|
|
150
|
+
summary: '',
|
|
151
|
+
findings: [],
|
|
152
|
+
proposed_changes: [],
|
|
153
|
+
artifacts: [],
|
|
154
|
+
blockers: [error instanceof Error ? error.message : String(error)]
|
|
155
|
+
}));
|
|
156
|
+
const message = result.status === 'done' ? localWorkerCommitMessage(result, changed) : null;
|
|
157
|
+
return {
|
|
158
|
+
message,
|
|
159
|
+
report: {
|
|
160
|
+
schema: 'sks.simple-git-local-worker.v1',
|
|
161
|
+
generated_at: nowIso(),
|
|
162
|
+
ok: result.status === 'done',
|
|
163
|
+
used: Boolean(message),
|
|
164
|
+
enabled: true,
|
|
165
|
+
provider: config.provider,
|
|
166
|
+
model: config.model,
|
|
167
|
+
worker_only: true,
|
|
168
|
+
parent_owned_git_mutation: true,
|
|
169
|
+
task: context.push ? 'commit-and-push-message-draft' : 'commit-message-draft',
|
|
170
|
+
artifacts: result.artifacts || [],
|
|
171
|
+
summary: result.summary || null,
|
|
172
|
+
blockers: result.blockers || [],
|
|
173
|
+
fallback: message ? null : 'deterministic_commit_message'
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function localWorkerCommitMessage(result, changed) {
|
|
178
|
+
const title = normalizeCommitTitle(result.summary);
|
|
179
|
+
if (!title)
|
|
180
|
+
return null;
|
|
181
|
+
const bodyLines = [
|
|
182
|
+
'Summary: local Ollama worker drafted this message from git status; parent SKS performed all git mutations.',
|
|
183
|
+
'',
|
|
184
|
+
...stringArray(result.proposed_changes || result.findings).slice(0, 8).map((line) => `- ${line}`),
|
|
185
|
+
...(stringArray(result.proposed_changes || result.findings).length ? [] : changed.slice(0, 8).map((line) => `- ${line}`))
|
|
186
|
+
];
|
|
187
|
+
return `${title}\n\n${bodyLines.join('\n')}`;
|
|
188
|
+
}
|
|
189
|
+
function normalizeCommitTitle(value) {
|
|
190
|
+
const raw = String(value || '').split(/\r?\n/)[0]?.trim() || '';
|
|
191
|
+
const stripped = raw.replace(/^["']|["']$/g, '').replace(/\s+/g, ' ').trim();
|
|
192
|
+
if (!stripped)
|
|
193
|
+
return '';
|
|
194
|
+
const conventional = /^[a-z][a-z0-9-]*(\([^)]+\))?:\s+\S/.test(stripped);
|
|
195
|
+
const title = conventional ? stripped : `chore: ${stripped.replace(/^(commit message|summary)\s*:\s*/i, '')}`;
|
|
196
|
+
return title.length > 96 ? title.slice(0, 93).trimEnd() + '...' : title;
|
|
197
|
+
}
|
|
198
|
+
function stringArray(value) {
|
|
199
|
+
return Array.isArray(value) ? value.map((line) => String(line || '').trim()).filter(Boolean) : [];
|
|
200
|
+
}
|
|
201
|
+
function localWorkerSkipped(reason) {
|
|
202
|
+
return {
|
|
203
|
+
message: null,
|
|
204
|
+
report: {
|
|
205
|
+
schema: 'sks.simple-git-local-worker.v1',
|
|
206
|
+
generated_at: nowIso(),
|
|
207
|
+
ok: true,
|
|
208
|
+
used: false,
|
|
209
|
+
enabled: false,
|
|
210
|
+
provider: 'ollama',
|
|
211
|
+
model: null,
|
|
212
|
+
worker_only: true,
|
|
213
|
+
parent_owned_git_mutation: true,
|
|
214
|
+
task: 'commit-message-draft',
|
|
215
|
+
blockers: [reason]
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
80
219
|
function ensureCodexTrailer(message = '') {
|
|
81
220
|
const withoutDuplicate = String(message || '')
|
|
82
221
|
.split(/\r?\n/)
|