@taiga-ai/mcp-server 0.12.0-dev.421 → 0.13.0-dev.422
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/index.js +348 -85
- package/package.json +1 -1
- package/src/client.ts +34 -21
- package/src/server.integration.test.ts +295 -92
- package/src/tools/annotations.ts +6 -0
- package/src/tools/backlogs.ts +118 -58
- package/src/tools/builds.ts +13 -29
- package/src/tools/index.ts +2 -0
- package/src/tools/instructions.ts +19 -13
- package/src/tools/jobs.ts +89 -0
- package/src/tools/queue.ts +61 -4
- package/src/tools/request-prompt.test.ts +32 -0
- package/src/tools/request-prompt.ts +28 -0
- package/src/tools/wait.ts +38 -0
- package/taiga-ai-mcp-server-0.13.0-dev.422.tgz +0 -0
- package/taiga-ai-mcp-server-0.12.0-dev.421.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -211,6 +211,21 @@ var TaigaClient = class {
|
|
|
211
211
|
const qs = params.toString();
|
|
212
212
|
return this.request("GET", `/products/${seg(productId)}/documents${qs ? `?${qs}` : ""}`);
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Stop or start the product's line. Stopping halts pulls and builds and
|
|
216
|
+
* stops the plan and build in flight; starting picks the interrupted work
|
|
217
|
+
* back up.
|
|
218
|
+
*/
|
|
219
|
+
async setLineStopped(productId, stopped) {
|
|
220
|
+
return this.request("PUT", `/products/${seg(productId)}/line`, { stopped });
|
|
221
|
+
}
|
|
222
|
+
/** Ask an agent for work: the job is queued and runs in the background. */
|
|
223
|
+
async createAgentJob(body) {
|
|
224
|
+
return this.request("POST", "/agent-jobs", body);
|
|
225
|
+
}
|
|
226
|
+
async getAgentJob(id, signal) {
|
|
227
|
+
return this.request("GET", `/agent-jobs/${seg(id)}`, void 0, { signal });
|
|
228
|
+
}
|
|
214
229
|
async getProductDocument(productId, type, origin) {
|
|
215
230
|
const qs = origin ? `?${new URLSearchParams({ origin })}` : "";
|
|
216
231
|
return this.request("GET", `/products/${seg(productId)}/documents/${seg(type)}${qs}`);
|
|
@@ -229,12 +244,6 @@ var TaigaClient = class {
|
|
|
229
244
|
async getBacklogItem(itemId, signal) {
|
|
230
245
|
return this.request("GET", `/backlog/${seg(itemId)}`, void 0, { signal });
|
|
231
246
|
}
|
|
232
|
-
async createBacklogItem(productId, body) {
|
|
233
|
-
return this.request("POST", `/products/${seg(productId)}/backlog`, {
|
|
234
|
-
...body,
|
|
235
|
-
productId
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
247
|
/** A person's move on the line board: the column dropped into, and what to follow there. */
|
|
239
248
|
async transitionInitiative(itemId, body) {
|
|
240
249
|
return this.request("PUT", `/backlog/${seg(itemId)}/state`, body);
|
|
@@ -283,9 +292,6 @@ var TaigaClient = class {
|
|
|
283
292
|
async stopBuildRun(id) {
|
|
284
293
|
return this.request("POST", `/builds/${seg(id)}/stop`);
|
|
285
294
|
}
|
|
286
|
-
async resumeBuildRun(id) {
|
|
287
|
-
return this.request("POST", `/builds/${seg(id)}/resume`);
|
|
288
|
-
}
|
|
289
295
|
async listBuildRunSteps(id) {
|
|
290
296
|
return this.request("GET", `/builds/${seg(id)}/steps`);
|
|
291
297
|
}
|
|
@@ -329,13 +335,6 @@ const READ_ONLY = {
|
|
|
329
335
|
idempotentHint: true,
|
|
330
336
|
openWorldHint: false
|
|
331
337
|
};
|
|
332
|
-
/** A write that adds or edits a row and starts nothing: a created initiative, a moved one. */
|
|
333
|
-
const WRITE = {
|
|
334
|
-
readOnlyHint: false,
|
|
335
|
-
destructiveHint: false,
|
|
336
|
-
idempotentHint: false,
|
|
337
|
-
openWorldHint: false
|
|
338
|
-
};
|
|
339
338
|
/**
|
|
340
339
|
* A write that starts or stops paid work, or opens the door to it: queueing
|
|
341
340
|
* an initiative, approving a plan, resuming or stopping a run. Each starts
|
|
@@ -347,6 +346,14 @@ const OPERATE = {
|
|
|
347
346
|
idempotentHint: false,
|
|
348
347
|
openWorldHint: false
|
|
349
348
|
};
|
|
349
|
+
/**
|
|
350
|
+
* An OPERATE write that sets a value rather than adding one: the same call
|
|
351
|
+
* twice leaves things as one call does (a review gate set, a line paused).
|
|
352
|
+
*/
|
|
353
|
+
const OPERATE_SETTING = {
|
|
354
|
+
...OPERATE,
|
|
355
|
+
idempotentHint: true
|
|
356
|
+
};
|
|
350
357
|
//#endregion
|
|
351
358
|
//#region src/tools/inputs.ts
|
|
352
359
|
/** The inputs several tools share, so a product or a page is named the same way everywhere. */
|
|
@@ -617,15 +624,7 @@ const PR_CLOSING_STATES = [
|
|
|
617
624
|
"todo",
|
|
618
625
|
"canceled"
|
|
619
626
|
];
|
|
620
|
-
|
|
621
|
-
* What `PUT /backlog/:id/state` accepts: the drops, plus `plan`.
|
|
622
|
-
*
|
|
623
|
-
* `plan` is not a column a person drops into any more, but it is still a state
|
|
624
|
-
* the endpoint takes -- it is how the Build station is told to start over
|
|
625
|
-
* ("Plan again"). One endpoint for both because they are the same write: the
|
|
626
|
-
* initiative's state, decided by a person.
|
|
627
|
-
*/
|
|
628
|
-
const INITIATIVE_TRANSITION_STATES = [...USER_DROP_STATES, "plan"];
|
|
627
|
+
[...USER_DROP_STATES];
|
|
629
628
|
/** Longest rejection comment on a plan (PRO-1324): a paragraph of direction,
|
|
630
629
|
* small enough that the prompt section it becomes stays a section. */
|
|
631
630
|
const PLAN_REJECTION_COMMENT_MAX_CHARS = 2e3;
|
|
@@ -965,6 +964,91 @@ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDispositio
|
|
|
965
964
|
*/
|
|
966
965
|
const BUILD_RUN_TERMINAL_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
|
|
967
966
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region ../../packages/types/dist/agents.js
|
|
969
|
+
/**
|
|
970
|
+
* Why a PROMPTED `generate_backlog` job planned nothing.
|
|
971
|
+
*
|
|
972
|
+
* A prompted add asks the planner a question, and "no new initiative" is one of
|
|
973
|
+
* its honest answers -- the backlog already covers the request, the request is
|
|
974
|
+
* not a piece of work, or it is outside what this project builds. None of those
|
|
975
|
+
* is a failed job, so each arrives as a completed one carrying the reason.
|
|
976
|
+
*
|
|
977
|
+
* The INITIAL bulk generate has no prompt and no such answer: an empty backlog
|
|
978
|
+
* there is a broken run and still fails.
|
|
979
|
+
*/
|
|
980
|
+
const BACKLOG_GENERATION_EMPTY_OUTCOMES = [
|
|
981
|
+
"already_covered",
|
|
982
|
+
"invalid_request",
|
|
983
|
+
"out_of_scope",
|
|
984
|
+
"needs_decision"
|
|
985
|
+
];
|
|
986
|
+
function isRecord$1(value) {
|
|
987
|
+
return typeof value === "object" && value !== null;
|
|
988
|
+
}
|
|
989
|
+
function isStringOrNull(value) {
|
|
990
|
+
return value === null || typeof value === "string";
|
|
991
|
+
}
|
|
992
|
+
function isDecisionReason(value) {
|
|
993
|
+
if (!isRecord$1(value)) return false;
|
|
994
|
+
const { policyCode, quotedSpan, conflict, alternative, overridePath } = value;
|
|
995
|
+
return [
|
|
996
|
+
policyCode,
|
|
997
|
+
quotedSpan,
|
|
998
|
+
conflict,
|
|
999
|
+
alternative,
|
|
1000
|
+
overridePath
|
|
1001
|
+
].every((f) => typeof f === "string");
|
|
1002
|
+
}
|
|
1003
|
+
function isCoveringInitiative(value) {
|
|
1004
|
+
return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.title === "string" && typeof value.priority === "number";
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Narrow an `agent_jobs.result` blob to {@link BacklogGenerationEmptyResult}.
|
|
1008
|
+
*
|
|
1009
|
+
* The blob is whatever the worker stored, and the reader navigates on
|
|
1010
|
+
* `coveredInitiative.id` and renders `message`, so EVERY field is checked: a
|
|
1011
|
+
* blob that only has the right `outcome` is not this shape, and narrowing it
|
|
1012
|
+
* would hand the page an undefined id to navigate to.
|
|
1013
|
+
*/
|
|
1014
|
+
function isBacklogGenerationEmptyResult(value) {
|
|
1015
|
+
if (!isRecord$1(value)) return false;
|
|
1016
|
+
const { outcome, message, coveredByPriority, coveredById, coveredInitiative, reason } = value;
|
|
1017
|
+
return typeof outcome === "string" && BACKLOG_GENERATION_EMPTY_OUTCOMES.includes(outcome) && isStringOrNull(message) && (coveredByPriority === null || typeof coveredByPriority === "number" && Number.isInteger(coveredByPriority)) && isStringOrNull(coveredById) && (coveredInitiative === null || isCoveringInitiative(coveredInitiative)) && (reason === null || reason === void 0 || isDecisionReason(reason));
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* The initiatives a `generate_backlog` job created, from its stored result
|
|
1021
|
+
* (PRO-2483), or null when the result does not say: a job finished before the
|
|
1022
|
+
* worker recorded them, a verdict that created nothing, or a blob that is not
|
|
1023
|
+
* this shape. Every entry must be a non-empty string, or none is trusted --
|
|
1024
|
+
* the reader navigates on them.
|
|
1025
|
+
*/
|
|
1026
|
+
function createdInitiativeIdsOf(result) {
|
|
1027
|
+
if (!isRecord$1(result)) return null;
|
|
1028
|
+
const ids = result.createdInitiativeIds;
|
|
1029
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) return null;
|
|
1030
|
+
return ids;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* The initiatives a `generate_backlog` run added, each with the title it was
|
|
1034
|
+
* created with, as the worker writes them on `agent_jobs.result.createdInitiatives`
|
|
1035
|
+
* (PRO-2496), in the order they were created: what a reader needs to NAME
|
|
1036
|
+
* them, where {@link createdInitiativeIdsOf} (PRO-2483) only links them.
|
|
1037
|
+
*
|
|
1038
|
+
* Null when the result carries no such list: a run from before PRO-2496, which
|
|
1039
|
+
* may still carry the ids alone, a run that added nothing (see
|
|
1040
|
+
* {@link isBacklogGenerationEmptyResult}), or a run still going. A list with a malformed entry is null too rather than partly
|
|
1041
|
+
* read, so a reader never links an undefined id.
|
|
1042
|
+
*/
|
|
1043
|
+
function createdInitiativesOf(result) {
|
|
1044
|
+
if (!isRecord$1(result)) return null;
|
|
1045
|
+
const list = result.createdInitiatives;
|
|
1046
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
1047
|
+
return list.every((entry) => isRecord$1(entry) && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.title === "string") ? list.map((entry) => ({
|
|
1048
|
+
id: entry.id,
|
|
1049
|
+
title: entry.title
|
|
1050
|
+
})) : null;
|
|
1051
|
+
}
|
|
968
1052
|
[...TURN_EVENT_TRIGGERS];
|
|
969
1053
|
//#endregion
|
|
970
1054
|
//#region ../../packages/types/dist/maintenance.js
|
|
@@ -2163,6 +2247,105 @@ function registerDocumentTools(server, client) {
|
|
|
2163
2247
|
}
|
|
2164
2248
|
}
|
|
2165
2249
|
//#endregion
|
|
2250
|
+
//#region src/tools/jobs.ts
|
|
2251
|
+
/** What a job says about itself: its kind, how far it got, and why it failed. */
|
|
2252
|
+
const JOB_FIELDS = [
|
|
2253
|
+
"id",
|
|
2254
|
+
"type",
|
|
2255
|
+
"documentType",
|
|
2256
|
+
"targetId",
|
|
2257
|
+
"status",
|
|
2258
|
+
"progress",
|
|
2259
|
+
"progressMessage",
|
|
2260
|
+
"error",
|
|
2261
|
+
"queuePosition",
|
|
2262
|
+
"createdAt",
|
|
2263
|
+
"startedAt",
|
|
2264
|
+
"completedAt",
|
|
2265
|
+
"cancelledAt"
|
|
2266
|
+
];
|
|
2267
|
+
/** A job that will not change again: it finished, failed, or was cancelled. */
|
|
2268
|
+
const SETTLED_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
2269
|
+
"completed",
|
|
2270
|
+
"failed",
|
|
2271
|
+
"cancelled"
|
|
2272
|
+
]);
|
|
2273
|
+
/**
|
|
2274
|
+
* What an initiative request came to, read off its finished job: the
|
|
2275
|
+
* initiatives it added, or the answer that added none (the backlog already
|
|
2276
|
+
* covers it, it is not work to build, it is outside the product, or one of
|
|
2277
|
+
* the organization's policies does not allow it as asked, with the policy's
|
|
2278
|
+
* own words and the way to decide otherwise). Null while the job runs, and
|
|
2279
|
+
* for a job that is not an initiative request.
|
|
2280
|
+
*/
|
|
2281
|
+
function requestOutcome(job) {
|
|
2282
|
+
if (job.type !== "generate_backlog" || job.status !== "completed") return null;
|
|
2283
|
+
const created = createdInitiativesOf(job.result);
|
|
2284
|
+
if (created) return {
|
|
2285
|
+
outcome: "created",
|
|
2286
|
+
initiatives: created
|
|
2287
|
+
};
|
|
2288
|
+
const ids = createdInitiativeIdsOf(job.result);
|
|
2289
|
+
if (ids && ids.length > 0) return {
|
|
2290
|
+
outcome: "created",
|
|
2291
|
+
initiatives: ids.map((id) => ({ id }))
|
|
2292
|
+
};
|
|
2293
|
+
if (isBacklogGenerationEmptyResult(job.result)) {
|
|
2294
|
+
const result = job.result;
|
|
2295
|
+
return {
|
|
2296
|
+
outcome: result.outcome,
|
|
2297
|
+
message: result.message,
|
|
2298
|
+
coveredBy: result.coveredInitiative,
|
|
2299
|
+
policy: result.reason ?? null
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
outcome: "unknown",
|
|
2304
|
+
note: "The job finished without a result this server can read."
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
/** A job as the tools hand it over: its own fields, and for an initiative request, what came of it. */
|
|
2308
|
+
function jobReport(job) {
|
|
2309
|
+
const outcome = requestOutcome(job);
|
|
2310
|
+
return {
|
|
2311
|
+
...pickFields([job], JOB_FIELDS)[0],
|
|
2312
|
+
...outcome ? { outcome } : {}
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
function registerJobTools(server, client) {
|
|
2316
|
+
if (!client.hasCapability("products:read")) return;
|
|
2317
|
+
server.registerTool("taiga_get_job", {
|
|
2318
|
+
title: "Get an agent job",
|
|
2319
|
+
description: "Read one agent job: background work a person asked an agent for, such as an initiative request (taiga_request_initiatives). It says where the job is (pending, processing, completed, failed, cancelled), its progress and any error. A finished initiative request also says what came of it: the initiatives it added, or why it added none (already covered by an existing initiative, not readable as work, outside the product, or not allowed by a policy, with the policy, the line it runs into and the way to decide otherwise). taiga_wait_for_job waits for a job to finish.",
|
|
2320
|
+
inputSchema: { jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it") },
|
|
2321
|
+
annotations: READ_ONLY
|
|
2322
|
+
}, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
|
|
2323
|
+
}
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region src/tools/request-prompt.ts
|
|
2326
|
+
/**
|
|
2327
|
+
* The planner's prompt for an initiative request, built as the web builds it
|
|
2328
|
+
* (apps/web/src/features/loops/delivery/components/planning/generateBacklogJob.ts):
|
|
2329
|
+
* title and description trimmed, the description's inline-image markdown
|
|
2330
|
+
* removed and the blank lines it leaves collapsed, joined by a blank line.
|
|
2331
|
+
* The planner reads `userPrompt` and nothing else of the request, so the same
|
|
2332
|
+
* words must make the same prompt from either surface.
|
|
2333
|
+
*
|
|
2334
|
+
* The image pattern is the web's (inline-image-doc-ids.ts), which keeps it in
|
|
2335
|
+
* lockstep with the API's `extractInlineImageDocIds`: the `` form,
|
|
2336
|
+
* an exact UUID, and a terminator after `/image`.
|
|
2337
|
+
*/
|
|
2338
|
+
const INLINE_IMAGE_RE = /!\[(?:[^\]\\]|\\.)*\]\([^)]*?\/custom-documents\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/image(?=[)?#\s])[^)]*\)/g;
|
|
2339
|
+
/** The description without its inline images, as the web strips it. */
|
|
2340
|
+
function stripInlineImages(markdown) {
|
|
2341
|
+
return markdown.replace(INLINE_IMAGE_RE, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
2342
|
+
}
|
|
2343
|
+
/** The prompt for a title and a description; undefined when both are empty, which asks for a suggestion. */
|
|
2344
|
+
function requestPrompt(title, description) {
|
|
2345
|
+
const body = stripInlineImages((description ?? "").trim());
|
|
2346
|
+
return [(title ?? "").trim(), body].filter(Boolean).join("\n\n") || void 0;
|
|
2347
|
+
}
|
|
2348
|
+
//#endregion
|
|
2166
2349
|
//#region src/tools/queue.ts
|
|
2167
2350
|
/** How many entries the read names; past this an entry keeps its backlogItemId only. */
|
|
2168
2351
|
const NAMED_ENTRIES = 25;
|
|
@@ -2220,7 +2403,8 @@ function waitingOn(rows) {
|
|
|
2220
2403
|
* rather than failing the queue.
|
|
2221
2404
|
*/
|
|
2222
2405
|
async function queueView(client, product, options = { name: "all" }) {
|
|
2223
|
-
const
|
|
2406
|
+
const [queue, paused] = await Promise.all([client.listQueue(product), linePausedSince(client, product)]);
|
|
2407
|
+
const entries = queue.data.filter((entry) => entry.state !== "settled");
|
|
2224
2408
|
const canName = client.hasCapability("backlog:read");
|
|
2225
2409
|
const blocking = blockingIndex(entries, (entry) => entry.state);
|
|
2226
2410
|
const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
|
|
@@ -2271,20 +2455,52 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
2271
2455
|
return row;
|
|
2272
2456
|
});
|
|
2273
2457
|
const note = unnamed === 0 ? null : !canName ? "Entries carry backlogItemId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry backlogItemId only.` : "An entry whose initiative could not be read carries backlogItemId only; taiga_get_backlog_item reads it.";
|
|
2458
|
+
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
2274
2459
|
return {
|
|
2460
|
+
linePaused: paused === null ? null : typeof paused === "string",
|
|
2275
2461
|
entries: rows,
|
|
2276
|
-
waitingOn: waitingOn(rows),
|
|
2462
|
+
waitingOn: line ?? waitingOn(rows),
|
|
2277
2463
|
...note ? { note } : {}
|
|
2278
2464
|
};
|
|
2279
2465
|
}
|
|
2466
|
+
/**
|
|
2467
|
+
* When the product's line was paused, false when it runs, null when the
|
|
2468
|
+
* product cannot be read (the queue is still worth reading without it).
|
|
2469
|
+
*/
|
|
2470
|
+
async function linePausedSince(client, product) {
|
|
2471
|
+
try {
|
|
2472
|
+
const row = (await client.getProduct(product)).data;
|
|
2473
|
+
return typeof row.lineStoppedAt === "string" ? row.lineStoppedAt : false;
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
2476
|
+
throw error;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2280
2479
|
function registerQueueTools(server, client) {
|
|
2281
2480
|
if (!client.hasCapability("products:read")) return;
|
|
2282
2481
|
server.registerTool("taiga_get_queue", {
|
|
2283
2482
|
title: "Get the product queue",
|
|
2284
|
-
description: "Read a product's queue, the line the coding agent works one initiative at a time: each entry in order with its title, where it is (planning, building, in review) and whose turn it is, a halted entry with why it stopped and what lifts it, and waitingOn, one sentence saying what the line is waiting for. The line advances only when the active entry's pull request merges, so an entry waiting on a review holds everything behind it. Use this when a queued initiative is not moving.",
|
|
2483
|
+
description: "Read a product's queue, the line the coding agent works one initiative at a time: each entry in order with its title, where it is (planning, building, in review) and whose turn it is, a halted entry with why it stopped and what lifts it, linePaused (null when the product could not be read), and waitingOn, one sentence saying what the line is waiting for. The line advances only when the active entry's pull request merges, so an entry waiting on a review holds everything behind it. Use this when a queued initiative is not moving.",
|
|
2285
2484
|
inputSchema: { productId },
|
|
2286
2485
|
annotations: READ_ONLY
|
|
2287
2486
|
}, (params) => runTool(async () => toolResult(await queueView(client, params.productId))));
|
|
2487
|
+
if (!client.hasCapability("products:write")) return;
|
|
2488
|
+
server.registerTool("taiga_pause_line", {
|
|
2489
|
+
title: "Pause or resume a product's line",
|
|
2490
|
+
description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read taiga_get_queue first to see what it would interrupt, and ask the person.",
|
|
2491
|
+
inputSchema: {
|
|
2492
|
+
productId,
|
|
2493
|
+
paused: z.boolean().describe("true pauses the line, false resumes it")
|
|
2494
|
+
},
|
|
2495
|
+
annotations: OPERATE_SETTING
|
|
2496
|
+
}, (params) => runTool(async () => {
|
|
2497
|
+
const row = (await client.setLineStopped(params.productId, params.paused)).data;
|
|
2498
|
+
return toolResult({
|
|
2499
|
+
productId: row.id,
|
|
2500
|
+
linePaused: typeof row.lineStoppedAt === "string",
|
|
2501
|
+
lineStoppedAt: row.lineStoppedAt ?? null
|
|
2502
|
+
});
|
|
2503
|
+
}));
|
|
2288
2504
|
}
|
|
2289
2505
|
//#endregion
|
|
2290
2506
|
//#region src/tools/backlogs.ts
|
|
@@ -2352,23 +2568,32 @@ const outcomeOf = (item) => {
|
|
|
2352
2568
|
const outcome = (item) => toolResult(outcomeOf(item));
|
|
2353
2569
|
/**
|
|
2354
2570
|
* Each tool is registered under the capability its route requires, so a
|
|
2355
|
-
* key sees exactly what it may call: reads on backlog:read
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2358
|
-
* resume and the review gate on products:write alone, as
|
|
2359
|
-
* (the item ID they take also comes back from a
|
|
2360
|
-
* products:write together with backlog:read,
|
|
2361
|
-
* comes only from the plan read
|
|
2362
|
-
*
|
|
2571
|
+
* key sees exactly what it may call: reads on backlog:read; the board move on
|
|
2572
|
+
* backlog:write; queueing on both backlog:write and products:write (the
|
|
2573
|
+
* state route checks both for a drop into queue); an initiative request
|
|
2574
|
+
* (an agent job), the resume and the review gate on products:write alone, as
|
|
2575
|
+
* their routes are (the item ID they take also comes back from a request or a
|
|
2576
|
+
* queue); the plan verdicts on products:write together with backlog:read,
|
|
2577
|
+
* because the plan ID they take comes only from the plan read; and Plan again
|
|
2578
|
+
* on the state route's two (backlog:write, then products:write for a move
|
|
2579
|
+
* into plan) together with backlog:read, because it reads the initiative's
|
|
2580
|
+
* place at the Build station first.
|
|
2581
|
+
*
|
|
2582
|
+
* The tools follow how Taiga is meant to be used (docs.tai.ga, Deliver):
|
|
2583
|
+
* new work is described and an agent writes the initiative; the queue is
|
|
2584
|
+
* the only way to start work; a run is stopped by moving its initiative out
|
|
2585
|
+
* of Build and carried on from the initiative.
|
|
2363
2586
|
*/
|
|
2364
2587
|
function registerBacklogTools(server, client) {
|
|
2365
2588
|
const may = (capability) => client.hasCapability(capability);
|
|
2366
2589
|
if (may("backlog:read")) registerBacklogReads(server, client);
|
|
2367
|
-
if (may("backlog:write"))
|
|
2590
|
+
if (may("backlog:write")) registerMove(server, client);
|
|
2591
|
+
if (may("products:write")) registerRequest(server, client);
|
|
2368
2592
|
if (may("backlog:write") && may("products:write")) registerQueue(server, client);
|
|
2369
2593
|
if (may("products:write")) registerResume(server, client);
|
|
2370
2594
|
if (may("products:write")) registerReviewGate(server, client);
|
|
2371
2595
|
if (may("backlog:read") && may("products:write")) registerPlanVerdicts(server, client);
|
|
2596
|
+
if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
|
|
2372
2597
|
}
|
|
2373
2598
|
function registerBacklogReads(server, client) {
|
|
2374
2599
|
server.registerTool("taiga_list_backlog_items", {
|
|
@@ -2428,46 +2653,65 @@ function registerBacklogReads(server, client) {
|
|
|
2428
2653
|
});
|
|
2429
2654
|
}));
|
|
2430
2655
|
}
|
|
2431
|
-
function
|
|
2432
|
-
server.registerTool("
|
|
2433
|
-
title: "
|
|
2434
|
-
description: "
|
|
2656
|
+
function registerRequest(server, client) {
|
|
2657
|
+
server.registerTool("taiga_request_initiatives", {
|
|
2658
|
+
title: "Request new initiatives",
|
|
2659
|
+
description: "Add work the way Taiga adds it: describe it, and an agent turns the request into initiatives in the product's Backlog. The agent reads the product documents and its repository, expands the request into a full brief, and sizes it: one piece of work stays one initiative, a program of work comes back as an ordered set with its dependencies linked. It can also add nothing, and says why: an existing initiative already covers the request, the request is not readable as work, it is outside what the product builds, or one of the organization's policies does not allow it as asked (named, with the line it runs into and the way to decide otherwise). Leave both title and description out and the agent suggests the one next initiative the product most needs. It runs in the background, a few minutes: this returns the job, and taiga_wait_for_job or taiga_get_job (both need products:read) say what came of it. One request per product at a time (CONFLICT while one runs). Inline images in the description are left out; the web is where a request carries images. It starts nothing on the line: new initiatives land in Backlog. Runs an agent; ask the person first.",
|
|
2435
2660
|
inputSchema: {
|
|
2436
2661
|
productId,
|
|
2437
|
-
title: z.string().
|
|
2438
|
-
description: z.string().
|
|
2439
|
-
category: z.enum([
|
|
2440
|
-
"feature",
|
|
2441
|
-
"bug",
|
|
2442
|
-
"tech_debt",
|
|
2443
|
-
"improvement"
|
|
2444
|
-
]).optional().describe("What kind of work (default feature)"),
|
|
2445
|
-
priority: z.number().int().min(1).optional().describe("1 is the most urgent (default 3)"),
|
|
2446
|
-
estimate: z.number().int().min(0).optional().describe("Story points, 0 for unestimated (default 0)")
|
|
2662
|
+
title: z.string().max(500).optional().describe("What the work is, in a line"),
|
|
2663
|
+
description: z.string().max(2e4).optional().describe("The request in Markdown: the problem, what it should look like, what it covers and leaves alone")
|
|
2447
2664
|
},
|
|
2448
|
-
annotations:
|
|
2665
|
+
annotations: OPERATE
|
|
2449
2666
|
}, (params) => runTool(async () => {
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2667
|
+
const userPrompt = requestPrompt(params.title, params.description);
|
|
2668
|
+
const job = (await client.createAgentJob({
|
|
2669
|
+
scope: "product",
|
|
2670
|
+
type: "generate_backlog",
|
|
2671
|
+
targetId: params.productId,
|
|
2672
|
+
...userPrompt ? { metadata: { userPrompt } } : {}
|
|
2673
|
+
})).data;
|
|
2674
|
+
return toolResult(jobReport(job));
|
|
2452
2675
|
}));
|
|
2676
|
+
}
|
|
2677
|
+
function registerMove(server, client) {
|
|
2453
2678
|
server.registerTool("taiga_move_backlog_item", {
|
|
2454
2679
|
title: "Move backlog item on the board",
|
|
2455
|
-
description: `Move an initiative
|
|
2680
|
+
description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (taiga_queue_backlog_item says more); done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged); Taiga leaves a comment on the pull request saying why. The state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.`,
|
|
2456
2681
|
inputSchema: {
|
|
2457
2682
|
itemId,
|
|
2458
|
-
state: z.enum(
|
|
2683
|
+
state: z.enum(USER_DROP_STATES).describe("The column to move into"),
|
|
2459
2684
|
afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
|
|
2460
2685
|
archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
|
|
2461
2686
|
},
|
|
2462
2687
|
annotations: OPERATE
|
|
2463
2688
|
}, (params) => runTool(async () => {
|
|
2464
2689
|
const { itemId: id, ...body } = params;
|
|
2465
|
-
if (
|
|
2690
|
+
if (body.state === "queue" && !client.hasCapability("products:write")) return toolError("FORBIDDEN", "A move into queue starts agent work and needs products:write, which this key lacks.");
|
|
2466
2691
|
return outcome((await client.transitionInitiative(id, body)).data);
|
|
2467
2692
|
}));
|
|
2468
2693
|
}
|
|
2469
|
-
/** The
|
|
2470
|
-
const
|
|
2694
|
+
/** The two halves of the Build station, where Plan again is offered. */
|
|
2695
|
+
const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
|
|
2696
|
+
function registerPlanAgain(server, client) {
|
|
2697
|
+
server.registerTool("taiga_plan_again", {
|
|
2698
|
+
title: "Plan an initiative again",
|
|
2699
|
+
description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (taiga_queue_backlog_item): the queue is the only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run; ask the person first.",
|
|
2700
|
+
inputSchema: {
|
|
2701
|
+
itemId,
|
|
2702
|
+
force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
|
|
2703
|
+
},
|
|
2704
|
+
annotations: OPERATE
|
|
2705
|
+
}, (params) => runTool(async () => {
|
|
2706
|
+
const item = (await client.getBacklogItem(params.itemId)).data;
|
|
2707
|
+
if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with taiga_queue_backlog_item and it is planned when its turn comes.`);
|
|
2708
|
+
const body = {
|
|
2709
|
+
state: "plan",
|
|
2710
|
+
...params.force ? { force: true } : {}
|
|
2711
|
+
};
|
|
2712
|
+
return outcome((await client.transitionInitiative(params.itemId, body)).data);
|
|
2713
|
+
}));
|
|
2714
|
+
}
|
|
2471
2715
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
2472
2716
|
const isApiFailure$1 = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
|
|
2473
2717
|
/**
|
|
@@ -2542,7 +2786,7 @@ function registerReviewGate(server, client) {
|
|
|
2542
2786
|
itemId,
|
|
2543
2787
|
autoBuild: autoBuildInput
|
|
2544
2788
|
},
|
|
2545
|
-
annotations:
|
|
2789
|
+
annotations: OPERATE_SETTING
|
|
2546
2790
|
}, (params) => runTool(async () => {
|
|
2547
2791
|
const item = (await client.setAutoBuild(params.itemId, params.autoBuild)).data;
|
|
2548
2792
|
const gate = await reviewGate(client, item);
|
|
@@ -2556,7 +2800,7 @@ function registerReviewGate(server, client) {
|
|
|
2556
2800
|
function registerResume(server, client) {
|
|
2557
2801
|
server.registerTool("taiga_resume_backlog_item", {
|
|
2558
2802
|
title: "Resume backlog item",
|
|
2559
|
-
description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step, a refused build start is cleared and retried.
|
|
2803
|
+
description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
|
|
2560
2804
|
inputSchema: { itemId },
|
|
2561
2805
|
annotations: OPERATE
|
|
2562
2806
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
|
|
@@ -2678,7 +2922,7 @@ function registerBuildTools(server, client) {
|
|
|
2678
2922
|
if (!client.hasCapability("products:read")) return;
|
|
2679
2923
|
server.registerTool("taiga_list_builds", {
|
|
2680
2924
|
title: "List build runs",
|
|
2681
|
-
description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each
|
|
2925
|
+
description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each. A run is the record of an attempt: work is carried on from its initiative (resume and retry_from_failed_step are taiga_resume_backlog_item; start_over is in the web only for now) and stopped by moving the initiative out of Build; a maintenance run, which has no initiative, is retried from its finding. With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
|
|
2682
2926
|
inputSchema: {
|
|
2683
2927
|
productId: productId.optional(),
|
|
2684
2928
|
backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
|
|
@@ -2697,7 +2941,7 @@ function registerBuildTools(server, client) {
|
|
|
2697
2941
|
}));
|
|
2698
2942
|
server.registerTool("taiga_get_build", {
|
|
2699
2943
|
title: "Get build run",
|
|
2700
|
-
description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions
|
|
2944
|
+
description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions name what the initiative accepts for it: resume and retry_from_failed_step are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
2701
2945
|
inputSchema: {
|
|
2702
2946
|
buildId,
|
|
2703
2947
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -2721,20 +2965,12 @@ function registerBuildTools(server, client) {
|
|
|
2721
2965
|
if (!client.hasCapability("products:write")) return;
|
|
2722
2966
|
server.registerTool("taiga_stop_build", {
|
|
2723
2967
|
title: "Stop a build run",
|
|
2724
|
-
description: "
|
|
2968
|
+
description: "The emergency brake: stop a run the coding agent is executing when its initiative cannot be moved. The way to stop work is on the initiative: move it out of Build (taiga_move_backlog_item), which stops the plan or run in flight. Use this only for a run whose initiative does not offer that (it is finished, canceled or gone) yet the run is still going. The agent halts, its branch and any draft pull request stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
|
|
2725
2969
|
inputSchema: { buildId },
|
|
2726
2970
|
annotations: OPERATE
|
|
2727
2971
|
}, (params) => runTool(async () => {
|
|
2728
2972
|
return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
|
|
2729
2973
|
}));
|
|
2730
|
-
server.registerTool("taiga_resume_build", {
|
|
2731
|
-
title: "Resume a build run",
|
|
2732
|
-
description: "Resume a stopped or failed run from its first incomplete step: the agent starts again on the same branch, which spends tokens and may push commits and open a pull request. This is the route behind both the resume and the retry_from_failed_step actions: the API accepts a run whose actions include one of them and refuses the rest with CONFLICT. start_over (a new run) is web-only for now. For an initiative the line stopped on, taiga_resume_backlog_item does whatever its cause needs instead. Ask the person before calling this.",
|
|
2733
|
-
inputSchema: { buildId },
|
|
2734
|
-
annotations: OPERATE
|
|
2735
|
-
}, (params) => runTool(async () => {
|
|
2736
|
-
return toolResult(pickFields([(await client.resumeBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
|
|
2737
|
-
}));
|
|
2738
2974
|
}
|
|
2739
2975
|
//#endregion
|
|
2740
2976
|
//#region src/tools/context.ts
|
|
@@ -18009,6 +18245,7 @@ function itemSignature(item) {
|
|
|
18009
18245
|
item.state,
|
|
18010
18246
|
item.planningStatus,
|
|
18011
18247
|
item.needsHumanCause,
|
|
18248
|
+
item.autoBuild,
|
|
18012
18249
|
lifecycle.stage,
|
|
18013
18250
|
lifecycle.turn,
|
|
18014
18251
|
lifecycle.cta,
|
|
@@ -18206,6 +18443,25 @@ function registerWaitTools(server, client, options) {
|
|
|
18206
18443
|
run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
|
|
18207
18444
|
});
|
|
18208
18445
|
}));
|
|
18446
|
+
if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
|
|
18447
|
+
title: "Wait for an agent job",
|
|
18448
|
+
description: "Wait until an agent job finishes (completed, failed or cancelled): an initiative request, a document, a refresh. Returns the job as taiga_get_job reads it, with what came of an initiative request, as soon as it settles (reached: true), or at the timeout with reached: false: call again to keep waiting. Most jobs take a few minutes.",
|
|
18449
|
+
inputSchema: {
|
|
18450
|
+
jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it"),
|
|
18451
|
+
timeoutSeconds
|
|
18452
|
+
},
|
|
18453
|
+
annotations: READ_ONLY
|
|
18454
|
+
}, (params, extra) => runTool(async () => {
|
|
18455
|
+
const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(params.jobId, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
|
|
18456
|
+
progressToken: extra._meta?.progressToken,
|
|
18457
|
+
sendNotification: extra.sendNotification,
|
|
18458
|
+
signal: extra.signal
|
|
18459
|
+
});
|
|
18460
|
+
return toolResult({
|
|
18461
|
+
...outcome,
|
|
18462
|
+
job: now ? jobReport(now) : null
|
|
18463
|
+
});
|
|
18464
|
+
}));
|
|
18209
18465
|
}
|
|
18210
18466
|
//#endregion
|
|
18211
18467
|
//#region src/tools/index.ts
|
|
@@ -18241,6 +18497,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
18241
18497
|
registerBacklogTools(server, client);
|
|
18242
18498
|
registerBuildTools(server, client);
|
|
18243
18499
|
registerQueueTools(server, client);
|
|
18500
|
+
registerJobTools(server, client);
|
|
18244
18501
|
registerWaitTools(server, client, options.wait ?? DEFAULT_WAIT_OPTIONS);
|
|
18245
18502
|
registerContextTools(server, client);
|
|
18246
18503
|
registerResources(server, client);
|
|
@@ -18286,20 +18543,26 @@ const SERVER_INSTRUCTIONS = [
|
|
|
18286
18543
|
"for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
|
|
18287
18544
|
"every product the key can see, and active: true the ones executing now.",
|
|
18288
18545
|
"",
|
|
18289
|
-
"**Operating the line**
|
|
18290
|
-
"
|
|
18291
|
-
"
|
|
18292
|
-
"
|
|
18293
|
-
"
|
|
18294
|
-
"
|
|
18295
|
-
"
|
|
18296
|
-
"
|
|
18546
|
+
"**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
|
|
18547
|
+
"taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
|
|
18548
|
+
"into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
|
|
18549
|
+
"policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
|
|
18550
|
+
"work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
|
|
18551
|
+
"the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
|
|
18552
|
+
"(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
|
|
18553
|
+
"taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
|
|
18554
|
+
"moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
|
|
18555
|
+
"plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
|
|
18556
|
+
"taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
|
|
18557
|
+
"resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
|
|
18558
|
+
"are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
|
|
18559
|
+
"for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
|
|
18297
18560
|
"",
|
|
18298
|
-
"**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build
|
|
18299
|
-
"read says what the line is waiting on (a
|
|
18300
|
-
"it). Planning takes minutes and a build up to an
|
|
18301
|
-
"same item again and again; each returns when the
|
|
18302
|
-
"or at its timeout (call again to keep waiting).",
|
|
18561
|
+
"**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
|
|
18562
|
+
"taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
|
|
18563
|
+
"to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
|
|
18564
|
+
"hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
|
|
18565
|
+
"point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
|
|
18303
18566
|
"",
|
|
18304
18567
|
"**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
|
|
18305
18568
|
"provenance fields you can read past: `source` says where the text originally came from (template:",
|
package/package.json
CHANGED