Skip to content
Open
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "open-prose",
"description": "Write a Markdown contract (`.prose.md`). Your agent reads it, wires services, runs subagents, and leaves an auditable trace.",
"version": "0.17.0",
"version": "0.18.0",
"license": "MIT",
"homepage": "https://github.com/openprose/prose",
"author": {
Expand Down
7 changes: 2 additions & 5 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "open-prose",
"version": "0.17.0",
"version": "0.18.0",
"description": "Write a Markdown contract (`.prose.md`). Your agent reads it, wires services, runs subagents, and leaves an auditable trace.",
"license": "MIT",
"homepage": "https://github.com/openprose/prose",
Expand All @@ -25,10 +25,7 @@
"shortDescription": "Write a Markdown contract (`.prose.md`). Your agent reads it, wires services, runs subagents, and leaves an auditable trace.",
"longDescription": "OpenProse is a programming language for AI sessions. Write a Markdown contract: an agent reads it, wires the services, runs the subagents, passes artifacts through the filesystem, and leaves a durable trace under the active OpenProse root. Plain prompts are great for one-off work; they get messy when the same process needs roles, handoffs, retries, memory, or a receipt. The plugin activates on `prose ...`, on `.prose.md` files, and on requests for reusable multi-agent orchestration.",
"category": "Productivity",
"capabilities": [
"Read",
"Write"
],
"capabilities": ["Read", "Write"],
"logo": "./assets/plugin/logo.png",
"composerIcon": "./assets/plugin/composer-icon.png",
"brandColor": "#8a6b2e",
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The contracts in this repo are **harness-agnostic**: OpenProse Markdown runs on
## Harnesses

The contracts in this repo are harness-agnostic: any Prose-Complete agent host runs them, and the
spec ([`spec/02-Harness.md`](spec/)) says what a conforming harness must do. The reference harness,
spec ([`spec/02-Harness.md`](spec/02-Harness.md)) says what a conforming harness must do. The reference harness,
**Reactor** (`@openprose/reactor`, `@openprose/reactor-cli`, `@openprose/reactor-devtools`, the `reactor`
binary), now lives at **[github.com/openprose/reactor](https://github.com/openprose/reactor)** and is
experimental (alpha): early software with no stability guarantees, to be evaluated on your own judgement.
Expand All @@ -79,7 +79,7 @@ Installs keep working under the same names.

In the spirit of the receipts:

- **The language:** the skill ships at `version 0.16.0`, `runtime_contract 2`; the spec ([`spec/`](spec/)) and the example corpus are migrated to the current vocabulary. The overhaul is recent: if you find a surface still speaking the old model, that's a bug, and we want the issue.
- **The language:** the skill's version of record is the `version:` frontmatter in [`skills/open-prose/SKILL.md`](skills/open-prose/SKILL.md) (its `runtime_contract` carries machine compatibility separately); the spec ([`spec/`](spec/)) and the example corpus are migrated to the current vocabulary. The overhaul is recent: if you find a surface still speaking the old model, that's a bug, and we want the issue.
- **Benchmarks are openly pending, on purpose.** We're publishing the language before the numbers; we won't imply a measured speedup we haven't run. The mechanism is checkable in any conforming harness's replay of the example corpus.
- The **fixpoint** (topology-as-responsibility) is specified and deferred; facet inference and ledger compaction are named roadmap.
- **Harness status** (what is built, what the receipts do and do not yet prove) is documented by each harness; for the reference harness see [Harnesses](#harnesses).
Expand Down
14 changes: 13 additions & 1 deletion scripts/bump-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,19 @@ write_field() {
tmp="$(mktemp)"
case "$kind" in
json)
jq --arg v "$value" "$(json_expr "$field") = \$v" "$REPO_ROOT/$path" > "$tmp"
# Rewrite only the line that carries the field. Round-tripping the file
# through jq re-serializes the whole manifest (it once re-expanded a
# one-line array to four lines), so a bump must touch one line and leave
# the rest byte-identical. Reads and --check still go through jq.
local pattern="^([[:space:]]*\"${field}\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")"
local hits
hits="$(grep -cE "$pattern" "$REPO_ROOT/$path" || true)"
if [[ "$hits" != "1" ]]; then
rm -f "$tmp"
echo "error: expected exactly one \"$field\" line in $path (found $hits)" >&2
exit 1
fi
sed -E "s/${pattern}/\1${value}\2/" "$REPO_ROOT/$path" > "$tmp"
mv "$tmp" "$REPO_ROOT/$path"
;;
shell-var)
Expand Down
149 changes: 149 additions & 0 deletions scripts/mint-contract-id.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env node
// Mint Contract Markdown identities.
//
// A `kind: responsibility` or `kind: gateway` file may carry `id:` frontmatter
// to give the contract an identity that survives filename and `name:` renames;
// without one, the slug is the identity. The format is defined in
// skills/open-prose/contract-markdown.md (Frontmatter): a UUIDv7-compatible
// 16-byte value rendered as uppercase Crockford base32, 26 characters from
// `0-9A-HJKMNP-TV-Z`, minted once and never hand-typed.
//
// node scripts/mint-contract-id.mjs print one fresh id
// node scripts/mint-contract-id.mjs --ensure [--add] <file>… repair ids in place
// node scripts/mint-contract-id.mjs --check [--add] <file>… report, change nothing
//
// `--ensure` replaces an `id:` that does not match the format, on a file of any
// kind, and never touches a well-formed one. It does not add an `id:` where
// none exists: the field is optional, so a missing id is not a defect. Pass
// `--add` to also insert one on responsibility and gateway files that lack it,
// placed after `version:` (or after `kind:` when there is no `version:`).
// `--check` runs the same logic and reports what `--ensure` would change.
//
// No dependencies; runs on any Node with `node:crypto` and `node:fs`.
import { randomBytes } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const ID_LINE = /^id:\s*([0-9A-HJKMNP-TV-Z]{26})\s*$/m;
const ANY_ID_LINE = /^id:.*$/m;
const KIND_LINE = /^kind:\s*(\S+)\s*$/m;
const VERSION_LINE = /^version:.*$/m;
const MAY_CARRY_ID = new Set(["responsibility", "gateway"]);

// UUIDv7: 48-bit millisecond timestamp, 4-bit version (7), 2-bit variant (10),
// and 74 random bits. Sorting the rendered id sorts by mint time.
export function uuidv7Bytes(now = Date.now()) {
const bytes = randomBytes(16);
let ms = BigInt(now);
for (let i = 5; i >= 0; i -= 1) {
bytes[i] = Number(ms & 0xffn);
ms >>= 8n;
}
bytes[6] = (bytes[6] & 0x0f) | 0x70;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
return bytes;
}

// 128 bits packed big-endian into 26 five-bit groups (130 bits); the two
// leading pad bits are zero, so the first character is always `0`–`7`.
export function toCrockford(bytes) {
let n = 0n;
for (const b of bytes) n = (n << 8n) | BigInt(b);
let out = "";
for (let shift = 125n; shift >= 0n; shift -= 5n) {
out += CROCKFORD[Number((n >> shift) & 31n)];
}
return out;
}

export function mintId() {
return toCrockford(uuidv7Bytes());
}

function frontmatterRange(source) {
if (!source.startsWith("---\n")) return null;
const end = source.indexOf("\n---", 4);
if (end === -1) return null;
return { start: 4, end: end + 1 }; // body of the block, exclusive of fences
}

// Returns { status, source } where status is one of:
// "ok" a well-formed id is already present
// "reminted" a malformed id was replaced
// "minted" a missing id was inserted (only with `add`)
// "skipped" no id present and none added
// "invalid" no frontmatter block to write into
export function ensureId(source, { add = false } = {}) {
const range = frontmatterRange(source);
if (!range) return { status: "invalid", source };
const block = source.slice(range.start, range.end);
if (ID_LINE.test(block)) return { status: "ok", source };

const fresh = `id: ${mintId()}`;

if (ANY_ID_LINE.test(block)) {
const next = block.replace(ANY_ID_LINE, fresh);
return {
status: "reminted",
source: source.slice(0, range.start) + next + source.slice(range.end),
};
}

const kind = KIND_LINE.exec(block)?.[1] ?? "";
if (!add || !MAY_CARRY_ID.has(kind)) {
return { status: "skipped", source };
}

const anchor = VERSION_LINE.exec(block) ?? KIND_LINE.exec(block);
if (!anchor) return { status: "invalid", source };
const at = anchor.index + anchor[0].length;
const next = `${block.slice(0, at)}\n${fresh}${block.slice(at)}`;
return {
status: "minted",
source: source.slice(0, range.start) + next + source.slice(range.end),
};
}

function usage() {
console.error(
[
"usage:",
" node scripts/mint-contract-id.mjs",
" node scripts/mint-contract-id.mjs --ensure [--add] <file>...",
" node scripts/mint-contract-id.mjs --check [--add] <file>...",
].join("\n"),
);
process.exit(2);
}

function main(argv) {
if (argv.length === 0) {
console.log(mintId());
return 0;
}
const mode = argv[0];
if (mode !== "--ensure" && mode !== "--check") usage();
const add = argv.includes("--add");
const files = argv.slice(1).filter((a) => a !== "--add");
if (files.length === 0) usage();

let failures = 0;
for (const file of files) {
const before = readFileSync(file, "utf8");
const { status, source } = ensureId(before, { add });
const changed = status === "minted" || status === "reminted";
if (status === "invalid") failures += 1;
if (mode === "--ensure" && changed) writeFileSync(file, source);
if (mode === "--check" && changed) failures += 1;
if (status !== "ok" && status !== "skipped") {
console.log(`${status.padEnd(8)} ${file}`);
}
}
return failures === 0 ? 0 : 1;
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exit(main(process.argv.slice(2)));
}
2 changes: 1 addition & 1 deletion skills/open-prose/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: open-prose
version: 0.17.0
version: 0.18.0
runtime_contract: 2
description: |
Activate when the user types `prose ...`, opens a `.prose.md` file with
Expand Down
41 changes: 38 additions & 3 deletions skills/open-prose/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ plan.
## Current Conventions

- Authored source files are `*.prose.md`.
- `kind: responsibility` files declare stable `id:` frontmatter. The id is
generated once by tooling as UUIDv7-compatible bytes, rendered as uppercase
Crockford base32, and preserved across display-name and filepath renames.
- `id:` frontmatter is optional on responsibilities and gateways: 26 characters
of uppercase Crockford base32 minted by `scripts/mint-contract-id.mjs`, for
identity that survives renames. The slug is the default identity.
- `version:` frontmatter is optional, author-owned provenance in semver form.
The compiler ignores it; it is not the skill version.
- `### Tools` applies to `function` and `responsibility`. Tool declarations
support both `cli:<name>` and `mcp:<name>` and fail closed when the host
cannot resolve a declared capability. Resolved responsibility tools are
Expand Down Expand Up @@ -45,6 +47,39 @@ plan.

## History

- `v0.18.0`: **Example corpus made compiler-clean.** `id:` frontmatter is now
optional on responsibilities and gateways: the slug is the identity by
default, and a declared id is the source identity that survives filename and
`name:` renames. A missing `id:` is no longer a compile error. Where an id is
present it must be 26 characters of uppercase Crockford base32, minted by
`scripts/mint-contract-id.mjs` (which also repairs a malformed id in place)
and never hand-typed; the hand-typed slug ids in the examples were dropped,
not replaced, since they named nothing the slug does not. `version:`
frontmatter is documented as optional author-owned provenance in semver form,
ignored by the compiler. `contract-markdown.md` gains *Facet families and
per-entity mounts*: a `#### name:<placeholder>` heading under `### Maintains`
declares a family, a placeholder facet-need in `### Requires` subscribes to
one member, `# Title [instance]` marks a contract mounted once per entity, and
the harness binds members at mount time while the compiler emits the family.
Every `###` heading in the examples is now a canonical section
(`### Continuity: external-driven` became `### Continuity` with an
`- external-driven` bullet; `### Postconditions` and `### Facets` folded into
`### Maintains`; `### Watches` moved into `### Receives`), and every
`### Requires` names a producer in its own example (competitor-activity gains
a `signal-feeds` gateway, research-inbox-triage a `research-registry`
gateway). Example READMEs whose node and edge counts exceed what mounting
`src/` alone produces now attribute the count to the reference harness's
per-entity expansion. `compiler/ir-v0.md` specifies node identity (`node` is
mount identity, defaulting to the slug for a single mount) and `artifact`
locators (paths relative to the OpenProse root); `concepts/reconciler.md`
gives the receipt `cost` field its sub-shape
(`{ provider, model, tokens: { fresh, reused }, surprise_cause }`).
Implementation-status claims in `spec/01-Language.md` and
`spec/03-AuthoringPattern.md` name the reference harness version they
describe. `runtime_contract` is unchanged (2): no source rewrite is needed.
`prose upgrade` may drop an `id:` that merely repeats the slug; it must never
add one.

- `v0.17.0`: introduced `prose init` / `prose compose` and the
obligation-centered `std/ops/compose` directory package, including bounded
architectural fronts, progressive Contract source, derived visual views,
Expand Down
16 changes: 9 additions & 7 deletions skills/open-prose/compiler/index.prose.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This is a pinned ProseScript compiler program. It is not a mounted node and is
not Forme-wired: the compiler itself owns its execution order and uses short,
isolated sessions to keep each lowering step on a narrow context budget. It is
the **intelligent compile phase**; the run phase that reads its output is dumb
(`architecture.md` §2).
(the two phases in `concepts/reconciler.md`).

### Parameters

Expand Down Expand Up @@ -98,9 +98,10 @@ agent responsibility_compiler:
Preserve Goal, Requires, Maintains, and Continuity as the node's contract.
Derive wake_source from Continuity: input-driven by default, self when a
cadence is declared, external for a gateway.
Use frontmatter `id:` as the responsibility identity backing the node. Never
derive identity from `name:`, filepath, title, or a slug; those are display
and source-location fields only.
The node key in the topology is mount identity, assigned at mount; a single
mount of a contract defaults it to the slug. Frontmatter `id:`, when declared,
is the source identity behind that node and survives renames. Never treat
filepath or title as identity.
Do not emit a judge activation, a verdict, pressure, or a fulfillment
activation; commit-gating is compiled postconditions plus render
self-attestation.
Expand Down Expand Up @@ -379,9 +380,10 @@ return write_result
```

Before forwarding to the compiler harness, the deterministic CLI preflights
the compile target source files for responsibility `id:` and required
`### Tools` sections, then resolves declared tools only within that target
(except `prose compile .`, which preserves whole-root preflight). After this
the compile target source files for well-formed `id:` frontmatter where present
and required `### Tools` sections, then resolves declared tools only within
that target (except `prose compile .`, which preserves whole-root preflight).
After this
program returns, the CLI validates the written manifest. That host validation
is the final guardrail; the compiler program should still treat `ir-v0.md` as
binding before it writes.
Loading
Loading