agentplane 0.2.4 → 0.2.5

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/assets/AGENTS.md CHANGED
@@ -455,7 +455,7 @@ Always follow `workflow_mode` from `.agentplane/config.json`.
455
455
  Rules:
456
456
 
457
457
  - Do all work in the current checkout.
458
- - Task branches are allowed in `direct` (single working directory). Note: `agentplane work start <task-id> --agent <ROLE> --slug <slug>` will create/checkout `task/<task-id>/<slug>` in-place.
458
+ - In `direct` (single working directory), agentplane uses a single-stream workflow in the current checkout. `agentplane work start <task-id> --agent <ROLE> --slug <slug>` records the active task and keeps the current branch (no task branches).
459
459
  - Do not use worktrees in `direct`. `agentplane work start ... --worktree` is `branch_pr`-only.
460
460
  - If you only need artifacts/docs without switching branches, prefer `agentplane task scaffold <task-id>`.
461
461
 
@@ -89,7 +89,7 @@ const ROLE_GUIDES = [
89
89
  {
90
90
  role: "CODER",
91
91
  lines: [
92
- "- direct mode: work in the current checkout; `agentplane work start <task-id> --agent <ROLE> --slug <slug>` creates/checks out `task/<task-id>/<slug>` in-place (no worktree). Use `agentplane task scaffold <task-id>` for docs without switching branches.",
92
+ "- direct mode: single-stream in the current checkout; `agentplane work start <task-id> --agent <ROLE> --slug <slug>` records the active task and keeps the current branch (no task branches). Use `agentplane task scaffold <task-id>` for docs without switching context.",
93
93
  "- branch_pr: `agentplane work start <task-id> --agent <ROLE> --slug <slug> --worktree`",
94
94
  '- Status updates: `agentplane start <task-id> --author <ROLE> --body "Start: ..."` / `agentplane block <task-id> --author <ROLE> --body "Blocked: ..."`',
95
95
  "- Verify Steps: `agentplane task verify-show <task-id>` (use as the verification contract before recording results).",
@@ -101,7 +101,7 @@ const ROLE_GUIDES = [
101
101
  {
102
102
  role: "TESTER",
103
103
  lines: [
104
- "- direct mode: work in the current checkout; `agentplane work start <task-id> --agent <ROLE> --slug <slug>` creates/checks out `task/<task-id>/<slug>` in-place (no worktree). Use `agentplane task scaffold <task-id>` for docs without switching branches.",
104
+ "- direct mode: single-stream in the current checkout; `agentplane work start <task-id> --agent <ROLE> --slug <slug>` records the active task and keeps the current branch (no task branches). Use `agentplane task scaffold <task-id>` for docs without switching context.",
105
105
  "- branch_pr: `agentplane work start <task-id> --agent <ROLE> --slug <slug> --worktree`",
106
106
  '- Status updates: `agentplane start <task-id> --author <ROLE> --body "Start: ..."` / `agentplane block <task-id> --author <ROLE> --body "Blocked: ..."`',
107
107
  "- Verify Steps: `agentplane task verify-show <task-id>` (treat as the verification contract).",
@@ -1 +1 @@
1
- {"version":3,"file":"work-start.command.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAc,MAAM,wBAAwB,CAAC;AAGtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,eAAe,CAgDtD,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,CAAC,sBAAsB,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,GAClE,cAAc,CAAC,eAAe,CAAC,CA4BjC"}
1
+ {"version":3,"file":"work-start.command.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAc,MAAM,wBAAwB,CAAC;AAGtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,eAAe,CAkDtD,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,CAAC,sBAAsB,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,GAClE,cAAc,CAAC,eAAe,CAAC,CA4BjC"}
@@ -3,7 +3,7 @@ import { cmdWorkStart } from "./index.js";
3
3
  export const workStartSpec = {
4
4
  id: ["work", "start"],
5
5
  group: "Work",
6
- summary: "Create or switch to the task branch (optionally via git worktree).",
6
+ summary: "Prepare the workspace for a task (direct: single-stream on current branch; branch_pr: task branch/worktree).",
7
7
  args: [
8
8
  { name: "task-id", required: true, valueHint: "<task-id>", description: "Existing task id." },
9
9
  ],
@@ -36,10 +36,11 @@ export const workStartSpec = {
36
36
  examples: [
37
37
  {
38
38
  cmd: "agentplane work start 202602030608-F1Q8AB --agent CODER --slug cli",
39
- why: "Start work by checking out / creating the branch in-place (direct mode).",
39
+ why: "Start work (direct mode records the active task; branch_pr uses a dedicated task branch/worktree).",
40
40
  },
41
41
  ],
42
42
  notes: [
43
+ "When workflow_mode=direct, agentplane does not create task branches; it records a single active task for the workspace.",
43
44
  "When workflow_mode=branch_pr, --worktree is required and the command must be run on the base branch.",
44
45
  ],
45
46
  parse: (raw) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"work-start.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAKnC,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoHlB"}
1
+ {"version":3,"file":"work-start.d.ts","sourceRoot":"","sources":["../../../src/commands/branch/work-start.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAuFnC,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkJlB"}
@@ -1,8 +1,9 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveBaseBranch } from "@agentplaneorg/core";
4
4
  import { mapBackendError } from "../../cli/error-map.js";
5
5
  import { fileExists } from "../../cli/fs-utils.js";
6
+ import { exitCodeForError } from "../../cli/exit-codes.js";
6
7
  import { successMessage } from "../../cli/output.js";
7
8
  import { CliError } from "../../shared/errors.js";
8
9
  import { execFileAsync, gitEnv } from "../shared/git.js";
@@ -11,6 +12,76 @@ import { isPathWithin } from "../shared/path.js";
11
12
  import { loadBackendTask, loadCommandContext, } from "../shared/task-backend.js";
12
13
  import { ensurePlanApprovedIfRequired } from "../task/shared.js";
13
14
  import { validateWorkAgent, validateWorkSlug } from "./internal/work-validate.js";
15
+ function directWorkLockPath(agentplaneDir) {
16
+ // Intentionally under cache/ so it stays out of git by default.
17
+ return path.join(agentplaneDir, "cache", "direct-work.json");
18
+ }
19
+ async function readDirectWorkLock(agentplaneDir) {
20
+ try {
21
+ const text = await readFile(directWorkLockPath(agentplaneDir), "utf8");
22
+ const parsed = JSON.parse(text);
23
+ if (!parsed || typeof parsed !== "object")
24
+ return null;
25
+ if (typeof parsed.task_id !== "string" ||
26
+ typeof parsed.agent !== "string" ||
27
+ typeof parsed.slug !== "string" ||
28
+ typeof parsed.branch !== "string" ||
29
+ typeof parsed.started_at !== "string") {
30
+ return null;
31
+ }
32
+ return parsed;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ async function writeDirectWorkLock(agentplaneDir, lock) {
39
+ const dir = path.dirname(directWorkLockPath(agentplaneDir));
40
+ await mkdir(dir, { recursive: true });
41
+ await writeFile(directWorkLockPath(agentplaneDir), JSON.stringify(lock, null, 2) + "\n", "utf8");
42
+ }
43
+ async function ensureGitClean(gitRoot) {
44
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain"], {
45
+ cwd: gitRoot,
46
+ env: gitEnv(),
47
+ });
48
+ const lines = stdout
49
+ .split("\n")
50
+ .map((line) => line.trimEnd())
51
+ .filter((line) => line.trim().length > 0);
52
+ if (lines.length === 0)
53
+ return;
54
+ // Allow task workflow artifacts to be dirty. In direct mode we want a single-stream
55
+ // workflow without task branches, but we still expect task docs to change.
56
+ const allowedPrefixes = [
57
+ ".agentplane/tasks/",
58
+ ".agentplane/tasks.json",
59
+ ".agentplane/cache/",
60
+ ".agentplane/.upgrade/",
61
+ ".agentplane/upgrade/",
62
+ ];
63
+ const isAllowed = (p) => allowedPrefixes.some((prefix) => p.startsWith(prefix));
64
+ const dirty = lines
65
+ .map((line) => {
66
+ // Format: XY <path> (we only need the path-ish tail).
67
+ const rest = line.slice(2).trim();
68
+ if (!rest)
69
+ return "";
70
+ // Rename/copy format: "old -> new"
71
+ const arrow = rest.lastIndexOf(" -> ");
72
+ if (arrow === -1)
73
+ return rest;
74
+ return rest.slice(arrow + 4).trim();
75
+ })
76
+ .filter((p) => p.length > 0 && !isAllowed(p));
77
+ if (dirty.length === 0)
78
+ return;
79
+ throw new CliError({
80
+ exitCode: exitCodeForError("E_GIT"),
81
+ code: "E_GIT",
82
+ message: "Working tree has non-task changes. In workflow_mode=direct, agentplane runs tasks in a single stream on the current branch; commit/stash your changes before starting a different task.",
83
+ });
84
+ }
14
85
  export async function cmdWorkStart(opts) {
15
86
  try {
16
87
  validateWorkAgent(opts.agent);
@@ -42,6 +113,28 @@ export async function cmdWorkStart(opts) {
42
113
  });
43
114
  ensurePlanApprovedIfRequired(task, config);
44
115
  const currentBranch = await gitCurrentBranch(resolved.gitRoot);
116
+ // direct mode: single-stream, no task branches.
117
+ if (mode === "direct") {
118
+ await ensureGitClean(resolved.gitRoot);
119
+ const existingLock = await readDirectWorkLock(resolved.agentplaneDir);
120
+ if (existingLock && existingLock.task_id !== opts.taskId) {
121
+ throw new CliError({
122
+ exitCode: 2,
123
+ code: "E_USAGE",
124
+ message: `Another task is already active in this workspace (workflow_mode=direct): ${existingLock.task_id}. ` +
125
+ `Finish it first, or delete ${path.relative(resolved.gitRoot, directWorkLockPath(resolved.agentplaneDir))} to override.`,
126
+ });
127
+ }
128
+ await writeDirectWorkLock(resolved.agentplaneDir, {
129
+ task_id: opts.taskId,
130
+ agent: opts.agent,
131
+ slug: opts.slug.trim(),
132
+ branch: currentBranch,
133
+ started_at: new Date().toISOString(),
134
+ });
135
+ process.stdout.write(`${successMessage("work start", opts.taskId, `mode=direct branch=${currentBranch}`)}\n`);
136
+ return 0;
137
+ }
45
138
  let baseRef = currentBranch;
46
139
  if (mode === "branch_pr") {
47
140
  const baseBranch = await resolveBaseBranch({
@@ -59,7 +152,7 @@ export async function cmdWorkStart(opts) {
59
152
  }
60
153
  if (currentBranch !== baseBranch) {
61
154
  throw new CliError({
62
- exitCode: 5,
155
+ exitCode: exitCodeForError("E_GIT"),
63
156
  code: "E_GIT",
64
157
  message: `work start must be run on base branch ${baseBranch} (current: ${currentBranch})`,
65
158
  });
@@ -1 +1 @@
1
- {"version":3,"file":"finish.d.ts","sourceRoot":"","sources":["../../../src/commands/task/finish.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAcnC,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,wBAAwB,EAAE,OAAO,CAAC;IAClC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyLlB"}
1
+ {"version":3,"file":"finish.d.ts","sourceRoot":"","sources":["../../../src/commands/task/finish.ts"],"names":[],"mappings":"AASA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AA+BnC,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,wBAAwB,EAAE,OAAO,CAAC;IAClC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgMlB"}
@@ -2,10 +2,28 @@ import { mapBackendError } from "../../cli/error-map.js";
2
2
  import { successMessage } from "../../cli/output.js";
3
3
  import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
4
4
  import { CliError } from "../../shared/errors.js";
5
+ import { readFile, rm } from "node:fs/promises";
6
+ import path from "node:path";
5
7
  import { commitFromComment } from "../guard/index.js";
6
8
  import { loadCommandContext, loadTaskFromContext, } from "../shared/task-backend.js";
7
9
  import { backendIsLocalFileBackend, getTaskStore } from "../shared/task-store.js";
8
10
  import { appendTaskEvent, defaultCommitEmojiForStatus, enforceStatusCommitPolicy, ensureVerificationSatisfiedIfRequired, nowIso, readCommitInfo, readHeadCommit, requireStructuredComment, } from "./shared.js";
11
+ async function clearDirectWorkLockIfMatches(opts) {
12
+ const lockPath = path.join(opts.agentplaneDir, "cache", "direct-work.json");
13
+ try {
14
+ const text = await readFile(lockPath, "utf8");
15
+ const parsed = JSON.parse(text);
16
+ const lockTaskId = parsed && typeof parsed.task_id === "string" ? parsed.task_id : null;
17
+ if (!lockTaskId)
18
+ return;
19
+ if (!opts.taskIds.includes(lockTaskId))
20
+ return;
21
+ await rm(lockPath, { force: true });
22
+ }
23
+ catch {
24
+ // best-effort
25
+ }
26
+ }
9
27
  export async function cmdFinish(opts) {
10
28
  try {
11
29
  const ctx = opts.ctx ??
@@ -162,6 +180,12 @@ export async function cmdFinish(opts) {
162
180
  config: ctx.config,
163
181
  });
164
182
  }
183
+ if (ctx.config.workflow_mode === "direct") {
184
+ await clearDirectWorkLockIfMatches({
185
+ agentplaneDir: ctx.resolvedProject.agentplaneDir,
186
+ taskIds: opts.taskIds,
187
+ });
188
+ }
165
189
  if (!opts.quiet) {
166
190
  process.stdout.write(`${successMessage("finished")}\n`);
167
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentplane",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Agent Plane CLI for task workflows, recipes, and project automation.",
5
5
  "keywords": [
6
6
  "agentplane",
@@ -54,7 +54,7 @@
54
54
  "prepublishOnly": "npm run prepack"
55
55
  },
56
56
  "dependencies": {
57
- "@agentplaneorg/core": "0.2.4",
57
+ "@agentplaneorg/core": "0.2.5",
58
58
  "yauzl": "^2.10.0"
59
59
  },
60
60
  "devDependencies": {