@sjawhar/opencode-legion-envoy 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/server.js +151 -23
- package/package.json +1 -1
- package/skills/AGENTS.md +14 -41
- package/skills/dispatch/SKILL.md +19 -4
package/dist/src/server.js
CHANGED
|
@@ -13816,6 +13816,20 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
13816
13816
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
13817
13817
|
};
|
|
13818
13818
|
}
|
|
13819
|
+
var documentEditValidation = {
|
|
13820
|
+
check: (value) => {
|
|
13821
|
+
if (!documentOwnerValidation(true, true).check(value))
|
|
13822
|
+
return false;
|
|
13823
|
+
const input = value;
|
|
13824
|
+
if (input.precondition === undefined)
|
|
13825
|
+
return true;
|
|
13826
|
+
if (typeof input.precondition !== "object" || input.precondition === null)
|
|
13827
|
+
return false;
|
|
13828
|
+
const precondition = input.precondition;
|
|
13829
|
+
return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
|
|
13830
|
+
},
|
|
13831
|
+
message: "Exactly one of issue and project is required; artifact or ref must name the document. A precondition selects exactly one of document or blocks."
|
|
13832
|
+
};
|
|
13819
13833
|
var commentOwner = documentOwnerValidation(true);
|
|
13820
13834
|
var commentValidation = {
|
|
13821
13835
|
check: (value) => {
|
|
@@ -14043,9 +14057,10 @@ var dispatchToolSpecs = [
|
|
|
14043
14057
|
example: {
|
|
14044
14058
|
issue: "DSP-1",
|
|
14045
14059
|
artifact: "spec",
|
|
14046
|
-
ops: [{ op: "delete_column", block: "table-123", index: 1 }]
|
|
14060
|
+
ops: [{ op: "delete_column", block: "table-123", index: 1 }],
|
|
14061
|
+
precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
|
|
14047
14062
|
},
|
|
14048
|
-
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. replace is inline: with is the new text of the matched span, so a leading list or heading marker stays literal text. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids
|
|
14063
|
+
description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. replace is inline: with is the new text of the matched span, so a leading list or heading marker stays literal text. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
14049
14064
|
arguments: (z) => ({
|
|
14050
14065
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14051
14066
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14064,14 +14079,21 @@ var dispatchToolSpecs = [
|
|
|
14064
14079
|
type: z.string().describe("Typed block name for retype.").optional(),
|
|
14065
14080
|
attributes: z.unknown().describe("Typed block attributes for retype.").optional()
|
|
14066
14081
|
})).describe("Flat tagged edits; the server validates fields required for each operation."),
|
|
14082
|
+
precondition: z.object({
|
|
14083
|
+
document: z.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch_doc_read.").optional(),
|
|
14084
|
+
blocks: z.array(z.object({
|
|
14085
|
+
id: z.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
|
|
14086
|
+
token: z.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
|
|
14087
|
+
}), { min: 1 }).describe("Every content block this batch changes, each with the token returned by /blocks.").optional()
|
|
14088
|
+
}).describe("Optional optimistic-concurrency guard; select exactly one of document or blocks.").optional(),
|
|
14067
14089
|
summary: z.string().describe("Optional named-version summary.").optional()
|
|
14068
14090
|
}),
|
|
14069
|
-
validation:
|
|
14091
|
+
validation: documentEditValidation
|
|
14070
14092
|
},
|
|
14071
14093
|
{
|
|
14072
14094
|
name: "dispatch_doc_read",
|
|
14073
14095
|
example: { issue: "DSP-1" },
|
|
14074
|
-
description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + OWNER_REFERENCE,
|
|
14096
|
+
description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + OWNER_REFERENCE,
|
|
14075
14097
|
arguments: (z) => ({
|
|
14076
14098
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14077
14099
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -14296,6 +14318,14 @@ var reviewSchema = baseHandoffSchema.extend({
|
|
|
14296
14318
|
verdict: _enum2(["approved", "changes_requested"]).optional(),
|
|
14297
14319
|
keyFindings: array(object({ severity: string2(), file: string2(), description: string2() }).passthrough()).optional()
|
|
14298
14320
|
});
|
|
14321
|
+
var nonEmptySkillList = array(string2().trim().min(1)).min(1);
|
|
14322
|
+
var planWriteSchema = planSchema.extend({
|
|
14323
|
+
requiredSkills: object({
|
|
14324
|
+
implement: nonEmptySkillList,
|
|
14325
|
+
test: nonEmptySkillList,
|
|
14326
|
+
review: nonEmptySkillList
|
|
14327
|
+
}).passthrough()
|
|
14328
|
+
});
|
|
14299
14329
|
var phaseHandoffSchema = discriminatedUnion("phase", [
|
|
14300
14330
|
architectSchema,
|
|
14301
14331
|
planSchema,
|
|
@@ -14919,12 +14949,16 @@ class DispatchServiceError extends Error {
|
|
|
14919
14949
|
code;
|
|
14920
14950
|
status;
|
|
14921
14951
|
candidates;
|
|
14952
|
+
current;
|
|
14953
|
+
mismatches;
|
|
14922
14954
|
name = "DispatchServiceError";
|
|
14923
|
-
constructor(code, status, message, candidates) {
|
|
14955
|
+
constructor(code, status, message, candidates, current, mismatches) {
|
|
14924
14956
|
super(message);
|
|
14925
14957
|
this.code = code;
|
|
14926
14958
|
this.status = status;
|
|
14927
14959
|
this.candidates = candidates;
|
|
14960
|
+
this.current = current;
|
|
14961
|
+
this.mismatches = mismatches;
|
|
14928
14962
|
}
|
|
14929
14963
|
}
|
|
14930
14964
|
function asErrorShape(value) {
|
|
@@ -15236,7 +15270,7 @@ class DispatchClient {
|
|
|
15236
15270
|
}
|
|
15237
15271
|
if (!response.ok) {
|
|
15238
15272
|
const error = asErrorShape(payload);
|
|
15239
|
-
throw new DispatchServiceError(error.code ?? `HTTP_${response.status}`, response.status, error.error ?? (typeof payload === "string" && payload ? payload : response.statusText), error.candidates);
|
|
15273
|
+
throw new DispatchServiceError(error.code ?? `HTTP_${response.status}`, response.status, error.error ?? (typeof payload === "string" && payload ? payload : response.statusText), error.candidates, error.current, error.mismatches);
|
|
15240
15274
|
}
|
|
15241
15275
|
return payload;
|
|
15242
15276
|
}
|
|
@@ -15402,6 +15436,36 @@ function notSubscribed(owner) {
|
|
|
15402
15436
|
function followsAsk(owner) {
|
|
15403
15437
|
return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
|
|
15404
15438
|
}
|
|
15439
|
+
var triageAdviceShown = new Set;
|
|
15440
|
+
function renderAdvice(tool, key, advice, opts) {
|
|
15441
|
+
if (advice === undefined)
|
|
15442
|
+
return [];
|
|
15443
|
+
const hasIssueAdvice = advice.issue_status !== undefined;
|
|
15444
|
+
const openAsks = advice.your_open_asks;
|
|
15445
|
+
const writesSinceHuman = advice.session_writes_since_human;
|
|
15446
|
+
const lines = [];
|
|
15447
|
+
if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
15448
|
+
lines.push('No decision blocks in this spec \u2014 nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
|
|
15449
|
+
}
|
|
15450
|
+
if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
|
|
15451
|
+
const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
|
|
15452
|
+
lines.push(`You've sent ${writesSinceHuman} messages on ${key} with no human response. ${middle} See the \`dispatch\` skill, "Structure over stream".`);
|
|
15453
|
+
}
|
|
15454
|
+
if (advice.issue_status === "triage" && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
|
|
15455
|
+
triageAdviceShown.add(key);
|
|
15456
|
+
lines.push(`${key} is still in triage \u2014 nobody can see its development status. See the \`dispatch\` skill, "Issue status is yours to move".`);
|
|
15457
|
+
}
|
|
15458
|
+
if (hasIssueAdvice && openAsks !== undefined && openAsks.length > 0 && (tool === "dispatch_message" || tool === "dispatch_doc_edit" || tool === "dispatch_issue_update" || tool === "dispatch_comment" && opts.replyToOwnAsk !== true)) {
|
|
15459
|
+
const askCount = Math.min(openAsks.length, 2);
|
|
15460
|
+
for (let index = 0;index < askCount; index += 1) {
|
|
15461
|
+
const ask = openAsks[index];
|
|
15462
|
+
if (ask === undefined)
|
|
15463
|
+
break;
|
|
15464
|
+
lines.push(`You still have an open ask on ${key}: "${ask.question.slice(0, 80)}" (${ask.id}). Still needed? See the \`dispatch\` skill, "Close what you opened".`);
|
|
15465
|
+
}
|
|
15466
|
+
}
|
|
15467
|
+
return lines;
|
|
15468
|
+
}
|
|
15405
15469
|
function documentResultDetails(artifact) {
|
|
15406
15470
|
return {
|
|
15407
15471
|
project: artifact.project,
|
|
@@ -16290,9 +16354,19 @@ async function executeDispatchTool(input) {
|
|
|
16290
16354
|
...Array.isArray(labels) ? { labels } : {},
|
|
16291
16355
|
actor
|
|
16292
16356
|
});
|
|
16357
|
+
const adviceLines = renderAdvice(input.tool, created.key, created.advice, {
|
|
16358
|
+
isPrimarySpec: spec !== undefined
|
|
16359
|
+
});
|
|
16293
16360
|
return {
|
|
16294
|
-
text:
|
|
16295
|
-
|
|
16361
|
+
text: [
|
|
16362
|
+
`Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
16363
|
+
...adviceLines
|
|
16364
|
+
].join(`
|
|
16365
|
+
`),
|
|
16366
|
+
details: {
|
|
16367
|
+
issue: created.key,
|
|
16368
|
+
...created.advice === undefined ? {} : { advice: created.advice }
|
|
16369
|
+
}
|
|
16296
16370
|
};
|
|
16297
16371
|
} catch (error) {
|
|
16298
16372
|
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
@@ -16349,12 +16423,20 @@ async function executeDispatchTool(input) {
|
|
|
16349
16423
|
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
16350
16424
|
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
16351
16425
|
];
|
|
16426
|
+
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
16427
|
+
setsStatus: status !== undefined
|
|
16428
|
+
});
|
|
16352
16429
|
return {
|
|
16353
|
-
text:
|
|
16430
|
+
text: [
|
|
16431
|
+
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
16432
|
+
...adviceLines
|
|
16433
|
+
].join(`
|
|
16434
|
+
`),
|
|
16354
16435
|
details: {
|
|
16355
16436
|
issue: after.key,
|
|
16356
16437
|
status: after.status,
|
|
16357
|
-
external_links: after.external_links.map((link) => link.url)
|
|
16438
|
+
external_links: after.external_links.map((link) => link.url),
|
|
16439
|
+
...after.advice === undefined ? {} : { advice: after.advice }
|
|
16358
16440
|
}
|
|
16359
16441
|
};
|
|
16360
16442
|
} catch (error) {
|
|
@@ -16485,10 +16567,18 @@ async function executeDispatchTool(input) {
|
|
|
16485
16567
|
};
|
|
16486
16568
|
const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
|
|
16487
16569
|
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue()) : documentTopic(resolved.artifact);
|
|
16570
|
+
const adviceLines = renderAdvice(input.tool, askOwner.label, ask.advice, {});
|
|
16488
16571
|
return {
|
|
16489
|
-
text:
|
|
16572
|
+
text: [
|
|
16573
|
+
`Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
16490
16574
|
${followsAsk(askOwner)}`,
|
|
16491
|
-
|
|
16575
|
+
...adviceLines
|
|
16576
|
+
].join(`
|
|
16577
|
+
`),
|
|
16578
|
+
details: {
|
|
16579
|
+
...await followedAskDetails(client, ask, resolved?.artifact),
|
|
16580
|
+
...ask.advice === undefined ? {} : { advice: ask.advice }
|
|
16581
|
+
}
|
|
16492
16582
|
};
|
|
16493
16583
|
}
|
|
16494
16584
|
case "dispatch_edit_ask": {
|
|
@@ -16533,21 +16623,34 @@ ${followsAsk(askOwner)}`,
|
|
|
16533
16623
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
16534
16624
|
const commentOwner = resolved === undefined ? issueTopic(issue()) : resolvedTopic(resolved);
|
|
16535
16625
|
const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
|
|
16626
|
+
const adviceLines = renderAdvice(input.tool, commentOwner.label, comment.advice, {
|
|
16627
|
+
isAskReply: replyToAsk !== undefined,
|
|
16628
|
+
replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
|
|
16629
|
+
});
|
|
16536
16630
|
if (replyToAsk !== undefined) {
|
|
16537
16631
|
const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
|
|
16538
16632
|
return {
|
|
16539
|
-
text:
|
|
16633
|
+
text: [
|
|
16634
|
+
`Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner)}`,
|
|
16635
|
+
...adviceLines
|
|
16636
|
+
].join(`
|
|
16637
|
+
`),
|
|
16540
16638
|
details: {
|
|
16541
16639
|
...commentDetails,
|
|
16542
16640
|
ask: replyToAsk,
|
|
16543
16641
|
follows: { ask: replyToAsk },
|
|
16544
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
16642
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn },
|
|
16643
|
+
...comment.advice === undefined ? {} : { advice: comment.advice }
|
|
16545
16644
|
}
|
|
16546
16645
|
};
|
|
16547
16646
|
}
|
|
16548
16647
|
return {
|
|
16549
|
-
text: `Posted comment ${comment.id} ${notSubscribed(commentOwner)}`,
|
|
16550
|
-
|
|
16648
|
+
text: [`Posted comment ${comment.id} ${notSubscribed(commentOwner)}`, ...adviceLines].join(`
|
|
16649
|
+
`),
|
|
16650
|
+
details: {
|
|
16651
|
+
...commentDetails,
|
|
16652
|
+
...comment.advice === undefined ? {} : { advice: comment.advice }
|
|
16653
|
+
}
|
|
16551
16654
|
};
|
|
16552
16655
|
}
|
|
16553
16656
|
case "dispatch_suggest": {
|
|
@@ -16577,28 +16680,43 @@ ${followsAsk(askOwner)}`,
|
|
|
16577
16680
|
actor
|
|
16578
16681
|
});
|
|
16579
16682
|
const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
|
|
16683
|
+
const adviceLines = renderAdvice(input.tool, issueKey, message.advice, {});
|
|
16580
16684
|
return {
|
|
16581
|
-
text:
|
|
16582
|
-
|
|
16685
|
+
text: [
|
|
16686
|
+
`Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
16687
|
+
...adviceLines
|
|
16688
|
+
].join(`
|
|
16689
|
+
`),
|
|
16690
|
+
details: {
|
|
16691
|
+
issue: issueKey,
|
|
16692
|
+
message: message.id,
|
|
16693
|
+
...message.advice === undefined ? {} : { advice: message.advice }
|
|
16694
|
+
}
|
|
16583
16695
|
};
|
|
16584
16696
|
}
|
|
16585
16697
|
case "dispatch_doc_edit": {
|
|
16586
16698
|
const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
|
|
16587
16699
|
const ops = args.ops;
|
|
16588
16700
|
const summary = optionalString(args, "summary");
|
|
16701
|
+
const { precondition: rawPrecondition } = args;
|
|
16702
|
+
const precondition = rawPrecondition;
|
|
16589
16703
|
const edited = await client.docEdit(resolved.artifact.id, {
|
|
16590
16704
|
ops,
|
|
16591
16705
|
...summary === undefined ? {} : { summary },
|
|
16706
|
+
...precondition === undefined ? {} : { precondition },
|
|
16592
16707
|
actor
|
|
16593
16708
|
});
|
|
16594
16709
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
16595
16710
|
const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
|
|
16596
16711
|
const applied = retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`;
|
|
16712
|
+
const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
|
|
16597
16713
|
return {
|
|
16598
|
-
text: `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
16714
|
+
text: [`${applied} ${notSubscribed(resolvedTopic(resolved))}`, ...adviceLines].join(`
|
|
16715
|
+
`),
|
|
16599
16716
|
details: writeResultDetails(resolved, {
|
|
16600
16717
|
applied: edited.applied,
|
|
16601
|
-
...edited.version === null ? {} : { version: edited.version.number }
|
|
16718
|
+
...edited.version === null ? {} : { version: edited.version.number },
|
|
16719
|
+
...edited.advice === undefined ? {} : { advice: edited.advice }
|
|
16602
16720
|
})
|
|
16603
16721
|
};
|
|
16604
16722
|
}
|
|
@@ -16616,6 +16734,7 @@ ${followsAsk(askOwner)}`,
|
|
|
16616
16734
|
const marks = marksResult.value;
|
|
16617
16735
|
const approval = approvalLine(resolved.artifact);
|
|
16618
16736
|
const trailer = [
|
|
16737
|
+
..."token" in document && document.token !== undefined ? [`Document token: ${document.token}`] : [],
|
|
16619
16738
|
...marks.length === 0 ? [] : [`Open anchored asks/comments: ${marks.join(", ")}`],
|
|
16620
16739
|
...approval === undefined ? [] : [approval]
|
|
16621
16740
|
];
|
|
@@ -16669,15 +16788,24 @@ ${trailer.join(`
|
|
|
16669
16788
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
16670
16789
|
const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue(), result.artifact.slug);
|
|
16671
16790
|
const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
|
|
16791
|
+
const adviceLines = renderAdvice(input.tool, uploadOwner.label, result.advice, {
|
|
16792
|
+
isPrimarySpec: result.artifact.primary || result.artifact.name === "spec.md"
|
|
16793
|
+
});
|
|
16672
16794
|
return {
|
|
16673
|
-
text:
|
|
16795
|
+
text: [
|
|
16796
|
+
`Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
|
|
16797
|
+
...adviceLines
|
|
16798
|
+
].join(`
|
|
16799
|
+
`),
|
|
16674
16800
|
details: artifactOwner.kind === "project" ? {
|
|
16675
16801
|
...documentResultDetails(result.artifact),
|
|
16676
|
-
version: result.version.number
|
|
16802
|
+
version: result.version.number,
|
|
16803
|
+
...result.advice === undefined ? {} : { advice: result.advice }
|
|
16677
16804
|
} : {
|
|
16678
16805
|
issue: issue(),
|
|
16679
16806
|
artifact: result.artifact.id,
|
|
16680
|
-
version: result.version.number
|
|
16807
|
+
version: result.version.number,
|
|
16808
|
+
...result.advice === undefined ? {} : { advice: result.advice }
|
|
16681
16809
|
}
|
|
16682
16810
|
};
|
|
16683
16811
|
}
|
package/package.json
CHANGED
package/skills/AGENTS.md
CHANGED
|
@@ -4,44 +4,17 @@ Legion skills guide the architect and its sequential phase workers in a shared i
|
|
|
4
4
|
They are Markdown instructions loaded by Oh My Pi sessions; the daemon and OMP extension own
|
|
5
5
|
event intake, process lifecycle, credentials, and role delivery.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
The extension supplies a phase worker with its issue, workspace, role token, and structured
|
|
23
|
-
output schema. The worker claims its supplied role, works only on its phase artifact, and
|
|
24
|
-
returns that schema to the architect. It writes the same phase-specific payload to
|
|
25
|
-
`.legion/<phase>.json`, verifies it exists, and commits the handoff before reporting completion.
|
|
26
|
-
The committed predecessor handoff wins after revival or re-creation.
|
|
27
|
-
|
|
28
|
-
Workers do not run a controller loop or mutate lifecycle labels. Workers coordinate
|
|
29
|
-
lifecycle, scope, and cross-phase decisions with the owning architect by `envoy_publish` to its
|
|
30
|
-
role topic, sending the verified observation and decision needed (`hub` reaches only subagents
|
|
31
|
-
inside the worker's own process). A worker may call the native `dispatch_*` tools directly for a
|
|
32
|
-
durable human question; replies come back to the worker's own session.
|
|
33
|
-
|
|
34
|
-
## Durable artifacts
|
|
35
|
-
|
|
36
|
-
Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
|
|
37
|
-
Only the implementer pushes them: it and the merger act as the code-writing GitHub App, while
|
|
38
|
-
the planner, tester, reviewer, and architects act as the review App (`appRoleForLegionRole`,
|
|
39
|
-
`packages/daemon/src/daemon/github-apps.ts`), which holds no `contents` permission — their
|
|
40
|
-
handoff commits stay on the shared workspace's issue branch and ride the implementer's next push.
|
|
41
|
-
A clean review ends with the `.legion/` deletion pushed by the implementer at the reviewer's
|
|
42
|
-
direction, which the reviewer then approves; retro records its learning in
|
|
43
|
-
`docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
|
|
44
|
-
footer so the daemon can attribute artifacts to their worker session.
|
|
45
|
-
The implement handoff carries the implementer's own production-like proof and the test handoff the
|
|
46
|
-
tester's verdict on it plus the tester's own; `legion handoff write` refuses a payload the phase's
|
|
47
|
-
schema rejects and names the field. Retro's message goes to the Dispatch issue (`dispatch_message`), never a GitHub issue.
|
|
7
|
+
| Skill | Who reads it | What it owns |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `dispatch/` | every role, and any session writing to Dispatch | specs, asks, comments, artifacts, and messages on native Dispatch |
|
|
10
|
+
| `envoy/` | every role | subscriptions, agent-to-agent messages, and topic formats |
|
|
11
|
+
| `legion-architect/` | root and sub-architects | tree ownership, decomposition, waves, gates, integration, sign-off |
|
|
12
|
+
| `legion-controller/` | the controller root process | wake routing, backlog admission, escalation |
|
|
13
|
+
| `legion-oracle/` | any role doing research | repository-grounded research |
|
|
14
|
+
| `legion-retro/` | the implementer, at retro | the pre-merge retrospective and its Dispatch message |
|
|
15
|
+
| `legion-worker/` | planner, implementer, tester, reviewer, merger | the phase contracts: handoffs, GitHub identity, PR body and READY discipline, the merge-gate order |
|
|
16
|
+
|
|
17
|
+
The owning skill above is where each contract is defined; a role prompt that needs a contract from its own seat points there or restates only its own step. This file lists and does not restate.
|
|
18
|
+
The text a worker boots with (its role prompt) lives in `packages/pi-envoy/roles/` and is
|
|
19
|
+
composed per role in `packages/daemon/src/daemon/processes.ts`; `packages/pi-envoy/roles/roles.test.ts`
|
|
20
|
+
holds the structural rules for those parts.
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dispatch
|
|
3
|
-
description: "Use when asking Sami a question, updating the spec, commenting on a document, attaching an artifact, or calling a dispatch_* tool."
|
|
3
|
+
description: "Use before posting a message, a status update, or a periodic status update; before asking a question that references another message, artifact, or eval; and when asking Sami a question, updating the spec, commenting on a document, attaching an artifact, or calling a dispatch_* tool."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dispatch
|
|
@@ -381,11 +381,12 @@ Read the current document before changing it:
|
|
|
381
381
|
```ts
|
|
382
382
|
dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
|
|
383
383
|
```
|
|
384
|
-
It returns live or versioned markdown with open marks.
|
|
385
|
-
`artifact
|
|
384
|
+
It returns live or versioned markdown with open marks. A live read ends with a document token; `issue` with an
|
|
385
|
+
omitted `artifact` reads the issue specification; a project needs `artifact`; and a
|
|
386
|
+
`dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the id, slug, or filename.
|
|
386
387
|
|
|
387
388
|
```ts
|
|
388
|
-
dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
|
|
389
|
+
dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
|
|
389
390
|
```
|
|
390
391
|
It returns issue or project-document owner details plus `applied` and optional `version`. `ops` is an array of this
|
|
391
392
|
exact `EditOp` shape:
|
|
@@ -445,6 +446,18 @@ an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation a
|
|
|
445
446
|
whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
|
|
446
447
|
`INVALID_OP` naming the field and the rule.
|
|
447
448
|
|
|
449
|
+
`GET /api/v1/artifacts/<artifact UUID>/blocks` includes a full-state `token` on every block, including
|
|
450
|
+
inline marks. To reject a stale edit, pass `precondition` with exactly one of
|
|
451
|
+
`{ document: "<token from dispatch_doc_read>" }` or
|
|
452
|
+
`{ blocks: [{ id: "<block id>", token: "<block token>" }] }`. The server resolves the whole batch before
|
|
453
|
+
mutation: a block guard must cover every content block it changes, or Dispatch returns
|
|
454
|
+
`400 INVALID_PRECONDITION` without applying anything. Use a document token for insert and move because they
|
|
455
|
+
depend on document order. A block token lets other sections change concurrently; a new anchored ask or comment
|
|
456
|
+
changes the relevant token. A stale guard returns `409 PRECONDITION_FAILED` with each mismatch and current
|
|
457
|
+
token; Dispatch applies no part of that batch. It is the hashline `#TAG` property applied to stable block ids,
|
|
458
|
+
not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
|
|
459
|
+
survive moves and retyping.
|
|
460
|
+
|
|
448
461
|
`retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
|
|
449
462
|
block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
|
|
450
463
|
an existing paragraph is the question that should become a decision.
|
|
@@ -665,6 +678,8 @@ title is invisible to the graph.
|
|
|
665
678
|
|
|
666
679
|
## References
|
|
667
680
|
|
|
681
|
+
**Every reference is a link, never an unlinked mention.** If you name a thing that has an address, link it: another issue, ask, comment, spec, or message (the `dispatch://` forms below), an artifact (`dispatch://KEY/artifact/<slug>`), an eval (its viewer URL), a Slack message (its permalink), a Drive file (its share link). Bare phrases like "see this eval", "his 09-04 run", "the comment above", or "per the spec" with no link are banned: they make the reader hunt for what you already had in hand, and nothing can be traversed from them. Linking every reference is what makes a body both consumable and navigable. If a thing genuinely has no linkable address, say so; otherwise the link is not optional.
|
|
682
|
+
|
|
668
683
|
Use these in document, ask, comment, and message bodies. In the dashboard, a reference renders
|
|
669
684
|
as an inline link whose text is the target's title (an issue's title, an ask's question, a
|
|
670
685
|
comment's first line, a document's name) once it resolves; a body that is only a bare reference
|