nikos 0.0.7 → 0.1.1
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/README.md +5 -4
- package/dist/cli.js +25 -5
- package/dist/commands/init.js +8 -1
- package/dist/commands/map.js +272 -0
- package/dist/commands/release.js +203 -0
- package/dist/commands/show.js +7 -7
- package/dist/commands/step.js +59 -0
- package/dist/commands/story.js +9 -3
- package/dist/commands/validate.js +46 -31
- package/dist/lib/bets.js +25 -0
- package/dist/lib/frontmatter.js +8 -0
- package/dist/lib/project.js +37 -0
- package/dist/lint/gherkin.js +2 -2
- package/dist/planning/bets.js +73 -0
- package/dist/planning/cli.js +152 -0
- package/dist/planning/delivery.js +20 -0
- package/dist/planning/krs.js +73 -0
- package/dist/planning/model.js +164 -0
- package/dist/planning/server.js +336 -0
- package/dist/planning/ui.js +452 -0
- package/dist/planning/upstream.js +175 -0
- package/docs/format.md +51 -55
- package/docs/gherkin.md +2 -1
- package/docs/install.md +12 -5
- package/docs/planning.md +124 -0
- package/package.json +2 -1
- package/schemas/planning/bet.schema.json +42 -0
- package/schemas/planning/kr.schema.json +35 -0
- package/schemas/planning/plan.schema.json +28 -0
- package/schemas/storymap.schema.json +94 -11
- package/skills/nikos-mode/playbooks/idea-to-blueprint.md +1 -1
- package/skills/nikos-mode/playbooks/index.md +1 -1
- package/skills/nikos-mode/playbooks/planning.md +11 -0
- package/skills/nikos-mode/principles/index.md +0 -2
- package/dist/commands/bet.js +0 -97
- package/docs/intentdocs-mapping.md +0 -117
- package/schemas/bet.schema.json +0 -23
- package/skills/nikos-mode/playbooks/bet.md +0 -14
package/dist/commands/bet.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { Document, parse } from "yaml";
|
|
4
|
-
import { STORY_ID, findIntentDir, read } from "../lib/project.js";
|
|
5
|
-
export const BET_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
6
|
-
/** Story ids in any release of storymap.yaml, or undefined if there is no story map. */
|
|
7
|
-
function mapStories(intentDir) {
|
|
8
|
-
const path = join(intentDir, "storymap.yaml");
|
|
9
|
-
if (!existsSync(path))
|
|
10
|
-
return undefined;
|
|
11
|
-
const map = parse(read(path));
|
|
12
|
-
return new Set((map?.releases ?? []).flatMap((r) => r?.stories ?? []));
|
|
13
|
-
}
|
|
14
|
-
/** Split a bet card into its front matter and body. */
|
|
15
|
-
export function parseBet(text) {
|
|
16
|
-
const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
|
|
17
|
-
if (!m)
|
|
18
|
-
return undefined;
|
|
19
|
-
return { meta: parse(m[1]), body: text.slice(m[0].length) };
|
|
20
|
-
}
|
|
21
|
-
/** Write intent/bets/<id>.md with every field filled in, linked to stories that exist. */
|
|
22
|
-
export function newBet(input, dir) {
|
|
23
|
-
if (!BET_ID.test(input.id))
|
|
24
|
-
throw new Error(`'${input.id}' is not a bet id (lowercase words joined by hyphens, e.g. faster-payment)`);
|
|
25
|
-
const fields = [
|
|
26
|
-
["hypothesis", input.hypothesis],
|
|
27
|
-
["key result (--kr)", input.keyResult],
|
|
28
|
-
["expected uplift (--uplift)", input.expectedUplift],
|
|
29
|
-
];
|
|
30
|
-
for (const [name, value] of fields)
|
|
31
|
-
if (!value?.trim())
|
|
32
|
-
throw new Error(`A bet needs a ${name}`);
|
|
33
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.timeBox ?? ""))
|
|
34
|
-
throw new Error("A bet needs a time box end date (--time-box YYYY-MM-DD)");
|
|
35
|
-
if (!input.stories.length)
|
|
36
|
-
throw new Error("A bet needs at least one story (--story <id>)");
|
|
37
|
-
const bad = input.stories.filter((s) => !STORY_ID.test(s));
|
|
38
|
-
if (bad.length)
|
|
39
|
-
throw new Error(`Not story ids: ${bad.join(", ")} (expected e.g. HT-1)`);
|
|
40
|
-
const intentDir = findIntentDir(dir);
|
|
41
|
-
const known = mapStories(intentDir);
|
|
42
|
-
const unknown = known ? input.stories.filter((s) => !known.has(s)) : [];
|
|
43
|
-
if (unknown.length)
|
|
44
|
-
throw new Error(`Not in storymap.yaml: ${unknown.join(", ")}`);
|
|
45
|
-
const meta = {
|
|
46
|
-
id: input.id,
|
|
47
|
-
hypothesis: input.hypothesis.trim(),
|
|
48
|
-
key_result: input.keyResult.trim(),
|
|
49
|
-
expected_uplift: input.expectedUplift.trim(),
|
|
50
|
-
time_box: input.timeBox,
|
|
51
|
-
blockers: input.blockers ?? [],
|
|
52
|
-
stories: [...new Set(input.stories)],
|
|
53
|
-
status: "open",
|
|
54
|
-
};
|
|
55
|
-
const body = "\n## Why we think so\nThe evidence behind the hypothesis.\n\n## Outcome\nFilled in when the time box ends: the key result as measured, and won, lost or killed.\n";
|
|
56
|
-
const path = join(intentDir, "bets", `${input.id}.md`);
|
|
57
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
-
// "wx" fails if the file exists, so an existing bet is never overwritten.
|
|
59
|
-
try {
|
|
60
|
-
const doc = new Document(meta);
|
|
61
|
-
for (const key of ["blockers", "stories"])
|
|
62
|
-
doc.get(key, true).flow = true;
|
|
63
|
-
writeFileSync(path, `---\n${doc.toString({ flowCollectionPadding: false, lineWidth: 0 })}---\n${body}`, { flag: "wx" });
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
if (err.code === "EEXIST")
|
|
67
|
-
throw new Error(`A bet called '${input.id}' already exists`);
|
|
68
|
-
throw err;
|
|
69
|
-
}
|
|
70
|
-
return path;
|
|
71
|
-
}
|
|
72
|
-
export function runBet(args) {
|
|
73
|
-
const [sub, id, ...rest] = args;
|
|
74
|
-
const all = (n) => rest.flatMap((a, i) => (a === `--${n}` && rest[i + 1] !== undefined ? [rest[i + 1]] : []));
|
|
75
|
-
const opt = (n) => all(n)[0];
|
|
76
|
-
if (sub !== "new" || !id || id.startsWith("--")) {
|
|
77
|
-
console.error('Usage: nikos bet new <bet-id> --hypothesis "<…>" --kr "<key result>" --uplift "<expected uplift>" --time-box YYYY-MM-DD --story <id> [--story <id>] [--blocker "<…>"]');
|
|
78
|
-
return 1;
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const path = newBet({
|
|
82
|
-
id,
|
|
83
|
-
hypothesis: opt("hypothesis") ?? "",
|
|
84
|
-
keyResult: opt("kr") ?? "",
|
|
85
|
-
expectedUplift: opt("uplift") ?? "",
|
|
86
|
-
timeBox: opt("time-box") ?? "",
|
|
87
|
-
stories: all("story"),
|
|
88
|
-
blockers: all("blocker"),
|
|
89
|
-
}, opt("dir") ?? process.cwd());
|
|
90
|
-
console.log(`Created ${path}`);
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
console.error(`✗ ${err.message}`);
|
|
95
|
-
return 1;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# Mapping intent/ to intentdocs
|
|
2
|
-
|
|
3
|
-
`intentdocs push` and `intentdocs pull` translate between the local
|
|
4
|
-
`intent/` folder and an intentdocs project. This table is the contract: every
|
|
5
|
-
local field maps to an intentdocs field, or is marked **local-only**.
|
|
6
|
-
Fields that exist only in intentdocs are listed at the end.
|
|
7
|
-
|
|
8
|
-
## Project and vision
|
|
9
|
-
|
|
10
|
-
| Local | intentdocs | Notes |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| `constitution.md` front matter `name` | project name | |
|
|
13
|
-
| `constitution.md` front matter `idea` | project idea | |
|
|
14
|
-
| `## Users` | vision `users` | |
|
|
15
|
-
| `## Problem` | vision `problem` | |
|
|
16
|
-
| `## Switch` | vision `switch` | |
|
|
17
|
-
| `## First` | vision `first` | |
|
|
18
|
-
| `## Proof` | vision `proof` | |
|
|
19
|
-
| extra `##` sections (e.g. `## Validation`) | **local-only** | pushed as a project note in a later format version |
|
|
20
|
-
| `storymap.yaml` `key` | project story key prefix | e.g. `HT` → `HT-1` |
|
|
21
|
-
| `storymap.yaml` `project` | **local-only** | slug used for paths |
|
|
22
|
-
| `storymap.yaml` `format_version` | **local-only** | |
|
|
23
|
-
|
|
24
|
-
## Activities and releases
|
|
25
|
-
|
|
26
|
-
| Local | intentdocs | Notes |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `activities[].title` | story `activity` | intentdocs groups by activity name |
|
|
29
|
-
| `activities[].id` | **local-only** | folder slug under `features/` |
|
|
30
|
-
| `releases[].number` | release order | |
|
|
31
|
-
| `releases[].title` | release label | |
|
|
32
|
-
| `releases[].stories` order | build sequence within the release | |
|
|
33
|
-
|
|
34
|
-
## Stories (feature files)
|
|
35
|
-
|
|
36
|
-
| Local | intentdocs | Notes |
|
|
37
|
-
|---|---|---|
|
|
38
|
-
| story id tag, e.g. `@HT-1` | story `key` | |
|
|
39
|
-
| `Feature:` title | story `title` | |
|
|
40
|
-
| description `As a …` | `as_a` | |
|
|
41
|
-
| description `I want …` | `i_want` | |
|
|
42
|
-
| description `So that …` | `so_that` | |
|
|
43
|
-
| `@must` / `@should` / `@could` on the Feature | story `priority` | |
|
|
44
|
-
| `@release-N` | story `release` | must agree with `storymap.yaml` |
|
|
45
|
-
| `@activity-<id>` | story `activity` | resolved to the activity title |
|
|
46
|
-
| `@effort-XS` … `@effort-XL` (optional) | story `effort` | |
|
|
47
|
-
| file path | **local-only** | |
|
|
48
|
-
|
|
49
|
-
## Acceptance criteria (scenarios)
|
|
50
|
-
|
|
51
|
-
| Local | intentdocs | Notes |
|
|
52
|
-
|---|---|---|
|
|
53
|
-
| `@AC<n>` tag and scenario title prefix | criterion `label` | |
|
|
54
|
-
| scenario title after the label + its steps | criterion `text` | pushed as one Given/When/Then sentence |
|
|
55
|
-
| `@must` / `@should` / `@could` on the Scenario | criterion `priority` | |
|
|
56
|
-
| scenario pass/fail from `nikos verify` | criterion `last_result` | sent via report_criteria |
|
|
57
|
-
| step definition file and test name | linked tests | sent via save_tests |
|
|
58
|
-
|
|
59
|
-
## Plans
|
|
60
|
-
|
|
61
|
-
| Local | intentdocs | Notes |
|
|
62
|
-
|---|---|---|
|
|
63
|
-
| file `plans/<id>.v<N>.md` | plan `version` N | |
|
|
64
|
-
| front matter `summary` | plan `summary` | |
|
|
65
|
-
| `## <n>. <title>` | step `title` | |
|
|
66
|
-
| `**Why:**` | step `why` | |
|
|
67
|
-
| `**Criteria:**` | step `criteria` | labels resolved to criterion ids |
|
|
68
|
-
| `**Files:**` | step `files` | |
|
|
69
|
-
| `**Tests:**` (optional) | step `tests` | |
|
|
70
|
-
| `**Needs you:**` (optional) | step `needs_you.question` | |
|
|
71
|
-
| body text | step `agent_notes` | |
|
|
72
|
-
| front matter `risk` (optional) | plan `risk` | |
|
|
73
|
-
| front matter `introduces` (optional) | plan `introduces` | new dependencies |
|
|
74
|
-
| front matter `supersedes`, `created` | **local-only** | intentdocs keeps its own history |
|
|
75
|
-
|
|
76
|
-
## Decisions
|
|
77
|
-
|
|
78
|
-
| Local | intentdocs | Notes |
|
|
79
|
-
|---|---|---|
|
|
80
|
-
| entry heading title | decision `title` | |
|
|
81
|
-
| entry body | decision `description` | |
|
|
82
|
-
| `**Category:**` | decision `category` | |
|
|
83
|
-
| entry date | decision created date | |
|
|
84
|
-
| story id in heading | decision linked story | |
|
|
85
|
-
| `**Supersedes:**` | **local-only** | intentdocs amends instead |
|
|
86
|
-
|
|
87
|
-
## State
|
|
88
|
-
|
|
89
|
-
Status isn't pushed: intentdocs sets it. New stories start as To do,
|
|
90
|
-
existing ones keep theirs, and they move through build and acceptance in
|
|
91
|
-
intentdocs. `intentdocs push` still sends the local status, and says how
|
|
92
|
-
many it ignored.
|
|
93
|
-
|
|
94
|
-
| Local | intentdocs | Notes |
|
|
95
|
-
|---|---|---|
|
|
96
|
-
| `status: todo` | **not pushed** | intentdocs sets status |
|
|
97
|
-
| `status: in_progress` | **not pushed** | intentdocs sets status |
|
|
98
|
-
| `status: needs_rework` | **not pushed** | intentdocs sets status |
|
|
99
|
-
| `status: done` | **not pushed** | intentdocs sets status; its own done needs acceptance after build |
|
|
100
|
-
| `blockers` | raised blockers | |
|
|
101
|
-
| `plan_version` | current plan version | |
|
|
102
|
-
| `claimed_by` | story claim | |
|
|
103
|
-
| `run.queue` | build run story order | |
|
|
104
|
-
| `updated` | **local-only** | |
|
|
105
|
-
|
|
106
|
-
## Bets
|
|
107
|
-
|
|
108
|
-
| Local | intentdocs | Notes |
|
|
109
|
-
|---|---|---|
|
|
110
|
-
| `outreach/*.md`, `## Validation` | **local-only** | intentdocs keeps the vision answers, not outreach drafts or results |
|
|
111
|
-
| `bets/*.md` | **local-only** | intentdocs has no bet object yet |
|
|
112
|
-
|
|
113
|
-
## intentdocs-only (not pulled into files)
|
|
114
|
-
|
|
115
|
-
Comments, design references, plan approvals, story approval after build,
|
|
116
|
-
founder to-dos, releases readiness checks, collaborators and workspace
|
|
117
|
-
settings stay in intentdocs. `intentdocs pull` never writes them to the repo.
|
package/schemas/bet.schema.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://nikos.tools/schemas/bet.schema.json",
|
|
4
|
-
"title": "nikOS bet card front matter",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"required": ["id", "hypothesis", "key_result", "expected_uplift", "time_box", "blockers", "stories", "status"],
|
|
7
|
-
"additionalProperties": false,
|
|
8
|
-
"properties": {
|
|
9
|
-
"id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
10
|
-
"hypothesis": { "type": "string", "minLength": 1 },
|
|
11
|
-
"key_result": { "type": "string", "minLength": 1 },
|
|
12
|
-
"expected_uplift": { "type": "string", "minLength": 1 },
|
|
13
|
-
"time_box": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
|
|
14
|
-
"blockers": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
15
|
-
"stories": {
|
|
16
|
-
"type": "array",
|
|
17
|
-
"minItems": 1,
|
|
18
|
-
"uniqueItems": true,
|
|
19
|
-
"items": { "type": "string", "pattern": "^[A-Z][A-Z0-9]{0,9}-[0-9]+$" }
|
|
20
|
-
},
|
|
21
|
-
"status": { "enum": ["open", "won", "lost", "killed"] }
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# bet
|
|
2
|
-
|
|
3
|
-
Frame work as a time-boxed bet you can judge: what you believe, how you'll
|
|
4
|
-
measure it, how much you expect it to move, by when, and which stories
|
|
5
|
-
deliver it. The person decides the bet and calls the outcome.
|
|
6
|
-
|
|
7
|
-
1. Ask what the person wants to move and why they think this work will move it, and read Proof, Founding hypothesis and Validation in `intent/constitution.md` and the related entries in `intent/decisions.md`.
|
|
8
|
-
2. Frame the bet with the person and confirm it: a hypothesis ("We believe <change> will <effect> for <users> because <evidence>"), a key result (one measure with a number), the expected uplift (from what to what), a time box end date, and any known blockers.
|
|
9
|
-
3. Pick the stories that deliver it from `intent/storymap.yaml`; if one is missing, propose it and add it only once the person agrees, following the story conventions in the idea-to-blueprint playbook.
|
|
10
|
-
4. Run `nikos bet new <bet-id> --hypothesis "<…>" --kr "<…>" --uplift "<…>" --time-box <yyyy-mm-dd> --story <id>` (one `--story` per story, one `--blocker` per blocker).
|
|
11
|
-
5. Fill in the card's `## Why we think so` with the evidence from step 1, quoting where it came from.
|
|
12
|
-
6. Run `nikos validate` and fix every error.
|
|
13
|
-
7. When the time box ends, or the person asks to close the bet: measure the key result, write it under `## Outcome`, propose won, lost or killed, set `status` once the person agrees, and record it with `nikos decide "<title>" --why "<why>" --category product`.
|
|
14
|
-
8. Report every step with its evidence or its skip reason.
|