agentweaver 0.1.16 → 0.1.17

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 (40) hide show
  1. package/README.md +50 -10
  2. package/dist/artifacts.js +73 -3
  3. package/dist/doctor/checks/executors.js +2 -2
  4. package/dist/flow-state.js +138 -1
  5. package/dist/index.js +175 -61
  6. package/dist/interactive/controller.js +56 -23
  7. package/dist/interactive/ink/index.js +22 -1
  8. package/dist/interactive/tree.js +2 -2
  9. package/dist/pipeline/auto-flow.js +9 -6
  10. package/dist/pipeline/context.js +6 -5
  11. package/dist/pipeline/declarative-flows.js +39 -20
  12. package/dist/pipeline/flow-catalog.js +36 -14
  13. package/dist/pipeline/flow-specs/auto-common.json +1 -0
  14. package/dist/pipeline/flow-specs/auto-golang.json +27 -1
  15. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +13 -1
  16. package/dist/pipeline/flow-specs/plan.json +4 -2
  17. package/dist/pipeline/launch-profile-config.js +30 -18
  18. package/dist/pipeline/node-contract.js +1 -0
  19. package/dist/pipeline/node-registry.js +74 -5
  20. package/dist/pipeline/nodes/flow-run-node.js +188 -173
  21. package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
  22. package/dist/pipeline/plugin-loader.js +389 -0
  23. package/dist/pipeline/plugin-types.js +1 -0
  24. package/dist/pipeline/registry.js +71 -4
  25. package/dist/pipeline/spec-compiler.js +1 -0
  26. package/dist/pipeline/spec-loader.js +14 -0
  27. package/dist/pipeline/spec-validator.js +6 -0
  28. package/dist/pipeline/value-resolver.js +2 -1
  29. package/dist/plugin-sdk.js +1 -0
  30. package/dist/runtime/artifact-registry.js +3 -0
  31. package/dist/runtime/execution-routing.js +25 -19
  32. package/dist/runtime/interactive-execution-routing.js +66 -57
  33. package/docs/example/.flows/examples/claude-example.json +50 -0
  34. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  35. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  36. package/docs/examples/.flows/claude-example.json +50 -0
  37. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  38. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  39. package/docs/plugin-sdk.md +731 -0
  40. package/package.json +6 -2
package/README.md CHANGED
@@ -34,13 +34,21 @@ In practice, this means you can treat an agent workflow like an engineered syste
34
34
 
35
35
  ## Core Concepts
36
36
 
37
- - `flow spec`: declarative JSON under `src/pipeline/flow-specs/` or project-local `.agentweaver/.flows/`
37
+ - `flow spec`: declarative JSON under `src/pipeline/flow-specs/`, global `~/.agentweaver/.flows/`, or project-local `.agentweaver/.flows/`
38
38
  - `node`: reusable runtime unit from `src/pipeline/nodes/`
39
39
  - `executor`: integration layer for Jira, Codex, OpenCode, GitLab, shell/process execution, Telegram notifications, and related actions
40
40
  - `scope`: isolated workspace key for artifacts and flow state; usually based on Jira task, otherwise derived from git context
41
41
  - `artifact`: file produced or consumed by flows, used as the stable contract between stages
42
42
  - `flow state`: compact persisted execution metadata used for resume/restart in long-running flows such as `auto-golang`
43
43
 
44
+ ## Семантика Запуска
45
+
46
+ - `resume` возобновляет только реально прерванный запуск и использует сохранённое состояние исполнения без пересборки уже выполненных шагов
47
+ - `continue` предназначен для завершённых итерационных циклов и запускает следующую итерацию от последних валидных артефактов без удаления исторических артефактов
48
+ - `restart` считается новым запуском: текущая активная попытка архивируется в `.agentweaver/scopes/<scope>/.artifacts/restart-archives/attempt-XXXX`, после чего создаётся новая активная попытка
49
+ - Для неоднозначных запусков оператор должен явно выбрать действие: в интерактивном режиме через подтверждение, в неинтерактивном режиме через `--resume`, `--continue` или `--restart`
50
+ - Контракт распространяется на `auto-common`, `auto-simple`, `auto-golang`, `instant-task`, `review-loop`, `run-go-linter-loop` и `run-go-tests-loop`
51
+
44
52
  ## Declarative Workflow Model
