taskplane 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,8 @@ export interface OrchestratorConfig {
36
36
  tools: string;
37
37
  verify: string[];
38
38
  order: "fewest-files-first" | "sequential";
39
+ /** Merge agent timeout in minutes. Default: 10. Increase for large batches. */
40
+ timeout_minutes: number;
39
41
  };
40
42
  failure: {
41
43
  on_task_failure: "skip-dependents" | "stop-wave" | "stop-all";
@@ -168,6 +170,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
168
170
  tools: "read,write,edit,bash,grep,find,ls",
169
171
  verify: [],
170
172
  order: "fewest-files-first",
173
+ timeout_minutes: 10,
171
174
  },
172
175
  failure: {
173
176
  on_task_failure: "skip-dependents",
@@ -1049,7 +1052,8 @@ export class MergeError extends Error {
1049
1052
  * Merge agents typically complete in 10-60 seconds. A 5-minute timeout
1050
1053
  * is generous and covers verification (go build) on large codebases.
1051
1054
  */
1052
- export const MERGE_TIMEOUT_MS = 5 * 60 * 1000;
1055
+ /** Default merge agent timeout. Use config.merge.timeout_minutes to override. */
1056
+ export const MERGE_TIMEOUT_MS = 10 * 60 * 1000;
1053
1057
 
1054
1058
  /**
1055
1059
  * Polling interval for merge result file (ms).
@@ -1796,6 +1800,14 @@ export interface ExecutionContext {
1796
1800
  taskRunnerConfig: TaskRunnerConfig;
1797
1801
  /** Loaded orchestrator configuration */
1798
1802
  orchestratorConfig: OrchestratorConfig;
1803
+ /**
1804
+ * Resolved pointer for config/agent paths (null in repo mode).
1805
+ *
1806
+ * When present, `pointer.configRoot` and `pointer.agentRoot` point to
1807
+ * the config repo's config directory. State/sidecar paths are NOT
1808
+ * affected — they always live at `<workspaceRoot>/.pi/`.
1809
+ */
1810
+ pointer: PointerResolution | null;
1799
1811
  }
1800
1812
 
1801
1813
 
@@ -1861,6 +1873,73 @@ export class WorkspaceConfigError extends Error {
1861
1873
  }
1862
1874
 
1863
1875
 
1876
+ // ── Pointer Resolution Types ─────────────────────────────────────────
1877
+
1878
+ /**
1879
+ * Canonical filename for the workspace pointer file.
1880
+ * Located at `<workspace-root>/.pi/taskplane-pointer.json`.
1881
+ *
1882
+ * Created by `taskplane init` in workspace mode. Points to the config
1883
+ * repo and config path within it. Not committed to git — each user
1884
+ * creates it during onboarding.
1885
+ */
1886
+ export const POINTER_FILENAME = "taskplane-pointer.json";
1887
+
1888
+ /**
1889
+ * Resolve the absolute path to the pointer file.
1890
+ * @param workspaceRoot - Absolute path to the workspace root
1891
+ */
1892
+ export function pointerFilePath(workspaceRoot: string): string {
1893
+ return join(workspaceRoot, ".pi", POINTER_FILENAME);
1894
+ }
1895
+
1896
+ /**
1897
+ * Result of resolving the workspace pointer file.
1898
+ *
1899
+ * This is the primary contract for downstream consumers (task-runner,
1900
+ * orchestrator, merge agent, dashboard). All pointer failures are
1901
+ * non-fatal: when the pointer cannot be resolved, `used` is false and
1902
+ * `configRoot`/`agentRoot` fall back to workspace-root paths.
1903
+ *
1904
+ * State/sidecar paths are NOT affected by the pointer — they always
1905
+ * live at `<workspace-root>/.pi/` regardless of pointer resolution.
1906
+ *
1907
+ * In repo mode, `resolvePointer()` returns null (pointer is ignored
1908
+ * entirely, even if a file happens to exist).
1909
+ */
1910
+ export interface PointerResolution {
1911
+ /**
1912
+ * Whether the pointer was successfully resolved.
1913
+ * - true: pointer file was found, parsed, and config_repo resolved
1914
+ * to a known repo in WorkspaceConfig.repos.
1915
+ * - false: pointer was missing, malformed, or referenced an unknown
1916
+ * repo. Fallback paths are used instead.
1917
+ */
1918
+ used: boolean;
1919
+
1920
+ /**
1921
+ * Resolved config root directory.
1922
+ * - When used=true: `<config-repo-path>/<config_path>/`
1923
+ * - When used=false: `<workspace-root>/.pi/` (existing fallback)
1924
+ */
1925
+ configRoot: string;
1926
+
1927
+ /**
1928
+ * Resolved agent overrides directory.
1929
+ * - When used=true: `<config-repo-path>/<config_path>/agents/`
1930
+ * - When used=false: `<workspace-root>/.pi/agents/` (existing fallback)
1931
+ */
1932
+ agentRoot: string;
1933
+
1934
+ /**
1935
+ * Warning message when pointer resolution fell back.
1936
+ * - undefined when used=true (no warning)
1937
+ * - Human-readable reason string when used=false
1938
+ */
1939
+ warning?: string;
1940
+ }
1941
+
1942
+
1864
1943
  // ── Workspace Defaults ───────────────────────────────────────────────
1865
1944
 
1866
1945
  /**
@@ -1900,6 +1979,7 @@ export function createRepoModeContext(
1900
1979
  workspaceConfig: null,
1901
1980
  taskRunnerConfig,
1902
1981
  orchestratorConfig,
1982
+ pointer: null,
1903
1983
  };
1904
1984
  }
1905
1985
 
@@ -37,16 +37,18 @@
37
37
  * @module orch/workspace
38
38
  */
39
39
  import { readFileSync, existsSync, realpathSync } from "fs";
40
- import { resolve } from "path";
40
+ import { resolve, relative, isAbsolute } from "path";
41
41
  import { parse as yamlParse } from "yaml";
42
42
 
43
43
  import { runGit } from "./git.ts";
44
44
  import {
45
45
  WorkspaceConfigError,
46
46
  workspaceConfigPath,
47
+ pointerFilePath,
47
48
  type WorkspaceConfig,
48
49
  type WorkspaceRepoConfig,
49
50
  type WorkspaceRoutingConfig,
51
+ type PointerResolution,
50
52
  } from "./types.ts";
51
53
 
52
54
 
@@ -94,6 +96,185 @@ function resolveAbsolutePath(p: string, base: string): string {
94
96
  }
95
97
 
96
98
 
99
+ // ── Pointer Resolution ───────────────────────────────────────────────
100
+
101
+ /**
102
+ * Resolve the workspace pointer file to find config and agent roots.
103
+ *
104
+ * The pointer file (`<workspace-root>/.pi/taskplane-pointer.json`) tells
105
+ * Taskplane where to find project config and agent overrides in workspace
106
+ * (polyrepo) mode. It's created by `taskplane init` and is local-only
107
+ * (not committed to git).
108
+ *
109
+ * **Repo mode:** Returns null. The pointer is workspace-only — in repo
110
+ * mode it is never read, even if a file happens to exist on disk.
111
+ *
112
+ * **Workspace mode:** Reads and validates the pointer, then resolves
113
+ * config and agent roots. All failures are non-fatal:
114
+ * - Missing pointer file → warn + fallback
115
+ * - Malformed JSON → warn + fallback
116
+ * - Missing required fields → warn + fallback
117
+ * - Unknown config_repo (not in WorkspaceConfig.repos) → warn + fallback
118
+ * - Path traversal in config_path → warn + fallback
119
+ *
120
+ * Fallback paths: `<workspace-root>/.pi/` for config,
121
+ * `<workspace-root>/.pi/agents/` for agents.
122
+ *
123
+ * State/sidecar paths are NOT affected by the pointer and are not
124
+ * included in the return value — they always live at
125
+ * `<workspace-root>/.pi/` regardless.
126
+ *
127
+ * @param workspaceRoot - Absolute path to the workspace root directory
128
+ * @param workspaceConfig - Loaded workspace config (null = repo mode → returns null)
129
+ * @returns PointerResolution with resolved paths, or null in repo mode
130
+ */
131
+ export function resolvePointer(
132
+ workspaceRoot: string,
133
+ workspaceConfig: WorkspaceConfig | null,
134
+ ): PointerResolution | null {
135
+ // ── Repo mode: pointer is ignored entirely ───────────────────
136
+ if (workspaceConfig === null) {
137
+ return null;
138
+ }
139
+
140
+ const fallbackConfigRoot = resolve(workspaceRoot, ".pi");
141
+ const fallbackAgentRoot = resolve(workspaceRoot, ".pi", "agents");
142
+
143
+ const filePath = pointerFilePath(workspaceRoot);
144
+
145
+ // ── 1. File existence ────────────────────────────────────────
146
+ if (!existsSync(filePath)) {
147
+ return {
148
+ used: false,
149
+ configRoot: fallbackConfigRoot,
150
+ agentRoot: fallbackAgentRoot,
151
+ warning: `Pointer file not found: ${filePath}. Run 'taskplane init' to create it.`,
152
+ };
153
+ }
154
+
155
+ // ── 2. Read file ─────────────────────────────────────────────
156
+ let rawContent: string;
157
+ try {
158
+ rawContent = readFileSync(filePath, "utf-8");
159
+ } catch (err: unknown) {
160
+ const msg = err instanceof Error ? err.message : String(err);
161
+ return {
162
+ used: false,
163
+ configRoot: fallbackConfigRoot,
164
+ agentRoot: fallbackAgentRoot,
165
+ warning: `Cannot read pointer file ${filePath}: ${msg}`,
166
+ };
167
+ }
168
+
169
+ // ── 3. Parse JSON ────────────────────────────────────────────
170
+ let parsed: unknown;
171
+ try {
172
+ parsed = JSON.parse(rawContent);
173
+ } catch {
174
+ return {
175
+ used: false,
176
+ configRoot: fallbackConfigRoot,
177
+ agentRoot: fallbackAgentRoot,
178
+ warning: `Pointer file ${filePath} contains invalid JSON.`,
179
+ };
180
+ }
181
+
182
+ // ── 4. Validate shape ────────────────────────────────────────
183
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
184
+ return {
185
+ used: false,
186
+ configRoot: fallbackConfigRoot,
187
+ agentRoot: fallbackAgentRoot,
188
+ warning: `Pointer file ${filePath} must be a JSON object.`,
189
+ };
190
+ }
191
+
192
+ const doc = parsed as Record<string, unknown>;
193
+ const configRepo = doc.config_repo;
194
+ const configPath = doc.config_path;
195
+
196
+ if (!configRepo || typeof configRepo !== "string" || configRepo.trim() === "") {
197
+ return {
198
+ used: false,
199
+ configRoot: fallbackConfigRoot,
200
+ agentRoot: fallbackAgentRoot,
201
+ warning: `Pointer file ${filePath} is missing required field 'config_repo'.`,
202
+ };
203
+ }
204
+
205
+ if (!configPath || typeof configPath !== "string" || configPath.trim() === "") {
206
+ return {
207
+ used: false,
208
+ configRoot: fallbackConfigRoot,
209
+ agentRoot: fallbackAgentRoot,
210
+ warning: `Pointer file ${filePath} is missing required field 'config_path'.`,
211
+ };
212
+ }
213
+
214
+ // ── 5. Guard path traversal ──────────────────────────────────
215
+ const normalizedConfigPath = configPath.trim().replace(/\\/g, "/");
216
+
217
+ // Reject absolute paths (POSIX `/...` and Windows `C:/...`, `\\...`)
218
+ if (isAbsolute(normalizedConfigPath) || isAbsolute(configPath.trim())) {
219
+ return {
220
+ used: false,
221
+ configRoot: fallbackConfigRoot,
222
+ agentRoot: fallbackAgentRoot,
223
+ warning: `Pointer file ${filePath} has invalid config_path '${configPath}' (absolute paths not allowed).`,
224
+ };
225
+ }
226
+
227
+ // Reject traversal sequences
228
+ if (
229
+ normalizedConfigPath.startsWith("..") ||
230
+ normalizedConfigPath.includes("/../") ||
231
+ normalizedConfigPath.endsWith("/..")
232
+ ) {
233
+ return {
234
+ used: false,
235
+ configRoot: fallbackConfigRoot,
236
+ agentRoot: fallbackAgentRoot,
237
+ warning: `Pointer file ${filePath} has invalid config_path '${configPath}' (path traversal not allowed).`,
238
+ };
239
+ }
240
+
241
+ // ── 6. Resolve config_repo against workspace repos map ──────
242
+ const repoId = configRepo.trim();
243
+ const repoConfig = workspaceConfig.repos.get(repoId);
244
+ if (!repoConfig) {
245
+ const available = Array.from(workspaceConfig.repos.keys()).join(", ");
246
+ return {
247
+ used: false,
248
+ configRoot: fallbackConfigRoot,
249
+ agentRoot: fallbackAgentRoot,
250
+ warning: `Pointer file ${filePath}: config_repo '${repoId}' not found in workspace repos. Available repos: ${available}`,
251
+ };
252
+ }
253
+
254
+ // ── 7. Build resolved paths + containment check ──────────────
255
+ const resolvedConfigRoot = resolve(repoConfig.path, normalizedConfigPath);
256
+
257
+ // Verify the resolved path is within the repo root (defense-in-depth)
258
+ const rel = relative(repoConfig.path, resolvedConfigRoot);
259
+ if (rel.startsWith("..") || isAbsolute(rel)) {
260
+ return {
261
+ used: false,
262
+ configRoot: fallbackConfigRoot,
263
+ agentRoot: fallbackAgentRoot,
264
+ warning: `Pointer file ${filePath} has invalid config_path '${configPath}' (resolved path escapes config repo root).`,
265
+ };
266
+ }
267
+
268
+ const resolvedAgentRoot = resolve(resolvedConfigRoot, "agents");
269
+
270
+ return {
271
+ used: true,
272
+ configRoot: resolvedConfigRoot,
273
+ agentRoot: resolvedAgentRoot,
274
+ };
275
+ }
276
+
277
+
97
278
  // ── Workspace Config Loading ─────────────────────────────────────────
