nikos 0.1.1 → 0.2.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.
Files changed (111) hide show
  1. package/README.md +45 -9
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +49 -1
  4. package/dist/commands/approve.d.ts +16 -0
  5. package/dist/commands/approve.js +120 -0
  6. package/dist/commands/ci.d.ts +18 -0
  7. package/dist/commands/ci.js +123 -0
  8. package/dist/commands/clear.d.ts +14 -0
  9. package/dist/commands/decide.d.ts +11 -0
  10. package/dist/commands/decide.js +3 -3
  11. package/dist/commands/decisions.d.ts +14 -0
  12. package/dist/commands/decisions.js +2 -2
  13. package/dist/commands/doctor.d.ts +11 -0
  14. package/dist/commands/doctor.js +92 -0
  15. package/dist/commands/export.d.ts +48 -0
  16. package/dist/commands/export.js +164 -0
  17. package/dist/commands/init.d.ts +16 -0
  18. package/dist/commands/init.js +8 -12
  19. package/dist/commands/join.d.ts +17 -0
  20. package/dist/commands/join.js +91 -0
  21. package/dist/commands/map.d.ts +65 -0
  22. package/dist/commands/map.js +202 -25
  23. package/dist/commands/migrate.d.ts +10 -0
  24. package/dist/commands/migrate.js +105 -0
  25. package/dist/commands/next.d.ts +19 -0
  26. package/dist/commands/next.js +29 -5
  27. package/dist/commands/plan.d.ts +8 -0
  28. package/dist/commands/plan.js +9 -8
  29. package/dist/commands/release.d.ts +66 -0
  30. package/dist/commands/release.js +75 -6
  31. package/dist/commands/show.d.ts +3 -0
  32. package/dist/commands/show.js +26 -4
  33. package/dist/commands/start.d.ts +28 -0
  34. package/dist/commands/start.js +138 -0
  35. package/dist/commands/state.d.ts +27 -0
  36. package/dist/commands/state.js +40 -22
  37. package/dist/commands/step.d.ts +17 -0
  38. package/dist/commands/step.js +38 -2
  39. package/dist/commands/story.d.ts +21 -0
  40. package/dist/commands/story.js +7 -10
  41. package/dist/commands/suggest.d.ts +16 -0
  42. package/dist/commands/suggest.js +140 -0
  43. package/dist/commands/tracker.d.ts +60 -0
  44. package/dist/commands/tracker.js +185 -0
  45. package/dist/commands/validate.d.ts +15 -0
  46. package/dist/commands/validate.js +149 -14
  47. package/dist/commands/verify.d.ts +20 -0
  48. package/dist/commands/video.d.ts +33 -0
  49. package/dist/commands/video.js +4 -3
  50. package/dist/index.d.ts +20 -0
  51. package/dist/index.js +24 -0
  52. package/dist/lib/approvals.d.ts +29 -0
  53. package/dist/lib/approvals.js +28 -0
  54. package/dist/lib/bets.d.ts +21 -0
  55. package/dist/lib/bets.js +9 -2
  56. package/dist/lib/frontmatter.d.ts +5 -0
  57. package/dist/lib/project.d.ts +25 -0
  58. package/dist/lib/project.js +25 -7
  59. package/dist/lib/settings.d.ts +29 -0
  60. package/dist/lib/settings.js +110 -0
  61. package/dist/lib/state-files.d.ts +44 -0
  62. package/dist/lib/state-files.js +102 -0
  63. package/dist/lib/store.d.ts +44 -0
  64. package/dist/lib/store.js +97 -0
  65. package/dist/lint/constitution.d.ts +5 -0
  66. package/dist/lint/gherkin.d.ts +12 -0
  67. package/dist/lint/gherkin.js +2 -2
  68. package/dist/lint/portable.d.ts +10 -0
  69. package/dist/mcp/server.d.ts +4 -0
  70. package/dist/mcp/server.js +239 -0
  71. package/dist/planning/bets.d.ts +31 -0
  72. package/dist/planning/cli.d.ts +8 -0
  73. package/dist/planning/cli.js +17 -1
  74. package/dist/planning/delivery.d.ts +12 -0
  75. package/dist/planning/delivery.js +8 -5
  76. package/dist/planning/krs.d.ts +28 -0
  77. package/dist/planning/model.d.ts +55 -0
  78. package/dist/planning/model.js +10 -10
  79. package/dist/planning/rollout.d.ts +72 -0
  80. package/dist/planning/rollout.js +206 -0
  81. package/dist/planning/server.d.ts +113 -0
  82. package/dist/planning/server.js +9 -6
  83. package/dist/planning/ui.d.ts +7 -0
  84. package/dist/planning/ui.js +26 -1
  85. package/dist/planning/upstream.d.ts +73 -0
  86. package/dist/planning/upstream.js +78 -20
  87. package/dist/verify/detect.d.ts +3 -0
  88. package/docs/format.md +110 -1
  89. package/docs/gherkin.md +3 -0
  90. package/docs/install.md +39 -0
  91. package/docs/library.md +53 -0
  92. package/docs/planning.md +51 -5
  93. package/docs/settings.md +112 -0
  94. package/package.json +12 -2
  95. package/schemas/approvals.schema.json +27 -0
  96. package/schemas/planning/kr.schema.json +1 -1
  97. package/schemas/settings.schema.json +40 -0
  98. package/schemas/state.schema.json +6 -2
  99. package/schemas/storymap.schema.json +13 -0
  100. package/skills/nikos-mode/SKILL.md +22 -1
  101. package/skills/nikos-mode/playbooks/build-a-story.md +1 -1
  102. package/skills/nikos-mode/playbooks/company.md +16 -0
  103. package/skills/nikos-mode/playbooks/index.md +4 -0
  104. package/skills/nikos-mode/playbooks/map-existing.md +14 -0
  105. package/skills/nikos-mode/playbooks/start.md +12 -0
  106. package/skills/nikos-mode/playbooks/tracker.md +14 -0
  107. package/skills/nikos-mode/trackers/github.md +18 -0
  108. package/skills/nikos-mode/trackers/gitlab.md +17 -0
  109. package/skills/nikos-mode/trackers/jira.md +19 -0
  110. package/skills/nikos-mode/trackers/linear.md +17 -0
  111. package/skills/verify/SKILL.md +9 -0
