pi-prompt-template-model 0.7.3 → 0.8.0
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/CHANGELOG.md +17 -0
- package/README.md +123 -0
- package/deterministic-renderer.ts +136 -0
- package/deterministic-step.ts +309 -0
- package/index.ts +68 -4
- package/package.json +3 -1
- package/prompt-loader.ts +452 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.0] - 2026-04-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added first-class deterministic prompt-template execution for single prompt templates via `deterministic:` or shorthand `run` / `script` frontmatter. Templates can run one direct command or script before any optional LLM turn.
|
|
9
|
+
- Added configurable deterministic-step handoff policies: `always`, `never`, `on-success`, and `on-failure`.
|
|
10
|
+
- Added deterministic result cards that always show the executed command or script, resolved `cwd`, exit code, duration, and stdout/stderr previews.
|
|
11
|
+
- Added deterministic-step `env` support plus `nonInteractive` control for deploy/release-style scripts that need explicit environment variables or want to disable the default non-interactive guardrail env bundle.
|
|
12
|
+
- Added a visible deterministic completion message for `handoff: never`, so no-handoff runs still end with an explicit completion marker after the result card.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- When a deterministic prompt hands off to the model, the extension now prepends a generated `[Deterministic step]` block with structured execution metadata and truncated stdout/stderr previews before the prompt body.
|
|
16
|
+
- Deterministic stdout/stderr payloads are now capped before they are stored in message details, while preserving total line/character counts and truncation metadata for both the card UI and the LLM handoff block.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Added regression coverage for deterministic loader parsing, relative script resolution, handoff gating, and the no-handoff fast path.
|
|
20
|
+
- Deterministic timeouts now escalate from `SIGTERM` to `SIGKILL` if the child process does not exit within the post-timeout grace window.
|
|
21
|
+
|
|
5
22
|
## [0.7.3] - 2026-04-14
|
|
6
23
|
|
|
7
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -353,6 +353,128 @@ Within a compare lineup, use `task` for a full per-slot override and `taskSuffix
|
|
|
353
353
|
|
|
354
354
|
When a compare prompt uses `bestOfN.worktree: true`, all worker slots must resolve to the same `cwd`. Mixed worker `cwd` values are only allowed when worktree isolation is off. Worktree isolation is for the worker phase only; `bestOfN.finalApplier` always applies on the real branch (`compareCwd`).
|
|
355
355
|
|
|
356
|
+
## Deterministic Steps
|
|
357
|
+
|
|
358
|
+
Prompt templates can run one deterministic command or script before any optional LLM turn. Use this when the first step should be direct code, not model latency.
|
|
359
|
+
|
|
360
|
+
The flow is simple:
|
|
361
|
+
|
|
362
|
+
1. Run one command or script.
|
|
363
|
+
2. Always render a visible deterministic result card with the command, exit code, duration, and stdout/stderr previews.
|
|
364
|
+
3. Optionally hand the structured result to the model as a `[Deterministic step]` preamble before the prompt body.
|
|
365
|
+
4. If `handoff: never`, stop after the result card and a visible completion marker — no LLM turn happens.
|
|
366
|
+
|
|
367
|
+
That handoff preamble is intentionally structured and uses stable field names like `status`, `executionKind`, `command`, `cwd`, `exitCode`, `signal`, `durationMs`, `timedOut`, `lineCount`, `charCount`, `truncated`, `omittedChars`, and `preview`.
|
|
368
|
+
|
|
369
|
+
V1 scope is intentionally narrow: deterministic execution only works on single prompt templates. It does not combine with chain templates, delegated/subagent prompts, `parallel`, or loops. At runtime, deterministic prompts explicitly reject `--loop`, `--subagent`, and `--fork` in v1.
|
|
370
|
+
|
|
371
|
+
### Authoring forms
|
|
372
|
+
|
|
373
|
+
You can write deterministic steps as **top-level shorthand** or **nested under `deterministic:`**. Both are equivalent. Use shorthand for brevity, nested when you want everything grouped under one key.
|
|
374
|
+
|
|
375
|
+
**Top-level shorthand** — put `run`, `script`, `handoff`, `timeout`, `cwd`, `env`, and `nonInteractive` directly in frontmatter:
|
|
376
|
+
|
|
377
|
+
```markdown
|
|
378
|
+
---
|
|
379
|
+
run: git push origin HEAD:main
|
|
380
|
+
handoff: on-failure
|
|
381
|
+
timeout: 30000
|
|
382
|
+
---
|
|
383
|
+
If the push failed, explain why and suggest the next step.
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
You can also use `script:` as shorthand:
|
|
387
|
+
|
|
388
|
+
```markdown
|
|
389
|
+
---
|
|
390
|
+
script: ./scripts/ship.sh
|
|
391
|
+
handoff: always
|
|
392
|
+
timeout: 15000
|
|
393
|
+
---
|
|
394
|
+
Summarize the script result.
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Nested form** — group everything under `deterministic:`:
|
|
398
|
+
|
|
399
|
+
```markdown
|
|
400
|
+
---
|
|
401
|
+
model: claude-sonnet-4-20250514
|
|
402
|
+
deterministic:
|
|
403
|
+
script:
|
|
404
|
+
path: ./scripts/ship.sh
|
|
405
|
+
args:
|
|
406
|
+
- --fast
|
|
407
|
+
handoff: always
|
|
408
|
+
timeout: 15000
|
|
409
|
+
cwd: ~/src/my-repo
|
|
410
|
+
---
|
|
411
|
+
Summarize the script result and call out anything risky.
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Structured command form** — when you need explicit args instead of a single shell string, use `deterministic.run.command` with `args`:
|
|
415
|
+
|
|
416
|
+
```markdown
|
|
417
|
+
---
|
|
418
|
+
model: claude-sonnet-4-20250514
|
|
419
|
+
deterministic:
|
|
420
|
+
run:
|
|
421
|
+
command: git
|
|
422
|
+
args: [status, --short]
|
|
423
|
+
handoff: always
|
|
424
|
+
---
|
|
425
|
+
Interpret the repo state.
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Do not mix top-level shorthand with nested `deterministic:` in the same prompt. Pick one style.
|
|
429
|
+
|
|
430
|
+
### Model requirement
|
|
431
|
+
|
|
432
|
+
Deterministic prompts that hand off to the model (`handoff: always`, `on-success`, or `on-failure`) need a model to continue into. You can either:
|
|
433
|
+
|
|
434
|
+
- Add a `model:` field explicitly
|
|
435
|
+
- Omit `model:` and let the prompt inherit whatever model is currently active
|
|
436
|
+
|
|
437
|
+
`handoff: never` prompts do not need a model field because they never reach the LLM.
|
|
438
|
+
|
|
439
|
+
### Handoff values
|
|
440
|
+
|
|
441
|
+
- `always` — always continue into the LLM after the deterministic card is emitted.
|
|
442
|
+
- `never` — stop after the deterministic card and completion marker.
|
|
443
|
+
- `on-success` — continue only when the command exits `0`.
|
|
444
|
+
- `on-failure` — continue only when the command exits non-zero.
|
|
445
|
+
|
|
446
|
+
Command descriptions in the slash-command picker show this feature as `deterministic-step:<handoff>`.
|
|
447
|
+
|
|
448
|
+
### Timeout
|
|
449
|
+
|
|
450
|
+
`timeout` is in milliseconds. When a timeout fires, the runner sends `SIGTERM` first. If the process still has not exited after a short grace window, it escalates to `SIGKILL`.
|
|
451
|
+
|
|
452
|
+
### Script path resolution
|
|
453
|
+
|
|
454
|
+
Relative script paths resolve from the prompt file's directory first, then fall back to the command invocation `cwd`. Absolute script paths also work.
|
|
455
|
+
|
|
456
|
+
### Environment and non-interactive mode
|
|
457
|
+
|
|
458
|
+
You can provide explicit environment variables and control the runner's non-interactive guardrails:
|
|
459
|
+
|
|
460
|
+
```markdown
|
|
461
|
+
---
|
|
462
|
+
deterministic:
|
|
463
|
+
run: ./deploy.sh
|
|
464
|
+
handoff: never
|
|
465
|
+
nonInteractive: false
|
|
466
|
+
env:
|
|
467
|
+
SPECIAL_TOKEN: abc123
|
|
468
|
+
RETRIES: 2
|
|
469
|
+
---
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`nonInteractive` defaults to `true`. In that mode the runner keeps stdin ignored and adds a few guardrail environment defaults such as `CI=1`, `GIT_TERMINAL_PROMPT=0`, `PAGER=cat`, and `GIT_PAGER=cat`. Set `nonInteractive: false` when the command needs a more normal process environment and you explicitly want to opt out of those defaults. Explicit `env` values override the built-in defaults.
|
|
473
|
+
|
|
474
|
+
### Output capping
|
|
475
|
+
|
|
476
|
+
Large stdout/stderr streams are capped before they are stored in the conversation card payload. The card and the LLM handoff block both show the total character and line counts plus explicit truncation metadata when output was capped.
|
|
477
|
+
|
|
356
478
|
## Loop Execution
|
|
357
479
|
|
|
358
480
|
Run a template multiple times with `--loop`:
|
|
@@ -672,3 +794,4 @@ $@
|
|
|
672
794
|
- In chains, model-less steps inherit the chain-start model snapshot, not the previous step's model. This is intentional for deterministic behavior.
|
|
673
795
|
- Delegated `subagent` prompts require [pi-subagents](https://github.com/nicobailon/pi-subagents/).
|
|
674
796
|
- `run-prompt` must be explicitly enabled with `/prompt-tool on`.
|
|
797
|
+
rompt-tool on`.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { formatDeterministicExecution, type DeterministicExecutionResult } from "./deterministic-step.js";
|
|
4
|
+
|
|
5
|
+
interface DeterministicMessage {
|
|
6
|
+
content?: unknown;
|
|
7
|
+
details?: DeterministicExecutionResult;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DeterministicCompletionMessage {
|
|
11
|
+
content?: unknown;
|
|
12
|
+
details?: {
|
|
13
|
+
promptName: string;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
timedOut: boolean;
|
|
16
|
+
status: "succeeded" | "failed";
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PREVIEW_LINES = 8;
|
|
21
|
+
|
|
22
|
+
function formatDuration(durationMs: number): string {
|
|
23
|
+
if (durationMs < 1_000) return `${durationMs}ms`;
|
|
24
|
+
if (durationMs < 10_000) return `${(durationMs / 1_000).toFixed(1)}s`;
|
|
25
|
+
return `${Math.round(durationMs / 1_000)}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildCapturedOutputLabel(
|
|
29
|
+
label: string,
|
|
30
|
+
meta: { totalChars: number; totalLines: number; truncated: boolean },
|
|
31
|
+
): string {
|
|
32
|
+
if (meta.totalChars === 0) return `${label} · empty`;
|
|
33
|
+
const lineCount = meta.totalLines;
|
|
34
|
+
const charCount = meta.totalChars.toLocaleString();
|
|
35
|
+
const truncated = meta.truncated ? " · capped" : "";
|
|
36
|
+
return `${label} · ${lineCount} line${lineCount === 1 ? "" : "s"} · ${charCount} chars${truncated}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderOutputSection(
|
|
40
|
+
box: Box,
|
|
41
|
+
label: string,
|
|
42
|
+
value: string,
|
|
43
|
+
meta: { totalChars: number; totalLines: number; truncated: boolean },
|
|
44
|
+
options: MessageRenderOptions,
|
|
45
|
+
theme: Theme,
|
|
46
|
+
) {
|
|
47
|
+
box.addChild(new Text(theme.fg("toolTitle", buildCapturedOutputLabel(label, meta)), 0, 0));
|
|
48
|
+
if (!value) {
|
|
49
|
+
box.addChild(new Text(theme.fg("dim", "(empty)"), 0, 0));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const lines = value.split("\n");
|
|
53
|
+
if (options.expanded || lines.length <= PREVIEW_LINES) {
|
|
54
|
+
box.addChild(new Text(theme.fg("toolOutput", value), 0, 0));
|
|
55
|
+
if (meta.truncated) {
|
|
56
|
+
box.addChild(new Text(theme.fg("warning", `\n... (stored preview capped, ${Math.max(0, meta.totalChars - value.length)} more chars hidden)`), 0, 0));
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
box.addChild(new Text(theme.fg("toolOutput", lines.slice(0, PREVIEW_LINES).join("\n")), 0, 0));
|
|
61
|
+
box.addChild(new Text(theme.fg("warning", `\n... (${lines.length - PREVIEW_LINES} more lines hidden — Ctrl+O to expand)`), 0, 0));
|
|
62
|
+
if (meta.truncated) {
|
|
63
|
+
box.addChild(new Text(theme.fg("warning", `\n... (stored preview capped, ${Math.max(0, meta.totalChars - value.length)} more chars hidden)`), 0, 0));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderDeterministicResult(message: DeterministicMessage, options: MessageRenderOptions, theme: Theme) {
|
|
68
|
+
const details = message.details;
|
|
69
|
+
const container = new Container();
|
|
70
|
+
container.addChild(new Spacer(1));
|
|
71
|
+
if (!details) {
|
|
72
|
+
container.addChild(new Text(theme.fg("warning", "Deterministic step message is missing details."), 0, 0));
|
|
73
|
+
return container;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const failed = details.exitCode !== 0;
|
|
77
|
+
const box = new Box(1, 1, (text: string) => theme.bg(failed ? "toolPendingBg" : "toolSuccessBg", text));
|
|
78
|
+
const icon = theme.fg(failed ? "error" : "success", failed ? "fail" : "ok");
|
|
79
|
+
const status = failed ? "failed" : "succeeded";
|
|
80
|
+
const title = formatDeterministicExecution(details.execution, details.resolvedScriptPath);
|
|
81
|
+
box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold("deterministic"))} | ${status} · exit ${details.exitCode} · ${formatDuration(details.durationMs)}`, 0, 0));
|
|
82
|
+
box.addChild(new Spacer(1));
|
|
83
|
+
box.addChild(new Text(theme.fg("dim", `command: ${title}`), 0, 0));
|
|
84
|
+
if (details.resolvedScriptPath) {
|
|
85
|
+
box.addChild(new Text(theme.fg("dim", `script: ${details.resolvedScriptPath}`), 0, 0));
|
|
86
|
+
}
|
|
87
|
+
box.addChild(new Text(theme.fg("dim", `cwd: ${details.cwd}`), 0, 0));
|
|
88
|
+
if (details.signal) {
|
|
89
|
+
box.addChild(new Text(theme.fg("dim", `signal: ${details.signal}`), 0, 0));
|
|
90
|
+
}
|
|
91
|
+
if (details.timedOut) {
|
|
92
|
+
box.addChild(new Text(theme.fg("error", "timeout reached before the process exited"), 0, 0));
|
|
93
|
+
}
|
|
94
|
+
box.addChild(new Text(theme.fg("dim", `nonInteractive: ${details.nonInteractive ? "true" : "false"}`), 0, 0));
|
|
95
|
+
box.addChild(new Spacer(1));
|
|
96
|
+
renderOutputSection(box, "stdout", details.stdout, {
|
|
97
|
+
totalChars: details.stdoutTotalChars,
|
|
98
|
+
totalLines: details.stdoutTotalLines,
|
|
99
|
+
truncated: details.stdoutTruncated,
|
|
100
|
+
}, options, theme);
|
|
101
|
+
box.addChild(new Spacer(1));
|
|
102
|
+
renderOutputSection(box, "stderr", details.stderr, {
|
|
103
|
+
totalChars: details.stderrTotalChars,
|
|
104
|
+
totalLines: details.stderrTotalLines,
|
|
105
|
+
truncated: details.stderrTruncated,
|
|
106
|
+
}, options, theme);
|
|
107
|
+
container.addChild(box);
|
|
108
|
+
return container;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderDeterministicCompletion(
|
|
112
|
+
message: DeterministicCompletionMessage,
|
|
113
|
+
_options: MessageRenderOptions,
|
|
114
|
+
theme: Theme,
|
|
115
|
+
) {
|
|
116
|
+
const details = message.details;
|
|
117
|
+
const container = new Container();
|
|
118
|
+
container.addChild(new Spacer(1));
|
|
119
|
+
if (!details) {
|
|
120
|
+
container.addChild(new Text(theme.fg("warning", "Deterministic completion message is missing details."), 0, 0));
|
|
121
|
+
return container;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const failed = details.status === "failed";
|
|
125
|
+
const box = new Box(1, 1, (text: string) => theme.bg(failed ? "toolPendingBg" : "toolSuccessBg", text));
|
|
126
|
+
const icon = theme.fg(failed ? "error" : "success", failed ? "fail" : "ok");
|
|
127
|
+
box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold("deterministic complete"))} | ${details.status} · exit ${details.exitCode}`, 0, 0));
|
|
128
|
+
box.addChild(new Spacer(1));
|
|
129
|
+
box.addChild(new Text(theme.fg("dim", `prompt: ${details.promptName}`), 0, 0));
|
|
130
|
+
box.addChild(new Text(theme.fg("dim", "model handoff: skipped"), 0, 0));
|
|
131
|
+
if (details.timedOut) {
|
|
132
|
+
box.addChild(new Text(theme.fg("error", "the command hit its timeout before completion"), 0, 0));
|
|
133
|
+
}
|
|
134
|
+
container.addChild(box);
|
|
135
|
+
return container;
|
|
136
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import type { PromptWithModel, DeterministicStep, DeterministicExecution, DeterministicEnv } from "./prompt-loader.js";
|
|
5
|
+
|
|
6
|
+
export const PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE = "prompt-template-deterministic";
|
|
7
|
+
export const PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE = "prompt-template-deterministic-complete";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_CAPTURE_STDOUT_CHARS = 16_000;
|
|
10
|
+
const DEFAULT_MAX_CAPTURE_STDERR_CHARS = 16_000;
|
|
11
|
+
const DEFAULT_TIMEOUT_KILL_AFTER_MS = 1_000;
|
|
12
|
+
|
|
13
|
+
interface CapturedOutput {
|
|
14
|
+
text: string;
|
|
15
|
+
totalChars: number;
|
|
16
|
+
totalNewlines: number;
|
|
17
|
+
trailingNewlineRun: number;
|
|
18
|
+
sawNonNewline: boolean;
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
maxChars: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeterministicExecutionResult {
|
|
24
|
+
execution: DeterministicExecution;
|
|
25
|
+
cwd: string;
|
|
26
|
+
nonInteractive: boolean;
|
|
27
|
+
resolvedScriptPath?: string;
|
|
28
|
+
exitCode: number;
|
|
29
|
+
signal?: NodeJS.Signals;
|
|
30
|
+
stdout: string;
|
|
31
|
+
stdoutTotalChars: number;
|
|
32
|
+
stdoutTotalLines: number;
|
|
33
|
+
stdoutTruncated: boolean;
|
|
34
|
+
stderr: string;
|
|
35
|
+
stderrTotalChars: number;
|
|
36
|
+
stderrTotalLines: number;
|
|
37
|
+
stderrTruncated: boolean;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
timedOut: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DeterministicPreambleOptions {
|
|
43
|
+
maxStdoutChars?: number;
|
|
44
|
+
maxStderrChars?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createCapturedOutput(maxChars: number): CapturedOutput {
|
|
48
|
+
return {
|
|
49
|
+
text: "",
|
|
50
|
+
totalChars: 0,
|
|
51
|
+
totalNewlines: 0,
|
|
52
|
+
trailingNewlineRun: 0,
|
|
53
|
+
sawNonNewline: false,
|
|
54
|
+
truncated: false,
|
|
55
|
+
maxChars,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendCapturedOutput(output: CapturedOutput, chunk: string): void {
|
|
60
|
+
if (!chunk) return;
|
|
61
|
+
output.totalChars += chunk.length;
|
|
62
|
+
const newlines = chunk.match(/\n/g)?.length ?? 0;
|
|
63
|
+
output.totalNewlines += newlines;
|
|
64
|
+
if (/[^\n]/.test(chunk)) output.sawNonNewline = true;
|
|
65
|
+
const trailingRun = chunk.match(/\n+$/)?.[0].length ?? 0;
|
|
66
|
+
if (trailingRun === 0) {
|
|
67
|
+
output.trailingNewlineRun = 0;
|
|
68
|
+
} else if (trailingRun === chunk.length) {
|
|
69
|
+
output.trailingNewlineRun += trailingRun;
|
|
70
|
+
} else {
|
|
71
|
+
output.trailingNewlineRun = trailingRun;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (output.text.length < output.maxChars) {
|
|
75
|
+
const remaining = output.maxChars - output.text.length;
|
|
76
|
+
output.text += chunk.slice(0, remaining);
|
|
77
|
+
}
|
|
78
|
+
if (output.totalChars > output.maxChars) output.truncated = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function capturedLineCount(output: Pick<CapturedOutput, "totalChars" | "sawNonNewline" | "totalNewlines" | "trailingNewlineRun">): number {
|
|
82
|
+
if (output.totalChars === 0) return 0;
|
|
83
|
+
if (!output.sawNonNewline) return 1;
|
|
84
|
+
return output.totalNewlines - output.trailingNewlineRun + 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function countLines(value: string): number {
|
|
88
|
+
if (!value) return 0;
|
|
89
|
+
const normalized = value.replace(/\n+$/g, "");
|
|
90
|
+
if (!normalized) return 1;
|
|
91
|
+
return normalized.split("\n").length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildTextPreview(label: string, value: string, totalChars: number, maxChars: number): { text: string; truncated: boolean; omittedChars: number } {
|
|
95
|
+
const shownChars = Math.min(value.length, maxChars);
|
|
96
|
+
const preview = value.slice(0, shownChars);
|
|
97
|
+
const omittedChars = Math.max(0, totalChars - shownChars);
|
|
98
|
+
if (omittedChars === 0) {
|
|
99
|
+
return { text: preview, truncated: false, omittedChars: 0 };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
text: `${preview}\n...[${label} truncated, ${omittedChars} more chars omitted]`,
|
|
103
|
+
truncated: true,
|
|
104
|
+
omittedChars,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function shellQuote(value: string): string {
|
|
109
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatDeterministicExecution(execution: DeterministicExecution, resolvedScriptPath?: string): string {
|
|
113
|
+
switch (execution.kind) {
|
|
114
|
+
case "run":
|
|
115
|
+
return execution.command;
|
|
116
|
+
case "command": {
|
|
117
|
+
const parts = [execution.command, ...execution.args].map((part) => shellQuote(part));
|
|
118
|
+
return execution.shell ? `${parts.join(" ")} (shell)` : parts.join(" ");
|
|
119
|
+
}
|
|
120
|
+
case "script": {
|
|
121
|
+
const scriptPath = resolvedScriptPath ?? execution.path;
|
|
122
|
+
const parts = [scriptPath, ...execution.args].map((part) => shellQuote(part));
|
|
123
|
+
return parts.join(" ");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function shouldHandoffToLlm(step: DeterministicStep, result: Pick<DeterministicExecutionResult, "exitCode">): boolean {
|
|
129
|
+
switch (step.handoff) {
|
|
130
|
+
case "always": return true;
|
|
131
|
+
case "never": return false;
|
|
132
|
+
case "on-success": return result.exitCode === 0;
|
|
133
|
+
case "on-failure": return result.exitCode !== 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildOutputPreambleSectionFromResult(
|
|
138
|
+
label: "stdout" | "stderr",
|
|
139
|
+
value: string,
|
|
140
|
+
meta: { totalChars: number; totalLines: number },
|
|
141
|
+
maxChars: number,
|
|
142
|
+
): string[] {
|
|
143
|
+
const preview = buildTextPreview(label, value, meta.totalChars, maxChars);
|
|
144
|
+
return [
|
|
145
|
+
`[${label}]`,
|
|
146
|
+
`lineCount: ${meta.totalLines}`,
|
|
147
|
+
`charCount: ${meta.totalChars}`,
|
|
148
|
+
`truncated: ${preview.truncated ? "true" : "false"}`,
|
|
149
|
+
preview.truncated ? `omittedChars: ${preview.omittedChars}` : undefined,
|
|
150
|
+
"preview:",
|
|
151
|
+
preview.text || "(empty)",
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildDeterministicPreamble(
|
|
156
|
+
result: DeterministicExecutionResult,
|
|
157
|
+
options: DeterministicPreambleOptions = {},
|
|
158
|
+
): string {
|
|
159
|
+
const maxStdoutChars = options.maxStdoutChars ?? 8_000;
|
|
160
|
+
const maxStderrChars = options.maxStderrChars ?? 4_000;
|
|
161
|
+
const command = formatDeterministicExecution(result.execution, result.resolvedScriptPath);
|
|
162
|
+
return [
|
|
163
|
+
"[Deterministic step]",
|
|
164
|
+
`status: ${result.exitCode === 0 ? "succeeded" : "failed"}`,
|
|
165
|
+
`executionKind: ${result.execution.kind}`,
|
|
166
|
+
`command: ${command.includes("\n") ? JSON.stringify(command) : command}`,
|
|
167
|
+
result.resolvedScriptPath ? `resolvedScript: ${result.resolvedScriptPath}` : undefined,
|
|
168
|
+
`cwd: ${result.cwd}`,
|
|
169
|
+
`nonInteractive: ${result.nonInteractive ? "true" : "false"}`,
|
|
170
|
+
`exitCode: ${result.exitCode}`,
|
|
171
|
+
result.signal ? `signal: ${result.signal}` : undefined,
|
|
172
|
+
`durationMs: ${result.durationMs}`,
|
|
173
|
+
`timedOut: ${result.timedOut ? "true" : "false"}`,
|
|
174
|
+
"",
|
|
175
|
+
...buildOutputPreambleSectionFromResult("stdout", result.stdout, {
|
|
176
|
+
totalChars: result.stdoutTotalChars,
|
|
177
|
+
totalLines: result.stdoutTotalLines,
|
|
178
|
+
}, maxStdoutChars),
|
|
179
|
+
"",
|
|
180
|
+
...buildOutputPreambleSectionFromResult("stderr", result.stderr, {
|
|
181
|
+
totalChars: result.stderrTotalChars,
|
|
182
|
+
totalLines: result.stderrTotalLines,
|
|
183
|
+
}, maxStderrChars),
|
|
184
|
+
].filter((line): line is string => line !== undefined).join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveScriptPath(prompt: Pick<PromptWithModel, "filePath">, cwd: string, execution: Extract<DeterministicExecution, { kind: "script" }>): string {
|
|
188
|
+
if (isAbsolute(execution.path)) return execution.path;
|
|
189
|
+
const promptRelative = resolve(dirname(prompt.filePath), execution.path);
|
|
190
|
+
if (existsSync(promptRelative)) return promptRelative;
|
|
191
|
+
return resolve(cwd, execution.path);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildDeterministicEnv(step: Pick<DeterministicStep, "env" | "nonInteractive">): NodeJS.ProcessEnv {
|
|
195
|
+
const nonInteractiveDefaults: DeterministicEnv = step.nonInteractive
|
|
196
|
+
? {
|
|
197
|
+
CI: "1",
|
|
198
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
199
|
+
PAGER: "cat",
|
|
200
|
+
GIT_PAGER: "cat",
|
|
201
|
+
}
|
|
202
|
+
: {};
|
|
203
|
+
return {
|
|
204
|
+
...process.env,
|
|
205
|
+
...nonInteractiveDefaults,
|
|
206
|
+
...(step.env ?? {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function spawnProcess(command: string, args: string[], options: { cwd: string; shell?: boolean; env: NodeJS.ProcessEnv }) {
|
|
211
|
+
return spawn(command, args, {
|
|
212
|
+
cwd: options.cwd,
|
|
213
|
+
shell: options.shell ?? false,
|
|
214
|
+
env: options.env,
|
|
215
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function runDeterministicStep(
|
|
220
|
+
prompt: Pick<PromptWithModel, "filePath">,
|
|
221
|
+
step: DeterministicStep,
|
|
222
|
+
cwd: string,
|
|
223
|
+
): Promise<DeterministicExecutionResult> {
|
|
224
|
+
const startedAt = Date.now();
|
|
225
|
+
const execution = step.execution;
|
|
226
|
+
const resolvedCwd = step.cwd ?? cwd;
|
|
227
|
+
const env = buildDeterministicEnv(step);
|
|
228
|
+
const resolvedScriptPath = execution.kind === "script"
|
|
229
|
+
? resolveScriptPath(prompt, resolvedCwd, execution)
|
|
230
|
+
: undefined;
|
|
231
|
+
const child = execution.kind === "run"
|
|
232
|
+
? spawnProcess("/bin/bash", ["-lc", execution.command], { cwd: resolvedCwd, env })
|
|
233
|
+
: execution.kind === "command"
|
|
234
|
+
? spawnProcess(execution.command, execution.args, { cwd: resolvedCwd, shell: execution.shell, env })
|
|
235
|
+
: spawnProcess(resolvedScriptPath!, execution.args, { cwd: resolvedCwd, env });
|
|
236
|
+
|
|
237
|
+
const stdout = createCapturedOutput(DEFAULT_MAX_CAPTURE_STDOUT_CHARS);
|
|
238
|
+
const stderr = createCapturedOutput(DEFAULT_MAX_CAPTURE_STDERR_CHARS);
|
|
239
|
+
let timedOut = false;
|
|
240
|
+
let timeoutKillHandle: NodeJS.Timeout | undefined;
|
|
241
|
+
|
|
242
|
+
child.stdout.on("data", (chunk) => {
|
|
243
|
+
appendCapturedOutput(stdout, chunk.toString());
|
|
244
|
+
});
|
|
245
|
+
child.stderr.on("data", (chunk) => {
|
|
246
|
+
appendCapturedOutput(stderr, chunk.toString());
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const timeoutHandle = step.timeoutMs
|
|
250
|
+
? setTimeout(() => {
|
|
251
|
+
timedOut = true;
|
|
252
|
+
child.kill("SIGTERM");
|
|
253
|
+
timeoutKillHandle = setTimeout(() => {
|
|
254
|
+
child.kill("SIGKILL");
|
|
255
|
+
}, DEFAULT_TIMEOUT_KILL_AFTER_MS);
|
|
256
|
+
}, step.timeoutMs)
|
|
257
|
+
: undefined;
|
|
258
|
+
|
|
259
|
+
return await new Promise((resolveResult) => {
|
|
260
|
+
let settled = false;
|
|
261
|
+
child.on("error", (error) => {
|
|
262
|
+
if (settled) return;
|
|
263
|
+
settled = true;
|
|
264
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
265
|
+
if (timeoutKillHandle) clearTimeout(timeoutKillHandle);
|
|
266
|
+
resolveResult({
|
|
267
|
+
execution,
|
|
268
|
+
cwd: resolvedCwd,
|
|
269
|
+
nonInteractive: step.nonInteractive,
|
|
270
|
+
resolvedScriptPath,
|
|
271
|
+
exitCode: 1,
|
|
272
|
+
stdout: stdout.text,
|
|
273
|
+
stdoutTotalChars: stdout.totalChars,
|
|
274
|
+
stdoutTotalLines: capturedLineCount(stdout),
|
|
275
|
+
stdoutTruncated: stdout.truncated,
|
|
276
|
+
stderr: stderr.text ? `${stderr.text}\n${error.message}` : error.message,
|
|
277
|
+
stderrTotalChars: stderr.totalChars + (stderr.text ? error.message.length + 1 : error.message.length),
|
|
278
|
+
stderrTotalLines: countLines(stderr.text ? `${stderr.text}\n${error.message}` : error.message),
|
|
279
|
+
stderrTruncated: stderr.truncated,
|
|
280
|
+
durationMs: Date.now() - startedAt,
|
|
281
|
+
timedOut,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
child.on("close", (exitCode, signal) => {
|
|
285
|
+
if (settled) return;
|
|
286
|
+
settled = true;
|
|
287
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
288
|
+
if (timeoutKillHandle) clearTimeout(timeoutKillHandle);
|
|
289
|
+
resolveResult({
|
|
290
|
+
execution,
|
|
291
|
+
cwd: resolvedCwd,
|
|
292
|
+
nonInteractive: step.nonInteractive,
|
|
293
|
+
resolvedScriptPath,
|
|
294
|
+
exitCode: exitCode ?? (timedOut ? 124 : 1),
|
|
295
|
+
signal: signal ?? undefined,
|
|
296
|
+
stdout: stdout.text,
|
|
297
|
+
stdoutTotalChars: stdout.totalChars,
|
|
298
|
+
stdoutTotalLines: capturedLineCount(stdout),
|
|
299
|
+
stdoutTruncated: stdout.truncated,
|
|
300
|
+
stderr: stderr.text,
|
|
301
|
+
stderrTotalChars: stderr.totalChars,
|
|
302
|
+
stderrTotalLines: capturedLineCount(stderr),
|
|
303
|
+
stderrTruncated: stderr.truncated,
|
|
304
|
+
durationMs: Date.now() - startedAt,
|
|
305
|
+
timedOut,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
package/index.ts
CHANGED
|
@@ -34,6 +34,14 @@ import { createToolManager } from "./tool-manager.js";
|
|
|
34
34
|
import { executeSubagentPromptStep, type DelegatedPromptParallelResult } from "./subagent-step.js";
|
|
35
35
|
import { DEFAULT_SUBAGENT_NAME, PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.js";
|
|
36
36
|
import { renderDelegatedSubagentResult } from "./subagent-renderer.js";
|
|
37
|
+
import {
|
|
38
|
+
PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE,
|
|
39
|
+
PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE,
|
|
40
|
+
buildDeterministicPreamble,
|
|
41
|
+
runDeterministicStep,
|
|
42
|
+
shouldHandoffToLlm,
|
|
43
|
+
} from "./deterministic-step.js";
|
|
44
|
+
import { renderDeterministicCompletion, renderDeterministicResult } from "./deterministic-renderer.js";
|
|
37
45
|
|
|
38
46
|
interface LoopState {
|
|
39
47
|
currentIteration: number;
|
|
@@ -125,6 +133,8 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
125
133
|
|
|
126
134
|
pi.registerMessageRenderer<SkillLoadedDetails>("skill-loaded", renderSkillLoaded);
|
|
127
135
|
pi.registerMessageRenderer(PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE, renderDelegatedSubagentResult);
|
|
136
|
+
pi.registerMessageRenderer(PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE, renderDeterministicResult);
|
|
137
|
+
pi.registerMessageRenderer(PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE, renderDeterministicCompletion);
|
|
128
138
|
|
|
129
139
|
function registerPromptCommand(name: string) {
|
|
130
140
|
pi.registerCommand(name, {
|
|
@@ -248,6 +258,40 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
248
258
|
taskPreamble?: string,
|
|
249
259
|
loopContext?: string,
|
|
250
260
|
): Promise<PromptStepResult | "aborted"> {
|
|
261
|
+
let deterministicPreamble: string | undefined;
|
|
262
|
+
if (prompt.deterministic) {
|
|
263
|
+
try {
|
|
264
|
+
const deterministicResult = await runDeterministicStep(prompt, prompt.deterministic, ctx.cwd);
|
|
265
|
+
const deterministicPreambleText = buildDeterministicPreamble(deterministicResult);
|
|
266
|
+
pi.sendMessage({
|
|
267
|
+
customType: PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE,
|
|
268
|
+
content: deterministicPreambleText,
|
|
269
|
+
display: true,
|
|
270
|
+
details: deterministicResult,
|
|
271
|
+
});
|
|
272
|
+
if (!shouldHandoffToLlm(prompt.deterministic, deterministicResult)) {
|
|
273
|
+
pi.sendMessage({
|
|
274
|
+
customType: PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE,
|
|
275
|
+
content: `[Deterministic complete: ${prompt.name}]`,
|
|
276
|
+
display: true,
|
|
277
|
+
details: {
|
|
278
|
+
promptName: prompt.name,
|
|
279
|
+
exitCode: deterministicResult.exitCode,
|
|
280
|
+
timedOut: deterministicResult.timedOut,
|
|
281
|
+
status: deterministicResult.exitCode === 0 ? "succeeded" : "failed",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
return { changed: false };
|
|
285
|
+
}
|
|
286
|
+
deterministicPreamble = deterministicPreambleText;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
notify(ctx, `Deterministic step failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
289
|
+
return "aborted";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const combinedTaskPreamble = [taskPreamble, deterministicPreamble].filter(Boolean).join("\n\n");
|
|
294
|
+
|
|
251
295
|
if (shouldDelegatePrompt(prompt, override)) {
|
|
252
296
|
try {
|
|
253
297
|
const delegated =
|
|
@@ -259,7 +303,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
259
303
|
override,
|
|
260
304
|
signal: ctx.signal,
|
|
261
305
|
inheritedModel,
|
|
262
|
-
taskPreamble,
|
|
306
|
+
taskPreamble: combinedTaskPreamble || undefined,
|
|
263
307
|
parallel: Array.from({ length: prompt.parallel }, (_, index) => ({
|
|
264
308
|
prompt,
|
|
265
309
|
args,
|
|
@@ -276,7 +320,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
276
320
|
override,
|
|
277
321
|
signal: ctx.signal,
|
|
278
322
|
inheritedModel,
|
|
279
|
-
taskPreamble,
|
|
323
|
+
taskPreamble: combinedTaskPreamble || undefined,
|
|
280
324
|
});
|
|
281
325
|
if (!delegated) {
|
|
282
326
|
notify(ctx, `Prompt \`${prompt.name}\` is not configured for delegated execution.`, "error");
|
|
@@ -327,7 +371,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
327
371
|
pendingSkillMessage = skillResolution.kind === "ready" ? skillResolution.message : undefined;
|
|
328
372
|
|
|
329
373
|
const startId = ctx.sessionManager.getLeafId();
|
|
330
|
-
const
|
|
374
|
+
const effectiveContent = combinedTaskPreamble
|
|
375
|
+
? `${combinedTaskPreamble}\n\n${prepared.content}`
|
|
376
|
+
: prepared.content;
|
|
377
|
+
const content = loopContext ? `[${loopContext}]\n\n${effectiveContent}` : effectiveContent;
|
|
331
378
|
pi.sendUserMessage(content);
|
|
332
379
|
await waitForTurnStart(ctx);
|
|
333
380
|
await ctx.waitForIdle();
|
|
@@ -1393,6 +1440,16 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1393
1440
|
return;
|
|
1394
1441
|
}
|
|
1395
1442
|
const argsWithoutSubagent = subagent.args;
|
|
1443
|
+
if (prompt.deterministic) {
|
|
1444
|
+
if (subagent.override || subagent.fork) {
|
|
1445
|
+
notify(ctx, `Deterministic prompts do not support runtime --subagent/--fork in v1`, "error");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (extractLoopCount(argsWithoutSubagent)) {
|
|
1449
|
+
notify(ctx, `Deterministic prompts do not support runtime --loop in v1`, "error");
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1396
1453
|
|
|
1397
1454
|
const hasCompareLineup = prompt.workers !== undefined || prompt.reviewers !== undefined || prompt.finalApplier !== undefined;
|
|
1398
1455
|
if (hasCompareLineup) {
|
|
@@ -1482,7 +1539,14 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1482
1539
|
return;
|
|
1483
1540
|
}
|
|
1484
1541
|
|
|
1485
|
-
const effectivePrompt = {
|
|
1542
|
+
const effectivePrompt = {
|
|
1543
|
+
...prompt,
|
|
1544
|
+
...(runtimeCwd ? {
|
|
1545
|
+
cwd: runtimeCwd,
|
|
1546
|
+
...(prompt.deterministic ? { deterministic: { ...prompt.deterministic, cwd: runtimeCwd } } : {}),
|
|
1547
|
+
} : {}),
|
|
1548
|
+
...promptOverrides,
|
|
1549
|
+
};
|
|
1486
1550
|
const savedModel = getCurrentModel(ctx);
|
|
1487
1551
|
const savedThinking = pi.getThinkingLevel();
|
|
1488
1552
|
const stepResult = await executePromptStep(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-prompt-template-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Prompt template model selector extension for pi coding agent",
|
|
6
6
|
"author": "Nico Bailon",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"notifications.ts",
|
|
31
31
|
"prompt-execution.ts",
|
|
32
32
|
"prompt-loader.ts",
|
|
33
|
+
"deterministic-step.ts",
|
|
34
|
+
"deterministic-renderer.ts",
|
|
33
35
|
"subagent-renderer.ts",
|
|
34
36
|
"subagent-runtime.ts",
|
|
35
37
|
"subagent-step.ts",
|
package/prompt-loader.ts
CHANGED
|
@@ -41,6 +41,24 @@ export interface DelegationLineupSlot {
|
|
|
41
41
|
count?: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export type DeterministicHandoff = "always" | "never" | "on-success" | "on-failure";
|
|
45
|
+
|
|
46
|
+
export type DeterministicExecution =
|
|
47
|
+
| { kind: "run"; command: string }
|
|
48
|
+
| { kind: "command"; command: string; args: string[]; shell: boolean }
|
|
49
|
+
| { kind: "script"; path: string; args: string[] };
|
|
50
|
+
|
|
51
|
+
export type DeterministicEnv = Record<string, string>;
|
|
52
|
+
|
|
53
|
+
export interface DeterministicStep {
|
|
54
|
+
execution: DeterministicExecution;
|
|
55
|
+
handoff: DeterministicHandoff;
|
|
56
|
+
nonInteractive: boolean;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
cwd?: string;
|
|
59
|
+
env?: DeterministicEnv;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
export interface PromptWithModel {
|
|
45
63
|
name: string;
|
|
46
64
|
description: string;
|
|
@@ -58,6 +76,7 @@ export interface PromptWithModel {
|
|
|
58
76
|
converge?: boolean;
|
|
59
77
|
parallel?: number;
|
|
60
78
|
worktree?: boolean;
|
|
79
|
+
deterministic?: DeterministicStep;
|
|
61
80
|
subagent?: true | string;
|
|
62
81
|
inheritContext?: boolean;
|
|
63
82
|
cwd?: string;
|
|
@@ -348,6 +367,378 @@ function normalizeParallel(
|
|
|
348
367
|
return undefined;
|
|
349
368
|
}
|
|
350
369
|
|
|
370
|
+
function normalizeStringArrayField(
|
|
371
|
+
field: string,
|
|
372
|
+
value: unknown,
|
|
373
|
+
filePath: string,
|
|
374
|
+
source: PromptSource,
|
|
375
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
376
|
+
): string[] | undefined {
|
|
377
|
+
if (value === undefined) return [];
|
|
378
|
+
if (!Array.isArray(value)) {
|
|
379
|
+
diagnostics.push(
|
|
380
|
+
createDiagnostic(
|
|
381
|
+
`invalid-${field}`,
|
|
382
|
+
filePath,
|
|
383
|
+
source,
|
|
384
|
+
`Ignoring invalid ${field} value in ${filePath}: expected an array of strings.`,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const args: string[] = [];
|
|
391
|
+
for (const entry of value) {
|
|
392
|
+
if (typeof entry !== "string") {
|
|
393
|
+
diagnostics.push(
|
|
394
|
+
createDiagnostic(
|
|
395
|
+
`invalid-${field}`,
|
|
396
|
+
filePath,
|
|
397
|
+
source,
|
|
398
|
+
`Ignoring invalid ${field} value in ${filePath}: expected an array of strings.`,
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
args.push(entry);
|
|
404
|
+
}
|
|
405
|
+
return args;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeDeterministicHandoff(
|
|
409
|
+
value: unknown,
|
|
410
|
+
filePath: string,
|
|
411
|
+
source: PromptSource,
|
|
412
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
413
|
+
): DeterministicHandoff {
|
|
414
|
+
if (value === undefined) return "always";
|
|
415
|
+
if (typeof value === "string") {
|
|
416
|
+
const normalized = value.trim().toLowerCase();
|
|
417
|
+
if (normalized === "always" || normalized === "never" || normalized === "on-success" || normalized === "on-failure") {
|
|
418
|
+
return normalized;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
diagnostics.push(
|
|
423
|
+
createDiagnostic(
|
|
424
|
+
"invalid-deterministic-handoff",
|
|
425
|
+
filePath,
|
|
426
|
+
source,
|
|
427
|
+
`Using default deterministic handoff=always for ${filePath}: expected "always", "never", "on-success", or "on-failure".`,
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
return "always";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeTimeoutMs(
|
|
434
|
+
value: unknown,
|
|
435
|
+
filePath: string,
|
|
436
|
+
source: PromptSource,
|
|
437
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
438
|
+
): number | undefined {
|
|
439
|
+
if (value === undefined) return undefined;
|
|
440
|
+
let timeoutMs: number | undefined;
|
|
441
|
+
if (typeof value === "number") timeoutMs = value;
|
|
442
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) timeoutMs = parseInt(value.trim(), 10);
|
|
443
|
+
if (timeoutMs !== undefined && Number.isInteger(timeoutMs) && timeoutMs >= 1) return timeoutMs;
|
|
444
|
+
|
|
445
|
+
diagnostics.push(
|
|
446
|
+
createDiagnostic(
|
|
447
|
+
"invalid-deterministic-timeout",
|
|
448
|
+
filePath,
|
|
449
|
+
source,
|
|
450
|
+
`Ignoring invalid deterministic timeout in ${filePath}: expected an integer greater than or equal to 1 (milliseconds).`,
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeDeterministicEnv(
|
|
457
|
+
value: unknown,
|
|
458
|
+
filePath: string,
|
|
459
|
+
source: PromptSource,
|
|
460
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
461
|
+
): DeterministicEnv | undefined {
|
|
462
|
+
if (value === undefined) return undefined;
|
|
463
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
464
|
+
diagnostics.push(
|
|
465
|
+
createDiagnostic(
|
|
466
|
+
"invalid-deterministic-env",
|
|
467
|
+
filePath,
|
|
468
|
+
source,
|
|
469
|
+
`Ignoring invalid deterministic env in ${filePath}: expected an object with string/number/boolean values.`,
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const env: DeterministicEnv = {};
|
|
476
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
477
|
+
if (!key.trim()) {
|
|
478
|
+
diagnostics.push(
|
|
479
|
+
createDiagnostic(
|
|
480
|
+
"invalid-deterministic-env",
|
|
481
|
+
filePath,
|
|
482
|
+
source,
|
|
483
|
+
`Ignoring invalid deterministic env in ${filePath}: env keys must be non-empty strings.`,
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
if (typeof raw !== "string" && typeof raw !== "number" && typeof raw !== "boolean") {
|
|
489
|
+
diagnostics.push(
|
|
490
|
+
createDiagnostic(
|
|
491
|
+
"invalid-deterministic-env",
|
|
492
|
+
filePath,
|
|
493
|
+
source,
|
|
494
|
+
`Ignoring invalid deterministic env in ${filePath}: env value for ${JSON.stringify(key)} must be a string, number, or boolean.`,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
env[key] = String(raw);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeDeterministicNonInteractive(
|
|
506
|
+
value: unknown,
|
|
507
|
+
filePath: string,
|
|
508
|
+
source: PromptSource,
|
|
509
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
510
|
+
): boolean {
|
|
511
|
+
if (value === undefined) return true;
|
|
512
|
+
if (typeof value === "boolean") return value;
|
|
513
|
+
if (typeof value === "string") {
|
|
514
|
+
const normalized = value.trim().toLowerCase();
|
|
515
|
+
if (normalized === "true") return true;
|
|
516
|
+
if (normalized === "false") return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
diagnostics.push(
|
|
520
|
+
createDiagnostic(
|
|
521
|
+
"invalid-deterministic-non-interactive",
|
|
522
|
+
filePath,
|
|
523
|
+
source,
|
|
524
|
+
`Using default deterministic nonInteractive=true for ${filePath}: expected true or false.`,
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeDeterministicRunValue(
|
|
531
|
+
value: unknown,
|
|
532
|
+
filePath: string,
|
|
533
|
+
source: PromptSource,
|
|
534
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
535
|
+
): DeterministicExecution | undefined {
|
|
536
|
+
if (typeof value === "string") {
|
|
537
|
+
const command = value.trim();
|
|
538
|
+
if (command) return { kind: "run", command };
|
|
539
|
+
diagnostics.push(
|
|
540
|
+
createDiagnostic(
|
|
541
|
+
"invalid-deterministic-run",
|
|
542
|
+
filePath,
|
|
543
|
+
source,
|
|
544
|
+
`Ignoring invalid deterministic run value in ${filePath}: expected a non-empty string or an object with command/args.`,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
550
|
+
diagnostics.push(
|
|
551
|
+
createDiagnostic(
|
|
552
|
+
"invalid-deterministic-run",
|
|
553
|
+
filePath,
|
|
554
|
+
source,
|
|
555
|
+
`Ignoring invalid deterministic run value in ${filePath}: expected a non-empty string or an object with command/args.`,
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const record = value as Record<string, unknown>;
|
|
562
|
+
const command = normalizeStringField("deterministic.run.command", record.command, filePath, source, diagnostics);
|
|
563
|
+
if (!command) {
|
|
564
|
+
diagnostics.push(
|
|
565
|
+
createDiagnostic(
|
|
566
|
+
"invalid-deterministic-run",
|
|
567
|
+
filePath,
|
|
568
|
+
source,
|
|
569
|
+
`Ignoring invalid deterministic run value in ${filePath}: expected object field "command" to be a non-empty string.`,
|
|
570
|
+
),
|
|
571
|
+
);
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
const args = normalizeStringArrayField("deterministic.run.args", record.args, filePath, source, diagnostics);
|
|
575
|
+
if (!args) return undefined;
|
|
576
|
+
let shell = false;
|
|
577
|
+
if (record.shell !== undefined) {
|
|
578
|
+
if (typeof record.shell === "boolean") {
|
|
579
|
+
shell = record.shell;
|
|
580
|
+
} else {
|
|
581
|
+
diagnostics.push(
|
|
582
|
+
createDiagnostic(
|
|
583
|
+
"invalid-deterministic-run",
|
|
584
|
+
filePath,
|
|
585
|
+
source,
|
|
586
|
+
`Ignoring invalid deterministic run value in ${filePath}: object field "shell" must be true or false.`,
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { kind: "command", command, args, shell };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizeDeterministicScriptValue(
|
|
596
|
+
value: unknown,
|
|
597
|
+
filePath: string,
|
|
598
|
+
source: PromptSource,
|
|
599
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
600
|
+
): DeterministicExecution | undefined {
|
|
601
|
+
if (typeof value === "string") {
|
|
602
|
+
const path = value.trim();
|
|
603
|
+
if (path) return { kind: "script", path, args: [] };
|
|
604
|
+
diagnostics.push(
|
|
605
|
+
createDiagnostic(
|
|
606
|
+
"invalid-deterministic-script",
|
|
607
|
+
filePath,
|
|
608
|
+
source,
|
|
609
|
+
`Ignoring invalid deterministic script value in ${filePath}: expected a non-empty string or an object with path/args.`,
|
|
610
|
+
),
|
|
611
|
+
);
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
615
|
+
diagnostics.push(
|
|
616
|
+
createDiagnostic(
|
|
617
|
+
"invalid-deterministic-script",
|
|
618
|
+
filePath,
|
|
619
|
+
source,
|
|
620
|
+
`Ignoring invalid deterministic script value in ${filePath}: expected a non-empty string or an object with path/args.`,
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const record = value as Record<string, unknown>;
|
|
627
|
+
const path = normalizeStringField("deterministic.script.path", record.path, filePath, source, diagnostics);
|
|
628
|
+
if (!path) {
|
|
629
|
+
diagnostics.push(
|
|
630
|
+
createDiagnostic(
|
|
631
|
+
"invalid-deterministic-script",
|
|
632
|
+
filePath,
|
|
633
|
+
source,
|
|
634
|
+
`Ignoring invalid deterministic script value in ${filePath}: expected object field "path" to be a non-empty string.`,
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
const args = normalizeStringArrayField("deterministic.script.args", record.args, filePath, source, diagnostics);
|
|
640
|
+
if (!args) return undefined;
|
|
641
|
+
return { kind: "script", path, args };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function normalizeDeterministic(
|
|
645
|
+
frontmatter: Record<string, unknown>,
|
|
646
|
+
filePath: string,
|
|
647
|
+
source: PromptSource,
|
|
648
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
649
|
+
): DeterministicStep | undefined {
|
|
650
|
+
const hasNested = Object.hasOwn(frontmatter, "deterministic");
|
|
651
|
+
const hasRun = Object.hasOwn(frontmatter, "run");
|
|
652
|
+
const hasScript = Object.hasOwn(frontmatter, "script");
|
|
653
|
+
const hasHandoff = Object.hasOwn(frontmatter, "handoff");
|
|
654
|
+
const hasTimeout = Object.hasOwn(frontmatter, "timeout");
|
|
655
|
+
const hasEnv = Object.hasOwn(frontmatter, "env");
|
|
656
|
+
const hasNonInteractive = Object.hasOwn(frontmatter, "nonInteractive");
|
|
657
|
+
if (!hasNested && !hasRun && !hasScript && !hasHandoff && !hasTimeout && !hasEnv && !hasNonInteractive) return undefined;
|
|
658
|
+
|
|
659
|
+
if (hasNested && (hasRun || hasScript || hasHandoff || hasTimeout || hasEnv || hasNonInteractive)) {
|
|
660
|
+
diagnostics.push(
|
|
661
|
+
createDiagnostic(
|
|
662
|
+
"invalid-deterministic-mixed-shorthand",
|
|
663
|
+
filePath,
|
|
664
|
+
source,
|
|
665
|
+
`Ignoring top-level deterministic shorthand in ${filePath}: use either "deterministic" or top-level run/script/handoff/timeout/env/nonInteractive, not both.`,
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
let record: Record<string, unknown>;
|
|
671
|
+
if (hasNested) {
|
|
672
|
+
const raw = frontmatter.deterministic;
|
|
673
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
674
|
+
diagnostics.push(
|
|
675
|
+
createDiagnostic(
|
|
676
|
+
"invalid-deterministic",
|
|
677
|
+
filePath,
|
|
678
|
+
source,
|
|
679
|
+
`Ignoring invalid deterministic config in ${filePath}: frontmatter field "deterministic" must be an object.`,
|
|
680
|
+
),
|
|
681
|
+
);
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
record = raw as Record<string, unknown>;
|
|
685
|
+
} else {
|
|
686
|
+
record = {
|
|
687
|
+
run: frontmatter.run,
|
|
688
|
+
script: frontmatter.script,
|
|
689
|
+
handoff: frontmatter.handoff,
|
|
690
|
+
timeout: frontmatter.timeout,
|
|
691
|
+
env: frontmatter.env,
|
|
692
|
+
nonInteractive: frontmatter.nonInteractive,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const runValue = Object.hasOwn(record, "run") ? record.run : undefined;
|
|
697
|
+
const scriptValue = Object.hasOwn(record, "script") ? record.script : undefined;
|
|
698
|
+
if (runValue !== undefined && scriptValue !== undefined) {
|
|
699
|
+
diagnostics.push(
|
|
700
|
+
createDiagnostic(
|
|
701
|
+
"invalid-deterministic",
|
|
702
|
+
filePath,
|
|
703
|
+
source,
|
|
704
|
+
`Ignoring deterministic config in ${filePath}: "run" and "script" cannot be declared together.`,
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const execution = runValue !== undefined
|
|
711
|
+
? normalizeDeterministicRunValue(runValue, filePath, source, diagnostics)
|
|
712
|
+
: scriptValue !== undefined
|
|
713
|
+
? normalizeDeterministicScriptValue(scriptValue, filePath, source, diagnostics)
|
|
714
|
+
: undefined;
|
|
715
|
+
if (!execution) {
|
|
716
|
+
diagnostics.push(
|
|
717
|
+
createDiagnostic(
|
|
718
|
+
"invalid-deterministic",
|
|
719
|
+
filePath,
|
|
720
|
+
source,
|
|
721
|
+
`Ignoring deterministic config in ${filePath}: expected either "run" or "script".`,
|
|
722
|
+
),
|
|
723
|
+
);
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const handoff = normalizeDeterministicHandoff(record.handoff, filePath, source, diagnostics);
|
|
728
|
+
const timeoutMs = normalizeTimeoutMs(record.timeout, filePath, source, diagnostics);
|
|
729
|
+
const cwd = normalizeCwd(record.cwd, filePath, source, diagnostics);
|
|
730
|
+
const env = normalizeDeterministicEnv(record.env, filePath, source, diagnostics);
|
|
731
|
+
const nonInteractive = normalizeDeterministicNonInteractive(record.nonInteractive, filePath, source, diagnostics);
|
|
732
|
+
return {
|
|
733
|
+
execution,
|
|
734
|
+
handoff,
|
|
735
|
+
nonInteractive,
|
|
736
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
737
|
+
...(cwd ? { cwd } : {}),
|
|
738
|
+
...(env ? { env } : {}),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
351
742
|
function normalizeLineupSlot(
|
|
352
743
|
value: unknown,
|
|
353
744
|
field: "workers" | "reviewers" | "finalApplier",
|
|
@@ -1033,6 +1424,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1033
1424
|
const parallel = normalizeParallel(frontmatter.parallel, fullPath, source, diagnostics);
|
|
1034
1425
|
const hasBestOfN = Object.hasOwn(frontmatter, "bestOfN");
|
|
1035
1426
|
const bestOfN = normalizeBestOfN(frontmatter.bestOfN, fullPath, source, diagnostics);
|
|
1427
|
+
let deterministic = normalizeDeterministic(frontmatter, fullPath, source, diagnostics);
|
|
1036
1428
|
const hasLegacyWorkers = Object.hasOwn(frontmatter, "workers");
|
|
1037
1429
|
const hasLegacyReviewers = Object.hasOwn(frontmatter, "reviewers");
|
|
1038
1430
|
const hasLegacyFinalApplier = Object.hasOwn(frontmatter, "finalApplier");
|
|
@@ -1073,6 +1465,17 @@ function loadPromptsWithModelFromDir(
|
|
|
1073
1465
|
);
|
|
1074
1466
|
subagent = undefined;
|
|
1075
1467
|
}
|
|
1468
|
+
if (chain && deterministic !== undefined) {
|
|
1469
|
+
diagnostics.push(
|
|
1470
|
+
createDiagnostic(
|
|
1471
|
+
"invalid-deterministic-chain",
|
|
1472
|
+
fullPath,
|
|
1473
|
+
source,
|
|
1474
|
+
`Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "chain".`,
|
|
1475
|
+
),
|
|
1476
|
+
);
|
|
1477
|
+
deterministic = undefined;
|
|
1478
|
+
}
|
|
1076
1479
|
if (chain && (safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined)) {
|
|
1077
1480
|
diagnostics.push(
|
|
1078
1481
|
createDiagnostic(
|
|
@@ -1099,6 +1502,17 @@ function loadPromptsWithModelFromDir(
|
|
|
1099
1502
|
safeReviewers = undefined;
|
|
1100
1503
|
safeFinalApplier = undefined;
|
|
1101
1504
|
}
|
|
1505
|
+
if (subagent !== undefined && deterministic !== undefined) {
|
|
1506
|
+
diagnostics.push(
|
|
1507
|
+
createDiagnostic(
|
|
1508
|
+
"invalid-deterministic-subagent",
|
|
1509
|
+
fullPath,
|
|
1510
|
+
source,
|
|
1511
|
+
`Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "subagent".`,
|
|
1512
|
+
),
|
|
1513
|
+
);
|
|
1514
|
+
deterministic = undefined;
|
|
1515
|
+
}
|
|
1102
1516
|
if (subagent === undefined && inheritContext) {
|
|
1103
1517
|
diagnostics.push(
|
|
1104
1518
|
createDiagnostic(
|
|
@@ -1145,6 +1559,17 @@ function loadPromptsWithModelFromDir(
|
|
|
1145
1559
|
safeReviewers = undefined;
|
|
1146
1560
|
safeFinalApplier = undefined;
|
|
1147
1561
|
}
|
|
1562
|
+
if (safeParallel !== undefined && deterministic !== undefined) {
|
|
1563
|
+
diagnostics.push(
|
|
1564
|
+
createDiagnostic(
|
|
1565
|
+
"invalid-deterministic-parallel",
|
|
1566
|
+
fullPath,
|
|
1567
|
+
source,
|
|
1568
|
+
`Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "parallel".`,
|
|
1569
|
+
),
|
|
1570
|
+
);
|
|
1571
|
+
deterministic = undefined;
|
|
1572
|
+
}
|
|
1148
1573
|
const hasLineup = safeWorkers !== undefined || safeReviewers !== undefined || safeFinalApplier !== undefined;
|
|
1149
1574
|
if (!hasBestOfN && hasLegacyCompareFields) {
|
|
1150
1575
|
diagnostics.push(
|
|
@@ -1169,14 +1594,18 @@ function loadPromptsWithModelFromDir(
|
|
|
1169
1594
|
continue;
|
|
1170
1595
|
}
|
|
1171
1596
|
if (!chain && subagent === undefined && !hasLineup && cwd) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1597
|
+
if (deterministic) {
|
|
1598
|
+
deterministic = { ...deterministic, ...(deterministic.cwd ? {} : { cwd }) };
|
|
1599
|
+
} else {
|
|
1600
|
+
diagnostics.push(
|
|
1601
|
+
createDiagnostic(
|
|
1602
|
+
"invalid-cwd",
|
|
1603
|
+
fullPath,
|
|
1604
|
+
source,
|
|
1605
|
+
`Ignoring cwd in ${fullPath}: frontmatter field "cwd" requires "subagent", "chain", or compare lineups ("workers"/"reviewers"/"finalApplier").`,
|
|
1606
|
+
),
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1180
1609
|
}
|
|
1181
1610
|
const hasModelField = Object.hasOwn(frontmatter, "model");
|
|
1182
1611
|
const parsedModels = chain ? [] : normalizeModelSpecs(frontmatter.model, fullPath, source, diagnostics);
|
|
@@ -1214,6 +1643,17 @@ function loadPromptsWithModelFromDir(
|
|
|
1214
1643
|
const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics);
|
|
1215
1644
|
const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics);
|
|
1216
1645
|
const converge = normalizeConverge(frontmatter.converge, fullPath, source, diagnostics);
|
|
1646
|
+
if (loop !== undefined && deterministic !== undefined) {
|
|
1647
|
+
diagnostics.push(
|
|
1648
|
+
createDiagnostic(
|
|
1649
|
+
"invalid-deterministic-loop",
|
|
1650
|
+
fullPath,
|
|
1651
|
+
source,
|
|
1652
|
+
`Ignoring deterministic config in ${fullPath}: frontmatter field "deterministic" cannot be combined with "loop" in v1.`,
|
|
1653
|
+
),
|
|
1654
|
+
);
|
|
1655
|
+
deterministic = undefined;
|
|
1656
|
+
}
|
|
1217
1657
|
const worktreeInput = hasBestOfN ? bestOfN?.worktree : frontmatter.worktree;
|
|
1218
1658
|
const worktree = normalizeWorktree(worktreeInput, fullPath, source, diagnostics);
|
|
1219
1659
|
let safeWorktree: boolean | undefined;
|
|
@@ -1256,6 +1696,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1256
1696
|
loop !== undefined ||
|
|
1257
1697
|
converge === false ||
|
|
1258
1698
|
safeParallel !== undefined ||
|
|
1699
|
+
deterministic !== undefined ||
|
|
1259
1700
|
hasLineup ||
|
|
1260
1701
|
safeWorktree === true ||
|
|
1261
1702
|
subagent !== undefined ||
|
|
@@ -1282,6 +1723,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1282
1723
|
converge: converge === false ? false : undefined,
|
|
1283
1724
|
parallel: safeParallel,
|
|
1284
1725
|
worktree: safeWorktree,
|
|
1726
|
+
deterministic,
|
|
1285
1727
|
subagent,
|
|
1286
1728
|
inheritContext: safeInheritContext || undefined,
|
|
1287
1729
|
cwd: safeCwd || undefined,
|
|
@@ -1381,6 +1823,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
1381
1823
|
const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : "";
|
|
1382
1824
|
const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : "";
|
|
1383
1825
|
const parallelLabel = prompt.parallel !== undefined ? ` parallel:${prompt.parallel}` : "";
|
|
1826
|
+
const deterministicLabel = prompt.deterministic ? ` deterministic-step:${prompt.deterministic.handoff}` : "";
|
|
1384
1827
|
const workersLabel = prompt.workers ? ` workers:${effectiveLineupCount(prompt.workers)}` : "";
|
|
1385
1828
|
const reviewersLabel = prompt.reviewers ? ` reviewers:${effectiveLineupCount(prompt.reviewers)}` : "";
|
|
1386
1829
|
const finalApplierLabel = prompt.finalApplier ? " final-applier" : "";
|
|
@@ -1388,7 +1831,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
1388
1831
|
const inheritContextLabel = prompt.inheritContext ? " fork" : "";
|
|
1389
1832
|
const worktreeLabel = prompt.worktree ? " worktree" : "";
|
|
1390
1833
|
const details =
|
|
1391
|
-
`[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${parallelLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
|
|
1834
|
+
`[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
|
|
1392
1835
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
1393
1836
|
}
|
|
1394
1837
|
|