agentweaver 0.1.15 → 0.1.16

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 (94) hide show
  1. package/README.md +26 -9
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +15 -0
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/flow-readiness.js +15 -18
  6. package/dist/flow-state.js +75 -15
  7. package/dist/index.js +391 -175
  8. package/dist/interactive/blessed-session.js +361 -0
  9. package/dist/interactive/controller.js +1293 -0
  10. package/dist/interactive/create-interactive-session.js +5 -0
  11. package/dist/interactive/ink/index.js +576 -0
  12. package/dist/interactive/progress.js +245 -0
  13. package/dist/interactive/selectors.js +14 -0
  14. package/dist/interactive/session.js +1 -0
  15. package/dist/interactive/state.js +34 -0
  16. package/dist/interactive/tree.js +155 -0
  17. package/dist/interactive/types.js +1 -0
  18. package/dist/interactive/view-model.js +1 -0
  19. package/dist/interactive-ui.js +159 -194
  20. package/dist/pipeline/context.js +1 -0
  21. package/dist/pipeline/declarative-flow-runner.js +212 -6
  22. package/dist/pipeline/declarative-flows.js +27 -0
  23. package/dist/pipeline/execution-routing-config.js +15 -0
  24. package/dist/pipeline/flow-catalog.js +19 -3
  25. package/dist/pipeline/flow-run-resume.js +29 -0
  26. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  27. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  28. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  29. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  30. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  31. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  32. package/dist/pipeline/flow-specs/design-review.json +10 -0
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  35. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  36. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  37. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  38. package/dist/pipeline/flow-specs/implement.json +13 -6
  39. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  40. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  41. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  42. package/dist/pipeline/flow-specs/plan.json +48 -70
  43. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  44. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  45. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  46. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  47. package/dist/pipeline/flow-specs/review/review.json +37 -31
  48. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  49. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  50. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  51. package/dist/pipeline/node-registry.js +41 -1
  52. package/dist/pipeline/node-runner.js +3 -2
  53. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  54. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  55. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  56. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  57. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  58. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  59. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  60. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  61. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  62. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  63. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  64. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  65. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  66. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  67. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  68. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  69. package/dist/pipeline/nodes/user-input-node.js +38 -3
  70. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  71. package/dist/pipeline/prompt-registry.js +3 -1
  72. package/dist/pipeline/prompt-runtime.js +4 -1
  73. package/dist/pipeline/review-iteration.js +26 -0
  74. package/dist/pipeline/spec-compiler.js +2 -0
  75. package/dist/pipeline/spec-types.js +3 -0
  76. package/dist/pipeline/spec-validator.js +14 -0
  77. package/dist/pipeline/value-resolver.js +74 -1
  78. package/dist/prompts.js +36 -14
  79. package/dist/review-severity.js +45 -0
  80. package/dist/runtime/artifact-registry.js +402 -0
  81. package/dist/runtime/design-review-input-contract.js +17 -16
  82. package/dist/runtime/env-loader.js +3 -0
  83. package/dist/runtime/execution-routing-store.js +134 -0
  84. package/dist/runtime/execution-routing.js +227 -0
  85. package/dist/runtime/interactive-execution-routing.js +462 -0
  86. package/dist/runtime/plan-revise-input-contract.js +35 -32
  87. package/dist/runtime/planning-bundle.js +123 -0
  88. package/dist/runtime/ready-to-merge.js +22 -1
  89. package/dist/runtime/review-input-contract.js +100 -0
  90. package/dist/structured-artifact-schema-registry.js +9 -0
  91. package/dist/structured-artifact-schemas.json +140 -1
  92. package/dist/structured-artifacts.js +77 -6
  93. package/dist/user-input.js +70 -3
  94. package/package.json +6 -3
