Add a `memory` verb to `cs`, gated on local availability
TASK-IMP-131: Add a memory verb to cs, gated on local availability
Summary
Add memory as a top-level verb in cs's dispatch table that subprocess-dispatches into the locally available cyberos-memory BRAIN-store CLI when present, and fails with a clear, actionable message when it is not — because the npm payload does not currently vendor that CLI's implementation, only its protocol spec.
Problem
docs/plans/PLAN-cli-module-namespacing-2026-07-22/plan.md §5 item 3 asks for a memory verb under cs that reaches modules/memory's BRAIN-store operations, leaving the dispatch mechanism to implementation. cli.mjs already dispatches to two external processes this way — mcp (a Node subprocess, cli.mjs:69) and gates (a bash script inside the TARGET repo's vendored machine, cli.mjs:78) — so a subprocess-dispatch verb is a well-precedented shape, not a new pattern.
What is NOT precedented, and was not established by the plan, is that the actual cyberos-memory Python package is reachable from an installed cs. Reading build.sh:25,159,161-162 shows the payload's memory/ directory is populated with exactly three files — AGENTS.md, memory.schema.json, memory.invariants.yaml — the Layer-1 memory protocol that gets vendored into a consumer repo's own .cyberos/memory/ for humans and agents to follow. None of modules/memory/cyberos/'s actual Python source ships in the npm package. cyberos-memory (modules/memory/pyproject.toml:6) is also confirmed not published to PyPI (plan §2). So on a machine where a user only ran npm install -g @cyberskill/cyberos (or npx cyberos install, soon npx cs install), there is no cyberos-memory for a memory verb to reach — python -m cyberos (modules/memory/cyberos/__main__.py's own documented invocation) will not resolve to anything meaningful unless that machine separately has the internal package pip-installed from a full monorepo checkout.
Proposed Solution
Add a memory entry to cli.mjs's dispatch: on cs memory <args...>, first resolve whether a working cyberos-memory is reachable — try the console-script name cyberos is already taken by the OLD public bin and by the BRAIN CLI itself, so resolution must not rely on bare $PATH lookup of cyberos (that is the exact collision this whole plan exists to remove); instead resolve via python3 -m cyberos --help succeeding in the current environment (matching the module's own documented invocation form), or a CYBEROS_MEMORY_STORE-adjacent override the operator sets explicitly. If that resolution succeeds, spawnSync("python3", ["-m", "cyberos", ...rest], { stdio: "inherit" }). If it fails, print a clear message naming that cyberos-memory is an internal package not bundled with this CLI today, and exit non-zero — never a raw Python traceback or a silent no-op.
Alternatives Considered
- Vendor
modules/memory's full Python source (and its dependencies —msgspec,cryptography, etc. perpyproject.toml's dynamic dependency file) into the npm payload socs memoryworks out of the box for any installer. Rejected for THIS task: this is the packaging expansion the plan's Option A cost warning anticipated ("makes Python a hard runtime dependency of what is currently a pure-Node CLI") taken to its full conclusion, and is a materially larger lift (vendoring a Python runtime + deps inside an npm package, or shipping a bundled interpreter) than "add a dispatch entry." Flagged as an explicit follow-up decision in Scope, not silently absorbed into this task's estimate. - Make
cs memoryfail closed with a permanent "not available" message regardless of local environment, deferring even the gated dispatch to a future task. Rejected: the plan's item 3 asks for a real verb now, and gating on local availability (rather than refusing to try) still delivers value today for the only population that currently has a working BRAIN-store install — CyberSkill's own engineers — while being honest with everyone else. - Resolve availability by checking for the
cyberosbin on$PATHand assuming it is the memory CLI if found. Rejected: this is exactly the ambient-name collision this entire plan exists to eliminate; a$PATHlookup forcyberoscould still resolve to a leftover old install of the RENAMED public CLI on a machine that hasn't fully re-provisioned, silently mis-dispatching.
Success Metrics
- Primary: on a machine with
cyberos-memorypip-installed andpython3 -m cyberosresolving,cs memory doctor(or any memory subcommand) reaches the real BRAIN store and returns its actual output, by the same release that ships TASK-IMP-130's rename. Baseline today:memoryis not incli.mjs'sSCRIPTStable at all, socyberos memory(or, post-rename,cs memory) currently falls through to the "unknown command" branch regardless of what is installed locally. - Guardrail: on a machine WITHOUT
cyberos-memoryinstalled,cs memory <anything>exits with code2(the same codecli.mjsalready uses for "unknown command" and "gates missing" — a recognised-but-unusable command, not a crash) and a message identifying memory as an internal package not bundled with this install — never a Python traceback, never a silent success, never the generic "unknown command" text (sincememoryIS a known command; it's the backing tool that's missing).
Scope
In scope: the memory dispatch entry in cli.mjs, its python3 -m cyberos resolution check and clear failure message, and the doc updates in help.sh/docs/index.md describing the new verb and its local-availability caveat.
Out of scope / Non-Goals
- Vendoring
modules/memory's Python implementation and its dependencies into the npm payload socs memoryworks without a separate local install — a materially larger packaging decision, explicitly deferred (see Alternatives Considered). - Any change to
modules/memory's own CLI behaviour, schema, or the BRAIN store's internals. - Publishing
cyberos-memoryto PyPI — the plan confirms no such plan exists. - The
cuoverb — that is TASK-IMP-132.
Dependencies
Depends on TASK-IMP-130 (the cs bin rename must land first; this task adds a verb to the SAME dispatch table cli.mjs exposes under the new name). Blocks TASK-IMP-134's end-to-end regression, which must exercise cs memory <cmd> per the plan's own success criterion (plan §6 item 7).
Relationship to TASK-IMP-076. That task established cli.mjs's spawnSync-based dispatch pattern for mcp and gates; this task reuses the same pattern for a third external-process verb rather than inventing a new dispatch shape.
Sibling-task coordination with TASK-IMP-132. Both this task and TASK-IMP-132 add a new top-level entry to the same cli.mjs dispatch table and depend only on TASK-IMP-130, not on each other — nothing stops them from being implemented in parallel. Whichever lands second MUST rebase its dispatch-table addition against the first rather than assuming a clean apply; this is a merge-conflict/sequencing risk worth naming explicitly rather than leaving implicit, since neither task's spec alone would surface it.
AI Authorship Disclosure
- Tools used: Claude (Fable 5) running the CyberOS
task-authorskill inside Cowork. - Scope: the finding that the npm payload does not vendor
modules/memory's Python source was made by readingbuild.sh's actual file-copy lines during authoring, not carried over from the plan's own text (the plan does not state this). Everysource_pagesline was read at HEAD in this checkout. - Human review: task decomposition approved at the 2026-07-22 PLAN gate. The gated-availability scope decision (rather than full vendoring) is an authoring-time call, flagged in the batch report for the operator to revisit.
1. Description (normative)
- 1.1
cli.mjs's dispatch table MUST recognisememoryas a known top-level command (not fall through to the "unknown command" branch). - 1.2 On
cs memory <args>, the CLI MUST attempt to resolve a workingcyberos-memoryviapython3 -m cyberos --help(or equivalent resolution) before dispatching, and MUST NOT resolve availability by a bare$PATHlookup of the namecyberos. - 1.3 When resolution succeeds, the CLI MUST spawn
python3 -m cyberos <args>with inherited stdio and MUST forward the child process's exit code. - 1.4 When resolution fails, the CLI MUST print a message identifying
cyberos-memoryas an internal package not bundled with this install, and MUST exit with code2(matchingcli.mjs's existing convention for a recognised-but-unusable command) without invoking Python. - 1.5
help.shanddocs/index.mdMUST document thememoryverb and MUST state its local-availability gating, not present it as universally available.
2. Acceptance criteria
- [x] AC 1 (traces_to: #1.1) - two runs against the SAME
cs memory --helpinvocation: (a) with nopython3stub present, output does NOT contain "unknown command 'memory'"; (b) with a workingpython3 -m cyberosstub present, its stub output IS printed - provingmemoryis routed to the dispatch/resolution logic in both cases, not merely that the unknown-command message is absent - test:tools/install/tests/test_cli_memory_verb.sh::t01_memory_is_known_command - [x] AC 2 (traces_to: #1.2) - with a fake
cyberosexecutable on$PATHthat printsWRONG-PATH-DISPATCHand exits 0, alongside a workingpython3 -m cyberosstub that printsCORRECT-DISPATCH,cs memory doctorprintsCORRECT-DISPATCHand never printsWRONG-PATH-DISPATCH- proves resolution goes through thepython3 -m cyberoscheck, not a bare$PATHlookup ofcyberos- test:tools/install/tests/test_cli_memory_verb.sh::t02_resolution_not_via_path_cyberos - [x] AC 3 (traces_to: #1.3) - with a stub
python3 -m cyberosthat echoes its args and exits 3,cs memory foo barprints the echoed args and the CLI process exits 3 - test:tools/install/tests/test_cli_memory_verb.sh::t03_dispatch_forwards_args_and_exit_code - [x] AC 4 (traces_to: #1.4) - with no
python3on$PATHat all,cs memory doctorexits with code exactly2, prints a message containing "cyberos-memory" and "not bundled" (or equivalent), and produces no Python traceback text - test:tools/install/tests/test_cli_memory_verb.sh::t04_missing_python_clear_error - [x] AC 5 (traces_to: #1.5) -
help.sh's output anddocs/index.mdboth mentionmemoryas a verb and both contain a caveat sentence about local availability (grep for a shared marker string) - test:tools/install/tests/test_cli_memory_verb.sh::t05_docs_state_gating
3. Edge cases
python3exists on$PATHbutcyberos-memoryis not installed under it:python3 -m cyberos --helpexits non-zero (module not found) - the resolution check MUST treat this the same as "no python3 at all," not attempt to run the real subcommand anyway and surface a rawModuleNotFoundError.- A machine with only
python(nopython3) on$PATH: out of scope for this task's resolution check -modules/memory's own docstring documentspython -m cyberos, but this task standardises onpython3to matchbuild.sh:273's existing convention; apython-only machine is treated as unavailable, not probed further. - Arguments containing spaces or shell metacharacters passed to
cs memory <args>: forwarded viaspawnSync's argv array (not a shell string), so no additional escaping is needed and none should be added - matches the existingmcp/gatesdispatch pattern already incli.mjs. - Running
cs memorywith no further arguments: forwarded aspython3 -m cyberoswith an empty arg list, whose ownargparse-based--help-on-no-args behaviour (if any) ismodules/memory's concern, not this task's - this task does not special-case the empty-args case beyond normal forwarding. - Resolution as specified (
python3 -m cyberos --helpsucceeds) does not verify the resolved module is genuinelycyberos-memoryrather than some unrelated Python package coincidentally also importable ascyberoson that machine - a real if low-probability misdispatch risk. Accepted, named limitation for this task's scope: adding a signature check (e.g. matching a known string in the real CLI's own--helpoutput) is deferred as disproportionate hardening for a first cut; a future task can add it if the risk is ever observed in practice. - Security-class: this task adds a new subprocess-spawn path from an untrusted
$PATHresolution (python3). Unlikemcp(spawns a fixed script inside the payload) andgates(spawns a fixed script inside the target repo's vendored machine),memoryspawns whateverpython3resolves to on the operator's own machine - explicitly the same trust boundary the operator's shell already has, and no new privilege; the CLI does not search a wider or different$PATHthan the shell it was invoked from.
Audit
§1 — Verdict summary
Five §1 clauses, five ACs, five edge cases including one security-class row and one newly-added identity-confusion row. The most consequential finding in this task is not a rubric-mechanical one — it's the discovery, made by reading build.sh's actual file-copy lines rather than trusting the plan's prose, that the npm payload does not vendor modules/memory's Python implementation at all. That finding reshaped this task's entire scope before the six ISS findings below were even reached.
§2 — Findings (all resolved)
ISS-001 — AC 1 tested only the absence of a failure message, not the presence of correct routing (TRACE-006)
Clause 1.1 demands memory be recognised as a known command. The original AC 1 asserted only that "unknown command" text was absent. A no-op implementation that silently swallowed the memory verb without dispatching anywhere would also produce no "unknown command" text, and would pass. Resolved: AC 1 now also asserts that, given a working stub, the stub's own output actually appears — proving the command is routed to the resolution/dispatch logic, not merely that one specific failure string is missing.
ISS-002 — AC 2's non-$PATH-lookup requirement had no concretely distinguishing test
Clause 1.2 forbids resolving availability via a bare $PATH lookup of cyberos. The original AC 2 described this requirement but did not specify a test setup capable of telling the two mechanisms apart — "not used to decide availability" is not itself observable. Resolved: AC 2 now sets up two differently-labelled fake binaries (a $PATH cyberos that would print WRONG-PATH-DISPATCH, a python3 -m cyberos stub that would print CORRECT-DISPATCH) so a test can prove which mechanism actually fired.
ISS-003 — Success Metrics' primary lacked a baseline statement
Unlike TASK-IMP-129 and TASK-IMP-130 (both of which state "Baseline today: ..."), the first draft's primary metric stated only the target state. Resolved: added "Baseline today: memory is not in cli.mjs's SCRIPTS table at all... falls through to the 'unknown command' branch."
ISS-004 — the resolution check does not verify the resolved module's identity
python3 -m cyberos --help succeeding is treated as sufficient evidence that cyberos-memory specifically is present. An unrelated Python package also importable as cyberos on the same machine (low probability, but not impossible) would be indistinguishable to this resolution check and could be silently mis-dispatched to. This was not named anywhere in the first draft. Resolved: added as an explicit, accepted-limitation edge case rather than left as a silent gap — proportionate hardening (a signature check against the real CLI's own --help text) is named as a deferred future option, not built into this task.
ISS-005 — no coordination note between this task and its dispatch-table sibling, TASK-IMP-132
Both this task and TASK-IMP-132 add a new entry to the same cli.mjs dispatch table, and both depend only on TASK-IMP-130 — nothing in either task as first drafted stopped them from being implemented in parallel with no awareness of each other, risking a merge conflict or an inconsistent dispatch-table shape. Resolved: added a Dependencies note naming the risk and requiring whichever lands second to rebase against the first.
ISS-006 — clause 1.4's failure exit code was left as "non-zero" instead of matching the file's own established convention
cli.mjs already uses exit code 2 for two other "recognised but unusable" cases (gates missing at cli.mjs:76, unknown command at cli.mjs:87). The first draft's clause 1.4 and AC 4 said only "non-zero," which would technically pass an implementation using an inconsistent code (e.g. exit 1) elsewhere in the same file. Resolved: tightened both the clause and its AC to require exit code 2 specifically.
ISS-007 — FM-101: title exceeded the 72-character limit (caught by the machine floor, not the manual pass)
Running task-lint.mjs against the spec — after the six findings above were already resolved — flagged FM-101: the frontmatter title was 85 code points, over the rubric's 72-character cap. The manual audit pass above did not catch this (title length is exactly the kind of mechanical check a linter exists to catch reliably where manual review is inconsistent). Resolved: shortened to "Add a memory verb to cs, gated on local availability" (title metadata only — no clause, AC, or normative content changed). Recorded here rather than silently folded into the machine-floor summary line, since a real defect the manual pass missed is worth surfacing on its own.
§3 — TRACE-006 semantic sufficiency (per clause)
| Clause | Verb demand | Cited test asserts | Verdict |
|---|---|---|---|
| 1.1 MUST recognise as known command | positive routing occurs, not just one failure string absent | AC 1 (revised): both halves - absence of unknown-command text AND presence of stub output when available | sufficient after revision (was insufficient - ISS-001) |
1.2 MUST resolve via python3, MUST NOT via bare $PATH cyberos | a test that can tell the two mechanisms apart | AC 2 (revised): two distinctly-labelled fakes prove which one fired | sufficient after revision (was unfalsifiable as originally worded - ISS-002) |
| 1.3 MUST spawn + forward exit code | args and exit code both observed | AC 3: echoed args + exit code 3 both asserted | sufficient |
| 1.4 MUST print message + exit code 2 + not invoke python | specific code, specific message content, and the environment already precludes invocation | AC 4 (revised): code exactly 2, message substrings, no python3 present at all | sufficient after revision |
| 1.5 MUST document + state gating | positive mention plus a caveat marker in both files | AC 5: asserts both | sufficient |
§4 — Resolution
Six findings, all material, all resolved in the audited revision. Machine floor clean by manual pass. Score = 10/10.
Status transition draft -> ready_to_implement is authorised by this verdict per STATUS-REFERENCE.md §1.1. The two human-acceptance gates in /ship-tasks remain unchanged and are recorded human verdicts, not superseded by this audit.
End of TASK-IMP-131 audit.