agentweaver 0.1.14 → 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 (100) hide show
  1. package/README.md +29 -7
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +21 -1
  4. package/dist/doctor/checks/cwd-context.js +4 -3
  5. package/dist/doctor/checks/env-diagnostics.js +193 -71
  6. package/dist/doctor/checks/flow-readiness.js +212 -203
  7. package/dist/doctor/index.js +1 -1
  8. package/dist/doctor/orchestrator.js +18 -7
  9. package/dist/doctor/runner.js +9 -8
  10. package/dist/doctor/types.js +12 -0
  11. package/dist/flow-state.js +75 -15
  12. package/dist/index.js +499 -199
  13. package/dist/interactive/blessed-session.js +361 -0
  14. package/dist/interactive/controller.js +1293 -0
  15. package/dist/interactive/create-interactive-session.js +5 -0
  16. package/dist/interactive/ink/index.js +576 -0
  17. package/dist/interactive/progress.js +245 -0
  18. package/dist/interactive/selectors.js +14 -0
  19. package/dist/interactive/session.js +1 -0
  20. package/dist/interactive/state.js +34 -0
  21. package/dist/interactive/tree.js +155 -0
  22. package/dist/interactive/types.js +1 -0
  23. package/dist/interactive/view-model.js +1 -0
  24. package/dist/interactive-ui.js +159 -194
  25. package/dist/pipeline/context.js +1 -0
  26. package/dist/pipeline/declarative-flow-runner.js +212 -6
  27. package/dist/pipeline/declarative-flows.js +27 -0
  28. package/dist/pipeline/execution-routing-config.js +15 -0
  29. package/dist/pipeline/flow-catalog.js +23 -3
  30. package/dist/pipeline/flow-run-resume.js +29 -0
  31. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  32. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  33. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  34. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  35. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  36. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  37. package/dist/pipeline/flow-specs/design-review.json +249 -0
  38. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  39. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  40. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  41. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  42. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  43. package/dist/pipeline/flow-specs/implement.json +24 -5
  44. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  45. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  46. package/dist/pipeline/flow-specs/plan-revise.json +267 -0
  47. package/dist/pipeline/flow-specs/plan.json +48 -70
  48. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  49. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  50. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  51. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  52. package/dist/pipeline/flow-specs/review/review.json +37 -31
  53. package/dist/pipeline/flow-specs/task-describe.json +62 -2
  54. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  55. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  56. package/dist/pipeline/node-registry.js +49 -1
  57. package/dist/pipeline/node-runner.js +3 -2
  58. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  59. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  60. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  61. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  62. package/dist/pipeline/nodes/ensure-summary-json-node.js +70 -0
  63. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  64. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  65. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  66. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  67. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  68. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  69. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  70. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  71. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  72. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  73. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  74. package/dist/pipeline/nodes/user-input-node.js +38 -3
  75. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  76. package/dist/pipeline/prompt-registry.js +5 -1
  77. package/dist/pipeline/prompt-runtime.js +4 -1
  78. package/dist/pipeline/review-iteration.js +26 -0
  79. package/dist/pipeline/spec-compiler.js +2 -0
  80. package/dist/pipeline/spec-types.js +5 -0
  81. package/dist/pipeline/spec-validator.js +14 -0
  82. package/dist/pipeline/value-resolver.js +84 -1
  83. package/dist/prompts.js +82 -13
  84. package/dist/review-severity.js +45 -0
  85. package/dist/runtime/artifact-registry.js +402 -0
  86. package/dist/runtime/design-review-input-contract.js +113 -0
  87. package/dist/runtime/env-loader.js +3 -0
  88. package/dist/runtime/execution-routing-store.js +134 -0
  89. package/dist/runtime/execution-routing.js +227 -0
  90. package/dist/runtime/interactive-execution-routing.js +462 -0
  91. package/dist/runtime/plan-revise-input-contract.js +147 -0
  92. package/dist/runtime/planning-bundle.js +123 -0
  93. package/dist/runtime/ready-to-merge.js +31 -0
  94. package/dist/runtime/review-input-contract.js +100 -0
  95. package/dist/scope.js +11 -2
  96. package/dist/structured-artifact-schema-registry.js +10 -0
  97. package/dist/structured-artifact-schemas.json +257 -1
  98. package/dist/structured-artifacts.js +83 -6
  99. package/dist/user-input.js +70 -3
  100. package/package.json +6 -3
