@mstar-harness/opencode 3.2.5 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/mstar.js +1329 -1271
- package/harness-skills/mstar-audit/SKILL.md +3 -2
- package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
- package/harness-skills/mstar-audit/references/codebase-audit.md +19 -3
- package/harness-skills/mstar-audit/references/finding-format.md +5 -1
- package/harness-skills/mstar-audit/references/pr-review.md +78 -9
- package/harness-skills/mstar-audit/references/security-review.md +221 -0
- package/harness-skills/mstar-conventions/SKILL.md +4 -2
- package/harness-skills/mstar-conventions/references/harness-bootstrap-and-agents-layering.md +5 -1
- package/harness-skills/mstar-harness-core/SKILL.md +1 -1
- package/harness-skills/mstar-roles/SKILL.md +17 -13
- package/harness-skills/mstar-roles/references/architect.md +9 -11
- package/harness-skills/mstar-roles/references/code-reviewer.md +12 -15
- package/harness-skills/mstar-roles/references/frontend-dev.md +9 -11
- package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +9 -11
- package/harness-skills/mstar-roles/references/ops-engineer.md +9 -11
- package/harness-skills/mstar-roles/references/product-manager.md +9 -11
- package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +2 -0
- package/harness-skills/mstar-roles/references/prompt-engineer.md +10 -13
- package/harness-skills/mstar-roles/references/qa-engineer.md +8 -12
- package/harness-skills/mstar-roles/references/qc-specialist-shared.md +10 -17
- package/harness-skills/mstar-roles/references/writing-specialist.md +8 -9
- package/package.json +1 -1
package/dist/mstar.js
CHANGED
|
@@ -50,6 +50,7 @@ __export(exports_engine, {
|
|
|
50
50
|
resolveSpecsDir: () => resolveSpecsDir,
|
|
51
51
|
resolveSkillRoot: () => resolveSkillRoot,
|
|
52
52
|
resolveSddDir: () => resolveSddDir,
|
|
53
|
+
resolveScaffoldDirs: () => resolveScaffoldDirs,
|
|
53
54
|
resolveRepoEnforcement: () => resolveRepoEnforcement,
|
|
54
55
|
resolveProjectRoot: () => resolveProjectRoot,
|
|
55
56
|
resolveProjectDir: () => resolveProjectDir,
|
|
@@ -99,6 +100,7 @@ __export(exports_engine, {
|
|
|
99
100
|
evaluatePhaseGate: () => evaluatePhaseGate,
|
|
100
101
|
emitGitignoreSnippet: () => emitGitignoreSnippet,
|
|
101
102
|
detectHost: () => detectHost,
|
|
103
|
+
detectHarnessKind: () => detectHarnessKind,
|
|
102
104
|
compoundRefreshScope: () => compoundRefreshScope,
|
|
103
105
|
composeDispatchGate: () => composeDispatchGate,
|
|
104
106
|
completenessLevel: () => completenessLevel,
|
|
@@ -162,30 +164,30 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
162
164
|
import { fileURLToPath } from "node:url";
|
|
163
165
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
164
166
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
165
|
-
import { existsSync as
|
|
167
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
166
168
|
import { execFileSync } from "node:child_process";
|
|
167
|
-
import { basename as basename2, dirname as
|
|
168
|
-
import { existsSync as
|
|
169
|
-
import {
|
|
170
|
-
import {
|
|
171
|
-
import {
|
|
169
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve5 } from "node:path";
|
|
170
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
171
|
+
import { join as join7 } from "node:path";
|
|
172
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
173
|
+
import { join as join3 } from "node:path";
|
|
174
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
175
|
+
import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
|
|
176
|
+
import { mkdirSync as mkdirSync2, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
177
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
|
|
172
178
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
173
179
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
174
|
-
import { mkdirSync as
|
|
180
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
175
181
|
import { join as join5 } from "node:path";
|
|
176
182
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
177
|
-
import { existsSync as
|
|
183
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
178
184
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
179
185
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
180
|
-
import { mkdirSync as mkdirSync5, readdirSync as
|
|
181
|
-
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as
|
|
182
|
-
import {
|
|
183
|
-
import { join as join8 } from "node:path";
|
|
184
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
185
|
-
import { join as join9 } from "node:path";
|
|
186
|
-
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
186
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
187
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
|
|
188
|
+
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
187
189
|
import { dirname as dirname7, isAbsolute as isAbsolute6, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
|
|
188
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as
|
|
190
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
189
191
|
import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
|
|
190
192
|
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
191
193
|
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
|
|
@@ -331,284 +333,338 @@ function isAtOrBelow(dir, root) {
|
|
|
331
333
|
const rel = relative(root, dir);
|
|
332
334
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
333
335
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (isDirectory(candidate))
|
|
349
|
-
return candidate;
|
|
336
|
+
function typeName(value) {
|
|
337
|
+
if (value === null)
|
|
338
|
+
return "null";
|
|
339
|
+
if (Array.isArray(value))
|
|
340
|
+
return "array";
|
|
341
|
+
return typeof value;
|
|
342
|
+
}
|
|
343
|
+
function validateCompassShape(doc) {
|
|
344
|
+
const issues = [];
|
|
345
|
+
const expectString = (key, opts = {}) => {
|
|
346
|
+
const value = doc[key];
|
|
347
|
+
if (typeof value !== "string") {
|
|
348
|
+
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
349
|
+
return;
|
|
350
350
|
}
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
351
|
+
if (opts.min !== undefined && value.length < opts.min) {
|
|
352
|
+
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
356
|
+
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
expectString("iteration_id", { min: 1 });
|
|
360
|
+
expectString("start_date", { regex: DATE_RE });
|
|
361
|
+
const status = doc.status;
|
|
362
|
+
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
363
|
+
issues.push({
|
|
364
|
+
path: ["status"],
|
|
365
|
+
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
366
|
+
});
|
|
357
367
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
expectString("iteration_base_branch", { min: 1 });
|
|
369
|
+
expectString("target_branch", { min: 1 });
|
|
370
|
+
const plans = doc.plans;
|
|
371
|
+
if (plans !== undefined) {
|
|
372
|
+
if (!Array.isArray(plans)) {
|
|
373
|
+
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
374
|
+
} else {
|
|
375
|
+
plans.forEach((entry, index) => {
|
|
376
|
+
if (typeof entry !== "string") {
|
|
377
|
+
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
378
|
+
} else if (entry.length < 1) {
|
|
379
|
+
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
372
382
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
}
|
|
384
|
+
const end_date = doc.end_date;
|
|
385
|
+
if (end_date !== undefined) {
|
|
386
|
+
if (typeof end_date !== "string") {
|
|
387
|
+
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
388
|
+
} else if (!DATE_RE.test(end_date)) {
|
|
389
|
+
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (issues.length > 0)
|
|
393
|
+
return { ok: false, issues };
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
data: {
|
|
397
|
+
iteration_id: doc.iteration_id,
|
|
398
|
+
start_date: doc.start_date,
|
|
399
|
+
status,
|
|
400
|
+
iteration_base_branch: doc.iteration_base_branch,
|
|
401
|
+
target_branch: doc.target_branch,
|
|
402
|
+
...plans !== undefined ? { plans } : {},
|
|
403
|
+
...end_date !== undefined ? { end_date } : {}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
376
406
|
}
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
407
|
+
function violation(severity, code, message, fix) {
|
|
408
|
+
return { ok: false, severity, code, message, fix };
|
|
380
409
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
const rc = loadMstarc(dir, dirname3(dir));
|
|
384
|
-
const declared = rc?.config[key];
|
|
385
|
-
return declared ? resolve3(rc.dir, declared) : null;
|
|
410
|
+
function isPlainObject(value) {
|
|
411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
386
412
|
}
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
function validateCompassFrontmatter(doc) {
|
|
414
|
+
if (!isPlainObject(doc)) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
violations: [
|
|
418
|
+
violation("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")
|
|
419
|
+
]
|
|
420
|
+
};
|
|
393
421
|
}
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
for (const candidate of candidates) {
|
|
404
|
-
if (isDirectory(candidate) && hasFiles(candidate))
|
|
405
|
-
return candidate;
|
|
422
|
+
const parsed = validateCompassShape(doc);
|
|
423
|
+
if (!parsed.ok) {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
violations: parsed.issues.map((issue) => {
|
|
427
|
+
const field = issue.path.join(".") || "(root)";
|
|
428
|
+
return violation("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)`);
|
|
429
|
+
})
|
|
430
|
+
};
|
|
406
431
|
}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
432
|
+
const violations = [];
|
|
433
|
+
const { status, end_date } = parsed.data;
|
|
434
|
+
if (status === "completed" && end_date === undefined) {
|
|
435
|
+
violations.push(violation("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"));
|
|
436
|
+
}
|
|
437
|
+
if (status !== "completed" && end_date !== undefined) {
|
|
438
|
+
violations.push(violation("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"));
|
|
439
|
+
}
|
|
440
|
+
return { ok: violations.length === 0, violations };
|
|
411
441
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const dir = resolve3(harnessDir);
|
|
417
|
-
const name = basename2(dir);
|
|
418
|
-
if (name === ".plans" || name === "plans")
|
|
419
|
-
return dir;
|
|
420
|
-
return join3(dir, "plans");
|
|
442
|
+
function registeredPlanIds(compassDoc) {
|
|
443
|
+
if (!Array.isArray(compassDoc.plans))
|
|
444
|
+
return [];
|
|
445
|
+
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
421
446
|
}
|
|
422
|
-
function
|
|
423
|
-
if (
|
|
424
|
-
|
|
447
|
+
function findPlanRow(snapshotDoc, planId) {
|
|
448
|
+
if (!Array.isArray(snapshotDoc.plans))
|
|
449
|
+
return null;
|
|
450
|
+
for (const row of snapshotDoc.plans) {
|
|
451
|
+
if (!isPlainObject(row))
|
|
452
|
+
continue;
|
|
453
|
+
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
454
|
+
if (rowId === planId)
|
|
455
|
+
return row;
|
|
425
456
|
}
|
|
457
|
+
return null;
|
|
426
458
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
459
|
+
function entryPlansAllDone(snapshotDoc, registered) {
|
|
460
|
+
const violations = [];
|
|
461
|
+
if (registered.length === 0) {
|
|
462
|
+
violations.push(violation("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`"));
|
|
463
|
+
return violations;
|
|
464
|
+
}
|
|
465
|
+
for (const planId of registered) {
|
|
466
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
467
|
+
if (row === null) {
|
|
468
|
+
violations.push(violation("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"));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (row.status !== PLAN_STATUS_DONE) {
|
|
472
|
+
violations.push(violation("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)`));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return violations;
|
|
439
476
|
}
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
if (declared !== null)
|
|
443
|
-
return declared;
|
|
444
|
-
return join3(resolve3(harnessDir), "knowledge");
|
|
477
|
+
function entryFrontmatterComplete(compassDoc) {
|
|
478
|
+
return validateCompassFrontmatter(compassDoc).violations;
|
|
445
479
|
}
|
|
446
|
-
function
|
|
447
|
-
const
|
|
448
|
-
if (
|
|
449
|
-
|
|
480
|
+
function exitFrontmatterClosed(compassDoc) {
|
|
481
|
+
const violations = [];
|
|
482
|
+
if (compassDoc.status !== "completed") {
|
|
483
|
+
violations.push(violation("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)`));
|
|
450
484
|
}
|
|
451
|
-
const
|
|
452
|
-
|
|
485
|
+
const endDate = compassDoc.end_date;
|
|
486
|
+
if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
|
|
487
|
+
violations.push(violation("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)"));
|
|
488
|
+
}
|
|
489
|
+
return violations;
|
|
453
490
|
}
|
|
454
|
-
function
|
|
455
|
-
|
|
491
|
+
function exitBranchCheck(opts) {
|
|
492
|
+
const violations = [];
|
|
493
|
+
const { currentBranch, specIntegrationBranch } = opts;
|
|
494
|
+
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
495
|
+
violations.push(violation("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)"));
|
|
496
|
+
} else if (currentBranch !== specIntegrationBranch) {
|
|
497
|
+
violations.push(violation("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
|
|
498
|
+
}
|
|
499
|
+
return violations;
|
|
456
500
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
501
|
+
function exitPrBaseCheck(compassDoc, opts) {
|
|
502
|
+
const violations = [];
|
|
503
|
+
const target = compassDoc.target_branch;
|
|
504
|
+
const { prBaseBranch } = opts;
|
|
505
|
+
if (prBaseBranch === undefined) {
|
|
506
|
+
violations.push(violation("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
|
|
507
|
+
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
508
|
+
violations.push(violation("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)`));
|
|
509
|
+
}
|
|
510
|
+
return violations;
|
|
459
511
|
}
|
|
460
|
-
function
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
512
|
+
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
513
|
+
const registered = registeredPlanIds(compassDoc);
|
|
514
|
+
const entryViolations = [
|
|
515
|
+
...entryPlansAllDone(snapshotDoc, registered),
|
|
516
|
+
...entryFrontmatterComplete(compassDoc)
|
|
517
|
+
];
|
|
518
|
+
const exitViolations = [
|
|
519
|
+
...exitFrontmatterClosed(compassDoc),
|
|
520
|
+
...exitBranchCheck(opts),
|
|
521
|
+
...exitPrBaseCheck(compassDoc, opts)
|
|
522
|
+
];
|
|
523
|
+
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
524
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
525
|
+
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
526
|
+
});
|
|
527
|
+
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
528
|
+
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
529
|
+
let transition;
|
|
530
|
+
if (!allPlansDone)
|
|
531
|
+
transition = "phase-2-execute";
|
|
532
|
+
else if (entry.ok && exit.ok)
|
|
533
|
+
transition = "phase-4-pr-delivery";
|
|
534
|
+
else
|
|
535
|
+
transition = "phase-3-close";
|
|
536
|
+
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
537
|
+
return {
|
|
538
|
+
transition,
|
|
539
|
+
allPlansDone,
|
|
540
|
+
entry,
|
|
541
|
+
exit,
|
|
542
|
+
ok: gateBlocking.length === 0,
|
|
543
|
+
violations: gateBlocking
|
|
544
|
+
};
|
|
468
545
|
}
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
546
|
+
function pushCadenceProbe(ciRunning, reviewWaveActive) {
|
|
547
|
+
const violations = [];
|
|
548
|
+
if (ciRunning) {
|
|
549
|
+
violations.push(violation("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head — do not push until the wave completes (mstar-iteration §5.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
|
|
550
|
+
}
|
|
551
|
+
if (reviewWaveActive) {
|
|
552
|
+
violations.push(violation("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head — do not push until it settles (mstar-iteration §5.1a push gate 2)", "Wait for the review wave, then push once"));
|
|
553
|
+
}
|
|
554
|
+
return { ok: violations.length === 0, violations };
|
|
475
555
|
}
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
const kind = detectHarnessKind(resolveHarnessDir(root));
|
|
479
|
-
let content;
|
|
480
|
-
try {
|
|
481
|
-
content = readFileSync3(gitignorePath, "utf8");
|
|
482
|
-
} catch {
|
|
556
|
+
function assertIndexRowObligations(iterationsDir) {
|
|
557
|
+
if (!existsSync2(iterationsDir)) {
|
|
483
558
|
return {
|
|
484
559
|
ok: false,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
|
|
560
|
+
violations: [
|
|
561
|
+
violation("high", "INDEX_ITERATIONS_DIR_MISSING", `{ITERATION_DIR} '${iterationsDir}' does not exist (mstar-iteration §1.4)`, "Create the iterations directory (path.resolveIterationDir)")
|
|
562
|
+
]
|
|
489
563
|
};
|
|
490
564
|
}
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
let missing;
|
|
495
|
-
let label;
|
|
496
|
-
if (kind === "agents") {
|
|
497
|
-
missing = agentsMissing;
|
|
498
|
-
label = ".agents/ set";
|
|
499
|
-
} else if (kind === "mstar") {
|
|
500
|
-
missing = mstarMissing;
|
|
501
|
-
label = ".mstar/ set";
|
|
502
|
-
} else {
|
|
503
|
-
label = "either .mstar/ or .agents/ set";
|
|
504
|
-
missing = mstarMissing.length === 0 || agentsMissing.length === 0 ? [] : mstarMissing.length <= agentsMissing.length ? mstarMissing : agentsMissing;
|
|
505
|
-
}
|
|
506
|
-
if (missing.length > 0) {
|
|
565
|
+
const iterationIds = readdirSync(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync2(join3(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
|
|
566
|
+
const readmePath = join3(iterationsDir, INDEX_README);
|
|
567
|
+
if (!existsSync2(readmePath)) {
|
|
507
568
|
return {
|
|
508
569
|
ok: false,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
|
|
570
|
+
violations: [
|
|
571
|
+
violation("high", "INDEX_README_MISSING", `{ITERATION_DIR}/README.md does not exist — one row per iteration is required (mstar-iteration §1.4)`, `Create {ITERATION_DIR}/README.md with the header '${INDEX_HEADER}' and one row per iteration`)
|
|
572
|
+
]
|
|
513
573
|
};
|
|
514
574
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
575
|
+
const violations = [];
|
|
576
|
+
const lines = readFileSync3(readmePath, "utf8").split(/\r?\n/);
|
|
577
|
+
if (!lines.some((line) => line.includes(INDEX_HEADER))) {
|
|
578
|
+
violations.push(violation("medium", "INDEX_HEADER_MISSING", `{ITERATION_DIR}/README.md lacks the table header '${INDEX_HEADER}' (mstar-iteration §1.4)`, "Add the header row on first creation"));
|
|
579
|
+
}
|
|
580
|
+
const indexed = new Set;
|
|
581
|
+
for (const line of lines) {
|
|
582
|
+
const match = line.match(/^\s*\|\s*`([^`]+)`\s*\|/);
|
|
583
|
+
if (match)
|
|
584
|
+
indexed.add(match[1].trim());
|
|
585
|
+
}
|
|
586
|
+
for (const id of iterationIds) {
|
|
587
|
+
if (!indexed.has(id)) {
|
|
588
|
+
violations.push(violation("medium", "INDEX_ROW_MISSING", `Iteration '${id}' has a delivery-compass.md but no index row in {ITERATION_DIR}/README.md — one row per iteration (mstar-iteration §1.4)`, `Add | \`${id}\` | [\`${id}/\`](${id}/) | <description> | <status> |`));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { ok: violations.length === 0, violations };
|
|
521
592
|
}
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
return null;
|
|
525
|
-
const name = basename2(resolve3(harnessDir));
|
|
526
|
-
if (name === ".mstar")
|
|
527
|
-
return "mstar";
|
|
528
|
-
if (name === ".agents")
|
|
529
|
-
return "agents";
|
|
530
|
-
return null;
|
|
593
|
+
function parseCompassFrontmatter(filePath) {
|
|
594
|
+
return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
|
|
531
595
|
}
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
ok: false,
|
|
537
|
-
severity: "high",
|
|
538
|
-
code: "plan-path.no-harness",
|
|
539
|
-
message: `persistent plan tracking is not enabled — cannot place plan ${planAbs} under {PLAN_DIR}`,
|
|
540
|
-
fix: "initialize the harness (scaffoldHarness) so plans land in {PLAN_DIR}"
|
|
541
|
-
};
|
|
596
|
+
function parseCompassFrontmatterText(content, filePath) {
|
|
597
|
+
const lines = content.split(/\r?\n/);
|
|
598
|
+
if (lines[0]?.trim() !== "---") {
|
|
599
|
+
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
542
600
|
}
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (!inside) {
|
|
547
|
-
return {
|
|
548
|
-
ok: false,
|
|
549
|
-
severity: "high",
|
|
550
|
-
code: "plan-path.outside-plan-dir",
|
|
551
|
-
message: `plan file ${planAbs} is outside {PLAN_DIR} (${planDir})`,
|
|
552
|
-
fix: `write the plan under ${planDir}`
|
|
553
|
-
};
|
|
601
|
+
const end = lines.indexOf("---", 1);
|
|
602
|
+
if (end === -1) {
|
|
603
|
+
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
554
604
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
605
|
+
const doc = {};
|
|
606
|
+
let listKey = null;
|
|
607
|
+
for (let i = 1;i < end; i += 1) {
|
|
608
|
+
const line = lines[i] ?? "";
|
|
609
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
610
|
+
continue;
|
|
611
|
+
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
612
|
+
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
613
|
+
if (!Array.isArray(doc[listKey]))
|
|
614
|
+
doc[listKey] = [];
|
|
615
|
+
doc[listKey].push(item);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
listKey = null;
|
|
619
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
620
|
+
if (!kv) {
|
|
621
|
+
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
622
|
+
}
|
|
623
|
+
const value = kv[2].trim();
|
|
624
|
+
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
625
|
+
listKey = value === "" ? kv[1] : null;
|
|
571
626
|
}
|
|
572
|
-
return
|
|
573
|
-
ok: true,
|
|
574
|
-
severity: "low",
|
|
575
|
-
code: "plan-path.ok",
|
|
576
|
-
message: `plan file ${planAbs} lives under {PLAN_DIR} (${planDir})`
|
|
577
|
-
};
|
|
627
|
+
return doc;
|
|
578
628
|
}
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return false;
|
|
629
|
+
function parseFlowArray(raw, filePath) {
|
|
630
|
+
const inner = raw.slice(1, -1);
|
|
631
|
+
if (/[[\]]/.test(inner)) {
|
|
632
|
+
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
|
|
584
633
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
634
|
+
let quote = null;
|
|
635
|
+
for (const ch of inner) {
|
|
636
|
+
if (ch === '"' || ch === "'") {
|
|
637
|
+
if (quote === null)
|
|
638
|
+
quote = ch;
|
|
639
|
+
else if (quote === ch)
|
|
640
|
+
quote = null;
|
|
641
|
+
} else if (ch === "," && quote !== null) {
|
|
642
|
+
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
595
643
|
}
|
|
596
|
-
return false;
|
|
597
|
-
} catch {
|
|
598
|
-
return false;
|
|
599
644
|
}
|
|
645
|
+
if (quote !== null) {
|
|
646
|
+
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
647
|
+
}
|
|
648
|
+
const items = [];
|
|
649
|
+
for (const part of inner.split(",")) {
|
|
650
|
+
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
651
|
+
if (item === "")
|
|
652
|
+
continue;
|
|
653
|
+
items.push(item);
|
|
654
|
+
}
|
|
655
|
+
return items;
|
|
600
656
|
}
|
|
601
|
-
function
|
|
657
|
+
function isPlainObject2(value) {
|
|
602
658
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
603
659
|
}
|
|
604
|
-
function
|
|
660
|
+
function violation2(severity, code, message, fix) {
|
|
605
661
|
return { ok: false, severity, code, message, fix };
|
|
606
662
|
}
|
|
607
663
|
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
608
664
|
if (value === undefined) {
|
|
609
|
-
violations.push(
|
|
665
|
+
violations.push(violation2("high", missingCode, `missing required field: ${field}`));
|
|
610
666
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
611
|
-
violations.push(
|
|
667
|
+
violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
|
|
612
668
|
}
|
|
613
669
|
}
|
|
614
670
|
function isValidClaimedAt(value) {
|
|
@@ -616,66 +672,66 @@ function isValidClaimedAt(value) {
|
|
|
616
672
|
}
|
|
617
673
|
function validateExecutionLease(lease) {
|
|
618
674
|
const violations = [];
|
|
619
|
-
if (!
|
|
675
|
+
if (!isPlainObject2(lease)) {
|
|
620
676
|
return {
|
|
621
677
|
ok: false,
|
|
622
678
|
violations: [
|
|
623
|
-
|
|
679
|
+
violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
|
|
624
680
|
]
|
|
625
681
|
};
|
|
626
682
|
}
|
|
627
683
|
validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
628
684
|
if (lease.claimed_at === undefined) {
|
|
629
|
-
violations.push(
|
|
685
|
+
violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
630
686
|
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
631
|
-
violations.push(
|
|
687
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
|
|
632
688
|
}
|
|
633
689
|
if (lease.worktree_path === undefined) {
|
|
634
|
-
violations.push(
|
|
690
|
+
violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
635
691
|
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
636
|
-
violations.push(
|
|
637
|
-
} else if (!
|
|
638
|
-
violations.push(
|
|
692
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
693
|
+
} else if (!isAbsolute2(lease.worktree_path)) {
|
|
694
|
+
violations.push(violation2("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)"));
|
|
639
695
|
}
|
|
640
696
|
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
641
697
|
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
642
|
-
violations.push(
|
|
698
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
643
699
|
}
|
|
644
700
|
return { ok: violations.length === 0, violations };
|
|
645
701
|
}
|
|
646
702
|
function validateIntegrationMergeLease(lease) {
|
|
647
703
|
const violations = [];
|
|
648
|
-
if (!
|
|
704
|
+
if (!isPlainObject2(lease)) {
|
|
649
705
|
return {
|
|
650
706
|
ok: false,
|
|
651
707
|
violations: [
|
|
652
|
-
|
|
708
|
+
violation2("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
|
|
653
709
|
]
|
|
654
710
|
};
|
|
655
711
|
}
|
|
656
712
|
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
657
713
|
if (lease.claimed_at === undefined) {
|
|
658
|
-
violations.push(
|
|
714
|
+
violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
659
715
|
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
660
|
-
violations.push(
|
|
716
|
+
violations.push(violation2("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
661
717
|
}
|
|
662
718
|
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
663
719
|
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
664
720
|
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
665
721
|
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
666
|
-
violations.push(
|
|
722
|
+
violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
667
723
|
}
|
|
668
724
|
return { ok: violations.length === 0, violations };
|
|
669
725
|
}
|
|
670
726
|
function claimLease(row, holder, fields) {
|
|
671
727
|
const lease = row.execution_lease;
|
|
672
728
|
if (lease !== undefined) {
|
|
673
|
-
if (!
|
|
729
|
+
if (!isPlainObject2(lease)) {
|
|
674
730
|
return {
|
|
675
731
|
ok: false,
|
|
676
732
|
row,
|
|
677
733
|
violations: [
|
|
678
|
-
|
|
734
|
+
violation2("high", "lease.claim.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before claiming")
|
|
679
735
|
]
|
|
680
736
|
};
|
|
681
737
|
}
|
|
@@ -684,7 +740,7 @@ function claimLease(row, holder, fields) {
|
|
|
684
740
|
ok: false,
|
|
685
741
|
row,
|
|
686
742
|
violations: [
|
|
687
|
-
|
|
743
|
+
violation2("high", "lease.claim.other-holder", `execution_lease held by ${JSON.stringify(lease.holder)} — no timestamp makes it stealable; Blocked unless the current-turn user explicitly overrides (then audit plans[].notes)`)
|
|
688
744
|
]
|
|
689
745
|
};
|
|
690
746
|
}
|
|
@@ -693,7 +749,7 @@ function claimLease(row, holder, fields) {
|
|
|
693
749
|
ok: false,
|
|
694
750
|
row,
|
|
695
751
|
violations: [
|
|
696
|
-
|
|
752
|
+
violation2("high", "lease.claim.verify-held-lease", `same holder but lease ${lease.worktree_path} @ ${lease.working_branch} does not match the Assignment ${fields.worktree_path} @ ${fields.working_branch} — verify-held-lease failed`)
|
|
697
753
|
]
|
|
698
754
|
};
|
|
699
755
|
}
|
|
@@ -704,7 +760,7 @@ function claimLease(row, holder, fields) {
|
|
|
704
760
|
ok: false,
|
|
705
761
|
row,
|
|
706
762
|
violations: [
|
|
707
|
-
|
|
763
|
+
violation2("high", "lease.claim.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery); do not invent a lease")
|
|
708
764
|
]
|
|
709
765
|
};
|
|
710
766
|
}
|
|
@@ -713,7 +769,7 @@ function claimLease(row, holder, fields) {
|
|
|
713
769
|
ok: false,
|
|
714
770
|
row,
|
|
715
771
|
violations: [
|
|
716
|
-
|
|
772
|
+
violation2("high", "lease.claim.status", `claim requires status Todo or Blocked (got ${JSON.stringify(row.status)}) — claim-before-InProgress contract`)
|
|
717
773
|
]
|
|
718
774
|
};
|
|
719
775
|
}
|
|
@@ -734,12 +790,12 @@ function releaseLease(row, holder) {
|
|
|
734
790
|
if (row.execution_lease === undefined) {
|
|
735
791
|
return { ok: true, row, outcome: "released", violations: [] };
|
|
736
792
|
}
|
|
737
|
-
if (!
|
|
793
|
+
if (!isPlainObject2(row.execution_lease)) {
|
|
738
794
|
return {
|
|
739
795
|
ok: false,
|
|
740
796
|
row,
|
|
741
797
|
violations: [
|
|
742
|
-
|
|
798
|
+
violation2("high", "lease.release.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before releasing")
|
|
743
799
|
]
|
|
744
800
|
};
|
|
745
801
|
}
|
|
@@ -748,7 +804,7 @@ function releaseLease(row, holder) {
|
|
|
748
804
|
ok: false,
|
|
749
805
|
row,
|
|
750
806
|
violations: [
|
|
751
|
-
|
|
807
|
+
violation2("high", "lease.release.other-holder", `execution_lease held by ${JSON.stringify(row.execution_lease.holder)} — release requires the same-session holder; a different holder must Blocked (no timestamp makes it stealable)`)
|
|
752
808
|
]
|
|
753
809
|
};
|
|
754
810
|
}
|
|
@@ -756,10 +812,10 @@ function releaseLease(row, holder) {
|
|
|
756
812
|
return { ok: true, row: rest, outcome: "released", violations: [] };
|
|
757
813
|
}
|
|
758
814
|
function sameHolderResume(lease, holder) {
|
|
759
|
-
return
|
|
815
|
+
return isPlainObject2(lease) && lease.holder === holder;
|
|
760
816
|
}
|
|
761
817
|
function canSteal(lease, holder, opts = {}) {
|
|
762
|
-
if (!
|
|
818
|
+
if (!isPlainObject2(lease) || lease.holder === holder)
|
|
763
819
|
return false;
|
|
764
820
|
return opts.userOverride === true;
|
|
765
821
|
}
|
|
@@ -774,14 +830,14 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
774
830
|
return {
|
|
775
831
|
ok: false,
|
|
776
832
|
violations: [
|
|
777
|
-
|
|
833
|
+
violation2("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
|
|
778
834
|
]
|
|
779
835
|
};
|
|
780
836
|
}
|
|
781
837
|
return {
|
|
782
838
|
ok: false,
|
|
783
839
|
violations: [
|
|
784
|
-
|
|
840
|
+
violation2("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
785
841
|
]
|
|
786
842
|
};
|
|
787
843
|
}
|
|
@@ -790,7 +846,7 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
790
846
|
return { ok: violations.length === 0, violations, lease };
|
|
791
847
|
}
|
|
792
848
|
async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
793
|
-
const lockDir = join4(
|
|
849
|
+
const lockDir = join4(dirname3(resolve3(statusPath)), STATUS_WRITE_LOCKDIR);
|
|
794
850
|
const held = heldLockDirs.getStore();
|
|
795
851
|
if (held !== undefined && held.has(lockDir)) {
|
|
796
852
|
throw new Error(`${lockDir} is already held by this process in this async context — withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
|
|
@@ -801,8 +857,8 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
801
857
|
let acquired = null;
|
|
802
858
|
for (;; ) {
|
|
803
859
|
try {
|
|
804
|
-
|
|
805
|
-
const st =
|
|
860
|
+
mkdirSync2(lockDir);
|
|
861
|
+
const st = statSync2(lockDir);
|
|
806
862
|
acquired = { dev: st.dev, ino: st.ino };
|
|
807
863
|
break;
|
|
808
864
|
} catch (error) {
|
|
@@ -824,7 +880,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
824
880
|
} finally {
|
|
825
881
|
owns.delete(lockDir);
|
|
826
882
|
try {
|
|
827
|
-
const current =
|
|
883
|
+
const current = statSync2(lockDir);
|
|
828
884
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
829
885
|
try {
|
|
830
886
|
unlinkSync2(join4(lockDir, LOCKDIR_HOLDER_PID));
|
|
@@ -834,7 +890,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
834
890
|
} catch {}
|
|
835
891
|
}
|
|
836
892
|
}
|
|
837
|
-
function
|
|
893
|
+
function violation3(severity, code, message, fix) {
|
|
838
894
|
return { ok: false, severity, code, message, fix };
|
|
839
895
|
}
|
|
840
896
|
function parseAssignmentFields(assignmentText) {
|
|
@@ -880,11 +936,11 @@ function parseEnforcementFlag(text) {
|
|
|
880
936
|
}
|
|
881
937
|
function requireField(violations, value, label, code) {
|
|
882
938
|
if (value === undefined) {
|
|
883
|
-
const v =
|
|
939
|
+
const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
|
|
884
940
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
885
941
|
violations.push(v);
|
|
886
942
|
} else if (value === "") {
|
|
887
|
-
const v =
|
|
943
|
+
const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
|
|
888
944
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
889
945
|
violations.push(v);
|
|
890
946
|
}
|
|
@@ -943,20 +999,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
|
|
|
943
999
|
const formCount = Number(workingPresent) + Number(policyPresent);
|
|
944
1000
|
const forms = parseAssignmentBranchForms(assignmentText);
|
|
945
1001
|
if (formCount === 0) {
|
|
946
|
-
violations.push(
|
|
1002
|
+
violations.push(violation3("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
947
1003
|
} else if (formCount > 1) {
|
|
948
|
-
violations.push(
|
|
1004
|
+
violations.push(violation3("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
949
1005
|
} else if (workingPresent) {
|
|
950
1006
|
const create = forms.createForm;
|
|
951
1007
|
if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
|
|
952
|
-
violations.push(
|
|
1008
|
+
violations.push(violation3("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
|
|
953
1009
|
}
|
|
954
1010
|
} else if (policyPresent) {
|
|
955
1011
|
const direct = forms.directOn;
|
|
956
1012
|
if (direct === undefined) {
|
|
957
|
-
violations.push(
|
|
1013
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
|
|
958
1014
|
} else if (direct.reason === "") {
|
|
959
|
-
violations.push(
|
|
1015
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
|
|
960
1016
|
}
|
|
961
1017
|
}
|
|
962
1018
|
}
|
|
@@ -967,7 +1023,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
|
|
|
967
1023
|
const violations = [];
|
|
968
1024
|
const normalized = branch.trim();
|
|
969
1025
|
if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
|
|
970
|
-
violations.push(
|
|
1026
|
+
violations.push(violation3("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} — <reason>" to the Assignment, or use a feature branch`));
|
|
971
1027
|
}
|
|
972
1028
|
return { ok: violations.length === 0, violations };
|
|
973
1029
|
}
|
|
@@ -976,7 +1032,7 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
976
1032
|
const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
977
1033
|
let n;
|
|
978
1034
|
if (mode === "") {
|
|
979
|
-
violations.push(
|
|
1035
|
+
violations.push(violation3("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
|
|
980
1036
|
} else if (mode === "sdd") {
|
|
981
1037
|
n = 3;
|
|
982
1038
|
} else if (mode === "inline") {
|
|
@@ -984,14 +1040,14 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
984
1040
|
} else if (mode === "targeted") {
|
|
985
1041
|
const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
|
|
986
1042
|
if (seats.length === 0) {
|
|
987
|
-
violations.push(
|
|
1043
|
+
violations.push(violation3("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
|
|
988
1044
|
} else if (seats.length > 3) {
|
|
989
|
-
violations.push(
|
|
1045
|
+
violations.push(violation3("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
|
|
990
1046
|
} else {
|
|
991
1047
|
n = seats.length;
|
|
992
1048
|
}
|
|
993
1049
|
} else {
|
|
994
|
-
violations.push(
|
|
1050
|
+
violations.push(violation3("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
|
|
995
1051
|
}
|
|
996
1052
|
return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
|
|
997
1053
|
}
|
|
@@ -1005,7 +1061,7 @@ function assertTriIdentity(reviewerRoles) {
|
|
|
1005
1061
|
return {
|
|
1006
1062
|
ok: false,
|
|
1007
1063
|
violations: [
|
|
1008
|
-
|
|
1064
|
+
violation3("high", "dispatch.tri-identity.invalid", `tri-review initial wave must be exactly qc-specialist / qc-specialist-2 / qc-specialist-3, got: ${got}`, "dispatch qc-specialist, qc-specialist-2 and qc-specialist-3 for the initial wave")
|
|
1009
1065
|
]
|
|
1010
1066
|
};
|
|
1011
1067
|
}
|
|
@@ -1049,7 +1105,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1049
1105
|
return {
|
|
1050
1106
|
ok: false,
|
|
1051
1107
|
violations: [
|
|
1052
|
-
|
|
1108
|
+
violation3("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
|
|
1053
1109
|
]
|
|
1054
1110
|
};
|
|
1055
1111
|
}
|
|
@@ -1057,51 +1113,51 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1057
1113
|
return {
|
|
1058
1114
|
ok: false,
|
|
1059
1115
|
violations: [
|
|
1060
|
-
|
|
1116
|
+
violation3("critical", "dispatch.anti-recursion.self-type", `recursive dispatch refused: role binding "${subagentType}" equals Execute as "${executeAs}" (leaf executors must not re-invoke their own role)`, "complete the work in this session, or return Blocked to project-manager")
|
|
1061
1117
|
]
|
|
1062
1118
|
};
|
|
1063
1119
|
}
|
|
1064
1120
|
return { ok: true, violations: [] };
|
|
1065
1121
|
}
|
|
1066
|
-
function
|
|
1122
|
+
function isPlainObject3(value) {
|
|
1067
1123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1068
1124
|
}
|
|
1069
|
-
function
|
|
1125
|
+
function violation4(severity, code, message, fix) {
|
|
1070
1126
|
return { ok: false, severity, code, message, fix };
|
|
1071
1127
|
}
|
|
1072
1128
|
function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
|
|
1073
1129
|
if (value === undefined) {
|
|
1074
|
-
violations.push(
|
|
1130
|
+
violations.push(violation4("high", missingCode, `missing required field: ${field}`));
|
|
1075
1131
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1076
|
-
violations.push(
|
|
1132
|
+
violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1077
1133
|
}
|
|
1078
1134
|
}
|
|
1079
1135
|
function validateWorkflowSnapshot(doc) {
|
|
1080
1136
|
const violations = [];
|
|
1081
|
-
if (!
|
|
1137
|
+
if (!isPlainObject3(doc)) {
|
|
1082
1138
|
return {
|
|
1083
1139
|
ok: false,
|
|
1084
|
-
violations: [
|
|
1140
|
+
violations: [violation4("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
1085
1141
|
};
|
|
1086
1142
|
}
|
|
1087
1143
|
if (doc.schema_version === undefined) {
|
|
1088
|
-
violations.push(
|
|
1144
|
+
violations.push(violation4("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
1089
1145
|
} else if (doc.schema_version !== 1) {
|
|
1090
|
-
violations.push(
|
|
1146
|
+
violations.push(violation4("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)`));
|
|
1091
1147
|
}
|
|
1092
1148
|
if (doc.version !== undefined) {
|
|
1093
|
-
violations.push(
|
|
1149
|
+
violations.push(violation4("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"));
|
|
1094
1150
|
}
|
|
1095
1151
|
validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
1096
1152
|
if (doc.type === undefined) {
|
|
1097
|
-
violations.push(
|
|
1153
|
+
violations.push(violation4("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
1098
1154
|
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
|
|
1099
|
-
violations.push(
|
|
1155
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
|
|
1100
1156
|
}
|
|
1101
1157
|
if (doc.status === undefined) {
|
|
1102
|
-
violations.push(
|
|
1158
|
+
violations.push(violation4("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
1103
1159
|
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
|
|
1104
|
-
violations.push(
|
|
1160
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
|
|
1105
1161
|
}
|
|
1106
1162
|
validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
1107
1163
|
validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
@@ -1109,35 +1165,35 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1109
1165
|
validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
1110
1166
|
}
|
|
1111
1167
|
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
1112
|
-
violations.push(
|
|
1168
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
1113
1169
|
}
|
|
1114
1170
|
if (doc.plans === undefined) {
|
|
1115
|
-
violations.push(
|
|
1171
|
+
violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
1116
1172
|
} else if (!Array.isArray(doc.plans)) {
|
|
1117
|
-
violations.push(
|
|
1173
|
+
violations.push(violation4("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
1118
1174
|
} else {
|
|
1119
1175
|
for (const row of doc.plans) {
|
|
1120
1176
|
violations.push(...validatePlanRow(row).violations);
|
|
1121
|
-
if (
|
|
1177
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
1122
1178
|
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
1123
1179
|
}
|
|
1124
1180
|
}
|
|
1125
1181
|
}
|
|
1126
1182
|
if (doc.execution_policy !== undefined) {
|
|
1127
|
-
if (!
|
|
1128
|
-
violations.push(
|
|
1183
|
+
if (!isPlainObject3(doc.execution_policy)) {
|
|
1184
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
1129
1185
|
}
|
|
1130
1186
|
}
|
|
1131
1187
|
if (doc.integration_merge_lease !== undefined) {
|
|
1132
1188
|
violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
|
|
1133
1189
|
}
|
|
1134
1190
|
if (doc.branch !== undefined) {
|
|
1135
|
-
if (!
|
|
1136
|
-
violations.push(
|
|
1191
|
+
if (!isPlainObject3(doc.branch)) {
|
|
1192
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
1137
1193
|
} else {
|
|
1138
1194
|
for (const key of ["base", "integration", "target"]) {
|
|
1139
1195
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
1140
|
-
violations.push(
|
|
1196
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
1141
1197
|
}
|
|
1142
1198
|
}
|
|
1143
1199
|
}
|
|
@@ -1145,8 +1201,8 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1145
1201
|
if (doc.control_worktree_path !== undefined) {
|
|
1146
1202
|
validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
|
|
1147
1203
|
}
|
|
1148
|
-
if (doc.legacy_metadata !== undefined && !
|
|
1149
|
-
violations.push(
|
|
1204
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject3(doc.legacy_metadata)) {
|
|
1205
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
1150
1206
|
}
|
|
1151
1207
|
if (doc.compass_ref !== undefined) {
|
|
1152
1208
|
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
@@ -1154,17 +1210,17 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1154
1210
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
1155
1211
|
if (terminal) {
|
|
1156
1212
|
if (doc.ended_at === undefined) {
|
|
1157
|
-
violations.push(
|
|
1213
|
+
violations.push(violation4("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
|
|
1158
1214
|
}
|
|
1159
1215
|
if (Array.isArray(doc.plans)) {
|
|
1160
1216
|
for (const row of doc.plans) {
|
|
1161
|
-
if (
|
|
1162
|
-
violations.push(
|
|
1217
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
1218
|
+
violations.push(violation4("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`));
|
|
1163
1219
|
}
|
|
1164
1220
|
}
|
|
1165
1221
|
}
|
|
1166
1222
|
if (doc.integration_merge_lease !== undefined) {
|
|
1167
|
-
violations.push(
|
|
1223
|
+
violations.push(violation4("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"));
|
|
1168
1224
|
}
|
|
1169
1225
|
}
|
|
1170
1226
|
return { ok: violations.length === 0, violations };
|
|
@@ -1176,15 +1232,15 @@ async function writeWorkflowSnapshot(snapshot, dir) {
|
|
|
1176
1232
|
throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
|
|
1177
1233
|
}
|
|
1178
1234
|
const snapshotPath = join5(dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1179
|
-
|
|
1235
|
+
mkdirSync3(dir, { recursive: true });
|
|
1180
1236
|
await withStatusWriteLock(snapshotPath, () => {
|
|
1181
1237
|
writeJson(snapshotPath, snapshot);
|
|
1182
1238
|
});
|
|
1183
1239
|
}
|
|
1184
|
-
function
|
|
1240
|
+
function isPlainObject4(value) {
|
|
1185
1241
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1186
1242
|
}
|
|
1187
|
-
function
|
|
1243
|
+
function violation5(severity, code, message, fix) {
|
|
1188
1244
|
return { ok: false, severity, code, message, fix };
|
|
1189
1245
|
}
|
|
1190
1246
|
function todayString() {
|
|
@@ -1207,19 +1263,19 @@ function isOpenResidual(entry) {
|
|
|
1207
1263
|
}
|
|
1208
1264
|
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
1209
1265
|
if (value === undefined) {
|
|
1210
|
-
violations.push(
|
|
1266
|
+
violations.push(violation5("high", missingCode, `missing required field: ${field}`));
|
|
1211
1267
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1212
|
-
violations.push(
|
|
1268
|
+
violations.push(violation5("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1213
1269
|
}
|
|
1214
1270
|
}
|
|
1215
1271
|
function validatePlanRow(row) {
|
|
1216
1272
|
const violations = [];
|
|
1217
|
-
if (!
|
|
1218
|
-
return { ok: false, violations: [
|
|
1273
|
+
if (!isPlainObject4(row)) {
|
|
1274
|
+
return { ok: false, violations: [violation5("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
1219
1275
|
}
|
|
1220
1276
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
1221
1277
|
if (id === undefined && planId === undefined) {
|
|
1222
|
-
violations.push(
|
|
1278
|
+
violations.push(violation5("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
1223
1279
|
} else {
|
|
1224
1280
|
if (id !== undefined) {
|
|
1225
1281
|
validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
@@ -1228,31 +1284,31 @@ function validatePlanRow(row) {
|
|
|
1228
1284
|
validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
1229
1285
|
}
|
|
1230
1286
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
1231
|
-
violations.push(
|
|
1287
|
+
violations.push(violation5("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
|
|
1232
1288
|
}
|
|
1233
1289
|
}
|
|
1234
1290
|
validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
1235
1291
|
validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
1236
1292
|
if (status === undefined) {
|
|
1237
|
-
violations.push(
|
|
1293
|
+
violations.push(violation5("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
1238
1294
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
1239
|
-
violations.push(
|
|
1295
|
+
violations.push(violation5("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
|
|
1240
1296
|
}
|
|
1241
|
-
if (metadata !== undefined && !
|
|
1242
|
-
violations.push(
|
|
1297
|
+
if (metadata !== undefined && !isPlainObject4(metadata)) {
|
|
1298
|
+
violations.push(violation5("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
1243
1299
|
}
|
|
1244
|
-
if (execution_lease !== undefined && !
|
|
1245
|
-
violations.push(
|
|
1300
|
+
if (execution_lease !== undefined && !isPlainObject4(execution_lease)) {
|
|
1301
|
+
violations.push(violation5("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
1246
1302
|
}
|
|
1247
1303
|
if (status === "Done" && execution_lease !== undefined) {
|
|
1248
|
-
violations.push(
|
|
1304
|
+
violations.push(violation5("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"'));
|
|
1249
1305
|
}
|
|
1250
1306
|
return { ok: violations.length === 0, violations };
|
|
1251
1307
|
}
|
|
1252
1308
|
function validateResidual(entry) {
|
|
1253
1309
|
const violations = [];
|
|
1254
|
-
if (!
|
|
1255
|
-
return { ok: false, violations: [
|
|
1310
|
+
if (!isPlainObject4(entry)) {
|
|
1311
|
+
return { ok: false, violations: [violation5("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
1256
1312
|
}
|
|
1257
1313
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
1258
1314
|
validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
|
|
@@ -1261,42 +1317,42 @@ function validateResidual(entry) {
|
|
|
1261
1317
|
validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
|
|
1262
1318
|
validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
|
|
1263
1319
|
if (severity === undefined) {
|
|
1264
|
-
violations.push(
|
|
1320
|
+
violations.push(violation5("high", "status.residual.missing-severity", "missing required field: severity"));
|
|
1265
1321
|
} else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
|
|
1266
|
-
violations.push(
|
|
1322
|
+
violations.push(violation5("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
|
|
1267
1323
|
} else if (severity === "warning") {
|
|
1268
|
-
violations.push(
|
|
1324
|
+
violations.push(violation5("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')`));
|
|
1269
1325
|
}
|
|
1270
1326
|
if (decision === undefined) {
|
|
1271
|
-
violations.push(
|
|
1327
|
+
violations.push(violation5("high", "status.residual.missing-decision", "missing required field: decision"));
|
|
1272
1328
|
} else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
|
|
1273
|
-
violations.push(
|
|
1329
|
+
violations.push(violation5("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
|
|
1274
1330
|
}
|
|
1275
1331
|
if (target === undefined) {
|
|
1276
|
-
violations.push(
|
|
1332
|
+
violations.push(violation5("high", "status.residual.missing-target", "missing required field: target"));
|
|
1277
1333
|
} else if (typeof target !== "string" && target !== null) {
|
|
1278
|
-
violations.push(
|
|
1334
|
+
violations.push(violation5("medium", "status.residual.invalid-target", "target must be a string or null"));
|
|
1279
1335
|
}
|
|
1280
1336
|
if (tracking === undefined) {
|
|
1281
|
-
violations.push(
|
|
1337
|
+
violations.push(violation5("high", "status.residual.missing-tracking", "missing required field: tracking"));
|
|
1282
1338
|
} else if (typeof tracking !== "string" && tracking !== null) {
|
|
1283
|
-
violations.push(
|
|
1339
|
+
violations.push(violation5("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
|
|
1284
1340
|
}
|
|
1285
1341
|
if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
|
|
1286
|
-
violations.push(
|
|
1342
|
+
violations.push(violation5("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
|
|
1287
1343
|
}
|
|
1288
|
-
if (closed_at !== undefined && (typeof closed_at !== "string" || !
|
|
1289
|
-
violations.push(
|
|
1344
|
+
if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE2.test(closed_at))) {
|
|
1345
|
+
violations.push(violation5("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
|
|
1290
1346
|
}
|
|
1291
1347
|
if (lifecycle !== undefined) {
|
|
1292
1348
|
if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
|
|
1293
|
-
violations.push(
|
|
1349
|
+
violations.push(violation5("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
|
|
1294
1350
|
} else if (lifecycle !== "open") {
|
|
1295
1351
|
if (closed_at === undefined) {
|
|
1296
|
-
violations.push(
|
|
1352
|
+
violations.push(violation5("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
|
|
1297
1353
|
}
|
|
1298
1354
|
if (entry.closure_note === undefined) {
|
|
1299
|
-
violations.push(
|
|
1355
|
+
violations.push(violation5("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
|
|
1300
1356
|
}
|
|
1301
1357
|
}
|
|
1302
1358
|
}
|
|
@@ -1311,25 +1367,25 @@ function isHarnessRelativePath(dir) {
|
|
|
1311
1367
|
}
|
|
1312
1368
|
function validateWorkflowEntry(entry) {
|
|
1313
1369
|
const violations = [];
|
|
1314
|
-
if (!
|
|
1370
|
+
if (!isPlainObject4(entry)) {
|
|
1315
1371
|
return {
|
|
1316
1372
|
ok: false,
|
|
1317
|
-
violations: [
|
|
1373
|
+
violations: [violation5("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
1318
1374
|
};
|
|
1319
1375
|
}
|
|
1320
1376
|
validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
1321
1377
|
if (entry.type === undefined) {
|
|
1322
|
-
violations.push(
|
|
1378
|
+
violations.push(violation5("high", "status.workflow.missing-type", "missing required field: type"));
|
|
1323
1379
|
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
|
|
1324
|
-
violations.push(
|
|
1380
|
+
violations.push(violation5("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
|
|
1325
1381
|
}
|
|
1326
1382
|
validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
1327
1383
|
if (entry.dir === undefined) {
|
|
1328
|
-
violations.push(
|
|
1384
|
+
violations.push(violation5("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
1329
1385
|
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
1330
|
-
violations.push(
|
|
1386
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
1331
1387
|
} else if (!isHarnessRelativePath(entry.dir)) {
|
|
1332
|
-
violations.push(
|
|
1388
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
|
|
1333
1389
|
}
|
|
1334
1390
|
return { ok: violations.length === 0, violations };
|
|
1335
1391
|
}
|
|
@@ -1339,24 +1395,24 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1339
1395
|
if (typeof docOrPath === "string") {
|
|
1340
1396
|
try {
|
|
1341
1397
|
doc = readJson(docOrPath);
|
|
1342
|
-
harnessDir =
|
|
1398
|
+
harnessDir = dirname4(resolve4(docOrPath));
|
|
1343
1399
|
} catch (error) {
|
|
1344
1400
|
return {
|
|
1345
1401
|
ok: false,
|
|
1346
|
-
violations: [
|
|
1402
|
+
violations: [violation5("high", "status.invalid-json", error.message)]
|
|
1347
1403
|
};
|
|
1348
1404
|
}
|
|
1349
1405
|
} else {
|
|
1350
1406
|
doc = docOrPath;
|
|
1351
1407
|
}
|
|
1352
|
-
if (!
|
|
1353
|
-
return { ok: false, violations: [
|
|
1408
|
+
if (!isPlainObject4(doc)) {
|
|
1409
|
+
return { ok: false, violations: [violation5("high", "status.invalid-doc", "status document must be an object")] };
|
|
1354
1410
|
}
|
|
1355
1411
|
if (doc.version !== 2) {
|
|
1356
1412
|
return {
|
|
1357
1413
|
ok: false,
|
|
1358
1414
|
violations: [
|
|
1359
|
-
|
|
1415
|
+
violation5("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`")
|
|
1360
1416
|
]
|
|
1361
1417
|
};
|
|
1362
1418
|
}
|
|
@@ -1364,7 +1420,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1364
1420
|
return {
|
|
1365
1421
|
ok: false,
|
|
1366
1422
|
violations: [
|
|
1367
|
-
|
|
1423
|
+
violation5("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`")
|
|
1368
1424
|
]
|
|
1369
1425
|
};
|
|
1370
1426
|
}
|
|
@@ -1372,27 +1428,27 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1372
1428
|
return {
|
|
1373
1429
|
ok: false,
|
|
1374
1430
|
violations: [
|
|
1375
|
-
|
|
1431
|
+
violation5("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`")
|
|
1376
1432
|
]
|
|
1377
1433
|
};
|
|
1378
1434
|
}
|
|
1379
1435
|
const violations = [];
|
|
1380
1436
|
if (doc.updated_at === undefined) {
|
|
1381
|
-
violations.push(
|
|
1382
|
-
} else if (typeof doc.updated_at !== "string" || !
|
|
1383
|
-
violations.push(
|
|
1437
|
+
violations.push(violation5("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
1438
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE2.test(doc.updated_at)) {
|
|
1439
|
+
violations.push(violation5("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
1384
1440
|
}
|
|
1385
1441
|
if (doc.workflows === undefined) {
|
|
1386
|
-
violations.push(
|
|
1442
|
+
violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
|
|
1387
1443
|
} else if (!Array.isArray(doc.workflows)) {
|
|
1388
|
-
violations.push(
|
|
1444
|
+
violations.push(violation5("high", "status.invalid-workflows", "workflows must be an array"));
|
|
1389
1445
|
} else {
|
|
1390
1446
|
const seen = new Set;
|
|
1391
1447
|
for (const entry of doc.workflows) {
|
|
1392
1448
|
violations.push(...validateWorkflowEntry(entry).violations);
|
|
1393
|
-
if (
|
|
1449
|
+
if (isPlainObject4(entry) && typeof entry.id === "string") {
|
|
1394
1450
|
if (seen.has(entry.id)) {
|
|
1395
|
-
violations.push(
|
|
1451
|
+
violations.push(violation5("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
1396
1452
|
}
|
|
1397
1453
|
seen.add(entry.id);
|
|
1398
1454
|
}
|
|
@@ -1401,47 +1457,47 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1401
1457
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
1402
1458
|
let realHarnessDir = null;
|
|
1403
1459
|
try {
|
|
1404
|
-
realHarnessDir =
|
|
1460
|
+
realHarnessDir = realpathSync(harnessDir);
|
|
1405
1461
|
} catch {}
|
|
1406
1462
|
for (const entry of doc.workflows) {
|
|
1407
|
-
if (!
|
|
1463
|
+
if (!isPlainObject4(entry) || typeof entry.dir !== "string")
|
|
1408
1464
|
continue;
|
|
1409
1465
|
const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1410
1466
|
const snapshotPath = join6(harnessDir, relSnapshot);
|
|
1411
1467
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
1412
1468
|
let physical;
|
|
1413
1469
|
try {
|
|
1414
|
-
physical =
|
|
1470
|
+
physical = realpathSync(snapshotPath);
|
|
1415
1471
|
} catch {
|
|
1416
|
-
violations.push(
|
|
1472
|
+
violations.push(violation5("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`));
|
|
1417
1473
|
continue;
|
|
1418
1474
|
}
|
|
1419
1475
|
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
1420
|
-
violations.push(
|
|
1476
|
+
violations.push(violation5("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)}`));
|
|
1421
1477
|
continue;
|
|
1422
1478
|
}
|
|
1423
1479
|
let snapshot;
|
|
1424
1480
|
try {
|
|
1425
1481
|
snapshot = readJson(snapshotPath);
|
|
1426
1482
|
} catch (error) {
|
|
1427
|
-
violations.push(
|
|
1483
|
+
violations.push(violation5("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
1428
1484
|
continue;
|
|
1429
1485
|
}
|
|
1430
1486
|
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
1431
|
-
violations.push(
|
|
1487
|
+
violations.push(violation5("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`));
|
|
1432
1488
|
}
|
|
1433
1489
|
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
1434
|
-
violations.push(
|
|
1490
|
+
violations.push(violation5("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`));
|
|
1435
1491
|
}
|
|
1436
1492
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
1437
|
-
violations.push(
|
|
1493
|
+
violations.push(violation5("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)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
|
|
1438
1494
|
}
|
|
1439
1495
|
}
|
|
1440
1496
|
}
|
|
1441
1497
|
return { ok: violations.length === 0, violations };
|
|
1442
1498
|
}
|
|
1443
1499
|
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1444
|
-
const harnessDir =
|
|
1500
|
+
const harnessDir = dirname4(statusPath);
|
|
1445
1501
|
const current = readJson(statusPath);
|
|
1446
1502
|
const fresh = Object.keys(current).length === 0;
|
|
1447
1503
|
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
@@ -1467,15 +1523,15 @@ async function registerWorkflow(root, entry) {
|
|
|
1467
1523
|
if (!entryGate.ok) {
|
|
1468
1524
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
1469
1525
|
}
|
|
1470
|
-
const statusPath =
|
|
1526
|
+
const statusPath = resolve4(root);
|
|
1471
1527
|
return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
|
|
1472
1528
|
}
|
|
1473
1529
|
async function unregisterWorkflow(root, id) {
|
|
1474
1530
|
if (typeof id !== "string" || id.trim() === "") {
|
|
1475
1531
|
throw new Error("refusing to unregister workflow: id must be a non-empty string");
|
|
1476
1532
|
}
|
|
1477
|
-
const statusPath =
|
|
1478
|
-
const harnessDir =
|
|
1533
|
+
const statusPath = resolve4(root);
|
|
1534
|
+
const harnessDir = dirname4(statusPath);
|
|
1479
1535
|
return withStatusWriteLock(statusPath, () => {
|
|
1480
1536
|
const current = readJson(statusPath);
|
|
1481
1537
|
if (Object.keys(current).length === 0) {
|
|
@@ -1532,8 +1588,8 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1532
1588
|
return { hard: false, source: "none" };
|
|
1533
1589
|
}
|
|
1534
1590
|
function resolveMstarcEnforcement(harnessDir) {
|
|
1535
|
-
const dir =
|
|
1536
|
-
const rc = loadMstarc(dir,
|
|
1591
|
+
const dir = resolve4(harnessDir);
|
|
1592
|
+
const rc = loadMstarc(dir, dirname4(dir));
|
|
1537
1593
|
const value = rc?.config.enforcement;
|
|
1538
1594
|
if (value === "hard")
|
|
1539
1595
|
return { hard: true, source: "mstarc" };
|
|
@@ -1547,980 +1603,946 @@ function resolveRepoEnforcement(harnessDir) {
|
|
|
1547
1603
|
return rc;
|
|
1548
1604
|
return resolveCompassEnforcement(harnessDir);
|
|
1549
1605
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
if (raw === undefined || raw.trim() === "")
|
|
1553
|
-
return DEFAULT_PROBE_TIMEOUT_MS;
|
|
1554
|
-
const parsed = Number(raw);
|
|
1555
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
1606
|
+
function isPlainObject5(value) {
|
|
1607
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1556
1608
|
}
|
|
1557
|
-
function
|
|
1609
|
+
function violation6(severity, code, message, fix) {
|
|
1558
1610
|
return { ok: false, severity, code, message, fix };
|
|
1559
1611
|
}
|
|
1560
|
-
function
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
encoding: "utf8",
|
|
1571
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1572
|
-
timeout
|
|
1573
|
-
});
|
|
1574
|
-
const branch = stdout.trim();
|
|
1575
|
-
if (branch === "")
|
|
1576
|
-
return { error: `no branch checked out (detached HEAD?) at "${worktreePath}"` };
|
|
1577
|
-
return { branch };
|
|
1578
|
-
} catch (err) {
|
|
1579
|
-
const e = err;
|
|
1580
|
-
if (e.killed === true || e.signal !== undefined) {
|
|
1581
|
-
return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
|
|
1582
|
-
}
|
|
1583
|
-
const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
|
|
1584
|
-
return { error: detail };
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
function l1PreDispatchCheck(input, opts = {}) {
|
|
1588
|
-
const violations = [];
|
|
1589
|
-
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
1590
|
-
if (controlWorktreePath.trim() === "") {
|
|
1591
|
-
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"));
|
|
1592
|
-
}
|
|
1593
|
-
if (leaseWorktreePath.trim() === "") {
|
|
1594
|
-
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"));
|
|
1595
|
-
}
|
|
1596
|
-
if (leaseWorkingBranch.trim() === "") {
|
|
1597
|
-
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"));
|
|
1598
|
-
}
|
|
1599
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
1600
|
-
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"));
|
|
1601
|
-
}
|
|
1602
|
-
if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
|
|
1603
|
-
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>`));
|
|
1604
|
-
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
1605
|
-
const probe = probeBranch(leaseWorktreePath, opts);
|
|
1606
|
-
if ("error" in probe) {
|
|
1607
|
-
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)"));
|
|
1608
|
-
} else if (probe.branch !== leaseWorkingBranch) {
|
|
1609
|
-
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`));
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return gate(violations);
|
|
1613
|
-
}
|
|
1614
|
-
function l2PreDispatchCheck(input, opts = {}) {
|
|
1615
|
-
const violations = [];
|
|
1616
|
-
const tracks = input.tracks ?? [];
|
|
1617
|
-
const seenPaths = new Set;
|
|
1618
|
-
if (tracks.length < 1) {
|
|
1619
|
-
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"));
|
|
1620
|
-
}
|
|
1621
|
-
tracks.forEach((track, index) => {
|
|
1622
|
-
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
1623
|
-
violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
if (!isAbsolute4(track.worktreePath)) {
|
|
1627
|
-
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>)`));
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
const normalized = resolve6(track.worktreePath);
|
|
1631
|
-
if (seenPaths.has(normalized)) {
|
|
1632
|
-
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"));
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
seenPaths.add(normalized);
|
|
1636
|
-
if (!existsSync4(track.worktreePath)) {
|
|
1637
|
-
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}`));
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
const probe = probeBranch(track.worktreePath, opts);
|
|
1641
|
-
if ("error" in probe) {
|
|
1642
|
-
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)"));
|
|
1643
|
-
} else if (probe.branch !== track.workingBranch) {
|
|
1644
|
-
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`));
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
return gate(violations);
|
|
1648
|
-
}
|
|
1649
|
-
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
|
|
1650
|
-
const violations = [];
|
|
1651
|
-
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(featureWorktreePath);
|
|
1652
|
-
if (samePath) {
|
|
1653
|
-
violations.push(violation5("critical", "worktree.control-feature.same", `control worktree path equals feature/lease worktree path "${controlWorktreePath}" — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "use a distinct feature worktree for the plan's product edits"));
|
|
1654
|
-
}
|
|
1655
|
-
return gate(violations);
|
|
1656
|
-
}
|
|
1657
|
-
function assertBranchAlignment(worktreePath, expectedBranch, opts = {}) {
|
|
1658
|
-
const violations = [];
|
|
1659
|
-
const probe = probeBranch(worktreePath, opts);
|
|
1660
|
-
if ("error" in probe) {
|
|
1661
|
-
violations.push(violation5("high", "worktree.branch-probe-failed", `cannot probe branch at "${worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on the expected branch (not detached)"));
|
|
1662
|
-
} else if (probe.branch !== expectedBranch) {
|
|
1663
|
-
violations.push(violation5("high", "worktree.branch-mismatch", `worktree "${worktreePath}" is on branch "${probe.branch}", expected "${expectedBranch}" (Assignment Working branch)`, `checkout ${expectedBranch} in that worktree`));
|
|
1664
|
-
}
|
|
1665
|
-
return gate(violations);
|
|
1666
|
-
}
|
|
1667
|
-
function assertQcAlignment(assignments) {
|
|
1668
|
-
const violations = [];
|
|
1669
|
-
const list = assignments ?? [];
|
|
1670
|
-
for (const { key, label } of QC_ALIGNMENT_FIELDS) {
|
|
1671
|
-
const distinct = [...new Set(list.map((a) => a[key]))];
|
|
1672
|
-
if (distinct.length > 1) {
|
|
1673
|
-
violations.push(violation5("high", "qc.alignment.mismatch", `QC/QA alignment field "${label}" is not byte-identical across ${list.length} assignments: ${distinct.map((v) => `"${v}"`).join(" vs ")}`, `copy the same ${label} value verbatim into every QC tri and QA Assignment`));
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
return gate(violations);
|
|
1677
|
-
}
|
|
1678
|
-
function singleReviewSnapshot(assignments) {
|
|
1679
|
-
const violations = [];
|
|
1680
|
-
const list = assignments ?? [];
|
|
1681
|
-
list.forEach((a, index) => {
|
|
1682
|
-
if ((a.head ?? "").trim() === "") {
|
|
1683
|
-
violations.push(violation5("high", "qc.alignment.snapshot-missing", `review head not provided for assignment ${index + 1} (plan_id "${a.planId}") — cannot confirm the single review snapshot precondition`, "precompute and pass the review HEAD (full SHA) for every assignment"));
|
|
1684
|
-
}
|
|
1685
|
-
});
|
|
1686
|
-
const distinct = [...new Set(list.map((a) => a.head ?? "").filter((h) => h.trim() !== ""))];
|
|
1687
|
-
if (distinct.length > 1) {
|
|
1688
|
-
violations.push(violation5("high", "qc.alignment.single-snapshot", `assignments cover ${distinct.length} different review heads (${distinct.join(", ")}) — all reviewable commits must sit on ONE Working branch HEAD before QC tri + QA`, "merge the parallel tracks to a single Working branch HEAD, then re-derive the heads"));
|
|
1689
|
-
}
|
|
1690
|
-
return gate(violations);
|
|
1691
|
-
}
|
|
1692
|
-
function isDirectory2(dir) {
|
|
1693
|
-
try {
|
|
1694
|
-
return statSync4(dir).isDirectory();
|
|
1695
|
-
} catch {
|
|
1696
|
-
return false;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
function isFile2(file) {
|
|
1700
|
-
try {
|
|
1701
|
-
return statSync4(file).isFile();
|
|
1702
|
-
} catch {
|
|
1703
|
-
return false;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
function gitOut(cwd, args) {
|
|
1707
|
-
try {
|
|
1708
|
-
return execFileSync3("git", args, {
|
|
1709
|
-
cwd,
|
|
1710
|
-
encoding: "utf8",
|
|
1711
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1712
|
-
maxBuffer: GIT_CAPTURE_MAX_BYTES
|
|
1713
|
-
}).trim();
|
|
1714
|
-
} catch {
|
|
1715
|
-
return null;
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
function probeHarnessWithStatus(root) {
|
|
1719
|
-
if (isFile2(join7(root, ".mstar", "status.json")))
|
|
1720
|
-
return join7(root, ".mstar");
|
|
1721
|
-
if (isFile2(join7(root, ".agents", "status.json")))
|
|
1722
|
-
return join7(root, ".agents");
|
|
1723
|
-
if (hasWorkflowSnapshot(join7(root, ".mstar")))
|
|
1724
|
-
return join7(root, ".mstar");
|
|
1725
|
-
if (hasWorkflowSnapshot(join7(root, ".agents")))
|
|
1726
|
-
return join7(root, ".agents");
|
|
1727
|
-
return null;
|
|
1728
|
-
}
|
|
1729
|
-
function hasWorkflowSnapshot(harnessDir) {
|
|
1730
|
-
let workflowsDir;
|
|
1731
|
-
try {
|
|
1732
|
-
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
1733
|
-
} catch {
|
|
1734
|
-
workflowsDir = join7(harnessDir, "workflows");
|
|
1735
|
-
}
|
|
1736
|
-
if (!isDirectory2(workflowsDir))
|
|
1737
|
-
return false;
|
|
1738
|
-
try {
|
|
1739
|
-
for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
|
|
1740
|
-
if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
|
|
1741
|
-
return true;
|
|
1742
|
-
}
|
|
1743
|
-
} catch {
|
|
1744
|
-
return false;
|
|
1745
|
-
}
|
|
1746
|
-
return false;
|
|
1747
|
-
}
|
|
1748
|
-
function isLinkedWorktree(root) {
|
|
1749
|
-
const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
|
|
1750
|
-
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
1751
|
-
if (gitDirRaw === null || commonRaw === null)
|
|
1752
|
-
return false;
|
|
1753
|
-
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
|
|
1754
|
-
const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
|
|
1755
|
-
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
1756
|
-
return true;
|
|
1612
|
+
function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
|
|
1613
|
+
if (value === undefined) {
|
|
1614
|
+
violations.push(violation6("high", missingCode, `missing required field: ${field}`));
|
|
1615
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1616
|
+
violations.push(violation6("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function validateRoadmap(filePath) {
|
|
1620
|
+
const violations = [];
|
|
1621
|
+
let content;
|
|
1757
1622
|
try {
|
|
1758
|
-
|
|
1759
|
-
const cmAbs = realpathSync3(common);
|
|
1760
|
-
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
1623
|
+
content = readFileSync5(filePath, "utf8");
|
|
1761
1624
|
} catch {
|
|
1762
|
-
return
|
|
1625
|
+
return {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
violations: [violation6("high", "project.roadmap.unreadable", `cannot read roadmap file: ${filePath}`)],
|
|
1628
|
+
warnings: []
|
|
1629
|
+
};
|
|
1763
1630
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1631
|
+
let doc;
|
|
1632
|
+
try {
|
|
1633
|
+
doc = parseCompassFrontmatterText(content, filePath);
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
const message = err instanceof Error ? err.message : `invalid roadmap frontmatter in ${filePath}`;
|
|
1636
|
+
return { ok: false, violations: [violation6("high", "project.roadmap.invalid-frontmatter", message)], warnings: [] };
|
|
1769
1637
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
root = realpathSync3(controlRoot);
|
|
1778
|
-
} else {
|
|
1779
|
-
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1780
|
-
root = realpathSync3(topLevel ?? cwd);
|
|
1638
|
+
validateNonEmptyString4(violations, doc.project_id, "project_id", "project.roadmap.missing-project-id", "project.roadmap.invalid-project-id");
|
|
1639
|
+
validateNonEmptyString4(violations, doc.title, "title", "project.roadmap.missing-title", "project.roadmap.invalid-title");
|
|
1640
|
+
if (doc.status === undefined) {
|
|
1641
|
+
violations.push(violation6("high", "project.roadmap.missing-status", "missing required field: status"));
|
|
1642
|
+
} else if (typeof doc.status !== "string" || !ROADMAP_STATUSES.includes(doc.status)) {
|
|
1643
|
+
violations.push(violation6("medium", "project.roadmap.invalid-status", `status must be one of ${ROADMAP_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
|
|
1781
1644
|
}
|
|
1782
|
-
if (
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
|
|
1645
|
+
if (doc.created_at === undefined) {
|
|
1646
|
+
violations.push(violation6("high", "project.roadmap.missing-created-at", "missing required field: created_at"));
|
|
1647
|
+
} else if (typeof doc.created_at !== "string" || !DATE_RE3.test(doc.created_at)) {
|
|
1648
|
+
violations.push(violation6("medium", "project.roadmap.invalid-created-at", "created_at must be YYYY-MM-DD"));
|
|
1787
1649
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
harnessDir = resolve7(root, harnessOverride);
|
|
1792
|
-
} else {
|
|
1793
|
-
const rc = findMstarc(root, root);
|
|
1794
|
-
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
|
|
1795
|
-
if (rcHarnessDir) {
|
|
1796
|
-
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
1650
|
+
if (doc.milestones !== undefined && doc.milestones !== null) {
|
|
1651
|
+
if (!Array.isArray(doc.milestones)) {
|
|
1652
|
+
violations.push(violation6("medium", "project.roadmap.invalid-milestones", "milestones must be a list of milestone names"));
|
|
1797
1653
|
} else {
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
} else if (isDirectory2(join7(root, ".agents"))) {
|
|
1804
|
-
harnessDir = join7(root, ".agents");
|
|
1805
|
-
} else {
|
|
1806
|
-
harnessDir = join7(root, ".mstar");
|
|
1654
|
+
for (const item of doc.milestones) {
|
|
1655
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
1656
|
+
violations.push(violation6("medium", "project.roadmap.invalid-milestones", "milestones items must be non-empty strings"));
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1807
1659
|
}
|
|
1808
1660
|
}
|
|
1809
1661
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
`);
|
|
1814
|
-
return realpathSync3(sddDir);
|
|
1815
|
-
}
|
|
1816
|
-
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
1817
|
-
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
1818
|
-
throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
|
|
1819
|
-
}
|
|
1820
|
-
let content;
|
|
1821
|
-
try {
|
|
1822
|
-
content = readFileSync5(planFile, "utf8");
|
|
1823
|
-
} catch {
|
|
1824
|
-
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
1825
|
-
}
|
|
1826
|
-
let out;
|
|
1827
|
-
if (outFile) {
|
|
1828
|
-
out = outFile;
|
|
1829
|
-
} else {
|
|
1830
|
-
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
1831
|
-
if (!sddDir) {
|
|
1832
|
-
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
1662
|
+
if (doc.residuals_ref !== undefined && doc.residuals_ref !== null) {
|
|
1663
|
+
if (typeof doc.residuals_ref !== "string" || doc.residuals_ref.trim() === "") {
|
|
1664
|
+
violations.push(violation6("medium", "project.roadmap.invalid-residuals-ref", "residuals_ref must be a non-empty string"));
|
|
1833
1665
|
}
|
|
1834
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
1835
|
-
out = join7(sddDir, `task-${taskN}-brief.md`);
|
|
1836
1666
|
}
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1667
|
+
const warnings = [];
|
|
1668
|
+
const fenceEnd = linesIndexOfClosingFence(content);
|
|
1669
|
+
const body = content.split(/\r?\n/).slice(fenceEnd + 1).join(`
|
|
1840
1670
|
`);
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
let infence = false;
|
|
1844
|
-
let intask = false;
|
|
1845
|
-
const printed = [];
|
|
1846
|
-
for (const line of records) {
|
|
1847
|
-
if (/^```/.test(line))
|
|
1848
|
-
infence = !infence;
|
|
1849
|
-
if (!infence && headingRe.test(line))
|
|
1850
|
-
intask = targetRe.test(line);
|
|
1851
|
-
if (intask)
|
|
1852
|
-
printed.push(line);
|
|
1671
|
+
if (!/^##\s+Direction\s*$/m.test(body)) {
|
|
1672
|
+
warnings.push(violation6("low", "project.roadmap.body.missing-direction", "roadmap body has no `## Direction` section (documented body convention) — state the project direction there"));
|
|
1853
1673
|
}
|
|
1854
|
-
|
|
1855
|
-
`)
|
|
1856
|
-
` : "";
|
|
1857
|
-
writeFileSync3(out, output);
|
|
1858
|
-
if (printed.length === 0) {
|
|
1859
|
-
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
1674
|
+
if (!/^\s*[-*]\s+\[[xX ]\]/m.test(body)) {
|
|
1675
|
+
warnings.push(violation6("low", "project.roadmap.body.no-goal-items", "roadmap body has no goal-item task list (documented body convention) — list goals as `- [ ]` / `- [x]` markdown task items"));
|
|
1860
1676
|
}
|
|
1861
|
-
return
|
|
1677
|
+
return { ok: violations.length === 0, violations, warnings };
|
|
1862
1678
|
}
|
|
1863
|
-
function
|
|
1864
|
-
|
|
1865
|
-
|
|
1679
|
+
function linesIndexOfClosingFence(content) {
|
|
1680
|
+
return content.split(/\r?\n/).indexOf("---", 1);
|
|
1681
|
+
}
|
|
1682
|
+
function validateProjectRegister(doc) {
|
|
1683
|
+
const violations = [];
|
|
1684
|
+
if (!isPlainObject5(doc)) {
|
|
1685
|
+
return {
|
|
1686
|
+
ok: false,
|
|
1687
|
+
violations: [violation6("high", "project.register.invalid", "project register must be an object")]
|
|
1688
|
+
};
|
|
1866
1689
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
} catch {
|
|
1872
|
-
throw new SddScriptError(`bad ${what}: ${ref}`, 2);
|
|
1873
|
-
}
|
|
1874
|
-
};
|
|
1875
|
-
verifyRef(base, "BASE");
|
|
1876
|
-
verifyRef(head, "HEAD");
|
|
1877
|
-
let out;
|
|
1878
|
-
if (outFile) {
|
|
1879
|
-
out = outFile;
|
|
1690
|
+
if (doc.entries === undefined) {
|
|
1691
|
+
violations.push(violation6("high", "project.register.missing-entries", "missing required field: entries"));
|
|
1692
|
+
} else if (!isPlainObject5(doc.entries)) {
|
|
1693
|
+
violations.push(violation6("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
|
|
1880
1694
|
} else {
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1695
|
+
for (const [key, entries] of Object.entries(doc.entries)) {
|
|
1696
|
+
if (key.trim() === "") {
|
|
1697
|
+
violations.push(violation6("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
|
|
1698
|
+
}
|
|
1699
|
+
if (!Array.isArray(entries)) {
|
|
1700
|
+
violations.push(violation6("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)`));
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
for (const entry of entries) {
|
|
1704
|
+
violations.push(...validateResidual(entry).violations);
|
|
1705
|
+
if (!isPlainObject5(entry))
|
|
1706
|
+
continue;
|
|
1707
|
+
validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
|
|
1708
|
+
if (entry.registered_at === undefined) {
|
|
1709
|
+
violations.push(violation6("high", "project.register.missing-registered-at", "missing required field: registered_at"));
|
|
1710
|
+
} else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
|
|
1711
|
+
violations.push(violation6("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
|
|
1712
|
+
}
|
|
1713
|
+
if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
|
|
1714
|
+
violations.push(violation6("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
|
|
1715
|
+
}
|
|
1716
|
+
if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
|
|
1717
|
+
violations.push(violation6("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`));
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1884
1720
|
}
|
|
1885
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
1886
|
-
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
1887
|
-
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
1888
|
-
out = join7(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
1889
|
-
}
|
|
1890
|
-
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
1891
|
-
const parts = [
|
|
1892
|
-
Buffer.from(`# Review package: ${base}..${head}
|
|
1893
|
-
|
|
1894
|
-
## Commits
|
|
1895
|
-
`),
|
|
1896
|
-
run(["log", "--oneline", `${base}..${head}`]),
|
|
1897
|
-
Buffer.from(`
|
|
1898
|
-
## Files changed
|
|
1899
|
-
`),
|
|
1900
|
-
run(["diff", "--stat", `${base}..${head}`]),
|
|
1901
|
-
Buffer.from(`
|
|
1902
|
-
## Diff
|
|
1903
|
-
`),
|
|
1904
|
-
run(["diff", "-U10", `${base}..${head}`])
|
|
1905
|
-
];
|
|
1906
|
-
writeFileSync3(out, Buffer.concat(parts));
|
|
1907
|
-
return out;
|
|
1908
|
-
}
|
|
1909
|
-
function assertBaseSha(ref, opts = {}) {
|
|
1910
|
-
if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
|
|
1911
|
-
throw new SddScriptError(`assertBaseSha: BASE must be a commit SHA (full or prefix); got ${JSON.stringify(ref)}. ` + "Never use HEAD~1 as review BASE (multi-commit tasks truncate).", 2);
|
|
1912
|
-
}
|
|
1913
|
-
try {
|
|
1914
|
-
execFileSync3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
1915
|
-
cwd: opts.cwd,
|
|
1916
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1917
|
-
});
|
|
1918
|
-
} catch {
|
|
1919
|
-
throw new SddScriptError(`assertBaseSha: commit not found: ${ref}`, 2);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
function taskReportExists(sddDir, taskN) {
|
|
1923
|
-
try {
|
|
1924
|
-
const st = statSync4(join7(sddDir, `task-${taskN}-report.md`));
|
|
1925
|
-
return st.isFile() && st.size > 0;
|
|
1926
|
-
} catch {
|
|
1927
|
-
return false;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
function readProgressLedger(sddDir) {
|
|
1931
|
-
let content;
|
|
1932
|
-
try {
|
|
1933
|
-
content = readFileSync5(join7(sddDir, "progress.md"), "utf8");
|
|
1934
|
-
} catch {
|
|
1935
|
-
return [];
|
|
1936
1721
|
}
|
|
1937
|
-
return
|
|
1938
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1722
|
+
return { ok: violations.length === 0, violations };
|
|
1939
1723
|
}
|
|
1940
|
-
function
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1724
|
+
function findingsCleanupGate(register, planId, opts) {
|
|
1725
|
+
const mode = opts?.mode ?? "allow-residual";
|
|
1726
|
+
const violations = [];
|
|
1727
|
+
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
1728
|
+
if (entries === undefined) {
|
|
1729
|
+
return { ok: true, violations };
|
|
1944
1730
|
}
|
|
1945
|
-
if (
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
reason: "host_agent_id is missing from implementer-session.json; fall back to fresh for this task " + "(mstar-sdd SKILL.md red flag: resume implementer without host_agent_id)"
|
|
1949
|
-
};
|
|
1731
|
+
if (!Array.isArray(entries)) {
|
|
1732
|
+
violations.push(violation6("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)`));
|
|
1733
|
+
return { ok: false, violations };
|
|
1950
1734
|
}
|
|
1951
|
-
if (
|
|
1952
|
-
return {
|
|
1953
|
-
resume: false,
|
|
1954
|
-
reason: `nextTask ${nextTask} <= last_task ${session.last_task}; task already completed in this session`
|
|
1955
|
-
};
|
|
1735
|
+
if (entries.length === 0) {
|
|
1736
|
+
return { ok: true, violations };
|
|
1956
1737
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
}
|
|
1738
|
+
for (const entry of entries) {
|
|
1739
|
+
if (!isOpenResidual(entry))
|
|
1740
|
+
continue;
|
|
1741
|
+
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
1742
|
+
const label = `R#${id}`;
|
|
1743
|
+
if (mode === "zero-residual") {
|
|
1744
|
+
if (entry.severity === "nit") {
|
|
1745
|
+
violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
|
|
1746
|
+
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
1747
|
+
violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
1748
|
+
} else if (entry.decision === "defer") {
|
|
1749
|
+
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
1750
|
+
violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
1751
|
+
}
|
|
1752
|
+
} else {
|
|
1753
|
+
violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
|
|
1754
|
+
}
|
|
1755
|
+
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
1756
|
+
violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
1757
|
+
}
|
|
1962
1758
|
}
|
|
1963
|
-
return {
|
|
1759
|
+
return { ok: violations.length === 0, violations };
|
|
1964
1760
|
}
|
|
1965
|
-
function
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1761
|
+
function groupCount(values) {
|
|
1762
|
+
const counts = new Map;
|
|
1763
|
+
for (const value of values) {
|
|
1764
|
+
const key = typeof value === "string" ? value : String(value);
|
|
1765
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1766
|
+
}
|
|
1767
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
1971
1768
|
}
|
|
1972
|
-
function
|
|
1973
|
-
const
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1769
|
+
function techDebtRollup(projectDir) {
|
|
1770
|
+
const items = [];
|
|
1771
|
+
let entries;
|
|
1772
|
+
try {
|
|
1773
|
+
entries = readdirSync3(projectDir, { withFileTypes: true });
|
|
1774
|
+
} catch {
|
|
1775
|
+
entries = [];
|
|
1776
|
+
}
|
|
1777
|
+
for (const project of entries) {
|
|
1778
|
+
if (!project.isDirectory())
|
|
1779
|
+
continue;
|
|
1780
|
+
const registerPath = join7(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
1781
|
+
if (!existsSync4(registerPath))
|
|
1782
|
+
continue;
|
|
1783
|
+
let register;
|
|
1784
|
+
try {
|
|
1785
|
+
register = readJson(registerPath);
|
|
1786
|
+
} catch {
|
|
1787
|
+
continue;
|
|
1983
1788
|
}
|
|
1984
|
-
if (
|
|
1985
|
-
|
|
1789
|
+
if (!isPlainObject5(register) || !isPlainObject5(register.entries))
|
|
1790
|
+
continue;
|
|
1791
|
+
for (const [plan, planEntries] of Object.entries(register.entries)) {
|
|
1792
|
+
if (!Array.isArray(planEntries))
|
|
1793
|
+
continue;
|
|
1794
|
+
for (const entry of planEntries) {
|
|
1795
|
+
if (!isPlainObject5(entry) || !isOpenResidual(entry))
|
|
1796
|
+
continue;
|
|
1797
|
+
items.push({ plan, entry });
|
|
1798
|
+
}
|
|
1986
1799
|
}
|
|
1800
|
+
}
|
|
1801
|
+
const bySeverity = {};
|
|
1802
|
+
for (const severity of SEVERITY_ORDER) {
|
|
1803
|
+
bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
|
|
1804
|
+
}
|
|
1805
|
+
const computed = {
|
|
1806
|
+
total_open: items.length,
|
|
1807
|
+
by_severity: bySeverity,
|
|
1808
|
+
by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
|
|
1809
|
+
by_plan: groupCount(items.map(({ plan }) => plan))
|
|
1987
1810
|
};
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
const
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1811
|
+
const stored = null;
|
|
1812
|
+
const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
|
|
1813
|
+
const overall = "DRIFT";
|
|
1814
|
+
return { computed, stored, checks, overall };
|
|
1815
|
+
}
|
|
1816
|
+
function listProjectReferenceFiles(projectDir) {
|
|
1817
|
+
const root = join7(projectDir, PROJECT_REFERENCES_DIR);
|
|
1818
|
+
let entries;
|
|
1819
|
+
try {
|
|
1820
|
+
entries = readdirSync3(root, { withFileTypes: true });
|
|
1821
|
+
} catch {
|
|
1822
|
+
return [];
|
|
1996
1823
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
if (
|
|
2002
|
-
|
|
2003
|
-
} else {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
1824
|
+
const files = [];
|
|
1825
|
+
for (const entry of entries) {
|
|
1826
|
+
if (entry.name === PROJECT_ROADMAP_FILE || entry.name === PROJECT_REGISTER_FILE)
|
|
1827
|
+
continue;
|
|
1828
|
+
if (entry.isFile()) {
|
|
1829
|
+
files.push(entry.name);
|
|
1830
|
+
} else if (entry.isDirectory()) {
|
|
1831
|
+
let nested;
|
|
1832
|
+
try {
|
|
1833
|
+
nested = readdirSync3(join7(root, entry.name), { withFileTypes: true });
|
|
1834
|
+
} catch {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
for (const child of nested) {
|
|
1838
|
+
if (child.isFile())
|
|
1839
|
+
files.push(`${entry.name}/${child.name}`);
|
|
1840
|
+
}
|
|
2011
1841
|
}
|
|
2012
1842
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1843
|
+
return files.sort();
|
|
1844
|
+
}
|
|
1845
|
+
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
1846
|
+
const start = resolve5(startDir);
|
|
1847
|
+
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
1848
|
+
if (explicit)
|
|
1849
|
+
return resolve5(start, explicit);
|
|
1850
|
+
const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
1851
|
+
const rc = loadMstarc(start, boundary);
|
|
1852
|
+
if (rc !== null && rc.config.harnessDir)
|
|
1853
|
+
return resolve5(rc.dir, rc.config.harnessDir);
|
|
1854
|
+
let dir = start;
|
|
1855
|
+
for (;; ) {
|
|
1856
|
+
if (!isAtOrBelow2(dir, boundary))
|
|
1857
|
+
return null;
|
|
1858
|
+
for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
|
|
1859
|
+
if (isDirectory(candidate))
|
|
1860
|
+
return candidate;
|
|
2019
1861
|
}
|
|
1862
|
+
if (dir === boundary)
|
|
1863
|
+
return null;
|
|
1864
|
+
const parent = dirname5(dir);
|
|
1865
|
+
if (parent === dir)
|
|
1866
|
+
return null;
|
|
1867
|
+
dir = parent;
|
|
2020
1868
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
1869
|
+
}
|
|
1870
|
+
function defaultWorkspaceRoot(startDir) {
|
|
1871
|
+
try {
|
|
1872
|
+
const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
|
|
1873
|
+
cwd: startDir,
|
|
1874
|
+
encoding: "utf8",
|
|
1875
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1876
|
+
}).trim();
|
|
1877
|
+
if (!cdup)
|
|
1878
|
+
return startDir;
|
|
1879
|
+
let boundary = startDir;
|
|
1880
|
+
for (const segment of cdup.split(/[\\/]/)) {
|
|
1881
|
+
if (segment && segment !== ".")
|
|
1882
|
+
boundary = dirname5(boundary);
|
|
2033
1883
|
}
|
|
2034
|
-
|
|
1884
|
+
return resolve5(boundary);
|
|
1885
|
+
} catch {}
|
|
1886
|
+
return startDir;
|
|
2035
1887
|
}
|
|
2036
|
-
function
|
|
2037
|
-
|
|
1888
|
+
function isAtOrBelow2(dir, root) {
|
|
1889
|
+
const rel = relative2(root, dir);
|
|
1890
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
2038
1891
|
}
|
|
2039
|
-
function
|
|
2040
|
-
|
|
1892
|
+
function mstarcDirOverride(harnessDir, key) {
|
|
1893
|
+
const dir = resolve5(harnessDir);
|
|
1894
|
+
const rc = loadMstarc(dir, dirname5(dir));
|
|
1895
|
+
const declared = rc?.config[key];
|
|
1896
|
+
return declared ? resolve5(rc.dir, declared) : null;
|
|
2041
1897
|
}
|
|
2042
|
-
function
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
]
|
|
2049
|
-
};
|
|
2050
|
-
}
|
|
2051
|
-
const parsed = validateCompassShape(doc);
|
|
2052
|
-
if (!parsed.ok) {
|
|
2053
|
-
return {
|
|
2054
|
-
ok: false,
|
|
2055
|
-
violations: parsed.issues.map((issue) => {
|
|
2056
|
-
const field = issue.path.join(".") || "(root)";
|
|
2057
|
-
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)`);
|
|
2058
|
-
})
|
|
2059
|
-
};
|
|
2060
|
-
}
|
|
2061
|
-
const violations = [];
|
|
2062
|
-
const { status, end_date } = parsed.data;
|
|
2063
|
-
if (status === "completed" && end_date === undefined) {
|
|
2064
|
-
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"));
|
|
1898
|
+
function resolveSpecsDir(harnessDir, opts = {}) {
|
|
1899
|
+
const declared = mstarcDirOverride(harnessDir, "specsDir");
|
|
1900
|
+
if (declared !== null) {
|
|
1901
|
+
if (opts.create !== false)
|
|
1902
|
+
mkdirSync4(declared, { recursive: true });
|
|
1903
|
+
return declared;
|
|
2065
1904
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
1905
|
+
const harness = resolve5(harnessDir);
|
|
1906
|
+
const repoRoot = dirname5(harness);
|
|
1907
|
+
const candidates = [
|
|
1908
|
+
join8(harness, "specs"),
|
|
1909
|
+
join8(repoRoot, "docs", "specs"),
|
|
1910
|
+
join8(repoRoot, "specs"),
|
|
1911
|
+
join8(harness, "designs"),
|
|
1912
|
+
join8(repoRoot, "designs")
|
|
1913
|
+
];
|
|
1914
|
+
for (const candidate of candidates) {
|
|
1915
|
+
if (isDirectory(candidate) && hasFiles(candidate))
|
|
1916
|
+
return candidate;
|
|
2068
1917
|
}
|
|
2069
|
-
|
|
1918
|
+
const fallback = join8(harness, "specs");
|
|
1919
|
+
if (opts.create !== false)
|
|
1920
|
+
mkdirSync4(fallback, { recursive: true });
|
|
1921
|
+
return fallback;
|
|
2070
1922
|
}
|
|
2071
|
-
function
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
1923
|
+
function resolvePlanDir(harnessDir) {
|
|
1924
|
+
const declared = mstarcDirOverride(harnessDir, "planDir");
|
|
1925
|
+
if (declared !== null)
|
|
1926
|
+
return declared;
|
|
1927
|
+
const dir = resolve5(harnessDir);
|
|
1928
|
+
const name = basename2(dir);
|
|
1929
|
+
if (name === ".plans" || name === "plans")
|
|
1930
|
+
return dir;
|
|
1931
|
+
return join8(dir, "plans");
|
|
2075
1932
|
}
|
|
2076
|
-
function
|
|
2077
|
-
if (
|
|
2078
|
-
|
|
2079
|
-
for (const row of snapshotDoc.plans) {
|
|
2080
|
-
if (!isPlainObject4(row))
|
|
2081
|
-
continue;
|
|
2082
|
-
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
2083
|
-
if (rowId === planId)
|
|
2084
|
-
return row;
|
|
1933
|
+
function assertSafePathComponent(value, what) {
|
|
1934
|
+
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
1935
|
+
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
|
|
2085
1936
|
}
|
|
2086
|
-
return null;
|
|
2087
1937
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
1938
|
+
function resolveSddDir(harnessDir, planId) {
|
|
1939
|
+
assertSafePathComponent(planId, "planId");
|
|
1940
|
+
const base = resolve5(harnessDir);
|
|
1941
|
+
const declared = mstarcDirOverride(base, "sddDir");
|
|
1942
|
+
const sddBase = declared !== null ? declared : join8(base, "sdd");
|
|
1943
|
+
return join8(sddBase, planId);
|
|
1944
|
+
}
|
|
1945
|
+
function resolveIterationDir(harnessDir) {
|
|
1946
|
+
const declared = mstarcDirOverride(harnessDir, "iterationDir");
|
|
1947
|
+
if (declared !== null)
|
|
1948
|
+
return declared;
|
|
1949
|
+
return join8(resolve5(harnessDir), "iterations");
|
|
1950
|
+
}
|
|
1951
|
+
function resolveKnowledgeDir(harnessDir) {
|
|
1952
|
+
const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
|
|
1953
|
+
if (declared !== null)
|
|
1954
|
+
return declared;
|
|
1955
|
+
return join8(resolve5(harnessDir), "knowledge");
|
|
1956
|
+
}
|
|
1957
|
+
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
1958
|
+
const harness = resolveHarnessDir(startDir, opts);
|
|
1959
|
+
if (harness === null) {
|
|
1960
|
+
throw new Error(`harness dir not found from ${resolve5(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
2103
1961
|
}
|
|
2104
|
-
|
|
1962
|
+
const declared = mstarcDirOverride(harness, key);
|
|
1963
|
+
return declared !== null ? declared : join8(resolve5(harness), fallback);
|
|
2105
1964
|
}
|
|
2106
|
-
function
|
|
2107
|
-
return
|
|
1965
|
+
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
1966
|
+
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
2108
1967
|
}
|
|
2109
|
-
function
|
|
2110
|
-
|
|
2111
|
-
if (compassDoc.status !== "completed") {
|
|
2112
|
-
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)`));
|
|
2113
|
-
}
|
|
2114
|
-
const endDate = compassDoc.end_date;
|
|
2115
|
-
if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
|
|
2116
|
-
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)"));
|
|
2117
|
-
}
|
|
2118
|
-
return violations;
|
|
1968
|
+
function resolveProjectDir(startDir = process.cwd(), opts = {}) {
|
|
1969
|
+
return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
|
|
2119
1970
|
}
|
|
2120
|
-
function
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
return
|
|
1971
|
+
function resolveScaffoldDirs(root) {
|
|
1972
|
+
const start = resolve5(root);
|
|
1973
|
+
const boundary = resolve5(start, defaultWorkspaceRoot(start));
|
|
1974
|
+
const rc = loadMstarc(start, boundary);
|
|
1975
|
+
const explicit = process.env.MSTAR_HARNESS_DIR;
|
|
1976
|
+
const harnessDir = explicit ? resolve5(start, explicit) : rc !== null && rc.config.harnessDir ? resolve5(rc.dir, rc.config.harnessDir) : join8(start, ".mstar");
|
|
1977
|
+
const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
|
|
1978
|
+
const projectDir = declaredProjectDir !== null ? declaredProjectDir : join8(harnessDir, "projects");
|
|
1979
|
+
return { harnessDir, projectDir };
|
|
2129
1980
|
}
|
|
2130
|
-
function
|
|
2131
|
-
const
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
}
|
|
2139
|
-
|
|
1981
|
+
function scaffoldHarness(root) {
|
|
1982
|
+
const { harnessDir, projectDir } = resolveScaffoldDirs(root);
|
|
1983
|
+
for (const dir of SCAFFOLD_DIRS)
|
|
1984
|
+
mkdirSync4(join8(harnessDir, dir), { recursive: true });
|
|
1985
|
+
const statusPath = join8(harnessDir, "status.json");
|
|
1986
|
+
if (Object.keys(readJson(statusPath)).length === 0)
|
|
1987
|
+
writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
|
|
1988
|
+
const defaultProjectDir = join8(projectDir, _DEFAULT_PROJECT);
|
|
1989
|
+
mkdirSync4(defaultProjectDir, { recursive: true });
|
|
1990
|
+
const roadmapPath = join8(defaultProjectDir, PROJECT_ROADMAP_FILE);
|
|
1991
|
+
if (!existsSync5(roadmapPath)) {
|
|
1992
|
+
const created = new Date().toISOString().slice(0, 10);
|
|
1993
|
+
writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
|
|
1994
|
+
}
|
|
1995
|
+
const registerPath = join8(defaultProjectDir, PROJECT_REGISTER_FILE);
|
|
1996
|
+
if (Object.keys(readJson(registerPath)).length === 0)
|
|
1997
|
+
writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
|
|
1998
|
+
return harnessDir;
|
|
2140
1999
|
}
|
|
2141
|
-
function
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const exitViolations = [
|
|
2148
|
-
...exitFrontmatterClosed(compassDoc),
|
|
2149
|
-
...exitBranchCheck(opts),
|
|
2150
|
-
...exitPrBaseCheck(compassDoc, opts)
|
|
2151
|
-
];
|
|
2152
|
-
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
2153
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
2154
|
-
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
2155
|
-
});
|
|
2156
|
-
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
2157
|
-
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
2158
|
-
let transition;
|
|
2159
|
-
if (!allPlansDone)
|
|
2160
|
-
transition = "phase-2-execute";
|
|
2161
|
-
else if (entry.ok && exit.ok)
|
|
2162
|
-
transition = "phase-4-pr-delivery";
|
|
2163
|
-
else
|
|
2164
|
-
transition = "phase-3-close";
|
|
2165
|
-
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
2166
|
-
return {
|
|
2167
|
-
transition,
|
|
2168
|
-
allPlansDone,
|
|
2169
|
-
entry,
|
|
2170
|
-
exit,
|
|
2171
|
-
ok: gateBlocking.length === 0,
|
|
2172
|
-
violations: gateBlocking
|
|
2173
|
-
};
|
|
2000
|
+
function emitGitignoreSnippet(kind) {
|
|
2001
|
+
if (kind === "agents")
|
|
2002
|
+
return GITIGNORE_SNIPPET_AGENTS;
|
|
2003
|
+
if (kind === "mstar")
|
|
2004
|
+
return GITIGNORE_SNIPPET;
|
|
2005
|
+
return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
|
|
2174
2006
|
}
|
|
2175
|
-
function
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
|
|
2007
|
+
function validateGitignore(root) {
|
|
2008
|
+
const gitignorePath = join8(resolve5(root), ".gitignore");
|
|
2009
|
+
const kind = detectHarnessKind(resolveHarnessDir(root));
|
|
2010
|
+
let content;
|
|
2011
|
+
try {
|
|
2012
|
+
content = readFileSync6(gitignorePath, "utf8");
|
|
2013
|
+
} catch {
|
|
2014
|
+
return {
|
|
2015
|
+
ok: false,
|
|
2016
|
+
severity: "medium",
|
|
2017
|
+
code: "gitignore.missing",
|
|
2018
|
+
message: `no .gitignore found at ${gitignorePath}`,
|
|
2019
|
+
fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
|
|
2020
|
+
};
|
|
2179
2021
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2022
|
+
const lines = new Set(content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0));
|
|
2023
|
+
const mstarMissing = GITIGNORE_PROCESS_ENTRIES.filter((entry) => !lines.has(entry));
|
|
2024
|
+
const agentsMissing = GITIGNORE_PROCESS_ENTRIES_AGENTS.filter((entry) => !lines.has(entry));
|
|
2025
|
+
let missing;
|
|
2026
|
+
let label;
|
|
2027
|
+
if (kind === "agents") {
|
|
2028
|
+
missing = agentsMissing;
|
|
2029
|
+
label = ".agents/ set";
|
|
2030
|
+
} else if (kind === "mstar") {
|
|
2031
|
+
missing = mstarMissing;
|
|
2032
|
+
label = ".mstar/ set";
|
|
2033
|
+
} else {
|
|
2034
|
+
label = "either .mstar/ or .agents/ set";
|
|
2035
|
+
missing = mstarMissing.length === 0 || agentsMissing.length === 0 ? [] : mstarMissing.length <= agentsMissing.length ? mstarMissing : agentsMissing;
|
|
2182
2036
|
}
|
|
2183
|
-
|
|
2037
|
+
if (missing.length > 0) {
|
|
2038
|
+
return {
|
|
2039
|
+
ok: false,
|
|
2040
|
+
severity: "medium",
|
|
2041
|
+
code: "gitignore.missing-entries",
|
|
2042
|
+
message: `.gitignore at ${gitignorePath} is missing canonical harness ignore entries (${label}): ${missing.join(", ")}`,
|
|
2043
|
+
fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
ok: true,
|
|
2048
|
+
severity: "low",
|
|
2049
|
+
code: "gitignore.ok",
|
|
2050
|
+
message: `.gitignore at ${gitignorePath} contains a complete canonical harness ignore set — default-ignore + tracked re-includes (${label})`
|
|
2051
|
+
};
|
|
2184
2052
|
}
|
|
2185
|
-
function
|
|
2186
|
-
if (!
|
|
2053
|
+
function detectHarnessKind(harnessDir) {
|
|
2054
|
+
if (!harnessDir)
|
|
2055
|
+
return null;
|
|
2056
|
+
const name = basename2(resolve5(harnessDir));
|
|
2057
|
+
if (name === ".mstar")
|
|
2058
|
+
return "mstar";
|
|
2059
|
+
if (name === ".agents")
|
|
2060
|
+
return "agents";
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
function assertPlanWritingPath(planPath, harnessDir) {
|
|
2064
|
+
const planAbs = resolve5(planPath);
|
|
2065
|
+
if (!harnessDir) {
|
|
2187
2066
|
return {
|
|
2188
2067
|
ok: false,
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2068
|
+
severity: "high",
|
|
2069
|
+
code: "plan-path.no-harness",
|
|
2070
|
+
message: `persistent plan tracking is not enabled — cannot place plan ${planAbs} under {PLAN_DIR}`,
|
|
2071
|
+
fix: "initialize the harness (scaffoldHarness) so plans land in {PLAN_DIR}"
|
|
2192
2072
|
};
|
|
2193
2073
|
}
|
|
2194
|
-
const
|
|
2195
|
-
const
|
|
2196
|
-
|
|
2074
|
+
const planDir = resolvePlanDir(harnessDir);
|
|
2075
|
+
const rel = relative2(planDir, planAbs);
|
|
2076
|
+
const inside = rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
2077
|
+
if (!inside) {
|
|
2197
2078
|
return {
|
|
2198
2079
|
ok: false,
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2080
|
+
severity: "high",
|
|
2081
|
+
code: "plan-path.outside-plan-dir",
|
|
2082
|
+
message: `plan file ${planAbs} is outside {PLAN_DIR} (${planDir})`,
|
|
2083
|
+
fix: `write the plan under ${planDir}`
|
|
2202
2084
|
};
|
|
2203
2085
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2086
|
+
if (existsSync5(planAbs)) {
|
|
2087
|
+
try {
|
|
2088
|
+
const canonicalPlan = realpathSync2(planAbs);
|
|
2089
|
+
const canonicalPlanDir = existsSync5(planDir) ? realpathSync2(planDir) : resolve5(planDir);
|
|
2090
|
+
const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
|
|
2091
|
+
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute3(canonicalRel);
|
|
2092
|
+
if (!canonicalInside) {
|
|
2093
|
+
return {
|
|
2094
|
+
ok: false,
|
|
2095
|
+
severity: "high",
|
|
2096
|
+
code: "plan-path.symlink-escape",
|
|
2097
|
+
message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
|
|
2098
|
+
fix: `write the plan under ${planDir}`
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
} catch {}
|
|
2208
2102
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2103
|
+
return {
|
|
2104
|
+
ok: true,
|
|
2105
|
+
severity: "low",
|
|
2106
|
+
code: "plan-path.ok",
|
|
2107
|
+
message: `plan file ${planAbs} lives under {PLAN_DIR} (${planDir})`
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function isDirectory(dir) {
|
|
2111
|
+
try {
|
|
2112
|
+
return statSync3(dir).isDirectory();
|
|
2113
|
+
} catch {
|
|
2114
|
+
return false;
|
|
2214
2115
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2116
|
+
}
|
|
2117
|
+
function hasFiles(dir) {
|
|
2118
|
+
try {
|
|
2119
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
2120
|
+
if (entry.isDirectory()) {
|
|
2121
|
+
if (hasFiles(join8(dir, entry.name)))
|
|
2122
|
+
return true;
|
|
2123
|
+
} else if (entry.isFile()) {
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2218
2126
|
}
|
|
2127
|
+
return false;
|
|
2128
|
+
} catch {
|
|
2129
|
+
return false;
|
|
2219
2130
|
}
|
|
2220
|
-
return { ok: violations.length === 0, violations };
|
|
2221
2131
|
}
|
|
2222
|
-
function
|
|
2223
|
-
|
|
2132
|
+
function probeTimeoutMs() {
|
|
2133
|
+
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
2134
|
+
if (raw === undefined || raw.trim() === "")
|
|
2135
|
+
return DEFAULT_PROBE_TIMEOUT_MS;
|
|
2136
|
+
const parsed = Number(raw);
|
|
2137
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
2224
2138
|
}
|
|
2225
|
-
function
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
2139
|
+
function violation7(severity, code, message, fix) {
|
|
2140
|
+
return { ok: false, severity, code, message, fix };
|
|
2141
|
+
}
|
|
2142
|
+
function gate(violations) {
|
|
2143
|
+
return { ok: violations.length === 0, violations };
|
|
2144
|
+
}
|
|
2145
|
+
function probeBranch(worktreePath, opts) {
|
|
2146
|
+
const precomputed = opts.branchOf?.(worktreePath);
|
|
2147
|
+
if (precomputed !== undefined)
|
|
2148
|
+
return { branch: precomputed };
|
|
2149
|
+
const timeout = opts.timeoutMs ?? probeTimeoutMs();
|
|
2150
|
+
try {
|
|
2151
|
+
const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
|
|
2152
|
+
encoding: "utf8",
|
|
2153
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2154
|
+
timeout
|
|
2155
|
+
});
|
|
2156
|
+
const branch = stdout.trim();
|
|
2157
|
+
if (branch === "")
|
|
2158
|
+
return { error: `no branch checked out (detached HEAD?) at "${worktreePath}"` };
|
|
2159
|
+
return { branch };
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
const e = err;
|
|
2162
|
+
if (e.killed === true || e.signal !== undefined) {
|
|
2163
|
+
return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
|
|
2251
2164
|
}
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
listKey = value === "" ? kv[1] : null;
|
|
2165
|
+
const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
|
|
2166
|
+
return { error: detail };
|
|
2255
2167
|
}
|
|
2256
|
-
return doc;
|
|
2257
2168
|
}
|
|
2258
|
-
function
|
|
2259
|
-
const
|
|
2260
|
-
|
|
2261
|
-
|
|
2169
|
+
function l1PreDispatchCheck(input, opts = {}) {
|
|
2170
|
+
const violations = [];
|
|
2171
|
+
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
2172
|
+
if (controlWorktreePath.trim() === "") {
|
|
2173
|
+
violations.push(violation7("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"));
|
|
2262
2174
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
if (ch === '"' || ch === "'") {
|
|
2266
|
-
if (quote === null)
|
|
2267
|
-
quote = ch;
|
|
2268
|
-
else if (quote === ch)
|
|
2269
|
-
quote = null;
|
|
2270
|
-
} else if (ch === "," && quote !== null) {
|
|
2271
|
-
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
2272
|
-
}
|
|
2175
|
+
if (leaseWorktreePath.trim() === "") {
|
|
2176
|
+
violations.push(violation7("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"));
|
|
2273
2177
|
}
|
|
2274
|
-
if (
|
|
2275
|
-
|
|
2178
|
+
if (leaseWorkingBranch.trim() === "") {
|
|
2179
|
+
violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
2276
2180
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
2280
|
-
if (item === "")
|
|
2281
|
-
continue;
|
|
2282
|
-
items.push(item);
|
|
2181
|
+
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
2182
|
+
violations.push(violation7("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"));
|
|
2283
2183
|
}
|
|
2284
|
-
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
if (value === undefined) {
|
|
2294
|
-
violations.push(violation7("high", missingCode, `missing required field: ${field}`));
|
|
2295
|
-
} else if (typeof value !== "string" || value.trim() === "") {
|
|
2296
|
-
violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
|
|
2184
|
+
if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
|
|
2185
|
+
violations.push(violation7("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>`));
|
|
2186
|
+
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
2187
|
+
const probe = probeBranch(leaseWorktreePath, opts);
|
|
2188
|
+
if ("error" in probe) {
|
|
2189
|
+
violations.push(violation7("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)"));
|
|
2190
|
+
} else if (probe.branch !== leaseWorkingBranch) {
|
|
2191
|
+
violations.push(violation7("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`));
|
|
2192
|
+
}
|
|
2297
2193
|
}
|
|
2194
|
+
return gate(violations);
|
|
2298
2195
|
}
|
|
2299
|
-
function
|
|
2196
|
+
function l2PreDispatchCheck(input, opts = {}) {
|
|
2300
2197
|
const violations = [];
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
return {
|
|
2306
|
-
ok: false,
|
|
2307
|
-
violations: [violation7("high", "project.roadmap.unreadable", `cannot read roadmap file: ${filePath}`)],
|
|
2308
|
-
warnings: []
|
|
2309
|
-
};
|
|
2310
|
-
}
|
|
2311
|
-
let doc;
|
|
2312
|
-
try {
|
|
2313
|
-
doc = parseCompassFrontmatterText(content, filePath);
|
|
2314
|
-
} catch (err) {
|
|
2315
|
-
const message = err instanceof Error ? err.message : `invalid roadmap frontmatter in ${filePath}`;
|
|
2316
|
-
return { ok: false, violations: [violation7("high", "project.roadmap.invalid-frontmatter", message)], warnings: [] };
|
|
2198
|
+
const tracks = input.tracks ?? [];
|
|
2199
|
+
const seenPaths = new Set;
|
|
2200
|
+
if (tracks.length < 1) {
|
|
2201
|
+
violations.push(violation7("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"));
|
|
2317
2202
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2203
|
+
tracks.forEach((track, index) => {
|
|
2204
|
+
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
2205
|
+
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
if (!isAbsolute4(track.worktreePath)) {
|
|
2209
|
+
violations.push(violation7("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>)`));
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
const normalized = resolve6(track.worktreePath);
|
|
2213
|
+
if (seenPaths.has(normalized)) {
|
|
2214
|
+
violations.push(violation7("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"));
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
seenPaths.add(normalized);
|
|
2218
|
+
if (!existsSync6(track.worktreePath)) {
|
|
2219
|
+
violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const probe = probeBranch(track.worktreePath, opts);
|
|
2223
|
+
if ("error" in probe) {
|
|
2224
|
+
violations.push(violation7("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)"));
|
|
2225
|
+
} else if (probe.branch !== track.workingBranch) {
|
|
2226
|
+
violations.push(violation7("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`));
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
return gate(violations);
|
|
2230
|
+
}
|
|
2231
|
+
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
|
|
2232
|
+
const violations = [];
|
|
2233
|
+
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(featureWorktreePath);
|
|
2234
|
+
if (samePath) {
|
|
2235
|
+
violations.push(violation7("critical", "worktree.control-feature.same", `control worktree path equals feature/lease worktree path "${controlWorktreePath}" — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "use a distinct feature worktree for the plan's product edits"));
|
|
2324
2236
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2237
|
+
return gate(violations);
|
|
2238
|
+
}
|
|
2239
|
+
function assertBranchAlignment(worktreePath, expectedBranch, opts = {}) {
|
|
2240
|
+
const violations = [];
|
|
2241
|
+
const probe = probeBranch(worktreePath, opts);
|
|
2242
|
+
if ("error" in probe) {
|
|
2243
|
+
violations.push(violation7("high", "worktree.branch-probe-failed", `cannot probe branch at "${worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on the expected branch (not detached)"));
|
|
2244
|
+
} else if (probe.branch !== expectedBranch) {
|
|
2245
|
+
violations.push(violation7("high", "worktree.branch-mismatch", `worktree "${worktreePath}" is on branch "${probe.branch}", expected "${expectedBranch}" (Assignment Working branch)`, `checkout ${expectedBranch} in that worktree`));
|
|
2329
2246
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
}
|
|
2247
|
+
return gate(violations);
|
|
2248
|
+
}
|
|
2249
|
+
function assertQcAlignment(assignments) {
|
|
2250
|
+
const violations = [];
|
|
2251
|
+
const list = assignments ?? [];
|
|
2252
|
+
for (const { key, label } of QC_ALIGNMENT_FIELDS) {
|
|
2253
|
+
const distinct = [...new Set(list.map((a) => a[key]))];
|
|
2254
|
+
if (distinct.length > 1) {
|
|
2255
|
+
violations.push(violation7("high", "qc.alignment.mismatch", `QC/QA alignment field "${label}" is not byte-identical across ${list.length} assignments: ${distinct.map((v) => `"${v}"`).join(" vs ")}`, `copy the same ${label} value verbatim into every QC tri and QA Assignment`));
|
|
2340
2256
|
}
|
|
2341
2257
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2258
|
+
return gate(violations);
|
|
2259
|
+
}
|
|
2260
|
+
function singleReviewSnapshot(assignments) {
|
|
2261
|
+
const violations = [];
|
|
2262
|
+
const list = assignments ?? [];
|
|
2263
|
+
list.forEach((a, index) => {
|
|
2264
|
+
if ((a.head ?? "").trim() === "") {
|
|
2265
|
+
violations.push(violation7("high", "qc.alignment.snapshot-missing", `review head not provided for assignment ${index + 1} (plan_id "${a.planId}") — cannot confirm the single review snapshot precondition`, "precompute and pass the review HEAD (full SHA) for every assignment"));
|
|
2345
2266
|
}
|
|
2267
|
+
});
|
|
2268
|
+
const distinct = [...new Set(list.map((a) => a.head ?? "").filter((h) => h.trim() !== ""))];
|
|
2269
|
+
if (distinct.length > 1) {
|
|
2270
|
+
violations.push(violation7("high", "qc.alignment.single-snapshot", `assignments cover ${distinct.length} different review heads (${distinct.join(", ")}) — all reviewable commits must sit on ONE Working branch HEAD before QC tri + QA`, "merge the parallel tracks to a single Working branch HEAD, then re-derive the heads"));
|
|
2346
2271
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2272
|
+
return gate(violations);
|
|
2273
|
+
}
|
|
2274
|
+
function isDirectory2(dir) {
|
|
2275
|
+
try {
|
|
2276
|
+
return statSync4(dir).isDirectory();
|
|
2277
|
+
} catch {
|
|
2278
|
+
return false;
|
|
2353
2279
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2280
|
+
}
|
|
2281
|
+
function isFile2(file) {
|
|
2282
|
+
try {
|
|
2283
|
+
return statSync4(file).isFile();
|
|
2284
|
+
} catch {
|
|
2285
|
+
return false;
|
|
2356
2286
|
}
|
|
2357
|
-
return { ok: violations.length === 0, violations, warnings };
|
|
2358
2287
|
}
|
|
2359
|
-
function
|
|
2360
|
-
|
|
2288
|
+
function gitOut(cwd, args) {
|
|
2289
|
+
try {
|
|
2290
|
+
return execFileSync3("git", args, {
|
|
2291
|
+
cwd,
|
|
2292
|
+
encoding: "utf8",
|
|
2293
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2294
|
+
maxBuffer: GIT_CAPTURE_MAX_BYTES
|
|
2295
|
+
}).trim();
|
|
2296
|
+
} catch {
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2361
2299
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2300
|
+
function probeHarnessWithStatus(root) {
|
|
2301
|
+
if (isFile2(join9(root, ".mstar", "status.json")))
|
|
2302
|
+
return join9(root, ".mstar");
|
|
2303
|
+
if (isFile2(join9(root, ".agents", "status.json")))
|
|
2304
|
+
return join9(root, ".agents");
|
|
2305
|
+
if (hasWorkflowSnapshot(join9(root, ".mstar")))
|
|
2306
|
+
return join9(root, ".mstar");
|
|
2307
|
+
if (hasWorkflowSnapshot(join9(root, ".agents")))
|
|
2308
|
+
return join9(root, ".agents");
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
function hasWorkflowSnapshot(harnessDir) {
|
|
2312
|
+
let workflowsDir;
|
|
2313
|
+
try {
|
|
2314
|
+
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2315
|
+
} catch {
|
|
2316
|
+
workflowsDir = join9(harnessDir, "workflows");
|
|
2369
2317
|
}
|
|
2370
|
-
if (
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
if (key.trim() === "") {
|
|
2377
|
-
violations.push(violation7("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
|
|
2378
|
-
}
|
|
2379
|
-
if (!Array.isArray(entries)) {
|
|
2380
|
-
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)`));
|
|
2381
|
-
continue;
|
|
2382
|
-
}
|
|
2383
|
-
for (const entry of entries) {
|
|
2384
|
-
violations.push(...validateResidual(entry).violations);
|
|
2385
|
-
if (!isPlainObject5(entry))
|
|
2386
|
-
continue;
|
|
2387
|
-
validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
|
|
2388
|
-
if (entry.registered_at === undefined) {
|
|
2389
|
-
violations.push(violation7("high", "project.register.missing-registered-at", "missing required field: registered_at"));
|
|
2390
|
-
} else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
|
|
2391
|
-
violations.push(violation7("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
|
|
2392
|
-
}
|
|
2393
|
-
if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
|
|
2394
|
-
violations.push(violation7("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
|
|
2395
|
-
}
|
|
2396
|
-
if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
|
|
2397
|
-
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`));
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2318
|
+
if (!isDirectory2(workflowsDir))
|
|
2319
|
+
return false;
|
|
2320
|
+
try {
|
|
2321
|
+
for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
|
|
2322
|
+
if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
|
|
2323
|
+
return true;
|
|
2400
2324
|
}
|
|
2325
|
+
} catch {
|
|
2326
|
+
return false;
|
|
2401
2327
|
}
|
|
2402
|
-
return
|
|
2328
|
+
return false;
|
|
2403
2329
|
}
|
|
2404
|
-
function
|
|
2405
|
-
const
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
if (
|
|
2412
|
-
|
|
2413
|
-
|
|
2330
|
+
function isLinkedWorktree(root) {
|
|
2331
|
+
const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
|
|
2332
|
+
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2333
|
+
if (gitDirRaw === null || commonRaw === null)
|
|
2334
|
+
return false;
|
|
2335
|
+
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
|
|
2336
|
+
const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
|
|
2337
|
+
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2338
|
+
return true;
|
|
2339
|
+
try {
|
|
2340
|
+
const gdParent = realpathSync3(dirname6(gitDir));
|
|
2341
|
+
const cmAbs = realpathSync3(common);
|
|
2342
|
+
return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2343
|
+
} catch {
|
|
2344
|
+
return false;
|
|
2414
2345
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2346
|
+
}
|
|
2347
|
+
function sddWorkspace(planId, opts = {}) {
|
|
2348
|
+
if (!planId) {
|
|
2349
|
+
throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
|
|
2350
|
+
` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
|
|
2417
2351
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2352
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2353
|
+
const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
|
|
2354
|
+
let root;
|
|
2355
|
+
if (controlRoot) {
|
|
2356
|
+
if (!isDirectory2(controlRoot)) {
|
|
2357
|
+
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2358
|
+
}
|
|
2359
|
+
root = realpathSync3(controlRoot);
|
|
2360
|
+
} else {
|
|
2361
|
+
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2362
|
+
root = realpathSync3(topLevel ?? cwd);
|
|
2363
|
+
}
|
|
2364
|
+
if (!controlRoot && isLinkedWorktree(root)) {
|
|
2365
|
+
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
2366
|
+
` + ` Refusing to create a second SDD tree under the feature checkout.
|
|
2367
|
+
` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
|
|
2368
|
+
` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
|
|
2369
|
+
}
|
|
2370
|
+
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2371
|
+
let harnessDir;
|
|
2372
|
+
if (harnessOverride) {
|
|
2373
|
+
harnessDir = resolve7(root, harnessOverride);
|
|
2374
|
+
} else {
|
|
2375
|
+
const rc = findMstarc(root, root);
|
|
2376
|
+
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
|
|
2377
|
+
if (rcHarnessDir) {
|
|
2378
|
+
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2379
|
+
} else {
|
|
2380
|
+
const probed = probeHarnessWithStatus(root);
|
|
2381
|
+
if (probed) {
|
|
2382
|
+
harnessDir = probed;
|
|
2383
|
+
} else if (isDirectory2(join9(root, ".mstar"))) {
|
|
2384
|
+
harnessDir = join9(root, ".mstar");
|
|
2385
|
+
} else if (isDirectory2(join9(root, ".agents"))) {
|
|
2386
|
+
harnessDir = join9(root, ".agents");
|
|
2432
2387
|
} else {
|
|
2433
|
-
|
|
2388
|
+
harnessDir = join9(root, ".mstar");
|
|
2434
2389
|
}
|
|
2435
|
-
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
2436
|
-
violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
2437
2390
|
}
|
|
2438
2391
|
}
|
|
2439
|
-
|
|
2392
|
+
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2393
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2394
|
+
writeFileSync4(join9(sddDir, ".gitignore"), `*
|
|
2395
|
+
`);
|
|
2396
|
+
return realpathSync3(sddDir);
|
|
2440
2397
|
}
|
|
2441
|
-
function
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
const key = typeof value === "string" ? value : String(value);
|
|
2445
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2398
|
+
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
2399
|
+
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
2400
|
+
throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
|
|
2446
2401
|
}
|
|
2447
|
-
|
|
2448
|
-
}
|
|
2449
|
-
function techDebtRollup(projectDir) {
|
|
2450
|
-
const items = [];
|
|
2451
|
-
let entries;
|
|
2402
|
+
let content;
|
|
2452
2403
|
try {
|
|
2453
|
-
|
|
2404
|
+
content = readFileSync7(planFile, "utf8");
|
|
2454
2405
|
} catch {
|
|
2455
|
-
|
|
2406
|
+
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2456
2407
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2408
|
+
let out;
|
|
2409
|
+
if (outFile) {
|
|
2410
|
+
out = outFile;
|
|
2411
|
+
} else {
|
|
2412
|
+
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2413
|
+
if (!sddDir) {
|
|
2414
|
+
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2415
|
+
}
|
|
2416
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2417
|
+
out = join9(sddDir, `task-${taskN}-brief.md`);
|
|
2418
|
+
}
|
|
2419
|
+
const records = content.endsWith(`
|
|
2420
|
+
`) ? content.split(`
|
|
2421
|
+
`).slice(0, -1) : content.split(`
|
|
2422
|
+
`);
|
|
2423
|
+
const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
|
|
2424
|
+
const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
|
|
2425
|
+
let infence = false;
|
|
2426
|
+
let intask = false;
|
|
2427
|
+
const printed = [];
|
|
2428
|
+
for (const line of records) {
|
|
2429
|
+
if (/^```/.test(line))
|
|
2430
|
+
infence = !infence;
|
|
2431
|
+
if (!infence && headingRe.test(line))
|
|
2432
|
+
intask = targetRe.test(line);
|
|
2433
|
+
if (intask)
|
|
2434
|
+
printed.push(line);
|
|
2435
|
+
}
|
|
2436
|
+
const output = printed.length > 0 ? `${printed.join(`
|
|
2437
|
+
`)}
|
|
2438
|
+
` : "";
|
|
2439
|
+
writeFileSync4(out, output);
|
|
2440
|
+
if (printed.length === 0) {
|
|
2441
|
+
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
2442
|
+
}
|
|
2443
|
+
return out;
|
|
2444
|
+
}
|
|
2445
|
+
function reviewPackage(base, head, outFile, opts = {}) {
|
|
2446
|
+
if (!base || !head) {
|
|
2447
|
+
throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
|
|
2448
|
+
}
|
|
2449
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2450
|
+
const verifyRef = (ref, what) => {
|
|
2464
2451
|
try {
|
|
2465
|
-
|
|
2452
|
+
execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2466
2453
|
} catch {
|
|
2467
|
-
|
|
2454
|
+
throw new SddScriptError(`bad ${what}: ${ref}`, 2);
|
|
2468
2455
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2456
|
+
};
|
|
2457
|
+
verifyRef(base, "BASE");
|
|
2458
|
+
verifyRef(head, "HEAD");
|
|
2459
|
+
let out;
|
|
2460
|
+
if (outFile) {
|
|
2461
|
+
out = outFile;
|
|
2462
|
+
} else {
|
|
2463
|
+
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2464
|
+
if (!sddDir) {
|
|
2465
|
+
throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
|
|
2479
2466
|
}
|
|
2467
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2468
|
+
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
2469
|
+
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
2470
|
+
out = join9(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
2480
2471
|
}
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2472
|
+
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
2473
|
+
const parts = [
|
|
2474
|
+
Buffer.from(`# Review package: ${base}..${head}
|
|
2475
|
+
|
|
2476
|
+
## Commits
|
|
2477
|
+
`),
|
|
2478
|
+
run(["log", "--oneline", `${base}..${head}`]),
|
|
2479
|
+
Buffer.from(`
|
|
2480
|
+
## Files changed
|
|
2481
|
+
`),
|
|
2482
|
+
run(["diff", "--stat", `${base}..${head}`]),
|
|
2483
|
+
Buffer.from(`
|
|
2484
|
+
## Diff
|
|
2485
|
+
`),
|
|
2486
|
+
run(["diff", "-U10", `${base}..${head}`])
|
|
2487
|
+
];
|
|
2488
|
+
writeFileSync4(out, Buffer.concat(parts));
|
|
2489
|
+
return out;
|
|
2490
|
+
}
|
|
2491
|
+
function assertBaseSha(ref, opts = {}) {
|
|
2492
|
+
if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
|
|
2493
|
+
throw new SddScriptError(`assertBaseSha: BASE must be a commit SHA (full or prefix); got ${JSON.stringify(ref)}. ` + "Never use HEAD~1 as review BASE (multi-commit tasks truncate).", 2);
|
|
2494
|
+
}
|
|
2495
|
+
try {
|
|
2496
|
+
execFileSync3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
2497
|
+
cwd: opts.cwd,
|
|
2498
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2499
|
+
});
|
|
2500
|
+
} catch {
|
|
2501
|
+
throw new SddScriptError(`assertBaseSha: commit not found: ${ref}`, 2);
|
|
2484
2502
|
}
|
|
2485
|
-
const computed = {
|
|
2486
|
-
total_open: items.length,
|
|
2487
|
-
by_severity: bySeverity,
|
|
2488
|
-
by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
|
|
2489
|
-
by_plan: groupCount(items.map(({ plan }) => plan))
|
|
2490
|
-
};
|
|
2491
|
-
const stored = null;
|
|
2492
|
-
const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
|
|
2493
|
-
const overall = "DRIFT";
|
|
2494
|
-
return { computed, stored, checks, overall };
|
|
2495
2503
|
}
|
|
2496
|
-
function
|
|
2497
|
-
|
|
2498
|
-
|
|
2504
|
+
function taskReportExists(sddDir, taskN) {
|
|
2505
|
+
try {
|
|
2506
|
+
const st = statSync4(join9(sddDir, `task-${taskN}-report.md`));
|
|
2507
|
+
return st.isFile() && st.size > 0;
|
|
2508
|
+
} catch {
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
function readProgressLedger(sddDir) {
|
|
2513
|
+
let content;
|
|
2499
2514
|
try {
|
|
2500
|
-
|
|
2515
|
+
content = readFileSync7(join9(sddDir, "progress.md"), "utf8");
|
|
2501
2516
|
} catch {
|
|
2502
2517
|
return [];
|
|
2503
2518
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
let nested;
|
|
2512
|
-
try {
|
|
2513
|
-
nested = readdirSync5(join9(root, entry.name), { withFileTypes: true });
|
|
2514
|
-
} catch {
|
|
2515
|
-
continue;
|
|
2516
|
-
}
|
|
2517
|
-
for (const child of nested) {
|
|
2518
|
-
if (child.isFile())
|
|
2519
|
-
files.push(`${entry.name}/${child.name}`);
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2519
|
+
return content.split(`
|
|
2520
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2521
|
+
}
|
|
2522
|
+
function implementerSessionStickyRules(input) {
|
|
2523
|
+
const { session, nextTask, microBatchTasks = 1 } = input;
|
|
2524
|
+
if (session.session_mode !== "sticky") {
|
|
2525
|
+
return { resume: false, reason: `session_mode is '${session.session_mode}'; sticky resume requires 'sticky'` };
|
|
2522
2526
|
}
|
|
2523
|
-
|
|
2527
|
+
if (typeof session.host_agent_id !== "string" || session.host_agent_id.length === 0) {
|
|
2528
|
+
return {
|
|
2529
|
+
resume: false,
|
|
2530
|
+
reason: "host_agent_id is missing from implementer-session.json; fall back to fresh for this task " + "(mstar-sdd SKILL.md red flag: resume implementer without host_agent_id)"
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
if (nextTask <= session.last_task) {
|
|
2534
|
+
return {
|
|
2535
|
+
resume: false,
|
|
2536
|
+
reason: `nextTask ${nextTask} <= last_task ${session.last_task}; task already completed in this session`
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
if (microBatchTasks < 1 || microBatchTasks > 3) {
|
|
2540
|
+
return {
|
|
2541
|
+
resume: false,
|
|
2542
|
+
reason: `micro-batch of ${microBatchTasks} tasks is outside 1..3 (max 3 without user override, ` + "sticky-implementer-session.md § Micro-batch fallback)"
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
|
|
2524
2546
|
}
|
|
2525
2547
|
function isPlainObject6(value) {
|
|
2526
2548
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3006,7 +3028,7 @@ async function applyMigratePlan(plan) {
|
|
|
3006
3028
|
const content = notes.lines.length > 0 ? `${notes.lines.join(`
|
|
3007
3029
|
`)}
|
|
3008
3030
|
` : "";
|
|
3009
|
-
|
|
3031
|
+
writeFileSync5(filePath, content, "utf8");
|
|
3010
3032
|
}
|
|
3011
3033
|
if (plan.register !== null) {
|
|
3012
3034
|
const gate2 = validateProjectRegister(plan.register.data);
|
|
@@ -3022,7 +3044,7 @@ async function applyMigratePlan(plan) {
|
|
|
3022
3044
|
if (plan.roadmap !== null) {
|
|
3023
3045
|
const filePath = projectTargetOf(plan.roadmap.file);
|
|
3024
3046
|
mkdirSync6(dirname7(filePath), { recursive: true });
|
|
3025
|
-
|
|
3047
|
+
writeFileSync5(filePath, plan.roadmap.content, "utf8");
|
|
3026
3048
|
}
|
|
3027
3049
|
const rootGate = validateStatusV2(plan.rootV2.data, { harnessDir: plan.root });
|
|
3028
3050
|
if (!rootGate.ok) {
|
|
@@ -3511,8 +3533,17 @@ function readPlanFileSummary(filePath) {
|
|
|
3511
3533
|
const blocks = parseStatusBlocks(text);
|
|
3512
3534
|
return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
|
|
3513
3535
|
}
|
|
3536
|
+
function extractSecurityDispositionSections(text) {
|
|
3537
|
+
const grab = (heading) => {
|
|
3538
|
+
const match = text.match(new RegExp(`(?:^|\\r?\\n)##[ \\t]+${heading}[ \\t]*\\r?\\n(?:[ \\t]*\\r?\\n)?([\\s\\S]*?)(?=\\r?\\n## |$)`, "i"));
|
|
3539
|
+
if (!match)
|
|
3540
|
+
return [];
|
|
3541
|
+
return match[1].split(/\r?\n/).map((line) => line.replace(/\r$/, "")).filter((line) => line.startsWith("- "));
|
|
3542
|
+
};
|
|
3543
|
+
return { needsVerification: grab("Needs verification"), hardeningChecked: grab("Hardening & checked notes") };
|
|
3544
|
+
}
|
|
3514
3545
|
function renderIndex(params) {
|
|
3515
|
-
const { date, repoName, repoShortSha, rows, rejected } = params;
|
|
3546
|
+
const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
|
|
3516
3547
|
const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
|
|
3517
3548
|
`);
|
|
3518
3549
|
const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
|
|
@@ -3533,6 +3564,12 @@ function renderIndex(params) {
|
|
|
3533
3564
|
if (directionRows !== "") {
|
|
3534
3565
|
sections.push("", "## Direction", "", directionRows);
|
|
3535
3566
|
}
|
|
3567
|
+
if (needsVerification.length > 0) {
|
|
3568
|
+
sections.push("", "## Needs verification", "", ...needsVerification);
|
|
3569
|
+
}
|
|
3570
|
+
if (hardeningChecked.length > 0) {
|
|
3571
|
+
sections.push("", "## Hardening & checked notes", "", ...hardeningChecked);
|
|
3572
|
+
}
|
|
3536
3573
|
sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
|
|
3537
3574
|
if (rejectedRows !== "") {
|
|
3538
3575
|
sections.push("", "## Findings considered and rejected", "", rejectedRows);
|
|
@@ -3546,6 +3583,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
3546
3583
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
3547
3584
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
3548
3585
|
mkdirSync7(outDir, { recursive: true });
|
|
3586
|
+
const existingReadme = join11(outDir, "README.md");
|
|
3587
|
+
const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
3549
3588
|
const existing = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
3550
3589
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
3551
3590
|
const written = [];
|
|
@@ -3561,7 +3600,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
3561
3600
|
}
|
|
3562
3601
|
usedSlugs.add(slug);
|
|
3563
3602
|
const file = `${num}-${slug}.md`;
|
|
3564
|
-
|
|
3603
|
+
writeFileSync6(join11(outDir, file), renderPlanFile(finding, plannedAt));
|
|
3565
3604
|
written.push(file);
|
|
3566
3605
|
next++;
|
|
3567
3606
|
}
|
|
@@ -3599,12 +3638,16 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
3599
3638
|
row.dependsOn = finding.dependsOn ?? "none";
|
|
3600
3639
|
}
|
|
3601
3640
|
});
|
|
3602
|
-
|
|
3641
|
+
const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(nv.lead)}: ${escapeCell(nv.how)}${nv.evidence ? ` (${escapeCell(nv.evidence)})` : ""}`) : carried.needsVerification;
|
|
3642
|
+
const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(hc.text)}`) : carried.hardeningChecked;
|
|
3643
|
+
writeFileSync6(join11(outDir, "README.md"), renderIndex({
|
|
3603
3644
|
date,
|
|
3604
3645
|
repoName: options.repoName ?? "repo",
|
|
3605
3646
|
repoShortSha: options.repoShortSha ?? "unknown",
|
|
3606
3647
|
rows,
|
|
3607
|
-
rejected: options.rejected ?? []
|
|
3648
|
+
rejected: options.rejected ?? [],
|
|
3649
|
+
needsVerification: needsVerificationLines,
|
|
3650
|
+
hardeningChecked: hardeningCheckedLines
|
|
3608
3651
|
}));
|
|
3609
3652
|
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
3610
3653
|
}
|
|
@@ -4443,7 +4486,19 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
4443
4486
|
function resolveAssetPath(skillName, relPath, host) {
|
|
4444
4487
|
return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
|
|
4445
4488
|
}
|
|
4446
|
-
var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_HARNESS_DIR_KEY = "harness_dir", MSTARC_PLAN_DIR_KEY = "plan_dir", MSTARC_SDD_DIR_KEY = "sdd_dir", MSTARC_ITERATION_DIR_KEY = "iteration_dir", MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir", MSTARC_SPECS_DIR_KEY = "specs_dir", MSTARC_WORKFLOW_DIR_KEY = "workflow_dir", MSTARC_PROJECT_DIR_KEY = "project_dir", MSTARC_ENFORCEMENT_KEY = "enforcement", CONFIG_KEYS,
|
|
4489
|
+
var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_HARNESS_DIR_KEY = "harness_dir", MSTARC_PLAN_DIR_KEY = "plan_dir", MSTARC_SDD_DIR_KEY = "sdd_dir", MSTARC_ITERATION_DIR_KEY = "iteration_dir", MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir", MSTARC_SPECS_DIR_KEY = "specs_dir", MSTARC_WORKFLOW_DIR_KEY = "workflow_dir", MSTARC_PROJECT_DIR_KEY = "project_dir", MSTARC_ENFORCEMENT_KEY = "enforcement", CONFIG_KEYS, COMPASS_STATUSES, DATE_RE, PLAN_STATUS_DONE = "Done", COMPASS_FILE = "delivery-compass.md", INDEX_README = "README.md", INDEX_HEADER = "| Iteration | Path | Description | Status |", DATE_PART, RFC3339_Z_RE, DATE_ONLY_RE, STATUS_WRITE_LOCKDIR = ".status-write.lockdir", LOCKDIR_HOLDER_PID = "holder.pid", heldLockDirs, BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"', REQUIRED_FIELDS, ASSIGNMENT_ENFORCEMENT_BOLD_RE, ASSIGNMENT_ENFORCEMENT_PLAIN_RE, COMPASS_ENFORCEMENT_RE, ASSIGNMENT_BODY_START_RE, ASSIGNMENT_HEADING_RE, ASSIGNMENT_FIELD_RE, WORKFLOW_SNAPSHOT_FILE = "snapshot.json", WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_TERMINAL_STATUSES, WORKFLOW_LIFECYCLE_TYPES, DATE_RE2, PLAN_STATUSES, RESIDUAL_DECISIONS, RESIDUAL_LIFECYCLES, validateStatus, PROJECT_ROADMAP_FILE = "roadmap.md", PROJECT_REFERENCES_DIR = "references", PROJECT_REGISTER_FILE = "residuals.json", _DEFAULT_PROJECT = "_default", ROADMAP_STATUSES, DATE_RE3, ROLLUP_FIELDS, EMPTY_STATUS_TEMPLATE, SCAFFOLD_DIRS, ROADMAP_TEMPLATE = `---
|
|
4490
|
+
project_id: _default
|
|
4491
|
+
title: Default Project
|
|
4492
|
+
status: active
|
|
4493
|
+
created_at: {created_at}
|
|
4494
|
+
---
|
|
4495
|
+
|
|
4496
|
+
# Roadmap
|
|
4497
|
+
|
|
4498
|
+
## Direction
|
|
4499
|
+
|
|
4500
|
+
State the project direction here.
|
|
4501
|
+
`, EMPTY_REGISTER_TEMPLATE, GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
4447
4502
|
# Principle: process stays local; results are shared with the team.
|
|
4448
4503
|
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
4449
4504
|
.mstar/**
|
|
@@ -4462,7 +4517,7 @@ var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_H
|
|
|
4462
4517
|
!.agents/knowledge/**
|
|
4463
4518
|
!.agents/specs/
|
|
4464
4519
|
!.agents/specs/**
|
|
4465
|
-
`, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS,
|
|
4520
|
+
`, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS, DEFAULT_PROBE_TIMEOUT_MS = 1e4, QC_ALIGNMENT_FIELDS, SddScriptError, GIT_CAPTURE_MAX_BYTES, MIGRATE_STATUS_FILE = "status.json", ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json", NOTES_LEDGER_FILE = "notes.jsonl", DATE_RE4, ROOT_METADATA_LIFT_KEYS, RAW_GROUP = "__raw", isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v), HEX_RE, OKLCH_RE, PX_RE, PLACEHOLDER_RE, TYPOGRAPHY_PROPS, REF_RE, REF_GROUPS, isPlaceholder = (value) => PLACEHOLDER_RE.test(value), PARITY_GROUPS, GRAY_STEPS, ALPHA_STEPS, ACCENT_SCALES, L3_BODY_ITEM_IDS, LEVEL_RANK, AUDIT_PRIORITIES, AUDIT_EFFORTS, AUDIT_RISKS, AUDIT_CATEGORIES, AUDIT_STATUS_FIELDS, escapeCell = (value) => value.replace(/\|/g, "\\|"), truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_SEVERITIES, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_CATEGORY_MAP, DATE_RE5, isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), REF_EXT_RE, SYMBOL_REF_RE, SCHEME_RE, LINE_SUFFIX_RE, ANCHOR_RE, WALK_SKIP_DIRS, MAX_WALK_FILES = 5000, COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)", REMOVAL_PATH_PATTERNS, TASK_ARTIFACT_RE, SDD_DEEPLINK_RE, TEST_FILE_PATH_RE, TEST_FILE_PHRASE_RE, COMMAND_PROMPT_RE, RUNNER_RE, OUTPUT_TOKEN_RE, PLACEHOLDER_TOKEN_RE, ELLIPSIS_RE, NEGATION_RE, WORKFLOW_VERB_START_RE, PRONOUN_RE, DESCRIPTION_MAX_WORDS = 120, REQUIRED_STRATEGY_SECTIONS, ROLE_MAPPING, SHARED_FAMILIES, DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, LOAD_ORDER_HEADING_RE, FIVE_QUESTION_SECTIONS, HEADING_RE, RUNTIME_HEADING_ALIASES;
|
|
4466
4521
|
var init_engine = __esm(() => {
|
|
4467
4522
|
SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
4468
4523
|
CONFIG_KEYS = {
|
|
@@ -4476,16 +4531,8 @@ var init_engine = __esm(() => {
|
|
|
4476
4531
|
[MSTARC_PROJECT_DIR_KEY]: "projectDir",
|
|
4477
4532
|
[MSTARC_ENFORCEMENT_KEY]: "enforcement"
|
|
4478
4533
|
};
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
updated_at: "1970-01-01",
|
|
4482
|
-
workflows: []
|
|
4483
|
-
};
|
|
4484
|
-
SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
|
|
4485
|
-
GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
4486
|
-
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
4487
|
-
GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
4488
|
-
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
4534
|
+
COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
4535
|
+
DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4489
4536
|
DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
4490
4537
|
RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
4491
4538
|
DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
@@ -4504,11 +4551,27 @@ var init_engine = __esm(() => {
|
|
|
4504
4551
|
WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
4505
4552
|
WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
4506
4553
|
WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
4507
|
-
|
|
4554
|
+
DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4508
4555
|
PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
4509
4556
|
RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
4510
4557
|
RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
4511
4558
|
validateStatus = validateStatusV2;
|
|
4559
|
+
ROADMAP_STATUSES = ["active", "paused", "completed"];
|
|
4560
|
+
DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4561
|
+
ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
|
|
4562
|
+
EMPTY_STATUS_TEMPLATE = {
|
|
4563
|
+
version: 2,
|
|
4564
|
+
updated_at: "1970-01-01",
|
|
4565
|
+
workflows: []
|
|
4566
|
+
};
|
|
4567
|
+
SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
|
|
4568
|
+
EMPTY_REGISTER_TEMPLATE = {
|
|
4569
|
+
entries: {}
|
|
4570
|
+
};
|
|
4571
|
+
GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
4572
|
+
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
4573
|
+
GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
4574
|
+
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
4512
4575
|
QC_ALIGNMENT_FIELDS = [
|
|
4513
4576
|
{ key: "planId", label: "plan_id" },
|
|
4514
4577
|
{ key: "reviewRange", label: "Review range" },
|
|
@@ -4523,11 +4586,6 @@ var init_engine = __esm(() => {
|
|
|
4523
4586
|
}
|
|
4524
4587
|
};
|
|
4525
4588
|
GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
|
|
4526
|
-
COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
4527
|
-
DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4528
|
-
ROADMAP_STATUSES = ["active", "paused", "completed"];
|
|
4529
|
-
DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4530
|
-
ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
|
|
4531
4589
|
DATE_RE4 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4532
4590
|
ROOT_METADATA_LIFT_KEYS = {
|
|
4533
4591
|
plan_parallelism: true,
|