45
53
 
46
54
  The center of the system is the declarative flow spec:
@@ -118,6 +126,34 @@ node dist/index.js --help
118
126
 
119
127
  Global install after publishing:
120
128
 
129
+ ## Plugin SDK
130
+
131
+ AgentWeaver supports local plugins and custom declarative flows from both global and project-local `.agentweaver` directories.
132
+
133
+ Plugin authors must use only the public SDK subpath: `agentweaver/plugin-sdk`.
134
+ The package root `agentweaver`, internal paths such as `agentweaver/dist/*` and `agentweaver/src/*`, and repository-relative source imports are not part of the supported SDK contract.
135
+
136
+ Supported plugin manifest locations are:
137
+
138
+ - `~/.agentweaver/.plugins/<plugin-id>/plugin.json`
139
+ - `.agentweaver/.plugins/<plugin-id>/plugin.json`
140
+
141
+ The plugin directory name and manifest `id` must match exactly.
142
+
143
+ Use the dedicated guide at [docs/plugin-sdk.md](docs/plugin-sdk.md) for:
144
+
145
+ - the executor versus node architecture
146
+ - manifest and entrypoint rules
147
+ - optional routing metadata for plugin LLM executors
148
+ - runtime context APIs available to plugin code
149
+ - global and project-local flow wiring under `~/.agentweaver/.flows/` and `.agentweaver/.flows/`
150
+ - compatibility, testing, troubleshooting, and a complete end-to-end walkthrough
151
+
152
+ Repository reference examples live under `docs/examples/`, for example:
153
+
154
+ - `docs/examples/.plugins/claude-example-plugin/`
155
+ - `docs/examples/.flows/claude-example.json`
156
+
121
157
  ```bash
122
158
  npm install -g agentweaver
123
159
  agentweaver --help
@@ -183,7 +219,7 @@ OPENCODE_MODEL=minimax-coding-plan/MiniMax-M2.7
183
219
 
184
220
  The full-screen TUI is not a cosmetic wrapper. It is the operator console for the harness:
185
221
 
186
- - browse built-in and project-local workflows
222
+ - browse built-in, global, and project-local workflows
187
223
  - launch flows in the current scope
188
224
  - inspect progress by phase and step
189
225
  - follow activity, prompts, summaries, and statuses
@@ -332,27 +368,31 @@ Current layout:
332
368
  Flow discovery behavior:
333
369
 
334
370
  - built-in flows are loaded from `src/pipeline/flow-specs/`
335
- - project-local flows are loaded from `.agentweaver/.flows/`
336
- - both built-in and project-local flow specs are validated at load time
337
- - duplicate flow ids fail fast
338
- - project-local flows are shown separately in the UI
371
+ - global custom flows are loaded from `~/.agentweaver/.flows/`
372
+ - project-local custom flows are loaded from `.agentweaver/.flows/`
373
+ - all discovered flow specs are validated at load time
374
+ - duplicate flow ids fail fast across built-in, global, and project-local sources
375
+ - custom flows are shown separately in the UI as global and project-local groups
339
376
 
340
- ## Project-Local Flows
377
+ ## Custom Flows
341
378
 
342
- You can add project-specific flow specs under:
379
+ You can add custom flow specs under either:
343
380
 
344
381
  ```bash
382
+ ~/.agentweaver/.flows/**/*.json
345
383
  .agentweaver/.flows/**/*.json
