@mstar-harness/engine 3.9.4 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +15 -0
- package/dist/audit.js +131 -31
- package/dist/coordination-write.d.ts +1 -1
- package/dist/coordination.d.ts +85 -0
- package/dist/dispatch.d.ts +15 -1
- package/dist/engine.js +1269 -131
- package/dist/index.d.ts +4 -4
- package/dist/iteration.d.ts +33 -1
- package/dist/migrate.d.ts +40 -1
- package/dist/sdd.d.ts +9 -1
- package/dist/workflow.d.ts +324 -2
- package/package.json +1 -1
package/dist/audit.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GateResult } from "./core.js";
|
|
2
|
+
import { type WorkflowDeliveryKind } from "./workflow.js";
|
|
2
3
|
/** Priority values (mstar-audit SKILL.md § Plan output (all variants) Status block). */
|
|
3
4
|
export declare const AUDIT_PRIORITIES: readonly ["P1", "P2", "P3"];
|
|
4
5
|
export type AuditPriority = (typeof AUDIT_PRIORITIES)[number];
|
|
@@ -226,6 +227,20 @@ export type PromoteAuditPlansOptions = {
|
|
|
226
227
|
harnessDir: string;
|
|
227
228
|
/** Default: basename of `outDir` (e.g. `audit-2026-08-22`). */
|
|
228
229
|
workflowId?: string;
|
|
230
|
+
/**
|
|
231
|
+
* Delivery kind declared for the promoted plans (contract §1/§4a). Required —
|
|
232
|
+
* the promoted lifecycle is a `type: plan` workflow, so its delivery kind is
|
|
233
|
+
* declared at registration, never inferred and never defaulted in code (an
|
|
234
|
+
* audit promotion that left it unset minted an active snapshot no close path
|
|
235
|
+
* could complete).
|
|
236
|
+
*/
|
|
237
|
+
deliveryKind: WorkflowDeliveryKind;
|
|
238
|
+
/** Delivery source branch, recorded as `branch.source`. Required for `development`. */
|
|
239
|
+
branchSource?: string;
|
|
240
|
+
/** Delivery target branch, recorded as `branch.target`. Required for `development`. */
|
|
241
|
+
branchTarget?: string;
|
|
242
|
+
/** Completion policy for `verification/report-only` promotions. Required for that kind. */
|
|
243
|
+
completionPolicy?: string;
|
|
229
244
|
};
|
|
230
245
|
/**
|
|
231
246
|
* Promote selected audit plans into the v2 workflow lifecycle as a
|
package/dist/audit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/audit.ts
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as
|
|
4
|
-
import { basename as basename5, join as join9, resolve as
|
|
3
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4
|
+
import { basename as basename5, join as join9, resolve as resolve9, sep as sep3 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/lease.ts
|
|
7
7
|
import { mkdirSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -500,9 +500,9 @@ function isArtifactVersion(value) {
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
// src/path.ts
|
|
503
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as
|
|
503
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
504
504
|
import { execFileSync } from "node:child_process";
|
|
505
|
-
import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join8, relative as relative2, resolve as
|
|
505
|
+
import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join8, relative as relative2, resolve as resolve8 } from "node:path";
|
|
506
506
|
|
|
507
507
|
// src/mstarc.ts
|
|
508
508
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
@@ -626,8 +626,8 @@ function writeJson(filePath, value) {
|
|
|
626
626
|
}
|
|
627
627
|
|
|
628
628
|
// src/status.ts
|
|
629
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as
|
|
630
|
-
import { dirname as dirname5, join as join7, resolve as
|
|
629
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
630
|
+
import { dirname as dirname5, join as join7, resolve as resolve7, sep as sep2 } from "node:path";
|
|
631
631
|
|
|
632
632
|
// src/store.ts
|
|
633
633
|
import { existsSync as existsSync3, readdirSync, unlinkSync as unlinkSync3 } from "node:fs";
|
|
@@ -789,12 +789,14 @@ function assertFsStorePath(store, ref, expectedPath) {
|
|
|
789
789
|
}
|
|
790
790
|
|
|
791
791
|
// src/workflow.ts
|
|
792
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
793
|
-
import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
|
|
792
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, rmdirSync as rmdirSync2 } from "node:fs";
|
|
793
|
+
import { isAbsolute as isAbsolute5, join as join6, resolve as resolve6 } from "node:path";
|
|
794
794
|
var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
795
795
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
796
796
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
797
797
|
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
798
|
+
var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
|
|
799
|
+
var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
|
|
798
800
|
function stableJson(value) {
|
|
799
801
|
if (Array.isArray(value))
|
|
800
802
|
return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -819,6 +821,61 @@ function validateWorktreePathValue(violations, value, field) {
|
|
|
819
821
|
violations.push(violation2("high", "workflow.snapshot.invalid-integration-worktree-path", `${field} must be a non-empty absolute path — got ${JSON.stringify(value)}`, "record the absolute integration checkout path (integration_worktree_path)"));
|
|
820
822
|
}
|
|
821
823
|
}
|
|
824
|
+
function deliveryEvidenceViolations(value, what) {
|
|
825
|
+
const violations = [];
|
|
826
|
+
const invalid2 = (message) => {
|
|
827
|
+
violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-evidence", `${what}: ${message}`));
|
|
828
|
+
};
|
|
829
|
+
if (!isPlainObject2(value)) {
|
|
830
|
+
invalid2("must be an object");
|
|
831
|
+
return violations;
|
|
832
|
+
}
|
|
833
|
+
const members = ["compound", "pr", "merge", "completion"];
|
|
834
|
+
const unknownMembers = Object.keys(value).filter((key) => !members.includes(key));
|
|
835
|
+
if (unknownMembers.length > 0)
|
|
836
|
+
invalid2(`unknown member(s) ${unknownMembers.join(", ")} — expected ${members.join(" | ")}`);
|
|
837
|
+
const compound = value.compound;
|
|
838
|
+
if (compound !== undefined) {
|
|
839
|
+
if (!isPlainObject2(compound))
|
|
840
|
+
invalid2("compound must be an object");
|
|
841
|
+
else {
|
|
842
|
+
const unknown = Object.keys(compound).filter((key) => key !== "outcome" && key !== "reason");
|
|
843
|
+
if (unknown.length > 0)
|
|
844
|
+
invalid2(`compound has unknown key(s) ${unknown.join(", ")}`);
|
|
845
|
+
if (typeof compound.outcome !== "string" || !WORKFLOW_COMPOUND_OUTCOMES.includes(compound.outcome)) {
|
|
846
|
+
invalid2(`compound.outcome must be one of ${WORKFLOW_COMPOUND_OUTCOMES.join(" | ")} — got ${JSON.stringify(compound.outcome)}`);
|
|
847
|
+
} else if (compound.outcome === "skipped" && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
|
|
848
|
+
invalid2("compound reason is required when the disposition outcome is 'skipped' (contract §4c)");
|
|
849
|
+
} else if (compound.reason !== undefined && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
|
|
850
|
+
invalid2("compound.reason must be a non-empty string when given");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const stringMembers = {
|
|
855
|
+
pr: ["repo", "head", "target"],
|
|
856
|
+
merge: ["provider", "evidence"],
|
|
857
|
+
completion: ["policy", "evidence"]
|
|
858
|
+
};
|
|
859
|
+
for (const member of ["pr", "merge", "completion"]) {
|
|
860
|
+
const block = value[member];
|
|
861
|
+
if (block === undefined)
|
|
862
|
+
continue;
|
|
863
|
+
if (!isPlainObject2(block)) {
|
|
864
|
+
invalid2(`${member} must be an object`);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const fields = stringMembers[member];
|
|
868
|
+
const unknown = Object.keys(block).filter((key) => !fields.includes(key));
|
|
869
|
+
if (unknown.length > 0)
|
|
870
|
+
invalid2(`${member} has unknown key(s) ${unknown.join(", ")}`);
|
|
871
|
+
for (const field of fields) {
|
|
872
|
+
if (typeof block[field] !== "string" || block[field].trim() === "") {
|
|
873
|
+
invalid2(`${member}.${field} must be a non-empty string`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return violations;
|
|
878
|
+
}
|
|
822
879
|
function validateWorkflowSnapshot(doc) {
|
|
823
880
|
const violations = [];
|
|
824
881
|
if (!isPlainObject2(doc)) {
|
|
@@ -884,7 +941,7 @@ function validateWorkflowSnapshot(doc) {
|
|
|
884
941
|
if (!isPlainObject2(doc.branch)) {
|
|
885
942
|
violations.push(violation2("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
886
943
|
} else {
|
|
887
|
-
for (const key of ["base", "integration", "target"]) {
|
|
944
|
+
for (const key of ["base", "source", "integration", "target"]) {
|
|
888
945
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
889
946
|
violations.push(violation2("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
890
947
|
}
|
|
@@ -910,6 +967,20 @@ function validateWorkflowSnapshot(doc) {
|
|
|
910
967
|
if (doc.compass_ref !== undefined) {
|
|
911
968
|
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
912
969
|
}
|
|
970
|
+
if (doc.delivery_kind !== undefined) {
|
|
971
|
+
if (typeof doc.delivery_kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(doc.delivery_kind)) {
|
|
972
|
+
violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-kind", `delivery_kind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(doc.delivery_kind)}`));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (doc.project !== undefined) {
|
|
976
|
+
validateNonEmptyString2(violations, doc.project, "project", "workflow.snapshot.missing-project", "workflow.snapshot.invalid-project");
|
|
977
|
+
}
|
|
978
|
+
if (doc.completion_policy !== undefined) {
|
|
979
|
+
validateNonEmptyString2(violations, doc.completion_policy, "completion_policy", "workflow.snapshot.missing-completion-policy", "workflow.snapshot.invalid-completion-policy");
|
|
980
|
+
}
|
|
981
|
+
if (doc.delivery !== undefined) {
|
|
982
|
+
violations.push(...deliveryEvidenceViolations(doc.delivery, "delivery"));
|
|
983
|
+
}
|
|
913
984
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
914
985
|
if (terminal) {
|
|
915
986
|
if (doc.ended_at === undefined) {
|
|
@@ -986,6 +1057,22 @@ function mergePhaseProjection(stored, incoming) {
|
|
|
986
1057
|
}
|
|
987
1058
|
return next;
|
|
988
1059
|
}
|
|
1060
|
+
function nonEmptyString(value) {
|
|
1061
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1062
|
+
}
|
|
1063
|
+
function assertDeliveryRegistrationCoherence(kind, evidence, what) {
|
|
1064
|
+
if (kind === "development" && (!nonEmptyString(evidence.branchSource) || !nonEmptyString(evidence.branchTarget))) {
|
|
1065
|
+
throw new Error(`${what}: a development workflow requires its delivery source and target branches (--branch-source/--branch-target) — missing branch fields are incomplete registration, not an exempt workflow (contract §1/§4a)`);
|
|
1066
|
+
}
|
|
1067
|
+
if (kind === "verification/report-only" && !nonEmptyString(evidence.completionPolicy)) {
|
|
1068
|
+
throw new Error(`${what}: a verification/report-only workflow requires the completion policy (--completion-policy) naming the evidence that completes it (contract §1)`);
|
|
1069
|
+
}
|
|
1070
|
+
for (const [field, value] of Object.entries({ branchSource: evidence.branchSource, branchTarget: evidence.branchTarget, completionPolicy: evidence.completionPolicy })) {
|
|
1071
|
+
if (value !== undefined && !nonEmptyString(value)) {
|
|
1072
|
+
throw new Error(`${what}: ${field} must be a non-empty string when given`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
989
1076
|
|
|
990
1077
|
// src/status.ts
|
|
991
1078
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -1080,7 +1167,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1080
1167
|
if (typeof docOrPath === "string") {
|
|
1081
1168
|
try {
|
|
1082
1169
|
doc = readJson(docOrPath);
|
|
1083
|
-
harnessDir = dirname5(
|
|
1170
|
+
harnessDir = dirname5(resolve7(docOrPath));
|
|
1084
1171
|
} catch (error) {
|
|
1085
1172
|
return {
|
|
1086
1173
|
ok: false,
|
|
@@ -1239,14 +1326,14 @@ function readRegisteredSnapshot(snapshotPath) {
|
|
|
1239
1326
|
|
|
1240
1327
|
// src/path.ts
|
|
1241
1328
|
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
1242
|
-
const start =
|
|
1329
|
+
const start = resolve8(startDir);
|
|
1243
1330
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
1244
1331
|
if (explicit)
|
|
1245
|
-
return
|
|
1246
|
-
const boundary =
|
|
1332
|
+
return resolve8(start, explicit);
|
|
1333
|
+
const boundary = resolve8(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
1247
1334
|
const rc = loadMstarc(start, boundary);
|
|
1248
1335
|
if (rc !== null && rc.config.harnessDir)
|
|
1249
|
-
return
|
|
1336
|
+
return resolve8(rc.dir, rc.config.harnessDir);
|
|
1250
1337
|
let dir = start;
|
|
1251
1338
|
for (;; ) {
|
|
1252
1339
|
if (!isAtOrBelow2(dir, boundary))
|
|
@@ -1277,7 +1364,7 @@ function defaultWorkspaceRoot(startDir) {
|
|
|
1277
1364
|
if (segment && segment !== ".")
|
|
1278
1365
|
boundary = dirname6(boundary);
|
|
1279
1366
|
}
|
|
1280
|
-
return
|
|
1367
|
+
return resolve8(boundary);
|
|
1281
1368
|
} catch {}
|
|
1282
1369
|
return startDir;
|
|
1283
1370
|
}
|
|
@@ -1286,10 +1373,10 @@ function isAtOrBelow2(dir, root) {
|
|
|
1286
1373
|
return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
|
|
1287
1374
|
}
|
|
1288
1375
|
function mstarcDirOverride(harnessDir, key) {
|
|
1289
|
-
const dir =
|
|
1376
|
+
const dir = resolve8(harnessDir);
|
|
1290
1377
|
const rc = loadMstarc(dir, dirname6(dir));
|
|
1291
1378
|
const declared = rc?.config[key];
|
|
1292
|
-
return declared ?
|
|
1379
|
+
return declared ? resolve8(rc.dir, declared) : null;
|
|
1293
1380
|
}
|
|
1294
1381
|
function assertSafePathComponent(value, what) {
|
|
1295
1382
|
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
@@ -1299,10 +1386,10 @@ function assertSafePathComponent(value, what) {
|
|
|
1299
1386
|
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
1300
1387
|
const harness = resolveHarnessDir(startDir, opts);
|
|
1301
1388
|
if (harness === null) {
|
|
1302
|
-
throw new Error(`harness dir not found from ${
|
|
1389
|
+
throw new Error(`harness dir not found from ${resolve8(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
1303
1390
|
}
|
|
1304
1391
|
const declared = mstarcDirOverride(harness, key);
|
|
1305
|
-
return declared !== null ? declared : join8(
|
|
1392
|
+
return declared !== null ? declared : join8(resolve8(harness), fallback);
|
|
1306
1393
|
}
|
|
1307
1394
|
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
1308
1395
|
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
@@ -1696,7 +1783,7 @@ var LOCKFILE_NAMES = [
|
|
|
1696
1783
|
function rootLockfiles(root) {
|
|
1697
1784
|
let entries;
|
|
1698
1785
|
try {
|
|
1699
|
-
entries =
|
|
1786
|
+
entries = readdirSync5(root, { withFileTypes: true });
|
|
1700
1787
|
} catch {
|
|
1701
1788
|
return [];
|
|
1702
1789
|
}
|
|
@@ -1729,7 +1816,7 @@ function supplyChainChecks(repoRoot) {
|
|
|
1729
1816
|
const workflowsDir = join9(repoRoot, ".github", "workflows");
|
|
1730
1817
|
let wfEntries = [];
|
|
1731
1818
|
try {
|
|
1732
|
-
wfEntries =
|
|
1819
|
+
wfEntries = readdirSync5(workflowsDir, { withFileTypes: true });
|
|
1733
1820
|
} catch {
|
|
1734
1821
|
wfEntries = [];
|
|
1735
1822
|
}
|
|
@@ -1895,7 +1982,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
1895
1982
|
mkdirSync5(outDir, { recursive: true });
|
|
1896
1983
|
const existingReadme = join9(outDir, "README.md");
|
|
1897
1984
|
const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
1898
|
-
const existing =
|
|
1985
|
+
const existing = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
1899
1986
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
1900
1987
|
const redactedFindings = findings.map(redactFinding);
|
|
1901
1988
|
const written = [];
|
|
@@ -1960,7 +2047,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
1960
2047
|
needsVerification: needsVerificationLines,
|
|
1961
2048
|
hardeningChecked: hardeningCheckedLines
|
|
1962
2049
|
}));
|
|
1963
|
-
return { outDir:
|
|
2050
|
+
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
1964
2051
|
}
|
|
1965
2052
|
async function promoteAuditPlans(outDir, selected, options) {
|
|
1966
2053
|
if (selected.length === 0) {
|
|
@@ -1969,9 +2056,13 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
1969
2056
|
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
1970
2057
|
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
1971
2058
|
}
|
|
1972
|
-
|
|
2059
|
+
if (typeof options.deliveryKind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(options.deliveryKind)) {
|
|
2060
|
+
throw new Error(`promoteAuditPlans: options.deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(options.deliveryKind)} (a promoted plan workflow declares its delivery kind at registration; it is never inferred and never defaulted)`);
|
|
2061
|
+
}
|
|
2062
|
+
assertDeliveryRegistrationCoherence(options.deliveryKind, options, "promoteAuditPlans");
|
|
2063
|
+
const workflowId = options.workflowId ?? basename5(resolve9(outDir));
|
|
1973
2064
|
assertSafePathComponent(workflowId, "workflow id");
|
|
1974
|
-
const harnessDir =
|
|
2065
|
+
const harnessDir = resolve9(options.harnessDir);
|
|
1975
2066
|
const statusPath = join9(harnessDir, "status.json");
|
|
1976
2067
|
const workflowDir = join9(harnessDir, "workflows", workflowId);
|
|
1977
2068
|
const snapshotPath = join9(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
@@ -1998,8 +2089,17 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
1998
2089
|
status: "running",
|
|
1999
2090
|
started_at: now.toISOString(),
|
|
2000
2091
|
updated_at: now.toISOString().slice(0, 10),
|
|
2001
|
-
plans
|
|
2092
|
+
plans,
|
|
2093
|
+
delivery_kind: options.deliveryKind
|
|
2002
2094
|
};
|
|
2095
|
+
if (options.branchSource !== undefined || options.branchTarget !== undefined) {
|
|
2096
|
+
snapshot.branch = {
|
|
2097
|
+
...options.branchSource !== undefined ? { source: options.branchSource } : {},
|
|
2098
|
+
...options.branchTarget !== undefined ? { target: options.branchTarget } : {}
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
if (options.completionPolicy !== undefined)
|
|
2102
|
+
snapshot.completion_policy = options.completionPolicy;
|
|
2003
2103
|
const entry = {
|
|
2004
2104
|
id: workflowId,
|
|
2005
2105
|
type: "plan",
|
|
@@ -2032,8 +2132,8 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
2032
2132
|
});
|
|
2033
2133
|
}
|
|
2034
2134
|
try {
|
|
2035
|
-
if (
|
|
2036
|
-
|
|
2135
|
+
if (readdirSync5(workflowDir).length === 0) {
|
|
2136
|
+
rmdirSync3(workflowDir);
|
|
2037
2137
|
}
|
|
2038
2138
|
} catch {}
|
|
2039
2139
|
throw error;
|
|
@@ -2043,7 +2143,7 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
2043
2143
|
return { workflowId, snapshotPath };
|
|
2044
2144
|
}
|
|
2045
2145
|
function resolveSelectedPlanFiles(outDir, selected) {
|
|
2046
|
-
const files =
|
|
2146
|
+
const files = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
|
|
2047
2147
|
const byNum = new Map;
|
|
2048
2148
|
const byStem = new Map;
|
|
2049
2149
|
for (const file of files) {
|
|
@@ -2058,7 +2158,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
2058
2158
|
for (const id of selected) {
|
|
2059
2159
|
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
2060
2160
|
if (file === undefined) {
|
|
2061
|
-
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${
|
|
2161
|
+
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
|
|
2062
2162
|
}
|
|
2063
2163
|
if (!seen.has(file)) {
|
|
2064
2164
|
seen.add(file);
|
|
@@ -2097,7 +2197,7 @@ function readExecutionOrderIndex(outDir) {
|
|
|
2097
2197
|
return rows;
|
|
2098
2198
|
}
|
|
2099
2199
|
function planFileRel(outDir, planFile) {
|
|
2100
|
-
const resolved =
|
|
2200
|
+
const resolved = resolve9(outDir);
|
|
2101
2201
|
const parts = resolved.split(sep3);
|
|
2102
2202
|
const plansIdx = parts.lastIndexOf("plans");
|
|
2103
2203
|
if (plansIdx >= 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ValidationResult } from "./core.js";
|
|
2
2
|
/** Stable refusal codes of the scoped coordination surface (spec §C4). */
|
|
3
|
-
export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.not-
|
|
3
|
+
export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.store", "coordination.prepare-amendment.stale", "coordination.prepare-amendment.invalid-patch", "coordination.prepare-amendment.not-prepare", "coordination.prepare-amendment.execution-started", "coordination.prepare-amendment.duplicate-plan", "coordination.prepare-amendment.invalid-plan", "coordination.prepare-amendment.compass-mismatch", "coordination.prepare-amendment.invalid-worktree"];
|
|
4
4
|
export type CoordinationErrorCode = (typeof COORDINATION_ERROR_CODES)[number];
|
|
5
5
|
/**
|
|
6
6
|
* Stable exception of the coordination surface: `code` is the consumer
|
package/dist/coordination.d.ts
CHANGED
|
@@ -282,3 +282,88 @@ export declare function mutatePlanCoordination(request: CoordinationRequest): Pr
|
|
|
282
282
|
* they refuse explicitly instead of silently no-opping.
|
|
283
283
|
*/
|
|
284
284
|
export declare function replaceCoordinatedArtifact(input: CoordinatedReplacement): Promise<VersionedArtifact>;
|
|
285
|
+
/**
|
|
286
|
+
* The guarded, coordinator-authenticated Prepare-stage amendment (§ New API and
|
|
287
|
+
* CLI, frozen). One approved plan row: the plan markdown that carries it plus
|
|
288
|
+
* the metadata the row records. Nothing else is addressable — the verb cannot
|
|
289
|
+
* edit an existing row.
|
|
290
|
+
*/
|
|
291
|
+
export type PreparePlanAppend = Readonly<{
|
|
292
|
+
id: string;
|
|
293
|
+
title: string;
|
|
294
|
+
file: string;
|
|
295
|
+
metadata: Readonly<{
|
|
296
|
+
primary_spec: string;
|
|
297
|
+
spec_refs: readonly string[];
|
|
298
|
+
iteration_compass: string;
|
|
299
|
+
iteration_refs: readonly string[];
|
|
300
|
+
working_branch: string;
|
|
301
|
+
spec_integration_branch: string;
|
|
302
|
+
merge_target: string;
|
|
303
|
+
}>;
|
|
304
|
+
}>;
|
|
305
|
+
/**
|
|
306
|
+
* The whole structural delta one amendment may apply (§ Admission and mutation
|
|
307
|
+
* step 5): the caller's main-worktree branch, the approved plan appends, the
|
|
308
|
+
* reviewed integration checkout, and the single approved execution-policy key.
|
|
309
|
+
*/
|
|
310
|
+
export type PrepareWorkflowPatch = Readonly<{
|
|
311
|
+
mainWorktreeBranch: string;
|
|
312
|
+
appendPlans: readonly PreparePlanAppend[];
|
|
313
|
+
integrationWorktreePath?: string;
|
|
314
|
+
planParallelism?: "serial" | "parallel";
|
|
315
|
+
}>;
|
|
316
|
+
/** What a coordinator observes about one workflow before amending it. */
|
|
317
|
+
export type PrepareWorkflowView = Readonly<{
|
|
318
|
+
workflowId: string;
|
|
319
|
+
/** `sha256:<64 hex>` of the snapshot bytes (the amendment CAS token). */
|
|
320
|
+
snapshotVersion: string;
|
|
321
|
+
/** `sha256:<64 hex>` of the reviewed compass Markdown bytes (CAS token). */
|
|
322
|
+
compassVersion: string;
|
|
323
|
+
/** Plan ids in row order — including this call's own appends on success. */
|
|
324
|
+
planIds: readonly string[];
|
|
325
|
+
/** `true` when `amendPrepareWorkflow` is admitted with fresh tokens. */
|
|
326
|
+
allowed: boolean;
|
|
327
|
+
/** One `<reason>: <message>` line per admission blocker; empty when allowed. */
|
|
328
|
+
blockers: readonly string[];
|
|
329
|
+
}>;
|
|
330
|
+
/** Success shape of the verb: the existing envelope with a workflow view. */
|
|
331
|
+
export type PrepareWorkflowResult = Omit<CoordinationResult, "view"> & {
|
|
332
|
+
view: PrepareWorkflowView;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Read the workflow-level Prepare view of one coordinator-bound workflow
|
|
336
|
+
* (spec § New API and CLI). Read-only: no snapshot lock, nothing written. The
|
|
337
|
+
* two byte versions are the tokens `amendPrepareWorkflow` requires, and
|
|
338
|
+
* `allowed`/`blockers` report the Prepare/no-execution admission exactly as the
|
|
339
|
+
* mutation evaluates it — an inadmissible *lifecycle state* is readable, not an
|
|
340
|
+
* error, so a caller can inspect a workflow before deciding to amend it.
|
|
341
|
+
* Problems that make the documents or the caller's identity unusable — a
|
|
342
|
+
* missing/foreign/mismatched envelope, an unregistered or unreadable snapshot,
|
|
343
|
+
* an unreadable or borrowed compass — still refuse with their own code, because
|
|
344
|
+
* no trustworthy answer can be produced from them.
|
|
345
|
+
*/
|
|
346
|
+
export declare function showPrepareWorkflow(input: Readonly<{
|
|
347
|
+
sessionPath: string;
|
|
348
|
+
cwd?: string;
|
|
349
|
+
}>): Promise<PrepareWorkflowResult>;
|
|
350
|
+
/**
|
|
351
|
+
* Apply one approved Prepare structural amendment (spec § Admission and
|
|
352
|
+
* mutation). The CAS read, both version comparisons, the admission, the whole
|
|
353
|
+
* patch validation, the compass recheck and the single atomic write all run
|
|
354
|
+
* under the canonical snapshot write lock, so two callers presenting the same
|
|
355
|
+
* tokens cannot both succeed: the loser inspects the winner's bytes and
|
|
356
|
+
* refuses as `stale`. Every refusal happens before any write — the protected
|
|
357
|
+
* snapshot, root register, other workflows and the compass stay byte-identical.
|
|
358
|
+
*
|
|
359
|
+
* A lock that cannot be acquired refuses explicitly (the shared
|
|
360
|
+
* `withStatusWriteLock` Blocked error); Git-unavailable probes refuse through
|
|
361
|
+
* the existing `coordination.git-unavailable`.
|
|
362
|
+
*/
|
|
363
|
+
export declare function amendPrepareWorkflow(input: Readonly<{
|
|
364
|
+
sessionPath: string;
|
|
365
|
+
cwd?: string;
|
|
366
|
+
expectedSnapshotVersion: string;
|
|
367
|
+
expectedCompassVersion: string;
|
|
368
|
+
patch: PrepareWorkflowPatch;
|
|
369
|
+
}>): Promise<PrepareWorkflowResult>;
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -19,6 +19,17 @@ export type AssignmentFields = {
|
|
|
19
19
|
* violation code per field so the PM sees exactly which label is missing.
|
|
20
20
|
*/
|
|
21
21
|
returnShape?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `Task budget (implement / ops rounds)` — the declared capacity of ONE
|
|
24
|
+
* implementer round closing the task's declared Files list and verification
|
|
25
|
+
* gates (capacity contract spec § A1/A2; the value cites the task's
|
|
26
|
+
* `Effort (agent-oriented)` band). Required, presence-only, on the exact
|
|
27
|
+
* complement of the review/audit rounds — implement/ops rounds including
|
|
28
|
+
* non-audit docs/Prepare specialists and orientation roles. The value is
|
|
29
|
+
* prose; it is never parsed numerically (adequacy stays in PM's Prepare
|
|
30
|
+
* check).
|
|
31
|
+
*/
|
|
32
|
+
taskBudget?: string;
|
|
22
33
|
};
|
|
23
34
|
export type ValidateAssignmentFieldsOptions = {
|
|
24
35
|
/**
|
|
@@ -161,7 +172,10 @@ export declare function isReadOnlyAssignmentRole(roleId: string): boolean;
|
|
|
161
172
|
* non-empty values (paste-only shells are caught here — every field missing).
|
|
162
173
|
* Review-seat and audit rounds must additionally declare both round-bounding
|
|
163
174
|
* fields, `Budget (review / QC seats)` and `Return shape (review / QC
|
|
164
|
-
* seats)` (one violation code per missing label).
|
|
175
|
+
* seats)` (one violation code per missing label). The complement — every
|
|
176
|
+
* implement/ops round, including non-audit docs/Prepare and orientation
|
|
177
|
+
* roles — must declare `Task budget (implement / ops rounds)`, presence-only.
|
|
178
|
+
* Writable assignments must
|
|
165
179
|
* carry EXACTLY ONE branch form; `create <new>`
|
|
166
180
|
* from <base>` without `<base>` (incl. the dangling `create <new> from`
|
|
167
181
|
* / `create from <base>` typos) and `Branch policy` without branch/reason
|