@sleepy_panda_srl/portulan 0.1.2 → 0.2.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.
Files changed (81) hide show
  1. package/README.md +7 -6
  2. package/cli/README.md +142 -291
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1443 -1210
  6. package/cli/context.mjs +883 -0
  7. package/cli/control-chars.mjs +12 -301
  8. package/cli/discover.mjs +17 -574
  9. package/cli/doctor.mjs +225 -1071
  10. package/cli/drills.mjs +1107 -0
  11. package/cli/feedback.mjs +16 -237
  12. package/cli/finish.mjs +437 -0
  13. package/cli/form.mjs +644 -0
  14. package/cli/fuzz-shell.mjs +595 -0
  15. package/cli/gate.mjs +7 -198
  16. package/cli/goldens.mjs +311 -0
  17. package/cli/index.mjs +305 -655
  18. package/cli/init.mjs +244 -564
  19. package/cli/inside.mjs +3 -28
  20. package/cli/instructions.mjs +829 -0
  21. package/cli/ledger.mjs +778 -0
  22. package/cli/librarian.mjs +135 -599
  23. package/cli/manifest.mjs +2 -39
  24. package/cli/mutants.mjs +764 -0
  25. package/cli/new.mjs +12 -243
  26. package/cli/pack-identity.mjs +6 -68
  27. package/cli/pack-version.mjs +13 -248
  28. package/cli/plugin-lint.mjs +42 -658
  29. package/cli/portulan.mjs +5 -124
  30. package/cli/recipe-set.mjs +16 -240
  31. package/cli/release-eval.mjs +748 -0
  32. package/cli/review-meter.mjs +500 -0
  33. package/cli/rule-carriers.mjs +12 -175
  34. package/cli/sessions.mjs +82 -0
  35. package/cli/skill-goldens.mjs +424 -0
  36. package/cli/skills-set.mjs +12 -414
  37. package/cli/stop-gate.mjs +126 -454
  38. package/cli/symbols.mjs +1132 -0
  39. package/cli/telemetry.mjs +582 -0
  40. package/cli/upgrade.mjs +171 -314
  41. package/cli/vendor.mjs +146 -476
  42. package/cli/version-carriers.mjs +9 -74
  43. package/core/README.md +1 -1
  44. package/core/engine.md +3 -9
  45. package/core/operating/autonomy.md +1 -1
  46. package/core/operating/context.md +183 -0
  47. package/core/operating/evolution.md +3 -3
  48. package/core/operating/loop.md +13 -20
  49. package/core/operating/memory.md +12 -3
  50. package/core/operating/sessions.md +100 -0
  51. package/core/personas/librarian.md +2 -2
  52. package/core/skills/consolidate/SKILL.md +4 -2
  53. package/core/templates/gate-policy.md +27 -8
  54. package/core/templates/handoff.md +28 -15
  55. package/core/templates/memory-entry.md +1 -0
  56. package/evals/README.md +320 -0
  57. package/evals/releases/0.1.3.json +134 -0
  58. package/evals/releases/0.1.3.md +64 -0
  59. package/evals/releases/0.2.0.json +146 -0
  60. package/evals/releases/0.2.0.md +67 -0
  61. package/evals/releases/README.md +32 -0
  62. package/package.json +9 -2
  63. package/packs/rituals/checkpoints/README.md +9 -2
  64. package/packs/rituals/checkpoints/pack.json +1 -1
  65. package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +32 -2
  66. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  67. package/spec/README.md +112 -6
  68. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  69. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  70. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  71. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  72. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  73. package/spec/migrations/0006-boot-card.mjs +77 -0
  74. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  75. package/spec/migrations/0008-card-reading.mjs +55 -0
  76. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  77. package/spec/migrations/0010-comment-rail.mjs +57 -0
  78. package/spec/migrations/README.md +45 -10
  79. package/spec/pack.schema.json +1 -1
  80. package/spec/slots.md +359 -23
  81. package/spec/workspace.schema.json +145 -4
