@selesai/code 0.3.7 → 0.3.8
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.
|
@@ -5,19 +5,7 @@ const { getCavemanInstructions } = require("./caveman-instructions.cjs");
|
|
|
5
5
|
|
|
6
6
|
export { getCavemanInstructions };
|
|
7
7
|
|
|
8
|
-
export function resolveSessionActive(entries, fallback = true) {
|
|
9
|
-
if (!Array.isArray(entries)) return fallback;
|
|
10
8
|
|
|
11
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
12
|
-
const entry = entries[i];
|
|
13
|
-
if (entry?.type !== "custom" || entry?.customType !== "caveman-mode") continue;
|
|
14
|
-
|
|
15
|
-
const active = entry?.data?.active;
|
|
16
|
-
if (typeof active === "boolean") return active;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return fallback;
|
|
20
|
-
}
|
|
21
9
|
|
|
22
10
|
export function parseCavemanCommand(text) {
|
|
23
11
|
const normalizedText = String(text || "").trim().toLowerCase();
|
|
@@ -93,8 +81,7 @@ export default function cavemanExtension(pi) {
|
|
|
93
81
|
});
|
|
94
82
|
|
|
95
83
|
pi.on("session_start", async (_event, ctx) => {
|
|
96
|
-
|
|
97
|
-
active = resolveSessionActive(entries, true);
|
|
84
|
+
active = true; // ponytail: always ON at session start; OFF is current-session-only
|
|
98
85
|
syncStatus(ctx);
|
|
99
86
|
ctx?.ui?.notify?.(`Caveman loaded: ${active ? "ON" : "OFF"}`, "info");
|
|
100
87
|
});
|
|
@@ -80,7 +80,7 @@ test("disabled caveman skips instruction injection", async () => {
|
|
|
80
80
|
assert.equal(result, undefined);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
test("session_start
|
|
83
|
+
test("session_start always ON even if persisted entry is OFF", async () => {
|
|
84
84
|
const { events } = createPiHarness();
|
|
85
85
|
const ctx = createCommandContext({
|
|
86
86
|
sessionManager: {
|
|
@@ -93,7 +93,22 @@ test("session_start restores latest persisted active flag", async () => {
|
|
|
93
93
|
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
94
94
|
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
95
95
|
|
|
96
|
-
assert.
|
|
96
|
+
assert.ok(result.systemPrompt.includes("CAVEMAN MODE ACTIVE"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("off then new session_start resets to ON", async () => {
|
|
100
|
+
const { commands, events } = createPiHarness();
|
|
101
|
+
const ctx = createCommandContext();
|
|
102
|
+
|
|
103
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
104
|
+
await commands.get("caveman").handler("off", ctx);
|
|
105
|
+
|
|
106
|
+
// Simulate a fresh session_start (new harness = fresh active state)
|
|
107
|
+
const { events: events2 } = createPiHarness();
|
|
108
|
+
await events2.get("session_start")({ reason: "new" }, ctx);
|
|
109
|
+
const result = await events2.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
110
|
+
|
|
111
|
+
assert.ok(result.systemPrompt.includes("CAVEMAN MODE ACTIVE"));
|
|
97
112
|
});
|
|
98
113
|
|
|
99
114
|
test("'stop caveman' as a whole message deactivates mid-session", async () => {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
getCavemanInstructions,
|
|
8
8
|
isDeactivationCommand,
|
|
9
9
|
parseCavemanCommand,
|
|
10
|
-
resolveSessionActive,
|
|
11
10
|
} from "../index.js";
|
|
12
11
|
|
|
13
12
|
test("parseCavemanCommand: bare toggles", () => {
|
|
@@ -27,20 +26,7 @@ test("parseCavemanCommand: unknown arg is invalid", () => {
|
|
|
27
26
|
assert.deepEqual(parseCavemanCommand("banana"), { type: "invalid", reason: "unknown-subcommand", arg: "banana" });
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
test("resolveSessionActive: latest persisted entry wins", () => {
|
|
31
|
-
const entries = [
|
|
32
|
-
{ type: "custom", customType: "caveman-mode", data: { active: false } },
|
|
33
|
-
{ type: "custom", customType: "caveman-mode", data: { active: true } },
|
|
34
|
-
];
|
|
35
|
-
assert.equal(resolveSessionActive(entries, true), true);
|
|
36
|
-
});
|
|
37
29
|
|
|
38
|
-
test("resolveSessionActive: non-array entries fall back", () => {
|
|
39
|
-
assert.equal(resolveSessionActive(null, true), true);
|
|
40
|
-
assert.equal(resolveSessionActive(undefined, false), false);
|
|
41
|
-
assert.equal(resolveSessionActive({}, true), true);
|
|
42
|
-
assert.equal(resolveSessionActive("nope"), true); // DEFAULT fallback
|
|
43
|
-
});
|
|
44
30
|
|
|
45
31
|
test("isDeactivationCommand: only the whole message, case-insensitive, trailing punct ok", () => {
|
|
46
32
|
assert.equal(isDeactivationCommand("stop caveman"), true);
|
|
@@ -81,6 +81,27 @@ function textFromToolResultContent(content: unknown): string | undefined {
|
|
|
81
81
|
return joined || undefined;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function validatorForPhaseArtifact(config: WorkflowConfig, phase: Phase) {
|
|
85
|
+
const phaseValidator = config.artifactValidators?.[phase];
|
|
86
|
+
if (phaseValidator) return phaseValidator;
|
|
87
|
+
const file = config.phaseArtifacts[phase];
|
|
88
|
+
if (!file) return undefined;
|
|
89
|
+
const isTerminal = config.phases[config.phases.length - 1] === phase;
|
|
90
|
+
if (!isTerminal || !config.closeArtifacts.includes(file)) return undefined;
|
|
91
|
+
return config.closeValidators?.[file];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function shouldReplaceInvalidArtifact(
|
|
95
|
+
config: WorkflowConfig,
|
|
96
|
+
phase: Phase,
|
|
97
|
+
currentContent: string,
|
|
98
|
+
fallbackContent: string,
|
|
99
|
+
): boolean {
|
|
100
|
+
const validator = validatorForPhaseArtifact(config, phase);
|
|
101
|
+
if (!validator) return false;
|
|
102
|
+
return !validator(currentContent).ok && validator(fallbackContent).ok;
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
// ponytail: rewrite the subagent tool input so the child writes its output
|
|
85
106
|
// directly to ${artifactDir}/${file} as an absolute path. Absolute paths pass
|
|
86
107
|
// through resolveSingleOutputPath verbatim, so injectOutputPathSystemPrompt
|
|
@@ -594,11 +615,32 @@ export function createWorkflowExtension(
|
|
|
594
615
|
// ── tool_result: auto-advance after subagent/bash artifacts. ──
|
|
595
616
|
pi.on("tool_result", async (event: any, ctx: ExtensionContext) => {
|
|
596
617
|
if (event.toolName !== "bash" && event.toolName !== "subagent") return;
|
|
618
|
+
// ponytail: a failed phase-owner tool used to leave the workflow quiet
|
|
619
|
+
// until the user manually typed /prototype or /quick continue. Re-check
|
|
620
|
+
// first (in case the tool wrote the artifact before failing), then if
|
|
621
|
+
// nothing advanced, re-queue the current phase prompt automatically.
|
|
622
|
+
if (event.isError && sm.snapshot.active) {
|
|
623
|
+
const eff = await sm.onArtifactMaybe(deps);
|
|
624
|
+
applyEffect(pi, ctx, config, eff);
|
|
625
|
+
if (eff.kind === "noOp") {
|
|
626
|
+
const retry = sm.continueCurrent();
|
|
627
|
+
continueAgent(
|
|
628
|
+
pi,
|
|
629
|
+
ctx,
|
|
630
|
+
`The previous ${event.toolName} call failed during the ${retry.phase} phase. Stay in this phase and try again.\n\n${retry.prompt}`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (sm.snapshot.phase !== "loop") {
|
|
634
|
+
controller.loopState = undefined;
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
597
638
|
// ponytail: if a subagent returned text but did not write the expected
|
|
598
639
|
// artifact (common with dumb/local models), save the returned text as
|
|
599
640
|
// the artifact so the workflow can still advance.
|
|
600
641
|
if (event.toolName === "subagent" && sm.snapshot.active && SUBAGENT_FALLBACK_PHASES.has(sm.snapshot.phase)) {
|
|
601
|
-
const
|
|
642
|
+
const phase = sm.snapshot.phase;
|
|
643
|
+
const file = config.phaseArtifacts[phase];
|
|
602
644
|
if (file) {
|
|
603
645
|
// ponytail: management actions (list/get/models/doctor/status/...)
|
|
604
646
|
// return text but are NOT the architect/recapper/... execution
|
|
@@ -608,9 +650,18 @@ export function createWorkflowExtension(
|
|
|
608
650
|
// calls set `action`. Only fall back for execution calls.
|
|
609
651
|
if (typeof event.input?.action !== "string") {
|
|
610
652
|
const expectedPath = resolve(sm.snapshot.artifactDir, file);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
653
|
+
const text = textFromToolResultContent(event.content);
|
|
654
|
+
if (text) {
|
|
655
|
+
let shouldWrite = !(await realFileExists(expectedPath));
|
|
656
|
+
if (!shouldWrite) {
|
|
657
|
+
try {
|
|
658
|
+
const current = await readFile(expectedPath, "utf8");
|
|
659
|
+
shouldWrite = shouldReplaceInvalidArtifact(config, phase, current, text);
|
|
660
|
+
} catch {
|
|
661
|
+
shouldWrite = false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (shouldWrite) {
|
|
614
665
|
await mkdir(sm.snapshot.artifactDir, { recursive: true });
|
|
615
666
|
await writeFile(expectedPath, text, "utf8");
|
|
616
667
|
}
|