@mstar-harness/dsh 2.4.0 → 3.0.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/README.i18n.yaml +2 -2
- package/README.md +10 -8
- package/README.zh.md +7 -5
- package/bundle/README.md +4 -4
- package/dist/client/panel/graph/project-graph.d.ts +29 -3
- package/dist/client/panel/locale.d.ts +1 -1
- package/dist/client/panel/zones/ProjectRollup.d.ts +24 -0
- package/dist/client.js +270 -34
- package/dist/gates/_shared.d.ts +4 -4
- package/dist/gates/adapter.d.ts +38 -12
- package/dist/gates/agent-flow.d.ts +109 -12
- package/dist/gates/dispatch.d.ts +16 -10
- package/dist/gates/goal-bridge.d.ts +1 -1
- package/dist/gates/status.d.ts +64 -14
- package/dist/gates/system-prompt.d.ts +6 -2
- package/dist/gates/workflow-ledger.d.ts +88 -17
- package/dist/gates/workflow-policy.d.ts +2 -1
- package/dist/gates/workflow-selection.d.ts +31 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1532 -692
- package/dist/service.d.ts +14 -9
- package/dist/types.d.ts +64 -2
- package/harness-agents/code-reviewer.md +0 -8
- package/harness-skills/mstar-branch-worktree/SKILL.md +14 -14
- package/harness-skills/mstar-branch-worktree/references/parallel-writable-pre-dispatch.md +2 -2
- package/harness-skills/mstar-compound/SKILL.md +2 -2
- package/harness-skills/mstar-compound/references/compound-workflow.md +3 -3
- package/harness-skills/mstar-compound-refresh/SKILL.md +4 -4
- package/harness-skills/mstar-compound-refresh/references/project-knowledge-bootstrap.md +1 -1
- package/harness-skills/mstar-dispatch-gates/SKILL.md +2 -2
- package/harness-skills/mstar-engine-legacy/SKILL.md +53 -0
- package/harness-skills/mstar-engine-legacy/references/anti-recursion-checklists.md +58 -0
- package/harness-skills/mstar-engine-legacy/references/engine-check-boilerplate.md +26 -0
- package/harness-skills/mstar-engine-legacy/references/lease-protocol.md +92 -0
- package/harness-skills/mstar-engine-legacy/references/qc-seat-n-restatements.md +57 -0
- package/harness-skills/mstar-engine-legacy/references/status-field-history.md +186 -0
- package/harness-skills/mstar-harness-core/SKILL.md +6 -3
- package/harness-skills/mstar-host/references/_shared/plan-mode-bridge-core.md +8 -8
- package/harness-skills/mstar-host/references/codex.md +1 -1
- package/harness-skills/mstar-host/references/cursor-plan-mode-bridge.md +19 -8
- package/harness-skills/mstar-host/references/cursor.md +5 -5
- package/harness-skills/mstar-host/references/dsh.md +10 -7
- package/harness-skills/mstar-host/references/kimi-plan-mode-bridge.md +3 -3
- package/harness-skills/mstar-host/references/kimi.md +1 -1
- package/harness-skills/mstar-host/references/omp-plan-mode-bridge.md +2 -2
- package/harness-skills/mstar-host/references/omp.md +2 -2
- package/harness-skills/mstar-host/references/zcode-plan-mode-bridge.md +3 -3
- package/harness-skills/mstar-host/references/zcode.md +1 -1
- package/harness-skills/mstar-iteration/SKILL.md +38 -37
- package/harness-skills/mstar-iteration/references/autonomous-direction-lock.md +2 -2
- package/harness-skills/mstar-iteration/references/iteration-compass-template.md +3 -3
- package/harness-skills/mstar-iteration/references/phase-2-worktree-lease.md +50 -38
- package/harness-skills/mstar-iteration/references/phase-3-iteration-close.md +7 -7
- package/harness-skills/mstar-iteration/references/phase-4-5-pr-delivery.md +2 -2
- package/harness-skills/mstar-phase-gates/SKILL.md +3 -3
- package/harness-skills/mstar-plan-artifacts/SKILL.md +21 -23
- package/harness-skills/mstar-plan-artifacts/references/knowledge-and-designs.md +6 -6
- package/harness-skills/mstar-plan-artifacts/references/plan-files-and-reports.md +10 -10
- package/harness-skills/mstar-plan-artifacts/references/status-and-residuals.md +190 -309
- package/harness-skills/mstar-plan-artifacts/templates/README.md +1 -3
- package/harness-skills/mstar-plan-artifacts/templates/status.empty.json +2 -4
- package/harness-skills/mstar-plan-conventions/SKILL.md +53 -13
- package/harness-skills/mstar-plan-conventions/references/artifact-storage-paths.md +6 -3
- package/harness-skills/mstar-plan-conventions/references/harness-bootstrap-and-agents-layering.md +9 -10
- package/harness-skills/mstar-project-governance/SKILL.md +118 -0
- package/harness-skills/mstar-review-qc/SKILL.md +3 -3
- package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +1 -1
- package/harness-skills/mstar-roles/references/project-manager/plan-management.md +6 -8
- package/harness-skills/mstar-roles/references/project-manager/qc-and-residuals.md +6 -6
- package/harness-skills/mstar-roles/references/project-manager.md +7 -7
- package/harness-skills/mstar-roles/references/qc-specialist/deep-review-lenses.md +1 -1
- package/harness-skills/mstar-roles/references/qc-specialist/report-template.md +1 -1
- package/harness-skills/mstar-roles/references/qc-specialist/reviewer-workflow.md +1 -1
- package/harness-skills/mstar-roles/references/qc-specialist-shared.md +2 -2
- package/harness-skills/mstar-sdd/SKILL.md +2 -2
- package/harness-skills/mstar-sdd/references/file-handoffs.md +1 -1
- package/package.json +14 -14
- package/harness-skills/mstar-plan-artifacts/references/done-compaction.md +0 -88
- package/harness-skills/mstar-plan-artifacts/templates/notes.empty.json +0 -5
- package/harness-skills/mstar-plan-artifacts/templates/plans-done.empty.json +0 -3
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
59
59
|
is: () => is,
|
|
60
60
|
isNonNullable: () => isNonNullable,
|
|
61
61
|
isNullable: () => isNullable,
|
|
62
|
-
isPlainObject: () =>
|
|
62
|
+
isPlainObject: () => isPlainObject6,
|
|
63
63
|
makeArray: () => makeArray,
|
|
64
64
|
mapValues: () => mapValues,
|
|
65
65
|
noop: () => noop,
|
|
@@ -82,7 +82,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
82
82
|
function isNonNullable(value) {
|
|
83
83
|
return !isNullable(value);
|
|
84
84
|
}
|
|
85
|
-
function
|
|
85
|
+
function isPlainObject6(data) {
|
|
86
86
|
return data && typeof data === "object" && !Array.isArray(data);
|
|
87
87
|
}
|
|
88
88
|
function filterKeys(object, filter) {
|
|
@@ -1106,8 +1106,8 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
1106
1106
|
});
|
|
1107
1107
|
|
|
1108
1108
|
// src/index.ts
|
|
1109
|
-
import { existsSync as
|
|
1110
|
-
import { join as
|
|
1109
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync14 } from "node:fs";
|
|
1110
|
+
import { join as join23 } from "node:path";
|
|
1111
1111
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1112
1112
|
import {
|
|
1113
1113
|
apply as applySkillLocal,
|
|
@@ -1120,24 +1120,26 @@ import {
|
|
|
1120
1120
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1121
1121
|
import { basename, dirname, join, resolve } from "node:path";
|
|
1122
1122
|
import { fileURLToPath } from "node:url";
|
|
1123
|
-
import {
|
|
1123
|
+
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1124
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
1125
|
+
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
1124
1126
|
import { execFileSync } from "node:child_process";
|
|
1125
|
-
import { basename as basename2, dirname as
|
|
1126
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1127
|
-
import { join as
|
|
1128
|
-
import { dirname as
|
|
1127
|
+
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
1128
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
1129
|
+
import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
|
|
1130
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
|
|
1129
1131
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1130
1132
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1131
1133
|
import { existsSync as existsSync3 } from "node:fs";
|
|
1132
|
-
import { isAbsolute as
|
|
1134
|
+
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
1133
1135
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1134
|
-
import { mkdirSync as
|
|
1135
|
-
import { basename as basename3, dirname as
|
|
1136
|
-
import { existsSync as existsSync4, readdirSync as
|
|
1137
|
-
import { existsSync as
|
|
1138
|
-
import { basename as basename4, isAbsolute as
|
|
1139
|
-
import { existsSync as
|
|
1140
|
-
import { join as
|
|
1136
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1137
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
|
|
1138
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
1139
|
+
import { existsSync as existsSync6, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
1140
|
+
import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
|
|
1141
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1142
|
+
import { join as join13 } from "node:path";
|
|
1141
1143
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
1142
1144
|
function applyEnforcement(gate, opts) {
|
|
1143
1145
|
return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
|
|
@@ -1189,23 +1191,111 @@ function harnessVersionFrom(moduleDir) {
|
|
|
1189
1191
|
function readHarnessVersion() {
|
|
1190
1192
|
return harnessVersionFrom(dirname(fileURLToPath(import.meta.url)));
|
|
1191
1193
|
}
|
|
1194
|
+
var MSTARC_FILE = ".mstarc";
|
|
1195
|
+
var MSTARC_SECTION = "config";
|
|
1196
|
+
var MSTARC_HARNESS_DIR_KEY = "harness_dir";
|
|
1197
|
+
var MSTARC_PLAN_DIR_KEY = "plan_dir";
|
|
1198
|
+
var MSTARC_SDD_DIR_KEY = "sdd_dir";
|
|
1199
|
+
var MSTARC_ITERATION_DIR_KEY = "iteration_dir";
|
|
1200
|
+
var MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir";
|
|
1201
|
+
var MSTARC_SPECS_DIR_KEY = "specs_dir";
|
|
1202
|
+
var MSTARC_WORKFLOW_DIR_KEY = "workflow_dir";
|
|
1203
|
+
var MSTARC_PROJECT_DIR_KEY = "project_dir";
|
|
1204
|
+
var MSTARC_ENFORCEMENT_KEY = "enforcement";
|
|
1205
|
+
var CONFIG_KEYS = {
|
|
1206
|
+
[MSTARC_HARNESS_DIR_KEY]: "harnessDir",
|
|
1207
|
+
[MSTARC_PLAN_DIR_KEY]: "planDir",
|
|
1208
|
+
[MSTARC_SDD_DIR_KEY]: "sddDir",
|
|
1209
|
+
[MSTARC_ITERATION_DIR_KEY]: "iterationDir",
|
|
1210
|
+
[MSTARC_KNOWLEDGE_DIR_KEY]: "knowledgeDir",
|
|
1211
|
+
[MSTARC_SPECS_DIR_KEY]: "specsDir",
|
|
1212
|
+
[MSTARC_WORKFLOW_DIR_KEY]: "workflowDir",
|
|
1213
|
+
[MSTARC_PROJECT_DIR_KEY]: "projectDir",
|
|
1214
|
+
[MSTARC_ENFORCEMENT_KEY]: "enforcement"
|
|
1215
|
+
};
|
|
1216
|
+
function parseMstarc(text) {
|
|
1217
|
+
let section = null;
|
|
1218
|
+
const out = {};
|
|
1219
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
1220
|
+
const line = raw.trim();
|
|
1221
|
+
if (line === "" || line.startsWith("#") || line.startsWith(";"))
|
|
1222
|
+
continue;
|
|
1223
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
1224
|
+
if (header !== null) {
|
|
1225
|
+
section = header[1].trim();
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (section !== MSTARC_SECTION)
|
|
1229
|
+
continue;
|
|
1230
|
+
const eq = line.indexOf("=");
|
|
1231
|
+
if (eq === -1)
|
|
1232
|
+
continue;
|
|
1233
|
+
const field = CONFIG_KEYS[line.slice(0, eq).trim()];
|
|
1234
|
+
if (field === undefined)
|
|
1235
|
+
continue;
|
|
1236
|
+
const value = line.slice(eq + 1).trim();
|
|
1237
|
+
if (value === "")
|
|
1238
|
+
continue;
|
|
1239
|
+
if (field === "enforcement" && value !== "hard" && value !== "soft")
|
|
1240
|
+
continue;
|
|
1241
|
+
out[field] = value;
|
|
1242
|
+
}
|
|
1243
|
+
return out;
|
|
1244
|
+
}
|
|
1245
|
+
function isFile(file) {
|
|
1246
|
+
try {
|
|
1247
|
+
return statSync(file).isFile();
|
|
1248
|
+
} catch {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function findMstarc(startDir, boundary) {
|
|
1253
|
+
let dir = resolve2(startDir);
|
|
1254
|
+
const bound = resolve2(boundary);
|
|
1255
|
+
for (;; ) {
|
|
1256
|
+
if (!isAtOrBelow(dir, bound))
|
|
1257
|
+
return null;
|
|
1258
|
+
const candidate = join2(dir, MSTARC_FILE);
|
|
1259
|
+
if (isFile(candidate))
|
|
1260
|
+
return candidate;
|
|
1261
|
+
if (dir === bound)
|
|
1262
|
+
return null;
|
|
1263
|
+
const parent = dirname2(dir);
|
|
1264
|
+
if (parent === dir)
|
|
1265
|
+
return null;
|
|
1266
|
+
dir = parent;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function loadMstarc(startDir, boundary) {
|
|
1270
|
+
const file = findMstarc(startDir, boundary);
|
|
1271
|
+
if (file === null)
|
|
1272
|
+
return null;
|
|
1273
|
+
return { file, dir: dirname2(file), config: parseMstarc(readFileSync2(file, "utf8")) };
|
|
1274
|
+
}
|
|
1275
|
+
function isAtOrBelow(dir, root) {
|
|
1276
|
+
const rel = relative(root, dir);
|
|
1277
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1278
|
+
}
|
|
1192
1279
|
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
1193
|
-
const start =
|
|
1280
|
+
const start = resolve3(startDir);
|
|
1194
1281
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
1195
1282
|
if (explicit)
|
|
1196
|
-
return
|
|
1197
|
-
const boundary =
|
|
1283
|
+
return resolve3(start, explicit);
|
|
1284
|
+
const boundary = resolve3(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
1285
|
+
const rc = loadMstarc(start, boundary);
|
|
1286
|
+
if (rc !== null && rc.config.harnessDir)
|
|
1287
|
+
return resolve3(rc.dir, rc.config.harnessDir);
|
|
1198
1288
|
let dir = start;
|
|
1199
1289
|
for (;; ) {
|
|
1200
|
-
if (!
|
|
1290
|
+
if (!isAtOrBelow2(dir, boundary))
|
|
1201
1291
|
return null;
|
|
1202
|
-
for (const candidate of [
|
|
1292
|
+
for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
|
|
1203
1293
|
if (isDirectory(candidate))
|
|
1204
1294
|
return candidate;
|
|
1205
1295
|
}
|
|
1206
1296
|
if (dir === boundary)
|
|
1207
1297
|
return null;
|
|
1208
|
-
const parent =
|
|
1298
|
+
const parent = dirname3(dir);
|
|
1209
1299
|
if (parent === dir)
|
|
1210
1300
|
return null;
|
|
1211
1301
|
dir = parent;
|
|
@@ -1223,15 +1313,21 @@ function defaultWorkspaceRoot(startDir) {
|
|
|
1223
1313
|
let boundary = startDir;
|
|
1224
1314
|
for (const segment of cdup.split(/[\\/]/)) {
|
|
1225
1315
|
if (segment && segment !== ".")
|
|
1226
|
-
boundary =
|
|
1316
|
+
boundary = dirname3(boundary);
|
|
1227
1317
|
}
|
|
1228
|
-
return
|
|
1318
|
+
return resolve3(boundary);
|
|
1229
1319
|
} catch {}
|
|
1230
1320
|
return startDir;
|
|
1231
1321
|
}
|
|
1232
|
-
function
|
|
1233
|
-
const rel =
|
|
1234
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
1322
|
+
function isAtOrBelow2(dir, root) {
|
|
1323
|
+
const rel = relative2(root, dir);
|
|
1324
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
1325
|
+
}
|
|
1326
|
+
function mstarcDirOverride(harnessDir, key) {
|
|
1327
|
+
const dir = resolve3(harnessDir);
|
|
1328
|
+
const rc = loadMstarc(dir, dirname3(dir));
|
|
1329
|
+
const declared = rc?.config[key];
|
|
1330
|
+
return declared ? resolve3(rc.dir, declared) : null;
|
|
1235
1331
|
}
|
|
1236
1332
|
function assertSafePathComponent(value, what) {
|
|
1237
1333
|
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
@@ -1240,10 +1336,30 @@ function assertSafePathComponent(value, what) {
|
|
|
1240
1336
|
}
|
|
1241
1337
|
function resolveSddDir(harnessDir, planId) {
|
|
1242
1338
|
assertSafePathComponent(planId, "planId");
|
|
1243
|
-
|
|
1339
|
+
const base = resolve3(harnessDir);
|
|
1340
|
+
const declared = mstarcDirOverride(base, "sddDir");
|
|
1341
|
+
const sddBase = declared !== null ? declared : join3(base, "sdd");
|
|
1342
|
+
return join3(sddBase, planId);
|
|
1244
1343
|
}
|
|
1245
1344
|
function resolveIterationDir(harnessDir) {
|
|
1246
|
-
|
|
1345
|
+
const declared = mstarcDirOverride(harnessDir, "iterationDir");
|
|
1346
|
+
if (declared !== null)
|
|
1347
|
+
return declared;
|
|
1348
|
+
return join3(resolve3(harnessDir), "iterations");
|
|
1349
|
+
}
|
|
1350
|
+
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
1351
|
+
const harness = resolveHarnessDir(startDir, opts);
|
|
1352
|
+
if (harness === null) {
|
|
1353
|
+
throw new Error(`harness dir not found from ${resolve3(startDir)} — cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
1354
|
+
}
|
|
1355
|
+
const declared = mstarcDirOverride(harness, key);
|
|
1356
|
+
return declared !== null ? declared : join3(resolve3(harness), fallback);
|
|
1357
|
+
}
|
|
1358
|
+
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
1359
|
+
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
1360
|
+
}
|
|
1361
|
+
function resolveProjectDir(startDir = process.cwd(), opts = {}) {
|
|
1362
|
+
return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
|
|
1247
1363
|
}
|
|
1248
1364
|
var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
1249
1365
|
# Principle: process stays local; results are shared with the team.
|
|
@@ -1254,6 +1370,8 @@ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
|
1254
1370
|
!.mstar/knowledge/**
|
|
1255
1371
|
!.mstar/specs/
|
|
1256
1372
|
!.mstar/specs/**
|
|
1373
|
+
# .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
|
|
1374
|
+
.mstarc
|
|
1257
1375
|
`;
|
|
1258
1376
|
var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
|
|
1259
1377
|
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
@@ -1270,7 +1388,7 @@ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
|
1270
1388
|
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
1271
1389
|
function isDirectory(dir) {
|
|
1272
1390
|
try {
|
|
1273
|
-
return
|
|
1391
|
+
return statSync2(dir).isDirectory();
|
|
1274
1392
|
} catch {
|
|
1275
1393
|
return false;
|
|
1276
1394
|
}
|
|
@@ -1314,7 +1432,7 @@ function validateExecutionLease(lease) {
|
|
|
1314
1432
|
violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
1315
1433
|
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
1316
1434
|
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
1317
|
-
} else if (!
|
|
1435
|
+
} else if (!isAbsolute3(lease.worktree_path)) {
|
|
1318
1436
|
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
|
|
1319
1437
|
}
|
|
1320
1438
|
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
@@ -1348,13 +1466,11 @@ function validateIntegrationMergeLease(lease) {
|
|
|
1348
1466
|
return { ok: violations.length === 0, violations };
|
|
1349
1467
|
}
|
|
1350
1468
|
function planExecutionLeaseLocations(row) {
|
|
1351
|
-
|
|
1352
|
-
const metadataLease = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.execution_lease : undefined;
|
|
1353
|
-
return { row: row.execution_lease, metadata: metadataLease };
|
|
1469
|
+
return { row: row.execution_lease };
|
|
1354
1470
|
}
|
|
1355
1471
|
function verifyPlanExecutionLease(row, planId) {
|
|
1356
|
-
const { row: rowLease
|
|
1357
|
-
const lease = rowLease
|
|
1472
|
+
const { row: rowLease } = planExecutionLeaseLocations(row);
|
|
1473
|
+
const lease = rowLease;
|
|
1358
1474
|
if (lease === undefined) {
|
|
1359
1475
|
if (row.status === "InProgress") {
|
|
1360
1476
|
return {
|
|
@@ -1367,16 +1483,11 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
1367
1483
|
return {
|
|
1368
1484
|
ok: false,
|
|
1369
1485
|
violations: [
|
|
1370
|
-
violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease
|
|
1486
|
+
violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
1371
1487
|
]
|
|
1372
1488
|
};
|
|
1373
1489
|
}
|
|
1374
1490
|
const violations = [];
|
|
1375
|
-
if (rowLease !== undefined && metadataLease !== undefined) {
|
|
1376
|
-
violations.push(violation("high", "lease.verify.dual-write", "execution_lease present in BOTH plans[].execution_lease (SSOT) and plans[].metadata.execution_lease — the row-level lease wins; delete the metadata copy to remove the dual write"));
|
|
1377
|
-
} else if (rowLease === undefined) {
|
|
1378
|
-
violations.push(violation("high", "lease.verify.non-ssot-location", "execution_lease found only under plans[].metadata.execution_lease — the SSOT location is plans[].execution_lease; the metadata location is a legacy/hand-written read-compat fallback, not equivalent to SSOT success (migrate the lease to the plan row)"));
|
|
1379
|
-
}
|
|
1380
1491
|
violations.push(...validateExecutionLease(lease).violations);
|
|
1381
1492
|
return { ok: violations.length === 0, violations, lease };
|
|
1382
1493
|
}
|
|
@@ -1602,14 +1713,124 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1602
1713
|
}
|
|
1603
1714
|
return { ok: true, violations: [] };
|
|
1604
1715
|
}
|
|
1716
|
+
var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
1717
|
+
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
1718
|
+
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
1719
|
+
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
1720
|
+
function isPlainObject2(value) {
|
|
1721
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1722
|
+
}
|
|
1723
|
+
function violation3(severity, code, message, fix) {
|
|
1724
|
+
return { ok: false, severity, code, message, fix };
|
|
1725
|
+
}
|
|
1726
|
+
function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
|
|
1727
|
+
if (value === undefined) {
|
|
1728
|
+
violations.push(violation3("high", missingCode, `missing required field: ${field}`));
|
|
1729
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1730
|
+
violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
function validateWorkflowSnapshot(doc) {
|
|
1734
|
+
const violations = [];
|
|
1735
|
+
if (!isPlainObject2(doc)) {
|
|
1736
|
+
return {
|
|
1737
|
+
ok: false,
|
|
1738
|
+
violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
if (doc.schema_version === undefined) {
|
|
1742
|
+
violations.push(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
1743
|
+
} else if (doc.schema_version !== 1) {
|
|
1744
|
+
violations.push(violation3("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
|
|
1745
|
+
}
|
|
1746
|
+
if (doc.version !== undefined) {
|
|
1747
|
+
violations.push(violation3("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
|
|
1748
|
+
}
|
|
1749
|
+
validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
1750
|
+
if (doc.type === undefined) {
|
|
1751
|
+
violations.push(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
1752
|
+
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
|
|
1753
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
|
|
1754
|
+
}
|
|
1755
|
+
if (doc.status === undefined) {
|
|
1756
|
+
violations.push(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
1757
|
+
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
|
|
1758
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
|
|
1759
|
+
}
|
|
1760
|
+
validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
1761
|
+
validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
1762
|
+
if (doc.ended_at !== undefined) {
|
|
1763
|
+
validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
1764
|
+
}
|
|
1765
|
+
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
1766
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
1767
|
+
}
|
|
1768
|
+
if (doc.plans === undefined) {
|
|
1769
|
+
violations.push(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
1770
|
+
} else if (!Array.isArray(doc.plans)) {
|
|
1771
|
+
violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
1772
|
+
} else {
|
|
1773
|
+
for (const row of doc.plans) {
|
|
1774
|
+
violations.push(...validatePlanRow(row).violations);
|
|
1775
|
+
if (isPlainObject2(row) && row.execution_lease !== undefined) {
|
|
1776
|
+
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (doc.execution_policy !== undefined) {
|
|
1781
|
+
if (!isPlainObject2(doc.execution_policy)) {
|
|
1782
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (doc.integration_merge_lease !== undefined) {
|
|
1786
|
+
violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
|
|
1787
|
+
}
|
|
1788
|
+
if (doc.branch !== undefined) {
|
|
1789
|
+
if (!isPlainObject2(doc.branch)) {
|
|
1790
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
1791
|
+
} else {
|
|
1792
|
+
for (const key of ["base", "integration", "target"]) {
|
|
1793
|
+
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
1794
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (doc.control_worktree_path !== undefined) {
|
|
1800
|
+
validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
|
|
1801
|
+
}
|
|
1802
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
|
|
1803
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
1804
|
+
}
|
|
1805
|
+
if (doc.compass_ref !== undefined) {
|
|
1806
|
+
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
1807
|
+
}
|
|
1808
|
+
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
1809
|
+
if (terminal) {
|
|
1810
|
+
if (doc.ended_at === undefined) {
|
|
1811
|
+
violations.push(violation3("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
|
|
1812
|
+
}
|
|
1813
|
+
if (Array.isArray(doc.plans)) {
|
|
1814
|
+
for (const row of doc.plans) {
|
|
1815
|
+
if (isPlainObject2(row) && row.execution_lease !== undefined) {
|
|
1816
|
+
violations.push(violation3("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (doc.integration_merge_lease !== undefined) {
|
|
1821
|
+
violations.push(violation3("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return { ok: violations.length === 0, violations };
|
|
1825
|
+
}
|
|
1605
1826
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1606
1827
|
var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
1607
1828
|
var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
1608
1829
|
var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
1609
|
-
function
|
|
1830
|
+
function isPlainObject3(value) {
|
|
1610
1831
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1611
1832
|
}
|
|
1612
|
-
function
|
|
1833
|
+
function violation4(severity, code, message, fix) {
|
|
1613
1834
|
return { ok: false, severity, code, message, fix };
|
|
1614
1835
|
}
|
|
1615
1836
|
function normalizeSeverity(value) {
|
|
@@ -1624,217 +1845,242 @@ function isOpenResidual(entry) {
|
|
|
1624
1845
|
const effective = lifecycle === false || lifecycle === null || lifecycle === undefined ? "open" : lifecycle;
|
|
1625
1846
|
return effective === "open";
|
|
1626
1847
|
}
|
|
1627
|
-
function
|
|
1848
|
+
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
1628
1849
|
if (value === undefined) {
|
|
1629
|
-
violations.push(
|
|
1850
|
+
violations.push(violation4("high", missingCode, `missing required field: ${field}`));
|
|
1630
1851
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1631
|
-
violations.push(
|
|
1852
|
+
violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1632
1853
|
}
|
|
1633
1854
|
}
|
|
1634
1855
|
function validatePlanRow(row) {
|
|
1635
1856
|
const violations = [];
|
|
1636
|
-
if (!
|
|
1637
|
-
return { ok: false, violations: [
|
|
1857
|
+
if (!isPlainObject3(row)) {
|
|
1858
|
+
return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
1638
1859
|
}
|
|
1639
1860
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
1640
1861
|
if (id === undefined && planId === undefined) {
|
|
1641
|
-
violations.push(
|
|
1862
|
+
violations.push(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
1642
1863
|
} else {
|
|
1643
1864
|
if (id !== undefined) {
|
|
1644
|
-
|
|
1865
|
+
validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
1645
1866
|
}
|
|
1646
1867
|
if (planId !== undefined) {
|
|
1647
|
-
|
|
1868
|
+
validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
1648
1869
|
}
|
|
1649
1870
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
1650
|
-
violations.push(
|
|
1871
|
+
violations.push(violation4("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
|
|
1651
1872
|
}
|
|
1652
1873
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1874
|
+
validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
1875
|
+
validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
1655
1876
|
if (status === undefined) {
|
|
1656
|
-
violations.push(
|
|
1877
|
+
violations.push(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
1657
1878
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
1658
|
-
violations.push(
|
|
1879
|
+
violations.push(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
|
|
1659
1880
|
}
|
|
1660
|
-
if (metadata !== undefined && !
|
|
1661
|
-
violations.push(
|
|
1881
|
+
if (metadata !== undefined && !isPlainObject3(metadata)) {
|
|
1882
|
+
violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
1662
1883
|
}
|
|
1663
|
-
if (execution_lease !== undefined && !
|
|
1664
|
-
violations.push(
|
|
1884
|
+
if (execution_lease !== undefined && !isPlainObject3(execution_lease)) {
|
|
1885
|
+
violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
1665
1886
|
}
|
|
1666
1887
|
if (status === "Done" && execution_lease !== undefined) {
|
|
1667
|
-
violations.push(
|
|
1888
|
+
violations.push(violation4("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
|
|
1668
1889
|
}
|
|
1669
1890
|
return { ok: violations.length === 0, violations };
|
|
1670
1891
|
}
|
|
1671
1892
|
function validateResidual(entry) {
|
|
1672
1893
|
const violations = [];
|
|
1673
|
-
if (!
|
|
1674
|
-
return { ok: false, violations: [
|
|
1894
|
+
if (!isPlainObject3(entry)) {
|
|
1895
|
+
return { ok: false, violations: [violation4("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
1675
1896
|
}
|
|
1676
1897
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1898
|
+
validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
|
|
1899
|
+
validateNonEmptyString3(violations, title, "title", "status.residual.missing-title", "status.residual.invalid-title");
|
|
1900
|
+
validateNonEmptyString3(violations, source, "source", "status.residual.missing-source", "status.residual.invalid-source");
|
|
1901
|
+
validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
|
|
1902
|
+
validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
|
|
1682
1903
|
if (severity === undefined) {
|
|
1683
|
-
violations.push(
|
|
1904
|
+
violations.push(violation4("high", "status.residual.missing-severity", "missing required field: severity"));
|
|
1684
1905
|
} else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
|
|
1685
|
-
violations.push(
|
|
1906
|
+
violations.push(violation4("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
|
|
1686
1907
|
} else if (severity === "warning") {
|
|
1687
|
-
violations.push(
|
|
1908
|
+
violations.push(violation4("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
|
|
1688
1909
|
}
|
|
1689
1910
|
if (decision === undefined) {
|
|
1690
|
-
violations.push(
|
|
1911
|
+
violations.push(violation4("high", "status.residual.missing-decision", "missing required field: decision"));
|
|
1691
1912
|
} else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
|
|
1692
|
-
violations.push(
|
|
1913
|
+
violations.push(violation4("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
|
|
1693
1914
|
}
|
|
1694
1915
|
if (target === undefined) {
|
|
1695
|
-
violations.push(
|
|
1916
|
+
violations.push(violation4("high", "status.residual.missing-target", "missing required field: target"));
|
|
1696
1917
|
} else if (typeof target !== "string" && target !== null) {
|
|
1697
|
-
violations.push(
|
|
1918
|
+
violations.push(violation4("medium", "status.residual.invalid-target", "target must be a string or null"));
|
|
1698
1919
|
}
|
|
1699
1920
|
if (tracking === undefined) {
|
|
1700
|
-
violations.push(
|
|
1921
|
+
violations.push(violation4("high", "status.residual.missing-tracking", "missing required field: tracking"));
|
|
1701
1922
|
} else if (typeof tracking !== "string" && tracking !== null) {
|
|
1702
|
-
violations.push(
|
|
1923
|
+
violations.push(violation4("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
|
|
1703
1924
|
}
|
|
1704
1925
|
if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
|
|
1705
|
-
violations.push(
|
|
1926
|
+
violations.push(violation4("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
|
|
1706
1927
|
}
|
|
1707
1928
|
if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE.test(closed_at))) {
|
|
1708
|
-
violations.push(
|
|
1929
|
+
violations.push(violation4("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
|
|
1709
1930
|
}
|
|
1710
1931
|
if (lifecycle !== undefined) {
|
|
1711
1932
|
if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
|
|
1712
|
-
violations.push(
|
|
1933
|
+
violations.push(violation4("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
|
|
1713
1934
|
} else if (lifecycle !== "open") {
|
|
1714
1935
|
if (closed_at === undefined) {
|
|
1715
|
-
violations.push(
|
|
1936
|
+
violations.push(violation4("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
|
|
1716
1937
|
}
|
|
1717
1938
|
if (entry.closure_note === undefined) {
|
|
1718
|
-
violations.push(
|
|
1939
|
+
violations.push(violation4("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
|
|
1719
1940
|
}
|
|
1720
1941
|
}
|
|
1721
1942
|
}
|
|
1722
1943
|
return { ok: violations.length === 0, violations };
|
|
1723
1944
|
}
|
|
1724
|
-
function
|
|
1945
|
+
function isHarnessRelativePath(dir) {
|
|
1946
|
+
if (dir.startsWith("/") || dir.startsWith("\\"))
|
|
1947
|
+
return false;
|
|
1948
|
+
if (/^[A-Za-z]:[\\/]/.test(dir))
|
|
1949
|
+
return false;
|
|
1950
|
+
return !dir.split(/[\\/]+/).includes("..");
|
|
1951
|
+
}
|
|
1952
|
+
function validateWorkflowEntry(entry) {
|
|
1953
|
+
const violations = [];
|
|
1954
|
+
if (!isPlainObject3(entry)) {
|
|
1955
|
+
return {
|
|
1956
|
+
ok: false,
|
|
1957
|
+
violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
1961
|
+
if (entry.type === undefined) {
|
|
1962
|
+
violations.push(violation4("high", "status.workflow.missing-type", "missing required field: type"));
|
|
1963
|
+
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
|
|
1964
|
+
violations.push(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
|
|
1965
|
+
}
|
|
1966
|
+
validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
1967
|
+
if (entry.dir === undefined) {
|
|
1968
|
+
violations.push(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
1969
|
+
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
1970
|
+
violations.push(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
1971
|
+
} else if (!isHarnessRelativePath(entry.dir)) {
|
|
1972
|
+
violations.push(violation4("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
|
|
1973
|
+
}
|
|
1974
|
+
return { ok: violations.length === 0, violations };
|
|
1975
|
+
}
|
|
1976
|
+
function validateStatusV2(docOrPath, opts = {}) {
|
|
1725
1977
|
let doc;
|
|
1978
|
+
let harnessDir = opts.harnessDir;
|
|
1726
1979
|
if (typeof docOrPath === "string") {
|
|
1727
1980
|
try {
|
|
1728
1981
|
doc = readJson(docOrPath);
|
|
1982
|
+
harnessDir = dirname5(resolve5(docOrPath));
|
|
1729
1983
|
} catch (error) {
|
|
1730
1984
|
return {
|
|
1731
1985
|
ok: false,
|
|
1732
|
-
violations: [
|
|
1986
|
+
violations: [violation4("high", "status.invalid-json", error.message)]
|
|
1733
1987
|
};
|
|
1734
1988
|
}
|
|
1735
1989
|
} else {
|
|
1736
1990
|
doc = docOrPath;
|
|
1737
1991
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
if (version === undefined) {
|
|
1741
|
-
violations.push(violation3("high", "status.missing-version", "missing required field: version"));
|
|
1742
|
-
} else if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
1743
|
-
violations.push(violation3("high", "status.invalid-version", "version must be an integer"));
|
|
1744
|
-
} else if (version !== 1) {
|
|
1745
|
-
violations.push(violation3("medium", "status.unsupported-version", `unsupported status.json schema version ${version} — expected 1`));
|
|
1746
|
-
}
|
|
1747
|
-
if (updated_at === undefined) {
|
|
1748
|
-
violations.push(violation3("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
1749
|
-
} else if (typeof updated_at !== "string" || !DATE_RE.test(updated_at)) {
|
|
1750
|
-
violations.push(violation3("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
1751
|
-
}
|
|
1752
|
-
if (plans === undefined) {
|
|
1753
|
-
violations.push(violation3("high", "status.missing-plans", "missing required field: plans"));
|
|
1754
|
-
} else if (!Array.isArray(plans)) {
|
|
1755
|
-
violations.push(violation3("high", "status.invalid-plans", "plans must be an array"));
|
|
1756
|
-
} else {
|
|
1757
|
-
for (const row of plans) {
|
|
1758
|
-
violations.push(...validatePlanRow(row).violations);
|
|
1759
|
-
}
|
|
1992
|
+
if (!isPlainObject3(doc)) {
|
|
1993
|
+
return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
|
|
1760
1994
|
}
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
violations.push(violation3("high", "status.residual.invalid-list", `residual_findings["${planId}"] must be an array`));
|
|
1769
|
-
} else if (list.length === 0) {
|
|
1770
|
-
violations.push(violation3("low", "status.residual.empty-key", `residual_findings["${planId}"] is empty — delete the key (no "plan-id": [])`));
|
|
1771
|
-
} else {
|
|
1772
|
-
for (const entry of list) {
|
|
1773
|
-
violations.push(...validateResidual(entry).violations);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1995
|
+
if (doc.version !== 2) {
|
|
1996
|
+
return {
|
|
1997
|
+
ok: false,
|
|
1998
|
+
violations: [
|
|
1999
|
+
violation4("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
|
|
2000
|
+
]
|
|
2001
|
+
};
|
|
1777
2002
|
}
|
|
1778
|
-
if (
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2003
|
+
if (Array.isArray(doc.plans)) {
|
|
2004
|
+
return {
|
|
2005
|
+
ok: false,
|
|
2006
|
+
violations: [
|
|
2007
|
+
violation4("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
2008
|
+
]
|
|
2009
|
+
};
|
|
1784
2010
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
continue;
|
|
1793
|
-
const rowId = row.id ?? row.plan_id;
|
|
1794
|
-
if (rowId !== planId)
|
|
1795
|
-
continue;
|
|
1796
|
-
if (!isPlainObject2(row.metadata))
|
|
1797
|
-
return;
|
|
1798
|
-
const mode = row.metadata.findings_cleanup;
|
|
1799
|
-
if (mode === "zero-residual" || mode === "allow-residual")
|
|
1800
|
-
return mode;
|
|
1801
|
-
return;
|
|
2011
|
+
if (doc.residual_findings !== undefined) {
|
|
2012
|
+
return {
|
|
2013
|
+
ok: false,
|
|
2014
|
+
violations: [
|
|
2015
|
+
violation4("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
2016
|
+
]
|
|
2017
|
+
};
|
|
1802
2018
|
}
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
function openResidualsOf(doc, planId) {
|
|
1806
|
-
if (!isPlainObject2(doc.residual_findings))
|
|
1807
|
-
return [];
|
|
1808
|
-
const list = doc.residual_findings[planId];
|
|
1809
|
-
if (!Array.isArray(list))
|
|
1810
|
-
return [];
|
|
1811
|
-
return list.filter((entry) => isPlainObject2(entry) && isOpenResidual(entry));
|
|
1812
|
-
}
|
|
1813
|
-
function findingsCleanupGate(doc, planId, opts) {
|
|
1814
|
-
const mode = opts?.mode ?? planFindingsCleanup(doc, planId) ?? "allow-residual";
|
|
1815
2019
|
const violations = [];
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2020
|
+
if (doc.updated_at === undefined) {
|
|
2021
|
+
violations.push(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
2022
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
|
|
2023
|
+
violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
2024
|
+
}
|
|
2025
|
+
if (doc.workflows === undefined) {
|
|
2026
|
+
violations.push(violation4("high", "status.missing-workflows", "missing required field: workflows"));
|
|
2027
|
+
} else if (!Array.isArray(doc.workflows)) {
|
|
2028
|
+
violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
|
|
2029
|
+
} else {
|
|
2030
|
+
const seen = new Set;
|
|
2031
|
+
for (const entry of doc.workflows) {
|
|
2032
|
+
violations.push(...validateWorkflowEntry(entry).violations);
|
|
2033
|
+
if (isPlainObject3(entry) && typeof entry.id === "string") {
|
|
2034
|
+
if (seen.has(entry.id)) {
|
|
2035
|
+
violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
1828
2036
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
2037
|
+
seen.add(entry.id);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
2042
|
+
let realHarnessDir = null;
|
|
2043
|
+
try {
|
|
2044
|
+
realHarnessDir = realpathSync(harnessDir);
|
|
2045
|
+
} catch {}
|
|
2046
|
+
for (const entry of doc.workflows) {
|
|
2047
|
+
if (!isPlainObject3(entry) || typeof entry.dir !== "string")
|
|
2048
|
+
continue;
|
|
2049
|
+
const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
2050
|
+
const snapshotPath = join6(harnessDir, relSnapshot);
|
|
2051
|
+
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
2052
|
+
let physical;
|
|
2053
|
+
try {
|
|
2054
|
+
physical = realpathSync(snapshotPath);
|
|
2055
|
+
} catch {
|
|
2056
|
+
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`));
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
2060
|
+
violations.push(violation4("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
let snapshot;
|
|
2064
|
+
try {
|
|
2065
|
+
snapshot = readJson(snapshotPath);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
violations.push(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
2071
|
+
violations.push(violation4("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
|
|
2072
|
+
}
|
|
2073
|
+
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
2074
|
+
violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
|
|
2075
|
+
}
|
|
2076
|
+
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
2077
|
+
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — the root entry mirrors the snapshot; align them`));
|
|
1831
2078
|
}
|
|
1832
|
-
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
1833
|
-
violations.push(violation3("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
1834
2079
|
}
|
|
1835
2080
|
}
|
|
1836
2081
|
return { ok: violations.length === 0, violations };
|
|
1837
2082
|
}
|
|
2083
|
+
var validateStatus = validateStatusV2;
|
|
1838
2084
|
function resolveCompassEnforcement(harnessDir) {
|
|
1839
2085
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
1840
2086
|
if (!existsSync2(iterationsDir))
|
|
@@ -1848,12 +2094,12 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1848
2094
|
for (const entry of entries) {
|
|
1849
2095
|
if (!entry.isDirectory())
|
|
1850
2096
|
continue;
|
|
1851
|
-
const compassPath =
|
|
2097
|
+
const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
|
|
1852
2098
|
if (!existsSync2(compassPath))
|
|
1853
2099
|
continue;
|
|
1854
2100
|
let content;
|
|
1855
2101
|
try {
|
|
1856
|
-
content =
|
|
2102
|
+
content = readFileSync4(compassPath, "utf8");
|
|
1857
2103
|
} catch {
|
|
1858
2104
|
continue;
|
|
1859
2105
|
}
|
|
@@ -1867,6 +2113,22 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1867
2113
|
}
|
|
1868
2114
|
return { hard: false, source: "none" };
|
|
1869
2115
|
}
|
|
2116
|
+
function resolveMstarcEnforcement(harnessDir) {
|
|
2117
|
+
const dir = resolve5(harnessDir);
|
|
2118
|
+
const rc = loadMstarc(dir, dirname5(dir));
|
|
2119
|
+
const value = rc?.config.enforcement;
|
|
2120
|
+
if (value === "hard")
|
|
2121
|
+
return { hard: true, source: "mstarc" };
|
|
2122
|
+
if (value === "soft")
|
|
2123
|
+
return { hard: false, source: "mstarc" };
|
|
2124
|
+
return { hard: false, source: "none" };
|
|
2125
|
+
}
|
|
2126
|
+
function resolveRepoEnforcement(harnessDir) {
|
|
2127
|
+
const rc = resolveMstarcEnforcement(harnessDir);
|
|
2128
|
+
if (rc.source !== "none")
|
|
2129
|
+
return rc;
|
|
2130
|
+
return resolveCompassEnforcement(harnessDir);
|
|
2131
|
+
}
|
|
1870
2132
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
1871
2133
|
function probeTimeoutMs() {
|
|
1872
2134
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -1875,7 +2137,7 @@ function probeTimeoutMs() {
|
|
|
1875
2137
|
const parsed = Number(raw);
|
|
1876
2138
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
1877
2139
|
}
|
|
1878
|
-
function
|
|
2140
|
+
function violation5(severity, code, message, fix) {
|
|
1879
2141
|
return { ok: false, severity, code, message, fix };
|
|
1880
2142
|
}
|
|
1881
2143
|
function gate(violations) {
|
|
@@ -1909,25 +2171,25 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
1909
2171
|
const violations = [];
|
|
1910
2172
|
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
1911
2173
|
if (controlWorktreePath.trim() === "") {
|
|
1912
|
-
violations.push(
|
|
2174
|
+
violations.push(violation5("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded — the L1 control worktree (integration-branch checkout) must be recorded in status.json before writable dispatch", "record the control worktree path in status.json metadata.control_worktree_path"));
|
|
1913
2175
|
}
|
|
1914
2176
|
if (leaseWorktreePath.trim() === "") {
|
|
1915
|
-
violations.push(
|
|
2177
|
+
violations.push(violation5("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" — no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
|
|
1916
2178
|
}
|
|
1917
2179
|
if (leaseWorkingBranch.trim() === "") {
|
|
1918
|
-
violations.push(
|
|
2180
|
+
violations.push(violation5("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
1919
2181
|
}
|
|
1920
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== "" &&
|
|
1921
|
-
violations.push(
|
|
2182
|
+
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
2183
|
+
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"));
|
|
1922
2184
|
}
|
|
1923
2185
|
if (leaseWorktreePath !== "" && !existsSync3(leaseWorktreePath)) {
|
|
1924
|
-
violations.push(
|
|
2186
|
+
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>`));
|
|
1925
2187
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
1926
2188
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
1927
2189
|
if ("error" in probe) {
|
|
1928
|
-
violations.push(
|
|
2190
|
+
violations.push(violation5("high", "worktree.l1.branch-probe-failed", `cannot probe branch at "${leaseWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the lease working branch (not detached)"));
|
|
1929
2191
|
} else if (probe.branch !== leaseWorkingBranch) {
|
|
1930
|
-
violations.push(
|
|
2192
|
+
violations.push(violation5("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
|
|
1931
2193
|
}
|
|
1932
2194
|
}
|
|
1933
2195
|
return gate(violations);
|
|
@@ -1937,32 +2199,32 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
1937
2199
|
const tracks = input.tracks ?? [];
|
|
1938
2200
|
const seenPaths = new Set;
|
|
1939
2201
|
if (tracks.length < 1) {
|
|
1940
|
-
violations.push(
|
|
2202
|
+
violations.push(violation5("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
|
|
1941
2203
|
}
|
|
1942
2204
|
tracks.forEach((track, index) => {
|
|
1943
2205
|
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
1944
|
-
violations.push(
|
|
2206
|
+
violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
1945
2207
|
return;
|
|
1946
2208
|
}
|
|
1947
|
-
if (!
|
|
1948
|
-
violations.push(
|
|
2209
|
+
if (!isAbsolute4(track.worktreePath)) {
|
|
2210
|
+
violations.push(violation5("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
|
|
1949
2211
|
return;
|
|
1950
2212
|
}
|
|
1951
|
-
const normalized =
|
|
2213
|
+
const normalized = resolve6(track.worktreePath);
|
|
1952
2214
|
if (seenPaths.has(normalized)) {
|
|
1953
|
-
violations.push(
|
|
2215
|
+
violations.push(violation5("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
|
|
1954
2216
|
return;
|
|
1955
2217
|
}
|
|
1956
2218
|
seenPaths.add(normalized);
|
|
1957
2219
|
if (!existsSync3(track.worktreePath)) {
|
|
1958
|
-
violations.push(
|
|
2220
|
+
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}`));
|
|
1959
2221
|
return;
|
|
1960
2222
|
}
|
|
1961
2223
|
const probe = probeBranch(track.worktreePath, opts);
|
|
1962
2224
|
if ("error" in probe) {
|
|
1963
|
-
violations.push(
|
|
2225
|
+
violations.push(violation5("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
|
|
1964
2226
|
} else if (probe.branch !== track.workingBranch) {
|
|
1965
|
-
violations.push(
|
|
2227
|
+
violations.push(violation5("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
|
|
1966
2228
|
}
|
|
1967
2229
|
});
|
|
1968
2230
|
return gate(violations);
|
|
@@ -1977,14 +2239,14 @@ class SddScriptError extends Error {
|
|
|
1977
2239
|
}
|
|
1978
2240
|
function isDirectory2(dir) {
|
|
1979
2241
|
try {
|
|
1980
|
-
return
|
|
2242
|
+
return statSync4(dir).isDirectory();
|
|
1981
2243
|
} catch {
|
|
1982
2244
|
return false;
|
|
1983
2245
|
}
|
|
1984
2246
|
}
|
|
1985
|
-
function
|
|
2247
|
+
function isFile2(file) {
|
|
1986
2248
|
try {
|
|
1987
|
-
return
|
|
2249
|
+
return statSync4(file).isFile();
|
|
1988
2250
|
} catch {
|
|
1989
2251
|
return false;
|
|
1990
2252
|
}
|
|
@@ -2003,25 +2265,48 @@ function gitOut(cwd, args) {
|
|
|
2003
2265
|
}
|
|
2004
2266
|
}
|
|
2005
2267
|
function probeHarnessWithStatus(root) {
|
|
2006
|
-
if (
|
|
2007
|
-
return
|
|
2008
|
-
if (
|
|
2009
|
-
return
|
|
2268
|
+
if (isFile2(join7(root, ".mstar", "status.json")))
|
|
2269
|
+
return join7(root, ".mstar");
|
|
2270
|
+
if (isFile2(join7(root, ".agents", "status.json")))
|
|
2271
|
+
return join7(root, ".agents");
|
|
2272
|
+
if (hasWorkflowSnapshot(join7(root, ".mstar")))
|
|
2273
|
+
return join7(root, ".mstar");
|
|
2274
|
+
if (hasWorkflowSnapshot(join7(root, ".agents")))
|
|
2275
|
+
return join7(root, ".agents");
|
|
2010
2276
|
return null;
|
|
2011
2277
|
}
|
|
2278
|
+
function hasWorkflowSnapshot(harnessDir) {
|
|
2279
|
+
let workflowsDir;
|
|
2280
|
+
try {
|
|
2281
|
+
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2282
|
+
} catch {
|
|
2283
|
+
workflowsDir = join7(harnessDir, "workflows");
|
|
2284
|
+
}
|
|
2285
|
+
if (!isDirectory2(workflowsDir))
|
|
2286
|
+
return false;
|
|
2287
|
+
try {
|
|
2288
|
+
for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
|
|
2289
|
+
if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
return false;
|
|
2296
|
+
}
|
|
2012
2297
|
function isLinkedWorktree(root) {
|
|
2013
2298
|
const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
|
|
2014
2299
|
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2015
2300
|
if (gitDirRaw === null || commonRaw === null)
|
|
2016
2301
|
return false;
|
|
2017
|
-
const gitDir =
|
|
2018
|
-
const common =
|
|
2302
|
+
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
|
|
2303
|
+
const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
|
|
2019
2304
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2020
2305
|
return true;
|
|
2021
2306
|
try {
|
|
2022
|
-
const gdParent =
|
|
2023
|
-
const cmAbs =
|
|
2024
|
-
return
|
|
2307
|
+
const gdParent = realpathSync2(dirname6(gitDir));
|
|
2308
|
+
const cmAbs = realpathSync2(common);
|
|
2309
|
+
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2025
2310
|
} catch {
|
|
2026
2311
|
return false;
|
|
2027
2312
|
}
|
|
@@ -2038,10 +2323,10 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2038
2323
|
if (!isDirectory2(controlRoot)) {
|
|
2039
2324
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2040
2325
|
}
|
|
2041
|
-
root =
|
|
2326
|
+
root = realpathSync2(controlRoot);
|
|
2042
2327
|
} else {
|
|
2043
2328
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2044
|
-
root =
|
|
2329
|
+
root = realpathSync2(topLevel ?? cwd);
|
|
2045
2330
|
}
|
|
2046
2331
|
if (!controlRoot && isLinkedWorktree(root)) {
|
|
2047
2332
|
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
@@ -2052,24 +2337,30 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2052
2337
|
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2053
2338
|
let harnessDir;
|
|
2054
2339
|
if (harnessOverride) {
|
|
2055
|
-
harnessDir =
|
|
2340
|
+
harnessDir = resolve7(root, harnessOverride);
|
|
2056
2341
|
} else {
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
harnessDir = join5(root, ".mstar");
|
|
2062
|
-
} else if (isDirectory2(join5(root, ".agents"))) {
|
|
2063
|
-
harnessDir = join5(root, ".agents");
|
|
2342
|
+
const rc = findMstarc(root, root);
|
|
2343
|
+
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
|
|
2344
|
+
if (rcHarnessDir) {
|
|
2345
|
+
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2064
2346
|
} else {
|
|
2065
|
-
|
|
2347
|
+
const probed = probeHarnessWithStatus(root);
|
|
2348
|
+
if (probed) {
|
|
2349
|
+
harnessDir = probed;
|
|
2350
|
+
} else if (isDirectory2(join7(root, ".mstar"))) {
|
|
2351
|
+
harnessDir = join7(root, ".mstar");
|
|
2352
|
+
} else if (isDirectory2(join7(root, ".agents"))) {
|
|
2353
|
+
harnessDir = join7(root, ".agents");
|
|
2354
|
+
} else {
|
|
2355
|
+
harnessDir = join7(root, ".mstar");
|
|
2356
|
+
}
|
|
2066
2357
|
}
|
|
2067
2358
|
}
|
|
2068
2359
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2069
|
-
|
|
2070
|
-
writeFileSync3(
|
|
2360
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2361
|
+
writeFileSync3(join7(sddDir, ".gitignore"), `*
|
|
2071
2362
|
`);
|
|
2072
|
-
return
|
|
2363
|
+
return realpathSync2(sddDir);
|
|
2073
2364
|
}
|
|
2074
2365
|
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
2075
2366
|
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
@@ -2077,7 +2368,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2077
2368
|
}
|
|
2078
2369
|
let content;
|
|
2079
2370
|
try {
|
|
2080
|
-
content =
|
|
2371
|
+
content = readFileSync5(planFile, "utf8");
|
|
2081
2372
|
} catch {
|
|
2082
2373
|
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2083
2374
|
}
|
|
@@ -2089,8 +2380,8 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2089
2380
|
if (!sddDir) {
|
|
2090
2381
|
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2091
2382
|
}
|
|
2092
|
-
|
|
2093
|
-
out =
|
|
2383
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2384
|
+
out = join7(sddDir, `task-${taskN}-brief.md`);
|
|
2094
2385
|
}
|
|
2095
2386
|
const records = content.endsWith(`
|
|
2096
2387
|
`) ? content.split(`
|
|
@@ -2192,18 +2483,18 @@ function validateCompassShape(doc) {
|
|
|
2192
2483
|
}
|
|
2193
2484
|
};
|
|
2194
2485
|
}
|
|
2195
|
-
function
|
|
2486
|
+
function violation6(severity, code, message, fix) {
|
|
2196
2487
|
return { ok: false, severity, code, message, fix };
|
|
2197
2488
|
}
|
|
2198
|
-
function
|
|
2489
|
+
function isPlainObject4(value) {
|
|
2199
2490
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2200
2491
|
}
|
|
2201
2492
|
function validateCompassFrontmatter(doc) {
|
|
2202
|
-
if (!
|
|
2493
|
+
if (!isPlainObject4(doc)) {
|
|
2203
2494
|
return {
|
|
2204
2495
|
ok: false,
|
|
2205
2496
|
violations: [
|
|
2206
|
-
|
|
2497
|
+
violation6("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
|
|
2207
2498
|
]
|
|
2208
2499
|
};
|
|
2209
2500
|
}
|
|
@@ -2213,17 +2504,17 @@ function validateCompassFrontmatter(doc) {
|
|
|
2213
2504
|
ok: false,
|
|
2214
2505
|
violations: parsed.issues.map((issue) => {
|
|
2215
2506
|
const field = issue.path.join(".") || "(root)";
|
|
2216
|
-
return
|
|
2507
|
+
return violation6("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
|
|
2217
2508
|
})
|
|
2218
2509
|
};
|
|
2219
2510
|
}
|
|
2220
2511
|
const violations = [];
|
|
2221
2512
|
const { status, end_date } = parsed.data;
|
|
2222
2513
|
if (status === "completed" && end_date === undefined) {
|
|
2223
|
-
violations.push(
|
|
2514
|
+
violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
|
|
2224
2515
|
}
|
|
2225
2516
|
if (status !== "completed" && end_date !== undefined) {
|
|
2226
|
-
violations.push(
|
|
2517
|
+
violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
|
|
2227
2518
|
}
|
|
2228
2519
|
return { ok: violations.length === 0, violations };
|
|
2229
2520
|
}
|
|
@@ -2232,11 +2523,11 @@ function registeredPlanIds(compassDoc) {
|
|
|
2232
2523
|
return [];
|
|
2233
2524
|
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
2234
2525
|
}
|
|
2235
|
-
function findPlanRow(
|
|
2236
|
-
if (!Array.isArray(
|
|
2526
|
+
function findPlanRow(snapshotDoc, planId) {
|
|
2527
|
+
if (!Array.isArray(snapshotDoc.plans))
|
|
2237
2528
|
return null;
|
|
2238
|
-
for (const row of
|
|
2239
|
-
if (!
|
|
2529
|
+
for (const row of snapshotDoc.plans) {
|
|
2530
|
+
if (!isPlainObject4(row))
|
|
2240
2531
|
continue;
|
|
2241
2532
|
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
2242
2533
|
if (rowId === planId)
|
|
@@ -2244,65 +2535,35 @@ function findPlanRow(statusDoc, planId) {
|
|
|
2244
2535
|
}
|
|
2245
2536
|
return null;
|
|
2246
2537
|
}
|
|
2247
|
-
function entryPlansAllDone(
|
|
2538
|
+
function entryPlansAllDone(snapshotDoc, registered) {
|
|
2248
2539
|
const violations = [];
|
|
2249
2540
|
if (registered.length === 0) {
|
|
2250
|
-
violations.push(
|
|
2541
|
+
violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
|
|
2251
2542
|
return violations;
|
|
2252
2543
|
}
|
|
2253
2544
|
for (const planId of registered) {
|
|
2254
|
-
const row = findPlanRow(
|
|
2545
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
2255
2546
|
if (row === null) {
|
|
2256
|
-
violations.push(
|
|
2547
|
+
violations.push(violation6("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
|
|
2257
2548
|
continue;
|
|
2258
2549
|
}
|
|
2259
2550
|
if (row.status !== PLAN_STATUS_DONE) {
|
|
2260
|
-
violations.push(
|
|
2551
|
+
violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
|
|
2261
2552
|
}
|
|
2262
2553
|
}
|
|
2263
2554
|
return violations;
|
|
2264
2555
|
}
|
|
2265
|
-
function entryResidualsOpen(statusDoc, planId) {
|
|
2266
|
-
const violations = [];
|
|
2267
|
-
const residualRoot = statusDoc.residual_findings;
|
|
2268
|
-
if (residualRoot === undefined || residualRoot === null)
|
|
2269
|
-
return violations;
|
|
2270
|
-
if (!isPlainObject3(residualRoot)) {
|
|
2271
|
-
violations.push(violation5("medium", "RESIDUAL_MALFORMED", "status.json residual_findings must be a plan-id → entries object (mstar-iteration §3.1 entry item 2)"));
|
|
2272
|
-
return violations;
|
|
2273
|
-
}
|
|
2274
|
-
const entries = residualRoot[planId];
|
|
2275
|
-
if (entries === undefined)
|
|
2276
|
-
return violations;
|
|
2277
|
-
if (!Array.isArray(entries)) {
|
|
2278
|
-
violations.push(violation5("medium", "RESIDUAL_MALFORMED", `status.json residual_findings['${planId}'] must be an array of residual entries (mstar-iteration §3.1 entry item 2)`));
|
|
2279
|
-
return violations;
|
|
2280
|
-
}
|
|
2281
|
-
const openIds = [];
|
|
2282
|
-
for (const entry of entries) {
|
|
2283
|
-
if (!isPlainObject3(entry) || !isOpenResidual(entry))
|
|
2284
|
-
continue;
|
|
2285
|
-
const isBlockerDefer = entry.decision === "defer" && typeof entry.target === "string" && entry.target.trim() !== "";
|
|
2286
|
-
if (isBlockerDefer)
|
|
2287
|
-
continue;
|
|
2288
|
-
openIds.push(typeof entry.id === "string" ? entry.id : "<unnamed>");
|
|
2289
|
-
}
|
|
2290
|
-
if (openIds.length > 0) {
|
|
2291
|
-
violations.push(violation5("high", "OPEN_RESIDUALS", `Plan '${planId}' has ${openIds.length} open residual finding(s) not exempted as blocker-defers (${openIds.join(", ")}) — residuals must be closed/archived before iteration-close; only zero-residual blocker-defers (decision: defer + target) may stay open (mstar-iteration §3.1 entry item 2)`, "Close or archive the open residuals, or convert them into blocker-defers (decision: defer + non-empty target) per mstar-plan-artifacts Findings cleanup modes"));
|
|
2292
|
-
}
|
|
2293
|
-
return violations;
|
|
2294
|
-
}
|
|
2295
2556
|
function entryFrontmatterComplete(compassDoc) {
|
|
2296
2557
|
return validateCompassFrontmatter(compassDoc).violations;
|
|
2297
2558
|
}
|
|
2298
2559
|
function exitFrontmatterClosed(compassDoc) {
|
|
2299
2560
|
const violations = [];
|
|
2300
2561
|
if (compassDoc.status !== "completed") {
|
|
2301
|
-
violations.push(
|
|
2562
|
+
violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
|
|
2302
2563
|
}
|
|
2303
2564
|
const endDate = compassDoc.end_date;
|
|
2304
2565
|
if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
|
|
2305
|
-
violations.push(
|
|
2566
|
+
violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
|
|
2306
2567
|
}
|
|
2307
2568
|
return violations;
|
|
2308
2569
|
}
|
|
@@ -2310,9 +2571,9 @@ function exitBranchCheck(opts) {
|
|
|
2310
2571
|
const violations = [];
|
|
2311
2572
|
const { currentBranch, specIntegrationBranch } = opts;
|
|
2312
2573
|
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
2313
|
-
violations.push(
|
|
2574
|
+
violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
|
|
2314
2575
|
} else if (currentBranch !== specIntegrationBranch) {
|
|
2315
|
-
violations.push(
|
|
2576
|
+
violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
|
|
2316
2577
|
}
|
|
2317
2578
|
return violations;
|
|
2318
2579
|
}
|
|
@@ -2321,17 +2582,16 @@ function exitPrBaseCheck(compassDoc, opts) {
|
|
|
2321
2582
|
const target = compassDoc.target_branch;
|
|
2322
2583
|
const { prBaseBranch } = opts;
|
|
2323
2584
|
if (prBaseBranch === undefined) {
|
|
2324
|
-
violations.push(
|
|
2585
|
+
violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
|
|
2325
2586
|
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
2326
|
-
violations.push(
|
|
2587
|
+
violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
|
|
2327
2588
|
}
|
|
2328
2589
|
return violations;
|
|
2329
2590
|
}
|
|
2330
|
-
function evaluatePhaseGate(
|
|
2591
|
+
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
2331
2592
|
const registered = registeredPlanIds(compassDoc);
|
|
2332
2593
|
const entryViolations = [
|
|
2333
|
-
...entryPlansAllDone(
|
|
2334
|
-
...registered.flatMap((planId) => entryResidualsOpen(statusDoc, planId)),
|
|
2594
|
+
...entryPlansAllDone(snapshotDoc, registered),
|
|
2335
2595
|
...entryFrontmatterComplete(compassDoc)
|
|
2336
2596
|
];
|
|
2337
2597
|
const exitViolations = [
|
|
@@ -2340,7 +2600,7 @@ function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
|
|
|
2340
2600
|
...exitPrBaseCheck(compassDoc, opts)
|
|
2341
2601
|
];
|
|
2342
2602
|
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
2343
|
-
const row = findPlanRow(
|
|
2603
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
2344
2604
|
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
2345
2605
|
});
|
|
2346
2606
|
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
@@ -2363,7 +2623,9 @@ function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
|
|
|
2363
2623
|
};
|
|
2364
2624
|
}
|
|
2365
2625
|
function parseCompassFrontmatter(filePath) {
|
|
2366
|
-
|
|
2626
|
+
return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
|
|
2627
|
+
}
|
|
2628
|
+
function parseCompassFrontmatterText(content, filePath) {
|
|
2367
2629
|
const lines = content.split(/\r?\n/);
|
|
2368
2630
|
if (lines[0]?.trim() !== "---") {
|
|
2369
2631
|
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
@@ -2424,7 +2686,103 @@ function parseFlowArray(raw, filePath) {
|
|
|
2424
2686
|
}
|
|
2425
2687
|
return items;
|
|
2426
2688
|
}
|
|
2427
|
-
|
|
2689
|
+
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
2690
|
+
var PROJECT_REGISTER_FILE = "residuals.json";
|
|
2691
|
+
var _DEFAULT_PROJECT = "_default";
|
|
2692
|
+
var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2693
|
+
function isPlainObject5(value) {
|
|
2694
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2695
|
+
}
|
|
2696
|
+
function violation7(severity, code, message, fix) {
|
|
2697
|
+
return { ok: false, severity, code, message, fix };
|
|
2698
|
+
}
|
|
2699
|
+
function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
|
|
2700
|
+
if (value === undefined) {
|
|
2701
|
+
violations.push(violation7("high", missingCode, `missing required field: ${field}`));
|
|
2702
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
2703
|
+
violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
function validateProjectRegister(doc) {
|
|
2707
|
+
const violations = [];
|
|
2708
|
+
if (!isPlainObject5(doc)) {
|
|
2709
|
+
return {
|
|
2710
|
+
ok: false,
|
|
2711
|
+
violations: [violation7("high", "project.register.invalid", "project register must be an object")]
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
if (doc.entries === undefined) {
|
|
2715
|
+
violations.push(violation7("high", "project.register.missing-entries", "missing required field: entries"));
|
|
2716
|
+
} else if (!isPlainObject5(doc.entries)) {
|
|
2717
|
+
violations.push(violation7("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
|
|
2718
|
+
} else {
|
|
2719
|
+
for (const [key, entries] of Object.entries(doc.entries)) {
|
|
2720
|
+
if (key.trim() === "") {
|
|
2721
|
+
violations.push(violation7("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
|
|
2722
|
+
}
|
|
2723
|
+
if (!Array.isArray(entries)) {
|
|
2724
|
+
violations.push(violation7("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
for (const entry of entries) {
|
|
2728
|
+
violations.push(...validateResidual(entry).violations);
|
|
2729
|
+
if (!isPlainObject5(entry))
|
|
2730
|
+
continue;
|
|
2731
|
+
validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
|
|
2732
|
+
if (entry.registered_at === undefined) {
|
|
2733
|
+
violations.push(violation7("high", "project.register.missing-registered-at", "missing required field: registered_at"));
|
|
2734
|
+
} else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
|
|
2735
|
+
violations.push(violation7("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
|
|
2736
|
+
}
|
|
2737
|
+
if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
|
|
2738
|
+
violations.push(violation7("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
|
|
2739
|
+
}
|
|
2740
|
+
if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
|
|
2741
|
+
violations.push(violation7("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
return { ok: violations.length === 0, violations };
|
|
2747
|
+
}
|
|
2748
|
+
function findingsCleanupGate(register, planId, opts) {
|
|
2749
|
+
const mode = opts?.mode ?? "allow-residual";
|
|
2750
|
+
const violations = [];
|
|
2751
|
+
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
2752
|
+
if (entries === undefined) {
|
|
2753
|
+
return { ok: true, violations };
|
|
2754
|
+
}
|
|
2755
|
+
if (!Array.isArray(entries)) {
|
|
2756
|
+
violations.push(violation7("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
2757
|
+
return { ok: false, violations };
|
|
2758
|
+
}
|
|
2759
|
+
if (entries.length === 0) {
|
|
2760
|
+
return { ok: true, violations };
|
|
2761
|
+
}
|
|
2762
|
+
for (const entry of entries) {
|
|
2763
|
+
if (!isOpenResidual(entry))
|
|
2764
|
+
continue;
|
|
2765
|
+
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
2766
|
+
const label = `R#${id}`;
|
|
2767
|
+
if (mode === "zero-residual") {
|
|
2768
|
+
if (entry.severity === "nit") {
|
|
2769
|
+
violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
|
|
2770
|
+
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
2771
|
+
violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
2772
|
+
} else if (entry.decision === "defer") {
|
|
2773
|
+
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
2774
|
+
violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
2775
|
+
}
|
|
2776
|
+
} else {
|
|
2777
|
+
violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
|
|
2778
|
+
}
|
|
2779
|
+
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
2780
|
+
violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return { ok: violations.length === 0, violations };
|
|
2784
|
+
}
|
|
2785
|
+
function violation8(severity, code, message, fix) {
|
|
2428
2786
|
return { ok: false, severity, code, message, fix };
|
|
2429
2787
|
}
|
|
2430
2788
|
var RAW_GROUP = "__raw";
|
|
@@ -2537,7 +2895,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
2537
2895
|
const violations = [];
|
|
2538
2896
|
const fm = parseDesignFrontmatter(frontmatterText);
|
|
2539
2897
|
if (fm === null) {
|
|
2540
|
-
violations.push(
|
|
2898
|
+
violations.push(violation8("medium", "design-md.tokens.missing-frontmatter", "no `---` YAML frontmatter block found — DESIGN.md must open with a fenced frontmatter holding the token SSOT (design-md-spec §1.5)", "add a `---` fenced frontmatter with version, name, description, and the colors/typography/spacing/rounded groups"));
|
|
2541
2899
|
return { ok: false, violations };
|
|
2542
2900
|
}
|
|
2543
2901
|
const groupEntries = (group) => {
|
|
@@ -2552,61 +2910,61 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
2552
2910
|
};
|
|
2553
2911
|
for (const group of ["colors", "typography", "spacing", "rounded"]) {
|
|
2554
2912
|
if (!isMap(fm[group])) {
|
|
2555
|
-
violations.push(
|
|
2913
|
+
violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
|
|
2556
2914
|
} else if (!groupIsMap(group)) {
|
|
2557
|
-
violations.push(
|
|
2915
|
+
violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
|
|
2558
2916
|
} else if (groupEntries(group).length === 0) {
|
|
2559
|
-
violations.push(
|
|
2917
|
+
violations.push(violation8("medium", "design-md.tokens.missing-group", `missing required token group "${group}" — colors/typography/spacing/rounded are required by the frontmatter SSOT (design-md-spec §1.5)`, `add an active \`${group}:\` block with concrete token values`));
|
|
2560
2918
|
}
|
|
2561
2919
|
}
|
|
2562
2920
|
if (!isMap(fm.components) || !groupIsMap("components")) {
|
|
2563
|
-
violations.push(
|
|
2921
|
+
violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "components" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`components\` as a nested map`));
|
|
2564
2922
|
}
|
|
2565
|
-
const placeholder = (group, name, value) => violations.push(
|
|
2923
|
+
const placeholder = (group, name, value) => violations.push(violation8("low", "design-md.tokens.placeholder", `token "${group}.${name}" uses a "[...]" template value — placeholders never count as concrete tokens (completeness-checklist § How to use item 5)`, `replace \`${value}\` with a concrete value`));
|
|
2566
2924
|
for (const [name, value] of groupEntries("colors")) {
|
|
2567
2925
|
if (typeof value !== "string") {
|
|
2568
|
-
violations.push(
|
|
2926
|
+
violations.push(violation8("medium", "design-md.tokens.color-format", `color "${name}" must be a string value (design-md-spec §2.2)`, "quote the color value"));
|
|
2569
2927
|
continue;
|
|
2570
2928
|
}
|
|
2571
2929
|
if (isPlaceholder(value)) {
|
|
2572
2930
|
placeholder("colors", name, value);
|
|
2573
2931
|
} else if (!HEX_RE.test(value) && !OKLCH_RE.test(value)) {
|
|
2574
|
-
violations.push(
|
|
2932
|
+
violations.push(violation8("medium", "design-md.tokens.color-format", `color "${name}" = "${value}" is not a hex (\`#rrggbb\`/\`#rrggbbaa\`) or oklch() value (design-md-spec §2.2)`, "use an sRGB hex value, optionally with a `-p3` oklch() twin"));
|
|
2575
2933
|
}
|
|
2576
2934
|
}
|
|
2577
2935
|
for (const [name, value] of groupEntries("typography")) {
|
|
2578
2936
|
if (!isMap(value)) {
|
|
2579
|
-
violations.push(
|
|
2937
|
+
violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" must be a map of the five properties (design-md-spec §1.5)`, "give it fontFamily/fontSize/fontWeight/lineHeight/letterSpacing"));
|
|
2580
2938
|
continue;
|
|
2581
2939
|
}
|
|
2582
2940
|
const keys = Object.keys(value);
|
|
2583
2941
|
const missing = TYPOGRAPHY_PROPS.filter((p) => !keys.includes(p));
|
|
2584
2942
|
const extra = keys.filter((k) => !TYPOGRAPHY_PROPS.includes(k));
|
|
2585
2943
|
if (missing.length > 0 || extra.length > 0) {
|
|
2586
|
-
violations.push(
|
|
2944
|
+
violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" must have exactly the five properties fontFamily/fontSize/fontWeight/lineHeight/letterSpacing (design-md-spec §1.5)${missing.length > 0 ? ` — missing: ${missing.join(", ")}` : ""}${extra.length > 0 ? ` — extra: ${extra.join(", ")}` : ""}`, "align the token with the five-property shape"));
|
|
2587
2945
|
}
|
|
2588
2946
|
for (const prop of ["fontFamily", "fontSize"]) {
|
|
2589
2947
|
const v = value[prop];
|
|
2590
2948
|
if (typeof v === "string" && isPlaceholder(v))
|
|
2591
2949
|
placeholder("typography", name, v);
|
|
2592
2950
|
else if (typeof v !== "string" || v.trim() === "") {
|
|
2593
|
-
violations.push(
|
|
2951
|
+
violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" has an empty \`${prop}\` (design-md-spec §1.5)`, `fill \`${prop}\` with a concrete value`));
|
|
2594
2952
|
}
|
|
2595
2953
|
}
|
|
2596
2954
|
}
|
|
2597
2955
|
if (groupIsMap("spacing")) {
|
|
2598
2956
|
const spacing = fm.spacing;
|
|
2599
2957
|
if (!Object.prototype.hasOwnProperty.call(spacing, "base")) {
|
|
2600
|
-
violations.push(
|
|
2958
|
+
violations.push(violation8("medium", "design-md.tokens.spacing-base", "spacing must declare the base unit as `base` (design-md-spec §2.4)", "add `base: 4px` (or 8px) to the spacing group"));
|
|
2601
2959
|
}
|
|
2602
2960
|
for (const [name, value] of Object.entries(spacing)) {
|
|
2603
2961
|
if (name !== "base" && name !== RAW_GROUP && !/^\d+$/.test(name)) {
|
|
2604
|
-
violations.push(
|
|
2962
|
+
violations.push(violation8("medium", "design-md.tokens.spacing-key", `spacing key "${name}" must be \`base\` or a numeric multiplier (design-md-spec §1.5)`, "use numeric scale-step keys or `base`"));
|
|
2605
2963
|
}
|
|
2606
2964
|
if (typeof value === "string" && isPlaceholder(value)) {
|
|
2607
2965
|
placeholder("spacing", name, value);
|
|
2608
2966
|
} else if (typeof value !== "string" || !PX_RE.test(value)) {
|
|
2609
|
-
violations.push(
|
|
2967
|
+
violations.push(violation8("medium", "design-md.tokens.spacing-format", `spacing value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `4px`"));
|
|
2610
2968
|
}
|
|
2611
2969
|
}
|
|
2612
2970
|
}
|
|
@@ -2614,12 +2972,12 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
2614
2972
|
if (typeof value === "string" && isPlaceholder(value)) {
|
|
2615
2973
|
placeholder("rounded", name, value);
|
|
2616
2974
|
} else if (typeof value !== "string" || !PX_RE.test(value)) {
|
|
2617
|
-
violations.push(
|
|
2975
|
+
violations.push(violation8("medium", "design-md.tokens.rounded-format", `rounded value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `6px`"));
|
|
2618
2976
|
}
|
|
2619
2977
|
}
|
|
2620
2978
|
for (const [name, value] of groupEntries("components")) {
|
|
2621
2979
|
if (!isMap(value)) {
|
|
2622
|
-
violations.push(
|
|
2980
|
+
violations.push(violation8("medium", "design-md.tokens.components-shape", `component token "${name}" must be a map of properties (design-md-spec §2.8)`, "give it backgroundColor/textColor/typography/rounded/padding/height"));
|
|
2623
2981
|
continue;
|
|
2624
2982
|
}
|
|
2625
2983
|
for (const [prop, v] of Object.entries(value)) {
|
|
@@ -2635,7 +2993,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
2635
2993
|
const [, refGroup, refKey] = ref;
|
|
2636
2994
|
const resolves = REF_GROUPS.includes(refGroup) && groupIsMap(refGroup) && Object.prototype.hasOwnProperty.call(fm[refGroup], refKey);
|
|
2637
2995
|
if (!resolves) {
|
|
2638
|
-
violations.push(
|
|
2996
|
+
violations.push(violation8("medium", "design-md.tokens.ref-unresolved", `component "${name}" references "${v}" which does not resolve to an active token in this frontmatter (design-md-spec §6 — {path} refs MUST trace back to a key)`, `add the referenced token or use a literal value`));
|
|
2639
2997
|
}
|
|
2640
2998
|
}
|
|
2641
2999
|
}
|
|
@@ -2647,7 +3005,7 @@ function assertLightDarkParity(lightFm, darkFm) {
|
|
|
2647
3005
|
const light = parseDesignFrontmatter(lightFm);
|
|
2648
3006
|
const dark = parseDesignFrontmatter(darkFm);
|
|
2649
3007
|
if (light === null || dark === null) {
|
|
2650
|
-
violations.push(
|
|
3008
|
+
violations.push(violation8("medium", "design-md.parity.missing-frontmatter", `light/dark parity needs a YAML frontmatter in both files — ${light === null ? "DESIGN.md" : "DESIGN.dark.md"} has none (design-md-spec §4 rules 1–2)`, "add the fenced frontmatter to both theme files"));
|
|
2651
3009
|
return { ok: false, violations };
|
|
2652
3010
|
}
|
|
2653
3011
|
const activeKeys = (fm) => {
|
|
@@ -2665,12 +3023,12 @@ function assertLightDarkParity(lightFm, darkFm) {
|
|
|
2665
3023
|
const darkKeys = activeKeys(dark);
|
|
2666
3024
|
for (const key of lightKeys) {
|
|
2667
3025
|
if (!darkKeys.has(key)) {
|
|
2668
|
-
violations.push(
|
|
3026
|
+
violations.push(violation8("medium", "design-md.parity.missing-dark", `token "${key}" is active in DESIGN.md but missing from DESIGN.dark.md — both files must define the same token set (design-md-spec §4 rule 3)`, "add the token to DESIGN.dark.md with a dark-appropriate value"));
|
|
2669
3027
|
}
|
|
2670
3028
|
}
|
|
2671
3029
|
for (const key of darkKeys) {
|
|
2672
3030
|
if (!lightKeys.has(key)) {
|
|
2673
|
-
violations.push(
|
|
3031
|
+
violations.push(violation8("medium", "design-md.parity.missing-light", `token "${key}" is active in DESIGN.dark.md but missing from DESIGN.md — DESIGN.md is the SSOT for token names (design-md-spec §4 rules 3–4)`, "add the token to DESIGN.md, or remove it from the dark file"));
|
|
2674
3032
|
}
|
|
2675
3033
|
}
|
|
2676
3034
|
return { ok: violations.length === 0, violations };
|
|
@@ -2823,7 +3181,7 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
2823
3181
|
const upgradeTo = candidate === undefined ? null : candidate;
|
|
2824
3182
|
return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
|
|
2825
3183
|
}
|
|
2826
|
-
function
|
|
3184
|
+
function violation9(severity, code, message, fix) {
|
|
2827
3185
|
return { ok: false, severity, code, message, fix };
|
|
2828
3186
|
}
|
|
2829
3187
|
var AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
|
|
@@ -2853,14 +3211,14 @@ function validateAuditStatusBlocks(planText) {
|
|
|
2853
3211
|
const violations = [];
|
|
2854
3212
|
const blocks = parseStatusBlocks(planText);
|
|
2855
3213
|
if (blocks.length === 0) {
|
|
2856
|
-
violations.push(
|
|
3214
|
+
violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL § Plan files)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
|
|
2857
3215
|
return { ok: false, violations };
|
|
2858
3216
|
}
|
|
2859
3217
|
blocks.forEach((block, index) => {
|
|
2860
3218
|
const label = blocks.length > 1 ? ` #${index + 1}` : "";
|
|
2861
3219
|
for (const field of AUDIT_STATUS_FIELDS) {
|
|
2862
3220
|
if (!block.fields.has(field)) {
|
|
2863
|
-
violations.push(
|
|
3221
|
+
violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL § Plan files)`, `add \`- **${field}**: <value>\` to the Status block`));
|
|
2864
3222
|
}
|
|
2865
3223
|
}
|
|
2866
3224
|
const check = (field, pattern, code, expected) => {
|
|
@@ -2868,7 +3226,7 @@ function validateAuditStatusBlocks(planText) {
|
|
|
2868
3226
|
if (value === undefined)
|
|
2869
3227
|
return;
|
|
2870
3228
|
if (!pattern.test(value)) {
|
|
2871
|
-
violations.push(
|
|
3229
|
+
violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL § Plan files)`, `fix \`- **${field}**:\` to one of: ${expected}`));
|
|
2872
3230
|
}
|
|
2873
3231
|
};
|
|
2874
3232
|
check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
|
|
@@ -2948,7 +3306,7 @@ function redactSecrets(text, filePath) {
|
|
|
2948
3306
|
const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
|
|
2949
3307
|
return { text: out, findings: sorted };
|
|
2950
3308
|
}
|
|
2951
|
-
function
|
|
3309
|
+
function violation10(severity, code, message, fix) {
|
|
2952
3310
|
return { ok: false, severity, code, message, fix };
|
|
2953
3311
|
}
|
|
2954
3312
|
var KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
|
|
@@ -3020,7 +3378,7 @@ var KNOWLEDGE_CATEGORY_MAP = {
|
|
|
3020
3378
|
developer_experience: "developer-experience",
|
|
3021
3379
|
documentation_gap: "documentation"
|
|
3022
3380
|
};
|
|
3023
|
-
var
|
|
3381
|
+
var DATE_RE5 = /^\d{4}-\d{2}-\d{2}$/;
|
|
3024
3382
|
var isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3025
3383
|
function parseScalar2(raw) {
|
|
3026
3384
|
const trimmed = raw.trim();
|
|
@@ -3113,36 +3471,36 @@ function validateSchemaYaml(frontmatterText) {
|
|
|
3113
3471
|
const violations = [];
|
|
3114
3472
|
const doc = parseYamlLite(frontmatterText);
|
|
3115
3473
|
if (doc === null) {
|
|
3116
|
-
violations.push(
|
|
3474
|
+
violations.push(violation10("medium", "compound.schema.missing-frontmatter", "no `---` YAML frontmatter block found — knowledge docs must open with the schema.yaml contract (mstar-compound/references/schema.yaml)", "add the fenced frontmatter with module, date, problem_type, category, severity"));
|
|
3117
3475
|
return { ok: false, violations };
|
|
3118
3476
|
}
|
|
3119
3477
|
const isStr = (v) => typeof v === "string";
|
|
3120
|
-
const missing = (field) => violations.push(
|
|
3478
|
+
const missing = (field) => violations.push(violation10("medium", "compound.schema.missing-field", `missing required frontmatter field "${field}" (schema.yaml required_fields)`, `add \`${field}: <value>\` to the frontmatter`));
|
|
3121
3479
|
for (const field of KNOWLEDGE_REQUIRED_FIELDS) {
|
|
3122
3480
|
if (!(field in doc) || doc[field] === "")
|
|
3123
3481
|
missing(field);
|
|
3124
3482
|
}
|
|
3125
|
-
if (doc.date !== undefined && (!isStr(doc.date) || !
|
|
3126
|
-
violations.push(
|
|
3483
|
+
if (doc.date !== undefined && (!isStr(doc.date) || !DATE_RE5.test(doc.date))) {
|
|
3484
|
+
violations.push(violation10("medium", "compound.schema.invalid-date", `date "${String(doc.date)}" must be a YYYY-MM-DD string (schema.yaml required_fields.date)`, "use `YYYY-MM-DD`"));
|
|
3127
3485
|
}
|
|
3128
3486
|
const problemType = doc.problem_type;
|
|
3129
3487
|
if (problemType !== undefined && !isStr(problemType)) {
|
|
3130
|
-
violations.push(
|
|
3488
|
+
violations.push(violation10("medium", "compound.schema.invalid-problem-type", `problem_type "${String(problemType)}" must be a string — one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
|
|
3131
3489
|
}
|
|
3132
3490
|
const problemTypeValid = isStr(problemType) && KNOWLEDGE_PROBLEM_TYPES.includes(problemType);
|
|
3133
3491
|
if (isStr(problemType) && !problemTypeValid) {
|
|
3134
|
-
violations.push(
|
|
3492
|
+
violations.push(violation10("medium", "compound.schema.invalid-problem-type", `problem_type "${problemType}" is not one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
|
|
3135
3493
|
}
|
|
3136
3494
|
if (doc.severity !== undefined && !isStr(doc.severity)) {
|
|
3137
|
-
violations.push(
|
|
3495
|
+
violations.push(violation10("medium", "compound.schema.invalid-severity", `severity "${String(doc.severity)}" must be a string — critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
|
|
3138
3496
|
}
|
|
3139
3497
|
if (isStr(doc.severity) && !KNOWLEDGE_SEVERITIES.includes(doc.severity)) {
|
|
3140
|
-
violations.push(
|
|
3498
|
+
violations.push(violation10("medium", "compound.schema.invalid-severity", `severity "${doc.severity}" must be critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
|
|
3141
3499
|
}
|
|
3142
3500
|
if (problemTypeValid && isStr(doc.category)) {
|
|
3143
3501
|
const expected = KNOWLEDGE_CATEGORY_MAP[problemType];
|
|
3144
3502
|
if (doc.category !== expected) {
|
|
3145
|
-
violations.push(
|
|
3503
|
+
violations.push(violation10("medium", "compound.schema.category-mismatch", `category "${doc.category}" does not match problem_type "${problemType}" — category-mapping.md rule 1 maps it to "${expected}"`, `set \`category: ${expected}\` (the directory name under {KNOWLEDGE_DIR})`));
|
|
3146
3504
|
}
|
|
3147
3505
|
}
|
|
3148
3506
|
if (problemTypeValid) {
|
|
@@ -3150,37 +3508,37 @@ function validateSchemaYaml(frontmatterText) {
|
|
|
3150
3508
|
if (isBug) {
|
|
3151
3509
|
for (const field of ["symptoms", "root_cause", "resolution_type"]) {
|
|
3152
3510
|
if (!(field in doc)) {
|
|
3153
|
-
violations.push(
|
|
3511
|
+
violations.push(violation10("medium", "compound.schema.missing-track-field", `bug-track doc missing required field "${field}" (schema.yaml track_rules.bug)`, `add \`${field}:\` to the frontmatter`));
|
|
3154
3512
|
}
|
|
3155
3513
|
}
|
|
3156
3514
|
if (doc.symptoms !== undefined && !Array.isArray(doc.symptoms)) {
|
|
3157
|
-
violations.push(
|
|
3515
|
+
violations.push(violation10("medium", "compound.schema.invalid-symptoms", "bug-track `symptoms` must be a YAML list (schema.yaml track_rules.bug)", "list the observable symptoms under `symptoms:`"));
|
|
3158
3516
|
}
|
|
3159
3517
|
if (doc.root_cause !== undefined && !isStr(doc.root_cause)) {
|
|
3160
|
-
violations.push(
|
|
3518
|
+
violations.push(violation10("medium", "compound.schema.invalid-root-cause", "bug-track `root_cause` must be a string (schema.yaml track_rules.bug)", "write the fundamental technical cause as a string"));
|
|
3161
3519
|
}
|
|
3162
3520
|
if (isStr(doc.resolution_type) && !KNOWLEDGE_RESOLUTION_TYPES.includes(doc.resolution_type)) {
|
|
3163
|
-
violations.push(
|
|
3521
|
+
violations.push(violation10("medium", "compound.schema.invalid-resolution-type", `resolution_type "${doc.resolution_type}" is not one of the schema.yaml track_rules.bug enum values`, "use code_fix | migration | config_change | test_fix | dependency_update | environment_setup | workflow_improvement | documentation_update | tooling_addition"));
|
|
3164
3522
|
}
|
|
3165
3523
|
} else if (doc.applies_when !== undefined && !Array.isArray(doc.applies_when)) {
|
|
3166
|
-
violations.push(
|
|
3524
|
+
violations.push(violation10("low", "compound.schema.invalid-applies-when", "knowledge-track `applies_when` must be a YAML list when present (schema.yaml track_rules.knowledge)", "list the conditions under `applies_when:`"));
|
|
3167
3525
|
}
|
|
3168
3526
|
}
|
|
3169
3527
|
if (doc.plan_id !== undefined && !isStr(doc.plan_id)) {
|
|
3170
|
-
violations.push(
|
|
3528
|
+
violations.push(violation10("low", "compound.schema.invalid-plan-id", "optional `plan_id` must be a string (schema.yaml optional_fields.plan_id)", "reference the status.json plan id as a string"));
|
|
3171
3529
|
}
|
|
3172
3530
|
if (doc.tags !== undefined) {
|
|
3173
3531
|
if (!Array.isArray(doc.tags)) {
|
|
3174
|
-
violations.push(
|
|
3532
|
+
violations.push(violation10("low", "compound.schema.invalid-tags", "optional `tags` must be a YAML list (schema.yaml optional_fields.tags)", "list lowercase, hyphen-separated keywords"));
|
|
3175
3533
|
} else if (doc.tags.length > 8) {
|
|
3176
|
-
violations.push(
|
|
3534
|
+
violations.push(violation10("low", "compound.schema.tags-too-many", `tags has ${doc.tags.length} entries — max 8 (schema.yaml optional_fields.tags.max_items)`, "trim the tag list to at most 8 keywords"));
|
|
3177
3535
|
}
|
|
3178
3536
|
}
|
|
3179
|
-
if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !
|
|
3180
|
-
violations.push(
|
|
3537
|
+
if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !DATE_RE5.test(doc.last_updated))) {
|
|
3538
|
+
violations.push(violation10("low", "compound.schema.invalid-last-updated", `last_updated "${String(doc.last_updated)}" must be YYYY-MM-DD (schema.yaml optional_fields.last_updated)`, "use `YYYY-MM-DD`"));
|
|
3181
3539
|
}
|
|
3182
3540
|
if (doc.related_components !== undefined && !Array.isArray(doc.related_components)) {
|
|
3183
|
-
violations.push(
|
|
3541
|
+
violations.push(violation10("low", "compound.schema.invalid-related-components", "optional `related_components` must be a YAML list (schema.yaml optional_fields.related_components)", "list the other components involved"));
|
|
3184
3542
|
}
|
|
3185
3543
|
return { ok: violations.length === 0, violations };
|
|
3186
3544
|
}
|
|
@@ -3202,7 +3560,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3202
3560
|
if (ref === "" || seen.has(ref))
|
|
3203
3561
|
continue;
|
|
3204
3562
|
seen.add(ref);
|
|
3205
|
-
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") ||
|
|
3563
|
+
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute7(ref)) {
|
|
3206
3564
|
continue;
|
|
3207
3565
|
}
|
|
3208
3566
|
if (ref.includes("/") || REF_EXT_RE.test(ref)) {
|
|
@@ -3221,7 +3579,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3221
3579
|
const dir = stack.pop();
|
|
3222
3580
|
let entries;
|
|
3223
3581
|
try {
|
|
3224
|
-
entries =
|
|
3582
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
3225
3583
|
} catch {
|
|
3226
3584
|
continue;
|
|
3227
3585
|
}
|
|
@@ -3230,7 +3588,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3230
3588
|
break;
|
|
3231
3589
|
if (entry.isDirectory()) {
|
|
3232
3590
|
if (!WALK_SKIP_DIRS.has(entry.name))
|
|
3233
|
-
stack.push(
|
|
3591
|
+
stack.push(join12(dir, entry.name));
|
|
3234
3592
|
} else if (!entry.isSymbolicLink()) {
|
|
3235
3593
|
const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
3236
3594
|
if (moduleNames.has(base))
|
|
@@ -3242,15 +3600,15 @@ function referenceExists(repoRoot, docText) {
|
|
|
3242
3600
|
for (const { ref, isSymbol, module } of refs) {
|
|
3243
3601
|
if (!isSymbol || module === undefined) {
|
|
3244
3602
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
3245
|
-
if (
|
|
3603
|
+
if (existsSync6(resolve10(repoRoot, candidate))) {
|
|
3246
3604
|
checked++;
|
|
3247
3605
|
} else {
|
|
3248
|
-
violations.push(
|
|
3606
|
+
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"));
|
|
3249
3607
|
}
|
|
3250
3608
|
} else if (foundModules.has(module)) {
|
|
3251
3609
|
checked++;
|
|
3252
3610
|
} else {
|
|
3253
|
-
violations.push(
|
|
3611
|
+
violations.push(violation10("low", "compound.reference.module-missing", `symbol ref \`${ref}\` — heuristic: no ${module}.ts|tsx|js|jsx|mjs|cjs module file found under ${repoRoot} (compound-refresh Phase 2)`, "verify the module file exists, or update the reference"));
|
|
3254
3612
|
}
|
|
3255
3613
|
}
|
|
3256
3614
|
return { ok: violations.length === 0, violations, checked };
|
|
@@ -3262,18 +3620,18 @@ function collectKnowledgeDocs(dir) {
|
|
|
3262
3620
|
const current = stack.pop();
|
|
3263
3621
|
let entries;
|
|
3264
3622
|
try {
|
|
3265
|
-
entries =
|
|
3623
|
+
entries = readdirSync8(current, { withFileTypes: true });
|
|
3266
3624
|
} catch {
|
|
3267
3625
|
continue;
|
|
3268
3626
|
}
|
|
3269
3627
|
for (const entry of entries) {
|
|
3270
3628
|
if (entry.isSymbolicLink())
|
|
3271
3629
|
continue;
|
|
3272
|
-
const full =
|
|
3630
|
+
const full = join12(current, entry.name);
|
|
3273
3631
|
if (entry.isDirectory()) {
|
|
3274
3632
|
stack.push(full);
|
|
3275
3633
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
3276
|
-
docs.push(
|
|
3634
|
+
docs.push(relative4(dir, full).split(sep3).join("/"));
|
|
3277
3635
|
}
|
|
3278
3636
|
}
|
|
3279
3637
|
}
|
|
@@ -3289,14 +3647,14 @@ function normalizeIndexRef(cell) {
|
|
|
3289
3647
|
}
|
|
3290
3648
|
function assertIndexRows(knowledgeDir) {
|
|
3291
3649
|
const violations = [];
|
|
3292
|
-
const readmePath =
|
|
3293
|
-
if (!
|
|
3294
|
-
violations.push(
|
|
3650
|
+
const readmePath = join12(knowledgeDir, "README.md");
|
|
3651
|
+
if (!existsSync6(readmePath)) {
|
|
3652
|
+
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"));
|
|
3295
3653
|
return { ok: false, violations };
|
|
3296
3654
|
}
|
|
3297
3655
|
const docs = collectKnowledgeDocs(knowledgeDir);
|
|
3298
3656
|
const rows = new Set;
|
|
3299
|
-
for (const line of
|
|
3657
|
+
for (const line of readFileSync10(readmePath, "utf8").split(/\r?\n/)) {
|
|
3300
3658
|
if (!line.trim().startsWith("|"))
|
|
3301
3659
|
continue;
|
|
3302
3660
|
const cells = line.split("|").map((c) => c.trim());
|
|
@@ -3308,7 +3666,7 @@ function assertIndexRows(knowledgeDir) {
|
|
|
3308
3666
|
}
|
|
3309
3667
|
for (const doc of docs) {
|
|
3310
3668
|
if (!rows.has(doc)) {
|
|
3311
|
-
violations.push(
|
|
3669
|
+
violations.push(violation10("medium", "compound.index.missing-row", `knowledge doc "${doc}" has no row in knowledge/README.md index (mstar-compound Phase 6 index obligations)`, `add a row \`| [<title>](${doc}) | <source plan> | <description> | <status> |\` to knowledge/README.md`));
|
|
3312
3670
|
}
|
|
3313
3671
|
}
|
|
3314
3672
|
return { ok: violations.length === 0, violations };
|
|
@@ -3317,24 +3675,24 @@ function isFileLikeRoot(root) {
|
|
|
3317
3675
|
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
|
|
3318
3676
|
}
|
|
3319
3677
|
function scopeGuard(path, allowedRoots) {
|
|
3320
|
-
const resolved =
|
|
3678
|
+
const resolved = resolve10(path);
|
|
3321
3679
|
for (const root of allowedRoots) {
|
|
3322
|
-
const r =
|
|
3680
|
+
const r = resolve10(root);
|
|
3323
3681
|
if (isFileLikeRoot(r)) {
|
|
3324
3682
|
if (resolved === r)
|
|
3325
3683
|
return { ok: true, violations: [] };
|
|
3326
|
-
} else if (resolved === r || resolved.startsWith(r +
|
|
3684
|
+
} else if (resolved === r || resolved.startsWith(r + sep3)) {
|
|
3327
3685
|
return { ok: true, violations: [] };
|
|
3328
3686
|
}
|
|
3329
3687
|
}
|
|
3330
3688
|
return {
|
|
3331
3689
|
ok: false,
|
|
3332
3690
|
violations: [
|
|
3333
|
-
|
|
3691
|
+
violation10("medium", "compound.scope.outside", `path "${path}" is outside the compound-refresh scope (allowed: ${allowedRoots.join(", ")}) — compound-refresh operates only on {HARNESS_DIR}/knowledge/**, {HARNESS_DIR}/knowledge/README.md, <repo-root>/CONCEPTS.md, {HARNESS_DIR}/status.json (mstar-compound-refresh SKILL.md § 产物与操作路径)`, "point the operation at one of the allowed paths")
|
|
3334
3692
|
]
|
|
3335
3693
|
};
|
|
3336
3694
|
}
|
|
3337
|
-
function
|
|
3695
|
+
function violation11(severity, code, message, fix) {
|
|
3338
3696
|
return { ok: false, severity, code, message, fix };
|
|
3339
3697
|
}
|
|
3340
3698
|
var TASK_ARTIFACT_RE = /\btask-\d+(?:-(?:brief|report|fix-report|diff)|\.diff)\b/g;
|
|
@@ -3364,18 +3722,18 @@ function lintSkillFrontmatter(frontmatterText) {
|
|
|
3364
3722
|
const violations = [];
|
|
3365
3723
|
const fm = parseFrontmatter(frontmatterText);
|
|
3366
3724
|
if (fm === null) {
|
|
3367
|
-
violations.push(
|
|
3725
|
+
violations.push(violation11("medium", "lint.frontmatter.missing", "no YAML frontmatter block found — a skill file must open with a `---` fenced frontmatter (mstar-skill-authoring § Frontmatter Contract)", "add a frontmatter block with `name` and `description` at the top of the file"));
|
|
3368
3726
|
return { ok: false, violations };
|
|
3369
3727
|
}
|
|
3370
3728
|
const name = fm.name ?? "";
|
|
3371
3729
|
if (name === "") {
|
|
3372
|
-
violations.push(
|
|
3730
|
+
violations.push(violation11("medium", "lint.frontmatter.name.missing", "frontmatter `name` is missing — required (mstar-skill-authoring § Frontmatter Contract)", "add `name: <lowercase-hyphen-id>` to the frontmatter"));
|
|
3373
3731
|
} else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
3374
|
-
violations.push(
|
|
3732
|
+
violations.push(violation11("medium", "lint.frontmatter.name.format", `frontmatter \`name\` must be lowercase-hyphen ("${name}") — e.g. example-skill (mstar-skill-authoring § Frontmatter Contract)`, "rename to a stable lowercase-hyphen id, e.g. `name: example-skill`"));
|
|
3375
3733
|
}
|
|
3376
3734
|
const description = fm.description ?? "";
|
|
3377
3735
|
if (description === "") {
|
|
3378
|
-
violations.push(
|
|
3736
|
+
violations.push(violation11("medium", "lint.frontmatter.description.missing", "frontmatter `description` is missing — the trigger contract is required (mstar-skill-authoring § Frontmatter Contract)", "add a `description:` that states when the skill loads (symptoms, context, roles, exclusions)"));
|
|
3379
3737
|
} else {
|
|
3380
3738
|
const stripped = description.replace(/`[^`]*`/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
|
|
3381
3739
|
let pronoun = null;
|
|
@@ -3386,15 +3744,15 @@ function lintSkillFrontmatter(frontmatterText) {
|
|
|
3386
3744
|
break;
|
|
3387
3745
|
}
|
|
3388
3746
|
if (pronoun !== null) {
|
|
3389
|
-
violations.push(
|
|
3747
|
+
violations.push(violation11("low", "lint.frontmatter.description.person", `description uses first/second-person pronoun "${pronoun[0]}" — keep the trigger contract third person (mstar-skill-authoring § Frontmatter Contract)`, 'rewrite without I/we/you/my/our/your/us, e.g. "Use when the user asks …"'));
|
|
3390
3748
|
}
|
|
3391
3749
|
const start = description.trim().replace(/^[*_#>]+/, "").replace(/^["'`]+/, "").trim();
|
|
3392
3750
|
if (WORKFLOW_VERB_START_RE.test(start)) {
|
|
3393
|
-
violations.push(
|
|
3751
|
+
violations.push(violation11("low", "lint.frontmatter.description.workflow", 'description reads as a workflow summary ("Explains/Describes/Covers …") — the description is the trigger contract, not a summary of steps (mstar-skill-authoring § Frontmatter Contract)', "describe when to load the skill (symptoms, context, roles, exclusions) instead of summarizing its steps"));
|
|
3394
3752
|
} else {
|
|
3395
3753
|
const words = description.trim().split(/\s+/).filter(Boolean).length;
|
|
3396
3754
|
if (words > DESCRIPTION_MAX_WORDS) {
|
|
3397
|
-
violations.push(
|
|
3755
|
+
violations.push(violation11("low", "lint.frontmatter.description.workflow", `description is ${words} words — paragraph-length summaries bury the trigger contract (threshold ${DESCRIPTION_MAX_WORDS}, above the longest corpus description at 114 words, mstar-design-md; mstar-skill-authoring § Frontmatter Contract)`, "trim the description to a scannable trigger contract and move detail into the body"));
|
|
3398
3756
|
}
|
|
3399
3757
|
}
|
|
3400
3758
|
}
|
|
@@ -3422,7 +3780,7 @@ function parseFrontmatter(text) {
|
|
|
3422
3780
|
return null;
|
|
3423
3781
|
return fields;
|
|
3424
3782
|
}
|
|
3425
|
-
function
|
|
3783
|
+
function violation12(severity, code, message, fix) {
|
|
3426
3784
|
return { ok: false, severity, code, message, fix };
|
|
3427
3785
|
}
|
|
3428
3786
|
var ROLE_MAPPING = [
|
|
@@ -3477,19 +3835,19 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
3477
3835
|
const violations = [];
|
|
3478
3836
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
3479
3837
|
for (const { agentId, reference } of mapping) {
|
|
3480
|
-
if (!
|
|
3481
|
-
violations.push(
|
|
3838
|
+
if (!existsSync7(join13(rolesDir, reference))) {
|
|
3839
|
+
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`));
|
|
3482
3840
|
}
|
|
3483
3841
|
}
|
|
3484
3842
|
for (const { family, memberIds } of families) {
|
|
3485
3843
|
const absent = memberIds.filter((id) => !referenceById.has(id));
|
|
3486
3844
|
for (const id of absent) {
|
|
3487
|
-
violations.push(
|
|
3845
|
+
violations.push(violation12("medium", "roles.mapping.family.member.missing", `shared family "${family}" member "${id}" is absent from the role mapping (mstar-roles § Role Reference Mapping)`, `add "${id}" to the mapping`));
|
|
3488
3846
|
}
|
|
3489
3847
|
if (absent.length === 0) {
|
|
3490
3848
|
const refs = new Set(memberIds.map((id) => referenceById.get(id)));
|
|
3491
3849
|
if (refs.size !== 1) {
|
|
3492
|
-
violations.push(
|
|
3850
|
+
violations.push(violation12("medium", "roles.mapping.family.shared", `shared family "${family}" (${memberIds.join(", ")}) must resolve to ONE shared reference file — got ${[...refs].join(", ")} (mstar-roles § Maintenance Rules: "Keep shared-family roles on one shared reference file")`, `point every "${family}" member at the same references/<role>-shared.md`));
|
|
3493
3851
|
}
|
|
3494
3852
|
}
|
|
3495
3853
|
}
|
|
@@ -3498,12 +3856,12 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
3498
3856
|
for (const row of rows) {
|
|
3499
3857
|
const existing = tableByRole.get(row.roleId);
|
|
3500
3858
|
if (existing !== undefined) {
|
|
3501
|
-
violations.push(
|
|
3859
|
+
violations.push(violation12("medium", "roles.param.role.duplicate", `role "${row.roleId}" appears in both the ${existing} and ${table} parameter rows (mstar-roles § Parameter Table (SSOT))`, "remove the duplicate row"));
|
|
3502
3860
|
} else {
|
|
3503
3861
|
tableByRole.set(row.roleId, table);
|
|
3504
3862
|
}
|
|
3505
3863
|
if (!referenceById.has(row.roleId)) {
|
|
3506
|
-
violations.push(
|
|
3864
|
+
violations.push(violation12("medium", "roles.param.role.missing", `${table} parameter row references unknown role "${row.roleId}" (mstar-roles § Parameter Table (SSOT))`, `add "${row.roleId}" to the role mapping or drop the row`));
|
|
3507
3865
|
}
|
|
3508
3866
|
}
|
|
3509
3867
|
};
|
|
@@ -3511,20 +3869,20 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
3511
3869
|
checkParamRoles(qcReviewers, "QC reviewer");
|
|
3512
3870
|
for (const row of devTrack) {
|
|
3513
3871
|
if (row.track !== "primary" && row.track !== "parallel_secondary") {
|
|
3514
|
-
violations.push(
|
|
3872
|
+
violations.push(violation12("medium", "roles.param.track", `dev track for "${row.roleId}" is "${String(row.track)}" — must be primary or parallel_secondary (mstar-roles § Parameter Table (SSOT))`, 'set track to "primary" or "parallel_secondary"'));
|
|
3515
3873
|
}
|
|
3516
3874
|
}
|
|
3517
3875
|
const indices = qcReviewers.map((r) => r.reviewerIndex).sort((a, b) => a - b);
|
|
3518
3876
|
const unique = new Set(indices);
|
|
3519
3877
|
if (indices.length !== 3 || unique.size !== 3 || indices[0] !== 1 || indices[1] !== 2 || indices[2] !== 3) {
|
|
3520
|
-
violations.push(
|
|
3878
|
+
violations.push(violation12("high", "roles.param.qc.index.set", `QC reviewer_index must be exactly {1, 2, 3} across the three qc-specialist* seats — got [${indices.join(", ")}] (mstar-roles § Parameter Table (SSOT))`, "assign reviewer_index 1/2/3 to qc-specialist / qc-specialist-2 / qc-specialist-3"));
|
|
3521
3879
|
}
|
|
3522
3880
|
for (const row of qcReviewers) {
|
|
3523
3881
|
if (row.focus.trim() === "") {
|
|
3524
|
-
violations.push(
|
|
3882
|
+
violations.push(violation12("medium", "roles.param.qc.focus.missing", `QC seat "${row.roleId}" (reviewer_index ${row.reviewerIndex}) has an empty focus (mstar-roles § Parameter Table (SSOT))`, "add the review focus"));
|
|
3525
3883
|
}
|
|
3526
3884
|
if (row.reportSuffix !== `qc${row.reviewerIndex}`) {
|
|
3527
|
-
violations.push(
|
|
3885
|
+
violations.push(violation12("medium", "roles.param.qc.suffix", `QC seat "${row.roleId}" report_suffix "${row.reportSuffix}" must equal qc${row.reviewerIndex} — tri reports land at {SDD_DIR}/review/qc1.md…qc3.md (mstar-roles § Parameter Table (SSOT))`, `set report_suffix to qc${row.reviewerIndex}`));
|
|
3528
3886
|
}
|
|
3529
3887
|
}
|
|
3530
3888
|
return { ok: violations.length === 0, violations };
|
|
@@ -3563,11 +3921,11 @@ function lintLoadOrder(skillTexts) {
|
|
|
3563
3921
|
continue;
|
|
3564
3922
|
const section = extractLoadOrderSection(text);
|
|
3565
3923
|
if (section === null) {
|
|
3566
|
-
violations.push(
|
|
3924
|
+
violations.push(violation12("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载约定; mstar-roles § Load Order (Required))`, `add a "## Load Order" section naming mstar-harness-core as the first read`));
|
|
3567
3925
|
continue;
|
|
3568
3926
|
}
|
|
3569
3927
|
if (!section.includes("mstar-harness-core")) {
|
|
3570
|
-
violations.push(
|
|
3928
|
+
violations.push(violation12("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载约定: 凡 mstar-*(name ≠ mstar-harness-core)假定读者已 Read 本 skill)`, "name mstar-harness-core first in the Load Order section"));
|
|
3571
3929
|
}
|
|
3572
3930
|
}
|
|
3573
3931
|
return { ok: violations.length === 0, violations };
|
|
@@ -3593,7 +3951,7 @@ function resolveSkillRoot(host, paths) {
|
|
|
3593
3951
|
return `deferred: pi has no plugin API in v1 — skill-root resolution lands with its adapter (roadmap §8.4)`;
|
|
3594
3952
|
}
|
|
3595
3953
|
}
|
|
3596
|
-
function
|
|
3954
|
+
function violation13(severity, code, message, fix) {
|
|
3597
3955
|
return { ok: false, severity, code, message, fix };
|
|
3598
3956
|
}
|
|
3599
3957
|
var FIVE_QUESTION_SECTIONS = [
|
|
@@ -3643,7 +4001,7 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
3643
4001
|
const aliases = mode === "runtime" ? RUNTIME_HEADING_ALIASES[section.key] ?? [] : [];
|
|
3644
4002
|
const covered = headings.some((heading) => heading.includes(label) || aliases.some((alias) => heading.includes(alias)));
|
|
3645
4003
|
if (!covered) {
|
|
3646
|
-
violations.push(
|
|
4004
|
+
violations.push(violation13("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" — no "${section.label}" section (mstar-skill-authoring § Body 必须回答的 5 问 / § 默认 Body 结构)`, `add a "## ${section.label}" section covering ${section.question}`));
|
|
3647
4005
|
}
|
|
3648
4006
|
}
|
|
3649
4007
|
return { ok: violations.length === 0, violations };
|
|
@@ -3669,8 +4027,8 @@ class DshMstar extends Service {
|
|
|
3669
4027
|
validateResidual(entry) {
|
|
3670
4028
|
return validateResidual(entry);
|
|
3671
4029
|
}
|
|
3672
|
-
findingsCleanupGate(
|
|
3673
|
-
return findingsCleanupGate(
|
|
4030
|
+
findingsCleanupGate(register, planId, opts) {
|
|
4031
|
+
return findingsCleanupGate(register, planId, opts);
|
|
3674
4032
|
}
|
|
3675
4033
|
resolveCompassEnforcement(harnessDir) {
|
|
3676
4034
|
return resolveCompassEnforcement(harnessDir);
|
|
@@ -3688,8 +4046,8 @@ class DshMstar extends Service {
|
|
|
3688
4046
|
|
|
3689
4047
|
// src/gates/_shared.ts
|
|
3690
4048
|
var import_schemastery = __toESM(require_lib2(), 1);
|
|
3691
|
-
import { existsSync as
|
|
3692
|
-
import { resolve as
|
|
4049
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
4050
|
+
import { resolve as resolve8 } from "node:path";
|
|
3693
4051
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3694
4052
|
var STATUS_FILE = "status.json";
|
|
3695
4053
|
var PERSONA_INTERPOLATION_HAZARD = /\{\{[\s\S]*\}\}/;
|
|
@@ -3726,8 +4084,8 @@ var Config = import_schemastery.default.object({
|
|
|
3726
4084
|
workflowNames: import_schemastery.default.array(import_schemastery.default.string()).default(undefined),
|
|
3727
4085
|
maxGoalRounds: import_schemastery.default.number().default(undefined)
|
|
3728
4086
|
});
|
|
3729
|
-
function formatViolation(
|
|
3730
|
-
return `[${
|
|
4087
|
+
function formatViolation(violation14) {
|
|
4088
|
+
return `[${violation14.severity}] ${violation14.code}: ${violation14.message}${violation14.fix !== undefined ? ` (fix: ${violation14.fix})` : ""}`;
|
|
3731
4089
|
}
|
|
3732
4090
|
function asRecord(value) {
|
|
3733
4091
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
@@ -3737,13 +4095,13 @@ function resolveSeamHard(harnessDir, config) {
|
|
|
3737
4095
|
return true;
|
|
3738
4096
|
if (config.enforcement === "soft")
|
|
3739
4097
|
return false;
|
|
3740
|
-
return harnessDir !== null &&
|
|
4098
|
+
return harnessDir !== null && resolveRepoEnforcement(harnessDir).hard;
|
|
3741
4099
|
}
|
|
3742
4100
|
function resolvePackagedSkillsDir(fileUrl) {
|
|
3743
4101
|
for (const rel of ["../harness-skills", "../../harness-skills"]) {
|
|
3744
4102
|
try {
|
|
3745
4103
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
3746
|
-
if (
|
|
4104
|
+
if (existsSync5(dir))
|
|
3747
4105
|
return dir;
|
|
3748
4106
|
} catch {}
|
|
3749
4107
|
}
|
|
@@ -3762,7 +4120,7 @@ function resolvePackagedAgentsDir(fileUrl) {
|
|
|
3762
4120
|
for (const rel of ["../harness-agents", "../../harness-agents"]) {
|
|
3763
4121
|
try {
|
|
3764
4122
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
3765
|
-
if (
|
|
4123
|
+
if (existsSync5(dir))
|
|
3766
4124
|
return dir;
|
|
3767
4125
|
} catch {}
|
|
3768
4126
|
}
|
|
@@ -3782,14 +4140,14 @@ class HarnessResolver {
|
|
|
3782
4140
|
explicit;
|
|
3783
4141
|
cache = new Map;
|
|
3784
4142
|
constructor(explicit) {
|
|
3785
|
-
this.explicit = explicit !== undefined && explicit.trim() !== "" ?
|
|
4143
|
+
this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve8(process.cwd(), explicit) : null;
|
|
3786
4144
|
}
|
|
3787
4145
|
forWorkspace(cwd) {
|
|
3788
4146
|
if (this.explicit !== null)
|
|
3789
4147
|
return this.explicit;
|
|
3790
4148
|
if (cwd === undefined || cwd.trim() === "")
|
|
3791
4149
|
return null;
|
|
3792
|
-
const abs =
|
|
4150
|
+
const abs = resolve8(cwd);
|
|
3793
4151
|
const hit = this.cache.get(abs);
|
|
3794
4152
|
if (hit !== undefined)
|
|
3795
4153
|
return hit;
|
|
@@ -3815,33 +4173,167 @@ function iterationViolationView(v) {
|
|
|
3815
4173
|
function iterationGateView(gate2) {
|
|
3816
4174
|
return { ok: gate2.ok, violations: gate2.violations.map(iterationViolationView) };
|
|
3817
4175
|
}
|
|
3818
|
-
function skillLocalConfig(config) {
|
|
3819
|
-
const customSkillDirs = config.skillRoots?.map((root) => root.trim()).filter((root) => root !== "");
|
|
3820
|
-
const bundledSkillDir = config.bundledSkillDir?.trim() ?? packagedSkillsDir();
|
|
3821
|
-
if ((customSkillDirs === undefined || customSkillDirs.length === 0) && bundledSkillDir === undefined) {
|
|
3822
|
-
return;
|
|
4176
|
+
function skillLocalConfig(config) {
|
|
4177
|
+
const customSkillDirs = config.skillRoots?.map((root) => root.trim()).filter((root) => root !== "");
|
|
4178
|
+
const bundledSkillDir = config.bundledSkillDir?.trim() ?? packagedSkillsDir();
|
|
4179
|
+
if ((customSkillDirs === undefined || customSkillDirs.length === 0) && bundledSkillDir === undefined) {
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
return {
|
|
4183
|
+
providerName: "mstar",
|
|
4184
|
+
includeDefaultRoots: false,
|
|
4185
|
+
...customSkillDirs !== undefined && customSkillDirs.length > 0 ? { customSkillDirs } : {},
|
|
4186
|
+
...bundledSkillDir !== undefined ? { bundledSkillDir } : {}
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
// src/gates/catalog.ts
|
|
4191
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "node:fs";
|
|
4192
|
+
import { join as join11 } from "node:path";
|
|
4193
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4194
|
+
|
|
4195
|
+
// src/gates/agent-flow.ts
|
|
4196
|
+
import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4197
|
+
import { join as join10 } from "node:path";
|
|
4198
|
+
|
|
4199
|
+
// src/gates/dispatch.ts
|
|
4200
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4201
|
+
import { basename as basename5, join as join8, resolve as resolve11 } from "node:path";
|
|
4202
|
+
|
|
4203
|
+
// src/gates/workflow-selection.ts
|
|
4204
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
|
|
4205
|
+
import { join as join5, relative as relative3 } from "node:path";
|
|
4206
|
+
var TERMINAL_STATUS_CACHE_MAX = 64;
|
|
4207
|
+
var terminalStatusCache = new Map;
|
|
4208
|
+
function terminalStatusOf(snapshotPath) {
|
|
4209
|
+
let mtimeMs;
|
|
4210
|
+
try {
|
|
4211
|
+
mtimeMs = statSync3(snapshotPath).mtimeMs;
|
|
4212
|
+
} catch {
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
const cached = terminalStatusCache.get(snapshotPath);
|
|
4216
|
+
if (cached !== undefined && cached.mtimeMs === mtimeMs)
|
|
4217
|
+
return cached;
|
|
4218
|
+
let terminal = false;
|
|
4219
|
+
try {
|
|
4220
|
+
const snapshot = readJson(snapshotPath);
|
|
4221
|
+
const status = snapshot.status;
|
|
4222
|
+
terminal = typeof status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(status);
|
|
4223
|
+
} catch {
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
const verdict = { mtimeMs, terminal };
|
|
4227
|
+
terminalStatusCache.set(snapshotPath, verdict);
|
|
4228
|
+
while (terminalStatusCache.size > TERMINAL_STATUS_CACHE_MAX) {
|
|
4229
|
+
const oldest = terminalStatusCache.keys().next().value;
|
|
4230
|
+
if (oldest === undefined)
|
|
4231
|
+
break;
|
|
4232
|
+
terminalStatusCache.delete(oldest);
|
|
4233
|
+
}
|
|
4234
|
+
return verdict;
|
|
4235
|
+
}
|
|
4236
|
+
function resolveActiveWorkflow(harnessDir) {
|
|
4237
|
+
const statusPath = join5(harnessDir, STATUS_FILE);
|
|
4238
|
+
if (!existsSync8(statusPath)) {
|
|
4239
|
+
return {
|
|
4240
|
+
kind: "error",
|
|
4241
|
+
code: "status.missing",
|
|
4242
|
+
message: `no ${STATUS_FILE} at ${harnessDir} — the root v2 document is required`
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
let doc;
|
|
4246
|
+
try {
|
|
4247
|
+
doc = readJson(statusPath);
|
|
4248
|
+
} catch {
|
|
4249
|
+
return { kind: "error", code: "status.unreadable", message: `cannot read ${statusPath}` };
|
|
4250
|
+
}
|
|
4251
|
+
if (doc.version !== 2) {
|
|
4252
|
+
return {
|
|
4253
|
+
kind: "error",
|
|
4254
|
+
code: "status.migration-required",
|
|
4255
|
+
message: `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
if (!Array.isArray(doc.workflows) || doc.workflows.length === 0) {
|
|
4259
|
+
return {
|
|
4260
|
+
kind: "error",
|
|
4261
|
+
code: "workflow.selection.no-active",
|
|
4262
|
+
message: "no active lifecycle in status.json workflows[] — the agent-flow writer appends only to an active lifecycle"
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
const first = doc.workflows[0];
|
|
4266
|
+
const entry = asRecord(first);
|
|
4267
|
+
const workflowId = typeof entry?.id === "string" ? entry.id : undefined;
|
|
4268
|
+
const dir = typeof entry?.dir === "string" ? entry.dir : undefined;
|
|
4269
|
+
if (workflowId === undefined || dir === undefined) {
|
|
4270
|
+
return {
|
|
4271
|
+
kind: "error",
|
|
4272
|
+
code: "workflow.selection.invalid-entry",
|
|
4273
|
+
message: `workflows[] first entry is missing id/dir: ${JSON.stringify(first)}`
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
if (doc.workflows.length > 1) {
|
|
4277
|
+
return {
|
|
4278
|
+
kind: "active",
|
|
4279
|
+
workflowId,
|
|
4280
|
+
dir,
|
|
4281
|
+
warning: {
|
|
4282
|
+
code: "workflow.selection.multi-active",
|
|
4283
|
+
message: `${doc.workflows.length} active lifecycles in status.json workflows[] — selected the first (${workflowId}); no silent pick`
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
return { kind: "active", workflowId, dir };
|
|
4288
|
+
}
|
|
4289
|
+
function resolveReadWorkflow(harnessDir) {
|
|
4290
|
+
const active = resolveActiveWorkflow(harnessDir);
|
|
4291
|
+
if (active.kind === "active")
|
|
4292
|
+
return active;
|
|
4293
|
+
if (active.kind === "error" && active.code !== "workflow.selection.no-active")
|
|
4294
|
+
return active;
|
|
4295
|
+
const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
4296
|
+
if (!existsSync8(workflowsDir)) {
|
|
4297
|
+
return {
|
|
4298
|
+
kind: "error",
|
|
4299
|
+
code: "workflow.selection.no-snapshot",
|
|
4300
|
+
message: `no workflow snapshots under ${workflowsDir} — run \`mstar migrate\` or start a lifecycle`
|
|
4301
|
+
};
|
|
3823
4302
|
}
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4303
|
+
let entries;
|
|
4304
|
+
try {
|
|
4305
|
+
entries = readdirSync5(workflowsDir, { withFileTypes: true });
|
|
4306
|
+
} catch {
|
|
4307
|
+
return {
|
|
4308
|
+
kind: "error",
|
|
4309
|
+
code: "workflow.selection.no-snapshot",
|
|
4310
|
+
message: `cannot read ${workflowsDir} — no workflow snapshot selected`
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
let best;
|
|
4314
|
+
for (const entry of entries) {
|
|
4315
|
+
if (!entry.isDirectory())
|
|
4316
|
+
continue;
|
|
4317
|
+
const snapshotPath = join5(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
|
|
4318
|
+
if (!existsSync8(snapshotPath))
|
|
4319
|
+
continue;
|
|
4320
|
+
const verdict = terminalStatusOf(snapshotPath);
|
|
4321
|
+
if (verdict === undefined || !verdict.terminal)
|
|
4322
|
+
continue;
|
|
4323
|
+
if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
|
|
4324
|
+
best = { workflowId: entry.name, dir: join5(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (best === undefined) {
|
|
4328
|
+
return {
|
|
4329
|
+
kind: "error",
|
|
4330
|
+
code: "workflow.selection.no-snapshot",
|
|
4331
|
+
message: `no terminal workflow snapshot under ${workflowsDir} — run \`mstar migrate\` or start a lifecycle`
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
return { kind: "terminal", workflowId: best.workflowId, dir: best.dir };
|
|
3830
4335
|
}
|
|
3831
4336
|
|
|
3832
|
-
// src/gates/catalog.ts
|
|
3833
|
-
import { existsSync as existsSync11, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
|
|
3834
|
-
import { join as join12 } from "node:path";
|
|
3835
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3836
|
-
|
|
3837
|
-
// src/gates/agent-flow.ts
|
|
3838
|
-
import { appendFileSync, existsSync as existsSync10, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3839
|
-
import { join as join10 } from "node:path";
|
|
3840
|
-
|
|
3841
|
-
// src/gates/dispatch.ts
|
|
3842
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
3843
|
-
import { basename as basename5, join as join6, resolve as resolve9 } from "node:path";
|
|
3844
|
-
|
|
3845
4337
|
// src/gates/workflow-policy.ts
|
|
3846
4338
|
var WORKFLOW_NAME_UNKNOWN_CODE = "workflow.name.unknown";
|
|
3847
4339
|
var WORKFLOW_LEASE_UNCOVERED_CODE = "workflow.lease.uncovered";
|
|
@@ -3946,7 +4438,7 @@ function resolveDispatchHard(harnessDir, config, assignmentText) {
|
|
|
3946
4438
|
return false;
|
|
3947
4439
|
if (parseEnforcementFlag(assignmentHeaderRegion(assignmentText)).hard)
|
|
3948
4440
|
return true;
|
|
3949
|
-
return harnessDir !== null &&
|
|
4441
|
+
return harnessDir !== null && resolveRepoEnforcement(harnessDir).hard;
|
|
3950
4442
|
}
|
|
3951
4443
|
function denyReason(tool, verdict) {
|
|
3952
4444
|
return [
|
|
@@ -4016,25 +4508,34 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
|
|
|
4016
4508
|
const planId = planIdOf(header);
|
|
4017
4509
|
if (planId === undefined || planId === "")
|
|
4018
4510
|
return [];
|
|
4019
|
-
const
|
|
4020
|
-
if (
|
|
4511
|
+
const selection = resolveActiveWorkflow(harnessDir);
|
|
4512
|
+
if (selection.kind !== "active") {
|
|
4021
4513
|
if (!sdd)
|
|
4022
4514
|
return [];
|
|
4023
|
-
|
|
4515
|
+
if (selection.kind === "error" && selection.code === "status.unreadable") {
|
|
4516
|
+
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join8(harnessDir, STATUS_FILE)}: the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid status.json (the status gate refuses invalid writes)")];
|
|
4517
|
+
}
|
|
4518
|
+
return [leaseViolation("lease.dispatch.unverifiable", `${join8(harnessDir, STATUS_FILE)}: ${selection.kind === "error" ? selection.message : "no active workflow"} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid v2 status.json registering an active workflow (first implement dispatch requires a plan row + lease)")];
|
|
4024
4519
|
}
|
|
4025
|
-
|
|
4520
|
+
const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4521
|
+
if (!existsSync10(snapshotPath)) {
|
|
4522
|
+
if (!sdd)
|
|
4523
|
+
return [];
|
|
4524
|
+
return [leaseViolation("lease.dispatch.unverifiable", `${snapshotPath} is missing — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid workflow snapshot registering the plan row (first implement dispatch requires a plan row)")];
|
|
4525
|
+
}
|
|
4526
|
+
let snapshot;
|
|
4026
4527
|
try {
|
|
4027
|
-
|
|
4528
|
+
snapshot = readJson(snapshotPath);
|
|
4028
4529
|
} catch (error) {
|
|
4029
4530
|
if (!sdd)
|
|
4030
4531
|
return [];
|
|
4031
|
-
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${
|
|
4532
|
+
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${snapshotPath}: ${error.message} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid workflow snapshot (the status gate refuses invalid writes)")];
|
|
4032
4533
|
}
|
|
4033
|
-
const row = Array.isArray(
|
|
4534
|
+
const row = Array.isArray(snapshot.plans) ? snapshot.plans.map(asRecord).find((r) => r?.id === planId || r?.plan_id === planId) : undefined;
|
|
4034
4535
|
if (row === undefined) {
|
|
4035
4536
|
if (!sdd)
|
|
4036
4537
|
return [];
|
|
4037
|
-
return [leaseViolation("lease.dispatch.plan-not-found", `plan ${planId} is not registered in ${
|
|
4538
|
+
return [leaseViolation("lease.dispatch.plan-not-found", `plan ${planId} is not registered in ${snapshotPath} — cannot verify its execution_lease before writable dispatch`, "register the plan row in the workflow snapshot (first implement dispatch requires a plan row)")];
|
|
4038
4539
|
}
|
|
4039
4540
|
if (!sdd && row.status !== "InProgress")
|
|
4040
4541
|
return [];
|
|
@@ -4050,7 +4551,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
|
|
|
4050
4551
|
const wt = worktree === undefined ? undefined : firstToken(worktree);
|
|
4051
4552
|
if (isNaValue(wt)) {
|
|
4052
4553
|
violations.push(leaseViolation("lease.dispatch.worktree-mismatch", "Assignment declares no Worktree path — cannot confirm this dispatch matches execution_lease.worktree_path", "add the absolute Worktree path to the Assignment (must equal the lease worktree_path)"));
|
|
4053
|
-
} else if (
|
|
4554
|
+
} else if (resolve11(wt) !== resolve11(String(lease.worktree_path ?? ""))) {
|
|
4054
4555
|
violations.push(leaseViolation("lease.dispatch.worktree-mismatch", `Assignment Worktree path "${wt}" differs from execution_lease.worktree_path "${String(lease.worktree_path)}"`, "align the Assignment with the lease worktree path (or update the lease)"));
|
|
4055
4556
|
}
|
|
4056
4557
|
const forms = parseAssignmentBranchForms(header);
|
|
@@ -4090,26 +4591,45 @@ function worktreeL2Violations(header) {
|
|
|
4090
4591
|
violations.push(...l2PreDispatchCheck({ tracks }).violations);
|
|
4091
4592
|
return violations;
|
|
4092
4593
|
}
|
|
4594
|
+
function activeSnapshotRows(harnessDir) {
|
|
4595
|
+
if (!existsSync10(join8(harnessDir, STATUS_FILE)))
|
|
4596
|
+
return { rows: null, controlWorktreePath: undefined, unreadable: false };
|
|
4597
|
+
const selection = resolveActiveWorkflow(harnessDir);
|
|
4598
|
+
if (selection.kind !== "active") {
|
|
4599
|
+
return {
|
|
4600
|
+
rows: null,
|
|
4601
|
+
controlWorktreePath: undefined,
|
|
4602
|
+
unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4606
|
+
if (!existsSync10(snapshotPath))
|
|
4607
|
+
return { rows: null, controlWorktreePath: undefined, unreadable: true };
|
|
4608
|
+
let doc;
|
|
4609
|
+
try {
|
|
4610
|
+
doc = readJson(snapshotPath);
|
|
4611
|
+
} catch {
|
|
4612
|
+
return { rows: null, controlWorktreePath: undefined, unreadable: true };
|
|
4613
|
+
}
|
|
4614
|
+
if (!Array.isArray(doc.plans))
|
|
4615
|
+
return { rows: null, controlWorktreePath: undefined, unreadable: true };
|
|
4616
|
+
const controlWorktreePath = typeof doc.control_worktree_path === "string" ? doc.control_worktree_path : undefined;
|
|
4617
|
+
const rows = doc.plans.map((row) => asRecord(row)).filter((row) => row !== undefined);
|
|
4618
|
+
return { rows, controlWorktreePath, unreadable: false };
|
|
4619
|
+
}
|
|
4093
4620
|
function worktreeL1Violations(harnessDir, header) {
|
|
4094
4621
|
if (harnessDir === null)
|
|
4095
4622
|
return [];
|
|
4096
4623
|
const planId = planIdOf(header);
|
|
4097
4624
|
if (planId === undefined || planId === "")
|
|
4098
4625
|
return [];
|
|
4099
|
-
const
|
|
4100
|
-
if (
|
|
4101
|
-
return [];
|
|
4102
|
-
let doc;
|
|
4103
|
-
try {
|
|
4104
|
-
doc = readJson(statusPath);
|
|
4105
|
-
} catch {
|
|
4626
|
+
const read = activeSnapshotRows(harnessDir);
|
|
4627
|
+
if (read.rows === null)
|
|
4106
4628
|
return [];
|
|
4107
|
-
|
|
4108
|
-
const metadata = asRecord(doc.metadata);
|
|
4109
|
-
const controlWorktreePath = typeof metadata?.control_worktree_path === "string" ? metadata.control_worktree_path : undefined;
|
|
4629
|
+
const controlWorktreePath = read.controlWorktreePath;
|
|
4110
4630
|
if (controlWorktreePath === undefined || controlWorktreePath.trim() === "")
|
|
4111
4631
|
return [];
|
|
4112
|
-
const row =
|
|
4632
|
+
const row = read.rows.find((r) => r?.id === planId || r?.plan_id === planId);
|
|
4113
4633
|
const lease = asRecord(row?.execution_lease);
|
|
4114
4634
|
const leaseWorktreePath = typeof lease?.worktree_path === "string" ? lease.worktree_path : undefined;
|
|
4115
4635
|
const leaseWorkingBranch = typeof lease?.working_branch === "string" ? lease.working_branch : undefined;
|
|
@@ -4121,19 +4641,10 @@ function worktreeL1Violations(harnessDir, header) {
|
|
|
4121
4641
|
function writableFanOutUncovered(harnessDir) {
|
|
4122
4642
|
if (harnessDir === null)
|
|
4123
4643
|
return { unreadable: false };
|
|
4124
|
-
const
|
|
4125
|
-
if (
|
|
4126
|
-
return { unreadable:
|
|
4127
|
-
|
|
4128
|
-
try {
|
|
4129
|
-
doc = readJson(statusPath);
|
|
4130
|
-
} catch {
|
|
4131
|
-
return { unreadable: true };
|
|
4132
|
-
}
|
|
4133
|
-
if (!Array.isArray(doc.plans))
|
|
4134
|
-
return { unreadable: true };
|
|
4135
|
-
for (const raw of doc.plans) {
|
|
4136
|
-
const row = asRecord(raw);
|
|
4644
|
+
const read = activeSnapshotRows(harnessDir);
|
|
4645
|
+
if (read.rows === null)
|
|
4646
|
+
return { unreadable: read.unreadable };
|
|
4647
|
+
for (const row of read.rows) {
|
|
4137
4648
|
if (row === undefined || row.status !== "InProgress")
|
|
4138
4649
|
continue;
|
|
4139
4650
|
const planId = typeof row.id === "string" && row.id !== "" ? row.id : typeof row.plan_id === "string" && row.plan_id !== "" ? row.plan_id : "in-progress-plan";
|
|
@@ -4182,7 +4693,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
|
|
|
4182
4693
|
}
|
|
4183
4694
|
const pb = writableFanOutUncovered(harnessDir);
|
|
4184
4695
|
if (pb.unreadable) {
|
|
4185
|
-
const statusLabel = harnessDir === null ? STATUS_FILE :
|
|
4696
|
+
const statusLabel = harnessDir === null ? STATUS_FILE : join8(harnessDir, STATUS_FILE);
|
|
4186
4697
|
ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
|
|
4187
4698
|
}
|
|
4188
4699
|
const verdict = workflowPolicy(config, adapter.workflowAskCache, {
|
|
@@ -4319,6 +4830,7 @@ var SETTLE_SEAM = "tools/post-execute";
|
|
|
4319
4830
|
var SETTLE_SEAM_PAIRING_NOTE = `settle seam "${SETTLE_SEAM}" IS part of the verified dsh-tools registry surface (runPostExecute dispatches it for every tool call) — foreground dispatch calls settle here, background subagents settle via ctx.jobs.onJobDone pairing; only UNPAIRED payloads (non-dispatch tools, calls outside the apply-scoped pairing window) stay dispatch-only — never a fabricated settle`;
|
|
4320
4831
|
var logSink;
|
|
4321
4832
|
var settleNoteLogged = false;
|
|
4833
|
+
var noActiveWarned = false;
|
|
4322
4834
|
var invalidator;
|
|
4323
4835
|
function setAgentFlowInvalidator(invalidate) {
|
|
4324
4836
|
const prior = invalidator;
|
|
@@ -4329,6 +4841,7 @@ function setAgentFlowLogger(sink) {
|
|
|
4329
4841
|
const prior = logSink;
|
|
4330
4842
|
logSink = sink;
|
|
4331
4843
|
settleNoteLogged = false;
|
|
4844
|
+
noActiveWarned = false;
|
|
4332
4845
|
return prior;
|
|
4333
4846
|
}
|
|
4334
4847
|
function log(level, message) {
|
|
@@ -4366,25 +4879,92 @@ function callPairingKey(exec) {
|
|
|
4366
4879
|
return;
|
|
4367
4880
|
return `${sessionIdOf(exec) ?? ""}\x00${callId}`;
|
|
4368
4881
|
}
|
|
4369
|
-
function
|
|
4370
|
-
const
|
|
4371
|
-
|
|
4882
|
+
function resolveAgentFlowWriteDir(harnessDir) {
|
|
4883
|
+
const selection = resolveActiveWorkflow(harnessDir);
|
|
4884
|
+
if (selection.kind === "active")
|
|
4885
|
+
return join10(harnessDir, selection.dir);
|
|
4886
|
+
const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
|
|
4887
|
+
if (!noActiveWarned) {
|
|
4888
|
+
noActiveWarned = true;
|
|
4889
|
+
log("warn", `agent-flow record skipped — ${message}`);
|
|
4890
|
+
}
|
|
4891
|
+
return null;
|
|
4892
|
+
}
|
|
4893
|
+
var WORKFLOW_LEDGER_LOCKDIR = ".ledger-write.lockdir";
|
|
4894
|
+
var WORKFLOW_LOCKER_TIMEOUT_MS = 30000;
|
|
4895
|
+
var WORKFLOW_LOCKER_POLL_MS = 10;
|
|
4896
|
+
var WORKFLOW_LOCKER_HOLDER_PID = "holder.pid";
|
|
4897
|
+
var heldWorkflowLocks = new Set;
|
|
4898
|
+
function sleepSync(ms) {
|
|
4899
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
4900
|
+
}
|
|
4901
|
+
function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
4902
|
+
const lockDir = join10(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
|
|
4903
|
+
if (heldWorkflowLocks.has(lockDir)) {
|
|
4904
|
+
throw new Error(`${lockDir} is already held by this process — withWorkflowDirLock is not reentrant; a nested acquisition on the same workflow dir is a bug`);
|
|
4905
|
+
}
|
|
4906
|
+
const timeoutMs = opts.timeoutMs ?? WORKFLOW_LOCKER_TIMEOUT_MS;
|
|
4907
|
+
const pollMs = opts.pollMs ?? WORKFLOW_LOCKER_POLL_MS;
|
|
4908
|
+
const deadline = Date.now() + timeoutMs;
|
|
4909
|
+
let acquired = null;
|
|
4910
|
+
for (;; ) {
|
|
4911
|
+
try {
|
|
4912
|
+
mkdirSync3(lockDir);
|
|
4913
|
+
const st = statSync5(lockDir);
|
|
4914
|
+
acquired = { dev: st.dev, ino: st.ino };
|
|
4915
|
+
break;
|
|
4916
|
+
} catch (error) {
|
|
4917
|
+
if (error.code !== "EEXIST")
|
|
4918
|
+
throw error;
|
|
4919
|
+
if (Date.now() >= deadline) {
|
|
4920
|
+
throw new Error(`${lockDir} already exists — another writer holds the workflow write lock; Blocked (same-host exclusive lock; same pattern as the engine's withStatusWriteLock). ` + `Recovery: remove ${lockDir} if no writer is alive (holder.pid inside names the acquiring process)`);
|
|
4921
|
+
}
|
|
4922
|
+
sleepSync(pollMs);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
heldWorkflowLocks.add(lockDir);
|
|
4926
|
+
try {
|
|
4927
|
+
try {
|
|
4928
|
+
writeFileSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
|
|
4929
|
+
} catch {}
|
|
4930
|
+
return fn();
|
|
4931
|
+
} finally {
|
|
4932
|
+
heldWorkflowLocks.delete(lockDir);
|
|
4933
|
+
try {
|
|
4934
|
+
const current = statSync5(lockDir);
|
|
4935
|
+
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
4936
|
+
try {
|
|
4937
|
+
unlinkSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
|
|
4938
|
+
} catch {}
|
|
4939
|
+
rmdirSync(lockDir);
|
|
4940
|
+
}
|
|
4941
|
+
} catch {}
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
function appendEvent(workflowDir, event) {
|
|
4945
|
+
const file = join10(workflowDir, AGENT_FLOW_FILE);
|
|
4946
|
+
withWorkflowDirLock(workflowDir, () => {
|
|
4947
|
+
appendFileSync(file, `${JSON.stringify(event)}
|
|
4372
4948
|
`);
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4949
|
+
if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
|
|
4950
|
+
return;
|
|
4951
|
+
const content = readFileSync7(file, "utf8");
|
|
4952
|
+
const lines = content.replace(/\n$/, "").split(`
|
|
4377
4953
|
`);
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4954
|
+
if (lines.length > AGENT_FLOW_MAX_EVENTS) {
|
|
4955
|
+
const tmp = `${file}.tmp`;
|
|
4956
|
+
writeFileSync2(tmp, `${lines.slice(-AGENT_FLOW_MAX_EVENTS).join(`
|
|
4381
4957
|
`)}
|
|
4382
4958
|
`);
|
|
4383
|
-
|
|
4384
|
-
|
|
4959
|
+
renameSync2(tmp, file);
|
|
4960
|
+
}
|
|
4961
|
+
});
|
|
4385
4962
|
}
|
|
4386
4963
|
function recordDispatch(input) {
|
|
4387
4964
|
try {
|
|
4965
|
+
const workflowDir = resolveAgentFlowWriteDir(input.harnessDir);
|
|
4966
|
+
if (workflowDir === null)
|
|
4967
|
+
return;
|
|
4388
4968
|
const header = assignmentHeaderRegion(input.prompt);
|
|
4389
4969
|
const fields = parseAssignmentFields(header);
|
|
4390
4970
|
const planId = planIdOf(header);
|
|
@@ -4403,7 +4983,7 @@ function recordDispatch(input) {
|
|
|
4403
4983
|
verdict: verdictOf(input.violations, input.hard),
|
|
4404
4984
|
hard: input.hard
|
|
4405
4985
|
};
|
|
4406
|
-
appendEvent(
|
|
4986
|
+
appendEvent(workflowDir, event);
|
|
4407
4987
|
try {
|
|
4408
4988
|
invalidator?.(input.harnessDir);
|
|
4409
4989
|
} catch (error) {
|
|
@@ -4415,6 +4995,7 @@ function recordDispatch(input) {
|
|
|
4415
4995
|
if (key !== undefined) {
|
|
4416
4996
|
input.pairing.dispatchByCallId.set(key, {
|
|
4417
4997
|
harnessDir: input.harnessDir,
|
|
4998
|
+
workflowDir,
|
|
4418
4999
|
...agent !== undefined ? { agent } : {},
|
|
4419
5000
|
role: fields.executeAs ?? "",
|
|
4420
5001
|
...planId !== undefined && !isNaValue(planId) ? { planId } : {},
|
|
@@ -4431,6 +5012,9 @@ function recordDispatch(input) {
|
|
|
4431
5012
|
}
|
|
4432
5013
|
function recordSettle(input) {
|
|
4433
5014
|
try {
|
|
5015
|
+
const workflowDir = input.workflowDir ?? resolveAgentFlowWriteDir(input.harnessDir);
|
|
5016
|
+
if (workflowDir === null)
|
|
5017
|
+
return;
|
|
4434
5018
|
const event = {
|
|
4435
5019
|
v: 1,
|
|
4436
5020
|
ts: Date.now(),
|
|
@@ -4442,7 +5026,7 @@ function recordSettle(input) {
|
|
|
4442
5026
|
...input.planId !== undefined && input.planId !== "" ? { planId: input.planId } : {},
|
|
4443
5027
|
...input.taskId !== undefined && input.taskId !== "" ? { taskId: input.taskId } : {}
|
|
4444
5028
|
};
|
|
4445
|
-
appendEvent(
|
|
5029
|
+
appendEvent(workflowDir, event);
|
|
4446
5030
|
try {
|
|
4447
5031
|
invalidator?.(input.harnessDir);
|
|
4448
5032
|
} catch (error) {
|
|
@@ -4454,7 +5038,10 @@ function recordSettle(input) {
|
|
|
4454
5038
|
}
|
|
4455
5039
|
function recordWorkflowEvent(input) {
|
|
4456
5040
|
try {
|
|
4457
|
-
|
|
5041
|
+
const workflowDir = input.workflowDir ?? resolveAgentFlowWriteDir(input.harnessDir);
|
|
5042
|
+
if (workflowDir === null)
|
|
5043
|
+
return false;
|
|
5044
|
+
appendEvent(workflowDir, input.event);
|
|
4458
5045
|
try {
|
|
4459
5046
|
invalidator?.(input.harnessDir);
|
|
4460
5047
|
} catch (error) {
|
|
@@ -4468,6 +5055,9 @@ function recordWorkflowEvent(input) {
|
|
|
4468
5055
|
}
|
|
4469
5056
|
function recordWorkflowVerdict(input) {
|
|
4470
5057
|
try {
|
|
5058
|
+
const workflowDir = resolveAgentFlowWriteDir(input.harnessDir);
|
|
5059
|
+
if (workflowDir === null)
|
|
5060
|
+
return;
|
|
4471
5061
|
const agent = input.exec !== undefined ? agentOfExec(input.exec) : undefined;
|
|
4472
5062
|
const event = {
|
|
4473
5063
|
v: 1,
|
|
@@ -4481,7 +5071,7 @@ function recordWorkflowVerdict(input) {
|
|
|
4481
5071
|
verdict: input.verdict,
|
|
4482
5072
|
...input.code !== undefined && input.code !== "" ? { code: input.code } : {}
|
|
4483
5073
|
};
|
|
4484
|
-
appendEvent(
|
|
5074
|
+
appendEvent(workflowDir, event);
|
|
4485
5075
|
try {
|
|
4486
5076
|
invalidator?.(input.harnessDir);
|
|
4487
5077
|
} catch (error) {
|
|
@@ -4706,14 +5296,14 @@ function summaryOf(events) {
|
|
|
4706
5296
|
return { role, outcome, count };
|
|
4707
5297
|
}).sort((a, b) => b.count - a.count || a.role.localeCompare(b.role) || a.outcome.localeCompare(b.outcome));
|
|
4708
5298
|
}
|
|
4709
|
-
function readAgentFlow(
|
|
4710
|
-
const file = join10(
|
|
4711
|
-
if (!
|
|
5299
|
+
function readAgentFlow(workflowDir, limit) {
|
|
5300
|
+
const file = join10(workflowDir, AGENT_FLOW_FILE);
|
|
5301
|
+
if (!existsSync11(file)) {
|
|
4712
5302
|
return { events: [], summary: [] };
|
|
4713
5303
|
}
|
|
4714
5304
|
let content;
|
|
4715
5305
|
try {
|
|
4716
|
-
content =
|
|
5306
|
+
content = readFileSync7(file, "utf8");
|
|
4717
5307
|
} catch {
|
|
4718
5308
|
return null;
|
|
4719
5309
|
}
|
|
@@ -4740,6 +5330,7 @@ function readAgentFlow(harnessDir, limit) {
|
|
|
4740
5330
|
function recordSettleWithRef(ref, outcome, durationMs) {
|
|
4741
5331
|
recordSettle({
|
|
4742
5332
|
harnessDir: ref.harnessDir,
|
|
5333
|
+
workflowDir: ref.workflowDir,
|
|
4743
5334
|
...ref.agent !== undefined && ref.agent.trim() !== "" ? { agent: ref.agent } : {},
|
|
4744
5335
|
outcome,
|
|
4745
5336
|
...durationMs !== undefined && Number.isFinite(durationMs) ? { durationMs } : {},
|
|
@@ -4832,7 +5423,7 @@ function residualSeverityIndex(severity) {
|
|
|
4832
5423
|
function pluginVersion() {
|
|
4833
5424
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
4834
5425
|
try {
|
|
4835
|
-
const pkg = JSON.parse(
|
|
5426
|
+
const pkg = JSON.parse(readFileSync9(new URL(rel, import.meta.url), "utf8"));
|
|
4836
5427
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
4837
5428
|
return pkg.version;
|
|
4838
5429
|
} catch {}
|
|
@@ -4846,7 +5437,7 @@ function engineStatusSource(harnessDir) {
|
|
|
4846
5437
|
form: "catalog",
|
|
4847
5438
|
version: pluginVersion(),
|
|
4848
5439
|
harnessDir,
|
|
4849
|
-
enforcement: harnessDir !== null ?
|
|
5440
|
+
enforcement: harnessDir !== null ? resolveRepoEnforcement(harnessDir) : { hard: false, source: "none" },
|
|
4850
5441
|
...iteration !== undefined ? { iteration } : {},
|
|
4851
5442
|
state: harnessDir !== null ? harnessStateSource(harnessDir) : null
|
|
4852
5443
|
};
|
|
@@ -4904,28 +5495,36 @@ function renderEngineStatusCatalog(source) {
|
|
|
4904
5495
|
}
|
|
4905
5496
|
const state = source.state;
|
|
4906
5497
|
if (state !== null) {
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
state.
|
|
4915
|
-
state.
|
|
4916
|
-
state.
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
5498
|
+
if (state.selection.kind === "error") {
|
|
5499
|
+
lines.push(`workflow selection: ERROR (${state.selection.code}) ${state.selection.message}`);
|
|
5500
|
+
} else {
|
|
5501
|
+
lines.push(`workflow: ${state.selection.workflowId} (${state.selection.kind})`);
|
|
5502
|
+
if (state.selection.kind === "active" && state.selection.warning !== undefined) {
|
|
5503
|
+
lines.push(`workflow warning: ${state.selection.warning.code} ${state.selection.warning.message}`);
|
|
5504
|
+
}
|
|
5505
|
+
lines.push(`plans: ${state.plans.length === 0 ? "none registered" : state.plans.map((p) => `${p.id}(${p.status})`).join(" ")}`);
|
|
5506
|
+
lines.push(`residuals: ${state.residuals.length === 0 ? "none open" : state.residuals.map((r) => `${r.severity} ${r.count}`).join(", ")}`);
|
|
5507
|
+
if (state.iterationBaseBranch !== null && state.targetBranch !== null) {
|
|
5508
|
+
const integration = state.specIntegrationBranch !== null ? ` (spec integration: ${state.specIntegrationBranch})` : "";
|
|
5509
|
+
lines.push(`branch: ${state.iterationBaseBranch} → ${state.targetBranch}${integration}`);
|
|
5510
|
+
}
|
|
5511
|
+
const policy = [
|
|
5512
|
+
state.pushPolicy !== null ? `push ${state.pushPolicy}` : null,
|
|
5513
|
+
state.worktreeMode !== null ? `worktree ${state.worktreeMode}` : null,
|
|
5514
|
+
state.controlWorktreePath !== null ? `control ${state.controlWorktreePath}` : null
|
|
5515
|
+
].filter((part) => part !== null).join("; ");
|
|
5516
|
+
if (policy !== "")
|
|
5517
|
+
lines.push(`policy: ${policy}`);
|
|
5518
|
+
lines.push(`leases: ${state.leases.length === 0 ? "none active" : state.leases.map((l) => `${l.planId} → ${l.holder}${l.worktreePath !== null ? ` (${l.worktreePath})` : ""}`).join("; ")}`);
|
|
5519
|
+
if (state.knowledge !== null) {
|
|
5520
|
+
lines.push(`knowledge: ${state.knowledge.docCount} doc${state.knowledge.docCount === 1 ? "" : "s"} (${state.knowledge.categories.join(", ")})`);
|
|
5521
|
+
}
|
|
5522
|
+
if (state.direction !== null)
|
|
5523
|
+
lines.push(`direction: ${state.direction}`);
|
|
5524
|
+
const flow = state.agentFlow;
|
|
5525
|
+
if (flow !== null && flow.events.length > 0) {
|
|
5526
|
+
lines.push(renderAgentFlowLine(flow));
|
|
5527
|
+
}
|
|
4929
5528
|
}
|
|
4930
5529
|
}
|
|
4931
5530
|
lines.push("</mstar_engine_status>");
|
|
@@ -4953,28 +5552,59 @@ function hhmm(ts) {
|
|
|
4953
5552
|
function harnessStateSource(harnessDir) {
|
|
4954
5553
|
if (harnessDir === null)
|
|
4955
5554
|
return null;
|
|
4956
|
-
const statusPath =
|
|
4957
|
-
if (!
|
|
5555
|
+
const statusPath = join11(harnessDir, STATUS_FILE);
|
|
5556
|
+
if (!existsSync12(statusPath))
|
|
4958
5557
|
return null;
|
|
4959
5558
|
try {
|
|
4960
|
-
const
|
|
5559
|
+
const selection = resolveReadWorkflow(harnessDir);
|
|
5560
|
+
const rollup = residualRollup(harnessDir);
|
|
4961
5561
|
const str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
4962
5562
|
const iterationRefsOf = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
5563
|
+
const compass = steeringCompassPath(harnessDir);
|
|
5564
|
+
let compassFields;
|
|
5565
|
+
if (compass !== undefined) {
|
|
5566
|
+
try {
|
|
5567
|
+
compassFields = parseCompassFrontmatter(compass.compassPath);
|
|
5568
|
+
} catch {
|
|
5569
|
+
compassFields = undefined;
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
if (selection.kind === "error") {
|
|
5573
|
+
return selectionErrorState(selection, rollup, harnessDir, compass);
|
|
5574
|
+
}
|
|
5575
|
+
const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5576
|
+
let snapshot;
|
|
5577
|
+
if (!existsSync12(snapshotPath)) {
|
|
5578
|
+
return selectionErrorState({
|
|
5579
|
+
kind: "error",
|
|
5580
|
+
code: "workflow.selection.snapshot-unreadable",
|
|
5581
|
+
message: `cannot read the selected workflow snapshot ${snapshotPath}`
|
|
5582
|
+
}, rollup, harnessDir, compass);
|
|
5583
|
+
}
|
|
5584
|
+
try {
|
|
5585
|
+
snapshot = readJson(snapshotPath);
|
|
5586
|
+
} catch {
|
|
5587
|
+
return selectionErrorState({
|
|
5588
|
+
kind: "error",
|
|
5589
|
+
code: "workflow.selection.snapshot-unreadable",
|
|
5590
|
+
message: `cannot read the selected workflow snapshot ${snapshotPath}`
|
|
5591
|
+
}, rollup, harnessDir, compass);
|
|
5592
|
+
}
|
|
4963
5593
|
const plans = [];
|
|
4964
5594
|
const leases = [];
|
|
4965
|
-
if (Array.isArray(
|
|
4966
|
-
for (const row of
|
|
5595
|
+
if (Array.isArray(snapshot.plans)) {
|
|
5596
|
+
for (const row of snapshot.plans.map(asRecord)) {
|
|
4967
5597
|
if (row === undefined)
|
|
4968
5598
|
continue;
|
|
4969
5599
|
const id = typeof row.plan_id === "string" ? row.plan_id : typeof row.id === "string" ? row.id : undefined;
|
|
4970
5600
|
if (id === undefined)
|
|
4971
5601
|
continue;
|
|
4972
|
-
const
|
|
5602
|
+
const metadata = asRecord(row.metadata);
|
|
4973
5603
|
plans.push({
|
|
4974
5604
|
id,
|
|
4975
5605
|
status: typeof row.status === "string" ? row.status : "",
|
|
4976
5606
|
doneAt: str(row.done_at),
|
|
4977
|
-
iterationRefs: iterationRefsOf(
|
|
5607
|
+
iterationRefs: iterationRefsOf(metadata?.iteration_refs)
|
|
4978
5608
|
});
|
|
4979
5609
|
const lease = asRecord(row.execution_lease);
|
|
4980
5610
|
if (lease !== undefined && typeof lease.holder === "string") {
|
|
@@ -4986,39 +5616,105 @@ function harnessStateSource(harnessDir) {
|
|
|
4986
5616
|
}
|
|
4987
5617
|
}
|
|
4988
5618
|
}
|
|
4989
|
-
const residuals =
|
|
4990
|
-
const
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5619
|
+
const { residuals, residualFindings } = rollup;
|
|
5620
|
+
const branch = asRecord(snapshot.branch);
|
|
5621
|
+
const executionPolicy = asRecord(snapshot.execution_policy);
|
|
5622
|
+
return {
|
|
5623
|
+
selection,
|
|
5624
|
+
plans,
|
|
5625
|
+
residuals,
|
|
5626
|
+
residualFindings,
|
|
5627
|
+
project: projectRollupSource(harnessDir, residuals),
|
|
5628
|
+
iterationBaseBranch: str(branch?.base) ?? str(compassFields?.iteration_base_branch) ?? null,
|
|
5629
|
+
targetBranch: str(branch?.target) ?? str(compassFields?.target_branch) ?? null,
|
|
5630
|
+
specIntegrationBranch: str(branch?.integration),
|
|
5631
|
+
pushPolicy: str(executionPolicy?.push_policy),
|
|
5632
|
+
worktreeMode: str(executionPolicy?.worktree_mode),
|
|
5633
|
+
controlWorktreePath: str(snapshot.control_worktree_path),
|
|
5634
|
+
leases,
|
|
5635
|
+
knowledge: knowledgeDigest(harnessDir),
|
|
5636
|
+
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
5637
|
+
agentFlow: readAgentFlow(join11(harnessDir, selection.dir), 50)
|
|
5638
|
+
};
|
|
5639
|
+
} catch {
|
|
5640
|
+
return null;
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
function selectionErrorState(selection, rollup, harnessDir, compass) {
|
|
5644
|
+
return {
|
|
5645
|
+
selection,
|
|
5646
|
+
plans: [],
|
|
5647
|
+
residuals: [],
|
|
5648
|
+
residualFindings: null,
|
|
5649
|
+
project: projectRollupSource(harnessDir, rollup.residuals),
|
|
5650
|
+
iterationBaseBranch: null,
|
|
5651
|
+
targetBranch: null,
|
|
5652
|
+
specIntegrationBranch: null,
|
|
5653
|
+
pushPolicy: null,
|
|
5654
|
+
worktreeMode: null,
|
|
5655
|
+
controlWorktreePath: null,
|
|
5656
|
+
leases: [],
|
|
5657
|
+
knowledge: knowledgeDigest(harnessDir),
|
|
5658
|
+
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
5659
|
+
agentFlow: null
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
function projectRollupSource(harnessDir, residuals) {
|
|
5663
|
+
const milestones = [];
|
|
5664
|
+
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5665
|
+
if (existsSync12(projectsDir)) {
|
|
5666
|
+
let entries;
|
|
5667
|
+
try {
|
|
5668
|
+
entries = readdirSync7(projectsDir, { withFileTypes: true });
|
|
5669
|
+
} catch {
|
|
5670
|
+
entries = [];
|
|
5671
|
+
}
|
|
5672
|
+
for (const entry of entries) {
|
|
5673
|
+
if (!entry.isDirectory())
|
|
5674
|
+
continue;
|
|
5675
|
+
const roadmapPath = join11(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
|
|
5676
|
+
if (!existsSync12(roadmapPath))
|
|
5677
|
+
continue;
|
|
5678
|
+
try {
|
|
5679
|
+
const doc = parseCompassFrontmatterText(readFileSync9(roadmapPath, "utf8"), roadmapPath);
|
|
5680
|
+
if (Array.isArray(doc.milestones)) {
|
|
5681
|
+
for (const milestone of doc.milestones) {
|
|
5682
|
+
if (typeof milestone === "string" && milestone.trim() !== "")
|
|
5683
|
+
milestones.push(milestone);
|
|
5001
5684
|
}
|
|
5002
5685
|
}
|
|
5003
|
-
}
|
|
5004
|
-
|
|
5005
|
-
const count = counts.get(severity);
|
|
5006
|
-
if (count !== undefined && count > 0)
|
|
5007
|
-
residuals.push({ severity, count });
|
|
5686
|
+
} catch {
|
|
5687
|
+
continue;
|
|
5008
5688
|
}
|
|
5009
5689
|
}
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5690
|
+
}
|
|
5691
|
+
return { milestones, openResiduals: residuals };
|
|
5692
|
+
}
|
|
5693
|
+
function residualRollup(harnessDir) {
|
|
5694
|
+
const registers = projectRegisters(harnessDir);
|
|
5695
|
+
const residuals = [];
|
|
5696
|
+
const counts = new Map;
|
|
5697
|
+
let residualFindings = null;
|
|
5698
|
+
if (registers.length > 0) {
|
|
5699
|
+
residualFindings = [];
|
|
5700
|
+
for (const register of registers) {
|
|
5701
|
+
const entries = asRecord(register.doc.entries);
|
|
5702
|
+
if (entries === undefined)
|
|
5703
|
+
continue;
|
|
5704
|
+
for (const planId of Object.keys(entries)) {
|
|
5705
|
+
const findings = entries[planId];
|
|
5015
5706
|
if (!Array.isArray(findings))
|
|
5016
5707
|
continue;
|
|
5017
5708
|
for (const finding of findings) {
|
|
5018
5709
|
const entry = asRecord(finding);
|
|
5019
|
-
if (entry === undefined
|
|
5710
|
+
if (entry === undefined)
|
|
5711
|
+
continue;
|
|
5712
|
+
if (!isOpenResidualParity(entry))
|
|
5020
5713
|
continue;
|
|
5021
5714
|
const severity = entry.severity;
|
|
5715
|
+
if (typeof severity === "string" && RESIDUAL_SEVERITIES.includes(severity)) {
|
|
5716
|
+
counts.set(severity, (counts.get(severity) ?? 0) + 1);
|
|
5717
|
+
}
|
|
5022
5718
|
if (typeof severity !== "string" || residualSeverityIndex(severity) === -1)
|
|
5023
5719
|
continue;
|
|
5024
5720
|
residualFindings.push({
|
|
@@ -5029,46 +5725,50 @@ function harnessStateSource(harnessDir) {
|
|
|
5029
5725
|
});
|
|
5030
5726
|
}
|
|
5031
5727
|
}
|
|
5032
|
-
residualFindings.sort((a, b) => residualSeverityIndex(a.severity) - residualSeverityIndex(b.severity));
|
|
5033
|
-
residualFindings = residualFindings.slice(0, 10);
|
|
5034
5728
|
}
|
|
5035
|
-
const
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
try {
|
|
5040
|
-
compassFields = parseCompassFrontmatter(compass.compassPath);
|
|
5041
|
-
} catch {
|
|
5042
|
-
compassFields = undefined;
|
|
5043
|
-
}
|
|
5729
|
+
for (const severity of RESIDUAL_SEVERITIES) {
|
|
5730
|
+
const count = counts.get(severity);
|
|
5731
|
+
if (count !== undefined && count > 0)
|
|
5732
|
+
residuals.push({ severity, count });
|
|
5044
5733
|
}
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
5058
|
-
agentFlow: readAgentFlow(harnessDir, 50)
|
|
5059
|
-
};
|
|
5734
|
+
residualFindings.sort((a, b) => residualSeverityIndex(a.severity) - residualSeverityIndex(b.severity));
|
|
5735
|
+
residualFindings = residualFindings.slice(0, 10);
|
|
5736
|
+
}
|
|
5737
|
+
return { residuals, residualFindings };
|
|
5738
|
+
}
|
|
5739
|
+
function projectRegisters(harnessDir) {
|
|
5740
|
+
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5741
|
+
if (!existsSync12(projectsDir))
|
|
5742
|
+
return [];
|
|
5743
|
+
let entries;
|
|
5744
|
+
try {
|
|
5745
|
+
entries = readdirSync7(projectsDir, { withFileTypes: true });
|
|
5060
5746
|
} catch {
|
|
5061
|
-
return
|
|
5747
|
+
return [];
|
|
5748
|
+
}
|
|
5749
|
+
const registers = [];
|
|
5750
|
+
for (const entry of entries) {
|
|
5751
|
+
if (!entry.isDirectory())
|
|
5752
|
+
continue;
|
|
5753
|
+
const registerPath = join11(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5754
|
+
if (!existsSync12(registerPath))
|
|
5755
|
+
continue;
|
|
5756
|
+
try {
|
|
5757
|
+
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
5758
|
+
} catch {
|
|
5759
|
+
continue;
|
|
5760
|
+
}
|
|
5062
5761
|
}
|
|
5762
|
+
return registers;
|
|
5063
5763
|
}
|
|
5064
5764
|
function knowledgeDigest(harnessDir) {
|
|
5065
|
-
const indexPath =
|
|
5066
|
-
if (!
|
|
5765
|
+
const indexPath = join11(harnessDir, "knowledge", "README.md");
|
|
5766
|
+
if (!existsSync12(indexPath))
|
|
5067
5767
|
return null;
|
|
5068
5768
|
try {
|
|
5069
5769
|
const categories = new Set;
|
|
5070
5770
|
let docCount = 0;
|
|
5071
|
-
for (const line of
|
|
5771
|
+
for (const line of readFileSync9(indexPath, "utf8").split(/\r?\n/)) {
|
|
5072
5772
|
const row = line.trim().match(/^\|(.+)\|$/);
|
|
5073
5773
|
if (row === null)
|
|
5074
5774
|
continue;
|
|
@@ -5091,7 +5791,7 @@ function knowledgeDigest(harnessDir) {
|
|
|
5091
5791
|
}
|
|
5092
5792
|
function compassDirection(compassPath) {
|
|
5093
5793
|
try {
|
|
5094
|
-
const content =
|
|
5794
|
+
const content = readFileSync9(compassPath, "utf8");
|
|
5095
5795
|
const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
|
|
5096
5796
|
if (section === null)
|
|
5097
5797
|
return null;
|
|
@@ -5108,23 +5808,23 @@ function compassDirection(compassPath) {
|
|
|
5108
5808
|
}
|
|
5109
5809
|
function steeringCompassPath(harnessDir) {
|
|
5110
5810
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
5111
|
-
if (!
|
|
5811
|
+
if (!existsSync12(iterationsDir))
|
|
5112
5812
|
return;
|
|
5113
5813
|
let entries;
|
|
5114
5814
|
try {
|
|
5115
|
-
entries =
|
|
5815
|
+
entries = readdirSync7(iterationsDir, { withFileTypes: true });
|
|
5116
5816
|
} catch {
|
|
5117
5817
|
return;
|
|
5118
5818
|
}
|
|
5119
5819
|
for (const entry of entries) {
|
|
5120
5820
|
if (!entry.isDirectory())
|
|
5121
5821
|
continue;
|
|
5122
|
-
const compassPath =
|
|
5123
|
-
if (!
|
|
5822
|
+
const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
|
|
5823
|
+
if (!existsSync12(compassPath))
|
|
5124
5824
|
continue;
|
|
5125
5825
|
let content;
|
|
5126
5826
|
try {
|
|
5127
|
-
content =
|
|
5827
|
+
content = readFileSync9(compassPath, "utf8");
|
|
5128
5828
|
} catch {
|
|
5129
5829
|
continue;
|
|
5130
5830
|
}
|
|
@@ -5138,16 +5838,20 @@ function steeringCompassPath(harnessDir) {
|
|
|
5138
5838
|
function iterationGateSource(harnessDir) {
|
|
5139
5839
|
if (harnessDir === null)
|
|
5140
5840
|
return;
|
|
5141
|
-
const statusPath =
|
|
5142
|
-
if (!
|
|
5841
|
+
const statusPath = join11(harnessDir, STATUS_FILE);
|
|
5842
|
+
if (!existsSync12(statusPath))
|
|
5143
5843
|
return;
|
|
5144
5844
|
const compass = steeringCompassPath(harnessDir);
|
|
5145
5845
|
if (compass === undefined)
|
|
5146
5846
|
return;
|
|
5847
|
+
const selection = resolveReadWorkflow(harnessDir);
|
|
5848
|
+
if (selection.kind === "error")
|
|
5849
|
+
return;
|
|
5850
|
+
const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5147
5851
|
try {
|
|
5148
|
-
const
|
|
5852
|
+
const snapshotDoc = readJson(snapshotPath);
|
|
5149
5853
|
const compassDoc = parseCompassFrontmatter(compass.compassPath);
|
|
5150
|
-
const result = evaluatePhaseGate(
|
|
5854
|
+
const result = evaluatePhaseGate(snapshotDoc, compassDoc);
|
|
5151
5855
|
const gate2 = {
|
|
5152
5856
|
transition: result.transition,
|
|
5153
5857
|
all_plans_done: result.allPlansDone,
|
|
@@ -5159,7 +5863,7 @@ function iterationGateSource(harnessDir) {
|
|
|
5159
5863
|
const compassStatus = compassDoc.status === "active" || compassDoc.status === "locked" ? compassDoc.status : undefined;
|
|
5160
5864
|
return {
|
|
5161
5865
|
iterationId: compass.iterationId,
|
|
5162
|
-
statusPath,
|
|
5866
|
+
statusPath: snapshotPath,
|
|
5163
5867
|
compassPath: compass.compassPath,
|
|
5164
5868
|
gate: gate2,
|
|
5165
5869
|
...compassStatus !== undefined ? { compassStatus } : {}
|
|
@@ -5197,44 +5901,103 @@ function agentDigestKey(agent, cwd) {
|
|
|
5197
5901
|
}
|
|
5198
5902
|
|
|
5199
5903
|
// src/gates/status.ts
|
|
5200
|
-
import {
|
|
5904
|
+
import { existsSync as existsSync13, readdirSync as readdirSync9 } from "node:fs";
|
|
5905
|
+
import { basename as basename7, join as join14, relative as relative5, resolve as resolve13 } from "node:path";
|
|
5201
5906
|
var LOGGER_NAME = "mstar/status-gate";
|
|
5202
|
-
function
|
|
5203
|
-
|
|
5907
|
+
function harnessDocKindOfTarget(harnessDir, targetPath) {
|
|
5908
|
+
const resolved = resolve13(targetPath);
|
|
5909
|
+
const name = basename7(resolved);
|
|
5910
|
+
if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
|
|
5911
|
+
return null;
|
|
5912
|
+
const rel = relative5(harnessDir, resolved);
|
|
5913
|
+
if (name === STATUS_FILE && rel === STATUS_FILE)
|
|
5914
|
+
return "status";
|
|
5915
|
+
let workflowDir;
|
|
5916
|
+
let projectDir;
|
|
5917
|
+
try {
|
|
5918
|
+
workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
5919
|
+
projectDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5920
|
+
} catch {
|
|
5921
|
+
workflowDir = join14(harnessDir, "workflows");
|
|
5922
|
+
projectDir = join14(harnessDir, "projects");
|
|
5923
|
+
}
|
|
5924
|
+
if (name === WORKFLOW_SNAPSHOT_FILE && /^[^/]+\/snapshot\.json$/.test(relative5(workflowDir, resolved)))
|
|
5925
|
+
return "snapshot";
|
|
5926
|
+
if (name === PROJECT_REGISTER_FILE && /^[^/]+\/residuals\.json$/.test(relative5(projectDir, resolved)))
|
|
5927
|
+
return "register";
|
|
5928
|
+
return null;
|
|
5204
5929
|
}
|
|
5205
5930
|
function resolveHard(harnessDir, config) {
|
|
5206
5931
|
if (config.enforcement === "hard")
|
|
5207
5932
|
return true;
|
|
5208
5933
|
if (config.enforcement === "soft")
|
|
5209
5934
|
return false;
|
|
5210
|
-
return
|
|
5935
|
+
return resolveRepoEnforcement(harnessDir).hard;
|
|
5936
|
+
}
|
|
5937
|
+
function validateStatusValue(doc, kind, harnessDir) {
|
|
5938
|
+
if (kind === "snapshot") {
|
|
5939
|
+
const base = validateWorkflowSnapshot(doc);
|
|
5940
|
+
if (!base.ok)
|
|
5941
|
+
return base;
|
|
5942
|
+
if (harnessDir === null || harnessDir === undefined)
|
|
5943
|
+
return base;
|
|
5944
|
+
const violations = snapshotFindingsCleanupViolations(doc, harnessDir);
|
|
5945
|
+
if (violations.length === 0)
|
|
5946
|
+
return base;
|
|
5947
|
+
return { ok: false, violations };
|
|
5948
|
+
}
|
|
5949
|
+
if (kind === "register")
|
|
5950
|
+
return validateProjectRegister(doc);
|
|
5951
|
+
return validateStatus(doc);
|
|
5211
5952
|
}
|
|
5212
|
-
function
|
|
5213
|
-
const base = validateStatus(doc);
|
|
5214
|
-
if (!base.ok)
|
|
5215
|
-
return base;
|
|
5953
|
+
function snapshotFindingsCleanupViolations(doc, harnessDir) {
|
|
5216
5954
|
const record = asRecord(doc);
|
|
5217
5955
|
if (record === undefined)
|
|
5218
|
-
return
|
|
5956
|
+
return [];
|
|
5219
5957
|
const violations = [];
|
|
5958
|
+
const registers = projectRegisterDocs(harnessDir);
|
|
5220
5959
|
for (const row of Array.isArray(record.plans) ? record.plans : []) {
|
|
5221
|
-
const
|
|
5960
|
+
const planRow = asRecord(row);
|
|
5961
|
+
const metadata = planRow === undefined ? undefined : asRecord(planRow.metadata);
|
|
5222
5962
|
const mode = metadata?.["findings_cleanup"];
|
|
5223
5963
|
if (mode !== "zero-residual" && mode !== "allow-residual")
|
|
5224
5964
|
continue;
|
|
5225
|
-
const planId = typeof
|
|
5965
|
+
const planId = typeof planRow?.id === "string" ? planRow.id : typeof planRow?.plan_id === "string" ? planRow.plan_id : undefined;
|
|
5226
5966
|
if (planId === undefined)
|
|
5227
5967
|
continue;
|
|
5228
|
-
|
|
5968
|
+
for (const register of registers) {
|
|
5969
|
+
violations.push(...findingsCleanupGate(register.doc, planId, { mode }).violations);
|
|
5970
|
+
}
|
|
5229
5971
|
}
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5972
|
+
return violations;
|
|
5973
|
+
}
|
|
5974
|
+
function projectRegisterDocs(harnessDir) {
|
|
5975
|
+
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5976
|
+
let entries;
|
|
5977
|
+
try {
|
|
5978
|
+
entries = readdirSync9(projectsDir, { withFileTypes: true });
|
|
5979
|
+
} catch {
|
|
5980
|
+
return [];
|
|
5981
|
+
}
|
|
5982
|
+
const registers = [];
|
|
5983
|
+
for (const entry of entries) {
|
|
5984
|
+
if (!entry.isDirectory())
|
|
5985
|
+
continue;
|
|
5986
|
+
const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5987
|
+
if (!existsSync13(registerPath))
|
|
5988
|
+
continue;
|
|
5989
|
+
try {
|
|
5990
|
+
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
5991
|
+
} catch {
|
|
5992
|
+
continue;
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
return registers;
|
|
5233
5996
|
}
|
|
5234
|
-
function validateStatusDoc(
|
|
5997
|
+
function validateStatusDoc(path, kind, harnessDir) {
|
|
5235
5998
|
let doc;
|
|
5236
5999
|
try {
|
|
5237
|
-
doc = readJson(
|
|
6000
|
+
doc = readJson(path);
|
|
5238
6001
|
} catch (error) {
|
|
5239
6002
|
return {
|
|
5240
6003
|
ok: false,
|
|
@@ -5246,16 +6009,16 @@ function validateStatusDoc(statusPath) {
|
|
|
5246
6009
|
}]
|
|
5247
6010
|
};
|
|
5248
6011
|
}
|
|
5249
|
-
return validateStatusValue(doc);
|
|
6012
|
+
return validateStatusValue(doc, kind, harnessDir);
|
|
5250
6013
|
}
|
|
5251
6014
|
function gateStatusIntent(ctx, harnessDir, config, adapter, operation, target) {
|
|
5252
6015
|
try {
|
|
5253
6016
|
if (harnessDir === null)
|
|
5254
6017
|
return;
|
|
5255
|
-
|
|
6018
|
+
const kind = harnessDocKindOfTarget(harnessDir, target.displayPath);
|
|
6019
|
+
if (kind === null)
|
|
5256
6020
|
return;
|
|
5257
|
-
const
|
|
5258
|
-
const result = adapter.statusGate(statusPath);
|
|
6021
|
+
const result = adapter.statusGate(target.displayPath, kind, harnessDir);
|
|
5259
6022
|
const hard = resolveHard(harnessDir, config);
|
|
5260
6023
|
const verdict = applyEnforcement(result, { hard });
|
|
5261
6024
|
if (!verdict.ok) {
|
|
@@ -5290,8 +6053,8 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
|
|
|
5290
6053
|
}
|
|
5291
6054
|
|
|
5292
6055
|
// src/gates/skill-lint.ts
|
|
5293
|
-
import { existsSync as
|
|
5294
|
-
import { basename as basename8, dirname as dirname7, resolve as
|
|
6056
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
6057
|
+
import { basename as basename8, dirname as dirname7, resolve as resolve14, sep as sep2 } from "node:path";
|
|
5295
6058
|
var SKILL_LINT_LOGGER = "mstar/skill-lint";
|
|
5296
6059
|
|
|
5297
6060
|
class SkillLintVetoError extends Error {
|
|
@@ -5354,8 +6117,8 @@ function skillRootsOf(config) {
|
|
|
5354
6117
|
function isSkillTarget(roots, target) {
|
|
5355
6118
|
if (basename8(target.displayPath) !== "SKILL.md")
|
|
5356
6119
|
return false;
|
|
5357
|
-
const resolvedPath =
|
|
5358
|
-
return roots.some((root) => resolvedPath.startsWith(
|
|
6120
|
+
const resolvedPath = resolve14(target.displayPath);
|
|
6121
|
+
return roots.some((root) => resolvedPath.startsWith(resolve14(root) + sep2));
|
|
5359
6122
|
}
|
|
5360
6123
|
function skillNameOf(target) {
|
|
5361
6124
|
return basename8(dirname7(target.displayPath));
|
|
@@ -5370,12 +6133,12 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
|
|
|
5370
6133
|
return;
|
|
5371
6134
|
if (!isSkillTarget(roots, target))
|
|
5372
6135
|
return;
|
|
5373
|
-
const skillPath =
|
|
5374
|
-
if (!
|
|
6136
|
+
const skillPath = resolve14(target.displayPath);
|
|
6137
|
+
if (!existsSync14(skillPath))
|
|
5375
6138
|
return;
|
|
5376
6139
|
let doc;
|
|
5377
6140
|
try {
|
|
5378
|
-
doc =
|
|
6141
|
+
doc = readFileSync11(skillPath, "utf8");
|
|
5379
6142
|
} catch (error) {
|
|
5380
6143
|
ctx.logger(SKILL_LINT_LOGGER).error(`skill lint degraded to allow (cannot read ${skillPath}): ${error.message}`);
|
|
5381
6144
|
ctx.emit("mstar/skill-lint", {
|
|
@@ -5422,8 +6185,8 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
|
|
|
5422
6185
|
}
|
|
5423
6186
|
|
|
5424
6187
|
// src/gates/seams.ts
|
|
5425
|
-
import { existsSync as
|
|
5426
|
-
import { basename as basename9, dirname as
|
|
6188
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
|
|
6189
|
+
import { basename as basename9, dirname as dirname9, join as join16, resolve as resolve15, sep as sep4 } from "node:path";
|
|
5427
6190
|
var SEAM_LOGGERS = {
|
|
5428
6191
|
"design-md": "mstar/design-md-lint",
|
|
5429
6192
|
audit: "mstar/audit-lint",
|
|
@@ -5440,7 +6203,7 @@ function isMarkdownDoc(path) {
|
|
|
5440
6203
|
function isAuditPlanTarget(path) {
|
|
5441
6204
|
if (!isMarkdownDoc(path))
|
|
5442
6205
|
return false;
|
|
5443
|
-
const segments =
|
|
6206
|
+
const segments = resolve15(path).split(sep4);
|
|
5444
6207
|
for (let i = 0;i < segments.length; i++) {
|
|
5445
6208
|
if (segments[i] !== "plans")
|
|
5446
6209
|
continue;
|
|
@@ -5451,7 +6214,7 @@ function isAuditPlanTarget(path) {
|
|
|
5451
6214
|
return false;
|
|
5452
6215
|
}
|
|
5453
6216
|
function isRolesTarget(path) {
|
|
5454
|
-
const segments =
|
|
6217
|
+
const segments = resolve15(path).split(sep4);
|
|
5455
6218
|
const idx = segments.indexOf("mstar-roles");
|
|
5456
6219
|
if (idx === -1)
|
|
5457
6220
|
return false;
|
|
@@ -5461,14 +6224,14 @@ function isRolesTarget(path) {
|
|
|
5461
6224
|
return rest[0] === "SKILL.md" || rest[0] === "references" && rest[rest.length - 1].endsWith(".md");
|
|
5462
6225
|
}
|
|
5463
6226
|
function rolesDirOf(path) {
|
|
5464
|
-
const segments =
|
|
6227
|
+
const segments = resolve15(path).split(sep4);
|
|
5465
6228
|
const idx = segments.indexOf("mstar-roles");
|
|
5466
6229
|
if (idx === -1)
|
|
5467
|
-
return
|
|
5468
|
-
return idx === 0 ?
|
|
6230
|
+
return dirname9(path);
|
|
6231
|
+
return idx === 0 ? sep4 : join16(sep4, ...segments.slice(1, idx + 1));
|
|
5469
6232
|
}
|
|
5470
6233
|
function isSeamTarget(seam, harnessDir, target) {
|
|
5471
|
-
const path =
|
|
6234
|
+
const path = resolve15(target.displayPath);
|
|
5472
6235
|
switch (seam) {
|
|
5473
6236
|
case "design-md": {
|
|
5474
6237
|
const base = basename9(path);
|
|
@@ -5479,7 +6242,7 @@ function isSeamTarget(seam, harnessDir, target) {
|
|
|
5479
6242
|
case "compound": {
|
|
5480
6243
|
if (harnessDir === null)
|
|
5481
6244
|
return false;
|
|
5482
|
-
return path.startsWith(
|
|
6245
|
+
return path.startsWith(resolve15(join16(harnessDir, "knowledge")) + sep4) && isMarkdownDoc(path);
|
|
5483
6246
|
}
|
|
5484
6247
|
case "roles":
|
|
5485
6248
|
return isRolesTarget(path);
|
|
@@ -5497,10 +6260,10 @@ function auditSecretsViolations(findings, file) {
|
|
|
5497
6260
|
function validateDesignDoc(doc, path) {
|
|
5498
6261
|
const violations = [...validateDesignTokenFrontmatter(doc).violations];
|
|
5499
6262
|
const isDark = basename9(path) === "DESIGN.dark.md";
|
|
5500
|
-
const sibling =
|
|
5501
|
-
if (
|
|
5502
|
-
const light = isDark ?
|
|
5503
|
-
const dark = isDark ? doc :
|
|
6263
|
+
const sibling = join16(dirname9(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
|
|
6264
|
+
if (existsSync15(sibling)) {
|
|
6265
|
+
const light = isDark ? readFileSync13(sibling, "utf8") : doc;
|
|
6266
|
+
const dark = isDark ? doc : readFileSync13(sibling, "utf8");
|
|
5504
6267
|
violations.push(...assertLightDarkParity(light, dark).violations);
|
|
5505
6268
|
}
|
|
5506
6269
|
return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
|
|
@@ -5514,21 +6277,21 @@ function validateAuditDoc(doc, path) {
|
|
|
5514
6277
|
function validateCompoundDoc(doc, _path, harnessDir) {
|
|
5515
6278
|
const violations = [...validateSchemaYaml(doc).violations];
|
|
5516
6279
|
if (harnessDir !== null) {
|
|
5517
|
-
violations.push(...referenceExists(
|
|
6280
|
+
violations.push(...referenceExists(dirname9(harnessDir), doc).violations);
|
|
5518
6281
|
}
|
|
5519
6282
|
return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
|
|
5520
6283
|
}
|
|
5521
|
-
function validateRolesState(rolesDir, skillsRoot =
|
|
6284
|
+
function validateRolesState(rolesDir, skillsRoot = dirname9(rolesDir)) {
|
|
5522
6285
|
const violations = [...validateRoleMapping(rolesDir).violations];
|
|
5523
6286
|
const skillTexts = {};
|
|
5524
|
-
for (const entry of
|
|
6287
|
+
for (const entry of readdirSync11(skillsRoot, { withFileTypes: true })) {
|
|
5525
6288
|
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
5526
6289
|
continue;
|
|
5527
|
-
const skillFile =
|
|
5528
|
-
if (!
|
|
6290
|
+
const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
|
|
6291
|
+
if (!existsSync15(skillFile))
|
|
5529
6292
|
continue;
|
|
5530
6293
|
try {
|
|
5531
|
-
skillTexts[entry.name] =
|
|
6294
|
+
skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
|
|
5532
6295
|
} catch {}
|
|
5533
6296
|
}
|
|
5534
6297
|
violations.push(...lintLoadOrder(skillTexts).violations);
|
|
@@ -5558,12 +6321,12 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
|
|
|
5558
6321
|
try {
|
|
5559
6322
|
if (!isSeamTarget(seam, harnessDir, target))
|
|
5560
6323
|
return;
|
|
5561
|
-
const path =
|
|
5562
|
-
if (!
|
|
6324
|
+
const path = resolve15(target.displayPath);
|
|
6325
|
+
if (!existsSync15(path))
|
|
5563
6326
|
return;
|
|
5564
6327
|
let doc;
|
|
5565
6328
|
try {
|
|
5566
|
-
doc =
|
|
6329
|
+
doc = readFileSync13(path, "utf8");
|
|
5567
6330
|
} catch (error) {
|
|
5568
6331
|
logger.error(`seam lint degraded to allow (cannot read ${path}): ${error.message}`);
|
|
5569
6332
|
emitSeamAdvisory(ctx, seam, target.displayPath, { ok: true, violations: [] }, false, { degraded: true });
|
|
@@ -5629,8 +6392,8 @@ function lintRolesWrite(doc, options) {
|
|
|
5629
6392
|
}
|
|
5630
6393
|
|
|
5631
6394
|
// src/gates/tools.ts
|
|
5632
|
-
import { existsSync as
|
|
5633
|
-
import { join as
|
|
6395
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
|
|
6396
|
+
import { join as join17, resolve as resolve16 } from "node:path";
|
|
5634
6397
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5635
6398
|
var ITERATION_VIOLATION_SCHEMA = {
|
|
5636
6399
|
type: "object",
|
|
@@ -5727,12 +6490,12 @@ function registerSddIterationTools(ctx, resolver) {
|
|
|
5727
6490
|
}));
|
|
5728
6491
|
toolsCtx.tools.register(defineTool({
|
|
5729
6492
|
name: "mstar_iteration_gate",
|
|
5730
|
-
description: "Evaluate the iteration phase-transition gate against a
|
|
6493
|
+
description: "Evaluate the iteration phase-transition gate against a workflow snapshot and a delivery-compass.md " + "(engine evaluatePhaseGate, mirror of `mstar iteration gate --workflow <id>`): returns the transition " + "(phase-2-execute / phase-3-close / phase-4-pr-delivery), the pass/fail verdict, and the " + "§3.1 entry / §3.5 exit checklists with violation codes.",
|
|
5731
6494
|
parameters: {
|
|
5732
|
-
|
|
6495
|
+
snapshot_path: {
|
|
5733
6496
|
type: "string",
|
|
5734
6497
|
required: true,
|
|
5735
|
-
description: "Path to {HARNESS_DIR}/
|
|
6498
|
+
description: "Path to {HARNESS_DIR}/workflows/<id>/snapshot.json (the selected workflow snapshot — v3 input, mirrors the CLI `iteration gate --workflow <id>`)."
|
|
5736
6499
|
},
|
|
5737
6500
|
compass_path: {
|
|
5738
6501
|
type: "string",
|
|
@@ -5795,13 +6558,13 @@ function registerSddIterationTools(ctx, resolver) {
|
|
|
5795
6558
|
presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
|
|
5796
6559
|
isConcurrencySafe: () => false,
|
|
5797
6560
|
async execute(args) {
|
|
5798
|
-
if (!
|
|
5799
|
-
throw new Error(`
|
|
5800
|
-
if (!
|
|
6561
|
+
if (!existsSync16(args.snapshot_path))
|
|
6562
|
+
throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
|
|
6563
|
+
if (!existsSync16(args.compass_path))
|
|
5801
6564
|
throw new Error(`compass file not found: ${args.compass_path}`);
|
|
5802
|
-
const
|
|
6565
|
+
const snapshotDoc = readJson(args.snapshot_path);
|
|
5803
6566
|
const compassDoc = parseCompassFrontmatter(args.compass_path);
|
|
5804
|
-
const result = evaluatePhaseGate(
|
|
6567
|
+
const result = evaluatePhaseGate(snapshotDoc, compassDoc, {
|
|
5805
6568
|
currentBranch: args.branch,
|
|
5806
6569
|
specIntegrationBranch: args.integration,
|
|
5807
6570
|
prBaseBranch: args.target
|
|
@@ -5850,11 +6613,11 @@ function registerSeamTools(ctx, resolver) {
|
|
|
5850
6613
|
presentCall: (args) => ({ card: "generic", title: "Validate DESIGN.md", kind: "other", rawInput: args.dir }),
|
|
5851
6614
|
isConcurrencySafe: () => false,
|
|
5852
6615
|
async execute(args) {
|
|
5853
|
-
const abs =
|
|
5854
|
-
const lightPath =
|
|
5855
|
-
if (!
|
|
6616
|
+
const abs = resolve16(args.dir);
|
|
6617
|
+
const lightPath = join17(abs, "DESIGN.md");
|
|
6618
|
+
if (!existsSync16(lightPath))
|
|
5856
6619
|
throw new Error(`design file not found: ${lightPath}`);
|
|
5857
|
-
const light =
|
|
6620
|
+
const light = readFileSync14(lightPath, "utf8");
|
|
5858
6621
|
const result = validateDesignDoc(light, lightPath);
|
|
5859
6622
|
const level = completenessLevel(light);
|
|
5860
6623
|
return {
|
|
@@ -5904,10 +6667,10 @@ function registerSeamTools(ctx, resolver) {
|
|
|
5904
6667
|
presentCall: (args) => ({ card: "generic", title: "Validate audit plan", kind: "other", rawInput: args.plan_path }),
|
|
5905
6668
|
isConcurrencySafe: () => false,
|
|
5906
6669
|
async execute(args) {
|
|
5907
|
-
const abs =
|
|
5908
|
-
if (!
|
|
6670
|
+
const abs = resolve16(args.plan_path);
|
|
6671
|
+
if (!existsSync16(abs))
|
|
5909
6672
|
throw new Error(`plan file not found: ${abs}`);
|
|
5910
|
-
const text =
|
|
6673
|
+
const text = readFileSync14(abs, "utf8");
|
|
5911
6674
|
const result = validateAuditDoc(text, abs);
|
|
5912
6675
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView), secrets: result.findings };
|
|
5913
6676
|
}
|
|
@@ -5944,17 +6707,17 @@ function registerSeamTools(ctx, resolver) {
|
|
|
5944
6707
|
presentCall: (args) => ({ card: "generic", title: "Validate knowledge doc", kind: "other", rawInput: args.doc_path }),
|
|
5945
6708
|
isConcurrencySafe: () => false,
|
|
5946
6709
|
async execute(args, exec) {
|
|
5947
|
-
const abs =
|
|
5948
|
-
if (!
|
|
6710
|
+
const abs = resolve16(args.doc_path);
|
|
6711
|
+
if (!existsSync16(abs))
|
|
5949
6712
|
throw new Error(`knowledge doc not found: ${abs}`);
|
|
5950
|
-
const text =
|
|
6713
|
+
const text = readFileSync14(abs, "utf8");
|
|
5951
6714
|
const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
|
|
5952
6715
|
const violations = [...base.violations];
|
|
5953
6716
|
if (args.repo_root !== undefined) {
|
|
5954
|
-
violations.push(...referenceExists(
|
|
6717
|
+
violations.push(...referenceExists(resolve16(args.repo_root), text).violations);
|
|
5955
6718
|
}
|
|
5956
6719
|
if (args.knowledge_dir !== undefined) {
|
|
5957
|
-
const knowledgeDir =
|
|
6720
|
+
const knowledgeDir = resolve16(args.knowledge_dir);
|
|
5958
6721
|
violations.push(...assertIndexRows(knowledgeDir).violations);
|
|
5959
6722
|
violations.push(...scopeGuard(abs, [knowledgeDir]).violations);
|
|
5960
6723
|
}
|
|
@@ -5989,10 +6752,10 @@ function registerSeamTools(ctx, resolver) {
|
|
|
5989
6752
|
presentCall: (args) => ({ card: "generic", title: "Validate roles mapping", kind: "other", rawInput: args.roles_dir }),
|
|
5990
6753
|
isConcurrencySafe: () => false,
|
|
5991
6754
|
async execute(args) {
|
|
5992
|
-
const rolesDir =
|
|
5993
|
-
if (!
|
|
6755
|
+
const rolesDir = resolve16(args.roles_dir);
|
|
6756
|
+
if (!existsSync16(join17(rolesDir, "SKILL.md")))
|
|
5994
6757
|
throw new Error(`roles dir not found: ${rolesDir}`);
|
|
5995
|
-
const result = validateRolesState(rolesDir, args.skills_root !== undefined ?
|
|
6758
|
+
const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
|
|
5996
6759
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
|
|
5997
6760
|
}
|
|
5998
6761
|
}));
|
|
@@ -6000,9 +6763,15 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6000
6763
|
}
|
|
6001
6764
|
|
|
6002
6765
|
// src/gates/adapter.ts
|
|
6003
|
-
import { existsSync as
|
|
6766
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
6767
|
+
import { join as join18 } from "node:path";
|
|
6004
6768
|
import { Service as Service2 } from "@deepseek-ai/cordis";
|
|
6005
6769
|
var HOST_LOGGER = "mstar/host-adapter";
|
|
6770
|
+
function mergeLeasesMatch(left, right) {
|
|
6771
|
+
const a = left ?? {};
|
|
6772
|
+
const b = right ?? {};
|
|
6773
|
+
return ["holder", "claimed_at", "plan_id", "source_branch", "target_branch"].every((key) => a[key] === b[key]);
|
|
6774
|
+
}
|
|
6006
6775
|
|
|
6007
6776
|
class DshHostAdapter extends Service2 {
|
|
6008
6777
|
host = "dsh";
|
|
@@ -6030,10 +6799,10 @@ class DshHostAdapter extends Service2 {
|
|
|
6030
6799
|
log(level, msg) {
|
|
6031
6800
|
this.logSink(level, msg);
|
|
6032
6801
|
}
|
|
6033
|
-
statusGate(
|
|
6034
|
-
if (!
|
|
6802
|
+
statusGate(path, kind, harnessDir) {
|
|
6803
|
+
if (!existsSync17(path))
|
|
6035
6804
|
return { ok: true, violations: [] };
|
|
6036
|
-
return validateStatusDoc(
|
|
6805
|
+
return validateStatusDoc(path, kind, harnessDir);
|
|
6037
6806
|
}
|
|
6038
6807
|
dispatchGate(prompt, exec, hard) {
|
|
6039
6808
|
const harnessDir = this.resolver.forAgent(exec?.agent);
|
|
@@ -6061,7 +6830,12 @@ class DshHostAdapter extends Service2 {
|
|
|
6061
6830
|
recordWorkflowVerdict(input);
|
|
6062
6831
|
}
|
|
6063
6832
|
async beforeStatusWrite(path, doc) {
|
|
6064
|
-
const
|
|
6833
|
+
const harnessDir = this.resolver.forWorkspace(undefined);
|
|
6834
|
+
const kind = harnessDir === null ? null : harnessDocKindOfTarget(harnessDir, path);
|
|
6835
|
+
if (kind === null) {
|
|
6836
|
+
return { ok: true, severity: "low", code: "host.beforeStatusWrite.ok", message: `status write to ${path} validated` };
|
|
6837
|
+
}
|
|
6838
|
+
const gate2 = doc !== undefined ? validateStatusValue(doc, kind, harnessDir) : this.statusGate(path, kind, harnessDir);
|
|
6065
6839
|
if (!gate2.ok) {
|
|
6066
6840
|
const first = gate2.violations[0];
|
|
6067
6841
|
return { ok: false, severity: first.severity, code: first.code, message: first.message, fix: first.fix, aliases: first.aliases };
|
|
@@ -6076,13 +6850,39 @@ class DshHostAdapter extends Service2 {
|
|
|
6076
6850
|
return applyEnforcement(gate2, { hard });
|
|
6077
6851
|
}
|
|
6078
6852
|
async beforeMerge(lease) {
|
|
6079
|
-
|
|
6853
|
+
const violations = [...validateIntegrationMergeLease(lease).violations];
|
|
6854
|
+
const harnessDir = this.resolver.forWorkspace(undefined);
|
|
6855
|
+
if (harnessDir !== null) {
|
|
6856
|
+
const selection = resolveActiveWorkflow(harnessDir);
|
|
6857
|
+
if (selection.kind === "active") {
|
|
6858
|
+
const snapshotPath = join18(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
6859
|
+
try {
|
|
6860
|
+
const snapshot = readJson(snapshotPath);
|
|
6861
|
+
const stored = snapshot?.integration_merge_lease;
|
|
6862
|
+
if (stored !== undefined) {
|
|
6863
|
+
violations.push(...validateIntegrationMergeLease(stored).violations);
|
|
6864
|
+
if (!mergeLeasesMatch(lease, stored)) {
|
|
6865
|
+
violations.push({
|
|
6866
|
+
ok: false,
|
|
6867
|
+
severity: "high",
|
|
6868
|
+
code: "lease.merge.snapshot-mismatch",
|
|
6869
|
+
message: `snapshot integration_merge_lease (${snapshotPath}) differs from the lease being reserved — the active merge belongs to another holder/plan`,
|
|
6870
|
+
fix: "merge only under the snapshot-recorded integration_merge_lease (or release the lease with user authorization + audit note)"
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
} catch (error) {
|
|
6875
|
+
this.ctx.logger(HOST_LOGGER).warn(`beforeMerge snapshot lease read failed (contained — the passed-lease shape gate stands): ${error.message}`);
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
return { ok: violations.length === 0, violations };
|
|
6080
6880
|
}
|
|
6081
6881
|
}
|
|
6082
6882
|
|
|
6083
6883
|
// src/gates/agent-personas.ts
|
|
6084
|
-
import { readFileSync as
|
|
6085
|
-
import { join as
|
|
6884
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync6 } from "node:fs";
|
|
6885
|
+
import { join as join19, resolve as resolve17, sep as sep6 } from "node:path";
|
|
6086
6886
|
var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
6087
6887
|
var defaultCache = new Map;
|
|
6088
6888
|
function personaFor(roleId, lookup, warn) {
|
|
@@ -6099,7 +6899,7 @@ function personaFor(roleId, lookup, warn) {
|
|
|
6099
6899
|
function subagentRoleIds(agentsDir) {
|
|
6100
6900
|
let names;
|
|
6101
6901
|
try {
|
|
6102
|
-
names =
|
|
6902
|
+
names = readdirSync12(agentsDir);
|
|
6103
6903
|
} catch {
|
|
6104
6904
|
return [];
|
|
6105
6905
|
}
|
|
@@ -6110,7 +6910,7 @@ function subagentRoleIds(agentsDir) {
|
|
|
6110
6910
|
const roleId = name.slice(0, -3);
|
|
6111
6911
|
let content;
|
|
6112
6912
|
try {
|
|
6113
|
-
content =
|
|
6913
|
+
content = readFileSync15(join19(agentsDir, name), "utf8");
|
|
6114
6914
|
} catch {
|
|
6115
6915
|
continue;
|
|
6116
6916
|
}
|
|
@@ -6123,13 +6923,13 @@ function subagentRoleIds(agentsDir) {
|
|
|
6123
6923
|
function defaultFromMirror(agentsDir, roleId, warn) {
|
|
6124
6924
|
if (!ROLE_ID_PATTERN.test(roleId))
|
|
6125
6925
|
return;
|
|
6126
|
-
const root =
|
|
6127
|
-
const file =
|
|
6128
|
-
if (!file.startsWith(root +
|
|
6926
|
+
const root = resolve17(agentsDir);
|
|
6927
|
+
const file = resolve17(agentsDir, `${roleId}.md`);
|
|
6928
|
+
if (!file.startsWith(root + sep6))
|
|
6129
6929
|
return;
|
|
6130
6930
|
let stat;
|
|
6131
6931
|
try {
|
|
6132
|
-
stat =
|
|
6932
|
+
stat = statSync6(file);
|
|
6133
6933
|
} catch {
|
|
6134
6934
|
return;
|
|
6135
6935
|
}
|
|
@@ -6140,7 +6940,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
|
|
|
6140
6940
|
return hit.text;
|
|
6141
6941
|
let content;
|
|
6142
6942
|
try {
|
|
6143
|
-
content =
|
|
6943
|
+
content = readFileSync15(file, "utf8");
|
|
6144
6944
|
} catch {
|
|
6145
6945
|
return;
|
|
6146
6946
|
}
|
|
@@ -6328,8 +7128,8 @@ function log2(level, message) {
|
|
|
6328
7128
|
}
|
|
6329
7129
|
|
|
6330
7130
|
// src/gates/workflow-ledger.ts
|
|
6331
|
-
import { readFileSync as
|
|
6332
|
-
import { join as
|
|
7131
|
+
import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7132
|
+
import { join as join20 } from "node:path";
|
|
6333
7133
|
var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
|
|
6334
7134
|
var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
|
|
6335
7135
|
var TOOL_WORKFLOW_AGENT_START = "tool-workflow/agent-start";
|
|
@@ -6338,6 +7138,7 @@ var TOOL_WORKFLOW_RUN_END = "tool-workflow/run-end";
|
|
|
6338
7138
|
var DEPTH_ADVISORY_THRESHOLD = 2;
|
|
6339
7139
|
var WORKFLOW_LEDGER_WATERMARK_FILE = "workflow-ledger-cursors.json";
|
|
6340
7140
|
var WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS = 256;
|
|
7141
|
+
var WORKFLOW_LEDGER_WATERMARK_MAX_DIRS = 64;
|
|
6341
7142
|
var workflowLedgerLogSink = () => {};
|
|
6342
7143
|
function setWorkflowLedgerLogger(sink) {
|
|
6343
7144
|
const prior = workflowLedgerLogSink;
|
|
@@ -6357,13 +7158,22 @@ function sessionIdOf2(session) {
|
|
|
6357
7158
|
return typeof id === "string" && id !== "" ? id : undefined;
|
|
6358
7159
|
}
|
|
6359
7160
|
var watermarkCache = new Map;
|
|
6360
|
-
function
|
|
6361
|
-
|
|
6362
|
-
|
|
7161
|
+
function cacheWatermark(workflowDir, watermark) {
|
|
7162
|
+
watermarkCache.set(workflowDir, watermark);
|
|
7163
|
+
while (watermarkCache.size > WORKFLOW_LEDGER_WATERMARK_MAX_DIRS) {
|
|
7164
|
+
const oldest = watermarkCache.keys().next().value;
|
|
7165
|
+
if (oldest === undefined)
|
|
7166
|
+
break;
|
|
7167
|
+
watermarkCache.delete(oldest);
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
function loadWatermark(workflowDir, fresh = false) {
|
|
7171
|
+
const cached = watermarkCache.get(workflowDir);
|
|
7172
|
+
if (cached !== undefined && !fresh)
|
|
6363
7173
|
return cached;
|
|
6364
7174
|
const watermark = new Map;
|
|
6365
7175
|
try {
|
|
6366
|
-
const raw =
|
|
7176
|
+
const raw = readFileSync16(join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE), "utf8");
|
|
6367
7177
|
const record = asRecord(JSON.parse(raw));
|
|
6368
7178
|
const cursors = asRecord(record?.cursors);
|
|
6369
7179
|
if (cursors !== undefined) {
|
|
@@ -6376,40 +7186,50 @@ function loadWatermark(harnessDir) {
|
|
|
6376
7186
|
} catch (error) {
|
|
6377
7187
|
const err = error;
|
|
6378
7188
|
if (err?.code !== "ENOENT") {
|
|
6379
|
-
log3("warn", `workflow-ledger watermark unreadable for ${
|
|
7189
|
+
log3("warn", `workflow-ledger watermark unreadable for ${workflowDir} — degrading to in-memory-only (restart re-records): ${errorMessage3(error)}`);
|
|
6380
7190
|
}
|
|
7191
|
+
if (fresh && cached !== undefined)
|
|
7192
|
+
return cached;
|
|
6381
7193
|
}
|
|
6382
|
-
|
|
7194
|
+
cacheWatermark(workflowDir, watermark);
|
|
6383
7195
|
return watermark;
|
|
6384
7196
|
}
|
|
6385
|
-
function saveWatermark(
|
|
7197
|
+
function saveWatermark(workflowDir, watermark) {
|
|
6386
7198
|
try {
|
|
6387
|
-
const file =
|
|
7199
|
+
const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
|
|
6388
7200
|
const tmp = `${file}.tmp`;
|
|
6389
7201
|
writeFileSync4(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
|
|
6390
7202
|
renameSync3(tmp, file);
|
|
6391
7203
|
} catch (error) {
|
|
6392
|
-
log3("warn", `workflow-ledger watermark write failed for ${
|
|
7204
|
+
log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
|
|
6393
7205
|
}
|
|
6394
7206
|
}
|
|
6395
|
-
function advanceWatermark(
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
7207
|
+
function advanceWatermark(workflowDir, sid, nextSeq, isEvictable, lockOpts = {}) {
|
|
7208
|
+
try {
|
|
7209
|
+
withWorkflowDirLock(workflowDir, () => {
|
|
7210
|
+
const watermark = loadWatermark(workflowDir, true);
|
|
7211
|
+
if (!watermark.has(sid) && watermark.size >= WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS) {
|
|
7212
|
+
let victim;
|
|
7213
|
+
for (const key of watermark.keys()) {
|
|
7214
|
+
if (isEvictable(key)) {
|
|
7215
|
+
victim = key;
|
|
7216
|
+
break;
|
|
7217
|
+
}
|
|
7218
|
+
}
|
|
7219
|
+
victim ??= watermark.keys().next().value;
|
|
7220
|
+
if (victim !== undefined) {
|
|
7221
|
+
watermark.delete(victim);
|
|
7222
|
+
log3("warn", `workflow-ledger watermark capped at ${WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS} sessions — evicted ${victim} (a restored evicted session re-records; bounded by the cap)`);
|
|
7223
|
+
}
|
|
6403
7224
|
}
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
7225
|
+
watermark.set(sid, Math.max(watermark.get(sid) ?? 0, nextSeq));
|
|
7226
|
+
saveWatermark(workflowDir, watermark);
|
|
7227
|
+
}, lockOpts);
|
|
7228
|
+
} catch (error) {
|
|
7229
|
+
const watermark = loadWatermark(workflowDir);
|
|
7230
|
+
watermark.set(sid, Math.max(watermark.get(sid) ?? 0, nextSeq));
|
|
7231
|
+
log3("warn", `workflow-ledger watermark durable advance failed for ${workflowDir} — in-memory cursor advanced, durable cursor NOT (restart reloads the file; outage rows re-record once — bounded duplicate): ${errorMessage3(error)}`);
|
|
6410
7232
|
}
|
|
6411
|
-
watermark.set(sid, nextSeq);
|
|
6412
|
-
saveWatermark(harnessDir, watermark);
|
|
6413
7233
|
}
|
|
6414
7234
|
function rowOf(session, envelope) {
|
|
6415
7235
|
const record = asRecord(envelope);
|
|
@@ -6517,15 +7337,18 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
|
|
|
6517
7337
|
const harnessDir = resolver.forWorkspace(typeof cwd === "string" && cwd.trim() !== "" ? cwd : undefined);
|
|
6518
7338
|
if (harnessDir === null)
|
|
6519
7339
|
return;
|
|
6520
|
-
const
|
|
7340
|
+
const workflowDir = resolveAgentFlowWriteDir(harnessDir);
|
|
7341
|
+
if (workflowDir === null)
|
|
7342
|
+
return;
|
|
7343
|
+
const watermark = loadWatermark(workflowDir);
|
|
6521
7344
|
let next = watermark.get(sid) ?? 0;
|
|
6522
7345
|
const events = session.events;
|
|
6523
7346
|
if (Array.isArray(events) && events.length < next)
|
|
6524
7347
|
next = 0;
|
|
6525
7348
|
if (row.seq < next)
|
|
6526
7349
|
return;
|
|
6527
|
-
if (recordWorkflowEvent({ harnessDir, event: row.event })) {
|
|
6528
|
-
advanceWatermark(
|
|
7350
|
+
if (recordWorkflowEvent({ harnessDir, workflowDir, event: row.event })) {
|
|
7351
|
+
advanceWatermark(workflowDir, sid, row.seq + 1, (candidate) => sessions.get(candidate) === undefined);
|
|
6529
7352
|
if (workflowAskCache !== undefined && row.event.kind === "workflow-run") {
|
|
6530
7353
|
try {
|
|
6531
7354
|
const runName = row.runName ?? row.event.name;
|
|
@@ -6589,8 +7412,8 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
|
|
|
6589
7412
|
}
|
|
6590
7413
|
|
|
6591
7414
|
// src/gates/goal-bridge.ts
|
|
6592
|
-
import { existsSync as
|
|
6593
|
-
import { join as
|
|
7415
|
+
import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
|
|
7416
|
+
import { join as join21 } from "node:path";
|
|
6594
7417
|
var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
|
|
6595
7418
|
var DEFAULT_MAX_GOAL_ROUNDS = 256;
|
|
6596
7419
|
var FLOW_SEQUENCE = "iteration-start → per-plan cycles → iteration-close → PR delivery → merge-ready";
|
|
@@ -6611,6 +7434,19 @@ function log4(level, message) {
|
|
|
6611
7434
|
function errorMessage4(error) {
|
|
6612
7435
|
return error instanceof Error ? error.message : String(error);
|
|
6613
7436
|
}
|
|
7437
|
+
function projectRegisterPointer(harnessDir) {
|
|
7438
|
+
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
7439
|
+
try {
|
|
7440
|
+
for (const entry of readdirSync13(projectsDir, { withFileTypes: true })) {
|
|
7441
|
+
if (!entry.isDirectory())
|
|
7442
|
+
continue;
|
|
7443
|
+
const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
7444
|
+
if (existsSync18(registerPath))
|
|
7445
|
+
return registerPath;
|
|
7446
|
+
}
|
|
7447
|
+
} catch {}
|
|
7448
|
+
return join21(projectsDir, _DEFAULT_PROJECT, PROJECT_REGISTER_FILE);
|
|
7449
|
+
}
|
|
6614
7450
|
function isRootLikeAgent(agent) {
|
|
6615
7451
|
const header = agent?.session?.header;
|
|
6616
7452
|
return header?.parentSession === undefined;
|
|
@@ -6620,23 +7456,23 @@ function iterationGoalObjective(iterationId) {
|
|
|
6620
7456
|
}
|
|
6621
7457
|
function steeringCompass(harnessDir) {
|
|
6622
7458
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
6623
|
-
if (!
|
|
7459
|
+
if (!existsSync18(iterationsDir))
|
|
6624
7460
|
return;
|
|
6625
7461
|
let entries;
|
|
6626
7462
|
try {
|
|
6627
|
-
entries =
|
|
7463
|
+
entries = readdirSync13(iterationsDir, { withFileTypes: true });
|
|
6628
7464
|
} catch {
|
|
6629
7465
|
return;
|
|
6630
7466
|
}
|
|
6631
7467
|
for (const entry of entries) {
|
|
6632
7468
|
if (!entry.isDirectory())
|
|
6633
7469
|
continue;
|
|
6634
|
-
const compassPath =
|
|
6635
|
-
if (!
|
|
7470
|
+
const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
|
|
7471
|
+
if (!existsSync18(compassPath))
|
|
6636
7472
|
continue;
|
|
6637
7473
|
let content;
|
|
6638
7474
|
try {
|
|
6639
|
-
content =
|
|
7475
|
+
content = readFileSync17(compassPath, "utf8");
|
|
6640
7476
|
} catch {
|
|
6641
7477
|
continue;
|
|
6642
7478
|
}
|
|
@@ -6745,7 +7581,7 @@ function warnBlockedGoal(harnessDir, advisory) {
|
|
|
6745
7581
|
const code = truncateLedgerField(advisory.code, GOAL_ADVISORY_CODE_CAP);
|
|
6746
7582
|
const reason = truncateLedgerField(normalizeWorkflowName(advisory.message), GOAL_ADVISORY_MESSAGE_CAP);
|
|
6747
7583
|
const objective = truncateLedgerField(normalizeWorkflowName(advisory.objective), GOAL_ADVISORY_OBJECTIVE_CAP);
|
|
6748
|
-
log4("warn", `goal blocked [${code}] — ${reason}; objective: ${objective}; residuals: see ${harnessDir}
|
|
7584
|
+
log4("warn", `goal blocked [${code}] — ${reason}; objective: ${objective}; residuals: see ${projectRegisterPointer(harnessDir)} — advisory only, zero harness writes`);
|
|
6749
7585
|
}
|
|
6750
7586
|
function rootAgentOf(agent, agents) {
|
|
6751
7587
|
let current = agent;
|
|
@@ -6812,8 +7648,8 @@ function registerGoalBridge(ctx, resolver, config) {
|
|
|
6812
7648
|
}
|
|
6813
7649
|
|
|
6814
7650
|
// src/gates/plan-mode-bridge.ts
|
|
6815
|
-
import { existsSync as
|
|
6816
|
-
import { join as
|
|
7651
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
7652
|
+
import { join as join22 } from "node:path";
|
|
6817
7653
|
var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
|
|
6818
7654
|
var PLAN_STATUS_TODO = "Todo";
|
|
6819
7655
|
var planModeBridgeLogSink = () => {};
|
|
@@ -6829,12 +7665,16 @@ function errorMessage5(error) {
|
|
|
6829
7665
|
return error instanceof Error ? error.message : String(error);
|
|
6830
7666
|
}
|
|
6831
7667
|
function hasPrepareWindow(harnessDir) {
|
|
6832
|
-
const statusPath =
|
|
6833
|
-
if (!
|
|
7668
|
+
const statusPath = join22(harnessDir, STATUS_FILE);
|
|
7669
|
+
if (!existsSync19(statusPath))
|
|
6834
7670
|
return false;
|
|
7671
|
+
const selection = resolveReadWorkflow(harnessDir);
|
|
7672
|
+
if (selection.kind === "error")
|
|
7673
|
+
return false;
|
|
7674
|
+
const snapshotPath = join22(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
6835
7675
|
let doc;
|
|
6836
7676
|
try {
|
|
6837
|
-
doc = readJson(
|
|
7677
|
+
doc = readJson(snapshotPath);
|
|
6838
7678
|
} catch {
|
|
6839
7679
|
return false;
|
|
6840
7680
|
}
|
|
@@ -7223,7 +8063,7 @@ function registerHarnessPrompt(ctx, options) {
|
|
|
7223
8063
|
return true;
|
|
7224
8064
|
}
|
|
7225
8065
|
function sectionEnforcement(harnessDir) {
|
|
7226
|
-
return harnessDir === null ? { hard: false, source: "none" } :
|
|
8066
|
+
return harnessDir === null ? { hard: false, source: "none" } : resolveRepoEnforcement(harnessDir);
|
|
7227
8067
|
}
|
|
7228
8068
|
function harnessRulesText(harnessDir, enforcement) {
|
|
7229
8069
|
return [
|
|
@@ -7284,7 +8124,7 @@ var inject = ["loader"];
|
|
|
7284
8124
|
function packagedCommandsDir() {
|
|
7285
8125
|
try {
|
|
7286
8126
|
const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
|
|
7287
|
-
return
|
|
8127
|
+
return existsSync20(dir) ? dir : undefined;
|
|
7288
8128
|
} catch {
|
|
7289
8129
|
return;
|
|
7290
8130
|
}
|
|
@@ -7329,10 +8169,10 @@ function registerMstarCommands(ctx) {
|
|
|
7329
8169
|
if (dir === undefined)
|
|
7330
8170
|
return;
|
|
7331
8171
|
ctx.inject(["commands"], (commandsCtx) => {
|
|
7332
|
-
for (const file of
|
|
8172
|
+
for (const file of readdirSync14(dir).sort()) {
|
|
7333
8173
|
if (!file.endsWith(".md"))
|
|
7334
8174
|
continue;
|
|
7335
|
-
const parsed = parseCommandMarkdown(
|
|
8175
|
+
const parsed = parseCommandMarkdown(readFileSync18(join23(dir, file), "utf8"));
|
|
7336
8176
|
if (parsed === undefined)
|
|
7337
8177
|
continue;
|
|
7338
8178
|
commandsCtx.commands.register({
|
|
@@ -7482,7 +8322,7 @@ function apply(ctx, config) {
|
|
|
7482
8322
|
if (skillConfig !== undefined) {
|
|
7483
8323
|
ctx.plugin({ name: skillLocalName, inject: skillLocalInject, Config: SkillLocalSchema, apply: applySkillLocal }, skillConfig);
|
|
7484
8324
|
}
|
|
7485
|
-
const effectiveHard = config.enforcement === "hard" || bootHarnessDir !== null &&
|
|
8325
|
+
const effectiveHard = config.enforcement === "hard" || bootHarnessDir !== null && resolveRepoEnforcement(bootHarnessDir).hard;
|
|
7486
8326
|
if (effectiveHard && (config.dispatchBinding ?? "").trim() === "") {
|
|
7487
8327
|
ctx.logger(DISPATCH_LOGGER).warn("Enforcement: hard is active but dispatchBinding is unset — the anti-recursion precheck is skipped (an Assignment whose Execute as equals the dispatching agent cannot be detected)");
|
|
7488
8328
|
}
|