@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
package/cli/instructions.mjs
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
14
15
|
import path from "node:path";
|
|
15
16
|
import { pathToFileURL } from "node:url";
|
|
16
17
|
|
|
17
|
-
import { BOOT_CARD_UNIT, CompileError, GUIDANCE_RULES_DIR, ON_READ_INDEX, claudeCodeGuidance, guidanceEdits, importPath, importSpans, parseUnit } from "./compile.mjs";
|
|
18
|
+
import { BOOT_CARD_UNIT, CompileError, GUIDANCE_RULES_DIR, ON_READ_INDEX, claudeCodeGuidance, guidanceEdits, importPath, importSpans, importTargetIsFile, parseUnit } from "./compile.mjs";
|
|
19
|
+
import { preflightNestedEdits } from "./guidance-files.mjs";
|
|
20
|
+
import { InstructionJournal, loadInstructionJournal, assertJournalScope } from "./instructions-journal.mjs";
|
|
18
21
|
import { isInside } from "./inside.mjs";
|
|
19
22
|
import { outlineMd } from "./symbols.mjs";
|
|
20
23
|
|
|
@@ -149,7 +152,7 @@ function importLines(text, from) {
|
|
|
149
152
|
for (const { target, index } of importSpans(text)) {
|
|
150
153
|
const bare = importPath(target);
|
|
151
154
|
if (bare === null) continue;
|
|
152
|
-
if (!bare.startsWith("~") && !
|
|
155
|
+
if (!bare.startsWith("~") && !importTargetIsFile(path.resolve(path.dirname(from), bare))) continue;
|
|
153
156
|
const line = starts.findLastIndex((start) => start <= index);
|
|
154
157
|
out.set(line, [...(out.get(line) ?? []), `@${target}`]);
|
|
155
158
|
}
|
|
@@ -164,7 +167,7 @@ function strayImports(body, unitFile) {
|
|
|
164
167
|
for (const { target } of importSpans(body)) {
|
|
165
168
|
const bare = importPath(target);
|
|
166
169
|
if (bare === null || bare.startsWith("~") || path.isAbsolute(bare)) continue;
|
|
167
|
-
if (
|
|
170
|
+
if (importTargetIsFile(path.resolve(path.dirname(unitFile), bare))) out.push(`@${target}`);
|
|
168
171
|
}
|
|
169
172
|
return out;
|
|
170
173
|
}
|
|
@@ -481,7 +484,7 @@ export function planJoin({ tree, context, read }) {
|
|
|
481
484
|
continue;
|
|
482
485
|
}
|
|
483
486
|
const bytes = Buffer.byteLength(body.join("\n"), "utf8") + (line < lines.length - 1 ? 1 : 0);
|
|
484
|
-
back.set(source, { source, body, edited: unitDigest(unitText) !== digest, description: unit.description, bytes });
|
|
487
|
+
back.set(source, { source, text:unitText, body, edited: unitDigest(unitText) !== digest, description: unit.description, bytes });
|
|
485
488
|
}
|
|
486
489
|
if (refusals.length > refused) continue;
|
|
487
490
|
const next = [];
|
|
@@ -563,18 +566,24 @@ export function linkRefusal(tree, rel) {
|
|
|
563
566
|
export function instructionReader(tree, read) {
|
|
564
567
|
return (rel) => {
|
|
565
568
|
const at = path.join(tree, ...rel.split("/"));
|
|
566
|
-
if (!INSTRUCTION_FILES.includes(rel)
|
|
567
|
-
let to;
|
|
569
|
+
if (!INSTRUCTION_FILES.includes(rel)) return read(rel);
|
|
570
|
+
let to, entry;
|
|
568
571
|
try {
|
|
572
|
+
entry = fs.lstatSync(at);
|
|
569
573
|
to = fs.realpathSync(at);
|
|
570
574
|
} catch (error) {
|
|
571
|
-
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
575
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return entry ? null : read(rel);
|
|
572
576
|
throw new InstructionsError(`${rel} could not be followed — ${error.code ?? error.message}`);
|
|
573
577
|
}
|
|
574
578
|
if (!isInside(fs.realpathSync(tree), to)) return null;
|
|
575
579
|
try {
|
|
576
|
-
|
|
580
|
+
if (!fs.statSync(to).isFile()) throw new InstructionsError(`${rel} is not a regular instruction source`);
|
|
581
|
+
if (!entry.isSymbolicLink()) return read(rel);
|
|
582
|
+
const raw = fs.readFileSync(to), text = raw.toString("utf8");
|
|
583
|
+
if (!Buffer.from(text, "utf8").equals(raw)) throw new InstructionsError(`${rel} is not valid UTF-8`);
|
|
584
|
+
return text;
|
|
577
585
|
} catch (error) {
|
|
586
|
+
if (error instanceof InstructionsError) throw error;
|
|
578
587
|
throw new InstructionsError(`${rel} could not be read — ${error.code ?? error.message}`);
|
|
579
588
|
}
|
|
580
589
|
};
|
|
@@ -596,7 +605,7 @@ export function splitOffers(tree, { ratio, floor, over = false }) {
|
|
|
596
605
|
const from = path.join(tree, ...rel.split("/"));
|
|
597
606
|
let text;
|
|
598
607
|
try {
|
|
599
|
-
if (!isInside(fs.realpathSync(tree), fs.realpathSync(from)) || fs.lstatSync(from).isSymbolicLink()) continue;
|
|
608
|
+
if (!isInside(fs.realpathSync(tree), fs.realpathSync(from)) || fs.lstatSync(from).isSymbolicLink() || !fs.statSync(from).isFile()) continue;
|
|
600
609
|
text = fs.readFileSync(from, "utf8");
|
|
601
610
|
} catch {
|
|
602
611
|
continue;
|
|
@@ -650,8 +659,12 @@ export function treeReader(tree) {
|
|
|
650
659
|
if (!isInside(tree, at)) throw new InstructionsError(`${rel} resolves outside the repository`);
|
|
651
660
|
let text;
|
|
652
661
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
662
|
+
const target = fs.realpathSync(at);
|
|
663
|
+
if (!isInside(real, target)) throw new InstructionsError(`${rel} is a link out of the repository, which this does not read`);
|
|
664
|
+
if (!fs.statSync(target).isFile()) throw new InstructionsError(`${rel} is not a regular instruction source`);
|
|
665
|
+
const raw = fs.readFileSync(target);
|
|
666
|
+
text = raw.toString("utf8");
|
|
667
|
+
if (!Buffer.from(text, "utf8").equals(raw)) throw new InstructionsError(`${rel} is not valid UTF-8`);
|
|
655
668
|
} catch (error) {
|
|
656
669
|
if (error instanceof InstructionsError) throw error;
|
|
657
670
|
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
@@ -707,6 +720,262 @@ function locate(workspaceDir) {
|
|
|
707
720
|
return { manifest, tree, context, taken };
|
|
708
721
|
}
|
|
709
722
|
|
|
723
|
+
function sourceEdits(plan,join,modeOf){
|
|
724
|
+
return join?[
|
|
725
|
+
...plan.files.map(f=>({root:"tree",file:f.rel,before:f.before,next:f.after,mode:modeOf(f.rel)})),
|
|
726
|
+
...plan.units.map(u=>({root:"tree",file:u.source,before:u.text,next:null,mode:modeOf(u.source)})),
|
|
727
|
+
]:[
|
|
728
|
+
...plan.units.map(u=>({root:"tree",file:u.source,before:null,next:u.text,mode:modeOf(u.from)})),
|
|
729
|
+
...plan.files.map(f=>({root:"tree",file:f.rel,before:f.before,next:f.after,mode:modeOf(f.rel)})),
|
|
730
|
+
];
|
|
731
|
+
}
|
|
732
|
+
function planningReader(tree){
|
|
733
|
+
const raw=treeReader(tree),reads=new Map();
|
|
734
|
+
const read=rel=>{const file=path.join(tree,...rel.split("/")),before=fs.lstatSync(file,{throwIfNoEntry:false}),text=raw(rel),after=fs.lstatSync(file,{throwIfNoEntry:false});if(Boolean(before)!==Boolean(after)||(before&&(before.dev!==after.dev||before.ino!==after.ino||before.mode!==after.mode)))throw new InstructionsError(`${rel} changed during planning`);const value={text,identity:after?{dev:after.dev,ino:after.ino,mode:after.mode}:null};const old=reads.get(rel);if(old&&JSON.stringify(old)!==JSON.stringify(value))throw new InstructionsError(`${rel} changed between planning reads`);reads.set(rel,value);return text;};
|
|
735
|
+
return {read,reads};
|
|
736
|
+
}
|
|
737
|
+
function verifySavedPlan(where,journal){
|
|
738
|
+
const o=journal.owner,reads=new Map(o.reads.map(row=>[row.file,row])),read=rel=>reads.get(rel)?.text??null;
|
|
739
|
+
const plan=o.operation==="join"?planJoin({tree:where.tree,context:where.context,read}):planSplit({tree:where.tree,context:where.context,taken:o.taken,read});
|
|
740
|
+
if(plan.refusals.length)throw new InstructionsError(`saved migration no longer proves its operation: ${plan.refusals.join("; ")}`);
|
|
741
|
+
const edits=sourceEdits(plan,o.operation==="join",rel=>reads.get(rel)?.identity?.mode);
|
|
742
|
+
const expected=edits.map(({file,before,next,mode})=>({file,before,next,mode})),saved=o.inputs.map(({file,before,next,mode})=>({file,before,next,mode}));
|
|
743
|
+
if(JSON.stringify(expected)!==JSON.stringify(saved))throw new InstructionsError("saved edits are not the proved split or join of their original sources; retained journal for review");
|
|
744
|
+
for(const input of o.inputs)if(input.before!==null&&(read(input.file)!==input.before||JSON.stringify(reads.get(input.file)?.identity)!==JSON.stringify(input.original)))throw new InstructionsError("saved source does not match its planning snapshot");
|
|
745
|
+
}
|
|
746
|
+
function preflightSources(where, workspaceDir, edits, planning) {
|
|
747
|
+
assertJournalScope(where.tree, workspaceDir);
|
|
748
|
+
for (const root of INSTRUCTION_FILES) {
|
|
749
|
+
const file = path.join(where.tree, ...root.split("/")),
|
|
750
|
+
at = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
751
|
+
if (at && (!at.isFile() || at.isSymbolicLink() || at.nlink !== 1))
|
|
752
|
+
throw new InstructionsError(`${root} is not an ordinary singly linked root; nothing was written`);
|
|
753
|
+
planning.read(root);
|
|
754
|
+
}
|
|
755
|
+
for (const edit of edits) {
|
|
756
|
+
const file = path.join(where.tree, ...edit.file.split("/")),
|
|
757
|
+
at = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
758
|
+
if (edit.before === null) {
|
|
759
|
+
if (at) throw new InstructionsError(`${edit.file} arrived after planning`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const held = planning.reads.get(edit.file);
|
|
763
|
+
if (
|
|
764
|
+
!at?.isFile() ||
|
|
765
|
+
at.isSymbolicLink() ||
|
|
766
|
+
at.nlink !== 1 ||
|
|
767
|
+
!held?.identity ||
|
|
768
|
+
at.dev !== held.identity.dev ||
|
|
769
|
+
at.ino !== held.identity.ino ||
|
|
770
|
+
at.mode !== held.identity.mode ||
|
|
771
|
+
planning.read(edit.file) !== edit.before
|
|
772
|
+
)
|
|
773
|
+
throw new InstructionsError(`${edit.file} changed identity, mode or bytes after planning`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function referenceKey(tree, file) {
|
|
777
|
+
const root = fs.realpathSync(tree);
|
|
778
|
+
let full = path.resolve(file);
|
|
779
|
+
const seen = new Set();
|
|
780
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
781
|
+
let real;
|
|
782
|
+
try {
|
|
783
|
+
real = fs.realpathSync(full);
|
|
784
|
+
} catch {
|
|
785
|
+
try {
|
|
786
|
+
const at = fs.lstatSync(full, { throwIfNoEntry: false });
|
|
787
|
+
if (at?.isSymbolicLink()) {
|
|
788
|
+
if (seen.has(full)) return null;
|
|
789
|
+
seen.add(full);
|
|
790
|
+
full = path.resolve(fs.realpathSync(path.dirname(full)), fs.readlinkSync(full));
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
real = path.join(fs.realpathSync(path.dirname(full)), path.basename(full));
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return isInside(root, real) ? posix(path.relative(root, real)).normalize("NFC").toLowerCase() : null;
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
function referencesOfRoots(tree, read) {
|
|
803
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }),
|
|
804
|
+
found = new Set();
|
|
805
|
+
const add = (file) => {
|
|
806
|
+
const key = referenceKey(tree, file);
|
|
807
|
+
if (key !== null) found.add(key);
|
|
808
|
+
};
|
|
809
|
+
for (const file of INSTRUCTION_FILES) {
|
|
810
|
+
const current = read(file);
|
|
811
|
+
if (current === null) continue;
|
|
812
|
+
const text = typeof current === "string" ? current : decoder.decode(current);
|
|
813
|
+
for (const marker of marksOf(text).moved) add(path.resolve(tree, ...marker.source.split("/")));
|
|
814
|
+
for (const span of importSpans(text)) {
|
|
815
|
+
const target = importPath(span.target);
|
|
816
|
+
if (target === null) continue;
|
|
817
|
+
add(
|
|
818
|
+
target.startsWith("~/")
|
|
819
|
+
? path.resolve(os.homedir(), target.slice(2))
|
|
820
|
+
: path.resolve(tree, path.dirname(file), target),
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return found;
|
|
825
|
+
}
|
|
826
|
+
function currentReferences(journal) {
|
|
827
|
+
return referencesOfRoots(journal.root, (file) => journal.source({ file })?.bytes ?? null);
|
|
828
|
+
}
|
|
829
|
+
function assertProjectedReferences(where, edits, planning) {
|
|
830
|
+
const roots = new Map(
|
|
831
|
+
edits.filter((edit) => INSTRUCTION_FILES.includes(edit.file)).map((edit) => [edit.file, edit.next]),
|
|
832
|
+
);
|
|
833
|
+
const references = referencesOfRoots(where.tree, (file) =>
|
|
834
|
+
roots.has(file) ? roots.get(file) : planning.read(file),
|
|
835
|
+
);
|
|
836
|
+
for (const edit of edits)
|
|
837
|
+
if (edit.next === null && references.has(edit.file.normalize("NFC").toLowerCase()))
|
|
838
|
+
throw new InstructionsError(`a resulting root still references ${edit.file}; its unit was preserved`);
|
|
839
|
+
}
|
|
840
|
+
async function writeSourceOperation({
|
|
841
|
+
journal,
|
|
842
|
+
where,
|
|
843
|
+
workspaceDir,
|
|
844
|
+
options,
|
|
845
|
+
say,
|
|
846
|
+
write,
|
|
847
|
+
applyEdits,
|
|
848
|
+
restore,
|
|
849
|
+
context,
|
|
850
|
+
declared,
|
|
851
|
+
ratio,
|
|
852
|
+
tokens,
|
|
853
|
+
alwaysBytes,
|
|
854
|
+
recovered = false,
|
|
855
|
+
}) {
|
|
856
|
+
let undoing = false;
|
|
857
|
+
const forwardGuard = (edit) => {
|
|
858
|
+
journal.guard();
|
|
859
|
+
const retiring =
|
|
860
|
+
journal.owner.operation === "join" &&
|
|
861
|
+
edit?.next === null &&
|
|
862
|
+
journal.owner.inputs.some((input) => input.file === edit.file);
|
|
863
|
+
journal.assertRootImages(retiring);
|
|
864
|
+
if (retiring && currentReferences(journal).has(edit.file.normalize("NFC").toLowerCase()))
|
|
865
|
+
throw new InstructionsError(`a live root still references ${edit.file}; its unit was preserved`);
|
|
866
|
+
};
|
|
867
|
+
const hook = (edit, phase, data) => {
|
|
868
|
+
journal.guard();
|
|
869
|
+
if (!undoing && phase !== "retired") forwardGuard(edit);
|
|
870
|
+
if (journal.owner.inputs.some((input) => input.file === edit.file)) journal.observe(edit, phase, data);
|
|
871
|
+
};
|
|
872
|
+
let applied = { ok: true, snapshots: [] };
|
|
873
|
+
const undo = (why) => {
|
|
874
|
+
let back;
|
|
875
|
+
undoing = true;
|
|
876
|
+
try {
|
|
877
|
+
back = restore(workspaceDir, applied.snapshots, {
|
|
878
|
+
treeDir: where.tree,
|
|
879
|
+
beforeMutation: (snapshot) => {
|
|
880
|
+
journal.guard();
|
|
881
|
+
if (
|
|
882
|
+
snapshot.previous === null &&
|
|
883
|
+
journal.owner.inputs.some((input) => input.file === snapshot.file) &&
|
|
884
|
+
currentReferences(journal).has(snapshot.file.normalize("NFC").toLowerCase())
|
|
885
|
+
)
|
|
886
|
+
throw new InstructionsError(
|
|
887
|
+
`a live root still references ${snapshot.file}; its unit was preserved`,
|
|
888
|
+
);
|
|
889
|
+
},
|
|
890
|
+
journal: hook,
|
|
891
|
+
stopOnFailure: true,
|
|
892
|
+
});
|
|
893
|
+
} catch (error) {
|
|
894
|
+
back = { ok: false };
|
|
895
|
+
why += `; ${error.message}`;
|
|
896
|
+
} finally {
|
|
897
|
+
undoing = false;
|
|
898
|
+
}
|
|
899
|
+
let complete = false;
|
|
900
|
+
try {
|
|
901
|
+
complete = back.ok && journal.originalsRestored();
|
|
902
|
+
if (complete) {
|
|
903
|
+
journal.cleanupSources();
|
|
904
|
+
journal.mark("aborted");
|
|
905
|
+
journal.clean();
|
|
906
|
+
}
|
|
907
|
+
} catch (error) {
|
|
908
|
+
complete = false;
|
|
909
|
+
why += `; ${error.message}`;
|
|
910
|
+
}
|
|
911
|
+
say(
|
|
912
|
+
`instructions: ${why}; ${complete ? "rolled back, nothing is changed" : `the rollback was INCOMPLETE — retained ${journal.dir}; resume the recorded migration after reviewing changed sources`}`,
|
|
913
|
+
);
|
|
914
|
+
return 2;
|
|
915
|
+
};
|
|
916
|
+
try {
|
|
917
|
+
const remaining = journal.remaining();
|
|
918
|
+
applied = applyEdits(workspaceDir, remaining, {
|
|
919
|
+
treeDir: where.tree,
|
|
920
|
+
write,
|
|
921
|
+
beforeMutation: forwardGuard,
|
|
922
|
+
journal: hook,
|
|
923
|
+
});
|
|
924
|
+
if (!applied.ok) return undo(`the write failed — ${applied.reason}`);
|
|
925
|
+
let compiled;
|
|
926
|
+
try {
|
|
927
|
+
compiled = guidanceEdits(workspaceDir).edits.map((edit) => ({ ...edit, root: "tree" }));
|
|
928
|
+
preflightNestedEdits(
|
|
929
|
+
where.tree,
|
|
930
|
+
compiled.filter((edit) => edit.nested),
|
|
931
|
+
);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
return undo(`\`compile\` refused the guidance — ${error.message}`);
|
|
934
|
+
}
|
|
935
|
+
const indexed = applyEdits(workspaceDir, compiled, {
|
|
936
|
+
treeDir: where.tree,
|
|
937
|
+
write,
|
|
938
|
+
beforeMutation: forwardGuard,
|
|
939
|
+
});
|
|
940
|
+
applied.snapshots.push(...indexed.snapshots);
|
|
941
|
+
if (!indexed.ok) return undo(`the index could not be written — ${indexed.reason}`);
|
|
942
|
+
const checked = await import("./compile.mjs");
|
|
943
|
+
if (checked.compileGuidance(workspaceDir, { check: true }).drifted)
|
|
944
|
+
throw new InstructionsError("the completed source set did not regenerate clean guidance");
|
|
945
|
+
journal.cleanupSources();
|
|
946
|
+
journal.mark("committed");
|
|
947
|
+
journal.clean();
|
|
948
|
+
let now;
|
|
949
|
+
try {
|
|
950
|
+
now = alwaysBytes();
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (!(error instanceof context.ContextError)) throw error;
|
|
953
|
+
say(
|
|
954
|
+
`instructions: written, and the index compiled; the always tier could not be measured after it — ${error.message}`,
|
|
955
|
+
);
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
const offer =
|
|
959
|
+
options.join || declared.budget !== null
|
|
960
|
+
? ""
|
|
961
|
+
: `. A budget is yours to declare: 0036 offers the larger of ${grouped(context.OFFER_FLOOR_TOKENS)} tokens and that, ${grouped(Math.max(context.OFFER_FLOOR_TOKENS, context.tokensOf(now, ratio)))}, as \`context.always.budget.tokens\``;
|
|
962
|
+
say(
|
|
963
|
+
`instructions: ${recovered ? "recovered the interrupted migration; " : ""}written, and the index compiled: the always tier is ${grouped(now)} B (${tokens(now)})${offer}`,
|
|
964
|
+
);
|
|
965
|
+
return 0;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
if (!journal.terminal && applied.snapshots.length) return undo(error.message);
|
|
968
|
+
say(`instructions: ${error.message}; recovery record retained at ${journal.dir}`);
|
|
969
|
+
return 2;
|
|
970
|
+
} finally {
|
|
971
|
+
try {
|
|
972
|
+
journal.release();
|
|
973
|
+
} catch (error) {
|
|
974
|
+
say(`instructions: recovery lease retained at ${journal.dir} — ${error.message}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
710
979
|
// `write` is the suite's, as `applyEdits` takes it: a write failing partway cannot be staged on a real disk.
|
|
711
980
|
export async function run(argv, say = (line) => process.stdout.write(`${line}\n`), { cwd = process.cwd(), write } = {}) {
|
|
712
981
|
let options;
|
|
@@ -720,7 +989,7 @@ export async function run(argv, say = (line) => process.stdout.write(`${line}\n`
|
|
|
720
989
|
return 2;
|
|
721
990
|
}
|
|
722
991
|
const workspaceDir = path.resolve(cwd, options.workspace);
|
|
723
|
-
const
|
|
992
|
+
const planning=planningReader(where.tree),read=planning.read;
|
|
724
993
|
// Loaded here, not at the top: `./context.mjs` and `./upgrade.mjs` reach this module, so a static edge would close a cycle.
|
|
725
994
|
const context = await import("./context.mjs");
|
|
726
995
|
const { applyEdits, restore } = await import("./upgrade.mjs");
|
|
@@ -736,6 +1005,16 @@ export async function run(argv, say = (line) => process.stdout.write(`${line}\n`
|
|
|
736
1005
|
const tokens = (bytes) => `~${grouped(context.tokensOf(bytes, ratio))} tokens`;
|
|
737
1006
|
const alwaysBytes = () => context.alwaysTier(where.tree).entries.reduce((n, e) => n + e.bytes, 0);
|
|
738
1007
|
|
|
1008
|
+
try{
|
|
1009
|
+
const pending=loadInstructionJournal(where.tree,workspaceDir,where.context);
|
|
1010
|
+
if(pending){
|
|
1011
|
+
if(!options.write){say(`instructions: interrupted migration at ${pending.dir}; resume with --write (${pending.owner?.operation??"cleanup"})`);return 2;}
|
|
1012
|
+
if(pending.terminal){pending.clean();say("instructions: completed interrupted journal cleanup; source files were preserved");return 0;}
|
|
1013
|
+
pending.acquire();try{pending.recoverClaims();if(pending.owner.operation!==(options.join?"join":"split"))throw new InstructionsError(`pending ${pending.owner.operation} must be resumed with its original command`);verifySavedPlan(where,pending);pending.restoreRetiredUnits(currentReferences(pending));}catch(error){try{pending.release();}catch{}throw error;}
|
|
1014
|
+
return writeSourceOperation({journal:pending,where,workspaceDir,options,say,write,applyEdits,restore,context,declared,ratio,tokens,alwaysBytes,recovered:true});
|
|
1015
|
+
}
|
|
1016
|
+
}catch(error){say(`instructions: ${error.message}`);return 2;}
|
|
1017
|
+
|
|
739
1018
|
let edits;
|
|
740
1019
|
try {
|
|
741
1020
|
if (options.join) {
|
|
@@ -746,34 +1025,59 @@ export async function run(argv, say = (line) => process.stdout.write(`${line}\n`
|
|
|
746
1025
|
say("instructions: no section of CLAUDE.md or .claude/CLAUDE.md is moved, so there is nothing to join");
|
|
747
1026
|
return 0;
|
|
748
1027
|
}
|
|
1028
|
+
preflightNestedEdits(
|
|
1029
|
+
where.tree,
|
|
1030
|
+
guidanceEdits(workspaceDir).edits.filter((edit) => edit.nested),
|
|
1031
|
+
);
|
|
749
1032
|
for (const f of joined.files) {
|
|
750
1033
|
for (const unit of f.units) say(`instructions: ${joinLine(unit, f.rel)}`);
|
|
751
1034
|
const n = f.units.length;
|
|
752
|
-
say(
|
|
1035
|
+
say(
|
|
1036
|
+
`instructions: ${f.rel} takes back ${n} section${n === 1 ? "" : "s"}, and each of the ${grouped(f.clauses.clauses)} clauses lands once, none missing and none doubled`,
|
|
1037
|
+
);
|
|
753
1038
|
}
|
|
754
|
-
edits =
|
|
755
|
-
...joined.files.map((f) => ({ root: "tree", file: f.rel, next: f.after })),
|
|
756
|
-
...joined.units.map((u) => ({ root: "tree", file: u.source, next: null })),
|
|
757
|
-
];
|
|
1039
|
+
edits = sourceEdits(joined, true, (rel) => planning.reads.get(rel)?.identity?.mode);
|
|
758
1040
|
} else {
|
|
759
1041
|
const split = planSplit({ tree: where.tree, context: where.context, taken: where.taken, read });
|
|
760
1042
|
for (const r of split.refusals) say(`instructions: refused — ${r}`);
|
|
761
1043
|
if (split.refusals.length) return 2;
|
|
762
1044
|
if (split.marked === 0) {
|
|
763
|
-
say(
|
|
1045
|
+
say(
|
|
1046
|
+
`instructions: no section of CLAUDE.md or .claude/CLAUDE.md is marked — a line \`${ON_READ_MARK}\` under a heading marks its section`,
|
|
1047
|
+
);
|
|
764
1048
|
return 0;
|
|
765
1049
|
}
|
|
766
1050
|
for (const line of splitLines(split)) say(`instructions: ${line}`);
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1051
|
+
const guidance = guidanceEdits(workspaceDir);
|
|
1052
|
+
preflightNestedEdits(
|
|
1053
|
+
where.tree,
|
|
1054
|
+
guidance.edits.filter((edit) => edit.nested),
|
|
1055
|
+
);
|
|
1056
|
+
edits = sourceEdits(split, false, (rel) => planning.reads.get(rel)?.identity?.mode);
|
|
1057
|
+
}
|
|
1058
|
+
preflightSources(where, workspaceDir, edits, planning);
|
|
1059
|
+
if (options.join) assertProjectedReferences(where, edits, planning);
|
|
1060
|
+
const { previewMigration } = await import("./init-preview.mjs");
|
|
1061
|
+
let projected;
|
|
1062
|
+
try {
|
|
1063
|
+
projected = previewMigration(where.tree, workspaceDir, edits);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
throw new InstructionsError(`prospective guidance refuses — ${error.message}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (!options.join) {
|
|
1068
|
+
const before = projected.before[0],
|
|
1069
|
+
after = projected.after?.[0];
|
|
1070
|
+
if (after === undefined)
|
|
1071
|
+
say(
|
|
1072
|
+
`instructions: the always tier is currently ${grouped(before)} B (${tokens(before)}); its after forecast is unavailable because native imports cannot be projected exactly. --write remeasures the emitted result.`,
|
|
1073
|
+
);
|
|
1074
|
+
else
|
|
1075
|
+
say(
|
|
1076
|
+
`instructions: the always tier goes from ${grouped(before)} B (${tokens(before)}) to ${grouped(after)} B (${tokens(after)}) at ${ratio} bytes per token, markers counted, which the host drops`,
|
|
1077
|
+
);
|
|
774
1078
|
}
|
|
775
1079
|
} catch (error) {
|
|
776
|
-
if (!(error instanceof InstructionsError || error instanceof context.ContextError)) throw error;
|
|
1080
|
+
if (!(error instanceof InstructionsError || error instanceof context.ContextError || error instanceof CompileError)) throw error;
|
|
777
1081
|
say(`instructions: ${error.message}`);
|
|
778
1082
|
return 2;
|
|
779
1083
|
}
|
|
@@ -782,37 +1086,8 @@ export async function run(argv, say = (line) => process.stdout.write(`${line}\n`
|
|
|
782
1086
|
return 0;
|
|
783
1087
|
}
|
|
784
1088
|
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
const back = restore(workspaceDir, applied.snapshots, { treeDir: where.tree });
|
|
788
|
-
say(`instructions: ${why}; ${back.ok ? "rolled back, nothing is changed" : `the rollback was INCOMPLETE — not put back: ${back.failed.join(", ")}`}`);
|
|
789
|
-
return 2;
|
|
790
|
-
};
|
|
791
|
-
if (!applied.ok) return undo(`the write failed — ${applied.reason}`);
|
|
792
|
-
let compiled;
|
|
793
|
-
try {
|
|
794
|
-
compiled = guidanceEdits(workspaceDir).edits.map((edit) => ({ root: "tree", file: edit.file, next: edit.next }));
|
|
795
|
-
} catch (error) {
|
|
796
|
-
return undo(`\`compile\` refused the guidance — ${error.message}`);
|
|
797
|
-
}
|
|
798
|
-
const indexed = applyEdits(workspaceDir, compiled, { treeDir: where.tree, write });
|
|
799
|
-
applied.snapshots.push(...indexed.snapshots);
|
|
800
|
-
if (!indexed.ok) return undo(`the index could not be written — ${indexed.reason}`);
|
|
801
|
-
let now;
|
|
802
|
-
try {
|
|
803
|
-
now = alwaysBytes();
|
|
804
|
-
} catch (error) {
|
|
805
|
-
if (!(error instanceof context.ContextError)) throw error;
|
|
806
|
-
say(`instructions: written, and the index compiled; the always tier could not be measured after it — ${error.message}`);
|
|
807
|
-
return 0;
|
|
808
|
-
}
|
|
809
|
-
const offer =
|
|
810
|
-
options.join || declared.budget !== null
|
|
811
|
-
? ""
|
|
812
|
-
: `. A budget is yours to declare: 0036 offers the larger of ${grouped(context.OFFER_FLOOR_TOKENS)} tokens and that, ` +
|
|
813
|
-
`${grouped(Math.max(context.OFFER_FLOOR_TOKENS, context.tokensOf(now, ratio)))}, as \`context.always.budget.tokens\``;
|
|
814
|
-
say(`instructions: written, and the index compiled: the always tier is ${grouped(now)} B (${tokens(now)})${offer}`);
|
|
815
|
-
return 0;
|
|
1089
|
+
try{preflightSources(where,workspaceDir,edits,planning);const journal=InstructionJournal.create(where.tree,workspaceDir,where.context,options.join?"join":"split",edits,planning.reads,where.taken);return writeSourceOperation({journal,where,workspaceDir,options,say,write,applyEdits,restore,context,declared,ratio,tokens,alwaysBytes});}
|
|
1090
|
+
catch(error){say(`instructions: ${error.message}`);return 2;}
|
|
816
1091
|
}
|
|
817
1092
|
|
|
818
1093
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|