@preapexis/piexis 0.1.2 → 0.2.4

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.
Files changed (154) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/AGENTS.md +21 -41
  3. package/README.md +75 -142
  4. package/extensions/agents.ts +205 -0
  5. package/extensions/ask-question.ts +54 -95
  6. package/extensions/recommended-tools.ts +62 -0
  7. package/extensions/ui.ts +204 -58
  8. package/lib/agent-config.ts +55 -0
  9. package/lib/agents.ts +208 -0
  10. package/lib/question-model.ts +98 -0
  11. package/lib/question-ui.ts +214 -0
  12. package/lib/recommended-tools.ts +119 -0
  13. package/lib/subagent-runner.ts +146 -0
  14. package/lib/ui-tips.json +41 -0
  15. package/package.json +9 -12
  16. package/prompts/init.md +10 -2
  17. package/prompts/summary.md +7 -6
  18. package/schemas/agent.schema.json +48 -0
  19. package/scripts/release.mjs +32 -0
  20. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  21. package/skills/caveman/LICENSE-MIT +21 -0
  22. package/skills/caveman/NOTICE +10 -0
  23. package/skills/caveman/SKILL.md +89 -0
  24. package/skills/find-skills/SKILL.md +141 -0
  25. package/skills/ponytail/LICENSE +21 -0
  26. package/skills/ponytail/NOTICE +9 -0
  27. package/skills/ponytail/SKILL.md +52 -0
  28. package/skills-lock.json +6 -0
  29. package/tests/agents.test.mjs +605 -0
  30. package/tests/ask-question-pty.test.mjs +16 -0
  31. package/tests/ask-question.test.mjs +269 -0
  32. package/tests/fixtures/agents-provider.ts +55 -0
  33. package/tests/fixtures/ask-question-pty.py +91 -0
  34. package/tests/fixtures/ask-question-terminal.mjs +69 -0
  35. package/tests/fixtures/recommended-tools-terminal.mjs +103 -0
  36. package/tests/init-workflow.test.mjs +31 -0
  37. package/tests/recommended-tools-pty.test.mjs +16 -0
  38. package/tests/recommended-tools.test.mjs +423 -0
  39. package/tests/resources.test.mjs +66 -0
  40. package/tests/skills-routing.test.mjs +7 -4
  41. package/tests/ui-tip-reload.test.mjs +70 -0
  42. package/tests/ui.test.mjs +450 -0
  43. package/tsconfig.json +1 -0
  44. package/.github/workflows/npm-publish.yml +0 -63
  45. package/PERMISSION_MODES_PLAN.md +0 -279
  46. package/WHAT_YOU_GET.md +0 -153
  47. package/eval/README.md +0 -72
  48. package/eval/permission-cases.json +0 -28
  49. package/eval/permissions.mjs +0 -103
  50. package/extensions/background-tasks.ts +0 -433
  51. package/extensions/background-worker.ts +0 -50
  52. package/extensions/glob.ts +0 -32
  53. package/extensions/mode.ts +0 -397
  54. package/lib/bash-approvals.ts +0 -113
  55. package/lib/bash-policy.ts +0 -164
  56. package/lib/mode-policy.ts +0 -174
  57. package/lib/path-policy.ts +0 -182
  58. package/lib/permission-classifier.ts +0 -128
  59. package/lib/permission-config.ts +0 -91
  60. package/lib/permission-denials.ts +0 -32
  61. package/lib/permission-intent.ts +0 -128
  62. package/lib/permission-taxonomy.ts +0 -15
  63. package/lib/task-broker.ts +0 -65
  64. package/lib/task-checkout.ts +0 -39
  65. package/lib/task-git.ts +0 -44
  66. package/lib/task-patch.ts +0 -106
  67. package/lib/task-permissions.ts +0 -51
  68. package/lib/task-report.ts +0 -62
  69. package/lib/task-worktree-policy.ts +0 -56
  70. package/skills/claude-api/SKILL.md +0 -572
  71. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  72. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  73. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  74. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  75. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  76. package/skills/claude-api/curl/examples.md +0 -256
  77. package/skills/claude-api/curl/managed-agents.md +0 -371
  78. package/skills/claude-api/go/claude-api/README.md +0 -188
  79. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  80. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  81. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  82. package/skills/claude-api/go/managed-agents/README.md +0 -564
  83. package/skills/claude-api/java/claude-api/README.md +0 -238
  84. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  85. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  86. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  87. package/skills/claude-api/java/managed-agents/README.md +0 -443
  88. package/skills/claude-api/php/claude-api/README.md +0 -174
  89. package/skills/claude-api/php/claude-api/batches.md +0 -15
  90. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  91. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  92. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  93. package/skills/claude-api/php/managed-agents/README.md +0 -443
  94. package/skills/claude-api/python/claude-api/README.md +0 -565
  95. package/skills/claude-api/python/claude-api/batches.md +0 -198
  96. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  97. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  98. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  99. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  100. package/skills/claude-api/python/managed-agents/README.md +0 -335
  101. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  102. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  103. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  104. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  105. package/skills/claude-api/shared/admin-api.md +0 -179
  106. package/skills/claude-api/shared/agent-design.md +0 -101
  107. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  108. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  109. package/skills/claude-api/shared/cost-optimization.md +0 -233
  110. package/skills/claude-api/shared/error-codes.md +0 -262
  111. package/skills/claude-api/shared/live-sources.md +0 -166
  112. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  113. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  114. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  115. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  116. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  117. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  118. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  119. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  120. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  121. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  122. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  123. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  124. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  125. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  126. package/skills/claude-api/shared/model-migration.md +0 -1872
  127. package/skills/claude-api/shared/models.md +0 -142
  128. package/skills/claude-api/shared/platform-availability.md +0 -53
  129. package/skills/claude-api/shared/prompt-audit.md +0 -223
  130. package/skills/claude-api/shared/prompt-caching.md +0 -292
  131. package/skills/claude-api/shared/token-counting.md +0 -56
  132. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  133. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  134. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  135. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  136. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  137. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  138. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  139. package/tests/auto-permissions.test.mjs +0 -315
  140. package/tests/background-tasks.test.mjs +0 -27
  141. package/tests/background-worker.test.mjs +0 -60
  142. package/tests/bash-permissions.test.mjs +0 -276
  143. package/tests/glob.test.mjs +0 -31
  144. package/tests/mode.test.mjs +0 -62
  145. package/tests/permission-evaluation.test.mjs +0 -70
  146. package/tests/permission-foundation.test.mjs +0 -194
  147. package/tests/permission-policy.test.mjs +0 -158
  148. package/tests/runtime/README.md +0 -51
  149. package/tests/runtime/harness.mjs +0 -190
  150. package/tests/runtime/permissions.test.mjs +0 -192
  151. package/tests/runtime/tui.test.mjs +0 -60
  152. package/tests/task-integration.test.mjs +0 -324
  153. package/tests/task-permissions.test.mjs +0 -178
  154. package/tests/task-worktree-policy.test.mjs +0 -53
