@ionivetech/mugiwara 0.7.0 → 0.8.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/README.md +196 -330
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +34 -33
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
- package/content/skills/mugiwara-frontend/SKILL.md +44 -44
- package/content/skills/mugiwara-gates/SKILL.md +28 -16
- package/content/skills/mugiwara-healing/SKILL.md +30 -25
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +28 -14
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
- package/content/skills/mugiwara-quality/SKILL.md +8 -13
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -9
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +17 -24
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +47 -35
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +13 -13
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +1802 -316
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/references/wave-banners.md +1 -2
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +8 -2
- package/scripts/gate-selftest.ts +104 -21
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +41 -2
- package/scripts/validate-content.ts +82 -3
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +18 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +221 -8
- package/src/cognition.ts +234 -0
- package/src/config.ts +113 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +189 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +65 -16
- package/src/investigation.ts +72 -0
- package/src/mission.ts +246 -16
- package/src/policy.ts +355 -2
- package/src/posture.ts +86 -0
- package/src/provenance.ts +29 -9
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +234 -18
- package/src/slop.ts +306 -0
- package/src/work.ts +273 -0
package/src/policy.ts
CHANGED
|
@@ -20,10 +20,17 @@ export type MugiwaraPolicy = {
|
|
|
20
20
|
require_human_approval?: string[];
|
|
21
21
|
};
|
|
22
22
|
evidence?: { required?: string[] };
|
|
23
|
+
integrity?: { extra_secret_patterns?: Array<{ pattern: string; label: string; severity?: 'block' | 'warn' }> };
|
|
24
|
+
attestation?: {
|
|
25
|
+
required?: boolean;
|
|
26
|
+
trusted_keys?: Array<{ id: string; pubkey: string; added?: string }>;
|
|
27
|
+
revoked?: Array<{ id: string; revoked?: string; reason?: string; pubkey?: string }>;
|
|
28
|
+
};
|
|
29
|
+
harness?: { require_enforcement?: boolean };
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
const POLICY_FILES = ['mugiwara.policy.yml', 'mugiwara.policy.yaml'];
|
|
26
|
-
const KNOWN_ROOTS = ['lanes', 'gates', 'evidence'];
|
|
33
|
+
const KNOWN_ROOTS = ['lanes', 'gates', 'evidence', 'integrity', 'attestation', 'harness'];
|
|
27
34
|
|
|
28
35
|
/**
|
|
29
36
|
* Minimal YAML subset: maps, `- item` string lists, scalars.
|
|
@@ -97,11 +104,238 @@ function scalar(v: string): unknown {
|
|
|
97
104
|
return t;
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Dedicated extractor for integrity.extra_secret_patterns list-of-maps.
|
|
109
|
+
* The minimal YAML subset parser only handles scalar lists; this scans the raw
|
|
110
|
+
* text line-by-line for map items so both forms work:
|
|
111
|
+
* - { pattern: "\\b...\\b", label: "NIK" }
|
|
112
|
+
* - - pattern: "\\b...\\b"
|
|
113
|
+
* label: "NIK"
|
|
114
|
+
* severity: warn
|
|
115
|
+
*/
|
|
116
|
+
export function extractExtraSecretPatterns(text: string): Array<{ pattern: string; label: string; severity?: string }> {
|
|
117
|
+
const lines = text.split(/\r?\n/);
|
|
118
|
+
let inBlock = false;
|
|
119
|
+
let baseIndent = -1;
|
|
120
|
+
const out: Array<Record<string, string>> = [];
|
|
121
|
+
let current: Record<string, string> | null = null;
|
|
122
|
+
let currentIndent = -1;
|
|
123
|
+
for (const rawLine of lines) {
|
|
124
|
+
const noComment = rawLine.replace(/(^|\s)#.*$/, '');
|
|
125
|
+
if (!noComment.trim()) continue;
|
|
126
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
127
|
+
const trimmed = noComment.trim();
|
|
128
|
+
if (!inBlock) {
|
|
129
|
+
if (trimmed.startsWith('extra_secret_patterns:')) {
|
|
130
|
+
inBlock = true;
|
|
131
|
+
baseIndent = indent;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// exit if a sibling key at same or shallower indent (not a list item) appears
|
|
136
|
+
if (indent <= baseIndent && !trimmed.startsWith('-') && trimmed.includes(':')) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (trimmed.startsWith('-')) {
|
|
140
|
+
if (current) out.push(current);
|
|
141
|
+
current = {};
|
|
142
|
+
currentIndent = indent;
|
|
143
|
+
const afterDash = trimmed.slice(1).trim();
|
|
144
|
+
if (!afterDash) continue;
|
|
145
|
+
if (afterDash.startsWith('{') && afterDash.endsWith('}')) {
|
|
146
|
+
const inner = afterDash.slice(1, -1);
|
|
147
|
+
for (const part of inner.split(',')) {
|
|
148
|
+
const colon = part.indexOf(':');
|
|
149
|
+
if (colon === -1) continue;
|
|
150
|
+
const k = part.slice(0, colon).trim();
|
|
151
|
+
const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
152
|
+
if (k && v) current[k] = v;
|
|
153
|
+
}
|
|
154
|
+
} else if (afterDash.includes(':')) {
|
|
155
|
+
const colon = afterDash.indexOf(':');
|
|
156
|
+
const k = afterDash.slice(0, colon).trim();
|
|
157
|
+
const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
158
|
+
if (k && v) current[k] = v;
|
|
159
|
+
} else {
|
|
160
|
+
const v = afterDash.replace(/^["']|["']$/g, '');
|
|
161
|
+
if (v) current['pattern'] = v;
|
|
162
|
+
}
|
|
163
|
+
} else if (current && trimmed.includes(':')) {
|
|
164
|
+
if (indent > currentIndent) {
|
|
165
|
+
const colon = trimmed.indexOf(':');
|
|
166
|
+
const k = trimmed.slice(0, colon).trim();
|
|
167
|
+
const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
168
|
+
if (k && v) current[k] = v;
|
|
169
|
+
} else if (indent <= baseIndent) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (current) out.push(current);
|
|
176
|
+
return out.filter((o) => typeof o.pattern === 'string' && typeof o.label === 'string' && o.pattern.length > 0) as Array<{ pattern: string; label: string; severity?: string }>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract attestation block: handles both inline `{ id: "...", pubkey: "ed25519:..." }`
|
|
181
|
+
* and multiline lists. Scans inside `attestation:` indented block so
|
|
182
|
+
* `evidence: required:` is not confused.
|
|
183
|
+
*/
|
|
184
|
+
export function extractAttestation(text: string): {
|
|
185
|
+
required?: boolean;
|
|
186
|
+
trusted_keys?: Array<Record<string, string>>;
|
|
187
|
+
revoked?: Array<Record<string, string>>;
|
|
188
|
+
} | null {
|
|
189
|
+
const lines = text.split(/\r?\n/);
|
|
190
|
+
let attBase = -1;
|
|
191
|
+
let inAtt = false;
|
|
192
|
+
let required: boolean | undefined;
|
|
193
|
+
const trusted: Array<Record<string, string>> = [];
|
|
194
|
+
const revoked: Array<Record<string, string>> = [];
|
|
195
|
+
// collection state
|
|
196
|
+
let collecting: 'trusted' | 'revoked' | null = null;
|
|
197
|
+
let collectBase = -1;
|
|
198
|
+
let current: Record<string, string> | null = null;
|
|
199
|
+
let curIndent = -1;
|
|
200
|
+
|
|
201
|
+
const flush = () => {
|
|
202
|
+
if (!current) return;
|
|
203
|
+
if (collecting === 'trusted') trusted.push(current);
|
|
204
|
+
else if (collecting === 'revoked') revoked.push(current);
|
|
205
|
+
current = null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
209
|
+
const rawLine = lines[idx];
|
|
210
|
+
const noComment = rawLine.replace(/(^|\s)#.*$/, '');
|
|
211
|
+
if (!noComment.trim()) continue;
|
|
212
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
213
|
+
const trimmed = noComment.trim();
|
|
214
|
+
|
|
215
|
+
if (!inAtt) {
|
|
216
|
+
if (trimmed === 'attestation:' || trimmed.startsWith('attestation:')) {
|
|
217
|
+
// handle `attestation: { ... }` inline — not used, but parse required if present
|
|
218
|
+
const after = trimmed.slice('attestation:'.length).trim();
|
|
219
|
+
if (after.startsWith('{')) {
|
|
220
|
+
// inline map form not needed for MVP
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
inAtt = true;
|
|
224
|
+
attBase = indent;
|
|
225
|
+
// if there is a value after colon on same line (e.g., attestation: foo) ignore
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// inside attestation block
|
|
232
|
+
// exit attestation when sibling root key at same/shallower indent
|
|
233
|
+
if (indent <= attBase && !trimmed.startsWith('-') && trimmed.includes(':')) {
|
|
234
|
+
flush();
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// handle collecting state first
|
|
239
|
+
if (collecting) {
|
|
240
|
+
// exiting collection to sibling key inside attestation (e.g., revoked: after trusted_keys:)
|
|
241
|
+
if (indent <= collectBase && !trimmed.startsWith('-') && trimmed.includes(':')) {
|
|
242
|
+
flush();
|
|
243
|
+
collecting = null;
|
|
244
|
+
// fall through to process this line as att child
|
|
245
|
+
} else if (trimmed.startsWith('-')) {
|
|
246
|
+
flush();
|
|
247
|
+
current = {};
|
|
248
|
+
curIndent = indent;
|
|
249
|
+
const afterDash = trimmed.slice(1).trim();
|
|
250
|
+
if (!afterDash) continue;
|
|
251
|
+
if (afterDash.startsWith('{') && afterDash.endsWith('}')) {
|
|
252
|
+
const inner = afterDash.slice(1, -1);
|
|
253
|
+
for (const part of inner.split(',')) {
|
|
254
|
+
const colon = part.indexOf(':');
|
|
255
|
+
if (colon === -1) continue;
|
|
256
|
+
const k = part.slice(0, colon).trim();
|
|
257
|
+
const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
258
|
+
if (k && v) current[k] = v;
|
|
259
|
+
}
|
|
260
|
+
} else if (afterDash.includes(':')) {
|
|
261
|
+
const colon = afterDash.indexOf(':');
|
|
262
|
+
const k = afterDash.slice(0, colon).trim();
|
|
263
|
+
const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
264
|
+
if (k && v) current[k] = v;
|
|
265
|
+
} else {
|
|
266
|
+
const v = afterDash.replace(/^["']|["']$/g, '');
|
|
267
|
+
if (v) current['id'] = v;
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
} else if (current && trimmed.includes(':')) {
|
|
271
|
+
if (indent > curIndent) {
|
|
272
|
+
const colon = trimmed.indexOf(':');
|
|
273
|
+
const k = trimmed.slice(0, colon).trim();
|
|
274
|
+
const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
275
|
+
if (k && v) current[k] = v;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// if still collecting but line is not part of current item, skip?
|
|
280
|
+
if (collecting) continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// not collecting (or just exited) — look for att children
|
|
284
|
+
if (trimmed.startsWith('required:')) {
|
|
285
|
+
const v = trimmed.slice('required:'.length).trim().replace(/^["']|["']$/g, '');
|
|
286
|
+
if (v === 'true') required = true;
|
|
287
|
+
else if (v === 'false') required = false;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (trimmed.startsWith('trusted_keys:')) {
|
|
291
|
+
const after = trimmed.slice('trusted_keys:'.length).trim();
|
|
292
|
+
// handle inline empty `[]`
|
|
293
|
+
if (after === '[]') continue;
|
|
294
|
+
collecting = 'trusted';
|
|
295
|
+
collectBase = indent;
|
|
296
|
+
current = null;
|
|
297
|
+
// if inline list with one map on same line? e.g., trusted_keys: [{ id: "a", pubkey: "x" }]
|
|
298
|
+
// MVP not needed; empty case already handled
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (trimmed.startsWith('revoked:')) {
|
|
302
|
+
const after = trimmed.slice('revoked:'.length).trim();
|
|
303
|
+
if (after === '[]') continue;
|
|
304
|
+
collecting = 'revoked';
|
|
305
|
+
collectBase = indent;
|
|
306
|
+
current = null;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
flush();
|
|
311
|
+
if (required === undefined && trusted.length === 0 && revoked.length === 0) return null;
|
|
312
|
+
const out: { required?: boolean; trusted_keys?: Array<Record<string, string>>; revoked?: Array<Record<string, string>> } = {};
|
|
313
|
+
if (required !== undefined) out.required = required;
|
|
314
|
+
if (trusted.length) out.trusted_keys = trusted;
|
|
315
|
+
if (revoked.length) out.revoked = revoked;
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
100
319
|
export function loadPolicy(projectDir: string): MugiwaraPolicy | null {
|
|
101
320
|
for (const name of POLICY_FILES) {
|
|
102
321
|
const file = join(projectDir, name);
|
|
103
322
|
if (!existsSync(file)) continue;
|
|
104
|
-
|
|
323
|
+
const text = readFileSync(file, 'utf8');
|
|
324
|
+
const raw = parsePolicyYaml(text);
|
|
325
|
+
const extra = extractExtraSecretPatterns(text);
|
|
326
|
+
if (extra.length) {
|
|
327
|
+
if (!raw.integrity || typeof raw.integrity !== 'object' || Array.isArray(raw.integrity)) raw.integrity = {};
|
|
328
|
+
(raw.integrity as Record<string, unknown>).extra_secret_patterns = extra;
|
|
329
|
+
}
|
|
330
|
+
const att = extractAttestation(text);
|
|
331
|
+
if (att) {
|
|
332
|
+
if (!raw.attestation || typeof raw.attestation !== 'object' || Array.isArray(raw.attestation)) raw.attestation = {};
|
|
333
|
+
const a = raw.attestation as Record<string, unknown>;
|
|
334
|
+
if (att.required !== undefined) a.required = att.required;
|
|
335
|
+
if (att.trusted_keys) a.trusted_keys = att.trusted_keys;
|
|
336
|
+
if (att.revoked) a.revoked = att.revoked;
|
|
337
|
+
}
|
|
338
|
+
return normalize(raw);
|
|
105
339
|
}
|
|
106
340
|
return null;
|
|
107
341
|
}
|
|
@@ -128,6 +362,58 @@ function normalize(raw: Record<string, unknown>): MugiwaraPolicy {
|
|
|
128
362
|
}
|
|
129
363
|
const evidence = raw.evidence as Record<string, unknown> | undefined;
|
|
130
364
|
if (evidence && Array.isArray(evidence.required)) out.evidence = { required: strings(evidence.required) };
|
|
365
|
+
const integrity = raw.integrity as Record<string, unknown> | undefined;
|
|
366
|
+
if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
|
|
367
|
+
const arr = integrity.extra_secret_patterns as unknown[];
|
|
368
|
+
const cleaned: Array<{ pattern: string; label: string; severity?: 'block' | 'warn' }> = [];
|
|
369
|
+
for (const e of arr) {
|
|
370
|
+
if (!e || typeof e !== 'object') continue;
|
|
371
|
+
const rec = e as Record<string, unknown>;
|
|
372
|
+
if (typeof rec.pattern !== 'string' || typeof rec.label !== 'string') continue;
|
|
373
|
+
const sev = rec.severity === 'warn' ? 'warn' as const : rec.severity === 'block' ? 'block' as const : undefined;
|
|
374
|
+
const entry: { pattern: string; label: string; severity?: 'block' | 'warn' } = { pattern: rec.pattern, label: rec.label };
|
|
375
|
+
if (sev) entry.severity = sev;
|
|
376
|
+
cleaned.push(entry);
|
|
377
|
+
}
|
|
378
|
+
if (cleaned.length) out.integrity = { extra_secret_patterns: cleaned };
|
|
379
|
+
}
|
|
380
|
+
const att = raw.attestation as Record<string, unknown> | undefined;
|
|
381
|
+
if (att) {
|
|
382
|
+
const a: NonNullable<MugiwaraPolicy['attestation']> = {};
|
|
383
|
+
if (typeof att.required === 'boolean') a.required = att.required;
|
|
384
|
+
if (Array.isArray(att.trusted_keys)) {
|
|
385
|
+
const cleanedTk: Array<{ id: string; pubkey: string; added?: string }> = [];
|
|
386
|
+
for (const e of att.trusted_keys as unknown[]) {
|
|
387
|
+
if (!e || typeof e !== 'object') continue;
|
|
388
|
+
const rec = e as Record<string, unknown>;
|
|
389
|
+
if (typeof rec.id !== 'string' || typeof rec.pubkey !== 'string') continue;
|
|
390
|
+
if (!rec.id.trim() || !rec.pubkey.trim()) continue;
|
|
391
|
+
const entry: { id: string; pubkey: string; added?: string } = { id: rec.id.trim(), pubkey: rec.pubkey.trim() };
|
|
392
|
+
if (typeof rec.added === 'string' && rec.added.trim()) entry.added = rec.added.trim();
|
|
393
|
+
cleanedTk.push(entry);
|
|
394
|
+
}
|
|
395
|
+
if (cleanedTk.length) a.trusted_keys = cleanedTk;
|
|
396
|
+
}
|
|
397
|
+
if (Array.isArray(att.revoked)) {
|
|
398
|
+
const cleanedRv: Array<{ id: string; revoked?: string; reason?: string; pubkey?: string }> = [];
|
|
399
|
+
for (const e of att.revoked as unknown[]) {
|
|
400
|
+
if (!e || typeof e !== 'object') continue;
|
|
401
|
+
const rec = e as Record<string, unknown>;
|
|
402
|
+
if (typeof rec.id !== 'string' || !rec.id.trim()) continue;
|
|
403
|
+
const entry: { id: string; revoked?: string; reason?: string; pubkey?: string } = { id: rec.id.trim() };
|
|
404
|
+
if (typeof rec.revoked === 'string' && rec.revoked.trim()) entry.revoked = rec.revoked.trim();
|
|
405
|
+
if (typeof rec.reason === 'string' && rec.reason.trim()) entry.reason = rec.reason.trim();
|
|
406
|
+
if (typeof rec.pubkey === 'string' && rec.pubkey.trim()) entry.pubkey = rec.pubkey.trim();
|
|
407
|
+
cleanedRv.push(entry);
|
|
408
|
+
}
|
|
409
|
+
if (cleanedRv.length) a.revoked = cleanedRv;
|
|
410
|
+
}
|
|
411
|
+
if (a.required !== undefined || a.trusted_keys || a.revoked) out.attestation = a;
|
|
412
|
+
}
|
|
413
|
+
const harness = raw.harness as Record<string, unknown> | undefined;
|
|
414
|
+
if (harness && typeof harness.require_enforcement === 'boolean') {
|
|
415
|
+
out.harness = { require_enforcement: harness.require_enforcement };
|
|
416
|
+
}
|
|
131
417
|
return out;
|
|
132
418
|
}
|
|
133
419
|
|
|
@@ -154,3 +440,70 @@ export function matchedGlobs(paths: string[], globs: string[]): string[] {
|
|
|
154
440
|
export function effectiveThreshold(configured: number, policyValue: number | undefined): number {
|
|
155
441
|
return Math.max(configured, policyValue ?? 0);
|
|
156
442
|
}
|
|
443
|
+
|
|
444
|
+
// ── Harness enforcement (D8) ────────────────────────────────────────────────
|
|
445
|
+
// Only opencode has runtime write-scope enforcement; the other 11 harnesses
|
|
446
|
+
// are rules-based. `harness.require_enforcement: true` refuses to run where
|
|
447
|
+
// the harness is not enforced.
|
|
448
|
+
|
|
449
|
+
/** Detect current harness. Mirrors savepoint.sh logic. */
|
|
450
|
+
export function detectHarness(projectDir?: string): string {
|
|
451
|
+
const e = process.env;
|
|
452
|
+
const has = (v: string | undefined) => v !== undefined && v !== '';
|
|
453
|
+
if (has(e.CLAUDECODE) || has(e.CLAUDE_CODE_ENTRYPOINT) || (typeof e.ANTHROPIC_MODEL === 'string' && /claude/i.test(e.ANTHROPIC_MODEL))) return 'claude';
|
|
454
|
+
if (has(e.OPENCODE) || has(e.OPENCODE_TOKENS_FILE)) return 'opencode';
|
|
455
|
+
const candidates = [
|
|
456
|
+
projectDir ? join(projectDir, '.opencode', 'config.json') : null,
|
|
457
|
+
join(process.cwd(), '.opencode', 'config.json'),
|
|
458
|
+
].filter(Boolean) as string[];
|
|
459
|
+
for (const p of candidates) {
|
|
460
|
+
try { if (existsSync(p)) return 'opencode'; } catch { /* ignore */ }
|
|
461
|
+
}
|
|
462
|
+
if (has(e.CURSOR) || has(e.VSCODE_GIT_ASKPASS_NODE)) return 'cursor';
|
|
463
|
+
return 'unknown';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Only opencode is runtime-enforced. */
|
|
467
|
+
export function isEnforcedHarness(projectDir?: string): boolean {
|
|
468
|
+
return detectHarness(projectDir) === 'opencode';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Pure check: returns error message when policy requires enforcement but harness is rules-based, else null. */
|
|
472
|
+
export function getHarnessEnforcementError(projectDir: string): string | null {
|
|
473
|
+
let policy: MugiwaraPolicy | null;
|
|
474
|
+
try { policy = loadPolicy(projectDir); } catch (e) { throw e; }
|
|
475
|
+
if (!policy?.harness?.require_enforcement) return null;
|
|
476
|
+
if (isEnforcedHarness(projectDir)) return null;
|
|
477
|
+
const h = detectHarness(projectDir);
|
|
478
|
+
return `harness enforcement required but current harness is rules-based only \u2014 use opencode or set harness.require_enforcement:false (detected: ${h})`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Fail closed when policy requires enforcement but harness is rules-based. */
|
|
482
|
+
export function enforceHarnessPolicy(projectDir: string): void {
|
|
483
|
+
const err = getHarnessEnforcementError(projectDir);
|
|
484
|
+
if (!err) return;
|
|
485
|
+
console.error(`\u2717 ${err}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Alias for CLI import convenience. */
|
|
490
|
+
export const checkHarnessEnforcement = enforceHarnessPolicy;
|
|
491
|
+
|
|
492
|
+
// ── Lane-aware gates (T3) ───────────────────────────────────────────────────
|
|
493
|
+
// Direct → minimal (typecheck+build only), lean → +validate-content, standard+
|
|
494
|
+
// → +evals/retrieval/conformance/benchmark. Single source for gate-selftest
|
|
495
|
+
// and the franky-gates skill doc. Full = 12 steps, direct = 3 steps.
|
|
496
|
+
export const GATE_STEPS_BY_LANE: Record<string, string[]> = {
|
|
497
|
+
direct: ['build-hooks:check', 'typecheck', 'build'],
|
|
498
|
+
lean: ['build-hooks:check', 'typecheck', 'build', 'validate-content', 'lane-base', 'check-doc-links'],
|
|
499
|
+
standard: ['build-hooks:check', 'typecheck', 'build', 'validate-content', 'lane-base', 'check-doc-links', 'test:coverage', 'coverage-gate', 'verify-install'],
|
|
500
|
+
full: ['build-hooks:check', 'typecheck', 'build', 'validate-content', 'lane-base', 'check-doc-links', 'test:coverage', 'coverage-gate', 'verify-install', 'run-evals', 'retrieval-eval', 'conformance'],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
export function gatesForLane(lane: string): string[] {
|
|
504
|
+
return GATE_STEPS_BY_LANE[lane] ?? GATE_STEPS_BY_LANE.full;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function isLaneAwareGateStep(step: string, lane: string): boolean {
|
|
508
|
+
return gatesForLane(lane).includes(step);
|
|
509
|
+
}
|
package/src/posture.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/posture.ts
|
|
2
|
+
// Mugiwara Phase B — deterministic posture selection matrix. Maps existing
|
|
3
|
+
// lane/risk/dependency/context/governor inputs to an execution posture with a
|
|
4
|
+
// concrete reason + evidence refs — never an opaque score. Pure, testable.
|
|
5
|
+
//
|
|
6
|
+
// Posture is independent of control mode (guided/semi/auto). The governor's
|
|
7
|
+
// verdict can pause but never silently changes mode or crew roles.
|
|
8
|
+
|
|
9
|
+
export type Posture =
|
|
10
|
+
| 'inline-sequential'
|
|
11
|
+
| 'inline-batched'
|
|
12
|
+
| 'parallel-workers'
|
|
13
|
+
| 'context-relief'
|
|
14
|
+
| 'phase-isolated'
|
|
15
|
+
| 'team-scoped';
|
|
16
|
+
|
|
17
|
+
export type GovernorVerdict = 'normal' | 'avoid' | 'stop';
|
|
18
|
+
|
|
19
|
+
export type PostureInput = {
|
|
20
|
+
lane: 'direct' | 'lean' | 'standard' | 'full' | 'spike';
|
|
21
|
+
risk: 'low' | 'medium' | 'high';
|
|
22
|
+
independent_tasks: number;
|
|
23
|
+
order_dependent: boolean;
|
|
24
|
+
context_pressure: boolean;
|
|
25
|
+
team_members: number;
|
|
26
|
+
phases: number;
|
|
27
|
+
plan_lines: number;
|
|
28
|
+
governor: GovernorVerdict;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PostureResult = {
|
|
32
|
+
posture: Posture;
|
|
33
|
+
pause: boolean;
|
|
34
|
+
reason: string;
|
|
35
|
+
evidence_refs: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function selectPosture(input: PostureInput): PostureResult {
|
|
39
|
+
// stop verdict → safe pause, keep prior topology (recorded, never silent)
|
|
40
|
+
if (input.governor === 'stop') {
|
|
41
|
+
return {
|
|
42
|
+
posture: 'inline-sequential',
|
|
43
|
+
pause: true,
|
|
44
|
+
reason: 'governor stop — pause safely, keep inline; state + continue emitted',
|
|
45
|
+
evidence_refs: ['governor circuit-breaker', 'state.json'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (input.team_members > 1) {
|
|
49
|
+
return {
|
|
50
|
+
posture: 'team-scoped',
|
|
51
|
+
pause: false,
|
|
52
|
+
reason: `${input.team_members} team members with non-overlapping scope`,
|
|
53
|
+
evidence_refs: ['plan ownership map'],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (input.phases > 3 || input.plan_lines > 1500) {
|
|
57
|
+
return {
|
|
58
|
+
posture: 'phase-isolated',
|
|
59
|
+
pause: false,
|
|
60
|
+
reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
|
|
61
|
+
evidence_refs: ['plan.md', 'large-campaign-subplan.md'],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (input.context_pressure && input.order_dependent) {
|
|
65
|
+
return {
|
|
66
|
+
posture: 'context-relief',
|
|
67
|
+
pause: false,
|
|
68
|
+
reason: 'context pressure with ordered dependent tasks — one worker at a time, order preserved',
|
|
69
|
+
evidence_refs: ['state context metrics', 'remaining task order'],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (input.independent_tasks >= 2) {
|
|
73
|
+
return {
|
|
74
|
+
posture: 'parallel-workers',
|
|
75
|
+
pause: false,
|
|
76
|
+
reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
|
|
77
|
+
evidence_refs: ['Nami dependency map', 'work-governor delegation verdict'],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
posture: 'inline-sequential',
|
|
82
|
+
pause: false,
|
|
83
|
+
reason: 'no parallel/phase/team/relief trigger — default inline in plan order',
|
|
84
|
+
evidence_refs: ['triage route', 'lane'],
|
|
85
|
+
};
|
|
86
|
+
}
|
package/src/provenance.ts
CHANGED
|
@@ -75,16 +75,34 @@ export function renderProvenanceMd(note: string, sha: string | null): string {
|
|
|
75
75
|
return lines.join('\n') + '\n';
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export function attachGitNote(projectDir: string, branch: string, note: string): { sha: string } | null {
|
|
78
|
+
export function attachGitNote(projectDir: string, branch: string, note: string, baseSha?: string): { sha: string; count: number } | null {
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
const range = baseSha ? `${baseSha}..${branch}` : branch;
|
|
81
|
+
let shas: string[] = [];
|
|
81
82
|
try {
|
|
82
|
-
|
|
83
|
+
const raw = git(projectDir, ['rev-list', range]);
|
|
84
|
+
shas = raw.split('\n').filter(Boolean);
|
|
83
85
|
} catch {
|
|
84
|
-
|
|
86
|
+
// rev-list failed (unknown baseSha/branch) — fall back to single head
|
|
87
|
+
shas = [];
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
// ponytail: cap at 200 commits, fallback to head-only beyond
|
|
90
|
+
if (shas.length > 200) {
|
|
91
|
+
console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
|
|
92
|
+
shas = [];
|
|
93
|
+
}
|
|
94
|
+
let targets = shas;
|
|
95
|
+
if (!targets.length) {
|
|
96
|
+
try {
|
|
97
|
+
targets = [git(projectDir, ['rev-parse', '--verify', branch])];
|
|
98
|
+
} catch {
|
|
99
|
+
targets = [git(projectDir, ['rev-parse', 'HEAD'])];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const sha of targets) {
|
|
103
|
+
git(projectDir, ['notes', '--ref=mugiwara', 'add', '-f', '-m', note, sha]);
|
|
104
|
+
}
|
|
105
|
+
return { sha: targets[0], count: targets.length };
|
|
88
106
|
} catch {
|
|
89
107
|
// not a repo, detached oddities, or notes disabled — degrade honestly
|
|
90
108
|
return null;
|
|
@@ -104,13 +122,15 @@ export function blamePath(projectDir: string, path: string): string {
|
|
|
104
122
|
const note = git(projectDir, ['notes', '--ref=mugiwara', 'show', sha]);
|
|
105
123
|
return `${path} @ ${sha.slice(0, 7)}\n${note}`;
|
|
106
124
|
} catch {
|
|
107
|
-
return `${path} @ ${sha.slice(0, 7)}\
|
|
125
|
+
return `${path} @ ${sha.slice(0, 7)}\nno per-commit note — see .mugiwara/missions/<m>/provenance.md`;
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/** Closure hook: write provenance.md + attach the git note. */
|
|
112
|
-
export function writeProvenance(projectDir: string, missionDir: string, state: NoteSource): void {
|
|
130
|
+
export function writeProvenance(projectDir: string, missionDir: string, state: NoteSource & { base_sha?: string }, baseSha?: string): void {
|
|
113
131
|
const note = buildNote(state);
|
|
114
|
-
const
|
|
132
|
+
const resolvedBase = baseSha ?? (typeof (state as Record<string, unknown>).base_sha === 'string' ? (state as Record<string, unknown>).base_sha as string : undefined);
|
|
133
|
+
const cleanBase = resolvedBase && resolvedBase !== 'unknown' ? resolvedBase : undefined;
|
|
134
|
+
const attached = attachGitNote(projectDir, state.branch, note, cleanBase);
|
|
115
135
|
writeFileSync(join(missionDir, 'provenance.md'), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
116
136
|
}
|