runward 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +13 -0
- package/README.md +128 -0
- package/dist/cli.js +76 -0
- package/dist/commands/check.js +50 -0
- package/dist/commands/doctor.js +75 -0
- package/dist/commands/init.js +90 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/update.js +48 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/mission.js +96 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/styles.js +60 -0
- package/dist/lib/tools.js +59 -0
- package/dist/lib/write.js +30 -0
- package/package.json +29 -0
- package/templates/mission/adr/ADR-0000-template.md +38 -0
- package/templates/mission/architecture.md +60 -0
- package/templates/mission/decision-matrix.md +37 -0
- package/templates/mission/evaluation-rubric.md +60 -0
- package/templates/mission/floor.md +47 -0
- package/templates/mission/framing.md +58 -0
- package/templates/mission/mission-contract.md +58 -0
- package/templates/mission/observability-schema.md +53 -0
- package/templates/mission/port-contract.md +63 -0
- package/templates/mission/reference-stack.md +70 -0
- package/templates/mission/runbook.md +70 -0
- package/templates/mission/shared-bricks.md +74 -0
- package/templates/mission/threat-model.md +53 -0
- package/templates/rules/async-job-guardrails.md +95 -0
- package/templates/rules/async-post-turn-pipeline.md +96 -0
- package/templates/rules/async-scheduled-maintenance.md +95 -0
- package/templates/rules/cache-three-tier-architecture.md +44 -0
- package/templates/rules/checklist-day-zero-project.md +97 -0
- package/templates/rules/checklist-pre-production-observability.md +99 -0
- package/templates/rules/checklist-pre-production-performance.md +109 -0
- package/templates/rules/checklist-pre-production-resilience.md +99 -0
- package/templates/rules/checklist-pre-production-security.md +86 -0
- package/templates/rules/config-secrets-boundary.md +57 -0
- package/templates/rules/config-typing-zod.md +78 -0
- package/templates/rules/contracts-governance.md +64 -0
- package/templates/rules/data-memory-consolidation.md +81 -0
- package/templates/rules/data-memory-invalidation.md +90 -0
- package/templates/rules/data-memory-scoring.md +154 -0
- package/templates/rules/data-migrations-forward-only.md +55 -0
- package/templates/rules/data-orphan-cleanup.md +142 -0
- package/templates/rules/data-ttl-types.md +100 -0
- package/templates/rules/eval-loop.md +52 -0
- package/templates/rules/frontier-deterministic-boundary.md +91 -0
- package/templates/rules/hexa-adapter-pattern.md +85 -0
- package/templates/rules/hexa-architecture.md +84 -0
- package/templates/rules/hexa-move-deterministic-out.md +93 -0
- package/templates/rules/hexa-recommended-stack.md +33 -0
- package/templates/rules/hexa-typescript-native.md +80 -0
- package/templates/rules/observability-alert-configuration.md +160 -0
- package/templates/rules/observability-llm-metrics.md +150 -0
- package/templates/rules/observability-startup-provider-log.md +128 -0
- package/templates/rules/observability-structured-json-logs.md +138 -0
- package/templates/rules/patterns-memory-router-tiered.md +139 -0
- package/templates/rules/patterns-prompt-compiler.md +126 -0
- package/templates/rules/patterns-request-id-propagation.md +137 -0
- package/templates/rules/process-adr-and-journal.md +50 -0
- package/templates/rules/provider-llm-auto-detection.md +54 -0
- package/templates/rules/provider-no-crash-missing-env.md +67 -0
- package/templates/rules/quality-codebase-metrics.md +115 -0
- package/templates/rules/quality-zod-input-validation.md +131 -0
- package/templates/rules/resilience-fail-open.md +65 -0
- package/templates/rules/resilience-multi-provider-fallback.md +100 -0
- package/templates/rules/resilience-retry-backoff.md +115 -0
- package/templates/rules/resilience-retryable-errors.md +107 -0
- package/templates/rules/routing-confidence-upgrade.md +89 -0
- package/templates/rules/routing-model-cost-ratios.md +68 -0
- package/templates/rules/routing-smart-complexity.md +131 -0
- package/templates/rules/scaling-db-connection-pooling.md +121 -0
- package/templates/rules/scaling-distributed-rate-limiting.md +141 -0
- package/templates/rules/scaling-state-externalization.md +84 -0
- package/templates/rules/security-prompt-injection.md +63 -0
- package/templates/rules/state-event-sourcing.md +66 -0
- package/templates/rules/tools-registry-pattern.md +153 -0
- package/templates/rules/tools-scope-atomicity.md +103 -0
- package/templates/targets/AGENTS.md +32 -0
- package/templates/workflows/architect.md +50 -0
- package/templates/workflows/brownfield.md +52 -0
- package/templates/workflows/decision-loop.md +52 -0
- package/templates/workflows/floor.md +60 -0
- package/templates/workflows/frame.md +69 -0
- package/templates/workflows/govern.md +55 -0
- package/templates/workflows/handover.md +55 -0
- package/templates/workflows/iterate.md +52 -0
- package/templates/workflows/method.md +53 -0
- package/templates/workflows/review.md +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thibault Souris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Notice — tooling vs canon
|
|
2
|
+
|
|
3
|
+
This repository (CLI, templates, workflows, examples) is licensed under **MIT**. Use it, fork it, contribute to it.
|
|
4
|
+
|
|
5
|
+
The doctrine **“Designing and Running Agentic Systems”** (Thibault Souris, 2026, registered deposit) is a separate work, published under **CC BY-ND 4.0**. It is the canon this framework implements. Its text is **not** included in this repository and contributions to this repository must not copy its text.
|
|
6
|
+
|
|
7
|
+
What this means in practice:
|
|
8
|
+
|
|
9
|
+
- The **ideas and methods** (the six-phase method, the LLM Boundary Principle, floor-first delivery, evolution on triggers, lethal trifecta rule…) are free by nature. This repository implements them; you can too.
|
|
10
|
+
- The **expression** of the doctrine (its text, figures, layout) is protected: you may share the published PDFs unmodified, you may not create derivative versions of the text.
|
|
11
|
+
- Workflows and templates in this repository are original MIT-licensed writings that implement the method. They are the contribution surface.
|
|
12
|
+
|
|
13
|
+
Doctrine (FR and EN editions): https://github.com/stranxik/designing-and-running-agentic-systems — also at https://thibaultsouris.fr
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Runward
|
|
2
|
+
|
|
3
|
+
[](LICENSE) [](https://github.com/stranxik/designing-and-running-agentic-systems) [](https://ko-fi.com/stranxik)
|
|
4
|
+
|
|
5
|
+
**From business need to a system that holds in production.**
|
|
6
|
+
|
|
7
|
+
Your agent builds fast. Runward frames what it builds — six gated phases your agent executes and one human signs off — so what ships actually holds.
|
|
8
|
+
|
|
9
|
+
Runward is a delivery framework for agentic systems. It covers the entire mission, from framing to handover — it opens before any spec in greenfield, or picks up where the spec tools stop, and carries through to the run and the handover either way. Three ways in: day one of a new project (Frame is the first gate, before any spec exists), from an existing spec (Spec Kit, OpenSpec or in-house — it becomes the input of framing), or from an existing prototype (brownfield: characterize before touching anything). Then: floor first (the floor: the smallest *running* system that proves value on real traffic), evolution on evidence, governance from day zero, and a handover that makes your team autonomous.
|
|
10
|
+
|
|
11
|
+
> Spec Kit, OpenSpec and BMAD stop at authoring: specs, plans, implementation. Runward pilots the whole mission — and picks up their output if you use them. What none of them *structures* is the run: governed memory, resilience, execution security, continuous evaluation, transmission.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Nearly everything that calls itself agentic today dies somewhere between the demo and production. Not because the model is weak, but because nobody can evaluate a non-deterministic behavior, govern it, or say in advance when it fails. These are architecture problems, not model problems. The industry's own signal is unambiguous: model vendors now deploy engineers directly into client organizations, because the bottleneck has moved from the model to its integration into the real world.
|
|
16
|
+
|
|
17
|
+
Spec-driven frameworks answered the first half of that problem: write the right thing. Nobody structured the second half: ship it, run it, hand it over. That is why Runward exists.
|
|
18
|
+
|
|
19
|
+
Agentic systems break four assumptions of classical distributed engineering. The core component is **non-deterministic**: same input, different output, by design. **Input is indistinguishable from instruction**: anything the model reads can try to command it, so prompt injection is structural, not a bug to patch. **Forgetting becomes an engineering problem**: memory that only grows drowns the signal, so decay, invalidation and consolidation have to be designed, not hoped for. And the **blast radius is unprecedented**: an agent with tools acts on the world, so a bad output is no longer just a bad answer.
|
|
20
|
+
|
|
21
|
+
Runward's answer opens with a founding inversion — the **LLM Boundary Principle**: the architecture constrains the model, never the other way around. That opening posture unfolds into five gestures: boundaries before the stack; start simple, isolate by contract, grow on evidence; keep the deterministic out of the model; explicit state and governed memory; govern, trace and evaluate from day zero. The full base is the method itself: six gated phases (a gate: a checkpoint you cross on evidence, never on assertion), each with a Definition of Ready and a Definition of Done, a 22-arbitration decision matrix, and 51 craft rules. The inversion opens the method; the phases, gestures and rules are what carry it.
|
|
22
|
+
|
|
23
|
+
## Who it's for — and when
|
|
24
|
+
|
|
25
|
+
Three profiles, three doors into the same method:
|
|
26
|
+
|
|
27
|
+
| You are | Where you enter |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **A tech lead with a working prototype** that must now hold production | The brownfield workflow: characterize the existing system before touching anything |
|
|
30
|
+
| **An operator or consultant delivering an agentic system for a client**, especially in a regulated sector | Frame: day one is a conversation with your sponsor, not code |
|
|
31
|
+
| **A team finishing a spec** with Spec Kit, OpenSpec or BMAD | Your spec becomes the input of framing; the gates continue from there |
|
|
32
|
+
|
|
33
|
+
**When NOT to use it.** An already-specified tooling request with no production stakes. Recurring operations — Runward hands over, it does not operate. A pure content or assistant use case, with no tools and no blast radius. And if you want a framework that decides for you: Runward makes *you* decide, at every gate.
|
|
34
|
+
|
|
35
|
+
**Monday morning.** Install, run `runward init`, and fill `runward/framing.md` with your sponsor — a conversation, not code. Day one produces a framing note and a mission contract. The first code you write is the floor — weeks in, not months.
|
|
36
|
+
|
|
37
|
+
**The honest timeline** — the one the mission contract states. Framing takes days. An executable floor takes weeks. Iteration is paced by evidence, and handover ends the mission. No dates promised; gates crossed on proof.
|
|
38
|
+
|
|
39
|
+
The cost is discipline: one accountable operator, governance from day zero, and every structural decision written as an ADR (Architecture Decision Record: a dated, locked decision with a re-evaluation trigger). The longer version — the three entry scenarios told concretely, the anti-cases, what you hold at the end — is in [When to use Runward, and when not to](docs/when-to-use.md).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx runward init # interactive wizard
|
|
45
|
+
npx runward --yes init # non-interactive, defaults (CI-friendly)
|
|
46
|
+
npx runward init --tools claude,cursor,copilot,gemini,windsurf
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
New here? Follow [your first mission in 15 minutes](docs/first-mission.md).
|
|
50
|
+
|
|
51
|
+
### Commands
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `runward init` | Scaffold the mission structure (wizard: entry mode, stopping tier, tool profiles) |
|
|
56
|
+
| `runward check` | **Gate audit**: which deliverable, expected at which phase, is missing, started, or filled — exit code 1 on gaps |
|
|
57
|
+
| `runward status` | Mission snapshot: current gate, decision journal (ADRs), workflows |
|
|
58
|
+
| `runward doctor` | Environment and installation checks |
|
|
59
|
+
| `runward update` | Refresh `runward/workflows/` from the package — mission state never touched, local edits preserved unless `--force` |
|
|
60
|
+
|
|
61
|
+
Global flags: `--yes`, `--dry-run`, `--verbose`, `--no-color`. Exit codes: 0 success, 1 gaps/warnings, 2 missing prerequisite.
|
|
62
|
+
|
|
63
|
+
`init` creates:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
your-project/
|
|
67
|
+
├── AGENTS.md # vendor-neutral agent charter (the "law" file)
|
|
68
|
+
├── runward/
|
|
69
|
+
│ ├── framing.md # problem, value, observable success criterion, floor vs target
|
|
70
|
+
│ ├── mission-contract.md # one-page steering contract: engagements, DoD, gates
|
|
71
|
+
│ ├── architecture.md # boundaries, ports, integration protocol — stack stays open
|
|
72
|
+
│ ├── decision-matrix.md # 22 arbitrations: one sober default + one explicit trigger each
|
|
73
|
+
│ ├── reference-stack.md # default adapter kit per layer, with evolution triggers
|
|
74
|
+
│ ├── shared-bricks.md # bricks beyond the app: placement families, brick matrix, sovereignty by data class
|
|
75
|
+
│ ├── floor.md # the smallest system that proves value on real traffic
|
|
76
|
+
│ ├── adr/ # one ADR per structural decision, with re-evaluation trigger
|
|
77
|
+
│ ├── rules/ # 51 craft rules your agent applies while building
|
|
78
|
+
│ ├── governance/
|
|
79
|
+
│ │ ├── threat-model.md # lethal trifecta, 2-of-3 rule on the context window
|
|
80
|
+
│ │ ├── evaluation-rubric.md # test the deterministic, evaluate the non-deterministic
|
|
81
|
+
│ │ └── observability-schema.md
|
|
82
|
+
│ ├── contracts/ # port contracts (versioned, additive, tolerant reader)
|
|
83
|
+
│ ├── runbook.md # recovery runbook for the team that inherits the system
|
|
84
|
+
│ └── workflows/ # the method, executable by your agent
|
|
85
|
+
└── .claude/ | .cursor/ # tool profiles (--tools)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## The method: six phases, gated
|
|
89
|
+
|
|
90
|
+
Each phase has entry conditions (Definition of Ready) and a Definition of Done. You do not move on assertion; you move on evidence.
|
|
91
|
+
|
|
92
|
+
| Phase | Output | Done when |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 1. Frame | `framing.md` | Sponsor validates the observable success criterion and the floor scope |
|
|
95
|
+
| 2. Architect | `architecture.md` + ADRs | Boundaries decided, stack still open |
|
|
96
|
+
| 3. Ship the floor | Running system + `floor.md` | First measured proof against the criterion, on real cases |
|
|
97
|
+
| 4. Iterate on evidence | Increments + ADRs | Every addition traced to an objective trigger |
|
|
98
|
+
| 5. Govern & evaluate | Threat model, rubric, observability | Instrumented from day zero, never retrofitted |
|
|
99
|
+
| 6. Hand over | Handover kit | The team redoes a task autonomously — demonstrated, not declared |
|
|
100
|
+
|
|
101
|
+
Phase 5 is transverse: it starts at day zero, not after the incident.
|
|
102
|
+
|
|
103
|
+
## What makes it different
|
|
104
|
+
|
|
105
|
+
- **Floor, not MVP deck.** The floor is the smallest *running* system that proves value on real traffic. A presentation is not a floor.
|
|
106
|
+
- **Evolution on evidence.** Multi-agent, long-term memory, microservices, a bigger model: each has a sober default and an explicit trigger. No trigger, no complexity. Every switch is an ADR.
|
|
107
|
+
- **Security by architecture, not detection.** Prompt injection is constrained structurally (lethal trifecta, 2-of-3 rule), not filtered heuristically.
|
|
108
|
+
- **Craft rules, not vibes.** 51 engineering craft rules ship with the mission (memory scoring, tiered retrieval, event sourcing, request-id propagation, multi-provider fallback, cost routing, post-turn pipelines, prompt-injection defenses…) — your agent applies them, `runward check` will not invent them. Code examples follow the reference-stack default (a single language in the core — TypeScript by default, an adapter decision like any other: see the decision matrix). The patterns are the contract; the language is the adapter.
|
|
109
|
+
- **One accountable operator, not a simulated team.** One human owns every gate while the agent executes the workflows — see [the operator role](docs/operator-role.md).
|
|
110
|
+
- **Handover as a deliverable.** The mission ends when the receiving team is autonomous, with proof.
|
|
111
|
+
|
|
112
|
+
## The reference floor
|
|
113
|
+
|
|
114
|
+
`floor-ts/` is a clonable scaffold that implements the Runward defaults: hexagonal core, the model behind a port (a port: a stable contract behind which implementations swap — here a deterministic echo adapter by default, zero keys, zero network), provider profiles keyed on base URL, middleware chain (logging, access, cost cap, approval), day-zero cost ceiling, secrets redaction, per-call prompt provenance (a SHA-256 of every prompt), suspend-and-rehydrate on human approval, full test pyramid and an evaluation harness. It is expressed in TypeScript because it implements the reference-stack defaults — one language in the core, and which one is an adapter decision like any other. The durable part is what it demonstrates: the ports, the contracts, the middleware chain, the deterministic guard. Port them to your language and the architecture survives. Its README maps each principle to the exact file that implements it. Start a floor by cloning it, not from a blank page.
|
|
115
|
+
|
|
116
|
+
## Relationship to the canon
|
|
117
|
+
|
|
118
|
+
Runward is the tooling of the doctrine **“Designing and Running Agentic Systems”** (Thibault Souris, 2026) — a 60-page architecture reference, published separately under CC BY-ND 4.0 in [its own repository](https://github.com/stranxik/designing-and-running-agentic-systems). The doctrine is the canon: read it for the *why*. Runward is MIT: fork it, adapt it, contribute. See [NOTICE.md](NOTICE.md).
|
|
119
|
+
|
|
120
|
+
Read the doctrine: [Concevoir et exécuter des systèmes agentiques (FR)](https://github.com/stranxik/designing-and-running-agentic-systems/blob/main/Concevoir-et-executer-des-systemes-agentiques-2026.pdf) · [Designing and Running Agentic Systems (EN)](https://github.com/stranxik/designing-and-running-agentic-systems/blob/main/Designing-and-Running-Agentic-Systems-2026.pdf)
|
|
121
|
+
|
|
122
|
+
## Supported tools
|
|
123
|
+
|
|
124
|
+
Tool profiles: **Claude Code** (slash commands `/rw-*`), **Cursor** (rules), **GitHub Copilot** (instructions), **Gemini CLI** (GEMINI.md), **Windsurf** (rules). `AGENTS.md` is always written — it is the vendor-neutral standard read by Codex CLI, opencode, Amp and a growing list of agents, so the method works even without a dedicated profile. The mission structure itself is plain markdown in your repo. More profiles welcome (see [ROADMAP.md](ROADMAP.md)).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT © Thibault Souris. The doctrine text is licensed separately (CC BY-ND 4.0) and is **not** included in this repository.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runward CLI — after the spec: ship and run.
|
|
4
|
+
* Commands: init (wizard), check (gate audit), status, doctor, update.
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { VERSION } from "./lib/paths.js";
|
|
8
|
+
import { c } from "./lib/styles.js";
|
|
9
|
+
import { initCommand } from "./commands/init.js";
|
|
10
|
+
import { checkCommand } from "./commands/check.js";
|
|
11
|
+
import { statusCommand } from "./commands/status.js";
|
|
12
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
13
|
+
import { updateCommand } from "./commands/update.js";
|
|
14
|
+
// Exit codes: 0 = success · 1 = gaps/warnings · 2 = missing prerequisite
|
|
15
|
+
process.on("uncaughtException", (err) => {
|
|
16
|
+
if (err.name === "ExitPromptError")
|
|
17
|
+
process.exit(130); // Ctrl+C in a prompt
|
|
18
|
+
console.error("\n " + c.error("✗") + " Unexpected error: " + err.message);
|
|
19
|
+
if (process.env.VERBOSE)
|
|
20
|
+
console.error(err.stack);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
process.on("unhandledRejection", (reason) => {
|
|
24
|
+
console.error("\n " + c.error("✗") + " Async error: " + (reason?.message || reason));
|
|
25
|
+
if (process.env.VERBOSE)
|
|
26
|
+
console.error(reason?.stack);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
const program = new Command();
|
|
30
|
+
program
|
|
31
|
+
.name("runward")
|
|
32
|
+
.description("After the spec: ship and run. Delivery framework for agentic systems.")
|
|
33
|
+
.version(VERSION)
|
|
34
|
+
.option("--no-color", "disable colored output")
|
|
35
|
+
.option("--verbose", "detailed logs")
|
|
36
|
+
.option("--yes", "non-interactive: accept all defaults (CI)")
|
|
37
|
+
.option("--dry-run", "print planned actions without writing")
|
|
38
|
+
.hook("preAction", (cmd) => {
|
|
39
|
+
const opts = cmd.opts();
|
|
40
|
+
if (opts.color === false)
|
|
41
|
+
process.env.NO_COLOR = "1";
|
|
42
|
+
if (opts.verbose)
|
|
43
|
+
process.env.VERBOSE = "1";
|
|
44
|
+
if (opts.yes)
|
|
45
|
+
process.env.RUNWARD_YES = "1";
|
|
46
|
+
if (opts.dryRun)
|
|
47
|
+
process.env.RUNWARD_DRY_RUN = "1";
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command("init")
|
|
51
|
+
.description("scaffold the mission structure (interactive wizard, or --yes)")
|
|
52
|
+
.option("-p, --path <path>", "project directory (default: prompt, or . with --yes)")
|
|
53
|
+
.option("-t, --tools <list>", "comma-separated tool profiles: claude,cursor,copilot,gemini,windsurf")
|
|
54
|
+
.option("--force", "overwrite existing files")
|
|
55
|
+
.action(initCommand);
|
|
56
|
+
program
|
|
57
|
+
.command("check")
|
|
58
|
+
.description("can I cross the gate — gate audit, exit 1 on gaps (CI-friendly)")
|
|
59
|
+
.option("-p, --path <path>", "project directory")
|
|
60
|
+
.action(checkCommand);
|
|
61
|
+
program
|
|
62
|
+
.command("status")
|
|
63
|
+
.description("where am I — mission snapshot: current gate, decision journal, workflows")
|
|
64
|
+
.option("-p, --path <path>", "project directory")
|
|
65
|
+
.action(statusCommand);
|
|
66
|
+
program
|
|
67
|
+
.command("doctor")
|
|
68
|
+
.description("environment and installation checks")
|
|
69
|
+
.action(doctorCommand);
|
|
70
|
+
program
|
|
71
|
+
.command("update")
|
|
72
|
+
.description("refresh runward/workflows/ and runward/rules/ from this package version (mission state untouched)")
|
|
73
|
+
.option("-p, --path <path>", "project directory")
|
|
74
|
+
.option("--force", "overwrite locally modified workflows")
|
|
75
|
+
.action(updateCommand);
|
|
76
|
+
program.parseAsync();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
3
|
+
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
4
|
+
import { VERSION } from "../lib/paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Gate audit — the gap analysis: which deliverable, expected at which
|
|
7
|
+
* phase, is present, started, or still a raw template.
|
|
8
|
+
* Exit codes: 0 = current gate clean, 1 = gaps, 2 = no mission found.
|
|
9
|
+
*/
|
|
10
|
+
export async function checkCommand(opts) {
|
|
11
|
+
const root = findMissionRoot(join(process.cwd(), opts.path ?? "."));
|
|
12
|
+
if (!root) {
|
|
13
|
+
console.error(status.error("No runward/ mission found here or above. Run `runward init` first."));
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const mission = join(root, "runward");
|
|
17
|
+
const report = analyze(mission);
|
|
18
|
+
console.log(createHeader(`Runward v${VERSION} — gate audit`, root));
|
|
19
|
+
const glyph = {
|
|
20
|
+
"filled": c.success("✓"),
|
|
21
|
+
"in-progress": c.warning("◐"),
|
|
22
|
+
"untouched": c.darkGray("○"),
|
|
23
|
+
"missing": c.error("✗"),
|
|
24
|
+
};
|
|
25
|
+
const legendNote = {
|
|
26
|
+
"filled": "",
|
|
27
|
+
"in-progress": c.warning(" — placeholders remain"),
|
|
28
|
+
"untouched": c.darkGray(" — raw template"),
|
|
29
|
+
"missing": c.error(" — file missing"),
|
|
30
|
+
};
|
|
31
|
+
let gaps = 0;
|
|
32
|
+
for (const phase of report.phases) {
|
|
33
|
+
console.log(section(phase.spec.label));
|
|
34
|
+
for (const { artifact, state } of phase.artifacts) {
|
|
35
|
+
console.log(` ${glyph[state]} ${c.white(artifact.label)} ${c.darkGray(`(runward/${artifact.relPath})`)}${legendNote[state]}`);
|
|
36
|
+
if (state !== "filled")
|
|
37
|
+
gaps++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log(section("Summary"));
|
|
41
|
+
console.log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
|
|
42
|
+
console.log(` ${c.primaryBold("ADRs")} ${c.white(String(report.adrCount))}${report.adrCount === 0 ? c.warning(" — no structural decision locked yet") : ""}`);
|
|
43
|
+
if (gaps === 0) {
|
|
44
|
+
console.log("\n" + status.success("All expected deliverables are filled. Cross gates on evidence, not on paperwork."));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log("\n" + status.warning(`${gaps} deliverable(s) not filled. The gate rule: no phase closes without its artifact.`));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { TEMPLATES, MISSION_LAYOUT, VERSION, WORKFLOWS } from "../lib/paths.js";
|
|
5
|
+
import { EXPECTED_RULES } from "../lib/constants.js";
|
|
6
|
+
import { findMissionRoot } from "../lib/mission.js";
|
|
7
|
+
import { createHeader, section, status } from "../lib/styles.js";
|
|
8
|
+
/**
|
|
9
|
+
* Environment and installation checks.
|
|
10
|
+
* Exit codes: 0 = all good, 1 = warnings, 2 = critical failure.
|
|
11
|
+
*/
|
|
12
|
+
export async function doctorCommand() {
|
|
13
|
+
console.log(createHeader(`Runward v${VERSION} — doctor`));
|
|
14
|
+
let warnings = 0;
|
|
15
|
+
let critical = 0;
|
|
16
|
+
const ok = (msg) => console.log(" " + status.success(msg));
|
|
17
|
+
const warn = (msg) => { warnings++; console.log(" " + status.warning(msg)); };
|
|
18
|
+
const fail = (msg) => { critical++; console.log(" " + status.error(msg)); };
|
|
19
|
+
console.log(section("Environment"));
|
|
20
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
21
|
+
nodeMajor >= 20 ? ok(`node ${process.versions.node}`) : fail(`node ${process.versions.node} — v20+ required`);
|
|
22
|
+
try {
|
|
23
|
+
const git = execFileSync("git", ["--version"], { encoding: "utf8" }).trim();
|
|
24
|
+
ok(git);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
warn("git not found — the method assumes versioned mission artifacts");
|
|
28
|
+
}
|
|
29
|
+
console.log(section("Package integrity"));
|
|
30
|
+
const missingTpl = Object.keys(MISSION_LAYOUT).filter((k) => !existsSync(join(TEMPLATES, "mission", k)));
|
|
31
|
+
missingTpl.length === 0 ? ok(`${Object.keys(MISSION_LAYOUT).length} mission templates`) : fail(`missing templates: ${missingTpl.join(", ")}`);
|
|
32
|
+
const missingWf = WORKFLOWS.filter((wf) => !existsSync(join(TEMPLATES, "workflows", `${wf}.md`)));
|
|
33
|
+
missingWf.length === 0 ? ok(`${WORKFLOWS.length} workflows`) : fail(`missing workflows: ${missingWf.join(", ")}`);
|
|
34
|
+
const rulesDir = join(TEMPLATES, "rules");
|
|
35
|
+
const ruleCount = existsSync(rulesDir) ? readdirSync(rulesDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
36
|
+
ruleCount === EXPECTED_RULES ? ok(`${ruleCount} craft rules`) : fail(`craft rules mismatch: ${ruleCount}/${EXPECTED_RULES}`);
|
|
37
|
+
console.log(section("Current directory"));
|
|
38
|
+
const root = findMissionRoot(process.cwd());
|
|
39
|
+
if (!root) {
|
|
40
|
+
warn("no runward/ mission here — `runward init` to scaffold one");
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
ok(`mission found at ${root}`);
|
|
44
|
+
existsSync(join(root, "AGENTS.md")) ? ok("AGENTS.md charter present") : warn("AGENTS.md missing — re-run `runward init`");
|
|
45
|
+
const gitDir = join(root, ".git");
|
|
46
|
+
existsSync(gitDir) ? ok("mission is under git") : warn("mission is not a git repository — ADRs and gates should be versioned");
|
|
47
|
+
const profiles = [];
|
|
48
|
+
if (existsSync(join(root, ".claude", "commands")) &&
|
|
49
|
+
readdirSync(join(root, ".claude", "commands")).some((f) => f.startsWith("rw-")))
|
|
50
|
+
profiles.push("claude");
|
|
51
|
+
if (existsSync(join(root, ".cursor", "rules", "runward.mdc")))
|
|
52
|
+
profiles.push("cursor");
|
|
53
|
+
if (existsSync(join(root, ".github", "copilot-instructions.md")))
|
|
54
|
+
profiles.push("copilot");
|
|
55
|
+
if (existsSync(join(root, "GEMINI.md")))
|
|
56
|
+
profiles.push("gemini");
|
|
57
|
+
if (existsSync(join(root, ".windsurf", "rules", "runward.md")))
|
|
58
|
+
profiles.push("windsurf");
|
|
59
|
+
profiles.length > 0
|
|
60
|
+
? ok(`tool profiles: ${profiles.join(", ")}`)
|
|
61
|
+
: warn("no tool profile detected — AGENTS.md still works with agents that read it");
|
|
62
|
+
}
|
|
63
|
+
console.log(section("Verdict"));
|
|
64
|
+
if (critical > 0) {
|
|
65
|
+
console.log(" " + status.error(`${critical} critical issue(s), ${warnings} warning(s)`));
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
else if (warnings > 0) {
|
|
69
|
+
console.log(" " + status.warning(`${warnings} warning(s)`));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
console.log(" " + status.success("all checks passed"));
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { checkbox, input, select } from "@inquirer/prompts";
|
|
4
|
+
import { TEMPLATES, MISSION_LAYOUT, VERSION } from "../lib/paths.js";
|
|
5
|
+
import { TOOL_PROFILES, TOOL_IDS } from "../lib/tools.js";
|
|
6
|
+
import { makeWriter } from "../lib/write.js";
|
|
7
|
+
import { c, createHeader, isNonInteractive, section, status } from "../lib/styles.js";
|
|
8
|
+
export async function initCommand(opts) {
|
|
9
|
+
console.log(createHeader(`Runward v${VERSION}`, "After the spec: ship and run"));
|
|
10
|
+
const yes = isNonInteractive();
|
|
11
|
+
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
12
|
+
// ── Gather answers (wizard, or defaults with --yes) ──────────────
|
|
13
|
+
const dir = opts.path
|
|
14
|
+
?? (yes ? "." : await input({ message: "Project directory", default: "." }));
|
|
15
|
+
// The idea seeds the framing note: you arrive with a project, not with paperwork.
|
|
16
|
+
const idea = yes ? "" : await input({
|
|
17
|
+
message: "What are you building? (one line — it seeds your framing note)",
|
|
18
|
+
default: "",
|
|
19
|
+
});
|
|
20
|
+
const tools = opts.tools !== undefined
|
|
21
|
+
? opts.tools.split(",").map((s) => s.trim()).filter(Boolean)
|
|
22
|
+
: yes
|
|
23
|
+
? ["claude"]
|
|
24
|
+
: await checkbox({
|
|
25
|
+
message: "Tool profiles (AGENTS.md is always written)",
|
|
26
|
+
choices: TOOL_PROFILES.map((t) => ({ value: t.id, name: t.label, checked: t.id === "claude" })),
|
|
27
|
+
});
|
|
28
|
+
const entryMode = yes ? "greenfield" : await select({
|
|
29
|
+
message: "Entry mode",
|
|
30
|
+
choices: [
|
|
31
|
+
{ value: "greenfield", name: "Greenfield — new system, run the chain from the top" },
|
|
32
|
+
{ value: "brownfield", name: "Brownfield — existing system, characterize before touching" },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
const tier = yes ? "floor" : await select({
|
|
36
|
+
message: "Stopping tier (the sponsor's choice — can be revised)",
|
|
37
|
+
choices: [
|
|
38
|
+
{ value: "framing", name: "Framing only" },
|
|
39
|
+
{ value: "floor", name: "Executable floor, proven on real traffic" },
|
|
40
|
+
{ value: "full chain", name: "Full chain through handover" },
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
const unknown = tools.filter((t) => !TOOL_IDS.includes(t));
|
|
44
|
+
for (const t of unknown)
|
|
45
|
+
console.log(status.warning(`Unknown tool profile "${t}" — supported: ${TOOL_IDS.join(", ")}`));
|
|
46
|
+
// ── Write ─────────────────────────────────────────────────────────
|
|
47
|
+
const root = join(process.cwd(), dir);
|
|
48
|
+
const mission = join(root, "runward");
|
|
49
|
+
const w = makeWriter({ force: opts.force ?? false, dryRun, root });
|
|
50
|
+
console.log(section("Mission structure"));
|
|
51
|
+
const prefill = (s) => {
|
|
52
|
+
let out = s
|
|
53
|
+
.replace("[greenfield | brownfield M1–M4]", entryMode)
|
|
54
|
+
.replace("[framing | floor | full chain]", tier);
|
|
55
|
+
if (idea) {
|
|
56
|
+
out = out
|
|
57
|
+
.replace("[system or mission name]", idea)
|
|
58
|
+
.replace("## 1. Problem\n", `## 1. Problem\n\n> Seed idea (from init): "${idea}" — now replace this with the process as actually observed.\n`);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
};
|
|
62
|
+
for (const [src, dest] of Object.entries(MISSION_LAYOUT)) {
|
|
63
|
+
w.copy(join(TEMPLATES, "mission", src), join(mission, dest), src === "framing.md" ? prefill : undefined);
|
|
64
|
+
}
|
|
65
|
+
console.log(section("Workflows"));
|
|
66
|
+
for (const wf of readdirSync(join(TEMPLATES, "workflows"))) {
|
|
67
|
+
w.copy(join(TEMPLATES, "workflows", wf), join(mission, "workflows", wf));
|
|
68
|
+
}
|
|
69
|
+
console.log(section("Craft rules"));
|
|
70
|
+
const rules = readdirSync(join(TEMPLATES, "rules"));
|
|
71
|
+
for (const r of rules) {
|
|
72
|
+
w.copy(join(TEMPLATES, "rules", r), join(mission, "rules", r));
|
|
73
|
+
}
|
|
74
|
+
console.log(section("Agent charter"));
|
|
75
|
+
w.copy(join(TEMPLATES, "targets", "AGENTS.md"), join(root, "AGENTS.md"));
|
|
76
|
+
for (const profile of TOOL_PROFILES.filter((p) => tools.includes(p.id))) {
|
|
77
|
+
console.log(section(`Profile · ${profile.label}`));
|
|
78
|
+
for (const f of profile.files(root))
|
|
79
|
+
w.write(f.path, f.content);
|
|
80
|
+
}
|
|
81
|
+
// ── Summary ───────────────────────────────────────────────────────
|
|
82
|
+
console.log(section("Done"));
|
|
83
|
+
console.log(status.success(`${w.stats.written} file(s) ${dryRun ? "planned" : "written"}, ${w.stats.skipped} skipped`));
|
|
84
|
+
console.log(`
|
|
85
|
+
${c.primaryBold("Next steps")}
|
|
86
|
+
1. Fill ${c.white("runward/framing.md")} — do not architect before the framing gate passes.
|
|
87
|
+
2. Point your agent at ${c.white("AGENTS.md")} and ${c.white("runward/workflows/method.md")}.
|
|
88
|
+
3. Run ${c.primary("runward check")} anytime to see which gate you are at.
|
|
89
|
+
${entryMode === "brownfield" ? c.warning("\n Brownfield entry: start with runward/workflows/brownfield.md — characterize before touching.\n") : ""}`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { analyze, findMissionRoot } from "../lib/mission.js";
|
|
4
|
+
import { c, createHeader, section } from "../lib/styles.js";
|
|
5
|
+
import { VERSION, WORKFLOWS } from "../lib/paths.js";
|
|
6
|
+
/** Mission snapshot: phase, ADR journal, framing summary, workflow presence. */
|
|
7
|
+
export async function statusCommand(opts) {
|
|
8
|
+
const root = findMissionRoot(join(process.cwd(), opts.path ?? "."));
|
|
9
|
+
if (!root) {
|
|
10
|
+
console.error(c.error("✗ ") + "No runward/ mission found. Run `runward init` first.");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
const mission = join(root, "runward");
|
|
14
|
+
const report = analyze(mission);
|
|
15
|
+
console.log(createHeader(`Runward v${VERSION} — mission status`, root));
|
|
16
|
+
// Framing summary: first line of Problem + success criterion heading state
|
|
17
|
+
console.log(section("Mission"));
|
|
18
|
+
const framingPath = join(mission, "framing.md");
|
|
19
|
+
if (existsSync(framingPath)) {
|
|
20
|
+
const title = readFileSync(framingPath, "utf8").split("\n")[0]?.replace(/^#\s*/, "") ?? "";
|
|
21
|
+
console.log(` ${c.white(title)}`);
|
|
22
|
+
}
|
|
23
|
+
console.log(` ${c.primaryBold("Current gate")} ${c.white(report.currentPhase)}`);
|
|
24
|
+
console.log(section("Decision journal"));
|
|
25
|
+
const adrDir = join(mission, "adr");
|
|
26
|
+
const adrs = existsSync(adrDir)
|
|
27
|
+
? readdirSync(adrDir).filter((f) => /^ADR-\d+/.test(f) && !f.includes("0000")).sort()
|
|
28
|
+
: [];
|
|
29
|
+
if (adrs.length === 0) {
|
|
30
|
+
console.log(c.darkGray(" no ADR yet — every structural decision must be locked"));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const f of adrs.slice(-5)) {
|
|
34
|
+
// Date the ADR from its own `**Date**:` line; fall back to file mtime.
|
|
35
|
+
const dateLine = readFileSync(join(adrDir, f), "utf8").match(/^\*\*Date\*\*:\s*(\d{4}-\d{2}-\d{2})/m);
|
|
36
|
+
const date = dateLine ? dateLine[1] : statSync(join(adrDir, f)).mtime.toISOString().slice(0, 10);
|
|
37
|
+
console.log(` ${c.primary("•")} ${c.white(f)} ${c.darkGray(date)}`);
|
|
38
|
+
}
|
|
39
|
+
if (adrs.length > 5)
|
|
40
|
+
console.log(c.darkGray(` … and ${adrs.length - 5} more`));
|
|
41
|
+
}
|
|
42
|
+
console.log(section("Workflows"));
|
|
43
|
+
const missing = WORKFLOWS.filter((wf) => !existsSync(join(mission, "workflows", `${wf}.md`)));
|
|
44
|
+
if (missing.length === 0)
|
|
45
|
+
console.log(c.success(" ✓ ") + c.white(`all ${WORKFLOWS.length} workflows present`));
|
|
46
|
+
else
|
|
47
|
+
console.log(c.warning(" ! ") + c.white(`missing: ${missing.join(", ")} — run \`runward update\``));
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { TEMPLATES, VERSION } from "../lib/paths.js";
|
|
4
|
+
import { findMissionRoot } from "../lib/mission.js";
|
|
5
|
+
import { makeWriter } from "../lib/write.js";
|
|
6
|
+
import { c, createHeader, section, status } from "../lib/styles.js";
|
|
7
|
+
/**
|
|
8
|
+
* Refresh runward/workflows/ and runward/rules/ from the installed package.
|
|
9
|
+
* Never touches mission state (framing, architecture, ADRs, governance).
|
|
10
|
+
* Local edits are preserved unless --force.
|
|
11
|
+
*/
|
|
12
|
+
export async function updateCommand(opts) {
|
|
13
|
+
const root = findMissionRoot(join(process.cwd(), opts.path ?? "."));
|
|
14
|
+
if (!root) {
|
|
15
|
+
console.error(status.error("No runward/ mission found. Run `runward init` first."));
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
const dryRun = process.env.RUNWARD_DRY_RUN === "1";
|
|
19
|
+
console.log(createHeader(`Runward v${VERSION} — update workflows & rules`, root));
|
|
20
|
+
let same = 0, drifted = 0, added = 0;
|
|
21
|
+
const w = makeWriter({ force: true, dryRun, root });
|
|
22
|
+
for (const dir of ["workflows", "rules"]) {
|
|
23
|
+
console.log(section(dir === "workflows" ? "Workflows" : "Craft rules"));
|
|
24
|
+
const dest = join(root, "runward", dir);
|
|
25
|
+
for (const file of readdirSync(join(TEMPLATES, dir))) {
|
|
26
|
+
const src = readFileSync(join(TEMPLATES, dir, file), "utf8");
|
|
27
|
+
const destPath = join(dest, file);
|
|
28
|
+
if (!existsSync(destPath)) {
|
|
29
|
+
w.write(destPath, src);
|
|
30
|
+
added++;
|
|
31
|
+
}
|
|
32
|
+
else if (readFileSync(destPath, "utf8") === src) {
|
|
33
|
+
same++;
|
|
34
|
+
}
|
|
35
|
+
else if (opts.force) {
|
|
36
|
+
w.write(destPath, src);
|
|
37
|
+
drifted++;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
drifted++;
|
|
41
|
+
console.log(` ${c.warning("drift")} ${c.white(`runward/${dir}/${file}`)} ${c.darkGray("(locally modified — --force to overwrite)")}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
console.log(section("Summary"));
|
|
46
|
+
console.log(` ${status.success(`${same} up to date`)} ${added ? status.info(`${added} added`) + " " : ""}${drifted ? status.warning(`${drifted} drifted${opts.force ? " (overwritten)" : ""}`) : ""}`);
|
|
47
|
+
console.log(c.darkGray("\n Mission state (framing, architecture, ADRs, governance) is never touched by update.\n"));
|
|
48
|
+
}
|