@selesai/code 0.3.6 → 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
- const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
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 restores latest persisted active flag", async () => {
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.equal(result, undefined); // persisted false -> no injection
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);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: builder
3
- model: tokenin/deepseek-v4-flash
3
+ model: tokenin/qwen3.5-397b
4
4
  thinking: high
5
5
  description: Implementation agent for normal tasks handoffs
6
6
  systemPromptMode: replace
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  name: researcher
3
+ model: tokenin/kimi-k2.7-code
4
+ thinking: high
3
5
  description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
4
6
  tools: read, write, web_explore, intercom
5
7
  thinking: medium
@@ -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 file = config.phaseArtifacts[sm.snapshot.phase];
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
- if (!(await realFileExists(expectedPath))) {
612
- const text = textFromToolResultContent(event.content);
613
- if (text) {
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
  }
@@ -441,7 +441,9 @@ export class WorkflowStateMachine {
441
441
  const sat = await this.artifactSatisfiedFor(this.phase, deps);
442
442
  if (!sat.exists) return { kind: "noOp" };
443
443
  if (sat.reason) {
444
- this.autoArmed = false;
444
+ // Keep armed: the artifact exists but failed the semantic gate. The
445
+ // next corrected write/subagent result must be able to advance this
446
+ // same phase; disarming here leaves the workflow stuck forever.
445
447
  const missing = this.config.phaseArtifacts[this.phase]!;
446
448
  return { kind: "blocked", phase: this.phase, missing, reason: sat.reason };
447
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Selesai coding agent",
5
5
  "type": "module",
6
6
  "piConfig": {