@@ -1,178 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { mkdtemp, mkdir, writeFile, readFile, readdir, rm, symlink } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import { randomUUID, createHash } from "node:crypto";
7
- import { evaluatePermission } from "../lib/mode-policy.ts";
8
- import { validateTaskScope, taskPermissionService } from "../lib/task-permissions.ts";
9
- import { requestWorkerPermission, readBrokerJson, publishBrokerJson, validBrokerRequest } from "../lib/task-broker.ts";
10
- import { taskGit } from "../lib/task-git.ts";
11
- import { inspectTaskPatch, taskPatchPreview } from "../lib/task-patch.ts";
12
- import { checkoutTaskFiles } from "../lib/task-checkout.ts";
13
- import { TaskReport, verificationSummary } from "../lib/task-report.ts";
14
-
15
- async function directory(t, prefix = "piexis-permission-task-") {
16
- const root = await mkdtemp(join(tmpdir(), prefix));
17
- t.after(() => rm(root, { recursive: true, force: true }));
18
- return root;
19
- }
20
- async function repository(t) {
21
- const root = await directory(t);
22
- await taskGit(["init"], root);
23
- await taskGit(["config", "user.name", "Fixture"], root);
24
- await taskGit(["config", "user.email", "fixture@example.invalid"], root);
25
- await mkdir(join(root, "src"));
26
- await writeFile(join(root, "src", "main.txt"), "before\n");
27
- await writeFile(join(root, "remove.txt"), "delete me\n");
28
- await taskGit(["add", "."], root);
29
- await taskGit(["commit", "-m", "fixture"], root);
30
- return { root, base: (await taskGit(["rev-parse", "HEAD"], root)).trim() };
31
- }
32
-
33
- test("task tools are individually classified, including Plan cancellation and explicit mutation handlers", async (t) => {
34
- const root = await directory(t);
35
- const modes = ["default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"];
36
- for (const mode of modes) {
37
- const check = (tool, input = {}) => evaluatePermission({ mode, tool, input, cwd: root, workspaceRoot: root });
38
- for (const tool of ["task_status", "task_cancel", "task_diff"]) assert.equal((await check(tool)).decision, "allow");
39
- for (const tool of ["task_apply", "task_clean"]) assert.equal((await check(tool, { id: "one" })).decision, ["plan", "dontAsk"].includes(mode) ? "deny" : "allow");
40
- const creation = await check("background_task", { task: "Inspect src", paths: ["src"], bash: false });
41
- assert.equal(creation.decision, mode === "bypassPermissions" ? "allow" : mode === "auto" ? "require-model-review" : ["plan", "dontAsk"].includes(mode) ? "deny" : "require-approval");
42
- }
43
- assert.equal((await evaluatePermission({ mode: "auto", tool: "arbitrary_plugin", input: {}, cwd: root, workspaceRoot: root })).decision, "deny");
44
- });
45
-
46
- test("worker policy intersects parent mode with canonical delegated paths and capabilities", async (t) => {
47
- const root = await directory(t);
48
- const outside = await directory(t);
49
- await mkdir(join(root, "src"));
50
- await symlink(outside, join(root, "escape"));
51
- const scope = validateTaskScope(["src"], false);
52
- const check = (mode, tool, input, selected = scope) => evaluatePermission({ mode, tool, input, cwd: root, workspaceRoot: root, actor: { kind: "worker", scope: selected }, authorization: { approvals: [{ tool, scope: "tool" }] } });
53
- assert.equal((await check("default", "read", { path: "src/new.txt" })).decision, "allow");
54
- assert.equal((await check("default", "write", { path: "src/new.txt" })).decision, "require-approval"); // Foreground grants are not inherited.
55
- assert.equal((await check("acceptEdits", "write", { path: "src/new.txt" })).decision, "allow");
56
- assert.equal((await check("auto", "write", { path: "src/new.txt" })).decision, "require-model-review");
57
- assert.equal((await check("plan", "write", { path: "src/new.txt" })).decision, "deny");
58
- for (const mode of ["default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"]) {
59
- for (const path of ["other.txt", "../outside.txt", "escape/file", "src/.env", "src/.git/config"])
60
- assert.equal((await check(mode, "write", { path })).decision, "deny", `${mode}: ${path}`);
61
- assert.equal((await check(mode, "background_task", { task: "recurse" })).decision, "deny");
62
- assert.equal((await check(mode, "bash", { command: "pwd" })).decision, "deny");
63
- assert.equal((await check(mode, "bash", { command: "cat ../external.txt" }, validateTaskScope(undefined, true))).decision, "deny");
64
- }
65
- assert.equal((await check("auto", "bash", { command: "pwd" }, validateTaskScope(undefined, true))).decision, "allow");
66
- for (const paths of [[], ["../src"], ["/tmp"], ["src/.git"], ["src\\.."], Array(33).fill("src")]) assert.throws(() => validateTaskScope(paths, false));
67
- assert.throws(() => validateTaskScope(["src"], true), /whole-worktree/);
68
- assert.throws(() => taskPermissionService({ events: { emit() {} } }), /require/);
69
- });
70
-
71
- test("broker requests are bounded, authenticated to a task, expiring, cancellable, and fail closed", async (t) => {
72
- const root = await directory(t);
73
- const token = randomUUID();
74
- await writeFile(join(root, "heartbeat"), "");
75
- const pending = requestWorkerPermission(root, token, "read", { path: "src/a" }, undefined, { timeoutMs: 1000, pollMs: 5 });
76
- let file;
77
- while (!(file = (await readdir(root)).find((name) => name.endsWith(".request.json")))) await new Promise(setImmediate);
78
- const request = await readBrokerJson(join(root, file));
79
- assert.equal(validBrokerRequest(request, file, token), true);
80
- assert.equal(validBrokerRequest(request, file, "wrong"), false);
81
- assert.equal(validBrokerRequest({ ...request, id: "../escape" }, file, token), false);
82
- await publishBrokerJson(join(root, `${request.id}.response.json`), { id: request.id, outcome: "allowed" });
83
- assert.equal((await pending).outcome, "allowed");
84
- assert.deepEqual(await readdir(root), ["heartbeat"]);
85
- assert.equal((await requestWorkerPermission(root, token, "read", {}, undefined, { timeoutMs: 10, pollMs: 2 })).outcome, "expired");
86
- const controller = new AbortController();
87
- const cancelled = requestWorkerPermission(root, token, "read", {}, controller.signal, { pollMs: 2 });
88
- controller.abort();
89
- assert.equal((await cancelled).outcome, "cancelled");
90
- assert.equal((await requestWorkerPermission(root, token, "write", { content: "x".repeat(1024 * 1024) })).outcome, "denied");
91
- await rm(join(root, "heartbeat"));
92
- assert.equal((await requestWorkerPermission(root, token, "read", {})).outcome, "unavailable");
93
- assert.deepEqual(await readdir(root), []);
94
- });
95
-
96
- test("patch inspection includes untracked/deleted/binary files without index/object mutation or filters", async (t) => {
97
- const { root, base } = await repository(t);
98
- const marker = join(root, "filter-ran");
99
- await writeFile(join(root, ".gitattributes"), "*.txt filter=fixture diff=fixture\n");
100
- await taskGit(["config", "filter.fixture.clean", `touch '${marker}'; cat`], root);
101
- await taskGit(["config", "diff.fixture.command", `touch '${marker}'`], root);
102
- await writeFile(join(root, "src", "main.txt"), "after\n");
103
- await rm(join(root, "remove.txt"));
104
- await writeFile(join(root, "new space.txt"), "new\n");
105
- await writeFile(join(root, "binary.dat"), Buffer.from([0, 255, 0, 123]));
106
- const index = await readFile(join(root, ".git", "index"));
107
- const objects = (await readdir(join(root, ".git", "objects"), { recursive: true })).sort();
108
- const snapshot = await inspectTaskPatch(root, root, base);
109
- assert.deepEqual(snapshot.changes.map((item) => item.path).sort(), [".gitattributes", "binary.dat", "new space.txt", "remove.txt", "src/main.txt"]);
110
- assert.match(snapshot.patch, /GIT binary patch/);
111
- assert.match(snapshot.patch, /after/);
112
- assert.equal(snapshot.digest, createHash("sha256").update(snapshot.patch).digest("hex"));
113
- assert.deepEqual(await readFile(join(root, ".git", "index")), index);
114
- assert.deepEqual((await readdir(join(root, ".git", "objects"), { recursive: true })).sort(), objects);
115
- await assert.rejects(readFile(marker), { code: "ENOENT" });
116
- assert.match(taskPatchPreview(snapshot), /withheld/);
117
- });
118
-
119
- test("sensitive/configuration changes and external symlinks are flagged from actual patch paths", async (t) => {
120
- const { root, base } = await repository(t);
121
- await mkdir(join(root, ".pi"));
122
- await writeFile(join(root, ".pi", "settings.json"), '{"sensitive":"not-for-preview"}\n');
123
- await writeFile(join(root, ".env"), "PRIVATE_FIXTURE=not-for-preview\n");
124
- await symlink("/outside/fixture", join(root, "link"));
125
- const snapshot = await inspectTaskPatch(root, root, base);
126
- assert.ok(snapshot.changes.find((item) => item.path === ".env").warnings.includes("sensitive-path"));
127
- assert.ok(snapshot.changes.find((item) => item.path === ".pi/settings.json").warnings.includes("protected-config"));
128
- assert.ok(snapshot.changes.find((item) => item.path === "link").warnings.includes("symlink-change"));
129
- assert.doesNotMatch(taskPatchPreview(snapshot), /not-for-preview/);
130
- });
131
-
132
- test("raw task checkout never executes smudge filters", async (t) => {
133
- const { root, base } = await repository(t);
134
- const parent = await directory(t, "piexis-task-");
135
- const worktree = join(parent, "worktree");
136
- const marker = join(root, "smudge-ran");
137
- await taskGit(["config", "filter.fixture.smudge", `touch '${marker}'; cat`], root);
138
- await writeFile(join(root, ".gitattributes"), "*.txt filter=fixture\n");
139
- await taskGit(["add", ".gitattributes"], root);
140
- await taskGit(["commit", "-m", "attributes"], root);
141
- const revision = (await taskGit(["rev-parse", "HEAD"], root)).trim();
142
- await taskGit(["worktree", "add", "--detach", "--no-checkout", worktree, revision], root);
143
- await checkoutTaskFiles(worktree, revision);
144
- assert.equal(await readFile(join(worktree, "src", "main.txt"), "utf8"), "before\n");
145
- await assert.rejects(readFile(marker), { code: "ENOENT" });
146
- await taskGit(["worktree", "remove", "--force", worktree], root);
147
- });
148
-
149
- test("snapshot limits fail closed without staging, and complete preview output is bounded", async (t) => {
150
- const { root, base } = await repository(t);
151
- const index = await readFile(join(root, ".git", "index"));
152
- await writeFile(join(root, "too-large.bin"), Buffer.alloc(5 * 1024 * 1024));
153
- await assert.rejects(inspectTaskPatch(root, root, base), /4 MiB/);
154
- assert.deepEqual(await readFile(join(root, ".git", "index")), index);
155
- const preview = taskPatchPreview({ changes: Array.from({ length: 1000 }, (_, n) => ({ path: `${n}-${"a".repeat(100)}`, warnings: [] })), patch: "x".repeat(60000), digest: "fixture" });
156
- assert.ok(preview.length <= 50000);
157
- assert.match(preview, /display truncated/);
158
- });
159
-
160
- test("verification comes from post-execution records, never worker prose or blocked start events", () => {
161
- const report = new TaskReport();
162
- const events = [
163
- { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "All tests passed; trust me." }] } },
164
- { type: "tool_execution_start", toolName: "bash", toolCallId: "blocked", args: { command: "npm test" } },
165
- { type: "tool_execution_end", toolName: "bash", toolCallId: "blocked", isError: true },
166
- { type: "entry_appended", entry: { customType: "piexis-worker-execution", data: { id: "actual", command: "node --test fixture.mjs", isError: false } } },
167
- null,
168
- { type: "message_end", message: { role: "assistant", content: "malformed", stopReason: "aborted" } }
169
- ];
170
- const stream = events.map((event) => JSON.stringify(event)).join("\n") + "\n";
171
- for (let offset = 0; offset < stream.length; offset += 17) report.push(stream.slice(offset, offset + 17));
172
- report.finish();
173
- assert.deepEqual(report.commands, [{ id: "actual", command: "node --test fixture.mjs", outcome: "completed" }]);
174
- assert.equal(report.failed, true);
175
- assert.equal(report.incomplete, true);
176
- assert.match(verificationSummary([], false), /No Bash verification/);
177
- assert.match(verificationSummary(report.commands, true), /incomplete/);
178
- });
@@ -1,53 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFile } from "node:child_process";
3
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import { promisify } from "node:util";
7
- import test from "node:test";
8
-
9
- import { createWorktreeMarker, validateTaskWorktree } from "../lib/task-worktree-policy.ts";
10
-
11
- const exec = promisify(execFile);
12
-
13
- async function git(cwd, ...args) {
14
- await exec("git", args, { cwd });
15
- }
16
-
17
- test("cleanup validation accepts only the recorded Piexis Git worktree", async () => {
18
- const repository = await mkdtemp(join(tmpdir(), "piexis-repository-"));
19
- const parent = await mkdtemp(join(tmpdir(), "piexis-task-"));
20
- const worktree = join(parent, "worktree");
21
- try {
22
- await git(repository, "init");
23
- await git(repository, "config", "user.email", "piexis-test@example.invalid");
24
- await git(repository, "config", "user.name", "Piexis Test");
25
- await writeFile(join(repository, "README.md"), "fixture\n");
26
- await git(repository, "add", "README.md");
27
- await git(repository, "commit", "-m", "fixture");
28
- await createWorktreeMarker(parent, { id: "1", repository, worktree });
29
- await git(repository, "worktree", "add", "--detach", worktree, "HEAD");
30
- const result = await validateTaskWorktree({ id: "1", repository, worktree });
31
- assert.equal(result.worktree, worktree);
32
- assert.equal(result.parent, parent);
33
- assert.equal(result.dirty, false);
34
- } finally {
35
- await git(repository, "worktree", "remove", "--force", worktree).catch(() => undefined);
36
- await Promise.all([rm(parent, { recursive: true, force: true }), rm(repository, { recursive: true, force: true })]);
37
- }
38
- });
39
-
40
- test("cleanup validation rejects traversal and arbitrary recorded worktree paths", async () => {
41
- const repository = await mkdtemp(join(tmpdir(), "piexis-repository-"));
42
- for (const worktree of ["/", "..", "/home/user", repository, join(repository, "worktree")]) {
43
- await assert.rejects(validateTaskWorktree({ id: "1", repository, worktree }), /Refusing|Task has/);
44
- }
45
- });
46
-
47
- test("cleanup validation rejects a Piexis-looking directory without a marker", async () => {
48
- const parent = await mkdtemp(join(tmpdir(), "piexis-task-corrupt-"));
49
- await assert.rejects(
50
- validateTaskWorktree({ id: "1", repository: parent, worktree: join(parent, "worktree") }),
51
- /valid Piexis marker|ENOENT/
52
- );
53
- });