project-autopilot 12.0.0 → 12.0.1
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 +10 -10
- package/SKILL.md +1 -3
- package/adapters/agents.json +11 -1
- package/config.example.json +5 -3
- package/config.schema.json +28 -9
- package/docs/configuration.md +5 -3
- package/package.json +1 -1
- package/scripts/adapter-drivers.mjs +209 -22
- package/scripts/agent-contract.mjs +24 -2
- package/scripts/agent-invocation.mjs +32 -5
- package/scripts/autopilot.mjs +787 -222
- package/scripts/config-layout.mjs +6 -0
- package/scripts/config-migration.mjs +3 -3
- package/scripts/config-validation.mjs +12 -4
- package/scripts/e2e-detect.mjs +30 -0
- package/scripts/escalation-naming.mjs +19 -0
- package/scripts/failure-contract.mjs +25 -9
- package/scripts/gen-config-schema.mjs +7 -0
- package/scripts/idle-hang-watchdog.mjs +34 -7
- package/scripts/implementation-baseline.mjs +95 -4
- package/scripts/model-discovery.mjs +176 -38
- package/scripts/opencode-generation.mjs +116 -0
- package/scripts/per-task-budget.mjs +11 -7
- package/scripts/plan-review-rounds.mjs +164 -9
- package/scripts/planning-boundary.mjs +16 -6
- package/scripts/planning-continuation.mjs +43 -23
- package/scripts/planning-review-decision.mjs +3 -0
- package/scripts/planning-review-wiring.mjs +12 -8
- package/scripts/relay-guards.mjs +133 -11
- package/scripts/review-canary.mjs +5 -2
- package/scripts/role-fallback.mjs +23 -8
- package/scripts/run-history.mjs +32 -1
- package/scripts/run-observability.mjs +46 -2
- package/scripts/setup-discovery.mjs +2 -1
- package/scripts/setup.mjs +183 -81
- package/scripts/skill-copy.mjs +49 -0
- package/scripts/task-requirements.mjs +24 -0
- package/scripts/watch-report.mjs +146 -21
- package/scripts/watch-summary.mjs +26 -3
- package/scripts/plan-mode-model-note.mjs +0 -27
package/scripts/setup.mjs
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
loadDocumentedCatalog,
|
|
12
12
|
loadDocumentedEffort,
|
|
13
13
|
discoverCapabilities,
|
|
14
|
-
canonicalModelId
|
|
14
|
+
canonicalModelId,
|
|
15
|
+
modelDiscoveryTimeoutLabel
|
|
15
16
|
} from "./model-discovery.mjs";
|
|
16
17
|
import { discoverAgentContract } from "./agent-contract.mjs";
|
|
17
18
|
import { buildDiscoveryCaches } from "./setup-discovery.mjs";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
import { detect as detectSpecKit } from "../adapters/speckit.mjs";
|
|
41
42
|
import { detect as detectGsd } from "../adapters/gsd.mjs";
|
|
42
43
|
import { detectTestCommand, resolveGatesCommands } from "./detect-test-command.mjs";
|
|
44
|
+
import { detectUiProject, detectE2eTool } from "./e2e-detect.mjs";
|
|
43
45
|
import { discoverPlaywrightRuntime } from "./playwright-runtime.mjs";
|
|
44
46
|
import { formatPlaywrightStatusBlock } from "./playwright-status-block.mjs";
|
|
45
47
|
import { PLATFORM_KINDS, detectPlatformCapabilities, detectPlatformKind, formatExecutionEnvironmentBlock } from "./platform-capabilities.mjs";
|
|
@@ -53,7 +55,6 @@ import {
|
|
|
53
55
|
reviewerIndependenceWarning
|
|
54
56
|
} from "./role-fallback-edit.mjs";
|
|
55
57
|
import { normalizeEscalationModel, ESCALATION_ROLES } from "./escalation-model.mjs";
|
|
56
|
-
import {isHaikuOnReadOnlyClaudeRole, planModeModelNote} from "./plan-mode-model-note.mjs";
|
|
57
58
|
import { networkIsolationPrefix } from "./shell-guard.mjs";
|
|
58
59
|
import { installAgentCommands } from "./agent-command-registry.mjs";
|
|
59
60
|
import { fileURLToPath } from "node:url";
|
|
@@ -411,7 +412,16 @@ async function pickModelAndCapabilities(
|
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
const
|
|
415
|
+
const cachedContract = contractCache instanceof Map
|
|
416
|
+
? contractCache.get(adapterId)
|
|
417
|
+
: contractCache;
|
|
418
|
+
let discovery = cachedContract;
|
|
419
|
+
if (!discovery) {
|
|
420
|
+
if (forceInteractive || process.stdin.isTTY) {
|
|
421
|
+
console.log(`Loading ${meta.binary} models…`);
|
|
422
|
+
}
|
|
423
|
+
discovery = await discoverAgentContract(adapterId, meta);
|
|
424
|
+
}
|
|
415
425
|
const catalog = discovery.models.status === "reported" ? discovery.models.items : [];
|
|
416
426
|
|
|
417
427
|
if (catalog.length) {
|
|
@@ -446,12 +456,12 @@ async function pickModelAndCapabilities(
|
|
|
446
456
|
// an otherwise-live result, so it is never used for "Use CLI default"
|
|
447
457
|
// (item.id is empty there — there is no selected model to look up).
|
|
448
458
|
let reasoningOptions = item.reasoning || [];
|
|
449
|
-
if (item.id && !reasoningOptions.length && meta.effort_flag === "--variant" && Array.isArray(item.variants) && item.variants.length) {
|
|
459
|
+
if (item.id && !reasoningOptions.length && (meta.effort_flag === "--variant" || meta.effort_mode === "model_suffix") && Array.isArray(item.variants) && item.variants.length) {
|
|
450
460
|
// OpenCode has no separate reasoning field in its own model output —
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
461
|
+
// 1.x names --variant itself as "provider-specific reasoning effort"
|
|
462
|
+
// (e.g. high, max, minimal) and 2.x encodes the same level as a model
|
|
463
|
+
// suffix; either way the live-discovered variants ARE the live
|
|
464
|
+
// reasoning levels, not a distinct dimension.
|
|
455
465
|
reasoningOptions = item.variants;
|
|
456
466
|
} else if (item.id && !reasoningOptions.length) {
|
|
457
467
|
const documentedEffort = loadDocumentedEffort(adapterId);
|
|
@@ -563,10 +573,22 @@ async function pickModelAndCapabilities(
|
|
|
563
573
|
};
|
|
564
574
|
}
|
|
565
575
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
576
|
+
if (discovery.models.status === "timeout") {
|
|
577
|
+
// The probe is cached and reused by every role, so the retry (and this
|
|
578
|
+
// fallback notice) is reported once per adapter, not once per role.
|
|
579
|
+
if (!discovery.models.timeoutReported) {
|
|
580
|
+
console.log(
|
|
581
|
+
`\n${label}: ${meta.binary} model list timed out after ${modelDiscoveryTimeoutLabel()} — ` +
|
|
582
|
+
`type a model name, or press Enter for the CLI default, or run ap config again later.`
|
|
583
|
+
);
|
|
584
|
+
discovery.models.timeoutReported = true;
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
console.log(
|
|
588
|
+
`\n${label}: ${meta.binary} does not expose a supported machine-readable model list; ` +
|
|
589
|
+
`Autopilot will not invent a catalog.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
570
592
|
|
|
571
593
|
return {
|
|
572
594
|
model: await valueOrDefault(
|
|
@@ -580,15 +602,45 @@ async function pickModelAndCapabilities(
|
|
|
580
602
|
};
|
|
581
603
|
}
|
|
582
604
|
|
|
605
|
+
function documentedLevelsForDefaultModel(adapterId) {
|
|
606
|
+
if (adapterId === "claude") {
|
|
607
|
+
const catalog = loadDocumentedCatalog("claude");
|
|
608
|
+
if (!catalog) return null;
|
|
609
|
+
const levels = catalog.models
|
|
610
|
+
.map(model => Array.isArray(model.reasoning) ? model.reasoning : [])
|
|
611
|
+
.find(list => list.length) || [];
|
|
612
|
+
if (!levels.length) return null;
|
|
613
|
+
return {levels, source: catalog.source, last_verified: catalog.last_verified};
|
|
614
|
+
}
|
|
615
|
+
const effort = loadDocumentedEffort(adapterId);
|
|
616
|
+
if (!effort) return null;
|
|
617
|
+
return {levels: effort.levels, source: effort.source, last_verified: effort.last_verified};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Merge live-discovered capabilities over an adapter manifest. Falls back to
|
|
621
|
+
// the manifest's own effort_mode so a `config` transport (codex) survives and
|
|
622
|
+
// a 2.x OpenCode capability can replace the manifest's --variant flag with
|
|
623
|
+
// the model-suffix transport.
|
|
624
|
+
function effectiveMetaFor(meta, caps) {
|
|
625
|
+
return {
|
|
626
|
+
...meta,
|
|
627
|
+
model_flag: caps.model ? caps.model_flag : null,
|
|
628
|
+
effort_flag: caps.effort ? caps.effort_flag : null,
|
|
629
|
+
effort_mode: caps.effort_mode || meta.effort_mode || null,
|
|
630
|
+
variant_flag: caps.variant ? caps.variant_flag : null,
|
|
631
|
+
variant_mode: meta.variant_mode || null
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
583
635
|
// Ask for reasoning/effort and (when supported) variant for an already
|
|
584
636
|
// picked model. Shared by the main-role flow and the per-role fallback
|
|
585
637
|
// add flows so both use identical prompts and options. May swap
|
|
586
638
|
// modelInfo.model when the model maps effort levels to exact ids.
|
|
587
|
-
// Adapters without an effort flag are not asked and yield "".
|
|
588
|
-
async function pickReasoningAndVariant(label, effectiveMeta, modelInfo, prior) {
|
|
639
|
+
// Adapters without an effort flag/mode are not asked and yield "".
|
|
640
|
+
async function pickReasoningAndVariant(label, effectiveMeta, modelInfo, prior, adapterId) {
|
|
589
641
|
let reasoning = "";
|
|
590
642
|
|
|
591
|
-
if (effectiveMeta.effort_flag) {
|
|
643
|
+
if (effectiveMeta.effort_flag || effectiveMeta.effort_mode === "model_suffix") {
|
|
592
644
|
if (modelInfo.effortModelIds && modelInfo.reasoningOptions.length) {
|
|
593
645
|
const opts = [...modelInfo.reasoningOptions];
|
|
594
646
|
const priorReasoning = prior.reasoning || "";
|
|
@@ -623,10 +675,48 @@ async function pickReasoningAndVariant(label, effectiveMeta, modelInfo, prior) {
|
|
|
623
675
|
) {
|
|
624
676
|
reasoning = "";
|
|
625
677
|
}
|
|
678
|
+
} else if (!modelInfo.model) {
|
|
679
|
+
const documented = documentedLevelsForDefaultModel(adapterId);
|
|
680
|
+
if (documented) {
|
|
681
|
+
console.log(
|
|
682
|
+
`\n${label}: documented reasoning/effort levels (not live-verified for the CLI default model)`
|
|
683
|
+
);
|
|
684
|
+
console.log(
|
|
685
|
+
`Source: ${documented.source} | verified: ${documented.last_verified}`
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const opts = [
|
|
689
|
+
"Use CLI default",
|
|
690
|
+
...documented.levels
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
const priorReasoning =
|
|
694
|
+
prior.reasoning || "";
|
|
695
|
+
|
|
696
|
+
const fallback = Math.max(
|
|
697
|
+
0,
|
|
698
|
+
opts.indexOf(priorReasoning)
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
reasoning = await choose(
|
|
702
|
+
`${label} reasoning/effort (for selected model)`,
|
|
703
|
+
opts,
|
|
704
|
+
fallback
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
if (
|
|
708
|
+
reasoning === "Use CLI default"
|
|
709
|
+
) {
|
|
710
|
+
reasoning = "";
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
console.log(
|
|
714
|
+
`\n${label}: the CLI default model has no selectable reasoning levels — the CLI decides.`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
626
717
|
} else {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
prior.reasoning || ""
|
|
718
|
+
console.log(
|
|
719
|
+
`\n${label}: ${modelInfo.model} has no selectable reasoning levels — the CLI decides.`
|
|
630
720
|
);
|
|
631
721
|
}
|
|
632
722
|
}
|
|
@@ -719,27 +809,12 @@ async function roleConfig( label,
|
|
|
719
809
|
console.log(
|
|
720
810
|
` capabilities: ` +
|
|
721
811
|
`model=${runtimeCaps.model ? runtimeCaps.model_flag : "no"}, ` +
|
|
722
|
-
`effort=${runtimeCaps.effort ? runtimeCaps.effort_flag : "no"}, ` +
|
|
812
|
+
`effort=${runtimeCaps.effort ? (runtimeCaps.effort_flag || (runtimeCaps.effort_mode === "model_suffix" ? "model#level" : "yes")) : "no"}, ` +
|
|
723
813
|
`variant=${runtimeCaps.variant ? runtimeCaps.variant_flag : (meta.variant_mode === "model_suffix" ? "model#variant" : "no")}, ` +
|
|
724
814
|
`resume=${runtimeCaps.resume ? "yes" : "no"}`
|
|
725
815
|
);
|
|
726
816
|
|
|
727
|
-
const effectiveMeta =
|
|
728
|
-
...meta,
|
|
729
|
-
model_flag:
|
|
730
|
-
runtimeCaps.model
|
|
731
|
-
? runtimeCaps.model_flag
|
|
732
|
-
: null,
|
|
733
|
-
effort_flag:
|
|
734
|
-
runtimeCaps.effort
|
|
735
|
-
? runtimeCaps.effort_flag
|
|
736
|
-
: null,
|
|
737
|
-
variant_flag:
|
|
738
|
-
runtimeCaps.variant
|
|
739
|
-
? runtimeCaps.variant_flag
|
|
740
|
-
: null,
|
|
741
|
-
variant_mode: meta.variant_mode || null
|
|
742
|
-
};
|
|
817
|
+
const effectiveMeta = effectiveMetaFor(meta, runtimeCaps);
|
|
743
818
|
|
|
744
819
|
const prior =
|
|
745
820
|
adapter === current.adapter
|
|
@@ -756,14 +831,16 @@ async function roleConfig( label,
|
|
|
756
831
|
label,
|
|
757
832
|
effectiveMeta,
|
|
758
833
|
prior,
|
|
759
|
-
adapter
|
|
834
|
+
adapter,
|
|
835
|
+
contractCache
|
|
760
836
|
);
|
|
761
837
|
|
|
762
838
|
const { reasoning, variant } = await pickReasoningAndVariant(
|
|
763
839
|
label,
|
|
764
840
|
effectiveMeta,
|
|
765
841
|
modelInfo,
|
|
766
|
-
prior
|
|
842
|
+
prior,
|
|
843
|
+
adapter
|
|
767
844
|
);
|
|
768
845
|
|
|
769
846
|
// Resume is an internal orchestration decision, not a setup choice.
|
|
@@ -818,9 +895,11 @@ async function repairOneMissingModel({entry, roles, agents, roleLabels, contract
|
|
|
818
895
|
const adapter = agents?.[adapterId];
|
|
819
896
|
const binary = adapter?.binary || adapterId;
|
|
820
897
|
let items = [];
|
|
898
|
+
let modelsStatus = null;
|
|
821
899
|
try {
|
|
822
900
|
const contract = contractCache?.get(adapterId) || await discoverAgentContract(adapterId, adapter);
|
|
823
901
|
items = contract?.models?.status === "reported" ? contract.models.items : [];
|
|
902
|
+
modelsStatus = contract?.models?.status || null;
|
|
824
903
|
} catch { items = []; }
|
|
825
904
|
const ids = [];
|
|
826
905
|
for (const item of items) {
|
|
@@ -835,7 +914,9 @@ async function repairOneMissingModel({entry, roles, agents, roleLabels, contract
|
|
|
835
914
|
console.log(`\nModel no longer available: ${targetLabel} uses '${entry.model}', which ${binary} no longer lists.`);
|
|
836
915
|
console.log(ids.length
|
|
837
916
|
? `${binary} currently lists: ${ids.join(", ")}`
|
|
838
|
-
:
|
|
917
|
+
: modelsStatus === "timeout"
|
|
918
|
+
? `${binary} model list timed out after ${modelDiscoveryTimeoutLabel()} — type a model id or drop this entry, or run ap config again later.`
|
|
919
|
+
: `${binary} currently reports no machine-readable model list.`);
|
|
839
920
|
|
|
840
921
|
const dropLabel = entry.kind === "fallback"
|
|
841
922
|
? "Drop this fallback"
|
|
@@ -1042,6 +1123,27 @@ function detectPlanningFrameworks(repo) {
|
|
|
1042
1123
|
// Platform the network question is decided for (single line so tests can pin it in a disposable copy).
|
|
1043
1124
|
const NETWORK_PLATFORM = process.platform;
|
|
1044
1125
|
|
|
1126
|
+
// E2E question (shared by first run and `ap config`): asked only for a UI project with no E2E tool
|
|
1127
|
+
// and no stored tests.e2e. "no" is stored as "none" so it is never asked again. Installs nothing.
|
|
1128
|
+
const E2E_PROMPT = "Add Playwright? (recommended) [yes/no/cypress]: ";
|
|
1129
|
+
|
|
1130
|
+
function storedE2e(current) {
|
|
1131
|
+
return current?.capabilities?.verification?.e2e ?? current?.tests?.e2e ?? null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function askE2eChoice(current, repo, interactive) {
|
|
1135
|
+
const stored = storedE2e(current);
|
|
1136
|
+
if (!interactive || stored != null) return stored;
|
|
1137
|
+
if (!detectUiProject(repo) || detectE2eTool(repo)) return null;
|
|
1138
|
+
for (;;) {
|
|
1139
|
+
const answer = (await ask(E2E_PROMPT)).trim().toLowerCase();
|
|
1140
|
+
if (answer === "" || answer === "y" || answer === "yes") return "playwright";
|
|
1141
|
+
if (answer === "n" || answer === "no") return "none";
|
|
1142
|
+
if (answer === "c" || answer === "cypress") return "cypress";
|
|
1143
|
+
console.log("Please answer yes, no or cypress.");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1045
1147
|
async function networkSetting(current, runtimeMode) {
|
|
1046
1148
|
let networkIsolation =
|
|
1047
1149
|
current?.gates?.network_isolation ??
|
|
@@ -1179,17 +1281,21 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1179
1281
|
return;
|
|
1180
1282
|
}
|
|
1181
1283
|
|
|
1182
|
-
// Probe each available CLI once per setup invocation.
|
|
1183
|
-
// The capability result is reused for every role, while model/variant
|
|
1184
|
-
// selection is still presented independently each time the CLI is chosen.
|
|
1185
|
-
const { capabilityCache, contractCache } =
|
|
1186
|
-
await buildDiscoveryCaches(available, agents);
|
|
1187
|
-
|
|
1188
1284
|
// Interactive once, reused by the role-fallback loop below and the gates
|
|
1189
1285
|
// question further down: a non-interactive stdin keeps defaults.
|
|
1190
1286
|
const interactive =
|
|
1191
1287
|
forceInteractive || !!process.stdin.isTTY;
|
|
1192
1288
|
|
|
1289
|
+
// Probe each available CLI once per setup invocation.
|
|
1290
|
+
// The capability result is reused for every role, while model/variant
|
|
1291
|
+
// selection is still presented independently each time the CLI is chosen.
|
|
1292
|
+
const { capabilityCache, contractCache } =
|
|
1293
|
+
await buildDiscoveryCaches(available, agents, undefined, {
|
|
1294
|
+
onStart: (_id, meta) => {
|
|
1295
|
+
if (interactive) console.log(`Loading ${meta.binary} models…`);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1193
1299
|
let detectedFrameworks = [];
|
|
1194
1300
|
let frameworkChoices = [];
|
|
1195
1301
|
|
|
@@ -1386,9 +1492,6 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1386
1492
|
contractCache,
|
|
1387
1493
|
r !== "implementer"
|
|
1388
1494
|
);
|
|
1389
|
-
if (isHaikuOnReadOnlyClaudeRole(r, roles[r]?.adapter, roles[r]?.model)) {
|
|
1390
|
-
console.log(`\n${planModeModelNote(r)}`);
|
|
1391
|
-
}
|
|
1392
1495
|
|
|
1393
1496
|
// Pending #22: roleConfig() rebuilds the role, so carry a configured
|
|
1394
1497
|
// escalation_model forward; the optional question below may change it.
|
|
@@ -1437,24 +1540,20 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1437
1540
|
const fallbackCaps =
|
|
1438
1541
|
capabilityCache.get(fallbackAdapter) ||
|
|
1439
1542
|
await discoverCapabilities(fallbackMeta);
|
|
1440
|
-
const fallbackEffectiveMeta =
|
|
1441
|
-
...fallbackMeta,
|
|
1442
|
-
model_flag: fallbackCaps.model ? fallbackCaps.model_flag : null,
|
|
1443
|
-
effort_flag: fallbackCaps.effort ? fallbackCaps.effort_flag : null,
|
|
1444
|
-
variant_flag: fallbackCaps.variant ? fallbackCaps.variant_flag : null,
|
|
1445
|
-
variant_mode: fallbackMeta.variant_mode || null
|
|
1446
|
-
};
|
|
1543
|
+
const fallbackEffectiveMeta = effectiveMetaFor(fallbackMeta, fallbackCaps);
|
|
1447
1544
|
const fallbackModelInfo = await pickModelAndCapabilities(
|
|
1448
1545
|
`${roleLabels[r]} fallback`,
|
|
1449
1546
|
fallbackEffectiveMeta,
|
|
1450
1547
|
null,
|
|
1451
|
-
fallbackAdapter
|
|
1548
|
+
fallbackAdapter,
|
|
1549
|
+
contractCache
|
|
1452
1550
|
);
|
|
1453
1551
|
const fallbackExtras = await pickReasoningAndVariant(
|
|
1454
1552
|
`${roleLabels[r]} fallback`,
|
|
1455
1553
|
fallbackEffectiveMeta,
|
|
1456
1554
|
fallbackModelInfo,
|
|
1457
|
-
{ reasoning: roles[r]?.reasoning || "", variant: "" }
|
|
1555
|
+
{ reasoning: roles[r]?.reasoning || "", variant: "" },
|
|
1556
|
+
fallbackAdapter
|
|
1458
1557
|
);
|
|
1459
1558
|
const candidate = {
|
|
1460
1559
|
adapter: fallbackAdapter,
|
|
@@ -1462,9 +1561,6 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1462
1561
|
reasoning: fallbackExtras.reasoning,
|
|
1463
1562
|
variant: fallbackExtras.variant
|
|
1464
1563
|
};
|
|
1465
|
-
if (isHaikuOnReadOnlyClaudeRole(r, candidate.adapter, candidate.model)) {
|
|
1466
|
-
console.log(`\n${planModeModelNote(r)}`);
|
|
1467
|
-
}
|
|
1468
1564
|
if (!probe(fallbackAdapter)) {
|
|
1469
1565
|
console.log(
|
|
1470
1566
|
`\nWarning: fallback CLI ${fallbackAdapter} probe failed; saving anyway.`
|
|
@@ -1568,24 +1664,20 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1568
1664
|
const fallbackCaps =
|
|
1569
1665
|
capabilityCache.get(fallbackAdapter) ||
|
|
1570
1666
|
await discoverCapabilities(fallbackMeta);
|
|
1571
|
-
const fallbackEffectiveMeta =
|
|
1572
|
-
...fallbackMeta,
|
|
1573
|
-
model_flag: fallbackCaps.model ? fallbackCaps.model_flag : null,
|
|
1574
|
-
effort_flag: fallbackCaps.effort ? fallbackCaps.effort_flag : null,
|
|
1575
|
-
variant_flag: fallbackCaps.variant ? fallbackCaps.variant_flag : null,
|
|
1576
|
-
variant_mode: fallbackMeta.variant_mode || null
|
|
1577
|
-
};
|
|
1667
|
+
const fallbackEffectiveMeta = effectiveMetaFor(fallbackMeta, fallbackCaps);
|
|
1578
1668
|
const fallbackModelInfo = await pickModelAndCapabilities(
|
|
1579
1669
|
`${roleLabels[r]} fallback`,
|
|
1580
1670
|
fallbackEffectiveMeta,
|
|
1581
1671
|
null,
|
|
1582
|
-
fallbackAdapter
|
|
1672
|
+
fallbackAdapter,
|
|
1673
|
+
contractCache
|
|
1583
1674
|
);
|
|
1584
1675
|
const fallbackExtras = await pickReasoningAndVariant(
|
|
1585
1676
|
`${roleLabels[r]} fallback`,
|
|
1586
1677
|
fallbackEffectiveMeta,
|
|
1587
1678
|
fallbackModelInfo,
|
|
1588
|
-
{ reasoning: roles[r]?.reasoning || "", variant: "" }
|
|
1679
|
+
{ reasoning: roles[r]?.reasoning || "", variant: "" },
|
|
1680
|
+
fallbackAdapter
|
|
1589
1681
|
);
|
|
1590
1682
|
const candidate = {
|
|
1591
1683
|
adapter: fallbackAdapter,
|
|
@@ -1593,9 +1685,6 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1593
1685
|
reasoning: fallbackExtras.reasoning,
|
|
1594
1686
|
variant: fallbackExtras.variant
|
|
1595
1687
|
};
|
|
1596
|
-
if (isHaikuOnReadOnlyClaudeRole(r, candidate.adapter, candidate.model)) {
|
|
1597
|
-
console.log(`\n${planModeModelNote(r)}`);
|
|
1598
|
-
}
|
|
1599
1688
|
|
|
1600
1689
|
// Warn-never-block per-pick checks: CLI probe plus model
|
|
1601
1690
|
// membership against the cached/live contract catalog.
|
|
@@ -1699,24 +1788,20 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1699
1788
|
const escAdapter = await choose(`${escLabel} CLI`, available, 0);
|
|
1700
1789
|
const escMeta = agents[escAdapter];
|
|
1701
1790
|
const escCaps = capabilityCache.get(escAdapter) || await discoverCapabilities(escMeta);
|
|
1702
|
-
const escEffectiveMeta =
|
|
1703
|
-
...escMeta,
|
|
1704
|
-
model_flag: escCaps.model ? escCaps.model_flag : null,
|
|
1705
|
-
effort_flag: escCaps.effort ? escCaps.effort_flag : null,
|
|
1706
|
-
variant_flag: escCaps.variant ? escCaps.variant_flag : null,
|
|
1707
|
-
variant_mode: escMeta.variant_mode || null
|
|
1708
|
-
};
|
|
1791
|
+
const escEffectiveMeta = effectiveMetaFor(escMeta, escCaps);
|
|
1709
1792
|
const escModelInfo = await pickModelAndCapabilities(
|
|
1710
1793
|
escLabel,
|
|
1711
1794
|
escEffectiveMeta,
|
|
1712
1795
|
null,
|
|
1713
|
-
escAdapter
|
|
1796
|
+
escAdapter,
|
|
1797
|
+
contractCache
|
|
1714
1798
|
);
|
|
1715
1799
|
const escExtras = await pickReasoningAndVariant(
|
|
1716
1800
|
escLabel,
|
|
1717
1801
|
escEffectiveMeta,
|
|
1718
1802
|
escModelInfo,
|
|
1719
|
-
{ reasoning: roles[escRoles[0]]?.reasoning || "", variant: "" }
|
|
1803
|
+
{ reasoning: roles[escRoles[0]]?.reasoning || "", variant: "" },
|
|
1804
|
+
escAdapter
|
|
1720
1805
|
);
|
|
1721
1806
|
const candidate = normalizeEscalationModel({
|
|
1722
1807
|
adapter: escAdapter,
|
|
@@ -1960,6 +2045,10 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
1960
2045
|
true
|
|
1961
2046
|
);
|
|
1962
2047
|
|
|
2048
|
+
// E2E question: same shared helper/prompt for first run and `ap config` (asked only when unset).
|
|
2049
|
+
const e2eChoice = await askE2eChoice(current, repo, interactive);
|
|
2050
|
+
const e2ePending = storedE2e(current) == null && (e2eChoice === "playwright" || e2eChoice === "cypress");
|
|
2051
|
+
|
|
1963
2052
|
const config = {
|
|
1964
2053
|
...(current || {}),
|
|
1965
2054
|
|
|
@@ -2069,7 +2158,20 @@ async function printStartScreen(detectedFrameworks) {
|
|
|
2069
2158
|
browser: "native",
|
|
2070
2159
|
require_browser: "when-task-requires"
|
|
2071
2160
|
},
|
|
2072
|
-
...(current?.capabilities || {})
|
|
2161
|
+
...(current?.capabilities || {}),
|
|
2162
|
+
// tests.e2e / tests.e2e_setup_pending in the config file (config-layout maps them here).
|
|
2163
|
+
...(e2eChoice != null
|
|
2164
|
+
? {
|
|
2165
|
+
verification: {
|
|
2166
|
+
...(current?.capabilities?.verification || {
|
|
2167
|
+
browser: "native",
|
|
2168
|
+
require_browser: "when-task-requires"
|
|
2169
|
+
}),
|
|
2170
|
+
e2e: e2eChoice,
|
|
2171
|
+
...(e2ePending ? { e2e_setup_pending: true } : {})
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
: {})
|
|
2073
2175
|
},
|
|
2074
2176
|
|
|
2075
2177
|
safety: {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// The folder holding package.json/bin/scripts of the running ap. Derived from
|
|
6
|
+
// this module's own import.meta.url (the module ships inside that folder), so
|
|
7
|
+
// it never depends on the caller's cwd.
|
|
8
|
+
export function skillCopyRoot(scriptUrl = import.meta.url) {
|
|
9
|
+
return path.resolve(path.dirname(fileURLToPath(scriptUrl)), "..");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// How this copy was installed: inside a node_modules tree (npm-global), a Git
|
|
13
|
+
// checkout (.git next to package.json), or any other copied skill folder.
|
|
14
|
+
export function skillCopyMode(root = skillCopyRoot()) {
|
|
15
|
+
const resolved = path.resolve(root);
|
|
16
|
+
if (resolved.split(/[\\/]/).includes("node_modules")) return "npm-global";
|
|
17
|
+
try {
|
|
18
|
+
if (fs.statSync(path.join(resolved, ".git")).isDirectory()) return "dev-checkout";
|
|
19
|
+
} catch { /* no .git directory */ }
|
|
20
|
+
return "skill-folder";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The identity the watch report prints: the copy's package version, install
|
|
24
|
+
// mode and root path. Never throws: a missing/unreadable package.json leaves
|
|
25
|
+
// the version null rather than inventing one.
|
|
26
|
+
export function skillCopyInfo(root = skillCopyRoot()) {
|
|
27
|
+
const resolved = path.resolve(root);
|
|
28
|
+
let version = null;
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(resolved, "package.json"), "utf8"));
|
|
31
|
+
if (typeof pkg?.version === "string" && pkg.version) version = pkg.version;
|
|
32
|
+
} catch { /* version stays null */ }
|
|
33
|
+
return { version, mode: skillCopyMode(resolved), path: resolved };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Record the running copy on the mission state.json the watch report reads.
|
|
37
|
+
// Called at mission run start; mutates and returns the state object.
|
|
38
|
+
export function recordSkillCopy(state, info = skillCopyInfo()) {
|
|
39
|
+
if (!state || typeof state !== "object") return state;
|
|
40
|
+
state.skill = { version: info.version, mode: info.mode, path: info.path };
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Record the entry command (and the stage it stops after, null = full loop) on
|
|
45
|
+
// the mission state at run start; the watch report prints it.
|
|
46
|
+
export function recordCommand(state, name, stopAfter = null) {
|
|
47
|
+
state.command = { name, stopAfter };
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
@@ -21,3 +21,27 @@ export function mergeTaskFacts(tasks = []) {
|
|
|
21
21
|
requiredCapabilities: [...new Set(facts.flatMap(fact => fact.requiredCapabilities || []))]
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
// Standalone and SpecKit task lines carry no structured `files`/`verify`
|
|
26
|
+
// fields, only one text line ("... — accept: ...; verify: ..."). Read the
|
|
27
|
+
// file paths named in the line and its `verify:` segment, then derive the
|
|
28
|
+
// same facts the GSD path derives from its plan XML.
|
|
29
|
+
export function deriveTaskFactsFromLine(task = {}) {
|
|
30
|
+
const text = String(task.text || "");
|
|
31
|
+
const verifyMatch = text.match(/;\s*verify:\s*(.+)$/i);
|
|
32
|
+
const files = text.split(/\s+/)
|
|
33
|
+
.map(token => token.replace(/^[`'"(\[]+|[`'"),;.:\]]+$/g, ""))
|
|
34
|
+
.filter(token => /[\\/]/.test(token) && /\.\w+$/.test(token));
|
|
35
|
+
return deriveTaskFacts({files, verify: verifyMatch ? verifyMatch[1] : "", requiredCapabilities: task.requiredCapabilities});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mergeTaskLineFacts(tasks = []) {
|
|
39
|
+
return mergeTaskFacts(tasks.map(task => ({taskFacts: deriveTaskFactsFromLine(task)})));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Brief text for the verifier when the phase needs a browser. The orchestrator
|
|
43
|
+
// does not launch a dev server; the verifier starts it or reports the gap.
|
|
44
|
+
export function browserVerifierNote(facts = {}) {
|
|
45
|
+
if (facts.browser !== true && facts.playwright !== true) return "";
|
|
46
|
+
return "Browser verification required: this phase changes UI files or verifies with Playwright. Start the project's dev server yourself (use the matching script in package.json, inside your allowed commands and scratch directory), open the changed page in the browser, and check the acceptance criteria there. If the dev server or browser cannot be started, report that check as NOT POSSIBLE with the reason — never skip it silently.";
|
|
47
|
+
}
|