@sjawhar/opencode-legion-envoy 9.4.0 → 9.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatch.js +401 -64
- package/dist/src/server.js +6 -1
- package/package.json +1 -1
- package/skills/dispatch-writing-specs/SKILL.md +8 -8
- package/skills/legion-architect/references/design-gate.md +4 -3
- package/skills/legion-retro/SKILL.md +1 -1
- package/skills/legion-worker/references/findings.md +2 -2
package/dist/dispatch.js
CHANGED
|
@@ -947,7 +947,301 @@ __export(exports_coerce, {
|
|
|
947
947
|
});
|
|
948
948
|
|
|
949
949
|
// ../envoy-client/bin/dispatch.ts
|
|
950
|
-
import { readFileSync as
|
|
950
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
951
|
+
|
|
952
|
+
// ../envoy-client/src/dispatch-cli.ts
|
|
953
|
+
import { closeSync as closeSync4, constants as constants2, fstatSync as fstatSync2, openSync as openSync4, readFileSync as readFileSync5 } from "fs";
|
|
954
|
+
|
|
955
|
+
// ../envoy-client/src/askgate-marker.ts
|
|
956
|
+
import { createHash, randomBytes } from "crypto";
|
|
957
|
+
import {
|
|
958
|
+
closeSync,
|
|
959
|
+
constants,
|
|
960
|
+
mkdirSync,
|
|
961
|
+
openSync,
|
|
962
|
+
readdirSync,
|
|
963
|
+
readFileSync,
|
|
964
|
+
renameSync,
|
|
965
|
+
rmSync,
|
|
966
|
+
statSync,
|
|
967
|
+
unlinkSync,
|
|
968
|
+
writeFileSync
|
|
969
|
+
} from "fs";
|
|
970
|
+
import { isAbsolute, join, resolve } from "path";
|
|
971
|
+
|
|
972
|
+
// ../envoy-client/src/errors.ts
|
|
973
|
+
function messageFor(error) {
|
|
974
|
+
return error instanceof Error ? error.message : String(error);
|
|
975
|
+
}
|
|
976
|
+
function hasErrnoCode(error, code) {
|
|
977
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ../envoy-client/src/askgate-marker.ts
|
|
981
|
+
var ASKGATE_RECORD = "askgate";
|
|
982
|
+
var MARKER_MAX_AGE_MS = 2 * 60 * 60000;
|
|
983
|
+
var MARKERS = "markers";
|
|
984
|
+
var ALWAYS_GATED = {
|
|
985
|
+
dispatch_ask: true,
|
|
986
|
+
dispatch_message: true,
|
|
987
|
+
dispatch_edit_ask: true,
|
|
988
|
+
dispatch_comment: true,
|
|
989
|
+
dispatch_doc_edit: true,
|
|
990
|
+
dispatch_request_approval: true
|
|
991
|
+
};
|
|
992
|
+
var MARKDOWN_NAME = /\.md$/i;
|
|
993
|
+
var MARKDOWN_PATH = /\.(md|markdown)$/i;
|
|
994
|
+
function uploadIsDocument(args) {
|
|
995
|
+
const { content, name, path } = args;
|
|
996
|
+
return typeof content === "string" || typeof name === "string" && MARKDOWN_NAME.test(name.trim()) || typeof path === "string" && MARKDOWN_PATH.test(path);
|
|
997
|
+
}
|
|
998
|
+
function gatedWrite(tool, args) {
|
|
999
|
+
if (ALWAYS_GATED[tool] === true)
|
|
1000
|
+
return true;
|
|
1001
|
+
const { spec } = args;
|
|
1002
|
+
if (tool === "dispatch_issue")
|
|
1003
|
+
return spec !== undefined;
|
|
1004
|
+
if (tool === "dispatch_artifact")
|
|
1005
|
+
return uploadIsDocument(args);
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
var digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1009
|
+
function argvDigest(argv) {
|
|
1010
|
+
return digest(argv);
|
|
1011
|
+
}
|
|
1012
|
+
function argsDigest(args) {
|
|
1013
|
+
return digest(args);
|
|
1014
|
+
}
|
|
1015
|
+
function fileOperands(argv) {
|
|
1016
|
+
const [command, ...flags] = argv;
|
|
1017
|
+
const files = [];
|
|
1018
|
+
for (let index = 0;index < flags.length; index++) {
|
|
1019
|
+
const word = flags[index];
|
|
1020
|
+
const eq = word.indexOf("=");
|
|
1021
|
+
const flag = eq === -1 ? word : word.slice(0, eq);
|
|
1022
|
+
const named = flag.startsWith("--") && flag.endsWith("-file");
|
|
1023
|
+
if (!named && !(command === "artifact" && flag === "--path"))
|
|
1024
|
+
continue;
|
|
1025
|
+
const operand = eq === -1 ? flags[index + 1] : word.slice(eq + 1);
|
|
1026
|
+
if (operand !== undefined && operand !== "-")
|
|
1027
|
+
files.push({ flag, operand });
|
|
1028
|
+
}
|
|
1029
|
+
return files;
|
|
1030
|
+
}
|
|
1031
|
+
function operandPath(cwd, file) {
|
|
1032
|
+
if (file.flag === "--path")
|
|
1033
|
+
return resolve(cwd, file.operand);
|
|
1034
|
+
return isAbsolute(file.operand) ? file.operand : `${cwd}/${file.operand}`;
|
|
1035
|
+
}
|
|
1036
|
+
function bytesDigest(bytes) {
|
|
1037
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1038
|
+
}
|
|
1039
|
+
function processStart(pid) {
|
|
1040
|
+
try {
|
|
1041
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
1042
|
+
return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19] ?? null;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function ownerLive(owner) {
|
|
1048
|
+
if (owner.start !== null)
|
|
1049
|
+
return processStart(owner.pid) === owner.start;
|
|
1050
|
+
try {
|
|
1051
|
+
process.kill(owner.pid, 0);
|
|
1052
|
+
return true;
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
return !hasErrnoCode(error, "ESRCH");
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function isOwner(value) {
|
|
1058
|
+
if (typeof value !== "object" || value === null)
|
|
1059
|
+
return false;
|
|
1060
|
+
const { pid, start, store } = value;
|
|
1061
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 && (start === null || typeof start === "string") && typeof store === "string" && store.startsWith("/");
|
|
1062
|
+
}
|
|
1063
|
+
function parseOwner(text) {
|
|
1064
|
+
let data;
|
|
1065
|
+
try {
|
|
1066
|
+
data = JSON.parse(text);
|
|
1067
|
+
} catch {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (!isOwner(data) || !("v" in data) || data.v !== 2)
|
|
1071
|
+
return;
|
|
1072
|
+
const { pid, start, store, mode } = data;
|
|
1073
|
+
return {
|
|
1074
|
+
owner: { pid, start, store },
|
|
1075
|
+
mode: mode === "block" ? "block" : "warn"
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
var OWNER_FILE = ".json";
|
|
1079
|
+
var STORE_NAME = /^([1-9][0-9]*)(?:-([0-9]+))?$/;
|
|
1080
|
+
function storeTouched(store) {
|
|
1081
|
+
const own = statSync(store).mtimeMs;
|
|
1082
|
+
try {
|
|
1083
|
+
return Math.max(own, statSync(join(store, MARKERS)).mtimeMs);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
1086
|
+
return own;
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function sweepStores(root, now) {
|
|
1091
|
+
const parent = join(root, "askgate");
|
|
1092
|
+
let names;
|
|
1093
|
+
try {
|
|
1094
|
+
names = readdirSync(parent);
|
|
1095
|
+
} catch {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
for (const name of names) {
|
|
1099
|
+
const match = STORE_NAME.exec(name);
|
|
1100
|
+
if (match === null)
|
|
1101
|
+
continue;
|
|
1102
|
+
const store = join(parent, name);
|
|
1103
|
+
try {
|
|
1104
|
+
if (ownerLive({ pid: Number(match[1]), start: match[2] ?? null, store }))
|
|
1105
|
+
continue;
|
|
1106
|
+
if (now - storeTouched(store) <= MARKER_MAX_AGE_MS)
|
|
1107
|
+
continue;
|
|
1108
|
+
rmSync(store, { recursive: true, force: true });
|
|
1109
|
+
} catch {}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function askgateState(dir) {
|
|
1113
|
+
const record = join(dir, ASKGATE_RECORD);
|
|
1114
|
+
let names;
|
|
1115
|
+
try {
|
|
1116
|
+
names = readdirSync(record);
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
1119
|
+
return { kind: "off" };
|
|
1120
|
+
return { kind: "broken", problem: `${record}, could not be read: ${messageFor(error)}` };
|
|
1121
|
+
}
|
|
1122
|
+
const live = [];
|
|
1123
|
+
for (const name of names) {
|
|
1124
|
+
if (!name.endsWith(OWNER_FILE))
|
|
1125
|
+
continue;
|
|
1126
|
+
const file = join(record, name);
|
|
1127
|
+
let text;
|
|
1128
|
+
try {
|
|
1129
|
+
text = readFileSync(file, "utf-8");
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
1132
|
+
continue;
|
|
1133
|
+
return { kind: "broken", problem: `${file}, could not be read: ${messageFor(error)}` };
|
|
1134
|
+
}
|
|
1135
|
+
const parsed = parseOwner(text);
|
|
1136
|
+
if (parsed === undefined)
|
|
1137
|
+
return { kind: "broken", problem: `${file}, is not an AskGate record` };
|
|
1138
|
+
if (ownerLive(parsed.owner))
|
|
1139
|
+
live.push({ file, store: parsed.owner.store, mode: parsed.mode });
|
|
1140
|
+
}
|
|
1141
|
+
if (live.length === 0)
|
|
1142
|
+
return { kind: "off" };
|
|
1143
|
+
for (const { file, store } of live) {
|
|
1144
|
+
try {
|
|
1145
|
+
if (!statSync(join(store, MARKERS)).isDirectory())
|
|
1146
|
+
throw new Error("not a directory");
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
return {
|
|
1149
|
+
kind: "broken",
|
|
1150
|
+
problem: `${file}, names the marker store ${store}, which cannot be read: ${messageFor(error)}`
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
kind: "on",
|
|
1156
|
+
stores: live.map(({ store }) => store),
|
|
1157
|
+
mode: live.some(({ mode }) => mode === "block") ? "block" : "warn"
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
var isFileDigest = (value) => typeof value === "object" && value !== null && typeof value.flag === "string" && typeof value.operand === "string" && typeof value.sha256 === "string";
|
|
1161
|
+
function readMarker(file) {
|
|
1162
|
+
let text;
|
|
1163
|
+
try {
|
|
1164
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1165
|
+
try {
|
|
1166
|
+
text = readFileSync(fd, "utf-8");
|
|
1167
|
+
} finally {
|
|
1168
|
+
closeSync(fd);
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
try {
|
|
1174
|
+
const data = JSON.parse(text);
|
|
1175
|
+
if (typeof data !== "object" || data === null)
|
|
1176
|
+
return;
|
|
1177
|
+
const { v, argv, args, files, createdAt } = data;
|
|
1178
|
+
if (v !== 2 || typeof argv !== "string" || typeof createdAt !== "number")
|
|
1179
|
+
return;
|
|
1180
|
+
if (args !== null && typeof args !== "string")
|
|
1181
|
+
return;
|
|
1182
|
+
if (!Array.isArray(files) || !files.every(isFileDigest))
|
|
1183
|
+
return;
|
|
1184
|
+
return { argv, args, files, createdAt };
|
|
1185
|
+
} catch {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function consumeMarker(stores, argv, args, files, now) {
|
|
1190
|
+
const key = argvDigest(argv);
|
|
1191
|
+
const reviewed = argsDigest(args);
|
|
1192
|
+
let changed;
|
|
1193
|
+
let mismatched = false;
|
|
1194
|
+
let expired = false;
|
|
1195
|
+
const usable = [];
|
|
1196
|
+
for (const store of stores) {
|
|
1197
|
+
const dir = join(store, MARKERS);
|
|
1198
|
+
for (const name of readdirSync(dir)) {
|
|
1199
|
+
if (!name.startsWith(`${key}.`))
|
|
1200
|
+
continue;
|
|
1201
|
+
const file = join(dir, name);
|
|
1202
|
+
const marker = readMarker(file);
|
|
1203
|
+
if (marker === undefined || marker.argv !== key)
|
|
1204
|
+
continue;
|
|
1205
|
+
if (now - marker.createdAt > MARKER_MAX_AGE_MS) {
|
|
1206
|
+
expired = true;
|
|
1207
|
+
rmSync(file, { force: true });
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (marker.args !== null) {
|
|
1211
|
+
const differs = marker.files.find((bound, index) => bound.flag !== files[index]?.flag || bound.operand !== files[index]?.operand || bound.sha256 !== files[index]?.sha256);
|
|
1212
|
+
if (differs !== undefined) {
|
|
1213
|
+
changed ??= { flag: differs.flag, operand: differs.operand };
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (marker.files.length !== files.length || marker.args !== reviewed) {
|
|
1217
|
+
mismatched = true;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
usable.push({ file, bound: marker.args !== null });
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
usable.sort((a, b) => Number(b.bound) - Number(a.bound));
|
|
1225
|
+
for (const { file } of usable) {
|
|
1226
|
+
try {
|
|
1227
|
+
unlinkSync(file);
|
|
1228
|
+
return { ok: true };
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (!hasErrnoCode(error, "ENOENT"))
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (changed !== undefined)
|
|
1235
|
+
return {
|
|
1236
|
+
ok: false,
|
|
1237
|
+
why: `a file this write posts changed after AskGate reviewed it (\`${changed.flag} ${changed.operand}\`)`,
|
|
1238
|
+
changed
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
ok: false,
|
|
1242
|
+
why: mismatched ? "its arguments differ from the ones AskGate reviewed" : expired ? "its AskGate marker expired before it ran" : "this call carries no AskGate marker"
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
951
1245
|
|
|
952
1246
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
|
|
953
1247
|
var NEVER = Object.freeze({
|
|
@@ -14619,14 +14913,6 @@ function zodSchemaApi(zod) {
|
|
|
14619
14913
|
}
|
|
14620
14914
|
};
|
|
14621
14915
|
}
|
|
14622
|
-
// ../envoy-client/src/errors.ts
|
|
14623
|
-
function messageFor(error) {
|
|
14624
|
-
return error instanceof Error ? error.message : String(error);
|
|
14625
|
-
}
|
|
14626
|
-
function hasErrnoCode(error, code) {
|
|
14627
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
14628
|
-
}
|
|
14629
|
-
|
|
14630
14916
|
// ../envoy-client/src/dispatch-command.ts
|
|
14631
14917
|
var STDIN = "-";
|
|
14632
14918
|
var BARE_WORD = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
|
@@ -18744,7 +19030,9 @@ ${trailer.join(`
|
|
|
18744
19030
|
const content = optionalString(args, "content");
|
|
18745
19031
|
const artifactInput = content === undefined ? {
|
|
18746
19032
|
name,
|
|
18747
|
-
file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
|
|
19033
|
+
file: input.uploadBytes === undefined ? Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))) : new Blob([new Uint8Array(input.uploadBytes)], {
|
|
19034
|
+
type: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))).type
|
|
19035
|
+
}),
|
|
18748
19036
|
...summary === undefined ? {} : { summary },
|
|
18749
19037
|
actor
|
|
18750
19038
|
} : {
|
|
@@ -18979,23 +19267,23 @@ function asObject(value) {
|
|
|
18979
19267
|
}
|
|
18980
19268
|
|
|
18981
19269
|
// ../envoy-client/src/dispatch-session-state.ts
|
|
18982
|
-
import { createHash } from "crypto";
|
|
19270
|
+
import { createHash as createHash2 } from "crypto";
|
|
18983
19271
|
import {
|
|
18984
19272
|
appendFileSync,
|
|
18985
|
-
closeSync,
|
|
19273
|
+
closeSync as closeSync2,
|
|
18986
19274
|
fstatSync,
|
|
18987
|
-
mkdirSync,
|
|
18988
|
-
openSync,
|
|
18989
|
-
readdirSync,
|
|
18990
|
-
readFileSync,
|
|
19275
|
+
mkdirSync as mkdirSync2,
|
|
19276
|
+
openSync as openSync2,
|
|
19277
|
+
readdirSync as readdirSync2,
|
|
19278
|
+
readFileSync as readFileSync2,
|
|
18991
19279
|
readSync,
|
|
18992
|
-
renameSync,
|
|
18993
|
-
rmSync,
|
|
18994
|
-
statSync,
|
|
18995
|
-
writeFileSync
|
|
19280
|
+
renameSync as renameSync2,
|
|
19281
|
+
rmSync as rmSync2,
|
|
19282
|
+
statSync as statSync2,
|
|
19283
|
+
writeFileSync as writeFileSync2
|
|
18996
19284
|
} from "fs";
|
|
18997
19285
|
import { homedir } from "os";
|
|
18998
|
-
import { join } from "path";
|
|
19286
|
+
import { join as join2 } from "path";
|
|
18999
19287
|
var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
19000
19288
|
var PRIVATE_DIR = 448;
|
|
19001
19289
|
var PRIVATE_FILE = 384;
|
|
@@ -19012,20 +19300,20 @@ function sessionStateRoot(env) {
|
|
|
19012
19300
|
const stateDir = env["DISPATCH_STATE_DIR"];
|
|
19013
19301
|
if (stateDir)
|
|
19014
19302
|
return stateDir;
|
|
19015
|
-
return
|
|
19303
|
+
return join2(env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state"), "dispatch");
|
|
19016
19304
|
}
|
|
19017
19305
|
function sessionDirectory(env, sessionId) {
|
|
19018
19306
|
if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
19019
19307
|
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
19020
19308
|
}
|
|
19021
|
-
return
|
|
19309
|
+
return join2(sessionStateRoot(env), "sessions", sessionId);
|
|
19022
19310
|
}
|
|
19023
19311
|
function ensureDirectory(dir) {
|
|
19024
|
-
|
|
19312
|
+
mkdirSync2(dir, { recursive: true, mode: PRIVATE_DIR });
|
|
19025
19313
|
}
|
|
19026
19314
|
function readIfPresent(path) {
|
|
19027
19315
|
try {
|
|
19028
|
-
return
|
|
19316
|
+
return readFileSync2(path, "utf-8");
|
|
19029
19317
|
} catch (error) {
|
|
19030
19318
|
if (hasErrnoCode(error, "ENOENT"))
|
|
19031
19319
|
return;
|
|
@@ -19034,14 +19322,14 @@ function readIfPresent(path) {
|
|
|
19034
19322
|
}
|
|
19035
19323
|
function writeAtomically(path, text) {
|
|
19036
19324
|
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
19037
|
-
|
|
19038
|
-
|
|
19325
|
+
writeFileSync2(temporary, text, { mode: PRIVATE_FILE });
|
|
19326
|
+
renameSync2(temporary, path);
|
|
19039
19327
|
}
|
|
19040
19328
|
function stringList(value) {
|
|
19041
19329
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
19042
19330
|
}
|
|
19043
19331
|
function loadSessionMemory(dir, sessionId) {
|
|
19044
|
-
const path =
|
|
19332
|
+
const path = join2(dir, "state.json");
|
|
19045
19333
|
const text = readIfPresent(path);
|
|
19046
19334
|
if (text === undefined)
|
|
19047
19335
|
return new Set;
|
|
@@ -19065,7 +19353,7 @@ function writeMemory(dir, sessionId, follows) {
|
|
|
19065
19353
|
pictures: [...shownPictures(sessionId)],
|
|
19066
19354
|
follows: [...follows]
|
|
19067
19355
|
};
|
|
19068
|
-
writeAtomically(
|
|
19356
|
+
writeAtomically(join2(dir, "state.json"), `${JSON.stringify(state)}
|
|
19069
19357
|
`);
|
|
19070
19358
|
}
|
|
19071
19359
|
function appendResult(dir, entry) {
|
|
@@ -19074,7 +19362,7 @@ function appendResult(dir, entry) {
|
|
|
19074
19362
|
}
|
|
19075
19363
|
function writeResult(dir, entry) {
|
|
19076
19364
|
const line = { at: new Date().toISOString(), ...entry };
|
|
19077
|
-
appendFileSync(
|
|
19365
|
+
appendFileSync(join2(dir, "results.jsonl"), `${JSON.stringify(line)}
|
|
19078
19366
|
`, { mode: PRIVATE_FILE });
|
|
19079
19367
|
}
|
|
19080
19368
|
function recordOutcome(dir, sessionId, follows, entry) {
|
|
@@ -19084,12 +19372,12 @@ function recordOutcome(dir, sessionId, follows, entry) {
|
|
|
19084
19372
|
}
|
|
19085
19373
|
function writePicture(dir, image) {
|
|
19086
19374
|
const bytes = Buffer.from(image.data, "base64");
|
|
19087
|
-
const name =
|
|
19088
|
-
const pictures =
|
|
19089
|
-
const path =
|
|
19375
|
+
const name = createHash2("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
19376
|
+
const pictures = join2(dir, "pictures");
|
|
19377
|
+
const path = join2(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
|
|
19090
19378
|
ensureDirectory(pictures);
|
|
19091
19379
|
try {
|
|
19092
|
-
|
|
19380
|
+
writeFileSync2(path, bytes, { mode: PRIVATE_FILE, flag: "wx" });
|
|
19093
19381
|
} catch (error) {
|
|
19094
19382
|
if (!hasErrnoCode(error, "EEXIST"))
|
|
19095
19383
|
throw error;
|
|
@@ -19097,28 +19385,28 @@ function writePicture(dir, image) {
|
|
|
19097
19385
|
return { path, bytes: bytes.length };
|
|
19098
19386
|
}
|
|
19099
19387
|
function writeLongOutput(dir, text) {
|
|
19100
|
-
const out =
|
|
19388
|
+
const out = join2(dir, "out");
|
|
19101
19389
|
ensureDirectory(out);
|
|
19102
|
-
const path =
|
|
19103
|
-
|
|
19390
|
+
const path = join2(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
|
|
19391
|
+
writeFileSync2(path, text, { mode: PRIVATE_FILE });
|
|
19104
19392
|
return path;
|
|
19105
19393
|
}
|
|
19106
19394
|
function readSessionTitle(dir) {
|
|
19107
|
-
return readIfPresent(
|
|
19395
|
+
return readIfPresent(join2(dir, "title")) || undefined;
|
|
19108
19396
|
}
|
|
19109
19397
|
function pruneSessions(root, now) {
|
|
19110
|
-
const stamp =
|
|
19398
|
+
const stamp = join2(root, PRUNED_STAMP);
|
|
19111
19399
|
try {
|
|
19112
|
-
if (now -
|
|
19400
|
+
if (now - statSync2(stamp).mtimeMs < PRUNE_INTERVAL_MS)
|
|
19113
19401
|
return;
|
|
19114
19402
|
} catch (error) {
|
|
19115
19403
|
if (!hasErrnoCode(error, "ENOENT"))
|
|
19116
19404
|
throw error;
|
|
19117
19405
|
}
|
|
19118
|
-
const sessions =
|
|
19406
|
+
const sessions = join2(root, "sessions");
|
|
19119
19407
|
let entries = [];
|
|
19120
19408
|
try {
|
|
19121
|
-
entries =
|
|
19409
|
+
entries = readdirSync2(sessions, { withFileTypes: true });
|
|
19122
19410
|
} catch (error) {
|
|
19123
19411
|
if (!hasErrnoCode(error, "ENOENT"))
|
|
19124
19412
|
throw error;
|
|
@@ -19126,32 +19414,33 @@ function pruneSessions(root, now) {
|
|
|
19126
19414
|
for (const entry of entries) {
|
|
19127
19415
|
if (!entry.isDirectory())
|
|
19128
19416
|
continue;
|
|
19129
|
-
const dir =
|
|
19417
|
+
const dir = join2(sessions, entry.name);
|
|
19130
19418
|
try {
|
|
19131
|
-
if (now -
|
|
19132
|
-
|
|
19419
|
+
if (now - statSync2(dir).mtimeMs > IDLE_SESSION_MS && askgateState(dir).kind === "off") {
|
|
19420
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
19133
19421
|
}
|
|
19134
19422
|
} catch (error) {
|
|
19135
19423
|
if (!hasErrnoCode(error, "ENOENT"))
|
|
19136
19424
|
throw error;
|
|
19137
19425
|
}
|
|
19138
19426
|
}
|
|
19427
|
+
sweepStores(root, now);
|
|
19139
19428
|
ensureDirectory(root);
|
|
19140
|
-
|
|
19429
|
+
writeFileSync2(stamp, "", { mode: PRIVATE_FILE });
|
|
19141
19430
|
}
|
|
19142
19431
|
|
|
19143
19432
|
// ../envoy-client/src/envoy-config.ts
|
|
19144
19433
|
import {
|
|
19145
19434
|
chmodSync,
|
|
19146
|
-
closeSync as
|
|
19435
|
+
closeSync as closeSync3,
|
|
19147
19436
|
existsSync,
|
|
19148
19437
|
lstatSync,
|
|
19149
19438
|
mkdtempSync,
|
|
19150
|
-
openSync as
|
|
19151
|
-
readFileSync as
|
|
19439
|
+
openSync as openSync3,
|
|
19440
|
+
readFileSync as readFileSync4,
|
|
19152
19441
|
readlinkSync,
|
|
19153
|
-
rmSync as
|
|
19154
|
-
statSync as
|
|
19442
|
+
rmSync as rmSync3,
|
|
19443
|
+
statSync as statSync3
|
|
19155
19444
|
} from "fs";
|
|
19156
19445
|
import { homedir as homedir2, tmpdir } from "os";
|
|
19157
19446
|
import * as path from "path";
|
|
@@ -19243,14 +19532,14 @@ function jsonErrorPosition(text) {
|
|
|
19243
19532
|
}
|
|
19244
19533
|
|
|
19245
19534
|
// ../envoy-client/src/secret-file.ts
|
|
19246
|
-
import { readFileSync as
|
|
19535
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
19247
19536
|
function sendable(value) {
|
|
19248
19537
|
return !/[\s\p{Cc}]/u.test(value);
|
|
19249
19538
|
}
|
|
19250
19539
|
function readSecretFile(variable, filePath) {
|
|
19251
19540
|
let contents;
|
|
19252
19541
|
try {
|
|
19253
|
-
contents =
|
|
19542
|
+
contents = readFileSync3(filePath, "utf8");
|
|
19254
19543
|
} catch (error) {
|
|
19255
19544
|
throw new Error(`${variable} names ${filePath}, which could not be read: ${messageFor(error)}`);
|
|
19256
19545
|
}
|
|
@@ -19348,6 +19637,7 @@ var CheckoutFileSchema = looseObject({
|
|
|
19348
19637
|
$schema: string2().optional(),
|
|
19349
19638
|
dispatch: DispatchSectionSchema.optional()
|
|
19350
19639
|
});
|
|
19640
|
+
var AskgateSectionSchema = strictObject({ enabled: boolean2().optional() });
|
|
19351
19641
|
function offWithNotice(notice) {
|
|
19352
19642
|
return { enabled: false, notice, error: null };
|
|
19353
19643
|
}
|
|
@@ -19384,7 +19674,7 @@ function danglingLink(filePath) {
|
|
|
19384
19674
|
function parseFile(filePath, schema, checkout) {
|
|
19385
19675
|
let raw;
|
|
19386
19676
|
try {
|
|
19387
|
-
raw =
|
|
19677
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
19388
19678
|
} catch (error) {
|
|
19389
19679
|
if (hasErrnoCode(error, "ENOENT") || hasErrnoCode(error, "ENOTDIR")) {
|
|
19390
19680
|
return { kind: "absent", link: danglingLink(filePath) };
|
|
@@ -19428,7 +19718,7 @@ function runCommand(command, env, cwd) {
|
|
|
19428
19718
|
try {
|
|
19429
19719
|
chmodSync(directory, 448);
|
|
19430
19720
|
const output = path.join(directory, "stdout");
|
|
19431
|
-
const fd =
|
|
19721
|
+
const fd = openSync3(output, "wx", 384);
|
|
19432
19722
|
let result;
|
|
19433
19723
|
try {
|
|
19434
19724
|
result = Bun.spawnSync(["/bin/sh", "-c", command], {
|
|
@@ -19443,7 +19733,7 @@ function runCommand(command, env, cwd) {
|
|
|
19443
19733
|
} catch (error) {
|
|
19444
19734
|
return { failure: `could not start: ${messageFor(error)}`, timedOut: false };
|
|
19445
19735
|
} finally {
|
|
19446
|
-
|
|
19736
|
+
closeSync3(fd);
|
|
19447
19737
|
}
|
|
19448
19738
|
try {
|
|
19449
19739
|
process.kill(-result.pid, "SIGKILL");
|
|
@@ -19460,7 +19750,7 @@ function runCommand(command, env, cwd) {
|
|
|
19460
19750
|
timedOut: false
|
|
19461
19751
|
};
|
|
19462
19752
|
}
|
|
19463
|
-
const value =
|
|
19753
|
+
const value = readFileSync4(output, "utf-8").trim();
|
|
19464
19754
|
if (value.length === 0)
|
|
19465
19755
|
return { failure: "printed nothing", timedOut: false };
|
|
19466
19756
|
if (!sendable(value)) {
|
|
@@ -19471,7 +19761,7 @@ function runCommand(command, env, cwd) {
|
|
|
19471
19761
|
}
|
|
19472
19762
|
return { value };
|
|
19473
19763
|
} finally {
|
|
19474
|
-
|
|
19764
|
+
rmSync3(directory, { recursive: true, force: true });
|
|
19475
19765
|
}
|
|
19476
19766
|
}
|
|
19477
19767
|
function fileValue(value, field, filePath, env) {
|
|
@@ -19495,7 +19785,7 @@ function fileValue(value, field, filePath, env) {
|
|
|
19495
19785
|
}
|
|
19496
19786
|
function statDevice(directory) {
|
|
19497
19787
|
try {
|
|
19498
|
-
return
|
|
19788
|
+
return statSync3(directory).dev;
|
|
19499
19789
|
} catch {
|
|
19500
19790
|
return;
|
|
19501
19791
|
}
|
|
@@ -19556,7 +19846,8 @@ function readEnvoyConfig(filePath, env, options) {
|
|
|
19556
19846
|
kind: "valid",
|
|
19557
19847
|
path: checkoutPath,
|
|
19558
19848
|
dispatch: checkoutFile.settings.dispatch ?? {},
|
|
19559
|
-
envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
|
|
19849
|
+
envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null,
|
|
19850
|
+
askgateError: Object.hasOwn(checkoutFile.settings, "askgate") ? `askgate in ${checkoutPath} is not read; set it in ${filePath}` : null
|
|
19560
19851
|
};
|
|
19561
19852
|
}
|
|
19562
19853
|
const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
|
|
@@ -20098,10 +20389,12 @@ var REFUSED = 1;
|
|
|
20098
20389
|
var USAGE = 2;
|
|
20099
20390
|
var HOSTS = ["omp", "claude", "opencode"];
|
|
20100
20391
|
var CLAUDE_OUTPUT_MAX = 25000;
|
|
20392
|
+
var SAME_MESSAGE = "AskGate reviews the calls of a message before any of them runs, so it did not review that change.";
|
|
20101
20393
|
var STATE_WRITE_FAILED = "dispatch: Dispatch took the call, but this session's state";
|
|
20102
20394
|
function stateWriteFailed(problem) {
|
|
20103
20395
|
return `${STATE_WRITE_FAILED} could not be written: ${problem}`;
|
|
20104
20396
|
}
|
|
20397
|
+
var LITERAL_FORM = "AskGate reviews a Dispatch write, and lets it post, only when it is sent as one bash call of literal words: `dispatch <command> <words\u2026>`. Each word is plain text, text in single quotes (also over more than one line), or text in double quotes with no `$`, backtick or `\\`. A `\\` at the end of a line continues the command. Before it, one `cd <path> &&` is permitted; after it, one `2>&1`, one here-document opened with a quoted delimiter (`<<'EOF'`) and one `| head -n N` or `| tail -n N`. For text with `$` or a backtick in it, use `--<field>-file - <<'EOF'`. A variable, `~`, a glob, an assignment, another redirect (`2>/dev/null`, `> file`), another pipe, another command (`;`, `&&`, `||`), a loop, xargs, `bash -c`, a script, an alias, or a process that a program or an eval cell starts carries no marker.";
|
|
20105
20398
|
var SUBSCRIPTIONS_TIMEOUT_MS = 1500;
|
|
20106
20399
|
function fitOutput(text, pictures, kept, limit, writeFull) {
|
|
20107
20400
|
const whole = [text, ...pictures, ...kept].join(`
|
|
@@ -20144,14 +20437,15 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20144
20437
|
if (value !== undefined && value !== "")
|
|
20145
20438
|
env[name] = value;
|
|
20146
20439
|
}
|
|
20147
|
-
const refuse = (text, code, tool) => {
|
|
20440
|
+
const refuse = (text, code, tool, askgate) => {
|
|
20148
20441
|
print(text);
|
|
20149
20442
|
const sessionId = env.CLAUDE_CODE_SESSION_ID ?? env.DISPATCH_SESSION_ID;
|
|
20150
20443
|
if (sessionId !== undefined) {
|
|
20151
20444
|
try {
|
|
20152
20445
|
appendResult(sessionDirectory(env, sessionId), {
|
|
20153
20446
|
tool: tool ?? "dispatch",
|
|
20154
|
-
error: text.trimEnd()
|
|
20447
|
+
error: text.trimEnd(),
|
|
20448
|
+
...askgate === undefined ? {} : { askgate }
|
|
20155
20449
|
});
|
|
20156
20450
|
} catch {}
|
|
20157
20451
|
}
|
|
@@ -20175,7 +20469,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20175
20469
|
const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
|
|
20176
20470
|
const host = HOSTS.find((name) => name === hostName);
|
|
20177
20471
|
if (host === undefined) {
|
|
20178
|
-
return refuse(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell
|
|
20472
|
+
return refuse(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell. From an agent's session, send the command as a bash call. To run dispatch by hand, set DISPATCH_HOST, and set DISPATCH_SESSION_ID to the id of your own session." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`, USAGE, parsed.tool);
|
|
20179
20473
|
}
|
|
20180
20474
|
const sessionVariable = host === "claude" ? "CLAUDE_CODE_SESSION_ID" : "DISPATCH_SESSION_ID";
|
|
20181
20475
|
const sessionId = env[sessionVariable];
|
|
@@ -20214,6 +20508,48 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20214
20508
|
return refuse(resolution.error === null ? `dispatch: ${resolution.notice}` : `dispatch: dispatch config: ${resolution.error}`, USAGE, parsed.tool);
|
|
20215
20509
|
}
|
|
20216
20510
|
const config = resolution;
|
|
20511
|
+
let uploadBytes;
|
|
20512
|
+
if (gatedWrite(parsed.tool, parsed.args)) {
|
|
20513
|
+
const notCalled = `dispatch ${commandName(parsed.tool)} was not called:`;
|
|
20514
|
+
try {
|
|
20515
|
+
const gate = askgateState(dir);
|
|
20516
|
+
if (gate.kind === "broken") {
|
|
20517
|
+
return refuse(`${notCalled} AskGate's record for this session, ${gate.problem}, so whether AskGate reviewed this write cannot be told.`, REFUSED, parsed.tool, { mode: "block", why: gate.problem });
|
|
20518
|
+
}
|
|
20519
|
+
if (gate.kind === "on") {
|
|
20520
|
+
const files = [];
|
|
20521
|
+
for (const file of fileOperands(argv)) {
|
|
20522
|
+
let bytes;
|
|
20523
|
+
try {
|
|
20524
|
+
const fd = openSync4(operandPath(io.cwd, file), constants2.O_RDONLY | constants2.O_NONBLOCK);
|
|
20525
|
+
try {
|
|
20526
|
+
if (fstatSync2(fd).isFile())
|
|
20527
|
+
bytes = readFileSync5(fd);
|
|
20528
|
+
} finally {
|
|
20529
|
+
closeSync4(fd);
|
|
20530
|
+
}
|
|
20531
|
+
} catch {}
|
|
20532
|
+
files.push({ ...file, sha256: bytes === undefined ? null : bytesDigest(bytes) });
|
|
20533
|
+
if (file.flag === "--path" && file.operand === parsed.args.path)
|
|
20534
|
+
uploadBytes = bytes;
|
|
20535
|
+
}
|
|
20536
|
+
const spent = consumeMarker(gate.stores, argv, parsed.args, files, Date.now());
|
|
20537
|
+
if (!spent.ok && gate.mode === "block") {
|
|
20538
|
+
return refuse(spent.changed === undefined ? `${notCalled} AskGate is on for this session, and ${spent.why}. ${LITERAL_FORM} Send the write again in that form, as a bash call of its own.` : `${notCalled} AskGate is on for this session, and ${spent.why}. ${SAME_MESSAGE} Send the call again in a later message.`, REFUSED, parsed.tool, { mode: "block", why: spent.why });
|
|
20539
|
+
}
|
|
20540
|
+
if (!spent.ok) {
|
|
20541
|
+
try {
|
|
20542
|
+
appendResult(dir, { tool: parsed.tool, askgate: { mode: "warn", why: spent.why } });
|
|
20543
|
+
} catch (error) {
|
|
20544
|
+
return refuse(`${notCalled} AskGate is on for this session in warn mode, and ${spent.why}. A write that posts unreviewed is first recorded in this session's ledger, and the ledger could not be written: ${messageFor(error)}.`, REFUSED, parsed.tool);
|
|
20545
|
+
}
|
|
20546
|
+
print(spent.changed === undefined ? `dispatch: warning: AskGate is on for this session in warn mode, and ${spent.why}, so this write posts unreviewed. ${LITERAL_FORM} Send the next one in that form.` : `dispatch: warning: AskGate is on for this session in warn mode, and ${spent.why}, so this write posts unreviewed. ${SAME_MESSAGE} Send a write in a later message than the call that changes its file.`);
|
|
20547
|
+
}
|
|
20548
|
+
}
|
|
20549
|
+
} catch (error) {
|
|
20550
|
+
return refuse(`${notCalled} AskGate's marker for this call could not be checked: ${messageFor(error)}.`, REFUSED, parsed.tool, { mode: "block", why: messageFor(error) });
|
|
20551
|
+
}
|
|
20552
|
+
}
|
|
20217
20553
|
let topics;
|
|
20218
20554
|
const readTopics = async () => {
|
|
20219
20555
|
try {
|
|
@@ -20255,6 +20591,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20255
20591
|
tool,
|
|
20256
20592
|
args,
|
|
20257
20593
|
cwd: io.cwd,
|
|
20594
|
+
...uploadBytes === undefined ? {} : { uploadBytes },
|
|
20258
20595
|
host,
|
|
20259
20596
|
sessionId,
|
|
20260
20597
|
sessionTitle: env.DISPATCH_SESSION_TITLE ?? readSessionTitle(dir),
|
|
@@ -20306,7 +20643,7 @@ async function runDispatchCli(argv, rawEnv, io) {
|
|
|
20306
20643
|
}
|
|
20307
20644
|
|
|
20308
20645
|
// ../envoy-client/bin/dispatch.ts
|
|
20309
|
-
var readText = (path) =>
|
|
20646
|
+
var readText = (path) => readFileSync6(path === "-" ? 0 : path, "utf-8");
|
|
20310
20647
|
var pending = [];
|
|
20311
20648
|
var writeTo = (stream) => (text) => {
|
|
20312
20649
|
const { promise, resolve } = Promise.withResolvers();
|
package/dist/src/server.js
CHANGED
|
@@ -14668,6 +14668,7 @@ var CheckoutFileSchema = looseObject({
|
|
|
14668
14668
|
$schema: string2().optional(),
|
|
14669
14669
|
dispatch: DispatchSectionSchema.optional()
|
|
14670
14670
|
});
|
|
14671
|
+
var AskgateSectionSchema = strictObject({ enabled: boolean2().optional() });
|
|
14671
14672
|
function offWithNotice(notice) {
|
|
14672
14673
|
return { enabled: false, notice, error: null };
|
|
14673
14674
|
}
|
|
@@ -14876,7 +14877,8 @@ function readEnvoyConfig(filePath, env, options) {
|
|
|
14876
14877
|
kind: "valid",
|
|
14877
14878
|
path: checkoutPath,
|
|
14878
14879
|
dispatch: checkoutFile.settings.dispatch ?? {},
|
|
14879
|
-
envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null
|
|
14880
|
+
envoyError: Object.hasOwn(checkoutFile.settings, "envoy") ? `${checkoutPath}: envoy is not read from a checkout's envoy.json; put it in ${filePath}` : null,
|
|
14881
|
+
askgateError: Object.hasOwn(checkoutFile.settings, "askgate") ? `askgate in ${checkoutPath} is not read; set it in ${filePath}` : null
|
|
14880
14882
|
};
|
|
14881
14883
|
}
|
|
14882
14884
|
const oldCheckout = path.resolve(options.cwd, ".opencode", "envoy.json");
|
|
@@ -15151,6 +15153,9 @@ function startNotice(config, dispatch, envoy, nats) {
|
|
|
15151
15153
|
}
|
|
15152
15154
|
if (checkout?.kind === "valid" && checkout.envoyError !== null)
|
|
15153
15155
|
parts.push(checkout.envoyError);
|
|
15156
|
+
if (checkout?.kind === "valid" && checkout.askgateError !== null) {
|
|
15157
|
+
parts.push(checkout.askgateError);
|
|
15158
|
+
}
|
|
15154
15159
|
if (checkout?.kind === "invalid" && (dispatch.enabled || dispatch.error !== checkout.error)) {
|
|
15155
15160
|
parts.push(`${checkout.error}; not read while DISPATCH_URL is set`);
|
|
15156
15161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dispatch-writing-specs
|
|
3
|
-
description: "Use in a session with Dispatch before you write or change a spec, and before an ask, comment or message a person reads. It replaces superpowers' brainstorming there.
|
|
3
|
+
description: "Use in a session with Dispatch before you write or change a spec, and before an ask, comment or message a person reads. It replaces superpowers' brainstorming there."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Writing specs
|
|
@@ -11,7 +11,7 @@ Reach shared understanding with the person at the least cost of their time. A Le
|
|
|
11
11
|
|
|
12
12
|
## The outline
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Find the issue's project with `dispatch projects` or `dispatch search`, never the environment. The spec's `##` parts, in order:
|
|
15
15
|
|
|
16
16
|
Part | Content
|
|
17
17
|
--- | ---
|
|
@@ -23,7 +23,7 @@ Part | Content
|
|
|
23
23
|
`Decisions` | Your own choices
|
|
24
24
|
`Acceptance` | `**A1.**` on, each naming the requirements it tests
|
|
25
25
|
|
|
26
|
-
Another agent can build and check the work from the spec alone.
|
|
26
|
+
Another agent can build and check the work from the spec alone. A small change gets a few sentences per part, no matrix. Split a large design into child issues.
|
|
27
27
|
|
|
28
28
|
## Rules for the text
|
|
29
29
|
|
|
@@ -32,10 +32,12 @@ For each spec, ask, comment and message:
|
|
|
32
32
|
- Write in `skill://asd-ste100`'s strict mode.
|
|
33
33
|
- State the current design, never how people reached it.
|
|
34
34
|
- Link each source where the person wrote it: `dispatch://KEY/comment/<id>`, `/ask/<id>`, `/message/<id>`, or a Slack URL. With no link, state the design.
|
|
35
|
+
- In a spec, cite code by file and symbol, never a line number: it goes stale at the next edit.
|
|
36
|
+
- A sentence that cites a gate passing (a test, review, check or proof) also says, in that sentence, what the pass does not show: "The tests pass; they catch a wrong verdict, not a proof of a contradiction."
|
|
35
37
|
- Write about the design, never the person. Never quote people or upload their words.
|
|
36
38
|
- Name your session only if a reader must reach it.
|
|
37
39
|
- Keep only text that helps a reader understand, decide, build, check.
|
|
38
|
-
- Explain each name or term a new reader may not know.
|
|
40
|
+
- Explain each name or term a new reader may not know. Say in a clause what each issue you name does for this reader.
|
|
39
41
|
- In an ask, ask block, comment or message, name a requirement or check by what it is, never its number.
|
|
40
42
|
|
|
41
43
|
## Questions
|
|
@@ -48,9 +50,9 @@ For each spec, ask, comment and message:
|
|
|
48
50
|
|
|
49
51
|
## Each version
|
|
50
52
|
|
|
51
|
-
1. After a person's answer, comment or edit, rewrite what it touches; delete what it contradicts.
|
|
53
|
+
1. After a person's answer, comment or edit, rewrite what it touches; delete what it contradicts. Never narrate an edit or what used to be here; versions and comments hold the history. Reply in a comment's thread.
|
|
52
54
|
2. Before each spec write, paste the whole spec, never a path, into a fresh subagent's assignment. It lists gaps, conflicts, uncheckable requirements, unclear text, and missing or needless questions. Fix or record each.
|
|
53
|
-
3. Before each approval request, a read-only agent checks its facts: `task(agent="scout")
|
|
55
|
+
3. Before each approval request, a read-only agent checks its facts: `task(agent="scout")`.
|
|
54
56
|
4. Name each version with `dispatch doc-edit --summary`.
|
|
55
57
|
|
|
56
58
|
## Decisions
|
|
@@ -67,9 +69,7 @@ At your next edit, rewrite a spec outside the outline whole, in one version. Mov
|
|
|
67
69
|
## Approval
|
|
68
70
|
|
|
69
71
|
- Request approval once, when each ask block is answered or waived, each claim is checked, and this version is unapproved. Unbuilt acceptance checks never delay it.
|
|
70
|
-
- When their answer is the whole change since approval, Dispatch carries it.
|
|
71
72
|
- After approval, version only a change to scope, acceptance, cost, risk or a person's decision, all at once. Put other fixes in the plan or a comment.
|
|
72
|
-
- A Legion root spec's design gate follows `skill://legion-architect`.
|
|
73
73
|
|
|
74
74
|
## The plan
|
|
75
75
|
|
|
@@ -51,12 +51,13 @@ When the gate is armed, run this exact sequence before the tree's work starts:
|
|
|
51
51
|
them to answer: give such a point its own decision block first, or take it out of the spec.
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
dispatch request-approval --issue <root issue> --summary '<what the human is approving> Reviews: <gap result>, <claim result>.'
|
|
54
|
+
dispatch request-approval --issue <root issue> --summary '<what the human is approving> Reviews: <gap result>, <claim result>; <what they do not show>.'
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
`--summary` says in one to three sentences what the human is approving and nothing else (no
|
|
58
|
-
commentary and no open question), then one sentence with both reviews' results
|
|
59
|
-
`Reviews: no open gap, 14 of 14 claims verified
|
|
58
|
+
commentary and no open question), then one sentence with both reviews' results and what they do
|
|
59
|
+
not show, for example `Reviews: no open gap, 14 of 14 claims verified; neither shows that the
|
|
60
|
+
design works once built.` The primary document is the default; the
|
|
60
61
|
command prints `Approval requested for spec.md (document id <UUID>) at version <N> (ask <id>)`.
|
|
61
62
|
5. Register that document version with the daemon:
|
|
62
63
|
|
|
@@ -82,7 +82,7 @@ missing proof goes back as step 1 says. The Dispatch message (step 7) is for a f
|
|
|
82
82
|
Dispatch's message limit, and ends with your attribution footer:
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
dispatch message --issue
|
|
85
|
+
dispatch message --issue '<KEY>' --body-file - <<'EOF'
|
|
86
86
|
## Retro Complete
|
|
87
87
|
|
|
88
88
|
**Learnings documented in:** docs/solutions/<path>.md, or none: <why no point qualified>.
|
|
@@ -157,13 +157,13 @@ takes. That issue,
|
|
|
157
157
|
it](#when-no-controller-takes-it) says:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
dispatch comment --issue <ASK KEY> --body-file - <<'EOF'
|
|
160
|
+
dispatch comment --issue '<ASK KEY>' --body-file - <<'EOF'
|
|
161
161
|
<the evidence: commands, output with its time, file:line, links>
|
|
162
162
|
EOF
|
|
163
163
|
# Keep `File it for Legion` only when your Legion addressing line lists the repository where the
|
|
164
164
|
# fix belongs. For a clear defect no controller took, replace the `File an issue` option below with
|
|
165
165
|
# 'Hand it to the owning tree: <the tree, when you know it>', the first option and recommended.
|
|
166
|
-
dispatch ask --issue <ASK KEY> --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
|
|
166
|
+
dispatch ask --issue '<ASK KEY>' --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
|
|
167
167
|
--option 'File an issue: <what it would track, and who would own it>' \
|
|
168
168
|
--option 'File it for Legion: Legion works it once it is filed' \
|
|
169
169
|
--option 'Do not file: <what stays as it is>' <<'EOF'
|