@sleepy_panda_srl/portulan 0.2.0 → 0.4.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/README.md +15 -4
- package/cli/README.md +61 -6
- package/cli/advisory.mjs +167 -53
- package/cli/cache-risk.mjs +69 -0
- package/cli/comments.mjs +8 -5
- package/cli/compile.mjs +781 -130
- package/cli/context.mjs +204 -43
- package/cli/doctor.mjs +248 -31
- package/cli/drills.mjs +56 -9
- package/cli/finish.md +34 -0
- package/cli/finish.mjs +8 -67
- package/cli/form.mjs +34 -18
- package/cli/frontmatter.mjs +39 -0
- package/cli/gate.mjs +14 -12
- package/cli/guidance-files.mjs +335 -0
- package/cli/handoff-policy.mjs +76 -0
- package/cli/html-entities.mjs +2130 -0
- package/cli/import-files.mjs +15 -0
- package/cli/index.mjs +77 -24
- package/cli/init-preview.mjs +553 -0
- package/cli/init.mjs +261 -46
- package/cli/instructions-journal.mjs +1185 -0
- package/cli/instructions.mjs +332 -57
- package/cli/ledger.mjs +230 -40
- package/cli/librarian.mjs +103 -19
- package/cli/persona-routing.mjs +209 -0
- package/cli/plugin-lint.mjs +77 -56
- package/cli/portulan.mjs +1 -1
- package/cli/recipe-output.mjs +91 -0
- package/cli/recipe-runner.md +68 -0
- package/cli/recipe-runner.mjs +131 -0
- package/cli/recipe-set.mjs +12 -3
- package/cli/review-meter.mjs +14 -9
- package/cli/ritual-contexts.mjs +103 -0
- package/cli/sessions.mjs +2 -1
- package/cli/spend-analysis.mjs +111 -0
- package/cli/spend-offer.md +53 -0
- package/cli/spend-offer.mjs +93 -0
- package/cli/stop-diagnostics.mjs +114 -0
- package/cli/stop-gate.mjs +133 -90
- package/cli/stop-policy.mjs +14 -0
- package/cli/symbols.mjs +55 -8
- package/cli/telemetry.mjs +66 -17
- package/cli/upgrade.mjs +62 -44
- package/cli/vendor.mjs +207 -125
- package/core/operating/context.md +401 -18
- package/core/operating/evolution.md +3 -2
- package/core/operating/loop.md +9 -4
- package/core/operating/sessions.md +6 -15
- package/core/personas/README.md +75 -0
- package/core/personas/implementer.md +3 -0
- package/core/personas/librarian.md +3 -0
- package/core/personas/reviewer.md +3 -0
- package/core/skills/clarify/SKILL.md +1 -1
- package/core/skills/consolidate/SKILL.md +1 -1
- package/core/templates/handoff.md +6 -2
- package/evals/README.md +23 -7
- package/evals/ab/personas/README.md +111 -0
- package/evals/ab/personas/acquisition.json +330 -0
- package/evals/ab/personas/bindings/implementer.txt +43 -0
- package/evals/ab/personas/bindings/librarian.txt +40 -0
- package/evals/ab/personas/bindings/reviewer.txt +41 -0
- package/evals/ab/personas/corpus.json +69 -0
- package/evals/ab/personas/grader.mjs +171 -0
- package/evals/ab/personas/implementer.json +774 -0
- package/evals/ab/personas/implementer.md +22 -0
- package/evals/ab/personas/librarian.json +758 -0
- package/evals/ab/personas/librarian.md +22 -0
- package/evals/ab/personas/protocol/continuation.mjs.txt +284 -0
- package/evals/ab/personas/protocol/first-outcome.mjs.txt +227 -0
- package/evals/ab/personas/protocol/first-probe.mjs.txt +172 -0
- package/evals/ab/personas/protocol/publisher.mjs.txt +126 -0
- package/evals/ab/personas/protocol/second-probe.mjs.txt +193 -0
- package/evals/ab/personas/repairs/first-control-answer.txt +23 -0
- package/evals/ab/personas/repairs/grader-before-decode-repair.mjs.txt +143 -0
- package/evals/ab/personas/reviewer.json +758 -0
- package/evals/ab/personas/reviewer.md +22 -0
- package/evals/releases/0.3.0.json +150 -0
- package/evals/releases/0.3.0.md +68 -0
- package/evals/releases/0.4.0.json +154 -0
- package/evals/releases/0.4.0.md +69 -0
- package/package.json +7 -2
- package/packs/.claude-plugin/plugin.json +2 -1
- package/packs/README.md +7 -4
- package/packs/rituals/checkpoints/README.md +35 -0
- package/packs/rituals/checkpoints/pack.json +3 -2
- package/packs/tools/README.md +8 -8
- package/packs/tools/local-llm/README.md +176 -0
- package/packs/tools/local-llm/pack.json +32 -0
- package/packs/tools/local-llm/personas/local-worker.md +44 -0
- package/packs/tools/local-llm/runner/local-llm.mjs +1779 -0
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +88 -0
- package/packs/tools/local-llm/verify/README.md +18 -0
- package/spec/README.md +37 -3
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -11
- package/spec/migrations/0011-persona-bindings-compiled.mjs +23 -0
- package/spec/migrations/README.md +6 -0
- package/spec/pack.schema.json +12 -3
- package/spec/slots.md +272 -123
- package/spec/workspace.schema.json +67 -11
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Frozen semantic predicates and declared cost indexes for the three persona comparisons.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
export const CORPUS = JSON.parse(fs.readFileSync(new URL("./corpus.json", import.meta.url), "utf8"));
|
|
8
|
+
const CLASSES = ["uncached", "written5m", "written1h", "writtenUnknown", "read", "output"];
|
|
9
|
+
const inputClasses = CLASSES.filter(k => k !== "output");
|
|
10
|
+
const plain = v => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
11
|
+
const exactKeys = (v, keys) => plain(v) && Object.keys(v).length === keys.length && keys.every(k => Object.hasOwn(v, k));
|
|
12
|
+
const sameSet = (values, expected) => Array.isArray(values) && values.length === expected.length && new Set(values).size === expected.length && expected.every(v => values.includes(v));
|
|
13
|
+
|
|
14
|
+
function parsed(answer) {
|
|
15
|
+
if (typeof answer !== "string") return null;
|
|
16
|
+
const text = answer.trim().replace(/^```(?:json)?\s*\n([\s\S]*?)\n```$/, "$1");
|
|
17
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function codeCases(kind) {
|
|
21
|
+
if (kind === "lower-bound") {
|
|
22
|
+
const arrays = [[], [0], [1, 1], [-4, -1, 0, 0, 7], [1, 2, 2, 3], [0, 10, 20, 30, 40]];
|
|
23
|
+
return arrays.flatMap(a => [-10, -4, -1, 0, 1, 2, 3, 7, 10, 40, 100].map(x => ({ args: [a, x], want: a.findIndex(v => v >= x) < 0 ? a.length : a.findIndex(v => v >= x) })));
|
|
24
|
+
}
|
|
25
|
+
if (kind === "dedup") return [
|
|
26
|
+
{ args: [[]], want: 0 },
|
|
27
|
+
{ args: [[{ id: "a", tokens: 2 }, { id: "a", tokens: 5 }, { id: "b", tokens: 3 }, { id: "a", tokens: 1 }]], want: 8 },
|
|
28
|
+
{ args: [[null, [], {}, { id: "", tokens: 3 }, { id: 2, tokens: 5 }, { id: "x", tokens: -1 }, { id: "x", tokens: 1.5 }, { id: "x", tokens: "9" }, { id: "x", tokens: 9007199254740992 }]], want: 0 },
|
|
29
|
+
{ args: [[{ id: "__proto__", tokens: 5 }, { id: "constructor", tokens: 7 }, { id: "__proto__", tokens: 3 }, { id: "zero", tokens: 0 }]], want: 12 },
|
|
30
|
+
];
|
|
31
|
+
if (kind === "containment") {
|
|
32
|
+
const pairs = [
|
|
33
|
+
["/work/pkg", "/work/pkg", true], ["/work/pkg/", "/work/pkg/a", true],
|
|
34
|
+
["/work/pkg", "/work/pkg-other/a", false], ["/work/pkg", "/work/pkg/a/../../outside", false],
|
|
35
|
+
["/work/pkg", "/work/pkg/a/../b", true], ["/work/pkg", "/work/pkg/a/../../pkg/b", true],
|
|
36
|
+
["/work//pkg/.", "/work/pkg/b", true], ["/", "/a/../b", true],
|
|
37
|
+
["/work/pkg", "work/pkg/a", false], ["work/pkg", "/work/pkg", false],
|
|
38
|
+
[null, "/work/pkg", false], ["/work/pkg", 2, false],
|
|
39
|
+
];
|
|
40
|
+
return pairs.map(([root, target, want]) => ({ args: [root, target], want }));
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EXECUTOR = `
|
|
46
|
+
import fs from 'node:fs';
|
|
47
|
+
import vm from 'node:vm';
|
|
48
|
+
const {code,cases}=JSON.parse(fs.readFileSync(0,'utf8'));
|
|
49
|
+
const context=vm.createContext({}, {codeGeneration:{strings:false,wasm:false}});
|
|
50
|
+
const snapshot=value=>{
|
|
51
|
+
if(value===null||typeof value!=='object')return {value};
|
|
52
|
+
return {value,proto:Object.getPrototypeOf(value),fields:Reflect.ownKeys(value).map(key=>[key,Object.getOwnPropertyDescriptor(value,key),snapshot(Object.getOwnPropertyDescriptor(value,key).value)])};
|
|
53
|
+
};
|
|
54
|
+
const unchanged=s=>{
|
|
55
|
+
if(!s.fields)return true;
|
|
56
|
+
if(Object.getPrototypeOf(s.value)!==s.proto||Reflect.ownKeys(s.value).length!==s.fields.length)return false;
|
|
57
|
+
return s.fields.every(([key,before,child])=>{const after=Object.getOwnPropertyDescriptor(s.value,key);return after&&after.value===before.value&&after.writable===before.writable&&after.enumerable===before.enumerable&&after.configurable===before.configurable&&unchanged(child);});
|
|
58
|
+
};
|
|
59
|
+
let ok=false;
|
|
60
|
+
try {
|
|
61
|
+
const candidate=new vm.Script('('+code+'\\n)').runInContext(context,{timeout:100});
|
|
62
|
+
if(typeof candidate==='function'&&!Object.hasOwn(context,'candidate')){
|
|
63
|
+
Object.defineProperty(context,'candidate',{value:candidate,writable:false,configurable:false});
|
|
64
|
+
ok=cases.every(c=>{
|
|
65
|
+
const args=new vm.Script('('+JSON.stringify(c.args)+')').runInContext(context,{timeout:100});
|
|
66
|
+
const before=snapshot(args);
|
|
67
|
+
context.args=args;
|
|
68
|
+
const actual=new vm.Script('candidate(...args)').runInContext(context,{timeout:100});
|
|
69
|
+
return typeof actual===typeof c.want&&actual===c.want&&unchanged(before);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
process.stdout.write(ok?'PASS':'FAIL');
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
function codePass(code, cases) {
|
|
77
|
+
if (typeof code !== "string" || code.length > 16000 || !/^(?:async\s+)?(?:function\b|\(?[\w\s,]*\)?\s*=>)/.test(code.trim())) return false;
|
|
78
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "portulan-persona-grade-"));
|
|
79
|
+
try {
|
|
80
|
+
const result = spawnSync(process.execPath, ["--max-old-space-size=48", "--input-type=module", "-e", EXECUTOR], {
|
|
81
|
+
input: JSON.stringify({ code, cases }), cwd: dir, env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
|
|
82
|
+
encoding: "utf8", timeout: 2000, maxBuffer: 65536,
|
|
83
|
+
});
|
|
84
|
+
return !result.error && result.status === 0 && result.stdout === "PASS";
|
|
85
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function reviewerPass(kind, value) {
|
|
89
|
+
if (!exactKeys(value, ["findings"]) || !Array.isArray(value.findings)) return false;
|
|
90
|
+
if (kind === "review-clean") return value.findings.length === 0;
|
|
91
|
+
const lines = kind === "review-verdict" ? [2] : [2, 5];
|
|
92
|
+
if (!sameSet(value.findings.map(f => f?.line), lines)) return false;
|
|
93
|
+
return value.findings.every(f => {
|
|
94
|
+
if (!exactKeys(f, ["line", "input"]) || !plain(f.input)) return false;
|
|
95
|
+
if (kind === "review-verdict") {
|
|
96
|
+
if (!exactKeys(f.input, ["status"])) return false;
|
|
97
|
+
const s = f.input.status;
|
|
98
|
+
return (s === 0 ? "green" : "red") !== (s === 0 ? "green" : s === 1 ? "red" : "could-not-run");
|
|
99
|
+
}
|
|
100
|
+
if (f.line === 5) return exactKeys(f.input, ["status"]) && (f.input.status !== 1) !== (f.input.status === 0);
|
|
101
|
+
if (!exactKeys(f.input, ["root", "target"])) return false;
|
|
102
|
+
const { root, target } = f.input;
|
|
103
|
+
if (typeof root !== "string" || typeof target !== "string" || !root.startsWith("/") || !target.startsWith("/") || path.posix.normalize(root) !== root || path.posix.normalize(target) !== target) return false;
|
|
104
|
+
const canonical = root.length > 1 ? root.replace(/\/+$/, "") : root;
|
|
105
|
+
const wanted = canonical === "/" || target === canonical || target.startsWith(canonical + "/");
|
|
106
|
+
return target.startsWith(root) !== wanted;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function librarianPass(kind, value) {
|
|
111
|
+
if (!exactKeys(value, ["draft", "actions"]) || value.draft !== true || !Array.isArray(value.actions)) return false;
|
|
112
|
+
const [action] = value.actions;
|
|
113
|
+
if (kind === "library-duplicate") return value.actions.length === 1 && exactKeys(action, ["kind", "ids", "obligations", "provenance", "retire_when"]) && action.kind === "merge" && sameSet(action.ids, ["a", "b"]) &&
|
|
114
|
+
sameSet(action.obligations, ["exit-0-is-green", "exit-2-is-unavailable"]) && sameSet(action.provenance, ["https://example.org/incidents/one", "https://example.org/incidents/two"]) && sameSet(action.retire_when, ["runner-replaced", "unavailable-is-tested"]);
|
|
115
|
+
if (kind === "library-conflict") return value.actions.length === 1 && exactKeys(action, ["kind", "ids", "decision"]) && action.kind === "escalate" && action.decision === "unresolved" && sameSet(action.ids, ["a", "b"]);
|
|
116
|
+
if (kind === "library-sealed") {
|
|
117
|
+
if (value.actions.length !== 2) return false;
|
|
118
|
+
const sealed = value.actions.find(a => a?.id === "sealed"), open = value.actions.find(a => a?.id === "public");
|
|
119
|
+
return exactKeys(sealed, ["kind", "id", "owner"]) && sealed.kind === "request_revalidation" && sealed.owner === "Example Team" &&
|
|
120
|
+
exactKeys(open, ["kind", "id", "evidence"]) && open.kind === "propose_retirement" && open.evidence === "https://example.org/evidence/parser-removal";
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function grade(task, answer) {
|
|
126
|
+
const value = parsed(answer);
|
|
127
|
+
const cases = codeCases(task.grader);
|
|
128
|
+
const ok = cases ? codePass(value?.code, cases) : task.role === "reviewer" ? reviewerPass(task.grader, value) : librarianPass(task.grader, value);
|
|
129
|
+
return { ok, check: task.grader };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function price(requests, prices = CORPUS.prices) {
|
|
133
|
+
if (!Array.isArray(requests) || !requests.length) throw Error("usage needs at least one completed request");
|
|
134
|
+
for (const request of requests) for (const key of CLASSES) if (!Number.isSafeInteger(request[key]) || request[key] < 0) throw Error(`invalid usage ${key}`);
|
|
135
|
+
for (const key of [...CLASSES, "coldFirstInput"]) if (!(Number.isFinite(prices[key]) && prices[key] > 0)) throw Error(`invalid price ${key}`);
|
|
136
|
+
const cost = request => CLASSES.reduce((sum, key) => sum + request[key] * prices[key], 0);
|
|
137
|
+
const first = requests[0];
|
|
138
|
+
const observed = requests.reduce((sum, request) => sum + cost(request), 0);
|
|
139
|
+
const original = inputClasses.reduce((sum, key) => sum + first[key] * prices[key], 0);
|
|
140
|
+
const normalized = observed - original + inputClasses.reduce((sum, key) => sum + first[key], 0) * prices.coldFirstInput;
|
|
141
|
+
if (![observed, normalized].every(Number.isFinite)) throw Error("cost index overflows");
|
|
142
|
+
return { requests: requests.length, observed, normalized };
|
|
143
|
+
}
|