@mingchuno/agent-workflows 0.1.0

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 (62) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +74 -0
  3. package/dist/drizzle/0000_initial.sql +45 -0
  4. package/dist/drizzle/meta/0000_snapshot.json +264 -0
  5. package/dist/drizzle/meta/_journal.json +13 -0
  6. package/dist/src/adapters/agent-worker.d.ts +1 -0
  7. package/dist/src/adapters/agent-worker.js +16 -0
  8. package/dist/src/adapters/agents.d.ts +24 -0
  9. package/dist/src/adapters/agents.js +142 -0
  10. package/dist/src/adapters/hosting.d.ts +33 -0
  11. package/dist/src/adapters/hosting.js +275 -0
  12. package/dist/src/adapters/sdk-protocol.d.ts +43 -0
  13. package/dist/src/adapters/sdk-protocol.js +64 -0
  14. package/dist/src/cli.d.ts +2 -0
  15. package/dist/src/cli.js +175 -0
  16. package/dist/src/config.d.ts +224 -0
  17. package/dist/src/config.js +82 -0
  18. package/dist/src/db/locks.d.ts +4 -0
  19. package/dist/src/db/locks.js +14 -0
  20. package/dist/src/db/migrate.d.ts +1 -0
  21. package/dist/src/db/migrate.js +12 -0
  22. package/dist/src/db/migrations.d.ts +2 -0
  23. package/dist/src/db/migrations.js +22 -0
  24. package/dist/src/db/schema.d.ts +486 -0
  25. package/dist/src/db/schema.js +46 -0
  26. package/dist/src/domain.d.ts +133 -0
  27. package/dist/src/domain.js +24 -0
  28. package/dist/src/index.d.ts +8 -0
  29. package/dist/src/index.js +8 -0
  30. package/dist/src/operations.d.ts +35 -0
  31. package/dist/src/operations.js +378 -0
  32. package/dist/src/run-record.d.ts +7 -0
  33. package/dist/src/run-record.js +19 -0
  34. package/dist/src/runner.d.ts +47 -0
  35. package/dist/src/runner.js +370 -0
  36. package/dist/src/runtime/ownership.d.ts +8 -0
  37. package/dist/src/runtime/ownership.js +84 -0
  38. package/dist/src/runtime/process.d.ts +18 -0
  39. package/dist/src/runtime/process.js +98 -0
  40. package/dist/src/runtime/redaction.d.ts +8 -0
  41. package/dist/src/runtime/redaction.js +33 -0
  42. package/dist/src/store.d.ts +87 -0
  43. package/dist/src/store.js +355 -0
  44. package/dist/src/tui-data.d.ts +25 -0
  45. package/dist/src/tui-data.js +89 -0
  46. package/dist/src/tui.d.ts +5 -0
  47. package/dist/src/tui.js +69 -0
  48. package/dist/src/workspace.d.ts +16 -0
  49. package/dist/src/workspace.js +186 -0
  50. package/docs/acceptance.md +35 -0
  51. package/docs/api.md +64 -0
  52. package/docs/architecture.md +24 -0
  53. package/docs/configuration.md +41 -0
  54. package/docs/database.md +28 -0
  55. package/docs/operations.md +46 -0
  56. package/docs/providers.md +49 -0
  57. package/docs/releases.md +89 -0
  58. package/examples/config.ts +57 -0
  59. package/examples/custom-workflow.ts +32 -0
  60. package/examples/observe.ts +18 -0
  61. package/examples/run.ts +31 -0
  62. package/package.json +78 -0
