@zuzuucodes/cli 1.5.0 → 1.6.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/bin/zuzuu.mjs +12 -3
- package/package.json +1 -1
- package/web-app/dist/auth.js +91 -0
- package/web-app/dist/server.js +16 -79
- package/web-app/dist/zuzuu-cli.js +124 -0
- package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +46 -116
- package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
- package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
- package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
- package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
- package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
- package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
- package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
- package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
- package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
- package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
- package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
- package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
- package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
- package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
- package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
- package/web-app/web-dist/assets/{cssMode-Dx3ub8Pk.js → cssMode-ByQBaInt.js} +1 -1
- package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
- package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
- package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
- package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
- package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
- package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
- package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
- package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
- package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
- package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
- package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
- package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
- package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
- package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
- package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
- package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
- package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-DM6oHc7c.js → htmlMode-BtdIDgA2.js} +1 -1
- package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
- package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
- package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
- package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
- package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
- package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
- package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
- package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
- package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
- package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
- package/web-app/web-dist/assets/{jsonMode-DflaUwqW.js → jsonMode-BG32YnTY.js} +1 -1
- package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
- package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
- package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
- package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
- package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
- package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
- package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
- package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
- package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
- package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
- package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
- package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-wbBeb0oN.js → monaco-setup-DM3A5_VI.js} +3 -3
- package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
- package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
- package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
- package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
- package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
- package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
- package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
- package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
- package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
- package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
- package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
- package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
- package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
- package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
- package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
- package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
- package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
- package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
- package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
- package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
- package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
- package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
- package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
- package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
- package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
- package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
- package/web-app/web-dist/assets/{tsMode-DRwkDcoK.js → tsMode-BndVBac5.js} +1 -1
- package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
- package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
- package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
- package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
- package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
- package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
- package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
- package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
- package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
- package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
- package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
- package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
- package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
- package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
- package/web-app/web-dist/index.html +6 -3
- package/zuzuu/actions/trail.mjs +1 -1
- package/zuzuu/commands/act.mjs +1 -1
- package/zuzuu/commands/capture.mjs +2 -2
- package/zuzuu/commands/code.mjs +2 -2
- package/zuzuu/commands/digest.mjs +2 -2
- package/zuzuu/commands/distill.mjs +15 -16
- package/zuzuu/commands/doctor.mjs +39 -4
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/faculty.mjs +102 -19
- package/zuzuu/commands/generation.mjs +3 -4
- package/zuzuu/commands/hook.mjs +7 -7
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +5 -4
- package/zuzuu/commands/knowledge.mjs +1 -1
- package/zuzuu/commands/migrations/home.mjs +96 -0
- package/zuzuu/commands/migrations/index.mjs +48 -0
- package/zuzuu/commands/{migrate.mjs → migrations/items.mjs} +34 -246
- package/zuzuu/commands/migrations/proposals.mjs +100 -0
- package/zuzuu/commands/proposals.mjs +131 -0
- package/zuzuu/commands/review.mjs +13 -227
- package/zuzuu/commands/session.mjs +8 -2
- package/zuzuu/commands/sessions.mjs +159 -0
- package/zuzuu/commands/status.mjs +3 -3
- package/zuzuu/commands/trace.mjs +1 -1
- package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
- package/zuzuu/{store.mjs → core/store.mjs} +1 -1
- package/zuzuu/digest/compose.mjs +96 -0
- package/zuzuu/eval/score.mjs +14 -1
- package/zuzuu/faculties/actions/index.mjs +283 -0
- package/zuzuu/faculties/guardrails/index.mjs +320 -0
- package/zuzuu/faculties/instructions/index.mjs +288 -0
- package/zuzuu/faculties/knowledge/index.mjs +185 -0
- package/zuzuu/{memory/adapter.mjs → faculties/memory/index.mjs} +37 -9
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +11 -5
- package/zuzuu/faculty/module.mjs +74 -0
- package/zuzuu/faculty/pending.mjs +63 -0
- package/zuzuu/faculty/registry.mjs +204 -18
- package/zuzuu/faculty/render.mjs +59 -0
- package/zuzuu/faculty/trail.mjs +1 -1
- package/zuzuu/{guardrails.mjs → guardrails/engine.mjs} +1 -1
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +12 -2
- package/zuzuu/live/live-store.mjs +2 -2
- package/zuzuu/live/reconcile.mjs +2 -2
- package/zuzuu/sessions/git.mjs +47 -0
- package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
- package/web-app/web-dist/assets/DiffTab-BpGp1akx.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CqbVacUZ.js +0 -1
- package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
- package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
- package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
- package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
- package/web-app/web-dist/assets/dist-C6R6xoyX.js +0 -153
- package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
- package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
- package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
- package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
- package/web-app/web-dist/assets/index-DHpC851f.js +0 -268
- package/web-app/web-dist/assets/index-O-t1gyMG.css +0 -2
- package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
- package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
- package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
- package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
- package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
- package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
- package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
- package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
- package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
- package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
- package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
- package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
- package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
- package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
- package/zuzuu/actions/adapter.mjs +0 -122
- package/zuzuu/digest.mjs +0 -154
- package/zuzuu/faculty/generation.mjs +0 -398
- package/zuzuu/guardrails/adapter.mjs +0 -103
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/miners/actions.mjs +0 -112
- package/zuzuu/miners/guardrails.mjs +0 -176
- package/zuzuu/miners/instructions.mjs +0 -157
- package/zuzuu/miners/knowledge.mjs +0 -25
- package/zuzuu/miners/memory.mjs +0 -27
- package/zuzuu/miners/registry.mjs +0 -31
- /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
- /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
- /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// zuzuu/faculties/guardrails/index.mjs — the Guardrails faculty module.
|
|
2
|
+
//
|
|
3
|
+
// Consolidates the adapter (rule proposals → envelope items), the miner
|
|
4
|
+
// (repeated destructive-command failures → ask-only rules, WS5-T3), the digest
|
|
5
|
+
// section and the GATE hook (the only faculty with one — the enforced
|
|
6
|
+
// PreToolUse engine) behind the Faculty Module contract. The rule engine
|
|
7
|
+
// itself stays in zuzuu/guardrails/engine.mjs (the gate's hot path imports it
|
|
8
|
+
// directly too); this module is the contract face.
|
|
9
|
+
//
|
|
10
|
+
// A guardrails proposal payload is a single rule record:
|
|
11
|
+
// { id, action: deny|ask|allow, tool, pattern, reason, body? }
|
|
12
|
+
//
|
|
13
|
+
// MANDATORY MINER SAFETY PROPERTIES (enforced in aggregate):
|
|
14
|
+
// 1. action is ALWAYS 'ask' — never 'deny'. Auto-proposed rules only escalate
|
|
15
|
+
// to the human prompt, they never hard-block.
|
|
16
|
+
// 2. Patterns are LITERAL-ESCAPED from the observed command — never a broad/
|
|
17
|
+
// free regex. escapeRegex() handles this.
|
|
18
|
+
// 3. Cross-session corroboration required — a destructive command must fail
|
|
19
|
+
// ≥minFailures (default 3) times across ≥minSessions (default 2) DISTINCT
|
|
20
|
+
// sessions. A single session — no matter how many failures — produces
|
|
21
|
+
// NOTHING.
|
|
22
|
+
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { writeFacultyItem } from '../../faculty/items.mjs';
|
|
25
|
+
import { deriveTitle } from '../../faculty/envelope.mjs';
|
|
26
|
+
import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../../faculty/proposal.mjs';
|
|
27
|
+
import { loadRules, evaluate } from '../../guardrails/engine.mjs';
|
|
28
|
+
import { slugify } from '../../knowledge/items.mjs';
|
|
29
|
+
|
|
30
|
+
const name = 'guardrails';
|
|
31
|
+
|
|
32
|
+
export const manifest = {
|
|
33
|
+
id: 'guardrails',
|
|
34
|
+
title: 'Guardrails',
|
|
35
|
+
tagline: 'what NOT to do — enforced rules on every tool call',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
contract: 1,
|
|
38
|
+
kinds: ['rule'],
|
|
39
|
+
itemsDir: 'items',
|
|
40
|
+
schema: 'schema.json',
|
|
41
|
+
hooks: { miner: true, digest: true, eval: false, gate: true },
|
|
42
|
+
ui: { icon: 'shield', accent: 'danger', teaching: 'Hard rules enforced by the zuzuu gate on every tool call — a refusal here is policy, not preference.' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const VALID_ACTIONS = new Set(['deny', 'ask', 'allow']);
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// adapter contract
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ingest a raw rule object. Pass-through: rule fields are the payload.
|
|
53
|
+
* @param {string} agentDir
|
|
54
|
+
* @param {object} raw — expected shape: { id, action, tool, pattern, reason }
|
|
55
|
+
* or { payload: { ... } } from the spine
|
|
56
|
+
*/
|
|
57
|
+
function ingest(_agentDir, raw) {
|
|
58
|
+
const payload = raw?.payload ?? raw ?? {};
|
|
59
|
+
return { payload, analysis: {}, dedupeKey: payload.id };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate a rule payload.
|
|
64
|
+
* @returns {{ok:boolean, errors:string[], warnings:string[]}}
|
|
65
|
+
*/
|
|
66
|
+
export function validate(_agentDir, payload) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (!payload?.id || typeof payload.id !== 'string' || !payload.id.trim()) {
|
|
69
|
+
errors.push('rule id is required (non-empty string slug)');
|
|
70
|
+
}
|
|
71
|
+
if (!VALID_ACTIONS.has(payload?.action)) {
|
|
72
|
+
errors.push(`action must be one of deny|ask|allow (got '${payload?.action}')`);
|
|
73
|
+
}
|
|
74
|
+
if (!payload?.tool || typeof payload.tool !== 'string') {
|
|
75
|
+
errors.push('tool is required (exact tool name or \'*\')');
|
|
76
|
+
}
|
|
77
|
+
if (typeof payload?.pattern !== 'string' || !payload.pattern) {
|
|
78
|
+
errors.push('pattern is required (a non-empty regex string)');
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
new RegExp(payload.pattern); // eslint-disable-line no-new
|
|
82
|
+
} catch (e) {
|
|
83
|
+
errors.push(`pattern does not compile as a RegExp: ${e.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!payload?.reason || !String(payload.reason).trim()) {
|
|
87
|
+
errors.push('reason is required (non-empty)');
|
|
88
|
+
}
|
|
89
|
+
return { ok: errors.length === 0, errors, warnings: [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Apply an approved rule proposal: write the envelope item (upsert by id).
|
|
94
|
+
* @returns {{ok:boolean, action:string, itemIds:string[]}}
|
|
95
|
+
*/
|
|
96
|
+
function apply(agentDir, proposal) {
|
|
97
|
+
const rule = proposal?.payload ?? {};
|
|
98
|
+
const id = rule.id;
|
|
99
|
+
writeFacultyItem(agentDir, {
|
|
100
|
+
id,
|
|
101
|
+
faculty: name,
|
|
102
|
+
kind: 'rule',
|
|
103
|
+
title: deriveTitle(rule.reason, id),
|
|
104
|
+
status: 'active',
|
|
105
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
106
|
+
provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
|
|
107
|
+
payload: { action: rule.action, tool: rule.tool || '*', pattern: rule.pattern, reason: rule.reason },
|
|
108
|
+
body: rule.body ?? '',
|
|
109
|
+
});
|
|
110
|
+
return { ok: true, action: `added rule ${id}`, itemIds: [id] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const applyProposal = apply;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render a rule proposal for the human gate.
|
|
117
|
+
* @returns {{line:string, card:string}}
|
|
118
|
+
*/
|
|
119
|
+
function render(proposal) {
|
|
120
|
+
const r = proposal?.payload ?? {};
|
|
121
|
+
const summary = `${r.action ?? '?'} ${r.tool ?? '*'} /${r.pattern ?? ''}/ — ${r.reason ?? ''}`;
|
|
122
|
+
return {
|
|
123
|
+
line: `${r.id ?? ''} [rule] ${summary}`,
|
|
124
|
+
card: summary,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const adapter = { name, ingest, validate, apply, render };
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// gate hook — the ONLY faculty with one; same fail-open law as the engine
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Evaluate one tool call against this home's rule items.
|
|
136
|
+
* @param {string} agentDir
|
|
137
|
+
* @param {{tool:string, input:any}} toolCall
|
|
138
|
+
* @returns {null | {action:'deny'|'ask'|'allow', rule:string, reason:string}}
|
|
139
|
+
* null = no rule matched / engine trouble → host's normal flow (fail-open)
|
|
140
|
+
*/
|
|
141
|
+
export function gate(agentDir, toolCall) {
|
|
142
|
+
try {
|
|
143
|
+
const loaded = loadRules(join(agentDir, 'guardrails'));
|
|
144
|
+
if (!loaded.ok) return null;
|
|
145
|
+
return evaluate(loaded.rules, toolCall);
|
|
146
|
+
} catch {
|
|
147
|
+
return null; // fail open
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// miner (WS5-T3 — destructive-failure clusters → ask-only rules, unchanged)
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Escape all regex metacharacters in `s` so that `new RegExp(escapeRegex(s))`
|
|
157
|
+
* matches exactly the string `s` and nothing broader.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} s
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
export function escapeRegex(s) {
|
|
163
|
+
// Standard set of regex metacharacters that need escaping.
|
|
164
|
+
return String(s).replace(/[.*+?^${}()|[\]\\\/\-]/g, '\\$&');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Normalise a command string (trim + collapse whitespace). */
|
|
168
|
+
const norm = (cmd) => String(cmd).trim().replace(/\s+/g, ' ').slice(0, 200);
|
|
169
|
+
|
|
170
|
+
/** Derive a guardrails-miner id for a command. */
|
|
171
|
+
function guardId(cmd) {
|
|
172
|
+
return 'guard-' + slugify(cmd, 50);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Ids of live rule items (guardrails/items/*.md); absent/unreadable → none. */
|
|
176
|
+
function liveRuleIds(agentDir) {
|
|
177
|
+
try {
|
|
178
|
+
return new Set(loadRules(join(agentDir, 'guardrails')).rules.map((r) => r.id));
|
|
179
|
+
} catch {
|
|
180
|
+
return new Set();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Group destructiveFailures by normalised command; emit a candidate ONLY when
|
|
186
|
+
* both the occurrence count and distinct-session count meet their thresholds.
|
|
187
|
+
*
|
|
188
|
+
* SAFETY: a single-session cluster, no matter how large, produces NOTHING.
|
|
189
|
+
*
|
|
190
|
+
* @param {Array<{sessionId:string, destructiveFailures:{cmd:string,tool:string}[]}>} sessions
|
|
191
|
+
* @param {object} opts
|
|
192
|
+
* @param {number} [opts.minFailures=3] min total failures across all sessions
|
|
193
|
+
* @param {number} [opts.minSessions=2] min distinct sessions with ≥1 failure each
|
|
194
|
+
* @returns {Array<{payload:{id,action,tool,pattern,reason}, evidence:{occurrences,sessions}}>}
|
|
195
|
+
*/
|
|
196
|
+
export function aggregate(sessions, { minFailures = 3, minSessions = 2 } = {}) {
|
|
197
|
+
// cmd (normalized) → { count: number, sessions: Set<sessionId>, tool: string }
|
|
198
|
+
const stats = new Map();
|
|
199
|
+
|
|
200
|
+
for (const s of sessions) {
|
|
201
|
+
if (!Array.isArray(s.destructiveFailures)) continue;
|
|
202
|
+
for (const { cmd, tool } of s.destructiveFailures) {
|
|
203
|
+
const key = norm(cmd);
|
|
204
|
+
const st = stats.get(key) ?? { count: 0, sessions: new Set(), tool: tool ?? 'Bash' };
|
|
205
|
+
st.count++;
|
|
206
|
+
st.sessions.add(s.sessionId);
|
|
207
|
+
// Keep first observed tool name (they should all be 'Bash' for destructive cmds).
|
|
208
|
+
stats.set(key, st);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const candidates = [];
|
|
213
|
+
for (const [cmd, st] of stats) {
|
|
214
|
+
// SAFETY: enforce BOTH thresholds — cross-session gate is the key one.
|
|
215
|
+
if (st.count < minFailures) continue;
|
|
216
|
+
if (st.sessions.size < minSessions) continue; // ← single-session always rejected here
|
|
217
|
+
|
|
218
|
+
const id = guardId(cmd);
|
|
219
|
+
const pattern = escapeRegex(cmd);
|
|
220
|
+
const tool = st.tool ?? 'Bash';
|
|
221
|
+
|
|
222
|
+
candidates.push({
|
|
223
|
+
payload: {
|
|
224
|
+
id,
|
|
225
|
+
// SAFETY: ALWAYS 'ask', never 'deny'.
|
|
226
|
+
action: 'ask',
|
|
227
|
+
tool,
|
|
228
|
+
pattern,
|
|
229
|
+
reason: `auto-proposed: '${cmd}' failed repeatedly across sessions — confirm before running`,
|
|
230
|
+
},
|
|
231
|
+
evidence: {
|
|
232
|
+
occurrences: st.count,
|
|
233
|
+
sessions: st.sessions.size,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return candidates;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Write a guardrails proposal into .zuzuu/guardrails/proposals/ for each candidate.
|
|
243
|
+
* Idempotent:
|
|
244
|
+
* - skips if a guardrails proposal with the same payload.id already exists
|
|
245
|
+
* - skips if a live rule item (guardrails/items/<id>.md) already exists
|
|
246
|
+
* - skips if the id is already resolved in proposals/archive/ — a rejection
|
|
247
|
+
* is remembered; re-distilling never resurrects it
|
|
248
|
+
*
|
|
249
|
+
* The proposals flow through `zuzuu review` → guardrails adapter on approval.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} agentDir
|
|
252
|
+
* @param {ReturnType<typeof aggregate>} aggregated
|
|
253
|
+
* @returns {number} count of new proposals written
|
|
254
|
+
*/
|
|
255
|
+
export function propose(agentDir, aggregated) {
|
|
256
|
+
// Load existing proposals (ids already pending).
|
|
257
|
+
const existing = listProposals(agentDir, 'guardrails');
|
|
258
|
+
const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
|
|
259
|
+
|
|
260
|
+
// Live rule items (ids already applied).
|
|
261
|
+
const rulesIds = liveRuleIds(agentDir);
|
|
262
|
+
|
|
263
|
+
let count = 0;
|
|
264
|
+
for (const c of aggregated) {
|
|
265
|
+
const { payload, evidence } = c;
|
|
266
|
+
|
|
267
|
+
// Idempotent: skip if already proposed or already a live rule.
|
|
268
|
+
if (existingIds.has(payload.id)) continue;
|
|
269
|
+
if (rulesIds.has(payload.id)) continue;
|
|
270
|
+
|
|
271
|
+
const proposal = makeProposal({
|
|
272
|
+
faculty: 'guardrails',
|
|
273
|
+
kind: 'rule',
|
|
274
|
+
source: 'distill',
|
|
275
|
+
payload,
|
|
276
|
+
evidence,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// A rejection is remembered: never resurrect an archive-resolved id.
|
|
280
|
+
if (isArchivedResolved(agentDir, 'guardrails', proposal.id)) continue;
|
|
281
|
+
|
|
282
|
+
writeProposal(agentDir, proposal);
|
|
283
|
+
count++;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return count;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const miner = { faculty: name, aggregate, propose };
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// digest section (moved from the pre-module digest, byte-identical output)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The Guardrails digest section: rule count + the enforcement reminder.
|
|
297
|
+
* @param {string} agentDir
|
|
298
|
+
* @returns {{lines: string[], data: object}}
|
|
299
|
+
*/
|
|
300
|
+
export function digestSection(agentDir) {
|
|
301
|
+
let guardrails;
|
|
302
|
+
try {
|
|
303
|
+
const loaded = loadRules(join(agentDir, 'guardrails'));
|
|
304
|
+
guardrails = { ok: loaded.ok, count: loaded.ok ? loaded.rules.length : 0, skipped: loaded.skipped?.length ?? 0 };
|
|
305
|
+
} catch {
|
|
306
|
+
guardrails = { ok: false, count: 0, skipped: 0 };
|
|
307
|
+
}
|
|
308
|
+
const lines = ['## Guardrails'];
|
|
309
|
+
lines.push(guardrails.count ? `${guardrails.count} rule(s) — the enforced gate is on; refusals are policy.` : 'no rules configured.');
|
|
310
|
+
return { lines, data: guardrails };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// session signals (the observability surface — `zuzuu session inspect`)
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/** Counts of the mined-signal superset slices this faculty grows from. */
|
|
318
|
+
export function sessionSignals(signals = {}) {
|
|
319
|
+
return { destructiveFailures: signals.destructiveFailures?.length ?? 0 };
|
|
320
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// zuzuu/faculties/instructions/index.mjs — the Instructions faculty module.
|
|
2
|
+
//
|
|
3
|
+
// Consolidates the adapter (steering-amendment proposals → envelope items),
|
|
4
|
+
// the miner (recurring corrective user turns → amendment proposals, WS5-T4)
|
|
5
|
+
// and the digest section (the steering text the agent grounds on) behind the
|
|
6
|
+
// Faculty Module contract.
|
|
7
|
+
//
|
|
8
|
+
// An instructions proposal payload is a steering amendment:
|
|
9
|
+
// { id?, text } — a line or paragraph of steering
|
|
10
|
+
//
|
|
11
|
+
// apply: writes the amendment as a Faculty Standard envelope item under
|
|
12
|
+
// .zuzuu/instructions/items/<id>.md (kind: amendment; body = the text).
|
|
13
|
+
// The pinned steering itself lives at items/steering.md; future
|
|
14
|
+
// amendments are MORE items, never edits to steering. Idempotent: a
|
|
15
|
+
// text already present in any instructions item is not duplicated.
|
|
16
|
+
|
|
17
|
+
import { listFacultyItems, writeFacultyItem } from '../../faculty/items.mjs';
|
|
18
|
+
import { deriveTitle } from '../../faculty/envelope.mjs';
|
|
19
|
+
import { makeProposal, writeProposal, listProposals, isArchivedResolved } from '../../faculty/proposal.mjs';
|
|
20
|
+
import { slugify } from '../../knowledge/items.mjs';
|
|
21
|
+
|
|
22
|
+
const name = 'instructions';
|
|
23
|
+
|
|
24
|
+
export const manifest = {
|
|
25
|
+
id: 'instructions',
|
|
26
|
+
title: 'Instructions',
|
|
27
|
+
tagline: 'who to BE — steering and project conventions',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
contract: 1,
|
|
30
|
+
kinds: ['steering', 'amendment'],
|
|
31
|
+
itemsDir: 'items',
|
|
32
|
+
schema: 'schema.json',
|
|
33
|
+
hooks: { miner: true, digest: true, eval: false, gate: false },
|
|
34
|
+
ui: { icon: 'compass', accent: 'warning', teaching: 'The pinned steering the agent is told at session start; corrections graduate into amendments here.' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// adapter contract
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ingest a raw amendment object. Pass-through: the payload IS the amendment.
|
|
43
|
+
*/
|
|
44
|
+
function ingest(_agentDir, raw) {
|
|
45
|
+
const payload = raw?.payload ?? raw ?? {};
|
|
46
|
+
return { payload, analysis: {} };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate an amendment payload.
|
|
51
|
+
* @returns {{ok:boolean, errors:string[], warnings:string[]}}
|
|
52
|
+
*/
|
|
53
|
+
export function validate(_agentDir, payload) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (!payload?.text || !String(payload.text).trim()) {
|
|
56
|
+
errors.push('text is required (non-empty steering amendment)');
|
|
57
|
+
}
|
|
58
|
+
return { ok: errors.length === 0, errors, warnings: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Apply an approved amendment: write an amendment item (idempotent on
|
|
63
|
+
* identical text — won't duplicate steering already present in any item).
|
|
64
|
+
* @returns {{ok:boolean, action:string, itemIds:string[]}}
|
|
65
|
+
*/
|
|
66
|
+
function apply(agentDir, proposal) {
|
|
67
|
+
const text = String(proposal?.payload?.text ?? '').trim();
|
|
68
|
+
|
|
69
|
+
// Idempotence: skip if the exact text already lives in an instructions item
|
|
70
|
+
const { items } = listFacultyItems(agentDir, 'instructions');
|
|
71
|
+
if (items.some((i) => String(i.body ?? '').includes(text))) {
|
|
72
|
+
return { ok: true, action: 'amended instructions (already present)', itemIds: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const id = proposal?.payload?.id || slugify(text, 50);
|
|
76
|
+
writeFacultyItem(agentDir, {
|
|
77
|
+
id,
|
|
78
|
+
faculty: name,
|
|
79
|
+
kind: 'amendment',
|
|
80
|
+
title: deriveTitle(text, id),
|
|
81
|
+
status: 'active',
|
|
82
|
+
created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
83
|
+
provenance: Array.isArray(proposal?.provenance) ? proposal.provenance : [],
|
|
84
|
+
payload: { scope: 'project' },
|
|
85
|
+
body: text,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { ok: true, action: 'amended instructions', itemIds: [id] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const applyProposal = apply;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Render an amendment proposal for the human gate.
|
|
95
|
+
* @returns {{line:string, card:string}}
|
|
96
|
+
*/
|
|
97
|
+
function render(proposal) {
|
|
98
|
+
const text = proposal?.payload?.text ?? '';
|
|
99
|
+
const preview = text.slice(0, 80).replace(/\n/g, ' ');
|
|
100
|
+
return {
|
|
101
|
+
line: `[amendment] ${preview}`,
|
|
102
|
+
card: text,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const adapter = { name, ingest, validate, apply, render };
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// miner (WS5-T4 — recurring corrective turns → amendment proposals, unchanged)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalise a correction text for grouping:
|
|
114
|
+
* lowercase, collapse whitespace, truncate to 200 chars.
|
|
115
|
+
*
|
|
116
|
+
* v1 grouping: near-identical normalised text (exact key match). Simple and
|
|
117
|
+
* deterministic; a fuzzy grouper can be earned later.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} text
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function normText(text) {
|
|
123
|
+
return String(text).toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Derive a proposal id fragment from a normalised text key.
|
|
128
|
+
* Keep it stable, short, and filesystem-safe.
|
|
129
|
+
*/
|
|
130
|
+
function instrId(normKey) {
|
|
131
|
+
// Use a slugified version of the first 60 chars of the normalised text,
|
|
132
|
+
// prefixed to make collisions with other faculties impossible.
|
|
133
|
+
const slug = normKey.slice(0, 60).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50) || 'instr';
|
|
134
|
+
return 'instr-' + slug;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Group correctionTurns from mined sessions; propose when a similar correction
|
|
139
|
+
* recurs across ≥minSessions (default 2) distinct sessions.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array<{sessionId:string, correctionTurns:{text:string}[]}>} sessions
|
|
142
|
+
* @param {object} opts
|
|
143
|
+
* @param {number} [opts.minSessions=2] min distinct sessions with the same normalised correction
|
|
144
|
+
* @returns {Array<{payload:{text:string}, evidence:{occurrences:number, sessions:number}}>}
|
|
145
|
+
*/
|
|
146
|
+
export function aggregate(sessions, { minSessions = 2 } = {}) {
|
|
147
|
+
// normalised text → { count, sessions: Set<sessionId>, rawText: string }
|
|
148
|
+
const stats = new Map();
|
|
149
|
+
|
|
150
|
+
for (const s of sessions) {
|
|
151
|
+
if (!Array.isArray(s.correctionTurns)) continue;
|
|
152
|
+
// Track distinct normalised texts per session to avoid double-counting
|
|
153
|
+
// the same session for the same correction text.
|
|
154
|
+
const seenInSession = new Set();
|
|
155
|
+
for (const { text } of s.correctionTurns) {
|
|
156
|
+
const key = normText(text);
|
|
157
|
+
if (!key) continue;
|
|
158
|
+
const st = stats.get(key) ?? { count: 0, sessions: new Set(), rawText: text };
|
|
159
|
+
st.count++;
|
|
160
|
+
st.sessions.add(s.sessionId);
|
|
161
|
+
seenInSession.add(key);
|
|
162
|
+
stats.set(key, st);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const candidates = [];
|
|
167
|
+
for (const [, st] of stats) {
|
|
168
|
+
if (st.sessions.size < minSessions) continue;
|
|
169
|
+
|
|
170
|
+
// Phrase the raw correction as an instruction for the steering amendment.
|
|
171
|
+
// The corrective turn text already reads like user guidance; use it directly
|
|
172
|
+
// (trimmed to 500 chars to match mineTranscript's cap).
|
|
173
|
+
const amendmentText = st.rawText.slice(0, 500).trim();
|
|
174
|
+
const id = instrId(normText(amendmentText));
|
|
175
|
+
|
|
176
|
+
candidates.push({
|
|
177
|
+
payload: { id, text: amendmentText },
|
|
178
|
+
evidence: { occurrences: st.count, sessions: st.sessions.size },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return candidates;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Write an instructions proposal into .zuzuu/instructions/proposals/ for each
|
|
187
|
+
* candidate.
|
|
188
|
+
*
|
|
189
|
+
* Idempotent:
|
|
190
|
+
* - skips if an instructions proposal with the same derived id already exists
|
|
191
|
+
* - skips if the text is already present in an instructions item (steering
|
|
192
|
+
* or a prior amendment)
|
|
193
|
+
* - skips if the id is already resolved in proposals/archive/ — a rejection
|
|
194
|
+
* is remembered; re-distilling never resurrects it
|
|
195
|
+
*
|
|
196
|
+
* @param {string} agentDir
|
|
197
|
+
* @param {ReturnType<typeof aggregate>} aggregated
|
|
198
|
+
* @returns {number} count of new proposals written
|
|
199
|
+
*/
|
|
200
|
+
export function propose(agentDir, aggregated) {
|
|
201
|
+
// Collect ids of existing pending proposals for this faculty.
|
|
202
|
+
const existing = listProposals(agentDir, 'instructions');
|
|
203
|
+
const existingIds = new Set(existing.map((p) => p.payload?.id).filter(Boolean));
|
|
204
|
+
|
|
205
|
+
// Read the instructions items (steering + amendments) to skip applied text.
|
|
206
|
+
let appliedText = '';
|
|
207
|
+
try {
|
|
208
|
+
appliedText = listFacultyItems(agentDir, 'instructions').items.map((i) => i.body ?? '').join('\n');
|
|
209
|
+
} catch { appliedText = ''; }
|
|
210
|
+
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (const c of aggregated) {
|
|
213
|
+
const { payload, evidence } = c;
|
|
214
|
+
|
|
215
|
+
// Idempotent: skip if already proposed.
|
|
216
|
+
if (existingIds.has(payload.id)) continue;
|
|
217
|
+
|
|
218
|
+
// Idempotent: skip if text already present in an instructions item.
|
|
219
|
+
if (appliedText.includes(payload.text)) continue;
|
|
220
|
+
|
|
221
|
+
const proposal = makeProposal({
|
|
222
|
+
faculty: 'instructions',
|
|
223
|
+
kind: 'block',
|
|
224
|
+
source: 'distill',
|
|
225
|
+
payload,
|
|
226
|
+
evidence,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// A rejection is remembered: never resurrect an archive-resolved id.
|
|
230
|
+
if (isArchivedResolved(agentDir, 'instructions', proposal.id)) continue;
|
|
231
|
+
|
|
232
|
+
writeProposal(agentDir, proposal);
|
|
233
|
+
count++;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return count;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const miner = { faculty: name, aggregate, propose };
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// digest section (moved from the pre-module digest, byte-identical output)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
const PLACEHOLDER_MARK = '<!-- Fill in:';
|
|
246
|
+
|
|
247
|
+
const INTERVIEW = [
|
|
248
|
+
'Project steering is empty. Before substantive work, interview your human',
|
|
249
|
+
'(what is this project, its conventions, its priorities), draft the steering item',
|
|
250
|
+
'.zuzuu/instructions/items/steering.md from their answers, and get their approval.',
|
|
251
|
+
].join(' ');
|
|
252
|
+
|
|
253
|
+
/** Read the instructions items (steering first, then amendments); classify
|
|
254
|
+
* empty vs steering text. Items are Faculty Standard envelopes (W24). */
|
|
255
|
+
function readInstructions(agentDir) {
|
|
256
|
+
let items = [];
|
|
257
|
+
try {
|
|
258
|
+
items = listFacultyItems(agentDir, 'instructions').items;
|
|
259
|
+
} catch { /* missing or unreadable → treat as empty */ }
|
|
260
|
+
// steering pins the top; amendments follow in id order (already sorted)
|
|
261
|
+
items.sort((a, b) => (a.kind === 'steering' ? -1 : 1) - (b.kind === 'steering' ? -1 : 1));
|
|
262
|
+
const bodies = items
|
|
263
|
+
.map((i) => String(i.body ?? ''))
|
|
264
|
+
.map((raw) => (raw.includes(PLACEHOLDER_MARK) ? '' : raw.replace(/^#.*$/gm, '').trim() && raw.trim()))
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
const text = bodies.join('\n\n');
|
|
267
|
+
return { empty: !text, text };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The Instructions digest section: the pinned steering (or the interview
|
|
272
|
+
* prompt when empty). Always renders — grounding starts here.
|
|
273
|
+
* @param {string} agentDir
|
|
274
|
+
* @returns {{lines: string[], data: object}}
|
|
275
|
+
*/
|
|
276
|
+
export function digestSection(agentDir) {
|
|
277
|
+
const instr = readInstructions(agentDir);
|
|
278
|
+
return { lines: ['## Instructions', instr.empty ? INTERVIEW : instr.text], data: instr };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// session signals (the observability surface — `zuzuu session inspect`)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/** Counts of the mined-signal superset slices this faculty grows from. */
|
|
286
|
+
export function sessionSignals(signals = {}) {
|
|
287
|
+
return { correctionTurns: signals.correctionTurns?.length ?? 0 };
|
|
288
|
+
}
|