agentweaver 0.1.13 → 0.1.15

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 (51) hide show
  1. package/README.md +24 -19
  2. package/dist/artifacts.js +6 -1
  3. package/dist/doctor/checks/cwd-context.js +4 -3
  4. package/dist/doctor/checks/env-diagnostics.js +168 -71
  5. package/dist/doctor/checks/flow-readiness.js +210 -198
  6. package/dist/doctor/index.js +1 -1
  7. package/dist/doctor/orchestrator.js +18 -7
  8. package/dist/doctor/runner.js +9 -8
  9. package/dist/doctor/types.js +12 -0
  10. package/dist/index.js +119 -55
  11. package/dist/interactive-ui.js +25 -25
  12. package/dist/pipeline/declarative-flows.js +1 -0
  13. package/dist/pipeline/flow-catalog.js +4 -0
  14. package/dist/pipeline/flow-specs/auto-common.json +1 -0
  15. package/dist/pipeline/flow-specs/auto-golang.json +2 -1
  16. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +1 -0
  17. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  18. package/dist/pipeline/flow-specs/design-review.json +239 -0
  19. package/dist/pipeline/flow-specs/git-commit.json +1 -0
  20. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +3 -2
  21. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +3 -2
  22. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  23. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +3 -2
  24. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +3 -2
  25. package/dist/pipeline/flow-specs/implement.json +13 -0
  26. package/dist/pipeline/flow-specs/plan-revise.json +261 -0
  27. package/dist/pipeline/flow-specs/plan.json +2 -1
  28. package/dist/pipeline/flow-specs/review/review-fix.json +1 -0
  29. package/dist/pipeline/flow-specs/review/review-loop.json +1 -0
  30. package/dist/pipeline/flow-specs/review/review-project.json +1 -0
  31. package/dist/pipeline/flow-specs/review/review.json +2 -1
  32. package/dist/pipeline/flow-specs/task-describe.json +67 -8
  33. package/dist/pipeline/node-registry.js +8 -0
  34. package/dist/pipeline/nodes/ensure-summary-json-node.js +59 -0
  35. package/dist/pipeline/nodes/git-commit-node.js +1 -1
  36. package/dist/pipeline/nodes/git-status-node.js +1 -1
  37. package/dist/pipeline/nodes/review-findings-form-node.js +8 -8
  38. package/dist/pipeline/nodes/user-input-node.js +2 -2
  39. package/dist/pipeline/prompt-registry.js +3 -1
  40. package/dist/pipeline/spec-types.js +2 -0
  41. package/dist/pipeline/value-resolver.js +11 -1
  42. package/dist/prompts.js +49 -2
  43. package/dist/runtime/design-review-input-contract.js +112 -0
  44. package/dist/runtime/plan-revise-input-contract.js +144 -0
  45. package/dist/runtime/process-runner.js +2 -2
  46. package/dist/runtime/ready-to-merge.js +10 -0
  47. package/dist/scope.js +13 -4
  48. package/dist/structured-artifact-schema-registry.js +1 -0
  49. package/dist/structured-artifact-schemas.json +117 -0
  50. package/dist/structured-artifacts.js +6 -0
  51. package/package.json +3 -2
@@ -1,12 +1,53 @@
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, jiraDescriptionJsonFile, latestArtifactIteration, planJsonFile, } from "../../artifacts.js";
8
8
  const GO_BINARY_PATTERNS = ["go", "go.exe"];
9
9
  const GIT_BINARY_PATTERNS = ["git", "git.exe"];
10
+ function statePriority(state) {
11
+ switch (state) {
12
+ case WorkflowContinuityState.InvalidState:
13
+ return 3;
14
+ case WorkflowContinuityState.NotConfigured:
15
+ return 2;
16
+ case WorkflowContinuityState.NeedsPreviousStage:
17
+ return 1;
18
+ case WorkflowContinuityState.Available:
19
+ default:
20
+ return 0;
21
+ }
22
+ }
23
+ function formatState(state) {
24
+ switch (state) {
25
+ case WorkflowContinuityState.Available:
26
+ return "available";
27
+ case WorkflowContinuityState.NeedsPreviousStage:
28
+ return "requires previous stage outputs";
29
+ case WorkflowContinuityState.NotConfigured:
30
+ return "not configured";
31
+ case WorkflowContinuityState.InvalidState:
32
+ return "invalid state";
33
+ default:
34
+ return state;
35
+ }
36
+ }
37
+ function setEntryState(entry, state, reason) {
38
+ if (statePriority(state) > statePriority(entry.state)) {
39
+ entry.state = state;
40
+ }
41
+ entry.reasons.push(reason);
42
+ }
43
+ function createEntry(flowId, mode) {
44
+ return {
45
+ flowId,
46
+ ...(mode ? { mode } : {}),
47
+ state: WorkflowContinuityState.Available,
48
+ reasons: [],
49
+ };
50
+ }
10
51
  function findBinary(name) {
11
52
  const envPath = process.env.PATH ?? "";
12
53
  const pathDirs = envPath.split(path.delimiter);
@@ -20,233 +61,150 @@ function findBinary(name) {
20
61
  }
21
62
  return null;
22
63
  }
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
64
  function isJiraConfigured() {
39
65
  return !!(process.env.JIRA_API_KEY && process.env.JIRA_BASE_URL);
40
66
  }
41
67
  function isGitLabConfigured() {
42
68
  return !!process.env.GITLAB_TOKEN;
43
69
  }
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);
70
+ function getLatestJsonArtifactIteration(scopeKey, prefix) {
71
+ return latestArtifactIteration(scopeKey, prefix, "json");
59
72
  }
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);
73
+ function checkStructuredArtifactFile(entry, artifactPath, schemaId, missingReason, invalidReasonPrefix) {
74
+ if (!existsSync(artifactPath)) {
75
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, missingReason);
76
+ return;
77
+ }
78
+ try {
79
+ validateStructuredArtifact(artifactPath, schemaId);
80
+ }
81
+ catch (error) {
82
+ setEntryState(entry, WorkflowContinuityState.InvalidState, `${invalidReasonPrefix}: ${error.message}`);
83
+ }
75
84
  }
76
- function checkImplementFlowReadiness(scopeKey) {
77
- const blockers = [];
78
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
79
- const latestPlanIteration = getLatestPlanIteration(scopeKey);
85
+ function checkImplementWorkflowContinuity(scopeKey) {
86
+ const entry = createEntry("implement");
87
+ const latestDesignIteration = getLatestJsonArtifactIteration(scopeKey, "design");
88
+ const latestPlanIteration = getLatestJsonArtifactIteration(scopeKey, "plan");
80
89
  if (latestDesignIteration === null) {
81
- blockers.push("design artifact not found");
90
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing design artifact from the planning stage.");
82
91
  }
83
92
  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
- }
93
+ checkStructuredArtifactFile(entry, designJsonFile(scopeKey, latestDesignIteration), "implementation-design/v1", "Missing design artifact from the planning stage.", "Design artifact schema is invalid");
96
94
  }
97
95
  if (latestPlanIteration === null) {
98
- blockers.push("plan artifact not found");
96
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing plan artifact from the planning stage.");
99
97
  }
100
98
  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
- }
99
+ checkStructuredArtifactFile(entry, planJsonFile(scopeKey, latestPlanIteration), "implementation-plan/v1", "Missing plan artifact from the planning stage.", "Plan artifact schema is invalid");
113
100
  }
114
- return {
115
- flowId: "implement",
116
- status: blockers.length === 0 ? "ready" : "not_ready",
117
- blockers,
118
- };
119
- }
120
- function checkReviewProjectFlowReadiness(scopeKey) {
121
- const blockers = [];
122
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
123
- if (latestDesignIteration === null) {
124
- blockers.push("design artifact not found");
101
+ if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
102
+ entry.nextStep = "Run plan to generate design, plan, and QA artifacts for this scope.";
125
103
  }
126
- else {
127
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
128
- if (!existsSync(designPath)) {
129
- blockers.push(`design artifact not found at ${designPath}`);
130
- }
104
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
105
+ entry.nextStep = "Regenerate the planning artifacts for this scope before running implement.";
131
106
  }
132
- return {
133
- flowId: "review",
134
- mode: "project",
135
- status: blockers.length === 0 ? "ready" : "not_ready",
136
- blockers,
137
- };
107
+ return entry;
108
+ }
109
+ function checkReviewProjectWorkflowContinuity() {
110
+ return createEntry("review", "project");
138
111
  }
