@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.
Files changed (260) hide show
  1. package/AGENTS.md +9 -8
  2. package/CLAUDE.md +13 -11
  3. package/README.md +78 -11
  4. package/dist/assets/web-console.js +169 -32
  5. package/dist/automation-evidence.d.ts +23 -0
  6. package/dist/automation-evidence.js +97 -0
  7. package/dist/automation-evidence.js.map +1 -0
  8. package/dist/autonomous-run-store.js +3 -3
  9. package/dist/autonomous-run-store.js.map +1 -1
  10. package/dist/benchmark.d.ts +4 -1
  11. package/dist/benchmark.js +93 -4
  12. package/dist/benchmark.js.map +1 -1
  13. package/dist/cli.js +73 -2
  14. package/dist/cli.js.map +1 -1
  15. package/dist/collaboration-flows.js +3 -5
  16. package/dist/collaboration-flows.js.map +1 -1
  17. package/dist/collection-utils.d.ts +3 -0
  18. package/dist/collection-utils.js +10 -0
  19. package/dist/collection-utils.js.map +1 -0
  20. package/dist/command-manifest.d.ts +12 -1
  21. package/dist/command-manifest.js +213 -10
  22. package/dist/command-manifest.js.map +1 -1
  23. package/dist/commands.d.ts +10 -5
  24. package/dist/commands.js +16 -6
  25. package/dist/commands.js.map +1 -1
  26. package/dist/config-migrations.d.ts +24 -0
  27. package/dist/config-migrations.js +102 -0
  28. package/dist/config-migrations.js.map +1 -0
  29. package/dist/constants.d.ts +2 -0
  30. package/dist/constants.js +23 -0
  31. package/dist/constants.js.map +1 -1
  32. package/dist/dashboard-commands.d.ts +2 -0
  33. package/dist/dashboard-commands.js +14 -0
  34. package/dist/dashboard-commands.js.map +1 -0
  35. package/dist/defaults.d.ts +13 -0
  36. package/dist/defaults.js +13 -0
  37. package/dist/defaults.js.map +1 -1
  38. package/dist/delegation-decision.js +23 -8
  39. package/dist/delegation-decision.js.map +1 -1
  40. package/dist/delivery-commands.js +5 -0
  41. package/dist/delivery-commands.js.map +1 -1
  42. package/dist/delivery-dashboard-charts.d.ts +4 -0
  43. package/dist/delivery-dashboard-charts.js +156 -0
  44. package/dist/delivery-dashboard-charts.js.map +1 -0
  45. package/dist/delivery-dashboard-html.d.ts +2 -0
  46. package/dist/delivery-dashboard-html.js +115 -0
  47. package/dist/delivery-dashboard-html.js.map +1 -0
  48. package/dist/delivery-dashboard-types.d.ts +78 -0
  49. package/dist/delivery-dashboard-types.js +2 -0
  50. package/dist/delivery-dashboard-types.js.map +1 -0
  51. package/dist/delivery-dashboard.d.ts +8 -0
  52. package/dist/delivery-dashboard.js +124 -0
  53. package/dist/delivery-dashboard.js.map +1 -0
  54. package/dist/effort-classification.d.ts +7 -0
  55. package/dist/effort-classification.js +72 -0
  56. package/dist/effort-classification.js.map +1 -0
  57. package/dist/extension-commands.d.ts +3 -0
  58. package/dist/extension-commands.js +40 -0
  59. package/dist/extension-commands.js.map +1 -0
  60. package/dist/extensions.d.ts +22 -0
  61. package/dist/extensions.js +126 -0
  62. package/dist/extensions.js.map +1 -0
  63. package/dist/github.d.ts +2 -0
  64. package/dist/github.js +15 -3
  65. package/dist/github.js.map +1 -1
  66. package/dist/health-checks.js +51 -0
  67. package/dist/health-checks.js.map +1 -1
  68. package/dist/lucid-story-map.d.ts +73 -0
  69. package/dist/lucid-story-map.js +112 -0
  70. package/dist/lucid-story-map.js.map +1 -0
  71. package/dist/mcp-integrations.d.ts +19 -0
  72. package/dist/mcp-integrations.js +58 -0
  73. package/dist/mcp-integrations.js.map +1 -0
  74. package/dist/mcp-tool-adapter.d.ts +21 -0
  75. package/dist/mcp-tool-adapter.js +56 -0
  76. package/dist/mcp-tool-adapter.js.map +1 -0
  77. package/dist/memory.js +18 -8
  78. package/dist/memory.js.map +1 -1
  79. package/dist/metrics-commands.js +47 -13
  80. package/dist/metrics-commands.js.map +1 -1
  81. package/dist/model-commands.d.ts +5 -0
  82. package/dist/model-commands.js +101 -3
  83. package/dist/model-commands.js.map +1 -1
  84. package/dist/model-providers.js +13 -1
  85. package/dist/model-providers.js.map +1 -1
  86. package/dist/package-update-check.d.ts +18 -0
  87. package/dist/package-update-check.js +20 -0
  88. package/dist/package-update-check.js.map +1 -1
  89. package/dist/phase-executor.d.ts +1 -0
  90. package/dist/phase-executor.js +118 -14
  91. package/dist/phase-executor.js.map +1 -1
  92. package/dist/phase-playbooks.d.ts +15 -0
  93. package/dist/phase-playbooks.js +82 -0
  94. package/dist/phase-playbooks.js.map +1 -1
  95. package/dist/planning-commands.d.ts +1 -0
  96. package/dist/planning-commands.js +24 -1
  97. package/dist/planning-commands.js.map +1 -1
  98. package/dist/project-detection.js +9 -7
  99. package/dist/project-detection.js.map +1 -1
  100. package/dist/prompt-registry-update.d.ts +2 -0
  101. package/dist/prompt-registry-update.js +25 -1
  102. package/dist/prompt-registry-update.js.map +1 -1
  103. package/dist/prompt-registry-validation.js +39 -2
  104. package/dist/prompt-registry-validation.js.map +1 -1
  105. package/dist/qa-commands.d.ts +2 -0
  106. package/dist/qa-commands.js +18 -0
  107. package/dist/qa-commands.js.map +1 -0
  108. package/dist/qa-coverage.d.ts +24 -0
  109. package/dist/qa-coverage.js +198 -0
  110. package/dist/qa-coverage.js.map +1 -0
  111. package/dist/qa-readiness.d.ts +5 -0
  112. package/dist/qa-readiness.js +26 -0
  113. package/dist/qa-readiness.js.map +1 -0
  114. package/dist/refresh-generated.d.ts +10 -1
  115. package/dist/refresh-generated.js +83 -6
  116. package/dist/refresh-generated.js.map +1 -1
  117. package/dist/release-candidate.d.ts +9 -1
  118. package/dist/release-candidate.js +52 -1
  119. package/dist/release-candidate.js.map +1 -1
  120. package/dist/release-commands.js +202 -12
  121. package/dist/release-commands.js.map +1 -1
  122. package/dist/release-readiness.d.ts +36 -1
  123. package/dist/release-readiness.js +217 -6
  124. package/dist/release-readiness.js.map +1 -1
  125. package/dist/runtime-bootstrap.js +1 -1
  126. package/dist/runtime-bootstrap.js.map +1 -1
  127. package/dist/runtime-commands.d.ts +2 -0
  128. package/dist/runtime-commands.js +77 -0
  129. package/dist/runtime-commands.js.map +1 -1
  130. package/dist/runtime-execution-renderer.d.ts +3 -2
  131. package/dist/runtime-execution-renderer.js +19 -1
  132. package/dist/runtime-execution-renderer.js.map +1 -1
  133. package/dist/runtime-execution.d.ts +2 -1
  134. package/dist/runtime-execution.js +71 -11
  135. package/dist/runtime-execution.js.map +1 -1
  136. package/dist/runtime-guardrails.d.ts +26 -0
  137. package/dist/runtime-guardrails.js +168 -0
  138. package/dist/runtime-guardrails.js.map +1 -0
  139. package/dist/setup-agents-import.js +5 -3
  140. package/dist/setup-agents-import.js.map +1 -1
  141. package/dist/skills-catalog.js +63 -0
  142. package/dist/skills-catalog.js.map +1 -1
  143. package/dist/skills-commands.d.ts +4 -0
  144. package/dist/skills-commands.js +55 -2
  145. package/dist/skills-commands.js.map +1 -1
  146. package/dist/skills-memory.d.ts +36 -2
  147. package/dist/skills-memory.js +165 -6
  148. package/dist/skills-memory.js.map +1 -1
  149. package/dist/skills-planning.js +2 -4
  150. package/dist/skills-planning.js.map +1 -1
  151. package/dist/skills-render.js +2 -4
  152. package/dist/skills-render.js.map +1 -1
  153. package/dist/skills.d.ts +1 -1
  154. package/dist/skills.js +1 -1
  155. package/dist/skills.js.map +1 -1
  156. package/dist/sprint-commands.js +2 -1
  157. package/dist/sprint-commands.js.map +1 -1
  158. package/dist/subagent-protocol.js +3 -5
  159. package/dist/subagent-protocol.js.map +1 -1
  160. package/dist/support-commands.d.ts +2 -0
  161. package/dist/support-commands.js +18 -0
  162. package/dist/support-commands.js.map +1 -0
  163. package/dist/support-diagnostics.d.ts +49 -0
  164. package/dist/support-diagnostics.js +86 -0
  165. package/dist/support-diagnostics.js.map +1 -0
  166. package/dist/task-graph-commands.js +5 -3
  167. package/dist/task-graph-commands.js.map +1 -1
  168. package/dist/telemetry-redaction.js +8 -1
  169. package/dist/telemetry-redaction.js.map +1 -1
  170. package/dist/tool-commands.d.ts +3 -0
  171. package/dist/tool-commands.js +62 -0
  172. package/dist/tool-commands.js.map +1 -1
  173. package/dist/tracker-adapters.d.ts +71 -0
  174. package/dist/tracker-adapters.js +186 -0
  175. package/dist/tracker-adapters.js.map +1 -0
  176. package/dist/tracker-commands.d.ts +2 -0
  177. package/dist/tracker-commands.js +119 -0
  178. package/dist/tracker-commands.js.map +1 -0
  179. package/dist/types/metrics.d.ts +24 -0
  180. package/dist/types/model-config.d.ts +39 -0
  181. package/dist/types/runtime.d.ts +56 -0
  182. package/dist/types/skills.d.ts +2 -0
  183. package/dist/types/tasks.d.ts +6 -0
  184. package/dist/types/workflow-run.d.ts +17 -0
  185. package/dist/types.d.ts +4 -4
  186. package/dist/types.js.map +1 -1
  187. package/dist/upgrade-commands.js +13 -4
  188. package/dist/upgrade-commands.js.map +1 -1
  189. package/dist/validation.js +2 -2
  190. package/dist/validation.js.map +1 -1
  191. package/dist/visual-validation.d.ts +81 -0
  192. package/dist/visual-validation.js +290 -0
  193. package/dist/visual-validation.js.map +1 -0
  194. package/dist/web-action-security.d.ts +11 -0
  195. package/dist/web-action-security.js +45 -0
  196. package/dist/web-action-security.js.map +1 -0
  197. package/dist/web-api-read-routes.js +101 -1
  198. package/dist/web-api-read-routes.js.map +1 -1
  199. package/dist/web-api.js +507 -5
  200. package/dist/web-api.js.map +1 -1
  201. package/dist/web-artifacts.d.ts +55 -0
  202. package/dist/web-artifacts.js +222 -0
  203. package/dist/web-artifacts.js.map +1 -0
  204. package/dist/web-console/assets/index-BNESIVvk.js +11 -0
  205. package/dist/web-console/assets/index-jxCY5eEc.css +1 -0
  206. package/dist/web-console/index.html +13 -0
  207. package/dist/web-console.js +9 -3
  208. package/dist/web-console.js.map +1 -1
  209. package/dist/web-recovery.d.ts +30 -0
  210. package/dist/web-recovery.js +163 -0
  211. package/dist/web-recovery.js.map +1 -0
  212. package/dist/web-workflow-progress.d.ts +41 -0
  213. package/dist/web-workflow-progress.js +114 -0
  214. package/dist/web-workflow-progress.js.map +1 -0
  215. package/dist/workflow-approval-service.d.ts +2 -1
  216. package/dist/workflow-approval-service.js +72 -0
  217. package/dist/workflow-approval-service.js.map +1 -1
  218. package/dist/workflow-evidence-service.js +8 -1
  219. package/dist/workflow-evidence-service.js.map +1 -1
  220. package/dist/workflow-gates.d.ts +2 -0
  221. package/dist/workflow-gates.js +221 -0
  222. package/dist/workflow-gates.js.map +1 -1
  223. package/dist/workflow-run-commands.js +13 -1
  224. package/dist/workflow-run-commands.js.map +1 -1
  225. package/dist/workflow-services.d.ts +16 -12
  226. package/dist/workflow-services.js +313 -253
  227. package/dist/workflow-services.js.map +1 -1
  228. package/dist/workflow-task-service.d.ts +11 -0
  229. package/dist/workflow-task-service.js +242 -0
  230. package/dist/workflow-task-service.js.map +1 -0
  231. package/dist/workspace-validator.js +109 -3
  232. package/dist/workspace-validator.js.map +1 -1
  233. package/dist/workspace.js +8 -2
  234. package/dist/workspace.js.map +1 -1
  235. package/docs/adoption-guide.md +147 -0
  236. package/docs/autonomous-workflow.md +118 -27
  237. package/docs/benchmark.md +15 -7
  238. package/docs/command-contracts.md +18 -1
  239. package/docs/core-command-surface.md +59 -13
  240. package/docs/end-to-end-demo.md +1 -0
  241. package/docs/extension-contracts.md +83 -0
  242. package/docs/orchestra-mvp.md +83 -3
  243. package/docs/persona-workflows.md +32 -0
  244. package/docs/release-test-matrix.md +42 -0
  245. package/docs/runtime-adapters.md +92 -0
  246. package/docs/runtime-llm-flow.md +13 -0
  247. package/docs/setup-agents-applicability-review.md +173 -0
  248. package/docs/skill-loading-strategy.md +1 -0
  249. package/docs/source-of-truth-and-agent-learning.md +14 -0
  250. package/docs/traceability-flow.md +16 -1
  251. package/docs/tracker-adapter-contract.md +10 -1
  252. package/docs/web-console-qa.md +35 -0
  253. package/package.json +12 -6
  254. package/rules/development-engineering.mdc +68 -0
  255. package/rules/devops-tooling.mdc +1 -0
  256. package/rules/dry-clean-code.mdc +1 -0
  257. package/rules/performance-reliability.mdc +1 -0
  258. package/rules/testing-discipline.mdc +4 -1
  259. package/skills/collection-standards/SKILL.md +63 -0
  260. 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 listTasks(root);
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 updateTask({ id: input.task, ownerRole: input.to }, root);
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
- try {
1065
- const provider = registry.get(providerId);
1066
- const response = await provider.complete({
1067
- model: routing.model,
1068
- jsonMode,
1069
- timeoutMs: routing.timeoutMs,
1070
- messages: [{ role: "user", content: prompt }],
1071
- });
1072
- if (index > 0) {
1073
- await appendEvent(root, removeUndefined({
1074
- type: "MODEL_FALLBACK_USED",
1075
- actor: role,
1076
- taskId,
1077
- summary: `Fallback provider used: ${providerId}`,
1078
- metadata: { provider: providerId, failedProviders },
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
- return {
1082
- provider: providerId,
1083
- model: routing.model,
1084
- response,
1085
- fallbackUsed: index > 0,
1086
- failedProviders,
1087
- };
1088
- }
1089
- catch (error) {
1090
- if (isProviderTimeoutError(error)) {
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: unique([
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 unique(values) {
1267
- return [...new Set(values)];
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}`)