@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.
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +87 -0
- package/cli/README.md +337 -0
- package/cli/compile.mjs +2209 -0
- package/cli/control-chars.mjs +570 -0
- package/cli/discover.mjs +985 -0
- package/cli/doctor.mjs +2803 -0
- package/cli/feedback.mjs +970 -0
- package/cli/gate.mjs +271 -0
- package/cli/index.mjs +1390 -0
- package/cli/init.mjs +1729 -0
- package/cli/librarian.mjs +1384 -0
- package/cli/manifest.mjs +59 -0
- package/cli/new.mjs +803 -0
- package/cli/pack-version.mjs +543 -0
- package/cli/plugin-lint.mjs +1566 -0
- package/cli/portulan.mjs +266 -0
- package/cli/recipe-set.mjs +425 -0
- package/cli/rule-carriers.mjs +489 -0
- package/cli/skills-set.mjs +715 -0
- package/cli/stop-gate.mjs +434 -0
- package/cli/upgrade.mjs +897 -0
- package/cli/vendor.mjs +1504 -0
- package/core/README.md +46 -0
- package/core/engine.md +47 -0
- package/core/operating/autonomy.md +100 -0
- package/core/operating/evolution.md +84 -0
- package/core/operating/loop.md +83 -0
- package/core/operating/memory.md +161 -0
- package/core/operating/safety.md +42 -0
- package/core/operating/verification.md +76 -0
- package/core/personas/README.md +40 -0
- package/core/personas/implementer.md +51 -0
- package/core/personas/librarian.md +73 -0
- package/core/personas/reviewer.md +52 -0
- package/core/skills/README.md +52 -0
- package/core/skills/clarify/SKILL.md +42 -0
- package/core/skills/codify/SKILL.md +65 -0
- package/core/skills/consolidate/SKILL.md +121 -0
- package/core/templates/gate-policy.md +76 -0
- package/core/templates/handoff.md +29 -0
- package/core/templates/memory-entry.md +44 -0
- package/core/templates/pack.md +94 -0
- package/core/templates/persona.md +69 -0
- package/core/templates/proposal.md +27 -0
- package/core/templates/repo-card.md +28 -0
- package/core/templates/skill.md +57 -0
- package/core/templates/task.md +26 -0
- package/core/templates/workspace.md +81 -0
- package/package.json +41 -0
- package/packs/.claude-plugin/plugin.json +22 -0
- package/packs/README.md +61 -0
- package/packs/rituals/README.md +13 -0
- package/packs/rituals/checkpoints/README.md +105 -0
- package/packs/rituals/checkpoints/pack.json +36 -0
- package/packs/rituals/checkpoints/personas/supervisor.md +64 -0
- package/packs/rituals/checkpoints/skills/milestone-close/SKILL.md +75 -0
- package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +75 -0
- package/packs/rituals/checkpoints/skills/session-open/SKILL.md +57 -0
- package/packs/stacks/README.md +8 -0
- package/packs/tools/README.md +17 -0
- package/packs/tools/github/README.md +65 -0
- package/packs/tools/github/pack.json +29 -0
- package/packs/tools/github/verify/README.md +51 -0
- package/packs/tools/github/verify/actions-pinned.sh +132 -0
- package/spec/README.md +399 -0
- package/spec/migrations/0001-repository-declares-its-tree.mjs +100 -0
- package/spec/migrations/0002-bundle-fallback-path.mjs +179 -0
- package/spec/migrations/README.md +95 -0
- package/spec/pack.schema.json +201 -0
- package/spec/slots.md +796 -0
- package/spec/workspace.schema.json +438 -0
package/cli/portulan.mjs
ADDED
|
@@ -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
|
+
}
|