Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ When in doubt, optimize for: **determinism, recoverability, and clear operator v

## Never do

1. **Never hardcode machine/user-specific paths or private environment assumptions.**
1. **Never `git reset --hard` when you have uncommitted or staged changes.** Use `git stash` first, or commit to a branch. Hard reset silently destroys work that must then be re-applied from scratch.
2. **Never hardcode machine/user-specific paths or private environment assumptions.**
2. **Never leak internal/planning artifacts into public docs/templates.**
3. **Never make template content project- or language-specific.**
4. **Never silently change command names/flags or config schema fields.**
Expand Down
87 changes: 85 additions & 2 deletions extensions/taskplane/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Main batch execution engine
* @module orch/engine
*/
import { readFileSync, readdirSync, unlinkSync } from "fs";
import { join } from "path";
import { existsSync, readFileSync, readdirSync, unlinkSync } from "fs";
import { dirname, join, resolve } from "path";

import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
import { execLog, executeWave, tmuxKillSession } from "./execution.ts";
Expand Down Expand Up @@ -339,6 +339,16 @@ export async function executeOrchBatch(
}
}

// ── Workspace mode: commit task artifacts to task-area repos ─
// In workspace mode, workers write .DONE and STATUS.md to the
// canonical task folder (e.g., shared-libs/task-management/...) via
// absolute paths, not to the lane worktree. These changes land as
// uncommitted modifications in the task-area repo's working tree.
// Commit them before the merge step so they appear in the orch branch.
if (workspaceConfig && waveResult.succeededTaskIds.length > 0) {
commitWorkspaceTaskArtifacts(discoveryRef, workspaceRoot ?? repoRoot, waveIdx + 1, batchState.batchId);
}

// ── Wave Merge ───────────────────────────────────────────
// Only merge if there are succeeded tasks in this wave
let mergeResult: MergeWaveResult | null = null;
Expand Down Expand Up @@ -847,5 +857,78 @@ export async function executeOrchBatch(
}


// ── Workspace Task Artifact Commit ───────────────────────────────────

/**
* In workspace mode, commit task artifacts (.DONE, STATUS.md) that workers
* wrote to the canonical task folder in the task-area repo.
*
* Workers write to absolute paths (e.g., shared-libs/task-management/.../TP-002/.DONE)
* which land as uncommitted changes in the task-area repo's working tree.
* This function finds all task-area repos with dirty task files and commits them
* so they appear in the lane branches and merge correctly.
*
* Best-effort: failures are logged but don't block the batch.
*/
function commitWorkspaceTaskArtifacts(
discovery: DiscoveryResult | null,
workspaceRoot: string,
waveIndex: number,
batchId: string,
): void {
if (!discovery) return;

// Collect unique repo roots that contain task folders
const repoRootsWithTasks = new Set<string>();
for (const [, task] of discovery.pending) {
const taskFolder = resolve(task.taskFolder);
// Walk up to find the git repo root for this task folder
const gitResult = runGit(["rev-parse", "--show-toplevel"], dirname(taskFolder));
if (gitResult.ok) {
repoRootsWithTasks.add(gitResult.stdout.trim().replace(/\\/g, "/"));
}
}

for (const taskRepoRoot of repoRootsWithTasks) {
// Check for uncommitted changes
const statusResult = runGit(["status", "--porcelain", "--", "task-management/"], taskRepoRoot);
if (!statusResult.ok) {
// Try without path filter (task area might have different name)
const statusAll = runGit(["status", "--porcelain"], taskRepoRoot);
if (!statusAll.ok || !statusAll.stdout.trim()) continue;
}
if (statusResult.ok && !statusResult.stdout.trim()) continue;

// Stage task artifacts (only .DONE and STATUS.md files)
const lines = (statusResult.stdout || "").split("\n").filter(l => l.trim());
let hasTaskArtifacts = false;
for (const line of lines) {
const file = line.slice(3).trim();
if (file.endsWith(".DONE") || file.endsWith("STATUS.md")) {
const addResult = runGit(["add", file], taskRepoRoot);
if (addResult.ok) hasTaskArtifacts = true;
}
}

if (!hasTaskArtifacts) continue;

// Commit
const commitResult = runGit(
["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md)`],
taskRepoRoot,
);
if (commitResult.ok) {
execLog("batch", batchId, `committed workspace task artifacts`, {
repoRoot: taskRepoRoot,
wave: waveIndex,
});
} else if (!commitResult.stderr.includes("nothing to commit")) {
execLog("batch", batchId, `workspace task artifact commit failed (non-fatal): ${commitResult.stderr.slice(0, 200)}`, {
repoRoot: taskRepoRoot,
});
}
}
}

// ── Dashboard Widget (Step 6) ────────────────────────────────────────