139
- function checkReviewJiraFlowReadiness(scopeKey) {
140
- const blockers = [];
112
+ function checkReviewJiraWorkflowContinuity(scopeKey) {
113
+ const entry = createEntry("review", "jira");
141
114
  if (!isJiraConfigured()) {
142
- blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
115
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Jira integration is not configured (JIRA_API_KEY or JIRA_BASE_URL missing).");
143
116
  }
144
- const latestDesignIteration = getLatestDesignIteration(scopeKey);
117
+ const latestDesignIteration = getLatestJsonArtifactIteration(scopeKey, "design");
118
+ const latestPlanIteration = getLatestJsonArtifactIteration(scopeKey, "plan");
145
119
  if (latestDesignIteration === null) {
146
- blockers.push("design artifact not found");
120
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing design artifact from the planning stage.");
147
121
  }
148
122
  else {
149
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
150
- if (!existsSync(designPath)) {
151
- blockers.push(`design artifact not found at ${designPath}`);
152
- }
123
+ checkStructuredArtifactFile(entry, designJsonFile(scopeKey, latestDesignIteration), "implementation-design/v1", "Missing design artifact from the planning stage.", "Design artifact schema is invalid");
153
124
  }
154
- const latestPlanIteration = getLatestPlanIteration(scopeKey);
155
125
  if (latestPlanIteration === null) {
156
- blockers.push("plan artifact not found");
126
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing plan artifact from the planning stage.");
157
127
  }
158
128
  else {
159
- const planPath = planJsonFile(scopeKey, latestPlanIteration);
160
- if (!existsSync(planPath)) {
161
- blockers.push(`plan artifact not found at ${planPath}`);
162
- }
129
+ checkStructuredArtifactFile(entry, planJsonFile(scopeKey, latestPlanIteration), "implementation-plan/v1", "Missing plan artifact from the planning stage.", "Plan artifact schema is invalid");
163
130
  }
164
- return {
165
- flowId: "review",
166
- mode: "jira",
167
- status: blockers.length === 0 ? "ready" : "not_ready",
168
- blockers,
169
- };
131
+ if (entry.state === WorkflowContinuityState.NotConfigured) {
132
+ entry.nextStep = "Configure Jira access and ensure the planning artifacts exist before running review:jira.";
133
+ }
134
+ else if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
135
+ entry.nextStep = "Run plan first to generate the design and plan artifacts required by review:jira.";
136
+ }
137
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
138
+ entry.nextStep = "Regenerate invalid planning artifacts before running review:jira.";
139
+ }
140
+ return entry;
170
141
  }
