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
@@ -1,9 +1,17 @@
1
1
  import { existsSync } from "node:fs";
2
- import { designFile, designJsonFile, designReviewFile, designReviewJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, requireArtifacts, } from "../artifacts.js";
2
+ import { designFile, designJsonFile, designReviewFile, designReviewJsonFile, instantTaskInputJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, taskContextJsonFile, requireArtifacts, } from "../artifacts.js";
3
3
  import { TaskRunnerError } from "../errors.js";
4
4
  import { validateStructuredArtifacts } from "../structured-artifacts.js";
5
+ import { resolveLatestCompletedPlanningIteration } from "./planning-bundle.js";
5
6
  const OPTIONAL_INPUT_NOT_PROVIDED = "not provided";
6
- function resolveLatestDesignReviewIteration(taskKey) {
7
+ function resolveLatestDesignReviewIteration(taskKey, sourcePlanningIteration) {
8
+ if (sourcePlanningIteration !== undefined) {
9
+ const candidateMd = designReviewFile(taskKey, sourcePlanningIteration);
10
+ const candidateJson = designReviewJsonFile(taskKey, sourcePlanningIteration);
11
+ if (existsSync(candidateMd) && existsSync(candidateJson)) {
12
+ return sourcePlanningIteration;
13
+ }
14
+ }
7
15
  const latestMd = latestArtifactIteration(taskKey, "design-review", "md");
8
16
  const latestJson = latestArtifactIteration(taskKey, "design-review", "json");
9
17
  const maxIteration = Math.max(latestMd ?? 0, latestJson ?? 0);
@@ -22,33 +30,6 @@ function resolveLatestDesignReviewIteration(taskKey) {
22
30
  requireArtifacts([fallbackMd, fallbackJson], "Plan-revise requires design-review markdown and JSON artifacts from the latest completed design-review run.");
23
31
  throw new TaskRunnerError("Unreachable plan-revise design-review artifact resolution state.");
24
32
  }
25
- function resolveLatestCompletedPlanningIteration(taskKey) {
26
- const latestDesignMd = latestArtifactIteration(taskKey, "design", "md") ?? 0;
27
- const latestDesignJson = latestArtifactIteration(taskKey, "design", "json") ?? 0;
28
- const latestPlanMd = latestArtifactIteration(taskKey, "plan", "md") ?? 0;
29
- const latestPlanJson = latestArtifactIteration(taskKey, "plan", "json") ?? 0;
30
- const maxIteration = Math.max(latestDesignMd, latestDesignJson, latestPlanMd, latestPlanJson);
31
- for (let iteration = maxIteration; iteration >= 1; iteration -= 1) {
32
- const paths = [
33
- designFile(taskKey, iteration),
34
- designJsonFile(taskKey, iteration),
35
- planFile(taskKey, iteration),
36
- planJsonFile(taskKey, iteration),
37
- ];
38
- if (paths.every((candidate) => existsSync(candidate))) {
39
- return iteration;
40
- }
41
- }
42
- const fallbackIteration = maxIteration || 1;
43
- const fallbackPaths = [
44
- designFile(taskKey, fallbackIteration),
45
- designJsonFile(taskKey, fallbackIteration),
46
- planFile(taskKey, fallbackIteration),
47
- planJsonFile(taskKey, fallbackIteration),
48
- ];
49
- requireArtifacts(fallbackPaths, "Plan-revise requires design and plan markdown/JSON artifacts from the latest completed planning run.");
50
- throw new TaskRunnerError("Unreachable plan-revise planning artifact resolution state.");
51
- }
52
33
  function resolveOptionalPromptFile(filePath) {
53
34
  if (!existsSync(filePath)) {
54
35
  return {
@@ -63,6 +44,14 @@ function resolveOptionalPromptFile(filePath) {
63
44
  promptValue: filePath,
64
45
  };
65
46
  }
47
+ function resolveOptionalValidatedStructuredFile(filePath, schemaId, message) {
48
+ const resolved = resolveOptionalPromptFile(filePath);
49
+ if (!resolved.present) {
50
+ return resolved;
51
+ }
52
+ validateStructuredArtifacts([{ path: filePath, schemaId }], message);
53
+ return resolved;
54
+ }
66
55
  function resolveOptionalQaPair(taskKey, iteration) {
67
56
  const markdownPath = qaFile(taskKey, iteration);
68
57
  const jsonPath = qaJsonFile(taskKey, iteration);
@@ -90,12 +79,15 @@ function resolveOptionalQaPair(taskKey, iteration) {
90
79
  };
91
80
  }
92
81
  export function resolvePlanReviseInputContract(taskKey) {
93
- const reviewIteration = resolveLatestDesignReviewIteration(taskKey);
82
+ const sourcePlanningIteration = resolveLatestCompletedPlanningIteration(taskKey, {
83
+ requireQa: false,
84
+ missingMessage: "Plan-revise requires design and plan markdown/JSON artifacts from the latest completed planning run.",
85
+ });
86
+ const reviewIteration = resolveLatestDesignReviewIteration(taskKey, sourcePlanningIteration);
94
87
  const reviewMd = designReviewFile(taskKey, reviewIteration);
95
88
  const reviewJson = designReviewJsonFile(taskKey, reviewIteration);
96
89
  requireArtifacts([reviewMd, reviewJson], "Plan-revise requires design-review markdown and JSON artifacts.");
97
90
  validateStructuredArtifacts([{ path: reviewJson, schemaId: "design-review/v1" }], "Plan-revise design-review structured artifact is invalid.");
98
- const sourcePlanningIteration = resolveLatestCompletedPlanningIteration(taskKey);
99
91
  const srcDesignMd = designFile(taskKey, sourcePlanningIteration);
100
92
  const srcDesignJson = designJsonFile(taskKey, sourcePlanningIteration);
101
93
  const srcPlanMd = planFile(taskKey, sourcePlanningIteration);
@@ -110,7 +102,12 @@ export function resolvePlanReviseInputContract(taskKey) {
110
102
  const jiraTask = resolveOptionalPromptFile(jiraTaskFile(taskKey));
111
103
  const jiraAttachmentsManifest = resolveOptionalPromptFile(jiraAttachmentsManifestFile(taskKey));
112
104
  const jiraAttachmentsContext = resolveOptionalPromptFile(jiraAttachmentsContextFile(taskKey));
113
- const planningAnswers = resolveOptionalPromptFile(planningAnswersJsonFile(taskKey));
105
+ const planningAnswers = resolveOptionalValidatedStructuredFile(planningAnswersJsonFile(taskKey), "user-input/v1", "Plan-revise planning answers structured artifact is invalid.");
106
+ const taskContextIteration = latestArtifactIteration(taskKey, "task-context", "json");
107
+ const taskContext = taskContextIteration === null
108
+ ? { present: false, path: null, promptValue: OPTIONAL_INPUT_NOT_PROVIDED }
109
+ : resolveOptionalValidatedStructuredFile(taskContextJsonFile(taskKey, taskContextIteration), "task-context/v1", "Plan-revise task-context structured artifact is invalid.");
110
+ const taskInput = resolveOptionalValidatedStructuredFile(instantTaskInputJsonFile(taskKey), "user-input/v1", "Plan-revise instant-task input structured artifact is invalid.");
114
111
  return {
115
112
  reviewIteration,
116
113
  reviewFile: reviewMd,
@@ -140,5 +137,11 @@ export function resolvePlanReviseInputContract(taskKey) {
140
137
  hasPlanningAnswersJsonFile: planningAnswers.present,
141
138
  planningAnswersJsonFilePath: planningAnswers.path,
142
139
  planningAnswersJsonFile: planningAnswers.promptValue,
140
+ hasTaskContextJsonFile: taskContext.present,
141
+ taskContextJsonFilePath: taskContext.path,
142
+ taskContextJsonFile: taskContext.promptValue,
143
+ hasTaskInputJsonFile: taskInput.present,
144
+ taskInputJsonFilePath: taskInput.path,
145
+ taskInputJsonFile: taskInput.promptValue,
143
146
  };
144
147
  }
@@ -0,0 +1,123 @@
1
+ import { existsSync } from "node:fs";
2
+ import { designFile, designJsonFile, latestArtifactIteration, planFile, planJsonFile, qaFile, qaJsonFile, } from "../artifacts.js";
3
+ import { TaskRunnerError } from "../errors.js";
4
+ import { validateStructuredArtifacts } from "../structured-artifacts.js";
5
+ function planningPrefixes(options) {
6
+ return options.requireQa ? ["design", "plan", "qa"] : ["design", "plan"];
7
+ }
8
+ function maxPlanningIteration(taskKey, options) {
9
+ let maxIteration = null;
10
+ for (const prefix of planningPrefixes(options)) {
11
+ for (const extension of ["md", "json"]) {
12
+ const iteration = latestArtifactIteration(taskKey, prefix, extension);
13
+ if (iteration === null) {
14
+ continue;
15
+ }
16
+ maxIteration = maxIteration === null ? iteration : Math.max(maxIteration, iteration);
17
+ }
18
+ }
19
+ return maxIteration;
20
+ }
21
+ export function planningBundlePaths(taskKey, iteration) {
22
+ return {
23
+ designFile: designFile(taskKey, iteration),
24
+ designJsonFile: designJsonFile(taskKey, iteration),
25
+ planFile: planFile(taskKey, iteration),
26
+ planJsonFile: planJsonFile(taskKey, iteration),
27
+ qaFile: qaFile(taskKey, iteration),
28
+ qaJsonFile: qaJsonFile(taskKey, iteration),
29
+ };
30
+ }
31
+ function requiredPlanningPaths(bundle, options) {
32
+ const required = [
33
+ bundle.designFile,
34
+ bundle.designJsonFile,
35
+ bundle.planFile,
36
+ bundle.planJsonFile,
37
+ ];
38
+ if (options.requireQa) {
39
+ required.push(bundle.qaFile, bundle.qaJsonFile);
40
+ }
41
+ return required;
42
+ }
43
+ export function findLatestCompletedPlanningIteration(taskKey, options) {
44
+ const latestKnownIteration = maxPlanningIteration(taskKey, options);
45
+ if (latestKnownIteration === null) {
46
+ return null;
47
+ }
48
+ for (let iteration = latestKnownIteration; iteration >= 1; iteration -= 1) {
49
+ const bundle = planningBundlePaths(taskKey, iteration);
50
+ const requiredPaths = requiredPlanningPaths(bundle, options);
51
+ if (requiredPaths.every((candidate) => existsSync(candidate))) {
52
+ return iteration;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ export function resolveLatestCompletedPlanningIteration(taskKey, options) {
58
+ const resolvedIteration = findLatestCompletedPlanningIteration(taskKey, options);
59
+ if (resolvedIteration !== null) {
60
+ return resolvedIteration;
61
+ }
62
+ const fallbackIteration = maxPlanningIteration(taskKey, options) ?? 1;
63
+ const fallbackBundle = planningBundlePaths(taskKey, fallbackIteration);
64
+ const requiredPaths = requiredPlanningPaths(fallbackBundle, options);
65
+ const missing = requiredPaths.filter((candidate) => !existsSync(candidate));
66
+ throw new TaskRunnerError(`${options.missingMessage}\nMissing files: ${missing.join(", ")}`);
67
+ }
68
+ export function inspectLatestPlanningBundle(taskKey) {
69
+ const latestKnownIteration = maxPlanningIteration(taskKey, { requireQa: true });
70
+ if (latestKnownIteration === null) {
71
+ return {
72
+ status: "missing",
73
+ planningIteration: null,
74
+ missingFiles: [],
75
+ bundle: null,
76
+ errorMessage: "Implement mode requires planning artifacts from the planning phase, but none were found.",
77
+ };
78
+ }
79
+ const bundle = planningBundlePaths(taskKey, latestKnownIteration);
80
+ const missingFiles = requiredPlanningPaths(bundle, { requireQa: true }).filter((candidate) => !existsSync(candidate));
81
+ if (missingFiles.length > 0) {
82
+ return {
83
+ status: "incomplete",
84
+ planningIteration: latestKnownIteration,
85
+ missingFiles,
86
+ bundle,
87
+ errorMessage: `Implement mode requires a complete planning bundle for iteration ${latestKnownIteration}.` +
88
+ `\nMissing files: ${missingFiles.join(", ")}`,
89
+ };
90
+ }
91
+ try {
92
+ validateStructuredArtifacts([
93
+ { path: bundle.designJsonFile, schemaId: "implementation-design/v1" },
94
+ { path: bundle.planJsonFile, schemaId: "implementation-plan/v1" },
95
+ { path: bundle.qaJsonFile, schemaId: "qa-plan/v1" },
96
+ ], `Implement mode requires a valid structured planning bundle for iteration ${latestKnownIteration}.`);
97
+ }
98
+ catch (error) {
99
+ return {
100
+ status: "invalid",
101
+ planningIteration: latestKnownIteration,
102
+ missingFiles: [],
103
+ bundle,
104
+ errorMessage: error.message,
105
+ };
106
+ }
107
+ return {
108
+ status: "valid",
109
+ planningIteration: latestKnownIteration,
110
+ missingFiles: [],
111
+ bundle,
112
+ };
113
+ }
114
+ export function resolveLatestPlanningBundle(taskKey) {
115
+ const inspection = inspectLatestPlanningBundle(taskKey);
116
+ if (inspection.status !== "valid") {
117
+ throw new TaskRunnerError(inspection.errorMessage);
118
+ }
119
+ return {
120
+ planningIteration: inspection.planningIteration,
121
+ ...inspection.bundle,
122
+ };
123
+ }
@@ -1,4 +1,5 @@
1
- import { existsSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
2
3
  import { readyToMergeFile } from "../artifacts.js";
3
4
  export function clearReadyToMergeFile(taskKey) {
4
5
  const filePath = readyToMergeFile(taskKey);
@@ -8,3 +9,23 @@ export function clearReadyToMergeFile(taskKey) {
8
9
  rmSync(filePath);
9
10
  return true;
10
11
  }
12
+ export function writeReadyToMergeFile(taskKey, options = {}) {
13
+ const filePath = readyToMergeFile(taskKey);
14
+ mkdirSync(path.dirname(filePath), { recursive: true });
15
+ const summary = options.summary?.trim();
16
+ const lines = options.mdLang === "ru"
17
+ ? [
18
+ "# Готово к слиянию",
19
+ "",
20
+ "Блокирующих замечаний не найдено по текущему порогу severity.",
21
+ ...(summary ? ["", "## Summary", "", summary] : []),
22
+ ]
23
+ : [
24
+ "# Ready to Merge",
25
+ "",
26
+ "No blocking findings were detected at the configured severity threshold.",
27
+ ...(summary ? ["", "## Summary", "", summary] : []),
28
+ ];
29
+ writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
30
+ return filePath;
31
+ }
@@ -0,0 +1,100 @@
1
+ import { existsSync } from "node:fs";
2
+ import { designFile, designJsonFile, instantTaskInputJsonFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, requireArtifacts, taskContextJsonFile, } from "../artifacts.js";
3
+ import { TaskRunnerError } from "../errors.js";
4
+ import { validateStructuredArtifacts } from "../structured-artifacts.js";
5
+ import { resolveLatestCompletedPlanningIteration } from "./planning-bundle.js";
6
+ const OPTIONAL_INPUT_NOT_PROVIDED = "not provided";
7
+ function resolveOptionalPromptFile(filePath) {
8
+ if (!existsSync(filePath)) {
9
+ return {
10
+ present: false,
11
+ path: null,
12
+ promptValue: OPTIONAL_INPUT_NOT_PROVIDED,
13
+ };
14
+ }
15
+ return {
16
+ present: true,
17
+ path: filePath,
18
+ promptValue: filePath,
19
+ };
20
+ }
21
+ function resolveOptionalValidatedStructuredFile(filePath, schemaId, message) {
22
+ const resolved = resolveOptionalPromptFile(filePath);
23
+ if (!resolved.present) {
24
+ return resolved;
25
+ }
26
+ validateStructuredArtifacts([{ path: filePath, schemaId }], message);
27
+ return resolved;
28
+ }
29
+ export function inspectReviewInputContract(taskKey) {
30
+ let planningIteration;
31
+ try {
32
+ planningIteration = resolveLatestCompletedPlanningIteration(taskKey, {
33
+ requireQa: false,
34
+ missingMessage: "Structured review requires design and plan markdown/JSON artifacts from the latest completed planning run.",
35
+ });
36
+ }
37
+ catch (error) {
38
+ if (error instanceof TaskRunnerError) {
39
+ return { status: "missing-planning" };
40
+ }
41
+ throw error;
42
+ }
43
+ const contract = {
44
+ planningIteration,
45
+ designFile: designFile(taskKey, planningIteration),
46
+ designJsonFile: designJsonFile(taskKey, planningIteration),
47
+ planFile: planFile(taskKey, planningIteration),
48
+ planJsonFile: planJsonFile(taskKey, planningIteration),
49
+ hasTaskContextJsonFile: false,
50
+ taskContextJsonFilePath: null,
51
+ taskContextJsonFile: OPTIONAL_INPUT_NOT_PROVIDED,
52
+ hasJiraTaskFile: false,
53
+ jiraTaskFilePath: null,
54
+ jiraTaskFile: OPTIONAL_INPUT_NOT_PROVIDED,
55
+ hasTaskInputJsonFile: false,
56
+ taskInputJsonFilePath: null,
57
+ taskInputJsonFile: OPTIONAL_INPUT_NOT_PROVIDED,
58
+ };
59
+ requireArtifacts([contract.designFile, contract.designJsonFile, contract.planFile, contract.planJsonFile], "Structured review requires design and plan markdown/JSON artifacts from the latest completed planning run.");
60
+ validateStructuredArtifacts([
61
+ { path: contract.designJsonFile, schemaId: "implementation-design/v1" },
62
+ { path: contract.planJsonFile, schemaId: "implementation-plan/v1" },
63
+ ], "Structured review requires valid design and plan structured artifacts.");
64
+ const taskContextIteration = latestArtifactIteration(taskKey, "task-context", "json");
65
+ const taskContext = taskContextIteration === null
66
+ ? { present: false, path: null, promptValue: OPTIONAL_INPUT_NOT_PROVIDED }
67
+ : resolveOptionalValidatedStructuredFile(taskContextJsonFile(taskKey, taskContextIteration), "task-context/v1", "Structured review task-context structured artifact is invalid.");
68
+ const jiraTask = resolveOptionalPromptFile(jiraTaskFile(taskKey));
69
+ const taskInput = resolveOptionalValidatedStructuredFile(instantTaskInputJsonFile(taskKey), "user-input/v1", "Structured review instant-task input structured artifact is invalid.");
70
+ if (!taskContext.present && !jiraTask.present && !taskInput.present) {
71
+ return {
72
+ status: "missing-task-context",
73
+ planningIteration,
74
+ };
75
+ }
76
+ contract.hasTaskContextJsonFile = taskContext.present;
77
+ contract.taskContextJsonFilePath = taskContext.path;
78
+ contract.taskContextJsonFile = taskContext.promptValue;
79
+ contract.hasJiraTaskFile = jiraTask.present;
80
+ contract.jiraTaskFilePath = jiraTask.path;
81
+ contract.jiraTaskFile = jiraTask.promptValue;
82
+ contract.hasTaskInputJsonFile = taskInput.present;
83
+ contract.taskInputJsonFilePath = taskInput.path;
84
+ contract.taskInputJsonFile = taskInput.promptValue;
85
+ return {
86
+ status: "ready",
87
+ contract,
88
+ };
89
+ }
90
+ export function resolveReviewInputContract(taskKey) {
91
+ const inspection = inspectReviewInputContract(taskKey);
92
+ if (inspection.status === "ready") {
93
+ return inspection.contract;
94
+ }
95
+ if (inspection.status === "missing-planning") {
96
+ throw new TaskRunnerError("Structured review requires design and plan markdown/JSON artifacts from the latest completed planning run.");
97
+ }
98
+ throw new TaskRunnerError(`Structured review requires a normalized task-context artifact, or legacy Jira/instant-task context, in scope '${taskKey}'.`);
99
+ }
100
+ export { OPTIONAL_INPUT_NOT_PROVIDED };
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { TaskRunnerError } from "./errors.js";
5
5
  export const STRUCTURED_ARTIFACT_SCHEMA_IDS = [
6
+ "artifact-manifest/v1",
6
7
  "bug-analysis/v1",
7
8
  "bug-fix-design/v1",
8
9
  "bug-fix-plan/v1",
@@ -18,9 +19,17 @@ export const STRUCTURED_ARTIFACT_SCHEMA_IDS = [
18
19
  "review-assessment/v1",
19
20
  "review-findings/v1",
20
21
  "review-fix-report/v1",
22
+ "task-context/v1",
21
23
  "task-summary/v1",
22
24
  "user-input/v1",
23
25
  ];
26
+ export const ARTIFACT_PAYLOAD_SCHEMA_IDS = [
27
+ ...STRUCTURED_ARTIFACT_SCHEMA_IDS,
28
+ "helper-json/v1",
29
+ "markdown/v1",
30
+ "opaque-file/v1",
31
+ "plain-text/v1",
32
+ ];
24
33
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
25
34
  export const SCHEMA_REGISTRY_PATH = path.join(MODULE_DIR, "structured-artifact-schemas.json");
26
35
  function isRecord(value) {
@@ -612,7 +612,7 @@
612
612
  "items": {
613
613
  "type": "object",
614
614
  "properties": {
615
- "severity": { "type": "string", "nonEmpty": true },
615
+ "severity": { "type": "string", "enum": ["blocker", "critical", "high", "medium", "low", "info"] },
616
616
  "title": { "type": "string", "nonEmpty": true },
617
617
  "description": { "type": "string", "nonEmpty": true }
618
618
  },
@@ -660,6 +660,145 @@
660
660
  },
661
661
  "required": ["summary", "completed_actions", "validation_steps"]
662
662
  },
663
+ "helper-json/v1": {
664
+ "type": "json"
665
+ },
666
+ "markdown/v1": {
667
+ "type": "string",
668
+ "nonEmpty": true
669
+ },
670
+ "opaque-file/v1": {
671
+ "type": "bytes"
672
+ },
673
+ "plain-text/v1": {
674
+ "type": "string",
675
+ "nonEmpty": true
676
+ },
677
+ "artifact-manifest/v1": {
678
+ "type": "object",
679
+ "properties": {
680
+ "artifact_id": { "type": "string", "nonEmpty": true },
681
+ "logical_key": { "type": "string", "nonEmpty": true, "pattern": "^[a-z0-9][a-z0-9._/-]*$" },
682
+ "scope": { "type": "string", "nonEmpty": true },
683
+ "run_id": { "type": "string", "nonEmpty": true },
684
+ "flow_id": { "type": "string", "nonEmpty": true },
685
+ "phase_id": { "type": "string", "nonEmpty": true },
686
+ "step_id": { "type": "string", "nonEmpty": true },
687
+ "kind": { "type": "string", "enum": ["artifact", "file"] },
688
+ "version": { "type": "number" },
689
+ "payload_family": { "type": "string", "enum": ["structured-json", "markdown", "plain-text", "helper-json", "opaque-file"] },
690
+ "schema_id": { "type": "string", "nonEmpty": true },
691
+ "schema_version": { "type": "number" },
692
+ "created_at": { "type": "string", "nonEmpty": true },
693
+ "producer": {
694
+ "type": "object",
695
+ "properties": {
696
+ "node": { "type": "string", "nonEmpty": true },
697
+ "executor": { "type": "string", "nonEmpty": true },
698
+ "model": { "type": "string", "nonEmpty": true },
699
+ "summary": { "type": "string", "nonEmpty": true }
700
+ },
701
+ "required": ["node"]
702
+ },
703
+ "inputs": {
704
+ "type": "array",
705
+ "items": {
706
+ "type": "object",
707
+ "properties": {
708
+ "source": { "type": "string", "enum": ["manifest", "external-path"] },
709
+ "path": { "type": "string", "nonEmpty": true },
710
+ "artifact_id": { "type": "string", "nonEmpty": true },
711
+ "logical_key": { "type": "string", "nonEmpty": true, "pattern": "^[a-z0-9][a-z0-9._/-]*$" },
712
+ "schema_id": { "type": "string", "nonEmpty": true },
713
+ "schema_version": { "type": "number" }
714
+ },
715
+ "required": ["source", "path"]
716
+ }
717
+ },
718
+ "content_hash": { "type": "string", "nonEmpty": true, "pattern": "^sha256:[a-f0-9]{64}$" },
719
+ "status": { "type": "string", "enum": ["ready", "superseded", "stale"] },
720
+ "payload_path": { "type": "string", "nonEmpty": true },
721
+ "manifest_path": { "type": "string", "nonEmpty": true },
722
+ "publication_key": { "type": "string", "nonEmpty": true },
723
+ "supersedes": { "type": "string", "nonEmpty": true },
724
+ "status_reason": { "type": "string", "nonEmpty": true },
725
+ "diagnostics": {
726
+ "type": "array",
727
+ "items": {
728
+ "type": "object",
729
+ "properties": {
730
+ "code": { "type": "string", "nonEmpty": true },
731
+ "severity": { "type": "string", "enum": ["info", "warning", "error"] },
732
+ "message": { "type": "string", "nonEmpty": true }
733
+ },
734
+ "required": ["code", "severity", "message"]
735
+ }
736
+ }
737
+ },
738
+ "required": [
739
+ "artifact_id",
740
+ "logical_key",
741
+ "scope",
742
+ "run_id",
743
+ "flow_id",
744
+ "phase_id",
745
+ "step_id",
746
+ "kind",
747
+ "version",
748
+ "payload_family",
749
+ "schema_id",
750
+ "schema_version",
751
+ "created_at",
752
+ "producer",
753
+ "inputs",
754
+ "content_hash",
755
+ "status",
756
+ "payload_path",
757
+ "manifest_path",
758
+ "publication_key"
759
+ ]
760
+ },
761
+ "task-context/v1": {
762
+ "type": "object",
763
+ "properties": {
764
+ "source_type": {
765
+ "type": "string",
766
+ "nonEmpty": true,
767
+ "enum": ["jira", "manual"]
768
+ },
769
+ "title": { "type": "string", "nonEmpty": true },
770
+ "summary": { "type": "string", "nonEmpty": true },
771
+ "problem": { "type": "string", "nonEmpty": true },
772
+ "goal": { "type": "string", "nonEmpty": true },
773
+ "acceptance_criteria": {
774
+ "type": "array",
775
+ "items": { "type": "string", "nonEmpty": true }
776
+ },
777
+ "constraints": {
778
+ "type": "array",
779
+ "items": { "type": "string", "nonEmpty": true }
780
+ },
781
+ "references": {
782
+ "type": "array",
783
+ "items": { "type": "string", "nonEmpty": true }
784
+ },
785
+ "notes": {
786
+ "type": "array",
787
+ "items": { "type": "string", "nonEmpty": true }
788
+ }
789
+ },
790
+ "required": [
791
+ "source_type",
792
+ "title",
793
+ "summary",
794
+ "problem",
795
+ "goal",
796
+ "acceptance_criteria",
797
+ "constraints",
798
+ "references",
799
+ "notes"
800
+ ]
801
+ },
663
802
  "task-summary/v1": {
664
803
  "type": "object",
665
804
  "properties": {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { TaskRunnerError } from "./errors.js";
3
- import { STRUCTURED_ARTIFACT_SCHEMA_IDS, getStructuredArtifactSchema, } from "./structured-artifact-schema-registry.js";
4
- export { STRUCTURED_ARTIFACT_SCHEMA_IDS };
3
+ import { ARTIFACT_PAYLOAD_SCHEMA_IDS, STRUCTURED_ARTIFACT_SCHEMA_IDS, getStructuredArtifactSchema, } from "./structured-artifact-schema-registry.js";
4
+ export { ARTIFACT_PAYLOAD_SCHEMA_IDS, STRUCTURED_ARTIFACT_SCHEMA_IDS };
5
5
  function isRecord(value) {
6
6
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
7
  }
@@ -14,9 +14,16 @@ function schemaLabel(node) {
14
14
  if (node.enum && node.enum.length > 0) {
15
15
  return `one of: ${node.enum.join(", ")}`;
16
16
  }
17
+ if (node.pattern) {
18
+ return "a string with the expected format";
19
+ }
17
20
  return node.nonEmpty ? "a non-empty string" : "a string";
18
21
  case "boolean":
19
22
  return "a boolean";
23
+ case "bytes":
24
+ return "a binary file";
25
+ case "json":
26
+ return "valid JSON";
20
27
  case "number":
21
28
  return "a number";
22
29
  case "null":
@@ -49,9 +56,24 @@ function validateNode(value, schema, currentPath) {
49
56
  if (schema.enum && !schema.enum.includes(value)) {
50
57
  return [`${currentPath} must be ${schemaLabel(schema)}`];
51
58
  }
59
+ if (schema.pattern) {
60
+ try {
61
+ const pattern = new RegExp(schema.pattern);
62
+ if (!pattern.test(value)) {
63
+ return [`${currentPath} must be ${schemaLabel(schema)}`];
64
+ }
65
+ }
66
+ catch {
67
+ return [`${currentPath} uses an invalid schema pattern`];
68
+ }
69
+ }
52
70
  return [];
53
71
  case "boolean":
54
72
  return typeof value === "boolean" ? [] : [`${currentPath} must be a boolean`];
73
+ case "bytes":
74
+ return value instanceof Uint8Array ? [] : [`${currentPath} must be a binary file`];
75
+ case "json":
76
+ return validateJsonValue(value, currentPath);
55
77
  case "number":
56
78
  return typeof value === "number" && !Number.isNaN(value) ? [] : [`${currentPath} must be a number`];
57
79
  case "null":
@@ -85,6 +107,28 @@ function validateNode(value, schema, currentPath) {
85
107
  }
86
108
  }
87
109
  }
110
+ function validateJsonValue(value, currentPath) {
111
+ if (value === null ||
112
+ typeof value === "string" ||
113
+ typeof value === "boolean" ||
114
+ (typeof value === "number" && Number.isFinite(value))) {
115
+ return [];
116
+ }
117
+ if (Array.isArray(value)) {
118
+ return value.flatMap((item, index) => validateJsonValue(item, `${currentPath}[${index}]`));
119
+ }
120
+ if (!isRecord(value)) {
121
+ return [`${currentPath} must be valid JSON`];
122
+ }
123
+ return Object.entries(value).flatMap(([key, candidate]) => validateJsonValue(candidate, `${currentPath}.${key}`));
124
+ }
125
+ function validateArtifactPayloadValue(value, schemaId, label = "$") {
126
+ const schema = getStructuredArtifactSchema(schemaId);
127
+ const issues = validateNode(value, schema, label);
128
+ if (issues.length > 0) {
129
+ throw new TaskRunnerError(`Structured artifact ${label} failed schema ${schemaId} validation:\n${issues.join("\n")}`);
130
+ }
131
+ }
88
132
  export function validateStructuredArtifact(path, schemaId) {
89
133
  if (!existsSync(path)) {
90
134
  throw new TaskRunnerError(`Structured artifact file not found: ${path}`);
@@ -96,11 +140,38 @@ export function validateStructuredArtifact(path, schemaId) {
96
140
  catch (error) {
97
141
  throw new TaskRunnerError(`Structured artifact ${path} is not valid JSON: ${error.message}`);
98
142
  }
99
- const schema = getStructuredArtifactSchema(schemaId);
100
- const issues = validateNode(parsed, schema, path);
101
- if (issues.length > 0) {
102
- throw new TaskRunnerError(`Structured artifact ${path} failed schema ${schemaId} validation:\n${issues.join("\n")}`);
143
+ validateArtifactPayloadValue(parsed, schemaId, path);
144
+ }
145
+ export function validateStructuredArtifactValue(value, schemaId, label = "$") {
146
+ validateArtifactPayloadValue(value, schemaId, label);
147
+ }
148
+ export function isArtifactPayloadSchemaId(schemaId) {
149
+ return ARTIFACT_PAYLOAD_SCHEMA_IDS.includes(schemaId);
150
+ }
151
+ export function validateArtifactPayload(path, schemaId) {
152
+ if (!existsSync(path)) {
153
+ throw new TaskRunnerError(`Structured artifact file not found: ${path}`);
154
+ }
155
+ if (schemaId === "markdown/v1" || schemaId === "plain-text/v1") {
156
+ validateArtifactPayloadValue(readFileSync(path, "utf8"), schemaId, path);
157
+ return;
158
+ }
159
+ if (schemaId === "helper-json/v1") {
160
+ let parsed;
161
+ try {
162
+ parsed = JSON.parse(readFileSync(path, "utf8"));
163
+ }
164
+ catch (error) {
165
+ throw new TaskRunnerError(`Structured artifact ${path} is not valid JSON: ${error.message}`);
166
+ }
167
+ validateArtifactPayloadValue(parsed, schemaId, path);
168
+ return;
169
+ }
170
+ if (schemaId === "opaque-file/v1") {
171
+ validateArtifactPayloadValue(readFileSync(path), schemaId, path);
172
+ return;
103
173
  }
174
+ validateStructuredArtifact(path, schemaId);
104
175
  }
105
176
  export function validateStructuredArtifacts(items, message) {
106
177
  try {