feat(cli): add openkos init — create an OKF workspace#3
Merged
Conversation
…derers Implements the OKF format seam (frontmatter framing, reserved filenames, §9 rules 1-2 conformance check) and the three bundle modules that render and write a fresh bundle's index.md and log.md, per D1-D3. Moves python-frontmatter from dev to runtime dependencies since okf.py now imports it. No CLI caller yet — init wiring lands in PR 4.
Add config.py with WorkspaceLayout and is_workspace() covering the four refusal conditions (openkos.yaml, AGENTS.md, non-empty raw/ or bundle/), plus write_config()/write_agents() generating openkos.yaml and AGENTS.md from packaged templates via importlib.resources, exclusive-create mode. Neither is wired into the init command yet (PR 4).
write_config used root.name directly, which is empty for an unresolved
relative path such as Path(".") -- Path(".").name == "". That silently
wrote a nameless workspace instead of failing loudly, and Path(".") is
exactly what init's cwd-based wiring (PR 4) will naturally pass. Resolve
root first so the function is correct for any caller, not dependent on
the caller passing an absolute path.
…ation Wires config.is_workspace, bundle.create, write_agents, and write_config together behind `openkos init`: a pure pre-flight check runs before any write (D1), and openkos.yaml is written last so a crashed init never falsely claims workspace status (D3).
The unit suite runs init against the editable install, so it would stay green even if templates/ silently dropped from a future packaging change. This step runs openkos init against the isolated built wheel in a scratch directory and asserts all five artifacts land, turning a lucky fact into an enforced one.
init generates a fixed qwen3.5:9b default rather than an interactive model picker, and does not pre-create concept folders inside bundle/ -- align docs/cli.md with what init actually does. Also correct the stale qwen3:8b example in the generated openkos.yaml block and the Ollama pull prerequisite, and add openkos init to AGENTS.md's MVP 1 vertical slice ahead of ingest.
datetime.now(UTC).date() filed the initialization entry under the wrong calendar day for any user west of UTC in the evening, or east of UTC before local midnight rolls into UTC's next day. log.md is a person's chronological history, not a machine timestamp, so it belongs in their own local time -- the same reasoning git applies to commit dates. DTZ only requires timezone-awareness, not UTC specifically; both datetime.now(UTC).date() and datetime.now().astimezone().date() satisfy it, so the rule never actually decided this. Corrected design.md's rationale to state what was actually decided and why. Regression test forces two extreme real UTC offsets (Etc/GMT+12 and Pacific/Kiritimati) via TZ + time.tzset() rather than a single fixed zone, since ubuntu-latest CI runs in UTC where local == UTC and a single-zone test could pass alongside this exact bug. Together the two offsets' "local date differs from UTC" windows cover all 24 hours of a UTC day, so at least one always disagrees with a UTC-based date. Sharpened the Bundle Log Shape requirement in spec.md to say whose "today" -- the machine's local date, not UTC's -- with a dedicated scenario for when the two differ.
The old docstring claimed the test proves a fresh bundle passes OKF §9 conformance. It cannot fail on any fresh bundle: index.md and log.md are both reserved, so check_conformance's loop never executes and it returns [] by construction regardless of what init wrote -- proven by writing garbage into both reserved files and observing check_conformance still returns []. Kept the test (a real regression guard for a future non-reserved .md write), corrected the docstring to say so.
The OKF Conformance requirement claimed rule 3 "passes actively" via the mechanical check. It does not: model/okf.py implements rules 1-2 only and defers rule 3 to lint, as its own docstring states. check_conformance skips reserved filenames, and a fresh bundle holds only index.md and log.md, so the loop body never runs and it returns [] regardless of file contents. The behaviour was always correct -- init writes well-formed reserved files, enforced by the Bundle Index Shape and Bundle Log Shape scenarios. Only the claim was wrong. Archive merges this delta into openspec/specs/ as the living contract, so the claim is corrected before it becomes permanent.
`openkos init` was silent on every path: no stderr on refusal, no stdout on success, and an uncaught FileExistsError if `raw` or `bundle` existed as a plain file instead of a directory. Add a fifth pre-flight condition (`config.refusal_reason`, replacing `is_workspace` at the call site) that names which condition blocked init, print it to stderr on refusal and a created-artifacts summary to stdout on success, and catch OSError around Phase B so permission/disk/collision failures exit cleanly instead of raising a raw traceback.
A 4R review found init silent on both refusal and success, while design.md's sequence diagram mandates stderr: reason / stdout: created. The requirement was lost because it lived only in the design: the spec never required it, so tasks never tasked it, so no test demanded it, so it was never built. User-visible behaviour belongs in the spec. Also adds a fifth pre-flight refusal condition -- raw or bundle existing as a non-directory -- which previously slipped past is_workspace and surfaced as an uncaught FileExistsError traceback, and requires write failures to exit non-zero with a clear message rather than a traceback.
config.refusal_reason reimplemented is_workspace's four conditions instead of calling it, so its "wraps is_workspace" docstring was false and the two could silently drift apart. Both now read one private generator (_refusal_conditions) that is the single source of truth for every reason init may refuse to write. Also strengthens two tests that only asserted a directory name appeared in stderr, which passed even if the two refusal messages for that directory were swapped, and a write-failure test that only asserted stderr was non-empty, which passed for a bare exception repr. Both now assert the distinguishing message content.
Merges the workspace-init delta spec into openspec/specs/ as the living contract (11 requirements, 19 scenarios, byte-identical to the delta -- openspec/specs/ was empty, so this is a pure additive create) and moves the change folder under openspec/changes/archive/. No ADRs to accept: the change deliberately created none. Both candidates failed the hard-to-reverse half of the gate -- the console entry point is one commit to revert with no wheel published and no publish step in CI, and removing model selection from init is a deferral, not a decision. Deferred and not delivered: the interactive model picker docs/cli.md:48 still promises (add-model-selection), git init (add-workspace-git), the repo-wide model-tag refresh (refresh-model-guidance), and OKF §9 rule 3's mechanical check (deferred to lint).
`write_config` substituted the workspace directory's basename into the openkos.yaml template with plain `str.format`. A basename carrying YAML-significant characters (`:`, quotes, a leading `#`) produced a malformed or silently altered document — a leading `#` commented the whole line out. `_yaml_scalar` now delegates the escaping decision to ruamel.yaml's safe dumper and extracts only the scalar, so the template stays hand-written and keeps its comments (D5). This moves ruamel.yaml to a runtime dependency, narrowly: it escapes one value, it does not emit the document. The four exclusive-create writes also opened in default text mode, so on any platform where os.linesep is not LF the template's bytes would be translated to CRLF, breaking write_agents' documented byte-identical contract. All four now pass newline="". CI is ubuntu-only and cannot observe this; the added tests are regression guards, and say so.
The docstring claimed that without the fifth pre-flight condition `Path.mkdir` would raise an uncaught FileExistsError. It would not: Phase B is wrapped in `try/except OSError`, which catches it. The condition's real value is a precise refusal before any write, instead of a generic caught error partway through Phase B.
Three claims in the archived artifacts were false: - D2 said exclusive-create "closes the Phase-A->B TOCTOU window". It closes it for the four file writes only. The two mkdir(parents=True, exist_ok=True) calls that gate the non-empty raw/bundle refusal remain racy; that limitation is now named. - The archive report's traceability table cited thirteen test names that do not exist. The verify report's table was already correct. Scenarios with no dedicated test are now marked as such rather than given an invented one. - The design's module map described `write_config(root, name, model)`; the shipped signature takes root alone. D5's runtime-dependency consequence is also corrected: ruamel.yaml does move to runtime, to escape the interpolated scalar.
README described an `init` that creates concept folders, helps you pick a local model, and initializes a git repository. It does none of those. Worse, the merged spec explicitly forbids the first: concept-type folders MUST NOT be pre-created inside bundle/. The README promised the user exactly what the contract prohibits. AGENTS.md still called the console entry point a pre-MVP stub that "moves to openkos.cli.main:app when the cli package lands" — a migration this same change already performed. bundle.create()'s docstring claimed exclusive-create closes the Phase-A -> B TOCTOU window. It closes it for the two leaf files the function writes, not for the mkdir above it. Scoped to match D2.
… near them The newline="" regression tests asserted no CR bytes in the output — true on POSIX whether or not the fix exists, so removing newline="" would have kept the suite green. They now spy on Path.open and assert the argument is really passed, which fails on Linux if it is dropped. _yaml_scalar's docstring promised defence against colons, quotes, a leading hash and control characters; the tests covered three of those four. The parametrization now also carries an embedded newline, a tab, unicode, a 100-character name, the empty string and a form feed. All round-trip exactly — the escaping was already sound, and is now proved rather than argued.
refusal_reason() ran outside init's try/except, which guarded only the Phase B writes. An unreadable pre-existing raw/ or bundle/ made _non_empty_dir's iterdir() raise PermissionError during pre-flight, so the command died with a raw traceback -- the one filesystem failure it did not report cleanly. Phase A now has its own guard. The message says "failed while checking the workspace": not "refusing", because no workspace was found, and not "failed while creating", because nothing was created.
The change was archived on this branch and the branch never merged, so the archive claimed a completion that was not real. A review then found that `name` in openkos.yaml is written by init and read by nobody, and that generating it is what dragged ruamel.yaml into runtime and, with it, a silent data-corruption bug on directory names holding a run of spaces past ruamel's fold column. Removing `name` inverts the spec's Generated Workspace Config requirement rather than trimming it, so the change goes back in flight: spec delta, design D5 and tasks are reopened, and the archive report is dropped because it asserted a completion that no longer holds. The promoted spec goes with it -- this change created it, and re-archiving regenerates it from the delta. Pure move plus deletions; no artifact content is edited here.
The spec's Generated Workspace Config requirement inverts rather than trims: with `name` gone, `openkos.yaml` is a byte-identical copy of the packaged template, like AGENTS.md already is. Renamed to Static openkos.yaml Template; its scenario now asserts byte-identity and that no directory-derived field survives, at any directory name or length. Design D5 returns to its original position -- ruamel.yaml does not move to runtime -- with a round-trip note recording why the intermediate correction happened and why undoing it is not a flip-flop. Proposal Q7.9 is annotated as superseded: the example file it cited as evidence had never been examined, and a normative example silently became a requirement. Model pinned qwen3:8b across the board.
Nothing in the codebase read openkos.yaml's name; init wrote it and no one consumed it. Generating that one field is what forced a YAML escaping step, which pulled ruamel.yaml into runtime, which carried a silent data-corruption bug: a directory name past ruamel's fold column with a run of two or more spaces round-tripped with the run collapsed. Removing name removes the whole chain. write_config now reads the template and writes it byte-for-byte under exclusive-create, the same shape as write_agents -- no substitution, no dumper, no escaping. _yaml_scalar and the ruamel import are gone, its tests replaced by one byte-identity test, and ruamel-yaml returns to a dev-only dependency.
The model default was written four different ways across the docs; docs/cli.md alone named qwen3.5:9b in three places while examples used qwen3:8b. tech_stack.md owns this decision and names the Qwen3 family at 7-8B, deferring the exact default to the model spike -- so all of it unifies on qwen3:8b, the value that does not argue the criterion. The openkos.yaml example blocks in docs/cli.md and examples/ still showed a name field the command no longer writes. An example is normative by inertia (this change's own Q7.9 is the cautionary tale), so those name lines are removed. user-journey.md's "helps you pick a local model" claim is dropped too -- init picks nothing; selection is deferred.
…name The spec scenario "no directory-derived field, regardless of directory name" had no test. It is the scenario the reopening exists for, so it should not rest on "by construction" alone -- that is the same footing the original bug stood on when it survived full branch coverage. The name is the exact shape that once corrupted the file: 40 chars, a double space, 40 more chars, which folded past ruamel's column and lost the space run on round-trip. Verified RED by reintroducing root.name interpolation into write_config (fails b'a' != b'm'), GREEN on the shipped code.
The verify report flagged the "no directory-derived field" scenario as having no dedicated test. It now does, so the report is updated to cite it in the traceability table and mark the warning resolved rather than leaving a stale "recommend a follow-up".
jasonssdev
added a commit
that referenced
this pull request
Jul 17, 2026
test_fresh_empty_directory checked only that "openkos.yaml" appeared in stdout. Because it is the trailing token in the message, that assertion could not catch a regression dropping or garbling any of the other four names. It now requires raw/, index.md, log.md, AGENTS.md, and openkos.yaml each to appear -- RED-confirmed by dropping log.md from the success message. Follow-up to PR #3 (WARNING-1 from the bounded review): a strictly stronger, test-only assertion, deferred there to keep the tree matching the approved receipt.
jasonssdev
added a commit
that referenced
this pull request
Jul 17, 2026
Merge the workspace-init delta spec into openspec/specs/ as the canonical spec, move the change folder to openspec/changes/archive/, and record the archive report. Closes out the change reopened for the D5 name-field revert and reshipped via PRs #3/#4. Doc-only move of prior-reviewed artifacts; no executable code.
This was referenced Jul 19, 2026
jasonssdev
added a commit
that referenced
this pull request
Jul 19, 2026
Close the SDD cycle for add-query-answer (MVP-1 query chain #3, merged in PR #27). Merge the query-answer delta into a new living capability spec at openspec/specs/query-answer/spec.md, move the change folder to openspec/changes/archive/2026-07-18-add-query-answer/, and add the verify report and archive report. Verify: PASS (7/7 requirements, 9/9 scenarios). Bounded review review-bd53402eecbf6307 (HIGH, full 4R): approved, 0 blockers.
This was referenced Jul 19, 2026
This was referenced Jul 21, 2026
jasonssdev
added a commit
that referenced
this pull request
Jul 21, 2026
…-side + follow-ups (closes slice) (#103) * fix: catch sqlite3.Error from the graph reindex write path sqlite_graph.reindex_graph() sat inside reindex()'s try block after vectors.db/fts.db committed, but the exception ladder (VecUnavailable, FtsUnavailable, OllamaError) never covered a bare sqlite3.Error from the graph write (permission/IO/corrupt graph.db) -- a graph-write failure after vectors/fts already succeeded crashed with a raw traceback instead of the documented clean exit 1, with no indication which store failed. Wraps the graph write in its own try/except sqlite3.Error, separate from the vectors/FTS ladder, printing a message that identifies the graph index specifically. Deliberately narrow: does not touch the still-open, separately-tracked follow-up of generic lock-contention errors from the vectors/FTS ladder. Carry-over fix from PR2 review (Engram bug #1470). * feat: add FtsSearchHandle protocol and open-time validation to fts.py FtsSearchHandle is the structural seam retrieval/answer.py injects as fts_index -- any object with a search(query, limit) method and a skipped list satisfies it, mirroring VectorStore/GraphStore's own Protocol-seam pattern. The real FtsIndex (in-memory or on-disk) already satisfies this shape without change. open_fts_index_readonly now runs one cheap validating read (SELECT 1 FROM docs LIMIT 1) immediately after connecting, so an EXISTING file that is not a valid SQLite database (or lacks the docs table) raises a sqlite3.Error at open time rather than only failing later on the first real search() call -- giving the CLI's open-or-degrade layer a single, well-defined call site to catch corruption. * feat: add open-time validation to open_graph_store_readonly Mirrors state/fts.py::open_fts_index_readonly's identical fix: runs one cheap validating read (SELECT 1 FROM nodes LIMIT 1) immediately after connecting, so an EXISTING graph.db that is not a valid SQLite database raises a sqlite3.Error at open time instead of only failing later on the first real query call. * refactor: dedupe graph_retrieve's pool floor via retrieval/pool.py graph_rank's own inline pool_limit = max(limit, 10) is replaced with pool.pool_limit(limit) -- follow-up #2, the last residual duplication of the max(limit, 10) floor now that retrieval/pool.py exists. * feat: rewire answer() to inject read-only fts_index/graph_index handles answer() no longer builds its own FTS index or graph projection internally -- it accepts injected, already-open, read-only fts_index/ graph_index handles (mirroring vector_store's existing contract exactly) and degrades to empty lists when either is None. answer() never computes or compares a bundle manifest hash (D2 binding contract): staleness detection is reindex's exclusive job, and a properly-reindexed handle is always treated as fresh at query time. The exception-vs-degrade boundary lives entirely at the caller's store-open call site: fts_index.search() is never wrapped in a try/except here, so a typed exception raised outside the open path still propagates unswallowed. graph_rank raising any exception still degrades gracefully, same as before. Also wires in retrieval.pool.pool_limit (replacing the inline max(limit, 10)) and drops the now-unused sqlite_graph/fts.build_index imports. Migrated every existing test in test_answer.py from monkeypatching fts.build_index/sqlite_graph.build_graph to direct fts_index=/ graph_index= injection, and added new coverage for: absent fts_index/ graph_index degrading cleanly, a typed exception from fts_index.search() propagating unswallowed, and a single consolidated empty-question spy test proving all four injected handles (fts_index, embedder, vector_store, graph_index) record zero calls (follow-up #1). * feat: wire query to inject read-only fts_index/graph_index Adds _open_fts_or_degrade/_open_graph_or_degrade (mirroring _open_vector_store_or_degrade exactly) and wires both into query(), alongside the existing vector_store injection. The reindex hint now fires on ANY of dense/FTS/graph being absent or unavailable/corrupt, not just dense. Updates query()'s docstring to describe all three derived stores as persisted, reindex-written, read-only indexes instead of claiming FTS/graph carry no persisted state. Also surfaces ReindexReport.prune_skipped as a distinct summary line in the reindex command. * feat: add ReindexReport.prune_skipped for prune-pass observability Distinguishes 'prune ran and genuinely found nothing to prune' (pruned == 0, prune_skipped == False) from 'prune was suppressed by a walk error' (pruned == 0, prune_skipped == True) -- previously indistinguishable from pruned alone. Surfaced as a distinct stdout line in the reindex CLI summary (follow-up #3). * docs(sdd): mark performance-caching Phase 3 tasks complete Also documents the carry-over fix from PR2 review and flags a discrepancy: follow-up #4 (WAL/busy_timeout + single-commit) was reported as already done in PR1, but code inspection shows vectorstore.py/vectors.db never received it -- left undone per explicit 'do NOT redo' instruction this batch, flagged for a follow-up slice. * feat: add WAL/busy_timeout PRAGMAs and batch writes to vectors.db open_vector_store now sets PRAGMA journal_mode=WAL and a busy_timeout of 5000ms (matching state/derived.py's fts.db/graph.db posture), completing follow-up #4 for the pre-existing (Slice 2b) vectors.db store that PR1/PR2 never touched. Adds upsert_many/prune_many/commit to the VectorStore Protocol and VectorStoreDB (additive, mirrors the Slice 2b Protocol-growth precedent): upsert_many/prune_many write many rows in one call without committing, so a caller can batch an entire run's writes into ONE commit via commit(). The existing single-item upsert()/prune() keep their own per-call-commit contract unchanged for any other caller. Empirically verified against the real sqlite-vec 0.1.9 extension that both WAL mode and multi-item uncommitted batches (including a re-upsert of the same concept_id) work correctly before this change was made. * feat: batch reindex()'s vector writes into one commit per run reindex() now calls db.upsert_many/db.prune_many (neither commits) for the embed/prune batches, then db.commit() ONCE at the end of the run -- covering both batches together -- instead of once per document via the old per-item upsert()/prune() calls. A run with nothing to embed and nothing to prune commits zero times. Completes follow-up #4's single-commit-per-run contract for vectors.db, matching fts.db/graph.db. * docs: describe FTS/graph as persisted, reindex-written derived stores docs/cli.md's query/reindex sections no longer describe FTS/graph as built fresh in-process per call with no persisted state -- they now describe all three on-disk derived stores (vectors.db, fts.db, graph.db) as reindex-written, read-only at query time, gated by the bundle-manifest-hash cache key, atomically rebuilt, and batched into one commit per store per run. docs/architecture.md's workspace-structure diagram gains a footnote reconciling the aspirational consolidated-openkos.db tree with the actual shipped three-file layout. docs/adr/0002's stale 'graph is ephemeral (rebuilt per run)' line gets an update note rather than a rewrite -- ADRs are historical decision records; the graph remains a rebuildable derived cache regardless, so the merge/unmerge reversibility reasoning is unaffected. * docs(sdd): mark performance-caching Phase 3 (29/29) and Phase 4 complete Follow-up #4 (WAL/busy_timeout + single-commit for vectors.db) is now genuinely done, closing the discrepancy flagged in the prior commit. Phase 4's full-gate re-verification and doc updates are also complete -- the performance-caching slice's tasks.md is now 60/60. * fix: surface reindex summary before the graph-write failure path The vectors.db/fts.db summary (embedded/cache-hit/pruned/skipped counts and the prune_skipped follow-up notice) now prints BEFORE the graph write attempt, not after it. Previously, a sqlite3.Error from sqlite_graph.reindex_graph() raised typer.Exit(1) before the summary block ran, so a graph-write failure silently swallowed the summary and prune_skipped notice even though vectors.db/fts.db had already committed durably. The clean exit-1 for the graph error is unchanged. Also corrects the _open_fts_or_degrade/_open_graph_or_degrade docstrings, which claimed to mirror _open_vector_store_or_degrade 'exactly' -- they are related in intent and return shape, but structurally different: no explicit path.exists() guard (the callees are already existence-gated internally) and a narrower except clause (sqlite3.Error only, since neither FTS nor the graph store has a typed 'unavailable' exception like VecUnavailable). No behavior change. Review findings R4 (WARNING) and R2 (SUGGESTION). * test: restore raw graph_rank spy assertion in determinism test test_graph_retrieval_is_deterministic_across_repeated_calls was rewritten during the DI migration to assert only the final, limit-truncated citation order across two calls -- dropping the stronger spy assertion on graph_retrieve.graph_rank's raw, pre-truncation output that the pre-migration version had. Restores it: spies on graph_rank and asserts its raw output is byte-for-byte identical across both calls, in addition to the citation-order check, re-establishing score-level determinism coverage a low final limit could otherwise mask. Review finding R3 (WARNING).
This was referenced Jul 22, 2026
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.
What this ships
openkos init— the first vertical slice. It creates a fresh OKF workspace in the current directory (raw/,bundle/index.md,bundle/log.md,openkos.yaml,AGENTS.md), or refuses without writing anything if the directory already looks like a workspace. Every other MVP 1 command depends on a workspace onlyinitcan produce.Design in one line
Check-everything-then-write over four modules:
cli/main.pysequences and maps exit codes,config.pyandbundle/own the writes,model/okf.pyowns conformance. Phase A is a pure read (refusal is decided before any byte is written); Phase B writes, with theopenkos.yamlmarker written last so a crash never leaves a directory falsely claiming workspace status.openkos.yamlis a byte-identical template copy — nonameThe generated config carries no
namefield. The directory is the single source of truth for the workspace's identity; anamecopy could only drift out of sync, and nothing in the codebase read it. Removing it meanswrite_configis a plain byte-for-byte template copy, exactly likewrite_agents.This closed a real, reproduced data-corruption bug: while
namewas interpolated, a directory name past ~80 chars containing a run of consecutive spaces was folded by the YAML emitter and the space run silently collapsed on round-trip. Removing the field removed the escaping step, which removed the runtimeruamel.yamldependency (now dev-only), which removed the whole bug class. A regression test pins byte-identity against that exact pathological name.The door stays open: a future human-authored display name (e.g. "My thesis" vs
thesis-mcd) would be a user-authored field — never derived, never written byinit— and adding it then would not reverse this.Model default unified to
qwen3:8bThe
model:default was written four inconsistent ways across template, docs, and examples.docs/tech_stack.mdowns this decision — it names the Qwen3 family at 7–8B and defers the exact default to the model spike. All four places now agree onqwen3:8b, the value that doesn't pre-empt the spike.Verification
ruff check,ruff format --check,mypyclean.uv build+ two wheel smoke tests green; the isolatedinitsmoke test diffs the generatedopenkos.yamlagainst the packaged template and gets byte-identical.ruamel-yamlis absent from the built wheel'sRequires-Dist.Review
Reviewed under a bounded 4R pass (risk / resilience / readability / reliability) over the final tree: 0 blocking findings. One non-blocking follow-up remains —
test_fresh_empty_directoryasserts the success message namesopenkos.yamlbut not all five artifacts; a strictly-stronger, test-only assertion, tracked for a follow-up.SDD trail
Full proposal, spec, design, tasks, and verify report live under
openspec/changes/add-init-command/. The change was archived, then reopened when review found thenamefield was write-only; the reopening history is preserved in those artifacts rather than rewritten.