chore(repo): merge opensop-cli into cli/ (subtree, history preserved)#67
Merged
Conversation
Thin client over the /sop/* HTTP API. 9 subcommands (list, schema, run, status, steps, submit, cancel, instances, config), tiny local cache for id→name resolution, TTY-aware pretty/JSON output. ~640 lines of bash; deps are curl + jq. Verified end-to-end against demo.opensop.ai: - list / schema / run / status / cache populate all green - submit / cancel / instances share the same api_call plumbing README + docs/CLAUDE-INTEGRATION.md included for agent onboarding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the highest-impact items from the Darwin team's adoption wishlist: ranked text search, inverse-retrieval suggest, tag filtering on list. Plus README updates with a worked example, sample list output, and the local-cache note lifted to a visible section. The framing change: discovery latency is the moat. These three commands turn `opensop list` from "remember the name" to "describe the intent" and are the load-bearing addition for agents to reach for OpenSOP first. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alidate, --local
Adds the rest of the Darwin agent-adoption wishlist:
Tier 3 (inspection + planning):
diff — compare two instances of the same process
compass — top processes by run-count, recency, failure-rate
history — discoverable alias for `instances --process X`
dry-run — client-side preview: validates inputs against schema,
walks steps and describes what each WOULD do, no execution
Tier 4 (process authoring):
register — POST a .sop.yaml to /sop/processes/register
schema validate — fully client-side YAML lint (yq or python3)
--local — global flag: override OPENSOP_URL to localhost:3000
The split with v0.2.0 was: search/suggest/list --tag = "find what
already exists" (consumer); v0.3.0 = "see what's there, see what
would happen, ship something new" (consumer + author). Together,
the wishlist's full Tier 1-4 surface is in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cy, by_failure_rate}
v0.3.0 documented the compass --json output as
{by_runs: [...], by_recency: [...], by_failure_rate: [...]}
but emitted a flat array. Brings the implementation in line with the
spec so agents and scripts can consume the three named dimensions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the Darwin wishlist's Tier 2 #8 ("structured error responses"). Today, server errors round-trip cleanly as {error, message, ...} via api_call. CLI-side errors (config missing, file not found, jq parse, etc.) printed prose to stderr regardless of --json mode — agents couldn't reliably parse them. This release: when --json is set, die()/err() emit {error, message, hint?} to stderr from the same JSON envelope. Prose-mode default (TTY) unchanged — backward-compatible. Adds CHANGELOG.md so fresh agents and contributors can see at a glance what's been shipped without grepping git log. Five entries: 0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.4.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… corpus widening Addresses four issues from Darwin's real-use testing on coba.opensop.ai (see proposals/opensop-agent-adoption-wishlist.md "Real-use findings"): A. cmd_history and cmd_instances now populate the local id→name cache from each response row, so subsequent `status <id>` doesn't have to scan /sop/instances. B. lookup_name's cache-miss fallback now paginates up to 1000 rows (5 pages of 200) instead of stopping at the first 200. C. search/suggest now tokenize queries on whitespace and score each token independently. Hyphenated process names also tokenized on - and _ so `search "morning briefing"` matches `darwin-morning-briefing`. D. The search/suggest corpus now also indexes inputs_summary and outputs_summary (already in the /sop/ discovery response, just unused). ~30% recall lift expected for "what produces X?" intents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a second backend to the one CLI: `opensop run X.sop.json --local` runs processes on-machine (bash + jq, no server/curl/network), threading a JSON context between steps and writing append-only receipts. New commands: runs, show, list --local. Includes review hardening of the failure path (set -e), curl gating, run lifecycle, and failure-path regression tests. BREAKING: --local now means local execution (no longer aliases OPENSOP_URL to localhost:3000).
Promote the Unreleased --local work to 0.5.0: bump OPENSOP_CLI_VERSION and the CHANGELOG section, including the review-hardening fixes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CLAUDE.md: single-file architecture, the two-backend (remote vs --local) split, the cross-cutting output/error contract, set -e/set -u bash invariants, and the add-a-command / release loops. Fix the README Configuration section, which still described --local as aliasing OPENSOP_URL to localhost:3000 — stale since v0.5.0, where --local means local execution. Point at OPENSOP_URL=... for a dev server. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: cell primitive (v0.6) — opensop init + opensop scope
Introduces the cell primitive that grounds the v0.6 substrate work: a
directory marked by .opensop/manifest.yaml. Cells nest; init creates
one in cwd; scope walks up from cwd and prints the active cell + its
ancestor chain.
* opensop init [--name N] [--parent PATH]
Creates .opensop/manifest.yaml + .opensop/{runs,archive}/ + an empty
.opensop/lineage.json. Defaults: --name to cwd basename; --parent
auto-detected by walking up for an ancestor cell (null if none).
Refuses to clobber an existing .opensop/.
* opensop scope
Prints active cell + ancestor chain, nearest-first. JSON or pretty.
Exits non-zero when cwd is not inside any cell.
Pure-additive. No existing command behavior changes — verified by the
full existing test suite passing untouched. Foundation for the rest of
v0.6 (lineage, per-cell receipts, name resolution across cells, fork
mechanic, executor field), which will land in follow-up PRs.
manifest.yaml uses a tiny grammar (name + parent on their own lines)
parsed by a 6-line grep+sed helper to avoid adding a yaml dependency.
lineage.json (not .yaml) is initialized empty for the same reason; the
forthcoming lineage commands will read/write JSON consistent with the
existing .sop.json convention.
Tests:
* init creates the .opensop/ tree as root cell (parent: null)
* init auto-detects ancestor cell as parent ("../")
* init refuses to clobber an existing .opensop/
* scope walks ancestor chain (child + parent = 2 entries, correct order)
* scope from root cell shows only itself
* scope outside any cell exits non-zero
* explicit --name / --parent flags honored
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(help): mention parent auto-detection + scope error condition
Two small clarifications to the CELLS help block, surfaced by a
docs-only readability test:
* init: note that parent is auto-detected when cwd is inside an ancestor
cell. Previously only the README mentioned this — a first-time user
reading only --help might pass --parent ../ defensively.
* scope: note that it errors when cwd is not inside any cell. Previously
the failure-mode was only discoverable by running it outside a cell.
No behavior change. Help text only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Carlos <carlos>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) * feat: lineage primitives (v0.6) — opensop annotate + opensop lineage Substrate-level event log per skill, stored in .opensop/lineage.json per cell. Sits on top of the cell primitive from PR #6. * opensop annotate <skill> <event-type> <json> Append a policy event to the skill's lineage history in the active cell. Creates the lineage entry with default fields if it doesn't exist. Event type is open-string; data is whatever JSON the policy needs. Records ISO 8601 UTC timestamp. * opensop lineage <skill> Print the skill's lineage entry (logical_name, forked_from, status, metadata, history) in the active cell. Returns the empty default entry if no events have been recorded for that skill yet. Policy-neutral: the substrate stores `status` (open string), `metadata` (open object), and `history[].type` (open string) — it does NOT interpret any of them. Evolution policies (mineralization, maturity grades, formal-verification gates, tag-based capability, etc.) sit on top to record promotions, demotions, locks, archives, blesses, etc. Lineage entry schema (per skill, keyed by logical_name): { logical_name, forked_from, history: [{at, type, data}], status, metadata } Atomic writes via temp + mv. Corruption guard: invalid JSON in lineage.json triggers an invalid_json error rather than silent acceptance. Tests: 9 new cases * annotate creates lineage entry + appends first event * annotate appends to existing entry, preserves order + prior events * lineage returns full entry with correct shape (json round-trip) * lineage returns empty default for never-annotated skill * annotate rejects invalid JSON data (invalid_json) * annotate rejects missing args (usage_error) * annotate errors when not inside a cell (usage_error) * lineage errors when not inside a cell (usage_error) * lineage refuses to read a corrupt lineage.json (invalid_json) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cell + lineage commands now respect TTY auto-mode The four v0.6 commands (init, scope, annotate, lineage) were calling _resolve_output_mode via command substitution: $(_resolve_output_mode). Inside that subshell, stdout is the capture pipe — not the terminal — so is_tty (which checks [[ -t 1 ]]) returned false even when the user was in a real TTY. Result: those four commands always emitted JSON in the terminal, ignoring the documented "pretty in TTY, JSON when piped" contract. Fix: inline the check using the same pattern the rest of the codebase uses (cmd_list, cmd_status, cmd_steps, etc.): if is_tty && [[ "$OUTPUT_MODE" != "json" ]]; then # pretty else # json fi is_tty is now evaluated in the parent shell where stdout actually IS the terminal, so the auto-mode resolves correctly. Tests: added 4 regression cases — one per command — that verify --pretty explicit override produces prose (not JSON) even when called from a non-TTY context (the test harness). These would have caught the bug had they existed at PR #6 ship time. Reported by a real-terminal test by Carlos; confirmed with [[ -t 1 ]] && echo "tty-detected". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: cell + lineage output-mode check now honors --pretty in non-TTY Follow-up to 8e48114 which only fixed half the bug. The simple inline pattern `is_tty && [[ "$OUTPUT_MODE" != "json" ]]` works in auto-mode inside a TTY, but ignores `--pretty` when called from a non-TTY (the 4 --pretty regression tests added in 8e48114 were failing on this). Replacing with three-state logic that matches what _resolve_output_mode does (but without the subshell capture bug that broke the original): if [[ "$OUTPUT_MODE" == "pretty" ]] \ || { [[ "$OUTPUT_MODE" == "auto" ]] && is_tty; }; then # pretty else # JSON fi Resolves all three documented states: --json explicit → json --pretty explicit → pretty no flag + TTY → pretty no flag + pipe → json All 24 tests now pass (20 prior + 4 --pretty regression). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: cell-aware default for OPENSOP_LOCAL_HOME (v0.6) When cwd is inside an OpenSOP cell AND the user has not explicitly set OPENSOP_LOCAL_HOME, local-mode receipts now default to the active cell's .opensop/ instead of the global ~/.opensop-local. Receipts land alongside the processes that produced them; each cell has its own receipt history. This is the first v0.6 PR that changes existing behavior, but it's backwards-compatible: the cell-aware default only kicks in when a .opensop/ marker exists in cwd's path, and no pre-v0.6 user has one. Explicit OPENSOP_LOCAL_HOME=… always wins. Resolution order for OPENSOP_LOCAL_HOME: 1. Explicit env var (if user set it) → wins 2. Active cell's .opensop/ (v0.6, new) → if cwd inside a cell 3. ~/.opensop-local (existing default) → otherwise The resolution happens in main() after helpers are loaded (so _find_scope_root is available). Constants section now tracks _OPENSOP_LOCAL_HOME_EXPLICIT so the late resolution knows whether to honor the user's explicit choice. Tests: 3 new cases * runs — inside cell, default OPENSOP_LOCAL_HOME → receipt in cell * runs — explicit OPENSOP_LOCAL_HOME wins over cell-aware default * runs — `opensop runs` inside cell reads cell receipts only Docs: * --help ENVIRONMENT now lists OPENSOP_LOCAL_HOME with new default * README's "Local execution" intro explains the cell-aware default * CHANGELOG [Unreleased] gets a Changed section Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: add coverage for corrupt cell + nested-cell routing Folding in the two coverage gaps surfaced during PR-test scenarios: * Corrupt cell: a `.opensop/` directory without `manifest.yaml` should NOT be treated as a cell (walk-up requires both). The smoke check confirmed the behavior; this test pins it. * Nested cells: when an inner cell exists below an outer cell, runs invoked from the inner cell must land in the inner cell's `.opensop/runs/`, NOT bubble up to the outer. Full suite is now 29 cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes to the local engine when a cell is active:
* opensop run <name> --local
Accepts a bare logical name in addition to a file path. The name
resolves to processes/<name>.sop.json in the active cell, then in
each ancestor cell — nearest wins. Explicit file paths still work
(detected when the arg ends in .sop.json or contains '/'), so
backwards-compatible with every existing usage.
* opensop list --local (no dir arg)
When invoked from inside a cell, walks the active cell + ancestors,
tagging each entry with [cell-name]. Passing an explicit dir
preserves the original find-based behavior with no cell awareness.
New helper: _find_skill_in_cells <name> — walks the cell chain looking
for processes/<name>.sop.json, returning the first (nearest) match or
empty if none.
No dedup yet — when the same logical name exists in multiple cells,
list shows all rows. A future --conflicts flag will distinguish
"nearest" from "shadowed."
Tests: 7 new cases
* list inside cell walks active + ancestor processes/ with [cell-name] tags
* list with explicit dir arg uses original find behavior (no tags)
* run by name resolves to ancestor cell's processes/<name>.sop.json
* nearest-wins resolution (child cell shadows parent's same-named skill)
* explicit path still works (backwards compat)
* run with non-existent name errors cleanly
* run by bare name outside any cell errors (no chain to search)
Co-authored-by: Carlos <carlos>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Materializes a copy of an ancestor cell's skill into the active cell
and records a lineage entry with forked_from = { cell, forked_at,
snapshot } where snapshot captures the parent's status + metadata
opaquely.
The substrate stores; evolution policies decide what to do with the
snapshot. Typical policy use: inherit parent's state + mark unverified
until first run in the child cell (the "knife metaphor" from the v0.6
spec — the shape carries its sharpness, the cell verifies fit on first
cut).
Behavior:
* Default: auto-detect source via walk-up (nearest ancestor with the
skill).
* --from <cell>: override; resolves relative to the active cell or
accepts absolute paths. Validated to be an actual cell (.opensop/ +
manifest.yaml).
* Refuses to overwrite an existing skill in the active cell (forces
the user to make removal explicit).
* Child's live status/metadata/history start empty. Substrate stays
policy-neutral.
* After fork, name resolution from PR #9 finds the child's copy first
(nearest-wins).
Tests: 9 new cases
* fork copies file from ancestor's processes/ into active cell
* forked_from captures parent's status + metadata as snapshot
* child's live status/metadata/history start empty
* parent's lineage is untouched
* refuses to overwrite existing skill in active cell
* errors when skill not found in any ancestor
* errors when not inside any cell
* --from override copies from a non-ancestor cell
* forked skill is nearest-wins for subsequent name resolution
Co-authored-by: Carlos <carlos>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Steps in a .sop.json may now declare `executor: internal|external`: * "external" → work happens in an outside process (script, webhook); the OpenSOP runtime orchestrates and receives the typed receipt * "internal" → the OpenSOP runtime handles the step directly Field is optional. Per-type defaults apply when absent: * automated, shell, webhook → external * noop, form, approval, notification, wait, judgment → internal Invalid values fail with parse_error at process load time — BEFORE any step runs and before a run directory is even created. Pre-validation pass walks all steps and refuses to start if any has an invalid value. The effective executor (explicit or defaulted) is recorded in each step's audit.jsonl entry, so receipts document where the work happened. This formalizes the B-mode-vs-A-mode distinction from the v0.6 spec and matches existing production patterns (deterministic external scripts producing typed receipts). Tests: 6 new cases * shell step defaults to external in receipts * noop step defaults to internal in receipts * explicit "internal" honored * explicit "external" honored * invalid executor errors with parse_error before any step runs * pre-validates ALL steps before creating a run dir (no partial runs) Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump OPENSOP_CLI_VERSION → 0.6.0. Consolidate CHANGELOG [Unreleased] into ## [0.6.0] — 2026-06-08 with one Added section + one Changed section (the prior accumulation had a duplicate ### Added from incremental PR landings). v0.6 shipped across six PRs (#6, #7, #8, #9, #10, #11): * Cell primitive — fractal addressing via .opensop/manifest.yaml * Lineage primitives — substrate-level event log per skill * Cell-aware OPENSOP_LOCAL_HOME — receipts live with the cell * Name resolution across the cell chain — nearest-wins * Fork mechanic — explicit inheritance with snapshot * Step executor field — internal|external, formalizes B-mode/A-mode Backwards-compatible: every pre-v0.6 usage continues to work; v0.6 features only activate inside a cell. Test suite: 51 cases, all pass. Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When inside a cell, --conflicts marks the first occurrence of each filename (nearest cell that has it) as "← active" and subsequent occurrences in ancestor cells as "← shadowed by [cell-name]". Resolves the deferred follow-up flagged in PR #9's CHANGELOG entry: "No dedup yet; same logical name in multiple cells shows all rows. A future --conflicts flag will distinguish 'nearest' from 'shadowed.'" * Default behavior unchanged (no markers, backwards-compatible). * --conflicts only annotates; it doesn't dedup or hide rows. * Tracking implemented with a space-separated string scan rather than an associative array (declare -A is bash 4+, but the existing repo works fine on bash 3.2 macOS). * Explicit-dir mode (`list --local /some/dir`) ignores --conflicts — no cell chain to compare against. Tests: 3 new cases * default list (no flag) preserves backwards-compatible output * --conflicts marks active + shadowed entries correctly * --conflicts with explicit dir arg is benign Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A step-by-step install guide written for AI coding agents (Claude Code, Codex, Cursor, etc.) that a user has asked to set up OpenSOP CLI. The file uses [ASK] markers to mark the points where an agent should pause for user input; everything else is mechanical and should proceed without prompting. Covers: * Prerequisites check (bash 4+, jq, curl) * Install via the canonical curl one-liner + no-sudo fallback + from-source fallback for hermetic envs * Verify install (version + help-text inspection) * Two configuration paths: - A: point at a server (URL + token) - B: local mode + v0.6 cell setup * Walkthrough of one working end-to-end example * Handoff confirmation + suggested next steps * Trust boundary callout for local execution * Common issues table mapping symptom → cause → fix Companions the Rails runtime's INSTALL_FOR_AGENTS.md referenced from the opensop.ai hero prompt; this one is for users who want the CLI specifically (lighter setup, no Docker, --local mode + v0.6 cells). Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bprocess/wait + pause/resume (#15) The CLI's --local mode now executes the full step-type set, not just automated/shell/noop. Adds a pause/resume keystone so form/approval/ wait-until/webhook-callback/subprocess can pause a local run and resume via `opensop submit <run_id> <step-id> --local`. - New local step types: form, approval, llm, webhook, subprocess, wait - Pause/resume: manifest state machine (running/waiting/completed/failed), cursor.next_index, live context.json checkpoint, submit --local - llm matches the runtime's Anthropic contract (OSL_LLM_STUB test seam) - webhook: ${...} templating, sync + callback modes, CRLF header guard, body sends only declared step inputs (no context over-share) - subprocess: recursive local run + context merge + depth guard - Security: CRLF guards on header keys+values, fork name validation - 164 tests pass (up from 81). Declares opensop: "0.6". Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- response_mode is now required for webhook steps (no silent sync default);
enforced in schema validate and the local engine
- callback mode fires the outbound request, then pauses (runtime parity)
- fallback body resolves declared inputs via from: (InputResolver parity),
no longer over-shares the full context to the endpoint
- ${process.inputs.X} supports nested dot-paths
- 172 tests pass (up from 164)
Co-authored-by: Carlos <carlos>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default backend is now LOCAL. Every command runs against local .sop.json files with no server/curl. Remote is opt-in via --remote (configured server) or --server <url>. --local is a deprecated no-op kept for script compatibility. - Phase 1: full local parity — search/suggest/dry-run/status/steps/ instances/compass/history/diff/cancel now have local implementations mirroring the runtime serializer shapes - Phase 2: inverted dispatch (REMOTE_MODE, default false); register and schema <name> gated remote-only; curl/config only in remote mode; run/instance vocabulary unified to "run" - 289 tests pass (from 172). Docs/help/README reframed local-first. BREAKING CHANGE: scripts expecting default-remote must add --remote or --server <url>. See CHANGELOG [0.8.0] migration note. Co-authored-by: Carlos <carlos> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidates the CLI into the opensop repo as part of the local-first pivot. opensop-cli's full commit history is preserved as a merge parent (git log --follow cli/bin/opensop traces across the merge). The standalone opensop-cli repo will be archived with a redirect; its tags and GitHub releases (v0.1.0..v0.8.0) remain there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Consolidates the CLI into this repo at
cli/as part of the local-first pivot ("Process as Infrastructure"). Done as a history-preserving subtree merge (merge -s ours+read-tree --prefix=cli/), soopensop-cli's full commit lineage is a merge parent.cli/bin/opensop(v0.8.0, local-first),cli/test/,cli/docs/,cli/CLAUDE.mdetc. now live here.opensop-clirepo will be archived with a redirect README; its tags + GitHub releases (v0.1.0..v0.8.0) remain there for provenance.Merge with a merge commit, NOT squash — squashing drops the second parent and loses the imported history.
Self-rating
cli/. Follow-up (Phase 4): reframe README/SPEC around manifesto + local-first and update the now-internal CLI links.Test plan
cd cli && bash -n bin/opensop && bash test/test.sh→ 289 PASS from the new location.🤖 Generated with Claude Code