Skip to content

feat(cli): Canonical install store with per-tool reference stubs#21

Merged
thecodedrift merged 12 commits into
mainfrom
jakob/oss-8
May 18, 2026
Merged

feat(cli): Canonical install store with per-tool reference stubs#21
thecodedrift merged 12 commits into
mainfrom
jakob/oss-8

Conversation

@thecodedrift
Copy link
Copy Markdown
Member

Replace the per-tool full-copy install model with a single canonical store plus thin reference stubs. taskless init/update now writes the skill and command content exactly once — to .taskless/skills/<name>/SKILL.md and .taskless/commands/tskl/<name>.md — and drops a small delegating stub into each enabled tool directory (.claude/, .cursor/, .opencode/, .agents/).

Why

The old model wrote a full identical copy of every skill into each detected tool's directory. N tools meant N copies that drift and churn PR diffs. A customer standardizing on a shared skill hit the deeper flaw: the model conflated where content lives with where tools read it. Codex's install directory is .agents, so the Codex target's rm -rf cleanup destroyed the directory other targets pointed into — symlinks broke update, and wrapper files were clobbered with full copies.

Putting the canonical content in .taskless/ — a directory no tool target ever installs into or cleans up — makes that whole class of bug structurally impossible. Each tool directory holds only a featherweight stub, so there is one source of truth and no drift.

What changed

  • Canonical store: full skill/command content written once to .taskless/skills/ and .taskless/commands/.
  • Reference stubs: each enabled tool directory gets an ordinary file (never a symlink) with name/description frontmatter, a metadata: { type: shim, version } marker, and a body that delegates to the canonical file. .claude/ and .cursor/ also get a tskl command stub.
  • Per-target mode (canonical | reference) in .taskless/taskless.json — additive and backward-compatible (absent reads as canonical).
  • Self-healing applyInstallPlan: rewrites any reference file that is not a current shim stub — a full copy from an older install, a symlink, or a drifted stub — converging stale layouts on the next init/update. The destructive rm -rf glob cleanup is gone.
  • Wizard: the location step is reframed as "which tools do you want to enable Taskless for?".

Approach notes

  • No symlinks — symlink-based skill discovery is unreliable across Cursor, OpenCode, and Codex (open upstream bugs) and breaks on Windows checkout. Stubs are ordinary files.
  • No migration — convergence is handled by self-healing applyInstallPlan rather than a .taskless/ schema migration. A migration would form an import cycle through install.ts/state.ts/migrate.ts and would run on every ensureTasklessDirectory call (including taskless check).

Review focus

  • packages/cli/src/install/install.ts — the install-plan model and applyInstallPlan.
  • packages/cli/src/install/canonical.ts — canonical writes, stub builders, drift/shim detection.

Full design and the synced cli-init spec are in openspec/changes/archive/2026-05-17-cli-canonical-install/.

A redundant-info/checkStaleness follow-up is filed as OSS-9.

Refs OSS-8

thecodedrift and others added 11 commits May 17, 2026 17:30
Add the OpenSpec change for moving skill/command installation to a
single canonical .taskless/ store with thin reference stubs in each
tool directory, replacing per-tool full copies.

Also allow the WebSearch tool in .claude/settings.json, used while
researching cross-tool skill discovery behavior for this change.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce an InstallMode (canonical | reference) recorded per target in
.taskless/taskless.json install state. A canonical target stores full
skill/command content; a reference target stores thin stubs.

readInstallState normalizes a missing mode to canonical, so manifests
written before this field round-trip unchanged. computeInstallDiff
entries now carry the effective mode so removal logic can respect it
without a second state lookup.

No write or cleanup logic acts on the mode yet — this lands the schema
ahead of the install-engine changes that depend on it.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add install/canonical.ts: writeCanonicalSkill/writeCanonicalCommand
write full content verbatim into the .taskless/ store; buildSkillStub/
buildCommandStub produce thin reference files that carry name and
description frontmatter and delegate to the canonical path. Command
stubs preserve the canonical argument-hint and pass $ARGUMENTS through.

stubFrontmatterDrifted lets update leave a still-matching stub untouched
instead of regenerating it.

These helpers are standalone — the install plan wires them in next.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the routed .agents/ design with a uniform model: every selected
tool directory (.claude/.cursor/.opencode/.agents) is a peer target and
receives its own reference stub. No directory is special-cased or routed.

The wizard location step is reframed as a fixed tool-selection
multiselect. The migration converts existing full per-tool copies into
stubs rather than removing them.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-tool full-copy install model with a resolved
install-plan: one canonical .taskless/ target holding full content,
plus one reference stub target per selected tool directory.

applyInstallPlan branches on each target's mode — canonical targets get
verbatim content, reference targets get a drift-checked stub that is
never overwritten with full content. Removals are scoped to each diff
entry's own directory, so no target's cleanup can reach the canonical
store; the destructive rm -rf glob is gone. The wizard's location step
is reframed as a fixed tool-selection multiselect.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark every reference stub with a metadata.type: shim frontmatter
marker. applyInstallPlan now rewrites a reference file unless it is
already a current, non-drifted shim stub — converging full copies left
by older installs, symlinked entries, and drifted stubs on the next
init/update.