@@ -0,0 +1,57 @@
1
+ // A form step — a `comments` recipe holds the comment lines that record a change's history.
2
+
3
+ import path from "node:path";
4
+
5
+ import { historyCount } from "../../cli/comments.mjs";
6
+ import { COMMENTS_RECIPE, commentsRecipe, commentsRecipeEntry, commentsRecipeOf, notYetForm } from "../../cli/form.mjs";
7
+
8
+ const RECIPE_FILE = "verify/comments.sh";
9
+
10
+ const posix = (p) => p.split(path.sep).join("/");
11
+
12
+ export const step = {
13
+ id: "0010-comment-rail",
14
+ kind: "form",
15
+ from: null,
16
+ to: null,
17
+ title: "a `comments` recipe holds the comment lines that record a change's history",
18
+ why:
19
+ "A comment is paid for on every read of its file, and a change's history belongs in its commit " +
20
+ "message. `verify/comments.sh` counts the comment lines that record one, and holds them at the count " +
21
+ "the tree has today, so it starts green and the count only falls. It is declared as the `comments` recipe.",
22
+
23
+ owed(ws, ctx) {
24
+ const behind = notYetForm(ws, ctx);
25
+ if (behind) return { owed: false, because: behind };
26
+ if (ws.manifest?.kind !== "repository") return { owed: false, because: `a \`${ws.manifest?.kind}\` workspace has no repository of its own to count` };
27
+ if (!ws.repository) return { owed: false, because: "this workspace declares no tree to count" };
28
+ if (ws.repository.git === null) return { owed: false, because: "no git work tree answers here, so no files are listed to count" };
29
+ const recipe = commentsRecipeOf(ws.manifest);
30
+ if (recipe?.drafted) return { owed: false, because: "a `comments` recipe is declared" };
31
+ if (recipe) return { owed: null, because: `the \`${COMMENTS_RECIPE}\` recipe runs ${recipe.run}, not a \`${RECIPE_FILE}\`: rename it, then upgrade` };
32
+ return { owed: true, because: "no `comments` recipe counts the comment lines that record a change's history: one is drafted at the tree's count" };
33
+ },
34
+
35
+ plan(ws, ctx) {
36
+ if (ws.list().includes(RECIPE_FILE)) {
37
+ return { ok: false, reason: `\`${RECIPE_FILE}\` is in the workspace and no recipe declares it: declare it as \`${COMMENTS_RECIPE}\`, or rename it, then upgrade` };
38
+ }
39
+ const tree = ws.repository.dir;
40
+ let limit;
41
+ try {
42
+ limit = historyCount(tree);
43
+ } catch (error) {
44
+ return { ok: false, reason: `the tree's comments could not be counted — ${error.message}` };
45
+ }
46
+ const entry = commentsRecipeEntry(posix(path.relative(tree, ws.dir)) || ".");
47
+ const manifest = { ...ws.manifest, verify: { ...ws.manifest.verify, recipes: [...ws.manifest.verify.recipes, entry] } };
48
+ const toTree = posix(path.relative(path.join(ws.dir, "verify"), tree));
49
+ return {
50
+ ok: true,
51
+ edits: [
52
+ { file: RECIPE_FILE, next: commentsRecipe({ bundle: ctx.bundle, limit, toTree }), mode: 0o755 },
53
+ { file: "workspace.json", next: `${JSON.stringify(manifest, null, 2)}\n` },
54
+ ],
55
+ };
56
+ },
57
+ };
@@ -5,7 +5,7 @@
5
5
  > stated condition: a migration needed code. The runner is
6
6
  > [`../../cli/upgrade.mjs`](../../cli/upgrade.mjs).
7
7
 
8
- ## A step, and the two kinds
8
+ ## A step, and the three kinds
9
9
 
10
10
  A migration is not one act. It is an ordered set of **steps**, each a zero-dependency ESM module
11
11
  exporting a single `step` object, and each of exactly one kind:
@@ -14,6 +14,7 @@ exporting a single `step` object, and each of exactly one kind:
14
14
  |---|---|---|
15
15
  | `version` | A Workspace Definition MAJOR migration. `spec/README.md`'s rule stands: **MINORs are additive and owe no migration**, so there is no such thing as a MINOR step and nothing here restamps one. | `from` → `to` |
16
16
  | `repair` | Something a rewriter owes a workspace it touches, independent of any version — a value that was true where it was written and is not true where the workspace now is. | the workspace's own state |