171
- function checkPlanJiraFlowReadiness(scopeKey) {
172
- const blockers = [];
142
+ function checkPlanJiraWorkflowContinuity(scopeKey) {
143
+ const entry = createEntry("plan", "jira");
173
144
  if (!isJiraConfigured()) {
174
- blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
145
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Jira integration is not configured (JIRA_API_KEY or JIRA_BASE_URL missing).");
175
146
  }
176
147
  if (!process.env.JIRA_ISSUE_KEY && !scopeKey.includes("@")) {
177
- blockers.push("Jira issue key not available");
148
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "No Jira issue key is available for the current scope.");
178
149
  }
179
- return {
180
- flowId: "plan",
181
- mode: "jira",
182
- status: blockers.length === 0 ? "ready" : "not_ready",
183
- blockers,
184
- };
150
+ if (entry.state === WorkflowContinuityState.NotConfigured) {
151
+ entry.nextStep = "Set Jira environment variables and provide a Jira issue key or browse URL before running plan:jira.";
152
+ }
153
+ return entry;
185
154
  }
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)");
155
+ function checkPlanProjectWorkflowContinuity(scopeKey) {
156
+ const entry = createEntry("plan", "project");
157
+ const latestDescriptionIteration = getLatestJsonArtifactIteration(scopeKey, "jira-description");
158
+ if (latestDescriptionIteration === null) {
159
+ setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing task description artifact from task-describe.");
191
160
  }
192
161
  else {
193
- const designPath = designJsonFile(scopeKey, latestDesignIteration);
194
- if (!existsSync(designPath)) {
195
- blockers.push("design artifact not found");
196
- }
162
+ checkStructuredArtifactFile(entry, jiraDescriptionJsonFile(scopeKey, latestDescriptionIteration), "jira-description/v1", "Missing task description artifact from task-describe.", "Task description artifact schema is invalid");
197
163
  }
198
- return {
199
- flowId: "plan",
200
- mode: "project",
201
- status: blockers.length === 0 ? "ready" : "not_ready",
202
- blockers,
203
- };
164
+ if (entry.state === WorkflowContinuityState.NeedsPreviousStage) {
165
+ entry.nextStep = "Run task-describe first to create a task description artifact for this scope.";
166
+ }
167
+ else if (entry.state === WorkflowContinuityState.InvalidState) {
168
+ entry.nextStep = "Regenerate the task description artifact before running plan:project.";
169
+ }
170
+ return entry;
204
171
  }
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
- }
172
+ function checkGenericWorkflowContinuity(flowId) {
173
+ const entry = createEntry(flowId);
174
+ const goFlows = new Set(["run-go-tests-loop", "run-go-linter-loop", "auto-golang"]);
175
+ const gitFlows = new Set(["git-commit", "gitlab-review", "gitlab-diff-review", "mr-description"]);
176
+ const gitLabFlows = new Set(["gitlab-review", "gitlab-diff-review", "mr-description"]);
177
+ if (goFlows.has(flowId) && findBinary("go") === null) {
178
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Go binary is not available in PATH.");
179
+ entry.nextStep = "Install Go and ensure the go binary is available in PATH.";
222
180
  }
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
181
+ if (gitFlows.has(flowId) && findBinary("git") === null) {
182
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "Git binary is not available in PATH.");
183
+ entry.nextStep = "Install Git and ensure the git binary is available in PATH.";
226
184
  }
227
- return {
228
- flowId,
229
- status: blockers.length === 0 ? "ready" : "not_ready",
230
- blockers,
231
- };
185
+ if (gitLabFlows.has(flowId) && !isGitLabConfigured()) {
186
+ setEntryState(entry, WorkflowContinuityState.NotConfigured, "GitLab integration is not configured (GITLAB_TOKEN missing).");
187
+ entry.nextStep = "Set GITLAB_TOKEN before running this GitLab-backed flow.";
188
+ }
189
+ return entry;
232
190
  }