package/README.md CHANGED
@@ -38,10 +38,29 @@ scenario, with every command and the output you should see.
38
38
  | Claude Code | `/plugin install nikos --marketplace nikovijay/NikOS` |
39
39
  | Codex | `codex plugin marketplace add nikovijay/NikOS` |
40
40
  | Cursor | Add `https://github.com/nikovijay/NikOS` as a plugin marketplace |
41
+ | Claude Desktop, or any app that runs MCP servers | `npx -y nikos mcp --dir <repo>` as a local MCP server: see [docs/install.md](docs/install.md#any-app-that-runs-mcp-servers) |
41
42
 
42
- Then ask your agent to plan or build a story: the `nikos-mode` skill takes it
43
- from there. The skills run the `nikos` command through `npx`, so there's
44
- nothing else to install. More in [docs/install.md](docs/install.md).
43
+ Then talk to your agent: the `nikos-mode` skill takes it from there. The
44
+ skills run the `nikos` command through `npx`, so there's nothing else to
45
+ install. More in [docs/install.md](docs/install.md).
46
+
47
+ ## Talk to it
48
+
49
+ You don't need to learn the commands. Your agent runs them and, after each
50
+ request, says where things stand and offers a short menu of what to do next.
51
+ Things you can say:
52
+
53
+ - "Set up nikOS here", or "Map what this repo already does"
54
+ - "Set nikOS up for our company" (a company plan every team joins)
55
+ - "What should we build next?"
56
+ - "Add a story: people can share a list with one other person"
57
+ - "Build HT-3"
58
+ - "Is release 1 ready to ship?"
59
+ - "Show me the story map"
60
+
61
+ When a story is done, your agent asks "Do you want to approve HT-3: Reorder
62
+ tasks?". Your answer is the sign-off. It's recorded in
63
+ `intent/approvals.yaml`, and your agent never approves on its own.
45
64
 
46
65
  ## The loop
47
66
 
@@ -54,8 +73,9 @@ npx -y nikos verify TC-1 # run the story: it fails until it's built
54
73
  npx -y nikos state set TC-1 done # refused unless its scenarios pass
55
74
  ```
56
75
 
57
- With an agent, you only write the request ("add a story for X and build
58
- it"); the playbooks run these steps and stop at each point that needs you.
76
+ That's what runs underneath. With an agent, you only make the request ("add
77
+ a story for X and build it"); the playbooks run these steps and stop at each
78
+ point that needs you.
59
79
 
60
80
  ## What lives in your repo
61
81
 
@@ -63,7 +83,9 @@ it"); the playbooks run these steps and stop at each point that needs you.
63
83
  intent/
64
84
  ├── constitution.md who it's for, the problem, what proves it works
65
85
  ├── storymap.yaml activities and steps, releases, and the order to build stories in
66
- ├── state.yaml each story's status: todo, in_progress, needs_rework, done
86
+ ├── state.yaml each story's status: todo, in_progress, needs_rework, done, cancelled
87
+ ├── approvals.yaml people's sign-offs on done stories (optional)
88
+ │ (or state/<ID>.yaml and approvals/<ID>.yaml, one per story)
67
89
  ├── decisions.md why things are the way they are, append-only
68
90
  ├── features/ one .feature file per story; its scenarios are the criteria
69
91
  │ └── <activity>/<story>.feature
@@ -71,28 +93,42 @@ intent/
71
93
  ```
72
94
 
73
95
  Every file is plain text you can read, review and diff. The full format:
74
- [docs/format.md](docs/format.md). A worked example:
96
+ [docs/format.md](docs/format.md). Tools that work with these files without a
97
+ shell can use nikOS as a library: [docs/library.md](docs/library.md). A worked example:
75
98
  [examples/hello-todo](examples/hello-todo/intent).
76
99
 
77
100
  ## Commands
78
101
 
79
102
  | Command | What it does |
80
103
  | --- | --- |
104
+ | `nikos start` | Set this repo up in one go: `intent/`, a scenario runner for its stack and CI for its host |
81
105
  | `nikos init` | Create `intent/` in this repo |
106
+ | `nikos company init` | Write a company plan repo that departments and teams join ([docs/settings.md](docs/settings.md)) |
107
+ | `nikos activity add "<title>"` | Add an activity to the story map |
82
108
  | `nikos validate` | Check `intent/` is well formed |
83
109
  | `nikos story add "<title>"` | Add a story to the map, state and a starter feature |
84
110
  | `nikos step add "<title>"` | Add a step under an activity |
85
- | `nikos release add\|rename\|move\|reorder` | Define releases and move stories between them, keeping feature tags in step |
111
+ | `nikos release add\|rename\|move\|reorder\|remove` | Define releases and move stories between them, keeping feature tags in step |
112
+ | `nikos release check <n>` | Say whether a release is ready to ship, and what's in the way |
86
113
  | `nikos clear <id>` | Check a story is ready to build |
87
114
  | `nikos plan new <id>` | Start the next version of a story's plan |
88
115
  | `nikos verify setup` | Add a Cucumber runner for this repo's stack |
116
+ | `nikos ci setup` | Run validate and every story's scenarios on each PR (GitHub Actions) or merge request (GitLab CI) |
89
117
  | `nikos verify [id]` | Run a story's scenarios, or all of them |
90
- | `nikos state set <id> <status>` | Move a story; done only if its scenarios pass |
118
+ | `nikos state set <id> <status>` | Move a story; done only if its scenarios pass; cancelled needs a reason |
119
+ | `nikos approve <id>...` | Sign off done stories, in your terminal or (`--via chat`) by answering your agent (writes `intent/approvals.yaml`) |
91
120
  | `nikos next` | Print the next story to build |
121
+ | `nikos migrate per-story` | Give each story its own state and approval file, so parallel branches never conflict |
122
+ | `nikos suggest` | Where things stand, and a short menu of what to do next |
123
+ | `nikos join <repo>` | Plan against a company or department plan and inherit its settings ([docs/settings.md](docs/settings.md)) |
124
+ | `nikos settings` | The settings that apply here, and where each comes from |
125
+ | `nikos doctor` | Check the repo is set up well, and what to do next |
126
+ | `nikos tracker link\|record\|sync\|import` | Keep the story map and Jira, Linear, GitHub or GitLab issues in step, through the tracker's MCP |
92
127
  | `nikos show <id>` | Summarise a story: feature, status, plan, bets, decisions |
93
128
  | `nikos decide "<title>"` | Append a decision to `decisions.md` |
94
129
  | `nikos decisions "<q>"` | Quote the decisions about a topic or story |
95
130
  | `nikos map` | Write a read-only story map page |
131
+ | `nikos export --format prd\|linear\|jira` | Write the story map as a PRD or a Linear or Jira import |
96
132
 
97
133
  ## Links
98
134
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js CHANGED
@@ -5,16 +5,26 @@ import { runPlan } from "./commands/plan.js";
5
5
  import { runInit } from "./commands/init.js";
6
6
  import { runClear } from "./commands/clear.js";
7
7
  import { runVerify } from "./commands/verify.js";
8
+ import { runCi } from "./commands/ci.js";
8
9
  import { runNext } from "./commands/next.js";
9
10
  import { runState } from "./commands/state.js";
11
+ import { runApprove } from "./commands/approve.js";
12
+ import { runSuggest } from "./commands/suggest.js";
13
+ import { runMcp } from "./mcp/server.js";
14
+ import { runJoin, runSettings } from "./commands/join.js";
15
+ import { runDoctor } from "./commands/doctor.js";
16
+ import { runMigrate } from "./commands/migrate.js";
17
+ import { runTracker } from "./commands/tracker.js";
18
+ import { runCompany, runStart } from "./commands/start.js";
10
19
  import { runDecide } from "./commands/decide.js";
11
20
  import { runShow } from "./commands/show.js";
12
21
  import { runDecisions } from "./commands/decisions.js";
13
22
  import { runStory } from "./commands/story.js";
14
- import { runStep } from "./commands/step.js";
23
+ import { runActivity, runStep } from "./commands/step.js";
15
24
  import { runRelease } from "./commands/release.js";
16
25
  import { runVideo } from "./commands/video.js";
17
26
  import { runMap } from "./commands/map.js";
27
+ import { runExport } from "./commands/export.js";
18
28
  import { runPlanning } from "./planning/cli.js";
19
29
  const commands = {
20
30
  init: runInit,
@@ -22,16 +32,29 @@ const commands = {
22
32
  plan: runPlan,
23
33
  clear: runClear,
24
34
  verify: runVerify,
35
+ ci: runCi,
25
36
  next: runNext,
26
37
  state: runState,
38
+ approve: runApprove,
39
+ suggest: runSuggest,
40
+ mcp: runMcp,
41
+ join: runJoin,
42
+ settings: runSettings,
43
+ doctor: runDoctor,
44
+ migrate: runMigrate,
45
+ tracker: runTracker,
46
+ start: runStart,
47
+ company: runCompany,
27
48
  decide: runDecide,
28
49
  show: runShow,
29
50
  decisions: runDecisions,
30
51
  story: runStory,
31
52
  step: runStep,
53
+ activity: runActivity,
32
54
  release: runRelease,
33
55
  video: runVideo,
34
56
  map: runMap,
57
+ export: runExport,
35
58
  planning: runPlanning,
36
59
  };
37
60
  const help = `nikos — spec-driven development for AI coding agents
@@ -39,29 +62,54 @@ const help = `nikos — spec-driven development for AI coding agents
39
62
  Usage: nikos <command> [options]
40
63
 
41
64
  Commands:
65
+ start Set this repo up in one go: intent/, a scenario runner for its stack
66
+ and CI for its host, each only if it's missing (--name, --key)
42
67
  init Create intent/ in this repo (--name, --key)
43
68
  validate [dir] Check the intent/ folder is well formed
44
69
  story add "<title>" Add a story to the map, state and a starter feature
45
70
  (--activity, --step, --release, --priority)
71
+ activity add "<title>" Add an activity to the story map (--id)
46
72
  step add "<title>" Add a step under an activity (--activity, --id)
47
73
  release add "<title>" Add the next release (--goal)
48
74
  release rename <n> Change a release's title or goal (--goal)
49
75
  release move <id> <n> Move a story to another release, retagging its feature
50
76
  release reorder <n> <pos> Move a release and renumber them all
51
77
  release remove <n> Remove an empty release and renumber the rest
78
+ release check <n> Is a release ready to ship? (--run reruns its scenarios)
52
79
  plan new <id> Start the next version of a story's plan
53
80
  clear <id> Check a story is Lean and Executable before building
54
81
  verify setup Set up a Cucumber runner for this repo's stack
55
82
  verify [id] Run a story's scenarios (or all of them)
83
+ ci setup Run validate and every story's scenarios on each PR or merge
84
+ request (GitHub Actions or GitLab CI; --github, --gitlab)
56
85
  show <id> Summarise a story: feature, status, plan, bets, decisions
57
86
  decisions "<q>" Quote the decisions.md entries that mention a topic or story
58
87
  next Print the next story to build
59
88
  state set <id> <status> Move a story; done only if its scenarios pass
89
+ (cancelled needs --reason)
90
+ approve <id>... Sign off done stories: in your terminal, or --via chat when
91
+ you've answered an agent's "Do you want to approve …?"
92
+ suggest What to do next: where things stand and a short menu (--json, --brief)
93
+ company init Write a company plan repo that departments and teams join
94
+ (--name, --period, --departments, --repo, --host)
95
+ join <repo> Plan against a company or department plan, and inherit its settings
96
+ (--host gitlab.com, --url, --ref)
97
+ settings [set <key> <value>] See the settings that apply here and where each comes
98
+ from, or change one this repo is allowed to
99
+ doctor Check this repo is set up well, and say what to do next (--offline)
100
+ migrate per-story Give each story its own state and approval file, so parallel
101
+ branches never conflict (migrate single-file goes back)
102
+ tracker link|record|sync|import Keep the story map and a Jira, Linear, GitHub or GitLab
103
+ tracker in step; the agent reads tickets through the tracker's MCP
104
+ mcp Run nikOS as a local MCP server for Claude Desktop, Cursor, Codex and
105
+ other apps (--dir <repo>)
60
106
  decide "<title>" Append a decision to decisions.md
61
107
  (--why, --story, --category, --supersedes)
62
108
  video Draft a video brief and storyboard from the done stories
63
109
  (--out, default marketing/storymap-video)
64
110
  map Write a read-only story map page (--out, default storymap.html)
111
+ export --format <prd|linear|jira> The story map as a PRD or tracker import
112
+ (--release, --out, --project-key)
65
113
  planning <cmd> Bets, key results and the planning dashboard (nikos-planning pack;
66
114
  run \`nikos planning help\`)
67
115
 
@@ -0,0 +1,16 @@
1
+ /** Who is approving: git's user.name, or the login name when git has none. */
2
+ export declare function approver(dir: string): string;
3
+ export interface ApproveResult {
4
+ story: string;
5
+ by: string;
6
+ on: string;
7
+ }
8
+ /** Check a story can be approved, rerun its scenarios, then record the approval. The terminal check lives in runApprove. */
9
+ export type Via = "terminal" | "chat" | "app" | "tracker";
10
+ export declare function approve(story: string, opts: {
11
+ dir?: string;
12
+ by: string;
13
+ date?: string;
14
+ via?: Via;
15
+ }): ApproveResult;
16
+ export declare function runApprove(args: string[]): Promise<number>;
@@ -0,0 +1,120 @@
1
+ // `nikos approve`: a person's sign-off on a done story, written to intent/approvals.yaml
2
+ // (or intent/approvals/<ID>.yaml). Two
3
+ // ways in, recorded as `via`:
4
+ // - terminal: the person runs it and types the story id back. A coding agent runs commands
5
+ // without a terminal, so it can't take this route.
6
+ // - chat: the agent asked "Do you want to approve …?" and the person answered Approve. The
7
+ // playbooks only allow it after that answer; `via: chat` shows it was given that way.
8
+ // - app: nikos mcp asked the person through their app (MCP elicitation), so the model never
9
+ // saw the question or gave the answer.
10
+ // - tracker: a person moved the story's ticket to a status mapped to approved (nikos tracker
11
+ // record), when tracker.approvals is on.
12
+ // A repo can go further and protect approvals.yaml with CODEOWNERS, so every approval also
13
+ // needs a review on GitHub or GitLab.
14
+ // The approval holds while the story stays done, since the same done, with the same feature.
15
+ import { spawnSync } from "node:child_process";
16
+ import { userInfo } from "node:os";
17
+ import { dirname } from "node:path";
18
+ import { createInterface } from "node:readline/promises";
19
+ import { approvalLapse, doneMark, loadApprovals } from "../lib/approvals.js";
20
+ import { noEntry, storyState, writeApproval } from "../lib/state-files.js";
21
+ import { resolveSettings } from "../lib/settings.js";
22
+ import { STORY_ID, findIntentDir, read, specHash } from "../lib/project.js";
23
+ import { featureFor } from "./clear.js";
24
+ import { StateError, failingScenarios } from "./state.js";
25
+ import { verify } from "./verify.js";
26
+ /** Who is approving: git's user.name, or the login name when git has none. */
27
+ export function approver(dir) {
28
+ const git = spawnSync("git", ["config", "user.name"], { cwd: dir, encoding: "utf8" });
29
+ return git.stdout?.trim() || userInfo().username;
30
+ }
31
+ export function approve(story, opts) {
32
+ if (!STORY_ID.test(story))
33
+ throw new StateError(`'${story}' is not a story id (expected e.g. HT-1)`);
34
+ const intentDir = findIntentDir(opts.dir ?? process.cwd());
35
+ const settings = resolveSettings(dirname(intentDir));
36
+ const policy = settings["approval.policy"];
37
+ if (policy.value === "off")
38
+ throw new StateError(`Approvals are off here (approval.policy, set by ${policy.from})`);
39
+ const via = opts.via ?? "terminal";
40
+ const routes = settings["approval.routes"];
41
+ // A tracker approval counts when tracker.approvals is on; the other routes by approval.routes.
42
+ if (via === "tracker" && !settings["tracker.approvals"].value)
43
+ throw new StateError("Tracker approvals don't count here (tracker.approvals is off)");
44
+ if (via !== "tracker" && !routes.value.includes(via)) {
45
+ throw new StateError(`Approvals ${via === "app" ? "through the app" : via === "chat" ? "in chat" : "in a terminal"} don't count here: approval.routes is ${routes.value.join(", ")} (set by ${routes.from})`);
46
+ }
47
+ const entry = storyState(intentDir, story);
48
+ if (!entry)
49
+ throw new StateError(noEntry(intentDir, story));
50
+ if (entry.status !== "done")
51
+ throw new StateError(`${story} is ${entry.status}: only a done story can be approved`);
52
+ const feature = featureFor(story, dirname(intentDir));
53
+ const hash = feature ? specHash(read(feature)) : undefined;
54
+ if (!hash)
55
+ throw new StateError(`No feature file is tagged @${story}`);
56
+ if (entry.spec_hash && entry.spec_hash !== hash) {
57
+ throw new StateError(`${story}'s feature changed after it was done. Move it to needs_rework and through done again before approving`);
58
+ }
59
+ const existing = loadApprovals(intentDir)[story];
60
+ if (existing && !approvalLapse(existing, entry, hash))
61
+ throw new StateError(`${story} is already approved (by ${existing.by} on ${existing.on})`);
62
+ let run;
63
+ try {
64
+ run = verify(dirname(intentDir), story);
65
+ }
66
+ catch (err) {
67
+ throw new StateError(`${story} isn't approved: its scenarios couldn't run. ${err.message}`);
68
+ }
69
+ if (run.status !== 0) {
70
+ const failing = failingScenarios(run.output);
71
+ throw new StateError(`${story} isn't approved: its scenarios don't all pass${failing.length ? `:\n${failing.map((f) => ` ✗ ${f}`).join("\n")}` : ". See the runner output."}`);
72
+ }
73
+ const on = opts.date ?? new Date().toISOString().slice(0, 10);
74
+ writeApproval(intentDir, story, { by: opts.by, on, via, spec_hash: hash, done_at: doneMark(entry) });
75
+ return { story, by: opts.by, on };
76
+ }
77
+ export async function runApprove(args) {
78
+ const flag = (n) => {
79
+ const i = args.indexOf(`--${n}`);
80
+ return i >= 0 ? args[i + 1] : undefined;
81
+ };
82
+ const stories = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1].startsWith("--")));
83
+ const dir = flag("dir") ?? process.cwd();
84
+ const via = flag("via") ?? "terminal";
85
+ if (!stories.length || (via !== "terminal" && via !== "chat")) {
86
+ console.error("Usage: nikos approve <story-id>... [--via chat] [--dir <path>]");
87
+ return 1;
88
+ }
89
+ const by = approver(dir);
90
+ if (via === "terminal") {
91
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
92
+ console.error("✗ nikos approve only runs in an interactive terminal. In a chat with an agent, the agent asks \"Do you want to approve …?\" and records your answer with --via chat.");
93
+ return 1;
94
+ }
95
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
96
+ try {
97
+ const want = stories.join(" ");
98
+ const typed = (await rl.question(`Approving ${want} as ${by}. Scenarios run again first.\nType ${want} to approve: `)).trim();
99
+ if (typed !== want) {
100
+ console.error("✗ Not approved: that wasn't the story id.");
101
+ return 1;
102
+ }
103
+ }
104
+ finally {
105
+ rl.close();
106
+ }
107
+ }
108
+ let failed = 0;
109
+ for (const story of stories) {
110
+ try {
111
+ approve(story, { dir, by, via });
112
+ console.log(`${story}: approved by ${by}${via === "chat" ? " in chat" : ""} (intent/approvals.yaml)`);
113
+ }
114
+ catch (err) {
115
+ console.error(`✗ ${err.message}`);
116
+ failed++;
117
+ }
118
+ }
119
+ return failed ? 1 : 0;
120
+ }
@@ -0,0 +1,18 @@
1
+ import { type Stack } from "../verify/detect.js";
2
+ export type Host = "github" | "gitlab";
3
+ /** github or gitlab, from the origin remote's url; undefined when it's neither or there's no remote. */
4
+ export declare function detectHost(root: string): Host | undefined;
5
+ export declare function githubWorkflow(stack: Stack, root: string): string;
6
+ export declare function gitlabJob(stack: Stack, root: string): string;
7
+ export interface CiResult {
8
+ host: Host;
9
+ created: string[];
10
+ skipped: string[];
11
+ /** Something the person (or their agent) still has to do by hand. */
12
+ todo?: string;
13
+ }
14
+ /** Write the pipeline for the repo's host and stack. Never overwrites a file. */
15
+ export declare function setupCi(root: string, opts?: {
16
+ host?: Host;
17
+ }): CiResult;
18
+ export declare function runCi(args: string[]): number;
@@ -0,0 +1,123 @@
1
+ // `nikos ci setup`: run nikOS's checks on every pull request (GitHub) or merge request (GitLab).
2
+ // The pipeline runs `nikos validate` and every story's scenarios with `nikos verify`, using the
3
+ // nikos version that wrote it, so a red pipeline means the specs or the code broke, not nikos.
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { detectStack } from "../verify/detect.js";
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const version = () => {
11
+ const pkg = [join(here, "../../package.json"), join(here, "../package.json")].find((p) => existsSync(p));
12
+ return JSON.parse(readFileSync(pkg, "utf8")).version;
13
+ };
14
+ /** github or gitlab, from the origin remote's url; undefined when it's neither or there's no remote. */
15
+ export function detectHost(root) {
16
+ const url = spawnSync("git", ["remote", "get-url", "origin"], { cwd: root, encoding: "utf8" }).stdout?.trim() ?? "";
17
+ if (/github\.com[:/]/.test(url))
18
+ return "github";
19
+ if (/gitlab/i.test(url))
20
+ return "gitlab";
21
+ return undefined;
22
+ }
23
+ /** The shell steps that install the runner and run the checks, for a stack. */
24
+ function steps(stack, root, nikos) {
25
+ const install = stack === "python"
26
+ ? [existsSync(join(root, "requirements.txt")) ? "pip install -r requirements.txt" : "", "pip install behave"].filter(Boolean)
27
+ : [existsSync(join(root, "package-lock.json")) ? "npm ci" : "npm install"];
28
+ return [...install, `npx -y ${nikos} validate`, `npx -y ${nikos} verify`];
29
+ }
30
+ export function githubWorkflow(stack, root) {
31
+ const nikos = `nikos@${version()}`;
32
+ const python = stack === "python" ? ` - uses: actions/setup-python@v5\n with:\n python-version: "3.12"\n` : "";
33
+ return `# Written by nikos ci setup: checks the specs in intent/ and runs every story's scenarios.
34
+ name: nikos
35
+ on:
36
+ pull_request:
37
+ push:
38
+ branches: [main]
39
+ jobs:
40
+ nikos:
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: actions/setup-node@v4
45
+ with:
46
+ node-version: 22
47
+ ${python}${steps(stack, root, nikos).map((s) => ` - run: ${s}`).join("\n")}
48
+ `;
49
+ }
50
+ export function gitlabJob(stack, root) {
51
+ const nikos = `nikos@${version()}`;
52
+ // Python stories need node for nikos itself, so that image installs it.
53
+ const image = stack === "python" ? "python:3.12" : "node:22";
54
+ const setup = stack === "python" ? ["apt-get update -qq && apt-get install -y -qq nodejs npm"] : [];
55
+ return `# Written by nikos ci setup: checks the specs in intent/ and runs every story's scenarios.
56
+ nikos:
57
+ image: ${image}
58
+ rules:
59
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
60
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
61
+ script:
62
+ ${[...setup, ...steps(stack, root, nikos)].map((s) => ` - ${s}`).join("\n")}
63
+ `;
64
+ }
65
+ const GITLAB_JOB = ".gitlab/ci/nikos.yml";
66
+ const INCLUDE = `include:\n - local: ${GITLAB_JOB}\n`;
67
+ /** Write the pipeline for the repo's host and stack. Never overwrites a file. */
68
+ export function setupCi(root, opts = {}) {
69
+ const host = opts.host ?? detectHost(root);
70
+ if (!host)
71
+ throw new Error("Couldn't tell where this repo is hosted from its origin remote. Say which: --github or --gitlab");
72
+ const stack = detectStack(root);
73
+ if (!stack)
74
+ throw new Error("Couldn't tell the stack: no package.json, pyproject.toml or requirements.txt. Run nikos verify setup first.");
75
+ const created = [];
76
+ const skipped = [];
77
+ const write = (rel, text) => {
78
+ const path = join(root, rel);
79
+ if (existsSync(path))
80
+ return void skipped.push(rel);
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ writeFileSync(path, text);
83
+ created.push(rel);
84
+ };
85
+ if (host === "github") {
86
+ write(".github/workflows/nikos.yml", githubWorkflow(stack, root));
87
+ return { host, created, skipped };
88
+ }
89
+ // GitLab reads one .gitlab-ci.yml, so the job lives in its own file and is included from it.
90
+ write(GITLAB_JOB, gitlabJob(stack, root));
91
+ const main = join(root, ".gitlab-ci.yml");
92
+ if (!existsSync(main)) {
93
+ write(".gitlab-ci.yml", INCLUDE);
94
+ return { host, created, skipped };
95
+ }
96
+ const included = readFileSync(main, "utf8").includes(GITLAB_JOB);
97
+ return { host, created, skipped, todo: included ? undefined : `Add this to .gitlab-ci.yml:\n${INCLUDE}` };
98
+ }
99
+ export function runCi(args) {
100
+ const [sub, ...rest] = args;
101
+ if (sub !== "setup") {
102
+ console.error("Usage: nikos ci setup [--github | --gitlab] [--dir <path>]");
103
+ return 1;
104
+ }
105
+ const i = rest.indexOf("--dir");
106
+ const root = i >= 0 ? rest[i + 1] : process.cwd();
107
+ const host = rest.includes("--github") ? "github" : rest.includes("--gitlab") ? "gitlab" : undefined;
108
+ try {
109
+ const r = setupCi(root, { host });
110
+ if (r.created.length)
111
+ console.log(`Created:\n${r.created.map((f) => ` + ${f}`).join("\n")}`);
112
+ if (r.skipped.length)
113
+ console.log(`Skipped (already there):\n${r.skipped.map((f) => ` = ${f}`).join("\n")}`);
114
+ if (r.todo)
115
+ console.log(`\n${r.todo}`);
116
+ console.log(`\nEvery ${r.host === "github" ? "pull request" : "merge request"} now runs nikos validate and every story's scenarios.`);
117
+ return 0;
118
+ }
119
+ catch (err) {
120
+ console.error(`✗ ${err.message}`);
121
+ return 1;
122
+ }
123
+ }
@@ -0,0 +1,14 @@
1
+ export type CheckName = "Clear" | "Lean" | "Executable" | "Accurate" | "Responsible";
2
+ export interface CheckResult {
3
+ check: CheckName;
4
+ /** `agent` means the check needs judgement, so the CLEAR skill decides it. */
5
+ status: "pass" | "fail" | "agent";
6
+ message: string;
7
+ fix?: string;
8
+ }
9
+ /** Run CLEAR's mechanical checks (Lean, Executable) on a feature file and report all five checks. */
10
+ export declare function clear(text: string): CheckResult[];
11
+ export declare function formatClear(results: CheckResult[]): string;
12
+ /** Find the feature file that carries a story id. */
13
+ export declare function featureFor(story: string, dir: string): string | undefined;
14
+ export declare function runClear(args: string[]): number;
@@ -0,0 +1,11 @@
1
+ export interface DecisionInput {
2
+ title: string;
3
+ why: string;
4
+ story?: string;
5
+ category?: string;
6
+ supersedes?: string;
7
+ date?: string;
8
+ }
9
+ /** Append a dated entry to decisions.md. Earlier entries are never touched. */
10
+ export declare function decide(input: DecisionInput, dir: string): string;
11
+ export declare function runDecide(args: string[]): number;
@@ -1,4 +1,4 @@
1
- import { appendFileSync, existsSync } from "node:fs";
1
+ import { store } from "../lib/store.js";
2
2
  import { join } from "node:path";