@@ -0,0 +1,402 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { buildArtifactId, buildLogicalKeyForPayload, buildPublicationKey, createProducerSummary, diagnosticsForManifest, inferPayloadContract, parseArtifactReference, validateArtifactManifest, } from "../artifact-manifest.js";
5
+ import { artifactIndexFile, artifactManifestSidecarPath, ensureScopeWorkspaceDir, scopeArtifactsDir, scopeWorkspaceDir, } from "../artifacts.js";
6
+ import { TaskRunnerError } from "../errors.js";
7
+ import { isArtifactPayloadSchemaId, validateArtifactPayload, } from "../structured-artifacts.js";
8
+ function nowIso8601() {
9
+ return new Date().toISOString();
10
+ }
11
+ function historyDir(scopeKey) {
12
+ return path.join(scopeArtifactsDir(scopeKey), "manifest-history");
13
+ }
14
+ function historyManifestPath(scopeKey, artifactId) {
15
+ return path.join(historyDir(scopeKey), `${encodeURIComponent(artifactId)}.manifest.json`);
16
+ }
17
+ function scopeKeyFromPayloadPath(payloadPath) {
18
+ const scopeMarker = `${path.sep}.agentweaver${path.sep}scopes${path.sep}`;
19
+ const markerIndex = payloadPath.lastIndexOf(scopeMarker);
20
+ if (markerIndex < 0) {
21
+ return null;
22
+ }
23
+ const scopePart = payloadPath.slice(markerIndex + scopeMarker.length);
24
+ return scopePart.split(path.sep)[0] || null;
25
+ }
26
+ function writeJsonAtomic(filePath, value) {
27
+ mkdirSync(path.dirname(filePath), { recursive: true });
28
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
29
+ writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
30
+ renameSync(tempPath, filePath);
31
+ }
32
+ function computeContentHash(payloadPath) {
33
+ const hash = createHash("sha256");
34
+ hash.update(readFileSync(payloadPath));
35
+ return `sha256:${hash.digest("hex")}`;
36
+ }
37
+ function toIndexRecord(manifest) {
38
+ const producerSummary = manifest.producer.summary ?? createProducerSummary(manifest.producer);
39
+ return {
40
+ artifact_id: manifest.artifact_id,
41
+ logical_key: manifest.logical_key,
42
+ payload_path: manifest.payload_path,
43
+ manifest_path: manifest.manifest_path,
44
+ version: manifest.version,
45
+ status: manifest.status,
46
+ schema_id: manifest.schema_id,
47
+ schema_version: manifest.schema_version,
48
+ created_at: manifest.created_at,
49
+ content_hash: manifest.content_hash,
50
+ producer_summary: producerSummary,
51
+ ...(manifest.supersedes ? { supersedes: manifest.supersedes } : {}),
52
+ is_latest: manifest.status === "ready",
53
+ };
54
+ }
55
+ function tryLoadManifest(filePath) {
56
+ if (!existsSync(filePath)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
61
+ validateArtifactManifest(parsed, filePath);
62
+ return parsed;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ function collectManifestFiles(rootDir) {
69
+ if (!existsSync(rootDir)) {
70
+ return [];
71
+ }
72
+ const queue = [rootDir];
73
+ const files = [];
74
+ while (queue.length > 0) {
75
+ const current = queue.shift();
76
+ if (!current) {
77
+ continue;
78
+ }
79
+ const entries = readdirSync(current, { withFileTypes: true })
80
+ .sort((left, right) => left.name.localeCompare(right.name));
81
+ for (const entry of entries) {
82
+ const fullPath = path.join(current, entry.name);
83
+ if (entry.isDirectory()) {
84
+ queue.push(fullPath);
85
+ continue;
86
+ }
87
+ if (entry.isFile() && fullPath.endsWith(".manifest.json")) {
88
+ files.push(fullPath);
89
+ }
90
+ }
91
+ }
92
+ return files;
93
+ }
94
+ function collectScopeManifests(scopeKey) {
95
+ const allPaths = [
96
+ ...collectManifestFiles(scopeWorkspaceDir(scopeKey)),
97
+ ...collectManifestFiles(historyDir(scopeKey)),
98
+ ];
99
+ const manifests = new Map();
100
+ for (const filePath of allPaths) {
101
+ const manifest = tryLoadManifest(filePath);
102
+ if (!manifest) {
103
+ continue;
104
+ }
105
+ manifests.set(manifest.artifact_id, manifest);
106
+ }
107
+ return Array.from(manifests.values()).sort((left, right) => {
108
+ if (left.logical_key !== right.logical_key) {
109
+ return left.logical_key.localeCompare(right.logical_key);
110
+ }
111
+ return left.version - right.version;
112
+ });
113
+ }
114
+ function writeManifestSidecar(manifest) {
115
+ writeJsonAtomic(manifest.manifest_path, manifest);
116
+ }
117
+ function writeManifestHistory(scopeKey, manifest) {
118
+ writeJsonAtomic(historyManifestPath(scopeKey, manifest.artifact_id), manifest);
119
+ }
120
+ function isRegistryTempFile(scopeKey, filePath) {
121
+ const relativePath = path.relative(scopeWorkspaceDir(scopeKey), filePath);
122
+ if (relativePath.startsWith("..")) {
123
+ return false;
124
+ }
125
+ if (/\.manifest\.json\.tmp-[^/\\]+$/.test(relativePath)) {
126
+ return true;
127
+ }
128
+ return relativePath.startsWith(`${path.join(".artifacts", path.basename(artifactIndexFile(scopeKey)))}.tmp-`);
129
+ }
130
+ function removeStaleTempFiles(scopeKey) {
131
+ const workspaceDir = scopeWorkspaceDir(scopeKey);
132
+ if (!existsSync(workspaceDir)) {
133
+ return;
134
+ }
135
+ const queue = [workspaceDir];
136
+ while (queue.length > 0) {
137
+ const current = queue.shift();
138
+ if (!current) {
139
+ continue;
140
+ }
141
+ const entries = readdirSync(current, { withFileTypes: true })
142
+ .sort((left, right) => left.name.localeCompare(right.name));
143
+ for (const entry of entries) {
144
+ const fullPath = path.join(current, entry.name);
145
+ if (entry.isDirectory()) {
146
+ queue.push(fullPath);
147
+ continue;
148
+ }
149
+ if (entry.isFile() && isRegistryTempFile(scopeKey, fullPath)) {
150
+ rmSync(fullPath, { force: true });
151
+ }
152
+ }
153
+ }
154
+ }
155
+ function indexRecordEquals(left, right) {
156
+ return left.artifact_id === right.artifact_id
157
+ && left.logical_key === right.logical_key
158
+ && left.payload_path === right.payload_path
159
+ && left.manifest_path === right.manifest_path
160
+ && left.version === right.version
161
+ && left.status === right.status
162
+ && left.schema_id === right.schema_id
163
+ && left.schema_version === right.schema_version
164
+ && left.created_at === right.created_at
165
+ && left.content_hash === right.content_hash
166
+ && left.producer_summary === right.producer_summary
167
+ && left.supersedes === right.supersedes
168
+ && left.is_latest === right.is_latest;
169
+ }
170
+ function selectLatestReadyManifest(manifests) {
171
+ return manifests
172
+ .filter((manifest) => manifest.status === "ready")
173
+ .sort((left, right) => right.version - left.version)[0] ?? null;
174
+ }
175
+ function selectNewestManifest(manifests) {
176
+ return manifests
177
+ .slice()
178
+ .sort((left, right) => right.version - left.version)[0] ?? null;
179
+ }
180
+ function buildScopeRecords(scopeKey, computeDiagnostics) {
181
+ const manifests = collectScopeManifests(scopeKey).map((manifest) => ({
182
+ ...manifest,
183
+ diagnostics: computeDiagnostics(manifest),
184
+ }));
185
+ const latestByLogicalKey = new Map();
186
+ for (const manifest of manifests) {
187
+ if (manifest.status === "ready") {
188
+ latestByLogicalKey.set(manifest.logical_key, manifest.artifact_id);
189
+ }
190
+ }
191
+ return manifests.map((manifest) => ({
192
+ ...toIndexRecord(manifest),
193
+ is_latest: latestByLogicalKey.get(manifest.logical_key) === manifest.artifact_id,
194
+ manifest,
195
+ }));
196
+ }
197
+ function readIndexProjection(scopeKey) {
198
+ const indexPath = artifactIndexFile(scopeKey);
199
+ if (!existsSync(indexPath)) {
200
+ return null;
201
+ }
202
+ try {
203
+ const parsed = JSON.parse(readFileSync(indexPath, "utf8"));
204
+ return Array.isArray(parsed.records) ? parsed.records : null;
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ }
210
+ function syncIndexProjection(scopeKey, records) {
211
+ const projected = records.map(({ manifest: _manifest, ...record }) => record);
212
+ const current = readIndexProjection(scopeKey);
213
+ if (!current && projected.length === 0) {
214
+ return;
215
+ }
216
+ const needsRewrite = !current
217
+ || current.length !== projected.length
218
+ || current.some((record, index) => !indexRecordEquals(record, projected[index]));
219
+ if (!needsRewrite) {
220
+ return;
221
+ }
222
+ writeJsonAtomic(artifactIndexFile(scopeKey), {
223
+ scope: scopeKey,
224
+ generated_at: nowIso8601(),
225
+ records: projected,
226
+ });
227
+ }
228
+ export function createArtifactRegistry() {
229
+ return {
230
+ publish(input) {
231
+ ensureScopeWorkspaceDir(input.scopeKey);
232
+ removeStaleTempFiles(input.scopeKey);
233
+ const sidecarPath = artifactManifestSidecarPath(input.payloadPath);
234
+ const manifests = collectScopeManifests(input.scopeKey);
235
+ const logicalKey = input.logicalKey ?? buildLogicalKeyForPayload(input.scopeKey, input.payloadPath);
236
+ const publicationKey = buildPublicationKey({
237
+ runId: input.runId,
238
+ ...(input.publicationRunId ? { publicationRunId: input.publicationRunId } : {}),
239
+ flowId: input.flowId,
240
+ phaseId: input.phaseId,
241
+ stepId: input.stepId,
242
+ logicalKey,
243
+ });
244
+ const existing = manifests.find((candidate) => candidate.publication_key === publicationKey);
245
+ if (existing) {
246
+ if (existing.payload_path !== input.payloadPath) {
247
+ throw new TaskRunnerError(`Manifest publication key collision for ${publicationKey}: ${existing.payload_path} and ${input.payloadPath} resolved to the same logical_key within one step.`);
248
+ }
249
+ return {
250
+ ...toIndexRecord(existing),
251
+ manifest: existing,
252
+ };
253
+ }
254
+ const contract = inferPayloadContract(input.scopeKey, input.payloadPath, {
255
+ ...(input.payloadFamily ? { payloadFamily: input.payloadFamily } : {}),
256
+ ...(input.schemaId ? { schemaId: input.schemaId } : {}),
257
+ ...(input.schemaVersion ? { schemaVersion: input.schemaVersion } : {}),
258
+ });
259
+ const versions = manifests
260
+ .filter((candidate) => candidate.logical_key === logicalKey)
261
+ .sort((left, right) => left.version - right.version);
262
+ const previousLatest = [...versions].reverse().find((candidate) => candidate.status === "ready") ?? versions.at(-1) ?? null;
263
+ const version = (versions.at(-1)?.version ?? 0) + 1;
264
+ const artifactId = buildArtifactId(input.scopeKey, logicalKey, version);
265
+ const manifest = {
266
+ artifact_id: artifactId,
267
+ logical_key: logicalKey,
268
+ scope: input.scopeKey,
269
+ run_id: input.runId,
270
+ flow_id: input.flowId,
271
+ phase_id: input.phaseId,
272
+ step_id: input.stepId,
273
+ kind: input.kind,
274
+ version,
275
+ payload_family: contract.payloadFamily,
276
+ schema_id: contract.schemaId,
277
+ schema_version: contract.schemaVersion,
278
+ created_at: nowIso8601(),
279
+ producer: {
280
+ node: input.nodeKind,
281
+ summary: createProducerSummary({
282
+ node: input.nodeKind,
283
+ ...(input.executor ? { executor: input.executor } : {}),
284
+ ...(input.model ? { model: input.model } : {}),
285
+ }),
286
+ ...(input.executor ? { executor: input.executor } : {}),
287
+ ...(input.model ? { model: input.model } : {}),
288
+ },
289
+ inputs: input.inputs,
290
+ content_hash: computeContentHash(input.payloadPath),
291
+ status: "ready",
292
+ payload_path: input.payloadPath,
293
+ manifest_path: sidecarPath,
294
+ publication_key: publicationKey,
295
+ ...(previousLatest ? { supersedes: previousLatest.artifact_id } : {}),
296
+ };
297
+ validateArtifactManifest(manifest, sidecarPath);
298
+ writeManifestHistory(input.scopeKey, manifest);
299
+ writeManifestSidecar(manifest);
300
+ if (previousLatest) {
301
+ const superseded = {
302
+ ...previousLatest,
303
+ status: "superseded",
304
+ status_reason: `Superseded by ${artifactId}`,
305
+ };
306
+ writeManifestHistory(input.scopeKey, superseded);
307
+ if (previousLatest.manifest_path !== manifest.manifest_path) {
308
+ writeManifestSidecar(superseded);
309
+ }
310
+ }
311
+ this.rebuildIndex(input.scopeKey);
312
+ return {
313
+ ...toIndexRecord(manifest),
314
+ manifest,
315
+ };
316
+ },
317
+ resolveArtifact(scopeKey, reference) {
318
+ removeStaleTempFiles(scopeKey);
319
+ const parsedReference = parseArtifactReference(reference);
320
+ if (!parsedReference) {
321
+ throw new TaskRunnerError(`Artifact reference '${reference}' is invalid. Expected an artifact_id or a logical reference in the form <logical_key>@latest or <logical_key>@vN.`);
322
+ }
323
+ const records = buildScopeRecords(scopeKey, this.computeDiagnostics);
324
+ syncIndexProjection(scopeKey, records);
325
+ const manifests = records.map((record) => record.manifest);
326
+ if (parsedReference.kind === "artifact-id") {
327
+ if (parsedReference.parsedId.scopeKey !== scopeKey) {
328
+ throw new TaskRunnerError(`Artifact id '${reference}' belongs to scope '${parsedReference.parsedId.scopeKey}', expected '${scopeKey}'.`);
329
+ }
330
+ const manifest = manifests.find((candidate) => candidate.artifact_id === parsedReference.artifactId);
331
+ if (!manifest) {
332
+ throw new TaskRunnerError(`Artifact '${reference}' was not found in scope '${scopeKey}'.`);
333
+ }
334
+ return manifest;
335
+ }
336
+ const candidates = manifests.filter((candidate) => candidate.logical_key === parsedReference.logicalKey);
337
+ if (parsedReference.version === "latest") {
338
+ const manifest = selectLatestReadyManifest(candidates);
339
+ if (!manifest) {
340
+ throw new TaskRunnerError(`No ready artifact found for logical reference '${reference}' in scope '${scopeKey}'.`);
341
+ }
342
+ return manifest;
343
+ }
344
+ const manifest = candidates.find((candidate) => candidate.version === parsedReference.version);
345
+ if (!manifest) {
346
+ throw new TaskRunnerError(`Artifact reference '${reference}' was not found in scope '${scopeKey}'.`);
347
+ }
348
+ return manifest;
349
+ },
350
+ loadManifestByPayloadPath(payloadPath) {
351
+ const manifest = tryLoadManifest(artifactManifestSidecarPath(payloadPath));
352
+ const scopeKey = scopeKeyFromPayloadPath(payloadPath);
353
+ if (!scopeKey) {
354
+ return manifest;
355
+ }
356
+ removeStaleTempFiles(scopeKey);
357
+ const records = buildScopeRecords(scopeKey, this.computeDiagnostics);
358
+ syncIndexProjection(scopeKey, records);
359
+ const candidates = records
360
+ .map((record) => record.manifest)
361
+ .filter((candidate) => candidate.payload_path === payloadPath);
362
+ return selectLatestReadyManifest(candidates) ?? selectNewestManifest(candidates) ?? manifest;
363
+ },
364
+ listScopeArtifacts(scopeKey) {
365
+ removeStaleTempFiles(scopeKey);
366
+ const records = buildScopeRecords(scopeKey, this.computeDiagnostics);
367
+ syncIndexProjection(scopeKey, records);
368
+ return records;
369
+ },
370
+ rebuildIndex(scopeKey) {
371
+ removeStaleTempFiles(scopeKey);
372
+ const records = buildScopeRecords(scopeKey, this.computeDiagnostics);
373
+ syncIndexProjection(scopeKey, records);
374
+ return records;
375
+ },
376
+ resolveLineageInputFromPath(scopeKey, payloadPath) {
377
+ const manifest = this.loadManifestByPayloadPath(payloadPath);
378
+ if (!manifest) {
379
+ return {
380
+ source: "external-path",
381
+ path: payloadPath,
382
+ };
383
+ }
384
+ return {
385
+ source: "manifest",
386
+ path: payloadPath,
387
+ artifact_id: manifest.artifact_id,
388
+ logical_key: manifest.logical_key,
389
+ schema_id: manifest.schema_id,
390
+ schema_version: manifest.schema_version,
391
+ };
392
+ },
393
+ computeDiagnostics(manifest) {
394
+ return diagnosticsForManifest(manifest, (schemaId, payloadPath) => {
395
+ if (!isArtifactPayloadSchemaId(schemaId)) {
396
+ throw new Error(`Structured artifact schema is not registered: ${schemaId}`);
397
+ }
398
+ validateArtifactPayload(payloadPath, schemaId);
399
+ });
400
+ },
401
+ };
402
+ }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
- import { designFile, designJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, requireArtifacts, } from "../artifacts.js";
3
- import { TaskRunnerError } from "../errors.js";
2
+ import { designFile, designJsonFile, instantTaskInputJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, taskContextJsonFile, requireArtifacts, } from "../artifacts.js";
4
3
  import { validateStructuredArtifacts } from "../structured-artifacts.js";
4
+ import { resolveLatestCompletedPlanningIteration } from "./planning-bundle.js";
5
5
  const OPTIONAL_INPUT_NOT_PROVIDED = "not provided";
6
6
  function requiredPlanningArtifactPaths(taskKey, iteration) {
7
7
  return {
@@ -11,19 +11,6 @@ function requiredPlanningArtifactPaths(taskKey, iteration) {
11
11
  planJsonFile: planJsonFile(taskKey, iteration),
12
12
  };
13
13
  }
14
- function resolveLatestCompletedPlanningIteration(taskKey) {
15
- const latestKnownIteration = Math.max(latestArtifactIteration(taskKey, "design", "md") ?? 0, latestArtifactIteration(taskKey, "design", "json") ?? 0, latestArtifactIteration(taskKey, "plan", "md") ?? 0, latestArtifactIteration(taskKey, "plan", "json") ?? 0);
16
- for (let iteration = latestKnownIteration; iteration >= 1; iteration -= 1) {
17
- const requiredPaths = Object.values(requiredPlanningArtifactPaths(taskKey, iteration));
18
- if (requiredPaths.every((candidate) => existsSync(candidate))) {
19
- return iteration;
20
- }
21
- }
22
- const fallbackIteration = latestKnownIteration || 1;
23
- const fallbackPaths = Object.values(requiredPlanningArtifactPaths(taskKey, fallbackIteration));
24
- requireArtifacts(fallbackPaths, "Design-review requires design and plan markdown/JSON artifacts from the latest completed planning run.");
25
- throw new TaskRunnerError("Unreachable design-review planning artifact resolution state.");
26
- }
27
14
  function resolveOptionalPromptFile(filePath) {
28
15
  if (!existsSync(filePath)) {
29
16
  return {
@@ -79,7 +66,10 @@ function resolveOptionalQaPair(taskKey, iteration) {
79
66
  * deterministic when some context is absent.
80
67
  */
81
68
  export function resolveDesignReviewInputContract(taskKey) {
82
- const planningIteration = resolveLatestCompletedPlanningIteration(taskKey);
69
+ const planningIteration = resolveLatestCompletedPlanningIteration(taskKey, {
70
+ requireQa: false,
71
+ missingMessage: "Design-review requires design and plan markdown/JSON artifacts from the latest completed planning run.",
72
+ });
83
73
  const requiredArtifacts = requiredPlanningArtifactPaths(taskKey, planningIteration);
84
74
  requireArtifacts(Object.values(requiredArtifacts), "Design-review requires design and plan markdown/JSON artifacts from the latest completed planning run.");
85
75
  validateStructuredArtifacts([
@@ -91,6 +81,11 @@ export function resolveDesignReviewInputContract(taskKey) {
91
81
  const jiraAttachmentsManifest = resolveOptionalPromptFile(jiraAttachmentsManifestFile(taskKey));
92
82
  const jiraAttachmentsContext = resolveOptionalPromptFile(jiraAttachmentsContextFile(taskKey));
93
83
  const planningAnswers = resolveOptionalValidatedStructuredFile(planningAnswersJsonFile(taskKey), "user-input/v1", "Design-review planning answers structured artifact is invalid.");
84
+ const taskContextIteration = latestArtifactIteration(taskKey, "task-context", "json");
85
+ const taskContext = taskContextIteration === null
86
+ ? { present: false, path: null, promptValue: OPTIONAL_INPUT_NOT_PROVIDED }
87
+ : resolveOptionalValidatedStructuredFile(taskContextJsonFile(taskKey, taskContextIteration), "task-context/v1", "Design-review task-context structured artifact is invalid.");
88
+ const taskInput = resolveOptionalValidatedStructuredFile(instantTaskInputJsonFile(taskKey), "user-input/v1", "Design-review instant-task input structured artifact is invalid.");
94
89
  return {
95
90
  planningIteration,
96
91
  ...requiredArtifacts,
@@ -107,6 +102,12 @@ export function resolveDesignReviewInputContract(taskKey) {
107
102
  hasPlanningAnswersJsonFile: planningAnswers.present,
108
103
  planningAnswersJsonFilePath: planningAnswers.path,
109
104
  planningAnswersJsonFile: planningAnswers.promptValue,
105
+ hasTaskContextJsonFile: taskContext.present,
106
+ taskContextJsonFilePath: taskContext.path,
107
+ taskContextJsonFile: taskContext.promptValue,
108
+ hasTaskInputJsonFile: taskInput.present,
109
+ taskInputJsonFilePath: taskInput.path,
110
+ taskInputJsonFile: taskInput.promptValue,
110
111
  };
111
112
  }
112
113
  export { OPTIONAL_INPUT_NOT_PROVIDED };
@@ -35,6 +35,9 @@ function globalConfigDir() {
35
35
  function ensureGlobalConfigDir() {
36
36
  mkdirSync(globalConfigDir(), { recursive: true });
37
37
  }
38
+ export function agentweaverConfigDir() {
39
+ return globalConfigDir();
40
+ }
38
41
  export function loadTieredEnv(projectDir) {
39
42
  ensureGlobalConfigDir();
40
43
  const shellEnvKeys = new Set(Object.keys(process.env));
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { TaskRunnerError } from "../errors.js";
4
+ import { resolveStoredExecutionRoutingSnapshot } from "./execution-routing.js";
5
+ import { agentweaverConfigDir } from "./env-loader.js";
6
+ const EXECUTION_ROUTING_STORE_VERSION = 1;
7
+ function storePath() {
8
+ return path.join(agentweaverConfigDir(), "execution-routing.json");
9
+ }
10
+ function nowIso8601() {
11
+ return new Date().toISOString();
12
+ }
13
+ function emptyStore() {
14
+ return {
15
+ version: EXECUTION_ROUTING_STORE_VERSION,
16
+ namedPresets: {},
17
+ flowDefaults: {},
18
+ lastUsedByFlow: {},
19
+ };
20
+ }
21
+ function validateSelectedPreset(value, pathLabel) {
22
+ if (!value || typeof value !== "object") {
23
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.`);
24
+ }
25
+ const candidate = value;
26
+ if (typeof candidate.kind !== "string" || typeof candidate.label !== "string") {
27
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.`);
28
+ }
29
+ if (candidate.kind === "built-in" || candidate.kind === "named") {
30
+ if (typeof candidate.presetId !== "string" || candidate.presetId.trim().length === 0) {
31
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.presetId.`);
32
+ }
33
+ return {
34
+ kind: candidate.kind,
35
+ presetId: candidate.presetId,
36
+ label: candidate.label,
37
+ };
38
+ }
39
+ if (candidate.kind === "flow-default" || candidate.kind === "last-used" || candidate.kind === "custom") {
40
+ return {
41
+ kind: candidate.kind,
42
+ label: candidate.label,
43
+ };
44
+ }
45
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.kind.`);
46
+ }
47
+ function validateRoutingEntry(value, pathLabel) {
48
+ if (!value || typeof value !== "object") {
49
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.`);
50
+ }
51
+ const candidate = value;
52
+ if (typeof candidate.updatedAt !== "string") {
53
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.updatedAt.`);
54
+ }
55
+ return {
56
+ routing: resolveStoredExecutionRoutingSnapshot(candidate.routing),
57
+ selectedPreset: validateSelectedPreset(candidate.selectedPreset, pathLabel),
58
+ updatedAt: candidate.updatedAt,
59
+ };
60
+ }
61
+ function validateEntryMap(value, pathLabel) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
63
+ throw new TaskRunnerError(`Invalid execution routing store section '${pathLabel}'.`);
64
+ }
65
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, validateRoutingEntry(entry, `${pathLabel}.${key}`)]));
66
+ }
67
+ function validateStore(raw) {
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
69
+ throw new TaskRunnerError("Execution routing store must be a JSON object.");
70
+ }
71
+ const candidate = raw;
72
+ if (candidate.version !== EXECUTION_ROUTING_STORE_VERSION) {
73
+ throw new TaskRunnerError(`Unsupported execution routing store version: ${String(candidate.version ?? "unknown")}.`);
74
+ }
75
+ return {
76
+ version: EXECUTION_ROUTING_STORE_VERSION,
77
+ namedPresets: validateEntryMap(candidate.namedPresets ?? {}, "namedPresets"),
78
+ flowDefaults: validateEntryMap(candidate.flowDefaults ?? {}, "flowDefaults"),
79
+ lastUsedByFlow: validateEntryMap(candidate.lastUsedByFlow ?? {}, "lastUsedByFlow"),
80
+ };
81
+ }
82
+ export function loadExecutionRoutingStore() {
83
+ const filePath = storePath();
84
+ if (!existsSync(filePath)) {
85
+ return emptyStore();
86
+ }
87
+ try {
88
+ return validateStore(JSON.parse(readFileSync(filePath, "utf8")));
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ throw new TaskRunnerError(`Failed to load execution routing store ${filePath}: ${message}. Delete or repair the file and try again.`);
93
+ }
94
+ }
95
+ export function saveExecutionRoutingStore(store) {
96
+ const filePath = storePath();
97
+ mkdirSync(path.dirname(filePath), { recursive: true });
98
+ const tempFilePath = `${filePath}.tmp`;
99
+ writeFileSync(`${tempFilePath}`, `${JSON.stringify(store, null, 2)}\n`, "utf8");
100
+ renameSync(tempFilePath, filePath);
101
+ }
102
+ function withUpdatedAt(entry) {
103
+ return {
104
+ ...entry,
105
+ updatedAt: nowIso8601(),
106
+ };
107
+ }
108
+ export function saveNamedExecutionPreset(name, routing, selectedPreset) {
109
+ const store = loadExecutionRoutingStore();
110
+ store.namedPresets[name] = withUpdatedAt({ routing, selectedPreset });
111
+ saveExecutionRoutingStore(store);
112
+ }
113
+ export function saveFlowDefaultExecutionRouting(flowKey, routing, selectedPreset) {
114
+ const store = loadExecutionRoutingStore();
115
+ store.flowDefaults[flowKey] = withUpdatedAt({ routing, selectedPreset });
116
+ saveExecutionRoutingStore(store);
117
+ }
118
+ export function saveLastUsedExecutionRouting(flowKey, routing, selectedPreset) {
119
+ const store = loadExecutionRoutingStore();
120
+ store.lastUsedByFlow[flowKey] = withUpdatedAt({ routing, selectedPreset });
121
+ saveExecutionRoutingStore(store);
122
+ }
123
+ export function getNamedExecutionPresets() {
124
+ return loadExecutionRoutingStore().namedPresets;
125
+ }
126
+ export function getFlowDefaultExecutionRouting(flowKey) {
127
+ return loadExecutionRoutingStore().flowDefaults[flowKey] ?? null;
128
+ }
129
+ export function getLastUsedExecutionRouting(flowKey) {
130
+ return loadExecutionRoutingStore().lastUsedByFlow[flowKey] ?? null;
131
+ }
132
+ export function executionRoutingStoreFile() {
133
+ return storePath();
134
+ }