role-os 2.2.0 → 2.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 +44 -0
- package/README.md +58 -14
- package/bin/roleos.mjs +20 -0
- package/package.json +2 -2
- package/src/artifacts.mjs +79 -1
- package/src/audit-cmd.mjs +401 -0
- package/src/brainstorm-roles.mjs +44 -1
- package/src/composite.mjs +41 -0
- package/src/dispatch.mjs +9 -83
- package/src/hooks.mjs +5 -5
- package/src/knowledge/analyze-artifact-evidence.mjs +420 -0
- package/src/knowledge/attach-bundle-to-evidence.mjs +62 -0
- package/src/knowledge/attach-bundle-to-packet.mjs +42 -0
- package/src/knowledge/fallback-policy.mjs +79 -0
- package/src/knowledge/index.mjs +14 -0
- package/src/knowledge/render-knowledge-block.mjs +215 -0
- package/src/knowledge/resolve-overlay.mjs +66 -0
- package/src/knowledge/retrieve-for-dispatch.mjs +150 -0
- package/src/mission-run.mjs +119 -2
- package/src/mission.mjs +130 -0
- package/src/packs.mjs +37 -0
- package/src/route.mjs +51 -0
- package/src/run-cmd.mjs +4 -1
- package/src/run.mjs +51 -3
- package/src/state-machine.mjs +70 -0
- package/src/swarm/build-gate.mjs +127 -0
- package/src/swarm/domain-detect.mjs +230 -0
- package/src/swarm/persist-bridge.mjs +174 -0
- package/src/swarm-cmd.mjs +424 -0
- package/src/tool-profiles.mjs +91 -0
- package/src/trial.mjs +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Block Renderer — transforms a retrieval bundle into a prompt fragment.
|
|
3
|
+
*
|
|
4
|
+
* This is a pure function: same input → same output. No retrieval logic.
|
|
5
|
+
* No raw JSON dump. No ad hoc search. Just governed prompt construction.
|
|
6
|
+
*
|
|
7
|
+
* The block has 4 sections:
|
|
8
|
+
* 1. Knowledge posture (one line)
|
|
9
|
+
* 2. Retrieved evidence (top N excerpts with citations)
|
|
10
|
+
* 3. Warnings / constraints
|
|
11
|
+
* 4. Usage law (status-specific behavioral rules)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Configuration ───────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const MAX_EVIDENCE_CHUNKS = 6;
|
|
17
|
+
const MAX_EXCERPT_LENGTH = 200;
|
|
18
|
+
|
|
19
|
+
// ── Status-Specific Usage Law ───────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const USAGE_LAW = {
|
|
22
|
+
strong: [
|
|
23
|
+
"Prioritize retrieved evidence when making claims in your domain.",
|
|
24
|
+
"Cite retrieved references when making specific substantive claims.",
|
|
25
|
+
"Do not invent sources that were not retrieved.",
|
|
26
|
+
],
|
|
27
|
+
weak: [
|
|
28
|
+
"Treat retrieved evidence as partial — it may not cover the full picture.",
|
|
29
|
+
"Avoid overclaiming based on limited evidence.",
|
|
30
|
+
"Escalate uncertainty where it affects your deliverable.",
|
|
31
|
+
"Do not invent sources that were not retrieved.",
|
|
32
|
+
],
|
|
33
|
+
stale: [
|
|
34
|
+
"Retrieved evidence may be outdated — note possible staleness in your analysis.",
|
|
35
|
+
"Do not present stale evidence as current truth without qualification.",
|
|
36
|
+
"Flag any claims that depend on time-sensitive data.",
|
|
37
|
+
"Do not invent sources that were not retrieved.",
|
|
38
|
+
],
|
|
39
|
+
conflicted: [
|
|
40
|
+
"Retrieved sources contain conflicting evidence.",
|
|
41
|
+
"Surface the conflict explicitly — do not flatten disagreement into fake consensus.",
|
|
42
|
+
"Acknowledge which claims are contested and by what sources.",
|
|
43
|
+
"Do not invent sources that were not retrieved.",
|
|
44
|
+
],
|
|
45
|
+
none: null, // No knowledge block rendered
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Main Renderer ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render a knowledge prompt block from packet.knowledge.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object|null} packetKnowledge - packet.knowledge (with retrieval_bundle and status)
|
|
54
|
+
* @returns {string|null} Prompt block string, or null if no knowledge to render
|
|
55
|
+
*/
|
|
56
|
+
export function renderKnowledgeBlock(packetKnowledge) {
|
|
57
|
+
if (!packetKnowledge || packetKnowledge.status === "none") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { retrieval_bundle: bundle, status } = packetKnowledge;
|
|
62
|
+
if (!bundle || !bundle.selected?.length) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sections = [];
|
|
67
|
+
|
|
68
|
+
// 1. Posture line
|
|
69
|
+
sections.push(`## Retrieved Knowledge\n\nKnowledge status: **${status}**`);
|
|
70
|
+
|
|
71
|
+
// 2. Evidence excerpts
|
|
72
|
+
const evidenceBlock = renderEvidenceBlock(bundle.selected);
|
|
73
|
+
if (evidenceBlock) {
|
|
74
|
+
sections.push(evidenceBlock);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Warnings
|
|
78
|
+
const warningsBlock = renderWarningsBlock(bundle.warnings);
|
|
79
|
+
if (warningsBlock) {
|
|
80
|
+
sections.push(warningsBlock);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Usage law
|
|
84
|
+
const lawBlock = renderUsageLaw(status);
|
|
85
|
+
if (lawBlock) {
|
|
86
|
+
sections.push(lawBlock);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sections.join("\n\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Evidence Renderer ───────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render the top N evidence excerpts.
|
|
96
|
+
*/
|
|
97
|
+
function renderEvidenceBlock(selected) {
|
|
98
|
+
if (!selected?.length) return null;
|
|
99
|
+
|
|
100
|
+
const top = selected.slice(0, MAX_EVIDENCE_CHUNKS);
|
|
101
|
+
const lines = ["### Evidence"];
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < top.length; i++) {
|
|
104
|
+
const chunk = top[i];
|
|
105
|
+
const trustLabel = chunk.metadata?.trust_tier ?? "general";
|
|
106
|
+
const freshnessLabel = chunk.metadata?.freshness?.status ?? "undated";
|
|
107
|
+
const citation = chunk.citation?.reference ?? chunk.chunk_id;
|
|
108
|
+
const excerpt = truncateExcerpt(chunk.content);
|
|
109
|
+
|
|
110
|
+
lines.push(`${i + 1}. [${trustLabel} | ${freshnessLabel}] ${citation}`);
|
|
111
|
+
lines.push(` "${excerpt}"`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Warnings Renderer ───────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Render retrieval warnings as constraints.
|
|
121
|
+
*/
|
|
122
|
+
function renderWarningsBlock(warnings) {
|
|
123
|
+
if (!warnings?.length) return null;
|
|
124
|
+
|
|
125
|
+
const meaningful = warnings.filter(
|
|
126
|
+
(w) => w.code !== "FORBIDDEN_SOURCE_HIT" // governance working, not a user-facing warning
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!meaningful.length) return null;
|
|
130
|
+
|
|
131
|
+
const lines = ["### Warnings"];
|
|
132
|
+
for (const warning of meaningful) {
|
|
133
|
+
lines.push(`- ${formatWarning(warning)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format a warning into human-readable text.
|
|
141
|
+
*/
|
|
142
|
+
function formatWarning(warning) {
|
|
143
|
+
switch (warning.code) {
|
|
144
|
+
case "NO_HIGH_TRUST_MATCH":
|
|
145
|
+
return "No authoritative sources matched — evidence quality is limited.";
|
|
146
|
+
case "ONLY_SHARED_CORPUS":
|
|
147
|
+
return "No role-specific evidence found — results are from shared corpus only.";
|
|
148
|
+
case "STALE_DOMINANT":
|
|
149
|
+
return "Most retrieved evidence is outdated — treat with caution.";
|
|
150
|
+
case "LOW_DIVERSITY":
|
|
151
|
+
return "Evidence comes from a single source — limited perspective.";
|
|
152
|
+
case "CONFLICTING_EVIDENCE":
|
|
153
|
+
return `Conflicting evidence: ${warning.message}`;
|
|
154
|
+
default:
|
|
155
|
+
return warning.message;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Usage Law Renderer ──────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Render status-specific usage law.
|
|
163
|
+
*/
|
|
164
|
+
function renderUsageLaw(status) {
|
|
165
|
+
const laws = USAGE_LAW[status];
|
|
166
|
+
if (!laws) return null;
|
|
167
|
+
|
|
168
|
+
const lines = ["### Knowledge Use Rules"];
|
|
169
|
+
for (const law of laws) {
|
|
170
|
+
lines.push(`- ${law}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Truncate content to MAX_EXCERPT_LENGTH, preserving word boundaries.
|
|
180
|
+
*/
|
|
181
|
+
function truncateExcerpt(content) {
|
|
182
|
+
if (!content) return "";
|
|
183
|
+
if (content.length <= MAX_EXCERPT_LENGTH) return content;
|
|
184
|
+
|
|
185
|
+
const truncated = content.slice(0, MAX_EXCERPT_LENGTH);
|
|
186
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
187
|
+
return (lastSpace > MAX_EXCERPT_LENGTH * 0.7 ? truncated.slice(0, lastSpace) : truncated) + "...";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Manifest Summary ────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Generate a compact knowledge summary for the dispatch manifest.
|
|
194
|
+
* Not full excerpts — just posture truth.
|
|
195
|
+
*
|
|
196
|
+
* @param {Object|null} packetKnowledge
|
|
197
|
+
* @returns {Object|null} Compact summary for manifest, or null
|
|
198
|
+
*/
|
|
199
|
+
export function knowledgeManifestSummary(packetKnowledge) {
|
|
200
|
+
if (!packetKnowledge || packetKnowledge.status === "none") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { retrieval_bundle: bundle, status } = packetKnowledge;
|
|
205
|
+
if (!bundle) return null;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
status,
|
|
209
|
+
selected_count: bundle.selected?.length ?? 0,
|
|
210
|
+
trust_posture: bundle.provenance?.trust_posture ?? "weak",
|
|
211
|
+
freshness_posture: bundle.provenance?.freshness_posture ?? "stale",
|
|
212
|
+
warning_codes: (bundle.warnings ?? []).map((w) => w.code),
|
|
213
|
+
rerank_strategy: bundle.summary?.rerank_strategy ?? "unknown",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a role overlay from knowledge/roles/*.json.
|
|
3
|
+
*
|
|
4
|
+
* Returns the overlay config for a given role, or null if no overlay exists.
|
|
5
|
+
* Roles without overlays fall back to shared-corpus-only retrieval.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join, resolve, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Default overlay search paths (relative to knowledge-core root)
|
|
15
|
+
const OVERLAY_PATHS = [
|
|
16
|
+
// Local knowledge-core checkout (development)
|
|
17
|
+
join(resolve(__dirname, "..", ".."), "knowledge-core", "knowledge", "roles"),
|
|
18
|
+
// Fallback: role-os local knowledge dir
|
|
19
|
+
join(resolve(__dirname, ".."), "knowledge", "roles"),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the overlay for a role.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} roleId - Role identifier (e.g. "security-reviewer")
|
|
26
|
+
* @param {Object} [options]
|
|
27
|
+
* @param {string[]} [options.searchPaths] - Override overlay search paths
|
|
28
|
+
* @returns {{ overlay: object, path: string } | null}
|
|
29
|
+
*/
|
|
30
|
+
export function resolveOverlay(roleId, options = {}) {
|
|
31
|
+
const paths = options.searchPaths || OVERLAY_PATHS;
|
|
32
|
+
|
|
33
|
+
for (const dir of paths) {
|
|
34
|
+
const filePath = join(dir, `${roleId}.json`);
|
|
35
|
+
if (existsSync(filePath)) {
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
38
|
+
if (data.version !== "1.0") {
|
|
39
|
+
console.warn(`[knowledge] Overlay ${roleId}: unsupported version ${data.version}`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (data.role_id !== roleId) {
|
|
43
|
+
console.warn(`[knowledge] Overlay file ${roleId}.json has mismatched role_id: ${data.role_id}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return { overlay: data, path: filePath };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(`[knowledge] Failed to parse overlay for ${roleId}:`, e.message);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a role has an overlay available.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} roleId
|
|
61
|
+
* @param {Object} [options]
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function hasOverlay(roleId, options = {}) {
|
|
65
|
+
return resolveOverlay(roleId, options) !== null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieve knowledge for a dispatch step.
|
|
3
|
+
*
|
|
4
|
+
* This is the integration seam between Role OS dispatch and knowledge-core.
|
|
5
|
+
* Phase 2: wired to the real retrieval pipeline.
|
|
6
|
+
*
|
|
7
|
+
* When a corpus store is available, runs the full pipeline:
|
|
8
|
+
* task + overlay + corpus → candidates → filter → rerank → bundle
|
|
9
|
+
*
|
|
10
|
+
* When no corpus is available, returns a governed stub (graceful degradation).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolveOverlay } from "./resolve-overlay.mjs";
|
|
14
|
+
import { applyFallbackPolicy } from "./fallback-policy.mjs";
|
|
15
|
+
|
|
16
|
+
// ── Corpus Store Singleton ──────────────────────────────────────────
|
|
17
|
+
// Lazy-loaded. Role OS sets this via configureKnowledge().
|
|
18
|
+
let _store = null;
|
|
19
|
+
let _retrieveFn = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configure the knowledge subsystem with a live corpus store.
|
|
23
|
+
* Call once at startup (e.g., in session init or dispatch init).
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {Object} options.store - CorpusStore instance from knowledge-core
|
|
27
|
+
* @param {Function} options.retrieve - retrieve() function from knowledge-core
|
|
28
|
+
*/
|
|
29
|
+
export function configureKnowledge({ store, retrieve }) {
|
|
30
|
+
_store = store;
|
|
31
|
+
_retrieveFn = retrieve;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if knowledge subsystem is configured with a live corpus.
|
|
36
|
+
*/
|
|
37
|
+
export function isKnowledgeConfigured() {
|
|
38
|
+
return _store !== null && _retrieveFn !== null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} RetrieveOptions
|
|
43
|
+
* @property {string} roleId - Role identifier
|
|
44
|
+
* @property {string} taskText - Task description from packet
|
|
45
|
+
* @property {string} [packetContextSummary] - Optional packet context
|
|
46
|
+
* @property {Object} [routeSignals] - Route scoring signals
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve knowledge for a role's dispatch step.
|
|
51
|
+
*
|
|
52
|
+
* @param {RetrieveOptions} options
|
|
53
|
+
* @returns {Promise<{ bundle: object, status: string, fallback: object }>}
|
|
54
|
+
*/
|
|
55
|
+
export async function retrieveForDispatch({ roleId, taskText, packetContextSummary, routeSignals }) {
|
|
56
|
+
const overlayResult = resolveOverlay(roleId);
|
|
57
|
+
const overlay = overlayResult?.overlay ?? null;
|
|
58
|
+
|
|
59
|
+
// ── Live retrieval (Phase 2) ────────────────────────────────────
|
|
60
|
+
if (_store && _retrieveFn) {
|
|
61
|
+
const bundle = await _retrieveFn({
|
|
62
|
+
store: _store,
|
|
63
|
+
roleId,
|
|
64
|
+
taskText,
|
|
65
|
+
overlay,
|
|
66
|
+
packetContextSummary,
|
|
67
|
+
lexicalOnly: true, // Phase 2: lexical-only until embeddings are populated
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const fallback = applyFallbackPolicy(bundle, overlay);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
bundle,
|
|
74
|
+
status: deriveStatus(fallback),
|
|
75
|
+
fallback,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Stub fallback (no corpus configured) ────────────────────────
|
|
80
|
+
const bundle = buildStubBundle(roleId, taskText, overlayResult);
|
|
81
|
+
const fallback = applyFallbackPolicy(bundle, overlay);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
bundle,
|
|
85
|
+
status: deriveStatus(fallback),
|
|
86
|
+
fallback,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Derive packet knowledge status from fallback state.
|
|
92
|
+
*/
|
|
93
|
+
function deriveStatus(fallback) {
|
|
94
|
+
switch (fallback.state) {
|
|
95
|
+
case "healthy": return "strong";
|
|
96
|
+
case "no_overlay": return "none";
|
|
97
|
+
case "no_strong_match": return "weak";
|
|
98
|
+
case "stale_dominant": return "stale";
|
|
99
|
+
case "conflicting": return "conflicted";
|
|
100
|
+
case "forbidden_hit": return "strong"; // forbidden sources removed = governance working
|
|
101
|
+
default: return "weak";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a stub retrieval bundle when no corpus is available.
|
|
107
|
+
*/
|
|
108
|
+
function buildStubBundle(roleId, taskText, overlayResult) {
|
|
109
|
+
const hasOverlayData = overlayResult !== null;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
version: "1.0",
|
|
113
|
+
role_id: roleId,
|
|
114
|
+
query: {
|
|
115
|
+
task_text: taskText,
|
|
116
|
+
lexical_query: [],
|
|
117
|
+
semantic_query: [],
|
|
118
|
+
applied_overlay_rules: hasOverlayData ? [`overlay:${roleId}`] : [],
|
|
119
|
+
applied_filters: [],
|
|
120
|
+
generated_at: new Date().toISOString(),
|
|
121
|
+
},
|
|
122
|
+
summary: {
|
|
123
|
+
total_candidates: 0,
|
|
124
|
+
selected_count: 0,
|
|
125
|
+
stale_count: 0,
|
|
126
|
+
forbidden_hits: 0,
|
|
127
|
+
trust_tier_breakdown: { authoritative: 0, preferred: 0, general: 0, untrusted: 0 },
|
|
128
|
+
source_breakdown: {},
|
|
129
|
+
rerank_strategy: "stub-no-corpus",
|
|
130
|
+
},
|
|
131
|
+
selected: [],
|
|
132
|
+
rejected: [],
|
|
133
|
+
provenance: {
|
|
134
|
+
source_ids: [],
|
|
135
|
+
document_ids: [],
|
|
136
|
+
trust_posture: "weak",
|
|
137
|
+
freshness_posture: "stale",
|
|
138
|
+
},
|
|
139
|
+
warnings: [
|
|
140
|
+
{ code: "ONLY_SHARED_CORPUS", message: "No corpus configured — stub bundle returned" },
|
|
141
|
+
],
|
|
142
|
+
diagnostics: {
|
|
143
|
+
latency_ms: 0,
|
|
144
|
+
candidate_pool_size: 0,
|
|
145
|
+
deduped_count: 0,
|
|
146
|
+
dropped_forbidden_count: 0,
|
|
147
|
+
dropped_stale_count: 0,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/mission-run.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { MISSIONS, getMission, validateMission } from "./mission.mjs";
|
|
13
13
|
import { validateArtifact } from "./artifacts.mjs";
|
|
14
|
+
import { STEP_TRANSITIONS, isValidStepTransition } from "./state-machine.mjs";
|
|
14
15
|
|
|
15
16
|
let _runCounter = 0;
|
|
16
17
|
|
|
@@ -74,7 +75,10 @@ export function createRun(missionKey, taskDescription, options = {}) {
|
|
|
74
75
|
let steps;
|
|
75
76
|
const dd = mission.dynamicDispatch;
|
|
76
77
|
|
|
77
|
-
if (dd && options.manifest) {
|
|
78
|
+
if (missionKey === "dogfood-swarm" && dd && options.manifest) {
|
|
79
|
+
// Swarm dispatch — build staged domain steps from swarm manifest
|
|
80
|
+
steps = buildSwarmSteps(mission, options.manifest);
|
|
81
|
+
} else if (dd && options.manifest) {
|
|
78
82
|
// Dynamic dispatch — build steps from manifest
|
|
79
83
|
steps = buildDynamicSteps(mission, options.manifest);
|
|
80
84
|
} else {
|
|
@@ -104,6 +108,7 @@ export function createRun(missionKey, taskDescription, options = {}) {
|
|
|
104
108
|
completionReport: null,
|
|
105
109
|
dynamicDispatch: dd && options.manifest ? true : false,
|
|
106
110
|
manifest: options.manifest || null,
|
|
111
|
+
knowledge: options.knowledge || null, // Phase 5: PacketKnowledge from retrieval
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -190,6 +195,93 @@ function buildDynamicSteps(mission, manifest) {
|
|
|
190
195
|
return steps;
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Build steps from swarm manifest for dogfood-swarm missions.
|
|
200
|
+
* Creates domain agent steps per stage with coordinator gates.
|
|
201
|
+
* @param {Object} mission
|
|
202
|
+
* @param {Object} manifest - The swarm-manifest.json content
|
|
203
|
+
* @returns {MissionStep[]}
|
|
204
|
+
*/
|
|
205
|
+
function buildSwarmSteps(mission, manifest) {
|
|
206
|
+
const steps = [];
|
|
207
|
+
const domains = manifest.domains || [];
|
|
208
|
+
const stages = manifest.stages || ["health-a", "health-b", "health-c", "feature"];
|
|
209
|
+
const waveLoops = mission.waveLoops || [];
|
|
210
|
+
|
|
211
|
+
// For each stage, create domain agent steps + coordinator gate
|
|
212
|
+
for (const stage of stages) {
|
|
213
|
+
const loopDef = waveLoops.find(w => w.stage === stage);
|
|
214
|
+
|
|
215
|
+
// One step per domain agent
|
|
216
|
+
for (const domain of domains) {
|
|
217
|
+
steps.push({
|
|
218
|
+
role: domain.role,
|
|
219
|
+
produces: "wave-report",
|
|
220
|
+
consumedBy: "Swarm Coordinator",
|
|
221
|
+
domain: domain.id,
|
|
222
|
+
stage,
|
|
223
|
+
waveIteration: 0,
|
|
224
|
+
patterns: domain.patterns,
|
|
225
|
+
status: "pending",
|
|
226
|
+
artifact: null,
|
|
227
|
+
artifactValidation: null,
|
|
228
|
+
note: null,
|
|
229
|
+
startedAt: null,
|
|
230
|
+
completedAt: null,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Coordinator gate step after domain agents
|
|
235
|
+
steps.push({
|
|
236
|
+
role: "Swarm Coordinator",
|
|
237
|
+
produces: "swarm-gate",
|
|
238
|
+
consumedBy: stage === stages[stages.length - 1] ? "Swarm Synthesizer" : domains[0]?.role || null,
|
|
239
|
+
stage,
|
|
240
|
+
isGate: true,
|
|
241
|
+
exitCondition: loopDef?.exitCondition || null,
|
|
242
|
+
maxIterations: loopDef?.maxIterations || 1,
|
|
243
|
+
buildGate: loopDef?.buildGate ?? true,
|
|
244
|
+
userApproval: loopDef?.userApproval ?? false,
|
|
245
|
+
lens: loopDef?.lens || null,
|
|
246
|
+
status: "pending",
|
|
247
|
+
artifact: null,
|
|
248
|
+
artifactValidation: null,
|
|
249
|
+
note: null,
|
|
250
|
+
startedAt: null,
|
|
251
|
+
completedAt: null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Final stage: Synthesizer + Critic
|
|
256
|
+
steps.push({
|
|
257
|
+
role: "Swarm Synthesizer",
|
|
258
|
+
produces: "swarm-final-report",
|
|
259
|
+
consumedBy: "Critic Reviewer",
|
|
260
|
+
stage: "final",
|
|
261
|
+
status: "pending",
|
|
262
|
+
artifact: null,
|
|
263
|
+
artifactValidation: null,
|
|
264
|
+
note: null,
|
|
265
|
+
startedAt: null,
|
|
266
|
+
completedAt: null,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
steps.push({
|
|
270
|
+
role: "Critic Reviewer",
|
|
271
|
+
produces: "review-verdict",
|
|
272
|
+
consumedBy: null,
|
|
273
|
+
stage: "final",
|
|
274
|
+
status: "pending",
|
|
275
|
+
artifact: null,
|
|
276
|
+
artifactValidation: null,
|
|
277
|
+
note: null,
|
|
278
|
+
startedAt: null,
|
|
279
|
+
completedAt: null,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return steps;
|
|
283
|
+
}
|
|
284
|
+
|
|
193
285
|
// ── Step through a run ──────────────────────────────────────────────────────
|
|
194
286
|
|
|
195
287
|
/**
|
|
@@ -198,6 +290,10 @@ function buildDynamicSteps(mission, manifest) {
|
|
|
198
290
|
* @returns {MissionStep|null} The started step, or null if no pending steps
|
|
199
291
|
*/
|
|
200
292
|
export function startNextStep(run) {
|
|
293
|
+
// Guard: refuse to activate a new step if one is already active (prevents dual-active)
|
|
294
|
+
const alreadyActive = run.steps.find((s) => s.status === "active");
|
|
295
|
+
if (alreadyActive) return null;
|
|
296
|
+
|
|
201
297
|
const next = run.steps.find((s) => s.status === "pending");
|
|
202
298
|
if (!next) return null;
|
|
203
299
|
|
|
@@ -386,6 +482,7 @@ export function generateCompletionReport(run) {
|
|
|
386
482
|
status: s.status,
|
|
387
483
|
hasArtifact: !!s.artifact,
|
|
388
484
|
note: s.note,
|
|
485
|
+
knowledge: s.knowledge || null, // Phase 5: per-step knowledge posture
|
|
389
486
|
}));
|
|
390
487
|
|
|
391
488
|
const isComplete = run.status === "completed";
|
|
@@ -408,6 +505,13 @@ export function generateCompletionReport(run) {
|
|
|
408
505
|
artifactChain,
|
|
409
506
|
escalationCount: run.escalations.length,
|
|
410
507
|
escalations: run.escalations,
|
|
508
|
+
knowledge: run.knowledge ? {
|
|
509
|
+
status: run.knowledge.status,
|
|
510
|
+
selected_count: run.knowledge.retrieval_bundle?.selected?.length ?? 0,
|
|
511
|
+
trust_posture: run.knowledge.retrieval_bundle?.provenance?.trust_posture ?? "unknown",
|
|
512
|
+
freshness_posture: run.knowledge.retrieval_bundle?.provenance?.freshness_posture ?? "unknown",
|
|
513
|
+
warning_codes: (run.knowledge.retrieval_bundle?.warnings ?? []).map((w) => w.code),
|
|
514
|
+
} : null,
|
|
411
515
|
honestPartial: isPartial || isFailed ? mission.honestPartial : null,
|
|
412
516
|
verdict: isComplete
|
|
413
517
|
? "Mission completed — all artifacts produced, all steps passed."
|
|
@@ -453,7 +557,20 @@ export function formatCompletionReport(report) {
|
|
|
453
557
|
step.status === "blocked" ? "[-]" : "[ ]";
|
|
454
558
|
const artifact = step.hasArtifact ? ` → ${step.produces}` : "";
|
|
455
559
|
const note = step.note ? ` (${step.note})` : "";
|
|
456
|
-
|
|
560
|
+
const kStatus = step.knowledge ? ` [knowledge: ${step.knowledge.status}]` : "";
|
|
561
|
+
lines.push(` ${icon} ${step.role}${artifact}${kStatus}${note}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Knowledge posture (Phase 5)
|
|
565
|
+
if (report.knowledge) {
|
|
566
|
+
lines.push("");
|
|
567
|
+
lines.push("## Knowledge");
|
|
568
|
+
lines.push(` Status: ${report.knowledge.status}`);
|
|
569
|
+
lines.push(` Evidence: ${report.knowledge.selected_count} chunks selected`);
|
|
570
|
+
lines.push(` Trust: ${report.knowledge.trust_posture} | Freshness: ${report.knowledge.freshness_posture}`);
|
|
571
|
+
if (report.knowledge.warning_codes.length > 0) {
|
|
572
|
+
lines.push(` Warnings: ${report.knowledge.warning_codes.join(", ")}`);
|
|
573
|
+
}
|
|
457
574
|
}
|
|
458
575
|
|
|
459
576
|
if (report.escalationCount > 0) {
|