@jterrats/open-orchestra 0.5.7 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +22 -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/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 +95 -1
- 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 +115 -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 +5 -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 +189 -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 +161 -8
- package/dist/release-commands.js.map +1 -1
- package/dist/release-readiness.d.ts +33 -0
- package/dist/release-readiness.js +187 -3
- 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-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 +35 -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-C9lx-V42.css +1 -0
- package/dist/web-console/assets/index-M3S0g1GK.js +11 -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 +1 -0
- package/dist/workflow-gates.js +57 -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 +311 -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/source-of-truth-and-agent-learning.md +14 -0
- package/docs/traceability-flow.md +5 -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 +66 -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
|
}
|
|
@@ -1039,6 +856,146 @@ export async function getWorkflowConfig(root = process.cwd()) {
|
|
|
1039
856
|
export async function listConfiguredModelProviders(root = process.cwd()) {
|
|
1040
857
|
return summarizeConfiguredProviders(await getWorkflowConfig(root));
|
|
1041
858
|
}
|
|
859
|
+
export async function listProviderRuntimeProfiles(root = process.cwd()) {
|
|
860
|
+
const config = await getWorkflowConfig(root);
|
|
861
|
+
const activeProfile = config.providers?.activeProfile;
|
|
862
|
+
return Object.entries(config.providers?.profiles ?? {})
|
|
863
|
+
.map(([name, profile]) => removeUndefined({
|
|
864
|
+
name,
|
|
865
|
+
active: name === activeProfile,
|
|
866
|
+
description: profile.description,
|
|
867
|
+
roles: Object.keys(profile.byRole ?? {}).sort(),
|
|
868
|
+
defaultProvider: profile.defaults?.provider,
|
|
869
|
+
defaultModel: profile.defaults?.model,
|
|
870
|
+
requiredEnv: profile.requiredEnv ?? [],
|
|
871
|
+
}))
|
|
872
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
873
|
+
}
|
|
874
|
+
export async function saveProviderRuntimeProfile(input, root = process.cwd()) {
|
|
875
|
+
const workspace = await loadWorkspace(root);
|
|
876
|
+
validateProfileName(input.name);
|
|
877
|
+
for (const role of input.roles) {
|
|
878
|
+
if (!workspace.roleIds.has(role)) {
|
|
879
|
+
throw new Error(`unknown role: ${role}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (input.roles.length === 0) {
|
|
883
|
+
throw new Error("at least one role is required");
|
|
884
|
+
}
|
|
885
|
+
validateProfileRouting(input.routing);
|
|
886
|
+
const configPath = resolveWorkflowPath(root, FILES.config);
|
|
887
|
+
const config = await readJson(configPath, {});
|
|
888
|
+
const profile = removeUndefined({
|
|
889
|
+
description: input.description,
|
|
890
|
+
byRole: Object.fromEntries(input.roles.map((role) => [role, input.routing])),
|
|
891
|
+
requiredEnv: input.requiredEnv,
|
|
892
|
+
});
|
|
893
|
+
config.providers = {
|
|
894
|
+
defaults: config.providers?.defaults,
|
|
895
|
+
byRole: config.providers?.byRole ?? {},
|
|
896
|
+
profiles: {
|
|
897
|
+
...(config.providers?.profiles ?? {}),
|
|
898
|
+
[input.name]: profile,
|
|
899
|
+
},
|
|
900
|
+
...(input.activate ? { activeProfile: input.name } : {}),
|
|
901
|
+
};
|
|
902
|
+
await writeJson(configPath, config);
|
|
903
|
+
if (input.apply || input.activate) {
|
|
904
|
+
await applyProviderRuntimeProfile(input.name, root);
|
|
905
|
+
}
|
|
906
|
+
await appendEvent(root, {
|
|
907
|
+
type: "MODEL_PROFILE_SAVED",
|
|
908
|
+
actor: "parent",
|
|
909
|
+
summary: `Provider runtime profile saved: ${input.name}`,
|
|
910
|
+
metadata: {
|
|
911
|
+
profile: input.name,
|
|
912
|
+
roles: input.roles,
|
|
913
|
+
provider: input.routing.provider,
|
|
914
|
+
model: input.routing.model,
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
const summaries = await listProviderRuntimeProfiles(root);
|
|
918
|
+
return summaries.find((summary) => summary.name === input.name);
|
|
919
|
+
}
|
|
920
|
+
export async function applyProviderRuntimeProfile(name, root = process.cwd()) {
|
|
921
|
+
validateProfileName(name);
|
|
922
|
+
const workspace = await loadWorkspace(root);
|
|
923
|
+
const configPath = resolveWorkflowPath(root, FILES.config);
|
|
924
|
+
const config = await readJson(configPath, {});
|
|
925
|
+
const profile = config.providers?.profiles?.[name];
|
|
926
|
+
if (!profile) {
|
|
927
|
+
throw new Error(`unknown provider runtime profile: ${name}`);
|
|
928
|
+
}
|
|
929
|
+
for (const role of Object.keys(profile.byRole ?? {})) {
|
|
930
|
+
if (!workspace.roleIds.has(role)) {
|
|
931
|
+
throw new Error(`profile ${name} references unknown role: ${role}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
for (const routing of Object.values(profile.byRole ?? {})) {
|
|
935
|
+
validateProfileRouting(routing);
|
|
936
|
+
}
|
|
937
|
+
if (profile.defaults) {
|
|
938
|
+
validateProfileRouting(profile.defaults);
|
|
939
|
+
}
|
|
940
|
+
config.providers = {
|
|
941
|
+
defaults: profile.defaults ?? config.providers.defaults,
|
|
942
|
+
byRole: {
|
|
943
|
+
...(config.providers.byRole ?? {}),
|
|
944
|
+
...(profile.byRole ?? {}),
|
|
945
|
+
},
|
|
946
|
+
profiles: config.providers.profiles ?? {},
|
|
947
|
+
activeProfile: name,
|
|
948
|
+
};
|
|
949
|
+
if (profile.budgets) {
|
|
950
|
+
config.budgets = profile.budgets;
|
|
951
|
+
}
|
|
952
|
+
if (profile.providerPolicy) {
|
|
953
|
+
config.providerPolicy = {
|
|
954
|
+
...(config.providerPolicy ?? {}),
|
|
955
|
+
...profile.providerPolicy,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
await writeJson(configPath, config);
|
|
959
|
+
await appendEvent(root, {
|
|
960
|
+
type: "MODEL_PROFILE_APPLIED",
|
|
961
|
+
actor: "parent",
|
|
962
|
+
summary: `Provider runtime profile applied: ${name}`,
|
|
963
|
+
metadata: { profile: name, roles: Object.keys(profile.byRole ?? {}) },
|
|
964
|
+
});
|
|
965
|
+
const summaries = await listProviderRuntimeProfiles(root);
|
|
966
|
+
return summaries.find((summary) => summary.name === name);
|
|
967
|
+
}
|
|
968
|
+
export async function smokeProviderRuntimeProfile(name, root = process.cwd(), env = process.env) {
|
|
969
|
+
validateProfileName(name);
|
|
970
|
+
const config = await getWorkflowConfig(root);
|
|
971
|
+
const profile = config.providers?.profiles?.[name];
|
|
972
|
+
if (!profile) {
|
|
973
|
+
throw new Error(`unknown provider runtime profile: ${name}`);
|
|
974
|
+
}
|
|
975
|
+
const checks = [];
|
|
976
|
+
for (const envName of profile.requiredEnv ?? []) {
|
|
977
|
+
checks.push({
|
|
978
|
+
scope: `env:${envName}`,
|
|
979
|
+
provider: "environment",
|
|
980
|
+
model: "not-applicable",
|
|
981
|
+
status: env[envName]?.trim() ? "pass" : "fail",
|
|
982
|
+
detail: env[envName]?.trim()
|
|
983
|
+
? "environment variable is configured"
|
|
984
|
+
: `missing required environment variable ${envName}`,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
const routes = Object.entries(profile.byRole ?? {}).map(([role, routing]) => [`role:${role}`, routing]);
|
|
988
|
+
if (profile.defaults)
|
|
989
|
+
routes.unshift(["defaults", profile.defaults]);
|
|
990
|
+
for (const [scope, routing] of routes) {
|
|
991
|
+
checks.push(...(await smokeRouting(scope, routing, config, root, env)));
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
profile: name,
|
|
995
|
+
passed: checks.every((check) => check.status === "pass"),
|
|
996
|
+
checks,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
1042
999
|
export async function completeWithProviderFallback(routing, prompt, { failingProviders = [], root = process.cwd(), taskId, role = "parent", jsonMode = false, providerMode = "fake", } = {}) {
|
|
1043
1000
|
const registry = new InMemoryModelProviderRegistry();
|
|
1044
1001
|
const config = await getWorkflowConfig(root);
|
|
@@ -1061,39 +1018,46 @@ export async function completeWithProviderFallback(routing, prompt, { failingPro
|
|
|
1061
1018
|
if (providerMode === "real" && index > 0) {
|
|
1062
1019
|
assertVendorFallbackAllowed(providerId, config.providerPolicy);
|
|
1063
1020
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1021
|
+
let attempts = 0;
|
|
1022
|
+
const maxAttempts = Math.max(1, 1 + routing.retries);
|
|
1023
|
+
while (attempts < maxAttempts) {
|
|
1024
|
+
attempts += 1;
|
|
1025
|
+
try {
|
|
1026
|
+
const provider = registry.get(providerId);
|
|
1027
|
+
const response = await provider.complete({
|
|
1028
|
+
model: routing.model,
|
|
1029
|
+
jsonMode,
|
|
1030
|
+
timeoutMs: routing.timeoutMs,
|
|
1031
|
+
messages: [{ role: "user", content: prompt }],
|
|
1032
|
+
});
|
|
1033
|
+
if (index > 0) {
|
|
1034
|
+
await appendEvent(root, removeUndefined({
|
|
1035
|
+
type: "MODEL_FALLBACK_USED",
|
|
1036
|
+
actor: role,
|
|
1037
|
+
taskId,
|
|
1038
|
+
summary: `Fallback provider used: ${providerId}`,
|
|
1039
|
+
metadata: { provider: providerId, failedProviders },
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
provider: providerId,
|
|
1044
|
+
model: routing.model,
|
|
1045
|
+
response,
|
|
1046
|
+
fallbackUsed: index > 0,
|
|
1047
|
+
failedProviders,
|
|
1048
|
+
};
|
|
1080
1049
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
throw error;
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
const failure = providerFailureFromError(providerId, error, attempts);
|
|
1052
|
+
if (failure.code === "timeout") {
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
if (failure.retryable && attempts < maxAttempts) {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
failedProviders.push(failure);
|
|
1059
|
+
break;
|
|
1092
1060
|
}
|
|
1093
|
-
failedProviders.push({
|
|
1094
|
-
provider: providerId,
|
|
1095
|
-
reason: sanitizeProviderError(error),
|
|
1096
|
-
});
|
|
1097
1061
|
}
|
|
1098
1062
|
}
|
|
1099
1063
|
throw new ProviderFallbackError(failedProviders);
|
|
@@ -1112,10 +1076,31 @@ export function providerFailuresFromError(error) {
|
|
|
1112
1076
|
function providerFailureSummary(failures) {
|
|
1113
1077
|
const providers = failures.map((failure) => failure.provider).join(", ");
|
|
1114
1078
|
const details = failures
|
|
1115
|
-
.map((failure) => `${failure.provider}: ${failure.reason}`)
|
|
1079
|
+
.map((failure) => `${failure.provider}: ${failure.reason} (${failure.code}, attempts=${failure.attempts})`)
|
|
1116
1080
|
.join("; ");
|
|
1117
1081
|
return `all providers failed: ${providers}${details ? ` (${details})` : ""}`;
|
|
1118
1082
|
}
|
|
1083
|
+
function providerFailureFromError(providerId, error, attempts) {
|
|
1084
|
+
const code = providerFailureCode(error);
|
|
1085
|
+
return {
|
|
1086
|
+
provider: providerId,
|
|
1087
|
+
code,
|
|
1088
|
+
reason: sanitizeProviderError(error),
|
|
1089
|
+
retryable: code === "provider_error",
|
|
1090
|
+
attempts,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function providerFailureCode(error) {
|
|
1094
|
+
const message = errorMessage(error).toLowerCase();
|
|
1095
|
+
if (isProviderTimeoutError(error))
|
|
1096
|
+
return "timeout";
|
|
1097
|
+
if (/permission|denied|forbidden|unauthorized/.test(message)) {
|
|
1098
|
+
return "permission_denied";
|
|
1099
|
+
}
|
|
1100
|
+
if (/policy|not allowed|blocked/.test(message))
|
|
1101
|
+
return "policy_blocked";
|
|
1102
|
+
return "provider_error";
|
|
1103
|
+
}
|
|
1119
1104
|
function sanitizeProviderError(error) {
|
|
1120
1105
|
const messages = [];
|
|
1121
1106
|
if (error instanceof Error) {
|
|
@@ -1141,6 +1126,9 @@ function sanitizeProviderError(error) {
|
|
|
1141
1126
|
}
|
|
1142
1127
|
return redactProviderError(messages.filter(Boolean).join(": "));
|
|
1143
1128
|
}
|
|
1129
|
+
function errorMessage(error) {
|
|
1130
|
+
return error instanceof Error ? error.message : String(error);
|
|
1131
|
+
}
|
|
1144
1132
|
function isErrorCauseRecord(value) {
|
|
1145
1133
|
return typeof value === "object" && value !== null;
|
|
1146
1134
|
}
|
|
@@ -1171,6 +1159,10 @@ export async function setRoleModelProvider(role, routing, root = process.cwd())
|
|
|
1171
1159
|
...(config.providers.byRole ?? {}),
|
|
1172
1160
|
[role]: routing,
|
|
1173
1161
|
},
|
|
1162
|
+
profiles: config.providers.profiles ?? {},
|
|
1163
|
+
...(config.providers.activeProfile
|
|
1164
|
+
? { activeProfile: config.providers.activeProfile }
|
|
1165
|
+
: {}),
|
|
1174
1166
|
};
|
|
1175
1167
|
await writeJson(configPath, config);
|
|
1176
1168
|
await appendEvent(root, {
|
|
@@ -1207,6 +1199,10 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1207
1199
|
config.providers = {
|
|
1208
1200
|
defaults: config.providers?.defaults ?? routing,
|
|
1209
1201
|
byRole,
|
|
1202
|
+
profiles: config.providers?.profiles ?? {},
|
|
1203
|
+
...(config.providers?.activeProfile
|
|
1204
|
+
? { activeProfile: config.providers.activeProfile }
|
|
1205
|
+
: {}),
|
|
1210
1206
|
};
|
|
1211
1207
|
const credential = removeUndefined({
|
|
1212
1208
|
apiKeyFile: input.apiKeyFile,
|
|
@@ -1227,7 +1223,7 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1227
1223
|
if (input.allowDirectProviderApi) {
|
|
1228
1224
|
config.providerPolicy = {
|
|
1229
1225
|
...(config.providerPolicy ?? {}),
|
|
1230
|
-
allowedProviders:
|
|
1226
|
+
allowedProviders: uniqueStrings([
|
|
1231
1227
|
...(config.providerPolicy?.allowedProviders ?? []),
|
|
1232
1228
|
input.provider,
|
|
1233
1229
|
]),
|
|
@@ -1239,6 +1235,9 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1239
1235
|
delegation: {
|
|
1240
1236
|
mode: config.runtimePolicy?.delegation?.mode ?? "runtime-native",
|
|
1241
1237
|
allowDirectProviderApi: true,
|
|
1238
|
+
...(config.runtimePolicy?.delegation?.guardrails
|
|
1239
|
+
? { guardrails: config.runtimePolicy.delegation.guardrails }
|
|
1240
|
+
: {}),
|
|
1242
1241
|
},
|
|
1243
1242
|
};
|
|
1244
1243
|
}
|
|
@@ -1263,8 +1262,85 @@ export async function connectModelProvider(input, root = process.cwd()) {
|
|
|
1263
1262
|
allowDirectProviderApi: input.allowDirectProviderApi,
|
|
1264
1263
|
});
|
|
1265
1264
|
}
|
|
1266
|
-
function
|
|
1267
|
-
|
|
1265
|
+
function validateProfileName(name) {
|
|
1266
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,62}$/i.test(name)) {
|
|
1267
|
+
throw new Error("profile name must be 2-63 characters and contain only letters, numbers, dots, underscores, or dashes");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function validateProfileRouting(routing) {
|
|
1271
|
+
if (!routing.provider?.trim()) {
|
|
1272
|
+
throw new Error("profile routing requires provider");
|
|
1273
|
+
}
|
|
1274
|
+
if (!routing.model?.trim()) {
|
|
1275
|
+
throw new Error("profile routing requires model");
|
|
1276
|
+
}
|
|
1277
|
+
if (new Set([routing.provider, ...(routing.fallbacks ?? [])]).size !==
|
|
1278
|
+
[routing.provider, ...(routing.fallbacks ?? [])].length) {
|
|
1279
|
+
throw new Error("profile routing fallback chain contains duplicates");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async function smokeRouting(scope, routing, config, root, env) {
|
|
1283
|
+
const checks = [];
|
|
1284
|
+
const providerIds = [routing.provider, ...(routing.fallbacks ?? [])];
|
|
1285
|
+
for (const [index, providerId] of providerIds.entries()) {
|
|
1286
|
+
const providerScope = index === 0 ? scope : `${scope}:fallback:${index}`;
|
|
1287
|
+
const policyError = providerPolicySmokeError(providerId, config, index);
|
|
1288
|
+
if (policyError) {
|
|
1289
|
+
checks.push({
|
|
1290
|
+
scope: providerScope,
|
|
1291
|
+
provider: providerId,
|
|
1292
|
+
model: routing.model,
|
|
1293
|
+
status: "fail",
|
|
1294
|
+
detail: policyError,
|
|
1295
|
+
});
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const credentialError = await providerCredentialSmokeError(providerId, config.providerCredentials?.byProvider?.[providerId], root, env);
|
|
1299
|
+
checks.push({
|
|
1300
|
+
scope: providerScope,
|
|
1301
|
+
provider: providerId,
|
|
1302
|
+
model: routing.model,
|
|
1303
|
+
status: credentialError ? "fail" : "pass",
|
|
1304
|
+
detail: credentialError ?? "provider configuration is present",
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
return checks;
|
|
1308
|
+
}
|
|
1309
|
+
function providerPolicySmokeError(providerId, config, fallbackIndex) {
|
|
1310
|
+
try {
|
|
1311
|
+
assertProviderAllowed(providerId, config.providerPolicy);
|
|
1312
|
+
if (fallbackIndex > 0) {
|
|
1313
|
+
assertVendorFallbackAllowed(providerId, config.providerPolicy);
|
|
1314
|
+
}
|
|
1315
|
+
return undefined;
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
return error instanceof Error ? error.message : String(error);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async function providerCredentialSmokeError(providerId, credential, root, env) {
|
|
1322
|
+
if (providerId === "none" ||
|
|
1323
|
+
providerId === "fake" ||
|
|
1324
|
+
providerId === "ollama") {
|
|
1325
|
+
return undefined;
|
|
1326
|
+
}
|
|
1327
|
+
const apiKeyEnv = credential?.apiKeyEnv ?? defaultApiKeyEnv(providerId);
|
|
1328
|
+
const apiKeyFileEnv = credential?.apiKeyFileEnv ?? defaultApiKeyFileEnv(providerId);
|
|
1329
|
+
const effectiveEnv = providerEnvFromCredentialConfig(providerId, credential, env);
|
|
1330
|
+
if (apiKeyEnv && effectiveEnv[apiKeyEnv]?.trim()) {
|
|
1331
|
+
return undefined;
|
|
1332
|
+
}
|
|
1333
|
+
if (apiKeyFileEnv && effectiveEnv[apiKeyFileEnv]?.trim()) {
|
|
1334
|
+
const keyFile = effectiveEnv[apiKeyFileEnv].trim();
|
|
1335
|
+
if (!path.isAbsolute(keyFile)) {
|
|
1336
|
+
return `${apiKeyFileEnv} must reference an absolute path`;
|
|
1337
|
+
}
|
|
1338
|
+
if (!(await exists(keyFile))) {
|
|
1339
|
+
return `${apiKeyFileEnv} references an unreadable secret file`;
|
|
1340
|
+
}
|
|
1341
|
+
return undefined;
|
|
1342
|
+
}
|
|
1343
|
+
return `${apiKeyEnv ?? "provider API key"} or ${apiKeyFileEnv ?? "provider API key file"} is required`;
|
|
1268
1344
|
}
|
|
1269
1345
|
export async function recordModelProvenance(input, root = process.cwd()) {
|
|
1270
1346
|
const workspace = await loadWorkspace(root);
|
|
@@ -1440,27 +1516,9 @@ async function writeBudgetEscalationProposal(root, taskId, proposal) {
|
|
|
1440
1516
|
].join("\n");
|
|
1441
1517
|
return writeArtifact(root, "approvals", `${taskId}-budget-fallback.md`, content);
|
|
1442
1518
|
}
|
|
1443
|
-
async function mutateTasks(base, update) {
|
|
1444
|
-
return updateJsonFile(path.join(base, FILES.tasks), [], update);
|
|
1445
|
-
}
|
|
1446
1519
|
async function mutateLocks(base, update) {
|
|
1447
1520
|
return updateJsonFile(path.join(base, FILES.locks), [], update);
|
|
1448
1521
|
}
|
|
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
1522
|
function flowRequirementLines(requirements) {
|
|
1465
1523
|
return requirements.length > 0
|
|
1466
1524
|
? requirements.map((requirement) => `- ${requirement}`)
|