role-os 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.es.md +35 -12
- package/README.fr.md +32 -9
- package/README.hi.md +32 -9
- package/README.it.md +36 -13
- package/README.ja.md +33 -10
- package/README.md +32 -9
- package/README.pt-BR.md +32 -9
- package/README.zh.md +32 -9
- package/bin/roleos.mjs +3 -2
- package/package.json +1 -1
- package/src/artifacts.mjs +14 -7
- package/src/audit-cmd.mjs +23 -23
- package/src/brainstorm-roles.mjs +6 -0
- package/src/citation-panel.mjs +26 -1
- package/src/composite.mjs +4 -0
- package/src/dispatch.mjs +3 -1
- package/src/dossier-block.mjs +74 -0
- package/src/entry.mjs +2 -2
- package/src/hooks.mjs +107 -27
- package/src/knowledge/analyze-artifact-evidence.mjs +19 -9
- package/src/knowledge/fallback-policy.mjs +19 -7
- package/src/knowledge/resolve-overlay.mjs +21 -8
- package/src/knowledge/retrieve-for-dispatch.mjs +9 -4
- package/src/mission-run.mjs +11 -2
- package/src/packs-cmd.mjs +1 -1
- package/src/review.mjs +11 -2
- package/src/role-dossiers.json +962 -0
- package/src/route.mjs +41 -8
- package/src/run-cmd.mjs +0 -1
- package/src/run.mjs +67 -15
- package/src/session.mjs +3 -1
- package/src/specialist/capability-gate.mjs +35 -18
- package/src/specialist/dispatch.mjs +8 -3
- package/src/specialist/registry.mjs +6 -0
- package/src/specialist/shadow.mjs +13 -3
- package/src/specialist/state.mjs +94 -26
- package/src/state-machine.mjs +2 -2
- package/src/status.mjs +4 -2
- package/src/swarm/build-gate.mjs +11 -2
- package/src/swarm/persist-bridge.mjs +4 -3
- package/src/swarm-cmd.mjs +88 -46
- package/src/verify-citations-cmd.mjs +17 -1
- package/src/verify-citations.mjs +31 -7
- package/starter-pack/README.md +22 -14
- package/starter-pack/handbook.md +4 -4
- package/starter-pack/policy/routing-rules.md +42 -0
- package/starter-pack/policy/tool-permissions.md +21 -0
- package/starter-pack/workflows/full-treatment.md +27 -16
package/src/hooks.mjs
CHANGED
|
@@ -95,11 +95,7 @@ const SESSION_STATE_FILE = ".claude/hooks/session-state.json";
|
|
|
95
95
|
*/
|
|
96
96
|
export function getSessionState(cwd) {
|
|
97
97
|
const path = join(cwd, SESSION_STATE_FILE);
|
|
98
|
-
|
|
99
|
-
try { return JSON.parse(readFileSync(path, "utf-8")); }
|
|
100
|
-
catch { /* fall through */ }
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
98
|
+
const defaults = {
|
|
103
99
|
sessionId: null,
|
|
104
100
|
routeCardPresent: false,
|
|
105
101
|
activeRole: null,
|
|
@@ -110,6 +106,18 @@ export function getSessionState(cwd) {
|
|
|
110
106
|
outcomeRecorded: false,
|
|
111
107
|
startedAt: null,
|
|
112
108
|
};
|
|
109
|
+
if (existsSync(path)) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
112
|
+
// Merge over the default shape: a PARTIAL state file (e.g. created from {} by the generated
|
|
113
|
+
// prompt-submit script when SessionStart never ran) must not crash the library hook functions
|
|
114
|
+
// — the generated scripts guard field-by-field; the library tolerates the same files.
|
|
115
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
116
|
+
return { ...defaults, ...parsed };
|
|
117
|
+
}
|
|
118
|
+
} catch { /* fall through */ }
|
|
119
|
+
}
|
|
120
|
+
return defaults;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
/**
|
|
@@ -450,20 +458,93 @@ process.exit(0);
|
|
|
450
458
|
|
|
451
459
|
function generatePreToolUseScript() {
|
|
452
460
|
return `#!/usr/bin/env node
|
|
453
|
-
// Role OS PreToolUse hook —
|
|
461
|
+
// Role OS PreToolUse hook — role tool law + capability gate (fail-closed) + conformance floor (advisory).
|
|
462
|
+
// SELF-CONTAINED: stdlib-only. A bare role-os import specifier resolves in NO npx/global-install
|
|
463
|
+
// consumer repo (the package has no "exports" self-reference), so the fail-closed gate logic is
|
|
464
|
+
// INLINED below — a security control must never depend on a best-effort import. Internal failures
|
|
465
|
+
// warn on stderr (once per repo, marker-file throttled); they are never a silent catch.
|
|
454
466
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
455
467
|
import { join } from "node:path";
|
|
468
|
+
import { pathToFileURL } from "node:url";
|
|
456
469
|
|
|
457
470
|
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
458
471
|
const cwd = input.cwd || process.cwd();
|
|
459
472
|
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
473
|
+
const toolName = input.tool_name || "";
|
|
474
|
+
|
|
475
|
+
// One-time stderr warning: degraded hook behavior must be VISIBLE, but must not spam every call.
|
|
476
|
+
function warnOnce(key, message) {
|
|
477
|
+
let line = "[role-os hook] " + message;
|
|
478
|
+
try {
|
|
479
|
+
const marker = join(cwd, ".claude", "hooks", "." + key + ".warned");
|
|
480
|
+
if (existsSync(marker)) return; // already surfaced in this repo
|
|
481
|
+
writeFileSync(marker, new Date().toISOString() + " " + message + "\\n");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
line += " (warn-marker write failed: " + err.message + ")";
|
|
484
|
+
}
|
|
485
|
+
process.stderr.write(line + "\\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Capability gate (opt-in via ROLEOS_CAPABILITY_GATE, FAIL-CLOSED) ─────────────────────────────
|
|
489
|
+
// Inlined gated set + grant law — keep in sync with src/specialist/capability-gate.mjs in the
|
|
490
|
+
// role-os repo. A gated irreversible action (NAMED_COMPENSATORS list) with no matching grant in
|
|
491
|
+
// .claude/role-os/capabilities.json is DENIED (exit 2 blocks). Default OFF => pure no-op. The
|
|
492
|
+
// patterns allow flags between command word and verb without crossing a shell separator; an
|
|
493
|
+
// unparseable "expires" DENIES (a typo'd date must never become a permanent grant).
|
|
494
|
+
const gateEnabled = process.env.ROLEOS_CAPABILITY_GATE === "1" || process.env.ROLEOS_CAPABILITY_GATE === "true";
|
|
495
|
+
if (gateEnabled && toolName === "Bash") {
|
|
496
|
+
const command = (input.tool_input && typeof input.tool_input.command === "string") ? input.tool_input.command : "";
|
|
497
|
+
const GATED = [
|
|
498
|
+
{ id: "npm:publish", label: "npm/pnpm/yarn publish", re: /\\b(?:npm|pnpm|yarn)\\b[^|;&\\n]*\\bpublish\\b/ },
|
|
499
|
+
{ id: "pypi:publish", label: "PyPI publish (twine/uv)", re: /\\btwine\\b[^|;&\\n]*\\bupload\\b|\\buv\\b[^|;&\\n]*\\bpublish\\b/ },
|
|
500
|
+
{ id: "gh:release", label: "gh release create", re: /\\bgh\\b[^|;&\\n]*\\brelease\\b[^|;&\\n]*\\bcreate\\b/ },
|
|
501
|
+
{ id: "gh:pr-create", label: "gh pr create", re: /\\bgh\\b[^|;&\\n]*\\bpr\\b[^|;&\\n]*\\bcreate\\b/ },
|
|
502
|
+
{ id: "gh:repo-edit", label: "gh repo edit/delete", re: /\\bgh\\b[^|;&\\n]*\\brepo\\b[^|;&\\n]*\\b(?:edit|delete)\\b/ },
|
|
503
|
+
{ id: "git:push", label: "git push", re: /\\bgit\\b[^|;&\\n]*\\bpush\\b/ },
|
|
504
|
+
{ id: "pages:deploy", label: "GitHub Pages / gh-pages deploy", re: /\\bgh-pages\\b|\\bpages\\b[^|;&\\n]*\\bdeploy\\b/ },
|
|
505
|
+
];
|
|
506
|
+
const action = command ? GATED.find((a) => a.re.test(command)) : undefined;
|
|
507
|
+
if (action) {
|
|
508
|
+
let problem = null;
|
|
509
|
+
try {
|
|
510
|
+
const capPath = join(cwd, ".claude", "role-os", "capabilities.json");
|
|
511
|
+
const manifest = existsSync(capPath) ? JSON.parse(readFileSync(capPath, "utf-8")) : {};
|
|
512
|
+
const g = manifest && typeof manifest === "object" ? manifest[action.id] : undefined;
|
|
513
|
+
if (!g || typeof g !== "object" || g.granted !== true) {
|
|
514
|
+
problem = 'No capability "' + action.id + '" is granted in .claude/role-os/capabilities.json';
|
|
515
|
+
} else if (typeof g.expires === "string") {
|
|
516
|
+
const t = Date.parse(g.expires);
|
|
517
|
+
if (Number.isNaN(t)) {
|
|
518
|
+
problem = 'the grant for "' + action.id + '" has an unparseable "expires" ("' + g.expires + '") — an invalid expiry DENIES (fail-closed), it never extends the grant; fix the date';
|
|
519
|
+
} else if (t < Date.now()) {
|
|
520
|
+
problem = 'the grant for "' + action.id + '" expired at ' + g.expires;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch (err) {
|
|
524
|
+
// FAIL CLOSED: a gated action whose grant cannot be evaluated is denied, with the cause named.
|
|
525
|
+
problem = "the grant could not be evaluated (" + err.message + ") — failing closed on an irreversible action";
|
|
526
|
+
}
|
|
527
|
+
if (problem) {
|
|
528
|
+
process.stderr.write(
|
|
529
|
+
'Capability gate: "' + action.label + '" is an irreversible action requiring an explicit grant. ' +
|
|
530
|
+
problem + '. To authorize it, the director adds {"' + action.id + '": {"granted": true}} to ' +
|
|
531
|
+
'.claude/role-os/capabilities.json, optionally with an "expires" date. (The gate enforces only ' +
|
|
532
|
+
'"granted"/"expires" — a grant authorizes ALL matching ' + action.label + ' calls; a "scope" field is informational only.)\\n'
|
|
533
|
+
);
|
|
534
|
+
process.exit(2);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
460
538
|
|
|
461
539
|
let state = {};
|
|
462
540
|
if (existsSync(statePath)) {
|
|
463
|
-
try { state = JSON.parse(readFileSync(statePath, "utf-8")); }
|
|
541
|
+
try { state = JSON.parse(readFileSync(statePath, "utf-8")); }
|
|
542
|
+
catch (err) {
|
|
543
|
+
warnOnce("session-state-unreadable", "session-state.json was unreadable (" + err.message + ") — continuing with fresh session state.");
|
|
544
|
+
state = {};
|
|
545
|
+
}
|
|
464
546
|
}
|
|
465
547
|
|
|
466
|
-
const toolName = input.tool_name || "";
|
|
467
548
|
if (!state.toolsUsed) state.toolsUsed = [];
|
|
468
549
|
if (!state.toolsUsed.includes(toolName)) {
|
|
469
550
|
state.toolsUsed.push(toolName);
|
|
@@ -476,33 +557,32 @@ if (writeTools.includes(toolName) && !state.routeCardPresent && (state.substanti
|
|
|
476
557
|
notes.push(\`Write tool "\${toolName}" used without route card. Consider /roleos-route.\`);
|
|
477
558
|
}
|
|
478
559
|
|
|
479
|
-
// Tool-Call Conformance floor (advisory, deterministic).
|
|
480
|
-
// .claude/role-os/tool-contracts.json catalog entry AND the role-os library is
|
|
481
|
-
//
|
|
560
|
+
// Tool-Call Conformance floor (advisory, deterministic, fail-open). Runs only when this tool has a
|
|
561
|
+
// .claude/role-os/tool-contracts.json catalog entry AND the role-os library is installed where this
|
|
562
|
+
// repo can resolve it (local node_modules). Unlike the inlined capability gate above (fail-closed),
|
|
563
|
+
// the advisory floor may degrade — but it must SAY so once on stderr, never silently no-op.
|
|
482
564
|
try {
|
|
483
565
|
const catPath = join(cwd, ".claude", "role-os", "tool-contracts.json");
|
|
484
566
|
if (existsSync(catPath)) {
|
|
485
567
|
const catalog = JSON.parse(readFileSync(catPath, "utf-8"));
|
|
486
568
|
const entry = catalog && catalog[toolName];
|
|
487
569
|
if (entry) {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
570
|
+
const libPath = join(cwd, "node_modules", "role-os", "src", "specialist", "conformance-consult.mjs");
|
|
571
|
+
if (!existsSync(libPath)) {
|
|
572
|
+
warnOnce("conformance-lib-unresolvable", "tool-contracts.json catalogs this tool but the role-os library is not installed locally (" + libPath + " not found) — the advisory conformance floor is OFF. Run: npm i -D role-os");
|
|
573
|
+
} else {
|
|
574
|
+
const { schemaFloor, contractFloor } = await import(pathToFileURL(libPath).href);
|
|
575
|
+
const tool = { name: toolName, contract: entry.contract, params: entry.params || [], constraints: entry.constraints || [] };
|
|
576
|
+
const call = (input.tool_input && typeof input.tool_input === "object") ? input.tool_input : {};
|
|
577
|
+
const v = [...schemaFloor(tool, call).violations, ...contractFloor(tool, call, entry.state_struct || null).violations];
|
|
578
|
+
if (v.length) notes.push(\`Tool-Call Conformance (advisory): "\${toolName}" appears NONCONFORMANT — \${v.join("; ")}.\`);
|
|
579
|
+
}
|
|
493
580
|
}
|
|
494
581
|
}
|
|
495
|
-
} catch
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// DO (POLA / CaMeL). Best-effort: if role-os is not resolvable here a hook-resolution failure must not
|
|
500
|
-
// itself block a call; the in-process onPreToolUse path still enforces it where role-os is resolvable.
|
|
501
|
-
try {
|
|
502
|
-
const { capabilityGate } = await import("role-os/src/specialist/capability-gate.mjs");
|
|
503
|
-
const cap = capabilityGate(cwd, toolName, input.tool_input || {});
|
|
504
|
-
if (cap.denied) { process.stderr.write(cap.reason + "\\n"); process.exit(2); }
|
|
505
|
-
} catch { /* role-os not resolvable / internal error -> no-op (in-process path enforces) */ }
|
|
582
|
+
} catch (err) {
|
|
583
|
+
// Advisory stays fail-open (never blocks a call) but never SILENT: surface the failure once.
|
|
584
|
+
warnOnce("conformance-floor-error", "Tool-Call Conformance advisory errored (" + err.message + ") — the advisory floor was skipped.");
|
|
585
|
+
}
|
|
506
586
|
|
|
507
587
|
// PreToolUse wire protocol (current Claude Code): inject advisory context via
|
|
508
588
|
// hookSpecificOutput.additionalContext + exit 0. A bare { addContext } is IGNORED; exit 2 would BLOCK.
|
|
@@ -206,11 +206,12 @@ export function analyzeArtifactEvidence({
|
|
|
206
206
|
locations.push("title-match");
|
|
207
207
|
}
|
|
208
208
|
// Check for source ID reference
|
|
209
|
-
|
|
209
|
+
const sourceId = (chunk.source_id ?? "").toLowerCase();
|
|
210
|
+
if (sourceId && artifactLower.includes(sourceId)) {
|
|
210
211
|
locations.push("source-id");
|
|
211
212
|
}
|
|
212
213
|
// Check for key content phrases (first 50 chars of content)
|
|
213
|
-
const contentSnippet = chunk.content.slice(0, 50).toLowerCase();
|
|
214
|
+
const contentSnippet = (chunk.content ?? "").slice(0, 50).toLowerCase();
|
|
214
215
|
if (contentSnippet.length > 20 && artifactLower.includes(contentSnippet)) {
|
|
215
216
|
locations.push("content-echo");
|
|
216
217
|
}
|
|
@@ -236,7 +237,7 @@ export function analyzeArtifactEvidence({
|
|
|
236
237
|
const knownRefs = new Set([
|
|
237
238
|
...(bundle?.selected?.map((c) => (c.citation?.reference ?? c.chunk_id).toLowerCase()) ?? []),
|
|
238
239
|
...(bundle?.selected?.map((c) => c.title?.toLowerCase()).filter(Boolean) ?? []),
|
|
239
|
-
...(bundle?.selected?.map((c) => c.source_id.toLowerCase()) ?? []),
|
|
240
|
+
...(bundle?.selected?.map((c) => (c.source_id ?? "").toLowerCase()).filter(Boolean) ?? []),
|
|
240
241
|
...known_external_refs.map((r) => r.toLowerCase()),
|
|
241
242
|
]);
|
|
242
243
|
|
|
@@ -268,7 +269,14 @@ export function analyzeArtifactEvidence({
|
|
|
268
269
|
|
|
269
270
|
if (!postureCompliance.compliant) {
|
|
270
271
|
verdict = verdict === "fail" ? "fail" : "warn";
|
|
271
|
-
|
|
272
|
+
const parts = [];
|
|
273
|
+
if (postureCompliance.missing_signals.length > 0) {
|
|
274
|
+
parts.push(`missing ${postureCompliance.missing_signals.join(", ")}`);
|
|
275
|
+
}
|
|
276
|
+
if ((postureCompliance.banned_violations ?? []).length > 0) {
|
|
277
|
+
parts.push(`banned phrase(s): ${postureCompliance.banned_violations.join(", ")}`);
|
|
278
|
+
}
|
|
279
|
+
reasons.push(`Posture compliance failed: ${parts.join("; ") || "expected posture signals absent"}`);
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
if (driftViolations.length > 0) {
|
|
@@ -390,16 +398,18 @@ function checkDrift(artifactLower, roleId) {
|
|
|
390
398
|
function extractCitationPatterns(text) {
|
|
391
399
|
const patterns = [];
|
|
392
400
|
|
|
393
|
-
// Bracketed references: [Something]
|
|
394
|
-
|
|
401
|
+
// Bracketed references: [Something] — but not markdown links [text](url)
|
|
402
|
+
// (checkbox brackets like [x] fall below the 5-char minimum already)
|
|
403
|
+
const bracketMatches = text.match(/\[([^\]]{5,80})\](?!\()/g) ?? [];
|
|
395
404
|
for (const m of bracketMatches) {
|
|
396
405
|
patterns.push(m.slice(1, -1));
|
|
397
406
|
}
|
|
398
407
|
|
|
399
|
-
// "Source: X" or "Reference: X"
|
|
400
|
-
|
|
408
|
+
// "Source: X" or "Reference: X" — word boundaries so "per" can't match
|
|
409
|
+
// inside words like "Super" or "paper"
|
|
410
|
+
const sourceMatches = text.match(/\b(?:source|reference|per|according to)\b:?\s+([^\n.]{5,80})/gi) ?? [];
|
|
401
411
|
for (const m of sourceMatches) {
|
|
402
|
-
const cleaned = m.replace(/^(?:source|reference|per|according to):?\s+/i, "").trim();
|
|
412
|
+
const cleaned = m.replace(/^(?:source|reference|per|according to)\b:?\s+/i, "").trim();
|
|
403
413
|
if (cleaned) patterns.push(cleaned);
|
|
404
414
|
}
|
|
405
415
|
|
|
@@ -13,12 +13,24 @@
|
|
|
13
13
|
* @returns {{ state: string, action: string, message: string }}
|
|
14
14
|
*/
|
|
15
15
|
export function applyFallbackPolicy(bundle, overlay) {
|
|
16
|
+
// Malformed bundle from a buggy/version-skewed retrieve() → named degraded
|
|
17
|
+
// state instead of a TypeError that callers would swallow silently.
|
|
18
|
+
if (!bundle || typeof bundle !== "object" || !Array.isArray(bundle.selected)) {
|
|
19
|
+
return {
|
|
20
|
+
state: "malformed_bundle",
|
|
21
|
+
action: "warn",
|
|
22
|
+
message: "Retrieval bundle is malformed (missing or invalid selected[]) — knowledge degraded",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const summary = bundle.summary ?? {};
|
|
27
|
+
|
|
16
28
|
// No overlay → shared corpus only
|
|
17
29
|
if (!overlay) {
|
|
18
30
|
return {
|
|
19
31
|
state: "no_overlay",
|
|
20
32
|
action: "continue",
|
|
21
|
-
message: `No overlay for role ${bundle.role_id} — using shared corpus only`,
|
|
33
|
+
message: `No overlay for role ${bundle.role_id ?? "unknown"} — using shared corpus only`,
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
|
|
@@ -32,22 +44,22 @@ export function applyFallbackPolicy(bundle, overlay) {
|
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
// Check for forbidden source hits
|
|
35
|
-
if (
|
|
47
|
+
if ((summary.forbidden_hits ?? 0) > 0) {
|
|
36
48
|
// Forbidden sources were removed, but log the diagnostic
|
|
37
49
|
return {
|
|
38
50
|
state: "forbidden_hit",
|
|
39
51
|
action: "continue",
|
|
40
|
-
message: `${
|
|
52
|
+
message: `${summary.forbidden_hits} forbidden source(s) removed from results`,
|
|
41
53
|
};
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
// Check for stale-dominant results
|
|
45
|
-
const totalRelevant =
|
|
46
|
-
if (totalRelevant > 0 &&
|
|
57
|
+
const totalRelevant = (summary.selected_count ?? 0) + (summary.stale_count ?? 0);
|
|
58
|
+
if (totalRelevant > 0 && (summary.stale_count ?? 0) / totalRelevant > 0.5) {
|
|
47
59
|
return {
|
|
48
60
|
state: "stale_dominant",
|
|
49
61
|
action: "warn",
|
|
50
|
-
message: `${
|
|
62
|
+
message: `${summary.stale_count} of ${totalRelevant} relevant candidates are stale`,
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -62,7 +74,7 @@ export function applyFallbackPolicy(bundle, overlay) {
|
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
// Check for weak trust posture
|
|
65
|
-
if (bundle.provenance
|
|
77
|
+
if ((bundle.provenance?.trust_posture ?? "weak") === "weak") {
|
|
66
78
|
return {
|
|
67
79
|
state: "no_strong_match",
|
|
68
80
|
action: "warn",
|
|
@@ -11,13 +11,26 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
-
// Default overlay search paths
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
// Default overlay search paths. ROLEOS_KNOWLEDGE_ROLES overrides everything;
|
|
15
|
+
// otherwise we look for a knowledge-core checkout SIBLING to the role-os repo
|
|
16
|
+
// (e.g. <workspace>/knowledge-core next to <workspace>/role-os), then a nested
|
|
17
|
+
// checkout, then role-os's own local knowledge dir.
|
|
18
|
+
function defaultOverlayPaths() {
|
|
19
|
+
const paths = [];
|
|
20
|
+
if (process.env.ROLEOS_KNOWLEDGE_ROLES) {
|
|
21
|
+
paths.push(resolve(process.env.ROLEOS_KNOWLEDGE_ROLES));
|
|
22
|
+
}
|
|
23
|
+
paths.push(
|
|
24
|
+
// Sibling knowledge-core checkout (development) — __dirname is <role-os>/src/knowledge,
|
|
25
|
+
// so three levels up is the workspace that contains both repos.
|
|
26
|
+
join(resolve(__dirname, "..", "..", ".."), "knowledge-core", "knowledge", "roles"),
|
|
27
|
+
// Nested knowledge-core checkout inside role-os
|
|
28
|
+
join(resolve(__dirname, "..", ".."), "knowledge-core", "knowledge", "roles"),
|
|
29
|
+
// Fallback: role-os local knowledge dir
|
|
30
|
+
join(resolve(__dirname, ".."), "knowledge", "roles"),
|
|
31
|
+
);
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
36
|
* Resolve the overlay for a role.
|
|
@@ -28,7 +41,7 @@ const OVERLAY_PATHS = [
|
|
|
28
41
|
* @returns {{ overlay: object, path: string } | null}
|
|
29
42
|
*/
|
|
30
43
|
export function resolveOverlay(roleId, options = {}) {
|
|
31
|
-
const paths = options.searchPaths ||
|
|
44
|
+
const paths = options.searchPaths || defaultOverlayPaths();
|
|
32
45
|
|
|
33
46
|
for (const dir of paths) {
|
|
34
47
|
const filePath = join(dir, `${roleId}.json`);
|
|
@@ -71,7 +71,7 @@ export async function retrieveForDispatch({ roleId, taskText, packetContextSumma
|
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
73
|
bundle,
|
|
74
|
-
status: deriveStatus(fallback),
|
|
74
|
+
status: deriveStatus(fallback, bundle),
|
|
75
75
|
fallback,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
@@ -82,18 +82,23 @@ export async function retrieveForDispatch({ roleId, taskText, packetContextSumma
|
|
|
82
82
|
|
|
83
83
|
return {
|
|
84
84
|
bundle,
|
|
85
|
-
status: deriveStatus(fallback),
|
|
85
|
+
status: deriveStatus(fallback, bundle),
|
|
86
86
|
fallback,
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Derive packet knowledge status from fallback state.
|
|
92
|
+
*
|
|
93
|
+
* no_overlay with selected chunks maps to "weak" (shared-corpus-only evidence),
|
|
94
|
+
* matching fallback-policy's "continue — using shared corpus only" action.
|
|
95
|
+
* "none" is reserved for genuinely empty retrievals, since renderKnowledgeBlock()
|
|
96
|
+
* drops the knowledge block entirely for status "none".
|
|
92
97
|
*/
|
|
93
|
-
function deriveStatus(fallback) {
|
|
98
|
+
function deriveStatus(fallback, bundle) {
|
|
94
99
|
switch (fallback.state) {
|
|
95
100
|
case "healthy": return "strong";
|
|
96
|
-
case "no_overlay": return "none";
|
|
101
|
+
case "no_overlay": return (bundle?.selected?.length ?? 0) > 0 ? "weak" : "none";
|
|
97
102
|
case "no_strong_match": return "weak";
|
|
98
103
|
case "stale_dominant": return "stale";
|
|
99
104
|
case "conflicting": return "conflicted";
|
package/src/mission-run.mjs
CHANGED
|
@@ -114,11 +114,12 @@ export function createRun(missionKey, taskDescription, options = {}) {
|
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* Build steps from manifest for dynamic dispatch missions.
|
|
117
|
+
* Exported so the persistent runner (run.mjs) can scale steps from a manifest.
|
|
117
118
|
* @param {Object} mission
|
|
118
119
|
* @param {Object} manifest - The audit-manifest.json content
|
|
119
120
|
* @returns {MissionStep[]}
|
|
120
121
|
*/
|
|
121
|
-
function buildDynamicSteps(mission, manifest) {
|
|
122
|
+
export function buildDynamicSteps(mission, manifest) {
|
|
122
123
|
const dd = mission.dynamicDispatch;
|
|
123
124
|
const steps = [];
|
|
124
125
|
|
|
@@ -198,11 +199,12 @@ function buildDynamicSteps(mission, manifest) {
|
|
|
198
199
|
/**
|
|
199
200
|
* Build steps from swarm manifest for dogfood-swarm missions.
|
|
200
201
|
* Creates domain agent steps per stage with coordinator gates.
|
|
202
|
+
* Exported so the persistent runner (run.mjs) can scale steps from a manifest.
|
|
201
203
|
* @param {Object} mission
|
|
202
204
|
* @param {Object} manifest - The swarm-manifest.json content
|
|
203
205
|
* @returns {MissionStep[]}
|
|
204
206
|
*/
|
|
205
|
-
function buildSwarmSteps(mission, manifest) {
|
|
207
|
+
export function buildSwarmSteps(mission, manifest) {
|
|
206
208
|
const steps = [];
|
|
207
209
|
const domains = manifest.domains || [];
|
|
208
210
|
const stages = manifest.stages || ["health-a", "health-b", "health-c", "feature", "treatment"];
|
|
@@ -412,6 +414,13 @@ export function recordEscalation(run, from, to, trigger, action) {
|
|
|
412
414
|
targetStep.note = `Re-opened by escalation: ${trigger}`;
|
|
413
415
|
targetStep.completedAt = null;
|
|
414
416
|
escalation.reopened = true;
|
|
417
|
+
|
|
418
|
+
// A completed run with a re-opened step is no longer complete —
|
|
419
|
+
// reset run status so the pending step is actionable again.
|
|
420
|
+
if (run.status === "completed") {
|
|
421
|
+
run.status = "running";
|
|
422
|
+
run.completedAt = null;
|
|
423
|
+
}
|
|
415
424
|
} else {
|
|
416
425
|
// S4-F2: Target role not found in chain (or no completed step to re-open).
|
|
417
426
|
// Warn the operator instead of silently doing nothing.
|
package/src/packs-cmd.mjs
CHANGED
|
@@ -57,7 +57,7 @@ function runSuggest(packetFile) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
console.log(`\nNext: roleos route ${packetFile} --pack
|
|
60
|
+
console.log(`\nNext: roleos route ${packetFile} --pack=${result.pack}\n`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// ── Show ──────────────────────────────────────────────────────────────────────
|
package/src/review.mjs
CHANGED
|
@@ -32,9 +32,18 @@ export async function reviewCommand(args) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Extract task ID from the packet
|
|
35
|
-
const taskIdMatch = content.match(/## Task ID\n(.+)/);
|
|
35
|
+
const taskIdMatch = content.match(/## Task ID\r?\n(.+)/);
|
|
36
36
|
const taskId = taskIdMatch ? taskIdMatch[1].trim() : basename(packetFile, ".md");
|
|
37
37
|
|
|
38
|
+
// Extract the producing role from the packet — reject escalations route the
|
|
39
|
+
// retry back to the role that PRODUCED the output, never the reviewer.
|
|
40
|
+
// Falls back to Orchestrator when the section is absent or unassigned.
|
|
41
|
+
const assignedRoleMatch = content.match(/## Assigned Role\r?\n(.+)/);
|
|
42
|
+
const assignedRole = assignedRoleMatch ? assignedRoleMatch[1].trim() : null;
|
|
43
|
+
const producingRole = assignedRole && !assignedRole.startsWith("<!--")
|
|
44
|
+
? assignedRole
|
|
45
|
+
: "Orchestrator";
|
|
46
|
+
|
|
38
47
|
console.log(`\nroleos review — ${verdict}\n`);
|
|
39
48
|
console.log(`Packet: ${packetFile}`);
|
|
40
49
|
console.log(`Task ID: ${taskId}\n`);
|
|
@@ -99,7 +108,7 @@ ${nextOwner}
|
|
|
99
108
|
console.log(`\nEscalation (auto-routed):`);
|
|
100
109
|
console.log(formatEscalation(escalation));
|
|
101
110
|
} else if (verdict === "reject") {
|
|
102
|
-
const escalation = resolveRejected(reason,
|
|
111
|
+
const escalation = resolveRejected(reason, producingRole);
|
|
103
112
|
console.log(`\nEscalation (auto-routed):`);
|
|
104
113
|
console.log(formatEscalation(escalation));
|
|
105
114
|
}
|