pi-crew 0.11.26 → 0.11.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.26",
3
+ "version": "0.11.27",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -235,7 +235,14 @@ export class ChainTeamRunExecutor implements ChainTaskRunner {
235
235
  // 1. Format chain history into the goal — this is how context reaches the worker.
236
236
  // Mirrors the parentContext pattern in child-pi.ts (parent context + task delimiter).
237
237
  const historyPrefix = formatChainHistory(context);
238
- const enrichedGoal = historyPrefix ? `${historyPrefix}\n\n---\n# Current Chain Step\n${packet.goal}` : packet.goal;
238
+ // v0.11.27 (R14-C1 tail): `--with-context` carries the previous step's FULL
239
+ // output as __chainPrevOutput (runner-capped); render it as its own block so
240
+ // the worker sees raw output, not just the size-capped handoff summary.
241
+ const prevOutput = typeof context.__chainPrevOutput === "string" ? context.__chainPrevOutput.trim() : "";
242
+ const prevOutputBlock = prevOutput ? `# Previous Step Output (--with-context)\n\n${prevOutput}` : "";
243
+ const headerParts = [historyPrefix, prevOutputBlock].filter((part) => part.length > 0);
244
+ const enrichedGoal =
245
+ headerParts.length > 0 ? `${headerParts.join("\n\n---\n")}\n\n---\n# Current Chain Step\n${packet.goal}` : packet.goal;
239
246
 
240
247
  // 2. Resolve team/workflow/model: step config (set by executeStep) → overrides → default.
241
248
  // BUG-44 (github #44): a workflow override of "chain" is a dispatcher-only marker
@@ -250,6 +257,11 @@ export class ChainTeamRunExecutor implements ChainTaskRunner {
250
257
  // 3. Call handleRun for the heavy lifting. async:false forces each step to
251
258
  // complete synchronously (overrides asyncByDefault) so the chain is sequential.
252
259
  // NO `chain` key here — so the run.ts guard does not re-enter chain dispatch.
260
+ // v0.11.27 (R14-C2): forward the previously-dead `--timeout N` (s) flag.
261
+ // resolveRunDeadline honors params.timeoutMs (CORE-8 unified deadline:
262
+ // params > config.limits.maxRunMinutes > 1h default) for foreground runs.
263
+ const stepTimeoutMs = typeof context.__chainStepTimeoutMs === "number" ? context.__chainStepTimeoutMs : undefined;
264
+
253
265
  const runParams: TeamToolParamsValue = {
254
266
  action: "run",
255
267
  goal: enrichedGoal,
@@ -257,6 +269,7 @@ export class ChainTeamRunExecutor implements ChainTaskRunner {
257
269
  async: false,
258
270
  ...(stepWorkflow ? { workflow: stepWorkflow } : {}),
259
271
  ...(stepModel ? { model: stepModel } : {}),
272
+ ...(stepTimeoutMs !== undefined && Number.isFinite(stepTimeoutMs) ? { timeoutMs: stepTimeoutMs } : {}),
260
273
  };
261
274
  const runRes = await this.handleRunRef(runParams, this.ctx);
262
275
 
@@ -6,6 +6,12 @@
6
6
  * step1:3 -> step2 --with-context -> step3
7
7
  * parallel(a, b, parallel(c, d)) -> e
8
8
  *
9
+ * `:N` loop counts are CLAMPED to 1..10 (v0.11.27): N=0/absent would silently
10
+ * skip nothing, N>10 would queue unbounded sequential runs. Clamping (not
11
+ * throwing) is deliberate — a throw here makes chain-runner fall back to the
12
+ * simple splitter, which turns `step:99` into ONE literal step named "step:99"
13
+ * (the exact R14-C1 silent-corruption class this parser exists to prevent).
14
+ *
9
15
  * Pattern origin: pi-prompt-template-model chain-parser.ts
10
16
  */
11
17
 
@@ -193,6 +199,21 @@ class ChainParser {
193
199
  return step;
194
200
  }
195
201
 
202
+ // v0.11.27 (probe-caught): a QUOTED string is a valid step too —
203
+ // `"polish output":2` is the natural way to combine an inline goal with
204
+ // DSL modifiers. Previously only bare NAMEs were accepted here, so any
205
+ // quoted-goal chain with `:N`/`--with-context` threw and chain-runner
206
+ // silently fell back to the simple splitter — the constructs died for
207
+ // the MOST COMMON goal syntax. A quoted step keeps its text as `name`
208
+ // (executeStep uses it as the goal, mirroring the simple path's
209
+ // inlineGoal semantics).
210
+ if (this.peek("QUOTED")) {
211
+ const quoted = this.consume("QUOTED");
212
+ const quotedStep: ChainStep = { name: quoted.value };
213
+ this.parseModifiers(quotedStep);
214
+ return quotedStep;
215
+ }
216
+
196
217
  // Normal step name
197
218
  const name = this.consume("NAME").value;
198
219
  const step: ChainStep = { name };
@@ -207,7 +228,9 @@ class ChainParser {
207
228
  if (this.peek("COLON")) {
208
229
  this.consume("COLON");
209
230
  const num = this.consume("NUMBER");
210
- step.loopCount = Number.parseInt(num.value, 10);
231
+ // v0.11.27: clamp 1..10 (see header). Clamp-not-throw: a throw silently
232
+ // degrades the whole chain to the simple parser (literal step names).
233
+ step.loopCount = Math.min(10, Math.max(1, Number.parseInt(num.value, 10)));
211
234
  } else if (this.peek("FLAG", "with-context")) {
212
235
  this.consume("FLAG");
213
236
  step.withContext = true;
@@ -105,6 +105,10 @@ export class ChainRunner {
105
105
  /** Maximum size per handoff entry to prevent memory issues from large artifacts */
106
106
  private static readonly MAX_HANDOFF_ENTRY_SIZE = 5000; // bytes per entry
107
107
 
108
+ /** v0.11.27: `--with-context` full-output cap (default history carries only
109
+ * 5000-char summaries; this is the explicit full-output channel, bounded). */
110
+ private static readonly MAX_WITH_CONTEXT_OUTPUT_CHARS = 20000; // chars
111
+
108
112
  private taskRunner: ChainTaskRunner;
109
113
  private handoffManager: HandoffManager;
110
114
 
@@ -186,8 +190,16 @@ export class ChainRunner {
186
190
  // success predicate cannot report a truncated chain as complete.
187
191
  let brokeEarly = false;
188
192
 
189
- for (let i = 0; i < spec.steps.length; i++) {
190
- const step = spec.steps[i];
193
+ // v0.11.27 (R14-C1 tail): expand `:N` loop steps into an execution plan —
194
+ // one entry per ITERATION. Each iteration is its own ChainStepResult
195
+ // (name#k for k>1) so a failing iteration stays visible; the success
196
+ // predicate counts planned EXECUTIONS, so a chain that broke inside a
197
+ // loop cannot pass unexecuted iterations off as done (R14-C5 class).
198
+ const plan = this.buildExecutionPlan(spec);
199
+ const plannedExecutions = plan.length;
200
+
201
+ for (const entry of plan) {
202
+ const { step, stepIndex: i, displayName, iterations } = entry;
191
203
  const stepStart = Date.now();
192
204
 
193
205
  try {
@@ -198,7 +210,7 @@ export class ChainRunner {
198
210
  const stepContext = this.enrichContextFromHandoffs(accumulatedContext, stepResults);
199
211
 
200
212
  // Execute step
201
- const result = await this.executeStep(effectiveConfig, stepContext);
213
+ const result = await this.executeStep(effectiveConfig, stepContext, stepResults);
202
214
 
203
215
  // Track tokens
204
216
  if (result.usage?.totalTokens) {
@@ -210,7 +222,7 @@ export class ChainRunner {
210
222
 
211
223
  stepResults.push({
212
224
  step: i + 1,
213
- name: step.name,
225
+ name: displayName,
214
226
  outcome: result.outcome,
215
227
  result,
216
228
  handoff,
@@ -243,10 +255,10 @@ export class ChainRunner {
243
255
  await appendEventAsync(eventsPath, {
244
256
  type: "chain.step_completed",
245
257
  runId: "chain",
246
- taskId: `step-${i + 1}`,
258
+ taskId: iterations > 1 ? `step-${i + 1}#${entry.iter}` : `step-${i + 1}`,
247
259
  data: {
248
260
  step: i + 1,
249
- name: step.name,
261
+ name: displayName,
250
262
  outcome: result.outcome,
251
263
  duration: Date.now() - stepStart,
252
264
  },
@@ -257,7 +269,7 @@ export class ChainRunner {
257
269
 
258
270
  stepResults.push({
259
271
  step: i + 1,
260
- name: step.name,
272
+ name: displayName,
261
273
  outcome: "failure",
262
274
  duration: Date.now() - stepStart,
263
275
  error: errMsg,
@@ -277,7 +289,7 @@ export class ChainRunner {
277
289
  // for a chain whose remaining steps never ran. An empty spec was vacuously
278
290
  // green for the same reason. Success now requires: at least one step, no
279
291
  // early break, every planned step executed, and every outcome `success`.
280
- const allStepsExecuted = stepResults.length === spec.steps.length;
292
+ const allStepsExecuted = stepResults.length === plannedExecutions;
281
293
  const allOutcomesSuccessful = stepResults.every((s) => s.outcome === "success");
282
294
 
283
295
  return {
@@ -393,9 +405,14 @@ export class ChainRunner {
393
405
  // Parse step timeout
394
406
  const timeoutStr = this.extractFlag(step, "timeout");
395
407
  if (timeoutStr) {
396
- const timeoutMs = parseInt(timeoutStr, 10);
397
- if (!Number.isNaN(timeoutMs) && timeoutMs > 0 && timeoutMs <= 86400000) {
398
- parsed.timeout = timeoutMs * 1000; // Convert seconds to ms
408
+ // v0.11.27 (R14-C2): the flag is SECONDS — the old bound compared the
409
+ // seconds value against 86400000 (a 1000-day ms bound) before
410
+ // multiplying, so `--timeout 99999999` was silently accepted. Now
411
+ // 1..86400 s (1 day), and the value is actually CONSUMED (chain-executor
412
+ // forwards it as runParams.timeoutMs).
413
+ const timeoutSec = parseInt(timeoutStr, 10);
414
+ if (!Number.isNaN(timeoutSec) && timeoutSec > 0 && timeoutSec <= 86400) {
415
+ parsed.timeout = timeoutSec * 1000;
399
416
  }
400
417
  }
401
418
 
@@ -412,7 +429,12 @@ export class ChainRunner {
412
429
  * DSL features: parallel(...), :loopCount, --with-context flag
413
430
  */
414
431
  private hasDSLConstructs(chainString: string): boolean {
415
- return /\bparallel\s*\(/.test(chainString) || /\w+:\d+\b/.test(chainString) || /--with-context/.test(chainString);
432
+ // v0.11.27 (probe-caught): `:\d+` no longer requires a leading \w — a
433
+ // QUOTED goal followed by `:2` (`"polish":2`) must route to the DSL
434
+ // parser too. Broadening is safe: time-like false positives
435
+ // ("deploy at 10:30") still throw inside the DSL parser and fall back to
436
+ // the simple splitter exactly as before.
437
+ return /\bparallel\s*\(/.test(chainString) || /:\d+\b/.test(chainString) || /--with-context/.test(chainString);
416
438
  }
417
439
 
418
440
  /**
@@ -613,7 +635,29 @@ export class ChainRunner {
613
635
  /**
614
636
  * Execute a single step.
615
637
  */
616
- private async executeStep(config: ChainStep, context: Record<string, unknown>): Promise<TaskResult> {
638
+ private async executeStep(
639
+ config: ChainStep,
640
+ context: Record<string, unknown>,
641
+ previousResults: ChainStepResult[],
642
+ ): Promise<TaskResult> {
643
+ // v0.11.27 (R14-C1 tail): DSL `args` append to the goal — positional goal
644
+ // arguments (`"research" "topic X"` -> goal "research topic X").
645
+ const dslArgs = Array.isArray(config.context?.args) ? (config.context.args as string[]) : undefined;
646
+ const baseGoal = config.inlineGoal ?? config.name;
647
+ const goal = dslArgs && dslArgs.length > 0 ? [baseGoal, ...dslArgs].join(" ") : baseGoal;
648
+
649
+ // v0.11.27 (R14-C1 tail): DSL `--with-context` passes the PREVIOUS step's
650
+ // full worker output (capped) — distinct from the default __chainHistory,
651
+ // which carries only size-capped handoff summaries.
652
+ const withContext = config.context?.withContext === true;
653
+ let chainPrevOutput: string | undefined;
654
+ if (withContext) {
655
+ const prev = [...previousResults].reverse().find((r) => r.outcome === "success" && r.result?.outputText);
656
+ if (prev?.result?.outputText) {
657
+ chainPrevOutput = prev.result.outputText.slice(0, ChainRunner.MAX_WITH_CONTEXT_OUTPUT_CHARS);
658
+ }
659
+ }
660
+
617
661
  // Pass step config (team/workflow/model) into the packet context so the
618
662
  // concrete ChainTaskRunner can resolve which team/workflow to run. Without
619
663
  // this, a step parsed to a @teamName reference would lose its team because
@@ -622,7 +666,7 @@ export class ChainRunner {
622
666
  const packet: TaskPacket = {
623
667
  taskId: `chain-${Date.now()}-${config.name}`,
624
668
  runId: "chain",
625
- goal: config.inlineGoal ?? config.name,
669
+ goal,
626
670
  summarizeThreshold: 3000,
627
671
  collapseContext: true,
628
672
  context: {
@@ -630,12 +674,53 @@ export class ChainRunner {
630
674
  __chainStepTeam: config.team,
631
675
  __chainStepWorkflow: config.workflow,
632
676
  __chainStepModel: config.model,
677
+ ...(chainPrevOutput !== undefined ? { __chainPrevOutput: chainPrevOutput } : {}),
678
+ // v0.11.27 (R14-C2): wire the previously-dead `--timeout` flag into
679
+ // the step run's deadline (ms; consumed by chain-executor as
680
+ // runParams.timeoutMs -> CORE-8 resolveRunDeadline).
681
+ ...(config.timeout !== undefined ? { __chainStepTimeoutMs: config.timeout } : {}),
633
682
  },
634
683
  };
635
684
 
636
685
  return this.taskRunner.runTask(packet);
637
686
  }
638
687
 
688
+ /** v0.11.27: iterations planned for a step — `:N` DSL (parser-clamped 1..10). */
689
+ private plannedIterations(step: ChainStep): number {
690
+ const n = step.context?.loopCount;
691
+ return typeof n === "number" && Number.isInteger(n) && n >= 1 ? Math.min(10, n) : 1;
692
+ }
693
+
694
+ /** v0.11.27: expand steps into per-iteration plan entries. */
695
+ private buildExecutionPlan(spec: ChainSpec): Array<{
696
+ step: ChainStep;
697
+ stepIndex: number;
698
+ iter: number;
699
+ iterations: number;
700
+ displayName: string;
701
+ }> {
702
+ const plan: Array<{
703
+ step: ChainStep;
704
+ stepIndex: number;
705
+ iter: number;
706
+ iterations: number;
707
+ displayName: string;
708
+ }> = [];
709
+ spec.steps.forEach((step, stepIndex) => {
710
+ const iterations = this.plannedIterations(step);
711
+ for (let iter = 1; iter <= iterations; iter++) {
712
+ plan.push({
713
+ step,
714
+ stepIndex,
715
+ iter,
716
+ iterations,
717
+ displayName: iter > 1 ? `${step.name}#${iter}` : step.name,
718
+ });
719
+ }
720
+ });
721
+ return plan;
722
+ }
723
+
639
724
  /**
640
725
  * Create minimal packet for handoff generation.
641
726
  */