This replaces the planned .taskless/ convergence migration: a migration
would import install.ts (an import cycle through state.ts) and would run
on every ensureTasklessDirectory call, including taskless check. The
manifest mode field is additive, so no schema migration is needed.

Also add a migration version-matrix test that seeds .taskless/ at each
prior schema version and asserts a clean forward-migration.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the init/update help recipes, the CLI README, and the
.taskless/README.md Files section to describe the canonical .taskless/
store plus per-tool reference stubs. Add a changeset for the release.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stub frontmatter now records metadata.version alongside the
metadata.type: shim marker, copied from the canonical content. The
drift check compares version too, so a canonical version bump
regenerates every stub on the next update — keeping the stub's
version reference accurate rather than frozen at install time.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the pure detection-to-choices logic from promptLocations into
a testable locationChoices() function, and unit-test it: every shim
target is offered, the canonical .taskless/ store is never selectable,
detected tools are pre-checked, and .agents/ is the default when
nothing is detected.

The interactive multiselect itself still needs a TTY and stays covered
only by the mocked wizard-integration tests; this closes the gap on the
detection mapping that feeds it.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync the cli-canonical-install delta into openspec/specs/cli-init/spec.md
(5 added, 9 modified requirements) so the main spec reflects the
shipped canonical-store-plus-stub install model, and move the change
to openspec/changes/archive/2026-05-17-cli-canonical-install/.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uirement

The "Re-install computes a diff against the previous manifest"
requirement lacked a SHALL/MUST keyword, failing openspec spec
validation. Reword to make the diff computation and confirmation
normative. Pre-existing error, surfaced while syncing OSS-8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 05:22
@thecodedrift thecodedrift marked this pull request as ready for review May 18, 2026 05:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the per-tool full-copy install model with a single canonical store (.taskless/skills/, .taskless/commands/) plus thin reference stubs in each enabled tool directory. This fixes a destructive-cleanup bug where Codex's .agents/ target's rm -rf could wipe content other targets pointed into, and ends the N-identical-copies drift problem.

Changes:

  • Introduces canonical store under .taskless/ with new canonical.ts (writes + stub builders + shim detection) and adds a per-target mode (canonical | reference) to the manifest (legacy entries default to canonical).
  • Refactors applyInstallPlan to be mode-aware and self-healing: rewrites any reference file that isn't a current shim stub (full copies, symlinks, or drifted stubs), and scopes manifest-driven cleanup to each target's own directory so it can never reach .taskless/.
  • Reframes the wizard's location step as a tool-selection multiselect over a fixed catalog (SHIM_TARGETS), removes AGENTS_FALLBACK plumbing and installForTool, and updates init/update non-interactive flow and help text accordingly.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/cli/src/install/install.ts Adds canonical/reference plan model, mode-aware writes, self-healing reference rewrite, removes glob cleanup and AGENTS_FALLBACK
packages/cli/src/install/canonical.ts New module: canonical write helpers, skill/command stub builders, shim detection, drift checks
packages/cli/src/install/state.ts Adds InstallMode, plumbs mode through state read/write and diff
packages/cli/src/filesystem/migrate.ts Adds optional mode field to manifest target type
packages/cli/src/filesystem/migrations/0001-init.ts Adds README entries for skills/ and commands/ canonical dirs
packages/cli/src/wizard/steps/locations.ts Refactors to pure locationChoices over SHIM_TARGETS; new prompt wording
packages/cli/src/wizard/index.ts Switches to buildInstallPlan / planToStateTargets; drops TOOL_BY_INSTALL_DIR
packages/cli/src/commands/init.ts Non-interactive flow uses new plan model + per-target summary with mode-aware noun
packages/cli/src/help/init.txt, update.txt Doc updates for canonical-plus-stub model
packages/cli/README.md, .taskless/README.md User-facing docs for new layout
packages/cli/test/* (apply-install-plan, install, install-state, migrate-install, canonical-store, wizard-steps) Rewrites/adds tests for canonical writes, stubs, drift, symlink/full-copy convergence, mode round-trip, migration matrix
.changeset/canonical-install.md Minor changeset documenting the new install model
openspec/changes/archive/2026-05-17-cli-canonical-install/* + openspec/specs/cli-init/spec.md Archived change proposal/design/tasks and synced spec
.claude/settings.json Adds WebSearch permission (unrelated)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

… output

init-no-interactive.test.ts and cli.test.ts exec the built CLI bundle
and asserted the old per-tool summary line ("<Tool>: installed"). The
canonical-install model prints "<Tool> (<dir>/): wrote N skill ..."
instead. These were missed locally because the tests ran against a
stale dist/ build; CI's fresh build caught them.

Refs OSS-8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thecodedrift thecodedrift merged commit 49bb95e into main May 18, 2026
2 checks passed
@thecodedrift thecodedrift deleted the jakob/oss-8 branch May 18, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants