intentional-core 0.1.2

Core model and release planning for intentional
Documentation

relationships: implements: intent-driven-polyglot-release

intentional

intentional is an intent-driven polyglot release and versioning CLI. Pending intent files determine what changes, annotated Git tags record what was released, and manifests are format-preserving projections of that state.

The tool supports npm, Cargo, Pub, PEP 621 Python projects, MSBuild projects, Go modules, Dev Container Features and Templates, and arbitrary JSON, TOML, and YAML version fields. It writes release state in the working tree and creates annotated tags. The surrounding harness owns commits, pushes, publication, registry observation, and forge operations. Generated tag templates do not prefix versions with v.

Install

With Homebrew:

brew tap wyrd-company/tools
brew install intentional

With Cargo:

cargo install intentional-cli --locked

With npm:

npm install --global @wyrd-company/intentional

Or run the scoped package without a global installation:

npx --yes @wyrd-company/intentional --version

The npm package downloads the matching checksum-verified GitHub Release binary. It supports Linux x64 and arm64, macOS arm64, and Windows x64. GitHub Releases also publish those archives and SHA256SUMS for direct installation. Other platforms can build from source with cargo install --path crates/cli --locked.

The minimum supported Rust version is 1.85. The library crate is intentional-core; the binary package is intentional-cli and installs the intentional executable.

Initialize a repository

intentional init

Initialization requires a Git repository, whose tags provide release authority and whose ignore rules bound discovery. It uses package-manager workspace membership and manifest-native package names. Repeated directory basenames do not collide because directories are not package identities. --scan-all explicitly includes supported manifests outside the declared workspace. Both modes honor Git ignore rules and always skip version-control metadata, Intentional state, and ecosystem caches; ordinary names such as build, dist, bin, and vendor are scanned unless the repository ignores them.

Every newly discovered npm, Cargo, Go, Python, MSBuild, Dart, Dev Container Feature, or Dev Container Template manifest first appears in .intentional/init-plan.yml as an unresolved candidate. Choose independent, projection, or excluded, then rerun intentional init to write configuration only after the complete candidate graph validates. New configurations use the compatibility pre-1.0 bump mapping.

$schema: https://intentional.foo/schemas/config.yml
contract: contract-1
settings:
  internal-dependency-bump: patch
  pre-1-0-bump-mapping: compatibility
workspace-tags:
  release:
    template: "{version}"
release-units:
  library:
    path: packages/library
    projections:
      - adapter: npm
        file: package.json
        mode: committed
      - adapter: pub
        file: pub/library/pubspec.yaml
        mode: committed
    tags:
      primary:
        role: primary
        template: "{id}@{version}"
      pub:
        role: projection
        template: "library-pub@{version}"
        require-phase: before-publication
  application:
    path: packages/application
    depends-on: [library]
    projections:
      - adapter: npm
        file: package.json
        mode: committed
    tags:
      primary:
        role: primary
        template: "{id}@{version}"

Every release unit has exactly one primary tag and any number of projection tags. All tags for one logical release carry the same version, target commit, interpretation contract, and plan digest. Named workspace tags provide repository-level records and continuous-delivery triggers without becoming a release unit's version authority.

When init reconciles an existing configuration, current non-development npm manifest dependencies own edges between npm release units. Removing such a manifest dependency removes its depends-on edge. Configured edges to release units without an npm projection remain authoritative because an npm manifest cannot represent those cross-ecosystem or non-native ordering relationships.

Projection modes are:

  • committed: apply writes the release version.
  • injected: stamp writes the computed build version.
  • none: no manifest version is written; Go uses this mode.

Generic json, toml, and yaml projections also require a pointer, such as /metadata/version. A release unit with no version-bearing projection is tag-only.

Discovery candidates and receipts

The shared detector contract uses one artifact-neutral candidate shape. Candidate ids are stable hashes of the detector id and exact workspace-relative path. File digests, native identity, and raw version text remain initialization evidence. Projection and tag suggestions appear only when extraction supplies the fields they require. Extraction diagnostics report unreadable or missing fields but do not claim that an artifact is publishable.

The devcontainer-feature.json and devcontainer-template.json detectors are distinct. Each reads only the top-level id and version, suggests the id as native identity, and exposes a committed JSON projection at /version when the version is Semantic Versioning 2.0.0. Missing, unreadable, or unsupported identity/version evidence produces an extraction diagnostic. These detectors do not inspect companion files such as install.sh or devcontainer.json, nor do they inspect workflows, OCI registries, publication state, or overall artifact correctness.

An initialization-plan candidate has this shape (shown as an excerpt):

discovery-candidates:
  - id: candidate:d888bb15d92b6478bbb66e9f2a01a12da7113e30d3a59440365a25e8f317ce0f
    detector: sample-manifest
    path: packages/library/manifest.json
    evidence:
      - path: packages/library/manifest.json
        digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
    native-identity: library
    raw-version:
      value: "1.2.3"
      evidence:
        - path: packages/library/manifest.json
          digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
    projection:
      adapter: json
      path: packages/library/manifest.json
      mode: committed
      pointer: /version
    tag:
      id: primary
      role: primary
      template: "{id}@{version}"
    resolution:
      kind: independent
      release-unit: library

A resolution is independent, projection, or excluded. Independent and projection resolutions name the final release-unit id. A projection names target-candidate when it follows an independent candidate in the same plan; without that field it targets an already configured release unit. Initialization rejects duplicate release-unit creators, absent targets, identity disagreement, and projection cycles.

Applied choices leave generic discovery receipts in canonical configuration:

discovery:
  managed-paths:
    - detector: sample-manifest
      path: packages/library/manifest.json
      release-unit: library
  excluded-paths:
    - detector: sample-manifest
      path: examples/example/manifest.json
      evidence-digest: sha256:0000000000000000000000000000000000000000000000000000000000000000

Managed receipts preserve the chosen release-unit relationship. Excluded receipts bind the exact detector/path identity to its evidence digest, so changed evidence requires a new resolution rather than silently inheriting an old exclusion. Paths are literal workspace-relative paths, not globs. Init scans configured repositories too: matching receipts make reruns no-ops, while new or changed excluded paths reopen the same candidate workflow.

Adopt a Changesets repository

When .changeset/config.json exists, ordinary intentional init preserves Changesets authority and writes .intentional/init-plan.yml. The plan contains source fingerprints, inferred configuration, stable diagnostics, exact source evidence, finite choices, recommendations, editable resolution fields, planned operations, pending-intent conversions, and a parity comparison for each release unit.

intentional init
# edit only the plan's resolution fields and make the reported repository edits
intentional init
intentional init --take-over --dry-run
intentional init --take-over

Initialization exits with:

  • 0 for a complete ordinary initialization, a ready adoption plan, or a completed takeover.
  • 2 when the plan needs resolutions or verified repository edits.
  • 1 for invalid input or operational failure.

intentional init --json emits the same plan as stable structured JSON. Reruns preserve resolutions while their evidence remains valid, report stale resolutions, and verify repository-integration edits from actual file state.

Changesets ignore entries become explicit suspended, excluded, or managed choices. suspended preserves release-unit identity while blocking any release that requires it, so it is the default recommendation unless repository evidence supports another disposition.

Pending Changesets Markdown bodies and package bumps are converted losslessly. Fixed and linked groups, npm dependency ranges and peer edges, internal dependency policy, and component-mapped pre-1.0 versions participate in the parity comparison. Private-package version and tag settings are recorded explicitly because Intentional separates version management from publication privacy and annotates every managed release. Unsupported or repository-specific behavior remains an explicit diagnostic. Repository release metadata and release scripts are retained as external evidence; Intentional does not interpret their private schemas or infer package, tag, publication, or ordering semantics from them. Cross-projection identity is provided only through explicit discovery-candidate resolutions. When a Changesets ignore entry names a native package that a candidate resolution projects onto another managed release unit, source parity is undefined at the package boundary. The target remains in both inventories and takeover stays blocked until the ignore entry is removed or the candidate resolution is revised. Changesets' range-conditional peer-dependent option is one such explicit contract choice because Intentional applies configured depends-on edges uniformly; any pending-release difference remains a parity blocker. Internal devDependencies participate in the Changesets source computation but are not authored as durable Intentional depends-on edges; adopting that contract is also explicit, and current divergence remains blocking.

--take-over is the only authority handoff. It refuses unresolved, stale, or non-equivalent plans, then performs a rollback-capable transaction that writes canonical Intentional state, moves pending intents, removes recognized .changeset/ state, and consumes the one transient initialization plan. References in workflows, package scripts and dependencies, lockfiles, release scripts, and tests are reported with exact locations; Intentional verifies the edits but does not rewrite those files.

After committing the takeover changes, establish canonical tag authority:

intentional tag --baseline

Baseline inference succeeds only when every version-bearing projection of a release unit agrees. Tag-only release units and new workspace tag streams require explicit versions:

intentional tag --baseline \
  --version library=1.2.3 \
  --version workspace/release=1.2.3

status and check report the missing-baseline diagnostic and command until the configured primary baselines exist.

Version interpretation and release groups

settings.pre-1-0-bump-mapping selects one of two contracts:

  • component: major, minor, and patch increment the corresponding SemVer component. This matches Changesets and is recommended during adoption.
  • compatibility: before 1.0, major advances 0.x.y to 0.(x+1).0, while minor and patch advance to 0.x.(y+1). New repositories use this mapping.

At and after 1.0, both mappings use ordinary SemVer component increments.

fixed:
  - [library, application]
linked:
  - [service, utility]

When any fixed member receives a direct or dependency-propagated bump, every member releases at one version derived from the group's highest current version and highest requested bump. A suspended fixed member blocks the shared release.

Linked groups release only affected members. Releasing members use the highest current version across the whole group and the highest bump among affected members; unaffected members retain their current versions. A suspended linked member blocks only when it would release. A release unit belongs to at most one fixed or linked group.

Author and inspect intents

intentional add \
  --release-unit library:minor \
  --release-unit application:patch \
  --message "Add a useful capability."

intentional status
intentional check

Without flags, add prompts for a release unit, bump, and changelog message. Intent frontmatter maps release-unit ids to bumps, and the Markdown body supplies release notes.

status lists pending intents, tag-derived current versions, computed next versions, suspended release blockers, missing baselines, and manifest drift. check validates configuration and intents, baseline presence, and deterministic release planning.

Plan, apply, and tag

intentional plan > release-plan.json
intentional apply

git add -A
git commit -m "chore: apply release"

intentional tag --plan release-plan.json

plan writes compact canonical JSON sealed by SHA-256. The sealed payload contains the interpretation contract, generator identity, release units, old and new versions, contributing intent ids, rendered release notes, release-unit and workspace tags, phase declarations, tag-after prerequisites, and tag order. It contains no publication graph.

apply updates committed projections, internal dependency ranges, and changelogs, then consumes final-release intents. It changes only the working tree. tag verifies the supplied sealed plan against the materialized tree and creates annotated records at the release commit. Without --plan, a final release is recovered from the intents deleted by the release commit; a channel release is recovered from its retained intents.

A tag with require-phase is created only by an equal executor declaration:

intentional tag --plan release-plan.json --phase before-publication
intentional tag --plan release-plan.json --phase after-publication

The declaration does not assert or verify publication. tag-after expresses only observable tag prerequisites; Intentional verifies prerequisite tag records before creating dependent tags. External continuous delivery owns all publication sequencing and registry evidence.

Stamp build versions

For an injected projection, stamp the intent-derived version without touching changelogs or intents:

intentional stamp
intentional stamp --prerelease alpha

The prerelease form composes the next intent version with first-parent commit height since the latest matching primary tag, for example 1.3.0-alpha.5.

Channel releases

Channel state derives from tags:

intentional plan --channel beta > release-plan.json
intentional apply --channel beta
git add -A
git commit -m "chore: apply beta release"
intentional tag --channel beta --plan release-plan.json

The first release is X.Y.Z-beta.1; each iteration advances from existing tag records. Channel applies retain intents. A later final apply replaces channel changelog sections with the consolidated final section.

Dry runs

Every mutating command supports --dry-run and prints the same operation set as the real invocation:

intentional init --dry-run
intentional init --take-over --dry-run
intentional add --release-unit library:patch --message "Correct a defect." --dry-run
intentional apply --dry-run
intentional stamp --prerelease alpha --dry-run
intentional tag --dry-run
intentional tag --baseline --dry-run

GitHub Action

The plumbing action downloads a released Linux binary and runs one command. Pin it to a plain SemVer tag. The action exposes canonical plan JSON and a changed-release-unit boolean. It accepts status, plan, stamp, apply, and tag; channel applies to release commands, prerelease applies to stamp, and dry-run applies to mutating commands.

Development checks

task build
task fmt-check
task lint
task test
task ci