@sjawhar/pi-legion-envoy 0.28.0 → 0.30.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/README.md +2 -2
- package/dist/envoy.js +32 -49
- package/dist/legion.js +77 -140
- package/dist/skills/dispatch/SKILL.md +40 -7
- package/dist/skills/github/SKILL.md +4 -4
- package/dist/skills/legion-architect/SKILL.md +43 -41
- package/dist/skills/legion-controller/SKILL.md +44 -43
- package/dist/skills/legion-worker/SKILL.md +31 -23
- package/dist/skills/linear/SKILL.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,8 +121,8 @@ The shared contract supplies the model-facing schemas and descriptions. The
|
|
|
121
121
|
feedback, documents, artifacts, and status reads.
|
|
122
122
|
|
|
123
123
|
`dispatch_artifact` accepts exactly one upload source: a local `path`, or inline `content`.
|
|
124
|
-
For example, an architect can post
|
|
125
|
-
`{ issue, name: "spec.md", content: "# Design"
|
|
124
|
+
For example, an architect can post a specification directly with
|
|
125
|
+
`{ issue, name: "spec.md", content: "# Design" }`.
|
|
126
126
|
|
|
127
127
|
Lifecycle and scope decisions between Legion roles go through `envoy_publish` to the owning
|
|
128
128
|
architect's role topic; Dispatch is for durable questions to the human and the shared
|
package/dist/envoy.js
CHANGED
|
@@ -29700,6 +29700,16 @@ function dispatchToolSchema(spec, z, opts) {
|
|
|
29700
29700
|
return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
|
|
29701
29701
|
}
|
|
29702
29702
|
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
|
|
29703
|
+
var SPEC_SECTIONS = [
|
|
29704
|
+
"Decisions needed",
|
|
29705
|
+
"Acceptance",
|
|
29706
|
+
"Requirements",
|
|
29707
|
+
"Design",
|
|
29708
|
+
"Errors",
|
|
29709
|
+
"Testing",
|
|
29710
|
+
"Rejected"
|
|
29711
|
+
];
|
|
29712
|
+
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Every line is a fact, decision, or risk; use tables over prose; see skills/dispatch Writing a spec.";
|
|
29703
29713
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29704
29714
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
29705
29715
|
var dispatchToolSpecs = [
|
|
@@ -29711,7 +29721,7 @@ var dispatchToolSpecs = [
|
|
|
29711
29721
|
title: z.string().describe("Concise issue title."),
|
|
29712
29722
|
parent: z.string().describe("Optional parent issue.").optional(),
|
|
29713
29723
|
external: z.string().describe("Optional external issue reference.").optional(),
|
|
29714
|
-
spec: z.string().describe(
|
|
29724
|
+
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
|
|
29715
29725
|
})
|
|
29716
29726
|
},
|
|
29717
29727
|
{
|
|
@@ -29777,7 +29787,7 @@ var dispatchToolSpecs = [
|
|
|
29777
29787
|
},
|
|
29778
29788
|
{
|
|
29779
29789
|
name: "dispatch_doc_edit",
|
|
29780
|
-
description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE}`,
|
|
29790
|
+
description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
29781
29791
|
arguments: (z) => ({
|
|
29782
29792
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29783
29793
|
artifact: z.string().describe("Artifact slug or id for the document."),
|
|
@@ -29811,7 +29821,6 @@ var dispatchToolSpecs = [
|
|
|
29811
29821
|
name: z.string().describe("Artifact filename shown in Dispatch."),
|
|
29812
29822
|
path: z.string().describe("Local path to the file to upload.").optional(),
|
|
29813
29823
|
content: z.string().describe("Inline text to store as a Markdown document.").optional(),
|
|
29814
|
-
primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
|
|
29815
29824
|
summary: z.string().describe("Optional version summary.").optional()
|
|
29816
29825
|
}),
|
|
29817
29826
|
validation: {
|
|
@@ -29988,11 +29997,17 @@ var legionRole = _enum2(LEGION_ROLES);
|
|
|
29988
29997
|
var requiredUnknown = unknown().refine((value) => value !== undefined, {
|
|
29989
29998
|
message: "Required"
|
|
29990
29999
|
});
|
|
29991
|
-
var
|
|
29992
|
-
|
|
29993
|
-
|
|
29994
|
-
|
|
29995
|
-
|
|
30000
|
+
var LIFECYCLE_STATUSES = [
|
|
30001
|
+
"triage",
|
|
30002
|
+
"icebox",
|
|
30003
|
+
"backlog",
|
|
30004
|
+
"todo",
|
|
30005
|
+
"in_progress",
|
|
30006
|
+
"testing",
|
|
30007
|
+
"needs_review",
|
|
30008
|
+
"retro",
|
|
30009
|
+
"done"
|
|
30010
|
+
];
|
|
29996
30011
|
var architectCapability = strictObject({
|
|
29997
30012
|
tree: nonEmptyString,
|
|
29998
30013
|
sessionId: nonEmptyString,
|
|
@@ -30045,33 +30060,10 @@ var LegionDaemonApi = {
|
|
|
30045
30060
|
request: architectCapability.extend({ generation: number2().int() }),
|
|
30046
30061
|
response: object({})
|
|
30047
30062
|
},
|
|
30048
|
-
IssueCreate: {
|
|
30049
|
-
request: architectCapability.extend({
|
|
30050
|
-
title: nonEmptyString,
|
|
30051
|
-
body: nonEmptyString,
|
|
30052
|
-
labels: array(architectMutableLabel).optional()
|
|
30053
|
-
}),
|
|
30054
|
-
response: object({ issue: nonEmptyString, url: nonEmptyString })
|
|
30055
|
-
},
|
|
30056
30063
|
WaveRelease: {
|
|
30057
|
-
request: architectCapability.extend({
|
|
30064
|
+
request: architectCapability.extend({ issues: array(nonEmptyString).optional() }),
|
|
30058
30065
|
response: object({ released: array(nonEmptyString) })
|
|
30059
30066
|
},
|
|
30060
|
-
Comment: {
|
|
30061
|
-
request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
|
|
30062
|
-
response: object({ commentId: number2().int(), url: nonEmptyString })
|
|
30063
|
-
},
|
|
30064
|
-
PostBody: {
|
|
30065
|
-
request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
|
|
30066
|
-
response: object({})
|
|
30067
|
-
},
|
|
30068
|
-
Labels: {
|
|
30069
|
-
request: architectCapability.extend({
|
|
30070
|
-
issue: nonEmptyString,
|
|
30071
|
-
add: array(architectMutableLabel).optional()
|
|
30072
|
-
}),
|
|
30073
|
-
response: object({ labels: array(nonEmptyString) })
|
|
30074
|
-
},
|
|
30075
30067
|
Escalate: {
|
|
30076
30068
|
request: architectCapability.extend({
|
|
30077
30069
|
kind: _enum2(["re-file", "capacity", "cross-tree"]),
|
|
@@ -30079,10 +30071,6 @@ var LegionDaemonApi = {
|
|
|
30079
30071
|
}),
|
|
30080
30072
|
response: object({})
|
|
30081
30073
|
},
|
|
30082
|
-
IssueClose: {
|
|
30083
|
-
request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
|
|
30084
|
-
response: object({})
|
|
30085
|
-
},
|
|
30086
30074
|
ProvisioningCredential: {
|
|
30087
30075
|
request: architectCapability.extend({ issue: nonEmptyString }),
|
|
30088
30076
|
response: object({ token: nonEmptyString })
|
|
@@ -30145,16 +30133,16 @@ var LegionDaemonApi = {
|
|
|
30145
30133
|
secret: nonEmptyString
|
|
30146
30134
|
})
|
|
30147
30135
|
},
|
|
30148
|
-
|
|
30149
|
-
request: controllerIssue
|
|
30136
|
+
IssueStatus: {
|
|
30137
|
+
request: controllerIssue.extend({
|
|
30138
|
+
status: _enum2(LIFECYCLE_STATUSES),
|
|
30139
|
+
tree: nonEmptyString.optional(),
|
|
30140
|
+
sessionId: nonEmptyString.optional()
|
|
30141
|
+
}),
|
|
30150
30142
|
response: object({})
|
|
30151
30143
|
},
|
|
30152
|
-
|
|
30153
|
-
request:
|
|
30154
|
-
response: object({ result: _enum2(["spawned", "queued"]) })
|
|
30155
|
-
},
|
|
30156
|
-
Backlog: {
|
|
30157
|
-
request: controllerIssue.extend({ marker: nonEmptyString }),
|
|
30144
|
+
GatesRegister: {
|
|
30145
|
+
request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
|
|
30158
30146
|
response: object({})
|
|
30159
30147
|
},
|
|
30160
30148
|
Grant: {
|
|
@@ -31218,8 +31206,6 @@ class DispatchClient {
|
|
|
31218
31206
|
return this.#json("POST", artifactPath, input);
|
|
31219
31207
|
const form = new FormData;
|
|
31220
31208
|
form.set("name", input.name);
|
|
31221
|
-
if (input.primary !== undefined)
|
|
31222
|
-
form.set("primary", String(input.primary));
|
|
31223
31209
|
if (input.summary !== undefined)
|
|
31224
31210
|
form.set("summary", input.summary);
|
|
31225
31211
|
if (input.actor !== undefined)
|
|
@@ -31738,20 +31724,17 @@ Open anchored asks/comments: ${marks.join(", ")}`,
|
|
|
31738
31724
|
};
|
|
31739
31725
|
}
|
|
31740
31726
|
case "dispatch_artifact": {
|
|
31741
|
-
const primary = optionalBoolean(args, "primary");
|
|
31742
31727
|
const summary = optionalString(args, "summary");
|
|
31743
31728
|
const name = stringArg(args, "name");
|
|
31744
31729
|
const content = optionalString(args, "content");
|
|
31745
31730
|
const result = await client.artifact(issue(), content === undefined ? {
|
|
31746
31731
|
name,
|
|
31747
31732
|
file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
|
|
31748
|
-
...primary === undefined ? {} : { primary },
|
|
31749
31733
|
...summary === undefined ? {} : { summary },
|
|
31750
31734
|
actor
|
|
31751
31735
|
} : {
|
|
31752
31736
|
name,
|
|
31753
31737
|
content,
|
|
31754
|
-
...primary === undefined ? {} : { primary },
|
|
31755
31738
|
...summary === undefined ? {} : { summary },
|
|
31756
31739
|
actor
|
|
31757
31740
|
});
|
package/dist/legion.js
CHANGED
|
@@ -29698,6 +29698,16 @@ function dispatchToolSchema(spec, z, opts) {
|
|
|
29698
29698
|
return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
|
|
29699
29699
|
}
|
|
29700
29700
|
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
|
|
29701
|
+
var SPEC_SECTIONS = [
|
|
29702
|
+
"Decisions needed",
|
|
29703
|
+
"Acceptance",
|
|
29704
|
+
"Requirements",
|
|
29705
|
+
"Design",
|
|
29706
|
+
"Errors",
|
|
29707
|
+
"Testing",
|
|
29708
|
+
"Rejected"
|
|
29709
|
+
];
|
|
29710
|
+
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Every line is a fact, decision, or risk; use tables over prose; see skills/dispatch Writing a spec.";
|
|
29701
29711
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29702
29712
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
29703
29713
|
var dispatchToolSpecs = [
|
|
@@ -29709,7 +29719,7 @@ var dispatchToolSpecs = [
|
|
|
29709
29719
|
title: z.string().describe("Concise issue title."),
|
|
29710
29720
|
parent: z.string().describe("Optional parent issue.").optional(),
|
|
29711
29721
|
external: z.string().describe("Optional external issue reference.").optional(),
|
|
29712
|
-
spec: z.string().describe(
|
|
29722
|
+
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
|
|
29713
29723
|
})
|
|
29714
29724
|
},
|
|
29715
29725
|
{
|
|
@@ -29775,7 +29785,7 @@ var dispatchToolSpecs = [
|
|
|
29775
29785
|
},
|
|
29776
29786
|
{
|
|
29777
29787
|
name: "dispatch_doc_edit",
|
|
29778
|
-
description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE}`,
|
|
29788
|
+
description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
|
|
29779
29789
|
arguments: (z) => ({
|
|
29780
29790
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29781
29791
|
artifact: z.string().describe("Artifact slug or id for the document."),
|
|
@@ -29809,7 +29819,6 @@ var dispatchToolSpecs = [
|
|
|
29809
29819
|
name: z.string().describe("Artifact filename shown in Dispatch."),
|
|
29810
29820
|
path: z.string().describe("Local path to the file to upload.").optional(),
|
|
29811
29821
|
content: z.string().describe("Inline text to store as a Markdown document.").optional(),
|
|
29812
|
-
primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
|
|
29813
29822
|
summary: z.string().describe("Optional version summary.").optional()
|
|
29814
29823
|
}),
|
|
29815
29824
|
validation: {
|
|
@@ -29986,11 +29995,17 @@ var legionRole = _enum2(LEGION_ROLES);
|
|
|
29986
29995
|
var requiredUnknown = unknown().refine((value) => value !== undefined, {
|
|
29987
29996
|
message: "Required"
|
|
29988
29997
|
});
|
|
29989
|
-
var
|
|
29990
|
-
|
|
29991
|
-
|
|
29992
|
-
|
|
29993
|
-
|
|
29998
|
+
var LIFECYCLE_STATUSES = [
|
|
29999
|
+
"triage",
|
|
30000
|
+
"icebox",
|
|
30001
|
+
"backlog",
|
|
30002
|
+
"todo",
|
|
30003
|
+
"in_progress",
|
|
30004
|
+
"testing",
|
|
30005
|
+
"needs_review",
|
|
30006
|
+
"retro",
|
|
30007
|
+
"done"
|
|
30008
|
+
];
|
|
29994
30009
|
var architectCapability = strictObject({
|
|
29995
30010
|
tree: nonEmptyString,
|
|
29996
30011
|
sessionId: nonEmptyString,
|
|
@@ -30043,33 +30058,10 @@ var LegionDaemonApi = {
|
|
|
30043
30058
|
request: architectCapability.extend({ generation: number2().int() }),
|
|
30044
30059
|
response: object({})
|
|
30045
30060
|
},
|
|
30046
|
-
IssueCreate: {
|
|
30047
|
-
request: architectCapability.extend({
|
|
30048
|
-
title: nonEmptyString,
|
|
30049
|
-
body: nonEmptyString,
|
|
30050
|
-
labels: array(architectMutableLabel).optional()
|
|
30051
|
-
}),
|
|
30052
|
-
response: object({ issue: nonEmptyString, url: nonEmptyString })
|
|
30053
|
-
},
|
|
30054
30061
|
WaveRelease: {
|
|
30055
|
-
request: architectCapability.extend({
|
|
30062
|
+
request: architectCapability.extend({ issues: array(nonEmptyString).optional() }),
|
|
30056
30063
|
response: object({ released: array(nonEmptyString) })
|
|
30057
30064
|
},
|
|
30058
|
-
Comment: {
|
|
30059
|
-
request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
|
|
30060
|
-
response: object({ commentId: number2().int(), url: nonEmptyString })
|
|
30061
|
-
},
|
|
30062
|
-
PostBody: {
|
|
30063
|
-
request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
|
|
30064
|
-
response: object({})
|
|
30065
|
-
},
|
|
30066
|
-
Labels: {
|
|
30067
|
-
request: architectCapability.extend({
|
|
30068
|
-
issue: nonEmptyString,
|
|
30069
|
-
add: array(architectMutableLabel).optional()
|
|
30070
|
-
}),
|
|
30071
|
-
response: object({ labels: array(nonEmptyString) })
|
|
30072
|
-
},
|
|
30073
30065
|
Escalate: {
|
|
30074
30066
|
request: architectCapability.extend({
|
|
30075
30067
|
kind: _enum2(["re-file", "capacity", "cross-tree"]),
|
|
@@ -30077,10 +30069,6 @@ var LegionDaemonApi = {
|
|
|
30077
30069
|
}),
|
|
30078
30070
|
response: object({})
|
|
30079
30071
|
},
|
|
30080
|
-
IssueClose: {
|
|
30081
|
-
request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
|
|
30082
|
-
response: object({})
|
|
30083
|
-
},
|
|
30084
30072
|
ProvisioningCredential: {
|
|
30085
30073
|
request: architectCapability.extend({ issue: nonEmptyString }),
|
|
30086
30074
|
response: object({ token: nonEmptyString })
|
|
@@ -30143,16 +30131,16 @@ var LegionDaemonApi = {
|
|
|
30143
30131
|
secret: nonEmptyString
|
|
30144
30132
|
})
|
|
30145
30133
|
},
|
|
30146
|
-
|
|
30147
|
-
request: controllerIssue
|
|
30134
|
+
IssueStatus: {
|
|
30135
|
+
request: controllerIssue.extend({
|
|
30136
|
+
status: _enum2(LIFECYCLE_STATUSES),
|
|
30137
|
+
tree: nonEmptyString.optional(),
|
|
30138
|
+
sessionId: nonEmptyString.optional()
|
|
30139
|
+
}),
|
|
30148
30140
|
response: object({})
|
|
30149
30141
|
},
|
|
30150
|
-
|
|
30151
|
-
request:
|
|
30152
|
-
response: object({ result: _enum2(["spawned", "queued"]) })
|
|
30153
|
-
},
|
|
30154
|
-
Backlog: {
|
|
30155
|
-
request: controllerIssue.extend({ marker: nonEmptyString }),
|
|
30142
|
+
GatesRegister: {
|
|
30143
|
+
request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
|
|
30156
30144
|
response: object({})
|
|
30157
30145
|
},
|
|
30158
30146
|
Grant: {
|
|
@@ -30399,18 +30387,12 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
|
30399
30387
|
workerReady: (input) => noContent("/legion/v1/worker/ready", input, LegionDaemonApi.WorkerReady.response),
|
|
30400
30388
|
spawnWorker: (input) => post("/legion/v1/worker/spawn", input, LegionDaemonApi.SpawnWorker.response),
|
|
30401
30389
|
mergeGate: (input) => post("/legion/v1/merge-gate", input, LegionDaemonApi.MergeGate.response),
|
|
30402
|
-
|
|
30403
|
-
waveRelease: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
|
|
30404
|
-
comment: (input) => post("/legion/v1/issues/comment", input, LegionDaemonApi.Comment.response),
|
|
30390
|
+
releaseWave: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
|
|
30405
30391
|
provisioningCredential: (input) => post("/legion/v1/provisioning-credential", input, LegionDaemonApi.ProvisioningCredential.response),
|
|
30406
|
-
postBody: (input) => noContent("/legion/v1/issues/body", input, LegionDaemonApi.PostBody.response),
|
|
30407
|
-
labels: (input) => post("/legion/v1/issues/labels", input, LegionDaemonApi.Labels.response),
|
|
30408
30392
|
escalate: (input) => noContent("/legion/v1/escalate", input, LegionDaemonApi.Escalate.response),
|
|
30409
|
-
|
|
30393
|
+
issueStatus: (input) => noContent("/legion/v1/issues/status", input, LegionDaemonApi.IssueStatus.response),
|
|
30394
|
+
gatesRegister: (input) => noContent("/legion/v1/gates/register", input, LegionDaemonApi.GatesRegister.response),
|
|
30410
30395
|
workerSession: (input) => post("/legion/v1/worker-session", input, LegionDaemonApi.WorkerSession.response),
|
|
30411
|
-
gatesApprove: (input) => noContent("/legion/v1/gates/approve", input, LegionDaemonApi.GatesApprove.response),
|
|
30412
|
-
admission: (input) => post("/legion/v1/admission", input, LegionDaemonApi.Admission.response),
|
|
30413
|
-
backlog: (input) => noContent("/legion/v1/backlog", input, LegionDaemonApi.Backlog.response),
|
|
30414
30396
|
processExit: (input) => noContent("/legion/v1/process/exit", input, LegionDaemonApi.ProcessExit.response),
|
|
30415
30397
|
grant: (input) => post("/legion/v1/grants", input, LegionDaemonApi.Grant.response),
|
|
30416
30398
|
githubToken: (input) => post("/legion/v1/gh-token", input, LegionDaemonApi.GitHubToken.response)
|
|
@@ -30504,15 +30486,25 @@ function toolFailure(error) {
|
|
|
30504
30486
|
|
|
30505
30487
|
// src/legion/tools.ts
|
|
30506
30488
|
var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
|
|
30489
|
+
var LIFECYCLE_STATUSES2 = [
|
|
30490
|
+
"triage",
|
|
30491
|
+
"icebox",
|
|
30492
|
+
"backlog",
|
|
30493
|
+
"todo",
|
|
30494
|
+
"in_progress",
|
|
30495
|
+
"testing",
|
|
30496
|
+
"needs_review",
|
|
30497
|
+
"retro",
|
|
30498
|
+
"done"
|
|
30499
|
+
];
|
|
30500
|
+
function isLifecycleStatus(value) {
|
|
30501
|
+
return LIFECYCLE_STATUSES2.includes(value);
|
|
30502
|
+
}
|
|
30507
30503
|
var LEGION_OP_FIELDS = {
|
|
30508
|
-
|
|
30509
|
-
|
|
30510
|
-
|
|
30511
|
-
post_spec: ["issue", "body"],
|
|
30512
|
-
label_add: ["issue", "label"],
|
|
30504
|
+
set_status: ["issue", "status"],
|
|
30505
|
+
register_gate: ["issue", "askId"],
|
|
30506
|
+
release_wave: ["issues"],
|
|
30513
30507
|
escalate: ["kind", "context"],
|
|
30514
|
-
request_refile: ["issue", "rationale"],
|
|
30515
|
-
issue_close: ["issue", "comment"],
|
|
30516
30508
|
merge_gate: ["pr"],
|
|
30517
30509
|
spawn_worker: ["issue", "role", "task"]
|
|
30518
30510
|
};
|
|
@@ -30520,27 +30512,20 @@ function legionToolSchema(pi) {
|
|
|
30520
30512
|
const z = pi.zod;
|
|
30521
30513
|
return z.object({
|
|
30522
30514
|
op: z.enum([
|
|
30523
|
-
"
|
|
30524
|
-
"
|
|
30525
|
-
"
|
|
30526
|
-
"post_spec",
|
|
30527
|
-
"label_add",
|
|
30515
|
+
"set_status",
|
|
30516
|
+
"register_gate",
|
|
30517
|
+
"release_wave",
|
|
30528
30518
|
"escalate",
|
|
30529
|
-
"request_refile",
|
|
30530
|
-
"issue_close",
|
|
30531
30519
|
"merge_gate",
|
|
30532
30520
|
"spawn_worker"
|
|
30533
30521
|
]),
|
|
30534
|
-
title: z.string().optional(),
|
|
30535
|
-
body: z.string().optional(),
|
|
30536
|
-
labels: z.array(z.enum(ARCHITECT_MUTABLE_LABELS)).optional(),
|
|
30537
|
-
children: z.array(z.string()).optional(),
|
|
30538
30522
|
issue: z.string().optional(),
|
|
30539
|
-
|
|
30523
|
+
status: z.enum(LIFECYCLE_STATUSES2).optional(),
|
|
30524
|
+
askId: z.string().optional(),
|
|
30540
30525
|
kind: z.enum(["re-file", "capacity", "cross-tree"]).optional(),
|
|
30541
30526
|
context: z.unknown().optional(),
|
|
30527
|
+
issues: z.array(z.string()).optional(),
|
|
30542
30528
|
rationale: z.string().optional(),
|
|
30543
|
-
comment: z.string().optional(),
|
|
30544
30529
|
pr: z.number().optional(),
|
|
30545
30530
|
role: z.enum(LEGION_ROLES).optional(),
|
|
30546
30531
|
task: z.string().optional()
|
|
@@ -30589,60 +30574,39 @@ function createLegionTool(deps) {
|
|
|
30589
30574
|
sessionId,
|
|
30590
30575
|
secret: architect.secret
|
|
30591
30576
|
}));
|
|
30592
|
-
case "
|
|
30593
|
-
const
|
|
30594
|
-
if (
|
|
30595
|
-
throw new Error("
|
|
30596
|
-
}
|
|
30597
|
-
return jsonSuccess(await daemon.issueCreate({
|
|
30598
|
-
tree: architect.tree,
|
|
30599
|
-
sessionId,
|
|
30600
|
-
secret: architect.secret,
|
|
30601
|
-
title: stringInput("title"),
|
|
30602
|
-
body: stringInput("body"),
|
|
30603
|
-
labels: labels ?? []
|
|
30604
|
-
}));
|
|
30605
|
-
}
|
|
30606
|
-
case "wave_release": {
|
|
30607
|
-
const children = parameters.children;
|
|
30608
|
-
if (!Array.isArray(children) || !children.every((child) => typeof child === "string")) {
|
|
30609
|
-
throw new Error("wave_release requires children");
|
|
30577
|
+
case "set_status": {
|
|
30578
|
+
const status = parameters.status;
|
|
30579
|
+
if (typeof status !== "string" || !isLifecycleStatus(status)) {
|
|
30580
|
+
throw new Error("set_status requires a valid Legion issue status");
|
|
30610
30581
|
}
|
|
30611
|
-
|
|
30612
|
-
tree: architect.tree,
|
|
30613
|
-
children,
|
|
30614
|
-
sessionId,
|
|
30615
|
-
secret: architect.secret
|
|
30616
|
-
}));
|
|
30617
|
-
}
|
|
30618
|
-
case "comment":
|
|
30619
|
-
return jsonSuccess(await daemon.comment({
|
|
30582
|
+
await daemon.issueStatus({
|
|
30620
30583
|
tree: architect.tree,
|
|
30621
30584
|
sessionId,
|
|
30622
30585
|
secret: architect.secret,
|
|
30623
30586
|
issue: stringInput("issue"),
|
|
30624
|
-
|
|
30625
|
-
})
|
|
30626
|
-
|
|
30627
|
-
|
|
30587
|
+
status
|
|
30588
|
+
});
|
|
30589
|
+
return jsonSuccess({});
|
|
30590
|
+
}
|
|
30591
|
+
case "register_gate":
|
|
30592
|
+
await daemon.gatesRegister({
|
|
30628
30593
|
tree: architect.tree,
|
|
30629
30594
|
sessionId,
|
|
30630
30595
|
secret: architect.secret,
|
|
30631
30596
|
issue: stringInput("issue"),
|
|
30632
|
-
|
|
30597
|
+
askId: stringInput("askId")
|
|
30633
30598
|
});
|
|
30634
30599
|
return jsonSuccess({});
|
|
30635
|
-
case "
|
|
30636
|
-
const
|
|
30637
|
-
if (!
|
|
30638
|
-
throw new Error("
|
|
30600
|
+
case "release_wave": {
|
|
30601
|
+
const issues = parameters.issues;
|
|
30602
|
+
if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
|
|
30603
|
+
throw new Error("release_wave requires issues");
|
|
30639
30604
|
}
|
|
30640
|
-
return jsonSuccess(await daemon.
|
|
30605
|
+
return jsonSuccess(await daemon.releaseWave({
|
|
30641
30606
|
tree: architect.tree,
|
|
30607
|
+
issues,
|
|
30642
30608
|
sessionId,
|
|
30643
|
-
secret: architect.secret
|
|
30644
|
-
issue: stringInput("issue"),
|
|
30645
|
-
add: [label]
|
|
30609
|
+
secret: architect.secret
|
|
30646
30610
|
}));
|
|
30647
30611
|
}
|
|
30648
30612
|
case "escalate": {
|
|
@@ -30661,28 +30625,6 @@ function createLegionTool(deps) {
|
|
|
30661
30625
|
});
|
|
30662
30626
|
return jsonSuccess({});
|
|
30663
30627
|
}
|
|
30664
|
-
case "request_refile":
|
|
30665
|
-
await daemon.escalate({
|
|
30666
|
-
tree: architect.tree,
|
|
30667
|
-
sessionId,
|
|
30668
|
-
secret: architect.secret,
|
|
30669
|
-
kind: "re-file",
|
|
30670
|
-
context: { issue: stringInput("issue"), rationale: stringInput("rationale") }
|
|
30671
|
-
});
|
|
30672
|
-
return jsonSuccess({});
|
|
30673
|
-
case "issue_close": {
|
|
30674
|
-
const comment = parameters.comment;
|
|
30675
|
-
if (comment !== undefined && typeof comment !== "string")
|
|
30676
|
-
throw new Error("issue_close comment must be a string");
|
|
30677
|
-
await daemon.issueClose({
|
|
30678
|
-
tree: architect.tree,
|
|
30679
|
-
sessionId,
|
|
30680
|
-
secret: architect.secret,
|
|
30681
|
-
issue: stringInput("issue"),
|
|
30682
|
-
...comment === undefined ? {} : { comment }
|
|
30683
|
-
});
|
|
30684
|
-
return jsonSuccess({});
|
|
30685
|
-
}
|
|
30686
30628
|
case "spawn_worker": {
|
|
30687
30629
|
const role = parameters.role;
|
|
30688
30630
|
if (typeof role !== "string" || !LEGION_ROLES.includes(role)) {
|
|
@@ -31703,8 +31645,6 @@ class DispatchClient {
|
|
|
31703
31645
|
return this.#json("POST", artifactPath, input);
|
|
31704
31646
|
const form = new FormData;
|
|
31705
31647
|
form.set("name", input.name);
|
|
31706
|
-
if (input.primary !== undefined)
|
|
31707
|
-
form.set("primary", String(input.primary));
|
|
31708
31648
|
if (input.summary !== undefined)
|
|
31709
31649
|
form.set("summary", input.summary);
|
|
31710
31650
|
if (input.actor !== undefined)
|
|
@@ -32223,20 +32163,17 @@ Open anchored asks/comments: ${marks.join(", ")}`,
|
|
|
32223
32163
|
};
|
|
32224
32164
|
}
|
|
32225
32165
|
case "dispatch_artifact": {
|
|
32226
|
-
const primary = optionalBoolean(args, "primary");
|
|
32227
32166
|
const summary = optionalString(args, "summary");
|
|
32228
32167
|
const name = stringArg(args, "name");
|
|
32229
32168
|
const content = optionalString(args, "content");
|
|
32230
32169
|
const result = await client.artifact(issue(), content === undefined ? {
|
|
32231
32170
|
name,
|
|
32232
32171
|
file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
|
|
32233
|
-
...primary === undefined ? {} : { primary },
|
|
32234
32172
|
...summary === undefined ? {} : { summary },
|
|
32235
32173
|
actor
|
|
32236
32174
|
} : {
|
|
32237
32175
|
name,
|
|
32238
32176
|
content,
|
|
32239
|
-
...primary === undefined ? {} : { primary },
|
|
32240
32177
|
...summary === undefined ? {} : { summary },
|
|
32241
32178
|
actor
|
|
32242
32179
|
});
|
|
@@ -12,6 +12,38 @@ The server enforces high signal: an ask question is at most 800 characters with
|
|
|
12
12
|
options; comment and message bodies are at most 2,000 characters; an artifact is at most 25 MiB.
|
|
13
13
|
It refuses over-limit input; it never truncates it. GitHub threads and markers no longer exist.
|
|
14
14
|
|
|
15
|
+
## Writing a spec
|
|
16
|
+
|
|
17
|
+
A spec is a decision record for the human who decides and the implementer who builds, not a
|
|
18
|
+
transcript of your thinking. Use exactly these document headings in this order.
|
|
19
|
+
|
|
20
|
+
| Section | Required content | Form |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| **Decisions needed** | Only decisions requiring human authority, taste, or risk appetite. Each states one question, two or three options with tradeoffs, and a recommendation. Every item is an anchored `dispatch_ask`. Answered items move into Requirements with provenance, then leave this section. No other section asks the reader anything. Empty means `None.` | One decision per line; anchor each ask to that line. |
|
|
23
|
+
| **Acceptance** | Every outcome names its check and user-facing surface. An outcome without a verification method is not acceptance criteria. | Numbered lines; browser scenario, API call, or CLI command. |
|
|
24
|
+
| **Requirements** | Provenance is a verbatim human quote or `inferred: <reasoning>`; readers treat inferred requirements as hypotheses. Do not restate the prompt in prose. | `requirement \| provenance` table. |
|
|
25
|
+
| **Design** | State the files, components, routes, and data flow that change. | Facts, not narrative; diagrams only for genuine structure. |
|
|
26
|
+
| **Errors** | Name the behaviour for every error condition; never specify a silent fallback. | `condition \| behaviour` table. |
|
|
27
|
+
| **Testing** | Map every acceptance line to the proof that exercises it. | Suite or scenario. |
|
|
28
|
+
| **Rejected** | Record each considered alternative and why it was rejected so it is not proposed again. | One alternative per line. |
|
|
29
|
+
|
|
30
|
+
### Rules
|
|
31
|
+
|
|
32
|
+
- Every sentence is a fact, decision, or risk; delete the rest.
|
|
33
|
+
- Use tables over prose and keep one idea per line.
|
|
34
|
+
- Do not use Overview, Background, Introduction, Summary, or Conclusion sections.
|
|
35
|
+
- Do not hedge with “might” or “could consider.”
|
|
36
|
+
- Do not use TBD, TODO, or placeholders; an open item is a Decision needed.
|
|
37
|
+
- Keep each section to one screen; work that exceeds one screen per section is two specs.
|
|
38
|
+
- Update the spec in place as decisions land. The spec is the record; comments are the discussion.
|
|
39
|
+
|
|
40
|
+
### Self-review
|
|
41
|
+
|
|
42
|
+
- [ ] No placeholders remain.
|
|
43
|
+
- [ ] No sections conflict.
|
|
44
|
+
- [ ] The spec covers one implementation plan's worth of work.
|
|
45
|
+
- [ ] Every requirement has exactly one reading.
|
|
46
|
+
|
|
15
47
|
## Your issue
|
|
16
48
|
|
|
17
49
|
Every session works on an issue. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key
|
|
@@ -26,6 +58,7 @@ dispatch_issue({ project, title, parent?, external?, spec? })
|
|
|
26
58
|
```
|
|
27
59
|
It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use
|
|
28
60
|
it to park a question.
|
|
61
|
+
When `spec` is supplied, follow [Writing a spec](#writing-a-spec).
|
|
29
62
|
|
|
30
63
|
## Asking
|
|
31
64
|
|
|
@@ -65,6 +98,7 @@ answer. Use `reply_to_ask` on `dispatch_comment` to reply under your own ask; it
|
|
|
65
98
|
exclusive with `reply_to`.
|
|
66
99
|
|
|
67
100
|
## The spec is where narrative goes
|
|
101
|
+
Write and update the issue specification according to [Writing a spec](#writing-a-spec).
|
|
68
102
|
|
|
69
103
|
Read the current document before changing it:
|
|
70
104
|
|
|
@@ -72,7 +106,7 @@ Read the current document before changing it:
|
|
|
72
106
|
dispatch_doc_read({ issue?, artifact?, version?, ref? })
|
|
73
107
|
```
|
|
74
108
|
It returns live or versioned markdown with open marks and `details` `{ issue }`; omit `artifact`
|
|
75
|
-
with `issue` to read the
|
|
109
|
+
with `issue` to read the issue specification. Then write narrative with:
|
|
76
110
|
|
|
77
111
|
```ts
|
|
78
112
|
dispatch_doc_edit({ issue, artifact, ops, summary? })
|
|
@@ -119,27 +153,26 @@ dispatch_suggest({ issue, artifact, quote, replace_with, body?, occurrence? })
|
|
|
119
153
|
It returns `details` `{ issue, topic, comment }`. A human accepts or rejects a suggestion. On
|
|
120
154
|
`TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
|
|
121
155
|
before retrying. `INVALID_OP` names a malformed edit; `CAP_EXCEEDED` never truncates;
|
|
122
|
-
`ISSUE_CLOSED` rejects a write. `ACTOR_KIND
|
|
123
|
-
invalid actor, route, or primary artifact.
|
|
156
|
+
`ISSUE_CLOSED` rejects a write. `ACTOR_KIND` and `ROUTE_INVALID` reject an invalid actor or route.
|
|
124
157
|
|
|
125
158
|
## Artifacts
|
|
126
159
|
|
|
127
160
|
Attach an image, diagram, or local file with:
|
|
128
161
|
|
|
129
162
|
```ts
|
|
130
|
-
dispatch_artifact({ issue, name, path,
|
|
163
|
+
dispatch_artifact({ issue, name, path, summary? })
|
|
131
164
|
```
|
|
132
165
|
|
|
133
166
|
Or, when the text is already in the call, post a Markdown document directly:
|
|
134
167
|
|
|
135
168
|
```ts
|
|
136
|
-
dispatch_artifact({ issue, name: "spec.md", content: "# Design\n..."
|
|
169
|
+
dispatch_artifact({ issue, name: "spec.md", content: "# Design\n..." })
|
|
137
170
|
```
|
|
138
171
|
|
|
139
172
|
Exactly one of `path` and `content` is required. The inline form sends JSON with
|
|
140
173
|
`Content-Type: application/json`. It returns `details` `{ issue, topic, artifact, version }`.
|
|
141
|
-
Uploading the same `name` creates its next version. Use `content`
|
|
142
|
-
|
|
174
|
+
Uploading the same `name` creates its next version. Use `content` when the text is already in
|
|
175
|
+
the call.
|
|
143
176
|
|
|
144
177
|
## Messages
|
|
145
178
|
|
|
@@ -104,7 +104,7 @@ gh api graphql -f query='query {
|
|
|
104
104
|
Add a label to an issue (additive — does not remove existing labels):
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
gh issue edit $ISSUE_NUMBER --add-label "
|
|
107
|
+
gh issue edit $ISSUE_NUMBER --add-label "bug" -R $OWNER/$REPO
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
**Parameters:**
|
|
@@ -114,7 +114,7 @@ gh issue edit $ISSUE_NUMBER --add-label "needs-approval" -R $OWNER/$REPO
|
|
|
114
114
|
|
|
115
115
|
**Example:**
|
|
116
116
|
```bash
|
|
117
|
-
gh issue edit 123 --add-label "
|
|
117
|
+
gh issue edit 123 --add-label "bug" -R acme/backend
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### Remove Label
|
|
@@ -122,7 +122,7 @@ gh issue edit 123 --add-label "needs-approval" -R acme/backend
|
|
|
122
122
|
Remove a label from an issue:
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
gh issue edit $ISSUE_NUMBER --remove-label "
|
|
125
|
+
gh issue edit $ISSUE_NUMBER --remove-label "wontfix" -R $OWNER/$REPO
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
**Parameters:**
|
|
@@ -132,7 +132,7 @@ gh issue edit $ISSUE_NUMBER --remove-label "needs-approval" -R $OWNER/$REPO
|
|
|
132
132
|
|
|
133
133
|
**Example:**
|
|
134
134
|
```bash
|
|
135
|
-
gh issue edit 123 --remove-label "
|
|
135
|
+
gh issue edit 123 --remove-label "wontfix" -R acme/backend
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
### Comment on Issue
|
|
@@ -10,29 +10,31 @@ with human-created children; either way you own its complete outcome. Work from
|
|
|
10
10
|
wakes and current artifacts. Do not perform code work yourself and do not rely on a
|
|
11
11
|
separate coordinator to finish necessary work.
|
|
12
12
|
|
|
13
|
+
This skill documents the target Dispatch-native contract. The `legion` tool's
|
|
14
|
+
`register_gate`/`release_wave`/`set_status` ops, the Dispatch key format, and the
|
|
15
|
+
`dispatch_*` tool family land with PR B (#TBD); until that PR merges, this skill's contract
|
|
16
|
+
is not yet runnable on `main`.
|
|
17
|
+
|
|
13
18
|
## Tool and ownership boundaries
|
|
14
19
|
|
|
15
|
-
- Use the `legion` tool for lifecycle writes. Its issue key
|
|
16
|
-
`
|
|
20
|
+
- Use the `legion` tool for lifecycle writes. Its issue key is the Dispatch key
|
|
21
|
+
(pattern `^[A-Z][A-Z0-9]*-[0-9]+$`, e.g. `LEGION-41`).
|
|
17
22
|
- Use `legion({ op: "spawn_worker", issue, role, task })` for every Legion role spawn.
|
|
18
23
|
Message a known phase worker with `envoy_publish` to `notifications.role.` followed by
|
|
19
24
|
its encoded role token (the token `spawn_worker` returned for it); re-assign it by
|
|
20
25
|
calling `spawn_worker` again on the same existing role, which resumes the same process
|
|
21
26
|
instead of starting a fresh one. Phase workers escalate lifecycle, scope, and
|
|
22
27
|
cross-phase matters the same way: `envoy_publish` to your own encoded token. Any role
|
|
23
|
-
may use `
|
|
28
|
+
may use `dispatch_ask` directly for a standalone human question; replies return to the
|
|
24
29
|
asking session.
|
|
25
30
|
- The daemon spawns each role as its own process with the issue's context already in its
|
|
26
31
|
environment. Never hand-format a role token: the daemon encodes one as
|
|
27
|
-
`legion-<project>-<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- Use only the live label vocabulary: `needs-approval`, `human-approved`,
|
|
34
|
-
`legion-child`, and `legion-backlog`. Do not attempt to apply a label whose ownership
|
|
35
|
-
belongs to the controller or Sami.
|
|
32
|
+
`legion-<project>-<KEY>-<role>`; for example, project `acme`, issue `LEGION-41`, role
|
|
33
|
+
`architect` encodes to `legion-acme-LEGION-41-architect`. Reuse a token you already
|
|
34
|
+
hold (your own, or one `spawn_worker` returned) or compute another with the
|
|
35
|
+
`roleToken` helper from `@legion/contracts` exactly the way the daemon does.
|
|
36
|
+
- There is no label vocabulary. Dispatch status replaces the board, and the design gate
|
|
37
|
+
is a `dispatch_ask` answered `Approve`, not a label. Never attempt to apply a label.
|
|
36
38
|
- Deferring necessary work is failure. The sole valid deferral is a new child issue you
|
|
37
39
|
create and continue to own. Re-file a genuinely independent child through the
|
|
38
40
|
controller rather than treating it as an abandoned dependency.
|
|
@@ -46,7 +48,7 @@ exercise a criterion end to end, building that path is a child issue of this tre
|
|
|
46
48
|
|
|
47
49
|
- **Existing children:** adopt them. Do not replace or re-decompose human-created work.
|
|
48
50
|
Put every adopted child into the initial wave. **You MUST call**
|
|
49
|
-
`legion({ op: "
|
|
51
|
+
`legion({ op: "release_wave", issues: ["LEGION-41", "LEGION-42"] })`
|
|
50
52
|
**before any `spawn_worker` call for an adopted child.** Until release, the daemon
|
|
51
53
|
holds that child's role activity. Then spawn each child's daemon-managed sub-architect
|
|
52
54
|
owner.
|
|
@@ -54,16 +56,21 @@ exercise a criterion end to end, building that path is a child issue of this tre
|
|
|
54
56
|
completed and integrated as one unit. Otherwise create complete child issues with:
|
|
55
57
|
|
|
56
58
|
```text
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
dispatch_issue({
|
|
60
|
+
project: "<project>",
|
|
61
|
+
parent: "<root issue>",
|
|
59
62
|
title: "<child outcome>",
|
|
60
|
-
|
|
61
|
-
labels: []
|
|
63
|
+
spec: "<acceptance criteria, scope, and context>"
|
|
62
64
|
})
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
the
|
|
67
|
+
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
|
|
68
|
+
the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string. A
|
|
69
|
+
root session has `LEGION_TREE == LEGION_ISSUE`. The daemon establishes the sub-issue
|
|
70
|
+
relationship from `parent`. Keep the returned issue keys in ordered waves; a child is
|
|
71
|
+
inert until released.
|
|
72
|
+
|
|
73
|
+
Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
|
|
67
74
|
|
|
68
75
|
Write one root specification containing the accepted scope, adoption/decomposition,
|
|
69
76
|
waves, acceptance criteria, and integration test. When the config-armed root design gate
|
|
@@ -71,20 +78,18 @@ applies, run this exact sequence **before any Legion-role spawn**, including a
|
|
|
71
78
|
sub-architect:
|
|
72
79
|
|
|
73
80
|
```text
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
context: "<what the tree is, what triggered the gate>",
|
|
80
|
-
question: "<specification summary and the decision requested>"
|
|
81
|
+
dispatch_artifact({ issue: "<root issue>", name: "spec.md", content: "<root specification>", summary: "<one-line summary>" })
|
|
82
|
+
askId = dispatch_ask({
|
|
83
|
+
issue: "<root issue>",
|
|
84
|
+
question: "<specification summary and the decision requested>",
|
|
85
|
+
options: [{ label: "Approve" }, ...]
|
|
81
86
|
})
|
|
87
|
+
legion({ op: "register_gate", issue: "<root issue>", askId })
|
|
82
88
|
```
|
|
83
89
|
|
|
84
90
|
Then park. Do not release a wave or spawn a Legion role until a later delivered wake
|
|
85
|
-
shows `
|
|
86
|
-
|
|
87
|
-
this sequence.
|
|
91
|
+
shows `design-approved` on the root. Approval covers the entire tree: later waves,
|
|
92
|
+
re-scopes, and integration-failure children do not repeat this sequence.
|
|
88
93
|
|
|
89
94
|
## 2. Children in flight
|
|
90
95
|
|
|
@@ -92,7 +97,7 @@ Release only the next useful wave, then give its owners their work. A release is
|
|
|
92
97
|
explicit lifecycle write:
|
|
93
98
|
|
|
94
99
|
```text
|
|
95
|
-
legion({ op: "
|
|
100
|
+
legion({ op: "release_wave", issues: ["LEGION-41", "LEGION-42"] })
|
|
96
101
|
```
|
|
97
102
|
|
|
98
103
|
After release, spawn each relevant owner; for example:
|
|
@@ -100,7 +105,7 @@ After release, spawn each relevant owner; for example:
|
|
|
100
105
|
```text
|
|
101
106
|
legion({
|
|
102
107
|
op: "spawn_worker",
|
|
103
|
-
issue: "
|
|
108
|
+
issue: "LEGION-41",
|
|
104
109
|
role: "architect",
|
|
105
110
|
task: "Own this child through its lifecycle and report its evidence."
|
|
106
111
|
})
|
|
@@ -122,7 +127,7 @@ and current `main` integration surface:
|
|
|
122
127
|
```text
|
|
123
128
|
legion({
|
|
124
129
|
op: "spawn_worker",
|
|
125
|
-
issue: "
|
|
130
|
+
issue: "LEGION-40",
|
|
126
131
|
role: "tester",
|
|
127
132
|
task: "Verify this parent issue against its acceptance criteria on current main; return reproducible integration evidence."
|
|
128
133
|
})
|
|
@@ -181,15 +186,12 @@ If anything changes the approved head, return to review; do not let the merger p
|
|
|
181
186
|
|
|
182
187
|
## 7. Close
|
|
183
188
|
|
|
184
|
-
After the merge result and sign-off are recorded, close this issue
|
|
185
|
-
|
|
189
|
+
After the merge result and sign-off are recorded, post the sign-off and close this issue
|
|
190
|
+
through the Legion write surface:
|
|
186
191
|
|
|
187
192
|
```text
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
issue: "owner/repo#40",
|
|
191
|
-
comment: "<sign-off: scope, integration evidence, review, retro, Sami approval, and merge>"
|
|
192
|
-
})
|
|
193
|
+
dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, Sami approval, and merge>" })
|
|
194
|
+
legion({ op: "set_status", issue: "LEGION-40", status: "done" })
|
|
193
195
|
```
|
|
194
196
|
|
|
195
197
|
Closing a child supplies the closure event to its parent. Do not close a parent until the
|
|
@@ -220,6 +222,6 @@ corresponding lifecycle procedure.
|
|
|
220
222
|
|
|
221
223
|
Controller-actionable matters are exactly re-filing a genuinely independent child,
|
|
222
224
|
capacity, and cross-tree conflict. Use the Legion escalation operation for those. Handle
|
|
223
|
-
everything else in the tree, or use `
|
|
224
|
-
Sami directly with `
|
|
225
|
+
everything else in the tree, or use `dispatch_ask` for a human question; workers may reach
|
|
226
|
+
Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
|
|
225
227
|
source.
|
|
@@ -9,6 +9,10 @@ The controller is the one persistent, wake-driven session for a Legion project.
|
|
|
9
9
|
triage, escalation, and human-interaction judgments; it never does phase-worker work or
|
|
10
10
|
routes raw events into an architect.
|
|
11
11
|
|
|
12
|
+
This skill documents the target Dispatch-native contract. The `legion` tool's `set_status`
|
|
13
|
+
op, `dispatch_read`/`dispatch_issue`, and the Dispatch key format land with PR B (#TBD);
|
|
14
|
+
until that PR merges, this skill's contract is not yet runnable on `main`.
|
|
15
|
+
|
|
12
16
|
## Start and claim the controller role
|
|
13
17
|
|
|
14
18
|
The Legion extension claims `legion-<project>-controller` and registers controller readiness
|
|
@@ -23,11 +27,11 @@ For an interactive takeover, start OMP with `LEGION_CONTROLLER_SECRET` and
|
|
|
23
27
|
|
|
24
28
|
The command resolves the project from daemon state, claims the Envoy role for the current
|
|
25
29
|
session, and posts readiness before controller commands can act. It retains the environment
|
|
26
|
-
capability for `legion
|
|
30
|
+
capability for `legion({ op: "set_status", issue, status })`. Never pass a secret as a command argument
|
|
27
31
|
or copy it into a transcript.
|
|
28
32
|
|
|
29
33
|
This handshake lets the daemon redeliver held controller work. It does not turn the controller
|
|
30
|
-
into a state holder: daemon state and
|
|
34
|
+
into a state holder: daemon state and the Dispatch project remain authoritative.
|
|
31
35
|
|
|
32
36
|
## Turn discipline
|
|
33
37
|
|
|
@@ -36,7 +40,7 @@ into a state holder: daemon state and GitHub artifacts remain authoritative.
|
|
|
36
40
|
- **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
|
|
37
41
|
poll, idle-loop, or wait for another event.
|
|
38
42
|
- **Wakes are advisory.** Before any side effect, verify the current daemon state and the
|
|
39
|
-
relevant
|
|
43
|
+
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
40
44
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
41
45
|
bookkeeping between turns.
|
|
42
46
|
|
|
@@ -44,52 +48,59 @@ into a state holder: daemon state and GitHub artifacts remain authoritative.
|
|
|
44
48
|
|
|
45
49
|
| Wake | Content | Controller action |
|
|
46
50
|
|---|---|---|
|
|
47
|
-
| New issue
|
|
48
|
-
| Backlog eligibility | slot freed / priority change | Reconsider parked items
|
|
49
|
-
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `
|
|
50
|
-
| Resync report | artifact-driven anomaly list (zero-owner trees,
|
|
51
|
-
|
|
|
52
|
-
|
|
|
51
|
+
| New issue created in the Dispatch project (`issue.created`, status `triage`; resync heals misses) | issue key + triage context (incl. pre-existing children) | Triage: `legion({ op: "set_status", issue, status: "todo" })` to admit, or set `backlog`/`icebox` to park |
|
|
52
|
+
| Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
53
|
+
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
|
|
54
|
+
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed) | Verify against fresh state, then heal |
|
|
55
|
+
| `child-status` | child key + status transition | Not controller-actionable by default; if the daemon could not route it to the parent's architect role, verify the transition and forward it with `envoy_publish` |
|
|
56
|
+
| Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
|
|
57
|
+
| Closed-tree activity (comment, review, CI on a closed tree) | issue, root, event summary | Read the artifact; if work should resume, `legion({ op: "set_status", issue: root, status: "todo" })`; otherwise no action — the event is not held or redelivered |
|
|
53
58
|
| Direct user message | — | Always first |
|
|
54
59
|
|
|
55
60
|
## New issue triage
|
|
56
61
|
|
|
57
|
-
1. Read `legion state --json`, then inspect the reported
|
|
58
|
-
Verify the issue is
|
|
59
|
-
has pre-existing children.
|
|
62
|
+
1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch_read`.
|
|
63
|
+
Verify the issue is in this project, is eligible for a root process, and whether it
|
|
64
|
+
has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
|
|
60
65
|
2. If it should run now, admit the root issue:
|
|
61
66
|
|
|
62
|
-
```
|
|
63
|
-
legion
|
|
67
|
+
```text
|
|
68
|
+
legion({ op: "set_status", issue: "<issue>", status: "todo" })
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
3. If it should deliberately wait,
|
|
71
|
+
3. If it should deliberately wait, move it to a parked status instead of leaving it in
|
|
72
|
+
`triage`:
|
|
67
73
|
|
|
68
|
-
```
|
|
69
|
-
legion
|
|
74
|
+
```text
|
|
75
|
+
legion({ op: "set_status", issue: "<issue>", status: "backlog" })
|
|
70
76
|
```
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
(or `status: "icebox"` for longer-term deferral). Dispatch status is the durable record;
|
|
79
|
+
there is no separate marker to maintain. Do not triage a system-created child as a root
|
|
80
|
+
issue.
|
|
74
81
|
|
|
75
82
|
## Backlog eligibility
|
|
76
83
|
|
|
77
|
-
When a slot frees or priority changes, use `legion state --json` and the current
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
is a deliberate controller decision, not a no-op.
|
|
84
|
+
When a slot frees or priority changes, use `legion state --json` and the current Dispatch
|
|
85
|
+
issue to reconsider parked roots. Admit the selected root with
|
|
86
|
+
`legion({ op: "set_status", issue, status: "todo" })`. Moving an item to or from `backlog`/
|
|
87
|
+
`icebox` is a deliberate controller decision, not a no-op.
|
|
81
88
|
|
|
82
89
|
## Architect escalation
|
|
83
90
|
|
|
84
91
|
Only decide controller-actionable escalations: re-filing independent work, capacity, and
|
|
85
|
-
cross-tree conflicts. Issue-scoped human Q&A goes through `
|
|
92
|
+
cross-tree conflicts. Issue-scoped human Q&A goes through `dispatch_ask` from the owning
|
|
86
93
|
architect, not the controller.
|
|
87
94
|
|
|
88
|
-
For an independence judgment, verify the child and its parent against
|
|
89
|
-
|
|
95
|
+
For an independence judgment, verify the child and its parent against current daemon state
|
|
96
|
+
and the Dispatch issue. If the work belongs in an independent root:
|
|
90
97
|
|
|
91
|
-
1. File a **fresh root issue** with `
|
|
92
|
-
|
|
98
|
+
1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`).
|
|
99
|
+
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
|
|
100
|
+
the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string.
|
|
101
|
+
2. Park the child (`legion({ op: "set_status", issue: child, status: "icebox" })`) and leave
|
|
102
|
+
a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
|
|
103
|
+
only — only the owning architect or the daemon closes an issue as `done`.
|
|
93
104
|
3. Admit or deliberately backlog the new root through the normal triage procedure.
|
|
94
105
|
|
|
95
106
|
Never promote a child in place. Resolve capacity and cross-tree conflicts from verified
|
|
@@ -98,11 +109,11 @@ judgments.
|
|
|
98
109
|
|
|
99
110
|
## Resync report
|
|
100
111
|
|
|
101
|
-
Treat a resync report as an anomaly list, not an instruction. For every zero-owner tree
|
|
102
|
-
|
|
103
|
-
Then heal the verified condition: admit an eligible root,
|
|
104
|
-
|
|
105
|
-
until their source artifact explains the anomaly.
|
|
112
|
+
Treat a resync report as an anomaly list, not an instruction. For every zero-owner tree,
|
|
113
|
+
untriaged-open, or launch-failed issue it names, verify `legion state --json` and the
|
|
114
|
+
current Dispatch issue first. Then heal the verified condition: admit an eligible root, move
|
|
115
|
+
an issue back to its intended status, or use the applicable daemon control path. Do not act
|
|
116
|
+
on stale entries until their source artifact explains the anomaly.
|
|
106
117
|
|
|
107
118
|
## Mentions
|
|
108
119
|
|
|
@@ -111,13 +122,3 @@ human-facing information. Otherwise resolve the authoritative owning architect r
|
|
|
111
122
|
route the verified context with `envoy_publish`. Do not route raw event traffic or invent a
|
|
112
123
|
role token from a partial issue reference.
|
|
113
124
|
|
|
114
|
-
## Label vocabulary
|
|
115
|
-
|
|
116
|
-
Use only the project labels below, with their stated ownership:
|
|
117
|
-
|
|
118
|
-
| Label | Applied by | Removed by | Meaning |
|
|
119
|
-
|---|---|---|---|
|
|
120
|
-
| `needs-approval` | architect | controller/Sami when applying `human-approved` | design gate armed, awaiting Sami |
|
|
121
|
-
| `human-approved` | Sami or controller | Sami | design gate open |
|
|
122
|
-
| `legion-child` | daemon | never | system-created child |
|
|
123
|
-
| `legion-backlog` | controller | controller | deliberately unowned root |
|
|
@@ -11,6 +11,10 @@ phase gets its own long-lived process against the same jj workspace, run in turn
|
|
|
11
11
|
the phase assigned to you, report its completion to the architect, and leave the durable
|
|
12
12
|
copy the next phase can trust.
|
|
13
13
|
|
|
14
|
+
This skill documents the target Dispatch-native contract. `LEGION_ISSUE` as a Dispatch key,
|
|
15
|
+
the `dispatch_ask` tool, and the `Dispatch: <KEY>` PR-body linkage land with PR B (#TBD);
|
|
16
|
+
until that PR merges, this skill's contract is not yet runnable on `main`.
|
|
17
|
+
|
|
14
18
|
## Identity, scope, and role
|
|
15
19
|
|
|
16
20
|
The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
|
|
@@ -21,13 +25,11 @@ completes the boot handshake for you at session start — it registers with the
|
|
|
21
25
|
your role, and signals readiness. You never call `envoy_role_set` yourself.
|
|
22
26
|
|
|
23
27
|
Your role token is not the issue key spelled out literally. The daemon encodes it as
|
|
24
|
-
`legion-<project>-<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is yours
|
|
30
|
-
with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
|
|
28
|
+
`legion-<project>-<KEY>-<role>`. For example, project `acme`, issue `LEGION-41`, role
|
|
29
|
+
`architect` encodes to `legion-acme-LEGION-41-architect`. Never hand-format one for another
|
|
30
|
+
role: your own role topic and your tree's architect's topic are stated at the end of your
|
|
31
|
+
system prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is
|
|
32
|
+
yours with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
|
|
31
33
|
computes any other one exactly the way the daemon does — prefer a topic you've already
|
|
32
34
|
been given before recomputing one.
|
|
33
35
|
|
|
@@ -47,7 +49,7 @@ Escalate a product, scope, cross-phase, or lifecycle decision to the owning arch
|
|
|
47
49
|
`envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
|
|
48
50
|
above), carrying the verified facts and the decision needed. `hub` only reaches subagents
|
|
49
51
|
inside your own process, not the architect's separate one. For a durable question that needs
|
|
50
|
-
Sami directly, you may use
|
|
52
|
+
Sami directly, you may use `dispatch_ask` yourself; replies return to your own
|
|
51
53
|
session.
|
|
52
54
|
|
|
53
55
|
Because the same agent is always resumed for its phase, you may receive more than one
|
|
@@ -116,6 +118,8 @@ path-scoped workflow.
|
|
|
116
118
|
|
|
117
119
|
## Phase work
|
|
118
120
|
|
|
121
|
+
Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
|
|
122
|
+
|
|
119
123
|
Follow the repository's normal engineering workflow and the assigned issue's acceptance
|
|
120
124
|
criteria. Your phase's own charter and the predecessor handoffs you read define the phase
|
|
121
125
|
artifact and its completion evidence. Do not replace architect-owned decomposition, gate
|
|
@@ -133,11 +137,13 @@ capability it needs; invoke GitHub through the credential helper:
|
|
|
133
137
|
legion gh -- <gh args…>
|
|
134
138
|
```
|
|
135
139
|
|
|
136
|
-
## GitHub comment attribution
|
|
140
|
+
## GitHub PR comment attribution
|
|
137
141
|
|
|
138
|
-
Append this exact structured footer to **every**
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
Append this exact structured footer to **every** pull-request comment and review that this
|
|
143
|
+
phase posts on GitHub. It preserves session provenance on the artifact itself so work stays
|
|
144
|
+
attributable to the session that produced it. Dispatch comments carry session provenance
|
|
145
|
+
natively through their own `actor`/`origin` fields; this footer is only for GitHub PR
|
|
146
|
+
artifacts:
|
|
141
147
|
|
|
142
148
|
```html
|
|
143
149
|
<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->
|
|
@@ -146,7 +152,7 @@ itself so work stays attributable to the session that produced it:
|
|
|
146
152
|
For example:
|
|
147
153
|
|
|
148
154
|
```bash
|
|
149
|
-
legion gh --
|
|
155
|
+
legion gh -- pr comment <pr-number> \
|
|
150
156
|
--body $'Verification complete.\n\n<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->' \
|
|
151
157
|
--repo <owner>/<repo>
|
|
152
158
|
```
|
|
@@ -159,17 +165,19 @@ procedure:
|
|
|
159
165
|
|
|
160
166
|
```bash
|
|
161
167
|
cd -- "$LEGION_WORKSPACE" && \
|
|
162
|
-
jj -R "$LEGION_WORKSPACE" bookmark set legion
|
|
163
|
-
jj -R "$LEGION_WORKSPACE" git push --bookmark legion
|
|
168
|
+
jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> && \
|
|
169
|
+
jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
|
|
164
170
|
```
|
|
165
171
|
|
|
166
172
|
The provisioned issue workspace configures `credential.helper` with the daemon's absolute
|
|
167
173
|
credential command, so `jj -R "$LEGION_WORKSPACE" git push` authenticates transparently
|
|
168
174
|
through the same session capability. Never handle a token.
|
|
169
175
|
|
|
170
|
-
Then open the pull request with `legion gh -- pr create`. The
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
Then open the pull request with `legion gh -- pr create`. The PR body **must** contain the
|
|
177
|
+
line `Dispatch: <KEY>` — the daemon's fallback link from a PR to its Dispatch issue when the
|
|
178
|
+
branch name alone is ambiguous. The credential helper and `legion gh` provide the GitHub
|
|
179
|
+
identity; never export, fetch, or replace a token. Other phases advance the existing branch
|
|
180
|
+
rather than creating a replacement bookmark or PR.
|
|
173
181
|
|
|
174
182
|
## PR body and merge-queue discipline
|
|
175
183
|
|
|
@@ -243,13 +251,13 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
243
251
|
```
|
|
244
252
|
|
|
245
253
|
If the issue bookmark exists locally, advance it and push it with the provisioned
|
|
246
|
-
credential helper. `--
|
|
247
|
-
first push:
|
|
254
|
+
credential helper. `--bookmark` also publishes the locally provisioned bookmark on its
|
|
255
|
+
first push — a bookmark not yet tracking a remote one is tracked automatically:
|
|
248
256
|
|
|
249
257
|
```bash
|
|
250
258
|
cd -- "$LEGION_WORKSPACE" && \
|
|
251
|
-
jj -R "$LEGION_WORKSPACE" bookmark set legion
|
|
252
|
-
jj -R "$LEGION_WORKSPACE" git push --bookmark legion
|
|
259
|
+
jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> && \
|
|
260
|
+
jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
|
|
253
261
|
```
|
|
254
262
|
|
|
255
263
|
Do not report phase completion until the write, existence check, and handoff commit
|
|
@@ -279,5 +287,5 @@ session, if this phase's work needs to run again.
|
|
|
279
287
|
|
|
280
288
|
When blocked on lifecycle, scope, or cross-phase matters, `envoy_publish` the owning
|
|
281
289
|
architect a concise message: issue, phase, verified observation, what you tried, and the
|
|
282
|
-
decision required. Reach for `
|
|
290
|
+
decision required. Reach for `dispatch_ask` yourself only for a standalone human question
|
|
283
291
|
outside that coordination.
|
|
@@ -39,7 +39,7 @@ Returns: title, description, status, labels, comments, attachments.
|
|
|
39
39
|
linear_linear(action="update", id="ABC-123", state="Done")
|
|
40
40
|
linear_linear(action="update", id="ABC-123", priority=1)
|
|
41
41
|
linear_linear(action="update", id="ABC-123", assignee="me")
|
|
42
|
-
linear_linear(action="update", id="ABC-123", labels=["
|
|
42
|
+
linear_linear(action="update", id="ABC-123", labels=["bug", "existing-label"])
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
**Labels array replaces all labels.** Fetch current labels first, then append.
|