@selesai/code 0.8.4 → 0.8.6
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/CHANGELOG.md +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
|
@@ -309,4 +309,32 @@ describe("finalizeSingleOutput", () => {
|
|
|
309
309
|
|
|
310
310
|
assert.equal(result.displayOutput, "truncated output");
|
|
311
311
|
});
|
|
312
|
+
|
|
313
|
+
it("preserves the saved-output reference for acceptance-only failures", () => {
|
|
314
|
+
const result = finalizeSingleOutput({
|
|
315
|
+
fullOutput: "useful output",
|
|
316
|
+
outputPath: "/tmp/review.md",
|
|
317
|
+
outputMode: "file-only",
|
|
318
|
+
exitCode: 1,
|
|
319
|
+
preserveSavedOutput: true,
|
|
320
|
+
savedPath: "/tmp/review.md",
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
assert.match(result.displayOutput, /^Output saved to:/);
|
|
324
|
+
assert.doesNotMatch(result.displayOutput, /useful output/);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does not duplicate an existing saved-output reference", () => {
|
|
328
|
+
const outputReference = formatSavedOutputReference("/tmp/review.md", "useful output");
|
|
329
|
+
const result = finalizeSingleOutput({
|
|
330
|
+
fullOutput: `useful output\n\n${outputReference.message}`,
|
|
331
|
+
outputPath: "/tmp/review.md",
|
|
332
|
+
exitCode: 1,
|
|
333
|
+
preserveSavedOutput: true,
|
|
334
|
+
savedPath: "/tmp/review.md",
|
|
335
|
+
outputReference,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
assert.equal(result.displayOutput.match(/Output saved to:/g)?.length, 1);
|
|
339
|
+
});
|
|
312
340
|
});
|
|
@@ -2,11 +2,12 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { describe, it } from "node:test";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
6
|
import { writeAtomicJson, writePrivateAtomicJson } from "../../src/shared/atomic-json.ts";
|
|
7
7
|
import { closeSteerInbox, interruptRequestPath, steerRequestsDir, writeSteerAck, type SteerRequest } from "../../src/runs/background/control-channel.ts";
|
|
8
8
|
import { steerAsyncRun } from "../../src/runs/foreground/async-steering-action.ts";
|
|
9
9
|
import { createSteeringStatus, recordSteeringRequest, updateSteeringTarget } from "../../src/runs/background/steering.ts";
|
|
10
|
+
import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
10
11
|
import { ASYNC_DIR, type AsyncStatus, type Details, type SteeringRecoveryDescriptor, type SteeringTargetState, type SubagentState } from "../../src/shared/types.ts";
|
|
11
12
|
|
|
12
13
|
function createState(): SubagentState {
|
|
@@ -29,6 +30,11 @@ function createState(): SubagentState {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
let statusWriteMtimeMs = Date.now();
|
|
33
|
+
const budgetDirectories: string[] = [];
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
for (const directory of budgetDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true });
|
|
37
|
+
});
|
|
32
38
|
|
|
33
39
|
function writeStatus(asyncDir: string, status: AsyncStatus): void {
|
|
34
40
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
@@ -91,8 +97,11 @@ async function readRequest(asyncDir: string): Promise<SteerRequest> {
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
function recoveryDescriptor(runId: string): SteeringRecoveryDescriptor {
|
|
100
|
+
const runFanoutBudget = createRunFanoutBudget(runId, 64);
|
|
101
|
+
budgetDirectories.push(runFanoutBudget.directory);
|
|
94
102
|
return {
|
|
95
103
|
version: 1,
|
|
104
|
+
runFanoutBudget,
|
|
96
105
|
sourceRunId: runId,
|
|
97
106
|
agent: "worker-0",
|
|
98
107
|
cwd: os.tmpdir(),
|
|
@@ -61,6 +61,45 @@ describe("classifyTaskMutationIntent", () => {
|
|
|
61
61
|
assert.equal(classifyTaskMutationIntent("worker", "Create a summary").kind, "unknown");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it("does not read hyphenated fix adjectives as the fix verb", () => {
|
|
65
|
+
// Core regression: "must-fix items" in review output specs (fails on old code).
|
|
66
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return a review with the top 2-3 must-fix items").kind, "unknown");
|
|
67
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return a review with the top 2-3 must-fix items.").kind, "unknown");
|
|
68
|
+
assert.equal(classifyTaskMutationIntent("worker", "Review and report MUST-FIX items").kind, "unknown");
|
|
69
|
+
// should-fix with a noun from the pattern's alternation (also fails on old code).
|
|
70
|
+
assert.equal(classifyTaskMutationIntent("worker", "List should-fix tests with severity labels").kind, "unknown");
|
|
71
|
+
// Same class for other verbs: hyphenated adjective + verb.
|
|
72
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return must-edit findings").kind, "unknown");
|
|
73
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return must-update items").kind, "unknown");
|
|
74
|
+
assert.equal(classifyTaskMutationIntent("worker", "List should-add files").kind, "unknown");
|
|
75
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return must-apply changes").kind, "unknown");
|
|
76
|
+
assert.equal(classifyTaskMutationIntent("worker", "List should-make changes").kind, "unknown");
|
|
77
|
+
assert.equal(classifyTaskMutationIntent("worker", "must-do those fixes").kind, "unknown");
|
|
78
|
+
// Dash coverage: U+2010 hyphen, U+2011 non-breaking hyphen, en/em dashes.
|
|
79
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return the must\u2010fix items").kind, "unknown");
|
|
80
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return the must\u2011fix items").kind, "unknown");
|
|
81
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return the must\u2013fix items").kind, "unknown");
|
|
82
|
+
assert.equal(classifyTaskMutationIntent("worker", "Return the must\u2014fix items").kind, "unknown");
|
|
83
|
+
// Clause-level em/en dashes are punctuation, not compounds: "branch—fix it".
|
|
84
|
+
assert.equal(classifyTaskMutationIntent("worker", "Inspect the failing branch\u2014fix it.").kind, "implementation");
|
|
85
|
+
assert.equal(classifyTaskMutationIntent("worker", "Inspect the failing branch\u2013fix it.").kind, "implementation");
|
|
86
|
+
assert.equal(taskMayMutate("Inspect the failing branch\u2014fix it."), true);
|
|
87
|
+
// Hyphenated genuine imperatives stay write-capable.
|
|
88
|
+
assert.equal(taskMayMutate("hot-fix the bug"), true);
|
|
89
|
+
assert.equal(taskMayMutate("quick-fix that bug"), true);
|
|
90
|
+
// Genuine imperative usage keeps classifying as implementation.
|
|
91
|
+
assert.equal(classifyTaskMutationIntent("worker", "Go through the list and fix items one by one").kind, "implementation");
|
|
92
|
+
assert.equal(classifyTaskMutationIntent("worker", "fix items on the sprint board").kind, "implementation");
|
|
93
|
+
// CLI flags keep write intent: "--fix" / "-w" are not hyphenated adjectives.
|
|
94
|
+
assert.equal(classifyTaskMutationIntent("worker", "Run eslint --fix code").kind, "implementation");
|
|
95
|
+
assert.equal(taskMayMutate("Run eslint --fix code"), true);
|
|
96
|
+
// "--write" was never a classifier verb (pre-existing unknown); the
|
|
97
|
+
// regression guard is that write-capability survives for acceptance.
|
|
98
|
+
assert.equal(taskMayMutate("Run prettier --write files"), true);
|
|
99
|
+
// Guard-facing mirror: the completion guard must not expect mutation.
|
|
100
|
+
assert.equal(expectsImplementationMutation("worker", "Return a review with the top 2-3 must-fix items"), false);
|
|
101
|
+
});
|
|
102
|
+
|
|
64
103
|
it("expectsImplementationMutation mirrors the classifier", () => {
|
|
65
104
|
assert.equal(expectsImplementationMutation("worker", "Do not modify tests; implement the fix"), true);
|
|
66
105
|
assert.equal(expectsImplementationMutation("worker", "Review the diff and suggest fixes only. Do not edit files."), false);
|
|
@@ -3,6 +3,7 @@ import { describe, it } from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_ASYNC_TIMEOUT_MS,
|
|
5
5
|
DEFAULT_FOREGROUND_TIMEOUT_MS,
|
|
6
|
+
resolveConfigDefaultTimeoutMs,
|
|
6
7
|
resolveSingleAgentLaunchTimeout,
|
|
7
8
|
} from "../../src/runs/foreground/subagent-executor.ts";
|
|
8
9
|
|
|
@@ -70,4 +71,68 @@ describe("single-agent launch timeout wiring", () => {
|
|
|
70
71
|
{ timeoutMs: 5_000 },
|
|
71
72
|
);
|
|
72
73
|
});
|
|
74
|
+
|
|
75
|
+
const NINETY_MIN = 90 * 60 * 1000;
|
|
76
|
+
|
|
77
|
+
it("applies the global config default in place of the foreground backstop", () => {
|
|
78
|
+
assert.deepEqual(resolveSingleAgentLaunchTimeout({}, false, NINETY_MIN), {
|
|
79
|
+
timeoutMs: NINETY_MIN,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("applies the global config default to composite foreground runs (parallel/chain)", () => {
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
resolveSingleAgentLaunchTimeout({ agent: "worker", task: "x", tasks: [{ agent: "worker", task: "y" }] }, false, NINETY_MIN),
|
|
86
|
+
{ timeoutMs: NINETY_MIN },
|
|
87
|
+
);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
resolveSingleAgentLaunchTimeout({ agent: "worker", task: "x", chain: [{ agent: "worker", task: "y" }] }, false, NINETY_MIN),
|
|
90
|
+
{ timeoutMs: NINETY_MIN },
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("applies the global config default in place of the single-agent async backstop", () => {
|
|
95
|
+
assert.deepEqual(resolveSingleAgentLaunchTimeout({}, true, NINETY_MIN), {
|
|
96
|
+
timeoutMs: NINETY_MIN,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("lets an explicit call timeout win over the global config default", () => {
|
|
101
|
+
assert.deepEqual(resolveSingleAgentLaunchTimeout({ timeoutMs: 5_000 }, false, NINETY_MIN), {
|
|
102
|
+
timeoutMs: 5_000,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("keeps composite async runs unbounded even with a global config default", () => {
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
resolveSingleAgentLaunchTimeout({ agent: "worker", task: "x", tasks: [{ agent: "worker", task: "y" }] }, true, NINETY_MIN),
|
|
109
|
+
{},
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("resolveConfigDefaultTimeoutMs", () => {
|
|
115
|
+
it("returns a positive integer unchanged", () => {
|
|
116
|
+
assert.equal(resolveConfigDefaultTimeoutMs(90 * 60 * 1000), 90 * 60 * 1000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("treats unset config as no default", () => {
|
|
120
|
+
assert.equal(resolveConfigDefaultTimeoutMs(undefined), undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("ignores non-positive, non-integer, and non-numeric values", () => {
|
|
124
|
+
assert.equal(resolveConfigDefaultTimeoutMs(0), undefined);
|
|
125
|
+
assert.equal(resolveConfigDefaultTimeoutMs(-1), undefined);
|
|
126
|
+
assert.equal(resolveConfigDefaultTimeoutMs(1.5), undefined);
|
|
127
|
+
assert.equal(resolveConfigDefaultTimeoutMs("600000" as unknown), undefined);
|
|
128
|
+
assert.equal(resolveConfigDefaultTimeoutMs(Number.NaN), undefined);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("accepts the maximum schedulable timer delay but ignores anything larger", () => {
|
|
132
|
+
// 2_147_483_647 is the largest delay a Node.js timer can honor; above it
|
|
133
|
+
// setTimeout overflows to ~1ms and the run would expire almost immediately.
|
|
134
|
+
assert.equal(resolveConfigDefaultTimeoutMs(2_147_483_647), 2_147_483_647);
|
|
135
|
+
assert.equal(resolveConfigDefaultTimeoutMs(2_147_483_648), undefined);
|
|
136
|
+
assert.equal(resolveConfigDefaultTimeoutMs(Number.MAX_SAFE_INTEGER), undefined);
|
|
137
|
+
});
|
|
73
138
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_MAX_WORKFLOW_AUTO_RELAUNCHES,
|
|
5
|
+
resolveAutoRelaunchDecision,
|
|
6
|
+
resolveMaxWorkflowAutoRelaunches,
|
|
7
|
+
} from "../../src/workflows/workflow-auto-relaunch.ts";
|
|
8
|
+
|
|
9
|
+
describe("workflow auto-relaunch policy", () => {
|
|
10
|
+
it("relaunches on a budget result while under the cap", () => {
|
|
11
|
+
const decision = resolveAutoRelaunchDecision({ result: "budget", rounds: 3 }, 2, 12);
|
|
12
|
+
assert.deepEqual(decision, { relaunch: true, capped: false });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("does not relaunch a clean result", () => {
|
|
16
|
+
const decision = resolveAutoRelaunchDecision({ result: "clean", rounds: 3 }, 0, 12);
|
|
17
|
+
assert.deepEqual(decision, { relaunch: false, capped: false });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not relaunch non-workflow values", () => {
|
|
21
|
+
for (const value of [undefined, null, 5, "budget", ["budget"], { result: "failed" }]) {
|
|
22
|
+
const decision = resolveAutoRelaunchDecision(value, 0, 12);
|
|
23
|
+
assert.deepEqual(decision, { relaunch: false, capped: false });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("marks the cap as reached when the relaunch count equals the cap", () => {
|
|
28
|
+
const decision = resolveAutoRelaunchDecision({ result: "budget" }, 12, 12);
|
|
29
|
+
assert.deepEqual(decision, { relaunch: false, capped: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("relaunches indefinitely when the cap is unlimited", () => {
|
|
33
|
+
const decision = resolveAutoRelaunchDecision({ result: "budget" }, 50, undefined);
|
|
34
|
+
assert.deepEqual(decision, { relaunch: true, capped: false });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("resolves the configured cap with a safe default and 0 as unlimited", () => {
|
|
38
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches(undefined), DEFAULT_MAX_WORKFLOW_AUTO_RELAUNCHES);
|
|
39
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches(5), 5);
|
|
40
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches(0), undefined);
|
|
41
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches(-3), DEFAULT_MAX_WORKFLOW_AUTO_RELAUNCHES);
|
|
42
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches("12"), DEFAULT_MAX_WORKFLOW_AUTO_RELAUNCHES);
|
|
43
|
+
assert.equal(resolveMaxWorkflowAutoRelaunches(2.5), DEFAULT_MAX_WORKFLOW_AUTO_RELAUNCHES);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -125,6 +125,67 @@ describe("worktree", () => {
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it("rejects worktree base directories inside Selesai extensions", () => {
|
|
129
|
+
const repoDir = createRepo("selesai-worktree-extension-dir-");
|
|
130
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-worktree-home-"));
|
|
131
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
132
|
+
const previousHome = process.env.HOME;
|
|
133
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
134
|
+
const extensionsDir = path.join(tempHome, ".selesai", "agent", "extensions");
|
|
135
|
+
try {
|
|
136
|
+
process.env.HOME = tempHome;
|
|
137
|
+
process.env.USERPROFILE = tempHome;
|
|
138
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
139
|
+
assert.throws(
|
|
140
|
+
() => createWorktrees(repoDir, "extension-dir", 1, { baseDir: extensionsDir }),
|
|
141
|
+
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
142
|
+
);
|
|
143
|
+
assert.throws(
|
|
144
|
+
() => createWorktrees(repoDir, "extension-subdir", 1, { baseDir: path.join(extensionsDir, "checkout") }),
|
|
145
|
+
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
149
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
150
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
151
|
+
else process.env.HOME = previousHome;
|
|
152
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
153
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
154
|
+
cleanupRepo(repoDir);
|
|
155
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("rejects symlinked worktree base directories inside Selesai extensions", () => {
|
|
160
|
+
const repoDir = createRepo("selesai-worktree-extension-symlink-");
|
|
161
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-worktree-home-"));
|
|
162
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
163
|
+
const previousHome = process.env.HOME;
|
|
164
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
165
|
+
const extensionsDir = path.join(tempHome, ".selesai", "agent", "extensions");
|
|
166
|
+
const aliasDir = path.join(tempHome, "extension-alias");
|
|
167
|
+
try {
|
|
168
|
+
process.env.HOME = tempHome;
|
|
169
|
+
process.env.USERPROFILE = tempHome;
|
|
170
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
171
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
172
|
+
fs.symlinkSync(extensionsDir, aliasDir, process.platform === "win32" ? "junction" : "dir");
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => createWorktrees(repoDir, "extension-symlink", 1, { baseDir: path.join(aliasDir, "checkout") }),
|
|
175
|
+
/worktree base directory cannot be inside Pi extensions directory/i,
|
|
176
|
+
);
|
|
177
|
+
} finally {
|
|
178
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
179
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
180
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
181
|
+
else process.env.HOME = previousHome;
|
|
182
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
183
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
184
|
+
cleanupRepo(repoDir);
|
|
185
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
128
189
|
it("uses SELESAI_SUBAGENTS_WORKTREE_DIR when no base directory is configured", () => {
|
|
129
190
|
const repoDir = createRepo("pi-worktree-env-base-dir-");
|
|
130
191
|
const previous = process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
|
|
@@ -16,6 +16,9 @@ export { filterSkillBodyForMode };
|
|
|
16
16
|
export const readDefaultMode = getDefaultMode;
|
|
17
17
|
|
|
18
18
|
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
|
|
19
|
+
// Unreachable: normalizePersistedMode always returns a valid mode or null,
|
|
20
|
+
// and the caller passes a valid default.
|
|
21
|
+
/* v8 ignore next 1 */
|
|
19
22
|
const fallback = normalizePersistedMode(fallbackMode) || DEFAULT_MODE;
|
|
20
23
|
if (!Array.isArray(entries)) return fallback;
|
|
21
24
|
|
|
@@ -59,6 +62,9 @@ export default function ponytailExtension(pi) {
|
|
|
59
62
|
|
|
60
63
|
const setMode = (mode, ctx) => {
|
|
61
64
|
const normalized = normalizePersistedMode(mode);
|
|
65
|
+
// Unreachable: setMode is only called with validated modes from the
|
|
66
|
+
// command handler and the deactivation input handler.
|
|
67
|
+
/* v8 ignore next 1 */
|
|
62
68
|
if (!normalized) return;
|
|
63
69
|
|
|
64
70
|
currentMode = normalized;
|
|
@@ -68,6 +74,8 @@ export default function ponytailExtension(pi) {
|
|
|
68
74
|
|
|
69
75
|
const sendAlias = (skillName, args, ctx) => {
|
|
70
76
|
const normalized = String(args || "").trim();
|
|
77
|
+
// Unreachable: the alias command handlers always pass empty args.
|
|
78
|
+
/* v8 ignore next 1 */
|
|
71
79
|
const message = normalized ? `${skillName} ${normalized}` : skillName;
|
|
72
80
|
|
|
73
81
|
if (ctx?.isIdle?.() === false) {
|
|
@@ -91,6 +99,9 @@ export default function ponytailExtension(pi) {
|
|
|
91
99
|
|
|
92
100
|
if (parsed.type === "set-default") {
|
|
93
101
|
const written = writeDefaultMode(parsed.mode);
|
|
102
|
+
// Unreachable: parsePonytailCommand only returns set-default for
|
|
103
|
+
// valid config modes, so writeDefaultMode always succeeds.
|
|
104
|
+
/* v8 ignore next 1 */
|
|
94
105
|
if (written) {
|
|
95
106
|
configuredDefaultMode = getDefaultMode();
|
|
96
107
|
const message = configuredDefaultMode === written
|
|
@@ -57,6 +57,8 @@ function getConfigDir() {
|
|
|
57
57
|
}
|
|
58
58
|
if (process.platform === 'win32') {
|
|
59
59
|
return path.join(
|
|
60
|
+
// Unreachable: APPDATA is always set on Windows.
|
|
61
|
+
/* v8 ignore next 1 */
|
|
60
62
|
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
61
63
|
'ponytail'
|
|
62
64
|
);
|
|
@@ -14,6 +14,9 @@ const COMPACT_MODE_RULES = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
function filterSkillBodyForMode(body, mode) {
|
|
17
|
+
// Unreachable: normalizeMode returns a valid mode or null, and the caller
|
|
18
|
+
// passes a valid mode.
|
|
19
|
+
/* v8 ignore next 1 */
|
|
17
20
|
const effectiveMode = normalizeMode(mode) || DEFAULT_MODE;
|
|
18
21
|
const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, '');
|
|
19
22
|
|
|
@@ -90,6 +93,9 @@ function getPonytailInstructions(mode, options) {
|
|
|
90
93
|
return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
// Unreachable: normalizePersistedMode above already falls back to
|
|
97
|
+
// DEFAULT_MODE, so normalizeMode always returns a valid mode here.
|
|
98
|
+
/* v8 ignore next 1 */
|
|
93
99
|
const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;
|
|
94
100
|
if (options?.compact === true) return getCompactInstructions(effectiveMode);
|
|
95
101
|
|