specrails-desktop 2.59.0 → 2.59.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/client/dist/assets/{ActivityFeedPage-DBFt8ZHP.js → ActivityFeedPage-vAY9C71W.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DiQx0W67.js → AgentBrowserCapture-CpwjGXQm.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-BmDSXp_J.js → AgentModeAnalyticsPane-BHeoJVxQ.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-BLQLPSwr.js → AgentModeCodePane-B0Nf3YkN.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-BbTXdKUa.js → AgentModeJobsPane-CpbIItY_.js} +1 -1
- package/client/dist/assets/{AgentsPage-Dei1jGR8.js → AgentsPage-BVJAFXn3.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-BhkeGIGn.js → AnalyticsPage-DDWees-K.js} +1 -1
- package/client/dist/assets/{CodePage-B_9yYWUx.js → CodePage-CzV5tkvS.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-DAcfEJac.js → DesktopAnalyticsPage-DfRHNK7i.js} +1 -1
- package/client/dist/assets/{DocsDialog-WCuiYgsc.js → DocsDialog-BxduBYbh.js} +1 -1
- package/client/dist/assets/{DocsPage-bGVYTMTU.js → DocsPage-DOVeRJn_.js} +1 -1
- package/client/dist/assets/{ExportDropdown-CsodZ-yb.js → ExportDropdown-BnKVV8UK.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-D5fh_eVL.js → InteractiveJobComposer-Da9dvM-0.js} +1 -1
- package/client/dist/assets/{JobDetailModal-DlpXF61O.js → JobDetailModal-DhwKbZGe.js} +1 -1
- package/client/dist/assets/{JobDetailPage-2yR19_Lb.js → JobDetailPage-MtPE-E_1.js} +1 -1
- package/client/dist/assets/{JobsPage-CS9IR6Dq.js → JobsPage-0vdlbNik.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-BwiAtmOr.js → LoopBuilderPage-DCUHozXh.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal--upwvgTv.js → LoopPreviewModal-Df-S8NGf.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-DRnUixtl.js → LoopWindowSurface-sdb7QbSo.js} +1 -1
- package/client/dist/assets/{LoopsPage-Xm3nRToQ.js → LoopsPage-CDfCC9Px.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-D_8L4EWZ.js → MinimizedChatsContext-DKlPf54Y.js} +1 -1
- package/client/dist/assets/{PluginsPage-CAFQ1B4Q.js → PluginsPage-iL0G3rQQ.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-DYSO4v90.js → ProjectSettingsDialog-DOkjT7xv.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-CbQmW7Qx.js → RepositoryDeliveries-CJVpOUpZ.js} +1 -1
- package/client/dist/assets/RepositoryScopeSelector-BeM-2gIZ.js +2 -0
- package/client/dist/assets/{ReviewPacketPage-SDBhYdQB.js → ReviewPacketPage-EBCim2w_.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-DHBUds51.js → TemplatePreviewModal-1-92UGwN.js} +1 -1
- package/client/dist/assets/{TicketDetailModalContext-UAeNm7BB.js → TicketDetailModalContext-CkVXuHxU.js} +1 -1
- package/client/dist/assets/{Trans-DExd3Z00.js → Trans-CzhPjnjB.js} +1 -1
- package/client/dist/assets/{agent-DrM4qned.js → agent--8QAfoXL.js} +1 -1
- package/client/dist/assets/{agent-CW74ODDP.js → agent-3HxXkIWX.js} +1 -1
- package/client/dist/assets/{agent-BElLtdKP.js → agent-BjttZLIl.js} +1 -1
- package/client/dist/assets/{agent-C-m9VugX.js → agent-CpFCz32T.js} +1 -1
- package/client/dist/assets/{agent-KdIXq3w5.js → agent-DVnw6ybO.js} +1 -1
- package/client/dist/assets/{agent-CGSKxfZk.js → agent-Z5s_GTP0.js} +1 -1
- package/client/dist/assets/{agent-DeCAZDhL.js → agent-hv57-zyG.js} +1 -1
- package/client/dist/assets/{agent-jK8mCisM.js → agent-xGrZ_aN1.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-aw7H5ORC.js → formatDistanceToNow-DlQaXc7c.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-NyC7gZJX.js → getTimezoneOffsetInMilliseconds-DxUFGcVq.js} +1 -1
- package/client/dist/assets/index-Bfunvi6w.css +2 -0
- package/client/dist/assets/{index-JSTZVJfe.js → index-Czov72g7.js} +43 -43
- package/client/dist/assets/{jira-api-DlXaRwA3.js → jira-api-CbJvbRYx.js} +1 -1
- package/client/dist/assets/{project-repositories-DXhb9FP4.js → project-repositories-BkBD3rxx.js} +1 -1
- package/client/dist/assets/{spending-BdcyVkcd.js → spending-CYotH-9N.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-DoWQv7l1.js → subscriptionUsage-BH73RO86.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-DQ02J25O.js → subscriptionUsage-BWTeWlHx.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-N9hB-ARf.js → subscriptionUsage-CBh82zVa.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-DWF3zKp4.js → subscriptionUsage-CHcQ7lLe.js} +1 -1
- package/client/dist/assets/subscriptionUsage-CdlE5Mg1.js +1 -0
- package/client/dist/assets/{subscriptionUsage-Cf87DeS_.js → subscriptionUsage-DUPBmSQi.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-DkIf6Zuh.js → subscriptionUsage-DrlwrJ17.js} +1 -1
- package/client/dist/assets/{subscriptionUsage-D6u7VBdz.js → subscriptionUsage-EFu0mIK3.js} +1 -1
- package/client/dist/assets/{useDesktop-D07TvesS.js → useDesktop-DpxVGfUC.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-pGLmtAC6.js → useRuntimeRuns-CfGRbhr9.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-TzKtPRp9.js → useSharedWebSocket-C17TGqT8.js} +2 -2
- package/client/dist/index.html +17 -17
- package/docs/internals/api-reference.md +1 -0
- package/docs/internals/core-runtime-updates.md +9 -2
- package/docs/internals/desktop-owned-workflows.md +23 -0
- package/docs/internals/mission-rail-cards.md +15 -0
- package/docs/internals/operations-runbook.md +6 -0
- package/docs/internals/safe-pr-review-flow.md +25 -7
- package/docs/internals/source-map.md +3 -1
- package/docs/internals/visual-design-audit.md +5 -0
- package/package.json +1 -1
- package/server/dist/core-update-manager.js +1 -1
- package/server/dist/db/migrations.js +9 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +114 -5
- package/server/dist/modules/delivery/runtime/multi-repo-execution.js +7 -5
- package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +65 -30
- package/server/dist/modules/delivery/runtime/rail-pr-store.js +3 -3
- package/server/dist/modules/delivery/runtime/rail-relaunch.js +154 -0
- package/server/dist/modules/delivery/runtime/rails-router.js +109 -10
- package/server/dist/modules/execution/runtime/revision-seed.js +6 -2
- package/server/dist/modules/loops/runtime/loop-agent-defaults.json +1 -1
- package/server/dist/modules/loops/runtime/loop-definition-events.js +1 -1
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +8 -4
- package/server/dist/modules/loops/runtime/loop-run-manager.js +27 -17
- package/server/dist/modules/subscription-usage/adapters/claude.js +30 -20
- package/server/dist/modules/subscription-usage/application.js +2 -1
- package/server/dist/modules/subscription-usage/domain.js +35 -1
- package/server/dist/modules/subscription-usage/runtime/usage-service.js +1 -1
- package/server/dist/worktree-manager.js +114 -6
- package/server/dist/worktree-overlay.js +4 -4
- package/client/dist/assets/RepositoryScopeSelector-DKzvIGSZ.js +0 -2
- package/client/dist/assets/index-CahchzS5.css +0 -2
- package/client/dist/assets/subscriptionUsage-zRcRPHkP.js +0 -1
|
@@ -42,6 +42,7 @@ const accept_ladder_1 = require("../../execution/runtime/accept-ladder");
|
|
|
42
42
|
const rail_pr_decision_1 = require("./rail-pr-decision");
|
|
43
43
|
const active_pr_continuation_1 = require("./active-pr-continuation");
|
|
44
44
|
const rail_isolated_launch_1 = require("./rail-isolated-launch");
|
|
45
|
+
const rail_relaunch_1 = require("./rail-relaunch");
|
|
45
46
|
const worktree_manager_1 = require("../../../worktree-manager");
|
|
46
47
|
const integration_branch_1 = require("../../../integration-branch");
|
|
47
48
|
const rail_worktree_release_1 = require("./rail-worktree-release");
|
|
@@ -94,10 +95,17 @@ function prDeliveryRevisionAllowed(delivery, revisionOfDeliveryId, ticketIds) {
|
|
|
94
95
|
return false;
|
|
95
96
|
if ((0, rail_pr_store_1.isTerminalPrDecision)(delivery.decision))
|
|
96
97
|
return false;
|
|
98
|
+
if (!hasDeliveredWork(delivery))
|
|
99
|
+
return false;
|
|
97
100
|
const covered = new Set(delivery.ticketIds);
|
|
98
101
|
const requested = new Set(ticketIds);
|
|
99
102
|
return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
|
|
100
103
|
}
|
|
104
|
+
function hasDeliveredWork(delivery) {
|
|
105
|
+
return Boolean((delivery.branch && delivery.deliverySha && ((delivery.prUrl && delivery.prState === 'pr-created') || (delivery.implementationOutcome !== 'failed' && delivery.deliveryOutcome !== 'blocked' && delivery.deliveryOutcome !== 'not_started' && delivery.deliveryOutcome !== 'no_changes')))
|
|
106
|
+
|| delivery.branches?.some(unit => unit.succeeded && unit.finalSha && unit.changed !== false && unit.implementationOutcome !== 'failed' && unit.deliveryOutcome !== 'blocked' && unit.deliveryOutcome !== 'not_started' && unit.deliveryOutcome !== 'no_changes')
|
|
107
|
+
|| delivery.repositoryDeliveries?.some(unit => unit.branch && unit.deliverySha && unit.implementationOutcome !== 'failed' && unit.deliveryOutcome !== 'blocked' && unit.deliveryOutcome !== 'not_started' && unit.deliveryOutcome !== 'no_changes'));
|
|
108
|
+
}
|
|
101
109
|
function prDeliveryCheckoutTarget(delivery) {
|
|
102
110
|
if (delivery.branch && delivery.deliverySha)
|
|
103
111
|
return { branch: delivery.branch, sha: delivery.deliverySha };
|
|
@@ -493,13 +501,47 @@ function createRailsRouter() {
|
|
|
493
501
|
res.status(500).json({ error: 'Failed to update rail name' });
|
|
494
502
|
}
|
|
495
503
|
});
|
|
496
|
-
//
|
|
497
|
-
|
|
504
|
+
// Both doors share admission checks; Relaunch resolves its original source
|
|
505
|
+
// before interpreting launch options, never the mutable slot's new specs.
|
|
506
|
+
router.post(['/:railIndex/launch', '/:railIndex/relaunch'], async (req, res) => {
|
|
498
507
|
const railIndex = parseInt(req.params.railIndex, 10);
|
|
499
508
|
if (isNaN(railIndex) || railIndex < 0 || railIndex >= rails_store_1.MAX_RAILS) {
|
|
500
509
|
res.status(400).json({ error: 'Invalid rail index' });
|
|
501
510
|
return;
|
|
502
511
|
}
|
|
512
|
+
const c = ctx(req);
|
|
513
|
+
let relaunchSource;
|
|
514
|
+
if (/\/relaunch\/?$/i.test(req.path)) {
|
|
515
|
+
try {
|
|
516
|
+
if (!(0, feature_flags_1.isLoopsEnabled)()) {
|
|
517
|
+
res.status(409).json({ error: 'relaunch_workflow_unavailable', detail: 'The original loop workflow cannot run while Loops are disabled.',
|
|
518
|
+
action: 'Enable Loops before retrying this attempt.' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
relaunchSource = (0, rail_relaunch_1.resolveRailRelaunch)(c, railIndex, req.body?.sourceId);
|
|
522
|
+
if (relaunchSource.delivery && !(0, rail_isolation_1.isRailPrDeliveryEnabled)()) {
|
|
523
|
+
res.status(409).json({ error: 'relaunch_delivery_unavailable', detail: 'The original attempt belongs to isolated PR delivery, which is disabled.',
|
|
524
|
+
action: 'Enable PR delivery before retrying this attempt.' });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (relaunchSource.delivery && hasDeliveredWork(relaunchSource.delivery)) {
|
|
528
|
+
res.status(409).json({ error: 'relaunch_requires_recovery',
|
|
529
|
+
detail: 'This attempt has delivered work that must be recovered before it can be retried.',
|
|
530
|
+
action: 'Use Recover & retry or the delivery actions on this card.' });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
req.body = { ...relaunchSource.config,
|
|
534
|
+
originConversationId: req.body?.originConversationId ?? relaunchSource.delivery?.originConversationId,
|
|
535
|
+
originSurface: req.body?.originSurface ?? relaunchSource.delivery?.originSurface ?? 'dashboard',
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
res.status(409).json({ error: error instanceof rail_relaunch_1.RailRelaunchError ? error.code : 'relaunch_config_unavailable',
|
|
540
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
541
|
+
action: error instanceof rail_relaunch_1.RailRelaunchError ? error.action : 'Configure a fresh launch on a free rail.' });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
503
545
|
let runtimeProviderOverride;
|
|
504
546
|
try {
|
|
505
547
|
runtimeProviderOverride = (0, agent_runtime_settings_1.validateRuntimeProviderOverride)(req.body?.runtimeProviderOverride);
|
|
@@ -597,6 +639,8 @@ function createRailsRouter() {
|
|
|
597
639
|
loopId = (0, loop_factory_1.getFactoryLoop)(loopId).id; // canonicalize saved legacy aliases
|
|
598
640
|
mode = fmode;
|
|
599
641
|
}
|
|
642
|
+
if (relaunchSource)
|
|
643
|
+
relaunchSource.config = { ...relaunchSource.config, mode };
|
|
600
644
|
if (!VALID_MODES.has(mode)) {
|
|
601
645
|
res.status(400).json({ error: 'mode must be "implement", "freestyle" or "loop"' });
|
|
602
646
|
return;
|
|
@@ -634,7 +678,6 @@ function createRailsRouter() {
|
|
|
634
678
|
// spawn-time gate in QueueManager (kill-switch + persistent-stdin
|
|
635
679
|
// capability) decides, so the launch no longer passes an explicit flag. A
|
|
636
680
|
// legacy `interactive` body param is accepted and ignored (wire compat).
|
|
637
|
-
const c = ctx(req);
|
|
638
681
|
try {
|
|
639
682
|
(0, process_admission_1.assertProcessAdmission)(c.project.id);
|
|
640
683
|
}
|
|
@@ -645,7 +688,15 @@ function createRailsRouter() {
|
|
|
645
688
|
}
|
|
646
689
|
throw err;
|
|
647
690
|
}
|
|
648
|
-
const
|
|
691
|
+
const storedRail = (0, rails_store_1.getRail)(c.db, railIndex);
|
|
692
|
+
const rail = relaunchSource ? { ...storedRail, ticketIds: [...relaunchSource.ticketIds],
|
|
693
|
+
profileName: typeof profileName === 'string' ? profileName : null,
|
|
694
|
+
aiEngine: typeof aiEngine === 'string' ? aiEngine : null } : storedRail;
|
|
695
|
+
if (relaunchSource && rail.ticketIds.some(id => !c.getTicketSpec?.(id))) {
|
|
696
|
+
res.status(409).json({ error: 'relaunch_specs_unavailable', detail: 'One or more original specs are no longer available.',
|
|
697
|
+
action: 'Refresh the mission and configure a launch with the current specs.' });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
649
700
|
if (rail.ticketIds.length === 0) {
|
|
650
701
|
res.status(400).json({ error: 'Rail has no tickets assigned' });
|
|
651
702
|
return;
|
|
@@ -676,6 +727,10 @@ function createRailsRouter() {
|
|
|
676
727
|
return;
|
|
677
728
|
}
|
|
678
729
|
let repositoryBases;
|
|
730
|
+
if (relaunchSource?.delivery?.executionManifest && baseDeliveryIds === undefined) {
|
|
731
|
+
repositoryBases = { repositoryBaseBranches: Object.fromEntries(relaunchSource.delivery.executionManifest.repositories.map(repo => [repo.repositoryId, repo.baseBranch])), repositoryBaseShas: {} };
|
|
732
|
+
baseBranch = repositoryBases.repositoryBaseBranches[primaryRepository.id] ?? baseBranch;
|
|
733
|
+
}
|
|
679
734
|
if (baseDeliveryIds !== undefined) {
|
|
680
735
|
try {
|
|
681
736
|
repositoryBases = await (0, multi_repo_bases_1.resolveRepositoryDeliveryBases)(c.db, c.project, repositoryIds, baseDeliveryIds, worktree_manager_1.defaultGitRunner);
|
|
@@ -983,6 +1038,18 @@ function createRailsRouter() {
|
|
|
983
1038
|
return;
|
|
984
1039
|
}
|
|
985
1040
|
const scope = loopGraph.config.ticketScope ?? (loopGraph.config.journal === 'implementation' ? 'all' : (0, loop_command_catalog_1.dominantTicketScope)(promptsText));
|
|
1041
|
+
// Freeze only admitted launch choices. Execution paths, run ids,
|
|
1042
|
+
// addendum claims and revision generation ids must never be replayed.
|
|
1043
|
+
const launchConfig = {
|
|
1044
|
+
mode, loopId, repositoryIds, workspaceSelection,
|
|
1045
|
+
aiEngine: rolesMode ? loop_role_engines_1.ROLES_ENGINE : loopProvider, model: loopModel,
|
|
1046
|
+
reasoning_effort: effort, profileName: resolvedProfile,
|
|
1047
|
+
...(runtimeProviderOverride ? { runtimeProviderOverride } : {}),
|
|
1048
|
+
...(baseBranch ? { baseBranch } : {}),
|
|
1049
|
+
...(baseDeliveryIds ? { baseDeliveryIds } : {}),
|
|
1050
|
+
...(typeof targetPrNumber === 'number' ? { targetPrNumber } : {}),
|
|
1051
|
+
...(followUp ? { followUp } : {}),
|
|
1052
|
+
};
|
|
986
1053
|
// Parallel isolation (default-on; disable with SPECRAILS_RAIL_WORKTREES=0):
|
|
987
1054
|
// a per-ticket rail on a repo-mutating loop runs each ticket in its own git
|
|
988
1055
|
// worktree, then merges the branches back. A non-git repo or an unborn
|
|
@@ -991,6 +1058,7 @@ function createRailsRouter() {
|
|
|
991
1058
|
let isolationUnavailable;
|
|
992
1059
|
let continuablePrDelivery = null;
|
|
993
1060
|
let revisionRequest = null;
|
|
1061
|
+
let retryOfDelivery = null;
|
|
994
1062
|
// Read-only vs mutating is DERIVED from the loop's nodes (see loop-effect),
|
|
995
1063
|
// not a user flag — a content-read-only loop (no ai-step/shell) never writes,
|
|
996
1064
|
// so it is not isolated; anything that can write is.
|
|
@@ -1006,7 +1074,7 @@ function createRailsRouter() {
|
|
|
1006
1074
|
// `pr_decision_pending` — the user already saw the failure on the card,
|
|
1007
1075
|
// and pressing Launch again IS the decision. A revision naming it is
|
|
1008
1076
|
// meaningless (there is no work to revise) and gets a precise 409.
|
|
1009
|
-
if (pending && (0, rail_pr_store_1.isPreparationFailureRow)(pending)) {
|
|
1077
|
+
if (pending && !relaunchSource && (0, rail_pr_store_1.isPreparationFailureRow)(pending)) {
|
|
1010
1078
|
if (revisionOfDeliveryId === pending.id) {
|
|
1011
1079
|
res.status(409).json({
|
|
1012
1080
|
error: 'invalid_revision_target',
|
|
@@ -1029,6 +1097,15 @@ function createRailsRouter() {
|
|
|
1029
1097
|
// Explicit change requests name it; open addenda on the same rail
|
|
1030
1098
|
// identify it implicitly. Both use the existing supersession contract.
|
|
1031
1099
|
const revisionsEnabled = (0, feature_flags_1.areDeliveryRevisionsEnabled)();
|
|
1100
|
+
const failedAddendumRetry = Boolean(launchAddenda.length && pendingSnapshot && !revisionOfDeliveryId
|
|
1101
|
+
&& ['implementation_failed', 'pr_failed'].includes(pendingSnapshot.decision)
|
|
1102
|
+
&& !pendingSnapshot.prUrl && !hasDeliveredWork(pendingSnapshot)
|
|
1103
|
+
&& new Set(pendingSnapshot.ticketIds).size === new Set(rail.ticketIds).size
|
|
1104
|
+
&& rail.ticketIds.every(id => pendingSnapshot.ticketIds.includes(id)));
|
|
1105
|
+
const identifiedFailureRetry = Boolean(relaunchSource?.delivery && pendingSnapshot
|
|
1106
|
+
&& relaunchSource.sourceId === pendingSnapshot.id
|
|
1107
|
+
&& ['implementation_failed', 'pr_failed'].includes(pendingSnapshot.decision)
|
|
1108
|
+
&& !hasDeliveredWork(pendingSnapshot));
|
|
1032
1109
|
const addendumContinuation = Boolean(launchAddenda.length && pendingSnapshot && pendingSnapshot.decision !== 'building'
|
|
1033
1110
|
&& !revisionOfDeliveryId
|
|
1034
1111
|
&& prDeliveryRevisionAllowed(pendingSnapshot, pendingSnapshot.id, rail.ticketIds));
|
|
@@ -1042,7 +1119,7 @@ function createRailsRouter() {
|
|
|
1042
1119
|
});
|
|
1043
1120
|
return;
|
|
1044
1121
|
}
|
|
1045
|
-
if (pendingSnapshot && !revisionOfPending && !prDeliveryContinuesTickets(pendingSnapshot, rail.ticketIds)) {
|
|
1122
|
+
if (pendingSnapshot && !revisionOfPending && !failedAddendumRetry && !identifiedFailureRetry && !prDeliveryContinuesTickets(pendingSnapshot, rail.ticketIds)) {
|
|
1046
1123
|
res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
|
|
1047
1124
|
return;
|
|
1048
1125
|
}
|
|
@@ -1055,11 +1132,13 @@ function createRailsRouter() {
|
|
|
1055
1132
|
: revisionNote,
|
|
1056
1133
|
};
|
|
1057
1134
|
}
|
|
1135
|
+
if ((failedAddendumRetry || identifiedFailureRetry) && pendingSnapshot)
|
|
1136
|
+
retryOfDelivery = { deliveryId: pendingSnapshot.id, decision: pendingSnapshot.decision };
|
|
1058
1137
|
// Explicit target vs an undecided continuable delivery: the slot's
|
|
1059
1138
|
// active generation owns its PR. A DIFFERENT explicit target would
|
|
1060
1139
|
// append to the undecided branches → 409; the SAME PR is redundant
|
|
1061
1140
|
// (the continuation contract below already drives it) and is dropped.
|
|
1062
|
-
if (pendingSnapshot && typeof targetPrNumber === 'number' && pendingSnapshot.prNumber !== targetPrNumber
|
|
1141
|
+
if (pendingSnapshot && !retryOfDelivery && typeof targetPrNumber === 'number' && pendingSnapshot.prNumber !== targetPrNumber
|
|
1063
1142
|
&& !pendingSnapshot.repositoryDeliveries?.some((delivery) => delivery.prNumber === targetPrNumber)) {
|
|
1064
1143
|
res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
|
|
1065
1144
|
return;
|
|
@@ -1080,6 +1159,11 @@ function createRailsRouter() {
|
|
|
1080
1159
|
res.status(409).json({ error: 'repository_isolation_required', detail: 'Selected repositories require worktree isolation for this loop.' });
|
|
1081
1160
|
return;
|
|
1082
1161
|
}
|
|
1162
|
+
if (relaunchSource?.delivery && !useIsolation) {
|
|
1163
|
+
res.status(409).json({ error: 'relaunch_isolation_required', detail: 'The original attempt was isolated; its retry must also use an isolated worktree.',
|
|
1164
|
+
action: 'Restore worktree isolation and the original workflow before retrying.' });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1083
1167
|
if (typeof targetPrNumber === 'number' && !useIsolation) {
|
|
1084
1168
|
res.status(400).json({ error: 'target_pr_requires_pr_mode', detail: 'targetPrNumber requires an isolated (worktree) launch; this launch would run in the shared checkout' });
|
|
1085
1169
|
return;
|
|
@@ -1133,7 +1217,10 @@ function createRailsRouter() {
|
|
|
1133
1217
|
}
|
|
1134
1218
|
}
|
|
1135
1219
|
try {
|
|
1220
|
+
if (relaunchSource && (0, rail_relaunch_1.restoreRelaunchRail)(c, railIndex, relaunchSource))
|
|
1221
|
+
broadcastRailUpdated(c, railIndex, 'tickets');
|
|
1136
1222
|
const ids = await (0, rail_isolated_launch_1.launchIsolatedRail)({
|
|
1223
|
+
launchConfig,
|
|
1137
1224
|
runtimeProviderOverride,
|
|
1138
1225
|
ctx: c, railIndex, ticketIds: [...rail.ticketIds], repositoryIds, workspaceSelection, ...repositoryBases, loopId, loopName, loopGraph,
|
|
1139
1226
|
provider: loopProvider, model: loopModel, effort, scope,
|
|
@@ -1142,7 +1229,7 @@ function createRailsRouter() {
|
|
|
1142
1229
|
originSurface: originSurface ?? 'dashboard',
|
|
1143
1230
|
originConversationId: originConversationId ?? null,
|
|
1144
1231
|
...(baseBranch ? { baseBranch } : {}),
|
|
1145
|
-
...(continuablePrDelivery?.executionManifest && !revisionRequest ? { repositoryContinuation: { deliveryId: continuablePrDelivery.id, decision: continuablePrDelivery.decision } } : {}),
|
|
1232
|
+
...(continuablePrDelivery?.executionManifest && !revisionRequest && !retryOfDelivery ? { repositoryContinuation: { deliveryId: continuablePrDelivery.id, decision: continuablePrDelivery.decision } } : {}),
|
|
1146
1233
|
...(continuablePrDelivery && !continuablePrDelivery.executionManifest && prDeliveryContinuesTickets(continuablePrDelivery, rail.ticketIds) ? {
|
|
1147
1234
|
requiredPrContinuation: {
|
|
1148
1235
|
deliveryId: continuablePrDelivery.id,
|
|
@@ -1157,18 +1244,23 @@ function createRailsRouter() {
|
|
|
1157
1244
|
// Explicit target only drives fresh launches; when the slot has
|
|
1158
1245
|
// a continuable delivery for the same PR, the continuation
|
|
1159
1246
|
// contract above is the (stricter) authority.
|
|
1160
|
-
...(typeof targetPrNumber === 'number' && !continuablePrDelivery
|
|
1247
|
+
...(typeof targetPrNumber === 'number' && (!continuablePrDelivery || retryOfDelivery)
|
|
1161
1248
|
? { explicitPrTarget: { prNumber: targetPrNumber } }
|
|
1162
1249
|
: {}),
|
|
1163
1250
|
// Revision of an undecided delivery: the guard above proved the
|
|
1164
1251
|
// exemption, so pass the contract through for supersession.
|
|
1165
1252
|
...(revisionRequest ? { revision: revisionRequest } : {}),
|
|
1253
|
+
...(retryOfDelivery ? { retryOfDelivery } : {}),
|
|
1166
1254
|
...(followUp ? { followUp } : {}),
|
|
1167
1255
|
});
|
|
1168
1256
|
res.status(202).json({ loopRunIds: ids, railIndex, mode, loopId, prDeliveryId: (0, rail_pr_store_1.getActivePrDeliveryByRail)(c.db, railIndex)?.id ?? null, isolated: true, ...(followUp ? { followUp: { id: followUp.id, version: followUp.version, hash: followUp.hash } } : {}) });
|
|
1169
1257
|
return;
|
|
1170
1258
|
}
|
|
1171
1259
|
catch (err) {
|
|
1260
|
+
if (err instanceof rail_relaunch_1.RailRelaunchError) {
|
|
1261
|
+
res.status(409).json({ error: err.code, detail: err.message, action: err.action });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1172
1264
|
if (err instanceof rail_pr_store_1.PrDeliveryGenerationConflict) {
|
|
1173
1265
|
res.status(409).json({
|
|
1174
1266
|
error: 'pr_delivery_generation_conflict',
|
|
@@ -1186,7 +1278,7 @@ function createRailsRouter() {
|
|
|
1186
1278
|
});
|
|
1187
1279
|
return;
|
|
1188
1280
|
}
|
|
1189
|
-
if (continuablePrDelivery || err instanceof rail_isolated_launch_1.PrContinuationIsolationError) {
|
|
1281
|
+
if ((continuablePrDelivery && !retryOfDelivery) || err instanceof rail_isolated_launch_1.PrContinuationIsolationError) {
|
|
1190
1282
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1191
1283
|
res.status(409).json({
|
|
1192
1284
|
error: 'pr_continuation_isolation_required',
|
|
@@ -1208,6 +1300,8 @@ function createRailsRouter() {
|
|
|
1208
1300
|
// Spawn from the SAME cwd a rail uses (workspace when relocated, else the
|
|
1209
1301
|
// repo) so native `{{cmd:*}}` slash commands resolve — and surface the repo
|
|
1210
1302
|
// via SPECRAILS_REPO_DIR + `--add-dir` exactly like QueueManager.
|
|
1303
|
+
if (relaunchSource && (0, rail_relaunch_1.restoreRelaunchRail)(c, railIndex, relaunchSource))
|
|
1304
|
+
broadcastRailUpdated(c, railIndex, 'tickets');
|
|
1211
1305
|
const loopExec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: c.project.slug, path: c.project.path });
|
|
1212
1306
|
const loopRunIds = [];
|
|
1213
1307
|
const loopTicketCompletionStatus = (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? 'on_review' : 'done';
|
|
@@ -1270,6 +1364,7 @@ function createRailsRouter() {
|
|
|
1270
1364
|
};
|
|
1271
1365
|
const runPromise = c.loopRunManager
|
|
1272
1366
|
.run({
|
|
1367
|
+
launchConfig,
|
|
1273
1368
|
runtimeProviderOverride,
|
|
1274
1369
|
runId,
|
|
1275
1370
|
loopId,
|
|
@@ -1414,6 +1509,10 @@ function createRailsRouter() {
|
|
|
1414
1509
|
res.status(202).json({ jobId, railIndex, mode });
|
|
1415
1510
|
}
|
|
1416
1511
|
catch (err) {
|
|
1512
|
+
if (err instanceof rail_relaunch_1.RailRelaunchError) {
|
|
1513
|
+
res.status(409).json({ error: err.code, detail: err.message, action: err.action });
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1417
1516
|
if (err instanceof queue_manager_1.ClaudeNotFoundError) {
|
|
1418
1517
|
res.status(503).json({ error: 'Claude CLI not found' });
|
|
1419
1518
|
return;
|
|
@@ -46,8 +46,12 @@ function buildRevisionSeed(input) {
|
|
|
46
46
|
return [
|
|
47
47
|
'## Revision briefing',
|
|
48
48
|
'',
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
input.branches.length > 0
|
|
50
|
+
? `This is revision ${input.revisionNumber} of recorded branch work for the spec(s) below.`
|
|
51
|
+
: `This is attempt ${input.revisionNumber} for the spec(s) below; no delivered branch was recorded.`,
|
|
52
|
+
input.branches.length > 0
|
|
53
|
+
? 'Inspect the recorded branch first. Preserve its existing implementation and apply the requested changes.'
|
|
54
|
+
: 'Implement the full spec together with the requested changes; do not assume prior implementation exists.',
|
|
51
55
|
'',
|
|
52
56
|
'## What the user asked to change',
|
|
53
57
|
'',
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
"architect": "## Your task: architecture\n\nYou are the Specrails architect. Turn the requested work into an unambiguous, implementable plan that a developer agent will execute without talking to you.\n\n1. Orient quickly: locate the code the change touches, the tests that cover it and the conventions that apply. Calibrate depth to the blast radius. A localized change gets a short proposal, focused design and only the tasks it needs; a cross-cutting change earns a full impact analysis.\nIn design.md include a Local reference patterns section: cite actual repository paths and symbols for the closest existing implementation and its tests. Explain async rendering/state updates, error propagation and test setup when relevant. Check installed framework versions. If no equivalent exists, state that explicitly; never invent references.\n2. Decide the approach, name the exact files/modules to create or change (inside the repository scope when one is declared), and call out risks, edge cases and compatibility concerns. Plan only the change the requested work needs; unrelated clean-ups are out of scope.\n3. Break the work into ordered, atomic tasks. Each task names concrete files and includes its own tests. Every task must be completable by an agent that can only edit files and run commands: never add tasks such as \"run the test suite\", \"verify\", \"test manually in a browser\", \"commit\" or \"open a PR\". Core runs verification and the host owns delivery.\n4. Execute the official OpenSpec fast-forward skill. Query status and instructions in dependency order; author the real delta specs and other artifacts using those templates and project rules. Preserve existing requirements through OpenSpec delta semantics. Do not fabricate complete replacement specs.\n5. Propose verification. For each repository listed below without a configured verification command, name the existing command that proves the change: the project's test script, type check, build or lint (for example `npm` with args `[\"test\"]`, or `cargo` with `[\"test\"]`). Only propose commands that exist in the repository today or that a task in this plan adds; omit repositories where nothing automated applies. For a repository with a scope, the command must exercise that scope: omit cwd (it then runs in the scope directory) or name a directory inside it; never a command that runs every workspace of a larger checkout. Core runs them after the developer finishes and feeds failures back.\n6. Score your confidence honestly: `high` when the code evidence is conclusive; `medium` when the design rests on one non-obvious assumption (name it in the design); `low` when several plausible designs exist and you cannot choose without missing information. With `low`, put the single question whose answer decides the design in `question`. Core first lets you investigate further, then either asks the requester that exact question or proceeds on your stated assumptions, depending on the project configuration.",
|
|
3
3
|
"developer": "## Your task: implementation\n\nYou are the Specrails developer. Execute the official openspec-apply-change skill for the approved change, implement its pending tasks completely and update their progress through that workflow. Do not substitute a hand-written approximation of apply.\n\n1. Load and execute openspec-apply-change through the supplied binding. Consult its status and instructions apply, read the context files OpenSpec returns, then the relevant existing code and tests.\n2. Work task by task in order. Use test-driven development: write or extend the test first, make it pass with the smallest correct change, then tidy up. Run only focused tests that cover what you touched while iterating. Core owns the complete verification plan and runs it after your turn; do not duplicate that full run. Fix the precise failures Core returns on a correction pass.\n3. Immediately after completing each task, mark it `- [x]` in `tasks.md`; do not postpone all progress updates until the end of the phase. Only mark tasks whose code and tests are complete. Change nothing else in `tasks.md`, and never edit `proposal.md`, `design.md` or the specs: those documents are frozen, and editing them invalidates the run. If a task cannot be completed, leave it `- [ ]` and list it under `incomplete` with the reason.\n4. Keep the implementation consistent with the repository: naming, error handling, import style, formatting and existing utilities. Do not add dependencies unless the design requires them. Change only what the tasks require: the diff should contain the requested change and its tests, nothing else.\nInvestigation budget: consult the local reference patterns in design.md and equivalent application tests before framework internals or node_modules. After three unsuccessful experiments on the same failure, stop repeating commands: state the hypothesis, evidence and next discriminating experiment, then change approach. If three further experiments add no evidence, report the specific blocker under incomplete rather than consuming the remaining turn budget. Never weaken assertions or change acceptance criteria to make a test pass.\nVerification evidence: run tests without piping their output through grep/head or other filters that mask the original exit status. Capture complete stdout/stderr in a temporary log and preserve the test process exit code; inspect that log separately. Report the command and original exit code. Remove temporary debug tests before finishing. Core independently runs the final verification commands.\n5. If the shell is unavailable, still finish every task that only needs code and tests; Core runs the verification commands after your turn and returns the exact failures to you.\n\n## Durable implementation progress\n\nWhen loading the official apply skill, read its savedProgress handoff before repeating investigation. Reconcile it with the current diff and tasks; it is advisory history, never accepted verification evidence. Preserve completed work and previously discovered test commands or environment blockers. Recheck a blocker only when its relevant condition changed.\nAfter each completed task, and whenever the next action or a blocker changes, call the scoped workflow action write_progress with progress: {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Replace the previous handoff with a concise current account (at most 8 KB); include exact focused test commands, original exit outcomes, relevant paths and the next discriminating action. Keep nextTasks concrete enough for a fresh session to continue unfinished work. Do not include full logs, credentials or claims that old checks authorize acceptance. The host persists this record outside the frozen OpenSpec artifacts. Use read_progress to refresh it. If interrupted, the next session receives it from load_skill; do not write runtime files directly.",
|
|
4
4
|
"reviewer": "## Your task: review\n\nYou are the Specrails reviewer and the last gate before this change is archived. Inspect the implementation, the approved artifacts and the verification evidence read-only. Do not fix anything.\n\nCheck, in this order:\n1. Spec completeness: every requirement in the change specs and every acceptance criterion is implemented. Cross-reference each one against the code.\n2. Task completion: every task in `tasks.md` is `- [x]` and is backed by real code and tests, not just a ticked box.\n3. Test quality: new behavior has tests that assert on behavior, cover error paths, and would fail without the change. Missing tests for production code are a blocking issue.\n4. Correctness and conventions: types and signatures fit the codebase, patterns match the repository, imports and error handling are consistent, no unrelated changes.\n5. Security: no secrets, injection, path traversal, unsafe deserialization, missing authorization or new attack surface. Scale scrutiny to what the change touches.\n6. Performance: no obvious N+1, unbounded loops or blocking work on hot paths introduced by the change.\n\nJudge BEHAVIOUR against the acceptance criteria, never the shape of the code against the wording of the plan: module layout, file lists, class or function names, naming conventions and implementation techniques named in the ticket, the design or a \"contract layer\" are suggestions the developer may legitimately improve on. A working implementation that satisfies a criterion by other means is correct; asking to rename, move or rewrite it to match the plan's wording is NOT an issue. Only a criterion that is not met, a defect, a missing test or a regression is.\n\nReview THE CHANGE. The change set below lists every file this run changed relative to its base, measured by Core from git. Judge those changes and the behavior they affect; code the change did not touch is not an issue unless the change breaks it. Never ask for edits to pre-existing code, other packages or workspaces, or configuration outside the change. An environment problem (credentials, registry access, missing tools) is not a code issue: record it under findings, never as an issue for the fixer.\n\nThe verification evidence below comes from real subprocesses run by Core after the developer finished; treat it as fact, not as a claim by the developer.",
|
|
5
|
-
"fixer": "## Your task: correction\n\nYou are the Specrails FIXER. The verification commands (or the review) failed after the developer's pass; your only job is to make them pass with minimal, precise edits to this change.\n\n1. Read the
|
|
5
|
+
"fixer": "## Your task: correction\n\nYou are the Specrails FIXER. The verification commands (or the review) failed after the developer's pass; your only job is to make them pass with minimal, precise edits to this change.\n\n1. Read the host failure facts first: command, cwd, original exit code, failureSummary, expected assertion and application file/line. If the excerpt is truncated, use read_verification_evidence with the command's evidenceId for complete stdout/stderr and source. Read the failing test and the implementation it exercises.\n2. Reproduce the failure with the narrowest test command. Compare the assertion, intended behavior, current implementation and actual diff. An unchanged file or a pre-existing test does not prove the failure is unrelated: changes can break existing tests indirectly. Classify the cause as an implementation defect, a test compatibility assumption, or an external/scope blocker. A claim that the failure existed before this change needs baseline or historical evidence; otherwise state that it is unconfirmed.\n3. Patch exactly the files and lines the failure names; touch neighbouring code only when the failure cannot be fixed otherwise. Rewrite a file only when a patch cannot express the change.\n4. Fix incorrect implementation behavior. A failing mandatory host test inside the admitted repository/workspace may also receive a minimal compatibility repair when you prove that its expected behavior is unchanged. For example, a source assertion broken only by formatting may tolerate whitespace while retaining every required operand and guard. This applies even to a pre-existing test outside the current changed-file list. Explain the original expectation and why the repair still rejects the prohibited behavior. Never weaken an assertion, delete or skip a test, hardcode success, change acceptance criteria, add dependencies or widen the repair beyond the diagnosed failure.\n5. A test file the host reports as never executed must be wired into the repository's test command (the test script or runner configuration) and then made to pass.\n6. Confirm the repair with the focused test command and a negative case for any repaired assertion: removing a required safety guard or returning the wrong value must still fail. Preserve the original test exit status; never pipe test execution through grep/head or another command that masks it. Capture stdout/stderr in a temporary log and inspect that log separately. Core runs the complete verification plan after your turn.\n7. Inspect only the reported files, direct dependencies and the evidence needed to establish the cause. Never edit outside the admitted repository/workspace, credentials, environment or unrelated configuration. For an external or unrepairable scope blocker, make no speculative edits and report the exact error, observed evidence and required action. `proposal.md`, `design.md` and the specs remain frozen; only tick completed tasks.\n8. Return the requested JSON contract. Include the diagnosis, exact focused commands and original exit codes, repair and preserved assertion in summary. List every unresolved failure and its reason under incomplete, even when the implementation tasks are already ticked. A failed test remains failed until real host verification passes.\n\n## Durable implementation progress\n\nWhen loading the official apply skill, read its savedProgress handoff before repeating investigation. Reconcile it with the current diff and tasks; it is advisory history, never accepted verification evidence. Preserve completed work and previously discovered test commands or environment blockers. Recheck a blocker only when its relevant condition changed.\nAfter each completed task, and whenever the next action or a blocker changes, call the scoped workflow action write_progress with progress: {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Replace the previous handoff with a concise current account (at most 8 KB); include exact focused test commands, original exit outcomes, relevant paths and the next discriminating action. Keep nextTasks concrete enough for a fresh session to continue unfinished work. Do not include full logs, credentials or claims that old checks authorize acceptance. The host persists this record outside the frozen OpenSpec artifacts. Use read_progress to refresh it. If interrupted, the next session receives it from load_skill; do not write runtime files directly."
|
|
6
6
|
}
|
|
@@ -129,7 +129,7 @@ function createDefinitionEventProjection(input) {
|
|
|
129
129
|
persist('loop_step', { index: nextCount, kind: 'core', title: workflow.nodePath, iteration: workflow.visit, startedAtMs: Date.parse(at), ...correlation });
|
|
130
130
|
}
|
|
131
131
|
else if (opening && ['step_succeeded', 'step_failed', 'step_blocked', 'step_interrupted', 'step_retrying', 'step_paused'].includes(String(workflow.type))) {
|
|
132
|
-
persist('loop_step_end', { index: opening.index, status: workflow.type === 'step_succeeded' ? 'ok' : workflow.type === 'step_paused' ? 'paused' : workflow.type === 'step_interrupted' ? 'interrupted' : 'failed',
|
|
132
|
+
persist('loop_step_end', { index: opening.index, status: workflow.type === 'step_succeeded' ? workflow.outcome === 'fail' ? 'failed' : 'ok' : workflow.type === 'step_paused' ? 'paused' : workflow.type === 'step_interrupted' ? 'interrupted' : 'failed',
|
|
133
133
|
runtimeStatus: String(workflow.type).slice(5), outcome: workflow.outcome, exitCode: null,
|
|
134
134
|
durationMs: Math.max(0, Date.parse(at) - Number(opening.startedAtMs)), ...correlation });
|
|
135
135
|
}
|
|
@@ -35,15 +35,19 @@ function configurableImplementGraph() {
|
|
|
35
35
|
const implementation = objectSchema({ summary: string, incomplete: strings });
|
|
36
36
|
turn('developer', 'Developer', 'build', `Implement every approved task for change ${change} using the bound official apply skill. Preserve proposal, design and specs; only tick tasks you completed. Report summary and incomplete task descriptions. Prior execution evidence and human answers: {{history.text}}`, implementation, 'tasks');
|
|
37
37
|
node('tasks', 'Check artifacts and completed tasks', 'artifact-contract', { change, contractId: 'plan', action: 'check', requireCompletedTasks: true }, { pass: 'verify', fail: 'developer', failed: 'failed' });
|
|
38
|
-
node('verify', 'Verify candidate', 'verify', { commands: 'configured', additionalCommandsFrom: 'architect' }, { pass: 'reviewer', fail: '
|
|
38
|
+
node('verify', 'Verify candidate', 'verify', { commands: 'configured', additionalCommandsFrom: 'architect' }, { pass: 'reviewer', fail: 'correction-context', failed: 'failed' });
|
|
39
39
|
turn('reviewer', 'Reviewer', 'assess', `Review change ${change} read-only against all approved artifacts and frozen ticket acceptance criteria. Inspect real code and the host verification evidence: {{outputs.verify}}. Return approved, summary, issues, score (0-100), and aspects. Prior execution: {{history.text}}`, objectSchema({ approved: { type: 'boolean' }, summary: string, issues: strings, score, aspects: objectSchema(Object.fromEntries(Object.keys(aspects).map(aspect => [aspect, score]))) }), 'review-policy');
|
|
40
|
-
node('review-policy', 'Review acceptance policy', 'condition', { expr: ['$outputs.reviewer.structured.approved == true', '$outputs.reviewer.structured.issues.length == 0', '$outputs.reviewer.structured.score >= 70', ...Object.entries(aspects).map(([aspect, threshold]) => `$outputs.reviewer.structured.aspects.${aspect} >= ${threshold}`)].join(' && ') }, { true: 'approve', false: '
|
|
41
|
-
|
|
40
|
+
node('review-policy', 'Review acceptance policy', 'condition', { expr: ['$outputs.reviewer.structured.approved == true', '$outputs.reviewer.structured.issues.length == 0', '$outputs.reviewer.structured.score >= 70', ...Object.entries(aspects).map(([aspect, threshold]) => `$outputs.reviewer.structured.aspects.${aspect} >= ${threshold}`)].join(' && ') }, { true: 'approve', false: 'correction-context' });
|
|
41
|
+
node('correction-context', 'Select current review findings', 'condition', { expr: '$outputs.verify.valid == true && exists($outputs.reviewer.candidateHash) && $outputs.reviewer.candidateHash == $outputs.verify.candidateHash' }, { true: 'capture-review', false: 'clear-review' });
|
|
42
|
+
node('capture-review', 'Capture current review findings', 'assign', { set: { latestReview: '{{outputs.reviewer}}' } }, { next: 'fixer', failed: 'failed' });
|
|
43
|
+
node('clear-review', 'Clear outdated review findings', 'assign', { set: { latestReview: 'No reviewer findings for the current verified candidate. Diagnose the current host verification failure first.' } }, { next: 'fixer', failed: 'failed' });
|
|
44
|
+
turn('fixer', 'Fixer', 'correct', `Correct the reported implementation, test or review failure for change ${change} using the bound apply skill and preserving approved artifacts. Read failureSummary, command, cwd, exitCode and evidenceId first; use read_verification_evidence for truncated evidence. Reproduce the exact focused failure, inspect the assertion and tested code, then apply the smallest repair that preserves the required behavior. An unchanged filename does not prove an unrelated failure. A mandatory test inside the admitted workspace may receive a proven formatting/compatibility repair while retaining every required guard. Report diagnosis, focused commands with original exit codes, repair and unresolved failures in summary and incomplete. If the cause cannot be repaired within scope, explain the evidence and required action; an unchanged candidate with failed verification stops this run. Host verification: {{outputs.verify}}. Latest review findings for this candidate: {{run.latestReview}}. Prior execution: {{history.text}}`, implementation, 'correction-progress');
|
|
45
|
+
node('correction-progress', 'Check correction progress', 'condition', { expr: '$outputs.verify.valid == true || (exists($outputs.fixer.candidateHash) && $outputs.fixer.candidateHash != null && $outputs.fixer.candidateHash != $outputs.verify.candidateHash)' }, { true: 'tasks', false: 'correction-stalled' });
|
|
42
46
|
node('approve', 'Approve archive', 'approval', { reason: `Approve archiving the reviewed and verified candidate for change ${change}.`, bindCandidate: true, enabled: agents.approvalBeforeArchive === true }, { next: 'approval-policy' });
|
|
43
47
|
node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
|
|
44
48
|
node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'verify' });
|
|
45
49
|
node('verify-archive', 'Verify archived candidate', 'verify', { commands: 'configured', additionalCommandsFrom: 'architect' }, { pass: 'done', fail: 'failed', failed: 'failed' });
|
|
46
|
-
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } });
|
|
50
|
+
nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Verification remains failed and the correction made no candidate changes. {{outputs.fixer.structured.summary}}' } });
|
|
47
51
|
edges.unshift({ id: 'e-start', source: 'start', target: 'architect' });
|
|
48
52
|
return { nodes, edges, config: { maxIterations: 12, maxTransitions: 200, timeoutMinutes: 0, aiStepTimeoutMinutes: 0, change: 'new', journal: 'ledger-only', ticketScope: 'all', agents, reviewerStepId: 'reviewer' } };
|
|
49
53
|
}
|
|
@@ -145,29 +145,37 @@ const CHANGE_ID_RE = /openspec[\\/]+changes[\\/]+(?!archive(?:[\\/]|$))([A-Za-z0
|
|
|
145
145
|
function resolveRunVars(text, vars) {
|
|
146
146
|
return text.replace(RUN_TOKEN_RE, (_m, key) => vars[key] ?? '');
|
|
147
147
|
}
|
|
148
|
-
const CHANGE_NAME_RE = /^[a-z0-9][a-z0-9
|
|
148
|
+
const CHANGE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
149
149
|
/** The authoritative OpenSpec target for a launch.
|
|
150
|
-
* Addenda/delivery deltas
|
|
150
|
+
* Addenda/delivery deltas use durable content/lineage identity; ordinary launches use the
|
|
151
151
|
* follow-up's declared name, else the spec's `openspecChangeName`. */
|
|
152
152
|
function seedChangeId(req) {
|
|
153
|
-
|
|
154
|
-
// A new generation never picks an old/archived proposal, even when the
|
|
155
|
-
// ticket metadata still names it. Resuming the same run keeps its target.
|
|
156
|
-
const hash = (0, crypto_1.createHash)('sha256').update(JSON.stringify([req.runId, req.addenda.ids, req.addenda.briefing])).digest('hex').slice(0, 20);
|
|
157
|
-
return { id: `spec-addenda-${hash}`, source: 'addenda' };
|
|
158
|
-
}
|
|
159
|
-
if (req.constants?.REVISION_REQUEST) {
|
|
160
|
-
const hash = (0, crypto_1.createHash)('sha256').update(JSON.stringify([req.runId, req.constants.REVISION_REQUEST])).digest('hex').slice(0, 20);
|
|
161
|
-
return { id: `delivery-change-${hash}`, source: 'revision' };
|
|
162
|
-
}
|
|
153
|
+
const valid = (value) => value.length <= 64 && CHANGE_NAME_RE.test(value);
|
|
163
154
|
const fromFollowUp = typeof req.followUp?.openspecChangeName === 'string' ? req.followUp.openspecChangeName.trim() : '';
|
|
164
|
-
if (fromFollowUp && CHANGE_NAME_RE.test(fromFollowUp))
|
|
165
|
-
return { id: fromFollowUp, source: 'followUp' };
|
|
166
155
|
const direct = typeof req.spec?.openspecChangeName === 'string' ? req.spec.openspecChangeName.trim() : '';
|
|
167
156
|
const fromMetadata = typeof req.spec?.metadata?.openspecChangeName === 'string' ? req.spec.metadata.openspecChangeName.trim() : '';
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
const declared = valid(fromFollowUp) ? fromFollowUp : valid(direct || fromMetadata) ? direct || fromMetadata : '';
|
|
158
|
+
const source = declared === fromFollowUp ? 'followUp' : 'spec';
|
|
159
|
+
if (req.addenda?.ids.length || req.constants?.REVISION_REQUEST) {
|
|
160
|
+
const state = declared ? openspecChangeState(req.cwd, declared) : 'missing';
|
|
161
|
+
// A pending implementation's addenda refine the same change. Only work
|
|
162
|
+
// already delivered/archived needs a separate delta proposal.
|
|
163
|
+
if (declared && (state === 'active' || (state === 'missing' && !req.constants?.REVISION_REQUEST && req.spec?.status !== 'on_review')))
|
|
164
|
+
return { id: declared, source };
|
|
165
|
+
const hash = (0, crypto_1.createHash)('sha256').update(JSON.stringify([
|
|
166
|
+
req.constants?.REVISION_OF_DELIVERY_ID ?? null,
|
|
167
|
+
req.constants?.REVISION_NUMBER ?? null,
|
|
168
|
+
req.constants?.REVISION_REQUEST ?? null,
|
|
169
|
+
req.addenda ? [...req.addenda.ids].sort() : [], req.addenda?.briefing ?? null,
|
|
170
|
+
])).digest('hex').slice(0, 12);
|
|
171
|
+
const revisionNumber = req.constants?.REVISION_NUMBER;
|
|
172
|
+
const kind = req.addenda?.ids.length ? 'addenda' : 'revision';
|
|
173
|
+
const suffix = req.constants?.REVISION_REQUEST ? `rev${revisionNumber && /^[1-9]\d{0,3}$/.test(revisionNumber) ? revisionNumber : '1'}-${hash}` : `addenda-${hash}`;
|
|
174
|
+
const stem = declared || (req.ticketId && req.ticketId > 0 ? `ticket-${req.ticketId}` : 'spec');
|
|
175
|
+
return { id: `${stem.slice(0, 63 - suffix.length).replace(/-+$/, '')}-${suffix}`, source: kind };
|
|
176
|
+
}
|
|
177
|
+
if (declared)
|
|
178
|
+
return { id: declared, source };
|
|
171
179
|
return undefined;
|
|
172
180
|
}
|
|
173
181
|
/** Where an OpenSpec change currently lives under `cwd`: an ACTIVE
|
|
@@ -854,6 +862,8 @@ class LoopRunManager {
|
|
|
854
862
|
});
|
|
855
863
|
if (definitionEngine)
|
|
856
864
|
(0, loop_runs_store_1.saveDefinitionRun)(this.db, runId, { request: req, source: 'definition' });
|
|
865
|
+
if (req.launchConfig)
|
|
866
|
+
this.db.prepare('UPDATE loop_runs SET launch_config_json = ? WHERE id = ?').run(JSON.stringify(req.launchConfig), runId);
|
|
857
867
|
if (req.executionManifest)
|
|
858
868
|
this.db.prepare('UPDATE loop_runs SET execution_manifest = ? WHERE id = ?').run(JSON.stringify(req.executionManifest), runId);
|
|
859
869
|
(0, db_1.createJob)(this.db, {
|
|
@@ -85,29 +85,39 @@ function createClaudeReader(options = {}) {
|
|
|
85
85
|
const oauth = (0, domain_1.record)(data.claudeAiOauth);
|
|
86
86
|
if (typeof oauth.accessToken !== 'string' || !oauth.accessToken)
|
|
87
87
|
return { availability: 'unsupported-auth', windows: [], plan: null, source: null };
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
async function query(route) {
|
|
89
|
+
const response = await (options.request ?? fetch)(`https://api.anthropic.com/api/oauth/${route}`, {
|
|
90
|
+
headers: { Authorization: `Bearer ${oauth.accessToken}`, 'anthropic-beta': 'oauth-2025-04-20', 'User-Agent': 'claude-code/2.1.0' },
|
|
91
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]), redirect: 'error',
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
await response.body?.cancel().catch(() => { });
|
|
95
|
+
if (response.status === 401)
|
|
96
|
+
throw new errors_1.UsageError('signed-out');
|
|
97
|
+
if (response.status === 403)
|
|
98
|
+
throw new errors_1.UsageError('permission-denied');
|
|
99
|
+
if (response.status === 429) {
|
|
100
|
+
const header = response.headers.get('retry-after');
|
|
101
|
+
const seconds = header !== null && header.trim() !== '' ? Number(header) : NaN;
|
|
102
|
+
const date = header ? Date.parse(header) : NaN;
|
|
103
|
+
const retry = Number.isFinite(seconds) ? seconds * 1000 : Number.isFinite(date) ? date - Date.now() : 60_000;
|
|
104
|
+
throw new errors_1.UsageError('rate-limited', true, Math.max(30_000, Math.min(24 * 60 * 60_000, retry)));
|
|
105
|
+
}
|
|
106
|
+
throw new errors_1.UsageError('provider-error', response.status >= 500);
|
|
104
107
|
}
|
|
105
|
-
|
|
108
|
+
return (0, local_auth_1.limitedJson)(response);
|
|
106
109
|
}
|
|
107
|
-
const payload = await (
|
|
108
|
-
|
|
110
|
+
const payload = await query('usage'), windows = (0, domain_1.normalizeClaude)(payload);
|
|
111
|
+
let plan = (0, domain_1.claudePlan)(oauth.subscriptionType) ?? (0, domain_1.claudePlan)(oauth.rateLimitTier);
|
|
112
|
+
const candidate = (0, domain_1.normalizeClaudeSpend)(payload, 'enterprise', (options.now ?? Date.now)());
|
|
113
|
+
if (!plan && candidate) {
|
|
114
|
+
const profile = (0, domain_1.record)(await query('profile')), organization = (0, domain_1.record)(profile.organization);
|
|
115
|
+
plan = (0, domain_1.claudePlan)(organization.organization_type) ?? (0, domain_1.claudePlan)(organization.rate_limit_tier);
|
|
116
|
+
}
|
|
117
|
+
const spend = plan === 'enterprise' ? candidate : null;
|
|
118
|
+
if (!windows.length && !spend)
|
|
109
119
|
throw new errors_1.UsageError('usage-unavailable');
|
|
110
|
-
return { availability: 'available', windows
|
|
120
|
+
return { availability: 'available', windows: spend ? [] : windows, spend, plan, source: 'oauth' };
|
|
111
121
|
},
|
|
112
122
|
};
|
|
113
123
|
}
|
|
@@ -5,7 +5,8 @@ exports.backoff = backoff;
|
|
|
5
5
|
function withFreshness(row, now) {
|
|
6
6
|
if (!row.observedAt)
|
|
7
7
|
return { ...row, freshness: 'unknown' };
|
|
8
|
-
const expired = now - Date.parse(row.observedAt) >= 300_000 || row.windows.some(w => w.resetsAt && Date.parse(w.resetsAt) <= now)
|
|
8
|
+
const expired = now - Date.parse(row.observedAt) >= 300_000 || row.windows.some(w => w.resetsAt && Date.parse(w.resetsAt) <= now)
|
|
9
|
+
|| !!row.spend?.resetsAt && Date.parse(row.spend.resetsAt) <= now;
|
|
9
10
|
return { ...row, freshness: expired || row.refreshState === 'error' ? 'stale' : 'fresh' };
|
|
10
11
|
}
|
|
11
12
|
function backoff(failures) { return Math.min(300_000, 30_000 * 2 ** Math.min(4, Math.max(0, failures - 1))); }
|
|
@@ -4,6 +4,9 @@ exports.duration = exports.percentage = exports.record = exports.USAGE_PROVIDERS
|
|
|
4
4
|
exports.timestamp = timestamp;
|
|
5
5
|
exports.windowLabel = windowLabel;
|
|
6
6
|
exports.normalizeClaude = normalizeClaude;
|
|
7
|
+
exports.claudePlan = claudePlan;
|
|
8
|
+
exports.hasClaudeWindows = hasClaudeWindows;
|
|
9
|
+
exports.normalizeClaudeSpend = normalizeClaudeSpend;
|
|
7
10
|
exports.normalizeCodex = normalizeCodex;
|
|
8
11
|
exports.initialUsage = initialUsage;
|
|
9
12
|
exports.USAGE_PROVIDERS = ['claude', 'codex'];
|
|
@@ -47,6 +50,37 @@ function normalizeClaude(payload) {
|
|
|
47
50
|
}
|
|
48
51
|
return windows;
|
|
49
52
|
}
|
|
53
|
+
function claudePlan(value) {
|
|
54
|
+
if (typeof value !== 'string')
|
|
55
|
+
return null;
|
|
56
|
+
const words = value.trim().toLowerCase().split(/[^a-z0-9]+/);
|
|
57
|
+
return ['enterprise', 'team', 'max', 'pro', 'ultra', 'free'].find(plan => words.includes(plan)) ?? null;
|
|
58
|
+
}
|
|
59
|
+
function hasClaudeWindows(windows) {
|
|
60
|
+
return windows.some(window => window.usedPercent !== null || window.resetsAt !== null);
|
|
61
|
+
}
|
|
62
|
+
function normalizeClaudeSpend(payload, plan, now) {
|
|
63
|
+
if (plan !== 'enterprise' || hasClaudeWindows(normalizeClaude(payload)))
|
|
64
|
+
return null;
|
|
65
|
+
const extra = (0, exports.record)((0, exports.record)(payload).extra_usage);
|
|
66
|
+
const amount = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
67
|
+
const used = amount(extra.used_credits), limit = amount(extra.monthly_limit);
|
|
68
|
+
if (used === null && limit === null)
|
|
69
|
+
return null;
|
|
70
|
+
const currency = extra.currency == null ? 'USD' : typeof extra.currency === 'string' ? extra.currency.trim().toUpperCase() : '';
|
|
71
|
+
if (!/^[A-Z]{3}$/.test(currency))
|
|
72
|
+
return null;
|
|
73
|
+
// OAuth spend is in currency minor units, including fractional minor units.
|
|
74
|
+
const exponent = new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
75
|
+
const divisor = 10 ** exponent;
|
|
76
|
+
const usedAmount = used === null ? null : used / divisor, limitAmount = limit === null ? null : limit / divisor;
|
|
77
|
+
const limitStatus = limit !== null ? 'limited' : extra.monthly_limit === null && extra.is_enabled === true ? 'unlimited' : 'unknown';
|
|
78
|
+
const ratio = used !== null && limit !== null && limit > 0 ? used / limit * 100 : null;
|
|
79
|
+
const usedPercent = limitStatus === 'limited' ? amount(extra.utilization) ?? (ratio !== null && Number.isFinite(ratio) ? ratio : null) : null;
|
|
80
|
+
const date = new Date(now);
|
|
81
|
+
return { kind: 'enterprise-on-demand', usedAmount, limitAmount, limitStatus, currency, usedPercent,
|
|
82
|
+
resetsAt: timestamp(extra.resets_at, typeof extra.resets_at === 'number') ?? new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1)).toISOString() };
|
|
83
|
+
}
|
|
50
84
|
function normalizeCodex(payload) {
|
|
51
85
|
const data = (0, exports.record)(payload), groups = (0, exports.record)(data.rateLimitsByLimitId);
|
|
52
86
|
const entries = Object.keys(groups).length ? Object.entries(groups) : [['codex', data.rateLimits]];
|
|
@@ -66,5 +100,5 @@ function normalizeCodex(payload) {
|
|
|
66
100
|
}
|
|
67
101
|
function initialUsage(providerId, generation) {
|
|
68
102
|
return { providerId, installed: null, generation, availability: 'unavailable', refreshState: 'idle', freshness: 'unknown', plan: null,
|
|
69
|
-
windows: [], source: null, observedAt: null, attemptedAt: null, retryAt: null, issue: null };
|
|
103
|
+
windows: [], spend: null, source: null, observedAt: null, attemptedAt: null, retryAt: null, issue: null };
|
|
70
104
|
}
|
|
@@ -59,7 +59,7 @@ function createUsageService(deps) {
|
|
|
59
59
|
const available = result.availability === 'available';
|
|
60
60
|
if (!available)
|
|
61
61
|
clear(id);
|
|
62
|
-
rows.set(id, { ...rows.get(id), availability: result.availability, plan: result.plan, source: result.source, windows: available ? result.windows : [],
|
|
62
|
+
rows.set(id, { ...rows.get(id), availability: result.availability, plan: result.plan, source: result.source, windows: available ? result.windows : [], spend: available ? result.spend ?? null : null,
|
|
63
63
|
// Identity is internal: explicitly select fields instead of returning adapter metadata.
|
|
64
64
|
providerId: id, installed: true, refreshState: 'idle', freshness: available ? 'fresh' : 'unknown',
|
|
65
65
|
observedAt: available ? new Date(now()).toISOString() : null, retryAt: new Date(now() + 30_000).toISOString(),
|