@shenlee/devcrew 0.1.2 → 0.1.4
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 +15 -7
- package/README.zh-CN.md +12 -7
- package/dist/packages/adapters/src/index.js +204 -10
- package/dist/packages/core/src/active-run.js +13 -1
- package/dist/packages/core/src/artifacts.js +14 -4
- package/dist/packages/core/src/config.js +88 -17
- package/dist/packages/core/src/index.js +1 -0
- package/dist/packages/core/src/lock.js +89 -0
- package/dist/packages/core/src/paths.js +10 -6
- package/dist/packages/core/src/store.js +40 -0
- package/dist/packages/core/src/types.js +4 -1
- package/dist/packages/core/src/validation.js +7 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/packages/core/src/workflow.js +87 -12
- package/dist/packages/orchestrator/src/index.js +317 -18
- package/dist/packages/plugins/src/index.js +2 -32
- package/dist/packages/service/src/tools.js +117 -17
- package/docs/claude-code.md +18 -3
- package/docs/codex.md +22 -5
- package/docs/quickstart.md +1 -1
- package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
- package/docs/superpowers/plans/2026-07-14-p0-remediation.md +213 -0
- package/docs/superpowers/plans/2026-07-14-reliability-recovery.md +208 -0
- package/docs/superpowers/plans/2026-07-14-structured-role-results.md +231 -0
- package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
- package/docs/superpowers/specs/2026-07-14-p0-remediation-design.md +52 -0
- package/docs/superpowers/specs/2026-07-14-reliability-recovery-design.md +92 -0
- package/docs/superpowers/specs/2026-07-14-structured-role-results-design.md +96 -0
- package/docs/workflow.md +28 -4
- package/package.json +1 -1
- package/packages/adapters/src/index.ts +250 -13
- package/packages/core/src/active-run.ts +13 -1
- package/packages/core/src/artifacts.ts +16 -4
- package/packages/core/src/config.ts +97 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/lock.ts +104 -0
- package/packages/core/src/paths.ts +11 -6
- package/packages/core/src/store.ts +45 -1
- package/packages/core/src/types.ts +92 -2
- package/packages/core/src/validation.ts +10 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/core/src/workflow.ts +101 -11
- package/packages/orchestrator/src/index.ts +349 -19
- package/packages/plugins/src/index.ts +2 -44
- package/packages/service/src/tools.ts +126 -15
- package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/devcrew-codex/.mcp.json +1 -1
- package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
- package/scripts/smoke-codex-plugin.mjs +1 -1
- package/plugins/devcrew-codex/agents/architect.toml +0 -12
- package/plugins/devcrew-codex/agents/implementer.toml +0 -12
- package/plugins/devcrew-codex/agents/pm.toml +0 -13
- package/plugins/devcrew-codex/agents/tester.toml +0 -12
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { devcrewDir, repositoryLockPath } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
interface RepositoryLockMetadata {
|
|
7
|
+
ownerId: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function metadataPath(cwd: string): string {
|
|
13
|
+
return `${repositoryLockPath(cwd)}/lock.json`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readMetadata(cwd: string): Promise<RepositoryLockMetadata | undefined> {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(await readFile(metadataPath(cwd), "utf8")) as Partial<RepositoryLockMetadata>;
|
|
19
|
+
const pid = parsed.pid;
|
|
20
|
+
if (
|
|
21
|
+
typeof parsed.ownerId !== "string" ||
|
|
22
|
+
typeof pid !== "number" ||
|
|
23
|
+
!Number.isInteger(pid) ||
|
|
24
|
+
typeof parsed.createdAt !== "string"
|
|
25
|
+
) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ownerId: parsed.ownerId,
|
|
30
|
+
pid,
|
|
31
|
+
createdAt: parsed.createdAt,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function processIsLive(pid: number): boolean {
|
|
39
|
+
if (pid <= 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function releaseRepositoryLock(cwd: string, ownerId: string): Promise<void> {
|
|
51
|
+
const metadata = await readMetadata(cwd);
|
|
52
|
+
if (metadata?.ownerId === ownerId) {
|
|
53
|
+
await rm(repositoryLockPath(cwd), { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function withRepositoryLock<T>(cwd: string, action: () => Promise<T>): Promise<T> {
|
|
58
|
+
await mkdir(devcrewDir(cwd), { recursive: true });
|
|
59
|
+
const ownerId = randomUUID();
|
|
60
|
+
try {
|
|
61
|
+
await mkdir(repositoryLockPath(cwd));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const metadata = await readMetadata(cwd);
|
|
67
|
+
if (metadata && processIsLive(metadata.pid)) {
|
|
68
|
+
throw new Error("A DevCrew operation is already in progress for this repository");
|
|
69
|
+
}
|
|
70
|
+
throw new Error("A stale DevCrew repository lock exists; use devcrew_recover for explicit recovery");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await writeFile(
|
|
74
|
+
metadataPath(cwd),
|
|
75
|
+
`${JSON.stringify({ ownerId, pid: process.pid, createdAt: new Date().toISOString() } satisfies RepositoryLockMetadata, null, 2)}\n`,
|
|
76
|
+
"utf8",
|
|
77
|
+
);
|
|
78
|
+
try {
|
|
79
|
+
return await action();
|
|
80
|
+
} finally {
|
|
81
|
+
await releaseRepositoryLock(cwd, ownerId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function recoverRepositoryLock(cwd: string): Promise<boolean> {
|
|
86
|
+
const lockPath = repositoryLockPath(cwd);
|
|
87
|
+
const metadata = await readMetadata(cwd);
|
|
88
|
+
if (!metadata) {
|
|
89
|
+
try {
|
|
90
|
+
await rm(lockPath, { recursive: true, force: false });
|
|
91
|
+
return true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (processIsLive(metadata.pid)) {
|
|
100
|
+
throw new Error("Cannot recover a DevCrew repository lock held by a live process");
|
|
101
|
+
}
|
|
102
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
@@ -31,28 +31,33 @@ export function activeRunPath(cwd: string): string {
|
|
|
31
31
|
return join(devcrewDir(cwd), "active-run.json");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function repositoryLockPath(cwd: string): string {
|
|
35
|
+
return join(devcrewDir(cwd), "lock");
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
export function standardsPath(cwd: string): string {
|
|
35
39
|
return join(devcrewDir(cwd), "standards.md");
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
export function docsRoot(cwd: string): string {
|
|
39
|
-
return join(cwd,
|
|
42
|
+
export function docsRoot(cwd: string, artifactDirectory: string = "docs/devcrew"): string {
|
|
43
|
+
return join(cwd, artifactDirectory);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export function docsRunDir(cwd: string, runId: string): string {
|
|
43
|
-
return join(docsRoot(cwd), runId);
|
|
46
|
+
export function docsRunDir(cwd: string, runId: string, artifactDirectory: string = "docs/devcrew"): string {
|
|
47
|
+
return join(docsRoot(cwd, artifactDirectory), runId);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
export function artifactPath(cwd: string, runId: string, artifact: ArtifactName): string {
|
|
50
|
+
export function artifactPath(cwd: string, runId: string, artifact: ArtifactName, artifactDirectory: string = "docs/devcrew"): string {
|
|
47
51
|
const filenameByArtifact: Record<ArtifactName, string> = {
|
|
48
52
|
requirements: "requirements.md",
|
|
49
53
|
architecture: "architecture.md",
|
|
50
54
|
"implementation-plan": "implementation-plan.md",
|
|
51
55
|
"implementation-review": "implementation-review.md",
|
|
56
|
+
"architecture-review": "architecture-review.md",
|
|
52
57
|
"test-report": "test-report.md",
|
|
53
58
|
acceptance: "acceptance.md",
|
|
54
59
|
};
|
|
55
|
-
return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
|
|
60
|
+
return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export async function ensureRunDirectories(cwd: string, runId: string): Promise<void> {
|
|
@@ -2,7 +2,24 @@ import { readFile, rename, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { ensureRunDirectories, statePath } from "./paths.js";
|
|
5
|
-
import type
|
|
5
|
+
import { GATES, type GateName, type RoleResult, type RunState } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function enabledGatesFromState(value: unknown): GateName[] {
|
|
8
|
+
const configured = Array.isArray(value)
|
|
9
|
+
? value.filter((gate): gate is GateName => typeof gate === "string" && GATES.includes(gate as GateName))
|
|
10
|
+
: [...GATES];
|
|
11
|
+
return [...new Set<GateName>([...configured, "implementation-review", "testing"])];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function roleResultsFromState(value: unknown): RoleResult[] {
|
|
15
|
+
if (!Array.isArray(value)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return value.filter((role): role is RoleResult => typeof role === "object" && role !== null).map((role) => ({
|
|
19
|
+
...role,
|
|
20
|
+
format: role.format === "structured" ? "structured" : "legacy",
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export async function saveState(state: RunState): Promise<RunState> {
|
|
8
25
|
state.updatedAt = new Date().toISOString();
|
|
@@ -18,18 +35,45 @@ export async function loadState(cwd: string, runId: string): Promise<RunState> {
|
|
|
18
35
|
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
19
36
|
const parsed = JSON.parse(raw) as RunState;
|
|
20
37
|
const executionWorkspace = parsed.executionWorkspace;
|
|
38
|
+
const executionInstruction = parsed.executionInstruction;
|
|
39
|
+
const verificationWaiver = parsed.verificationWaiver;
|
|
21
40
|
return {
|
|
22
41
|
...parsed,
|
|
23
42
|
executionMode: parsed.executionMode ?? "plan",
|
|
43
|
+
executionPolicy: parsed.executionPolicy ?? "interactive-host",
|
|
44
|
+
roles: roleResultsFromState(parsed.roles),
|
|
45
|
+
enabledGates: enabledGatesFromState(parsed.enabledGates),
|
|
46
|
+
artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
|
|
47
|
+
? parsed.artifactDirectory
|
|
48
|
+
: "docs/devcrew",
|
|
24
49
|
executionWorkspace:
|
|
25
50
|
executionWorkspace &&
|
|
26
51
|
typeof executionWorkspace.path === "string" &&
|
|
27
52
|
typeof executionWorkspace.baseCommit === "string"
|
|
28
53
|
? executionWorkspace
|
|
29
54
|
: undefined,
|
|
55
|
+
executionInstruction:
|
|
56
|
+
executionInstruction &&
|
|
57
|
+
(executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
|
|
58
|
+
typeof executionInstruction.workspacePath === "string" &&
|
|
59
|
+
typeof executionInstruction.instructions === "string" &&
|
|
60
|
+
typeof executionInstruction.createdAt === "string"
|
|
61
|
+
? executionInstruction
|
|
62
|
+
: undefined,
|
|
30
63
|
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
64
|
+
pendingQuestions: Array.isArray(parsed.pendingQuestions)
|
|
65
|
+
? parsed.pendingQuestions.filter((question): question is string => typeof question === "string" && question.trim().length > 0)
|
|
66
|
+
: [],
|
|
31
67
|
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
32
68
|
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
69
|
+
verificationStatus:
|
|
70
|
+
parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
|
|
71
|
+
verificationWaiver:
|
|
72
|
+
verificationWaiver &&
|
|
73
|
+
typeof verificationWaiver.reason === "string" &&
|
|
74
|
+
typeof verificationWaiver.createdAt === "string"
|
|
75
|
+
? verificationWaiver
|
|
76
|
+
: undefined,
|
|
33
77
|
lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
|
|
34
78
|
};
|
|
35
79
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const WORKFLOW_MODES = ["feature", "greenfield"] as const;
|
|
2
2
|
export const EXECUTION_MODES = ["plan", "apply"] as const;
|
|
3
|
+
export const EXECUTION_POLICIES = ["interactive-host", "headless-restricted", "headless-unattended"] as const;
|
|
3
4
|
export const HOSTS = ["codex", "claude"] as const;
|
|
4
5
|
export const BACKENDS = ["codex", "claude", "local"] as const;
|
|
5
6
|
export const PHASES = [
|
|
@@ -7,35 +8,74 @@ export const PHASES = [
|
|
|
7
8
|
"architecture",
|
|
8
9
|
"implementation",
|
|
9
10
|
"execution",
|
|
11
|
+
"review",
|
|
10
12
|
"testing",
|
|
11
13
|
"acceptance",
|
|
12
14
|
"complete",
|
|
13
15
|
] as const;
|
|
14
|
-
export const GATES = ["requirements", "architecture", "implementation", "testing"] as const;
|
|
16
|
+
export const GATES = ["requirements", "architecture", "implementation", "implementation-review", "testing"] as const;
|
|
15
17
|
export const ARTIFACTS = [
|
|
16
18
|
"requirements",
|
|
17
19
|
"architecture",
|
|
18
20
|
"implementation-plan",
|
|
19
21
|
"implementation-review",
|
|
22
|
+
"architecture-review",
|
|
20
23
|
"test-report",
|
|
21
24
|
"acceptance",
|
|
22
25
|
] as const;
|
|
23
26
|
|
|
24
27
|
export type WorkflowMode = (typeof WORKFLOW_MODES)[number];
|
|
25
28
|
export type ExecutionMode = (typeof EXECUTION_MODES)[number];
|
|
29
|
+
export type ExecutionPolicy = (typeof EXECUTION_POLICIES)[number];
|
|
30
|
+
export type VerificationStatus = "not_run" | "passed" | "failed";
|
|
31
|
+
export type ArchitectureReviewDecision = "approved" | "changes_required";
|
|
26
32
|
export type Host = (typeof HOSTS)[number];
|
|
27
33
|
export type BackendName = (typeof BACKENDS)[number];
|
|
28
34
|
export type Phase = (typeof PHASES)[number];
|
|
29
35
|
export type GateName = (typeof GATES)[number];
|
|
30
36
|
export type ArtifactName = (typeof ARTIFACTS)[number];
|
|
31
37
|
export type GateStatus = "not_started" | "pending" | "approved" | "rejected";
|
|
32
|
-
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "complete";
|
|
38
|
+
export type RunStatus = "ready" | "awaiting_input" | "awaiting_approval" | "awaiting_execution" | "complete" | "aborted";
|
|
33
39
|
|
|
34
40
|
export interface RoleSection {
|
|
35
41
|
heading: string;
|
|
36
42
|
description: string;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
export type RoleResultFormat = "legacy" | "structured";
|
|
46
|
+
|
|
47
|
+
export interface CommandEvidence {
|
|
48
|
+
command: string;
|
|
49
|
+
exitCode: number;
|
|
50
|
+
output?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RoleQuestion {
|
|
54
|
+
id: string;
|
|
55
|
+
prompt: string;
|
|
56
|
+
context?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TestCaseEvidence {
|
|
60
|
+
id: string;
|
|
61
|
+
scenario: string;
|
|
62
|
+
type: "happy" | "edge" | "failure" | "regression";
|
|
63
|
+
expected: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StructuredRoleData {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
role: "pm" | "architect" | "implementer" | "tester";
|
|
69
|
+
summary: string;
|
|
70
|
+
risks: string[];
|
|
71
|
+
evidence: CommandEvidence[];
|
|
72
|
+
questions?: RoleQuestion[];
|
|
73
|
+
decisions?: string[];
|
|
74
|
+
reviewDecision?: ArchitectureReviewDecision;
|
|
75
|
+
changedFiles?: string[];
|
|
76
|
+
testCases?: TestCaseEvidence[];
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
export const ROLE_SECTIONS: Record<Exclude<RoleResult["role"], "conductor">, RoleSection[]> = {
|
|
40
80
|
pm: [
|
|
41
81
|
{ heading: "Functional Scope", description: "explicit In Scope and Out of Scope lists" },
|
|
@@ -88,6 +128,16 @@ export interface RoleResult {
|
|
|
88
128
|
summary: string;
|
|
89
129
|
markdown: string;
|
|
90
130
|
usedFallback: boolean;
|
|
131
|
+
format: RoleResultFormat;
|
|
132
|
+
structured?: StructuredRoleData;
|
|
133
|
+
questions?: string[];
|
|
134
|
+
reviewDecision?: ArchitectureReviewDecision;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ArchitectureReview {
|
|
138
|
+
decision: ArchitectureReviewDecision;
|
|
139
|
+
summary: string;
|
|
140
|
+
reviewedAt: string;
|
|
91
141
|
}
|
|
92
142
|
|
|
93
143
|
export interface WorkflowFeedback {
|
|
@@ -107,6 +157,16 @@ export interface WorkflowAnswer {
|
|
|
107
157
|
createdAt: string;
|
|
108
158
|
}
|
|
109
159
|
|
|
160
|
+
export interface VerificationWaiver {
|
|
161
|
+
reason: string;
|
|
162
|
+
createdAt: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface RunAbort {
|
|
166
|
+
reason: string;
|
|
167
|
+
abortedAt: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
110
170
|
// Reused for both verification and lint results — the command shape is identical.
|
|
111
171
|
export interface VerificationResult {
|
|
112
172
|
command: string;
|
|
@@ -121,6 +181,13 @@ export interface ExecutionWorkspace {
|
|
|
121
181
|
baseCommit: string;
|
|
122
182
|
}
|
|
123
183
|
|
|
184
|
+
export interface ExecutionInstruction {
|
|
185
|
+
phase: "execution" | "testing";
|
|
186
|
+
workspacePath: string;
|
|
187
|
+
instructions: string;
|
|
188
|
+
createdAt: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
124
191
|
export interface RunState {
|
|
125
192
|
version: 1;
|
|
126
193
|
runId: string;
|
|
@@ -128,7 +195,11 @@ export interface RunState {
|
|
|
128
195
|
host: Host;
|
|
129
196
|
mode: WorkflowMode;
|
|
130
197
|
executionMode: ExecutionMode;
|
|
198
|
+
executionPolicy: ExecutionPolicy;
|
|
199
|
+
enabledGates: GateName[];
|
|
200
|
+
artifactDirectory: string;
|
|
131
201
|
executionWorkspace?: ExecutionWorkspace;
|
|
202
|
+
executionInstruction?: ExecutionInstruction;
|
|
132
203
|
backend: BackendName;
|
|
133
204
|
request: string;
|
|
134
205
|
phase: Phase;
|
|
@@ -138,6 +209,8 @@ export interface RunState {
|
|
|
138
209
|
gates: Record<GateName, GateStatus>;
|
|
139
210
|
artifacts: Partial<Record<ArtifactName, string>>;
|
|
140
211
|
roles: RoleResult[];
|
|
212
|
+
architectureReview?: ArchitectureReview;
|
|
213
|
+
pendingQuestions: string[];
|
|
141
214
|
answers: WorkflowAnswer[];
|
|
142
215
|
approvals: WorkflowApproval[];
|
|
143
216
|
feedback: WorkflowFeedback[];
|
|
@@ -145,6 +218,9 @@ export interface RunState {
|
|
|
145
218
|
changedFiles: string[];
|
|
146
219
|
implementationDiff: string;
|
|
147
220
|
verification: VerificationResult[];
|
|
221
|
+
verificationStatus: VerificationStatus;
|
|
222
|
+
verificationWaiver?: VerificationWaiver;
|
|
223
|
+
abort?: RunAbort;
|
|
148
224
|
// VerificationResult is reused for lint output — same shape, different semantics.
|
|
149
225
|
lintResults: VerificationResult[];
|
|
150
226
|
}
|
|
@@ -156,6 +232,7 @@ export interface StartWorkflowInput {
|
|
|
156
232
|
request: string;
|
|
157
233
|
backend?: BackendName;
|
|
158
234
|
executionMode?: ExecutionMode;
|
|
235
|
+
executionPolicy?: ExecutionPolicy;
|
|
159
236
|
}
|
|
160
237
|
|
|
161
238
|
export interface RunRef {
|
|
@@ -177,6 +254,19 @@ export interface AnswerWorkflowInput extends RunRef {
|
|
|
177
254
|
answer: string;
|
|
178
255
|
}
|
|
179
256
|
|
|
257
|
+
export interface WaiveVerificationInput extends RunRef {
|
|
258
|
+
reason: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface AbortWorkflowInput extends RunRef {
|
|
262
|
+
reason: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface CompleteExecutionInput extends RunRef {
|
|
266
|
+
summary: string;
|
|
267
|
+
verification?: VerificationResult[];
|
|
268
|
+
}
|
|
269
|
+
|
|
180
270
|
export interface ArtifactRef extends RunRef {
|
|
181
271
|
name: ArtifactName;
|
|
182
272
|
}
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
ARTIFACTS,
|
|
3
3
|
BACKENDS,
|
|
4
4
|
EXECUTION_MODES,
|
|
5
|
+
EXECUTION_POLICIES,
|
|
5
6
|
GATES,
|
|
6
7
|
HOSTS,
|
|
7
8
|
WORKFLOW_MODES,
|
|
8
9
|
type ArtifactName,
|
|
9
10
|
type BackendName,
|
|
10
11
|
type ExecutionMode,
|
|
12
|
+
type ExecutionPolicy,
|
|
11
13
|
type GateName,
|
|
12
14
|
type Host,
|
|
13
15
|
type WorkflowMode,
|
|
@@ -35,6 +37,10 @@ export function parseExecutionMode(value: unknown): ExecutionMode {
|
|
|
35
37
|
return oneOf(value, "executionMode", EXECUTION_MODES);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export function parseExecutionPolicy(value: unknown): ExecutionPolicy {
|
|
41
|
+
return oneOf(value, "executionPolicy", EXECUTION_POLICIES);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
export function parseHost(value: unknown): Host {
|
|
39
45
|
return oneOf(value, "host", HOSTS);
|
|
40
46
|
}
|
|
@@ -77,3 +83,7 @@ export function parseFeedback(value: unknown): string {
|
|
|
77
83
|
export function parseAnswer(value: unknown): string {
|
|
78
84
|
return assertString(value, "answer");
|
|
79
85
|
}
|
|
86
|
+
|
|
87
|
+
export function parseWaiverReason(value: unknown): string {
|
|
88
|
+
return assertString(value, "reason");
|
|
89
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DEVCREW_VERSION = "0.1.
|
|
1
|
+
export const DEVCREW_VERSION = "0.1.4";
|
|
2
2
|
export const DEVCREW_NPM_PACKAGE = "@shenlee/devcrew";
|
|
@@ -10,16 +10,19 @@ import {
|
|
|
10
10
|
parseBackend,
|
|
11
11
|
parseCwd,
|
|
12
12
|
parseExecutionMode,
|
|
13
|
+
parseExecutionPolicy,
|
|
13
14
|
parseFeedback,
|
|
14
15
|
parseGate,
|
|
15
16
|
parseHost,
|
|
16
17
|
parseOptionalNote,
|
|
17
18
|
parseRequest,
|
|
18
19
|
parseRunId,
|
|
20
|
+
parseWaiverReason,
|
|
19
21
|
parseWorkflowMode,
|
|
20
22
|
} from "./validation.js";
|
|
21
23
|
import type {
|
|
22
24
|
AnswerWorkflowInput,
|
|
25
|
+
AbortWorkflowInput,
|
|
23
26
|
ApproveWorkflowInput,
|
|
24
27
|
ArtifactName,
|
|
25
28
|
ArtifactReadResult,
|
|
@@ -29,7 +32,9 @@ import type {
|
|
|
29
32
|
RunRef,
|
|
30
33
|
RunState,
|
|
31
34
|
StartWorkflowInput,
|
|
35
|
+
WaiveVerificationInput,
|
|
32
36
|
} from "./types.js";
|
|
37
|
+
import { GATES } from "./types.js";
|
|
33
38
|
|
|
34
39
|
function now(): string {
|
|
35
40
|
return new Date().toISOString();
|
|
@@ -47,6 +52,8 @@ export function nextPhaseAfterGate(state: RunState, gate: GateName): RunState["p
|
|
|
47
52
|
return "implementation";
|
|
48
53
|
case "implementation":
|
|
49
54
|
return state.executionMode === "apply" ? "execution" : "testing";
|
|
55
|
+
case "implementation-review":
|
|
56
|
+
return "testing";
|
|
50
57
|
case "testing":
|
|
51
58
|
return "acceptance";
|
|
52
59
|
}
|
|
@@ -58,6 +65,7 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
|
58
65
|
architecture: "architecture",
|
|
59
66
|
implementation: "implementation-plan",
|
|
60
67
|
execution: "implementation-review",
|
|
68
|
+
review: "architecture-review",
|
|
61
69
|
testing: "test-report",
|
|
62
70
|
acceptance: "acceptance",
|
|
63
71
|
complete: "acceptance",
|
|
@@ -65,15 +73,31 @@ export function artifactForPhase(phase: RunState["phase"]): ArtifactName {
|
|
|
65
73
|
return artifactByPhase[phase];
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
export function gateForPhase(phase: RunState["phase"]): GateName | undefined {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
export function gateForPhase(phase: RunState["phase"], enabledGates?: readonly GateName[]): GateName | undefined {
|
|
77
|
+
const gate = phase === "review"
|
|
78
|
+
? "implementation-review"
|
|
79
|
+
: (
|
|
80
|
+
phase === "requirements" ||
|
|
81
|
+
phase === "architecture" ||
|
|
82
|
+
phase === "implementation" ||
|
|
83
|
+
phase === "testing"
|
|
84
|
+
? phase
|
|
85
|
+
: undefined
|
|
86
|
+
);
|
|
87
|
+
return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function configuredGates(gates: readonly GateName[]): GateName[] {
|
|
91
|
+
const configured = new Set(
|
|
92
|
+
gates.filter((gate): gate is GateName => GATES.includes(gate)),
|
|
93
|
+
);
|
|
94
|
+
configured.add("implementation-review");
|
|
95
|
+
configured.add("testing");
|
|
96
|
+
return [...configured];
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
function assertPendingCurrentGate(state: RunState, gate: GateName): void {
|
|
76
|
-
const expected = gateForPhase(state.phase);
|
|
100
|
+
const expected = gateForPhase(state.phase, state.enabledGates);
|
|
77
101
|
if (expected !== gate) {
|
|
78
102
|
throw new Error(
|
|
79
103
|
expected
|
|
@@ -105,6 +129,8 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
105
129
|
const config = await ensureConfig(cwd);
|
|
106
130
|
const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
|
|
107
131
|
const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
|
|
132
|
+
const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
|
|
133
|
+
const enabledGates = configuredGates(config.workflow.gates);
|
|
108
134
|
if (executionMode === "apply" && backend === "local") {
|
|
109
135
|
throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
|
|
110
136
|
}
|
|
@@ -116,20 +142,25 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
116
142
|
host,
|
|
117
143
|
mode,
|
|
118
144
|
executionMode,
|
|
145
|
+
executionPolicy,
|
|
146
|
+
enabledGates,
|
|
147
|
+
artifactDirectory: config.workflow.artifactDirectory,
|
|
119
148
|
backend,
|
|
120
149
|
request,
|
|
121
150
|
phase: "requirements",
|
|
122
|
-
status: "awaiting_approval",
|
|
151
|
+
status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
|
|
123
152
|
createdAt,
|
|
124
153
|
updatedAt: createdAt,
|
|
125
154
|
gates: {
|
|
126
|
-
requirements: "pending",
|
|
155
|
+
requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
|
|
127
156
|
architecture: "not_started",
|
|
128
157
|
implementation: "not_started",
|
|
158
|
+
"implementation-review": "not_started",
|
|
129
159
|
testing: "not_started",
|
|
130
160
|
},
|
|
131
161
|
artifacts: {},
|
|
132
162
|
roles: [],
|
|
163
|
+
pendingQuestions: [],
|
|
133
164
|
answers: [],
|
|
134
165
|
approvals: [],
|
|
135
166
|
feedback: [],
|
|
@@ -137,6 +168,7 @@ export async function startWorkflow(input: StartWorkflowInput, options: Workflow
|
|
|
137
168
|
changedFiles: [],
|
|
138
169
|
implementationDiff: "",
|
|
139
170
|
verification: [],
|
|
171
|
+
verificationStatus: "not_run",
|
|
140
172
|
lintResults: [],
|
|
141
173
|
};
|
|
142
174
|
|
|
@@ -152,7 +184,13 @@ export async function getWorkflowStatus(input: RunRef): Promise<RunState> {
|
|
|
152
184
|
|
|
153
185
|
export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
154
186
|
const state = await getWorkflowStatus(input);
|
|
155
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
state.status === "awaiting_approval" ||
|
|
189
|
+
state.status === "awaiting_input" ||
|
|
190
|
+
state.status === "awaiting_execution" ||
|
|
191
|
+
state.status === "complete" ||
|
|
192
|
+
state.status === "aborted"
|
|
193
|
+
) {
|
|
156
194
|
return state;
|
|
157
195
|
}
|
|
158
196
|
|
|
@@ -167,8 +205,14 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
167
205
|
throw new Error("DevCrew execution phase requires orchestrated continuation");
|
|
168
206
|
}
|
|
169
207
|
|
|
170
|
-
const
|
|
208
|
+
const configuredGate = gateForPhase(state.phase);
|
|
209
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
171
210
|
if (!gate) {
|
|
211
|
+
if (configuredGate) {
|
|
212
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
213
|
+
state.status = "ready";
|
|
214
|
+
return saveState(state);
|
|
215
|
+
}
|
|
172
216
|
state.status = "complete";
|
|
173
217
|
return saveState(state);
|
|
174
218
|
}
|
|
@@ -178,6 +222,23 @@ export async function continueWorkflow(input: RunRef): Promise<RunState> {
|
|
|
178
222
|
return saveState(state);
|
|
179
223
|
}
|
|
180
224
|
|
|
225
|
+
export async function abortWorkflow(input: AbortWorkflowInput): Promise<RunState> {
|
|
226
|
+
const state = await getWorkflowStatus(input);
|
|
227
|
+
if (state.status === "aborted") {
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
if (state.status === "complete") {
|
|
231
|
+
throw new Error("Completed DevCrew runs cannot be aborted");
|
|
232
|
+
}
|
|
233
|
+
state.abort = {
|
|
234
|
+
reason: parseWaiverReason(input.reason),
|
|
235
|
+
abortedAt: now(),
|
|
236
|
+
};
|
|
237
|
+
delete state.executionInstruction;
|
|
238
|
+
state.status = "aborted";
|
|
239
|
+
return saveState(state);
|
|
240
|
+
}
|
|
241
|
+
|
|
181
242
|
export async function validateWorkflowApproval(input: ApproveWorkflowInput): Promise<RunState> {
|
|
182
243
|
const state = await getWorkflowStatus(input);
|
|
183
244
|
const gate = parseGate(input.gate);
|
|
@@ -227,7 +288,16 @@ export async function rejectWorkflow(input: RejectWorkflowInput): Promise<RunSta
|
|
|
227
288
|
|
|
228
289
|
export async function answerWorkflow(input: AnswerWorkflowInput, options: WorkflowMutationOptions = {}): Promise<RunState> {
|
|
229
290
|
const state = await getWorkflowStatus(input);
|
|
230
|
-
const gate = gateForPhase(state.phase);
|
|
291
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
292
|
+
if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
|
|
293
|
+
state.answers.push({
|
|
294
|
+
answer: parseAnswer(input.answer),
|
|
295
|
+
createdAt: now(),
|
|
296
|
+
});
|
|
297
|
+
state.pendingQuestions = [];
|
|
298
|
+
state.status = "ready";
|
|
299
|
+
return saveState(state);
|
|
300
|
+
}
|
|
231
301
|
if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
|
|
232
302
|
throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
|
|
233
303
|
}
|
|
@@ -243,6 +313,26 @@ export async function answerWorkflow(input: AnswerWorkflowInput, options: Workfl
|
|
|
243
313
|
return saveState(state);
|
|
244
314
|
}
|
|
245
315
|
|
|
316
|
+
export async function waiveVerificationWorkflow(input: WaiveVerificationInput): Promise<RunState> {
|
|
317
|
+
const state = await getWorkflowStatus(input);
|
|
318
|
+
if (
|
|
319
|
+
state.executionMode !== "apply" ||
|
|
320
|
+
state.phase !== "testing" ||
|
|
321
|
+
state.status !== "awaiting_input" ||
|
|
322
|
+
state.gates.testing !== "rejected" ||
|
|
323
|
+
(state.verificationStatus !== "failed" && state.verificationStatus !== "not_run")
|
|
324
|
+
) {
|
|
325
|
+
throw new Error("A verification waiver is only available after failed or missing apply-mode verification");
|
|
326
|
+
}
|
|
327
|
+
state.verificationWaiver = {
|
|
328
|
+
reason: parseWaiverReason(input.reason),
|
|
329
|
+
createdAt: now(),
|
|
330
|
+
};
|
|
331
|
+
state.gates.testing = "pending";
|
|
332
|
+
state.status = "awaiting_approval";
|
|
333
|
+
return saveState(state);
|
|
334
|
+
}
|
|
335
|
+
|
|
246
336
|
export async function getArtifact(input: ArtifactRef): Promise<ArtifactReadResult> {
|
|
247
337
|
const state = await getWorkflowStatus(input);
|
|
248
338
|
return readArtifact(state, parseArtifactName(input.name));
|