@@ -1,12 +1,54 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { DoctorStatus } from "../types.js";
3
+ import { DoctorImpact, DoctorStatus, WorkflowContinuityState } from "../types.js";
4
4
  import { CATEGORY } from "./category.js";
5
5
  import { BUILT_IN_COMMAND_FLOW_IDS } from "../../pipeline/flow-catalog.js";
6
6
  import { validateStructuredArtifact } from "../../structured-artifacts.js";
7
- import { designJsonFile, planJsonFile, scopeArtifactsDir } from "../../artifacts.js";
7
+ import { designJsonFile, latestArtifactIteration, planJsonFile, taskContextJsonFile, } from "../../artifacts.js";
8
+ import { inspectLatestPlanningBundle } from "../../runtime/planning-bundle.js";
8
9
  const GO_BINARY_PATTERNS = ["go", "go.exe"];
9
10
  const GIT_BINARY_PATTERNS = ["git", "git.exe"];
11
+ function statePriority(state) {
12
+ switch (state) {
13
+ case WorkflowContinuityState.InvalidState:
14
+ return 3;
15
+ case WorkflowContinuityState.NotConfigured:
16
+ return 2;
17
+ case WorkflowContinuityState.NeedsPreviousStage:
18
+ return 1;
19
+ case WorkflowContinuityState.Available:
20
+ default:
21
+ return 0;
22
+ }
23
+ }
24
+ function formatState(state) {
25
+ switch (state) {
26
+ case WorkflowContinuityState.Available:
27
+ return "available";
28
+ case WorkflowContinuityState.NeedsPreviousStage:
29
+ return "requires previous stage outputs";
30
+ case WorkflowContinuityState.NotConfigured:
31
+ return "not configured";
32
+ case WorkflowContinuityState.InvalidState:
33
+ return "invalid state";
34
+ default:
35
+ return state;
36
+ }
37
+ }
38
+ function setEntryState(entry, state, reason) {
39
+ if (statePriority(state) > statePriority(entry.state)) {
40
+ entry.state = state;
41
+ }
42
+ entry.reasons.push(reason);
43
+ }
44
+ function createEntry(flowId, mode) {
45
+ return {
46
+ flowId,
47
+ ...(mode ? { mode } : {}),
48
+ state: WorkflowContinuityState.Available,
49
+ reasons: [],
50
+ };
51
+ }
10
52
  function findBinary(name) {
11
53
  const envPath = process.env.PATH ?? "";
12
54
  const pathDirs = envPath.split(path.delimiter);
@@ -20,233 +62,146 @@ function findBinary(name) {
20
62
  }
21
63
  return null;
22
64
  }
23
- function checkBinaryPresence(flowId) {
24
- const goFlows = new Set([
25
- "run-go-tests-loop",
26
- "run-go-linter-loop",
27
- "auto-golang",
28
- ]);
29
- const gitFlows = new Set(["git-commit", "gitlab-review", "gitlab-diff-review", "mr-description"]);
30
- if (goFlows.has(flowId)) {
31
- return findBinary("go");
32
- }
33
- if (gitFlows.has(flowId)) {
34
- return findBinary("git");
35
- }
36
- return null;
37
- }
38
65
  function isJiraConfigured() {
39
66
  return !!(process.env.JIRA_API_KEY && process.env.JIRA_BASE_URL);
40
67
  }
41
68
  function isGitLabConfigured() {
42
69
  return !!process.env.GITLAB_TOKEN;
43
70
  }
