aiki-cli 0.3.1 → 0.3.3
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/CHANGELOG.md +80 -5
- package/README.md +73 -15
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +159 -4
- package/dist/cli/bench.js +5 -5
- package/dist/cli/index.js +12 -2
- package/dist/cli/resume.js +20 -0
- package/dist/cli/run.js +76 -5
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +64 -15
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +69 -6
- package/dist/orchestration/decision-dossier.js +450 -89
- package/dist/orchestration/decision-graph.js +33 -2
- package/dist/orchestration/engine.js +8 -4
- package/dist/orchestration/evidence-origin.js +17 -0
- package/dist/orchestration/jsonStage.js +47 -5
- package/dist/orchestration/legacy-idea-adapter.js +3 -0
- package/dist/orchestration/modes.js +18 -10
- package/dist/orchestration/preflight.js +36 -3
- package/dist/orchestration/quick-analysis.js +29 -7
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +196 -84
- package/dist/orchestration/stages/s4-analyze.js +10 -2
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s6-positions.js +18 -0
- package/dist/orchestration/stages/s7-decision-graph.js +3 -0
- package/dist/orchestration/stages/s8-verify.js +66 -12
- package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
- package/dist/orchestration/stages/s9-judge.js +52 -14
- package/dist/orchestration/stages/s9b-plan.js +227 -48
- package/dist/orchestration/url-sources.js +21 -0
- package/dist/providers/adapter-core.js +1 -1
- package/dist/providers/claude.js +18 -0
- package/dist/schemas/index.js +112 -3
- package/dist/serve/flight-deck.js +830 -0
- package/dist/serve/followup.js +50 -0
- package/dist/serve/frames.js +168 -0
- package/dist/serve/gates.js +72 -0
- package/dist/serve/projections.js +283 -0
- package/dist/serve/server.js +219 -0
- package/dist/serve/threads.js +145 -0
- package/dist/serve-ui/Five.png +0 -0
- package/dist/serve-ui/app.js +820 -0
- package/dist/serve-ui/index.html +171 -0
- package/dist/serve-ui/workspace.css +662 -0
- package/dist/skills/idea-refinement/analyst.md +5 -5
- package/dist/skills/idea-refinement/chair.md +18 -0
- package/dist/skills/idea-refinement/economics-delivery.md +11 -0
- package/dist/skills/idea-refinement/market-adoption.md +12 -0
- package/dist/skills/idea-refinement/planner.md +25 -19
- package/dist/skills/idea-refinement/rebuttal.md +15 -0
- package/dist/skills/idea-refinement/verifier.md +17 -0
- package/dist/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +130 -24
- package/package.json +2 -2
package/dist/council/view.js
CHANGED
|
@@ -3,7 +3,8 @@ import { basename, join } from 'node:path';
|
|
|
3
3
|
import { DISPLAY_NAME } from '../providers/types.js';
|
|
4
4
|
import { RoleOutput as RoleOutputSchema } from '../schemas/index.js';
|
|
5
5
|
import { deriveAudit, deriveScorecard, mergeOpenQuestions } from '../orchestration/stages/s10-render.js';
|
|
6
|
-
import {
|
|
6
|
+
import { sanitizeLocalPaths } from '../orchestration/sanitize-paths.js';
|
|
7
|
+
import { buildReaderProjection, claimLookup, readerClaimLabel, readerClaimRefs, renderDecisionDossierMarkdown, stripReaderClaimIds } from '../orchestration/decision-dossier.js';
|
|
7
8
|
import { IDEA_RUBRIC } from '../workflows/idea-refinement.js';
|
|
8
9
|
import { listArtifacts, readJsonArtifact } from '../storage/runs-read.js';
|
|
9
10
|
import { adaptIdeaOutput, adaptLegacyDecisionGraph } from '../orchestration/legacy-idea-adapter.js';
|
|
@@ -356,6 +357,12 @@ function dossierCoverageLabel(value) {
|
|
|
356
357
|
return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
|
|
357
358
|
}
|
|
358
359
|
function dossierCouncilRead(report) {
|
|
360
|
+
if (report.fastPath)
|
|
361
|
+
return 'Single-pass analysis; council escalation was not required.';
|
|
362
|
+
if (report.adaptiveAuto)
|
|
363
|
+
return report.autoEscalationReasons?.length
|
|
364
|
+
? `The primary analysis tripped structural gates; two task readings checked the interpretation${report.autoChallengeUsed ? ', and one targeted challenger checked eligible claims' : '; no challenger could add information'}. No full council was convened.`
|
|
365
|
+
: 'Two task readings informed one primary decision analyst. No full council was convened.';
|
|
359
366
|
if (report.mode === 'quick')
|
|
360
367
|
return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
|
|
361
368
|
const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
|
|
@@ -377,19 +384,23 @@ function renderDossierIdeaBody(report) {
|
|
|
377
384
|
const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
|
|
378
385
|
const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
|
|
379
386
|
: dossier.recommendation.status === 'REJECTED' ? 'risk' : 'caution';
|
|
380
|
-
const
|
|
381
|
-
|
|
387
|
+
const labelFor = claimLookup(report);
|
|
388
|
+
// Substitute ids then dedupe before escaping, mirroring the markdown renderer: old stored artifacts
|
|
389
|
+
// may carry duplicate / bare-G# condition strings.
|
|
390
|
+
const conditionItems = [...new Set(dossier.recommendation.conditions.map((condition) => stripReaderClaimIds(condition.text, labelFor)))];
|
|
391
|
+
const conditions = conditionItems.length
|
|
392
|
+
? `<details class="decision-details"><summary>Conditions and decision state</summary><div><p><strong>Internal state:</strong> ${escapeHtml(dossier.recommendation.status)}</p><ul>${conditionItems.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div></details>`
|
|
382
393
|
: '';
|
|
383
394
|
const startHere = dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.';
|
|
384
|
-
const recommendationLead = stripReaderClaimIds(dossier.recommendation.reason);
|
|
385
|
-
const recommendationDetail = stripReaderClaimIds(dossier.recommendation.summary);
|
|
395
|
+
const recommendationLead = stripReaderClaimIds(dossier.recommendation.reason, labelFor);
|
|
396
|
+
const recommendationDetail = stripReaderClaimIds(dossier.recommendation.summary, labelFor);
|
|
386
397
|
const factLabels = ['Decisive result', 'Consequence', 'Supporting signal'];
|
|
387
398
|
const facts = keyFindings.slice(0, 3).map((finding, index) => `<article class="decision-fact"><span>${factLabels[index]}</span><p>${escapeHtml(finding)}</p></article>`).join('');
|
|
388
399
|
const snapshot = report.decisionSnapshot;
|
|
389
400
|
const decisiveNumbers = snapshot ? `<div class="decision-numbers">
|
|
390
401
|
<span class="section-eyebrow">Decisive numbers</span>
|
|
391
402
|
<div class="table-wrap"><table class="snapshot-table decisive-table"><thead><tr><th>Metric</th><th>Value</th><th>What it means</th></tr></thead><tbody>${snapshot.decisiveNumbers.map((item) => `<tr><td>${escapeHtml(item.label)}</td><td class="number-value">${escapeHtml(item.value)}</td><td>${escapeHtml(item.meaning)}</td></tr>`).join('')}</tbody></table></div>
|
|
392
|
-
${snapshot.payback ? `<div class="payback-result"><span>Payback · ${escapeHtml(snapshot.payback.status.replaceAll('_', ' '))}</span><strong>${escapeHtml(snapshot.payback.result)}</strong><p>${escapeHtml(snapshot.payback.basis)}</p></div>` : ''}
|
|
403
|
+
${snapshot.payback && snapshot.payback.status !== 'NOT_COMPUTABLE' ? `<div class="payback-result"><span>Payback · ${escapeHtml(snapshot.payback.status.replaceAll('_', ' '))}</span><strong>${escapeHtml(snapshot.payback.result)}</strong><p>${escapeHtml(snapshot.payback.basis)}</p></div>` : ''}
|
|
393
404
|
</div>` : '';
|
|
394
405
|
const optionComparison = snapshot ? `<div class="option-comparison">
|
|
395
406
|
<span class="section-eyebrow">Options at a glance</span>
|
|
@@ -465,12 +476,16 @@ function renderDossierIdeaBody(report) {
|
|
|
465
476
|
const contributions = `${dossierWarning(report, ['verification_skipped', 'single_model', 'low_diversity'])}<p class="lede">Only unique claims that survived independent verification receive credit.</p>${dossierTable(['Provider', 'Verified unique contributions', 'Count'], dossier.contributions.map((item) => [item.name, readerClaimRefs(report, item.verifiedUniqueClaimIds), String(item.verifiedUniqueClaimIds.length)]))}`;
|
|
466
477
|
const receipt = `<div class="receipt">
|
|
467
478
|
<span>mode ${escapeHtml(report.mode)}</span><span>${report.receipt.calls}/${report.receipt.budget} provider calls</span>
|
|
479
|
+
${report.autoEscalationReasons?.length ? `<span>adaptive escalation ${escapeHtml(report.autoEscalationReasons.join('; '))}</span>` : ''}
|
|
468
480
|
<span>discovery ${report.receipt.categories.discovery}</span><span>verification ${report.receipt.categories.verification}</span>
|
|
469
481
|
<span>repair ${report.receipt.categories.repair}</span><span>planning ${report.receipt.categories.planning}</span>
|
|
470
482
|
<span>by provider ${escapeHtml(Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none')}</span>
|
|
471
483
|
<span>model time ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s</span>
|
|
472
484
|
</div>${report.flags.length ? `<div class="warns">${report.flags.map((flag) => `<span class="warn">⚑ ${escapeHtml(flag)}</span>`).join('')}</div>` : '<p class="muted">No degradation flags.</p>'}`;
|
|
473
|
-
const
|
|
485
|
+
const shownRisks = report.risks.slice(0, 8);
|
|
486
|
+
const risks = `${dossierTable(['Risk', 'Severity'], shownRisks.map((item) => [item.risk, item.severity]))}${report.risks.length > shownRisks.length
|
|
487
|
+
? `<p class="muted">${report.risks.length - shownRisks.length} lower-severity items — more in the technical audit (full list in the stored JSON).</p>`
|
|
488
|
+
: ''}`;
|
|
474
489
|
const questions = report.openQuestions.length
|
|
475
490
|
? `<ul class="checks">${report.openQuestions.map((question) => `<li>${escapeHtml(question)}</li>`).join('')}</ul>`
|
|
476
491
|
: '<p class="muted">No verdict-flipping open question was recorded.</p>';
|
|
@@ -500,7 +515,37 @@ function renderDossierIdeaBody(report) {
|
|
|
500
515
|
section('08', 'What the council added', `${shared}${unique}<h3>Verified unique contributions</h3>${contributions}`, 340),
|
|
501
516
|
section('09', 'Run details', runDetails, 380),
|
|
502
517
|
].join('');
|
|
503
|
-
return `${stripReaderClaimIds(readerBody)}${section('10', 'Technical audit', technical, 420)}`;
|
|
518
|
+
return `${stripReaderClaimIds(readerBody, labelFor)}${section('10', 'Technical audit', technical, 420)}`;
|
|
519
|
+
}
|
|
520
|
+
function renderReaderBriefIdeaBody(report) {
|
|
521
|
+
const dossier = report.dossier;
|
|
522
|
+
const projection = buildReaderProjection(report);
|
|
523
|
+
const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
|
|
524
|
+
: dossier.recommendation.status === 'REJECTED' ? 'risk' : 'caution';
|
|
525
|
+
const hero = `<section class="verdict tone-${tone} reveal" style="animation-delay:60ms">
|
|
526
|
+
<span class="pill">Recommendation</span>
|
|
527
|
+
<p class="verdict-text">${escapeHtml(projection.headline)}</p>
|
|
528
|
+
<p class="verdict-detail">${escapeHtml(projection.bottomLine)}</p>
|
|
529
|
+
</section>`;
|
|
530
|
+
const warnings = projection.warnings.length || projection.notices.length
|
|
531
|
+
? `<div class="warns">${projection.warnings.map((warning) => `<span class="warn">⚑ ${escapeHtml(warning.message)}</span>`).join('')}${projection.notices.map((notice) => `<span class="warn">ⓘ ${escapeHtml(notice.message)}</span>`).join('')}</div>`
|
|
532
|
+
: '';
|
|
533
|
+
const snapshot = projection.snapshot ? section('', 'Decision numbers', `${dossierTable(['Metric', 'Value', 'What it means'], projection.snapshot.decisiveNumbers.map((item) => [item.label, item.value, item.meaning]))}${projection.snapshot.payback && projection.snapshot.payback.status !== 'NOT_COMPUTABLE' ? `<div class="action-callout"><span>Payback · ${escapeHtml(projection.snapshot.payback.status.replaceAll('_', ' '))}</span><p>${escapeHtml(projection.snapshot.payback.result)} — ${escapeHtml(projection.snapshot.payback.basis)}</p></div>` : ''}<h3>Options at a glance</h3>${dossierTable(['Path', 'Commitment', 'Basis', 'Trade-off'], projection.snapshot.options.map((item) => [item.label, item.commitment, item.commitmentKind.replace('_', ' '), item.tradeoff]))}${projection.snapshot.tripwire ? `<div class="action-callout"><span>Go/no-go tripwire</span><p><strong>${escapeHtml(projection.snapshot.tripwire.metric)}: ${escapeHtml(projection.snapshot.tripwire.threshold)}</strong> — ${escapeHtml(projection.snapshot.tripwire.decisionRule)}</p></div>` : ''}`, 80) : '';
|
|
534
|
+
const editorial = projection.sections.map((item, index) => section(String(index + 1).padStart(2, '0'), item.heading, `<p class="lede">${escapeHtml(item.summary)}</p>${item.bullets.length
|
|
535
|
+
? `<ul class="reasons">${item.bullets.map((bullet) => `<li>${escapeHtml(bullet)}</li>`).join('')}</ul>`
|
|
536
|
+
: ''}`, 100 + index * 40)).join('');
|
|
537
|
+
const backlog = projection.featureBacklog;
|
|
538
|
+
const features = backlog ? section('', 'Feature priorities', `<div class="feature-groups">${[
|
|
539
|
+
['MUST', 'Build for the first useful release', backlog.must],
|
|
540
|
+
['SHOULD', 'Add after the golden path is stable', backlog.should],
|
|
541
|
+
['LATER', 'Keep outside the current build', backlog.later],
|
|
542
|
+
].filter(([, , items]) => items.length).map(([priority, description, items]) => `<section class="feature-group priority-${priority.toLowerCase()}"><header><div><span>${priority}</span><h4>${description}</h4></div><strong>${items.length}</strong></header><ul>${items.map((item) => `<li><div class="feature-title"><strong>${escapeHtml(item.feature)}</strong><span>${item.effort}</span></div><p>${escapeHtml(item.user_value)}</p><small>${escapeHtml(item.rationale)}</small></li>`).join('')}</ul></section>`).join('')}</div>${backlog.wont.length ? `<h3>Not in this scope</h3><ul class="checks">${backlog.wont.map((item) => `<li><strong>${escapeHtml(item.feature)}</strong> — ${escapeHtml(item.reason)}</li>`).join('')}</ul>` : ''}`, 340) : '';
|
|
543
|
+
const buildPlan = projection.implementationPlan ? section('', 'Build plan', `<ol class="milestone-list">${projection.implementationPlan.milestones.map((milestone) => `<li><div class="milestone-marker"><span>${String(milestone.order).padStart(2, '0')}</span><small>${escapeHtml(milestone.timebox)}</small></div><article><h4>${escapeHtml(milestone.outcome)}</h4><ul>${milestone.tasks.map((task) => `<li>${escapeHtml(task)}</li>`).join('')}</ul><div class="acceptance"><span>Done when</span><p>${escapeHtml(milestone.acceptance_test)}</p></div></article></li>`).join('')}</ol>`, 380) : '';
|
|
544
|
+
const caveats = projection.caveats.length ? section('', 'Top caveats', `<ul class="checks">${projection.caveats.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`, 420) : '';
|
|
545
|
+
const sources = projection.sources.length ? section('', 'Sources', `<ul class="agree">${projection.sources.map((source) => `<li><p>${source.url ? `<a href="${escapeHtml(source.url)}" rel="noopener noreferrer">${escapeHtml(source.label)}</a>` : escapeHtml(source.label)}</p>${source.citedFor.length ? `<small>Cited for: ${source.citedFor.map(escapeHtml).join('; ')}</small>` : ''}</li>`).join('')}</ul>`, 460) : '';
|
|
546
|
+
const nextStep = section('', 'Next step', `<div class="action-callout"><span>Do this now</span><p>${escapeHtml(projection.nextStep)}</p></div>`, 500);
|
|
547
|
+
const audit = `<details class="fold council-audit"><summary>Council audit — reasoning, evidence, dissent, and run receipt</summary><div class="fold-body">${renderDossierIdeaBody(report)}</div></details>`;
|
|
548
|
+
return `${hero}${warnings}${snapshot}${editorial}${features}${buildPlan}${caveats}${sources}${nextStep}${audit}`;
|
|
504
549
|
}
|
|
505
550
|
function renderLegacyIdeaBody(view) {
|
|
506
551
|
const risks = view.risks ?? [];
|
|
@@ -891,23 +936,27 @@ function renderTechnical(view) {
|
|
|
891
936
|
export function renderCouncilHtml(view) {
|
|
892
937
|
const isIdea = view.workflow !== 'code-review';
|
|
893
938
|
const quick = isIdea && view.mode === 'quick';
|
|
939
|
+
const hasReaderBrief = Boolean(view.decisionReport?.dossier.readerBrief);
|
|
894
940
|
const kicker = quick ? 'aiki · quick analysis' : isIdea ? 'aiki · idea refinement' : 'aiki · code review';
|
|
895
941
|
const title = isIdea && view.topic ? cleanTopic(view.topic) : (isIdea ? 'Idea refinement' : 'Code review');
|
|
896
942
|
const panel = view.columns.map((c) => c.title);
|
|
897
|
-
const metaBits = [
|
|
943
|
+
const metaBits = hasReaderBrief ? [] : [
|
|
898
944
|
panel.length ? `${quick ? 'Analyst' : 'Panel'}: ${panel.join(' · ')}` : '',
|
|
899
945
|
!quick && view.moderator ? `Chair: ${view.moderator}` : '',
|
|
900
946
|
view.calls,
|
|
901
947
|
].filter(Boolean);
|
|
902
|
-
const flags = view.flags.length
|
|
948
|
+
const flags = !hasReaderBrief && view.flags.length
|
|
903
949
|
? `<div class="warns">${view.flags.map((f) => `<span class="warn">⚑ ${escapeHtml(f.replaceAll('_', ' '))}</span>`).join('')}</div>`
|
|
904
950
|
: '';
|
|
905
951
|
const body = isIdea && view.decisionReport?.dossier
|
|
906
|
-
?
|
|
952
|
+
? view.decisionReport.dossier.readerBrief
|
|
953
|
+
? renderReaderBriefIdeaBody(view.decisionReport)
|
|
954
|
+
: renderDossierIdeaBody(view.decisionReport)
|
|
907
955
|
: quick ? renderQuickIdeaBody(view) : isIdea ? renderIdeaBody(view) : renderReviewBody(view);
|
|
908
956
|
// Embed the report as Markdown for the Copy button. Escape "<" so a "</script>" in content can't break out.
|
|
909
957
|
const md = JSON.stringify(councilMarkdown(view)).replace(/</g, '\\u003c');
|
|
910
|
-
|
|
958
|
+
// v6: the whole rendered page is path-sanitized — no local usernames in any artifact (T8).
|
|
959
|
+
return sanitizeLocalPaths(`<!doctype html>
|
|
911
960
|
<html lang="en">
|
|
912
961
|
<head>
|
|
913
962
|
<meta charset="utf-8">
|
|
@@ -928,14 +977,14 @@ html{-webkit-text-size-adjust:100%;}
|
|
|
928
977
|
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;}
|
|
929
978
|
main{max-width:940px;margin:0 auto;padding:34px 24px 90px;}
|
|
930
979
|
a{color:var(--accent);}
|
|
931
|
-
h1{font-family:var(--
|
|
980
|
+
h1{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
|
|
932
981
|
h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
|
|
933
982
|
p{margin:0 0 .6em;}
|
|
934
983
|
|
|
935
984
|
/* masthead */
|
|
936
985
|
.mast{border-bottom:1px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
|
|
937
986
|
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);}
|
|
938
|
-
.mast h1{font-size:clamp(
|
|
987
|
+
.mast h1{font-size:clamp(28px,4.6vw,42px);line-height:1.12;margin:12px 0 16px;}
|
|
939
988
|
.mmeta{display:flex;flex-wrap:wrap;gap:7px;}
|
|
940
989
|
.mmeta span{font-family:var(--mono);font-size:11.5px;color:var(--soft);border:1px solid var(--line);background:var(--panel);border-radius:100px;padding:3px 10px;}
|
|
941
990
|
.warns{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
|
|
@@ -1190,7 +1239,7 @@ async function copyReport(btn){
|
|
|
1190
1239
|
document.getElementById('copy-report').addEventListener('click',function(){ copyReport(this); });
|
|
1191
1240
|
</script>
|
|
1192
1241
|
</body>
|
|
1193
|
-
</html
|
|
1242
|
+
</html>`);
|
|
1194
1243
|
}
|
|
1195
1244
|
export async function writeCouncilHtml(runId, dir) {
|
|
1196
1245
|
const view = await loadCouncilView(runId, dir);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// v7 Phase B — `--mode auto`: a deterministic quick-vs-council routing decision made in the CLI
|
|
2
|
+
// BEFORE the engine runs. No model call, no learned routing (§22 forbids that). The rules err toward
|
|
3
|
+
// council: a false council only costs more calls, a false quick under-scrutinizes a risky decision.
|
|
4
|
+
// ponytail: v1 keyword heuristics, not NLP. Upgrade path = a scored intent matcher if precision matters.
|
|
5
|
+
const SENSITIVE = /\b(?:regulat|complian|legal|lawsuit|licens|liabilit|hipaa|gdpr|pci|soc\s?2|financ|fintech|invest|tax(?:es|ation)?|loan|mortgage|insuran|securities|security|vulnerab|exploit|breach|malware|phishing|credential|medical|patient|clinical|diagnos)\w*/i;
|
|
6
|
+
const RESEARCH = /\b(?:research|investigat|competitive analysis|market size|find (?:sources|evidence|data)|look up|up[- ]to[- ]date|latest data|current (?:data|figures|numbers|pricing)|cite sources?)\w*/i;
|
|
7
|
+
// ponytail: v1 decision-question heuristic; upgrade path = the same scored matcher as SENSITIVE/RESEARCH.
|
|
8
|
+
const DECISION_INTENT = /\b(?:should (?:i|we|you)|is it worth|worth (?:it|building|doing|switching|the)|do i (?:need|use)|(?:choose|pick|decide|select) (?:between|whether)|go with|switch(?:ing)? (?:to|from)|migrate (?:to|from)|which\b[^.?!]*\bor\b)/i;
|
|
9
|
+
export function buildTaskProfile(input, opts) {
|
|
10
|
+
const wordCount = input.trim().split(/\s+/).filter(Boolean).length;
|
|
11
|
+
return {
|
|
12
|
+
wordCount,
|
|
13
|
+
urlCount: opts.urlCount,
|
|
14
|
+
hasEvidencePack: opts.hasEvidencePack,
|
|
15
|
+
deliverablesBeyondDecision: opts.requestedOutputs.some((o) => o !== 'DECISION'),
|
|
16
|
+
hasSensitiveKeyword: SENSITIVE.test(input),
|
|
17
|
+
hasResearchWording: RESEARCH.test(input),
|
|
18
|
+
hasDecisionIntent: wordCount >= 4 && DECISION_INTENT.test(input),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Ordered deterministic rules: council if ANY signal fires, else quick. `fastPath` (Phase C, quick-only)
|
|
22
|
+
* is the 1-call single-pass path — eligible when quick AND the input reads as a direct decision question. */
|
|
23
|
+
export function resolveAutoMode(profile) {
|
|
24
|
+
const reasons = [];
|
|
25
|
+
if (profile.urlCount > 0)
|
|
26
|
+
reasons.push('URLs supplied');
|
|
27
|
+
if (profile.hasEvidencePack)
|
|
28
|
+
reasons.push('evidence pack supplied');
|
|
29
|
+
if (profile.hasSensitiveKeyword)
|
|
30
|
+
reasons.push('regulated/financial/security topic');
|
|
31
|
+
if (profile.deliverablesBeyondDecision)
|
|
32
|
+
reasons.push('deliverables beyond a decision requested');
|
|
33
|
+
if (profile.hasResearchWording)
|
|
34
|
+
reasons.push('research wording detected');
|
|
35
|
+
if (profile.wordCount > 120)
|
|
36
|
+
reasons.push('long or complex input (>120 words)');
|
|
37
|
+
if (reasons.length > 0)
|
|
38
|
+
return { mode: 'council', reasons, fastPath: false };
|
|
39
|
+
return { mode: 'quick', reasons: ['plain single-decision prompt'], fastPath: profile.hasDecisionIntent };
|
|
40
|
+
}
|
|
41
|
+
function evidenceForClaim(graph, claimId) {
|
|
42
|
+
const ids = new Set(graph.edges
|
|
43
|
+
.filter((edge) => edge.to === claimId && (edge.type === 'SUPPORTS' || edge.type === 'ATTACKS'))
|
|
44
|
+
.map((edge) => edge.from));
|
|
45
|
+
return graph.evidence.filter((evidence) => ids.has(evidence.id));
|
|
46
|
+
}
|
|
47
|
+
/** Phase D hard gates over validated graph structure; ordered for stable receipts and fixtures. */
|
|
48
|
+
export function structuralEscalationGates(graph) {
|
|
49
|
+
const gates = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const add = (claimId, kind, reason) => {
|
|
52
|
+
const key = `${claimId}:${kind}`;
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
return;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
gates.push({ claimId, kind, reason });
|
|
57
|
+
};
|
|
58
|
+
for (const claim of graph.claims) {
|
|
59
|
+
const evidence = evidenceForClaim(graph, claim.id);
|
|
60
|
+
if (claim.sensitivity === 'DECISIVE'
|
|
61
|
+
&& !evidence.some((item) => item.source_kind === 'PRIMARY' || item.source_kind === 'SECONDARY')) {
|
|
62
|
+
add(claim.id, 'NO_INDEPENDENT_EVIDENCE', 'decisive claim has no independent evidence');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const claim of graph.claims) {
|
|
66
|
+
const evidence = evidenceForClaim(graph, claim.id);
|
|
67
|
+
if ((claim.if_false === 'STOP' || claim.if_false === 'PIVOT')
|
|
68
|
+
&& evidence.length > 0
|
|
69
|
+
&& evidence.every((item) => item.source_kind === 'MODEL_KNOWLEDGE')) {
|
|
70
|
+
add(claim.id, 'MODEL_KNOWLEDGE_DECISION', 'STOP/PIVOT claim rests on model knowledge');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const check of graph.calculation_checks) {
|
|
74
|
+
if (check.status === 'FAIL')
|
|
75
|
+
add(check.claim_id, 'FAILED_CALCULATION', 'deterministic calculation failed');
|
|
76
|
+
}
|
|
77
|
+
for (const claim of graph.claims) {
|
|
78
|
+
if (claim.load_bearing && evidenceForClaim(graph, claim.id)
|
|
79
|
+
.some((item) => item.source_kind === 'USER' && item.support === 'CONTRADICTS')) {
|
|
80
|
+
add(claim.id, 'SUPPLIED_SOURCE_CONTRADICTION', 'user-supplied evidence contradicts a load-bearing claim');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const claim of graph.claims) {
|
|
84
|
+
if (claim.load_bearing && claim.state === 'DISAGREEMENT') {
|
|
85
|
+
add(claim.id, 'LOAD_BEARING_DISAGREEMENT', 'load-bearing claims are mutually inconsistent');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return gates;
|
|
89
|
+
}
|
|
90
|
+
/** One challenge call is useful only when the stored graph contains something new to inspect. */
|
|
91
|
+
export function canProduceNewInformation(graph, claimId) {
|
|
92
|
+
if (!graph.claims.some((claim) => claim.id === claimId))
|
|
93
|
+
return false;
|
|
94
|
+
return evidenceForClaim(graph, claimId).some((item) => item.source_kind !== 'MODEL_KNOWLEDGE')
|
|
95
|
+
|| graph.calculation_checks.some((check) => check.claim_id === claimId && check.status === 'FAIL')
|
|
96
|
+
|| graph.edges.some((edge) => edge.type === 'DEPENDS_ON' && (edge.from === claimId || edge.to === claimId));
|
|
97
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// v6 semantic claim join (plan/AIKI-v6-council-integrity-plan.md T5). The lexical ≥0.8 grouping in
|
|
2
|
+
// S7 cannot join cross-provider paraphrases, so "0 consensus · 0 disputes" was structural (run
|
|
3
|
+
// f740: codex G3 and agy G13 both said "build `aiki serve`" and sat as two UNIQUE claims). S8 now
|
|
4
|
+
// emits `claim_groups`; this module validates them deterministically and OVERLAYS states onto the
|
|
5
|
+
// compiled graph — claim ids, propositions, edges, and stored artifacts are never mutated, and the
|
|
6
|
+
// state itself is computed by the SAME `classifyClaimState` machine the lexical path uses.
|
|
7
|
+
import { classifyClaimState } from './decision-graph.js';
|
|
8
|
+
/** Drop anything the model got wrong: unknown claim ids, duplicate ids, and groups whose claims
|
|
9
|
+
* all come from ONE provider — a model agreeing with itself is not consensus. */
|
|
10
|
+
export function sanitizeClaimGroups(graph, groups) {
|
|
11
|
+
if (!groups?.length)
|
|
12
|
+
return [];
|
|
13
|
+
const positionById = new Map(graph.positions.map((position) => [position.id, position]));
|
|
14
|
+
const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
|
|
15
|
+
return groups.flatMap((group) => {
|
|
16
|
+
const ids = [...new Set(group.ids)].filter((id) => claimById.has(id));
|
|
17
|
+
if (ids.length < 2)
|
|
18
|
+
return [];
|
|
19
|
+
const providers = new Set(ids.flatMap((id) => claimById.get(id).position_ids.map((positionId) => positionById.get(positionId)?.provider)
|
|
20
|
+
.filter((provider) => provider !== undefined)));
|
|
21
|
+
if (providers.size < 2)
|
|
22
|
+
return [];
|
|
23
|
+
return [{ ids, relation: group.relation }];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Overlay group-derived states onto a copy of the graph. SAME → the union of the member claims'
|
|
27
|
+
* positions is re-classified by the existing state machine (CONSENSUS/SHARED_CONCERN/…);
|
|
28
|
+
* OPPOSES → DISAGREEMENT, which wins over any SAME assignment. Old artifacts (no groups) pass
|
|
29
|
+
* through unchanged, so every pre-v6 run keeps its exact rendering. */
|
|
30
|
+
export function overlayClaimGroups(graph, groups) {
|
|
31
|
+
const sane = sanitizeClaimGroups(graph, groups);
|
|
32
|
+
if (!sane.length)
|
|
33
|
+
return graph;
|
|
34
|
+
const positionById = new Map(graph.positions.map((position) => [position.id, position]));
|
|
35
|
+
const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
|
|
36
|
+
const stateById = new Map();
|
|
37
|
+
for (const group of sane) {
|
|
38
|
+
if (group.relation === 'OPPOSES') {
|
|
39
|
+
for (const id of group.ids)
|
|
40
|
+
stateById.set(id, 'DISAGREEMENT');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const union = group.ids
|
|
44
|
+
.flatMap((id) => claimById.get(id).position_ids)
|
|
45
|
+
.map((id) => positionById.get(id))
|
|
46
|
+
.filter((position) => position !== undefined);
|
|
47
|
+
const state = classifyClaimState(union);
|
|
48
|
+
for (const id of group.ids)
|
|
49
|
+
if (stateById.get(id) !== 'DISAGREEMENT')
|
|
50
|
+
stateById.set(id, state);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...graph,
|
|
54
|
+
claims: graph.claims.map((claim) => (stateById.has(claim.id) ? { ...claim, state: stateById.get(claim.id) } : claim)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -15,7 +15,7 @@ import { extractJson } from '../providers/adapter-core.js';
|
|
|
15
15
|
import { detect } from '../providers/detect.js';
|
|
16
16
|
import { probeFlags } from '../providers/probe.js';
|
|
17
17
|
import { replayKey } from '../storage/replay.js';
|
|
18
|
-
import { callCategory, defaultBudgetFor, IDEA_MODE_PLANS, isOptionalStage, LEGACY_DEFAULT_BUDGET } from './modes.js';
|
|
18
|
+
import { callCategory, defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS, isOptionalStage, LEGACY_DEFAULT_BUDGET } from './modes.js';
|
|
19
19
|
/** Bracket a stage call with the TUI's start/end events (no-op headless). A thrown StageError marks
|
|
20
20
|
* the row `failed` and re-propagates unchanged — the engine's failure handling is untouched. */
|
|
21
21
|
export async function runStage(ctx, id, fn) {
|
|
@@ -39,7 +39,7 @@ export const DEFAULT_DEADLINE_MS = 20 * 60 * 1000; // wall-clock cap
|
|
|
39
39
|
// (2026-07-13) after the spawn timeout became actually enforced and killed a LEGITIMATE deep call: codex's
|
|
40
40
|
// S4 analysis of a hard build case ran ~10 min to a valid output (run 20260713-1341, 13:44→13:54). 900s
|
|
41
41
|
// per attempt covers observed deep work; the wall-clock deadline above remains the outer bound.
|
|
42
|
-
const DEFAULT_CALL_TIMEOUT_MS = 900_000;
|
|
42
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 900_000;
|
|
43
43
|
export class BudgetExceeded extends Error {
|
|
44
44
|
constructor(limit) {
|
|
45
45
|
super(`call budget exhausted (limit ${limit})`);
|
|
@@ -74,6 +74,9 @@ export class RunCtx {
|
|
|
74
74
|
cwd;
|
|
75
75
|
events;
|
|
76
76
|
evidencePack;
|
|
77
|
+
allowBlockedSources;
|
|
78
|
+
urlSources;
|
|
79
|
+
isAuto;
|
|
77
80
|
budget;
|
|
78
81
|
calls = [];
|
|
79
82
|
/** Logical provider-call stages, including resume cache hits. Used by bounded protocol caps. */
|
|
@@ -87,6 +90,8 @@ export class RunCtx {
|
|
|
87
90
|
deadlineAt;
|
|
88
91
|
now;
|
|
89
92
|
replay; // resume replay cache (V6.3)
|
|
93
|
+
autoDecision; // v7 Phase B/D: routing record + output escalation receipt
|
|
94
|
+
fastPathActive;
|
|
90
95
|
seq = 0; // monotonic per-call counter for raw/ filenames (== budget.used on a fresh run)
|
|
91
96
|
constructor(opts) {
|
|
92
97
|
this.runId = opts.runId;
|
|
@@ -97,13 +102,36 @@ export class RunCtx {
|
|
|
97
102
|
this.cwd = opts.cwd;
|
|
98
103
|
this.events = opts.events;
|
|
99
104
|
this.evidencePack = opts.evidencePack;
|
|
105
|
+
this.allowBlockedSources = opts.allowBlockedSources;
|
|
106
|
+
this.urlSources = opts.urlSources;
|
|
107
|
+
this.isAuto = opts.autoDecision !== undefined;
|
|
108
|
+
this.fastPathActive = this.mode === 'quick'
|
|
109
|
+
&& opts.autoDecision?.resolved === 'quick'
|
|
110
|
+
&& opts.autoDecision.fast_path === true;
|
|
100
111
|
this.budget = { limit: opts.budget ?? defaultBudgetFor(opts.workflow, this.mode), used: 0 };
|
|
101
112
|
this.handles = new Map(opts.handles.map((h) => [h.id, h]));
|
|
102
113
|
this.signal = opts.signal;
|
|
103
|
-
this.deadlineMs = opts.deadlineMs ??
|
|
114
|
+
this.deadlineMs = opts.deadlineMs ?? defaultDeadlineFor(opts.workflow, this.mode);
|
|
104
115
|
this.now = opts.now ?? Date.now;
|
|
105
116
|
this.deadlineAt = this.now() + this.deadlineMs;
|
|
106
117
|
this.replay = opts.replay;
|
|
118
|
+
this.autoDecision = opts.autoDecision ? { ...opts.autoDecision } : undefined;
|
|
119
|
+
}
|
|
120
|
+
get fastPath() {
|
|
121
|
+
return this.fastPathActive;
|
|
122
|
+
}
|
|
123
|
+
get autoEscalationReasons() {
|
|
124
|
+
return this.autoDecision?.escalation_reasons ?? [];
|
|
125
|
+
}
|
|
126
|
+
/** Phase D: retain initial fast-path eligibility in meta, but stop rendering it as single-pass. */
|
|
127
|
+
markAutoEscalated(reasons) {
|
|
128
|
+
if (!this.autoDecision || reasons.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
this.fastPathActive = false;
|
|
131
|
+
this.autoDecision = {
|
|
132
|
+
...this.autoDecision,
|
|
133
|
+
escalation_reasons: [...new Set([...(this.autoDecision.escalation_reasons ?? []), ...reasons])],
|
|
134
|
+
};
|
|
107
135
|
}
|
|
108
136
|
/** Provider ids available this run (READY at setup). */
|
|
109
137
|
available() {
|
|
@@ -135,6 +163,8 @@ export class RunCtx {
|
|
|
135
163
|
optionalCallsRemaining() {
|
|
136
164
|
if (this.workflow !== 'idea-refinement')
|
|
137
165
|
return 0;
|
|
166
|
+
if (this.isAuto)
|
|
167
|
+
return 0; // Phase D adaptive topology owns its single optional challenge directly.
|
|
138
168
|
const plan = IDEA_MODE_PLANS[this.mode];
|
|
139
169
|
const logicalUsed = this.attemptedStages.filter(isOptionalStage).length;
|
|
140
170
|
const protocolRoom = Math.max(0, plan.optionalCalls - logicalUsed);
|
|
@@ -160,13 +190,16 @@ export class RunCtx {
|
|
|
160
190
|
// Resume (V6.3): if this exact (provider, prompt) already succeeded in the run we're resuming,
|
|
161
191
|
// replay its output — no real call, no budget spend. Only never-completed calls hit the model.
|
|
162
192
|
const cachedOut = this.replay?.get(replayKey(handle.id, req.prompt));
|
|
193
|
+
const category = callCategory(stage);
|
|
194
|
+
const replayed = cachedOut !== undefined;
|
|
195
|
+
if (!replayed && this.budget.used + 1 > this.budget.limit)
|
|
196
|
+
throw new BudgetExceeded(this.budget.limit);
|
|
197
|
+
this.events?.onCallStart?.(handle.id, stage, category, replayed);
|
|
163
198
|
let res;
|
|
164
199
|
if (cachedOut !== undefined) {
|
|
165
200
|
res = { ok: true, text: cachedOut, json: req.expectJson ? extractJson(cachedOut) : undefined, durationMs: 0 };
|
|
166
201
|
}
|
|
167
202
|
else {
|
|
168
|
-
if (this.budget.used + 1 > this.budget.limit)
|
|
169
|
-
throw new BudgetExceeded(this.budget.limit);
|
|
170
203
|
this.budget.used++;
|
|
171
204
|
res = await handle.adapter.run({
|
|
172
205
|
prompt: req.prompt,
|
|
@@ -174,7 +207,7 @@ export class RunCtx {
|
|
|
174
207
|
timeoutMs: req.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS,
|
|
175
208
|
expectJson: req.expectJson,
|
|
176
209
|
readOnly: true,
|
|
177
|
-
research: this.
|
|
210
|
+
research: this.workflow === 'idea-refinement' && this.mode !== 'quick' && stage.startsWith('S4'),
|
|
178
211
|
signal: this.signal, // Ctrl+C kills the in-flight child (T8); undefined headless
|
|
179
212
|
}, handle.flags);
|
|
180
213
|
// Only REAL calls count toward the ledger/budget; a replayed call is free and already recorded in
|
|
@@ -184,10 +217,12 @@ export class RunCtx {
|
|
|
184
217
|
stage,
|
|
185
218
|
category: callCategory(stage),
|
|
186
219
|
durationMs: res.durationMs,
|
|
220
|
+
usage: (res.ok && res.usage) || estimateUsage(req.prompt, res.ok ? res.text : ''),
|
|
187
221
|
...(res.ok ? {} : { error: res.error }),
|
|
188
222
|
});
|
|
189
223
|
}
|
|
190
224
|
await this.writer.writeRaw(`${stage}-${handle.id}-${seq}.out`, res.ok ? res.text : `[${res.error}]\n${res.stderrTail}`);
|
|
225
|
+
this.events?.onCallEnd?.(handle.id, stage, res.durationMs, res.ok, replayed);
|
|
191
226
|
return res;
|
|
192
227
|
}
|
|
193
228
|
/** Assemble the run's `meta.json` payload (§15) from the handles + call ledger accumulated so
|
|
@@ -211,6 +246,7 @@ export class RunCtx {
|
|
|
211
246
|
this.roles.s4.forEach((id, i) => (roles[`s4_${i + 1}`] = id));
|
|
212
247
|
// Fold stage-raised flags in with any explicitly passed by the caller; dedupe.
|
|
213
248
|
const allFlags = [...new Set([...(flags ?? []), ...this.flags])];
|
|
249
|
+
const usage_totals = this.sumUsage();
|
|
214
250
|
return {
|
|
215
251
|
run_id: this.runId,
|
|
216
252
|
workflow: this.workflow,
|
|
@@ -223,11 +259,38 @@ export class RunCtx {
|
|
|
223
259
|
call_count: this.calls.length,
|
|
224
260
|
budget: { limit: this.budget.limit, used: this.budget.used },
|
|
225
261
|
receipt: this.receipt(),
|
|
262
|
+
...(usage_totals ? { usage_totals } : {}),
|
|
263
|
+
...(this.autoDecision ? { auto_decision: this.autoDecision } : {}),
|
|
226
264
|
exit_status: exitStatus,
|
|
227
265
|
aborted,
|
|
228
266
|
...(allFlags.length ? { flags: allFlags } : {}),
|
|
229
267
|
};
|
|
230
268
|
}
|
|
269
|
+
/** Sum per-call usage into run totals. Undefined when no call carries usage (empty run). */
|
|
270
|
+
sumUsage() {
|
|
271
|
+
let inputTokens = 0, outputTokens = 0, reportedCalls = 0, estimatedCalls = 0, reportedCostUsd = 0, anyCost = false;
|
|
272
|
+
for (const c of this.calls) {
|
|
273
|
+
if (!c.usage)
|
|
274
|
+
continue;
|
|
275
|
+
inputTokens += c.usage.inputTokens ?? 0;
|
|
276
|
+
outputTokens += c.usage.outputTokens ?? 0;
|
|
277
|
+
if (c.usage.estimated)
|
|
278
|
+
estimatedCalls++;
|
|
279
|
+
else
|
|
280
|
+
reportedCalls++;
|
|
281
|
+
if (c.usage.reportedCostUsd !== undefined) {
|
|
282
|
+
reportedCostUsd += c.usage.reportedCostUsd;
|
|
283
|
+
anyCost = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (reportedCalls === 0 && estimatedCalls === 0)
|
|
287
|
+
return undefined;
|
|
288
|
+
return { inputTokens, outputTokens, reportedCalls, estimatedCalls, ...(anyCost ? { reportedCostUsd } : {}) };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** ponytail: chars/4 heuristic, labeled estimated — good enough until a provider reports. */
|
|
292
|
+
function estimateUsage(prompt, out) {
|
|
293
|
+
return { inputTokens: Math.ceil(prompt.length / 4), outputTokens: Math.ceil(out.length / 4), estimated: true };
|
|
231
294
|
}
|
|
232
295
|
/** Run-fatal errors abort the whole run; everything else (e.g. a single provider failure in a
|
|
233
296
|
* fan-out) is handled locally by the stage (drop provider, check quorum). */
|