@yagni-app/code-staging 1.2.3-staging.1570.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/askAdvisorTool.d.ts +6 -1
- package/dist/extension/askAdvisorTool.js +6 -1
- 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/budget.d.ts +32 -0
- package/dist/extension/pipeline/budget.js +29 -0
- package/dist/extension/pipeline/goCommand.d.ts +3 -0
- package/dist/extension/pipeline/goCommand.js +30 -0
- package/dist/extension/pipeline/orchestrator.d.ts +22 -1
- package/dist/extension/pipeline/orchestrator.js +42 -9
- package/dist/extension/pipeline/resilience.d.ts +18 -0
- package/dist/extension/pipeline/resilience.js +6 -2
- package/dist/extension/pipeline/stages.js +5 -5
- package/dist/extension/pipeline/types.d.ts +9 -6
- package/dist/extension/pipeline/types.js +8 -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/dist/extension/subagents.d.ts +3 -2
- package/dist/extension/subagents.js +3 -2
- package/package.json +2 -2
|
@@ -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
|