Publish an Ideaspace
Goal: login check → plan (publish without --yes, zero mutations) → the user agrees →
apply (publish --yes) → narrate result. The plan-then-apply split is the CLI's contract, not a
courtesy — the outward tier of the agreement principle.
Pi command available: for human-triggered publishing, prefer /is-publish. It checks scaffold/branch state, confirms destination, runs the same CLI publish command, and offers login/retry if the CLI reports missing credentials. Use this skill when the agent needs the protocol, when /is-publish is unavailable, or for recovery reasoning after a failed publish.
This skill is the conversational layer around the IdeaSpaces CLI. The extension exposes the resolved CLI as $IS_CLI_PATH when available. Define this helper in any bash command that invokes the CLI:
is_cli() {
if [ -n "$IS_CLI_PATH" ] && [ -f "$IS_CLI_PATH" ]; then
case "$IS_CLI_PATH" in
*.js) node "$IS_CLI_PATH" "$@" ;; # dev: a .js bundle needs node
*) "$IS_CLI_PATH" "$@" ;; # a compiled sidecar runs directly
esac
else
ideaspaces "$@"
fi
}
No separate install required.
1. Pre-flight checks
Inside an ideaspace? This dir should be a git repo with _agent/foundation.md already scaffolded. If not, suggest /is-setup first.
test -f _agent/foundation.md && test -d .git && echo "ok" || echo "missing"
Portable identity agrees? Current shared scaffolds declare root_node_id; legacy Spaces may validly omit it. Never mint, edit, stage, or commit identity during publish. Run is_cli status --json and inspect root_identity: stop on invalid, drift, ambiguous, or declaration.dirty. The CLI repeats this preflight against HEAD, index, worktree, canonical origin, and local registry before login or remote mutation.
Markdown frontmatter parses? ideaspaces publish preflights all tracked Markdown for YAML syntax. If it fails, surface the CLI output and ask the user to fix and commit the reported YAML.
On the main branch? IdeaSpaces uses main as the default branch — publishing requires the local branch to match so server and clones stay aligned. Detect:
git rev-parse --abbrev-ref HEAD
If output is the literal string HEAD, the user is in detached-HEAD state. Don't offer a rename — short-circuit with: "You're in detached-HEAD state. Check out a branch first (e.g. git checkout main) and re-run /is-publish."
Otherwise, if output isn't main, ask before proceeding:
"You're on
<current-branch>. IdeaSpaces usesmainas the default — keeping local and remote consistent makes futuregit pull/ clones work without surprises. Rename<current-branch>→mainfor this folder?"
In a non-interactive session there is nobody to answer — never rename on your own; stop with the question as the result. If yes, run git branch -m main. If the rename fails (most common cause: a local main branch already exists — perhaps stale or orphaned), surface git's error verbatim and stop with: "You may already have a local main branch. Resolve manually (git branch -d main if it's stale, or git checkout main if it's the one you want) and re-run /is-publish." On success, continue. If the user declines the rename, abort: "Switch to main and re-run /is-publish when ready." — don't try to push a non-main branch; ideaspaces publish refuses anyway.
Logged in? Check the credentials file directly — its presence is the login signal:
test -f ~/.ideaspaces/credentials.json && echo "yes" || echo "no"
If no, propose login. If yes, continue.
Already published? Check the folder-keyed map, if present:
node - <<'NODE'
const fs = require('fs');
const path = `${process.env.HOME}/.ideaspaces/spaces.json`;
if (!fs.existsSync(path)) { console.log('null'); process.exit(0); }
const map = JSON.parse(fs.readFileSync(path, 'utf8'));
console.log(JSON.stringify(map[process.cwd()] || null));
NODE
If non-null, this folder is already mapped to a remote. Re-publishing is fine — the CLI reuses the existing repo_id and pushes to the same remote.
2. Login if needed
"You'll need to log in first — that's how IdeaSpaces knows the space belongs to you. I'll open a browser; complete the OAuth flow there and credentials save locally. OK?"
On confirm:
is_cli login
If the user is in a remote shell or browser open fails, surface the CLI output and let them decide the next step.
3. Plan — run publish without --yes
The CLI is plan-first: without --yes it runs every preflight, prints exactly what would happen —
destination namespace/slug, the dir-local git identity write, whether the tip-commit author gets
rewritten, the origin URL, the commit count — and changes nothing, local or remote.
is_cli publish [--slug ...] [--name ...] [--hostname ...]
Show the user the plan and get their yes. Publishing is an outward action: even when the user
already said "publish this", the plan surfaces side effects they haven't seen — one look before it
happens is the agreement, not friction. Flag overrides: --slug <name>, --name "<display>",
--hostname <host> for org spaces (first publish only). For re-publish the plan says it reuses
the existing Space identity — don't re-ask names.
Non-interactive sessions stop here. With nobody to agree, the plan output is the honest
result; never add --yes on the user's behalf.
4. Apply — run with --yes
On the user's yes:
is_cli publish --yes [same flags as the plan]
The CLI:
- Evaluates the committed foundation against index/worktree, canonical origin, and local registry evidence.
- Preflights tracked Markdown syntax and size before network work.
- Confirms login and asks Keeper to adopt the exact committed
root_node_idon first publish. - Creates or reuses the one matching hosted Space;
--forcecannot fork or rekey it. - Sets repo-local Git attribution, configures the matching canonical origin, and pushes
main. - Records the verified folder ↔ Space binding in
~/.ideaspaces/spaces.json.
Size-cap recovery (oversized tracked files)
If the CLI exits 1 with Cannot publish yet: N tracked file(s) exceed the 200,000-byte server limit. followed by a list of path (bytes) lines, the offenders are tracked files larger than the server cap. The CLI fails fast locally — no push attempted. Parse the offender list and decide:
Known clutter — if every offender path matches one of these patterns, offer the conversational fix as a single yes/no:
.obsidian/ node_modules/ .DS_Store .cache/ .idea/ .vscode/
"I see
<matched paths>tracked — that's <vault config / build output / editor metadata>, not your knowledge. I can append to.gitignore, untrack withgit rm --cached -r <path>, commit, and retry publish. OK?"
Before changing anything, require a clean index with git diff --cached --quiet. If it is not clean, stop and ask the user to commit or unstage their existing work first — never mix publish cleanup with an in-progress commit. Also require git status --short -- .gitignore to be empty before appending. If it is not empty, stop and surface the pre-existing modified or untracked file instead of absorbing it into the cleanup commit.
On confirm, run in this order:
- Append the matching patterns to
.gitignorewithout duplicating existing lines, then stage only that file withgit add -- .gitignoreif it changed. - Run
git rm --cached -r -- <path>for each confirmed clutter path. This removes it from git while preserving the local files. - Review
git diff --cached --name-status. Every staged entry must be.gitignoreor a removal under the confirmed clutter paths. If anything else is staged, stop — do not commit or publish. - Only with that exact staged set, run
git commit -m "Untrack non-publishable clutter". The clean-index gate and staged-set check are mandatory because this commit intentionally records the prepared index; skipping either could sweep unrelated work. - Re-run
is_cli publishwith the previously confirmed arguments.
If any step fails, leave the state visible with git status --short and stop for the user to review.
Mixed or unknown offenders — if any offender is outside the clutter list (e.g. a 5 MB image the user might want), don't auto-fix. Surface the CLI output verbatim and stop with: "These files are over the 200KB cap. Shrink them, store externally, or link via frontmatter (attached_to:). Re-run /is-publish when resolved." — the user might have intent for that file.
5. Narrate result
On success, surface the remote URL and the local changes:
"Published
<name>to<remote_url>. This folder's git identity is nowperson:<username>@ideaspaceslocally, so server-side attribution works. The folder mapping is saved at~/.ideaspaces/spaces.json."
Failure modes
| Symptom | Likely cause | What to suggest |
|---|---|---|
Cannot publish yet: markdown frontmatter is invalid. |
Malformed YAML frontmatter | Fix the reported YAML syntax, commit the repair, and re-run publish. |
Not logged in |
No stored credentials | Run ideaspaces login. |
Cannot publish yet: N tracked file(s) exceed the 200,000-byte server limit. |
CLI size preflight | See "Size-cap recovery" above — auto-handle known clutter, surface the rest. |
Push failed: ... size cap |
Server-side cap (only if CLI preflight is bypassed) | Same as above; re-run /is-publish so the local preflight surfaces the offender list. |
Push failed: ... attribution doesn't match |
Commit author doesn't match account | Re-run publish; it sets local user.email. Amend/recommit if needed. |
Local branch is \`; IdeaSpaces uses `main`` |
Pre-flight didn't run / user invoked CLI directly | Rename via git branch -m main and retry, or use /is-publish which offers the rename. |
Couldn't determine the current branch — is HEAD detached? |
Detached-HEAD state (rare; pre-flight catches via skill) | Check out a branch (git checkout main) and retry. |
Root identity evidence is invalid / Root identity drift / Root identity is ambiguous |
Foundation, origin, or registry evidence is unsafe | Stop. Inspect is_cli status --json; repair evidence explicitly without rewriting identity. |
The root identity declaration differs between HEAD, the index, and the worktree |
Identity has an uncommitted change | Commit the intended declaration or restore it; never publish a different worktree value than HEAD. |
publish --force cannot fork or rekey |
Existing hosted binding | Re-publish normally, or use the explicit local Fork lifecycle in a separate destination. |
--name only apply on first publish |
Re-publish path | Drop the flag. A new name/identity requires an explicit local Fork, not --force. |
Recovery posture: re-running publish is safe after repair. Never delete or replace a mapping merely to make publish proceed: restore the matching remote/access, use ideaspaces link when the hosted identity is known, or create an explicit local Fork in a separate destination when the user intends a new Space.
What comes next
- is-capture — propose saving knowledge during work
- is-reflect — propose updating direction when it drifts
- is-space — navigation reference