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.
- package/README.md +29 -7
- package/dist/artifact-manifest.js +219 -0
- package/dist/artifacts.js +21 -1
- package/dist/doctor/checks/cwd-context.js +4 -3
- package/dist/doctor/checks/env-diagnostics.js +193 -71
- package/dist/doctor/checks/flow-readiness.js +212 -203
- package/dist/doctor/index.js +1 -1
- package/dist/doctor/orchestrator.js +18 -7
- package/dist/doctor/runner.js +9 -8
- package/dist/doctor/types.js +12 -0
- package/dist/flow-state.js +75 -15
- package/dist/index.js +499 -199
- package/dist/interactive/blessed-session.js +361 -0
- package/dist/interactive/controller.js +1293 -0
- package/dist/interactive/create-interactive-session.js +5 -0
- package/dist/interactive/ink/index.js +576 -0
- package/dist/interactive/progress.js +245 -0
- package/dist/interactive/selectors.js +14 -0
- package/dist/interactive/session.js +1 -0
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/tree.js +155 -0
- package/dist/interactive/types.js +1 -0
- package/dist/interactive/view-model.js +1 -0
- package/dist/interactive-ui.js +159 -194
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flow-runner.js +212 -6
- package/dist/pipeline/declarative-flows.js +27 -0
- package/dist/pipeline/execution-routing-config.js +15 -0
- package/dist/pipeline/flow-catalog.js +23 -3
- package/dist/pipeline/flow-run-resume.js +29 -0
- package/dist/pipeline/flow-specs/auto-common.json +89 -360
- package/dist/pipeline/flow-specs/auto-golang.json +58 -363
- package/dist/pipeline/flow-specs/auto-simple.json +141 -0
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
- package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
- package/dist/pipeline/flow-specs/design-review.json +249 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
- package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
- package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
- package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +24 -5
- package/dist/pipeline/flow-specs/instant-task.json +177 -0
- package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
- package/dist/pipeline/flow-specs/plan-revise.json +267 -0
- package/dist/pipeline/flow-specs/plan.json +48 -70
- package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
- package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
- package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
- package/dist/pipeline/flow-specs/review/review-project.json +12 -0
- package/dist/pipeline/flow-specs/review/review.json +37 -31
- package/dist/pipeline/flow-specs/task-describe.json +62 -2
- package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
- package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
- package/dist/pipeline/node-registry.js +49 -1
- package/dist/pipeline/node-runner.js +3 -2
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
- package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
- package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
- package/dist/pipeline/nodes/ensure-summary-json-node.js +70 -0
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
- package/dist/pipeline/nodes/flow-run-node.js +226 -7
- package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
- package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
- package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
- package/dist/pipeline/nodes/review-verdict-node.js +86 -0
- package/dist/pipeline/nodes/select-files-form-node.js +8 -0
- package/dist/pipeline/nodes/structured-summary-node.js +24 -0
- package/dist/pipeline/nodes/user-input-node.js +38 -3
- package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
- package/dist/pipeline/prompt-registry.js +5 -1
- package/dist/pipeline/prompt-runtime.js +4 -1
- package/dist/pipeline/review-iteration.js +26 -0
- package/dist/pipeline/spec-compiler.js +2 -0
- package/dist/pipeline/spec-types.js +5 -0
- package/dist/pipeline/spec-validator.js +14 -0
- package/dist/pipeline/value-resolver.js +84 -1
- package/dist/prompts.js +82 -13
- package/dist/review-severity.js +45 -0
- package/dist/runtime/artifact-registry.js +402 -0
- package/dist/runtime/design-review-input-contract.js +113 -0
- package/dist/runtime/env-loader.js +3 -0
- package/dist/runtime/execution-routing-store.js +134 -0
- package/dist/runtime/execution-routing.js +227 -0
- package/dist/runtime/interactive-execution-routing.js +462 -0
- package/dist/runtime/plan-revise-input-contract.js +147 -0
- package/dist/runtime/planning-bundle.js +123 -0
- package/dist/runtime/ready-to-merge.js +31 -0
- package/dist/runtime/review-input-contract.js +100 -0
- package/dist/scope.js +11 -2
- package/dist/structured-artifact-schema-registry.js +10 -0
- package/dist/structured-artifact-schemas.json +257 -1
- package/dist/structured-artifacts.js +83 -6
- package/dist/user-input.js +70 -3
- 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,
|
|
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
|
|
45
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
140
|
-
|
|
106
|
+
function checkReviewProjectWorkflowContinuity() {
|
|
107
|
+
return createEntry("review", "project");
|
|
108
|
+
}
|
|
109
|
+
function checkReviewJiraWorkflowContinuity(scopeKey) {
|
|
110
|
+
const entry = createEntry("review", "jira");
|
|
141
111
|
if (!isJiraConfigured()) {
|
|
142
|
-
|
|
112
|
+
setEntryState(entry, WorkflowContinuityState.NotConfigured, "Jira integration is not configured (JIRA_API_KEY or JIRA_BASE_URL missing).");
|
|
143
113
|
}
|
|
144
|
-
const latestDesignIteration =
|
|
114
|
+
const latestDesignIteration = getLatestJsonArtifactIteration(scopeKey, "design");
|
|
115
|
+
const latestPlanIteration = getLatestJsonArtifactIteration(scopeKey, "plan");
|
|
145
116
|
if (latestDesignIteration === null) {
|
|
146
|
-
|
|
117
|
+
setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing design artifact from the planning stage.");
|
|
147
118
|
}
|
|
148
119
|
else {
|
|
149
|
-
|
|
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
|
-
|
|
123
|
+
setEntryState(entry, WorkflowContinuityState.NeedsPreviousStage, "Missing plan artifact from the planning stage.");
|
|
157
124
|
}
|
|
158
125
|
else {
|
|
159
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
172
|
-
const
|
|
139
|
+
function checkPlanJiraWorkflowContinuity(scopeKey) {
|
|
140
|
+
const entry = createEntry("plan", "jira");
|
|
173
141
|
if (!isJiraConfigured()) {
|
|
174
|
-
|
|
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
|
-
|
|
145
|
+
setEntryState(entry, WorkflowContinuityState.NotConfigured, "No Jira issue key is available for the current scope.");
|
|
178
146
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
188
|
+
function determineWorkflowContinuity(flowId, scopeKey) {
|
|
234
189
|
if (flowId === "implement") {
|
|
235
|
-
return [
|
|
190
|
+
return [checkImplementWorkflowContinuity(scopeKey)];
|
|
236
191
|
}
|
|
237
192
|
if (flowId === "review") {
|
|
238
193
|
return [
|
|
239
|
-
|
|
240
|
-
|
|
194
|
+
checkReviewProjectWorkflowContinuity(),
|
|
195
|
+
checkReviewJiraWorkflowContinuity(scopeKey),
|
|
241
196
|
];
|
|
242
197
|
}
|
|
243
198
|
if (flowId === "plan") {
|
|
244
199
|
return [
|
|
245
|
-
|
|
246
|
-
|
|
200
|
+
checkPlanJiraWorkflowContinuity(scopeKey),
|
|
201
|
+
checkPlanProjectWorkflowContinuity(scopeKey),
|
|
247
202
|
];
|
|
248
203
|
}
|
|
249
|
-
return [
|
|
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
|
|
255
|
+
const entries = [];
|
|
266
256
|
for (const flowId of BUILT_IN_COMMAND_FLOW_IDS) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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:
|
|
289
|
+
status: DoctorStatus.Ok,
|
|
290
|
+
impact: DoctorImpact.Advisory,
|
|
286
291
|
message,
|
|
287
|
-
details
|
|
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: "
|
|
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: "
|
|
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
|
};
|
package/dist/doctor/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
83
|
+
if (blockingResults.some((r) => r.status === DoctorStatus.Warn)) {
|
|
73
84
|
return ReadinessStatus.ReadyWithWarnings;
|
|
74
85
|
}
|
|
75
86
|
return ReadinessStatus.Ready;
|
package/dist/doctor/runner.js
CHANGED
|
@@ -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]: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/doctor/types.js
CHANGED
|
@@ -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 = {}));
|