@sleepy_panda_srl/portulan 0.1.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 (73) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +87 -0
  4. package/cli/README.md +337 -0
  5. package/cli/compile.mjs +2209 -0
  6. package/cli/control-chars.mjs +570 -0
  7. package/cli/discover.mjs +985 -0
  8. package/cli/doctor.mjs +2803 -0
  9. package/cli/feedback.mjs +970 -0
  10. package/cli/gate.mjs +271 -0
  11. package/cli/index.mjs +1390 -0
  12. package/cli/init.mjs +1729 -0
  13. package/cli/librarian.mjs +1384 -0
  14. package/cli/manifest.mjs +59 -0
  15. package/cli/new.mjs +803 -0
  16. package/cli/pack-version.mjs +543 -0
  17. package/cli/plugin-lint.mjs +1566 -0
  18. package/cli/portulan.mjs +266 -0
  19. package/cli/recipe-set.mjs +425 -0
  20. package/cli/rule-carriers.mjs +489 -0
  21. package/cli/skills-set.mjs +715 -0
  22. package/cli/stop-gate.mjs +434 -0
  23. package/cli/upgrade.mjs +897 -0
  24. package/cli/vendor.mjs +1504 -0
  25. package/core/README.md +46 -0
  26. package/core/engine.md +47 -0
  27. package/core/operating/autonomy.md +100 -0
  28. package/core/operating/evolution.md +84 -0
  29. package/core/operating/loop.md +83 -0
  30. package/core/operating/memory.md +161 -0
  31. package/core/operating/safety.md +42 -0
  32. package/core/operating/verification.md +76 -0
  33. package/core/personas/README.md +40 -0
  34. package/core/personas/implementer.md +51 -0
  35. package/core/personas/librarian.md +73 -0
  36. package/core/personas/reviewer.md +52 -0
  37. package/core/skills/README.md +52 -0
  38. package/core/skills/clarify/SKILL.md +42 -0
  39. package/core/skills/codify/SKILL.md +65 -0
  40. package/core/skills/consolidate/SKILL.md +121 -0
  41. package/core/templates/gate-policy.md +76 -0
  42. package/core/templates/handoff.md +29 -0
  43. package/core/templates/memory-entry.md +44 -0
  44. package/core/templates/pack.md +94 -0
  45. package/core/templates/persona.md +69 -0
  46. package/core/templates/proposal.md +27 -0
  47. package/core/templates/repo-card.md +28 -0
  48. package/core/templates/skill.md +57 -0
  49. package/core/templates/task.md +26 -0
  50. package/core/templates/workspace.md +81 -0
  51. package/package.json +41 -0
  52. package/packs/.claude-plugin/plugin.json +22 -0
  53. package/packs/README.md +61 -0
  54. package/packs/rituals/README.md +13 -0
  55. package/packs/rituals/checkpoints/README.md +105 -0
  56. package/packs/rituals/checkpoints/pack.json +36 -0
  57. package/packs/rituals/checkpoints/personas/supervisor.md +64 -0
  58. package/packs/rituals/checkpoints/skills/milestone-close/SKILL.md +75 -0
  59. package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +75 -0
  60. package/packs/rituals/checkpoints/skills/session-open/SKILL.md +57 -0
  61. package/packs/stacks/README.md +8 -0
  62. package/packs/tools/README.md +17 -0
  63. package/packs/tools/github/README.md +65 -0
  64. package/packs/tools/github/pack.json +29 -0
  65. package/packs/tools/github/verify/README.md +51 -0
  66. package/packs/tools/github/verify/actions-pinned.sh +132 -0
  67. package/spec/README.md +399 -0
  68. package/spec/migrations/0001-repository-declares-its-tree.mjs +100 -0
  69. package/spec/migrations/0002-bundle-fallback-path.mjs +179 -0
  70. package/spec/migrations/README.md +95 -0
  71. package/spec/pack.schema.json +201 -0
  72. package/spec/slots.md +796 -0
  73. package/spec/workspace.schema.json +438 -0
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ // Portulan — the one entry point the `npx` package exposes.
3
+ //
4
+ // `docs/vision.md` names **eight** subcommands and is human-owned: `init` · `doctor` · `compile` ·
5
+ // `vendor` · `index` · `upgrade` · `new` · `feedback`. This is milestone 7's packaging clause.
6
+ //
7
+ // It named six until 2026-08-03. `new` and `feedback` arrived in the CLI first, licensed by row 7 of
8
+ // `docs/plan.md` naming them in its own ratified text, and the maintainer then folded both into the
9
+ // constitution — so the two-carrier state that licence created lasted one pull request and is gone. The
10
+ // history is worth one sentence rather than none: a subcommand may be licensed by the row before the
11
+ // constitution catches up, and when that happens this file says which document names it. Everything
12
+ // beyond these eight is still the maintainer's call.
13
+ //
14
+ // ## What it is NOT
15
+ //
16
+ // **Not a reimplementation.** `doctor`, `compile`, `index` and `init` each live in their own file and
17
+ // are exercised by their own suites; this dispatches to them and adds nothing to what they do. A
18
+ // wrapper that re-derived any part of their behaviour would be a second carrier of one fact, which is
19
+ // the class `0020` names — a rule enforced at two sites and repaired at one — so the dispatch imports each tool's
20
+ // exported `run` and returns its exit code untouched.
21
+ //
22
+ // **Not a home for the tools that are off the list.** Several tools live in `cli/` and are deliberately
23
+ // absent here. `docs/vision.md` names eight, it is human-owned, and whether any of the others ever joins
24
+ // that list is the maintainer's call and not an implementer's. Wiring one in would mint a ninth
25
+ // subcommand into a list this file does not own. They stay runnable exactly as they are today —
26
+ // `node cli/plugin-lint.mjs …` — and every verify recipe still invokes them that way.
27
+ //
28
+ // **The roster itself is `cli/README.md`'s and is cited rather than repeated here.** This paragraph
29
+ // said **two**, then **three**, and named a different three from the root `README.md`'s three while
30
+ // **four** were on disk — one rule with three carriers, obeyed at the narrowest, in the file whose own
31
+ // comments name that class. _(It also named the wrong ordinal once — *a seventh subcommand*, written
32
+ // when the list held six. A count in prose beside a list nothing derives it from is the drift this
33
+ // file's own suite catches for `SUBCOMMANDS.length` and cannot catch here.)_
34
+ //
35
+ // ## All eight are built, and the shape of the answer while one was not is kept
36
+ //
37
+ // `upgrade` carried `module: null` from milestone 7 session 0 until session 9 and exited **2 — could
38
+ // not run**, naming where it arrived. It dispatches now like the rest. The paragraph is kept rather
39
+ // than deleted, because the state it describes recurs: the constitution may name a verb before the
40
+ // tree carries it, and both alternatives to that answer were worse — hiding the entry makes the
41
+ // package look like a smaller tool and the row like it is further along than it is, and stubbing it
42
+ // to exit 0 is a fail-open in the one place a user is most likely to trust silence. "Nothing looked"
43
+ // is never "nothing wrong" — the same three-code discipline the verify recipes hold
44
+ // (`.portulan/memory/verify-preconditions-fail-closed.md`). The machinery for the next such entry is
45
+ // still here: `module: null` plus `arrives`, and the refusal below reads them.
46
+ //
47
+ // _(`vendor` was one of three until milestone 7 session 3, and `feedback` one of two until session 6.
48
+ // The count is written out here and derived in the suite, which is the split that has held: the
49
+ // sentence explains, the assertion counts.)_
50
+ //
51
+ // ## Exit codes, which are the tools' own
52
+ //
53
+ // `0` the subcommand succeeded · `1` it rendered a red verdict · `2` it could not run — including
54
+ // an unknown subcommand, an unbuilt one, and being invoked with **no arguments at all**, because
55
+ // none of those is a verdict about anybody's workspace. An **explicit** `--help` exits 0: asking for
56
+ // help is a request, and it succeeded.
57
+ //
58
+ // Zero dependencies and no build step, which is a ruling rather than an accident: the maintainer
59
+ // settled on 2026-07-31 that this ships as plain ESM on Node, against `.portulan/identity.md`'s
60
+ // older "TypeScript on Node" line, so that the repository stays checkable by cloning it.
61
+
62
+ import fs from "node:fs";
63
+ import path from "node:path";
64
+ import { fileURLToPath, pathToFileURL } from "node:url";
65
+
66
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
67
+
68
+ // ONE carrier for the version, read from the manifest that publishes it — and read in `manifest.mjs`
69
+ // rather than here, since milestone 7 session 6.
70
+ //
71
+ // It was written down twice — here and in `package.json` — which is this repository's dominant defect
72
+ // class committed in the file whose own comments name it. The read moved down a level when `feedback`
73
+ // needed the same value: importing it *from this file* made the subcommand circular with the entry
74
+ // point and hung the command line on an unsettled top-level await, which `manifest.mjs`'s header
75
+ // records in full. Re-exported here so this file's published surface is unchanged.
76
+ export { VERSION } from "./manifest.mjs";
77
+ import { VERSION } from "./manifest.mjs";
78
+
79
+ // The eight, in the order `docs/vision.md` lists them. `module` is the file that carries the
80
+ // subcommand's `run`; `null` means the subcommand exists in the vision and not yet in the tree.
81
+ //
82
+ // `arrives` is not decoration. Condition 4 of `.portulan/dod.md` is that a document describing
83
+ // capability either has the capability or names where it arrives, and a help screen listing a verb
84
+ // that does nothing is exactly that document.
85
+ export const SUBCOMMANDS = [
86
+ {
87
+ name: "init",
88
+ module: "init.mjs",
89
+ summary: "draft a workspace for a repository that has none",
90
+ },
91
+ {
92
+ name: "doctor",
93
+ module: "doctor.mjs",
94
+ summary: "validate a workspace against the Workspace Definition",
95
+ },
96
+ {
97
+ name: "compile",
98
+ module: "compile.mjs",
99
+ summary: "compile a workspace's gate policy into host enforcement",
100
+ },
101
+ {
102
+ name: "vendor",
103
+ module: "vendor.mjs",
104
+ // BOTH directions, because the constitution now glosses both. It said "a self-contained
105
+ // AGENTS.md + .portulan/ for any host" until 2026-08-03, when the maintainer widened the gloss
106
+ // to cover materialising a workspace out of a repository as well as into one — which is what
107
+ // settled the verb row 7 had deliberately left unassigned. A summary carrying only the older
108
+ // half would be the narrower of two carriers of one fact, which is the shape a reader obeys.
109
+ summary: "materialise a workspace where it is needed: into a host, or between residences",
110
+ },
111
+ {
112
+ name: "index",
113
+ module: "index.mjs",
114
+ summary: "regenerate the memory, handoff and scope indexes",
115
+ },
116
+ {
117
+ name: "upgrade",
118
+ module: "upgrade.mjs",
119
+ // BOTH kinds, because the chain carries both. A summary naming only the version half would
120
+ // be the narrower of two carriers of one fact — and the narrower is the one a reader obeys.
121
+ // What a step is lives in `spec/migrations/README.md`; this line only has to not contradict
122
+ // it, which the older wording ("migrate a workspace to a newer Workspace Definition") would
123
+ // have started doing the moment the maintainer admitted repairs on 2026-08-12.
124
+ summary: "migrate a workspace: the Workspace Definition steps it owes, and the repairs a rewriter owes it",
125
+ },
126
+ {
127
+ name: "new",
128
+ module: "new.mjs",
129
+ summary: "scaffold a skill, persona, pack, workspace, gate policy or repo card from a core template",
130
+ },
131
+ {
132
+ name: "feedback",
133
+ module: "feedback.mjs",
134
+ summary: "file an issue from a report you previewed, seam-scanned before it leaves the machine",
135
+ },
136
+ ];
137
+
138
+ export function find(name) {
139
+ return SUBCOMMANDS.find((entry) => entry.name === name) ?? null;
140
+ }
141
+
142
+ export function usage() {
143
+ const width = Math.max(...SUBCOMMANDS.map((entry) => entry.name.length));
144
+ const lines = [
145
+ `portulan ${VERSION} — the agentic-engineering framework's command line`,
146
+ "",
147
+ " portulan <subcommand> [options]",
148
+ "",
149
+ ];
150
+ for (const entry of SUBCOMMANDS) {
151
+ // The state is printed per subcommand rather than as a footnote, because a reader scanning
152
+ // this list is deciding what to type, and a footnote is read after that decision.
153
+ const state = entry.module ? "" : ` (not built — arrives at ${entry.arrives})`;
154
+ lines.push(` ${entry.name.padEnd(width)} ${entry.summary}${state}`);
155
+ }
156
+ lines.push(
157
+ "",
158
+ "Exit codes: 0 succeeded · 1 a red verdict · 2 could not run.",
159
+ "",
160
+ "Other tools live in cli/ and are deliberately not here — docs/vision.md names these eight",
161
+ "subcommands and is human-owned, so a ninth is the maintainer's call. cli/README.md lists them.",
162
+ );
163
+ return lines.join("\n");
164
+ }
165
+
166
+ export async function run(argv, options = {}) {
167
+ const say = options.say ?? ((line) => process.stdout.write(`${line}\n`));
168
+ const warn = options.warn ?? ((line) => process.stderr.write(`${line}\n`));
169
+ const load = options.load ?? ((file) => import(pathToFileURL(path.join(HERE, file)).href));
170
+
171
+ const [name, ...rest] = argv;
172
+
173
+ if (name === undefined || name === "--help" || name === "-h" || name === "help") {
174
+ say(usage());
175
+ // The split is the point: a bare `portulan` was asked to do something and did nothing, which
176
+ // is could-not-run; an explicit `--help` asked for the help screen and got it, which is
177
+ // success. Written out because the two look alike in a shell and a reader scripting against
178
+ // this will meet both. _(These lines previously argued that explicit `--help` should exit 2
179
+ // as well — an argument the code never implemented and the tests contradict. Corrected in
180
+ // the direction of the behaviour, which is the conventional one.)_
181
+ return name === undefined ? 2 : 0;
182
+ }
183
+
184
+ if (name === "--version" || name === "-v" || name === "version") {
185
+ say(VERSION);
186
+ return 0;
187
+ }
188
+
189
+ const entry = find(name);
190
+ if (!entry) {
191
+ warn(`portulan: unknown subcommand \`${name}\`.`);
192
+ // Derived, never a literal. A hard-coded count is wrong the hour a subcommand lands — which is
193
+ // exactly what happened to a literal `3` in this file's own suite when `init` shipped.
194
+ warn(`portulan: the ${SUBCOMMANDS.length} subcommands are ${SUBCOMMANDS.map((s) => s.name).join(", ")}. Run \`portulan --help\`.`);
195
+ return 2;
196
+ }
197
+
198
+ if (!entry.module) {
199
+ // WHERE the name comes from, not a blanket "docs/vision.md". All eight are in the constitution
200
+ // as of 2026-08-03, so `namedIn` is unset on every entry today and the fallback is the whole
201
+ // behaviour — but the field stays, because the state it exists for is real and recurred once
202
+ // already: `feedback` was named by row 7 and by nothing else for one pull request, and the
203
+ // shipped tool told users the constitution named something it did not. A test asserts the
204
+ // sentence now, which is what that round was missing.
205
+ warn(`portulan: \`${name}\` is named in ${entry.namedIn ?? "docs/vision.md"} and is not built yet — it arrives at ${entry.arrives}.`);
206
+ warn("portulan: refusing to exit 0 on a subcommand that did nothing.");
207
+ return 2;
208
+ }
209
+
210
+ // Loaded on demand, never at startup. Two reasons and both are measured elsewhere in this
211
+ // repository: a tool that fails to parse takes down only its own subcommand rather than the
212
+ // whole command line, and the built modules together are large enough that eagerly importing
213
+ // them would make `portulan --help` pay for `doctor`.
214
+ let module;
215
+ try {
216
+ module = await load(entry.module);
217
+ } catch (error) {
218
+ warn(`portulan: could not load \`${name}\` from ${entry.module} — ${error.message}`);
219
+ return 2;
220
+ }
221
+
222
+ if (typeof module.run !== "function") {
223
+ // A guard rather than an assumption. Every built tool exports `run` today; if one stops, the
224
+ // failure must name itself here instead of surfacing as `module.run is not a function`.
225
+ warn(`portulan: ${entry.module} does not export a \`run\` function — refusing to guess at its entry point.`);
226
+ return 2;
227
+ }
228
+
229
+ // The tool's own exit code, returned unchanged. Re-mapping it here would put a second opinion
230
+ // about a workspace between the tool and its user.
231
+ return await module.run(rest);
232
+ }
233
+
234
+ // THE MAIN-MODULE GUARD RESOLVES SYMLINKS, and this file is the one in the repository that must.
235
+ //
236
+ // Every other tool here guards with a bare `pathToFileURL(process.argv[1])` comparison and is right
237
+ // to: nothing ever invokes them through a link. This one is a `bin`, and **npm installs a bin as a
238
+ // symlink** — `node_modules/.bin/portulan` → `cli/portulan.mjs`. Node realpaths the main module for
239
+ // `import.meta.url` while `process.argv[1]` keeps the link path, so the bare comparison is FALSE
240
+ // through the link and `run` never executes.
241
+ //
242
+ // Measured on the staged tree before this line existed, by packing the package and installing the
243
+ // tarball: through `node_modules/.bin/portulan`, `--version` printed nothing and exited 0, and
244
+ // `doctor <nonexistent>` exited **0** where the same command exits 1 from a checkout. **Every
245
+ // verdict became a silent success** — "nothing looked" reported as "nothing wrong", at the one
246
+ // surface a user reaches by the documented route. Found by the pre-commit checkpoint, which
247
+ // installed the tarball rather than reading the guard.
248
+ //
249
+ // `realpathSync` rather than `import.meta.main`, which needs a newer Node than `engines` asks for.
250
+ // Wrapped, because `argv[1]` may be absent or unreadable and a guard that throws would be a worse
251
+ // failure than the one it prevents — falling back to the unresolved path leaves the old behaviour
252
+ // rather than crashing.
253
+ function isMain() {
254
+ const invoked = process.argv[1];
255
+ if (!invoked) return false;
256
+ if (import.meta.url === pathToFileURL(invoked).href) return true;
257
+ try {
258
+ return import.meta.url === pathToFileURL(fs.realpathSync(invoked)).href;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ if (isMain()) {
265
+ process.exitCode = await run(process.argv.slice(2));
266
+ }