@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,89 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { devcrewDir, repositoryLockPath } from "./paths.js";
|
|
4
|
+
function metadataPath(cwd) {
|
|
5
|
+
return `${repositoryLockPath(cwd)}/lock.json`;
|
|
6
|
+
}
|
|
7
|
+
async function readMetadata(cwd) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(await readFile(metadataPath(cwd), "utf8"));
|
|
10
|
+
const pid = parsed.pid;
|
|
11
|
+
if (typeof parsed.ownerId !== "string" ||
|
|
12
|
+
typeof pid !== "number" ||
|
|
13
|
+
!Number.isInteger(pid) ||
|
|
14
|
+
typeof parsed.createdAt !== "string") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
ownerId: parsed.ownerId,
|
|
19
|
+
pid,
|
|
20
|
+
createdAt: parsed.createdAt,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function processIsLive(pid) {
|
|
28
|
+
if (pid <= 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return error.code === "EPERM";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function releaseRepositoryLock(cwd, ownerId) {
|
|
40
|
+
const metadata = await readMetadata(cwd);
|
|
41
|
+
if (metadata?.ownerId === ownerId) {
|
|
42
|
+
await rm(repositoryLockPath(cwd), { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function withRepositoryLock(cwd, action) {
|
|
46
|
+
await mkdir(devcrewDir(cwd), { recursive: true });
|
|
47
|
+
const ownerId = randomUUID();
|
|
48
|
+
try {
|
|
49
|
+
await mkdir(repositoryLockPath(cwd));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== "EEXIST") {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const metadata = await readMetadata(cwd);
|
|
56
|
+
if (metadata && processIsLive(metadata.pid)) {
|
|
57
|
+
throw new Error("A DevCrew operation is already in progress for this repository");
|
|
58
|
+
}
|
|
59
|
+
throw new Error("A stale DevCrew repository lock exists; use devcrew_recover for explicit recovery");
|
|
60
|
+
}
|
|
61
|
+
await writeFile(metadataPath(cwd), `${JSON.stringify({ ownerId, pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
62
|
+
try {
|
|
63
|
+
return await action();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await releaseRepositoryLock(cwd, ownerId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function recoverRepositoryLock(cwd) {
|
|
70
|
+
const lockPath = repositoryLockPath(cwd);
|
|
71
|
+
const metadata = await readMetadata(cwd);
|
|
72
|
+
if (!metadata) {
|
|
73
|
+
try {
|
|
74
|
+
await rm(lockPath, { recursive: true, force: false });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (processIsLive(metadata.pid)) {
|
|
85
|
+
throw new Error("Cannot recover a DevCrew repository lock held by a live process");
|
|
86
|
+
}
|
|
87
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
@@ -21,25 +21,29 @@ export function configPath(cwd) {
|
|
|
21
21
|
export function activeRunPath(cwd) {
|
|
22
22
|
return join(devcrewDir(cwd), "active-run.json");
|
|
23
23
|
}
|
|
24
|
+
export function repositoryLockPath(cwd) {
|
|
25
|
+
return join(devcrewDir(cwd), "lock");
|
|
26
|
+
}
|
|
24
27
|
export function standardsPath(cwd) {
|
|
25
28
|
return join(devcrewDir(cwd), "standards.md");
|
|
26
29
|
}
|
|
27
|
-
export function docsRoot(cwd) {
|
|
28
|
-
return join(cwd,
|
|
30
|
+
export function docsRoot(cwd, artifactDirectory = "docs/devcrew") {
|
|
31
|
+
return join(cwd, artifactDirectory);
|
|
29
32
|
}
|
|
30
|
-
export function docsRunDir(cwd, runId) {
|
|
31
|
-
return join(docsRoot(cwd), runId);
|
|
33
|
+
export function docsRunDir(cwd, runId, artifactDirectory = "docs/devcrew") {
|
|
34
|
+
return join(docsRoot(cwd, artifactDirectory), runId);
|
|
32
35
|
}
|
|
33
|
-
export function artifactPath(cwd, runId, artifact) {
|
|
36
|
+
export function artifactPath(cwd, runId, artifact, artifactDirectory = "docs/devcrew") {
|
|
34
37
|
const filenameByArtifact = {
|
|
35
38
|
requirements: "requirements.md",
|
|
36
39
|
architecture: "architecture.md",
|
|
37
40
|
"implementation-plan": "implementation-plan.md",
|
|
38
41
|
"implementation-review": "implementation-review.md",
|
|
42
|
+
"architecture-review": "architecture-review.md",
|
|
39
43
|
"test-report": "test-report.md",
|
|
40
44
|
acceptance: "acceptance.md",
|
|
41
45
|
};
|
|
42
|
-
return join(docsRunDir(cwd, runId), filenameByArtifact[artifact]);
|
|
46
|
+
return join(docsRunDir(cwd, runId, artifactDirectory), filenameByArtifact[artifact]);
|
|
43
47
|
}
|
|
44
48
|
export async function ensureRunDirectories(cwd, runId) {
|
|
45
49
|
await mkdir(runDir(cwd, runId), { recursive: true });
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { ensureRunDirectories, statePath } from "./paths.js";
|
|
4
|
+
import { GATES } from "./types.js";
|
|
5
|
+
function enabledGatesFromState(value) {
|
|
6
|
+
const configured = Array.isArray(value)
|
|
7
|
+
? value.filter((gate) => typeof gate === "string" && GATES.includes(gate))
|
|
8
|
+
: [...GATES];
|
|
9
|
+
return [...new Set([...configured, "implementation-review", "testing"])];
|
|
10
|
+
}
|
|
11
|
+
function roleResultsFromState(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return value.filter((role) => typeof role === "object" && role !== null).map((role) => ({
|
|
16
|
+
...role,
|
|
17
|
+
format: role.format === "structured" ? "structured" : "legacy",
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
4
20
|
export async function saveState(state) {
|
|
5
21
|
state.updatedAt = new Date().toISOString();
|
|
6
22
|
await ensureRunDirectories(state.cwd, state.runId);
|
|
@@ -14,17 +30,41 @@ export async function loadState(cwd, runId) {
|
|
|
14
30
|
const raw = await readFile(statePath(cwd, runId), "utf8");
|
|
15
31
|
const parsed = JSON.parse(raw);
|
|
16
32
|
const executionWorkspace = parsed.executionWorkspace;
|
|
33
|
+
const executionInstruction = parsed.executionInstruction;
|
|
34
|
+
const verificationWaiver = parsed.verificationWaiver;
|
|
17
35
|
return {
|
|
18
36
|
...parsed,
|
|
19
37
|
executionMode: parsed.executionMode ?? "plan",
|
|
38
|
+
executionPolicy: parsed.executionPolicy ?? "interactive-host",
|
|
39
|
+
roles: roleResultsFromState(parsed.roles),
|
|
40
|
+
enabledGates: enabledGatesFromState(parsed.enabledGates),
|
|
41
|
+
artifactDirectory: typeof parsed.artifactDirectory === "string" && parsed.artifactDirectory.trim().length > 0
|
|
42
|
+
? parsed.artifactDirectory
|
|
43
|
+
: "docs/devcrew",
|
|
20
44
|
executionWorkspace: executionWorkspace &&
|
|
21
45
|
typeof executionWorkspace.path === "string" &&
|
|
22
46
|
typeof executionWorkspace.baseCommit === "string"
|
|
23
47
|
? executionWorkspace
|
|
24
48
|
: undefined,
|
|
49
|
+
executionInstruction: executionInstruction &&
|
|
50
|
+
(executionInstruction.phase === "execution" || executionInstruction.phase === "testing") &&
|
|
51
|
+
typeof executionInstruction.workspacePath === "string" &&
|
|
52
|
+
typeof executionInstruction.instructions === "string" &&
|
|
53
|
+
typeof executionInstruction.createdAt === "string"
|
|
54
|
+
? executionInstruction
|
|
55
|
+
: undefined,
|
|
25
56
|
changedFiles: Array.isArray(parsed.changedFiles) ? parsed.changedFiles : [],
|
|
57
|
+
pendingQuestions: Array.isArray(parsed.pendingQuestions)
|
|
58
|
+
? parsed.pendingQuestions.filter((question) => typeof question === "string" && question.trim().length > 0)
|
|
59
|
+
: [],
|
|
26
60
|
implementationDiff: typeof parsed.implementationDiff === "string" ? parsed.implementationDiff : "",
|
|
27
61
|
verification: Array.isArray(parsed.verification) ? parsed.verification : [],
|
|
62
|
+
verificationStatus: parsed.verificationStatus === "passed" || parsed.verificationStatus === "failed" ? parsed.verificationStatus : "not_run",
|
|
63
|
+
verificationWaiver: verificationWaiver &&
|
|
64
|
+
typeof verificationWaiver.reason === "string" &&
|
|
65
|
+
typeof verificationWaiver.createdAt === "string"
|
|
66
|
+
? verificationWaiver
|
|
67
|
+
: undefined,
|
|
28
68
|
lintResults: Array.isArray(parsed.lintResults) ? parsed.lintResults : [],
|
|
29
69
|
};
|
|
30
70
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const WORKFLOW_MODES = ["feature", "greenfield"];
|
|
2
2
|
export const EXECUTION_MODES = ["plan", "apply"];
|
|
3
|
+
export const EXECUTION_POLICIES = ["interactive-host", "headless-restricted", "headless-unattended"];
|
|
3
4
|
export const HOSTS = ["codex", "claude"];
|
|
4
5
|
export const BACKENDS = ["codex", "claude", "local"];
|
|
5
6
|
export const PHASES = [
|
|
@@ -7,16 +8,18 @@ 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
|
];
|
|
14
|
-
export const GATES = ["requirements", "architecture", "implementation", "testing"];
|
|
16
|
+
export const GATES = ["requirements", "architecture", "implementation", "implementation-review", "testing"];
|
|
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
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARTIFACTS, BACKENDS, EXECUTION_MODES, GATES, HOSTS, WORKFLOW_MODES, } from "./types.js";
|
|
1
|
+
import { ARTIFACTS, BACKENDS, EXECUTION_MODES, EXECUTION_POLICIES, GATES, HOSTS, WORKFLOW_MODES, } from "./types.js";
|
|
2
2
|
function assertString(value, field) {
|
|
3
3
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4
4
|
throw new Error(`${field} must be a non-empty string`);
|
|
@@ -17,6 +17,9 @@ export function parseWorkflowMode(value) {
|
|
|
17
17
|
export function parseExecutionMode(value) {
|
|
18
18
|
return oneOf(value, "executionMode", EXECUTION_MODES);
|
|
19
19
|
}
|
|
20
|
+
export function parseExecutionPolicy(value) {
|
|
21
|
+
return oneOf(value, "executionPolicy", EXECUTION_POLICIES);
|
|
22
|
+
}
|
|
20
23
|
export function parseHost(value) {
|
|
21
24
|
return oneOf(value, "host", HOSTS);
|
|
22
25
|
}
|
|
@@ -50,3 +53,6 @@ export function parseFeedback(value) {
|
|
|
50
53
|
export function parseAnswer(value) {
|
|
51
54
|
return assertString(value, "answer");
|
|
52
55
|
}
|
|
56
|
+
export function parseWaiverReason(value) {
|
|
57
|
+
return assertString(value, "reason");
|
|
58
|
+
}
|
|
@@ -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";
|
|
@@ -3,7 +3,8 @@ import { ensureConfig } from "./config.js";
|
|
|
3
3
|
import { readArtifact, writeArtifact } from "./artifacts.js";
|
|
4
4
|
import { discoverStandards } from "./standards.js";
|
|
5
5
|
import { loadState, saveState } from "./store.js";
|
|
6
|
-
import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWorkflowMode, } from "./validation.js";
|
|
6
|
+
import { parseAnswer, parseArtifactName, parseBackend, parseCwd, parseExecutionMode, parseExecutionPolicy, parseFeedback, parseGate, parseHost, parseOptionalNote, parseRequest, parseRunId, parseWaiverReason, parseWorkflowMode, } from "./validation.js";
|
|
7
|
+
import { GATES } from "./types.js";
|
|
7
8
|
function now() {
|
|
8
9
|
return new Date().toISOString();
|
|
9
10
|
}
|
|
@@ -18,6 +19,8 @@ export function nextPhaseAfterGate(state, gate) {
|
|
|
18
19
|
return "implementation";
|
|
19
20
|
case "implementation":
|
|
20
21
|
return state.executionMode === "apply" ? "execution" : "testing";
|
|
22
|
+
case "implementation-review":
|
|
23
|
+
return "testing";
|
|
21
24
|
case "testing":
|
|
22
25
|
return "acceptance";
|
|
23
26
|
}
|
|
@@ -28,20 +31,32 @@ export function artifactForPhase(phase) {
|
|
|
28
31
|
architecture: "architecture",
|
|
29
32
|
implementation: "implementation-plan",
|
|
30
33
|
execution: "implementation-review",
|
|
34
|
+
review: "architecture-review",
|
|
31
35
|
testing: "test-report",
|
|
32
36
|
acceptance: "acceptance",
|
|
33
37
|
complete: "acceptance",
|
|
34
38
|
};
|
|
35
39
|
return artifactByPhase[phase];
|
|
36
40
|
}
|
|
37
|
-
export function gateForPhase(phase) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
export function gateForPhase(phase, enabledGates) {
|
|
42
|
+
const gate = phase === "review"
|
|
43
|
+
? "implementation-review"
|
|
44
|
+
: (phase === "requirements" ||
|
|
45
|
+
phase === "architecture" ||
|
|
46
|
+
phase === "implementation" ||
|
|
47
|
+
phase === "testing"
|
|
48
|
+
? phase
|
|
49
|
+
: undefined);
|
|
50
|
+
return gate && (!enabledGates || enabledGates.includes(gate)) ? gate : undefined;
|
|
51
|
+
}
|
|
52
|
+
function configuredGates(gates) {
|
|
53
|
+
const configured = new Set(gates.filter((gate) => GATES.includes(gate)));
|
|
54
|
+
configured.add("implementation-review");
|
|
55
|
+
configured.add("testing");
|
|
56
|
+
return [...configured];
|
|
42
57
|
}
|
|
43
58
|
function assertPendingCurrentGate(state, gate) {
|
|
44
|
-
const expected = gateForPhase(state.phase);
|
|
59
|
+
const expected = gateForPhase(state.phase, state.enabledGates);
|
|
45
60
|
if (expected !== gate) {
|
|
46
61
|
throw new Error(expected
|
|
47
62
|
? `Cannot act on ${gate} while current gate is ${expected}`
|
|
@@ -65,6 +80,8 @@ export async function startWorkflow(input, options = {}) {
|
|
|
65
80
|
const config = await ensureConfig(cwd);
|
|
66
81
|
const backend = input.backend ? parseBackend(input.backend) : config.defaultBackend === "host-preferred" ? host : config.defaultBackend;
|
|
67
82
|
const executionMode = input.executionMode ? parseExecutionMode(input.executionMode) : config.executionMode;
|
|
83
|
+
const executionPolicy = input.executionPolicy ? parseExecutionPolicy(input.executionPolicy) : "interactive-host";
|
|
84
|
+
const enabledGates = configuredGates(config.workflow.gates);
|
|
68
85
|
if (executionMode === "apply" && backend === "local") {
|
|
69
86
|
throw new Error("DevCrew apply mode requires a codex or claude backend; local is plan-only");
|
|
70
87
|
}
|
|
@@ -76,20 +93,25 @@ export async function startWorkflow(input, options = {}) {
|
|
|
76
93
|
host,
|
|
77
94
|
mode,
|
|
78
95
|
executionMode,
|
|
96
|
+
executionPolicy,
|
|
97
|
+
enabledGates,
|
|
98
|
+
artifactDirectory: config.workflow.artifactDirectory,
|
|
79
99
|
backend,
|
|
80
100
|
request,
|
|
81
101
|
phase: "requirements",
|
|
82
|
-
status: "awaiting_approval",
|
|
102
|
+
status: enabledGates.includes("requirements") ? "awaiting_approval" : "ready",
|
|
83
103
|
createdAt,
|
|
84
104
|
updatedAt: createdAt,
|
|
85
105
|
gates: {
|
|
86
|
-
requirements: "pending",
|
|
106
|
+
requirements: enabledGates.includes("requirements") ? "pending" : "not_started",
|
|
87
107
|
architecture: "not_started",
|
|
88
108
|
implementation: "not_started",
|
|
109
|
+
"implementation-review": "not_started",
|
|
89
110
|
testing: "not_started",
|
|
90
111
|
},
|
|
91
112
|
artifacts: {},
|
|
92
113
|
roles: [],
|
|
114
|
+
pendingQuestions: [],
|
|
93
115
|
answers: [],
|
|
94
116
|
approvals: [],
|
|
95
117
|
feedback: [],
|
|
@@ -97,6 +119,7 @@ export async function startWorkflow(input, options = {}) {
|
|
|
97
119
|
changedFiles: [],
|
|
98
120
|
implementationDiff: "",
|
|
99
121
|
verification: [],
|
|
122
|
+
verificationStatus: "not_run",
|
|
100
123
|
lintResults: [],
|
|
101
124
|
};
|
|
102
125
|
if (!options.skipArtifactWrite) {
|
|
@@ -109,7 +132,11 @@ export async function getWorkflowStatus(input) {
|
|
|
109
132
|
}
|
|
110
133
|
export async function continueWorkflow(input) {
|
|
111
134
|
const state = await getWorkflowStatus(input);
|
|
112
|
-
if (state.status === "awaiting_approval" ||
|
|
135
|
+
if (state.status === "awaiting_approval" ||
|
|
136
|
+
state.status === "awaiting_input" ||
|
|
137
|
+
state.status === "awaiting_execution" ||
|
|
138
|
+
state.status === "complete" ||
|
|
139
|
+
state.status === "aborted") {
|
|
113
140
|
return state;
|
|
114
141
|
}
|
|
115
142
|
if (state.phase === "acceptance") {
|
|
@@ -121,8 +148,14 @@ export async function continueWorkflow(input) {
|
|
|
121
148
|
if (state.phase === "execution") {
|
|
122
149
|
throw new Error("DevCrew execution phase requires orchestrated continuation");
|
|
123
150
|
}
|
|
124
|
-
const
|
|
151
|
+
const configuredGate = gateForPhase(state.phase);
|
|
152
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
125
153
|
if (!gate) {
|
|
154
|
+
if (configuredGate) {
|
|
155
|
+
state.phase = nextPhaseAfterGate(state, configuredGate);
|
|
156
|
+
state.status = "ready";
|
|
157
|
+
return saveState(state);
|
|
158
|
+
}
|
|
126
159
|
state.status = "complete";
|
|
127
160
|
return saveState(state);
|
|
128
161
|
}
|
|
@@ -131,6 +164,22 @@ export async function continueWorkflow(input) {
|
|
|
131
164
|
await writeCurrentArtifact(state);
|
|
132
165
|
return saveState(state);
|
|
133
166
|
}
|
|
167
|
+
export async function abortWorkflow(input) {
|
|
168
|
+
const state = await getWorkflowStatus(input);
|
|
169
|
+
if (state.status === "aborted") {
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
if (state.status === "complete") {
|
|
173
|
+
throw new Error("Completed DevCrew runs cannot be aborted");
|
|
174
|
+
}
|
|
175
|
+
state.abort = {
|
|
176
|
+
reason: parseWaiverReason(input.reason),
|
|
177
|
+
abortedAt: now(),
|
|
178
|
+
};
|
|
179
|
+
delete state.executionInstruction;
|
|
180
|
+
state.status = "aborted";
|
|
181
|
+
return saveState(state);
|
|
182
|
+
}
|
|
134
183
|
export async function validateWorkflowApproval(input) {
|
|
135
184
|
const state = await getWorkflowStatus(input);
|
|
136
185
|
const gate = parseGate(input.gate);
|
|
@@ -176,7 +225,16 @@ export async function rejectWorkflow(input) {
|
|
|
176
225
|
}
|
|
177
226
|
export async function answerWorkflow(input, options = {}) {
|
|
178
227
|
const state = await getWorkflowStatus(input);
|
|
179
|
-
const gate = gateForPhase(state.phase);
|
|
228
|
+
const gate = gateForPhase(state.phase, state.enabledGates);
|
|
229
|
+
if (state.status === "awaiting_input" && state.pendingQuestions.length > 0 && state.phase === "requirements") {
|
|
230
|
+
state.answers.push({
|
|
231
|
+
answer: parseAnswer(input.answer),
|
|
232
|
+
createdAt: now(),
|
|
233
|
+
});
|
|
234
|
+
state.pendingQuestions = [];
|
|
235
|
+
state.status = "ready";
|
|
236
|
+
return saveState(state);
|
|
237
|
+
}
|
|
180
238
|
if (state.status !== "awaiting_input" || !gate || state.gates[gate] !== "rejected") {
|
|
181
239
|
throw new Error("Workflow must be awaiting_input at a rejected current gate before recording an answer");
|
|
182
240
|
}
|
|
@@ -191,6 +249,23 @@ export async function answerWorkflow(input, options = {}) {
|
|
|
191
249
|
}
|
|
192
250
|
return saveState(state);
|
|
193
251
|
}
|
|
252
|
+
export async function waiveVerificationWorkflow(input) {
|
|
253
|
+
const state = await getWorkflowStatus(input);
|
|
254
|
+
if (state.executionMode !== "apply" ||
|
|
255
|
+
state.phase !== "testing" ||
|
|
256
|
+
state.status !== "awaiting_input" ||
|
|
257
|
+
state.gates.testing !== "rejected" ||
|
|
258
|
+
(state.verificationStatus !== "failed" && state.verificationStatus !== "not_run")) {
|
|
259
|
+
throw new Error("A verification waiver is only available after failed or missing apply-mode verification");
|
|
260
|
+
}
|
|
261
|
+
state.verificationWaiver = {
|
|
262
|
+
reason: parseWaiverReason(input.reason),
|
|
263
|
+
createdAt: now(),
|
|
264
|
+
};
|
|
265
|
+
state.gates.testing = "pending";
|
|
266
|
+
state.status = "awaiting_approval";
|
|
267
|
+
return saveState(state);
|
|
268
|
+
}
|
|
194
269
|
export async function getArtifact(input) {
|
|
195
270
|
const state = await getWorkflowStatus(input);
|
|
196
271
|
return readArtifact(state, parseArtifactName(input.name));
|