@@ -0,0 +1,186 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { access, lstat, mkdir, readFile, realpath } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { BlockedError, } from "./domain.js";
5
+ import { command } from "./runtime/process.js";
6
+ export class ExistingCheckout {
7
+ processDirectories = new Map();
8
+ processDirectory(project) {
9
+ let directory = this.processDirectories.get(project.checkout);
10
+ if (!directory) {
11
+ directory = (async () => {
12
+ const result = await command("git", ["rev-parse", "--absolute-git-dir"], {
13
+ cwd: project.checkout,
14
+ });
15
+ const path = join(result.stdout.trim(), "agent-workflows-processes");
16
+ await mkdir(path, { recursive: true, mode: 0o700 });
17
+ return path;
18
+ })();
19
+ this.processDirectories.set(project.checkout, directory);
20
+ }
21
+ return directory;
22
+ }
23
+ async runGit(project, args, options = {}) {
24
+ const directory = await this.processDirectory(project);
25
+ return command("git", args, {
26
+ ...options,
27
+ cwd: project.checkout,
28
+ processFile: join(directory, `${randomUUID()}.process.json`),
29
+ });
30
+ }
31
+ async git(project, ...args) {
32
+ return (await this.runGit(project, args)).stdout.trimEnd();
33
+ }
34
+ async check(project) {
35
+ const top = await this.git(project, "rev-parse", "--show-toplevel");
36
+ if ((await realpath(top)) !== (await realpath(project.checkout)))
37
+ throw new BlockedError("Checkout must be the canonical repository root");
38
+ await this.assertNoOperation(project);
39
+ if (await this.git(project, "status", "--porcelain", "--untracked-files=all"))
40
+ throw new BlockedError("Checkout must be clean; unfinished files preserved");
41
+ }
42
+ async assertNoOperation(project) {
43
+ for (const marker of [
44
+ "MERGE_HEAD",
45
+ "CHERRY_PICK_HEAD",
46
+ "REVERT_HEAD",
47
+ "rebase-merge",
48
+ "rebase-apply",
49
+ "BISECT_LOG",
50
+ ]) {
51
+ const path = await this.git(project, "rev-parse", "--git-path", marker);
52
+ const exists = await access(path.startsWith("/") ? path : join(project.checkout, path)).then(() => true, () => false);
53
+ if (exists)
54
+ throw new BlockedError(`Unresolved Git operation: ${marker}`);
55
+ }
56
+ }
57
+ async prepare(project, branch, signal) {
58
+ signal?.throwIfAborted();
59
+ await this.check(project);
60
+ const original = await this.inspect(project);
61
+ await this.git(project, "check-ref-format", "--branch", project.baseBranch);
62
+ await this.git(project, "check-ref-format", "--branch", branch);
63
+ const existing = await this.runGit(project, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { signal, allowFailure: true });
64
+ if (existing.exitCode === 0)
65
+ throw new BlockedError(`Branch already exists: ${branch}`);
66
+ await this.runGit(project, ["fetch", "--no-tags", project.remote, project.baseBranch], { signal });
67
+ const base = await this.git(project, "rev-parse", "FETCH_HEAD");
68
+ await this.verify(project, original);
69
+ await this.runGit(project, ["switch", "-c", branch, base], { signal });
70
+ const prepared = await this.inspect(project);
71
+ if (prepared.paths.length)
72
+ throw new BlockedError("Checkout changed during branch preparation");
73
+ return prepared;
74
+ }
75
+ async inspect(project) {
76
+ await this.assertNoOperation(project);
77
+ const head = await this.git(project, "rev-parse", "HEAD");
78
+ const branch = await this.git(project, "symbolic-ref", "--short", "HEAD");
79
+ const status = await this.git(project, "status", "--porcelain=v1", "-z", "--untracked-files=all");
80
+ const diff = await this.git(project, "diff", "HEAD", "--binary", "--no-ext-diff");
81
+ const tracked = await this.git(project, "diff", "HEAD", "--no-renames", "--name-only", "-z");
82
+ const untracked = await this.git(project, "ls-files", "--others", "--exclude-standard", "-z");
83
+ const paths = [
84
+ ...new Set([...tracked.split("\0"), ...untracked.split("\0")].filter(Boolean)),
85
+ ].sort();
86
+ const hash = createHash("sha256")
87
+ .update(head)
88
+ .update(branch)
89
+ .update(status)
90
+ .update(diff);
91
+ let fullDiff = diff;
92
+ const files = {};
93
+ for (const path of paths) {
94
+ const absolute = join(project.checkout, path);
95
+ try {
96
+ const stat = await lstat(absolute);
97
+ if (stat.isSymbolicLink())
98
+ throw new BlockedError(`Changed symlink requires manual handling: ${path}`);
99
+ const content = await readFile(absolute);
100
+ files[path] = createHash("sha256").update(content).digest("hex");
101
+ hash.update(path).update(content).update(String(stat.mode));
102
+ if (untracked.split("\0").includes(path))
103
+ fullDiff += `\n--- /dev/null\n+++ b/${path}\n${content.toString()}`;
104
+ }
105
+ catch (error) {
106
+ if (error.code !== "ENOENT")
107
+ throw error;
108
+ hash.update(`deleted:${path}`);
109
+ files[path] = null;
110
+ }
111
+ }
112
+ return {
113
+ branch,
114
+ head,
115
+ fingerprint: hash.digest("hex"),
116
+ diff: fullDiff,
117
+ paths,
118
+ files,
119
+ };
120
+ }
121
+ async verify(project, expected) {
122
+ const actual = await this.inspect(project);
123
+ if (actual.fingerprint !== expected.fingerprint)
124
+ throw new BlockedError("Unexpected checkout mutation; files preserved");
125
+ }
126
+ async commit(project, expected, publication, runId, signal) {
127
+ signal?.throwIfAborted();
128
+ const current = await this.inspect(project);
129
+ if (current.head !== expected.head) {
130
+ const message = await this.git(project, "log", "-1", "--format=%B");
131
+ const parent = await this.git(project, "rev-parse", "HEAD^");
132
+ if (parent === expected.head &&
133
+ message.includes(`Agent-Workflows-Run: ${runId}`) &&
134
+ current.paths.length === 0) {
135
+ const changed = (await this.git(project, "diff", "HEAD^", "HEAD", "--no-renames", "--name-only", "-z"))
136
+ .split("\0")
137
+ .filter(Boolean)
138
+ .sort();
139
+ if (JSON.stringify(changed) !== JSON.stringify(expected.paths))
140
+ throw new BlockedError("Reconciled commit has an unexpected change set");
141
+ for (const [path, digest] of Object.entries(expected.files)) {
142
+ const actual = await readFile(join(project.checkout, path)).then((content) => createHash("sha256").update(content).digest("hex"), (error) => {
143
+ if (error.code === "ENOENT")
144
+ return null;
145
+ throw error;
146
+ });
147
+ if (actual !== digest)
148
+ throw new BlockedError("Reconciled commit content differs from validated changes");
149
+ }
150
+ return current.head;
151
+ }
152
+ throw new BlockedError("Cannot reconcile commit with recorded changes");
153
+ }
154
+ await this.verify(project, expected);
155
+ if (!expected.paths.length)
156
+ throw new BlockedError("No changes to commit");
157
+ await this.runGit(project, ["add", "--", ...expected.paths], { signal });
158
+ await this.runGit(project, [
159
+ "-c",
160
+ `user.name=${project.gitIdentity.name}`,
161
+ "-c",
162
+ `user.email=${project.gitIdentity.email}`,
163
+ "-c",
164
+ "core.hooksPath=/dev/null",
165
+ "commit",
166
+ "-m",
167
+ `${publication.commitMessage}\n\nAgent-Workflows-Run: ${runId}`,
168
+ ], { signal });
169
+ return this.git(project, "rev-parse", "HEAD");
170
+ }
171
+ async push(project, branch, head, signal) {
172
+ signal?.throwIfAborted();
173
+ await this.check(project);
174
+ if ((await this.git(project, "rev-parse", "HEAD")) !== head)
175
+ throw new BlockedError("Head changed before push");
176
+ const result = await this.runGit(project, ["ls-remote", "--heads", project.remote, `refs/heads/${branch}`], { signal });
177
+ const existing = result.stdout.trimEnd();
178
+ if (existing && existing.split(/\s/)[0] !== head)
179
+ throw new BlockedError("Remote branch changed; refusing overwrite");
180
+ if (!existing)
181
+ await this.runGit(project, ["push", project.remote, `${head}:refs/heads/${branch}`], { signal });
182
+ }
183
+ async release(project) {
184
+ await this.check(project);
185
+ }
186
+ }
@@ -0,0 +1,35 @@
1
+ # Phase 1 review guide
2
+
3
+ Implementation scope is the unchanged [specification in issue #1](https://github.com/mingchuno/agent-workflows/issues/1). Review the public contracts first, then the workspace/recovery boundaries. No task-specific worktrees, clones, merges or automatic review/fix loops are implemented.
4
+
5
+ | Acceptance area | Implementation / behavioral evidence |
6
+ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | Existing checkout, clean startup, branch policy and base transition | `ExistingCheckout`; real Git tests cover dirty files, collisions, unresolved Git operations, mutation detection and next-task base |
8
+ | Durable default workflow and extension | `Operations`, `defaultWorkflow`; PostgreSQL runner tests and the executable reporting-workflow example |
9
+ | Deduplication, one task/project, independent progress | Instance-qualified task keys, DBOS concurrency-one queues; all four agent/host combinations and multi-project tests |
10
+ | GitHub/GitLab intake and publication | Octokit/Gitbeaker contracts; pagination, PR exclusion, label queries, custom GitLab root/credentials, draft MR and inline review fixtures |
11
+ | Agent profiles, SDKs, prompts and sessions | Isolated SDK workers; profile/default isolation, supported-setting rejection, SDK argument/event contracts and invocation records |
12
+ | Validation and generated publication | Actual Git diff/fingerprints, schema-checked text; failed validation, malformed text, no-change and timeout tests |
13
+ | Recovery and external effects | Process-level interruptions after edits, commit, push, request creation and review publication; blocked ambiguous agent recovery and no duplicate publication effects |
14
+ | Cancellation and ownership | Real stubborn child process, journals, local leases, advisory locks; cancellation/reuse and duplicate-owner tests |
15
+ | Independent exact-revision review | Fresh sessions, published diff/head context, added-line mapping and stale-head rejection |
16
+ | Public observability and controls | Store query/subscription, retained sessions, timestamps, artifacts, credential redaction, CLI JSON and explicit retries |
17
+ | CLI/TUI | Init/error/help tests; keyboard step/session navigation, live state and action feedback; controls share application commands |
18
+ | SDK documentation | Quickstart, configuration, API, providers, operations/recovery and type-checked examples; reporting example runs with controlled adapters |
19
+
20
+ ## Local verification
21
+
22
+ Run `pnpm check`, `pnpm test`, `pnpm build`, and `pnpm lint`. Tests create a disposable PostgreSQL database unless `TEST_DATABASE_URL` is set, use real temporary Git repositories, and use controlled agents/hosting endpoints. `pnpm audit --prod` checks runtime dependency advisories. The GitHub Actions job supplies the same fixture boundary on Linux.
23
+
24
+ The implementation was verified locally on macOS with Node 24 and PostgreSQL 17. Remote CI has not run for this uncommitted working tree. No authenticated live Codex/Copilot model invocation, real GitHub publication or real GitLab instance smoke test was performed. Those require a deliberately chosen test repository and provider access and are separate from deterministic acceptance.
25
+
26
+ ## Deliberate operational boundaries
27
+
28
+ - Interrupted agent work blocks if ownership/state is ambiguous; it never starts another writer merely because DBOS recovered.
29
+ - Explicit retry creates a new attempt from the base after the developer restores a clean checkout. It preserves the previous run and branch and does not silently amend an existing PR/MR.
30
+ - Runtime context controls are supported only where exposed by the provider; unknown defaults remain unknown. Codex explicit-model validation depends on its local model catalog or an injected catalog.
31
+ - Git hooks are disabled for the application-authored task commit; configure required checks as validation commands. Changed symlinks and submodules require manual handling.
32
+ - The runner assumes exclusive checkout use. It detects boundary changes but does not sandbox arbitrary custom adapters or prevent every unrelated local tool write.
33
+ - macOS/Linux process groups are required. Windows startup fails explicitly.
34
+
35
+ Human review should concentrate on cancellation/ownership, recovery reconciliation and adapter permissions before enabling unattended work on a real repository. Completion means the automation has published its draft and review, not that the code is approved.
package/docs/api.md ADDED
@@ -0,0 +1,64 @@
1
+ # Public SDK API
2
+
3
+ Exports are in `src/index.ts`; the built package resolves to `dist/src/index.js`. Generated TypeScript declarations describe full argument/return types. Node ESM is required.
4
+
5
+ ## Runner and controls
6
+
7
+ `new Runner({config,databaseUrl,hosting,agents,workspace?,workflow?,workflowVersion?})` injects hosting/agent adapters and optionally a workspace strategy or workflow. `hosting(project)` returns a host-qualified adapter. `agents` maps provider names to adapters. The default workspace uses the existing checkout. One DBOS runtime runs per Node process; one runner owns each configuration and checkout.
8
+
9
+ `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. `shutdown()` stops intake, cancels and awaits active work, closes DBOS and releases ownership.
10
+
11
+ 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.
12
+
13
+ Polling runs independently per project, with at most one scan in flight per project. `start()` does not wait for scans to finish; `poll(projectId?)` waits for the requested scans, joining any already in flight. Shutdown drains outstanding scans without admitting their results or starting queued work.
14
+
15
+ Retry admission serializes competing requests per project. One request creates
16
+ the next attempt; another request for the same task fails while that retry is
17
+ queued or running. Replaying the same command ID returns its existing retry,
18
+ without rechecking the checkout or emitting events. A command ID cannot identify
19
+ retries of different runs. Retry creation, project unblocking and their events
20
+ commit together; failure preserves the blocked state.
21
+
22
+ ## Durable operations
23
+
24
+ `defaultWorkflow(operations)` composes the standard issue-to-review path. The runner calls your optional `workflow(operations)` inside an ordinary registered DBOS workflow. Use [DBOS TypeScript documentation](https://docs.dbos.dev/typescript/programming-guide) for workflow, step, queue and determinism semantics.
25
+
26
+ | Operation | Contract |
27
+ | ---------------------------------------- | ---------------------------------------------------------------------------- |
28
+ | `eligible()` | Re-fetch issue; true only if open and still labelled |
29
+ | `prepare()` | Require clean Git state; fetch base and create unique branch |
30
+ | `implement()` | Fresh implementation session; reject unexpected commits or branch changes |
31
+ | `validate()` | Run commands and verify unchanged diff; false means no change |
32
+ | `writePublication()` | Fresh writer session; schema-check and persist generated text |
33
+ | `commit()` / `push()` | Separate reconciled Git effects using the verified change set |
34
+ | `publish()` | Find existing request by branch before creating a draft |
35
+ | `review()` | Fresh read-only reviewer; exact published diff, head and validation evidence |
36
+ | `publishReview()` | Reject stale head; reconcile review marker; map valid added-line findings |
37
+ | `complete(outcome?)` | Require clean checkout and persist terminal outcome |
38
+ | `step(name, operation)` | Custom durable operation receiving current `RunRecord` |
39
+ | `invoke(name, stage, prompt, readOnly?)` | Custom agentic step with profile resolution and session history |
40
+
41
+ Put side effects inside `step`; custom effects must be idempotent or reconcile their own ambiguous results. A DBOS checkpoint does not snapshot a checkout. Returning from a custom workflow without calling a terminal operation is invalid. [Reporting workflow](../examples/custom-workflow.ts) inserts a validation report without editing provider code.
42
+
43
+ ## Extension contracts
44
+
45
+ `Workspace` separates `check`, `prepare`, `inspect`, `verify`, `commit`, `push` and `release`. `Snapshot` contains branch/head, changed paths, diff and a content fingerprint. Never implement release by discarding files. A future isolated workspace implementation can replace this interface without changing workflow composition.
46
+
47
+ `prepare`, `commit` and `push` receive an optional final `AbortSignal`. Custom workspaces must stop their subprocesses before settling a cancelled operation. The existing-checkout strategy journals Git processes under the Git directory; ownership acquisition rejects surviving process groups after a runner crash.
48
+
49
+ `AgentAdapter.validate(profile)` returns observable effective settings. `invoke(input)` receives working directory, prompt/skills, read-only intent, abort signal, stage `timeoutMs` and session/event callbacks. The abort signal also covers cancellation and time spent validating the profile. Call `session(id)` immediately when available. Await event persistence; invocation must not settle until its work has stopped. SDK adapters enforce process-group lifecycle; custom adapters must uphold the same contract. `processFile` is available for controlled subprocess ownership.
50
+
51
+ `HostingAdapter` provides issue pagination/revalidation, instance-qualified `identity`, change-request lookup/create, remote head, and idempotent review publication. `preflight` is optional. Reconciliation keys must be stable across response loss; providers must never infer successful publication from agent prose.
52
+
53
+ ## Query and event interface
54
+
55
+ `runner.store` is a `Store`. Independently construct `new Store(databaseUrl, runnerId)` to inspect history after shutdown; always `close()` it. `projects()`, `runs()`, `run(id)`, `invocations(runId)` and `events(afterSequence)` return persisted data. Events are ordered by monotonic sequence, paged at 1000; advance the cursor to retrieve more. `subscribe(listener,{after,intervalMs})` polls and returns an unsubscribe function. Delivery resumes from the caller's cursor; persist it if needed.
56
+
57
+ `Store.admitRetry` owns persisted retry admission. Runner supplies its checkout
58
+ and process safety check, which runs under the project lock for new admissions
59
+ only. This callback must not mutate Store records. Operator tools should use
60
+ `retry` commands or `Runner.retry`, preserving those safety checks.
61
+
62
+ Invocation records include project/run IDs, stable DBOS step ID and name, invocation ID, attempt, timestamps, requested/effective profile, provider, prompt/skill snapshots, artifact path and session state (`pending`, `available`, `unavailable`). Repeated custom steps retain separate invocations. A retry has a separate run record linked to its predecessor.
63
+
64
+ `request(kind,target)` queues the same `pause`, `resume`, `stop`, or `retry` commands used by the CLI/TUI; `commands()` reports pending/success/failure. A runner must be active to execute them. `finishCommand` and record-writing methods support adapters and custom workflows; operator tools should prefer commands over direct mutation.
@@ -0,0 +1,24 @@
1
+ # Architecture
2
+
3
+ DBOS owns workflow execution, durable steps and concurrency-one project queues. The runner only discovers candidates, dispatches durable identities and handles local operator commands. It does not introduce a workflow language or an interchangeable scheduler.
4
+
5
+ - `config.ts` / `domain.ts`: validated configuration, vocabulary and adapter contracts.
6
+ - `runner.ts`: local ownership, intake/deduplication, DBOS lifecycle and operator controls.
7
+ - `operations.ts`: reusable durable coding operations and the default workflow.
8
+ - `workspace.ts`: existing-checkout Git operations and change verification.
9
+ - `store.ts`: typed Drizzle queries for run, invocation, project, command and event records; `db/schema.ts` and `drizzle/` own the application schema and migrations.
10
+ - `adapters/`: provider clients and isolated SDK workers.
11
+ - `runtime/`: process groups, ownership journals and redacted logging.
12
+ - `cli.ts` / `tui.tsx`: shared command/query interfaces.
13
+
14
+ Application records live in `agent_workflows`; DBOS maintains its own execution schema in the same PostgreSQL database. Large streamed agent/validation logs live under the configured state directory, referenced by records. Agent calls are never automatically retried. Publication retries reconcile external state first. Clean terminal state and terminal workflow outcome are deliberately separate.
15
+
16
+ Node/PostgreSQL/Git are the only runtime infrastructure; providers require their normal local authentication. Zod, Commander, Ink/React, Drizzle/node-postgres, Pino, Octokit and Gitbeaker handle standard infrastructure. Drizzle ORM and Codex SDK are Apache-2.0; the other listed runtime libraries and Copilot SDK are MIT-licensed. Exact dependency versions are pinned by the lockfile. No custom HTTP client, CLI parser or terminal renderer is introduced.
17
+
18
+ The test boundary is the public runner/workflow API using real PostgreSQL, real temporary Git repositories and controlled adapters. Separate adapter contracts exercise SDK argument/event mapping and HTTP behavior. Process-level recovery tests terminate a runner after external effects and restart it against the same state. Runtime/provider smoke calls are intentionally separate from deterministic acceptance tests.
19
+
20
+ Phase 1 supports existing checkouts only. Higher per-project concurrency requires isolated workspaces and lifecycle design; changing the DBOS queue limit alone is unsafe.
21
+
22
+ ## Package boundary
23
+
24
+ Keep one package while the SDK, CLI and TUI share a runtime, schema and release cycle. `src/adapters`, `src/runtime` and `src/db` provide internal boundaries without workspace packages. Split into a monorepo when a separately deployed app or independently versioned package needs its own dependencies and build. `pnpm-workspace.yaml` currently configures installation policy only.
@@ -0,0 +1,41 @@
1
+ # Configuration
2
+
3
+ The CLI reads `agent-workflows.json`, or `--config PATH`. Unknown properties are rejected by Zod. Paths are local filesystem paths; checkout paths are canonicalized before ownership is acquired. Use absolute paths when launching from different working directories.
4
+
5
+ | Runner field | Default / meaning |
6
+ | ---------------- | --------------------------------------------------------------------------------------------------------- |
7
+ | `id` | Required stable letters/digits/underscore/hyphen identity; scopes records and queues |
8
+ | `databaseUrlEnv` | `AGENT_WORKFLOWS_DATABASE_URL`; environment variable containing a PostgreSQL connection URL with username |
9
+ | `stateDirectory` | `.agent-workflows`; persistent local artifact directory |
10
+ | `projects` | Nonempty array; duplicate IDs or canonical checkout roots are rejected |
11
+
12
+ | Project field | Default / meaning |
13
+ | ---------------------- | --------------------------------------------------------------------------------------------------- |
14
+ | `id`, `checkout` | Required stable identity and existing Git repository root |
15
+ | `hosting` | `provider` (`github`/`gitlab`), web `origin`, `repository`, and `tokenEnv`; no serialized tokens |
16
+ | `labels` | `['ready-for-agent']`; all labels must match |
17
+ | `baseBranch`, `remote` | `main`, `origin`; Git remote is independent of hosting API origin |
18
+ | `branchTemplate` | `agent/{issue}-{attempt}`; `{issue}` required; `{attempt}` and `{run}` supported |
19
+ | `pollIntervalMs` | 30000; minimum 100 |
20
+ | `gitIdentity` | Required `name` and `email`; used by application commits |
21
+ | `validation` | Array of `{command,args,timeoutMs}`; no shell expansion; timeout defaults to 300000 ms |
22
+ | `agent` | Required default profile |
23
+ | `stages` | `implementation`, `writing`, `review`; each has optional `profile`, `prompt`, `skills`, `timeoutMs` |
24
+
25
+ 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`.
26
+
27
+ ## Profiles
28
+
29
+ A profile has `provider`, optional `model`, optional `reasoningEffort`, and optional `context`. Each stage merges its profile over project defaults. Switching provider discards the old provider's settings, so incompatible defaults cannot leak between providers. Each invocation starts a fresh session, including repeated custom steps.
30
+
31
+ Codex rejects `context`: its TypeScript SDK exposes no runtime context-budget/compaction controls. Explicit Codex model/effort settings are checked against the local runtime's `models_cache.json`, or a supplied `new SDKAgent("codex", {models})` catalog. A missing catalog fails clearly; omitted settings use runtime defaults and remain `unknown` in effective-profile records. The cache can be stale; refresh it using the authenticated Codex runtime when a newly available model is rejected.
32
+
33
+ Copilot queries its SDK model catalog for explicit settings. Supported context controls are `backgroundCompactionThreshold` and `bufferExhaustionThreshold`, fractions of context utilization, with defaults 0.8 and 0.95 when a partial context object is supplied. Background must be lower than exhaustion. These control compaction, not the model's hard context-window capacity. Hard capacity is recorded separately in tokens when the model catalog supplies it.
34
+
35
+ See [checked examples](../examples/config.ts). Model IDs in `modelOverrides` are placeholders that must be replaced with available models. No provider silently clamps or substitutes explicit options.
36
+
37
+ ## Prompts and skills
38
+
39
+ `prompt` is literal stage text. `skills` contains paths to `SKILL.md` files, relative to the checkout or absolute. The runner snapshots their content and SHA-256 revision into invocation records and explicitly tells the agent to apply them. Copilot additionally receives skill directories; Codex receives explicit skill text. Automatic provider discovery is not assumed equivalent.
40
+
41
+ Stage timeout defaults to 30 minutes. Custom agent steps use the same `Stage` schema, profile resolution, snapshots, cancellation and session tracking as built-in stages. Credentials stay in environment variables/runtime authentication stores; do not place them in prompts or source files.
@@ -0,0 +1,28 @@
1
+ # Database maintenance
2
+
3
+ `src/db/schema.ts` defines application tables in `agent_workflows`. Drizzle generates versioned SQL in `drizzle/`; its migration history lives in `agent_workflows_migrations`. DBOS owns its execution schema separately.
4
+
5
+ ## Change the schema
6
+
7
+ 1. Edit `src/db/schema.ts`.
8
+ 2. Run `pnpm db:generate --name describe_change` and review the generated SQL and snapshot.
9
+ 3. Run `pnpm test` against disposable PostgreSQL before applying the change. The test role needs `CREATEDB` for isolated migration fixtures.
10
+ 4. Back up an existing database, stop runners, then run `AGENT_WORKFLOWS_DATABASE_URL=... pnpm db:migrate`.
11
+
12
+ Do not edit applied migration files or use schema push against existing data. Add a new migration for later changes. The migration command uses `AGENT_WORKFLOWS_DATABASE_URL`; with a custom `databaseUrlEnv`, supply that URL under this name for the command.
13
+
14
+ `Store.initialize()` also applies pending migrations, preserving automatic startup setup. A dedicated PostgreSQL advisory lock serializes migration attempts across processes. Build copies migrations into `dist/drizzle`, so the built runtime works outside the repository working directory; ship the whole `dist` directory.
15
+
16
+ The initial migration adopts the original application's identical tables using `IF NOT EXISTS`, preserving records and constraints. This supports the original schema, not arbitrary manually altered schemas; inspect and reconcile any local schema changes first.
17
+
18
+ ## Query boundary
19
+
20
+ `Store` uses typed Drizzle inserts, updates and selects. Concurrent JSON record patches use a transaction and row lock to preserve unrelated fields. Full-scope run and invocation queries sort their JSON fields in memory; introduce typed indexed columns if history size requires database pagination.
21
+
22
+ Retry admission locks the project row before checking task history, then commits
23
+ the new run, project unblocking and admission events in one transaction. The
24
+ project lock serializes requests even when they target different historical
25
+ attempts. Runner's checkout/process safety check runs while that lock is held;
26
+ command replay skips it and does not write new events.
27
+
28
+ `src/db/locks.ts` contains the only application driver SQL: fixed, parameterized PostgreSQL session-lock calls, which have no Drizzle query-builder equivalent. Generated migration SQL and the frozen legacy-schema test fixture are intentional SQL artifacts. No interpolated SQL template strings are used for record access.
@@ -0,0 +1,46 @@
1
+ # Operating the local runner
2
+
3
+ ## CLI and monitor
4
+
5
+ All commands accept `--config PATH` before the subcommand.
6
+
7
+ | Command | Effect |
8
+ | ---------------------------------- | --------------------------------------------------------------- |
9
+ | `init` | Write a starter config; refuse overwrite |
10
+ | `run [--project ID ...]` | Start selected projects in the foreground |
11
+ | `status [--json]` | Projects, runs and command outcomes |
12
+ | `inspect RUN` | Full run and invocation/session records as JSON |
13
+ | `logs RUN [--invocation ID]` | Local agent and validation artifacts |
14
+ | `pause PROJECT` / `resume PROJECT` | Queue an intake control command |
15
+ | `stop RUN` | Queue cancellation; success means active local work has stopped |
16
+ | `retry RUN` | Queue an explicit new attempt after checkout validation |
17
+ | `monitor` | Attach an interactive terminal view |
18
+
19
+ 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.
20
+
21
+ In the monitor, Left/Right selects a project, Up/Down a run, `[`/`]` a step/attempt event, Tab an agent invocation, `l` displays its log tail, `v` cycles validation logs, `p` pauses/resumes intake, `s` stops the selected run, `r` retries it, and `q` closes the view. Session IDs are displayed in full for terminal selection/copying. Outcomes, validation, profiles and session state use text as well as color. Noninteractive tools use `status --json` and `inspect`.
22
+
23
+ ## Ownership and recovery
24
+
25
+ Only the runner may edit or switch managed checkouts while it is active. PostgreSQL advisory locks protect runner/configuration and checkout identities. A local Git-directory lease also prevents runners using different databases from owning the same checkout. Worker/validation process-group journals prevent reuse while old work may still run.
26
+
27
+ Git process journals live in `<git-directory>/agent-workflows-processes/`, independently of the configured state directory. A surviving Git process blocks ownership acquisition after a crash. Stop requests propagate to active fetch, staging, commit and push commands; interrupted effects still require reconciliation.
28
+
29
+ Startup and phase boundaries check ownership assumptions, branch/head and actual changes. Unfinished files are never reset, cleaned, stashed or discarded automatically. Dirty files, unresolved Git operations, branch collisions, unexpected mutations and ambiguous agent recovery become inspectable blocked states. Other eligible projects continue.
30
+
31
+ Publication effects have independent DBOS checkpoints. A task commit carries `Agent-Workflows-Run`; commit recovery checks parent and marker, push recovery checks the remote ref, request creation checks the source branch, and review publication checks stable markers. Transient publication failures use bounded retries and reconciliation. Interrupted agent stages block rather than starting another writer.
32
+
33
+ After a blocked/failed task:
34
+
35
+ 1. Read `inspect RUN`, logs, session IDs and the local Git diff.
36
+ 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.
37
+ 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.
38
+ 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.
39
+
40
+ 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.
41
+
42
+ ## Evidence and limits
43
+
44
+ Validation records say exactly which command ran, when, its exit code and artifact path. No-change work skips publication. Generated commit/request text is validated and saved before Git/API writes. Logs, prompts and errors redact configured credentials and recognized secret environment values; this does not sanitize arbitrary repository content or secrets unknown to the runner.
45
+
46
+ Back up both PostgreSQL and the state directory if history/artifacts matter. The checkout and runtime session stores are separate local state. Losing them cannot be repaired from DBOS checkpoints alone. Do not change a custom workflow's step order or rename projects while its runs are pending; use a new workflow version and finish or explicitly resolve existing runs first.
@@ -0,0 +1,49 @@
1
+ # Prerequisites and provider capabilities
2
+
3
+ Install Node.js 22.12+, Git and PostgreSQL 17+. Native PostgreSQL and a local PostgreSQL container are both supported. Tests were developed against PostgreSQL 17 and Node 24 on macOS; A Linux fixture CI job is supplied; remote CI has not been run for this working tree. The database must be writable by the configured role so DBOS and the application can create their schemas.
4
+
5
+ Authenticate the selected local agent runtime before starting. Codex SDK uses the bundled Codex executable and its local authentication/configuration; Copilot SDK manages a local Copilot runtime. Subscription/access policy and model availability belong to those runtimes. See [official Codex SDK documentation](https://developers.openai.com/codex/sdk/) and [Copilot SDK documentation](https://github.com/github/copilot-sdk/tree/main/docs). Routine tests never invoke paid models.
6
+
7
+ | Capability | Codex | Copilot |
8
+ | ------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- |
9
+ | Implementation, writing, independent review | SDK fresh thread | SDK fresh session |
10
+ | Explicit model/effort validation | Runtime model cache or injected catalog | SDK model catalog or injected catalog |
11
+ | Runtime context controls | Rejected | Compaction/exhaustion utilization fractions |
12
+ | Structured publication/review | Application JSON schema validation | Application JSON schema validation |
13
+ | Session ID | `thread.started` event | Session creation response |
14
+ | Read-only stages | Read-only sandbox | Read permissions only; shell/write permission denied |
15
+ | Cancellation | Owned worker process group, TERM then KILL | Owned worker process group, TERM then KILL |
16
+ | Inspection/resumption | Saved thread ID; SDK `resumeThread` supports local persisted threads | Saved session ID; SDK `resumeSession` supports persisted sessions |
17
+
18
+ Codex implementation uses its workspace-write sandbox. Copilot implementation approves runtime permission requests and is not an operating-system sandbox; its read-only stages deny non-read permissions. Run selected issues and skills with the permissions of the local runtime account. Checkout checks detect unexpected changes at phase boundaries.
19
+
20
+ The runner deliberately does not automatically resume interrupted agent work. Runtime session existence does not establish whether old processes are still writing. Use runtime-specific tools/SDKs to inspect sessions after stopping the runner and establishing ownership; there is no universal session-opening command.
21
+
22
+ ## GitHub
23
+
24
+ Set `hosting.origin` to `https://github.com` or the GitHub Enterprise web origin. Repository is `owner/name`; the adapter derives the REST endpoint. `tokenEnv` names the environment variable holding a token authorized to read issues and create change requests/reviews. Git push uses the checkout's configured remote and Git credentials, independently of the API token. Do not embed credentials in remote URLs.
25
+
26
+ Issue intake paginates and excludes PRs. Publications default to draft. Reviews use the exact commit ID and right-side added lines where valid; other findings appear in the summary.
27
+
28
+ ## GitLab.com and self-hosted GitLab
29
+
30
+ Supported range: GitLab **17.x–19.x**, REST API v4. Startup checks the metadata endpoint and rejects versions outside this range. Controlled version-shaped HTTP fixtures test the shared issue/MR/review contract; no real GitLab instance has been smoke-tested in this implementation run.
31
+
32
+ ```json
33
+ {
34
+ "provider": "gitlab",
35
+ "origin": "https://git.example.com/gitlab",
36
+ "repository": "group/project",
37
+ "tokenEnv": "WORK_GITLAB_TOKEN"
38
+ }
39
+ ```
40
+
41
+ The relative root is preserved in API routes (`/gitlab/api/v4/...`). Provider identity includes origin, root and repository, so equal project IDs on different instances do not collide. Returned issue/MR links come from that instance. The API origin does not rewrite the Git push remote.
42
+
43
+ Use a token with API access and the project permissions needed to publish MRs and comments. Configure private CA trust with `NODE_EXTRA_CA_CERTS` before starting Node; TLS verification remains enabled. Plain HTTP is allowed only for localhost fixture servers. Never disable TLS verification globally.
44
+
45
+ Draft MRs use the supported `Draft:` title prefix. Revision-bound inline discussions use GitLab diff refs and reconcile per-finding markers; a final summary marker reconciles overall completion. See [GitLab merge requests](https://docs.gitlab.com/api/merge_requests/) and [discussions](https://docs.gitlab.com/api/discussions/) for provider semantics.
46
+
47
+ ## Explicit smoke verification
48
+
49
+ Real-provider smoke tests are opt-in manual runs: configure a disposable repository/issue, authenticated agent, hosting token, Git push access and validation; run one project; inspect the draft request, exact-head review and session records. This performs paid agent usage and real remote writes. The automated acceptance evidence is fixture-based, not a claim of live-provider compatibility or account access.
@@ -0,0 +1,89 @@
1
+ # Releases
2
+
3
+ One public npm package, `@mingchuno/agent-workflows`, contains the SDK and
4
+ `agent-workflows` CLI/TUI. Release Please opens a version/changelog PR from
5
+ Conventional Commits on `main`. Review and merge that PR to create its GitHub
6
+ release and publish to npm when enabled. The first release is `0.1.0`; fixes
7
+ bump patch, features bump minor, and breaking changes bump minor before 1.0.
8
+
9
+ ## GitHub setup
10
+
11
+ - Add this repository to the existing release GitHub App installation. It needs
12
+ Contents, Pull requests and Issues read/write. Store `RELEASE_APP_ID` and
13
+ `RELEASE_APP_PRIVATE_KEY` as repository Actions secrets. Tokens are scoped to
14
+ this repository; no webhook server is needed. App-created release PRs trigger CI.
15
+ - Enable squash merging with the PR title and description as the commit message.
16
+ Protect `main` with the `check` and `commitlint` checks after their first run.
17
+ Preserve `!` or `BREAKING CHANGE:` when squashing breaking changes.
18
+ - Create the `npm` environment with deployment branches restricted to `main`.
19
+ Required reviewers are optional: merging the release PR is the normal release
20
+ decision; environment reviewers would add a second publishing approval.
21
+ - Leave the repository variable `NPM_PUBLISH_ENABLED` unset until npm setup is
22
+ complete. Setting it to `true` enables subsequent automatic publications.
23
+
24
+ ## First publication and npm setup
25
+
26
+ The maintainer must control the `@mingchuno` npm scope and have account 2FA.
27
+ The package must exist before configuring its trusted publisher.
28
+
29
+ 1. Merge the implementation, let Release Please open its first release PR, then
30
+ review and merge the proposed `0.1.0` release. Keep automated publishing disabled.
31
+ 2. In a clean checkout of tag `v0.1.0`, install the pinned tools and dependencies,
32
+ then verify and test the package:
33
+
34
+ ```sh
35
+ mise trust
36
+ mise install
37
+ mise exec -- pnpm install --frozen-lockfile
38
+ mise exec -- pnpm verify
39
+ mise exec -- pnpm pack:smoke
40
+ ```
41
+
42
+ The smoke test leaves `.artifacts/mingchuno-agent-workflows-0.1.0.tgz`.
43
+ Authenticate interactively and publish that tested tarball:
44
+
45
+ ```sh
46
+ mise exec -- pnpm exec npm login --registry=https://registry.npmjs.org/
47
+ mise exec -- pnpm exec npm publish .artifacts/mingchuno-agent-workflows-0.1.0.tgz --access public --ignore-scripts --registry=https://registry.npmjs.org/
48
+ ```
49
+
50
+ 3. In the npm package settings, add a GitHub Actions trusted publisher:
51
+
52
+ | Field | Value |
53
+ | --- | --- |
54
+ | Owner | `mingchuno` |
55
+ | Repository | `agent-workflows` |
56
+ | Workflow filename | `release.yml` |
57
+ | Environment | `npm` |
58
+ | Publication permission | Allow direct `npm publish` |
59
+
60
+ 4. Set GitHub repository variable `NPM_PUBLISH_ENABLED=true`. The next releasable
61
+ change exercises OIDC; the manual first publication does not verify it.
62
+
63
+ The GitHub App manages releases; npm OIDC authenticates publication independently.
64
+ No `NPM_TOKEN` is required. The publishing job uses a GitHub-hosted runner,
65
+ `id-token: write`, and pinned npm 12. Public repository/package visibility enables
66
+ automatic provenance. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
67
+
68
+ ## Verification and recovery
69
+
70
+ `pnpm verify` runs type checking, linting, release guard tests and the existing
71
+ PostgreSQL-backed application tests. `pnpm pack:smoke` additionally needs registry
72
+ access: it installs the tarball in a temporary consumer, checks SDK imports and
73
+ TypeScript resolution, runs the CLI, and applies migrations to an isolated database.
74
+ Both commands use disposable local PostgreSQL or `TEST_DATABASE_URL`; its role must
75
+ be able to create test databases. Neither invokes paid agents or real hosting writes.
76
+
77
+ Publishing checks the tag, release SHA, package version and tested tarball integrity.
78
+ It publishes the same tarball that passed the smoke test. Release runs are serialized
79
+ and never cancelled by newer pushes.
80
+
81
+ After a publishing failure, choose **Re-run failed jobs** on that Actions run to
82
+ retain the release outputs and commit. An existing npm version is skipped only when
83
+ its integrity matches; differing contents require a new version. Re-running the
84
+ whole workflow or dispatching a new run may find no new release and skip publishing.
85
+ If Release Please failed after creating a tag, inspect the existing release before
86
+ recovering; never overwrite a published version or delete a release tag to retry.
87
+
88
+ To pause future publishing, unset `NPM_PUBLISH_ENABLED` or set it to `false`.
89
+ Inspect active runs separately: they may already have evaluated the switch.