17
+ | `form` | A move of a consumer's records or boot to the form Portulan moved its own to, in the repository beside the workspace as well as in it (2026-09-24). Today's form stays legitimate and boots as it did, so a form step is owed only where the new form is not there yet, and `doctor` reports the form without failing it. | the workspace's and its tree's own state |
17
18
 
18
19
  The second kind exists by the maintainer's ruling of 2026-08-12. Without it this directory would hold
19
20
  one step (`1.0 → 2.0`) with **no subject in this repository and none in any tree we have seen** —
@@ -22,19 +23,37 @@ machinery demonstrable only on a fixture. What actually bites an adopter is a wo
22
23
  ```js
23
24
  export const step = {
24
25
  id: "0002-bundle-fallback-path", // stable, sortable; the id IS the chain's order
25
- kind: "repair", // "version" | "repair"
26
- from: null, to: null, // version steps only; null on a repair
26
+ kind: "repair", // "version" | "repair" | "form"
27
+ from: null, to: null, // version steps only; null on a repair or a form step
27
28
  title: "…", // one line, printed in the plan
28
29
  why: "…", // why it is owed at all, printed under -v
29
- owed(ws), // → { owed: true | false | null, because }
30
+ owed(ws, ctx), // → { owed: true | false | null, because, hand? }
30
31
  plan(ws, ctx), // → { ok: true, edits } | { ok: false, reason }
31
32
  };
32
33
  ```
33
34
 
34
- `ws` is a read view of the workspace — `{ dir, manifest, manifestText, list(), read(rel) }`. `ctx`
35
- carries `{ bundle, spec, tree }`: the CLI root this run is executing from, the Workspace Definition
36
- version **this bundle implements**, and the `--tree` a step may need and must never invent. An `edit`
37
- is `{ file, next }` — a relative path and its whole next contents.
35
+ `ws` is a read view of the workspace — `{ dir, manifest, manifestText, list(), read(rel), repository }`,
36
+ `repository` being a read view of the tree the manifest declares, `{ dir, read(rel), names(rel), git }`,
37
+ or `null` where it declares none; `git` runs git in the tree, and is `null` where no work tree answers
38
+ there. `ctx` carries `{ bundle, spec, tree, today }`: the CLI root this run is executing from, the
39
+ Workspace Definition version **this bundle implements**, the `--tree` a step may need and must never
40
+ invent, and the date a record written today carries. An `edit` is `{ file, next }` — a relative path
41
+ and its whole next contents — relative to the workspace, or to the tree where it says `root: "tree"`;
42
+ a `next` of `null` deletes the file, and a `mode` is the mode of a file the edit creates, as a recipe
43
+ must be executable. Rollback puts back what each edit replaced or deleted, in either
44
+ root, and the report names each file as its root names it.
45
+
46
+ **After each step applied, the steps after it are asked again.** A step owed only once an earlier one
47
+ has landed, `0007` compiling the card `0006` drafts, is applied in the same run rather than the next.
48
+ **And after a pass that applied a step, the whole chain is asked again** until a pass applies nothing
49
+ (2026-09-24), so a later step can make an earlier one owed, as `0008` does `0007` by editing the card;
50
+ a chain still applying after as many passes as it has steps is refused and rolled back.
51
+ **A step owed where it does not place its edits answers `hand: true`** (2026-09-24), its `because` naming
52
+ what a person adds: `upgrade --write` applies the rest of the chain, reports that step as owed and not
53
+ placed once `doctor` is green, and exits 1, as `--check` does over a step owed.
54
+ A form step reads a workspace at this bundle's MAJOR only (`notYetForm` in
55
+ [`../../cli/form.mjs`](../../cli/form.mjs)): one a MAJOR behind is moved by a version step first, and
56
+ the form steps are then asked again, so a workspace no version step reaches is still refused.
38
57
 
39
58
  ## Three properties, and what each one replaces
40
59
 
@@ -69,7 +88,10 @@ is a question that could not be answered.
69
88
  claiming the migration caused it.
70
89
  - **Nothing here restamps a MINOR.** A manifest declares the version its content needs, not the newest
71
90
  one — `spec/README.md`'s own rule, the one that keeps `examples/` at 2.4 as live compatibility