44
- function getLatestDesignIteration(scopeKey) {
45
- const artifactsDir = scopeArtifactsDir(scopeKey);
46
- if (!existsSync(artifactsDir)) {
47
- return null;
48
- }
49
- const files = readdirSync(artifactsDir);
50
- const designFiles = files.filter((f) => /^design-.*-\d+\.json$/.test(f));
51
- if (designFiles.length === 0) {
52
- return null;
53
- }
54
- const iterations = designFiles.map((f) => {
55
- const match = f.match(/^design-.*-(\d+)\.json$/);
56
- return match?.[1] ? parseInt(match[1], 10) : 0;
57
- });
58
- return Math.max(...iterations);
59
- }
60
- function getLatestPlanIteration(scopeKey) {
61
- const artifactsDir = scopeArtifactsDir(scopeKey);
62
- if (!existsSync(artifactsDir)) {
63
- return null;
64
- }
65
- const files = readdirSync(artifactsDir);
66
- const planFiles = files.filter((f) => /^plan-.*-\d+\.json$/.test(f));
67
- if (planFiles.length === 0) {
68
- return null;
69
- }
70
- const iterations = planFiles.map((f) => {
71
- const match = f.match(/^plan-.*-(\d+)\.json$/);
72
- return match?.[1] ? parseInt(match[1], 10) : 0;
73
- });
74
- return Math.max(...iterations);
71
+ function getLatestJsonArtifactIteration(scopeKey, prefix) {
72
+ return latestArtifactIteration(scopeKey, prefix, "json");
75
73
  }
76
- function checkImplementFlowReadiness(scopeKey) {
77
- const blockers = [];
78
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
79
- const latestPlanIteration = getLatestPlanIteration(scopeKey);
80
- if (latestDesignIteration === null) {
81
- blockers.push("design artifact not found");
74
+ function checkStructuredArtifactFile(entry, artifactPath, schemaId, missingReason, invalidReasonPrefix) {
75
+ if (!existsSync(artifactPath)) {
76
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, missingReason);
77
+ return;
82
78
  }
83
- else {
84
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
85
- if (!existsSync(designPath)) {
86
- blockers.push(`design artifact not found at ${designPath}`);
87
- }
88
- else {
89
- try {
90
- validateStructuredArtifact(designPath, "implementation-design/v1");
91
- }
92
- catch (error) {
93
- blockers.push(`design artifact schema invalid: ${error.message}`);
94
- }
95
- }
96
- }
97
- if (latestPlanIteration === null) {
98
- blockers.push("plan artifact not found");
79
+ try {
80
+ validateStructuredArtifact(artifactPath, schemaId);
99
81
  }
100
- else {
101
- const planPath = planJsonFile(scopeKey, latestPlanIteration);
102
- if (!existsSync(planPath)) {
103
- blockers.push(`plan artifact not found at ${planPath}`);
104
- }
105
- else {
106
- try {
107
- validateStructuredArtifact(planPath, "implementation-plan/v1");
108
- }
109
- catch (error) {
110
- blockers.push(`plan artifact schema invalid: ${error.message}`);
111
- }
112
- }
82
+ catch (error) {
83
+ setEntryState(entry, WorkflowContinuityState.InvalidState, `${invalidReasonPrefix}: ${error.message}`);
113
84
  }
114
- return {
115
- flowId: "implement",
116
- status: blockers.length === 0 ? "ready" : "not_ready",
117
- blockers,
118
- };
119
85
  }
120
- function checkReviewProjectFlowReadiness(scopeKey) {
121
- const blockers = [];
122
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
123
- if (latestDesignIteration === null) {
124
- blockers.push("design artifact not found");
86
+ function checkImplementWorkflowContinuity(scopeKey) {
87
+ const entry = createEntry("implement");
88
+ const planningBundle = inspectLatestPlanningBundle(scopeKey);
89
+ if (planningBundle.status === "missing") {
90
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, planningBundle.errorMessage);
125
91
  }
126
- else {
127
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
128
- if (!existsSync(designPath)) {
129
- blockers.push(`design artifact not found at ${designPath}`);
130
- }
92
+ else if (planningBundle.status === "incomplete") {
93
+ setEntryState(entry, WorkflowContinuityState.InvalidState, planningBundle.errorMessage);
131
94
  }
132
- return {
133
- flowId: "review",
134
- mode: "project",
135
- status: blockers.length === 0 ? "ready" : "not_ready",
136
- blockers,
137
- };
95
+ else if (planningBundle.status === "invalid") {
96
+ setEntryState(entry, WorkflowContinuityState.InvalidState, planningBundle.errorMessage);
97
+ }
98
+ if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
99
+ entry.nextStep = "Run plan to generate design, plan, and QA artifacts for this scope.";
100
+ }
101
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
102
+ entry.nextStep = "Regenerate the planning artifacts for this scope before running implement.";
103
+ }
104
+ return entry;
138
105
  }
139
- function checkReviewJiraFlowReadiness(scopeKey) {
140
- const blockers = [];
106
+ function checkReviewProjectWorkflowContinuity() {
107
+ return createEntry("review", "project");
108
+ }
109
+ function checkReviewJiraWorkflowContinuity(scopeKey) {
110
+ const entry = createEntry("review", "jira");
141
111
  if (!isJiraConfigured()) {
142
- blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
112
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Jira integration is not configured (JIRA_API_KEY or JIRA_BASE_URL missing).");
143
113
  }
144
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
114
+ const latestDesignIteration = getLatestJsonArtifactIteration(scopeKey, "design");
115
+ const latestPlanIteration = getLatestJsonArtifactIteration(scopeKey, "plan");
145
116
  if (latestDesignIteration === null) {
146
- blockers.push("design artifact not found");
117
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing design artifact from the planning stage.");
147
118
  }
148
119
  else {
149
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
150
- if (!existsSync(designPath)) {
151
- blockers.push(`design artifact not found at ${designPath}`);
152
- }
120
+ checkStructuredArtifactFile(entry, designJsonFile(scopeKey, latestDesignIteration), "implementation-design/v1", "Missing design artifact from the planning stage.", "Design artifact schema is invalid");
153
121
  }
154
- const latestPlanIteration = getLatestPlanIteration(scopeKey);
155
122
  if (latestPlanIteration === null) {
156
- blockers.push("plan artifact not found");
123
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing plan artifact from the planning stage.");
157
124
  }
158
125
  else {
159
- const planPath = planJsonFile(scopeKey, latestPlanIteration);
160
- if (!existsSync(planPath)) {
161
- blockers.push(`plan artifact not found at ${planPath}`);
162
- }
126
+ checkStructuredArtifactFile(entry, planJsonFile(scopeKey, latestPlanIteration), "implementation-plan/v1", "Missing plan artifact from the planning stage.", "Plan artifact schema is invalid");
163
127
  }
164
- return {
165
- flowId: "review",
166
- mode: "jira",
167
- status: blockers.length === 0 ? "ready" : "not_ready",
168
- blockers,
169
- };
128
+ if (entry.state === WorkflowContinuityState.NotConfigured) {
129
+ entry.nextStep = "Configure Jira access and ensure the planning artifacts exist before running review:jira.";
130
+ }
131
+ else if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
132
+ entry.nextStep = "Run plan first to generate the design and plan artifacts required by review:jira.";
133
+ }
134
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
135
+ entry.nextStep = "Regenerate invalid planning artifacts before running review:jira.";
136
+ }
137
+ return entry;
170
138
  }
171
- function checkPlanJiraFlowReadiness(scopeKey) {
172
- const blockers = [];
139
+ function checkPlanJiraWorkflowContinuity(scopeKey) {
140
+ const entry = createEntry("plan", "jira");
173
141
  if (!isJiraConfigured()) {
174
- blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
142
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Jira integration is not configured (JIRA_API_KEY or JIRA_BASE_URL missing).");
175
143
  }
176
144
  if (!process.env.JIRA_ISSUE_KEY && !scopeKey.includes("@")) {
177
- blockers.push("Jira issue key not available");
145
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "No Jira issue key is available for the current scope.");
178
146
  }
179
- return {
180
- flowId: "plan",
181
- mode: "jira",
182
- status: blockers.length === 0 ? "ready" : "not_ready",
183
- blockers,
184
- };
147
+ if (entry.state === WorkflowContinuityState.NotConfigured) {
148
+ entry.nextStep = "Set Jira environment variables and provide a Jira issue key or browse URL before running plan:jira.";
149
+ }
150
+ return entry;
185
151
  }
186
- function checkPlanProjectFlowReadiness(scopeKey) {
187
- const blockers = [];
188
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
189
- if (latestDesignIteration === null) {
190
- blockers.push("design artifact not found (task-describe output required)");
152
+ function checkPlanProjectWorkflowContinuity(scopeKey) {
153
+ const entry = createEntry("plan", "project");
154
+ const latestTaskContextIteration = getLatestJsonArtifactIteration(scopeKey, "task-context");
155
+ if (latestTaskContextIteration === null) {
156
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing normalized task-context artifact.");
191
157
  }
192
158
  else {
193
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
194
- if (!existsSync(designPath)) {
195
- blockers.push("design artifact not found");
196
- }
159
+ checkStructuredArtifactFile(entry, taskContextJsonFile(scopeKey, latestTaskContextIteration), "task-context/v1", "Missing normalized task-context artifact.", "Task-context artifact schema is invalid");
197
160
  }
198
- return {
199
- flowId: "plan",
200
- mode: "project",
201
- status: blockers.length === 0 ? "ready" : "not_ready",
202
- blockers,
203
- };
161
+ if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
162
+ entry.nextStep = "Run a source flow that produces task-context for this scope before running plan:project.";
163
+ }
164
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
165
+ entry.nextStep = "Regenerate the task-context artifact before running plan:project.";
166
+ }
167
+ return entry;
204
168
  }
205
- function checkGenericFlowReadiness(flowId, scopeKey) {
206
- const blockers = [];
207
- const binaryPath = checkBinaryPresence(flowId);
208
- if (binaryPath === null) {
209
- const requiredBinaries = {
210
- "run-go-tests-loop": "go",
211
- "run-go-linter-loop": "go",
212
- "auto-golang": "go",
213
- "git_commit": "git",
214
- "gitlab-review": "git",
215
- "gitlab-diff-review": "git",
216
- "mr-description": "git",
217
- };
218
- const required = requiredBinaries[flowId];
219
- if (required) {
220
- blockers.push(`${required} binary not found in PATH`);
221
- }
169
+ function checkGenericWorkflowContinuity(flowId) {
170
+ const entry = createEntry(flowId);
171
+ const goFlows = new Set(["run-go-tests-loop", "run-go-linter-loop", "auto-golang"]);
172
+ const gitFlows = new Set(["git-commit", "gitlab-review", "gitlab-diff-review", "mr-description"]);
173
+ const gitLabFlows = new Set(["gitlab-review", "gitlab-diff-review", "mr-description"]);
174
+ if (goFlows.has(flowId) && findBinary("go") === null) {
175
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Go binary is not available in PATH.");
176
+ entry.nextStep = "Install Go and ensure the go binary is available in PATH.";
222
177
  }
223
- const jiraFlows = new Set(["gitlab-review", "gitlab-diff-review", "mr-description"]);
224
- if (jiraFlows.has(flowId) && isGitLabConfigured()) {
225
- // GitLab configured - could add connectivity check here
178
+ if (gitFlows.has(flowId) && findBinary("git") === null) {
179
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Git binary is not available in PATH.");
180
+ entry.nextStep = "Install Git and ensure the git binary is available in PATH.";
226
181
  }
227
- return {
228
- flowId,
229
- status: blockers.length === 0 ? "ready" : "not_ready",
230
- blockers,
231
- };
182
+ if (gitLabFlows.has(flowId) && !isGitLabConfigured()) {
183
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "GitLab integration is not configured (GITLAB_TOKEN missing).");
184
+ entry.nextStep = "Set GITLAB_TOKEN before running this GitLab-backed flow.";
185
+ }
186
+ return entry;
232
187
  }
233
- function determineFlowReadiness(flowId, scopeKey) {
188
+ function determineWorkflowContinuity(flowId, scopeKey) {
234
189
  if (flowId === "implement") {
235
- return [checkImplementFlowReadiness(scopeKey)];
190
+ return [checkImplementWorkflowContinuity(scopeKey)];
236
191
  }
237
192
  if (flowId === "review") {
238
193
  return [
239
- checkReviewProjectFlowReadiness(scopeKey),
240
- checkReviewJiraFlowReadiness(scopeKey),
194
+ checkReviewProjectWorkflowContinuity(),
195
+ checkReviewJiraWorkflowContinuity(scopeKey),
241
196
  ];
242
197
  }
243
198
  if (flowId === "plan") {
244
199
  return [
245
- checkPlanJiraFlowReadiness(scopeKey),
246
- checkPlanProjectFlowReadiness(scopeKey),
200
+ checkPlanJiraWorkflowContinuity(scopeKey),
201
+ checkPlanProjectWorkflowContinuity(scopeKey),
247
202
  ];
248
203
  }
249
- return [checkGenericFlowReadiness(flowId, scopeKey)];
204
+ return [checkGenericWorkflowContinuity(flowId)];
250
205
  }
251
206
  function getScopeKey() {
252
207
  const cwd = process.cwd();
@@ -260,46 +215,100 @@ function getScopeKey() {
260
215
  }
261
216
  return entries[0];
262
217
  }
218
+ function buildSummary(entries) {
219
+ return {
220
+ available: entries.filter((entry) => entry.state === WorkflowContinuityState.Available).length,
221
+ needsPreviousStage: entries.filter((entry) => entry.state === WorkflowContinuityState.NeedsPreviousStage).length,
222
+ notConfigured: entries.filter((entry) => entry.state === WorkflowContinuityState.NotConfigured).length,
223
+ invalidState: entries.filter((entry) => entry.state === WorkflowContinuityState.InvalidState).length,
224
+ };
225
+ }
226
+ function buildMessage(summary) {
227
+ const parts = [`${summary.available} flows available`];
228
+ if (summary.needsPreviousStage > 0) {
229
+ parts.push(`${summary.needsPreviousStage} require earlier stage outputs`);
230
+ }
231
+ if (summary.notConfigured > 0) {
232
+ parts.push(`${summary.notConfigured} not configured`);
233
+ }
234
+ if (summary.invalidState > 0) {
235
+ parts.push(`${summary.invalidState} in invalid state`);
236
+ }
237
+ return parts.join(", ");
238
+ }
239
+ function buildDetails(scopeKey, entries) {
240
+ const lines = [`scope: ${scopeKey}`];
241
+ for (const entry of entries) {
242
+ const modeSuffix = entry.mode ? `:${entry.mode}` : "";
243
+ lines.push(` ${entry.flowId}${modeSuffix}: ${formatState(entry.state)}`);
244
+ for (const reason of entry.reasons) {
245
+ lines.push(` - ${reason}`);
246
+ }
247
+ if (entry.nextStep) {
248
+ lines.push(` next: ${entry.nextStep}`);
249
+ }
250
+ }
251
+ return lines.join("\n");
252
+ }
263
253
  function performFlowReadinessCheck() {
264
254
  const scopeKey = getScopeKey();
265
- const allEntries = [];
255
+ const entries = [];
266
256
  for (const flowId of BUILT_IN_COMMAND_FLOW_IDS) {
267
- const entries = determineFlowReadiness(flowId, scopeKey);
268
- allEntries.push(...entries);
269
- }
270
- const readyCount = allEntries.filter((e) => e.status === "ready").length;
271
- const notReadyCount = allEntries.filter((e) => e.status === "not_ready").length;
272
- const notConfiguredCount = allEntries.filter((e) => e.status === "not_configured").length;
273
- const lines = [];
274
- for (const entry of allEntries) {
275
- const modeStr = entry.mode ? `:${entry.mode}` : "";
276
- const statusStr = entry.status === "ready" ? "✓ ready" : entry.status === "not_configured" ? "⚠ not configured" : "✗ not ready";
277
- lines.push(` ${entry.flowId}${modeStr}: ${statusStr}`);
278
- for (const blocker of entry.blockers) {
279
- lines.push(` - ${blocker}`);
280
- }
257
+ entries.push(...determineWorkflowContinuity(flowId, scopeKey));
258
+ }
259
+ const summary = buildSummary(entries);
260
+ const message = buildMessage(summary);
261
+ const details = buildDetails(scopeKey, entries);
262
+ const data = {
263
+ kind: "workflow-continuity",
264
+ scopeKey,
265
+ summary,
266
+ entries,
267
+ };
268
+ if (summary.invalidState > 0) {
269
+ return {
270
+ status: DoctorStatus.Warn,
271
+ impact: DoctorImpact.Blocking,
272
+ message,
273
+ hint: "Current scope contains invalid workflow artifacts. Regenerate or clean them up before continuing those flows.",
274
+ details,
275
+ data,
276
+ };
277
+ }
278
+ if (summary.needsPreviousStage > 0 || summary.notConfigured > 0) {
279
+ return {
280
+ status: DoctorStatus.Warn,
281
+ impact: DoctorImpact.Advisory,
282
+ message,
283
+ hint: "These continuity findings describe what can be continued in the current scope and do not affect overall application readiness.",
284
+ details,
285
+ data,
286
+ };
281
287
  }
282
- const overallStatus = notReadyCount > 0 ? DoctorStatus.Fail : notConfiguredCount > 0 ? DoctorStatus.Warn : DoctorStatus.Ok;
283
- const message = `${readyCount} flows ready, ${notReadyCount} not ready, ${notConfiguredCount} not configured`;
284
288
  return {
285
- status: overallStatus,
289
+ status: DoctorStatus.Ok,
290
+ impact: DoctorImpact.Advisory,
286
291
  message,
287
- details: lines.join("\n"),
292
+ details,
293
+ data,
288
294
  };
289
295
  }
290
296
  export const flowReadinessCheck = {
291
297
  id: "flow-readiness-01",
292
298
  category: CATEGORY.FLOW_READINESS,
293
- title: "flow-readiness",
299
+ title: "workflow-continuity",
294
300
  dependencies: ["env-diagnostics-01", "cwd-context-01"],
295
301
  execute: async () => {
296
302
  const result = performFlowReadinessCheck();
297
303
  return {
298
304
  id: "flow-readiness-01",
305
+ impact: result.impact,
299
306
  status: result.status,
300
- title: "flow-readiness",
307
+ title: "workflow-continuity",
301
308
  message: result.message,
309
+ ...(result.hint ? { hint: result.hint } : {}),
302
310
  details: result.details,
311
+ data: result.data,
303
312
  };
304
313
  },
305
314
  };
@@ -1,4 +1,4 @@
1
- export { DoctorStatus, ReadinessStatus } from "./types.js";
1
+ export { DoctorImpact, DoctorStatus, ReadinessStatus, WorkflowContinuityState } from "./types.js";
2
2
  export { CheckRegistry, REGISTRY } from "./registry.js";
3
3
  export { DoctorOrchestrator } from "./orchestrator.js";
4
4
  export { runDoctorCommand } from "./runner.js";
@@ -1,6 +1,5 @@
1
- import { DoctorStatus, ReadinessStatus } from "./types.js";
1
+ import { DoctorImpact, DoctorStatus, ReadinessStatus } from "./types.js";
2
2
  import { REGISTRY } from "./registry.js";
3
- import { SOFT_CHECK_IDS } from "./checks/cwd-context.js";
4
3
  class DoctorOrchestrator {
5
4
  async run(checks, filter) {
6
5
  let checksToRun;
@@ -35,7 +34,7 @@ class DoctorOrchestrator {
35
34
  for (const check of checksToRun) {
36
35
  const result = await this.executeCheck(check);
37
36
  results.push(result);
38
- if (result.status === DoctorStatus.Fail) {
37
+ if (result.status === DoctorStatus.Fail && result.impact === DoctorImpact.Blocking) {
39
38
  break;
40
39
  }
41
40
  }
@@ -48,28 +47,40 @@ class DoctorOrchestrator {
48
47
  }
49
48
  async executeCheck(check) {
50
49
  const timeout = check.timeout ?? 30000;
50
+ let timeoutHandle;
51
51
  try {
52
52
  const timeoutPromise = new Promise((_, reject) => {
53
- setTimeout(() => reject(new Error(`Check '${check.id}' timed out after ${timeout}ms`)), timeout);
53
+ timeoutHandle = setTimeout(() => reject(new Error(`Check '${check.id}' timed out after ${timeout}ms`)), timeout);
54
+ timeoutHandle.unref?.();
54
55
  });
55
56
  const result = await Promise.race([check.execute(), timeoutPromise]);
56
- return result;
57
+ return {
58
+ ...result,
59
+ impact: result.impact ?? check.impact ?? DoctorImpact.Blocking,
60
+ };
57
61
  }
58
62
  catch (error) {
59
63
  return {
60
64
  id: check.id,
65
+ impact: check.impact ?? DoctorImpact.Blocking,
61
66
  status: DoctorStatus.Fail,
62
67
  title: check.title,
63
68
  message: error instanceof Error ? error.message : "Unknown error occurred",
64
69
  hint: `Check execution failed: ${check.id}`,
65
70
  };
66
71
  }
72
+ finally {
73
+ if (timeoutHandle) {
74
+ clearTimeout(timeoutHandle);
75
+ }
76
+ }
67
77
  }
68
78
  aggregateReadiness(results) {
69
- if (results.some((r) => r.status === DoctorStatus.Fail)) {
79
+ const blockingResults = results.filter((result) => result.impact === DoctorImpact.Blocking);
80
+ if (blockingResults.some((r) => r.status === DoctorStatus.Fail)) {
70
81
  return ReadinessStatus.NotReady;
71
82
  }
72
- if (results.some((r) => r.status === DoctorStatus.Warn && !SOFT_CHECK_IDS.includes(r.id))) {
83
+ if (blockingResults.some((r) => r.status === DoctorStatus.Warn)) {
73
84
  return ReadinessStatus.ReadyWithWarnings;
74
85
  }
75
86
  return ReadinessStatus.Ready;
@@ -1,4 +1,4 @@
1
- import { DoctorStatus, ReadinessStatus } from "./types.js";
1
+ import { DoctorImpact, DoctorStatus, ReadinessStatus } from "./types.js";
2
2
  import { REGISTRY } from "./registry.js";
3
3
  import { DoctorOrchestrator } from "./orchestrator.js";
4
4
  import { CATEGORY } from "./checks/category.js";
@@ -17,8 +17,13 @@ const CATEGORY_LABELS = {
17
17
  [CATEGORY.SYSTEM]: "System",
18
18
  [CATEGORY.EXECUTORS]: "Executors",
19
19
  [CATEGORY.ENV_DIAGNOSTICS]: "Environment",
20
- [CATEGORY.FLOW_READINESS]: "Flow Readiness",
20
+ [CATEGORY.FLOW_READINESS]: "Workflow Continuity",
21
21
  };
22
+ function formatCheckLine(result) {
23
+ const icon = STATUS_ICONS[result.status];
24
+ const impactLabel = result.impact === DoctorImpact.Advisory ? " (advisory)" : "";
25
+ return `[${icon}] ${result.title}${impactLabel} - ${result.message}`;
26
+ }
22
27
  async function runDoctorCommand(args) {
23
28
  const jsonMode = args.includes("--json");
24
29
  const filter = args.find((arg) => arg !== "--json");
@@ -51,9 +56,7 @@ async function runDoctorCommand(args) {
51
56
  console.log(`## ${label}`);
52
57
  console.log();
53
58
  for (const result of items) {
54
- const icon = STATUS_ICONS[result.status];
55
- const line = `[${icon}] ${result.title} - ${result.message}`;
56
- console.log(line);
59
+ console.log(formatCheckLine(result));
57
60
  if (result.hint) {
58
61
  console.log(` Hint: ${result.hint}`);
59
62
  }
@@ -70,9 +73,7 @@ async function runDoctorCommand(args) {
70
73
  console.log(`## ${label}`);
71
74
  console.log();
72
75
  for (const result of items) {
73
- const icon = STATUS_ICONS[result.status];
74
- const line = `[${icon}] ${result.title} - ${result.message}`;
75
- console.log(line);
76
+ console.log(formatCheckLine(result));
76
77
  if (result.hint) {
77
78
  console.log(` Hint: ${result.hint}`);
78
79
  }
@@ -4,9 +4,21 @@ export var DoctorStatus;
4
4
  DoctorStatus["Warn"] = "warn";
5
5
  DoctorStatus["Fail"] = "fail";
6
6
  })(DoctorStatus || (DoctorStatus = {}));
7
+ export var DoctorImpact;
8
+ (function (DoctorImpact) {
9
+ DoctorImpact["Blocking"] = "blocking";
10
+ DoctorImpact["Advisory"] = "advisory";
11
+ })(DoctorImpact || (DoctorImpact = {}));
7
12
  export var ReadinessStatus;
8
13
  (function (ReadinessStatus) {
9
14
  ReadinessStatus["Ready"] = "ready";
10
15
  ReadinessStatus["ReadyWithWarnings"] = "ready_with_warnings";
11
16
  ReadinessStatus["NotReady"] = "not_ready";
12
17
  })(ReadinessStatus || (ReadinessStatus = {}));
18
+ export var WorkflowContinuityState;
19
+ (function (WorkflowContinuityState) {
20
+ WorkflowContinuityState["Available"] = "available";
21
+ WorkflowContinuityState["NeedsPreviousStage"] = "needs_previous_stage";
22
+ WorkflowContinuityState["NotConfigured"] = "not_configured";
23
+ WorkflowContinuityState["InvalidState"] = "invalid_state";
24
+ })(WorkflowContinuityState || (WorkflowContinuityState = {}));