@yagni-app/code-staging 1.2.3-staging.1575.1 → 1.2.3-staging.1580.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension/index.d.ts +2 -0
- package/dist/extension/index.js +24 -0
- package/dist/extension/permission/gate.js +3 -3
- package/dist/extension/pipeline/goCommand.d.ts +3 -0
- package/dist/extension/pipeline/goCommand.js +30 -0
- package/dist/extension/pipeline/orchestrator.d.ts +6 -0
- package/dist/extension/pipeline/orchestrator.js +16 -5
- package/dist/extension/pipeline/stages.js +5 -5
- package/dist/extension/prototypeTool.d.ts +151 -0
- package/dist/extension/prototypeTool.js +330 -0
- package/dist/extension/prototypeVerify.d.ts +77 -0
- package/dist/extension/prototypeVerify.js +219 -0
- package/package.json +2 -2
|
@@ -166,6 +166,8 @@ export type { MakeRecordContextToolOptions } from "./recordContextTool.js";
|
|
|
166
166
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
167
167
|
export type { MakeRecordDecisionToolOptions } from "./recordDecisionTool.js";
|
|
168
168
|
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
169
|
+
export { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
170
|
+
export type { MakePrototypeToolOptions, PrototypeSummary } from "./prototypeTool.js";
|
|
169
171
|
export type { MakeNextWorkToolOptions, DefaultNextAction, NextActionOption, } from "./nextWorkTool.js";
|
|
170
172
|
export { recordDecision } from "./recordDecisionTool.js";
|
|
171
173
|
export type { RecordDecisionParams } from "./recordDecisionTool.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -13,8 +13,10 @@ import { makeFileTicketTool, makeUpdateTicketStatusTool } from "./ticketTools.js
|
|
|
13
13
|
import { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
14
14
|
import { registerCmuxBridge } from "./cmux/index.js";
|
|
15
15
|
import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
16
|
+
import { makePrototypeVerifyTool } from "./prototypeVerify.js";
|
|
16
17
|
import { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
17
18
|
import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
|
|
19
|
+
import { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
18
20
|
import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE } from "./branding.js";
|
|
19
21
|
import { claudeRulesSection } from "./claudeRules.js";
|
|
20
22
|
import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadOwnerRootFor, scratchpadSection } from "./scratchpad.js";
|
|
@@ -408,6 +410,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
408
410
|
onRecorded: (id) => recordedThisSession.add(id),
|
|
409
411
|
}));
|
|
410
412
|
}
|
|
413
|
+
if (!evalMode) {
|
|
414
|
+
// Prototypes v1: create/revise/list/share runnable single-file HTML
|
|
415
|
+
// Prototypes through the app's REST API. Workspace writes, so gated out of
|
|
416
|
+
// eval mode with the other external-side-effect tools.
|
|
417
|
+
pi.registerTool(makePrototypeReadTool(toolOpts));
|
|
418
|
+
pi.registerTool(makePrototypeDraftTool({ ...toolOpts, getSessionId: () => env.YAGNI_SESSION_ID }));
|
|
419
|
+
pi.registerTool(makePrototypeVerificationTool(toolOpts));
|
|
420
|
+
pi.registerTool(makePrototypeVerifyTool(toolOpts));
|
|
421
|
+
pi.registerTool(makePrototypeCreateTool({ ...toolOpts, getSessionId: () => env.YAGNI_SESSION_ID }));
|
|
422
|
+
pi.registerTool(makePrototypeAttachTool(toolOpts));
|
|
423
|
+
pi.registerTool(makePrototypeReviseTool(toolOpts));
|
|
424
|
+
pi.registerTool(makePrototypeListTool(toolOpts));
|
|
425
|
+
pi.registerTool(makePrototypeShareTool(toolOpts));
|
|
426
|
+
}
|
|
427
|
+
else if (env.YAGNI_MISSION_DIR && env.YAGNI_RUN_ID) {
|
|
428
|
+
// The server authorizes only exact revisions in this Run's approved packet.
|
|
429
|
+
pi.registerTool(makePrototypeReadTool(toolOpts));
|
|
430
|
+
pi.registerTool(makePrototypeVerifyTool(toolOpts));
|
|
431
|
+
}
|
|
411
432
|
// YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
|
|
412
433
|
// process has no direct user to answer, so it is never nudged (same gating
|
|
413
434
|
// as the delegation identity in branding.ts); eval mode is untouched so its
|
|
@@ -444,6 +465,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
444
465
|
// map → plan → implement → review → fix, each child grounded by inheritance.
|
|
445
466
|
registerGoCommand(pi, {
|
|
446
467
|
childUsage,
|
|
468
|
+
canPrepareDesign: () => !evalMode && modeHolder.get() === "auto",
|
|
447
469
|
attribution,
|
|
448
470
|
// The grounding switch threads into every /go run: `grounded: false`
|
|
449
471
|
// selects the existing blind personas + blind lens clauses (the M6
|
|
@@ -2029,6 +2051,8 @@ export { makeReviewBusinessMatchTool } from "./reviewTool.js";
|
|
|
2029
2051
|
export { makeRecordEngineeringContextTool } from "./recordContextTool.js";
|
|
2030
2052
|
export { makeRecordDecisionTool } from "./recordDecisionTool.js";
|
|
2031
2053
|
export { makeSuggestNextWorkTool, defaultNextAction } from "./nextWorkTool.js";
|
|
2054
|
+
// Prototypes v1: the four prototype tools (create/revise/list/share).
|
|
2055
|
+
export { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
|
|
2032
2056
|
export { recordDecision } from "./recordDecisionTool.js";
|
|
2033
2057
|
// Onramp Door B (as amended by ADR-0033): the first-run welcome (write-free,
|
|
2034
2058
|
// free text first-class) + the explicit /setup-team drafting flow.
|
|
@@ -51,9 +51,9 @@ export function createModeHolder(initial = "auto") {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
export const DEFAULT_PERMISSION_POLICY = {
|
|
54
|
-
planBlockTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
55
|
-
reviewConfirmTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status"],
|
|
56
|
-
alwaysConfirmTools: ["file_ticket", "update_ticket_status"],
|
|
54
|
+
planBlockTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status", "prototype_share", "prototype_create", "prototype_attach", "prototype_revise", "prototype_draft", "prototype_verification", "prototype_verify"],
|
|
55
|
+
reviewConfirmTools: ["write", "edit", "bash", "file_ticket", "update_ticket_status", "prototype_share", "prototype_create", "prototype_attach", "prototype_revise", "prototype_draft", "prototype_verification", "prototype_verify"],
|
|
56
|
+
alwaysConfirmTools: ["file_ticket", "update_ticket_status", "prototype_share"],
|
|
57
57
|
};
|
|
58
58
|
export function allowRuleFloorVerdict(toolName, params, mode, policy) {
|
|
59
59
|
// Floor 0 — plan mode: the mode's contract (no mutations without Guardian
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
*/
|
|
61
61
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
62
62
|
import type { SpendResponse } from "../costHud.js";
|
|
63
|
+
import { draftPrototype } from "../prototypeTool.js";
|
|
63
64
|
import { type CrashReporter } from "../crashReport.js";
|
|
64
65
|
import { runFinish as defaultRunFinish } from "./finish.js";
|
|
65
66
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
@@ -144,6 +145,8 @@ export interface RegisterGoDeps {
|
|
|
144
145
|
* test) simply means "not ultra", which is today's behavior.
|
|
145
146
|
*/
|
|
146
147
|
isUltra?: () => boolean;
|
|
148
|
+
canPrepareDesign?: () => boolean;
|
|
149
|
+
draftPrototype?: typeof draftPrototype;
|
|
147
150
|
/**
|
|
148
151
|
* The boot-time `grounding` settings switch, threaded into the orchestrator's
|
|
149
152
|
* `grounded` flag: false runs the whole pipeline on the existing blind
|
|
@@ -69,6 +69,7 @@ import { RunState } from "./runState.js";
|
|
|
69
69
|
import { aggregateRunUsage } from "./budget.js";
|
|
70
70
|
import { formatRunCostTable } from "./runCostTable.js";
|
|
71
71
|
import { makeCombinedCheckpointStore, makeFileCheckpointStore, makePiJournalCheckpointStore, } from "./checkpoint.js";
|
|
72
|
+
import { absoluteUrl, draftPrototype } from "../prototypeTool.js";
|
|
72
73
|
import { getToken as defaultGetToken, resolveBaseUrl } from "../config.js";
|
|
73
74
|
import { makeCrashReporter } from "../crashReport.js";
|
|
74
75
|
import { logEvent } from "../errorSink.js";
|
|
@@ -785,6 +786,35 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
785
786
|
let result = await runPipeline(ticket, {
|
|
786
787
|
cwd: runCwd,
|
|
787
788
|
signal: runSignal,
|
|
789
|
+
// Interactive /go tracks no backend work item (#2134), so the draft
|
|
790
|
+
// is private to this session and keyed on the run: a resumed run
|
|
791
|
+
// reuses its prototype instead of drafting a second one.
|
|
792
|
+
...(deps.canPrepareDesign?.() ? {
|
|
793
|
+
prepareDesign: async ({ ticket: designTicket, exploration, signal }) => {
|
|
794
|
+
if (!deps.canPrepareDesign?.())
|
|
795
|
+
return undefined;
|
|
796
|
+
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
797
|
+
try {
|
|
798
|
+
const draft = await (deps.draftPrototype ?? draftPrototype)({ baseUrl, getToken: deps.getToken ?? defaultGetToken }, {
|
|
799
|
+
title: designTicket.slice(0, 160), spec: `${designTicket}\n\nRepository exploration:\n${exploration}`,
|
|
800
|
+
sessionId: (deps.env ?? process.env).YAGNI_SESSION_ID,
|
|
801
|
+
automatic: true, requestKey: `go:${sessionKey}`,
|
|
802
|
+
}, signal);
|
|
803
|
+
if (draft.reference) {
|
|
804
|
+
notify(`Prototype ready: ${absoluteUrl(baseUrl, draft.prototype?.url ?? draft.reference.url ?? `/prototypes/${draft.reference.prototypeId}`)}`, "info");
|
|
805
|
+
return `## Prototype design reference\n${JSON.stringify(draft.reference)}\nRead this exact revision with prototype_read. Review it with the plan. Implement with the repository's real components. Preserve this reference through implementation and review. Run prototype_verify against the implemented local app with agreed interaction states; it captures mobile/desktop screenshots and records observed checks or explicitly unverified when no browser is available. Visually inspect those screenshots against this design and document intentional departures. Document intentional departures. Never substitute a newer revision or share automatically.`;
|
|
806
|
+
}
|
|
807
|
+
if (draft.status === "failed")
|
|
808
|
+
notify("Prototype drafting failed. Continue without it or retry with prototype_draft before approving the plan.", "warning");
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
if (!signal?.aborted)
|
|
813
|
+
notify("Prototype drafting unavailable. Continue without it or retry before plan approval.", "warning");
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
} : {}),
|
|
788
818
|
fanout: fanoutMode,
|
|
789
819
|
grounded: goGrounded,
|
|
790
820
|
...(isUltra ? { isUltra } : {}),
|
|
@@ -32,6 +32,12 @@ import { type VerifyOutcome, type WorkstreamCheckResult, type WorkstreamCheckTar
|
|
|
32
32
|
import { snapshotWorkspace as defaultSnapshotWorkspace } from "./workspace.js";
|
|
33
33
|
import { type CheckpointStore, type FanoutBudgetVerdict, type FanoutMode, type PipelineStage, type JsonEvent, type PipelineProgress, type PipelineResult, type ResiliencePolicy, type ResumePlan, type StageId, type StageResult, type StageTag } from "./types.js";
|
|
34
34
|
export interface RunPipelineDeps {
|
|
35
|
+
/** Bounded external design preparation; never runs inside the read-only planner or approved missions. */
|
|
36
|
+
prepareDesign?: (input: {
|
|
37
|
+
ticket: string;
|
|
38
|
+
exploration: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}) => Promise<string | undefined>;
|
|
35
41
|
cwd: string;
|
|
36
42
|
signal?: AbortSignal;
|
|
37
43
|
runStage?: typeof defaultRunStage;
|
|
@@ -335,6 +335,11 @@ export async function runPipeline(ticket, deps) {
|
|
|
335
335
|
const rounds = resume ? [...resume.priorRounds] : [];
|
|
336
336
|
let round = resume ? resume.round : 1;
|
|
337
337
|
let reviewInput = resume ? resume.reviewInput : "";
|
|
338
|
+
const designMarker = "## Pinned design context";
|
|
339
|
+
const resumedDesignOffset = reviewInput.indexOf(designMarker);
|
|
340
|
+
let designContext = mission?.plan
|
|
341
|
+
? `${designMarker}\nApproved mission plan (preserve exact prototype references):\n${mission.plan}`
|
|
342
|
+
: resumedDesignOffset >= 0 ? reviewInput.slice(resumedDesignOffset) : undefined;
|
|
338
343
|
let stopReason = "clean";
|
|
339
344
|
// P4: honest note carried out when the verify gate could not give a verdict on
|
|
340
345
|
// the final round (no command / unrunnable), so /go never claims a build it did
|
|
@@ -749,6 +754,12 @@ export async function runPipeline(ticket, deps) {
|
|
|
749
754
|
baselinePaths = parseChangedPaths(beforeBuild.status);
|
|
750
755
|
// Sequential build half — stop at the first failure (mirrors the chain).
|
|
751
756
|
for (const stage of buildStages) {
|
|
757
|
+
if (stage.id === "plan" && !mission && deps.prepareDesign && !deps.signal?.aborted && !overBudget()) {
|
|
758
|
+
const prepared = await deps.prepareDesign({ ticket: promptTicket, exploration: previous ?? "", signal: deps.signal });
|
|
759
|
+
designContext = prepared ? `${designMarker}\n${prepared}` : undefined;
|
|
760
|
+
if (designContext)
|
|
761
|
+
previous = `${previous ?? ""}\n\n${designContext}`;
|
|
762
|
+
}
|
|
752
763
|
progress({ kind: "stage_start", stageId: stage.id });
|
|
753
764
|
stageEvent(stage.id, "start");
|
|
754
765
|
// The implement stage is a DIAMOND (partition → parallel builders →
|
|
@@ -839,12 +850,12 @@ export async function runPipeline(ticket, deps) {
|
|
|
839
850
|
progress({ kind: "stage_done", stageId: stage.id });
|
|
840
851
|
// The plan finish carries its finalOutput so /go can record the plan onto the
|
|
841
852
|
// work item; no other boundary streams the full stage text.
|
|
842
|
-
stageEvent(stage.id, "finish", stage.id === "plan" ? { output: r.finalOutput } : undefined);
|
|
853
|
+
stageEvent(stage.id, "finish", stage.id === "plan" ? { output: designContext ? `${r.finalOutput}\n\n${designContext}` : r.finalOutput } : undefined);
|
|
843
854
|
// The map brief is the partitioner's repo context on an interactive run
|
|
844
855
|
// (mission runs skip map and hand it the injected memo instead).
|
|
845
856
|
if (stage.id === "map")
|
|
846
857
|
mapBrief = r.finalOutput;
|
|
847
|
-
previous = r.finalOutput;
|
|
858
|
+
previous = designContext ? `${r.finalOutput}\n\n${designContext}` : r.finalOutput;
|
|
848
859
|
// R3-b: stop honestly if the build half alone blew the run budget.
|
|
849
860
|
const over = overBudget();
|
|
850
861
|
if (over) {
|
|
@@ -1064,7 +1075,7 @@ export async function runPipeline(ticket, deps) {
|
|
|
1064
1075
|
const fixStartedAt = clock();
|
|
1065
1076
|
progress({ kind: "stage_start", stageId: "fix", round });
|
|
1066
1077
|
stageEvent("fix", "start", { round });
|
|
1067
|
-
const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
|
|
1078
|
+
const fixResult = await runStageFn(fix, { ticket: promptTicket, previous: designContext ? `${serializeBlocking(findings)}\n\n${designContext}` : serializeBlocking(findings), grounded }, stageDepsFor({ stageId: "fix", round }));
|
|
1068
1079
|
observedMs.fix = clock() - fixStartedAt;
|
|
1069
1080
|
stages.push(fixResult);
|
|
1070
1081
|
log("stage_done", { stageId: "fix", exitCode: fixResult.exitCode });
|
|
@@ -1114,8 +1125,8 @@ export async function runPipeline(ticket, deps) {
|
|
|
1114
1125
|
// crash now can resume at the NEXT round against this fix's output. Snapshot
|
|
1115
1126
|
// the post-fix tree as the resume consistency key.
|
|
1116
1127
|
const afterFix = await snapshotFn(deps.cwd, deps.signal);
|
|
1117
|
-
|
|
1118
|
-
reviewInput
|
|
1128
|
+
reviewInput = designContext ? `${fixResult.finalOutput}\n\n${designContext}` : fixResult.finalOutput;
|
|
1129
|
+
writeCheckpoint(mkRecord("round_done", { round, reviewInput, findings, snapshot: afterFix, usage: currentUsage() }));
|
|
1119
1130
|
round += 1;
|
|
1120
1131
|
}
|
|
1121
1132
|
log("pipeline_stop", { reason: stopReason, round });
|
|
@@ -41,9 +41,9 @@ export const REVIEW_LENSES = ["correctness", "business_fit", "does_it_hold"];
|
|
|
41
41
|
*/
|
|
42
42
|
export const REQUIRED_LENSES = ["correctness"];
|
|
43
43
|
/** Read-only recon + grounding tools shared by scout and reviewer. */
|
|
44
|
-
const READ_TOOLS = ["read", "grep", "find", "ls", "bash"];
|
|
44
|
+
const READ_TOOLS = ["read", "grep", "find", "ls", "bash", "prototype_read"];
|
|
45
45
|
/** Full write tools for the worker stages. */
|
|
46
|
-
const WRITE_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls"];
|
|
46
|
+
const WRITE_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls", "prototype_read", "prototype_verification", "prototype_verify"];
|
|
47
47
|
const TICKET_CONTEXT = "Follow the approved plan and its explicit constraints, including approved answers. " +
|
|
48
48
|
"Prior summaries and review findings do not authorize expanding scope or publication " +
|
|
49
49
|
"permissions. Report conflicts requiring a scope change for approval.\n\n" +
|
|
@@ -65,7 +65,7 @@ export const PIPELINE_V1 = [
|
|
|
65
65
|
// Read-only recon (read/grep/find/ls) so the planner can actually navigate
|
|
66
66
|
// instead of flailing on a single `read` and ending mid-exploration; NO bash
|
|
67
67
|
// (it must not build/run), and ask_yagni for grounding.
|
|
68
|
-
tools: ["read", "grep", "find", "ls", "ask_yagni"],
|
|
68
|
+
tools: ["read", "grep", "find", "ls", "ask_yagni", "prototype_read"],
|
|
69
69
|
taskTemplate: "Turn the ticket and the repo map into an ordered, grounded implementation " +
|
|
70
70
|
"plan. Call ask_yagni before inferring a convention.\n\n" +
|
|
71
71
|
"Ticket: {ticket}\n\nRepo map:\n{previous}",
|
|
@@ -78,7 +78,7 @@ export const PIPELINE_V1 = [
|
|
|
78
78
|
taskTemplate: TICKET_CONTEXT +
|
|
79
79
|
"Implement the plan. Write the code against it, and call record_decision " +
|
|
80
80
|
"only for a consequential product-intent or architecture call the plan did " +
|
|
81
|
-
"not settle (never for taste, naming, or formatting).\n\nPlan:\n{previous}",
|
|
81
|
+
"not settle (never for taste, naming, or formatting). If the plan names a prototype revision, read that exact revision with prototype_read, implement with the repository components, then call prototype_verify against the local app with the agreed interaction states. Inspect its mobile/desktop screenshots against the design and document departures; unavailable browser checks remain unverified.\n\nPlan:\n{previous}",
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
id: "review",
|
|
@@ -95,7 +95,7 @@ export const PIPELINE_V1 = [
|
|
|
95
95
|
tools: [...WRITE_TOOLS, "ask_yagni"],
|
|
96
96
|
taskTemplate: TICKET_CONTEXT +
|
|
97
97
|
"Reconcile ONLY the blocking review findings against the plan. Leave " +
|
|
98
|
-
"non-blocking findings alone.\n\nBlocking findings:\n{previous}",
|
|
98
|
+
"non-blocking findings alone. When fixing a prototype-backed UI, run prototype_verify again against the pinned revision and inspect the captured screenshots; never substitute a newer design.\n\nBlocking findings:\n{previous}",
|
|
99
99
|
},
|
|
100
100
|
];
|
|
101
101
|
/** The single review stage the orchestrator fans out across REVIEW_LENSES. */
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
/**
|
|
4
|
+
* Prototype tools (Prototypes v1): create, revise, list, and share runnable
|
|
5
|
+
* single-file HTML impressions of a UI through the token-authed YAGNI Code
|
|
6
|
+
* proxy (`/api/yagni-code/prototypes`, same contract as the session-authed
|
|
7
|
+
* `/api/prototypes` — the extension's device token only works on the
|
|
8
|
+
* yagni-code surface). A Prototype renders live in the app at
|
|
9
|
+
* `/prototypes/:id` and is throwaway by design: it proposes what to build,
|
|
10
|
+
* never ships as the build, and touches no product code.
|
|
11
|
+
*
|
|
12
|
+
* Sharing from here is the HUMAN path: the developer typed the ask in their own
|
|
13
|
+
* terminal, so the share is a direct REST call that takes effect immediately
|
|
14
|
+
* (recorded actor + time, revocable in-app). The agent-in-app path stages a
|
|
15
|
+
* Decision instead; that lives server-side, not in this client.
|
|
16
|
+
*
|
|
17
|
+
* These are single-purpose tools (create/revise/list/share), matching the
|
|
18
|
+
* extension's one-tool-per-verb idiom (ask_yagni, suggest_next_work, ...).
|
|
19
|
+
*/
|
|
20
|
+
/** Options shared by the four prototype tool factories. */
|
|
21
|
+
export interface MakePrototypeToolOptions {
|
|
22
|
+
getSessionId?: () => string | undefined;
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
getToken: () => string | undefined;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
/** The summary fields of a Prototype the tools read back and print. */
|
|
28
|
+
export interface PrototypeSummary {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
currentRevision: number;
|
|
32
|
+
/** In-app path stamped by the backend mapper (`/prototypes/:id`). */
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve the backend's in-app path against the base URL so the terminal shows a clickable link. */
|
|
36
|
+
export declare function absoluteUrl(baseUrl: string, url: string): string;
|
|
37
|
+
declare const createParameters: Type.TObject<{
|
|
38
|
+
title: Type.TString;
|
|
39
|
+
html: Type.TString;
|
|
40
|
+
workItemId: Type.TOptional<Type.TString>;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Build the `prototype_create` tool.
|
|
44
|
+
*
|
|
45
|
+
* Creates a Prototype from a title and one complete single-file HTML document
|
|
46
|
+
* and prints the in-app URL where it renders.
|
|
47
|
+
*/
|
|
48
|
+
export declare function makePrototypeCreateTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof createParameters, {
|
|
49
|
+
prototype: PrototypeSummary | null;
|
|
50
|
+
}>;
|
|
51
|
+
declare const reviseParameters: Type.TObject<{
|
|
52
|
+
expectedRevision: Type.TOptional<Type.TInteger>;
|
|
53
|
+
prototypeId: Type.TString;
|
|
54
|
+
edits: Type.TOptional<Type.TArray<Type.TObject<{
|
|
55
|
+
find: Type.TString;
|
|
56
|
+
replace: Type.TString;
|
|
57
|
+
}>>>;
|
|
58
|
+
html: Type.TOptional<Type.TString>;
|
|
59
|
+
note: Type.TOptional<Type.TString>;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Build the `prototype_revise` tool.
|
|
63
|
+
*
|
|
64
|
+
* Writes a new revision: targeted `edits` (each `find` must match exactly once
|
|
65
|
+
* in the current HTML) or a full `html` replacement, with an optional `note`
|
|
66
|
+
* shown on the revision timeline. A 422 edit miss surfaces as a retryable error
|
|
67
|
+
* naming the failed find so the model can adjust and call again.
|
|
68
|
+
*/
|
|
69
|
+
export declare function makePrototypeReviseTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof reviseParameters, {
|
|
70
|
+
prototype: PrototypeSummary | null;
|
|
71
|
+
}>;
|
|
72
|
+
declare const listParameters: Type.TObject<{
|
|
73
|
+
workItemId: Type.TOptional<Type.TString>;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Build the `prototype_list` tool.
|
|
77
|
+
*
|
|
78
|
+
* Lists the workspace's Prototypes (optionally filtered to one work item) with
|
|
79
|
+
* their current revision and in-app URL.
|
|
80
|
+
*/
|
|
81
|
+
export declare function makePrototypeListTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof listParameters, {
|
|
82
|
+
prototypes: PrototypeSummary[];
|
|
83
|
+
}>;
|
|
84
|
+
declare const shareParameters: Type.TObject<{
|
|
85
|
+
prototypeId: Type.TString;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Build the `prototype_share` tool.
|
|
89
|
+
*
|
|
90
|
+
* Mints a revocable public share link for a Prototype. This is the HUMAN share
|
|
91
|
+
* path (the developer asked in their own terminal), so it is a direct REST call
|
|
92
|
+
* with immediate effect; the in-app agent path stages a Decision instead.
|
|
93
|
+
*/
|
|
94
|
+
export declare function makePrototypeShareTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof shareParameters, {
|
|
95
|
+
shareUrl: string | null;
|
|
96
|
+
}>;
|
|
97
|
+
declare const readParameters: Type.TObject<{
|
|
98
|
+
prototypeId: Type.TString;
|
|
99
|
+
revision: Type.TInteger;
|
|
100
|
+
}>;
|
|
101
|
+
export declare function makePrototypeReadTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof readParameters>;
|
|
102
|
+
declare const draftParameters: Type.TObject<{
|
|
103
|
+
title: Type.TString;
|
|
104
|
+
spec: Type.TString;
|
|
105
|
+
workItemId: Type.TOptional<Type.TString>;
|
|
106
|
+
automatic: Type.TBoolean;
|
|
107
|
+
requestKey: Type.TOptional<Type.TString>;
|
|
108
|
+
}>;
|
|
109
|
+
export interface PrototypeDraftResult {
|
|
110
|
+
status: "created" | "reused" | "skipped" | "failed";
|
|
111
|
+
prototype?: PrototypeSummary;
|
|
112
|
+
reference?: {
|
|
113
|
+
prototypeId: string;
|
|
114
|
+
revision: number;
|
|
115
|
+
url?: string;
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
};
|
|
118
|
+
reason?: string;
|
|
119
|
+
}
|
|
120
|
+
export declare function draftPrototype(opts: MakePrototypeToolOptions, params: {
|
|
121
|
+
title: string;
|
|
122
|
+
spec: string;
|
|
123
|
+
workItemId?: string;
|
|
124
|
+
sessionId?: string;
|
|
125
|
+
automatic: boolean;
|
|
126
|
+
requestKey?: string;
|
|
127
|
+
}, signal?: AbortSignal): Promise<PrototypeDraftResult>;
|
|
128
|
+
export declare function makePrototypeDraftTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof draftParameters>;
|
|
129
|
+
declare const verificationParameters: Type.TObject<{
|
|
130
|
+
prototypeId: Type.TString;
|
|
131
|
+
revision: Type.TInteger;
|
|
132
|
+
status: Type.TUnion<[Type.TLiteral<"passed">, Type.TLiteral<"failed">, Type.TLiteral<"unverified">]>;
|
|
133
|
+
summary: Type.TString;
|
|
134
|
+
checks: Type.TArray<Type.TObject<{
|
|
135
|
+
name: Type.TString;
|
|
136
|
+
status: Type.TUnion<[Type.TLiteral<"passed">, Type.TLiteral<"failed">, Type.TLiteral<"unverified">]>;
|
|
137
|
+
detail: Type.TOptional<Type.TString>;
|
|
138
|
+
}>>;
|
|
139
|
+
screenshots: Type.TArray<Type.TObject<{
|
|
140
|
+
label: Type.TString;
|
|
141
|
+
url: Type.TString;
|
|
142
|
+
}>>;
|
|
143
|
+
}>;
|
|
144
|
+
export declare function makePrototypeVerificationTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof verificationParameters>;
|
|
145
|
+
declare const attachParameters: Type.TObject<{
|
|
146
|
+
prototypeId: Type.TString;
|
|
147
|
+
workItemId: Type.TString;
|
|
148
|
+
}>;
|
|
149
|
+
export declare function makePrototypeAttachTool(opts: MakePrototypeToolOptions): ToolDefinition<typeof attachParameters>;
|
|
150
|
+
export {};
|
|
151
|
+
//# sourceMappingURL=prototypeTool.d.ts.map
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
+
const MAX_FAILED_FIND = 200;
|
|
4
|
+
/** Resolve the backend's in-app path against the base URL so the terminal shows a clickable link. */
|
|
5
|
+
export function absoluteUrl(baseUrl, url) {
|
|
6
|
+
return /^https?:\/\//.test(url) ? url : `${baseUrl}${url}`;
|
|
7
|
+
}
|
|
8
|
+
function authHeaders(opts) {
|
|
9
|
+
return {
|
|
10
|
+
"content-type": "application/json",
|
|
11
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const createParameters = Type.Object({
|
|
15
|
+
title: Type.String(),
|
|
16
|
+
html: Type.String(),
|
|
17
|
+
workItemId: Type.Optional(Type.String()),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Build the `prototype_create` tool.
|
|
21
|
+
*
|
|
22
|
+
* Creates a Prototype from a title and one complete single-file HTML document
|
|
23
|
+
* and prints the in-app URL where it renders.
|
|
24
|
+
*/
|
|
25
|
+
export function makePrototypeCreateTool(opts) {
|
|
26
|
+
return {
|
|
27
|
+
name: "prototype_create",
|
|
28
|
+
label: "Create prototype",
|
|
29
|
+
description: "Create a YAGNI Prototype: a runnable, throwaway impression of a UI, rendered live in the " +
|
|
30
|
+
"YAGNI app. Pass a `title` and the COMPLETE single-file HTML document (inline CSS and JS, " +
|
|
31
|
+
"no external requests; it runs in a sandboxed iframe). Optionally pass `workItemId` to " +
|
|
32
|
+
"attach it to the work item it explores. Returns the in-app URL where it renders.",
|
|
33
|
+
promptSnippet: "prototype_create: create a runnable single-file HTML Prototype in the YAGNI app (returns its URL).",
|
|
34
|
+
promptGuidelines: [
|
|
35
|
+
"A Prototype is a single self-contained HTML file: inline all CSS and JS, no external requests.",
|
|
36
|
+
"Attach the Prototype to the work item it explores via `workItemId` when one is in play.",
|
|
37
|
+
"Show the user the returned in-app URL so they can open the rendered Prototype.",
|
|
38
|
+
],
|
|
39
|
+
parameters: createParameters,
|
|
40
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
41
|
+
onUpdate?.({
|
|
42
|
+
content: [{ type: "text", text: "Creating the Prototype in YAGNI…" }],
|
|
43
|
+
details: { prototype: null },
|
|
44
|
+
});
|
|
45
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: authHeaders(opts),
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
title: params.title,
|
|
50
|
+
html: params.html,
|
|
51
|
+
sessionId: opts.getSessionId?.(),
|
|
52
|
+
...(params.workItemId ? { workItemId: params.workItemId } : {}),
|
|
53
|
+
}),
|
|
54
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(await friendlyFetchError("prototype_create", res));
|
|
57
|
+
}
|
|
58
|
+
const data = (await res.json());
|
|
59
|
+
const url = absoluteUrl(opts.baseUrl, data.prototype.url);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `Created Prototype "${data.prototype.title}" (revision ${data.prototype.currentRevision}).\n` +
|
|
65
|
+
`Open: ${url}`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
details: { prototype: data.prototype },
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const reviseParameters = Type.Object({
|
|
74
|
+
expectedRevision: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
75
|
+
prototypeId: Type.String(),
|
|
76
|
+
edits: Type.Optional(Type.Array(Type.Object({
|
|
77
|
+
find: Type.String(),
|
|
78
|
+
replace: Type.String(),
|
|
79
|
+
}))),
|
|
80
|
+
html: Type.Optional(Type.String()),
|
|
81
|
+
note: Type.Optional(Type.String()),
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Build the `prototype_revise` tool.
|
|
85
|
+
*
|
|
86
|
+
* Writes a new revision: targeted `edits` (each `find` must match exactly once
|
|
87
|
+
* in the current HTML) or a full `html` replacement, with an optional `note`
|
|
88
|
+
* shown on the revision timeline. A 422 edit miss surfaces as a retryable error
|
|
89
|
+
* naming the failed find so the model can adjust and call again.
|
|
90
|
+
*/
|
|
91
|
+
export function makePrototypeReviseTool(opts) {
|
|
92
|
+
return {
|
|
93
|
+
name: "prototype_revise",
|
|
94
|
+
label: "Revise prototype",
|
|
95
|
+
description: "Revise an existing YAGNI Prototype as a new revision. Prefer targeted `edits` (each `find` " +
|
|
96
|
+
"must match the current HTML exactly once) over sending the full `html` again; pass `html` " +
|
|
97
|
+
"only for a full replacement. Add a short `note` describing the change; it labels the " +
|
|
98
|
+
"revision timeline. Returns the in-app URL of the updated Prototype.",
|
|
99
|
+
promptSnippet: "prototype_revise: apply find/replace edits (or full html) to a Prototype as a new revision.",
|
|
100
|
+
promptGuidelines: [
|
|
101
|
+
"Prefer small `edits` over resending full `html`; each `find` must match exactly once.",
|
|
102
|
+
"If prototype_revise reports a failed find, adjust the find text so it matches exactly once and try again.",
|
|
103
|
+
"Pass a short `note` with each revision so the design timeline stays readable.",
|
|
104
|
+
],
|
|
105
|
+
parameters: reviseParameters,
|
|
106
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
107
|
+
onUpdate?.({
|
|
108
|
+
content: [{ type: "text", text: "Revising the Prototype in YAGNI…" }],
|
|
109
|
+
details: { prototype: null },
|
|
110
|
+
});
|
|
111
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(params.prototypeId)}/revisions`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: authHeaders(opts),
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
...(params.expectedRevision !== undefined ? { expectedRevision: params.expectedRevision } : {}),
|
|
116
|
+
...(params.edits ? { edits: params.edits } : {}),
|
|
117
|
+
...(params.html !== undefined ? { html: params.html } : {}),
|
|
118
|
+
...(params.note ? { note: params.note } : {}),
|
|
119
|
+
}),
|
|
120
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
121
|
+
// A 422 is an edit miss, not a transport failure: the backend rejected the
|
|
122
|
+
// whole batch because one `find` did not match exactly once, and wrote no
|
|
123
|
+
// revision. Name the failed find so the model can fix it and try again.
|
|
124
|
+
if (res.status === 422) {
|
|
125
|
+
let failedFind;
|
|
126
|
+
try {
|
|
127
|
+
const body = (await res.json());
|
|
128
|
+
failedFind = body.failedFind;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* body unreadable; fall through to the generic message */
|
|
132
|
+
}
|
|
133
|
+
const shown = failedFind && failedFind.length > MAX_FAILED_FIND
|
|
134
|
+
? `${failedFind.slice(0, MAX_FAILED_FIND)} ...`
|
|
135
|
+
: failedFind;
|
|
136
|
+
throw new Error("prototype_revise could not apply the edits: each `find` must match exactly once in the " +
|
|
137
|
+
`current HTML, and this one did not${shown ? `: "${shown}"` : ""}. No revision was ` +
|
|
138
|
+
"written. Adjust the find text (or send the full `html` instead) and try again.");
|
|
139
|
+
}
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
throw new Error(await friendlyFetchError("prototype_revise", res));
|
|
142
|
+
}
|
|
143
|
+
const data = (await res.json());
|
|
144
|
+
const url = absoluteUrl(opts.baseUrl, data.prototype.url);
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `Revised Prototype "${data.prototype.title}" to revision ${data.revision.seq}.\n` +
|
|
150
|
+
`Open: ${url}`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
details: { prototype: data.prototype },
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const listParameters = Type.Object({
|
|
159
|
+
workItemId: Type.Optional(Type.String()),
|
|
160
|
+
});
|
|
161
|
+
/**
|
|
162
|
+
* Build the `prototype_list` tool.
|
|
163
|
+
*
|
|
164
|
+
* Lists the workspace's Prototypes (optionally filtered to one work item) with
|
|
165
|
+
* their current revision and in-app URL.
|
|
166
|
+
*/
|
|
167
|
+
export function makePrototypeListTool(opts) {
|
|
168
|
+
return {
|
|
169
|
+
name: "prototype_list",
|
|
170
|
+
label: "List prototypes",
|
|
171
|
+
description: "List the workspace's YAGNI Prototypes with their current revision and in-app URL. " +
|
|
172
|
+
"Optionally pass `workItemId` to list only the Prototypes attached to that work item. " +
|
|
173
|
+
"Use it to find an existing Prototype to revise instead of creating a duplicate.",
|
|
174
|
+
promptSnippet: "prototype_list: list the workspace's Prototypes (optionally by work item), with URLs.",
|
|
175
|
+
promptGuidelines: [
|
|
176
|
+
"Before creating a Prototype for a work item, call prototype_list to check whether one already exists to revise.",
|
|
177
|
+
],
|
|
178
|
+
parameters: listParameters,
|
|
179
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
180
|
+
onUpdate?.({
|
|
181
|
+
content: [{ type: "text", text: "Listing Prototypes…" }],
|
|
182
|
+
details: { prototypes: [] },
|
|
183
|
+
});
|
|
184
|
+
const query = params.workItemId
|
|
185
|
+
? `?workItemId=${encodeURIComponent(params.workItemId)}`
|
|
186
|
+
: "";
|
|
187
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes${query}`, { method: "GET", headers: authHeaders(opts) }, { fetchImpl: opts.fetchImpl, signal });
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
throw new Error(await friendlyFetchError("prototype_list", res));
|
|
190
|
+
}
|
|
191
|
+
const data = (await res.json());
|
|
192
|
+
const prototypes = data.prototypes ?? [];
|
|
193
|
+
const text = prototypes.length === 0
|
|
194
|
+
? "No Prototypes found."
|
|
195
|
+
: prototypes
|
|
196
|
+
.map((p) => `- "${p.title}" (revision ${p.currentRevision}): ${absoluteUrl(opts.baseUrl, p.url)}`)
|
|
197
|
+
.join("\n");
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text }],
|
|
200
|
+
details: { prototypes },
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const shareParameters = Type.Object({
|
|
206
|
+
prototypeId: Type.String(),
|
|
207
|
+
});
|
|
208
|
+
/**
|
|
209
|
+
* Build the `prototype_share` tool.
|
|
210
|
+
*
|
|
211
|
+
* Mints a revocable public share link for a Prototype. This is the HUMAN share
|
|
212
|
+
* path (the developer asked in their own terminal), so it is a direct REST call
|
|
213
|
+
* with immediate effect; the in-app agent path stages a Decision instead.
|
|
214
|
+
*/
|
|
215
|
+
export function makePrototypeShareTool(opts) {
|
|
216
|
+
return {
|
|
217
|
+
name: "prototype_share",
|
|
218
|
+
label: "Share prototype",
|
|
219
|
+
description: "Mint a public share link for a YAGNI Prototype so someone outside the workspace can view " +
|
|
220
|
+
"it. Sharing is an explicit act: it records who shared and when, and the link can be " +
|
|
221
|
+
"revoked from the Prototype's page in the app. Returns the share URL and the in-app URL.",
|
|
222
|
+
promptSnippet: "prototype_share: mint a revocable public share link for a Prototype.",
|
|
223
|
+
promptGuidelines: [
|
|
224
|
+
"Only call prototype_share when the user asks to send a Prototype outside the workspace; sharing is an explicit, recorded act.",
|
|
225
|
+
"Give the user both links: the share URL to send out and the in-app URL where the share can be revoked.",
|
|
226
|
+
],
|
|
227
|
+
parameters: shareParameters,
|
|
228
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
229
|
+
onUpdate?.({
|
|
230
|
+
content: [{ type: "text", text: "Minting a share link…" }],
|
|
231
|
+
details: { shareUrl: null },
|
|
232
|
+
});
|
|
233
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(params.prototypeId)}/share`, { method: "POST", headers: authHeaders(opts) }, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
throw new Error(await friendlyFetchError("prototype_share", res));
|
|
236
|
+
}
|
|
237
|
+
const data = (await res.json());
|
|
238
|
+
const shareUrl = absoluteUrl(opts.baseUrl, data.shareUrl);
|
|
239
|
+
const inAppUrl = absoluteUrl(opts.baseUrl, `/prototypes/${params.prototypeId}`);
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: "Share link minted. Anyone with the link can view this Prototype until it is revoked.\n" +
|
|
245
|
+
`Share: ${shareUrl}\n` +
|
|
246
|
+
`Open: ${inAppUrl}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
details: { shareUrl },
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const readParameters = Type.Object({ prototypeId: Type.String(), revision: Type.Integer({ minimum: 1 }) });
|
|
255
|
+
export function makePrototypeReadTool(opts) {
|
|
256
|
+
return {
|
|
257
|
+
name: "prototype_read", label: "Read prototype revision",
|
|
258
|
+
description: "Read the exact prototype revision, HTML, feedback and design provenance. Use the approved revision, never substitute the latest revision.",
|
|
259
|
+
parameters: readParameters,
|
|
260
|
+
async execute(_id, params, signal) {
|
|
261
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(params.prototypeId)}/read?revision=${params.revision}`, { headers: authHeaders(opts) }, { fetchImpl: opts.fetchImpl, signal });
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
throw new Error(await friendlyFetchError("prototype_read", res));
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }], details: data };
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const draftParameters = Type.Object({
|
|
270
|
+
title: Type.String(), spec: Type.String(), workItemId: Type.Optional(Type.String()),
|
|
271
|
+
automatic: Type.Boolean({ default: false }), requestKey: Type.Optional(Type.String()),
|
|
272
|
+
});
|
|
273
|
+
export async function draftPrototype(opts, params, signal) {
|
|
274
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/draft`, {
|
|
275
|
+
method: "POST", headers: authHeaders(opts), body: JSON.stringify({ ...params, sessionId: params.sessionId ?? opts.getSessionId?.() }),
|
|
276
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
277
|
+
if (!res.ok)
|
|
278
|
+
throw new Error(await friendlyFetchError("prototype_draft", res));
|
|
279
|
+
return await res.json();
|
|
280
|
+
}
|
|
281
|
+
export function makePrototypeDraftTool(opts) {
|
|
282
|
+
return {
|
|
283
|
+
name: "prototype_draft", label: "Draft grounded prototype",
|
|
284
|
+
description: "Draft one private interactive UI prototype using repository design context and synthetic examples. Use for new screens, multi-step flows or uncertain interactions before plan approval. Pass automatic=true only for automatic planning; the server enforces eligibility, feature flag, budget and deduplication. Never replace an approved design. Returns created, reused, skipped or failed with a reason.",
|
|
285
|
+
parameters: draftParameters,
|
|
286
|
+
async execute(_id, params, signal) {
|
|
287
|
+
const data = await draftPrototype(opts, params, signal);
|
|
288
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }], details: data };
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const verificationStatus = Type.Union([Type.Literal("passed"), Type.Literal("failed"), Type.Literal("unverified")]);
|
|
293
|
+
const verificationParameters = Type.Object({
|
|
294
|
+
prototypeId: Type.String(), revision: Type.Integer({ minimum: 1 }), status: verificationStatus, summary: Type.String(),
|
|
295
|
+
checks: Type.Array(Type.Object({ name: Type.String(), status: verificationStatus, detail: Type.Optional(Type.String()) })),
|
|
296
|
+
screenshots: Type.Array(Type.Object({ label: Type.String(), url: Type.String() })),
|
|
297
|
+
});
|
|
298
|
+
export function makePrototypeVerificationTool(opts) {
|
|
299
|
+
return {
|
|
300
|
+
name: "prototype_verification", label: "Record design verification",
|
|
301
|
+
description: "Record observed browser visual and interaction checks against the approved prototype revision. Use credential-free browser tools, compare agreed states, and document intentional departures. Use status=unverified when browser checks are unavailable; never report passed without observed results. Screenshots must be real captured evidence. Build the production UI using repository components, not prototype HTML.",
|
|
302
|
+
parameters: verificationParameters,
|
|
303
|
+
async execute(_id, params, signal) {
|
|
304
|
+
const { prototypeId, ...body } = params;
|
|
305
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(prototypeId)}/verification`, { method: "POST", headers: authHeaders(opts), body: JSON.stringify(body) }, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
306
|
+
if (!res.ok)
|
|
307
|
+
throw new Error(await friendlyFetchError("prototype_verification", res));
|
|
308
|
+
const data = await res.json();
|
|
309
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }], details: data };
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const attachParameters = Type.Object({ prototypeId: Type.String(), workItemId: Type.String() });
|
|
314
|
+
export function makePrototypeAttachTool(opts) {
|
|
315
|
+
return {
|
|
316
|
+
name: "prototype_attach", label: "Attach prototype to work item",
|
|
317
|
+
description: "Attach an existing session prototype to a work item so people can review it beside the plan. The server derives its audience from the work item and revokes existing public links. This does not select a revision or approve a plan.",
|
|
318
|
+
parameters: attachParameters,
|
|
319
|
+
async execute(_id, params, signal) {
|
|
320
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(params.prototypeId)}/attach`, {
|
|
321
|
+
method: "POST", headers: authHeaders(opts), body: JSON.stringify({ workItemId: params.workItemId }),
|
|
322
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
323
|
+
if (!res.ok)
|
|
324
|
+
throw new Error(await friendlyFetchError("prototype_attach", res));
|
|
325
|
+
const data = await res.json();
|
|
326
|
+
return { content: [{ type: "text", text: `Attached prototype to work item ${params.workItemId}.` }], details: data };
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=prototypeTool.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { MakePrototypeToolOptions } from "./prototypeTool.js";
|
|
4
|
+
interface Action {
|
|
5
|
+
action: "click" | "fill";
|
|
6
|
+
role: string;
|
|
7
|
+
name: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Viewport {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
interface VerificationInput {
|
|
15
|
+
prototypeId: string;
|
|
16
|
+
revision: number;
|
|
17
|
+
url: string;
|
|
18
|
+
checks: {
|
|
19
|
+
name: string;
|
|
20
|
+
actions?: Action[];
|
|
21
|
+
expectText: string;
|
|
22
|
+
}[];
|
|
23
|
+
viewports?: Viewport[];
|
|
24
|
+
}
|
|
25
|
+
interface VerificationPage {
|
|
26
|
+
navigate(url: string): Promise<void>;
|
|
27
|
+
act(action: Action): Promise<void>;
|
|
28
|
+
hasText(text: string): Promise<boolean>;
|
|
29
|
+
screenshot(): Promise<Buffer>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface VerificationBrowser {
|
|
33
|
+
open(viewport: Viewport, allowRequest: (url: string) => boolean): Promise<VerificationPage>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
interface VerifyOptions extends MakePrototypeToolOptions {
|
|
37
|
+
launchBrowser?: (cwd: string) => Promise<VerificationBrowser>;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
type Status = "passed" | "failed" | "unverified";
|
|
41
|
+
interface VerificationResult {
|
|
42
|
+
revision: number;
|
|
43
|
+
status: Status;
|
|
44
|
+
summary: string;
|
|
45
|
+
checks: {
|
|
46
|
+
name: string;
|
|
47
|
+
status: Status;
|
|
48
|
+
detail?: string;
|
|
49
|
+
}[];
|
|
50
|
+
screenshots: {
|
|
51
|
+
label: string;
|
|
52
|
+
url: string;
|
|
53
|
+
}[];
|
|
54
|
+
}
|
|
55
|
+
export declare function verifyPrototype(opts: VerifyOptions, input: VerificationInput, cwd: string, signal?: AbortSignal): Promise<VerificationResult>;
|
|
56
|
+
declare const parameters: Type.TObject<{
|
|
57
|
+
prototypeId: Type.TString;
|
|
58
|
+
revision: Type.TInteger;
|
|
59
|
+
url: Type.TString;
|
|
60
|
+
checks: Type.TArray<Type.TObject<{
|
|
61
|
+
name: Type.TString;
|
|
62
|
+
expectText: Type.TString;
|
|
63
|
+
actions: Type.TOptional<Type.TArray<Type.TObject<{
|
|
64
|
+
action: Type.TUnion<[Type.TLiteral<"click">, Type.TLiteral<"fill">]>;
|
|
65
|
+
role: Type.TString;
|
|
66
|
+
name: Type.TString;
|
|
67
|
+
value: Type.TOptional<Type.TString>;
|
|
68
|
+
}>>>;
|
|
69
|
+
}>>;
|
|
70
|
+
viewports: Type.TOptional<Type.TArray<Type.TObject<{
|
|
71
|
+
width: Type.TInteger;
|
|
72
|
+
height: Type.TInteger;
|
|
73
|
+
}>>>;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function makePrototypeVerifyTool(opts: VerifyOptions): ToolDefinition<typeof parameters>;
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=prototypeVerify.d.ts.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
5
|
+
import { logEvent } from "./errorSink.js";
|
|
6
|
+
async function launchProjectBrowser(cwd) {
|
|
7
|
+
const require = createRequire(join(cwd, "package.json"));
|
|
8
|
+
let runtime;
|
|
9
|
+
try {
|
|
10
|
+
runtime = require("playwright");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
runtime = require("@playwright/test");
|
|
14
|
+
}
|
|
15
|
+
const browser = await runtime.chromium.launch({ headless: true, timeout: 10_000 });
|
|
16
|
+
return {
|
|
17
|
+
async open(viewport, allow) {
|
|
18
|
+
const context = await browser.newContext({ viewport, serviceWorkers: "block", permissions: [], acceptDownloads: false });
|
|
19
|
+
await context.route("**/*", async (route) => {
|
|
20
|
+
if (allow(route.request().url()))
|
|
21
|
+
await route.continue();
|
|
22
|
+
else
|
|
23
|
+
await route.abort();
|
|
24
|
+
});
|
|
25
|
+
// Block sockets too; request routing alone does not cover WebSockets.
|
|
26
|
+
await context.routeWebSocket("**/*", socket => socket.close());
|
|
27
|
+
const page = await context.newPage();
|
|
28
|
+
return {
|
|
29
|
+
async navigate(url) {
|
|
30
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10_000 });
|
|
31
|
+
if (!allow(page.url()))
|
|
32
|
+
throw new Error("Navigation left the verification origin");
|
|
33
|
+
},
|
|
34
|
+
async act(action) {
|
|
35
|
+
const target = page.getByRole(action.role, { name: action.name, exact: true });
|
|
36
|
+
if (action.action === "click")
|
|
37
|
+
await target.click({ timeout: 3_000 });
|
|
38
|
+
else
|
|
39
|
+
await target.fill(action.value ?? "", { timeout: 3_000 });
|
|
40
|
+
if (!allow(page.url()))
|
|
41
|
+
throw new Error("Navigation left the verification origin");
|
|
42
|
+
},
|
|
43
|
+
async hasText(text) {
|
|
44
|
+
try {
|
|
45
|
+
await page.getByText(text, { exact: false }).first().waitFor({ state: "visible", timeout: 3_000 });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
screenshot: () => page.screenshot({ type: "png", fullPage: false, timeout: 3_000 }),
|
|
53
|
+
close: () => context.close(),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
close: () => browser.close(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function loopbackOrigin(value) {
|
|
60
|
+
const url = new URL(value);
|
|
61
|
+
if (!["http:", "https:"].includes(url.protocol) || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password) {
|
|
62
|
+
throw new Error("Prototype verification requires a credential-free loopback http(s) URL");
|
|
63
|
+
}
|
|
64
|
+
return url.origin;
|
|
65
|
+
}
|
|
66
|
+
/** Race an operation against the run's deadline, so a hung browser call cannot outlive it. */
|
|
67
|
+
function boundedBy(deadline) {
|
|
68
|
+
return async (operation) => {
|
|
69
|
+
if (deadline.aborted) {
|
|
70
|
+
void operation.catch(() => { });
|
|
71
|
+
deadline.throwIfAborted();
|
|
72
|
+
}
|
|
73
|
+
let rejectAbort;
|
|
74
|
+
try {
|
|
75
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
76
|
+
rejectAbort = () => reject(deadline.reason);
|
|
77
|
+
deadline.addEventListener("abort", rejectAbort, { once: true });
|
|
78
|
+
})]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (rejectAbort)
|
|
82
|
+
deadline.removeEventListener("abort", rejectAbort);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** One viewport: navigate, run each check's actions, then the screenshot the reviewer compares. */
|
|
87
|
+
async function verifyViewport(page, input, label, bounded, checkCancelled) {
|
|
88
|
+
const checks = [];
|
|
89
|
+
await bounded(page.navigate(input.url));
|
|
90
|
+
for (const check of input.checks) {
|
|
91
|
+
checkCancelled();
|
|
92
|
+
let passed = false;
|
|
93
|
+
try {
|
|
94
|
+
for (const action of check.actions ?? []) {
|
|
95
|
+
checkCancelled();
|
|
96
|
+
await bounded(page.act(action));
|
|
97
|
+
}
|
|
98
|
+
passed = await bounded(page.hasText(check.expectText));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
checkCancelled();
|
|
102
|
+
}
|
|
103
|
+
checks.push({ name: `${label}: ${check.name}`, status: passed ? "passed" : "failed", detail: passed ? "Expected text observed after the requested interactions." : "Requested interaction or visible text did not match." });
|
|
104
|
+
}
|
|
105
|
+
checkCancelled();
|
|
106
|
+
const png = await bounded(page.screenshot());
|
|
107
|
+
if (png.length > 2 * 1024 * 1024)
|
|
108
|
+
throw new Error("Screenshot exceeds the upload limit");
|
|
109
|
+
return { checks, png };
|
|
110
|
+
}
|
|
111
|
+
function errorReason(error) {
|
|
112
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 200);
|
|
113
|
+
}
|
|
114
|
+
export async function verifyPrototype(opts, input, cwd, signal) {
|
|
115
|
+
const origin = loopbackOrigin(input.url);
|
|
116
|
+
if (!Number.isInteger(input.revision) || input.revision < 1 || input.checks.length < 1 || input.checks.length > 8 || input.checks.some(check => (check.actions?.length ?? 0) > 8))
|
|
117
|
+
throw new Error("Verification requires a revision and 1–8 bounded checks");
|
|
118
|
+
const viewports = input.viewports ?? [{ width: 390, height: 844 }, { width: 1280, height: 800 }];
|
|
119
|
+
if (viewports.length < 1 || viewports.length > 3 || viewports.some(v => !Number.isInteger(v.width) || !Number.isInteger(v.height) || v.width < 320 || v.width > 1920 || v.height < 320 || v.height > 1200))
|
|
120
|
+
throw new Error("Invalid verification viewports");
|
|
121
|
+
const result = { revision: input.revision, status: "unverified", summary: "Browser verification unavailable.", checks: [], screenshots: [] };
|
|
122
|
+
const allow = (url) => { try {
|
|
123
|
+
return new URL(url).origin === origin;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
} };
|
|
128
|
+
const deadline = new AbortController();
|
|
129
|
+
const post = async (suffix, body) => {
|
|
130
|
+
const response = await resilientFetch(`${opts.baseUrl}/api/yagni-code/prototypes/${encodeURIComponent(input.prototypeId)}/${suffix}`, {
|
|
131
|
+
method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${opts.getToken() ?? ""}` }, body: JSON.stringify(body),
|
|
132
|
+
}, { fetchImpl: opts.fetchImpl, signal: suffix === "verification" ? signal : deadline.signal, policy: METERED_POST_FETCH_POLICY });
|
|
133
|
+
if (!response.ok)
|
|
134
|
+
throw new Error(await friendlyFetchError("prototype_verify", response));
|
|
135
|
+
return await response.json();
|
|
136
|
+
};
|
|
137
|
+
let browser;
|
|
138
|
+
let timedOut = false;
|
|
139
|
+
let closing;
|
|
140
|
+
const close = () => { if (browser && !closing)
|
|
141
|
+
closing = browser.close().catch(() => { }); };
|
|
142
|
+
const abort = () => { deadline.abort(signal?.reason); close(); };
|
|
143
|
+
const timer = setTimeout(() => { timedOut = true; deadline.abort(new Error("Verification timed out")); close(); }, Math.min(opts.timeoutMs ?? 45_000, 45_000));
|
|
144
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
145
|
+
const bounded = boundedBy(deadline.signal);
|
|
146
|
+
const checkCancelled = () => {
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
if (timedOut)
|
|
149
|
+
throw new Error("Verification timed out");
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
checkCancelled();
|
|
153
|
+
const activeBrowser = await bounded((opts.launchBrowser ?? launchProjectBrowser)(cwd).then(launched => {
|
|
154
|
+
if (deadline.signal.aborted)
|
|
155
|
+
void launched.close().catch(() => { });
|
|
156
|
+
return launched;
|
|
157
|
+
}));
|
|
158
|
+
browser = activeBrowser;
|
|
159
|
+
checkCancelled();
|
|
160
|
+
for (const viewport of viewports) {
|
|
161
|
+
checkCancelled();
|
|
162
|
+
const page = await bounded(activeBrowser.open(viewport, allow));
|
|
163
|
+
try {
|
|
164
|
+
const label = `${viewport.width}x${viewport.height}`;
|
|
165
|
+
const { checks, png } = await verifyViewport(page, input, label, bounded, checkCancelled);
|
|
166
|
+
result.checks.push(...checks);
|
|
167
|
+
const uploaded = await post("screenshots", { revision: input.revision, label, pngBase64: png.toString("base64") });
|
|
168
|
+
if (!uploaded.url)
|
|
169
|
+
throw new Error("Screenshot upload returned no evidence URL");
|
|
170
|
+
result.screenshots.push({ label, url: uploaded.url });
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await bounded(page.close()).catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
checkCancelled();
|
|
177
|
+
result.status = result.checks.every(check => check.status === "passed") && result.screenshots.length === viewports.length ? "passed" : "failed";
|
|
178
|
+
result.summary = result.status === "passed" ? "Requested interactions and visible text passed at every viewport; screenshots captured for visual review." : "One or more requested browser checks failed; review the captured screenshots.";
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
signal?.throwIfAborted();
|
|
182
|
+
// Every failure records the same honest status, but keeps its cause: a
|
|
183
|
+
// missing Playwright, a crashed browser and a failed upload need
|
|
184
|
+
// different fixes.
|
|
185
|
+
const reason = errorReason(error);
|
|
186
|
+
logEvent({ source: "tool", level: "warn", event: "prototype_verify.unavailable", fields: { timedOut, reason } });
|
|
187
|
+
result.status = "unverified";
|
|
188
|
+
result.summary = timedOut
|
|
189
|
+
? "Browser verification exceeded its time limit; incomplete checks remain unverified."
|
|
190
|
+
: `Browser, local app, or screenshot evidence unavailable (${reason}); verification is incomplete.`;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
signal?.removeEventListener("abort", abort);
|
|
195
|
+
close();
|
|
196
|
+
if (closing)
|
|
197
|
+
await Promise.race([closing, new Promise(resolve => { const cleanupTimer = setTimeout(resolve, 2_000); cleanupTimer.unref(); })]);
|
|
198
|
+
}
|
|
199
|
+
signal?.throwIfAborted();
|
|
200
|
+
await post("verification", result);
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
const parameters = Type.Object({
|
|
204
|
+
prototypeId: Type.String(), revision: Type.Integer({ minimum: 1 }), url: Type.String(),
|
|
205
|
+
checks: Type.Array(Type.Object({ name: Type.String(), expectText: Type.String(), actions: Type.Optional(Type.Array(Type.Object({ action: Type.Union([Type.Literal("click"), Type.Literal("fill")]), role: Type.String(), name: Type.String(), value: Type.Optional(Type.String()) }), { maxItems: 8 })) }), { minItems: 1, maxItems: 8 }),
|
|
206
|
+
viewports: Type.Optional(Type.Array(Type.Object({ width: Type.Integer({ minimum: 320, maximum: 1920 }), height: Type.Integer({ minimum: 320, maximum: 1200 }) }), { minItems: 1, maxItems: 3 })),
|
|
207
|
+
});
|
|
208
|
+
export function makePrototypeVerifyTool(opts) {
|
|
209
|
+
return {
|
|
210
|
+
name: "prototype_verify", label: "Verify prototype implementation",
|
|
211
|
+
description: "Run real browser interaction checks on the implemented local app against an exact prototype revision and capture desktop/mobile screenshots for visual review. Supply agreed state checks from prototype_read and synthetic fill values only. Uses existing project Playwright without installing anything, in a fresh credential-free context restricted to the local app origin. Records passed/failed observed checks or explicitly unverified when the browser/app/evidence is unavailable. Screenshots support human visual comparison; passing text checks does not prove visual equivalence.",
|
|
212
|
+
parameters,
|
|
213
|
+
async execute(_id, input, signal, _update, context) {
|
|
214
|
+
const result = await verifyPrototype(opts, input, context.cwd, signal);
|
|
215
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=prototypeVerify.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.3-staging.
|
|
3
|
+
"version": "1.2.3-staging.1580.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "8592117c432a85b840ac26dd74f48e84d6bb3a1a"
|
|
62
62
|
}
|