72
- evidence and keeps `init`, `new` and `vendor` writing `2.7`.
91
+ evidence and keeps `new` and `vendor` writing `2.7`. `0006` writes 2.10 into a manifest older than it
92
+ because the step declares `slots.context`, the key 2.10 added, which is the same rule; `init`
93
+ writes 2.10 for the same reason, and 2.11 where a person takes the cache lifetime it offers, which
94
+ `sessions`, 2.11's key, declares.
73
95
 
74
96
  ## The steps that exist
75
97
 
@@ -77,11 +99,24 @@ is a question that could not be answered.
77
99
  |---|---|---|
78
100
  | [`0001-repository-declares-its-tree.mjs`](0001-repository-declares-its-tree.mjs) | `version` 1.0 → 2.0 | The one migration `spec/README.md` documents: a `repository` workspace must declare `tree`. It **will not guess** the value — `"../"` only where the parent is verifiably the repository root, otherwise it refuses and names `--tree`. |
79
101
  | [`0002-bundle-fallback-path.mjs`](0002-bundle-fallback-path.mjs) | `repair` | `init` bakes the bundle it ran from into `verify/index.sh` as an absolute path, marked `# portulan:bundle-fallback`. `vendor --switch` copies it byte for byte. The rail then exits **2 — could not run**, which is fail-closed and therefore easy never to notice. This re-derives it. |
102
+ | [`0003-handoff-index-not-kept.mjs`](0003-handoff-index-not-kept.mjs) | `form` | A kept handoff index is deleted and its path git-ignored, so none is committed again; `portulan index --handoffs` prints it. Owed by git's answer: a copy git ignores and does not track is one machine's own, and is left alone. |
103
+ | [`0004-changelog-fragments.mjs`](0004-changelog-fragments.mjs) | `form` | Each entry under `CHANGELOG.md`'s Unreleased becomes a fragment under `changes/`, and the heading keeps a two-line pointer. **Proved before it is offered**: the fragments, rendered as the cut renders them, must print each section back as the changelog held it, or the step refuses; a heading naming none of the six sections is refused too. The one change is an entry that does not open `- `, a `*` or `+` marker or a tab after one, whose fragment does since the cut accepts no other, and the step names each such entry by its line. |
104
+ | [`0005-session-log-retired.mjs`](0005-session-log-retired.mjs) | `form` | Each tracked Markdown file with a `Session log` holding entries keeps its heading and two lines naming the last commit that holds them. **Refused on a file with changes not committed**, since the pointer would name a commit that lacks them. |
105
+ | [`0006-boot-card.mjs`](0006-boot-card.mjs) | `form` | Where `slots.context` is undeclared, a boot card is drafted from the workspace's own files, as `init` drafts one, and the slot declared at 2.10. A declared slot with no `boot` unit is a workspace that boots through its slots, and is owed nothing. |
106
+ | [`0007-guidance-compiled.mjs`](0007-guidance-compiled.mjs) | `form` | The guidance half of `portulan compile` written where it drifted: the card and the rules the host loads, through `compile`'s own planner and refusals, and never `.claude/settings.json`, which enforces gates and is a person's to compile. |
107
+ | [`0008-card-reading.mjs`](0008-card-reading.mjs) | `form` | A card drafted before it carried the engine's rules on reading and the cache gets that section, which `compile` writes out from the installed Portulan's `core/operating/context.md`, and `0007` compiles it. A card lacking the section under any other head is owed it by hand: the run names the head the step looks for and the line to add, applies the rest of the chain and exits 1, and `doctor` names the card until the line is there. |
108
+ | [`0009-instruction-sections-on-read.mjs`](0009-instruction-sections-on-read.mjs) | `form` | Each section a team marks with a line `<!-- portulan: on-read -->` under its heading, in `CLAUDE.md` or `.claude/CLAUDE.md`, moves byte for byte into an on-read unit of `slots.context`, and a comment is left where it was. **Proved before it is offered**: the file must reassemble from its units byte for byte and each clause land once, or the step refuses; a section importing a file, a mark inside a marked section and a mark under no heading are refused too, and a mark in an instruction file that is a link is owed by hand while the rest of the chain runs. Owed only where a team marked a section, and put back by [`../../cli/instructions.mjs`](../../cli/instructions.mjs) `--join`. |
109
+ | [`0010-comment-rail.mjs`](0010-comment-rail.mjs) | `form` | Where a `repository` workspace's tree is one git lists and no `comments` recipe is declared, `verify/comments.sh` is drafted as `init` drafts it, holding the comment lines that record a change's history at the count the tree has, and declared. **Refused where `verify/comments.sh` is already there undeclared**, rather than written over. |
80
110
 
