@mstar-harness/engine 3.1.0 → 3.1.2
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.d.ts +14 -5
- package/dist/engine.js +54 -31
- package/package.json +1 -1
package/dist/dispatch.d.ts
CHANGED
|
@@ -173,8 +173,10 @@ export declare function assertTriIdentity(reviewerRoles: readonly string[]): Gat
|
|
|
173
173
|
export type ComposeDispatchGateOptions = {
|
|
174
174
|
/**
|
|
175
175
|
* Host role-binding field (omp task entry `agent` / opencode `subagent` /
|
|
176
|
-
* cursor `subagent_type`)
|
|
177
|
-
*
|
|
176
|
+
* cursor `subagent_type` / dsh `dispatchBinding`). The anti-recursion
|
|
177
|
+
* precheck runs for EVERY Assignment-shaped text: an empty or omitted
|
|
178
|
+
* binding fails closed (`dispatch.anti-recursion.empty-binding`) — the
|
|
179
|
+
* host cannot prove the dispatching agent is not recursing.
|
|
178
180
|
*/
|
|
179
181
|
agent?: string;
|
|
180
182
|
/**
|
|
@@ -204,7 +206,10 @@ export type ComposeDispatchGateResult = GateResult & {
|
|
|
204
206
|
* Assignment-shaped passes silently (`shaped: false`).
|
|
205
207
|
* 2. `validateAssignmentFields` with `writable: false` when `opts.writable
|
|
206
208
|
* === false` (read-only roles), else the writable default.
|
|
207
|
-
* 3. Anti-recursion precheck
|
|
209
|
+
* 3. Anti-recursion precheck — runs for every Assignment-shaped text; an
|
|
210
|
+
* empty/omitted host binding fails closed (`dispatch.anti-recursion.
|
|
211
|
+
* empty-binding`), a binding equal to `Execute as` is the existing
|
|
212
|
+
* `dispatch.anti-recursion.self-type` critical.
|
|
208
213
|
* 4. Default-branch gate for writable text: the branch comes from the
|
|
209
214
|
* Assignment's own branch forms (create-form name / Working branch /
|
|
210
215
|
* Branch policy branch), else `$MSTAR_WORKING_BRANCH`; a well-formed
|
|
@@ -221,7 +226,11 @@ export declare function composeDispatchGate(text: string, opts?: ComposeDispatch
|
|
|
221
226
|
* Anti-recursion precheck (NEVER red line, mstar-dispatch-gates § 承接方反递归
|
|
222
227
|
* 红线): a leaf executor MUST NOT invoke a Task/subagent whose role-binding
|
|
223
228
|
* field (`subagent_type` / `agent` / `subagent`) equals its own `Execute as`.
|
|
224
|
-
* Comparison is case-insensitive after trim
|
|
225
|
-
*
|
|
229
|
+
* Comparison is case-insensitive after trim. An EMPTY binding fails closed
|
|
230
|
+
* (`dispatch.anti-recursion.empty-binding`, critical): the host cannot
|
|
231
|
+
* report which agent is calling, so anti-recursion cannot be proven — the
|
|
232
|
+
* dispatch must not proceed as if the NEVER red line held. An empty
|
|
233
|
+
* `executeAs` with a set binding stays ok (field presence is
|
|
234
|
+
* `validateAssignmentFields`' job, not this precheck).
|
|
226
235
|
*/
|
|
227
236
|
export declare function antiRecursionPrecheck(subagentType: string, executeAs: string): GateResult;
|
package/dist/engine.js
CHANGED
|
@@ -170,7 +170,7 @@ function isAtOrBelow(dir, root) {
|
|
|
170
170
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
171
171
|
}
|
|
172
172
|
// src/path.ts
|
|
173
|
-
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
173
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
|
174
174
|
import { execFileSync } from "node:child_process";
|
|
175
175
|
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
176
176
|
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
@@ -425,6 +425,23 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
425
425
|
fix: `write the plan under ${planDir}`
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
|
+
if (existsSync2(planAbs)) {
|
|
429
|
+
try {
|
|
430
|
+
const canonicalPlan = realpathSync(planAbs);
|
|
431
|
+
const canonicalPlanDir = existsSync2(planDir) ? realpathSync(planDir) : resolve3(planDir);
|
|
432
|
+
const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
|
|
433
|
+
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute2(canonicalRel);
|
|
434
|
+
if (!canonicalInside) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
severity: "high",
|
|
438
|
+
code: "plan-path.symlink-escape",
|
|
439
|
+
message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
|
|
440
|
+
fix: `write the plan under ${planDir}`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
} catch {}
|
|
444
|
+
}
|
|
428
445
|
return {
|
|
429
446
|
ok: true,
|
|
430
447
|
severity: "low",
|
|
@@ -455,7 +472,7 @@ function hasFiles(dir) {
|
|
|
455
472
|
}
|
|
456
473
|
}
|
|
457
474
|
// src/status.ts
|
|
458
|
-
import { existsSync as
|
|
475
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
459
476
|
import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
|
|
460
477
|
|
|
461
478
|
// src/lease.ts
|
|
@@ -910,10 +927,8 @@ function composeDispatchGate(text, opts = {}) {
|
|
|
910
927
|
const violations = [];
|
|
911
928
|
const writable = opts.writable !== false;
|
|
912
929
|
violations.push(...validateAssignmentFields(text, { writable }).violations);
|
|
913
|
-
const agent =
|
|
914
|
-
|
|
915
|
-
violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
|
|
916
|
-
}
|
|
930
|
+
const agent = opts.agent ?? "";
|
|
931
|
+
violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
|
|
917
932
|
if (writable) {
|
|
918
933
|
const forms = parseAssignmentBranchForms(text);
|
|
919
934
|
const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
|
|
@@ -932,7 +947,15 @@ function composeDispatchGate(text, opts = {}) {
|
|
|
932
947
|
function antiRecursionPrecheck(subagentType, executeAs) {
|
|
933
948
|
const binding = subagentType.trim().toLowerCase();
|
|
934
949
|
const role = executeAs.trim().toLowerCase();
|
|
935
|
-
if (binding
|
|
950
|
+
if (binding === "") {
|
|
951
|
+
return {
|
|
952
|
+
ok: false,
|
|
953
|
+
violations: [
|
|
954
|
+
violation2("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
|
|
955
|
+
]
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (binding === role) {
|
|
936
959
|
return {
|
|
937
960
|
ok: false,
|
|
938
961
|
violations: [
|
|
@@ -1294,7 +1317,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1294
1317
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
1295
1318
|
let realHarnessDir = null;
|
|
1296
1319
|
try {
|
|
1297
|
-
realHarnessDir =
|
|
1320
|
+
realHarnessDir = realpathSync2(harnessDir);
|
|
1298
1321
|
} catch {}
|
|
1299
1322
|
for (const entry of doc.workflows) {
|
|
1300
1323
|
if (!isPlainObject3(entry) || typeof entry.dir !== "string")
|
|
@@ -1304,7 +1327,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1304
1327
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
1305
1328
|
let physical;
|
|
1306
1329
|
try {
|
|
1307
|
-
physical =
|
|
1330
|
+
physical = realpathSync2(snapshotPath);
|
|
1308
1331
|
} catch {
|
|
1309
1332
|
violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
1310
1333
|
continue;
|
|
@@ -1394,7 +1417,7 @@ async function unregisterWorkflow(root, id) {
|
|
|
1394
1417
|
}
|
|
1395
1418
|
function resolveCompassEnforcement(harnessDir) {
|
|
1396
1419
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
1397
|
-
if (!
|
|
1420
|
+
if (!existsSync3(iterationsDir))
|
|
1398
1421
|
return { hard: false, source: "none" };
|
|
1399
1422
|
let entries;
|
|
1400
1423
|
try {
|
|
@@ -1406,7 +1429,7 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1406
1429
|
if (!entry.isDirectory())
|
|
1407
1430
|
continue;
|
|
1408
1431
|
const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
|
|
1409
|
-
if (!
|
|
1432
|
+
if (!existsSync3(compassPath))
|
|
1410
1433
|
continue;
|
|
1411
1434
|
let content;
|
|
1412
1435
|
try {
|
|
@@ -1442,7 +1465,7 @@ function resolveRepoEnforcement(harnessDir) {
|
|
|
1442
1465
|
}
|
|
1443
1466
|
// src/worktree.ts
|
|
1444
1467
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1445
|
-
import { existsSync as
|
|
1468
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1446
1469
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
1447
1470
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
1448
1471
|
function probeTimeoutMs() {
|
|
@@ -1497,7 +1520,7 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
1497
1520
|
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
1498
1521
|
violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
1499
1522
|
}
|
|
1500
|
-
if (leaseWorktreePath !== "" && !
|
|
1523
|
+
if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
|
|
1501
1524
|
violations.push(violation5("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
|
|
1502
1525
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
1503
1526
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
@@ -1531,7 +1554,7 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
1531
1554
|
return;
|
|
1532
1555
|
}
|
|
1533
1556
|
seenPaths.add(normalized);
|
|
1534
|
-
if (!
|
|
1557
|
+
if (!existsSync4(track.worktreePath)) {
|
|
1535
1558
|
violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
1536
1559
|
return;
|
|
1537
1560
|
}
|
|
@@ -1594,7 +1617,7 @@ function singleReviewSnapshot(assignments) {
|
|
|
1594
1617
|
}
|
|
1595
1618
|
// src/sdd.ts
|
|
1596
1619
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1597
|
-
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as
|
|
1620
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1598
1621
|
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
|
|
1599
1622
|
class SddScriptError extends Error {
|
|
1600
1623
|
exitCode;
|
|
@@ -1671,8 +1694,8 @@ function isLinkedWorktree(root) {
|
|
|
1671
1694
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
1672
1695
|
return true;
|
|
1673
1696
|
try {
|
|
1674
|
-
const gdParent =
|
|
1675
|
-
const cmAbs =
|
|
1697
|
+
const gdParent = realpathSync3(dirname6(gitDir));
|
|
1698
|
+
const cmAbs = realpathSync3(common);
|
|
1676
1699
|
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
1677
1700
|
} catch {
|
|
1678
1701
|
return false;
|
|
@@ -1690,10 +1713,10 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
1690
1713
|
if (!isDirectory2(controlRoot)) {
|
|
1691
1714
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
1692
1715
|
}
|
|
1693
|
-
root =
|
|
1716
|
+
root = realpathSync3(controlRoot);
|
|
1694
1717
|
} else {
|
|
1695
1718
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1696
|
-
root =
|
|
1719
|
+
root = realpathSync3(topLevel ?? cwd);
|
|
1697
1720
|
}
|
|
1698
1721
|
if (!controlRoot && isLinkedWorktree(root)) {
|
|
1699
1722
|
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
@@ -1727,7 +1750,7 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
1727
1750
|
mkdirSync5(sddDir, { recursive: true });
|
|
1728
1751
|
writeFileSync3(join7(sddDir, ".gitignore"), `*
|
|
1729
1752
|
`);
|
|
1730
|
-
return
|
|
1753
|
+
return realpathSync3(sddDir);
|
|
1731
1754
|
}
|
|
1732
1755
|
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
1733
1756
|
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
@@ -1879,7 +1902,7 @@ function implementerSessionStickyRules(input) {
|
|
|
1879
1902
|
return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
|
|
1880
1903
|
}
|
|
1881
1904
|
// src/iteration.ts
|
|
1882
|
-
import { existsSync as
|
|
1905
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
1883
1906
|
import { join as join8 } from "node:path";
|
|
1884
1907
|
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
1885
1908
|
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -2108,7 +2131,7 @@ function pushCadenceProbe(ciRunning, reviewWaveActive) {
|
|
|
2108
2131
|
return { ok: violations.length === 0, violations };
|
|
2109
2132
|
}
|
|
2110
2133
|
function assertIndexRowObligations(iterationsDir) {
|
|
2111
|
-
if (!
|
|
2134
|
+
if (!existsSync5(iterationsDir)) {
|
|
2112
2135
|
return {
|
|
2113
2136
|
ok: false,
|
|
2114
2137
|
violations: [
|
|
@@ -2116,9 +2139,9 @@ function assertIndexRowObligations(iterationsDir) {
|
|
|
2116
2139
|
]
|
|
2117
2140
|
};
|
|
2118
2141
|
}
|
|
2119
|
-
const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) =>
|
|
2142
|
+
const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync5(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
|
|
2120
2143
|
const readmePath = join8(iterationsDir, INDEX_README);
|
|
2121
|
-
if (!
|
|
2144
|
+
if (!existsSync5(readmePath)) {
|
|
2122
2145
|
return {
|
|
2123
2146
|
ok: false,
|
|
2124
2147
|
violations: [
|
|
@@ -2209,7 +2232,7 @@ function parseFlowArray(raw, filePath) {
|
|
|
2209
2232
|
return items;
|
|
2210
2233
|
}
|
|
2211
2234
|
// src/project.ts
|
|
2212
|
-
import { existsSync as
|
|
2235
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
2213
2236
|
import { join as join9 } from "node:path";
|
|
2214
2237
|
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
2215
2238
|
var PROJECT_REFERENCES_DIR = "references";
|
|
@@ -2393,7 +2416,7 @@ function techDebtRollup(projectDir) {
|
|
|
2393
2416
|
if (!project.isDirectory())
|
|
2394
2417
|
continue;
|
|
2395
2418
|
const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
2396
|
-
if (!
|
|
2419
|
+
if (!existsSync6(registerPath))
|
|
2397
2420
|
continue;
|
|
2398
2421
|
let register;
|
|
2399
2422
|
try {
|
|
@@ -3674,7 +3697,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
3674
3697
|
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
3675
3698
|
}
|
|
3676
3699
|
// src/compound.ts
|
|
3677
|
-
import { existsSync as
|
|
3700
|
+
import { existsSync as existsSync7, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
3678
3701
|
import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
|
|
3679
3702
|
function violation10(severity, code, message, fix) {
|
|
3680
3703
|
return { ok: false, severity, code, message, fix };
|
|
@@ -3982,7 +4005,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3982
4005
|
for (const { ref, isSymbol, module } of refs) {
|
|
3983
4006
|
if (!isSymbol || module === undefined) {
|
|
3984
4007
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
3985
|
-
if (
|
|
4008
|
+
if (existsSync7(resolve10(repoRoot, candidate))) {
|
|
3986
4009
|
checked++;
|
|
3987
4010
|
} else {
|
|
3988
4011
|
violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
|
|
@@ -4030,7 +4053,7 @@ function normalizeIndexRef(cell) {
|
|
|
4030
4053
|
function assertIndexRows(knowledgeDir) {
|
|
4031
4054
|
const violations = [];
|
|
4032
4055
|
const readmePath = join12(knowledgeDir, "README.md");
|
|
4033
|
-
if (!
|
|
4056
|
+
if (!existsSync7(readmePath)) {
|
|
4034
4057
|
violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
|
|
4035
4058
|
return { ok: false, violations };
|
|
4036
4059
|
}
|
|
@@ -4315,7 +4338,7 @@ function lintStrategySections(docText) {
|
|
|
4315
4338
|
return { ok: violations.length === 0, violations };
|
|
4316
4339
|
}
|
|
4317
4340
|
// src/roles.ts
|
|
4318
|
-
import { existsSync as
|
|
4341
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4319
4342
|
import { join as join13 } from "node:path";
|
|
4320
4343
|
function violation12(severity, code, message, fix) {
|
|
4321
4344
|
return { ok: false, severity, code, message, fix };
|
|
@@ -4372,7 +4395,7 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4372
4395
|
const violations = [];
|
|
4373
4396
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
4374
4397
|
for (const { agentId, reference } of mapping) {
|
|
4375
|
-
if (!
|
|
4398
|
+
if (!existsSync8(join13(rolesDir, reference))) {
|
|
4376
4399
|
violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join13(rolesDir, reference)} or fix the mapping row`));
|
|
4377
4400
|
}
|
|
4378
4401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/engine",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|