@selesai/code 0.4.0 → 0.5.1
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/core/agent-session-auto-handoff.test.d.ts +2 -0
- package/dist/core/agent-session-auto-handoff.test.d.ts.map +1 -0
- package/dist/core/agent-session-auto-handoff.test.js +161 -0
- package/dist/core/agent-session-auto-handoff.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +43 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/settings-manager-auto-handoff.test.d.ts +2 -0
- package/dist/core/settings-manager-auto-handoff.test.d.ts.map +1 -0
- package/dist/core/settings-manager-auto-handoff.test.js +29 -0
- package/dist/core/settings-manager-auto-handoff.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +9 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +22 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/extensions/context-compaction-reminder.ts +1 -1
- package/dist/extensions/pi-powerline-footer/index.ts +43 -2
- package/dist/extensions/pi-powerline-footer/session-usage.ts +44 -0
- package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +47 -0
- package/dist/extensions/pi-subagents/README.md +1 -1
- package/dist/extensions/pi-subagents/agents/architect.md +150 -22
- package/dist/extensions/pi-subagents/agents/builder.md +1 -1
- package/dist/extensions/pi-subagents/agents/commentator.md +2 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +2 -3
- package/dist/extensions/pi-subagents/agents/recapper.md +2 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +2 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +46 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +25 -1
- package/dist/extensions/workflow/adapter.ts +34 -174
- package/dist/extensions/workflow/modes/prototype.ts +12 -14
- package/dist/extensions/workflow/modes/quick.ts +12 -14
- package/dist/extensions/workflow/modes/task.ts +8 -5
- package/dist/extensions/workflow/state-machine.ts +3 -0
- package/dist/extensions/workflow/task-validators.ts +69 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +20 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +8 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +8 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +12 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +14 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +20 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/workflows.md +9 -4
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
13
13
|
import { Type } from "typebox";
|
|
14
14
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { basename,
|
|
16
|
+
import { basename, resolve } from "node:path";
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
WorkflowStateMachine,
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
type WorkflowEffect,
|
|
23
23
|
type WorkflowEntry,
|
|
24
24
|
type FooterState,
|
|
25
|
-
type Phase,
|
|
26
25
|
type WorkflowSnapshot,
|
|
27
26
|
} from "./state-machine.ts";
|
|
28
27
|
import {
|
|
@@ -53,18 +52,6 @@ async function realFileExists(path: string): Promise<boolean> {
|
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
// ponytail: phases where one spawned subagent owns the phase artifact. Force
|
|
57
|
-
// the child `output` path; child processes do not share the parent's workflow
|
|
58
|
-
// state, so the parent-scoped write_workflow_artifact tool cannot help there.
|
|
59
|
-
// Plan 5: these are also the single-owner phases — parallel/chain calls are
|
|
60
|
-
// blocked here because one agent must own plan.md / reuse.md / handoff.md /
|
|
61
|
-
// review.md. Loop is engine-owned (Plan 3) and may fan out.
|
|
62
|
-
const FORCE_OUTPUT_PHASES = new Set<Phase>(["plan", "reuse", "handoff", "audit"]);
|
|
63
|
-
|
|
64
|
-
// ponytail: phases where the parent can save subagent text as the artifact
|
|
65
|
-
// when the child did not write the file itself (dumb/local models).
|
|
66
|
-
const SUBAGENT_FALLBACK_PHASES = new Set<Phase>(["plan", "reuse", "handoff", "audit"]);
|
|
67
|
-
|
|
68
55
|
function textFromToolResultContent(content: unknown): string | undefined {
|
|
69
56
|
if (!Array.isArray(content)) return undefined;
|
|
70
57
|
const parts: string[] = [];
|
|
@@ -77,75 +64,23 @@ function textFromToolResultContent(content: unknown): string | undefined {
|
|
|
77
64
|
return joined || undefined;
|
|
78
65
|
}
|
|
79
66
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
const isTerminal = config.phases[config.phases.length - 1] === phase;
|
|
86
|
-
if (!isTerminal || !config.closeArtifacts.includes(file)) return undefined;
|
|
87
|
-
return config.closeValidators?.[file];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function shouldReplaceInvalidArtifact(
|
|
91
|
-
config: WorkflowConfig,
|
|
92
|
-
phase: Phase,
|
|
93
|
-
currentContent: string,
|
|
94
|
-
fallbackContent: string,
|
|
95
|
-
): boolean {
|
|
96
|
-
const validator = validatorForPhaseArtifact(config, phase);
|
|
97
|
-
if (!validator) return false;
|
|
98
|
-
return !validator(currentContent).ok && validator(fallbackContent).ok;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ponytail: rewrite the subagent tool input so the child writes its output
|
|
102
|
-
// directly to ${artifactDir}/${file} as an absolute path. Absolute paths pass
|
|
103
|
-
// through resolveSingleOutputPath verbatim, so injectOutputPathSystemPrompt
|
|
104
|
-
// then forces the child to the correct location instead of repo root.
|
|
105
|
-
// Mutates `input` in place (tool_call handlers can patch event.input).
|
|
106
|
-
// No-op unless the workflow is active, in a force-output phase, and the call
|
|
107
|
-
// targets the `subagent` tool. Respects an explicit absolute caller output.
|
|
108
|
-
function forceSubagentOutputToArtifactDir(
|
|
109
|
-
input: Record<string, unknown>,
|
|
110
|
-
artifactDir: string,
|
|
111
|
-
file: string,
|
|
112
|
-
onlyAgents?: Set<string>,
|
|
113
|
-
): void {
|
|
114
|
-
const dest = resolve(artifactDir, file);
|
|
115
|
-
const shouldForce = (agent: string) => !onlyAgents || onlyAgents.has(agent);
|
|
116
|
-
// Single-agent call: { agent, task, output?, ... }
|
|
117
|
-
if (typeof input.agent === "string") {
|
|
118
|
-
if (!shouldForce(input.agent)) return;
|
|
119
|
-
const existing = input.output;
|
|
120
|
-
if (typeof existing === "string" && isAbsolute(existing)) return; // caller pinned it
|
|
121
|
-
input.output = dest;
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
// Top-level parallel: { tasks: [{ agent, output? }, ...] }
|
|
67
|
+
// Mutates a workflow subagent invocation to suppress both its configured
|
|
68
|
+
// default output and any caller-provided child output path. `output: false`
|
|
69
|
+
// preserves the normal inline result, which the parent must write explicitly.
|
|
70
|
+
function disableSubagentOutput(input: Record<string, unknown>): void {
|
|
71
|
+
if (typeof input.agent === "string") input.output = false;
|
|
125
72
|
if (Array.isArray(input.tasks)) {
|
|
126
73
|
for (const task of input.tasks) {
|
|
127
|
-
if (task && typeof task === "object"
|
|
128
|
-
const ex = task.output;
|
|
129
|
-
if (typeof ex === "string" && isAbsolute(ex)) continue;
|
|
130
|
-
task.output = dest;
|
|
131
|
-
}
|
|
74
|
+
if (task && typeof task === "object") task.output = false;
|
|
132
75
|
}
|
|
133
|
-
return;
|
|
134
76
|
}
|
|
135
|
-
// Chain: { chain: [{ agent, output?, parallel: [{ agent, output? }] }] }
|
|
136
77
|
if (Array.isArray(input.chain)) {
|
|
137
78
|
for (const step of input.chain) {
|
|
138
79
|
if (!step || typeof step !== "object") continue;
|
|
139
|
-
|
|
140
|
-
const ex = step.output;
|
|
141
|
-
if (!(typeof ex === "string" && isAbsolute(ex))) step.output = dest;
|
|
142
|
-
}
|
|
80
|
+
step.output = false;
|
|
143
81
|
if (Array.isArray(step.parallel)) {
|
|
144
82
|
for (const task of step.parallel) {
|
|
145
|
-
if (task && typeof task === "object"
|
|
146
|
-
const ex = task.output;
|
|
147
|
-
if (!(typeof ex === "string" && isAbsolute(ex))) task.output = dest;
|
|
148
|
-
}
|
|
83
|
+
if (task && typeof task === "object") task.output = false;
|
|
149
84
|
}
|
|
150
85
|
}
|
|
151
86
|
}
|
|
@@ -546,9 +481,10 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
546
481
|
details: { mode: controller.config.mode, phase: snap.phase, path, persistenceError: true },
|
|
547
482
|
};
|
|
548
483
|
}
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
484
|
+
// Most workflows pause at a user-controlled artifact boundary. Task's
|
|
485
|
+
// plan → loop transition queues the builder/review prompt immediately.
|
|
486
|
+
const queueNextPhase = controller.config.continueAfterArtifact === true && eff.kind === "advanced";
|
|
487
|
+
applyControllerEffect(controller, ctx, eff, { queuePrompt: queueNextPhase });
|
|
552
488
|
if (eff.kind === "blocked" && eff.reason) {
|
|
553
489
|
return {
|
|
554
490
|
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.` }],
|
|
@@ -558,12 +494,14 @@ function registerSharedArtifactWriter(pi: ExtensionAPI): void {
|
|
|
558
494
|
const advanced = eff.kind === "advanced";
|
|
559
495
|
return {
|
|
560
496
|
content: [{ type: "text", text: advanced
|
|
561
|
-
?
|
|
497
|
+
? queueNextPhase
|
|
498
|
+
? `Wrote ${path}. Phase advanced to ${eff.phase}; the next workflow phase is queued.`
|
|
499
|
+
: `Wrote ${path}. Phase advanced to ${eff.phase}; wait for the user to continue the workflow.`
|
|
562
500
|
: `Wrote ${path}.` }],
|
|
563
501
|
details: { mode: controller.config.mode, phase: snap.phase, path, file, advanced },
|
|
564
|
-
// Stop
|
|
565
|
-
//
|
|
566
|
-
terminate: advanced,
|
|
502
|
+
// Stop only at user-controlled boundaries. Task deliberately queues
|
|
503
|
+
// its loop prompt, so terminating here would discard that continuation.
|
|
504
|
+
terminate: advanced && !queueNextPhase,
|
|
567
505
|
};
|
|
568
506
|
},
|
|
569
507
|
renderResult(result, _options, theme) {
|
|
@@ -797,117 +735,38 @@ export function createWorkflowExtension(
|
|
|
797
735
|
}
|
|
798
736
|
});
|
|
799
737
|
|
|
800
|
-
// ── tool_call: enforce
|
|
738
|
+
// ── tool_call: enforce parent-owned artifact boundaries. ──
|
|
801
739
|
pi.on("tool_call", (event: any, _ctx: ExtensionContext) => {
|
|
802
740
|
if (!isRegisteredController(controller)) return;
|
|
803
741
|
const tool = event.toolName;
|
|
804
742
|
if (tool !== "subagent" && tool !== "write" && tool !== "edit") return;
|
|
805
743
|
const snap = sm.snapshot;
|
|
806
744
|
if (!snap.active) return;
|
|
807
|
-
if (tool === "subagent" && isSubagentManagementAction(event.input)) return;
|
|
808
|
-
const file = config.phaseArtifacts[snap.phase];
|
|
809
745
|
if (tool === "write" || tool === "edit") {
|
|
810
746
|
return {
|
|
811
747
|
block: true,
|
|
812
748
|
reason: `Workflow is active (${mode}/${snap.phase}). Use ${WORKFLOW_ARTIFACT_TOOL} for workflow artifacts; workspace edits must be delegated to subagents.`,
|
|
813
749
|
};
|
|
814
750
|
}
|
|
815
|
-
if (!
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (Array.isArray(event.input.tasks) || Array.isArray(event.input.chain)) {
|
|
820
|
-
return {
|
|
821
|
-
block: true,
|
|
822
|
-
reason: `Workflow phase ${snap.phase} expects one subagent owner for ${file}; use a single { agent, task } call, not tasks/chain.`,
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
// ponytail: Plan 5 — workflow-spawned subagents run fresh by default so
|
|
826
|
-
// no hidden parent context leaks into the phase owner. Only override
|
|
827
|
-
// when the caller explicitly pins a context.
|
|
828
|
-
if (typeof event.input.context !== "string") {
|
|
829
|
-
event.input.context = "fresh";
|
|
751
|
+
if (!isSubagentManagementAction(event.input) && event.input) {
|
|
752
|
+
// All child results return inline. The parent persists normal phase
|
|
753
|
+
// artifacts; the loop engine persists its own review files/marker.
|
|
754
|
+
disableSubagentOutput(event.input);
|
|
830
755
|
}
|
|
831
|
-
// ponytail: Plan 5 — ban model override on workflow-owned phases. The
|
|
832
|
-
// workflow, not the parent model, picks the agent; a model pin would
|
|
833
|
-
// silently change cost/quality without the workflow knowing.
|
|
834
|
-
if (typeof event.input.model === "string") {
|
|
835
|
-
return {
|
|
836
|
-
block: true,
|
|
837
|
-
reason: `Workflow phase ${snap.phase} does not allow a model override on subagent calls; remove the model parameter.`,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
const onlyAgents = snap.phase === "audit" ? new Set(["commentator", "reviewer"]) : undefined;
|
|
841
|
-
forceSubagentOutputToArtifactDir(event.input, snap.artifactDir, file, onlyAgents);
|
|
842
756
|
});
|
|
843
757
|
|
|
844
|
-
// ── tool_result:
|
|
758
|
+
// ── tool_result: only the engine-owned loop persists subagent output. ──
|
|
845
759
|
pi.on("tool_result", async (event: any, ctx: ExtensionContext) => {
|
|
846
760
|
if (!isRegisteredController(controller)) return;
|
|
847
|
-
if (event.toolName !== "
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
if (!sm.snapshot.active) return;
|
|
761
|
+
if (event.toolName !== "subagent" || event.isError) return;
|
|
762
|
+
// Parent-owned phases intentionally do nothing here: the main agent must
|
|
763
|
+
// inspect the inline child result and call write_workflow_artifact.
|
|
764
|
+
if (!sm.snapshot.active || sm.snapshot.phase !== "loop" || isSubagentManagementAction(event.input)) return;
|
|
851
765
|
const eventBefore = checkpoint(controller);
|
|
852
|
-
if (
|
|
766
|
+
if (typeof event.toolCallId === "string") {
|
|
853
767
|
if (controller.seenToolCallIds.has(event.toolCallId)) return;
|
|
854
768
|
controller.seenToolCallIds.add(event.toolCallId);
|
|
855
769
|
}
|
|
856
|
-
// Re-check after an error in case the tool wrote its artifact before
|
|
857
|
-
// failing. The normal tool-result continuation receives the error; do
|
|
858
|
-
// not inject another parent turn.
|
|
859
|
-
if (event.isError && sm.snapshot.active) {
|
|
860
|
-
if (event.toolName === "subagent" && isSubagentManagementAction(event.input)) return;
|
|
861
|
-
const eff = await sm.onArtifactMaybe(deps);
|
|
862
|
-
try {
|
|
863
|
-
await persistAfter(controller, eventBefore);
|
|
864
|
-
} catch (error) {
|
|
865
|
-
ctx.ui.notify(`Workflow state was not saved: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
|
|
869
|
-
if (sm.snapshot.phase !== "loop") {
|
|
870
|
-
controller.loopState = undefined;
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
// ponytail: if a subagent returned text but did not write the expected
|
|
875
|
-
// artifact (common with dumb/local models), save the returned text as
|
|
876
|
-
// the artifact so the workflow can still advance.
|
|
877
|
-
if (event.toolName === "subagent" && sm.snapshot.active && SUBAGENT_FALLBACK_PHASES.has(sm.snapshot.phase)) {
|
|
878
|
-
const phase = sm.snapshot.phase;
|
|
879
|
-
const file = config.phaseArtifacts[phase];
|
|
880
|
-
if (file) {
|
|
881
|
-
// ponytail: management actions (list/get/models/doctor/status/...)
|
|
882
|
-
// return text but are NOT the architect/recapper/... execution
|
|
883
|
-
// result. Writing their text to plan.md would advance the workflow
|
|
884
|
-
// on agent-listing output (the `subagent list` result is plain
|
|
885
|
-
// text). Execution calls set `agent`/`chain`/`tasks`; management
|
|
886
|
-
// calls set `action`. Only fall back for execution calls.
|
|
887
|
-
if (!isSubagentManagementAction(event.input)) {
|
|
888
|
-
const expectedPath = resolve(sm.snapshot.artifactDir, file);
|
|
889
|
-
const text = textFromToolResultContent(event.content);
|
|
890
|
-
if (text) {
|
|
891
|
-
const validator = validatorForPhaseArtifact(config, phase);
|
|
892
|
-
let shouldWrite = false;
|
|
893
|
-
if (!(await realFileExists(expectedPath))) {
|
|
894
|
-
shouldWrite = !validator || validator(text).ok;
|
|
895
|
-
} else {
|
|
896
|
-
try {
|
|
897
|
-
const current = await readFile(expectedPath, "utf8");
|
|
898
|
-
shouldWrite = shouldReplaceInvalidArtifact(config, phase, current, text);
|
|
899
|
-
} catch {
|
|
900
|
-
shouldWrite = false;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
if (shouldWrite) {
|
|
904
|
-
await mkdir(sm.snapshot.artifactDir, { recursive: true });
|
|
905
|
-
await writeFile(expectedPath, text, "utf8");
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
770
|
// The adapter owns durable loop state; the normal subagent tool-result
|
|
912
771
|
// continuation lets the parent choose the next builder/commentator call.
|
|
913
772
|
// No synthetic follow-up chat is queued. loopState clears when the phase
|
|
@@ -973,9 +832,10 @@ export function createWorkflowExtension(
|
|
|
973
832
|
ctx.ui.notify(`Workflow state was not saved: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
974
833
|
return;
|
|
975
834
|
}
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
|
|
835
|
+
// The only artifact written in this handler is loop-complete.md, which
|
|
836
|
+
// is engine-owned. Parent-written phase artifacts advance inside
|
|
837
|
+
// write_workflow_artifact instead.
|
|
838
|
+
applyControllerEffect(controller, ctx, eff);
|
|
979
839
|
});
|
|
980
840
|
|
|
981
841
|
// ── /<command> ──
|
|
@@ -80,11 +80,11 @@ Produce the concrete build plan for the actual prototype/output — not a plan f
|
|
|
80
80
|
|
|
81
81
|
If research was skipped, proceed directly — you already understand the user's intent from grilling.
|
|
82
82
|
|
|
83
|
-
Call the subagent tool with { agent: "architect", task: "..." } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/research.md so the architect plans for THIS task.
|
|
83
|
+
Call the subagent tool with { agent: "architect", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/research.md so the architect plans for THIS task. It must return the complete plan inline; do not tell it to write any artifact.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
86
86
|
WORKFLOW_PLAN_STATUS: ready
|
|
87
|
-
|
|
87
|
+
Then immediately call write_workflow_artifact with the complete validated plan as content. Only that parent tool call writes ${artifactDir}/plan.md and advances the workflow.`,
|
|
88
88
|
reuse: ({ artifactDir }) =>
|
|
89
89
|
`You are in the REUSE phase (optional).
|
|
90
90
|
|
|
@@ -92,7 +92,7 @@ Decide whether codebase exploration is useful. Skip if the project is empty, the
|
|
|
92
92
|
|
|
93
93
|
If unsure, ask the user one focused question: "Should I explore the existing codebase for reusable patterns before implementing?" Then follow their answer.
|
|
94
94
|
|
|
95
|
-
If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "..." } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies (e.g., Tailwind setup, state-machine usage).
|
|
95
|
+
If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies (e.g., Tailwind setup, state-machine usage). It must return the reuse findings inline; do not tell it to write any artifact. Immediately call write_workflow_artifact with those findings as content for ${artifactDir}/reuse.md.
|
|
96
96
|
|
|
97
97
|
If NO: call write_workflow_artifact with a brief skip note explaining why.
|
|
98
98
|
|
|
@@ -108,19 +108,19 @@ Draw from all prior phases:
|
|
|
108
108
|
- ${artifactDir}/plan.md (the build plan)
|
|
109
109
|
- ${artifactDir}/reuse.md (codebase exploration findings)
|
|
110
110
|
|
|
111
|
-
Call the subagent tool with { agent: "recapper", task: "..." } (do NOT pass a model parameter). Craft the task pointing the recapper at all four artifact files and telling it what the prototype is about.
|
|
111
|
+
Call the subagent tool with { agent: "recapper", task: "...", output: false } (do NOT pass a model parameter). Craft the task pointing the recapper at all four artifact files and telling it what the prototype is about. It must return the complete handoff inline; do not tell it to write any artifact.
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
114
114
|
WORKFLOW_HANDOFF_STATUS: ready
|
|
115
|
-
|
|
115
|
+
Then immediately call write_workflow_artifact with the complete validated handoff as content. Only that parent tool call writes ${artifactDir}/handoff.md and advances the workflow.`,
|
|
116
116
|
loop: ({ artifactDir, loopMaxIterations }) =>
|
|
117
117
|
`You are in the LOOP (orchestration) phase. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
|
|
118
118
|
|
|
119
119
|
Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/handoff.md, ${artifactDir}/research.md, and ${artifactDir}/reuse.md. Using that context, GENERATE YOUR OWN delegation prompt (no static template):
|
|
120
120
|
|
|
121
|
-
Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it plan + handoff + any research/reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
|
|
121
|
+
Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it plan + handoff + any research/reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
|
|
122
122
|
|
|
123
|
-
After the builder returns, call the commentator. Craft the review task YOURSELF based on what matters for this task. Each commentator review MUST end with exactly one machine-readable line:
|
|
123
|
+
After the builder returns, call the subagent tool with { agent: "commentator", task: "...", output: false }. Craft the review task YOURSELF based on what matters for this task. The commentator must return its review inline. Each commentator review MUST end with exactly one machine-readable line:
|
|
124
124
|
WORKFLOW_REVIEW_STATUS: clean
|
|
125
125
|
OR
|
|
126
126
|
WORKFLOW_REVIEW_STATUS: blocking
|
|
@@ -131,11 +131,9 @@ If a review is blocking, call the builder again with the recorded issues. This r
|
|
|
131
131
|
|
|
132
132
|
Review uncommitted changes for correctness, plan adherence, and over-engineering. Use ponytail-review style: cut bloat, unnecessary abstractions, dead flexibility, and reinvented stdlib/native behavior.
|
|
133
133
|
|
|
134
|
-
1. Call the subagent tool with { agent: "commentator", task: "..." } (do NOT pass a model parameter). Craft the task from the full uncommitted diff and ${artifactDir}/plan.md.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
2. If ${artifactDir}/review.md lists actionable issues, call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}.
|
|
138
|
-
3. Re-dispatch the commentator until ${artifactDir}/review.md ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
134
|
+
1. Call the subagent tool with { agent: "commentator", task: "...", output: false } (do NOT pass a model parameter). Craft the task from the full uncommitted diff and ${artifactDir}/plan.md. It must return the review inline; do not tell it to write any artifact. Verify the review ends with exactly one machine-readable line, WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking, then immediately call write_workflow_artifact with that review as content for ${artifactDir}/review.md.
|
|
135
|
+
2. If that review lists actionable issues, call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}, then return its completion summary inline.
|
|
136
|
+
3. Re-dispatch the commentator and overwrite the parent-owned review artifact through write_workflow_artifact until it ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
139
137
|
|
|
140
138
|
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow to complete the workflow.`,
|
|
141
139
|
};
|
|
@@ -64,11 +64,11 @@ The workflow advances once ${artifactDir}/requirements.md exists.`,
|
|
|
64
64
|
|
|
65
65
|
This is a QUICK workflow — no separate research phase. Use ${artifactDir}/requirements.md to produce the concrete build plan: what to build, how, in order, components, and what the finished prototype looks like.
|
|
66
66
|
|
|
67
|
-
Call the subagent tool with { agent: "architect", task: "..." } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md so the architect plans for THIS task.
|
|
67
|
+
Call the subagent tool with { agent: "architect", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md so the architect plans for THIS task. It must return the complete plan inline; do not tell it to write any artifact.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
70
70
|
WORKFLOW_PLAN_STATUS: ready
|
|
71
|
-
|
|
71
|
+
Then immediately call write_workflow_artifact with the complete validated plan as content. Only that parent tool call writes ${artifactDir}/plan.md and advances the workflow.`,
|
|
72
72
|
reuse: ({ artifactDir }) =>
|
|
73
73
|
`You are in the REUSE phase (optional).
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ Decide whether codebase exploration is useful. Skip if the project is empty, the
|
|
|
76
76
|
|
|
77
77
|
If unsure, ask the user one focused question: "Should I explore the existing codebase for reusable patterns before implementing?" Then follow their answer.
|
|
78
78
|
|
|
79
|
-
If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "..." } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies.
|
|
79
|
+
If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies. It must return the reuse findings inline; do not tell it to write any artifact. Immediately call write_workflow_artifact with those findings as content for ${artifactDir}/reuse.md.
|
|
80
80
|
|
|
81
81
|
If NO: call write_workflow_artifact with a brief skip note explaining why.
|
|
82
82
|
|
|
@@ -91,19 +91,19 @@ Draw from all prior phases:
|
|
|
91
91
|
- ${artifactDir}/plan.md
|
|
92
92
|
- ${artifactDir}/reuse.md
|
|
93
93
|
|
|
94
|
-
Call the subagent tool with { agent: "recapper", task: "..." } (do NOT pass a model parameter). Craft the task pointing the recapper at all three artifact files and telling it what the prototype is about.
|
|
94
|
+
Call the subagent tool with { agent: "recapper", task: "...", output: false } (do NOT pass a model parameter). Craft the task pointing the recapper at all three artifact files and telling it what the prototype is about. It must return the complete handoff inline; do not tell it to write any artifact.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
97
97
|
WORKFLOW_HANDOFF_STATUS: ready
|
|
98
|
-
|
|
98
|
+
Then immediately call write_workflow_artifact with the complete validated handoff as content. Only that parent tool call writes ${artifactDir}/handoff.md and advances the workflow.`,
|
|
99
99
|
loop: ({ artifactDir, loopMaxIterations }) =>
|
|
100
100
|
`You are in the LOOP (orchestration) phase. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
|
|
101
101
|
|
|
102
102
|
Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/handoff.md, and ${artifactDir}/reuse.md. Using that context, GENERATE YOUR OWN delegation prompt:
|
|
103
103
|
|
|
104
|
-
Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it plan + handoff + reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
|
|
104
|
+
Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it plan + handoff + reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
|
|
105
105
|
|
|
106
|
-
After the builder returns, call the commentator. Craft the review task YOURSELF. Each commentator review MUST end with exactly one machine-readable line:
|
|
106
|
+
After the builder returns, call the subagent tool with { agent: "commentator", task: "...", output: false }. Craft the review task YOURSELF. The commentator must return its review inline. Each commentator review MUST end with exactly one machine-readable line:
|
|
107
107
|
WORKFLOW_REVIEW_STATUS: clean
|
|
108
108
|
OR
|
|
109
109
|
WORKFLOW_REVIEW_STATUS: blocking
|
|
@@ -114,11 +114,9 @@ If a review is blocking, call the builder again with the recorded issues. This r
|
|
|
114
114
|
|
|
115
115
|
Review uncommitted changes for correctness, plan adherence, and over-engineering. Use ponytail-review style: cut bloat, unnecessary abstractions, dead flexibility, and reinvented stdlib/native behavior.
|
|
116
116
|
|
|
117
|
-
1. Call the subagent tool with { agent: "commentator", task: "..." } (do NOT pass a model parameter). Craft the task from the full uncommitted diff and ${artifactDir}/plan.md.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
2. If ${artifactDir}/review.md lists actionable issues, call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}.
|
|
121
|
-
3. Re-run the commentator until ${artifactDir}/review.md ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
117
|
+
1. Call the subagent tool with { agent: "commentator", task: "...", output: false } (do NOT pass a model parameter). Craft the task from the full uncommitted diff and ${artifactDir}/plan.md. It must return the review inline; do not tell it to write any artifact. Verify the review ends with exactly one machine-readable line, WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking, then immediately call write_workflow_artifact with that review as content for ${artifactDir}/review.md.
|
|
118
|
+
2. If that review lists actionable issues, call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}, then return its completion summary inline.
|
|
119
|
+
3. Re-run the commentator and overwrite the parent-owned review artifact through write_workflow_artifact until it ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
122
120
|
|
|
123
121
|
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_quick_workflow to complete the workflow.`,
|
|
124
122
|
};
|
|
@@ -19,19 +19,19 @@ ${userPrompt}
|
|
|
19
19
|
|
|
20
20
|
Produce a concrete implementation plan: what to build, how, in what order, which files, components, and what the finished result looks like.
|
|
21
21
|
|
|
22
|
-
Call the subagent tool with { agent: "architect", task: "..." } (do NOT pass a model parameter). Craft the task so the architect plans for THIS request.
|
|
22
|
+
Call the subagent tool with { agent: "architect", task: "...", output: false } (do NOT pass a model parameter). Craft the task so the architect plans for THIS request. It must return the complete plan inline; do not tell it to write any artifact.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
25
25
|
WORKFLOW_PLAN_STATUS: ready
|
|
26
|
-
|
|
26
|
+
Then immediately call write_workflow_artifact with the complete validated plan as content. Only that parent tool call writes ${artifactDir}/plan.md and advances the workflow.`,
|
|
27
27
|
loop: ({ artifactDir, loopMaxIterations }) =>
|
|
28
28
|
`You are in the LOOP (orchestration) phase of a TASK workflow. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
|
|
29
29
|
|
|
30
30
|
Use read to inspect ${artifactDir}/plan.md. Using that context, GENERATE YOUR OWN delegation prompt:
|
|
31
31
|
|
|
32
|
-
Call the subagent tool with { agent: "builder", task: "..." } (do NOT pass a model parameter). Give it the plan context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}.
|
|
32
|
+
Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it the plan context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
|
|
33
33
|
|
|
34
|
-
After the builder returns, call the commentator. Craft the review task YOURSELF. Each commentator review MUST end with exactly one machine-readable line:
|
|
34
|
+
After the builder returns, call the subagent tool with { agent: "commentator", task: "...", output: false }. Craft the review task YOURSELF. The commentator must return its review inline. Each commentator review MUST end with exactly one machine-readable line:
|
|
35
35
|
WORKFLOW_REVIEW_STATUS: clean
|
|
36
36
|
OR
|
|
37
37
|
WORKFLOW_REVIEW_STATUS: blocking
|
|
@@ -55,6 +55,9 @@ const config: WorkflowConfig = {
|
|
|
55
55
|
closeValidators: { "loop-complete.md": loopCompleteValidator },
|
|
56
56
|
closeArtifacts: ["loop-complete.md"],
|
|
57
57
|
loopMaxIterations: 3,
|
|
58
|
+
// Task is plan → build. Continue directly into the loop when architect
|
|
59
|
+
// finishes, rather than requiring a second /workflow-task interaction.
|
|
60
|
+
continueAfterArtifact: true,
|
|
58
61
|
statusKey: "task",
|
|
59
62
|
entryType: "task-phase",
|
|
60
63
|
footerLabel: "task",
|
|
@@ -82,6 +82,9 @@ export interface WorkflowConfig {
|
|
|
82
82
|
// ponytail: Plan 3 — engine-owned loop. Max review rounds before the loop
|
|
83
83
|
// stops and asks the user instead of silently passing. Default 3.
|
|
84
84
|
loopMaxIterations?: number;
|
|
85
|
+
// Most workflows pause at artifact boundaries. A mode can opt into queuing
|
|
86
|
+
// the next phase prompt after the parent writes a valid artifact.
|
|
87
|
+
continueAfterArtifact?: boolean;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
export interface WorkflowDeps {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ponytail: task-specific validators enforce the acceptance/verification
|
|
2
|
+
// contract used only by task mode. Pure string functions; no fs/pi.
|
|
3
|
+
|
|
4
|
+
import { markerValidator } from "./validators.ts";
|
|
5
|
+
|
|
6
|
+
const PLAN_READY = markerValidator("WORKFLOW_PLAN_STATUS", "ready");
|
|
7
|
+
const REVIEW_CLEAN = markerValidator("WORKFLOW_REVIEW_STATUS", "clean");
|
|
8
|
+
|
|
9
|
+
const AC_ID_RE = /\bAC-(\d+)\b/gi;
|
|
10
|
+
const ACCEPTANCE_CRITERIA_HEADING_RE = /^##\s+Acceptance Criteria\b/im;
|
|
11
|
+
const IMPLEMENTATION_HEADING_RE = /^##\s+Implementation Outline\b/im;
|
|
12
|
+
const VERIFICATION_RE = /\bVerification\s*:/gi;
|
|
13
|
+
|
|
14
|
+
function extractACIds(text: string): string[] {
|
|
15
|
+
const ids: string[] = [];
|
|
16
|
+
let m: RegExpExecArray | null;
|
|
17
|
+
AC_ID_RE.lastIndex = 0;
|
|
18
|
+
while ((m = AC_ID_RE.exec(text)) !== null) {
|
|
19
|
+
ids.push(m[0]!.toUpperCase());
|
|
20
|
+
}
|
|
21
|
+
return ids;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const taskPlanValidator = (content: string) => {
|
|
25
|
+
const marker = PLAN_READY(content);
|
|
26
|
+
if (!marker.ok) return marker;
|
|
27
|
+
|
|
28
|
+
if (!ACCEPTANCE_CRITERIA_HEADING_RE.test(content)) {
|
|
29
|
+
return { ok: false as const, reason: "missing `## Acceptance Criteria` section" };
|
|
30
|
+
}
|
|
31
|
+
const ids = extractACIds(content);
|
|
32
|
+
if (ids.length === 0) {
|
|
33
|
+
return { ok: false as const, reason: "acceptance criteria section must contain at least one AC-<n> ID" };
|
|
34
|
+
}
|
|
35
|
+
const unique = new Set(ids);
|
|
36
|
+
if (unique.size !== ids.length) {
|
|
37
|
+
return { ok: false as const, reason: "acceptance criteria AC-<n> IDs must be unique" };
|
|
38
|
+
}
|
|
39
|
+
if (!IMPLEMENTATION_HEADING_RE.test(content)) {
|
|
40
|
+
return { ok: false as const, reason: "missing `## Implementation Outline` section" };
|
|
41
|
+
}
|
|
42
|
+
const verifications = (content.match(VERIFICATION_RE) ?? []).length;
|
|
43
|
+
if (verifications < unique.size) {
|
|
44
|
+
return { ok: false as const, reason: "each AC-<n> must have a Verification: step" };
|
|
45
|
+
}
|
|
46
|
+
return { ok: true as const };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function taskReviewValidator(
|
|
50
|
+
planContent: string | undefined,
|
|
51
|
+
reviewText: string,
|
|
52
|
+
): { ok: true } | { ok: false; reason: string } {
|
|
53
|
+
const marker = REVIEW_CLEAN(reviewText);
|
|
54
|
+
if (!marker.ok) return marker;
|
|
55
|
+
|
|
56
|
+
const planIds = extractACIds(planContent ?? "");
|
|
57
|
+
if (planIds.length === 0) {
|
|
58
|
+
return { ok: false as const, reason: "plan contains no AC-<n> criteria; cannot verify coverage" };
|
|
59
|
+
}
|
|
60
|
+
const reviewIds = extractACIds(reviewText);
|
|
61
|
+
const missing = planIds.filter((id) => !reviewIds.includes(id));
|
|
62
|
+
if (missing.length > 0) {
|
|
63
|
+
return { ok: false as const, reason: `review is missing coverage for ${missing.join(", ")}` };
|
|
64
|
+
}
|
|
65
|
+
if (!/\bpassed\b/i.test(reviewText) || !/\bverification\b/i.test(reviewText)) {
|
|
66
|
+
return { ok: false as const, reason: "review must report that verification passed" };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true as const };
|
|
69
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./co
|
|
|
16
16
|
export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts";
|
|
17
17
|
export { AgentSessionRuntime, type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionOptions, type CreateAgentSessionResult, type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeResult, type CreateAgentSessionServicesOptions, createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, type PromptTemplate, } from "./core/sdk.ts";
|
|
18
18
|
export { type BranchSummaryEntry, buildContextEntries, buildSessionContext, type CompactionEntry, CURRENT_SESSION_VERSION, type CustomEntry, type CustomMessageEntry, type FileEntry, getLatestCompactionEntry, type ModelChangeEntry, migrateSessionEntries, type NewSessionOptions, parseSessionEntries, type SessionContext, type SessionEntry, type SessionEntryBase, type SessionHeader, type SessionInfo, type SessionInfoEntry, SessionManager, type SessionMessageEntry, type SessionTreeNode, sessionEntryToContextMessages, type ThinkingLevelChangeEntry, } from "./core/session-manager.ts";
|
|
19
|
-
export { type CompactionSettings, type DefaultProjectTrust, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, } from "./core/settings-manager.ts";
|
|
19
|
+
export { type AutoHandoffSettings, type CompactionSettings, type DefaultProjectTrust, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, } from "./core/settings-manager.ts";
|
|
20
20
|
export { formatSkillsForPrompt, type LoadSkillsFromDirOptions, type LoadSkillsResult, loadSkills, loadSkillsFromDir, type Skill, type SkillFrontmatter, } from "./core/skills.ts";
|
|
21
21
|
export { createSyntheticSourceInfo } from "./core/source-info.ts";
|
|
22
22
|
export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts";
|