81
111
  **`0001` has no subject in this tree** — nothing declares 1.0 — and is exercised against a fixture,
82
112
  which is said here rather than dressed up. **`0002` has no subject in this repository either**: this
83
113
  workspace's `verify/index.sh` was written by hand, not drafted by `init`, and carries no marker. It is
84
- exercised against workspaces the real `init` drafts.
114
+ exercised against workspaces the real `init` drafts. **Neither have the form steps**: this repository
115
+ moved its own records and boot by hand on 2026-09-23 and 24, and `0003`–`0008` are exercised against a
116
+ consumer the real `init` drafts, put back in the form it drafted before, and committed. `0009` has none
117
+ either, since this repository keeps no `CLAUDE.md`, and is exercised against such a consumer whose
118
+ instruction file the suite marks; `0010` against one `init` drafted where git listed no files, then
119
+ committed.
85
120
 
86
121
  ## Adding one
87
122
 
@@ -132,7 +132,7 @@
132
132
  },
133
133
 
134
134
  "fragment": {
135
- "description": "One gate-policy fragment: an id-addressed rule this pack contributes, in the same shape .portulan/gates.json already uses, because that policy was deliberately modelled as a list of id-addressed rules with no dependence on being the only source so that a merge step would be an addition rather than a redesign (.portulan/compile/README.md). WHERE THE TIGHTEN-ONLY RULE IS ENFORCED, stated plainly because a schema implying a guarantee it does not carry is what .portulan/dod.md condition 4 refuses: tightening is relative to what the same id already holds in the layer beneath, and a manifest cannot see that layer. So the shape forecloses what shape can — `auto` is not in the tier enum below, which makes demotion-to-unattended unexpressible in a pack at all — and the remaining comparisons are the merge step's. TIGHTENING HAS TWO AXES AND THE MERGE CHECKS BOTH. The tier may only rise. The `action` may not change at all: a fragment naming an existing id must carry that rule's action unaltered, because raising the tier while replacing the matcher passes every tier comparison, is reported as a tightening, and removes the gate — measured, on a real policy, as a fragment that took `git push --force` from gated to prohibited and deleted the emitted gate in the same move. A pack that wants to gate a different action contributes a NEW id; changing what an existing rule matches belongs to the workspace, which owns its policy. Neither layer is sufficient alone, and a release that consumes fragments without the merge's refusals would be trusting what it claims to check.",
135
+ "description": "One gate-policy fragment: an id-addressed rule this pack contributes, in the same shape .portulan/gates.json already uses, because that policy was deliberately modelled as a list of id-addressed rules with no dependence on being the only source so that a merge step would be an addition rather than a redesign (.portulan/gate-map/compiler.md). WHERE THE TIGHTEN-ONLY RULE IS ENFORCED, stated plainly because a schema implying a guarantee it does not carry is what .portulan/dod.md condition 4 refuses: tightening is relative to what the same id already holds in the layer beneath, and a manifest cannot see that layer. So the shape forecloses what shape can — `auto` is not in the tier enum below, which makes demotion-to-unattended unexpressible in a pack at all — and the remaining comparisons are the merge step's. TIGHTENING HAS TWO AXES AND THE MERGE CHECKS BOTH. The tier may only rise. The `action` may not change at all: a fragment naming an existing id must carry that rule's action unaltered, because raising the tier while replacing the matcher passes every tier comparison, is reported as a tightening, and removes the gate — measured, on a real policy, as a fragment that took `git push --force` from gated to prohibited and deleted the emitted gate in the same move. A pack that wants to gate a different action contributes a NEW id; changing what an existing rule matches belongs to the workspace, which owns its policy. Neither layer is sufficient alone, and a release that consumes fragments without the merge's refusals would be trusting what it claims to check.",
136
136
  "type": "object",
137
137
  "additionalProperties": false,
138
138
  "required": ["id", "tier", "action", "reason"],