98
279
 
99
280
  /**
@@ -366,34 +547,47 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
366
547
  */
367
548
  export function buildExecutionContext(
368
549
  cwd: string,
369
- loadOrchConfig: (root: string) => import("./types.ts").OrchestratorConfig,
370
- loadTaskConfig: (root: string) => import("./types.ts").TaskRunnerConfig,
550
+ loadOrchConfig: (root: string, pointerConfigRoot?: string) => import("./types.ts").OrchestratorConfig,
551
+ loadTaskConfig: (root: string, pointerConfigRoot?: string) => import("./types.ts").TaskRunnerConfig,
371
552
  ): import("./types.ts").ExecutionContext {
372
- const orchestratorConfig = loadOrchConfig(cwd);
373
- const taskRunnerConfig = loadTaskConfig(cwd);
374
-
375
553
  const workspaceConfig = loadWorkspaceConfig(cwd);
376
554
 
377
555
  if (workspaceConfig === null) {
378
- // Repo mode: cwd is both workspace root and repo root
556
+ // Repo mode: pointer is ignored entirely. Config loads from cwd.
557
+ const orchestratorConfig = loadOrchConfig(cwd);
558
+ const taskRunnerConfig = loadTaskConfig(cwd);
559
+
379
560
  return {
380
561
  workspaceRoot: cwd,
381
562
  repoRoot: cwd,
382
563
  mode: "repo",
383
564
  workspaceConfig: null,
384
- taskRunnerConfig,
385
565
  orchestratorConfig,
566
+ taskRunnerConfig,
567
+ pointer: null,
386
568
  };
387
569
  }
388
570
 
389
- // Workspace mode: workspace root is cwd, repo root is the default repo
571
+ // Workspace mode: resolve pointer once, pass configRoot to config loaders.
572
+ const pointer = resolvePointer(cwd, workspaceConfig);
573
+
574
+ // Log pointer warning once at startup (non-fatal).
575
+ if (pointer && pointer.warning) {
576
+ console.error(`[taskplane] pointer warning: ${pointer.warning}`);
577
+ }
578
+
579
+ const pointerConfigRoot = pointer?.configRoot;
580
+ const orchestratorConfig = loadOrchConfig(cwd, pointerConfigRoot);
581
+ const taskRunnerConfig = loadTaskConfig(cwd, pointerConfigRoot);
582
+
390
583
  const defaultRepo = workspaceConfig.repos.get(workspaceConfig.routing.defaultRepo)!;
391
584
  return {
392
585
  workspaceRoot: cwd,
393
586
  repoRoot: defaultRepo.path,
394
587
  mode: "workspace",
395
588
  workspaceConfig,
396
- taskRunnerConfig,
397
589
  orchestratorConfig,
590
+ taskRunnerConfig,
591
+ pointer,
398
592
  };
399
593
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -326,11 +326,13 @@ Verify every task against this before reporting the launch command:
326
326
 
327
327
  ## Git Commit Convention
328
328
 
329
- The prompt template includes a `## Git Commit Convention` section with the full
330
- format table (`feat(TASK-ID):`, `fix(TASK-ID):`, `checkpoint: TASK-ID`, etc.).
331
- Always include it without task ID prefixes, there's no way to trace commits
332
- back to the task that produced them (`git log --grep="PM-004"` only works if
333
- the prefix is there).
329
+ The prompt template includes a `## Git Commit Convention` section. Workers
330
+ commit at **step boundaries** (not after every checkbox) to keep git history
331
+ meaningful. Hydration commits are the exception STATUS.md expansions are
332
+ committed immediately to preserve the plan for crash recovery. Always include
333
+ the task ID prefix without it, there's no way to trace commits back to the
334
+ task that produced them (`git log --grep="PM-004"` only works if the prefix
335
+ is there).
334
336
 
335
337
  ---
336
338
 
@@ -118,12 +118,13 @@ Copy this template when creating a new task. Replace all `[bracketed]` fields.
118
118
 
119
119
  ## Git Commit Convention
120
120
 
121
- All commits for this task MUST include the task ID for traceability:
121
+ Commits happen at **step boundaries** (not after every checkbox). All commits
122
+ for this task MUST include the task ID for traceability:
122
123
 
123
- - **Implementation:** `feat([PREFIX-###]): description`
124
+ - **Step completion:** `feat([PREFIX-###]): complete Step N — description`
124
125
  - **Bug fixes:** `fix([PREFIX-###]): description`
125
126
  - **Tests:** `test([PREFIX-###]): description`
126
- - **Checkpoints:** `checkpoint: [PREFIX-###] description`
127
+ - **Hydration:** `hydrate: [PREFIX-###] expand Step N checkboxes`
127
128
 
128
129
  ## Do NOT
129
130
 
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: task-merger
3
+ # tools: read,write,edit,bash,grep,find,ls
4
+ # model:
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Merger Guidance
10
+
11
+ This file is COMPOSED with the base task-merger prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - Branch merge workflow (fast-forward, 3-way, conflict resolution)
16
+ - Post-merge verification command execution
17
+ - Result file JSON format and writing conventions
18
+
19
+ Add project-specific merge rules below. Common examples:
20
+ - Post-merge verification commands (build, lint, test)
21
+ - Conflict resolution preferences
22
+ - Protected files that should never be auto-merged
23
+
24
+ To override frontmatter values (tools, model), uncomment and edit above.
25
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
26
+ uncomment `standalone: true` above and write the complete prompt below.
27
+ ═══════════════════════════════════════════════════════════════════ -->
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: task-reviewer
3
+ # tools: read,write,bash,grep,find,ls
4
+ # model: openai/gpt-5.3-codex
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Reviewer Guidance
10
+
11
+ This file is COMPOSED with the base task-reviewer prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - Plan review and code review workflows
16
+ - Verdict format (APPROVE / REVISE)
17
+ - Review file output conventions
18
+ - Plan granularity guidance
19
+
20
+ Add project-specific review criteria below. Common examples:
21
+ - Required test coverage thresholds
22
+ - Security review checklist items
23
+ - Architecture constraints to enforce
24
+ - Performance requirements
25
+
26
+ To override frontmatter values (tools, model), uncomment and edit above.
27
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
28
+ uncomment `standalone: true` above and write the complete prompt below.
29
+ ═══════════════════════════════════════════════════════════════════ -->
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: task-worker
3
+ # tools: read,write,edit,bash,grep,find,ls
4
+ # model: anthropic/claude-sonnet-4-20250514
5
+ # standalone: true
6
+ ---
7
+
8
+ <!-- ═══════════════════════════════════════════════════════════════════
9
+ Project-Specific Worker Guidance
10
+
11
+ This file is COMPOSED with the base task-worker prompt shipped in the
12
+ taskplane package. Your content here is appended after the base prompt.
13
+
14
+ The base prompt (maintained by taskplane) handles:
15
+ - STATUS.md-first workflow and checkpoint discipline
16
+ - Fresh-context loop behavior and iteration rules
17
+ - Git commit conventions and .DONE file creation
18
+ - Review response handling
19
+
20
+ Add project-specific rules below. Common examples:
21
+ - Preferred package manager (pnpm, yarn, bun)
22
+ - Test commands (make test, npm run test:unit)
23
+ - Coding standards (linting, formatting)
24
+ - Framework-specific patterns
25
+ - Environment or deployment constraints
26
+
27
+ To override frontmatter values (tools, model), uncomment and edit above.
28
+ To use this file as a FULLY STANDALONE prompt (ignoring the base),
29
+ uncomment `standalone: true` above and write the complete prompt below.
30
+ ═══════════════════════════════════════════════════════════════════ -->
@@ -18,45 +18,59 @@ ONLY memory.
18
18
 
19
19
  ## Checkpoint Discipline (CRITICAL)
20
20
 
21
- After completing EACH checkbox item, you MUST:
21
+ There are two distinct actions: **checking off items** and **git commits**.
22
+ They happen at different cadences.
22
23
 
23
- 1. **Edit STATUS.md** to check off the item. Use the `edit` tool:
24
- - oldText: `- [ ] The item text`
25
- - newText: `- [x] The item text`
24
+ ### Checking off items (after EACH checkbox)
26
25
 
27
- 2. **Git commit** the checkpoint:
28
- ```bash
29
- git add -A && git commit -m "checkpoint: <what you did>"
30
- ```
31
-
32
- 3. **Check for wrap-up signal:**
33
- ```bash
34
- if test -f "<TASK_FOLDER>/.task-wrap-up" || test -f "<TASK_FOLDER>/.wiggum-wrap-up"; then
35
- echo "WRAP_UP_SIGNAL"
36
- fi
37
- ```
38
- Primary signal file is `.task-wrap-up`; `.wiggum-wrap-up` is legacy and still supported.
39
- If either signal exists, STOP immediately after this checkpoint.
40
-
41
- ### Example checkpoint sequence:
42
-
43
- After verifying that source files exist, immediately do:
26
+ After completing each checkbox item, **immediately update STATUS.md**:
44
27
 
45
28
  ```
46
29
  edit STATUS.md
47
- oldText: "- [ ] Verify all source files exist"
48
- newText: "- [x] Verify all source files exist"
30
+ oldText: "- [ ] The item text"
31
+ newText: "- [x] The item text"
49
32
  ```
50
33
 
51
- Then:
34
+ Then **check for wrap-up signal:**
52
35
  ```bash
53
- git add -A && git commit -m "checkpoint: verified source files exist"
36
+ if test -f "<TASK_FOLDER>/.task-wrap-up" || test -f "<TASK_FOLDER>/.wiggum-wrap-up"; then
37
+ echo "WRAP_UP_SIGNAL"
38
+ fi
54
39
  ```
40
+ Primary signal file is `.task-wrap-up`; `.wiggum-wrap-up` is legacy and still supported.
41
+ If either signal exists, STOP immediately after this checkpoint.
55
42
 
56
- **NEVER batch updates.** Check off ONE item, commit, then do the next.
57
43
  If you do work but don't edit STATUS.md, that work is INVISIBLE to the
58
44
  orchestrator and you will be re-spawned to do it again.
59
45
 
46
+ ### Git commits (after completing a STEP)
47
+
48
+ Git commits happen at **step boundaries**, not after every checkbox. When all
49
+ checkboxes in a step are checked off:
50
+
51
+ ```bash
52
+ git add -A && git commit -m "feat(TASK-ID): complete Step N — description"
53
+ ```
54
+
55
+ This keeps the git history meaningful — one coherent commit per step instead of
56
+ dozens of micro-commits that nobody reads.
57
+
58
+ **Exceptions** — commit immediately (before step completion) in these cases:
59
+ - **Hydration:** After expanding STATUS.md with new checkboxes, commit before
60
+ implementing: `git add -A && git commit -m "hydrate: expand Step N checkboxes"`
61
+ - **REVISE response:** After adding reviewer revision items to STATUS.md:
62
+ `git add -A && git commit -m "hydrate: add R00N revision items to Step N"`
63
+ - **Wrap-up signal:** If stopping mid-step due to a wrap-up signal, commit
64
+ whatever is done so far.
65
+
66
+ ### Why this approach
67
+
68
+ STATUS.md is the worker's memory, not git. Checking off items in STATUS.md
69
+ ensures the next worker iteration knows where to resume. Git commits preserve
70
+ file changes at meaningful milestones. Per-checkbox commits waste tool calls
71
+ on git housekeeping without adding recovery value — the files are already on
72
+ disk in the worktree.
73
+
60
74
  ## STATUS.md Hydration (MANDATORY)
61
75
 
62
76
  STATUS.md is your ONLY memory. It needs enough structure so progress survives
@@ -85,7 +99,7 @@ Before implementing anything, assess whether the step needs expansion:
85
99
  3. **If expansion is needed**, add checkboxes for **distinct outcomes** you've
86
100
  identified — not for every individual code change. Think: "what are the 2-5
87
101
  things that need to be true when this step is done?"
88
- 4. **Commit the hydrated STATUS.md immediately** this IS a checkpoint:
102
+ 4. **Commit the hydrated STATUS.md immediately** (see Checkpoint Discipline exceptions):
89
103
  ```bash
90
104
  git add -A && git commit -m "hydrate: expand Step N checkboxes"
91
105
  ```
@@ -104,7 +118,7 @@ When a reviewer returns REVISE with specific feedback items:
104
118
  1. **Read the review file** in `.reviews/`
105
119
  2. **Add revision items as new checkboxes** in the current step — group related
106
120
  fixes into single checkboxes rather than creating one per reviewer sentence
107
- 3. **Commit the hydrated STATUS.md:**
121
+ 3. **Commit the hydrated STATUS.md** (see Checkpoint Discipline exceptions):
108
122
  ```bash
109
123
  git add -A && git commit -m "hydrate: add R00N revision items to Step N"
110
124
  ```
@@ -112,9 +126,9 @@ When a reviewer returns REVISE with specific feedback items:
112
126
 
113
127
  ### Rules
114
128
 
115
- - **Hydration is a checkpoint.** Always commit STATUS.md after hydrating, before
116
- implementing. If the iteration ends between hydration and implementation, the
117
- plan is preserved for the next worker.
129
+ - **Hydration gets an immediate commit.** Always commit STATUS.md after hydrating,
130
+ before implementing. If the iteration ends between hydration and implementation,
131
+ the plan is preserved for the next worker.
118
132
  - **One checkbox per meaningful outcome.** "Implement the CRUD methods" is one
119
133
  checkbox if they're straightforward. "Implement create + implement delete" is
120
134
  two checkboxes if they involve genuinely different logic. Use judgment — the
@@ -76,6 +76,9 @@ merge:
76
76
 
77
77
  order: "fewest-files-first"
78
78
 
79
+ # Merge agent timeout in minutes. Increase for large batches with many files.
80
+ timeout_minutes: 10
81
+
79
82
  # ── Failure Handling ──────────────────────────────────────────────────
80
83
 
81
84
  failure: