@bastani/atomic 0.8.20 → 0.8.21-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 +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
- package/dist/builtin/subagents/agents/debugger.md +4 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +19 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
- package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +67 -3
- package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
- package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
- package/dist/builtin/workflows/src/shared/store.ts +37 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
- package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +2 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -29
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/docs/quickstart.md +1 -2
- package/package.json +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
|
@@ -27,7 +27,7 @@ const HARNESS_DIRS = [
|
|
|
27
27
|
|
|
28
28
|
// Valid sub-command names
|
|
29
29
|
const VALID_COMMANDS = [
|
|
30
|
-
'craft', '
|
|
30
|
+
'craft', 'init', 'extract', 'document', 'shape',
|
|
31
31
|
'critique', 'audit',
|
|
32
32
|
'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
|
|
33
33
|
'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
|
|
@@ -11,6 +11,12 @@ import { renderRunBanner, renderRunSummary } from "./renderers.js";
|
|
|
11
11
|
import type { RunEndPayload, RunStartPayload } from "./renderers.js";
|
|
12
12
|
import type { StageSnapshot, StageStatus, ToolEvent } from "../shared/store-types.js";
|
|
13
13
|
import { store } from "../shared/store.js";
|
|
14
|
+
import { stageUiBroker } from "../shared/stage-ui-broker.js";
|
|
15
|
+
import {
|
|
16
|
+
coerceStageInputAnswer,
|
|
17
|
+
hasStageInputAnswerContent,
|
|
18
|
+
type StageInputAnswer,
|
|
19
|
+
} from "../shared/stage-prompt.js";
|
|
14
20
|
import { restoreOnSessionStart } from "../shared/persistence-restore.js";
|
|
15
21
|
import type { SessionManager } from "../shared/persistence-restore.js";
|
|
16
22
|
import { installCompactionHook } from "../shared/persistence-compaction-policy.js";
|
|
@@ -646,6 +652,10 @@ function renderStagesToolContent(
|
|
|
646
652
|
lines.push("pendingPrompt:");
|
|
647
653
|
lines.push(JSON.stringify(stage.pendingPrompt, null, 2));
|
|
648
654
|
}
|
|
655
|
+
if (stage.inputRequest !== undefined) {
|
|
656
|
+
lines.push("inputRequest:");
|
|
657
|
+
lines.push(JSON.stringify(stage.inputRequest, null, 2));
|
|
658
|
+
}
|
|
649
659
|
});
|
|
650
660
|
return lines.join("\n");
|
|
651
661
|
}
|
|
@@ -745,6 +755,7 @@ type WorkflowStageSummary = {
|
|
|
745
755
|
error?: string;
|
|
746
756
|
awaitingInputSince?: number;
|
|
747
757
|
pendingPrompt?: StageSnapshot["pendingPrompt"];
|
|
758
|
+
inputRequest?: StageSnapshot["inputRequest"];
|
|
748
759
|
};
|
|
749
760
|
|
|
750
761
|
type WorkflowTranscriptEntry = {
|
|
@@ -781,6 +792,9 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
|
|
|
781
792
|
pendingPrompt: stage.pendingPrompt === undefined
|
|
782
793
|
? undefined
|
|
783
794
|
: structuredClone(stage.pendingPrompt),
|
|
795
|
+
inputRequest: stage.inputRequest === undefined
|
|
796
|
+
? undefined
|
|
797
|
+
: structuredClone(stage.inputRequest),
|
|
784
798
|
};
|
|
785
799
|
}
|
|
786
800
|
|
|
@@ -860,6 +874,23 @@ function promptPayloadFromArgs(args: WorkflowToolArgs): unknown {
|
|
|
860
874
|
return args.message;
|
|
861
875
|
}
|
|
862
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Shape a `send` payload into a headless answer for a brokered stage prompt
|
|
879
|
+
* (ask_user_question / readiness gate). A structured `response` (object or
|
|
880
|
+
* JSON string) is normalized so it matches the question's options instead of
|
|
881
|
+
* being forwarded verbatim as a result that violates the QuestionnaireResult
|
|
882
|
+
* contract; otherwise the plain text / message payload is matched against
|
|
883
|
+
* option labels / indices by the stage prompt adapter.
|
|
884
|
+
*/
|
|
885
|
+
function brokerAnswerFromArgs(args: WorkflowToolArgs): StageInputAnswer {
|
|
886
|
+
if (args.response !== undefined) {
|
|
887
|
+
const coerced = coerceStageInputAnswer(args.response);
|
|
888
|
+
if (hasStageInputAnswerContent(coerced)) return coerced;
|
|
889
|
+
}
|
|
890
|
+
const text = textPayloadFromArgs(args);
|
|
891
|
+
return text !== undefined ? { text } : {};
|
|
892
|
+
}
|
|
893
|
+
|
|
863
894
|
function textPayloadFromArgs(args: WorkflowToolArgs): string | undefined {
|
|
864
895
|
if (args.text !== undefined) return args.text;
|
|
865
896
|
if (typeof args.response === "string") {
|
|
@@ -1289,6 +1320,30 @@ export function makeExecuteWorkflowTool(
|
|
|
1289
1320
|
}
|
|
1290
1321
|
const run = store.runs().find((r) => r.id === target.runId);
|
|
1291
1322
|
const snapshot = run?.stages.find((s) => s.id === stage.stageId);
|
|
1323
|
+
// Brokered structured prompts (in-stage ask_user_question / readiness
|
|
1324
|
+
// gate) resolve through StageUiBroker rather than store.pendingPrompt.
|
|
1325
|
+
// Answer those first when one is pending and the promptId (if any) lines
|
|
1326
|
+
// up — otherwise fall through to the store-prompt / live-handle paths.
|
|
1327
|
+
const brokerPrompt = stageUiBroker.peekStagePrompt(target.runId, stage.stageId);
|
|
1328
|
+
const targetsBrokerPrompt =
|
|
1329
|
+
brokerPrompt !== undefined &&
|
|
1330
|
+
(args.promptId === undefined || args.promptId === brokerPrompt.id) &&
|
|
1331
|
+
(requestedDelivery === "answer" ||
|
|
1332
|
+
args.promptId !== undefined ||
|
|
1333
|
+
requestedDelivery === "auto");
|
|
1334
|
+
if (targetsBrokerPrompt && brokerPrompt !== undefined) {
|
|
1335
|
+
if (!hasPayloadProperty(args)) {
|
|
1336
|
+
return workflowSendResult(target.runId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
|
|
1337
|
+
}
|
|
1338
|
+
const ok = stageUiBroker.answerStagePrompt(target.runId, stage.stageId, brokerAnswerFromArgs(args));
|
|
1339
|
+
return workflowSendResult(
|
|
1340
|
+
target.runId,
|
|
1341
|
+
stage.stageId,
|
|
1342
|
+
"answer",
|
|
1343
|
+
ok ? "ok" : "noop",
|
|
1344
|
+
ok ? `Answered input request ${brokerPrompt.id}.` : `No matching pending input request ${brokerPrompt.id}.`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1292
1347
|
const targetsPrompt =
|
|
1293
1348
|
requestedDelivery === "answer" ||
|
|
1294
1349
|
args.promptId !== undefined ||
|
|
@@ -2284,14 +2339,23 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2284
2339
|
},
|
|
2285
2340
|
renderCall: (args, _theme, _context) =>
|
|
2286
2341
|
dynamicTextRenderComponent((width) => renderCall(args, { width })),
|
|
2287
|
-
renderResult: (result, opts, _theme, context) =>
|
|
2288
|
-
|
|
2342
|
+
renderResult: (result, opts, _theme, context) => {
|
|
2343
|
+
// Capture wall-clock ONCE per chat entry. The lambda below is
|
|
2344
|
+
// invoked on every TUI re-render; without a captured `now`, every
|
|
2345
|
+
// tick would recompute elapsed/running durations and trigger
|
|
2346
|
+
// pi-tui's full-redraw path for any entry above the viewport —
|
|
2347
|
+
// visible as whole-screen flicker on terminals without
|
|
2348
|
+
// synchronized output support (e.g. mosh).
|
|
2349
|
+
const capturedNow = Date.now();
|
|
2350
|
+
return dynamicTextRenderComponent((width) =>
|
|
2289
2351
|
renderResult(result.details, {
|
|
2290
2352
|
...opts,
|
|
2291
2353
|
width,
|
|
2354
|
+
now: capturedNow,
|
|
2292
2355
|
runInputs: (context as { args?: WorkflowToolArgs }).args?.inputs,
|
|
2293
2356
|
}),
|
|
2294
|
-
)
|
|
2357
|
+
);
|
|
2358
|
+
},
|
|
2295
2359
|
});
|
|
2296
2360
|
}
|
|
2297
2361
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - pi-subagents src/extension/index.ts renderResult slot
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { PendingPrompt, RunSnapshot, StageSnapshot, StageStatus } from "../shared/store-types.js";
|
|
14
|
+
import type { PendingPrompt, RunSnapshot, StageInputRequest, StageSnapshot, StageStatus } from "../shared/store-types.js";
|
|
15
15
|
import type { WorkflowDetails } from "../shared/types.js";
|
|
16
16
|
import type { RunDetail } from "../runs/background/status.js";
|
|
17
17
|
import { renderInputsSchema } from "../shared/render-inputs-schema.js";
|
|
@@ -107,6 +107,7 @@ type StageListItem = {
|
|
|
107
107
|
error?: string;
|
|
108
108
|
awaitingInputSince?: number;
|
|
109
109
|
pendingPrompt?: PendingPrompt;
|
|
110
|
+
inputRequest?: StageInputRequest;
|
|
110
111
|
};
|
|
111
112
|
type StageListResult = { action: "stages"; runId: string; filter: string; stages: StageListItem[]; error?: string };
|
|
112
113
|
type StageDetailResult = { action: "stage"; runId: string; stage?: StageSnapshot; error?: string };
|
|
@@ -161,6 +162,15 @@ export interface RenderResultOpts {
|
|
|
161
162
|
* When false/undefined the canonical Catppuccin chrome is rendered.
|
|
162
163
|
*/
|
|
163
164
|
plain?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Stable wall-clock used for elapsed-time labels in scrollback. Capture
|
|
167
|
+
* once when the chat entry is created so subsequent host re-renders do
|
|
168
|
+
* not silently tick `elapsed` / `running` durations — every off-viewport
|
|
169
|
+
* tick forces pi-tui's full-redraw path (CSI 2J + CSI H + CSI 3J) and
|
|
170
|
+
* is visible as whole-screen flicker under terminal emulators that do
|
|
171
|
+
* not implement synchronized output (e.g. mosh).
|
|
172
|
+
*/
|
|
173
|
+
now?: number;
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
/**
|
|
@@ -213,6 +223,19 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
|
|
|
213
223
|
const partial = opts?.isPartial === true;
|
|
214
224
|
const themed = opts?.plain !== true;
|
|
215
225
|
|
|
226
|
+
// Runtime guard. The tool-result renderer passes `result.details`, which can
|
|
227
|
+
// be absent or not yet shaped during streaming/partial renders or on error
|
|
228
|
+
// paths that return content without a structured payload. Dereferencing a
|
|
229
|
+
// missing `action` here previously threw and crashed the TUI render loop, so
|
|
230
|
+
// degrade gracefully instead.
|
|
231
|
+
if (
|
|
232
|
+
result === null ||
|
|
233
|
+
typeof result !== "object" ||
|
|
234
|
+
typeof (result as { action?: unknown }).action !== "string"
|
|
235
|
+
) {
|
|
236
|
+
return partial ? "" : renderNotice("WORKFLOW", "no result", opts, themed);
|
|
237
|
+
}
|
|
238
|
+
|
|
216
239
|
switch (result.action) {
|
|
217
240
|
case "list": {
|
|
218
241
|
const r = result as ListResult;
|
|
@@ -227,6 +250,7 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
|
|
|
227
250
|
return renderStatusList(r.snapshots, {
|
|
228
251
|
theme: themed ? deriveGraphTheme({}) : undefined,
|
|
229
252
|
width: opts?.width,
|
|
253
|
+
now: opts?.now,
|
|
230
254
|
});
|
|
231
255
|
}
|
|
232
256
|
|
|
@@ -239,6 +263,7 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
|
|
|
239
263
|
return renderRunDetail(r.detail, {
|
|
240
264
|
theme: themed ? deriveGraphTheme({}) : undefined,
|
|
241
265
|
width: opts?.width,
|
|
266
|
+
now: opts?.now,
|
|
242
267
|
});
|
|
243
268
|
}
|
|
244
269
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
-
import { CONFIG_DIR_NAME } from "@bastani/atomic";
|
|
8
|
+
import { CONFIG_DIR_NAME, createAskUserQuestionToolDefinition } from "@bastani/atomic";
|
|
9
|
+
import { stageUiBroker } from "../../shared/stage-ui-broker.js";
|
|
10
|
+
import { buildStagePromptAdapter } from "../../shared/stage-prompt.js";
|
|
9
11
|
import type {
|
|
10
12
|
WorkflowDefinition,
|
|
11
13
|
WorkflowRunContext,
|
|
@@ -89,6 +91,21 @@ export interface RunOpts {
|
|
|
89
91
|
ui?: WorkflowUIAdapter;
|
|
90
92
|
/** Internal detached-run mode: surface ctx.ui.* as node-local workflow prompt stages. */
|
|
91
93
|
usePromptNodesForUi?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Readiness-gate confirmation seam (#1099). When an ask_user_question tool
|
|
96
|
+
* call is observed during a stage, the executor calls this after the model
|
|
97
|
+
* turn ends to ask whether to advance. Returning false keeps execution in the
|
|
98
|
+
* stage (the executor steers the stage to continue and re-gates after the
|
|
99
|
+
* next turn); true advances. When omitted, runs with usePromptNodesForUi
|
|
100
|
+
* render the gate through the stage UI broker, and other runs proceed without
|
|
101
|
+
* gating (tests/headless).
|
|
102
|
+
*/
|
|
103
|
+
confirmStageReadiness?: (request: {
|
|
104
|
+
readonly runId: string;
|
|
105
|
+
readonly stageId: string;
|
|
106
|
+
readonly stageName: string;
|
|
107
|
+
readonly signal: AbortSignal;
|
|
108
|
+
}) => Promise<boolean>;
|
|
92
109
|
/** Store override (for testing; defaults to singleton store) */
|
|
93
110
|
store?: Store;
|
|
94
111
|
/** Persistence port for writing session entries (run.start, stage.start, etc.). */
|
|
@@ -285,7 +302,7 @@ function makeUnavailableUIContext(): WorkflowUIContext {
|
|
|
285
302
|
}
|
|
286
303
|
|
|
287
304
|
type AskUserQuestionToolEvent =
|
|
288
|
-
| { phase: "start"; callId?: string }
|
|
305
|
+
| { phase: "start"; callId?: string; args?: unknown }
|
|
289
306
|
| { phase: "end"; callId?: string; nameMatched: boolean };
|
|
290
307
|
|
|
291
308
|
function stringField(value: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
@@ -309,7 +326,7 @@ function askUserQuestionToolEvent(event: unknown): AskUserQuestionToolEvent | un
|
|
|
309
326
|
const callId = stringField(record, ["toolCallId", "tool_call_id", "toolUseId", "tool_use_id", "id"]);
|
|
310
327
|
|
|
311
328
|
if (type === "tool_execution_start" && isAskUserQuestionToolName(toolName)) {
|
|
312
|
-
return { phase: "start", callId };
|
|
329
|
+
return { phase: "start", callId, args: record["args"] };
|
|
313
330
|
}
|
|
314
331
|
if (type === "tool_execution_end" || type === "tool_execution_error" || type === "tool_result") {
|
|
315
332
|
return { phase: "end", callId, nameMatched: isAskUserQuestionToolName(toolName) };
|
|
@@ -317,6 +334,127 @@ function askUserQuestionToolEvent(event: unknown): AskUserQuestionToolEvent | un
|
|
|
317
334
|
return undefined;
|
|
318
335
|
}
|
|
319
336
|
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Readiness gate (#1099)
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// A stage's agent turn returns control to the user when it ends. If that turn
|
|
341
|
+
// issued no ask_user_question call, the stage completes and the workflow
|
|
342
|
+
// advances automatically. If the turn DID ask the user something, a
|
|
343
|
+
// deterministic readiness gate (the structured ask_user_question UI, rendered
|
|
344
|
+
// inline in the attached stage chat via the broker) is shown when the turn
|
|
345
|
+
// ends. Choosing "I'm ready to move on…" advances; anything else (the
|
|
346
|
+
// keep-exploring option, a typed answer, "Chat about this", or cancelling)
|
|
347
|
+
// returns control to the user, who keeps working in the normal stage composer.
|
|
348
|
+
// The same per-turn check re-applies after each subsequent user-driven turn.
|
|
349
|
+
|
|
350
|
+
export const READINESS_GATE_ADVANCE_LABEL = "I'm ready to move on to the next workflow stage.";
|
|
351
|
+
|
|
352
|
+
const READINESS_GATE_ADVANCE_NORMALIZED = READINESS_GATE_ADVANCE_LABEL.trim().toLowerCase();
|
|
353
|
+
|
|
354
|
+
export const READINESS_GATE_QUESTION_PARAMS = {
|
|
355
|
+
questions: [
|
|
356
|
+
{
|
|
357
|
+
question: "Any additional points to explore before moving on?",
|
|
358
|
+
header: "Continue?",
|
|
359
|
+
options: [
|
|
360
|
+
{
|
|
361
|
+
label: READINESS_GATE_ADVANCE_LABEL,
|
|
362
|
+
description: "Complete this stage and advance the workflow.",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
label: "I have more to explore or ask about.",
|
|
366
|
+
description: "Stay in this stage and keep working in the chat composer.",
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Decide whether a brokered readiness-gate result selected the "advance"
|
|
375
|
+
* option. Tolerant of case/whitespace and of the advance label arriving via a
|
|
376
|
+
* multi-select `selected[]` entry, so a structured answer that canonicalized to
|
|
377
|
+
* the advance option still completes the stage. Anything else (the explore
|
|
378
|
+
* option, a typed answer, a cancelled/empty result) means "stay".
|
|
379
|
+
*/
|
|
380
|
+
export function readinessResultMeansAdvance(result: unknown): boolean {
|
|
381
|
+
if (result === null || typeof result !== "object") return false;
|
|
382
|
+
const details = (result as {
|
|
383
|
+
details?: {
|
|
384
|
+
answers?: ReadonlyArray<{ answer?: unknown; selected?: ReadonlyArray<unknown> }>;
|
|
385
|
+
cancelled?: boolean;
|
|
386
|
+
};
|
|
387
|
+
}).details;
|
|
388
|
+
if (details === undefined || details.cancelled === true) return false;
|
|
389
|
+
const first = details.answers?.[0];
|
|
390
|
+
if (first === undefined) return false;
|
|
391
|
+
const candidates: unknown[] = [first.answer];
|
|
392
|
+
if (Array.isArray(first.selected)) candidates.push(...first.selected);
|
|
393
|
+
return candidates.some(
|
|
394
|
+
(candidate) =>
|
|
395
|
+
typeof candidate === "string" &&
|
|
396
|
+
candidate.trim().toLowerCase() === READINESS_GATE_ADVANCE_NORMALIZED,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let cachedReadinessGateTool: ReturnType<typeof createAskUserQuestionToolDefinition> | undefined;
|
|
401
|
+
function readinessGateTool(): ReturnType<typeof createAskUserQuestionToolDefinition> {
|
|
402
|
+
return (cachedReadinessGateTool ??= createAskUserQuestionToolDefinition());
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Render the readiness gate inline in the attached stage chat by invoking the
|
|
407
|
+
* ask_user_question tool with a pre-filled body, routing its custom UI through
|
|
408
|
+
* the stage UI broker for (runId, stageId). Returns "advance" only when the
|
|
409
|
+
* user chooses the move-on option; the keep-exploring option, "Chat about
|
|
410
|
+
* this", a typed answer, or cancellation all mean "stay". If no stage chat host
|
|
411
|
+
* is attached the broker request stays pending (the stage shows awaiting_input)
|
|
412
|
+
* exactly like the tool itself.
|
|
413
|
+
*/
|
|
414
|
+
export async function askReadinessViaStageBroker(
|
|
415
|
+
runId: string,
|
|
416
|
+
stageId: string,
|
|
417
|
+
signal: AbortSignal,
|
|
418
|
+
): Promise<"advance" | "stay"> {
|
|
419
|
+
const execute = readinessGateTool().execute;
|
|
420
|
+
if (execute === undefined) return "advance";
|
|
421
|
+
const gateContext = {
|
|
422
|
+
hasUI: true,
|
|
423
|
+
ui: {
|
|
424
|
+
custom: (factory: unknown, options?: unknown): Promise<unknown> =>
|
|
425
|
+
stageUiBroker.requestCustomUi(
|
|
426
|
+
runId,
|
|
427
|
+
stageId,
|
|
428
|
+
factory as Parameters<typeof stageUiBroker.requestCustomUi>[2],
|
|
429
|
+
options as Parameters<typeof stageUiBroker.requestCustomUi>[3],
|
|
430
|
+
signal,
|
|
431
|
+
),
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
// Expose a headless-answer adapter for the gate so it can be answered
|
|
435
|
+
// programmatically (e.g. `workflow send`) without a TUI host. The gate
|
|
436
|
+
// question params are known statically here.
|
|
437
|
+
const gateAdapter = buildStagePromptAdapter(
|
|
438
|
+
`readiness-gate-${stageId}`,
|
|
439
|
+
"readiness_gate",
|
|
440
|
+
READINESS_GATE_QUESTION_PARAMS,
|
|
441
|
+
Date.now(),
|
|
442
|
+
);
|
|
443
|
+
if (gateAdapter) stageUiBroker.provideStagePrompt(runId, stageId, gateAdapter);
|
|
444
|
+
try {
|
|
445
|
+
const result = await execute(
|
|
446
|
+
`readiness-gate-${stageId}`,
|
|
447
|
+
READINESS_GATE_QUESTION_PARAMS as Parameters<typeof execute>[1],
|
|
448
|
+
signal,
|
|
449
|
+
undefined,
|
|
450
|
+
gateContext as unknown as Parameters<typeof execute>[4],
|
|
451
|
+
);
|
|
452
|
+
return readinessResultMeansAdvance(result) ? "advance" : "stay";
|
|
453
|
+
} finally {
|
|
454
|
+
stageUiBroker.clearStagePrompt(runId, stageId);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
320
458
|
// ---------------------------------------------------------------------------
|
|
321
459
|
// raceAbort — races a promise against an AbortSignal
|
|
322
460
|
// ---------------------------------------------------------------------------
|
|
@@ -2061,15 +2199,32 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2061
2199
|
});
|
|
2062
2200
|
const activeAskUserQuestionCalls = new Set<string>();
|
|
2063
2201
|
let activeAskUserQuestionAnonymousCalls = 0;
|
|
2202
|
+
// Set whenever an ask_user_question tool call is observed during the
|
|
2203
|
+
// current model turn. Drives the deterministic readiness gate (#1099):
|
|
2204
|
+
// after a turn that asked the user a question ends, the workflow must
|
|
2205
|
+
// confirm readiness before completing/advancing the stage.
|
|
2206
|
+
let askUserQuestionObservedThisTurn = false;
|
|
2064
2207
|
const hasActiveAskUserQuestion = (): boolean =>
|
|
2065
2208
|
activeAskUserQuestionCalls.size > 0 || activeAskUserQuestionAnonymousCalls > 0;
|
|
2066
2209
|
const unsubscribeAskUserQuestionWatcher = innerCtx.subscribe((event) => {
|
|
2067
2210
|
const toolEvent = askUserQuestionToolEvent(event);
|
|
2068
2211
|
if (!toolEvent) return;
|
|
2069
2212
|
if (toolEvent.phase === "start") {
|
|
2213
|
+
askUserQuestionObservedThisTurn = true;
|
|
2070
2214
|
if (toolEvent.callId !== undefined) activeAskUserQuestionCalls.add(toolEvent.callId);
|
|
2071
2215
|
else activeAskUserQuestionAnonymousCalls += 1;
|
|
2072
2216
|
activeStore.recordStageAwaitingInput(runId, stageId, true);
|
|
2217
|
+
// Expose a headless-answer adapter so the prompt can be answered
|
|
2218
|
+
// programmatically (e.g. `workflow send`) without a TUI host. The
|
|
2219
|
+
// (runId, stageId) key joins this to the broker request the tool's
|
|
2220
|
+
// ctx.ui.custom() call raises.
|
|
2221
|
+
const adapter = buildStagePromptAdapter(
|
|
2222
|
+
toolEvent.callId ?? `ask-user-question-${stageId}`,
|
|
2223
|
+
"ask_user_question",
|
|
2224
|
+
toolEvent.args,
|
|
2225
|
+
Date.now(),
|
|
2226
|
+
);
|
|
2227
|
+
if (adapter) stageUiBroker.provideStagePrompt(runId, stageId, adapter);
|
|
2073
2228
|
return;
|
|
2074
2229
|
}
|
|
2075
2230
|
|
|
@@ -2083,6 +2238,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2083
2238
|
|
|
2084
2239
|
if (!hasActiveAskUserQuestion()) {
|
|
2085
2240
|
activeStore.recordStageAwaitingInput(runId, stageId, false);
|
|
2241
|
+
stageUiBroker.clearStagePrompt(runId, stageId);
|
|
2086
2242
|
}
|
|
2087
2243
|
});
|
|
2088
2244
|
const disposeInnerContext = async (): Promise<void> => {
|
|
@@ -2090,6 +2246,7 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2090
2246
|
activeAskUserQuestionCalls.clear();
|
|
2091
2247
|
activeAskUserQuestionAnonymousCalls = 0;
|
|
2092
2248
|
activeStore.recordStageAwaitingInput(runId, stageId, false);
|
|
2249
|
+
stageUiBroker.clearStagePrompt(runId, stageId);
|
|
2093
2250
|
await innerCtx.__dispose();
|
|
2094
2251
|
};
|
|
2095
2252
|
let unregisterStageHandle = (): void => {};
|
|
@@ -2271,6 +2428,31 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2271
2428
|
}
|
|
2272
2429
|
};
|
|
2273
2430
|
|
|
2431
|
+
// Deterministic readiness gate (#1099). After a model turn that issued an
|
|
2432
|
+
// ask_user_question tool call ends, confirm with the user before the stage
|
|
2433
|
+
// completes/advances. "No" keeps execution in this stage (steer + re-gate
|
|
2434
|
+
// after the next turn); "Yes" resumes progression. The gate engages only
|
|
2435
|
+
// when a confirmation seam is available, so headless/test runs proceed.
|
|
2436
|
+
const readinessGateEnabled =
|
|
2437
|
+
opts.confirmStageReadiness !== undefined || opts.usePromptNodesForUi === true;
|
|
2438
|
+
const confirmReadiness = async (): Promise<"advance" | "stay"> => {
|
|
2439
|
+
try {
|
|
2440
|
+
if (opts.confirmStageReadiness !== undefined) {
|
|
2441
|
+
const ready = await opts.confirmStageReadiness({
|
|
2442
|
+
runId,
|
|
2443
|
+
stageId,
|
|
2444
|
+
stageName: name,
|
|
2445
|
+
signal: ownController.signal,
|
|
2446
|
+
});
|
|
2447
|
+
return ready ? "advance" : "stay";
|
|
2448
|
+
}
|
|
2449
|
+
return await askReadinessViaStageBroker(runId, stageId, ownController.signal);
|
|
2450
|
+
} catch {
|
|
2451
|
+
// A gate failure must not strand the workflow; proceed on error.
|
|
2452
|
+
return "advance";
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
|
|
2274
2456
|
const runTrackedStageCall = async (call: () => Promise<string>): Promise<string> => {
|
|
2275
2457
|
await waitForStageRelease();
|
|
2276
2458
|
if (stageFinalized) {
|
|
@@ -2320,7 +2502,49 @@ export async function run<TInputs extends Record<string, unknown>>(
|
|
|
2320
2502
|
else ownController.signal.addEventListener("abort", abortSession, { once: true });
|
|
2321
2503
|
let result = "";
|
|
2322
2504
|
try {
|
|
2505
|
+
// Run the stage's initial agent turn.
|
|
2506
|
+
askUserQuestionObservedThisTurn = false;
|
|
2323
2507
|
result = await raceAbort(call(), ownController.signal);
|
|
2508
|
+
|
|
2509
|
+
// Per-turn readiness gate (#1099). When an agent turn ENDS (control
|
|
2510
|
+
// returns to the user): if the turn issued no ask_user_question
|
|
2511
|
+
// call, complete/advance automatically; if it DID, show the gate.
|
|
2512
|
+
// "advance" completes the stage; anything else hands control back to
|
|
2513
|
+
// the user, who keeps working in the normal stage composer — we wait
|
|
2514
|
+
// for their next turn to end (the session's agent_end event) and
|
|
2515
|
+
// re-apply the same check. No canned auto-steer, so the user is
|
|
2516
|
+
// never trapped re-gating and the stage never auto-drives a hidden
|
|
2517
|
+
// turn that could strand the stream.
|
|
2518
|
+
if (!ownController.signal.aborted && readinessGateEnabled) {
|
|
2519
|
+
let resolveNextTurnEnd: (() => void) | null = null;
|
|
2520
|
+
const unsubscribeTurnWatcher = innerCtx.subscribe((event) => {
|
|
2521
|
+
if ((event as { type?: unknown }).type === "agent_end" && resolveNextTurnEnd) {
|
|
2522
|
+
const resolve = resolveNextTurnEnd;
|
|
2523
|
+
resolveNextTurnEnd = null;
|
|
2524
|
+
resolve();
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
try {
|
|
2528
|
+
while (askUserQuestionObservedThisTurn) {
|
|
2529
|
+
if ((await confirmReadiness()) === "advance") break;
|
|
2530
|
+
if (ownController.signal.aborted) break;
|
|
2531
|
+
// Stay: return control to the user and await their next
|
|
2532
|
+
// composer-driven turn end before re-checking.
|
|
2533
|
+
askUserQuestionObservedThisTurn = false;
|
|
2534
|
+
await raceAbort(
|
|
2535
|
+
new Promise<void>((resolve) => {
|
|
2536
|
+
resolveNextTurnEnd = resolve;
|
|
2537
|
+
}),
|
|
2538
|
+
ownController.signal,
|
|
2539
|
+
);
|
|
2540
|
+
if (ownController.signal.aborted) break;
|
|
2541
|
+
result = innerCtx.__getLastAssistantText() ?? result;
|
|
2542
|
+
}
|
|
2543
|
+
} finally {
|
|
2544
|
+
resolveNextTurnEnd = null;
|
|
2545
|
+
unsubscribeTurnWatcher();
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2324
2548
|
} finally {
|
|
2325
2549
|
ownController.signal.removeEventListener("abort", abortSession);
|
|
2326
2550
|
}
|