@jterrats/open-orchestra 0.5.7 → 1.0.2
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/AGENTS.md +9 -8
- package/CLAUDE.md +13 -11
- package/README.md +78 -11
- package/dist/assets/web-console.js +169 -32
- package/dist/automation-evidence.d.ts +23 -0
- package/dist/automation-evidence.js +97 -0
- package/dist/automation-evidence.js.map +1 -0
- package/dist/autonomous-run-store.js +3 -3
- package/dist/autonomous-run-store.js.map +1 -1
- package/dist/benchmark.d.ts +4 -1
- package/dist/benchmark.js +93 -4
- package/dist/benchmark.js.map +1 -1
- package/dist/cli.js +73 -2
- package/dist/cli.js.map +1 -1
- package/dist/collaboration-flows.js +3 -5
- package/dist/collaboration-flows.js.map +1 -1
- package/dist/collection-utils.d.ts +3 -0
- package/dist/collection-utils.js +10 -0
- package/dist/collection-utils.js.map +1 -0
- package/dist/command-manifest.d.ts +12 -1
- package/dist/command-manifest.js +213 -10
- package/dist/command-manifest.js.map +1 -1
- package/dist/commands.d.ts +10 -5
- package/dist/commands.js +16 -6
- package/dist/commands.js.map +1 -1
- package/dist/config-migrations.d.ts +24 -0
- package/dist/config-migrations.js +102 -0
- package/dist/config-migrations.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +23 -0
- package/dist/constants.js.map +1 -1
- package/dist/dashboard-commands.d.ts +2 -0
- package/dist/dashboard-commands.js +14 -0
- package/dist/dashboard-commands.js.map +1 -0
- package/dist/defaults.d.ts +13 -0
- package/dist/defaults.js +13 -0
- package/dist/defaults.js.map +1 -1
- package/dist/delegation-decision.js +23 -8
- package/dist/delegation-decision.js.map +1 -1
- package/dist/delivery-commands.js +5 -0
- package/dist/delivery-commands.js.map +1 -1
- package/dist/delivery-dashboard-charts.d.ts +4 -0
- package/dist/delivery-dashboard-charts.js +156 -0
- package/dist/delivery-dashboard-charts.js.map +1 -0
- package/dist/delivery-dashboard-html.d.ts +2 -0
- package/dist/delivery-dashboard-html.js +115 -0
- package/dist/delivery-dashboard-html.js.map +1 -0
- package/dist/delivery-dashboard-types.d.ts +78 -0
- package/dist/delivery-dashboard-types.js +2 -0
- package/dist/delivery-dashboard-types.js.map +1 -0
- package/dist/delivery-dashboard.d.ts +8 -0
- package/dist/delivery-dashboard.js +124 -0
- package/dist/delivery-dashboard.js.map +1 -0
- package/dist/effort-classification.d.ts +7 -0
- package/dist/effort-classification.js +72 -0
- package/dist/effort-classification.js.map +1 -0
- package/dist/extension-commands.d.ts +3 -0
- package/dist/extension-commands.js +40 -0
- package/dist/extension-commands.js.map +1 -0
- package/dist/extensions.d.ts +22 -0
- package/dist/extensions.js +126 -0
- package/dist/extensions.js.map +1 -0
- package/dist/github.d.ts +2 -0
- package/dist/github.js +15 -3
- package/dist/github.js.map +1 -1
- package/dist/health-checks.js +51 -0
- package/dist/health-checks.js.map +1 -1
- package/dist/lucid-story-map.d.ts +73 -0
- package/dist/lucid-story-map.js +112 -0
- package/dist/lucid-story-map.js.map +1 -0
- package/dist/mcp-integrations.d.ts +19 -0
- package/dist/mcp-integrations.js +58 -0
- package/dist/mcp-integrations.js.map +1 -0
- package/dist/mcp-tool-adapter.d.ts +21 -0
- package/dist/mcp-tool-adapter.js +56 -0
- package/dist/mcp-tool-adapter.js.map +1 -0
- package/dist/memory.js +18 -8
- package/dist/memory.js.map +1 -1
- package/dist/metrics-commands.js +47 -13
- package/dist/metrics-commands.js.map +1 -1
- package/dist/model-commands.d.ts +5 -0
- package/dist/model-commands.js +101 -3
- package/dist/model-commands.js.map +1 -1
- package/dist/model-providers.js +13 -1
- package/dist/model-providers.js.map +1 -1
- package/dist/package-update-check.d.ts +18 -0
- package/dist/package-update-check.js +20 -0
- package/dist/package-update-check.js.map +1 -1
- package/dist/phase-executor.d.ts +1 -0
- package/dist/phase-executor.js +118 -14
- package/dist/phase-executor.js.map +1 -1
- package/dist/phase-playbooks.d.ts +15 -0
- package/dist/phase-playbooks.js +82 -0
- package/dist/phase-playbooks.js.map +1 -1
- package/dist/planning-commands.d.ts +1 -0
- package/dist/planning-commands.js +24 -1
- package/dist/planning-commands.js.map +1 -1
- package/dist/project-detection.js +9 -7
- package/dist/project-detection.js.map +1 -1
- package/dist/prompt-registry-update.d.ts +2 -0
- package/dist/prompt-registry-update.js +25 -1
- package/dist/prompt-registry-update.js.map +1 -1
- package/dist/prompt-registry-validation.js +39 -2
- package/dist/prompt-registry-validation.js.map +1 -1
- package/dist/qa-commands.d.ts +2 -0
- package/dist/qa-commands.js +18 -0
- package/dist/qa-commands.js.map +1 -0
- package/dist/qa-coverage.d.ts +24 -0
- package/dist/qa-coverage.js +198 -0
- package/dist/qa-coverage.js.map +1 -0
- package/dist/qa-readiness.d.ts +5 -0
- package/dist/qa-readiness.js +26 -0
- package/dist/qa-readiness.js.map +1 -0
- package/dist/refresh-generated.d.ts +10 -1
- package/dist/refresh-generated.js +83 -6
- package/dist/refresh-generated.js.map +1 -1
- package/dist/release-candidate.d.ts +9 -1
- package/dist/release-candidate.js +52 -1
- package/dist/release-candidate.js.map +1 -1
- package/dist/release-commands.js +202 -12
- package/dist/release-commands.js.map +1 -1
- package/dist/release-readiness.d.ts +36 -1
- package/dist/release-readiness.js +217 -6
- package/dist/release-readiness.js.map +1 -1
- package/dist/runtime-bootstrap.js +1 -1
- package/dist/runtime-bootstrap.js.map +1 -1
- package/dist/runtime-commands.d.ts +2 -0
- package/dist/runtime-commands.js +77 -0
- package/dist/runtime-commands.js.map +1 -1
- package/dist/runtime-execution-renderer.d.ts +3 -2
- package/dist/runtime-execution-renderer.js +19 -1
- package/dist/runtime-execution-renderer.js.map +1 -1
- package/dist/runtime-execution.d.ts +2 -1
- package/dist/runtime-execution.js +71 -11
- package/dist/runtime-execution.js.map +1 -1
- package/dist/runtime-guardrails.d.ts +26 -0
- package/dist/runtime-guardrails.js +168 -0
- package/dist/runtime-guardrails.js.map +1 -0
- package/dist/setup-agents-import.js +5 -3
- package/dist/setup-agents-import.js.map +1 -1
- package/dist/skills-catalog.js +63 -0
- package/dist/skills-catalog.js.map +1 -1
- package/dist/skills-commands.d.ts +4 -0
- package/dist/skills-commands.js +55 -2
- package/dist/skills-commands.js.map +1 -1
- package/dist/skills-memory.d.ts +36 -2
- package/dist/skills-memory.js +165 -6
- package/dist/skills-memory.js.map +1 -1
- package/dist/skills-planning.js +2 -4
- package/dist/skills-planning.js.map +1 -1
- package/dist/skills-render.js +2 -4
- package/dist/skills-render.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/sprint-commands.js +2 -1
- package/dist/sprint-commands.js.map +1 -1
- package/dist/subagent-protocol.js +3 -5
- package/dist/subagent-protocol.js.map +1 -1
- package/dist/support-commands.d.ts +2 -0
- package/dist/support-commands.js +18 -0
- package/dist/support-commands.js.map +1 -0
- package/dist/support-diagnostics.d.ts +49 -0
- package/dist/support-diagnostics.js +86 -0
- package/dist/support-diagnostics.js.map +1 -0
- package/dist/task-graph-commands.js +5 -3
- package/dist/task-graph-commands.js.map +1 -1
- package/dist/telemetry-redaction.js +8 -1
- package/dist/telemetry-redaction.js.map +1 -1
- package/dist/tool-commands.d.ts +3 -0
- package/dist/tool-commands.js +62 -0
- package/dist/tool-commands.js.map +1 -1
- package/dist/tracker-adapters.d.ts +71 -0
- package/dist/tracker-adapters.js +186 -0
- package/dist/tracker-adapters.js.map +1 -0
- package/dist/tracker-commands.d.ts +2 -0
- package/dist/tracker-commands.js +119 -0
- package/dist/tracker-commands.js.map +1 -0
- package/dist/types/metrics.d.ts +24 -0
- package/dist/types/model-config.d.ts +39 -0
- package/dist/types/runtime.d.ts +56 -0
- package/dist/types/skills.d.ts +2 -0
- package/dist/types/tasks.d.ts +6 -0
- package/dist/types/workflow-run.d.ts +17 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js.map +1 -1
- package/dist/upgrade-commands.js +13 -4
- package/dist/upgrade-commands.js.map +1 -1
- package/dist/validation.js +2 -2
- package/dist/validation.js.map +1 -1
- package/dist/visual-validation.d.ts +81 -0
- package/dist/visual-validation.js +290 -0
- package/dist/visual-validation.js.map +1 -0
- package/dist/web-action-security.d.ts +11 -0
- package/dist/web-action-security.js +45 -0
- package/dist/web-action-security.js.map +1 -0
- package/dist/web-api-read-routes.js +101 -1
- package/dist/web-api-read-routes.js.map +1 -1
- package/dist/web-api.js +507 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-artifacts.d.ts +55 -0
- package/dist/web-artifacts.js +222 -0
- package/dist/web-artifacts.js.map +1 -0
- package/dist/web-console/assets/index-BNESIVvk.js +11 -0
- package/dist/web-console/assets/index-jxCY5eEc.css +1 -0
- package/dist/web-console/index.html +13 -0
- package/dist/web-console.js +9 -3
- package/dist/web-console.js.map +1 -1
- package/dist/web-recovery.d.ts +30 -0
- package/dist/web-recovery.js +163 -0
- package/dist/web-recovery.js.map +1 -0
- package/dist/web-workflow-progress.d.ts +41 -0
- package/dist/web-workflow-progress.js +114 -0
- package/dist/web-workflow-progress.js.map +1 -0
- package/dist/workflow-approval-service.d.ts +2 -1
- package/dist/workflow-approval-service.js +72 -0
- package/dist/workflow-approval-service.js.map +1 -1
- package/dist/workflow-evidence-service.js +8 -1
- package/dist/workflow-evidence-service.js.map +1 -1
- package/dist/workflow-gates.d.ts +2 -0
- package/dist/workflow-gates.js +221 -0
- package/dist/workflow-gates.js.map +1 -1
- package/dist/workflow-run-commands.js +13 -1
- package/dist/workflow-run-commands.js.map +1 -1
- package/dist/workflow-services.d.ts +16 -12
- package/dist/workflow-services.js +313 -253
- package/dist/workflow-services.js.map +1 -1
- package/dist/workflow-task-service.d.ts +11 -0
- package/dist/workflow-task-service.js +242 -0
- package/dist/workflow-task-service.js.map +1 -0
- package/dist/workspace-validator.js +109 -3
- package/dist/workspace-validator.js.map +1 -1
- package/dist/workspace.js +8 -2
- package/dist/workspace.js.map +1 -1
- package/docs/adoption-guide.md +147 -0
- package/docs/autonomous-workflow.md +118 -27
- package/docs/benchmark.md +15 -7
- package/docs/command-contracts.md +18 -1
- package/docs/core-command-surface.md +59 -13
- package/docs/end-to-end-demo.md +1 -0
- package/docs/extension-contracts.md +83 -0
- package/docs/orchestra-mvp.md +83 -3
- package/docs/persona-workflows.md +32 -0
- package/docs/release-test-matrix.md +42 -0
- package/docs/runtime-adapters.md +92 -0
- package/docs/runtime-llm-flow.md +13 -0
- package/docs/setup-agents-applicability-review.md +173 -0
- package/docs/skill-loading-strategy.md +1 -0
- package/docs/source-of-truth-and-agent-learning.md +14 -0
- package/docs/traceability-flow.md +16 -1
- package/docs/tracker-adapter-contract.md +10 -1
- package/docs/web-console-qa.md +35 -0
- package/package.json +12 -6
- package/rules/development-engineering.mdc +68 -0
- package/rules/devops-tooling.mdc +1 -0
- package/rules/dry-clean-code.mdc +1 -0
- package/rules/performance-reliability.mdc +1 -0
- package/rules/testing-discipline.mdc +4 -1
- package/skills/collection-standards/SKILL.md +63 -0
- package/skills/collection-standards/manifest.json +69 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { uniqueStrings } from "./collection-utils.js";
|
|
3
4
|
import { FILES } from "./constants.js";
|
|
4
5
|
import { removeUndefined } from "./command-utils.js";
|
|
5
|
-
import { ensureDir, readJson, resolveWorkflowPath, updateJsonFile, writeJson, } from "./fs-utils.js";
|
|
6
|
-
import { assertProviderAllowed, assertVendorFallbackAllowed, createModelProvider, defaultApiKeyFileEnv, defaultBaseUrlEnv, FakeModelProvider, InMemoryModelProviderRegistry, providerEnvFromCredentialConfig, summarizeConfiguredProviders, } from "./model-providers.js";
|
|
6
|
+
import { ensureDir, exists, readJson, resolveWorkflowPath, updateJsonFile, writeJson, } from "./fs-utils.js";
|
|
7
|
+
import { assertProviderAllowed, assertVendorFallbackAllowed, createModelProvider, defaultApiKeyEnv, defaultApiKeyFileEnv, defaultBaseUrlEnv, FakeModelProvider, InMemoryModelProviderRegistry, providerEnvFromCredentialConfig, summarizeConfiguredProviders, } from "./model-providers.js";
|
|
7
8
|
import { appendEvent, loadWorkspace, readEvents, writeArtifact, } from "./workspace.js";
|
|
8
9
|
import { validateReadiness } from "./validation.js";
|
|
9
10
|
import { getWorkflowGate } from "./workflow-gates.js";
|
|
@@ -13,6 +14,7 @@ import { getTelemetryConsent } from "./telemetry.js";
|
|
|
13
14
|
import { latestDelegationDecision } from "./delegation-decision.js";
|
|
14
15
|
import { listAutonomousRuns } from "./autonomous-workflow.js";
|
|
15
16
|
import { readEstimate } from "./benchmark.js";
|
|
17
|
+
import { qaPlanReadinessBlocker } from "./qa-readiness.js";
|
|
16
18
|
import { handoffFlowRequirements, recommendCollaborationFlow, } from "./collaboration-flows.js";
|
|
17
19
|
import { queryMemory, recordMemoryEvent } from "./memory.js";
|
|
18
20
|
import { applyContextBudget, DEFAULT_CONTEXT_TOKEN_BUDGET, } from "./context-budget.js";
|
|
@@ -20,137 +22,12 @@ import { selectWorkflowTemplates } from "./workflow-templates.js";
|
|
|
20
22
|
import { findStoredApprovalForProposal } from "./workflow-approval-service.js";
|
|
21
23
|
import { listWorkflowEventsByType } from "./workflow-event-query.js";
|
|
22
24
|
import { aggregateUsage, aggregateUsageBy, budgetEstimateWarningsForBudgets, budgetViolations, emptyUsageBreakdown, projectUsageCost, } from "./workflow-budget-utils.js";
|
|
25
|
+
import { listTasks as listWorkflowTasks, updateTask as updateWorkflowTask, } from "./workflow-task-service.js";
|
|
23
26
|
import { SIZING_LABELS } from "./types.js";
|
|
27
|
+
export { addTask, archiveTask, deleteTask, getWorkflowStatus, listTasks, updateTask, } from "./workflow-task-service.js";
|
|
24
28
|
export { addEvidence, addPlaywrightEvidence, listEvidence, listReviews, recordReview, } from "./workflow-evidence-service.js";
|
|
25
29
|
export { generatePlaywrightTestPlan, generatePullRequestSummary, getWorkflowSummary, } from "./workflow-summary-service.js";
|
|
26
|
-
export { approveApproval, approveWorkflowGate, listApprovals, rejectApproval, showApproval, } from "./workflow-approval-service.js";
|
|
27
|
-
export async function getWorkflowStatus(root = process.cwd()) {
|
|
28
|
-
const workspace = await loadWorkspace(root);
|
|
29
|
-
const config = await readJson(resolveWorkflowPath(root, FILES.config), {});
|
|
30
|
-
const counts = Object.create(null);
|
|
31
|
-
const blocked = [];
|
|
32
|
-
for (const task of workspace.tasks) {
|
|
33
|
-
counts[task.status] = (counts[task.status] ?? 0) + 1;
|
|
34
|
-
if (task.status === "blocked") {
|
|
35
|
-
blocked.push({
|
|
36
|
-
id: task.id,
|
|
37
|
-
title: task.title,
|
|
38
|
-
reason: task.blockedReason ?? "not specified",
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
...(config.mode ? { mode: config.mode } : {}),
|
|
44
|
-
tasks: {
|
|
45
|
-
total: workspace.tasks.length,
|
|
46
|
-
byStatus: counts,
|
|
47
|
-
blocked,
|
|
48
|
-
},
|
|
49
|
-
locks: {
|
|
50
|
-
total: workspace.locks.length,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
export async function addTask(input, root = process.cwd()) {
|
|
55
|
-
const workspace = await loadWorkspace(root);
|
|
56
|
-
if (!workspace.roleIds.has(input.ownerRole)) {
|
|
57
|
-
throw new Error(`unknown owner role: ${input.ownerRole}`);
|
|
58
|
-
}
|
|
59
|
-
const now = new Date().toISOString();
|
|
60
|
-
const task = removeUndefined({
|
|
61
|
-
...input,
|
|
62
|
-
status: "pending",
|
|
63
|
-
createdAt: now,
|
|
64
|
-
updatedAt: now,
|
|
65
|
-
});
|
|
66
|
-
await mutateTasks(workspace.base, (tasks) => {
|
|
67
|
-
if (tasks.some((candidate) => candidate.id === input.id)) {
|
|
68
|
-
throw new Error(`task already exists: ${input.id}`);
|
|
69
|
-
}
|
|
70
|
-
return [...tasks, task];
|
|
71
|
-
});
|
|
72
|
-
await appendEvent(root, {
|
|
73
|
-
type: "TASK_ASSIGNED",
|
|
74
|
-
taskId: input.id,
|
|
75
|
-
actor: "parent",
|
|
76
|
-
summary: `Task assigned to ${input.ownerRole}`,
|
|
77
|
-
metadata: { title: input.title, ownerRole: input.ownerRole },
|
|
78
|
-
});
|
|
79
|
-
return task;
|
|
80
|
-
}
|
|
81
|
-
export async function listTasks(root = process.cwd()) {
|
|
82
|
-
const workspace = await loadWorkspace(root);
|
|
83
|
-
return workspace.tasks;
|
|
84
|
-
}
|
|
85
|
-
export async function deleteTask(taskId, options = {}, root = process.cwd()) {
|
|
86
|
-
const workspace = await loadWorkspace(root);
|
|
87
|
-
let deleted;
|
|
88
|
-
await mutateTasks(workspace.base, (tasks) => {
|
|
89
|
-
const taskIndex = tasks.findIndex((task) => task.id === taskId);
|
|
90
|
-
if (taskIndex < 0) {
|
|
91
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
92
|
-
}
|
|
93
|
-
const current = tasks[taskIndex];
|
|
94
|
-
if (!current) {
|
|
95
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
96
|
-
}
|
|
97
|
-
assertTaskCanBeRemoved(current, tasks, options.force ?? false);
|
|
98
|
-
deleted = current;
|
|
99
|
-
return tasks.filter((task) => task.id !== taskId);
|
|
100
|
-
});
|
|
101
|
-
if (!deleted) {
|
|
102
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
103
|
-
}
|
|
104
|
-
await appendEvent(root, {
|
|
105
|
-
type: "TASK_DELETED",
|
|
106
|
-
taskId,
|
|
107
|
-
actor: "parent",
|
|
108
|
-
summary: `Task deleted: ${taskId}`,
|
|
109
|
-
metadata: {
|
|
110
|
-
title: deleted.title,
|
|
111
|
-
status: deleted.status,
|
|
112
|
-
forced: Boolean(options.force),
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
return deleted;
|
|
116
|
-
}
|
|
117
|
-
export async function archiveTask(taskId, options = {}, root = process.cwd()) {
|
|
118
|
-
const workspace = await loadWorkspace(root);
|
|
119
|
-
let archived;
|
|
120
|
-
await mutateTasks(workspace.base, (tasks) => {
|
|
121
|
-
const taskIndex = tasks.findIndex((task) => task.id === taskId);
|
|
122
|
-
if (taskIndex < 0) {
|
|
123
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
124
|
-
}
|
|
125
|
-
const current = tasks[taskIndex];
|
|
126
|
-
if (!current) {
|
|
127
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
128
|
-
}
|
|
129
|
-
assertTaskCanBeRemoved(current, tasks, options.force ?? false);
|
|
130
|
-
archived = {
|
|
131
|
-
...current,
|
|
132
|
-
status: "archived",
|
|
133
|
-
updatedAt: new Date().toISOString(),
|
|
134
|
-
};
|
|
135
|
-
const nextTasks = [...tasks];
|
|
136
|
-
nextTasks[taskIndex] = archived;
|
|
137
|
-
return nextTasks;
|
|
138
|
-
});
|
|
139
|
-
if (!archived) {
|
|
140
|
-
throw new Error(`unknown task: ${taskId}`);
|
|
141
|
-
}
|
|
142
|
-
await appendEvent(root, {
|
|
143
|
-
type: "TASK_ARCHIVED",
|
|
144
|
-
taskId,
|
|
145
|
-
actor: "parent",
|
|
146
|
-
summary: `Task archived: ${taskId}`,
|
|
147
|
-
metadata: {
|
|
148
|
-
title: archived.title,
|
|
149
|
-
forced: Boolean(options.force),
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
return archived;
|
|
153
|
-
}
|
|
30
|
+
export { approveApproval, approveWorkflowGate, listApprovals, recordWorkflowGateDecision, rejectApproval, showApproval, } from "./workflow-approval-service.js";
|
|
154
31
|
export async function listRoles(root = process.cwd()) {
|
|
155
32
|
const workspace = await loadWorkspace(root);
|
|
156
33
|
return workspace.roles;
|
|
@@ -198,74 +75,6 @@ export async function validatePreRun(taskId, options = {}, root = process.cwd())
|
|
|
198
75
|
bypassDecisionArtifact,
|
|
199
76
|
});
|
|
200
77
|
}
|
|
201
|
-
export async function updateTask(input, root = process.cwd()) {
|
|
202
|
-
const workspace = await loadWorkspace(root);
|
|
203
|
-
if (input.ownerRole && !workspace.roleIds.has(input.ownerRole)) {
|
|
204
|
-
throw new Error(`unknown owner role: ${input.ownerRole}`);
|
|
205
|
-
}
|
|
206
|
-
let updated;
|
|
207
|
-
let changedFields = [];
|
|
208
|
-
await mutateTasks(workspace.base, (tasks) => {
|
|
209
|
-
const taskIndex = tasks.findIndex((task) => task.id === input.id);
|
|
210
|
-
if (taskIndex < 0) {
|
|
211
|
-
throw new Error(`unknown task: ${input.id}`);
|
|
212
|
-
}
|
|
213
|
-
const current = tasks[taskIndex];
|
|
214
|
-
if (!current) {
|
|
215
|
-
throw new Error(`unknown task: ${input.id}`);
|
|
216
|
-
}
|
|
217
|
-
const next = { ...current };
|
|
218
|
-
const changes = new Set();
|
|
219
|
-
applyTaskUpdate(next, current, input, changes);
|
|
220
|
-
changedFields = [...changes].sort();
|
|
221
|
-
updated = { ...next, updatedAt: new Date().toISOString() };
|
|
222
|
-
const nextTasks = [...tasks];
|
|
223
|
-
nextTasks[taskIndex] = updated;
|
|
224
|
-
return nextTasks;
|
|
225
|
-
});
|
|
226
|
-
if (!updated) {
|
|
227
|
-
throw new Error(`unknown task: ${input.id}`);
|
|
228
|
-
}
|
|
229
|
-
await appendEvent(root, {
|
|
230
|
-
type: "TASK_UPDATED",
|
|
231
|
-
taskId: input.id,
|
|
232
|
-
actor: "parent",
|
|
233
|
-
summary: `Task updated: ${input.id}`,
|
|
234
|
-
metadata: { status: updated.status, changedFields },
|
|
235
|
-
});
|
|
236
|
-
return updated;
|
|
237
|
-
}
|
|
238
|
-
function applyTaskUpdate(next, current, input, changedFields) {
|
|
239
|
-
setTaskField(next, current, "title", input.title, changedFields);
|
|
240
|
-
setTaskField(next, current, "ownerRole", input.ownerRole, changedFields);
|
|
241
|
-
setTaskField(next, current, "goal", input.goal, changedFields);
|
|
242
|
-
setTaskField(next, current, "scope", input.scope, changedFields);
|
|
243
|
-
setTaskField(next, current, "paths", input.paths, changedFields);
|
|
244
|
-
setTaskField(next, current, "testStrategy", input.testStrategy, changedFields);
|
|
245
|
-
setTaskField(next, current, "status", input.status, changedFields);
|
|
246
|
-
setTaskField(next, current, "blockedReason", input.blockedReason, changedFields);
|
|
247
|
-
setTaskField(next, current, "claimedAt", input.claimedAt, changedFields);
|
|
248
|
-
setTaskField(next, current, "doneAt", input.doneAt, changedFields);
|
|
249
|
-
appendTaskField(next, "acceptanceCriteria", input.acceptanceCriteria, changedFields);
|
|
250
|
-
appendTaskField(next, "assumptions", input.assumptions, changedFields);
|
|
251
|
-
appendTaskField(next, "risks", input.risks, changedFields);
|
|
252
|
-
}
|
|
253
|
-
function setTaskField(next, current, key, value, changedFields) {
|
|
254
|
-
if (value === undefined) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (JSON.stringify(current[key]) !== JSON.stringify(value)) {
|
|
258
|
-
changedFields.add(String(key));
|
|
259
|
-
}
|
|
260
|
-
next[key] = value;
|
|
261
|
-
}
|
|
262
|
-
function appendTaskField(next, key, values, changedFields) {
|
|
263
|
-
if (!values || values.length === 0) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
next[key] = [...(next[key] ?? []), ...values];
|
|
267
|
-
changedFields.add(key);
|
|
268
|
-
}
|
|
269
78
|
export async function checkTaskDependencies(taskId, root = process.cwd()) {
|
|
270
79
|
const workspace = await loadWorkspace(root);
|
|
271
80
|
const task = workspace.tasks.find((candidate) => candidate.id === taskId);
|
|
@@ -292,7 +101,7 @@ export async function checkTaskDependencies(taskId, root = process.cwd()) {
|
|
|
292
101
|
};
|
|
293
102
|
}
|
|
294
103
|
export async function generateTaskGraphPlan(root = process.cwd()) {
|
|
295
|
-
const tasks = await
|
|
104
|
+
const tasks = await listWorkflowTasks(root);
|
|
296
105
|
const locks = await listLocks(root);
|
|
297
106
|
const ready = [];
|
|
298
107
|
const blocked = [];
|
|
@@ -314,6 +123,14 @@ export async function generateTaskGraphPlan(root = process.cwd()) {
|
|
|
314
123
|
}
|
|
315
124
|
const dependencies = await checkTaskDependencies(task.id, root);
|
|
316
125
|
if (dependencies.isSatisfied) {
|
|
126
|
+
const qaBlocker = qaPlanReadinessBlocker(task);
|
|
127
|
+
if (qaBlocker) {
|
|
128
|
+
blocked.push({
|
|
129
|
+
...item,
|
|
130
|
+
incomplete: [qaBlocker],
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
317
134
|
const taskLocks = locksForTask(task, locks);
|
|
318
135
|
if (taskLocks.length > 0) {
|
|
319
136
|
locked.push({
|
|
@@ -560,7 +377,7 @@ export async function createHandoff(input, root = process.cwd()) {
|
|
|
560
377
|
metadata: { to: input.to, updateOwner: Boolean(input.updateOwner) },
|
|
561
378
|
});
|
|
562
379
|
if (input.updateOwner) {
|
|
563
|
-
await
|
|
380
|
+
await updateWorkflowTask({ id: input.task, ownerRole: input.to }, root);
|
|
564
381
|
}
|
|
565
382
|
return { artifact, content };
|
|
566
383
|
}
|
|
@@ -748,6 +565,8 @@ export async function executePlanWithFakeProvider(taskId, root = process.cwd(),
|
|
|
748
565
|
inputTokens: response.usage.inputTokens,
|
|
749
566
|
outputTokens: response.usage.outputTokens,
|
|
750
567
|
estimatedCostUsd: 0,
|
|
568
|
+
usageSource: "provider",
|
|
569
|
+
costSource: "free",
|
|
751
570
|
finishReason: response.finishReason,
|
|
752
571
|
}, root);
|
|
753
572
|
const artifact = await writeRunArtifact(root, taskId, step.id, [
|
|
@@ -1039,6 +858,146 @@ export async function getWorkflowConfig(root = process.cwd()) {
|
|
|
1039
858
|
export async function listConfiguredModelProviders(root = process.cwd()) {
|
|
1040
859
|
return summarizeConfiguredProviders(await getWorkflowConfig(root));
|
|
1041
860
|
}
|
|
861
|
+
export async function listProviderRuntimeProfiles(root = process.cwd()) {
|
|
862
|
+
const config = await getWorkflowConfig(root);
|
|
863
|
+
const activeProfile = config.providers?.activeProfile;
|
|
864
|
+
return Object.entries(config.providers?.profiles ?? {})
|
|
865
|
+
.map(([name, profile]) => removeUndefined({
|
|
866
|
+
name,
|
|
867
|
+
active: name === activeProfile,
|
|
868
|
+
description: profile.description,
|
|
869
|
+
roles: Object.keys(profile.byRole ?? {}).sort(),
|
|
870
|
+
defaultProvider: profile.defaults?.provider,
|
|
871
|
+
defaultModel: profile.defaults?.model,
|
|
872
|
+
requiredEnv: profile.requiredEnv ?? [],
|
|
873
|
+
}))
|
|
874
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
875
|
+
}
|
|
876
|
+
export async function saveProviderRuntimeProfile(input, root = process.cwd()) {
|
|
877
|
+
const workspace = await loadWorkspace(root);
|
|
878
|
+
validateProfileName(input.name);
|
|
879
|
+
for (const role of input.roles) {
|
|
880
|
+
if (!workspace.roleIds.has(role)) {
|
|
881
|
+
throw new Error(`unknown role: ${role}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (input.roles.length === 0) {
|
|
885
|
+
throw new Error("at least one role is required");
|
|
886
|
+
}
|
|
887
|
+
validateProfileRouting(input.routing);
|
|
888
|
+
const configPath = resolveWorkflowPath(root, FILES.config);
|
|
889
|
+
const config = await readJson(configPath, {});
|
|
890
|
+
const profile = removeUndefined({
|
|
891
|
+
description: input.description,
|
|
892
|
+
byRole: Object.fromEntries(input.roles.map((role) => [role, input.routing])),
|
|
893
|
+
requiredEnv: input.requiredEnv,
|
|
894
|
+
});
|
|
895
|
+
config.providers = {
|
|
896
|
+
defaults: config.providers?.defaults,
|
|
897
|
+
byRole: config.providers?.byRole ?? {},
|
|
898
|
+
profiles: {
|
|
899
|
+
...(config.providers?.profiles ?? {}),
|
|
900
|
+
[input.name]: profile,
|
|
901
|
+
},
|
|
902
|
+
...(input.activate ? { activeProfile: input.name } : {}),
|
|
903
|
+
};
|
|
904
|
+
await writeJson(configPath, config);
|
|
905
|
+
if (input.apply || input.activate) {
|
|
906
|
+
await applyProviderRuntimeProfile(input.name, root);
|
|
907
|
+
}
|
|
908
|
+
await appendEvent(root, {
|
|
909
|
+
type: "MODEL_PROFILE_SAVED",
|
|
910
|
+
actor: "parent",
|
|
911
|
+
summary: `Provider runtime profile saved: ${input.name}`,
|
|
912
|
+
metadata: {
|
|
913
|
+
profile: input.name,
|
|
914
|
+
roles: input.roles,
|
|
915
|
+
provider: input.routing.provider,
|
|
916
|
+
model: input.routing.model,
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
const summaries = await listProviderRuntimeProfiles(root);
|
|
920
|
+
return summaries.find((summary) => summary.name === input.name);
|
|
921
|
+
}
|
|
922
|
+
export async function applyProviderRuntimeProfile(name, root = process.cwd()) {
|
|
923
|
+
validateProfileName(name);
|
|
924
|
+
const workspace = await loadWorkspace(root);
|
|
925
|
+
const configPath = resolveWorkflowPath(root, FILES.config);
|
|
926
|
+
const config = await readJson(configPath, {});
|
|
927
|
+
const profile = config.providers?.profiles?.[name];
|
|
928
|
+
if (!profile) {
|
|
929
|
+
throw new Error(`unknown provider runtime profile: ${name}`);
|
|
930
|
+
}
|
|
931
|
+
for (const role of Object.keys(profile.byRole ?? {})) {
|
|
932
|
+
if (!workspace.roleIds.has(role)) {
|
|
933
|
+
throw new Error(`profile ${name} references unknown role: ${role}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
for (const routing of Object.values(profile.byRole ?? {})) {
|
|
937
|
+
validateProfileRouting(routing);
|
|
938
|
+
}
|
|
939
|
+
if (profile.defaults) {
|
|
940
|
+
validateProfileRouting(profile.defaults);
|
|
941
|
+
}
|
|
942
|
+
config.providers = {
|
|
943
|
+
defaults: profile.defaults ?? config.providers.defaults,
|
|
944
|
+
byRole: {
|
|
945
|
+
...(config.providers.byRole ?? {}),
|
|
946
|
+
...(profile.byRole ?? {}),
|
|
947
|
+
},
|
|
948
|
+
profiles: config.providers.profiles ?? {},
|
|
949
|
+
activeProfile: name,
|
|
950
|
+
};
|
|
951
|
+
if (profile.budgets) {
|
|
952
|
+
config.budgets = profile.budgets;
|
|
953
|
+
}
|
|
954
|
+
if (profile.providerPolicy) {
|
|
955
|
+
config.providerPolicy = {
|
|
956
|
+
...(config.providerPolicy ?? {}),
|
|
957
|
+
...profile.providerPolicy,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
await writeJson(configPath, config);
|
|
961
|
+
await appendEvent(root, {
|
|
962
|
+
type: "MODEL_PROFILE_APPLIED",
|
|
963
|
+
actor: "parent",
|
|
964
|
+
summary: `Provider runtime profile applied: ${name}`,
|
|
965
|
+
metadata: { profile: name, roles: Object.keys(profile.byRole ?? {}) },
|
|
966
|
+
});
|
|
967
|
+
const summaries = await listProviderRuntimeProfiles(root);
|
|
968
|
+
return summaries.find((summary) => summary.name === name);
|
|
969
|
+
}
|
|
970
|
+
export async function smokeProviderRuntimeProfile(name, root = process.cwd(), env = process.env) {
|
|
971
|
+
validateProfileName(name);
|
|
972
|
+
const config = await getWorkflowConfig(root);
|
|
973
|
+
const profile = config.providers?.profiles?.[name];
|
|
974
|
+
if (!profile) {
|
|
975
|
+
throw new Error(`unknown provider runtime profile: ${name}`);
|
|
976
|
+
}
|
|
977
|
+
const checks = [];
|
|
978
|
+
for (const envName of profile.requiredEnv ?? []) {
|
|
979
|
+
checks.push({
|
|
980
|
+
scope: `env:${envName}`,
|
|
981
|
+
provider: "environment",
|
|
982
|
+
model: "not-applicable",
|
|
983
|
+
status: env[envName]?.trim() ? "pass" : "fail",
|
|
984
|
+
detail: env[envName]?.trim()
|
|
985
|
+
? "environment variable is configured"
|
|
986
|
+
: `missing required environment variable ${envName}`,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
const routes = Object.entries(profile.byRole ?? {}).map(([role, routing]) => [`role:${role}`, routing]);
|
|
990
|
+
if (profile.defaults)
|
|
991
|
+
routes.unshift(["defaults", profile.defaults]);
|
|
992
|
+
for (const [scope, routing] of routes) {
|
|
993
|
+
checks.push(...(await smokeRouting(scope, routing, config, root, env)));
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
profile: name,
|
|
997
|
+
passed: checks.every((check) => check.status === "pass"),
|
|
998
|
+
checks,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1042
1001
|
export async function completeWithProviderFallback(routing, prompt, { failingProviders = [], root = process.cwd(), taskId, role = "parent", jsonMode = false, providerMode = "fake", } = {}) {
|
|
1043
1002
|
const registry = new InMemoryModelProviderRegistry();
|
|
1044
1003
|
const config = await getWorkflowConfig(root);
|
|
@@ -1061,39 +1020,46 @@ export async function completeWithProviderFallback(routing, prompt, { failingPro
|
|
|
1061
1020
|
if (providerMode === "real" && index > 0) {
|
|
1062
1021
|
assertVendorFallbackAllowed(providerId, config.providerPolicy);
|
|
1063
1022
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1023
|
+
let attempts = 0;
|
|
1024
|
+
const maxAttempts = Math.max(1, 1 + routing.retries);
|
|
1025
|
+
while (attempts < maxAttempts) {
|
|
1026
|
+
attempts += 1;
|
|
1027
|
+
try {
|
|
1028
|
+
const provider = registry.get(providerId);
|
|
1029
|
+
const response = await provider.complete({
|
|
1030
|
+
model: routing.model,
|
|
1031
|
+
jsonMode,
|
|
1032
|
+
timeoutMs: routing.timeoutMs,
|
|
1033
|
+
messages: [{ role: "user", content: prompt }],
|
|
1034
|
+
});
|
|
1035
|
+
if (index > 0) {
|
|
1036
|
+
await appendEvent(root, removeUndefined({
|
|
1037
|
+
type: "MODEL_FALLBACK_USED",
|
|
1038
|
+
actor: role,
|
|
1039
|
+
taskId,
|
|
1040
|
+
summary: `Fallback provider used: ${providerId}`,
|
|
1041
|
+
metadata: { provider: providerId, failedProviders },
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
provider: providerId,
|
|
1046
|
+
model: routing.model,
|
|
1047
|
+
response,
|
|
1048
|
+
fallbackUsed: index > 0,
|
|
1049
|
+
failedProviders,
|
|
1050
|
+
};
|
|
1080
1051
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
throw error;
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
const failure = providerFailureFromError(providerId, error, attempts);
|
|
1054
|
+
if (failure.code === "timeout") {
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
if (failure.retryable && attempts < maxAttempts) {
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
failedProviders.push(failure);
|
|
1061
|
+
break;
|
|
1092
1062
|
}
|
|
1093
|
-
failedProviders.push({
|
|
1094
|
-
provider: providerId,
|
|
1095
|
-
reason: sanitizeProviderError(error),
|
|
1096
|
-
});
|
|
1097
1063
|
}
|
|
1098
1064
|
}
|
|
1099
1065
|
throw new ProviderFallbackError(failedProviders);
|
|
@@ -1112,10 +1078,31 @@ export function providerFailuresFromError(error) {
|
|
|
1112
1078
|
function providerFailureSummary(failures) {
|
|
1113
1079
|
const providers = failures.map((failure) => failure.provider).join(", ");
|
|
1114
1080
|
const details = failures
|
|
1115
|
-
.map((failure) => `${failure.provider}: ${failure.reason}`)
|
|
1081
|
+
.map((failure) => `${failure.provider}: ${failure.reason} (${failure.code}, attempts=${failure.attempts})`)
|
|
1116
1082
|
.join("; ");
|
|
1117
1083
|
return `all providers failed: ${providers}${details ? ` (${details})` : ""}`;
|
|
1118
1084
|
}
|
|
1085
|
+
function providerFailureFromError(providerId, error, attempts) {
|
|
1086
|
+
const code = providerFailureCode(error);
|
|
1087
|
+
return {
|
|
1088
|
+
provider: providerId,
|
|
1089
|
+
code,
|
|
1090
|
+
reason: sanitizeProviderError(error),
|
|
1091
|
+
retryable: code === "provider_error",
|
|
1092
|
+
attempts,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function providerFailureCode(error) {
|
|
1096
|
+
const message = errorMessage(error).toLowerCase();
|
|
1097
|
+
if (isProviderTimeoutError(error))
|
|
1098
|
+
return "timeout";
|
|
1099
|
+
if (/permission|denied|forbidden|unauthorized/.test(message)) {
|
|
1100
|
+
return "permission_denied";
|
|
1101
|
+
}
|
|
1102
|
+
if (/policy|not allowed|blocked/.test(message))
|
|
1103
|
+
return "policy_blocked";
|
|
1104
|
+
return "provider_error";
|
|
1105
|
+
}
|
|
1119
1106
|
function sanitizeProviderError(error) {
|
|
1120
1107
|
const messages = [];
|
|
1121
1108
|
if (error instanceof Error) {
|
|
@@ -1141,6 +1128,9 @@ function sanitizeProviderError(error) {
|
|
|
1141
1128
|
}
|
|
1142
1129
|
return redactProviderError(messages.filter(Boolean).join(": "));
|
|
1143
1130
|
}
|
|
1131
|
+
function errorMessage(error) {
|
|
1132
|
+
return error instanceof Error ? error.message : String(error);
|
|
1133
|
+
}
|
|
1144
1134
|
function isErrorCauseRecord(value) {
|
|
1145
1135
|
return typeof value === "object" && value !== null;
|
|
1146
1136
|
}
|
|
@@ -1171,6 +1161,10 @@ export async function setRoleModelProvider(role, routing, root = process.cwd())
|
|
|
1171
1161
|
...(config.providers.byRole ?? {}),
|
|
1172
1162
|
[role]: routing,
|
|
1173
1163
|
},
|
|
1164
|
+
profiles: config.providers.profiles ?? {},
|
|
1165
|
+
...(config.providers.activeProfile
|
|
1166
|
+
? { activeProfile: config.providers.activeProfile }
|
|
1167
|
+
: {}),
|
|
1174
1168
|
};
|
|
1175
1169
|
await writeJson(configPath, config);
|
|
1176
1170
|
await appendEvent(root, {
|
|
@@ -1207,6 +1201,10 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1207
1201
|
config.providers = {
|
|
1208
1202
|
defaults: config.providers?.defaults ?? routing,
|
|
1209
1203
|
byRole,
|
|
1204
|
+
profiles: config.providers?.profiles ?? {},
|
|
1205
|
+
...(config.providers?.activeProfile
|
|
1206
|
+
? { activeProfile: config.providers.activeProfile }
|
|
1207
|
+
: {}),
|
|
1210
1208
|
};
|
|
1211
1209
|
const credential = removeUndefined({
|
|
1212
1210
|
apiKeyFile: input.apiKeyFile,
|
|
@@ -1227,7 +1225,7 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1227
1225
|
if (input.allowDirectProviderApi) {
|
|
1228
1226
|
config.providerPolicy = {
|
|
1229
1227
|
...(config.providerPolicy ?? {}),
|
|
1230
|
-
allowedProviders:
|
|
1228
|
+
allowedProviders: uniqueStrings([
|
|
1231
1229
|
...(config.providerPolicy?.allowedProviders ?? []),
|
|
1232
1230
|
input.provider,
|
|
1233
1231
|
]),
|
|
@@ -1239,6 +1237,9 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1239
1237
|
delegation: {
|
|
1240
1238
|
mode: config.runtimePolicy?.delegation?.mode ?? "runtime-native",
|
|
1241
1239
|
allowDirectProviderApi: true,
|
|
1240
|
+
...(config.runtimePolicy?.delegation?.guardrails
|
|
1241
|
+
? { guardrails: config.runtimePolicy.delegation.guardrails }
|
|
1242
|
+
: {}),
|
|
1242
1243
|
},
|
|
1243
1244
|
};
|
|
1244
1245
|
}
|
|
@@ -1263,8 +1264,85 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1263
1264
|
allowDirectProviderApi: input.allowDirectProviderApi,
|
|
1264
1265
|
});
|
|
1265
1266
|
}
|
|
1266
|
-
function
|
|
1267
|
-
|
|
1267
|
+
function validateProfileName(name) {
|
|
1268
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,62}$/i.test(name)) {
|
|
1269
|
+
throw new Error("profile name must be 2-63 characters and contain only letters, numbers, dots, underscores, or dashes");
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function validateProfileRouting(routing) {
|
|
1273
|
+
if (!routing.provider?.trim()) {
|
|
1274
|
+
throw new Error("profile routing requires provider");
|
|
1275
|
+
}
|
|
1276
|
+
if (!routing.model?.trim()) {
|
|
1277
|
+
throw new Error("profile routing requires model");
|
|
1278
|
+
}
|
|
1279
|
+
if (new Set([routing.provider, ...(routing.fallbacks ?? [])]).size !==
|
|
1280
|
+
[routing.provider, ...(routing.fallbacks ?? [])].length) {
|
|
1281
|
+
throw new Error("profile routing fallback chain contains duplicates");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
async function smokeRouting(scope, routing, config, root, env) {
|
|
1285
|
+
const checks = [];
|
|
1286
|
+
const providerIds = [routing.provider, ...(routing.fallbacks ?? [])];
|
|
1287
|
+
for (const [index, providerId] of providerIds.entries()) {
|
|
1288
|
+
const providerScope = index === 0 ? scope : `${scope}:fallback:${index}`;
|
|
1289
|
+
const policyError = providerPolicySmokeError(providerId, config, index);
|
|
1290
|
+
if (policyError) {
|
|
1291
|
+
checks.push({
|
|
1292
|
+
scope: providerScope,
|
|
1293
|
+
provider: providerId,
|
|
1294
|
+
model: routing.model,
|
|
1295
|
+
status: "fail",
|
|
1296
|
+
detail: policyError,
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const credentialError = await providerCredentialSmokeError(providerId, config.providerCredentials?.byProvider?.[providerId], root, env);
|
|
1301
|
+
checks.push({
|
|
1302
|
+
scope: providerScope,
|
|
1303
|
+
provider: providerId,
|
|
1304
|
+
model: routing.model,
|
|
1305
|
+
status: credentialError ? "fail" : "pass",
|
|
1306
|
+
detail: credentialError ?? "provider configuration is present",
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
return checks;
|
|
1310
|
+
}
|
|
1311
|
+
function providerPolicySmokeError(providerId, config, fallbackIndex) {
|
|
1312
|
+
try {
|
|
1313
|
+
assertProviderAllowed(providerId, config.providerPolicy);
|
|
1314
|
+
if (fallbackIndex > 0) {
|
|
1315
|
+
assertVendorFallbackAllowed(providerId, config.providerPolicy);
|
|
1316
|
+
}
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
catch (error) {
|
|
1320
|
+
return error instanceof Error ? error.message : String(error);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async function providerCredentialSmokeError(providerId, credential, root, env) {
|
|
1324
|
+
if (providerId === "none" ||
|
|
1325
|
+
providerId === "fake" ||
|
|
1326
|
+
providerId === "ollama") {
|
|
1327
|
+
return undefined;
|
|
1328
|
+
}
|
|
1329
|
+
const apiKeyEnv = credential?.apiKeyEnv ?? defaultApiKeyEnv(providerId);
|
|
1330
|
+
const apiKeyFileEnv = credential?.apiKeyFileEnv ?? defaultApiKeyFileEnv(providerId);
|
|
1331
|
+
const effectiveEnv = providerEnvFromCredentialConfig(providerId, credential, env);
|
|
1332
|
+
if (apiKeyEnv && effectiveEnv[apiKeyEnv]?.trim()) {
|
|
1333
|
+
return undefined;
|
|
1334
|
+
}
|
|
1335
|
+
if (apiKeyFileEnv && effectiveEnv[apiKeyFileEnv]?.trim()) {
|
|
1336
|
+
const keyFile = effectiveEnv[apiKeyFileEnv].trim();
|
|
1337
|
+
if (!path.isAbsolute(keyFile)) {
|
|
1338
|
+
return `${apiKeyFileEnv} must reference an absolute path`;
|
|
1339
|
+
}
|
|
1340
|
+
if (!(await exists(keyFile))) {
|
|
1341
|
+
return `${apiKeyFileEnv} references an unreadable secret file`;
|
|
1342
|
+
}
|
|
1343
|
+
return undefined;
|
|
1344
|
+
}
|
|
1345
|
+
return `${apiKeyEnv ?? "provider API key"} or ${apiKeyFileEnv ?? "provider API key file"} is required`;
|
|
1268
1346
|
}
|
|
1269
1347
|
export async function recordModelProvenance(input, root = process.cwd()) {
|
|
1270
1348
|
const workspace = await loadWorkspace(root);
|
|
@@ -1440,27 +1518,9 @@ async function writeBudgetEscalationProposal(root, taskId, proposal) {
|
|
|
1440
1518
|
].join("\n");
|
|
1441
1519
|
return writeArtifact(root, "approvals", `${taskId}-budget-fallback.md`, content);
|
|
1442
1520
|
}
|
|
1443
|
-
async function mutateTasks(base, update) {
|
|
1444
|
-
return updateJsonFile(path.join(base, FILES.tasks), [], update);
|
|
1445
|
-
}
|
|
1446
1521
|
async function mutateLocks(base, update) {
|
|
1447
1522
|
return updateJsonFile(path.join(base, FILES.locks), [], update);
|
|
1448
1523
|
}
|
|
1449
|
-
function assertTaskCanBeRemoved(task, tasks, isForced) {
|
|
1450
|
-
if (!isForced &&
|
|
1451
|
-
(task.status === "in_progress" || task.status === "blocked")) {
|
|
1452
|
-
throw new Error(`task ${task.id} is ${task.status}; use --force to remove it`);
|
|
1453
|
-
}
|
|
1454
|
-
const dependent = tasks.find((candidate) => candidate.id !== task.id &&
|
|
1455
|
-
!isTerminalTaskStatus(candidate.status) &&
|
|
1456
|
-
candidate.dependencies.includes(task.id));
|
|
1457
|
-
if (dependent) {
|
|
1458
|
-
throw new Error(`task ${task.id} is a dependency of active task ${dependent.id}`);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
function isTerminalTaskStatus(status) {
|
|
1462
|
-
return ["done", "canceled", "rejected", "archived"].includes(status);
|
|
1463
|
-
}
|
|
1464
1524
|
function flowRequirementLines(requirements) {
|
|
1465
1525
|
return requirements.length > 0
|
|
1466
1526
|
? requirements.map((requirement) => `- ${requirement}`)
|