@mingchuno/agent-workflows 0.4.0 → 0.5.1

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.
@@ -0,0 +1,29 @@
1
+ const openingFence = /^ {0,3}```agent-workflows-validation[ \t]*$/;
2
+ const closingFence = /^ {0,3}```[ \t]*$/;
3
+ const profileName = /^[a-zA-Z0-9_-]+$/;
4
+ /** Resolve the one ticket-selected set against project-owned commands. */
5
+ export function selectValidation(body, project) {
6
+ const lines = body.split(/\r?\n/);
7
+ let profile;
8
+ for (let index = 0; index < lines.length; index++) {
9
+ if (!openingFence.test(lines[index]))
10
+ continue;
11
+ if (profile !== undefined)
12
+ throw new Error("Duplicate agent-workflows validation block");
13
+ const selected = lines[++index]?.trim();
14
+ if (!selected || !profileName.test(selected))
15
+ throw new Error("Invalid agent-workflows validation profile name");
16
+ if (!closingFence.test(lines[++index] ?? ""))
17
+ throw new Error("Validation block must contain one profile name and a closing fence");
18
+ if (!Object.hasOwn(project.validationProfiles, selected))
19
+ throw new Error(`Unknown validation profile: ${selected}`);
20
+ profile = selected;
21
+ }
22
+ return {
23
+ ...(profile === undefined ? {} : { profile }),
24
+ commands: [
25
+ ...project.validation,
26
+ ...(profile === undefined ? [] : project.validationProfiles[profile]),
27
+ ],
28
+ };
29
+ }
@@ -86,8 +86,9 @@ export class ExistingCheckout {
86
86
  const diff = await this.git(project, "diff", "HEAD", "--binary", "--no-ext-diff");
87
87
  const tracked = await this.git(project, "diff", "HEAD", "--no-renames", "--name-only", "-z");
88
88
  const untracked = await this.git(project, "ls-files", "--others", "--exclude-standard", "-z");
89
+ const untrackedPaths = new Set(untracked.split("\0").filter(Boolean));
89
90
  const paths = [
90
- ...new Set([...tracked.split("\0"), ...untracked.split("\0")].filter(Boolean)),
91
+ ...new Set([...tracked.split("\0"), ...untrackedPaths].filter(Boolean)),
91
92
  ].sort();
92
93
  const hash = createHash("sha256")
93
94
  .update(head)
@@ -105,7 +106,7 @@ export class ExistingCheckout {
105
106
  const content = await readFile(absolute);
106
107
  files[path] = fileStateDigest(content, stat.mode);
107
108
  hash.update(path).update(content).update(String(stat.mode));
108
- if (untracked.split("\0").includes(path))
109
+ if (untrackedPaths.has(path))
109
110
  fullDiff += `\n--- /dev/null\n+++ b/${path}\n${content.toString()}`;
110
111
  }
111
112
  catch (error) {
package/docs/api.md CHANGED
@@ -12,7 +12,7 @@ Omitting it preserves current-working-directory behavior for SDK callers.
12
12
  `promptBaseDirectory` retains its narrower role and, when both are supplied,
13
13
  overrides only relative prompt files.
14
14
 
15
- `start()` validates registration, acquires ownership, launches DBOS, registers concurrency-one project queues, and starts polling. `poll(projectId?)` performs an immediate scan. `pause(projectId)` stops new starts while active work continues. `resume(projectId)` refuses blocked checkouts. `stop(runId)` waits for the active invocation/process to end, or cancels queued work. `retry(runId)` requires a terminal failed/blocked/cancelled run and a clean checkout, then returns a new linked run ID. `recover(runId)` returns a new execution ID for publication recovery of the same run. `shutdown()` stops intake, cancels and awaits active work, closes DBOS and releases ownership.
15
+ `start()` validates registration, acquires ownership, launches DBOS, registers concurrency-one project queues, and starts polling. `poll(projectId?)` performs an immediate scan. `pause(projectId)` stops new starts while active work continues. `resume(projectId)` refuses blocked checkouts. `stop(runId)` waits for the active invocation/process to end, or cancels queued work. `retry(runId)` requires a terminal failed/blocked/cancelled run and a clean checkout, then returns a new linked run ID using its saved issue. `retry(runId, commandId, { refreshIssue: true })` fetches and validates the current hosted issue once during admission, saving it on the new run. `recover(runId)` returns a new execution ID for publication recovery of the same run. `shutdown()` stops intake, cancels and awaits active work, closes DBOS and releases ownership.
16
16
 
17
17
  Use `try/finally` to call `shutdown()`, including failed startup. A custom `workflowVersion` must change when its durable step order changes; finish existing work before replacing an incompatible version.
18
18
 
@@ -28,11 +28,44 @@ part of the public SDK `Configuration` type.
28
28
  | `pollIntervalMs` | 30000; minimum 100 |
29
29
  | `includeAgentCoAuthors` | `true`; append co-author trailers for providers whose writable invocations produced retained changes |
30
30
  | `validation` | Array of `{command,args,timeoutMs}`; no shell expansion; timeout defaults to 300000 ms |
31
+ | `validationProfiles` | Named, nonempty arrays of validation commands selectable by a ticket; defaults to `{}` |
31
32
  | `agent` | Required default profile |
32
33
  | `stages` | `implementation`, `publication`, `review`; each has optional `profile`, `prompt`, `promptFile`, `timeoutMs` |
33
34
 
34
35
  Issues are selected in ascending issue-number order within each intake scan. Deduplication persists across restarts. An explicit retry is a new numbered attempt linked through `retryOf`.
35
36
 
37
+ ## Ticket-selected validation
38
+
39
+ Define optional checks under a project in `validationProfiles` using the same
40
+ command format as `validation`:
41
+
42
+ ```json
43
+ {
44
+ "validation": [{ "command": "pnpm", "args": ["lint"] }],
45
+ "validationProfiles": {
46
+ "migration": [{ "command": "pnpm", "args": ["test:migrations"] }]
47
+ }
48
+ }
49
+ ```
50
+
51
+ An issue description selects one profile with a standalone fenced block:
52
+
53
+ ````markdown
54
+ ```agent-workflows-validation
55
+ migration
56
+ ```
57
+ ````
58
+
59
+ The runner executes the project's `validation` commands first, then the selected
60
+ profile's commands. Without the block, only the baseline runs. The block must
61
+ contain exactly one configured profile name (`A-Z`, `a-z`, digits, `_`, or `-`);
62
+ duplicate, malformed, and unknown selections fail the run before checkout
63
+ preparation or agent invocation. The issue body is saved with the run, so edits
64
+ to the hosted issue do not change an existing run. A plain retry creates a new
65
+ run from its recorded issue. Use `retry RUN --refresh-issue` (or `R` in the
66
+ monitor) to snapshot the current hosted issue and its validation selection for
67
+ the new run. Later issue edits do not change that run.
68
+
36
69
  Absolute configuration paths remain absolute. Effective state and checkout
37
70
  paths are normalized once during startup before safety and ownership checks, so
38
71
  a later working-directory change cannot redirect a running process. Validation
@@ -10,11 +10,12 @@ All commands accept `--config PATH` before the subcommand.
10
10
  | `run [--project ID ...]` | Start selected projects in the foreground |
11
11
  | `status [--json]` | Projects, runs and command outcomes |
12
12
  | `inspect RUN` | Full run and invocation/session records as JSON |
13
- | `logs RUN [--invocation ID]` | Local agent and validation artifacts |
13
+ | `logs RUN [--invocation ID]` | Local stage, agent and validation artifacts |
14
14
  | `pause PROJECT` / `resume PROJECT` | Queue an intake control command |
15
15
  | `stop RUN` | Queue cancellation; success means active local work has stopped |
16
16
  | `recover RUN` | Continue a failed publication step using completed checkpoints |
17
17
  | `retry RUN` | Queue an explicit new attempt after checkout validation |
18
+ | `retry RUN --refresh-issue` | Queue a new attempt using the current hosted issue |
18
19
  | `monitor [--notify]` | Attach an interactive terminal view; optionally alert on outcomes |
19
20
 
20
21
  Control commands return a command ID and `pending`; inspect `status --json` or the monitor for success/failure. With no runner, commands stay pending. Run and monitor are separate processes. Closing the monitor never cancels work. Ctrl-C on the runner stops intake, cancels active work, waits for process termination and releases ownership. Queued issues remain durable for the next start.
@@ -57,7 +58,7 @@ fallback. An overflowing document shows its visible line range in the heading.
57
58
  | Context | Keys |
58
59
  | --- | --- |
59
60
  | Dashboard | Left/Right project; Tab/Shift+Tab pane; Up/Down selection or scroll |
60
- | Details | Enter opens; Up/Down or PgUp/PgDn scroll; Esc returns; `l` opens the latest current-execution session, preferring a running session |
61
+ | Details | Enter opens; Up/Down or PgUp/PgDn scroll; Esc returns; `l` opens the latest current-execution session, or the stage diagnostic before a session exists |
61
62
  | Progress | `[`/`]` inspect step history; End follows latest event |
62
63
  | Sessions | `a` focuses session list; Up/Down selects invocation; `l` opens log |
63
64
  | Validation | `v` opens validation logs |
@@ -176,7 +177,7 @@ After a blocked/failed task that cannot be recovered:
176
177
  1. Read `inspect RUN`, logs, session IDs and the local Git diff.
177
178
  2. Establish that no worker/process group is still running. If startup reports an old PID or process journal, inspect that exact process and stop it before recovery. Never remove a live owner's lease.
178
179
  3. Preserve unfinished work on a developer-owned commit/branch or move it to a safe location. Resolve merge/rebase state yourself. Do not rely on DBOS to restore files.
179
- 4. Once the checkout is clean, request `retry RUN`. This creates a new attempt and branch from the configured base, keeping the old run and files/commits inspectable.
180
+ 4. Once the checkout is clean, request `retry RUN`. This creates a new attempt and branch from the configured base, keeping the old run and files/commits inspectable. Use `retry RUN --refresh-issue` to capture the current hosted issue description and validation selection in the new run. The hosted issue must retain its identity, be open, and have the required labels; its selected validation profile must be configured.
180
181
 
181
182
  A failed review after publication remains a failed automation attempt, even if its draft request exists. Explicit retry starts the full workflow as a new attempt; it does not silently modify the old request. Human review/merging remains separate. A stale review never claims coverage of a changed remote head.
182
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingchuno/agent-workflows",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local durable coding workflows on DBOS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,16 +0,0 @@
1
- import type { RunRecord } from "../domain.js";
2
- import type { ProjectState } from "../store.js";
3
- /** Display eligibility only; the runner still validates command admission. */
4
- export declare function actionAvailability({ run, project, projectRuns, pending, }: {
5
- run?: RunRecord;
6
- project?: ProjectState;
7
- projectRuns: RunRecord[];
8
- pending: boolean;
9
- }): {
10
- recoveryReason: string | undefined;
11
- available: {
12
- stop: boolean;
13
- retry: boolean;
14
- recover: boolean;
15
- };
16
- };
@@ -1,23 +0,0 @@
1
- import { recoveryUnavailable } from "../recovery.js";
2
- /** Display eligibility only; the runner still validates command admission. */
3
- export function actionAvailability({ run, project, projectRuns, pending, }) {
4
- const recoveryReason = run
5
- ? (recoveryUnavailable(run) ??
6
- project?.blocked ??
7
- (projectRuns.some((item) => item.taskKey === run.taskKey && item.attempt > run.attempt)
8
- ? "A newer attempt has superseded this run"
9
- : undefined))
10
- : undefined;
11
- return {
12
- recoveryReason,
13
- available: {
14
- stop: Boolean(run && !pending && ["queued", "running"].includes(run.outcome)),
15
- retry: Boolean(run &&
16
- !pending &&
17
- ["failed", "blocked", "cancelled"].includes(run.outcome) &&
18
- !projectRuns.some((item) => item.taskKey === run.taskKey &&
19
- ["queued", "running"].includes(item.outcome))),
20
- recover: Boolean(run && !pending && !recoveryReason),
21
- },
22
- };
23
- }