@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.
Files changed (53) hide show
  1. package/README.md +15 -7
  2. package/README.zh-CN.md +12 -7
  3. package/dist/packages/adapters/src/index.js +204 -10
  4. package/dist/packages/core/src/active-run.js +13 -1
  5. package/dist/packages/core/src/artifacts.js +14 -4
  6. package/dist/packages/core/src/config.js +88 -17
  7. package/dist/packages/core/src/index.js +1 -0
  8. package/dist/packages/core/src/lock.js +89 -0
  9. package/dist/packages/core/src/paths.js +10 -6
  10. package/dist/packages/core/src/store.js +40 -0
  11. package/dist/packages/core/src/types.js +4 -1
  12. package/dist/packages/core/src/validation.js +7 -1
  13. package/dist/packages/core/src/version.js +1 -1
  14. package/dist/packages/core/src/workflow.js +87 -12
  15. package/dist/packages/orchestrator/src/index.js +317 -18
  16. package/dist/packages/plugins/src/index.js +2 -32
  17. package/dist/packages/service/src/tools.js +117 -17
  18. package/docs/claude-code.md +18 -3
  19. package/docs/codex.md +22 -5
  20. package/docs/quickstart.md +1 -1
  21. package/docs/superpowers/plans/2026-07-13-safety-semantics.md +313 -0
  22. package/docs/superpowers/plans/2026-07-14-p0-remediation.md +213 -0
  23. package/docs/superpowers/plans/2026-07-14-reliability-recovery.md +208 -0
  24. package/docs/superpowers/plans/2026-07-14-structured-role-results.md +231 -0
  25. package/docs/superpowers/specs/2026-07-13-execution-boundaries-design.md +126 -0
  26. package/docs/superpowers/specs/2026-07-14-p0-remediation-design.md +52 -0
  27. package/docs/superpowers/specs/2026-07-14-reliability-recovery-design.md +92 -0
  28. package/docs/superpowers/specs/2026-07-14-structured-role-results-design.md +96 -0
  29. package/docs/workflow.md +28 -4
  30. package/package.json +1 -1
  31. package/packages/adapters/src/index.ts +250 -13
  32. package/packages/core/src/active-run.ts +13 -1
  33. package/packages/core/src/artifacts.ts +16 -4
  34. package/packages/core/src/config.ts +97 -18
  35. package/packages/core/src/index.ts +1 -0
  36. package/packages/core/src/lock.ts +104 -0
  37. package/packages/core/src/paths.ts +11 -6
  38. package/packages/core/src/store.ts +45 -1
  39. package/packages/core/src/types.ts +92 -2
  40. package/packages/core/src/validation.ts +10 -0
  41. package/packages/core/src/version.ts +1 -1
  42. package/packages/core/src/workflow.ts +101 -11
  43. package/packages/orchestrator/src/index.ts +349 -19
  44. package/packages/plugins/src/index.ts +2 -44
  45. package/packages/service/src/tools.ts +126 -15
  46. package/plugins/devcrew-codex/.codex-plugin/plugin.json +1 -1
  47. package/plugins/devcrew-codex/.mcp.json +1 -1
  48. package/plugins/devcrew-codex/skills/devcrew/SKILL.md +8 -7
  49. package/scripts/smoke-codex-plugin.mjs +1 -1
  50. package/plugins/devcrew-codex/agents/architect.toml +0 -12
  51. package/plugins/devcrew-codex/agents/implementer.toml +0 -12
  52. package/plugins/devcrew-codex/agents/pm.toml +0 -13
  53. 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, "docs", "devcrew");
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.2";
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
- if (phase === "requirements" || phase === "architecture" || phase === "implementation" || phase === "testing") {
39
- return phase;
40
- }
41
- return undefined;
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" || state.status === "awaiting_input" || state.status === "complete") {
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 gate = gateForPhase(state.phase);
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));