From cea421927a3c20620361fe41b7e6bf4408b6e391 Mon Sep 17 00:00:00 2001 From: Henry Lach Date: Wed, 18 Mar 2026 20:25:35 -0400 Subject: [PATCH 1/2] docs: add never-reset-hard rule to AGENTS.md --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 3525054b..9ffbc10d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.** From 82cf3359ebc9aba95286204df70a534037d5f2c2 Mon Sep 17 00:00:00 2001 From: Henry Lach Date: Wed, 18 Mar 2026 20:56:23 -0400 Subject: [PATCH 2/2] fix: commit workspace task artifacts before wave merge In workspace mode, workers write .DONE and STATUS.md to the canonical task folder (shared-libs) via absolute paths, not to the lane worktree. These changes were left uncommitted in the task-area repo's working tree, causing conflicts when /orch-integrate tried to merge. New commitWorkspaceTaskArtifacts() runs after each wave's tasks complete, before the merge step. It finds repos with dirty task files and commits them so they appear in the orch branch correctly. --- extensions/taskplane/engine.ts | 87 +++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/extensions/taskplane/engine.ts b/extensions/taskplane/engine.ts index 1573cc29..82e42af7 100644 --- a/extensions/taskplane/engine.ts +++ b/extensions/taskplane/engine.ts @@ -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"; @@ -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; @@ -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(); + 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) ────────────────────────────────────────