@selesai/code 0.3.7 → 0.3.9

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/qwen3.5-397b
3
+ model: tokenin/qwen3.6-35b
4
4
  thinking: high
5
5
  description: Implementation agent for normal tasks handoffs
6
6
  systemPromptMode: replace
@@ -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
@@ -154,6 +175,8 @@ interface WorkflowController {
154
175
  // Ceiling: not persisted across session restart — a restart re-runs the
155
176
  // loop from round 0. Acceptable for a prototype workflow.
156
177
  loopState?: LoopState;
178
+ seenToolCallIds: Set<string>;
179
+ lastBlockedKey?: string;
157
180
  }
158
181
 
159
182
  // ponytail: Plan 3 — loop orchestration state. The parent model does not
@@ -317,6 +340,30 @@ function applyEffect(
317
340
  }
318
341
  }
319
342
 
343
+ function blockedKeyFor(eff: WorkflowEffect): string | undefined {
344
+ if (eff.kind !== "blocked") return undefined;
345
+ return `${eff.phase}:${eff.missing}:${eff.reason ?? ""}`;
346
+ }
347
+
348
+ function applyControllerEffect(
349
+ controller: WorkflowController,
350
+ ctx: ExtensionContext,
351
+ eff: WorkflowEffect,
352
+ ): void {
353
+ const blockedKey = blockedKeyFor(eff);
354
+ if (blockedKey) {
355
+ if (controller.lastBlockedKey === blockedKey) return;
356
+ controller.lastBlockedKey = blockedKey;
357
+ } else if (eff.kind !== "noOp") {
358
+ controller.lastBlockedKey = undefined;
359
+ }
360
+ applyEffect(controller.pi, ctx, controller.config, eff);
361
+ }
362
+
363
+ function isSubagentManagementAction(input: Record<string, unknown> | undefined): boolean {
364
+ return typeof input?.action === "string";
365
+ }
366
+
320
367
  function registerSharedArtifactWriter(pi: ExtensionAPI): void {
321
368
  if (registry.writerRegisteredFor.has(pi)) return;
322
369
  registry.writerRegisteredFor.add(pi);
@@ -353,7 +400,7 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
353
400
  await mkdir(snap.artifactDir, { recursive: true });
354
401
  await writeFile(path, params.content, "utf8");
355
402
  const eff = await controller.sm.onArtifactMaybe(controller.deps);
356
- applyEffect(pi, ctx, controller.config, eff);
403
+ applyControllerEffect(controller, ctx, eff);
357
404
  if (eff.kind === "blocked" && eff.reason) {
358
405
  return {
359
406
  content: [{ type: "text", text: `Wrote ${path}, but it is not approved: ${eff.reason}. Re-write it via write_workflow_artifact to add the required marker.` }],
@@ -389,7 +436,7 @@ export function createWorkflowExtension(
389
436
  },
390
437
  ];
391
438
  const sm = new WorkflowStateMachine({ ...config, skipRules });
392
- const controller: WorkflowController = { pi, config, sm, deps };
439
+ const controller: WorkflowController = { pi, config, sm, deps, seenToolCallIds: new Set() };
393
440
  registry.controllers.push(controller);
394
441
  registerSharedArtifactWriter(pi);
395
442
  const { start, end } = options.toolNames;
@@ -419,7 +466,11 @@ export function createWorkflowExtension(
419
466
  };
420
467
  }
421
468
  const eff = await sm.start(params.goal, deps);
422
- applyEffect(pi, ctx, config, eff);
469
+ if (eff.kind === "started") {
470
+ controller.seenToolCallIds.clear();
471
+ controller.lastBlockedKey = undefined;
472
+ }
473
+ applyControllerEffect(controller, ctx, eff);
423
474
  if (eff.kind === "alreadyActive") {
424
475
  return {
425
476
  content: [
@@ -459,7 +510,11 @@ export function createWorkflowExtension(
459
510
  parameters: Type.Object({}),
460
511
  async execute(_id, _params, _signal, _onUpdate, ctx) {
461
512
  const eff = await sm.end(deps);
462
- applyEffect(pi, ctx, config, eff);
513
+ applyControllerEffect(controller, ctx, eff);
514
+ if (eff.kind === "closed") {
515
+ controller.seenToolCallIds.clear();
516
+ controller.lastBlockedKey = undefined;
517
+ }
463
518
  switch (eff.kind) {
464
519
  case "closed":
465
520
  return {
@@ -554,7 +609,7 @@ export function createWorkflowExtension(
554
609
  if (tool !== "subagent" && tool !== "write" && tool !== "edit") return;
555
610
  const snap = sm.snapshot;
556
611
  if (!snap.active) return;
557
- if (tool === "subagent" && typeof event.input?.action === "string") return;
612
+ if (tool === "subagent" && isSubagentManagementAction(event.input)) return;
558
613
  const file = config.phaseArtifacts[snap.phase];
559
614
  if (tool === "write" || tool === "edit") {
560
615
  return {
@@ -594,11 +649,37 @@ export function createWorkflowExtension(
594
649
  // ── tool_result: auto-advance after subagent/bash artifacts. ──
595
650
  pi.on("tool_result", async (event: any, ctx: ExtensionContext) => {
596
651
  if (event.toolName !== "bash" && event.toolName !== "subagent") return;
652
+ if (sm.snapshot.active && typeof event.toolCallId === "string") {
653
+ if (controller.seenToolCallIds.has(event.toolCallId)) return;
654
+ controller.seenToolCallIds.add(event.toolCallId);
655
+ }
656
+ // ponytail: a failed phase-owner tool used to leave the workflow quiet
657
+ // until the user manually typed /prototype or /quick continue. Re-check
658
+ // first (in case the tool wrote the artifact before failing), then if
659
+ // nothing advanced, re-queue the current phase prompt automatically.
660
+ if (event.isError && sm.snapshot.active) {
661
+ if (event.toolName === "subagent" && isSubagentManagementAction(event.input)) return;
662
+ const eff = await sm.onArtifactMaybe(deps);
663
+ applyControllerEffect(controller, ctx, eff);
664
+ if (eff.kind === "noOp") {
665
+ const retry = sm.continueCurrent();
666
+ continueAgent(
667
+ pi,
668
+ ctx,
669
+ `The previous ${event.toolName} call failed during the ${retry.phase} phase. Stay in this phase and try again.\n\n${retry.prompt}`,
670
+ );
671
+ }
672
+ if (sm.snapshot.phase !== "loop") {
673
+ controller.loopState = undefined;
674
+ }
675
+ return;
676
+ }
597
677
  // ponytail: if a subagent returned text but did not write the expected
598
678
  // artifact (common with dumb/local models), save the returned text as
599
679
  // the artifact so the workflow can still advance.
600
680
  if (event.toolName === "subagent" && sm.snapshot.active && SUBAGENT_FALLBACK_PHASES.has(sm.snapshot.phase)) {
601
- const file = config.phaseArtifacts[sm.snapshot.phase];
681
+ const phase = sm.snapshot.phase;
682
+ const file = config.phaseArtifacts[phase];
602
683
  if (file) {
603
684
  // ponytail: management actions (list/get/models/doctor/status/...)
604
685
  // return text but are NOT the architect/recapper/... execution
@@ -606,11 +687,23 @@ export function createWorkflowExtension(
606
687
  // on agent-listing output (the `subagent list` result is plain
607
688
  // text). Execution calls set `agent`/`chain`/`tasks`; management
608
689
  // calls set `action`. Only fall back for execution calls.
609
- if (typeof event.input?.action !== "string") {
690
+ if (!isSubagentManagementAction(event.input)) {
610
691
  const expectedPath = resolve(sm.snapshot.artifactDir, file);
611
- if (!(await realFileExists(expectedPath))) {
612
- const text = textFromToolResultContent(event.content);
613
- if (text) {
692
+ const text = textFromToolResultContent(event.content);
693
+ if (text) {
694
+ const validator = validatorForPhaseArtifact(config, phase);
695
+ let shouldWrite = false;
696
+ if (!(await realFileExists(expectedPath))) {
697
+ shouldWrite = !validator || validator(text).ok;
698
+ } else {
699
+ try {
700
+ const current = await readFile(expectedPath, "utf8");
701
+ shouldWrite = shouldReplaceInvalidArtifact(config, phase, current, text);
702
+ } catch {
703
+ shouldWrite = false;
704
+ }
705
+ }
706
+ if (shouldWrite) {
614
707
  await mkdir(sm.snapshot.artifactDir, { recursive: true });
615
708
  await writeFile(expectedPath, text, "utf8");
616
709
  }
@@ -675,7 +768,7 @@ or
675
768
  // onArtifactMaybe (a no-op unless loop-complete.md exists).
676
769
  }
677
770
  const eff = await sm.onArtifactMaybe(deps);
678
- applyEffect(pi, ctx, config, eff);
771
+ applyControllerEffect(controller, ctx, eff);
679
772
  // Clear loop state once we have left the loop phase (advanced to audit).
680
773
  if (sm.snapshot.phase !== "loop") {
681
774
  controller.loopState = undefined;
@@ -708,7 +801,7 @@ or
708
801
  if (choice === undefined) return;
709
802
  if (choice.startsWith("Continue")) {
710
803
  const eff = sm.continueCurrent();
711
- applyEffect(pi, ctx, config, eff);
804
+ applyControllerEffect(controller, ctx, eff);
712
805
  if (eff.kind === "advanced" && eff.prompt) {
713
806
  // continueCurrent already applied footer+entry; send the prompt.
714
807
  continueAgent(pi, ctx, eff.prompt);
@@ -745,7 +838,11 @@ or
745
838
  return;
746
839
  }
747
840
  const eff = await sm.start(args, deps);
748
- applyEffect(pi, ctx, config, eff);
841
+ if (eff.kind === "started") {
842
+ controller.seenToolCallIds.clear();
843
+ controller.lastBlockedKey = undefined;
844
+ }
845
+ applyControllerEffect(controller, ctx, eff);
749
846
  },
750
847
  });
751
848
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Selesai coding agent",
5
5
  "type": "module",
6
6
  "piConfig": {