@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.
- package/dist/extensions/caveman/index.js +1 -14
- package/dist/extensions/caveman/test/extension.test.js +17 -2
- package/dist/extensions/caveman/test/helpers.test.js +0 -14
- package/dist/extensions/pi-subagents/agents/builder.md +1 -1
- package/dist/extensions/workflow/adapter.ts +110 -13
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
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
|
|
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 (
|
|
690
|
+
if (!isSubagentManagementAction(event.input)) {
|
|
610
691
|
const expectedPath = resolve(sm.snapshot.artifactDir, file);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|