taskplane 0.22.12 → 0.22.14
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/bin/rpc-wrapper.mjs +43 -3
- package/dashboard/public/app.js +108 -55
- package/dashboard/public/style.css +18 -0
- package/dashboard/server.cjs +29 -3
- package/extensions/task-runner.ts +474 -89
- package/extensions/taskplane/abort.ts +2 -5
- package/extensions/taskplane/cleanup.ts +30 -9
- package/extensions/taskplane/engine.ts +11 -12
- package/extensions/taskplane/execution.ts +20 -3
- package/extensions/taskplane/extension.ts +499 -8
- package/extensions/taskplane/merge.ts +90 -8
- package/extensions/taskplane/supervisor-primer.md +6 -0
- package/package.json +1 -1
- package/templates/agents/task-worker.md +15 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Merge orchestration, merge agents, merge worktree
|
|
3
3
|
* @module orch/merge
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync, copyFileSync, mkdirSync, rmSync } from "fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, copyFileSync, mkdirSync, rmSync, readdirSync } from "fs";
|
|
6
6
|
import { readFile as fsReadFile } from "fs/promises";
|
|
7
7
|
import { execSync, spawnSync } from "child_process";
|
|
8
8
|
import { join, dirname, resolve, relative } from "path";
|
|
@@ -1841,16 +1841,48 @@ export async function mergeWave(
|
|
|
1841
1841
|
// ── Stage workspace task artifacts into merge worktree ──────────
|
|
1842
1842
|
// TP-035: Tightened artifact staging — only allowlisted task-owned files
|
|
1843
1843
|
// are staged. The allowlist is derived per-task-folder from completed lanes:
|
|
1844
|
-
//
|
|
1844
|
+
// `.DONE`, `STATUS.md`, `REVIEW_VERDICT.json`, and `.reviews/**` files.
|
|
1845
1845
|
// Files outside known task folders, worktree internals, and repo-escape
|
|
1846
1846
|
// paths are rejected. Uses resolve+relative path containment consistent
|
|
1847
1847
|
// with ensureTaskFilesCommitted() in execution.ts.
|
|
1848
1848
|
if (mergeWorkDir) {
|
|
1849
1849
|
// Build the set of allowed artifact paths (repo-root-relative) from
|
|
1850
1850
|
// the completed lanes' task folders.
|
|
1851
|
+
//
|
|
1852
|
+
// Allowlist policy:
|
|
1853
|
+
// - task marker files: .DONE, STATUS.md, REVIEW_VERDICT.json
|
|
1854
|
+
// - review outputs under task-local .reviews/**
|
|
1851
1855
|
const ALLOWED_ARTIFACT_NAMES = [".DONE", "STATUS.md", "REVIEW_VERDICT.json"];
|
|
1856
|
+
const ALLOWED_ARTIFACT_DIRS = [".reviews"];
|
|
1852
1857
|
const resolvedRepoRoot = resolve(repoRoot);
|
|
1853
1858
|
const allowedRelPaths = new Set<string>();
|
|
1859
|
+
const relPathToWorktree = new Map<string, string>();
|
|
1860
|
+
|
|
1861
|
+
const listFilesRecursively = (rootDir: string): string[] => {
|
|
1862
|
+
if (!existsSync(rootDir)) return [];
|
|
1863
|
+
const files: string[] = [];
|
|
1864
|
+
const walk = (dir: string): void => {
|
|
1865
|
+
let entries;
|
|
1866
|
+
try {
|
|
1867
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1868
|
+
} catch {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
for (const entry of entries) {
|
|
1872
|
+
const absPath = join(dir, entry.name);
|
|
1873
|
+
if (entry.isDirectory()) {
|
|
1874
|
+
walk(absPath);
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
if (!entry.isFile()) continue;
|
|
1878
|
+
const relPath = relative(rootDir, absPath).replace(/\\/g, "/");
|
|
1879
|
+
if (!relPath || relPath.startsWith("..") || relPath.startsWith("/")) continue;
|
|
1880
|
+
files.push(relPath);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
walk(rootDir);
|
|
1884
|
+
return files;
|
|
1885
|
+
};
|
|
1854
1886
|
|
|
1855
1887
|
for (const lane of orderedLanes) {
|
|
1856
1888
|
for (const allocTask of lane.tasks) {
|
|
@@ -1867,7 +1899,24 @@ export async function mergeWave(
|
|
|
1867
1899
|
}
|
|
1868
1900
|
|
|
1869
1901
|
for (const name of ALLOWED_ARTIFACT_NAMES) {
|
|
1870
|
-
|
|
1902
|
+
const rp = `${relFolder}/${name}`;
|
|
1903
|
+
allowedRelPaths.add(rp);
|
|
1904
|
+
relPathToWorktree.set(rp, join(lane.worktreePath, rp));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
for (const dirName of ALLOWED_ARTIFACT_DIRS) {
|
|
1908
|
+
const laneDir = join(lane.worktreePath, relFolder, dirName);
|
|
1909
|
+
for (const relFile of listFilesRecursively(laneDir)) {
|
|
1910
|
+
const rp = `${relFolder}/${dirName}/${relFile}`;
|
|
1911
|
+
allowedRelPaths.add(rp);
|
|
1912
|
+
relPathToWorktree.set(rp, join(lane.worktreePath, rp));
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const repoDir = join(repoRoot, relFolder, dirName);
|
|
1916
|
+
for (const relFile of listFilesRecursively(repoDir)) {
|
|
1917
|
+
const rp = `${relFolder}/${dirName}/${relFile}`;
|
|
1918
|
+
allowedRelPaths.add(rp);
|
|
1919
|
+
}
|
|
1871
1920
|
}
|
|
1872
1921
|
}
|
|
1873
1922
|
}
|
|
@@ -1875,12 +1924,44 @@ export async function mergeWave(
|
|
|
1875
1924
|
if (allowedRelPaths.size > 0) {
|
|
1876
1925
|
let staged = 0;
|
|
1877
1926
|
let skipped = 0;
|
|
1927
|
+
let preserved = 0;
|
|
1878
1928
|
|
|
1879
1929
|
for (const relPath of allowedRelPaths) {
|
|
1880
|
-
const srcPath = join(repoRoot, relPath);
|
|
1881
|
-
if (!existsSync(srcPath)) continue; // File not present (e.g., no REVIEW_VERDICT.json) — skip silently
|
|
1882
|
-
|
|
1883
1930
|
const destPath = join(mergeWorkDir, relPath);
|
|
1931
|
+
|
|
1932
|
+
// TP-099: If the file already exists in mergeWorkDir (from lane merge),
|
|
1933
|
+
// do NOT overwrite it — the lane merge brought the correct worker-updated
|
|
1934
|
+
// version (e.g., STATUS.md with checked items, execution log, discoveries).
|
|
1935
|
+
// Overwriting from repoRoot would revert to the pre-execution template.
|
|
1936
|
+
if (existsSync(destPath)) {
|
|
1937
|
+
preserved++;
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// File missing from mergeWorkDir — backfill from best available source.
|
|
1942
|
+
// Primary: lane worktree (has worker-generated .DONE/STATUS/.reviews content).
|
|
1943
|
+
// Fallback: repoRoot (original task folder, with path containment check).
|
|
1944
|
+
const worktreeSrc = relPathToWorktree.get(relPath);
|
|
1945
|
+
let srcPath: string | null = null;
|
|
1946
|
+
|
|
1947
|
+
// Try lane worktree first (trusted engine-allocated path)
|
|
1948
|
+
if (worktreeSrc && existsSync(worktreeSrc)) {
|
|
1949
|
+
srcPath = worktreeSrc;
|
|
1950
|
+
} else {
|
|
1951
|
+
// Fallback to repoRoot with path containment check (TP-035 hardening)
|
|
1952
|
+
const repoRootSrc = join(repoRoot, relPath);
|
|
1953
|
+
if (existsSync(repoRootSrc)) {
|
|
1954
|
+
const resolvedSrc = resolve(repoRootSrc);
|
|
1955
|
+
const srcRelToRepo = relative(resolvedRepoRoot, resolvedSrc).replace(/\\/g, "/");
|
|
1956
|
+
if (srcRelToRepo.startsWith("..") || srcRelToRepo.startsWith("/")) {
|
|
1957
|
+
execLog("merge", `W${waveIndex}`, `skipping artifact source outside repo root`, { path: relPath, src: repoRootSrc });
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
srcPath = repoRootSrc;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (!srcPath) continue; // File not present anywhere — skip silently
|
|
1964
|
+
|
|
1884
1965
|
try {
|
|
1885
1966
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
1886
1967
|
copyFileSync(srcPath, destPath);
|
|
@@ -1894,13 +1975,14 @@ export async function mergeWave(
|
|
|
1894
1975
|
}
|
|
1895
1976
|
|
|
1896
1977
|
if (staged > 0) {
|
|
1897
|
-
spawnSync("git", ["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md, REVIEW_VERDICT.json)`], { cwd: mergeWorkDir });
|
|
1978
|
+
spawnSync("git", ["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md, REVIEW_VERDICT.json, .reviews/*)`], { cwd: mergeWorkDir });
|
|
1898
1979
|
execLog("merge", `W${waveIndex}`, `committed ${staged} task artifact(s) to merge worktree`, {
|
|
1899
1980
|
skipped,
|
|
1981
|
+
preserved,
|
|
1900
1982
|
allowedCandidates: allowedRelPaths.size,
|
|
1901
1983
|
});
|
|
1902
1984
|
} else {
|
|
1903
|
-
execLog("merge", `W${waveIndex}`, `no task artifacts to stage (0 of ${allowedRelPaths.size} candidates present/changed)`);
|
|
1985
|
+
execLog("merge", `W${waveIndex}`, `no task artifacts to stage (0 of ${allowedRelPaths.size} candidates present/changed, ${preserved} preserved from lane merge)`);
|
|
1904
1986
|
}
|
|
1905
1987
|
|
|
1906
1988
|
// Keep both .DONE and STATUS.md in develop's working tree:
|
|
@@ -770,6 +770,12 @@ You have these orchestrator tools available:
|
|
|
770
770
|
**Note:** `orch_retry_task`, `orch_skip_task`, and `orch_force_merge` require the batch to be paused/stopped first.
|
|
771
771
|
If the batch is actively running, call `orch_pause()` first.
|
|
772
772
|
|
|
773
|
+
**Diagnostic & Recovery Tools (TP-096):**
|
|
774
|
+
- `read_agent_status(lane?)` — Read STATUS.md + telemetry for a lane (step, progress, context %, cost, elapsed). Omit lane for all lanes.
|
|
775
|
+
- `trigger_wrap_up(lane)` — Write `.task-wrap-up` signal to gracefully stop a worker on a lane.
|
|
776
|
+
- `read_lane_logs(lane)` — Read stderr/crash logs and exit diagnostics for a lane.
|
|
777
|
+
- `list_active_agents()` — List all tmux sessions with role, lane, task, context %, elapsed, cost.
|
|
778
|
+
|
|
773
779
|
Plus general tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`
|
|
774
780
|
for inspecting files, running git commands, and editing batch state.
|
|
775
781
|
|
package/package.json
CHANGED
|
@@ -53,12 +53,11 @@ edit STATUS.md
|
|
|
53
53
|
|
|
54
54
|
Then **check for wrap-up signal:**
|
|
55
55
|
```bash
|
|
56
|
-
if test -f "<TASK_FOLDER>/.task-wrap-up"
|
|
56
|
+
if test -f "<TASK_FOLDER>/.task-wrap-up"; then
|
|
57
57
|
echo "WRAP_UP_SIGNAL"
|
|
58
58
|
fi
|
|
59
59
|
```
|
|
60
|
-
|
|
61
|
-
If either signal exists, STOP immediately after this checkpoint.
|
|
60
|
+
If the signal exists, STOP immediately after this checkpoint.
|
|
62
61
|
|
|
63
62
|
If you do work but don't edit STATUS.md, that work is INVISIBLE to the
|
|
64
63
|
orchestrator and you will be re-spawned to do it again.
|
|
@@ -262,6 +261,19 @@ Do NOT:
|
|
|
262
261
|
- Modify docs listed in `task-runner.yaml → protected_docs` without explicit approval
|
|
263
262
|
- Expand task scope — add tech debt instead
|
|
264
263
|
|
|
264
|
+
## Steering Messages
|
|
265
|
+
|
|
266
|
+
During orchestrated runs, the supervisor may send steering messages to adjust
|
|
267
|
+
your approach. These messages appear in your conversation as user messages at
|
|
268
|
+
turn boundaries. They are also logged in the STATUS.md execution log as
|
|
269
|
+
`⚠️ Steering` entries for audit visibility.
|
|
270
|
+
|
|
271
|
+
When you receive a steering message:
|
|
272
|
+
1. **Read it carefully** — it contains course corrections from the supervisor
|
|
273
|
+
2. **Adjust your approach** as directed
|
|
274
|
+
3. **Continue working** — do not stop or restart; incorporate the guidance naturally
|
|
275
|
+
4. Steering messages are authoritative — treat them like direct instructions
|
|
276
|
+
|
|
265
277
|
## Error Handling
|
|
266
278
|
|
|
267
279
|
- If stuck on the same issue after 3 attempts, document the blocker in STATUS.md
|