3
3
  import { STORY_ID, findIntentDir, read } from "../lib/project.js";
4
4
  import { formatResult, validate } from "./validate.js";
@@ -16,7 +16,7 @@ export function decide(input, dir) {
16
16
  if (!CATEGORIES.includes(category))
17
17
  throw new Error(`Category must be one of ${CATEGORIES.join(", ")}`);
18
18
  const path = join(findIntentDir(dir), "decisions.md");
19
- if (!existsSync(path))
19
+ if (!store().exists(path))
20
20
  throw new Error("No intent/decisions.md. Run `nikos init` first.");
21
21
  const date = input.date ?? new Date().toISOString().slice(0, 10);
22
22
  const entry = [
@@ -29,7 +29,7 @@ export function decide(input, dir) {
29
29
  ].join("\n");
30
30
  const existing = read(path);
31
31
  const gap = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
32
- appendFileSync(path, gap + entry);
32
+ store().append(path, gap + entry);
33
33
  return entry;
34
34
  }
35
35
  export function runDecide(args) {
@@ -0,0 +1,14 @@
1
+ export interface DecisionEntry {
2
+ /** First and last line of the entry in decisions.md, 1-based. */
3
+ start: number;
4
+ end: number;
5
+ title: string;
6
+ text: string;
7
+ }
8
+ /** Every "## " entry in decisions.md, with its line range. HTML comments are skipped. */
9
+ export declare function decisionEntries(dir: string): DecisionEntry[];
10
+ export declare function queryTerms(query: string): string[];
11
+ /** Entries that mention the query, best match first. Entries matching under half as many terms as the best are dropped. */
12
+ export declare function findDecisions(query: string, dir: string): DecisionEntry[];
13
+ export declare function formatDecisions(query: string, found: DecisionEntry[]): string;
14
+ export declare function runDecisions(args: string[]): number;
@@ -1,11 +1,11 @@
1
- import { existsSync } from "node:fs";
1
+ import { store } from "../lib/store.js";
2
2
  import { join } from "node:path";
3
3
  import { findIntentDir, read } from "../lib/project.js";
4
4
  const STOP = new Set(["the", "and", "for", "why", "did", "was", "were", "are", "you", "how", "what", "with", "this", "that", "not", "use", "using", "instead", "of", "we", "our", "decide", "decided", "choose", "chose", "rather", "than"]);
5
5
  /** Every "## " entry in decisions.md, with its line range. HTML comments are skipped. */
6
6
  export function decisionEntries(dir) {
7
7
  const path = join(findIntentDir(dir), "decisions.md");
8
- if (!existsSync(path))
8
+ if (!store().exists(path))
9
9
  return [];
10
10
  const lines = read(path).split("\n");
11
11
  const entries = [];
@@ -0,0 +1,11 @@
1
+ export interface Check {
2
+ name: string;
3
+ status: "ok" | "warn" | "fail";
4
+ found: string;
5
+ /** What to do about it, when it isn't ok. */
6
+ next?: string;
7
+ }
8
+ export declare function doctor(root: string, opts?: {
9
+ offline?: boolean;
10
+ }): Check[];
11
+ export declare function runDoctor(args: string[]): number;