@shrkcrft/cli 0.1.0-alpha.26 → 0.1.0-alpha.27
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/dist/command-registry.d.ts +12 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +25 -0
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +21 -0
- package/dist/commands/delegate.command.d.ts +76 -1
- package/dist/commands/delegate.command.d.ts.map +1 -1
- package/dist/commands/delegate.command.js +585 -25
- package/dist/commands/finish.command.js +4 -4
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +73 -0
- package/dist/commands/registry-resolve.d.ts +11 -4
- package/dist/commands/registry-resolve.d.ts.map +1 -1
- package/dist/commands/registry-resolve.js +50 -24
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +7 -1
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +88 -14
- package/dist/exit-codes.d.ts +41 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +85 -0
- package/dist/finish/run-finish.d.ts +22 -3
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +185 -15
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +21 -4
- package/dist/output/output-compression.d.ts.map +1 -1
- package/dist/output/output-compression.js +4 -1
- package/package.json +33 -33
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* through the same apply primitives `shrk apply` uses. A failed verification
|
|
12
12
|
* auto-reverts the edit, so a bad generation costs a retry, never a wrong write.
|
|
13
13
|
*/
|
|
14
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import * as nodePath from 'node:path';
|
|
16
16
|
import { containsTraversal, safeResolveTargetPath } from '@shrkcrft/core';
|
|
17
|
-
import { AiMessageRole, callDelegateWithRetry, delegateRepromptMessage, selectAiProvider, } from '@shrkcrft/ai';
|
|
17
|
+
import { AiMessageRole, DELEGATE_ANALYSIS_JSON_SCHEMA, callDelegateWithRetry, callDelegateAnalysisWithRetry, delegateAnalysisRepromptMessage, delegateRepromptMessage, parseDelegateAnalysis, runBoundedQueryLoop, selectAiProvider, } from '@shrkcrft/ai';
|
|
18
18
|
import { loadProjectConfig } from '@shrkcrft/config';
|
|
19
19
|
import { compressCode, compressDiff } from '@shrkcrft/compress';
|
|
20
20
|
import { listIndexableFiles } from '@shrkcrft/embeddings';
|
|
21
|
-
import {
|
|
21
|
+
import { loadGraphApiCached } from '@shrkcrft/graph';
|
|
22
|
+
import { analyzeTestImpact, buildCoverageReport, buildDelegateAnalysisReport, buildDelegateFailureFacts, buildTaskRiskReport, checkGuardrailGlobs, crossCheckFindings, inspectSharkcraft, resolveDelegateCatalogForProject, runGroundingReport, unifiedDiff, } from '@shrkcrft/inspector';
|
|
22
23
|
import { evaluateSavedPlanInPlace, packageDelegatePlan, savePlanToFile, signPlan, verifyPlan, writeSyntheticPlan, } from '@shrkcrft/generator';
|
|
23
24
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
24
25
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
@@ -56,7 +57,7 @@ export function gatherRecipeContext(projectRoot, recipe) {
|
|
|
56
57
|
let candidates;
|
|
57
58
|
try {
|
|
58
59
|
const all = listIndexableFiles(projectRoot, 3000);
|
|
59
|
-
candidates = checkGuardrailGlobs(all, recipe.guardrailGlobs).allowed;
|
|
60
|
+
candidates = checkGuardrailGlobs(all, recipe.guardrailGlobs ?? []).allowed;
|
|
60
61
|
}
|
|
61
62
|
catch {
|
|
62
63
|
return '';
|
|
@@ -79,12 +80,14 @@ export function gatherRecipeContext(projectRoot, recipe) {
|
|
|
79
80
|
return `Files in scope you may edit (current contents):\n\n${blocks.join('\n\n')}${more}`;
|
|
80
81
|
}
|
|
81
82
|
function systemPrompt(recipe) {
|
|
82
|
-
const
|
|
83
|
+
const allowedOps = recipe.allowedOps ?? [];
|
|
84
|
+
const guardrailGlobs = recipe.guardrailGlobs ?? [];
|
|
85
|
+
const example = allowedOps.map(opExample).find((e) => e !== null);
|
|
83
86
|
const lines = [
|
|
84
87
|
'You are a deterministic mechanical code-edit worker.',
|
|
85
88
|
'Output ONLY a single JSON object matching the provided schema — no prose, no markdown fences.',
|
|
86
|
-
`You may emit ONLY operations of these kinds: ${
|
|
87
|
-
`You may target ONLY files matching one of these globs: ${
|
|
89
|
+
`You may emit ONLY operations of these kinds: ${allowedOps.join(', ')}.`,
|
|
90
|
+
`You may target ONLY files matching one of these globs: ${guardrailGlobs.join(', ')}.`,
|
|
88
91
|
'Make the SMALLEST mechanical edit that satisfies the task. Never invent files, never change unrelated code, never reformat.',
|
|
89
92
|
'Each op has a "targetPath" (relative to project root) and an "operation" with a "kind" and the fields that kind needs.',
|
|
90
93
|
];
|
|
@@ -136,12 +139,43 @@ export async function executeDelegateRun(input) {
|
|
|
136
139
|
last = { ...(await runOneDelegateAttempt(input, [...baseMessages, ...feedback])), attempts: attempt };
|
|
137
140
|
if (!RETRYABLE_STATUSES.has(last.status) || attempt === maxAttempts)
|
|
138
141
|
return last;
|
|
139
|
-
|
|
142
|
+
// Best-effort LLM diagnosis of the failure (Phase 2). Never blocks the loop:
|
|
143
|
+
// a null return or a throw simply means no extra hint this round.
|
|
144
|
+
let advisory;
|
|
145
|
+
if (input.retryAdvisor) {
|
|
146
|
+
try {
|
|
147
|
+
const adv = await input.retryAdvisor(failureContextOf(last, input.recipe, attempt), attempt);
|
|
148
|
+
advisory = adv ?? undefined;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
advisory = undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
feedback.push(buildRetryFeedback(last, input.recipe, advisory));
|
|
140
155
|
}
|
|
141
156
|
return last;
|
|
142
157
|
}
|
|
143
|
-
/**
|
|
144
|
-
function
|
|
158
|
+
/** Map a delegate run result onto the layer-neutral failure context for grounding. */
|
|
159
|
+
function failureContextOf(r, recipe, attempt) {
|
|
160
|
+
return {
|
|
161
|
+
status: r.status,
|
|
162
|
+
message: r.message,
|
|
163
|
+
...(r.conflicts ? { conflicts: r.conflicts } : {}),
|
|
164
|
+
...(r.verification?.commandsFailed ? { commandsFailed: r.verification.commandsFailed } : {}),
|
|
165
|
+
...(r.refused ? { refused: r.refused } : {}),
|
|
166
|
+
...(r.droppedOps ? { droppedOps: r.droppedOps.map((d) => ({ kind: d.kind, targetPath: d.targetPath })) } : {}),
|
|
167
|
+
...(recipe.guardrailGlobs ? { guardrailGlobs: recipe.guardrailGlobs } : {}),
|
|
168
|
+
...(recipe.allowedOps ? { allowedOps: recipe.allowedOps } : {}),
|
|
169
|
+
attempt,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the User message that tells the worker why the previous attempt failed.
|
|
174
|
+
* `advisory` (optional) is a corrected-instruction hint from a `retry-analysis`
|
|
175
|
+
* pass (Phase 2, `--assisted-retry`); it augments — never replaces — the
|
|
176
|
+
* deterministic failure detail.
|
|
177
|
+
*/
|
|
178
|
+
function buildRetryFeedback(r, recipe, advisory) {
|
|
145
179
|
let detail;
|
|
146
180
|
if (r.conflicts && r.conflicts.length > 0) {
|
|
147
181
|
detail = `Your previous edit was REFUSED with conflicts: ${r.conflicts.join('; ')}. Fix the target paths / anchors and try again.`;
|
|
@@ -150,17 +184,18 @@ function buildRetryFeedback(r, recipe) {
|
|
|
150
184
|
detail = `Your previous edit FAILED verification (${r.verification?.commandsFailed.join(', ') || 'see logs'}) and was reverted. Produce a CORRECT edit.`;
|
|
151
185
|
}
|
|
152
186
|
else if (r.status === 'guardrail-refused') {
|
|
153
|
-
detail = `You targeted files outside the allowed scope (${(r.refused ?? []).join(', ')}). You may ONLY touch files matching: ${recipe.guardrailGlobs.join(', ')}.`;
|
|
187
|
+
detail = `You targeted files outside the allowed scope (${(r.refused ?? []).join(', ')}). You may ONLY touch files matching: ${(recipe.guardrailGlobs ?? []).join(', ')}.`;
|
|
154
188
|
}
|
|
155
189
|
else if (r.status === 'package-error') {
|
|
156
|
-
detail = `${r.message}. You may ONLY use op kinds: ${recipe.allowedOps.join(', ')}.`;
|
|
190
|
+
detail = `${r.message}. You may ONLY use op kinds: ${(recipe.allowedOps ?? []).join(', ')}.`;
|
|
157
191
|
}
|
|
158
192
|
else {
|
|
159
193
|
detail = `Your previous reply was unusable: ${r.message}.`;
|
|
160
194
|
}
|
|
195
|
+
const advisoryLine = advisory && advisory.trim().length > 0 ? `\nDiagnostic hint: ${advisory.trim()}` : '';
|
|
161
196
|
return {
|
|
162
197
|
role: AiMessageRole.User,
|
|
163
|
-
content: `${detail}\nReturn a corrected single JSON object matching the schema — no prose.`,
|
|
198
|
+
content: `${detail}${advisoryLine}\nReturn a corrected single JSON object matching the schema — no prose.`,
|
|
164
199
|
};
|
|
165
200
|
}
|
|
166
201
|
/** One generate→guardrail→package→sign→apply→verify pass. */
|
|
@@ -215,7 +250,7 @@ async function runOneDelegateAttempt(input, messages) {
|
|
|
215
250
|
};
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
|
-
const guard = checkGuardrailGlobs(normalizedTargets, recipe.guardrailGlobs);
|
|
253
|
+
const guard = checkGuardrailGlobs(normalizedTargets, recipe.guardrailGlobs ?? []);
|
|
219
254
|
if (!guard.ok) {
|
|
220
255
|
return {
|
|
221
256
|
status: 'guardrail-refused',
|
|
@@ -228,7 +263,7 @@ async function runOneDelegateAttempt(input, messages) {
|
|
|
228
263
|
// 4. Package into a synthetic plan (drops disallowed ops, evaluates conflicts).
|
|
229
264
|
const packaged = packageDelegatePlan({
|
|
230
265
|
ops: edit.ops,
|
|
231
|
-
allowedOps: recipe.allowedOps,
|
|
266
|
+
allowedOps: recipe.allowedOps ?? [],
|
|
232
267
|
recipeId: recipe.id,
|
|
233
268
|
projectRoot,
|
|
234
269
|
});
|
|
@@ -286,7 +321,7 @@ async function runOneDelegateAttempt(input, messages) {
|
|
|
286
321
|
// A recipe with no verification has no deterministic gate — refuse to apply an
|
|
287
322
|
// unverified edit (runValidationLoop reports passed:true when no command runs,
|
|
288
323
|
// so this must be caught here). The plan is already signed + saved on disk.
|
|
289
|
-
if (recipe.verificationIds.length === 0) {
|
|
324
|
+
if ((recipe.verificationIds ?? []).length === 0) {
|
|
290
325
|
return {
|
|
291
326
|
...baseResult,
|
|
292
327
|
status: 'no-verification',
|
|
@@ -319,7 +354,7 @@ async function runOneDelegateAttempt(input, messages) {
|
|
|
319
354
|
// 7. Deterministic verification gate.
|
|
320
355
|
const validation = await runValidationLoop({
|
|
321
356
|
cwd: projectRoot,
|
|
322
|
-
verificationIds: recipe.verificationIds,
|
|
357
|
+
verificationIds: recipe.verificationIds ?? [],
|
|
323
358
|
allVerifications: false,
|
|
324
359
|
allowPackCommands: false,
|
|
325
360
|
reportDir: input.reportDir ?? nodePath.join(projectRoot, '.sharkcraft', 'delegate', 'reports'),
|
|
@@ -435,7 +470,8 @@ async function resolveRecipe(cwd, recipeId) {
|
|
|
435
470
|
if (!found) {
|
|
436
471
|
return { ok: false, message: `unknown recipe "${recipeId}". Available: ${c.catalog.map((r) => r.id).join(', ')}` };
|
|
437
472
|
}
|
|
438
|
-
// Fold the resolved provider/model onto the recipe
|
|
473
|
+
// Fold the resolved provider/model onto the recipe (keeping the resolved type
|
|
474
|
+
// so patch consumers see present write-fence arrays) for the run/analyze cores.
|
|
439
475
|
const recipe = {
|
|
440
476
|
...found,
|
|
441
477
|
provider: found.resolvedProvider,
|
|
@@ -443,7 +479,479 @@ async function resolveRecipe(cwd, recipeId) {
|
|
|
443
479
|
};
|
|
444
480
|
return { ok: true, recipe, projectRoot: c.projectRoot };
|
|
445
481
|
}
|
|
482
|
+
/** Human descriptions of the read-only queries shown to the model in the loop. */
|
|
483
|
+
export const DELEGATE_QUERY_CATALOG = [
|
|
484
|
+
{ name: 'task-risk', description: 'deterministic per-task risk (level, reasons, affected files). args: {task?}' },
|
|
485
|
+
{ name: 'coverage', description: 'project-intelligence coverage gaps (weakest categories). args: {}' },
|
|
486
|
+
{ name: 'test-impact', description: 'likely + missing tests and risk areas. args: {files?: string[]}' },
|
|
487
|
+
{ name: 'graph-callers', description: 'who calls/references a symbol, as path:line. args: {symbol: string}' },
|
|
488
|
+
{ name: 'graph-context', description: 'a symbol\'s declaring file + importers/imports (is it wired?). args: {symbol: string}' },
|
|
489
|
+
];
|
|
490
|
+
/**
|
|
491
|
+
* Build the read-only query executor: maps each `DELEGATE_QUERY_IDS` value to a
|
|
492
|
+
* deterministic inspector function. Every query is read-only — there is no
|
|
493
|
+
* write/apply query. Returns compact text + the entities it surfaced (merged
|
|
494
|
+
* into the analysis ground truth so a finding citing a pulled fact is grounded).
|
|
495
|
+
*/
|
|
496
|
+
export function buildAnalysisQueryExecutor(inspection, task) {
|
|
497
|
+
// The graph index is loaded lazily (only when a graph query is actually asked)
|
|
498
|
+
// and cached across queries in one run. `undefined` = not yet attempted.
|
|
499
|
+
let graphApi;
|
|
500
|
+
const graph = () => {
|
|
501
|
+
if (graphApi === undefined)
|
|
502
|
+
graphApi = loadGraphApiCached(inspection.projectRoot);
|
|
503
|
+
return graphApi;
|
|
504
|
+
};
|
|
505
|
+
return async (name, args) => {
|
|
506
|
+
switch (name) {
|
|
507
|
+
case 'task-risk': {
|
|
508
|
+
const r = await buildTaskRiskReport(typeof args.task === 'string' ? args.task : task, inspection, {});
|
|
509
|
+
const entities = [...r.affectedFiles, ...r.highFanInFiles, ...r.reasons.map((x) => x.code)];
|
|
510
|
+
const content = `risk=${r.riskLevel} score=${r.score}; reasons: ${r.reasons.slice(0, 6).map((x) => x.code).join(', ') || '(none)'}; files: ${r.affectedFiles.slice(0, 8).join(', ') || '(none)'}`;
|
|
511
|
+
return { content, entities };
|
|
512
|
+
}
|
|
513
|
+
case 'coverage': {
|
|
514
|
+
const c = buildCoverageReport(inspection);
|
|
515
|
+
const weak = c.categories.filter((cat) => cat.score < 100).slice(0, 6);
|
|
516
|
+
return {
|
|
517
|
+
content: `overall=${c.overall}%; weakest: ${weak.map((cat) => `${cat.id} ${cat.score}%`).join(', ') || '(all covered)'}`,
|
|
518
|
+
entities: weak.map((cat) => cat.id),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
case 'test-impact': {
|
|
522
|
+
const files = Array.isArray(args.files)
|
|
523
|
+
? args.files.filter((x) => typeof x === 'string')
|
|
524
|
+
: [];
|
|
525
|
+
const t = analyzeTestImpact(inspection, { task, ...(files.length > 0 ? { files } : {}) });
|
|
526
|
+
return {
|
|
527
|
+
content: `existing: ${t.likelyTestFiles.slice(0, 8).join(', ') || '(none)'}; missing: ${t.missingTestFiles.slice(0, 8).join(', ') || '(none)'}; risk: ${t.riskAreas.slice(0, 4).join('; ') || '(none)'}`,
|
|
528
|
+
entities: [...t.missingTestFiles, ...t.likelyTestFiles, ...t.riskAreas],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
case 'graph-callers': {
|
|
532
|
+
const api = graph();
|
|
533
|
+
if (!api)
|
|
534
|
+
return { content: 'graph index missing — run `shrk graph index`', entities: [] };
|
|
535
|
+
const symbol = typeof args.symbol === 'string' ? args.symbol.trim() : '';
|
|
536
|
+
if (!symbol)
|
|
537
|
+
return { content: 'graph-callers needs {symbol}', entities: [] };
|
|
538
|
+
const sym = api.findSymbol(symbol, { limit: 1 })[0];
|
|
539
|
+
if (!sym)
|
|
540
|
+
return { content: `no symbol matched "${symbol}"`, entities: [] };
|
|
541
|
+
const sites = api.callerSitesOf(sym.id).slice(0, 15);
|
|
542
|
+
const lines = sites.map((s) => `${s.node.path ?? s.node.label}${s.line ? `:${s.line}` : ''}`);
|
|
543
|
+
return {
|
|
544
|
+
content: `${symbol}: ${sites.length} caller site(s)\n${lines.join('\n') || '(none)'}`,
|
|
545
|
+
entities: sites.map((s) => s.node.path ?? '').filter((p) => p.length > 0),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
case 'graph-context': {
|
|
549
|
+
const api = graph();
|
|
550
|
+
if (!api)
|
|
551
|
+
return { content: 'graph index missing — run `shrk graph index`', entities: [] };
|
|
552
|
+
const symbol = typeof args.symbol === 'string' ? args.symbol.trim() : '';
|
|
553
|
+
if (!symbol)
|
|
554
|
+
return { content: 'graph-context needs {symbol}', entities: [] };
|
|
555
|
+
const sym = api.findSymbol(symbol, { limit: 1 })[0];
|
|
556
|
+
if (!sym)
|
|
557
|
+
return { content: `no symbol matched "${symbol}"`, entities: [] };
|
|
558
|
+
const file = api.declaringFileOf(sym.id);
|
|
559
|
+
const importers = file ? api.importersOf(file.id).slice(0, 10) : [];
|
|
560
|
+
const imports = file ? api.importsFrom(file.id).slice(0, 10) : [];
|
|
561
|
+
const ent = [sym.path, file?.path, ...importers.map((n) => n.path), ...imports.map((n) => n.path)].filter((x) => typeof x === 'string' && x.length > 0);
|
|
562
|
+
return {
|
|
563
|
+
content: `${symbol} declared in ${sym.path ?? file?.path ?? '?'}${sym.line ? `:${sym.line}` : ''}; importers=${importers.length}, imports=${imports.length}`,
|
|
564
|
+
entities: ent,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
return { content: `unknown query "${name}"`, entities: [] };
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** Split `xs` into `g` contiguous, deterministic chunks (last may be smaller). */
|
|
573
|
+
function contiguousChunks(xs, g) {
|
|
574
|
+
const groups = Math.max(1, Math.min(g, xs.length));
|
|
575
|
+
const size = Math.ceil(xs.length / groups);
|
|
576
|
+
const out = [];
|
|
577
|
+
for (let i = 0; i < xs.length; i += size)
|
|
578
|
+
out.push(xs.slice(i, i + size));
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
/** Dedup raw findings by normalised message, preserving first-seen order. */
|
|
582
|
+
function dedupeFindingsByMessage(findings) {
|
|
583
|
+
const seen = new Set();
|
|
584
|
+
const out = [];
|
|
585
|
+
for (const f of findings) {
|
|
586
|
+
const key = f.message.trim().toLowerCase();
|
|
587
|
+
if (key.length === 0 || seen.has(key))
|
|
588
|
+
continue;
|
|
589
|
+
seen.add(key);
|
|
590
|
+
out.push(f);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
function analysisSystemPrompt(recipe) {
|
|
595
|
+
return [
|
|
596
|
+
'You are a READ-ONLY code-analysis assistant. You add judgment on top of a deterministic report; you never propose edits.',
|
|
597
|
+
'Output ONLY a single JSON object matching the provided schema — no prose, no markdown fences.',
|
|
598
|
+
'Return a "findings" array. Each finding has a "message" (your judgment) and "refs" (the files / constructs / reason-codes it is about).',
|
|
599
|
+
'CRITICAL: every ref MUST be copied verbatim from the ground truth provided below. Do NOT invent files, symbols, or codes. If you cannot ground a claim in the report, omit it.',
|
|
600
|
+
`Recipe: ${recipe.title ?? recipe.id} — prioritise and explain the risks; flag which ones need human judgment.`,
|
|
601
|
+
].join('\n');
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* The testable analysis core (no I/O beyond the injected provider). Runs the
|
|
605
|
+
* model on the deterministic ground truth, cross-checks its findings, and
|
|
606
|
+
* assembles the advisory report. NEVER writes — analysis mode is read-only.
|
|
607
|
+
* `provider === null` degrades to a deterministic grounding-only report (ok).
|
|
608
|
+
*/
|
|
609
|
+
export async function executeDelegateAnalyze(input) {
|
|
610
|
+
const { recipe, task, facts, provider } = input;
|
|
611
|
+
// No local LLM → deterministic grounding only (NOT an error).
|
|
612
|
+
if (provider === null) {
|
|
613
|
+
const crossCheck = crossCheckFindings([], facts, {});
|
|
614
|
+
return {
|
|
615
|
+
status: 'no-provider',
|
|
616
|
+
recipeId: recipe.id,
|
|
617
|
+
report: buildDelegateAnalysisReport({
|
|
618
|
+
recipeId: recipe.id,
|
|
619
|
+
groundedOn: facts.groundedOn,
|
|
620
|
+
task,
|
|
621
|
+
provider: 'none',
|
|
622
|
+
facts,
|
|
623
|
+
crossCheck,
|
|
624
|
+
modelUnavailable: true,
|
|
625
|
+
generatedAt: input.generatedAt,
|
|
626
|
+
}),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const messages = [
|
|
630
|
+
{ role: AiMessageRole.System, content: analysisSystemPrompt(recipe) },
|
|
631
|
+
{
|
|
632
|
+
role: AiMessageRole.User,
|
|
633
|
+
content: `Task: ${task}\n\nGround truth (do NOT contradict; cite only entities from it in "refs"):\n${facts.summary}`,
|
|
634
|
+
},
|
|
635
|
+
];
|
|
636
|
+
const model = recipe.model ?? recipe.resolvedModel;
|
|
637
|
+
// Phase 4 — fan-out. Split the grounding entities into deterministic slices,
|
|
638
|
+
// run one focused pass per slice, then merge + dedup. Engine-owned control flow
|
|
639
|
+
// (the model never schedules anything). Takes precedence over the query loop.
|
|
640
|
+
if (recipe.fanOut === true && facts.entities.length >= 2) {
|
|
641
|
+
const g = Math.max(2, Math.min(6, recipe.maxFanOut ?? 3));
|
|
642
|
+
const slices = contiguousChunks(facts.entities, g);
|
|
643
|
+
const merged = [];
|
|
644
|
+
let anySuccess = false;
|
|
645
|
+
for (const slice of slices) {
|
|
646
|
+
const sliceMessages = [
|
|
647
|
+
{ role: AiMessageRole.System, content: analysisSystemPrompt(recipe) },
|
|
648
|
+
{
|
|
649
|
+
role: AiMessageRole.User,
|
|
650
|
+
content: `Task: ${task}\n\nFocus your analysis ONLY on these items from the ground truth: ${slice.join(', ')}.\n\nGround truth (cite refs only from it):\n${facts.summary}`,
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
const call = await callDelegateAnalysisWithRetry({
|
|
654
|
+
provider,
|
|
655
|
+
messages: sliceMessages,
|
|
656
|
+
...(model ? { model } : {}),
|
|
657
|
+
timeoutMs: recipe.maxBudgetMs ?? DEFAULT_MAX_BUDGET_MS,
|
|
658
|
+
});
|
|
659
|
+
if (call.ok) {
|
|
660
|
+
anySuccess = true;
|
|
661
|
+
merged.push(...call.value.analysis.findings);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (!anySuccess) {
|
|
665
|
+
return {
|
|
666
|
+
status: 'analyze-failed',
|
|
667
|
+
recipeId: recipe.id,
|
|
668
|
+
report: buildDelegateAnalysisReport({
|
|
669
|
+
recipeId: recipe.id,
|
|
670
|
+
groundedOn: facts.groundedOn,
|
|
671
|
+
task,
|
|
672
|
+
provider: input.providerLabel,
|
|
673
|
+
facts,
|
|
674
|
+
crossCheck: crossCheckFindings([], facts, {}),
|
|
675
|
+
modelUnavailable: true,
|
|
676
|
+
modelNote: 'every fan-out slice failed to produce a valid analysis',
|
|
677
|
+
fanOutSlices: slices.length,
|
|
678
|
+
generatedAt: input.generatedAt,
|
|
679
|
+
}),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const crossCheck = crossCheckFindings(dedupeFindingsByMessage(merged), facts, { strict: input.strict ?? false });
|
|
683
|
+
return {
|
|
684
|
+
status: 'analyzed',
|
|
685
|
+
recipeId: recipe.id,
|
|
686
|
+
report: buildDelegateAnalysisReport({
|
|
687
|
+
recipeId: recipe.id,
|
|
688
|
+
groundedOn: facts.groundedOn,
|
|
689
|
+
task,
|
|
690
|
+
provider: input.providerLabel,
|
|
691
|
+
facts,
|
|
692
|
+
crossCheck,
|
|
693
|
+
fanOutSlices: slices.length,
|
|
694
|
+
...(recipe.escalateTo ? { escalateTo: recipe.escalateTo } : {}),
|
|
695
|
+
generatedAt: input.generatedAt,
|
|
696
|
+
}),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
// Phase 3 — bounded read-only query loop. When the recipe opts in
|
|
700
|
+
// (allowedQueries + maxQueryRounds > 0) and a query executor is injected, let
|
|
701
|
+
// the model pull a few read-only facts before answering; entities it surfaces
|
|
702
|
+
// are merged into the ground truth so a finding citing them is grounded.
|
|
703
|
+
if ((recipe.allowedQueries?.length ?? 0) > 0 && (recipe.maxQueryRounds ?? 0) > 0 && input.queryExecutor) {
|
|
704
|
+
const loop = await runBoundedQueryLoop({
|
|
705
|
+
provider,
|
|
706
|
+
messages,
|
|
707
|
+
...(model ? { model } : {}),
|
|
708
|
+
timeoutMs: recipe.maxBudgetMs ?? DEFAULT_MAX_BUDGET_MS,
|
|
709
|
+
allowedQueries: recipe.allowedQueries ?? [],
|
|
710
|
+
maxQueryRounds: recipe.maxQueryRounds ?? 0,
|
|
711
|
+
...(recipe.maxBudgetMs ? { budgetMs: recipe.maxBudgetMs } : {}),
|
|
712
|
+
catalog: DELEGATE_QUERY_CATALOG,
|
|
713
|
+
executeQuery: input.queryExecutor,
|
|
714
|
+
finalInstruction: 'Now output ONLY your findings as a single JSON object matching the schema — no prose, no markdown fences. Cite refs only from the ground truth and the query results.',
|
|
715
|
+
finalResponseFormat: { type: 'json_schema', schema: DELEGATE_ANALYSIS_JSON_SCHEMA, schemaName: 'DelegateAnalysis' },
|
|
716
|
+
});
|
|
717
|
+
if (!loop.ok) {
|
|
718
|
+
return {
|
|
719
|
+
status: 'analyze-failed',
|
|
720
|
+
recipeId: recipe.id,
|
|
721
|
+
report: buildDelegateAnalysisReport({
|
|
722
|
+
recipeId: recipe.id,
|
|
723
|
+
groundedOn: facts.groundedOn,
|
|
724
|
+
task,
|
|
725
|
+
provider: input.providerLabel,
|
|
726
|
+
facts,
|
|
727
|
+
crossCheck: crossCheckFindings([], facts, {}),
|
|
728
|
+
modelUnavailable: true,
|
|
729
|
+
modelNote: `query loop failed: ${loop.error.message}`,
|
|
730
|
+
generatedAt: input.generatedAt,
|
|
731
|
+
}),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const parsed = parseDelegateAnalysis(loop.value.content);
|
|
735
|
+
if (!parsed.ok) {
|
|
736
|
+
return {
|
|
737
|
+
status: 'analyze-failed',
|
|
738
|
+
recipeId: recipe.id,
|
|
739
|
+
report: buildDelegateAnalysisReport({
|
|
740
|
+
recipeId: recipe.id,
|
|
741
|
+
groundedOn: facts.groundedOn,
|
|
742
|
+
task,
|
|
743
|
+
provider: loop.value.model || input.providerLabel,
|
|
744
|
+
facts,
|
|
745
|
+
crossCheck: crossCheckFindings([], facts, {}),
|
|
746
|
+
modelUnavailable: true,
|
|
747
|
+
modelNote: `model produced an unparseable analysis: ${parsed.error.message}`,
|
|
748
|
+
queriesRun: loop.value.roundsRun,
|
|
749
|
+
generatedAt: input.generatedAt,
|
|
750
|
+
}),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
// Merge query-surfaced entities into the ground truth for the cross-check —
|
|
754
|
+
// a finding citing a fact the model legitimately pulled is grounded.
|
|
755
|
+
const enrichedFacts = { ...facts, entities: [...facts.entities, ...loop.value.gatheredEntities] };
|
|
756
|
+
const crossCheck = crossCheckFindings(parsed.value.findings, enrichedFacts, { strict: input.strict ?? false });
|
|
757
|
+
return {
|
|
758
|
+
status: 'analyzed',
|
|
759
|
+
recipeId: recipe.id,
|
|
760
|
+
report: buildDelegateAnalysisReport({
|
|
761
|
+
recipeId: recipe.id,
|
|
762
|
+
groundedOn: facts.groundedOn,
|
|
763
|
+
task,
|
|
764
|
+
provider: loop.value.model || input.providerLabel,
|
|
765
|
+
facts: enrichedFacts,
|
|
766
|
+
crossCheck,
|
|
767
|
+
...(parsed.value.note ? { modelNote: parsed.value.note } : {}),
|
|
768
|
+
queriesRun: loop.value.roundsRun,
|
|
769
|
+
...(recipe.escalateTo ? { escalateTo: recipe.escalateTo } : {}),
|
|
770
|
+
generatedAt: input.generatedAt,
|
|
771
|
+
}),
|
|
772
|
+
...(loop.value.usage ? { usage: loop.value.usage } : {}),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const call = await callDelegateAnalysisWithRetry({
|
|
776
|
+
provider,
|
|
777
|
+
messages,
|
|
778
|
+
...(model ? { model } : {}),
|
|
779
|
+
timeoutMs: recipe.maxBudgetMs ?? DEFAULT_MAX_BUDGET_MS,
|
|
780
|
+
reprompt: (bad, error) => [...messages, delegateAnalysisRepromptMessage(bad, error)],
|
|
781
|
+
});
|
|
782
|
+
if (!call.ok) {
|
|
783
|
+
// The model ran but failed — degrade to grounding-only, note the failure.
|
|
784
|
+
const crossCheck = crossCheckFindings([], facts, {});
|
|
785
|
+
return {
|
|
786
|
+
status: 'analyze-failed',
|
|
787
|
+
recipeId: recipe.id,
|
|
788
|
+
report: buildDelegateAnalysisReport({
|
|
789
|
+
recipeId: recipe.id,
|
|
790
|
+
groundedOn: facts.groundedOn,
|
|
791
|
+
task,
|
|
792
|
+
provider: input.providerLabel,
|
|
793
|
+
facts,
|
|
794
|
+
crossCheck,
|
|
795
|
+
modelUnavailable: true,
|
|
796
|
+
modelNote: `model failed to produce a valid analysis: ${call.error.message}`,
|
|
797
|
+
generatedAt: input.generatedAt,
|
|
798
|
+
}),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const analysis = call.value.analysis;
|
|
802
|
+
const crossCheck = crossCheckFindings(analysis.findings, facts, { strict: input.strict ?? false });
|
|
803
|
+
return {
|
|
804
|
+
status: 'analyzed',
|
|
805
|
+
recipeId: recipe.id,
|
|
806
|
+
report: buildDelegateAnalysisReport({
|
|
807
|
+
recipeId: recipe.id,
|
|
808
|
+
groundedOn: facts.groundedOn,
|
|
809
|
+
task,
|
|
810
|
+
provider: call.value.model || input.providerLabel,
|
|
811
|
+
facts,
|
|
812
|
+
crossCheck,
|
|
813
|
+
...(analysis.note ? { modelNote: analysis.note } : {}),
|
|
814
|
+
...(recipe.escalateTo ? { escalateTo: recipe.escalateTo } : {}),
|
|
815
|
+
generatedAt: input.generatedAt,
|
|
816
|
+
}),
|
|
817
|
+
...(call.value.usage ? { usage: call.value.usage } : {}),
|
|
818
|
+
retried: call.value.retried,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** The single corrected instruction to feed back: a grounded finding wins. */
|
|
822
|
+
export function correctedInstruction(report) {
|
|
823
|
+
const grounded = report.findings.find((f) => f.grounded);
|
|
824
|
+
if (grounded)
|
|
825
|
+
return grounded.message;
|
|
826
|
+
if (report.findings.length > 0)
|
|
827
|
+
return report.findings[0].message;
|
|
828
|
+
return report.modelNote ?? null;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Build a retry advisor from a `retry-analysis` recipe: it grounds on the failed
|
|
832
|
+
* attempt (`buildDelegateFailureFacts`), runs the analysis model, and returns the
|
|
833
|
+
* single corrected instruction. Read-only — it never writes; a null provider or
|
|
834
|
+
* an empty analysis yields `null` (no enrichment). The corrected instruction is
|
|
835
|
+
* cross-checked against the failure ground truth just like any analysis finding.
|
|
836
|
+
*/
|
|
837
|
+
export function buildRetryAnalysisAdvisor(deps) {
|
|
838
|
+
return async (failure, attempt) => {
|
|
839
|
+
if (deps.provider === null)
|
|
840
|
+
return null;
|
|
841
|
+
const facts = buildDelegateFailureFacts(failure);
|
|
842
|
+
const result = await executeDelegateAnalyze({
|
|
843
|
+
task: `The delegate patch recipe "${deps.patchRecipeId}" failed on attempt ${attempt} (${failure.status}). Diagnose why and give ONE corrected mechanical instruction, citing the affected files/ops from the ground truth.`,
|
|
844
|
+
recipe: deps.retryRecipe,
|
|
845
|
+
facts,
|
|
846
|
+
provider: deps.provider,
|
|
847
|
+
providerLabel: deps.providerLabel,
|
|
848
|
+
generatedAt: new Date().toISOString(),
|
|
849
|
+
});
|
|
850
|
+
return correctedInstruction(result.report);
|
|
851
|
+
};
|
|
852
|
+
}
|
|
446
853
|
// ─── CLI surface ─────────────────────────────────────────────────────────────
|
|
854
|
+
async function runDelegateAnalyze(args) {
|
|
855
|
+
const cwd = resolveCwd(args);
|
|
856
|
+
const wantJson = flagBool(args, 'json');
|
|
857
|
+
const strict = flagBool(args, 'strict-grounding');
|
|
858
|
+
const task = args.positional.slice(1).join(' ').trim();
|
|
859
|
+
if (!task) {
|
|
860
|
+
process.stderr.write('Usage: shrk delegate analyze "<task>" --recipe <id> [--provider auto] [--strict-grounding] [--json]\n');
|
|
861
|
+
return 2;
|
|
862
|
+
}
|
|
863
|
+
const resolved = await resolveRecipe(cwd, flagString(args, 'recipe'));
|
|
864
|
+
if (!resolved.ok) {
|
|
865
|
+
if (wantJson)
|
|
866
|
+
process.stdout.write(asJson({ ok: false, error: resolved.message }) + '\n');
|
|
867
|
+
else
|
|
868
|
+
process.stderr.write(resolved.message + '\n');
|
|
869
|
+
return 1;
|
|
870
|
+
}
|
|
871
|
+
const recipe = resolved.recipe;
|
|
872
|
+
if (recipe.mode !== 'analysis') {
|
|
873
|
+
const msg = `recipe "${recipe.id}" is a patch recipe — use \`shrk delegate run\`. \`analyze\` requires mode: "analysis".`;
|
|
874
|
+
if (wantJson)
|
|
875
|
+
process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
|
|
876
|
+
else
|
|
877
|
+
process.stderr.write(msg + '\n');
|
|
878
|
+
return 1;
|
|
879
|
+
}
|
|
880
|
+
if (!recipe.groundedOn) {
|
|
881
|
+
const msg = `analysis recipe "${recipe.id}" has no groundedOn — nothing to ground the model against.`;
|
|
882
|
+
if (wantJson)
|
|
883
|
+
process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
|
|
884
|
+
else
|
|
885
|
+
process.stderr.write(msg + '\n');
|
|
886
|
+
return 1;
|
|
887
|
+
}
|
|
888
|
+
const planPath = flagString(args, 'plan');
|
|
889
|
+
const inspection = await inspectSharkcraft({ cwd: resolved.projectRoot });
|
|
890
|
+
const facts = await runGroundingReport(recipe.groundedOn, task, inspection, { ...(planPath ? { planPath } : {}) });
|
|
891
|
+
const providerKind = flagString(args, 'provider') ?? recipe.provider ?? 'auto';
|
|
892
|
+
const { provider } = selectAiProvider(providerKind);
|
|
893
|
+
const result = await executeDelegateAnalyze({
|
|
894
|
+
task,
|
|
895
|
+
recipe,
|
|
896
|
+
facts,
|
|
897
|
+
provider,
|
|
898
|
+
strict,
|
|
899
|
+
queryExecutor: buildAnalysisQueryExecutor(inspection, task),
|
|
900
|
+
providerLabel: provider ? providerKind : 'none',
|
|
901
|
+
generatedAt: new Date().toISOString(),
|
|
902
|
+
});
|
|
903
|
+
// Phase 4 escalation: `--escalate` GENERATES (never applies) the target patch
|
|
904
|
+
// recipe's signed plan through the four fences, from the advisory task. The
|
|
905
|
+
// human reviews + applies — analysis never writes.
|
|
906
|
+
let escalationPlan;
|
|
907
|
+
if (flagBool(args, 'escalate') && result.report.escalation && provider) {
|
|
908
|
+
const patch = await resolveRecipe(cwd, result.report.escalation.recipe);
|
|
909
|
+
if (patch.ok && patch.recipe.mode !== 'analysis') {
|
|
910
|
+
escalationPlan = await executeDelegateRun({
|
|
911
|
+
task: result.report.escalation.task,
|
|
912
|
+
recipe: patch.recipe,
|
|
913
|
+
projectRoot: resolved.projectRoot,
|
|
914
|
+
provider,
|
|
915
|
+
apply: false, // generate-only — the human runs the write
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// --save: persist the advisory report under `.sharkcraft/reports/` (writes-drafts,
|
|
920
|
+
// never source). Markdown for humans + JSON for tooling.
|
|
921
|
+
let savedPath;
|
|
922
|
+
if (flagBool(args, 'save')) {
|
|
923
|
+
const slug = task.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'analysis';
|
|
924
|
+
const dir = nodePath.join(resolved.projectRoot, '.sharkcraft', 'reports');
|
|
925
|
+
mkdirSync(dir, { recursive: true });
|
|
926
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
927
|
+
savedPath = nodePath.join(dir, `delegate-analysis-${slug}-${stamp}.md`);
|
|
928
|
+
writeFileSync(savedPath, result.report.markdown, 'utf8');
|
|
929
|
+
writeFileSync(savedPath.replace(/\.md$/, '.json'), asJson(result.report) + '\n', 'utf8');
|
|
930
|
+
}
|
|
931
|
+
const exit = result.status === 'analyze-failed' ? 1 : 0;
|
|
932
|
+
if (wantJson) {
|
|
933
|
+
process.stdout.write(asJson({ ok: exit === 0, ...result, ...(escalationPlan ? { escalationPlan } : {}), ...(savedPath ? { savedPath } : {}) }) + '\n');
|
|
934
|
+
return exit;
|
|
935
|
+
}
|
|
936
|
+
const rep = result.report;
|
|
937
|
+
process.stdout.write(header(`Delegate analysis: ${result.recipeId}`));
|
|
938
|
+
process.stdout.write(kv('status', result.status) + '\n');
|
|
939
|
+
process.stdout.write(kv('grounded on', rep.groundedOn) + '\n');
|
|
940
|
+
process.stdout.write(kv('provider', rep.provider) + '\n');
|
|
941
|
+
process.stdout.write(kv('findings', `${rep.findings.length} (grounded ${rep.groundedCount}, unverified ${rep.unverifiedCount})`) + '\n');
|
|
942
|
+
if (savedPath)
|
|
943
|
+
process.stdout.write(kv('saved', savedPath) + '\n');
|
|
944
|
+
process.stdout.write('\n' + rep.markdown + '\n');
|
|
945
|
+
if (escalationPlan) {
|
|
946
|
+
process.stdout.write(`\n── Escalation (generate-only, review before applying) ──\n`);
|
|
947
|
+
process.stdout.write(kv('patch recipe', escalationPlan.recipeId) + '\n');
|
|
948
|
+
process.stdout.write(kv('status', escalationPlan.status) + '\n');
|
|
949
|
+
if (escalationPlan.planPath)
|
|
950
|
+
process.stdout.write(kv('signed plan', escalationPlan.planPath) + '\n');
|
|
951
|
+
process.stdout.write(kv('message', escalationPlan.message) + '\n');
|
|
952
|
+
}
|
|
953
|
+
return exit;
|
|
954
|
+
}
|
|
447
955
|
async function runDelegateRun(args) {
|
|
448
956
|
const cwd = resolveCwd(args);
|
|
449
957
|
const wantJson = flagBool(args, 'json');
|
|
@@ -462,12 +970,32 @@ async function runDelegateRun(args) {
|
|
|
462
970
|
}
|
|
463
971
|
const providerKind = flagString(args, 'provider') ?? resolved.recipe.provider ?? 'auto';
|
|
464
972
|
const { provider } = selectAiProvider(providerKind);
|
|
973
|
+
// Opt-in assisted retry: enrich the deterministic retry feedback with an LLM
|
|
974
|
+
// diagnosis from a `retry-analysis` recipe (mode: analysis, groundedOn:
|
|
975
|
+
// delegate-failure). Best-effort — absent a recipe or provider, the loop runs
|
|
976
|
+
// exactly as before.
|
|
977
|
+
let retryAdvisor;
|
|
978
|
+
if (flagBool(args, 'assisted-retry') && provider) {
|
|
979
|
+
const cat = await loadResolvedCatalog(cwd);
|
|
980
|
+
const retryRecipe = cat.ok
|
|
981
|
+
? cat.catalog.find((r) => r.mode === 'analysis' && r.groundedOn === 'delegate-failure' && r.delegatable)
|
|
982
|
+
: undefined;
|
|
983
|
+
if (retryRecipe) {
|
|
984
|
+
retryAdvisor = buildRetryAnalysisAdvisor({
|
|
985
|
+
retryRecipe,
|
|
986
|
+
provider,
|
|
987
|
+
providerLabel: providerKind,
|
|
988
|
+
patchRecipeId: resolved.recipe.id,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
465
992
|
const result = await executeDelegateRun({
|
|
466
993
|
task,
|
|
467
994
|
recipe: resolved.recipe,
|
|
468
995
|
projectRoot: resolved.projectRoot,
|
|
469
996
|
provider,
|
|
470
997
|
apply: flagBool(args, 'apply'),
|
|
998
|
+
...(retryAdvisor ? { retryAdvisor } : {}),
|
|
471
999
|
});
|
|
472
1000
|
if (wantJson) {
|
|
473
1001
|
process.stdout.write(asJson({ ok: isOkStatus(result.status), ...result }) + '\n');
|
|
@@ -569,10 +1097,23 @@ async function runDelegateList(args) {
|
|
|
569
1097
|
}
|
|
570
1098
|
for (const r of catalog) {
|
|
571
1099
|
const src = r.source === 'pack' ? ` [pack: ${r.packageName}]` : '';
|
|
572
|
-
process.stdout.write(` ${r.delegatable ? '✓' : '✗'} ${r.id} — ${r.title ?? r.id}${src}\n`);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1100
|
+
process.stdout.write(` ${r.delegatable ? '✓' : '✗'} ${r.id} — ${r.title ?? r.id} [${r.mode}]${src}\n`);
|
|
1101
|
+
if (r.mode === 'analysis') {
|
|
1102
|
+
const extras = [
|
|
1103
|
+
(r.allowedQueries?.length ?? 0) > 0 ? `queries: ${r.allowedQueries.join('/')}` : '',
|
|
1104
|
+
r.fanOut ? 'fan-out' : '',
|
|
1105
|
+
r.escalateTo ? `→ ${r.escalateTo}` : '',
|
|
1106
|
+
].filter((x) => x.length > 0);
|
|
1107
|
+
process.stdout.write(` grounded on: ${r.groundedOn ?? '(unset)'}${extras.length ? ` | ${extras.join(' | ')}` : ''} (read-only)\n`);
|
|
1108
|
+
if (!r.delegatable) {
|
|
1109
|
+
process.stdout.write(` ⚠ NOT usable — groundedOn "${r.groundedOn ?? '(unset)'}" is not a known grounding report\n`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
process.stdout.write(` ops: ${r.allowedOps.join(', ')} | globs: ${r.guardrailGlobs.join(', ')} | verify: ${r.verificationIds.join(', ') || '(none)'}\n`);
|
|
1114
|
+
if (!r.delegatable) {
|
|
1115
|
+
process.stdout.write(` ⚠ NOT delegatable — ${r.unboundVerificationIds.length > 0 ? `unbound verificationIds: ${r.unboundVerificationIds.join(', ')}` : 'no verificationIds declared'}\n`);
|
|
1116
|
+
}
|
|
576
1117
|
}
|
|
577
1118
|
}
|
|
578
1119
|
process.stdout.write(`\nRun \`shrk delegate explain <id>\` for the full fence.\n`);
|
|
@@ -611,6 +1152,22 @@ async function runDelegateExplain(args) {
|
|
|
611
1152
|
process.stdout.write(header(`Delegate recipe: ${r.id}`));
|
|
612
1153
|
process.stdout.write(kv('title', r.title ?? r.id) + '\n');
|
|
613
1154
|
process.stdout.write(kv('source', r.source === 'pack' ? `pack: ${r.packageName}` : 'config') + '\n');
|
|
1155
|
+
process.stdout.write(kv('mode', r.mode) + '\n');
|
|
1156
|
+
if (r.mode === 'analysis') {
|
|
1157
|
+
process.stdout.write(kv('grounded on', `${r.groundedOn ?? '(unset)'}${r.groundingBound ? '' : ' — NOT a known grounding report'}`) + '\n');
|
|
1158
|
+
process.stdout.write(kv('usable', r.delegatable ? 'yes (read-only, grounded)' : 'no — set a known groundedOn first') + '\n');
|
|
1159
|
+
process.stdout.write(kv('provider', `${r.resolvedProvider}${r.resolvedModel ? ` (${r.resolvedModel})` : ''}`) + '\n');
|
|
1160
|
+
if ((r.allowedQueries?.length ?? 0) > 0) {
|
|
1161
|
+
process.stdout.write(kv('query loop', `${r.allowedQueries.join(', ')} (max ${r.maxQueryRounds ?? 0} round(s))`) + '\n');
|
|
1162
|
+
}
|
|
1163
|
+
if (r.fanOut)
|
|
1164
|
+
process.stdout.write(kv('fan-out', `yes (max ${r.maxFanOut ?? 3} slices)`) + '\n');
|
|
1165
|
+
if (r.escalateTo)
|
|
1166
|
+
process.stdout.write(kv('escalates to', r.escalateTo) + '\n');
|
|
1167
|
+
process.stdout.write('\nAnalysis recipes are READ-ONLY: a local model adds judgment on top of the grounded\n' +
|
|
1168
|
+
'deterministic report; findings citing entities absent from it are flagged unverified.\nNo write is ever performed.\n');
|
|
1169
|
+
return r.delegatable ? 0 : 1;
|
|
1170
|
+
}
|
|
614
1171
|
process.stdout.write(kv('delegatable', r.delegatable ? 'yes' : 'no — fix the verification binding first') + '\n');
|
|
615
1172
|
process.stdout.write(kv('allowed ops', r.allowedOps.join(', ')) + '\n');
|
|
616
1173
|
process.stdout.write(kv('guardrail globs', r.guardrailGlobs.join(', ')) + '\n');
|
|
@@ -636,22 +1193,25 @@ function exitFor(s) {
|
|
|
636
1193
|
export const delegateCommand = {
|
|
637
1194
|
name: 'delegate',
|
|
638
1195
|
description: 'Hand a mechanical, deterministically-verifiable edit to a local-LLM worker. The engine verifies the result (config verificationCommands) and auto-reverts on failure — a bad generation costs a retry, never a wrong write. Local-only.',
|
|
639
|
-
usage: 'shrk delegate run "<task>" --recipe <id> [--apply] [--provider auto|ollama|llamacpp] [--json]\n' +
|
|
1196
|
+
usage: 'shrk delegate run "<task>" --recipe <id> [--apply] [--assisted-retry] [--provider auto|ollama|llamacpp] [--json]\n' +
|
|
1197
|
+
'shrk delegate analyze "<task>" --recipe <id> [--strict-grounding] [--plan <p>] [--escalate] [--save] [--provider ...] [--json] — read-only grounded analysis (no write)\n' +
|
|
640
1198
|
'shrk delegate brief "<task>" --recipe <id> [--json]\n' +
|
|
641
1199
|
'shrk delegate list [--json] — recipes + whether each is safely delegatable\n' +
|
|
642
1200
|
'shrk delegate explain <id> [--json] — the full fence for one recipe',
|
|
643
|
-
booleanFlags: new Set(['apply', 'json']),
|
|
1201
|
+
booleanFlags: new Set(['apply', 'json', 'strict-grounding', 'assisted-retry', 'escalate', 'save']),
|
|
644
1202
|
async run(args) {
|
|
645
1203
|
const sub = args.positional[0];
|
|
646
1204
|
if (sub === 'run')
|
|
647
1205
|
return runDelegateRun(args);
|
|
1206
|
+
if (sub === 'analyze')
|
|
1207
|
+
return runDelegateAnalyze(args);
|
|
648
1208
|
if (sub === 'brief')
|
|
649
1209
|
return runDelegateBrief(args);
|
|
650
1210
|
if (sub === 'list')
|
|
651
1211
|
return runDelegateList(args);
|
|
652
1212
|
if (sub === 'explain')
|
|
653
1213
|
return runDelegateExplain(args);
|
|
654
|
-
process.stderr.write('Usage: shrk delegate run|brief|list|explain ...\n');
|
|
1214
|
+
process.stderr.write('Usage: shrk delegate run|analyze|brief|list|explain ...\n');
|
|
655
1215
|
return 2;
|
|
656
1216
|
},
|
|
657
1217
|
};
|