346
384
  ```
347
385
 
348
- Project-local flows:
386
+ Custom flows:
349
387
 
350
388
  - are discovered recursively
351
389
  - get their flow id from the relative path without `.json`
352
390
  - share the same validator and runtime as built-in flows
353
391
  - cannot conflict with an existing built-in or other discovered flow id
354
392
 
355
- Nested `flow-run` steps can reference built-in or project-local specs by file name, as long as the name resolves unambiguously.
393
+ Use the global directory for reusable personal flows and plugins across repositories, and the project-local directory for repo-specific wiring.
394
+
395
+ Nested `flow-run` steps can reference built-in, global, or project-local specs by file name, as long as the name resolves unambiguously.
356
396
 
357
397
  ## Development
358
398
 
package/dist/artifacts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync } from "node:fs";
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { TaskRunnerError } from "./errors.js";
@@ -214,8 +214,78 @@ export function gitlabDiffReviewInputJsonFile(taskKey) {
214
214
  export function flowStateFile(scopeKey, flowId) {
215
215
  return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${encodeURIComponent(flowId)}.json`);
216
216
  }
217
- export function planArtifacts(taskKey) {
218
- return [designFile(taskKey), designJsonFile(taskKey), planFile(taskKey), planJsonFile(taskKey), qaFile(taskKey), qaJsonFile(taskKey)];
217
+ export function restartArchivesDir(scopeKey) {
218
+ return scopeArtifactsFile(scopeKey, "restart-archives");
219
+ }
220
+ function nextRestartArchiveName(scopeKey) {
221
+ const archiveRoot = restartArchivesDir(scopeKey);
222
+ if (!existsSync(archiveRoot)) {
223
+ return "attempt-0001";
224
+ }
225
+ const attemptNumbers = readdirSync(archiveRoot, { withFileTypes: true })
226
+ .filter((entry) => entry.isDirectory())
227
+ .map((entry) => /^attempt-(\d{4})$/.exec(entry.name)?.[1] ?? null)
228
+ .filter((value) => value !== null)
229
+ .map((value) => Number.parseInt(value, 10));
230
+ const nextNumber = (attemptNumbers.length === 0 ? 0 : Math.max(...attemptNumbers)) + 1;
231
+ return `attempt-${String(nextNumber).padStart(4, "0")}`;
232
+ }
233
+ export function archiveActiveAttempt(scopeKey) {
234
+ const workspaceDir = scopeWorkspaceDir(scopeKey);
235
+ if (!existsSync(workspaceDir)) {
236
+ return null;
237
+ }
238
+ const workspaceEntries = readdirSync(workspaceDir, { withFileTypes: true })
239
+ .filter((entry) => entry.name !== ".artifacts");
240
+ const artifactEntries = readdirSync(scopeArtifactsDir(scopeKey), { withFileTypes: true })
241
+ .filter((entry) => entry.name !== "restart-archives");
242
+ if (workspaceEntries.length === 0 && artifactEntries.length === 0) {
243
+ return null;
244
+ }
245
+ const archiveRoot = restartArchivesDir(scopeKey);
246
+ mkdirSync(archiveRoot, { recursive: true });
247
+ const archiveDir = path.join(archiveRoot, nextRestartArchiveName(scopeKey));
248
+ const workspaceArchiveDir = path.join(archiveDir, "workspace");
249
+ const artifactsArchiveDir = path.join(archiveDir, "artifacts");
250
+ mkdirSync(workspaceArchiveDir, { recursive: true });
251
+ mkdirSync(artifactsArchiveDir, { recursive: true });
252
+ try {
253
+ for (const entry of workspaceEntries) {
254
+ cpSync(path.join(workspaceDir, entry.name), path.join(workspaceArchiveDir, entry.name), {
255
+ recursive: true,
256
+ errorOnExist: true,
257
+ force: false,
258
+ });
259
+ }
260
+ for (const entry of artifactEntries) {
261
+ cpSync(path.join(scopeArtifactsDir(scopeKey), entry.name), path.join(artifactsArchiveDir, entry.name), {
262
+ recursive: true,
263
+ errorOnExist: true,
264
+ force: false,
265
+ });
266
+ }
267
+ }
268
+ catch (error) {
269
+ rmSync(archiveDir, { recursive: true, force: true });
270
+ throw new TaskRunnerError(`Failed to archive active attempt for restart: ${error.message}`);
271
+ }
272
+ for (const entry of workspaceEntries) {
273
+ rmSync(path.join(workspaceDir, entry.name), { recursive: true, force: true });
274
+ }
275
+ for (const entry of artifactEntries) {
276
+ rmSync(path.join(scopeArtifactsDir(scopeKey), entry.name), { recursive: true, force: true });
277
+ }
278
+ return archiveDir;
279
+ }
280
+ export function planArtifacts(taskKey, iteration) {
281
+ return [
282
+ designFile(taskKey, iteration),
283
+ designJsonFile(taskKey, iteration),
284
+ planFile(taskKey, iteration),
285
+ planJsonFile(taskKey, iteration),
286
+ qaFile(taskKey, iteration),
287
+ qaJsonFile(taskKey, iteration),
288
+ ];
219
289
  }
220
290
  export function bugAnalyzeArtifacts(taskKey) {
221
291
  return [
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { DoctorStatus } from "../types.js";
3
3
  import { CATEGORY } from "./category.js";
4
- import { ALLOWED_MODELS_BY_EXECUTOR } from "../../pipeline/launch-profile-config.js";
4
+ import { allowedModelsForExecutor } from "../../pipeline/launch-profile-config.js";
5
5
  import { findCmdPath, isExecutable } from "../../runtime/command-resolution.js";
6
6
  function getEnvVarName(executorId) {
7
7
  return executorId === "codex" ? "CODEX_BIN" : "OPENCODE_BIN";
@@ -72,7 +72,7 @@ function checkExecutor(executorId) {
72
72
  if (versionOutput === null) {
73
73
  return createResult(executorId, DoctorStatus.Fail, `${executorId} --version check failed`, `${executorId} --version did not produce expected output`, `path: ${resolution.path}, source: ${resolution.source}`, resolution, null);
74
74
  }
75
- const allowedModels = ALLOWED_MODELS_BY_EXECUTOR[executorId];
75
+ const allowedModels = allowedModelsForExecutor(executorId);
76
76
  const modelWarnings = [];
77
77
  for (const model of allowedModels) {
78
78
  const modelResult = spawnSync(resolution.path, ["--model", model, "--version"], { encoding: "utf8", stdio: "pipe" });
@@ -4,7 +4,25 @@ import { ensureScopeWorkspaceDir, flowStateFile } from "./artifacts.js";
4
4
  import { TaskRunnerError } from "./errors.js";
5
5
  import { isFlowRunResumeEnvelope } from "./pipeline/flow-run-resume.js";
6
6
  import { resolveStoredExecutionRoutingSnapshot, singleLaunchProfileExecutionRouting } from "./runtime/execution-routing.js";
7
- const FLOW_STATE_SCHEMA_VERSION = 2;
7
+ const FLOW_STATE_SCHEMA_VERSION = 3;
8
+ const CONTINUABLE_FLOW_KINDS = new Set([
9
+ "design-review-loop-flow",
10
+ "review-loop-flow",
11
+ "review-project-loop-flow",
12
+ "run-go-linter-loop-flow",
13
+ "run-go-tests-loop-flow",
14
+ ]);
15
+ const CONTINUABLE_PARENT_FLOW_IDS = new Set([
16
+ "auto-common",
17
+ "auto-simple",
18
+ "auto-golang",
19
+ "instant-task",
20
+ ]);
21
+ const CONTINUABLE_DIRECT_FLOW_IDS = new Set([
22
+ "review-loop",
23
+ "run-go-linter-loop",
24
+ "run-go-tests-loop",
25
+ ]);
8
26
  function nowIso8601() {
9
27
  return new Date().toISOString();
10
28
  }
@@ -45,6 +63,7 @@ export function createFlowRunState(scopeKey, flowId, executionState, jiraRef, la
45
63
  ensurePublicationRunId(executionState);
46
64
  const effectiveExecutionRouting = executionRouting ?? (launchProfile ? singleLaunchProfileExecutionRouting(launchProfile) : undefined);
47
65
  const effectiveLaunchProfile = launchProfile ?? effectiveExecutionRouting?.defaultRoute;
66
+ const continuation = inferContinuationMetadata(flowId, executionState);
48
67
  return {
49
68
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
50
69
  flowId,
@@ -56,6 +75,7 @@ export function createFlowRunState(scopeKey, flowId, executionState, jiraRef, la
56
75
  ...(effectiveLaunchProfile ? { launchProfile: effectiveLaunchProfile } : {}),
57
76
  ...(effectiveExecutionRouting ? { executionRouting: effectiveExecutionRouting, routingFingerprint: effectiveExecutionRouting.fingerprint } : {}),
58
77
  ...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
78
+ continuation,
59
79
  executionState: stripExecutionStatePayload(executionState),
60
80
  };
61
81
  }
@@ -66,6 +86,43 @@ function upgradeFlowRunStateV1(state) {
66
86
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
67
87
  ...(executionRouting ? { executionRouting, routingFingerprint: executionRouting.fingerprint } : {}),
68
88
  ...(executionRouting ? { selectedRoutingPreset: { kind: "custom", label: "Legacy launch profile" } } : {}),
89
+ continuation: {
90
+ continueEligible: false,
91
+ },
92
+ };
93
+ }
94
+ function upgradeFlowRunStateV2(state) {
95
+ return {
96
+ ...state,
97
+ schemaVersion: FLOW_STATE_SCHEMA_VERSION,
98
+ continuation: {
99
+ continueEligible: false,
100
+ },
101
+ };
102
+ }
103
+ function parseTerminationLocation(terminationReason) {
104
+ if (typeof terminationReason !== "string") {
105
+ return {};
106
+ }
107
+ const match = /^Stopped by ([^:]+):(.+)$/.exec(terminationReason.trim());
108
+ if (!match) {
109
+ return {};
110
+ }
111
+ const stopPhaseId = match[1];
112
+ const stopStepId = match[2];
113
+ return {
114
+ ...(stopPhaseId ? { stopPhaseId } : {}),
115
+ ...(stopStepId ? { stopStepId } : {}),
116
+ };
117
+ }
118
+ function inferContinuationMetadata(flowId, executionState) {
119
+ const stopLocation = parseTerminationLocation(executionState.terminationReason);
120
+ const continueEligible = CONTINUABLE_FLOW_KINDS.has(executionState.flowKind)
121
+ || (CONTINUABLE_PARENT_FLOW_IDS.has(flowId) && Boolean(stopLocation.stopPhaseId && stopLocation.stopStepId));
122
+ return {
123
+ continueEligible,
124
+ ...(stopLocation.stopPhaseId ? { stopPhaseId: stopLocation.stopPhaseId } : {}),
125
+ ...(stopLocation.stopStepId ? { stopStepId: stopLocation.stopStepId } : {}),
69
126
  };
70
127
  }
71
128
  function normalizeFlowRunState(raw, flowId, filePath) {
@@ -77,6 +134,9 @@ function normalizeFlowRunState(raw, flowId, filePath) {
77
134
  if (schemaVersion === 1) {
78
135
  state = upgradeFlowRunStateV1(raw);
79
136
  }
137
+ else if (schemaVersion === 2) {
138
+ state = upgradeFlowRunStateV2(raw);
139
+ }
80
140
  else if (schemaVersion === FLOW_STATE_SCHEMA_VERSION) {
81
141
  state = raw;
82
142
  }
@@ -97,6 +157,11 @@ function normalizeFlowRunState(raw, flowId, filePath) {
97
157
  state.executionRouting = executionRouting;
98
158
  state.routingFingerprint = executionRouting.fingerprint;
99
159
  }
160
+ const inferredContinuation = inferContinuationMetadata(state.flowId, state.executionState);
161
+ state.continuation = {
162
+ ...inferredContinuation,
163
+ continueEligible: inferredContinuation.continueEligible && state.continuation?.continueEligible !== false,
164
+ };
100
165
  return state;
101
166
  }
102
167
  export function loadFlowRunState(scopeKey, flowId) {
@@ -125,6 +190,7 @@ export function saveFlowRunState(state) {
125
190
  state.executionRouting = singleLaunchProfileExecutionRouting(state.launchProfile);
126
191
  state.routingFingerprint = state.executionRouting.fingerprint;
127
192
  }
193
+ state.continuation = inferContinuationMetadata(state.flowId, state.executionState);
128
194
  ensureScopeWorkspaceDir(state.scopeKey);
129
195
  writeFileSync(flowStateFile(state.scopeKey, state.flowId), `${JSON.stringify({
130
196
  ...state,
@@ -154,6 +220,53 @@ export function hasResumableFlowState(state) {
154
220
  }
155
221
  return state.executionState.phases.some((phase) => phase.steps.some((step) => step.status === "done" || step.status === "running"));
156
222
  }
223
+ function hasContinuableFlowState(state) {
224
+ if (!state) {
225
+ return false;
226
+ }
227
+ if (!state.executionState.terminated && state.status !== "completed") {
228
+ return false;
229
+ }
230
+ return state.continuation?.continueEligible === true;
231
+ }
232
+ export function classifyFlowLaunchAvailability(state) {
233
+ if (!state) {
234
+ return {
235
+ hasExistingState: false,
236
+ requiresExplicitChoice: false,
237
+ resume: { available: false, reason: "No saved state found." },
238
+ continue: { available: false, reason: "No saved state found." },
239
+ restart: { available: true, reason: "Start a fresh attempt." },
240
+ };
241
+ }
242
+ const resumeAvailable = hasResumableFlowState(state);
243
+ const continueAvailable = hasContinuableFlowState(state);
244
+ const availability = {
245
+ hasExistingState: true,
246
+ requiresExplicitChoice: resumeAvailable || continueAvailable,
247
+ resume: resumeAvailable
248
+ ? { available: true, reason: "Continue the interrupted execution state." }
249
+ : {
250
+ available: false,
251
+ reason: state.executionState.terminated || state.status === "completed"
252
+ ? "The saved run already terminated and cannot be resumed."
253
+ : "The saved state is not resumable.",
254
+ },
255
+ continue: continueAvailable
256
+ ? { available: true, reason: "Start the next iteration from the latest active artifacts." }
257
+ : {
258
+ available: false,
259
+ reason: state.schemaVersion < FLOW_STATE_SCHEMA_VERSION
260
+ ? "Legacy flow state lacks safe continuation metadata."
261
+ : "The saved run does not expose a continuable loop boundary.",
262
+ },
263
+ restart: {
264
+ available: true,
265
+ reason: "Archive the active attempt and start a fresh run.",
266
+ },
267
+ };
268
+ return availability;
269
+ }
157
270
  function normalizeStepState(step) {
158
271
  if (step.status !== "running") {
159
272
  return step;
@@ -237,3 +350,27 @@ export function prepareFlowStateForResume(state) {
237
350
  delete state.executionState.terminationReason;
238
351
  return state;
239
352
  }
353
+ export function prepareFlowStateForContinue(state, orderedPhases) {
354
+ state.status = "pending";
355
+ state.lastError = null;
356
+ state.currentStep = null;
357
+ const flowKind = state.executionState.flowKind;
358
+ if (CONTINUABLE_FLOW_KINDS.has(flowKind) || CONTINUABLE_DIRECT_FLOW_IDS.has(state.flowId)) {
359
+ state.executionState = {
360
+ ...state.executionState,
361
+ publicationRunId: randomUUID(),
362
+ terminated: false,
363
+ phases: orderedPhases.map(createPendingPhaseState),
364
+ };
365
+ delete state.executionState.terminationReason;
366
+ delete state.executionState.terminationOutcome;
367
+ return state;
368
+ }
369
+ const targetPhaseId = state.continuation?.stopPhaseId ?? parseTerminationLocation(state.executionState.terminationReason).stopPhaseId;
370
+ if (!targetPhaseId) {
371
+ throw new TaskRunnerError("Continue is impossible because the stop phase could not be determined safely. Use restart.");
372
+ }
373
+ rewindFlowRunStateToPhase(state, orderedPhases, targetPhaseId);
374
+ state.executionState.publicationRunId = randomUUID();
375
+ return state;
376
+ }