@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.
Files changed (249) 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 +22 -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/metrics-commands.js +47 -13
  78. package/dist/metrics-commands.js.map +1 -1
  79. package/dist/model-commands.d.ts +5 -0
  80. package/dist/model-commands.js +95 -1
  81. package/dist/model-commands.js.map +1 -1
  82. package/dist/model-providers.js +13 -1
  83. package/dist/model-providers.js.map +1 -1
  84. package/dist/package-update-check.d.ts +18 -0
  85. package/dist/package-update-check.js +20 -0
  86. package/dist/package-update-check.js.map +1 -1
  87. package/dist/phase-executor.d.ts +1 -0
  88. package/dist/phase-executor.js +115 -14
  89. package/dist/phase-executor.js.map +1 -1
  90. package/dist/phase-playbooks.d.ts +15 -0
  91. package/dist/phase-playbooks.js +82 -0
  92. package/dist/phase-playbooks.js.map +1 -1
  93. package/dist/planning-commands.d.ts +1 -0
  94. package/dist/planning-commands.js +24 -1
  95. package/dist/planning-commands.js.map +1 -1
  96. package/dist/project-detection.js +9 -7
  97. package/dist/project-detection.js.map +1 -1
  98. package/dist/prompt-registry-update.d.ts +2 -0
  99. package/dist/prompt-registry-update.js +5 -1
  100. package/dist/prompt-registry-update.js.map +1 -1
  101. package/dist/prompt-registry-validation.js +39 -2
  102. package/dist/prompt-registry-validation.js.map +1 -1
  103. package/dist/qa-commands.d.ts +2 -0
  104. package/dist/qa-commands.js +18 -0
  105. package/dist/qa-commands.js.map +1 -0
  106. package/dist/qa-coverage.d.ts +24 -0
  107. package/dist/qa-coverage.js +189 -0
  108. package/dist/qa-coverage.js.map +1 -0
  109. package/dist/qa-readiness.d.ts +5 -0
  110. package/dist/qa-readiness.js +26 -0
  111. package/dist/qa-readiness.js.map +1 -0
  112. package/dist/refresh-generated.d.ts +10 -1
  113. package/dist/refresh-generated.js +83 -6
  114. package/dist/refresh-generated.js.map +1 -1
  115. package/dist/release-candidate.d.ts +9 -1
  116. package/dist/release-candidate.js +52 -1
  117. package/dist/release-candidate.js.map +1 -1
  118. package/dist/release-commands.js +161 -8
  119. package/dist/release-commands.js.map +1 -1
  120. package/dist/release-readiness.d.ts +33 -0
  121. package/dist/release-readiness.js +187 -3
  122. package/dist/release-readiness.js.map +1 -1
  123. package/dist/runtime-bootstrap.js +1 -1
  124. package/dist/runtime-bootstrap.js.map +1 -1
  125. package/dist/runtime-commands.d.ts +2 -0
  126. package/dist/runtime-commands.js +77 -0
  127. package/dist/runtime-commands.js.map +1 -1
  128. package/dist/runtime-execution-renderer.d.ts +3 -2
  129. package/dist/runtime-execution-renderer.js +19 -1
  130. package/dist/runtime-execution-renderer.js.map +1 -1
  131. package/dist/runtime-execution.d.ts +2 -1
  132. package/dist/runtime-execution.js +71 -11
  133. package/dist/runtime-execution.js.map +1 -1
  134. package/dist/runtime-guardrails.d.ts +26 -0
  135. package/dist/runtime-guardrails.js +168 -0
  136. package/dist/runtime-guardrails.js.map +1 -0
  137. package/dist/setup-agents-import.js +5 -3
  138. package/dist/setup-agents-import.js.map +1 -1
  139. package/dist/skills-commands.d.ts +4 -0
  140. package/dist/skills-commands.js +55 -2
  141. package/dist/skills-commands.js.map +1 -1
  142. package/dist/skills-memory.d.ts +36 -2
  143. package/dist/skills-memory.js +165 -6
  144. package/dist/skills-memory.js.map +1 -1
  145. package/dist/skills-planning.js +2 -4
  146. package/dist/skills-planning.js.map +1 -1
  147. package/dist/skills-render.js +2 -4
  148. package/dist/skills-render.js.map +1 -1
  149. package/dist/skills.d.ts +1 -1
  150. package/dist/skills.js +1 -1
  151. package/dist/skills.js.map +1 -1
  152. package/dist/sprint-commands.js +2 -1
  153. package/dist/sprint-commands.js.map +1 -1
  154. package/dist/subagent-protocol.js +3 -5
  155. package/dist/subagent-protocol.js.map +1 -1
  156. package/dist/support-commands.d.ts +2 -0
  157. package/dist/support-commands.js +18 -0
  158. package/dist/support-commands.js.map +1 -0
  159. package/dist/support-diagnostics.d.ts +49 -0
  160. package/dist/support-diagnostics.js +86 -0
  161. package/dist/support-diagnostics.js.map +1 -0
  162. package/dist/task-graph-commands.js +5 -3
  163. package/dist/task-graph-commands.js.map +1 -1
  164. package/dist/telemetry-redaction.js +8 -1
  165. package/dist/telemetry-redaction.js.map +1 -1
  166. package/dist/tool-commands.d.ts +3 -0
  167. package/dist/tool-commands.js +62 -0
  168. package/dist/tool-commands.js.map +1 -1
  169. package/dist/tracker-adapters.d.ts +71 -0
  170. package/dist/tracker-adapters.js +186 -0
  171. package/dist/tracker-adapters.js.map +1 -0
  172. package/dist/tracker-commands.d.ts +2 -0
  173. package/dist/tracker-commands.js +119 -0
  174. package/dist/tracker-commands.js.map +1 -0
  175. package/dist/types/metrics.d.ts +24 -0
  176. package/dist/types/model-config.d.ts +35 -0
  177. package/dist/types/runtime.d.ts +56 -0
  178. package/dist/types/skills.d.ts +2 -0
  179. package/dist/types/tasks.d.ts +6 -0
  180. package/dist/types/workflow-run.d.ts +17 -0
  181. package/dist/types.d.ts +4 -4
  182. package/dist/types.js.map +1 -1
  183. package/dist/upgrade-commands.js +13 -4
  184. package/dist/upgrade-commands.js.map +1 -1
  185. package/dist/validation.js +2 -2
  186. package/dist/validation.js.map +1 -1
  187. package/dist/visual-validation.d.ts +81 -0
  188. package/dist/visual-validation.js +290 -0
  189. package/dist/visual-validation.js.map +1 -0
  190. package/dist/web-action-security.d.ts +11 -0
  191. package/dist/web-action-security.js +45 -0
  192. package/dist/web-action-security.js.map +1 -0
  193. package/dist/web-api-read-routes.js +101 -1
  194. package/dist/web-api-read-routes.js.map +1 -1
  195. package/dist/web-api.js +507 -5
  196. package/dist/web-api.js.map +1 -1
  197. package/dist/web-artifacts.d.ts +55 -0
  198. package/dist/web-artifacts.js +222 -0
  199. package/dist/web-artifacts.js.map +1 -0
  200. package/dist/web-console/assets/index-C9lx-V42.css +1 -0
  201. package/dist/web-console/assets/index-M3S0g1GK.js +11 -0
  202. package/dist/web-console/index.html +13 -0
  203. package/dist/web-console.js +9 -3
  204. package/dist/web-console.js.map +1 -1
  205. package/dist/web-recovery.d.ts +30 -0
  206. package/dist/web-recovery.js +163 -0
  207. package/dist/web-recovery.js.map +1 -0
  208. package/dist/web-workflow-progress.d.ts +41 -0
  209. package/dist/web-workflow-progress.js +114 -0
  210. package/dist/web-workflow-progress.js.map +1 -0
  211. package/dist/workflow-approval-service.d.ts +2 -1
  212. package/dist/workflow-approval-service.js +72 -0
  213. package/dist/workflow-approval-service.js.map +1 -1
  214. package/dist/workflow-evidence-service.js +8 -1
  215. package/dist/workflow-evidence-service.js.map +1 -1
  216. package/dist/workflow-gates.d.ts +1 -0
  217. package/dist/workflow-gates.js +57 -0
  218. package/dist/workflow-gates.js.map +1 -1
  219. package/dist/workflow-run-commands.js +13 -1
  220. package/dist/workflow-run-commands.js.map +1 -1
  221. package/dist/workflow-services.d.ts +16 -12
  222. package/dist/workflow-services.js +311 -253
  223. package/dist/workflow-services.js.map +1 -1
  224. package/dist/workflow-task-service.d.ts +11 -0
  225. package/dist/workflow-task-service.js +242 -0
  226. package/dist/workflow-task-service.js.map +1 -0
  227. package/dist/workspace-validator.js +109 -3
  228. package/dist/workspace-validator.js.map +1 -1
  229. package/dist/workspace.js +8 -2
  230. package/dist/workspace.js.map +1 -1
  231. package/docs/adoption-guide.md +147 -0
  232. package/docs/autonomous-workflow.md +118 -27
  233. package/docs/benchmark.md +15 -7
  234. package/docs/command-contracts.md +18 -1
  235. package/docs/core-command-surface.md +59 -13
  236. package/docs/end-to-end-demo.md +1 -0
  237. package/docs/extension-contracts.md +83 -0
  238. package/docs/orchestra-mvp.md +83 -3
  239. package/docs/persona-workflows.md +32 -0
  240. package/docs/release-test-matrix.md +42 -0
  241. package/docs/runtime-adapters.md +92 -0
  242. package/docs/runtime-llm-flow.md +13 -0
  243. package/docs/setup-agents-applicability-review.md +173 -0
  244. package/docs/source-of-truth-and-agent-learning.md +14 -0
  245. package/docs/traceability-flow.md +5 -1
  246. package/docs/tracker-adapter-contract.md +10 -1
  247. package/docs/web-console-qa.md +35 -0
  248. package/package.json +12 -6
  249. 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 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
  }
@@ -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
- 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
- }));
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
- 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;
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: unique([
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 unique(values) {
1267
- return [...new Set(values)];
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}`)