233
- function determineFlowReadiness(flowId, scopeKey) {
191
+ function determineWorkflowContinuity(flowId, scopeKey) {
234
192
  if (flowId === "implement") {
235
- return [checkImplementFlowReadiness(scopeKey)];
193
+ return [checkImplementWorkflowContinuity(scopeKey)];
236
194
  }
237
195
  if (flowId === "review") {
238
196
  return [
239
- checkReviewProjectFlowReadiness(scopeKey),
240
- checkReviewJiraFlowReadiness(scopeKey),
197
+ checkReviewProjectWorkflowContinuity(),
198
+ checkReviewJiraWorkflowContinuity(scopeKey),
241
199
  ];
242
200
  }
243
201
  if (flowId === "plan") {
244
202
  return [
245
- checkPlanJiraFlowReadiness(scopeKey),
246
- checkPlanProjectFlowReadiness(scopeKey),
203
+ checkPlanJiraWorkflowContinuity(scopeKey),
204
+ checkPlanProjectWorkflowContinuity(scopeKey),
247
205
  ];
248
206
  }
249
- return [checkGenericFlowReadiness(flowId, scopeKey)];
207
+ return [checkGenericWorkflowContinuity(flowId)];
250
208
  }
251
209
  function getScopeKey() {
252
210
  const cwd = process.cwd();
@@ -260,46 +218,100 @@ function getScopeKey() {
260
218
  }
261
219
  return entries[0];
262
220
  }
221
+ function buildSummary(entries) {
222
+ return {
223
+ available: entries.filter((entry) => entry.state === WorkflowContinuityState.Available).length,
224
+ needsPreviousStage: entries.filter((entry) => entry.state === WorkflowContinuityState.NeedsPreviousStage).length,
225
+ notConfigured: entries.filter((entry) => entry.state === WorkflowContinuityState.NotConfigured).length,
226
+ invalidState: entries.filter((entry) => entry.state === WorkflowContinuityState.InvalidState).length,
227
+ };
228
+ }
229
+ function buildMessage(summary) {
230
+ const parts = [`${summary.available} flows available`];
231
+ if (summary.needsPreviousStage > 0) {
232
+ parts.push(`${summary.needsPreviousStage} require earlier stage outputs`);
233
+ }
234
+ if (summary.notConfigured > 0) {
235
+ parts.push(`${summary.notConfigured} not configured`);
236
+ }
237
+ if (summary.invalidState > 0) {
238
+ parts.push(`${summary.invalidState} in invalid state`);
239
+ }
240
+ return parts.join(", ");
241
+ }
242
+ function buildDetails(scopeKey, entries) {
243
+ const lines = [`scope: ${scopeKey}`];
244
+ for (const entry of entries) {
245
+ const modeSuffix = entry.mode ? `:${entry.mode}` : "";
246
+ lines.push(` ${entry.flowId}${modeSuffix}: ${formatState(entry.state)}`);
247
+ for (const reason of entry.reasons) {
248
+ lines.push(` - ${reason}`);
249
+ }
250
+ if (entry.nextStep) {
251
+ lines.push(` next: ${entry.nextStep}`);
252
+ }
253
+ }
254
+ return lines.join("\n");
255
+ }
263
256
  function performFlowReadinessCheck() {
264
257
  const scopeKey = getScopeKey();
265
- const allEntries = [];
258
+ const entries = [];
266
259
  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
- }
260
+ entries.push(...determineWorkflowContinuity(flowId, scopeKey));
261
+ }
262
+ const summary = buildSummary(entries);
263
+ const message = buildMessage(summary);
264
+ const details = buildDetails(scopeKey, entries);
265
+ const data = {
266
+ kind: "workflow-continuity",
267
+ scopeKey,
268
+ summary,
269
+ entries,
270
+ };
271
+ if (summary.invalidState > 0) {
272
+ return {
273
+ status: DoctorStatus.Warn,
274
+ impact: DoctorImpact.Blocking,
275
+ message,
276
+ hint: "Current scope contains invalid workflow artifacts. Regenerate or clean them up before continuing those flows.",
277
+ details,
278
+ data,
279
+ };
280
+ }
281
+ if (summary.needsPreviousStage > 0 || summary.notConfigured > 0) {
282
+ return {
283
+ status: DoctorStatus.Warn,
284
+ impact: DoctorImpact.Advisory,
285
+ message,
286
+ hint: "These continuity findings describe what can be continued in the current scope and do not affect overall application readiness.",
287
+ details,
288
+ data,
289
+ };
281
290
  }
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
291
  return {
285
- status: overallStatus,
292
+ status: DoctorStatus.Ok,
293
+ impact: DoctorImpact.Advisory,
286
294
  message,
287
- details: lines.join("\n"),
295
+ details,
296
+ data,
288
297
  };
289
298
  }
290
299
  export const flowReadinessCheck = {
291
300
  id: "flow-readiness-01",
292
301
  category: CATEGORY.FLOW_READINESS,
293
- title: "flow-readiness",
302
+ title: "workflow-continuity",
294
303
  dependencies: ["env-diagnostics-01", "cwd-context-01"],
295
304
  execute: async () => {
296
305
  const result = performFlowReadinessCheck();
297
306
  return {
298
307
  id: "flow-readiness-01",
308
+ impact: result.impact,
299
309
  status: result.status,
300
- title: "flow-readiness",
310
+ title: "workflow-continuity",
301
311
  message: result.message,
312
+ ...(result.hint ? { hint: result.hint } : {}),
302
313
  details: result.details,
314
+ data: result.data,
303
315
  };
304
316
  },
305
317
  };
@@ -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 = {}));