@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.
Files changed (60) hide show
  1. package/dist/core/agent-session-auto-handoff.test.d.ts +2 -0
  2. package/dist/core/agent-session-auto-handoff.test.d.ts.map +1 -0
  3. package/dist/core/agent-session-auto-handoff.test.js +161 -0
  4. package/dist/core/agent-session-auto-handoff.test.js.map +1 -0
  5. package/dist/core/agent-session.d.ts +4 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +43 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/settings-manager-auto-handoff.test.d.ts +2 -0
  10. package/dist/core/settings-manager-auto-handoff.test.d.ts.map +1 -0
  11. package/dist/core/settings-manager-auto-handoff.test.js +29 -0
  12. package/dist/core/settings-manager-auto-handoff.test.js.map +1 -0
  13. package/dist/core/settings-manager.d.ts +9 -0
  14. package/dist/core/settings-manager.d.ts.map +1 -1
  15. package/dist/core/settings-manager.js +22 -0
  16. package/dist/core/settings-manager.js.map +1 -1
  17. package/dist/extensions/context-compaction-reminder.ts +1 -1
  18. package/dist/extensions/pi-powerline-footer/index.ts +43 -2
  19. package/dist/extensions/pi-powerline-footer/session-usage.ts +44 -0
  20. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +47 -0
  21. package/dist/extensions/pi-subagents/README.md +1 -1
  22. package/dist/extensions/pi-subagents/agents/architect.md +150 -22
  23. package/dist/extensions/pi-subagents/agents/builder.md +1 -1
  24. package/dist/extensions/pi-subagents/agents/commentator.md +2 -2
  25. package/dist/extensions/pi-subagents/agents/explorer.md +2 -3
  26. package/dist/extensions/pi-subagents/agents/recapper.md +2 -2
  27. package/dist/extensions/pi-subagents/agents/researcher.md +2 -2
  28. package/dist/extensions/pi-subagents/src/agents/agents.ts +46 -0
  29. package/dist/extensions/pi-subagents/src/extension/index.ts +2 -1
  30. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +2 -0
  31. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +24 -0
  32. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +25 -1
  33. package/dist/extensions/workflow/adapter.ts +34 -174
  34. package/dist/extensions/workflow/modes/prototype.ts +12 -14
  35. package/dist/extensions/workflow/modes/quick.ts +12 -14
  36. package/dist/extensions/workflow/modes/task.ts +8 -5
  37. package/dist/extensions/workflow/state-machine.ts +3 -0
  38. package/dist/extensions/workflow/task-validators.ts +69 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  43. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  44. package/dist/modes/interactive/components/settings-selector.js +20 -0
  45. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  46. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  47. package/dist/modes/interactive/interactive-mode.js +8 -0
  48. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  49. package/dist/modes/rpc/rpc-client.d.ts +8 -0
  50. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  51. package/dist/modes/rpc/rpc-client.js +12 -0
  52. package/dist/modes/rpc/rpc-client.js.map +1 -1
  53. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  54. package/dist/modes/rpc/rpc-mode.js +14 -0
  55. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  56. package/dist/modes/rpc/rpc-types.d.ts +20 -0
  57. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  58. package/dist/modes/rpc/rpc-types.js.map +1 -1
  59. package/docs/workflows.md +9 -4
  60. 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, isAbsolute, resolve } from "node:path";
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
- function validatorForPhaseArtifact(config: WorkflowConfig, phase: Phase) {
81
- const phaseValidator = config.artifactValidators?.[phase];
82
- if (phaseValidator) return phaseValidator;
83
- const file = config.phaseArtifacts[phase];
84
- if (!file) return undefined;
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" && typeof task.agent === "string" && shouldForce(task.agent)) {
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
- if (typeof step.agent === "string" && shouldForce(step.agent)) {
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" && typeof task.agent === "string" && shouldForce(task.agent)) {
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
- // A phase boundary is user-controlled: persist and show the new phase,
550
- // but do not inject a follow-up turn that immediately starts it.
551
- applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
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
- ? `Wrote ${path}. Phase advanced to ${eff.phase}; wait for the user to continue the workflow.`
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 the parent turn at a user-approved artifact boundary. Without
565
- // this, the queued phase prompt can immediately launch the next agent.
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 workflow boundaries. ──
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 (!file || !event.input || !FORCE_OUTPUT_PHASES.has(snap.phase)) return;
816
- // ponytail: Plan 5 — single-owner phases reject parallel/chain. One agent
817
- // must own the artifact; tasks/chain split ownership and the output
818
- // injector cannot pin one file per agent. Loop is exempt (engine-owned).
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: auto-advance after subagent/bash artifacts. ──
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 !== "bash" && event.toolName !== "subagent") return;
848
- // Both modes receive every tool result. Inactive controllers have no
849
- // attached run and must not try to persist ordinary agent activity.
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 (sm.snapshot.active && typeof event.toolCallId === "string") {
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
- // Do not turn an artifact/subagent result into a new parent turn.
977
- // The user resumes the next phase deliberately via the workflow command.
978
- applyControllerEffect(controller, ctx, eff, { queuePrompt: false });
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. The architect returns the plan; the workflow saves it to ${artifactDir}/plan.md.
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
- The plan MUST end with exactly one machine-readable line on its own:
85
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
86
86
  WORKFLOW_PLAN_STATUS: ready
87
- The workflow will NOT advance until ${artifactDir}/plan.md contains that marker.`,
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). Synthesize the findings into ${artifactDir}/reuse.md: what is reusable, where, and how to leverage it.
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. The recapper returns the handoff; the workflow saves it to ${artifactDir}/handoff.md.
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
- The handoff MUST end with exactly one machine-readable line on its own:
113
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
114
114
  WORKFLOW_HANDOFF_STATUS: ready
115
- The workflow will NOT advance until ${artifactDir}/handoff.md contains that marker.`,
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. The commentator returns its review; the workflow saves it to ${artifactDir}/review.md. The review MUST end with exactly one machine-readable line on its own:
135
- WORKFLOW_REVIEW_STATUS: clean
136
- (use WORKFLOW_REVIEW_STATUS: blocking if actionable issues remain)
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. The architect returns the plan; the workflow saves it to ${artifactDir}/plan.md.
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
- The plan MUST end with exactly one machine-readable line on its own:
69
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
70
70
  WORKFLOW_PLAN_STATUS: ready
71
- The workflow will NOT advance until ${artifactDir}/plan.md contains that marker.`,
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. Synthesize the findings into ${artifactDir}/reuse.md: what is reusable, where, and how to leverage it.
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. The recapper returns the handoff; the workflow saves it to ${artifactDir}/handoff.md.
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
- The handoff MUST end with exactly one machine-readable line on its own:
96
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
97
97
  WORKFLOW_HANDOFF_STATUS: ready
98
- The workflow will NOT advance until ${artifactDir}/handoff.md contains that marker.`,
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. The commentator returns its review; the workflow saves it to ${artifactDir}/review.md. The review MUST end with exactly one machine-readable line on its own:
118
- WORKFLOW_REVIEW_STATUS: clean
119
- (use WORKFLOW_REVIEW_STATUS: blocking if actionable issues remain)
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. The architect returns the plan; the workflow saves it to ${artifactDir}/plan.md.
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
- The plan MUST end with exactly one machine-readable line on its own:
24
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
25
25
  WORKFLOW_PLAN_STATUS: ready
26
- The workflow will NOT advance until ${artifactDir}/plan.md contains that marker.`,
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";