@sleepy_panda_srl/portulan 0.3.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 +55 -3
- package/cli/advisory.mjs +167 -53
- package/cli/cache-risk.mjs +69 -0
- package/cli/comments.mjs +8 -5
- package/cli/compile.mjs +492 -146
- package/cli/context.mjs +204 -43
- package/cli/doctor.mjs +211 -33
- package/cli/drills.mjs +32 -7
- 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 +123 -37
- package/cli/librarian.mjs +103 -19
- package/cli/persona-routing.mjs +209 -0
- package/cli/plugin-lint.mjs +77 -56
- 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/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 +19 -12
- package/cli/upgrade.mjs +62 -44
- package/cli/vendor.mjs +207 -125
- package/core/operating/context.md +368 -60
- 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 +52 -10
- package/core/personas/implementer.md +3 -0
- package/core/personas/librarian.md +3 -0
- package/core/personas/reviewer.md +3 -0
- package/core/templates/handoff.md +6 -2
- 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.4.0.json +154 -0
- package/evals/releases/0.4.0.md +69 -0
- package/package.json +6 -2
- package/packs/rituals/checkpoints/README.md +35 -0
- package/packs/rituals/checkpoints/pack.json +3 -2
- package/packs/tools/local-llm/README.md +46 -6
- package/packs/tools/local-llm/runner/local-llm.mjs +127 -13
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +26 -2
- package/spec/README.md +37 -4
- 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 +262 -117
- package/spec/workspace.schema.json +67 -11
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Optional project import probes.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
// A denied read or a loop of links is not absence: hiding it could report less than the host loads.
|
|
5
|
+
const ABSENT = new Set(["ENOENT", "ENOTDIR", "ENAMETOOLONG", "ERR_INVALID_ARG_VALUE"]);
|
|
6
|
+
export const isAbsentImportError = (error) => ABSENT.has(error?.code);
|
|
7
|
+
|
|
8
|
+
export function importTargetIsFile(file) {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(file).isFile();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isAbsentImportError(error)) return false;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/cli/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { pathToFileURL } from "node:url";
|
|
|
14
14
|
|
|
15
15
|
import { resolvePack, rootPlan, shadowedCopy } from "./compile.mjs";
|
|
16
16
|
import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
|
|
17
|
+
import { HandoffError, handoffPolicy, handoffOverages, handoffTokens } from "./handoff-policy.mjs";
|
|
17
18
|
|
|
18
19
|
/** Raised when `index` cannot run, or cannot judge honestly. Always exit 2, never 1. */
|
|
19
20
|
export class IndexError extends Error {
|
|
@@ -23,7 +24,7 @@ export class IndexError extends Error {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const KNOWN_SPECS = new Set(["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"]);
|
|
27
|
+
const KNOWN_SPECS = new Set(["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16"]);
|
|
27
28
|
|
|
28
29
|
// `doctor` skips the same name, so the two count the same records.
|
|
29
30
|
const NOT_A_RECORD = new Set(["README.md"]);
|
|
@@ -51,12 +52,17 @@ export function headingOf(source) {
|
|
|
51
52
|
return first?.match(/^#[ \t]+(.+?)[ \t]*$/)?.[1] ?? null;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
+
/** Valid leading filename date, or null. */
|
|
55
56
|
export function dateOf(filename) {
|
|
56
57
|
const m = filename.match(/^(\d{4})-(\d{2})-(\d{2})-/);
|
|
57
58
|
return m ? realDay(m[1], m[2], m[3]) : null;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
export function eligibleHandoffDate(filename, today) {
|
|
62
|
+
const date = dateOf(filename);
|
|
63
|
+
return date !== null && date <= nextDay(today) ? date : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
function realDay(y, mo, d) {
|
|
61
67
|
const at = new Date(0);
|
|
62
68
|
// Not `Date.UTC`, which reads a year from 0 to 99 as 1900 to 1999.
|
|
@@ -127,8 +133,20 @@ export function readStore(dir, workspace) {
|
|
|
127
133
|
return { records, bytes };
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
function regularHandoff(seriesDir, slot, file) {
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.lstatSync(path.join(seriesDir, file));
|
|
139
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
140
|
+
throw new IndexError(`${path.join(slot, file)} is not a regular file: a handoff is a file of its own, never a link or a directory`);
|
|
141
|
+
}
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
if (cause instanceof IndexError) throw cause;
|
|
144
|
+
throw new IndexError(`cannot read the handoff ${path.join(slot, file)} — ${cause.code ?? cause.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The series' metadata, newest first; bodies stay with the reader that needs them. */
|
|
149
|
+
export function handoffEntries(dir, workspace) {
|
|
132
150
|
const slot = workspace?.slots?.handoffs;
|
|
133
151
|
if (!slot) {
|
|
134
152
|
throw new IndexError(
|
|
@@ -141,23 +159,31 @@ export function readHandoffs(dir, workspace) {
|
|
|
141
159
|
// `readdirSync` follows a link, so a link on the series path is refused before it is listed.
|
|
142
160
|
refuseLinks(dir, slot, seriesDir, "a handoff series is read where its path says");
|
|
143
161
|
const files = listSeries(seriesDir, slot, "handoff series").reverse();
|
|
162
|
+
return files.map(file => {
|
|
163
|
+
regularHandoff(seriesDir, slot, file);
|
|
164
|
+
return { file, date: dateOf(file) };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A workspace's handoff series, `{ records, bytes }`, newest first; a record's `date` or `heading` is null where it has none. */
|
|
169
|
+
export function readHandoffs(dir, workspace) {
|
|
170
|
+
const entries = handoffEntries(dir, workspace), slot = workspace.slots.handoffs;
|
|
171
|
+
const seriesDir = path.resolve(dir, slot);
|
|
144
172
|
const records = [];
|
|
145
173
|
let bytes = 0;
|
|
146
174
|
|
|
147
|
-
for (const file of
|
|
175
|
+
for (const { file, date } of entries) {
|
|
148
176
|
let source;
|
|
149
177
|
try {
|
|
150
|
-
|
|
151
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
152
|
-
throw new IndexError(`${path.join(slot, file)} is not a regular file: a handoff is a file of its own, never a link or a directory`);
|
|
153
|
-
}
|
|
178
|
+
regularHandoff(seriesDir, slot, file);
|
|
154
179
|
source = fs.readFileSync(path.join(seriesDir, file), "utf8");
|
|
155
180
|
} catch (cause) {
|
|
156
181
|
if (cause instanceof IndexError) throw cause;
|
|
157
182
|
throw new IndexError(`cannot read the handoff ${path.join(slot, file)} — ${cause.code ?? cause.message}`);
|
|
158
183
|
}
|
|
159
|
-
|
|
160
|
-
|
|
184
|
+
const recordBytes = Buffer.byteLength(source);
|
|
185
|
+
bytes += recordBytes;
|
|
186
|
+
records.push({ file, date, heading: headingOf(source), bytes: recordBytes });
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
return { records, bytes };
|
|
@@ -358,13 +384,17 @@ export function renderHandoffIndex(workspace, series) {
|
|
|
358
384
|
const posix = (p) => p.split(path.sep).join(path.posix.sep);
|
|
359
385
|
const from = path.posix.dirname(posix(indexPath));
|
|
360
386
|
|
|
387
|
+
let policy;
|
|
388
|
+
try { policy = handoffPolicy(workspace); }
|
|
389
|
+
catch (error) { throw new IndexError(error.message); }
|
|
361
390
|
const header = [
|
|
362
391
|
`# Handoff index — ${workspace.name}`,
|
|
363
392
|
"",
|
|
364
393
|
`> Generated from \`${slot}\` by \`node cli/index.mjs\`. Do not edit by hand: it is regenerated`,
|
|
365
394
|
`> and byte-compared, so a hand-edit survives exactly until the next run.`,
|
|
366
|
-
|
|
367
|
-
|
|
395
|
+
...(policy.window === null ? [
|
|
396
|
+
`> ${series.records.length} handoff(s), newest first. No index budget declared; the full series is listed.`,
|
|
397
|
+
] : [`> ${series.records.length} handoff(s), newest first. Index budget: ${policy.window} tokens at ${policy.ratio} bytes per token.`]),
|
|
368
398
|
"",
|
|
369
399
|
];
|
|
370
400
|
|
|
@@ -373,7 +403,17 @@ export function renderHandoffIndex(workspace, series) {
|
|
|
373
403
|
return `- ${r.date} · [${r.heading}](${href})`;
|
|
374
404
|
});
|
|
375
405
|
|
|
376
|
-
return [...header, ...entries].join("\n") + "\n";
|
|
406
|
+
if (policy.window === null) return [...header, ...entries].join("\n") + "\n";
|
|
407
|
+
const text = (count, budget = policy.window) => [...header.map(line => line.replace(`Index budget: ${policy.window} tokens`, `Index budget: ${budget} tokens`)), ...entries.slice(0, count), "", `> ${entries.length - count} handoff(s) omitted; all remain in \`${slot}\`, filenames ordered by date.`].join("\n") + "\n";
|
|
408
|
+
const overhead = text(0);
|
|
409
|
+
if (handoffTokens(Buffer.byteLength(overhead), policy.ratio) > policy.window) {
|
|
410
|
+
let minimum = 1;
|
|
411
|
+
while (handoffTokens(Buffer.byteLength(text(0, minimum)), policy.ratio) > minimum) minimum = handoffTokens(Buffer.byteLength(text(0, minimum)), policy.ratio);
|
|
412
|
+
throw new IndexError(`handoffs.index.budget.tokens is too small: at least ${minimum} tokens hold the complete header and omission notice`);
|
|
413
|
+
}
|
|
414
|
+
let count = 0;
|
|
415
|
+
while (count < entries.length && handoffTokens(Buffer.byteLength(text(count + 1)), policy.ratio) <= policy.window) count++;
|
|
416
|
+
return text(count);
|
|
377
417
|
}
|
|
378
418
|
|
|
379
419
|
export function renderScopeIndex(workspace, series) {
|
|
@@ -430,7 +470,7 @@ export function inspect(dir, { write = false, packRoots: extraRoots, discoverPac
|
|
|
430
470
|
].join("");
|
|
431
471
|
|
|
432
472
|
const memory = judgeMemory(dir, workspace, { write, fail, note, today, remedyFlags });
|
|
433
|
-
const handoffs = judgeHandoffs(dir, workspace, { write, fail, remedyFlags });
|
|
473
|
+
const handoffs = judgeHandoffs(dir, workspace, { write, fail, note, today, remedyFlags });
|
|
434
474
|
const scopes = judgeScopes(dir, workspace, { write, fail, packRoots: extraRoots, discoverPacks, remedyFlags });
|
|
435
475
|
|
|
436
476
|
return {
|
|
@@ -558,26 +598,32 @@ function judgeMemory(dir, workspace, { write, fail, note, today, remedyFlags = "
|
|
|
558
598
|
return { declared: true, path: indexPath, expected, budgets };
|
|
559
599
|
}
|
|
560
600
|
|
|
561
|
-
function judgeHandoffs(dir, workspace, { write, fail, remedyFlags = "" }) {
|
|
601
|
+
function judgeHandoffs(dir, workspace, { write, fail, note, today, remedyFlags = "" }) {
|
|
562
602
|
const declaredPath = workspace.handoffs?.index?.path;
|
|
563
|
-
|
|
603
|
+
let policy;
|
|
604
|
+
try { policy = handoffPolicy(workspace); }
|
|
605
|
+
catch (error) {
|
|
606
|
+
if (!(error instanceof HandoffError)) throw error;
|
|
607
|
+
throw new IndexError(error.message);
|
|
608
|
+
}
|
|
609
|
+
if (!workspace.slots?.handoffs && !workspace.handoffs) return { declared: false, path: null, expected: null };
|
|
564
610
|
|
|
565
|
-
const indexPath = siteOutside(dir, declaredPath, workspace.slots?.handoffs, "series");
|
|
611
|
+
const indexPath = declaredPath ? siteOutside(dir, declaredPath, workspace.slots?.handoffs, "series") : null;
|
|
566
612
|
const series = readHandoffs(dir, workspace);
|
|
567
613
|
|
|
568
614
|
let broken = 0;
|
|
569
615
|
for (const r of series.records) {
|
|
570
616
|
const where = path.join(workspace.slots.handoffs, r.file);
|
|
571
|
-
if (r.
|
|
617
|
+
if (eligibleHandoffDate(r.file, today) === null) {
|
|
572
618
|
broken += 1;
|
|
573
619
|
fail(
|
|
574
620
|
"handoffs",
|
|
575
621
|
"date",
|
|
576
|
-
`${where}
|
|
622
|
+
`${where} must lead with a valid YYYY-MM-DD date no more than a day after ${today} (UTC). ` +
|
|
577
623
|
"core/operating/loop.md fixes the form as `YYYY-MM-DD-{slug}.md` — rename the file",
|
|
578
624
|
);
|
|
579
625
|
}
|
|
580
|
-
if (r.heading === null) {
|
|
626
|
+
if (declaredPath && r.heading === null) {
|
|
581
627
|
broken += 1;
|
|
582
628
|
fail(
|
|
583
629
|
"handoffs",
|
|
@@ -587,6 +633,12 @@ function judgeHandoffs(dir, workspace, { write, fail, remedyFlags = "" }) {
|
|
|
587
633
|
);
|
|
588
634
|
}
|
|
589
635
|
}
|
|
636
|
+
const overages = handoffOverages(series, policy);
|
|
637
|
+
for (const record of overages.bound) fail("handoffs", "budget",
|
|
638
|
+
`${path.join(workspace.slots.handoffs, record.file)} is ${record.tokens} tokens at ${policy.ratio} bytes per token, against a cap of ${policy.cap} — over by ${record.tokens - policy.cap}. ` +
|
|
639
|
+
"Compress it or move facts to their owning records and link them; keep each decision and its why. Never split, retire, raise the cap or move the cutoff to repair this breach");
|
|
640
|
+
if (overages.historical.length) note("handoffs", `${overages.historical.length} handoff(s) on or before cutoff ${policy.cutoff} exceed ${policy.cap} tokens: reported, never railed`);
|
|
641
|
+
if (!declaredPath) return { declared: true, path: null, expected: null, count: series.records.length, kept: false };
|
|
590
642
|
if (broken) return { declared: true, path: indexPath, expected: null, count: series.records.length };
|
|
591
643
|
|
|
592
644
|
const expected = renderHandoffIndex(workspace, series);
|
|
@@ -1052,7 +1104,8 @@ export function run(argv, say = console.log) {
|
|
|
1052
1104
|
for (const f of broken) say(` ✗ ${dir}: ${f.message}`);
|
|
1053
1105
|
if (broken.length && worst < 1) worst = 1;
|
|
1054
1106
|
else if (!result.series.handoffs.declared) say(` · ${dir}: declares no handoff index`);
|
|
1055
|
-
else if (!broken.length) say(result.series.handoffs.expected.trimEnd());
|
|
1107
|
+
else if (!broken.length && result.series.handoffs.expected !== null) say(result.series.handoffs.expected.trimEnd());
|
|
1108
|
+
else say(` · ${dir}: declares no handoff index`);
|
|
1056
1109
|
continue;
|
|
1057
1110
|
}
|
|
1058
1111
|
if (!result.declared) {
|
|
@@ -1074,8 +1127,8 @@ export function run(argv, say = console.log) {
|
|
|
1074
1127
|
}
|
|
1075
1128
|
}
|
|
1076
1129
|
if (result.series.handoffs.declared) {
|
|
1077
|
-
const { kept, count } = result.series.handoffs;
|
|
1078
|
-
parts.push(kept ? `handoff index ${state}` : `handoff index renders, ${count} handoff(s), none kept on disk`);
|
|
1130
|
+
const { kept, count, path: indexPath } = result.series.handoffs;
|
|
1131
|
+
parts.push(indexPath === null ? `handoff dates checked, ${count} handoff(s); no index declared` : kept ? `handoff index ${state}` : `handoff index renders, ${count} handoff(s), none kept on disk`);
|
|
1079
1132
|
}
|
|
1080
1133
|
if (result.series.scopes.declared) parts.push(`scope index ${state}`);
|
|
1081
1134
|
say(` ok ${dir}: ${parts.join("; ")}`);
|