nubos-pilot 0.7.0 → 0.7.2
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/agents/np-executor.md +32 -0
- package/agents/np-planner.md +28 -0
- package/agents/np-researcher.md +28 -0
- package/agents/np-verifier.md +15 -0
- package/bin/np-tools/_commands.cjs +10 -0
- package/bin/np-tools/dashboard.cjs +30 -0
- package/bin/np-tools/doctor.cjs +38 -6
- package/bin/np-tools/doctor.test.cjs +29 -0
- package/bin/np-tools/handoff-list.cjs +27 -0
- package/bin/np-tools/handoff-read.cjs +20 -0
- package/bin/np-tools/handoff-status.cjs +26 -0
- package/bin/np-tools/handoff-write.cjs +59 -0
- package/bin/np-tools/plan-milestone.cjs +14 -0
- package/bin/np-tools/render-todo.cjs +24 -0
- package/bin/np-tools/reset-slice.cjs +31 -2
- package/bin/np-tools/resume-work.cjs +42 -0
- package/bin/np-tools/worktree-create.cjs +24 -0
- package/bin/np-tools/worktree-ff-merge.cjs +33 -0
- package/bin/np-tools/worktree-list.cjs +14 -0
- package/bin/np-tools/worktree-remove.cjs +38 -0
- package/docs/adr/0008-worktree-isolation-per-slice.md +140 -0
- package/docs/adr/0009-tui-framework-for-dashboard.md +95 -0
- package/lib/config-defaults.cjs +1 -0
- package/lib/dashboard.cjs +145 -0
- package/lib/dashboard.test.cjs +179 -0
- package/lib/git.cjs +21 -0
- package/lib/handoff.cjs +277 -0
- package/lib/handoff.test.cjs +227 -0
- package/lib/tasks.cjs +13 -2
- package/lib/todo.cjs +128 -0
- package/lib/todo.test.cjs +179 -0
- package/lib/worktree.cjs +304 -0
- package/lib/worktree.test.cjs +228 -0
- package/np-tools.cjs +10 -0
- package/package.json +1 -1
- package/workflows/dashboard.md +49 -0
- package/workflows/execute-phase.md +33 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { listSliceWorktrees } = require('../../lib/worktree.cjs');
|
|
4
|
+
|
|
5
|
+
function run(args, opts) {
|
|
6
|
+
const o = opts || {};
|
|
7
|
+
const cwd = o.cwd || process.cwd();
|
|
8
|
+
const stdout = o.stdout || process.stdout;
|
|
9
|
+
const list = listSliceWorktrees(cwd);
|
|
10
|
+
stdout.write(JSON.stringify(list) + '\n');
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { run };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { removeSliceWorktree } = require('../../lib/worktree.cjs');
|
|
5
|
+
|
|
6
|
+
function _parseArgs(args) {
|
|
7
|
+
const out = { sliceFullId: null, force: false, keepBranch: false };
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const a = args[i];
|
|
10
|
+
if (a === '--force') { out.force = true; continue; }
|
|
11
|
+
if (a === '--keep-branch') { out.keepBranch = true; continue; }
|
|
12
|
+
if (!a.startsWith('-') && !out.sliceFullId) out.sliceFullId = a;
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function run(args, opts) {
|
|
18
|
+
const o = opts || {};
|
|
19
|
+
const cwd = o.cwd || process.cwd();
|
|
20
|
+
const stdout = o.stdout || process.stdout;
|
|
21
|
+
const parsed = _parseArgs(Array.isArray(args) ? args : []);
|
|
22
|
+
if (!parsed.sliceFullId) {
|
|
23
|
+
throw new NubosPilotError(
|
|
24
|
+
'worktree-remove-missing-slice',
|
|
25
|
+
'slice full-id required (e.g. M001-S001)',
|
|
26
|
+
{},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const result = removeSliceWorktree(
|
|
30
|
+
parsed.sliceFullId,
|
|
31
|
+
cwd,
|
|
32
|
+
{ force: parsed.force, deleteBranch: !parsed.keepBranch },
|
|
33
|
+
);
|
|
34
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ADR-0008: Worktree Isolation per Slice
|
|
2
|
+
|
|
3
|
+
* Status: **Accepted**
|
|
4
|
+
* Date: 2026-04-23
|
|
5
|
+
* Accepted: 2026-04-23 (all 11 decisions D-8.1..D-8.11 ratified at defaults)
|
|
6
|
+
* Supersedes: None
|
|
7
|
+
* Relates-to: [ADR-0001](0001-no-daemon-invariant.md), [ADR-0004](0004-atomic-commit-per-unit.md), [ADR-0005](0005-three-orthogonal-file-trees.md)
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
Today the `np-executor` edits, verifies, and commits directly on the user's active branch. Three failure modes follow:
|
|
12
|
+
|
|
13
|
+
* **Cross-contamination** — if execution is interrupted mid-slice (crash, timeout, manual abort), half-applied edits pollute the user's working tree. Recovery requires manual `git restore` judgment calls.
|
|
14
|
+
* **No inspection point** — once a slice is merged (or aborted) its intermediate state is lost. Post-mortem analysis of "what did the executor do before it broke" requires git-stash archaeology.
|
|
15
|
+
* **No parallelism pathway** — two unrelated slices of the same milestone cannot run concurrently because they share one working tree. Even when parallelism is NOT the immediate goal, the single-working-tree assumption blocks it forever.
|
|
16
|
+
|
|
17
|
+
Octogent (hesamsheikh/octogent) solves adjacent problems via per-job "tentacle" worktrees. The question is whether a similar isolation layer fits nubos-pilot without violating existing invariants (ADR-0001 no-daemon, ADR-0004 atomic commits, ADR-0005 three file-trees).
|
|
18
|
+
|
|
19
|
+
## Decision Drivers
|
|
20
|
+
|
|
21
|
+
* **Reversibility** — a failed slice must not leave the user's working tree dirty. Rollback = delete worktree.
|
|
22
|
+
* **Inspectability** — post-execution, the worktree of a slice (passed or failed) can be opened in an editor and examined like any git-checkout.
|
|
23
|
+
* **No-daemon preservation** — the mechanism must work with plain `git worktree add/remove` inside the bash blocks of each workflow; no long-running coordinator, no RPC.
|
|
24
|
+
* **ADR-0004 preservation** — atomic-commit-per-unit survives. Each task commit lands as-is on the slice branch; merge-back must be fast-forward so history stays linear and `np:undo-task` still resolves to a single SHA.
|
|
25
|
+
* **ADR-0005 preservation** — the Project-State tree (`.nubos-pilot/`) stays single-writer-single-location; worktrees reference it, do not duplicate it.
|
|
26
|
+
* **Backward compatibility** — existing projects and existing slices must continue to work without worktree mode. The feature is additive, not a hard migration.
|
|
27
|
+
* **Host-simplicity** — nubos-pilot must not require the user to clean up stray worktrees manually in the common case.
|
|
28
|
+
|
|
29
|
+
## Considered Options
|
|
30
|
+
|
|
31
|
+
* **A — Status quo (no worktree isolation).** Rejected: the failure modes above are real and will only compound as nubos-pilot is used on longer milestones.
|
|
32
|
+
* **B — Worktree per task.** Rejected: per-task worktrees multiply disk usage and setup/teardown cost without proportional benefit — tasks within a slice are already atomic via ADR-0004; the isolation need is at slice boundaries (multiple tasks running as one unit of work).
|
|
33
|
+
* **C — Worktree per slice, opt-in via config, merge-back fast-forward.** **Chosen.**
|
|
34
|
+
* **D — Branch-only isolation (no separate working tree).** Rejected: still shares one working tree across slices; solves nothing that's actually broken.
|
|
35
|
+
* **E — External worktree location (`~/.nubos-pilot/worktrees/<hash>/`).** Rejected: cross-project path management, cleanup ambiguity, surprises for users who expect `rm -rf .nubos-pilot/` to remove everything.
|
|
36
|
+
|
|
37
|
+
## Decision Outcome
|
|
38
|
+
|
|
39
|
+
Chosen: **Option C — Worktree per slice, opt-in via config, merge-back fast-forward.**
|
|
40
|
+
|
|
41
|
+
### Open Decisions (need explicit sign-off before implementation)
|
|
42
|
+
|
|
43
|
+
The ADR captures the currently-recommended defaults; each is marked **[DEFAULT]** and the alternative is documented so a reviewer can dissent before code is written.
|
|
44
|
+
|
|
45
|
+
#### D-8.1 — Activation
|
|
46
|
+
|
|
47
|
+
* **[DEFAULT]** Opt-in via `workflow.worktree_isolation: false` in `.nubos-pilot/config.json`. Default off.
|
|
48
|
+
* **Alternative:** Default on for all new projects (detected via absent config key).
|
|
49
|
+
* **Rationale for default:** Additive feature; zero surprise for existing projects. Opting in is one line of config.
|
|
50
|
+
|
|
51
|
+
#### D-8.2 — Worktree Location
|
|
52
|
+
|
|
53
|
+
* **[DEFAULT]** `.nubos-pilot/worktrees/<milestone-id>/<slice-id>/` — inside the Project-State tree.
|
|
54
|
+
* **Alternative A:** `../<repo-name>-worktrees/<milestone-id>/<slice-id>/` — sibling directory.
|
|
55
|
+
* **Alternative B:** `~/.nubos-pilot/worktrees/<project-hash>/…` — user-home, cross-project.
|
|
56
|
+
* **Rationale for default:** Matches octogent convention; single cleanup target (`rm -rf .nubos-pilot/`); survives ADR-0005 because worktrees are a git-runtime artifact adjacent to state (analog to `.nubos-pilot/checkpoints/`), not a fourth file-tree.
|
|
57
|
+
|
|
58
|
+
#### D-8.3 — Shared State Resolution
|
|
59
|
+
|
|
60
|
+
* **[DEFAULT]** `.nubos-pilot/` lives **only** in the main workspace. The worktree references it via its absolute path (resolved on worktree-create, stored in `.nubos-pilot/worktrees/<mid>/<sid>/.np-origin` for recovery).
|
|
61
|
+
* **Alternative:** Symlink `.nubos-pilot/` into each worktree.
|
|
62
|
+
* **Rationale for default:** Symlinks break on Windows without developer mode; absolute-path reference is portable. All CLI commands already accept a `cwd` argument — we pass the main-workspace path explicitly from inside the worktree.
|
|
63
|
+
|
|
64
|
+
#### D-8.4 — Branch Naming
|
|
65
|
+
|
|
66
|
+
* **[DEFAULT]** `np/<milestone-id>-<slice-id>` (e.g. `np/M001-S001`).
|
|
67
|
+
* **Alternative:** `np/<milestone-id>-<slice-id>-<slug>`.
|
|
68
|
+
* **Rationale for default:** Deterministic, short, scriptable. Slug adds no machine value and may drift from the slice-name frontmatter.
|
|
69
|
+
|
|
70
|
+
#### D-8.5 — Base Branch
|
|
71
|
+
|
|
72
|
+
* **[DEFAULT]** Worktree branches off the **current HEAD of the invoking workspace** at slice-start time.
|
|
73
|
+
* **Alternative:** Always branches off a configured base (`config.git.base_branch`, e.g. `development`).
|
|
74
|
+
* **Rationale for default:** Matches user expectation ("executor works from where I am"). Users on Nubos-Platform are on `development` per memory; this is already handled.
|
|
75
|
+
|
|
76
|
+
#### D-8.6 — Commit Flow
|
|
77
|
+
|
|
78
|
+
* **[DEFAULT]** Executor `cd`s into the worktree; `commit-task` runs inside the worktree and commits to `np/<mid>-<sid>`. Each task is its own atomic commit (ADR-0004 preserved).
|
|
79
|
+
* **Alternative:** Executor stays in main workspace, operates on worktree via `git -C <worktree-path> …`.
|
|
80
|
+
* **Rationale for default:** `cd`-based is what every other `np:*` workflow already assumes. `git -C` would require every child command to know about the worktree path.
|
|
81
|
+
|
|
82
|
+
#### D-8.7 — Merge-Back Strategy
|
|
83
|
+
|
|
84
|
+
* **[DEFAULT]** Fast-forward merge only. After all tasks of a slice pass verify + commit, the workflow runs `git merge --ff-only np/<mid>-<sid>` from the main workspace. If FF is impossible (main branch advanced during execution), workflow stops and surfaces the conflict to the user for manual resolve.
|
|
85
|
+
* **Alternative A:** Rebase the slice branch onto current main, then FF.
|
|
86
|
+
* **Alternative B:** Three-way merge with auto-commit.
|
|
87
|
+
* **Rationale for default:** Preserves linear history (so `git log --oneline --grep='^task('` stays a plan-trace per ADR-0004). Surfacing conflicts to the user is the honest failure mode; auto-rebase can silently rewrite task commits, breaking `np:undo-task` SHA resolution.
|
|
88
|
+
|
|
89
|
+
#### D-8.8 — Parallelism
|
|
90
|
+
|
|
91
|
+
* **[DEFAULT]** Sequential execution only in this ADR. The worktree mechanism *enables* parallelism but does not introduce it. Parallel execution would require a disjoint-file-set check between slices and is **out of scope for ADR-0008** — a separate ADR-0009 would address it.
|
|
92
|
+
* **Rationale:** Isolation ≠ speedup. Ship the isolation property first; measure real-world merge-conflict rates before adding concurrency.
|
|
93
|
+
|
|
94
|
+
#### D-8.9 — Cleanup Policy
|
|
95
|
+
|
|
96
|
+
* **[DEFAULT]** After successful merge-back and commit, worktree is removed via `git worktree remove` and branch `np/<mid>-<sid>` is deleted. A failed slice leaves the worktree in place for inspection; cleanup is manual via `np:reset-slice`.
|
|
97
|
+
* **Alternative:** Retention period (e.g. keep successful worktrees for 24h).
|
|
98
|
+
* **Rationale for default:** Disk usage and clutter dominate once many slices ship. Retention complicates reasoning; inspection of successful slices is served by `git log` on main.
|
|
99
|
+
|
|
100
|
+
#### D-8.10 — Crash Recovery
|
|
101
|
+
|
|
102
|
+
* **[DEFAULT]** `resume-work` detects `.nubos-pilot/worktrees/<mid>/<sid>/` and re-enters it. If the worktree is unsalvageable (corrupt git state), `np:reset-slice` now also runs `git worktree remove --force` on the slice's worktree.
|
|
103
|
+
* **Rationale:** Maps the existing crash-recovery story onto the new artifact.
|
|
104
|
+
|
|
105
|
+
#### D-8.11 — Gitignore / Commit Policy
|
|
106
|
+
|
|
107
|
+
* **[DEFAULT]** `.nubos-pilot/worktrees/` is **always gitignored**, regardless of `workflow.commit_artifacts`. Committing worktree contents would duplicate the repo inside itself.
|
|
108
|
+
* **Rationale:** Hard safety rule. Install step adds `.nubos-pilot/worktrees/` to `.gitignore` when worktree isolation is first enabled.
|
|
109
|
+
|
|
110
|
+
### Consequences
|
|
111
|
+
|
|
112
|
+
**Good, because:**
|
|
113
|
+
|
|
114
|
+
* A failed slice leaves the main workspace clean; recovery is deletion, not fix-up.
|
|
115
|
+
* Post-mortem of any slice (passed or failed) is a `cd .nubos-pilot/worktrees/<mid>/<sid>/` away.
|
|
116
|
+
* Linear task-commit history survives (FF-merge); ADR-0004 undo semantics unchanged.
|
|
117
|
+
* Opt-in model: legacy projects untouched until a user flips the config flag.
|
|
118
|
+
* Worktree mechanism paves the path to parallel slices (ADR-0009, future) without re-architecting.
|
|
119
|
+
|
|
120
|
+
**Bad, because:**
|
|
121
|
+
|
|
122
|
+
* Disk usage grows per active slice (full working-tree copy — git worktrees share `.git` but not working-tree contents).
|
|
123
|
+
* Users must learn one new directory (`.nubos-pilot/worktrees/`) and one new CLI surface (`worktree list|prune|inspect` if we add it).
|
|
124
|
+
* FF-merge constraint is strict: if the user force-updates the main branch mid-slice, the slice must be rebased or discarded. This is an intentional sharp edge.
|
|
125
|
+
* Merge-back semantics may surprise users on Nubos-Platform's `development` → `main` reconcile flow (memory: "MRs development→main werden gesquasht"). The ADR does not change squash-merge — that happens at MR time, not slice time. Needs explicit test.
|
|
126
|
+
|
|
127
|
+
## More Information
|
|
128
|
+
|
|
129
|
+
* **ADR-0004 preservation test:** after implementing, `git log --oneline --grep='^task(' origin/main` must still return one line per committed task — no squash, no merge-commit noise.
|
|
130
|
+
* **ADR-0005 preservation:** the worktree physically contains code under `.nubos-pilot/worktrees/`, but that code is a git-working-tree view of the Source tree, not a copy, not state. `.nubos-pilot/` (state) still lives once, in the main workspace.
|
|
131
|
+
* **Out of scope:** parallel slices (ADR-0009), cross-milestone worktree pooling, worktree-based dev-mode (`np:dev --worktree` shell), TUI dashboard for worktree status (part of #4 in the octogent roadmap).
|
|
132
|
+
* **Implementation phasing (post-approval):**
|
|
133
|
+
1. `lib/worktree.cjs` — create/list/remove/prune/ff-merge.
|
|
134
|
+
2. `lib/worktree.test.cjs` — unit + integration (spawned git in sandbox).
|
|
135
|
+
3. `bin/np-tools/worktree-*.cjs` — CLI surfaces (`worktree-create`, `worktree-remove`, `worktree-list`, `worktree-ff-merge`).
|
|
136
|
+
4. `workflows/execute-phase.md` — conditional worktree-create at slice-start, ff-merge at slice-end.
|
|
137
|
+
5. `bin/np-tools/reset-slice.cjs` — worktree teardown path.
|
|
138
|
+
6. `bin/np-tools/resume-work.cjs` — worktree re-entry.
|
|
139
|
+
7. `bin/install/` — add `.nubos-pilot/worktrees/` to `.gitignore` when `worktree_isolation=true` is first set.
|
|
140
|
+
8. `lib/config-defaults.cjs` — `workflow.worktree_isolation: false`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ADR-0009: TUI Framework for Interactive Dashboard (Amendment to ADR-0002)
|
|
2
|
+
|
|
3
|
+
* Status: **Accepted — Option D (no TUI framework adopted)**
|
|
4
|
+
* Date: 2026-04-23
|
|
5
|
+
* Accepted: 2026-04-23
|
|
6
|
+
* Amends: [ADR-0002](0002-zero-runtime-dependencies.md) — **no amendment takes effect.** ADR-0002's `dependencies: []` invariant stays intact.
|
|
7
|
+
* Relates-to: [ADR-0001](0001-no-daemon-invariant.md), [ADR-0006](0006-yaml-dependency-amendment.md)
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
The one-shot dashboard (`np-tools.cjs dashboard`, ADR-0008 adjacent, already shipped) renders milestone/slice/handoff/worktree state as a formatted block with ANSI colors, plus an optional `--watch <seconds>` mode that repeatedly clear-screens and re-renders. This satisfies read-only inspection well.
|
|
12
|
+
|
|
13
|
+
A second user-facing surface is being requested: an **interactive** dashboard that supports keyboard-driven navigation — arrow keys through milestones/slices, `Enter` to drill into handoffs or task plans, `s`/`p` hotkeys to `skip`/`park` tasks in flight, flicker-free differential repainting. This is a concrete usability ask, not speculative scope.
|
|
14
|
+
|
|
15
|
+
The question: **does nubos-pilot adopt a terminal-UI library to implement this, and if so, which one?**
|
|
16
|
+
|
|
17
|
+
ADR-0002 makes `dependencies: []` the invariant. Only `yaml@^2.8` has been amended in (ADR-0006). Any TUI library is a new runtime dependency — so a new amendment is required per the ADR lifecycle.
|
|
18
|
+
|
|
19
|
+
## Decision Drivers
|
|
20
|
+
|
|
21
|
+
* **Install-anywhere preservation** — every extra transitive dep is payload size pulled on every `npx nubos-pilot` run.
|
|
22
|
+
* **Supply-chain surface** — each dep (and each of its transitives) is attack surface and a future maintenance risk.
|
|
23
|
+
* **Feature value** — does the interactive surface justify the cost?
|
|
24
|
+
* **Maintenance status** — nubos-pilot survives by zero-surprise dependencies; a library abandoned by its author is a liability we do not want to inherit.
|
|
25
|
+
* **ADR-0001 preservation** — a TUI library must not pull in a long-running daemon. Foreground, synchronous, user-interactive processes are fine (analog to `htop` / `top`).
|
|
26
|
+
|
|
27
|
+
## Considered Options
|
|
28
|
+
|
|
29
|
+
* **A — Adopt `ink` + React** — "React for the terminal", virtual-DOM diffing, component model, flicker-free repaint.
|
|
30
|
+
* **B — Adopt `neo-blessed`** — fork of Christopher Jeffrey's `blessed`, maintained, ncurses-style widget framework. No React.
|
|
31
|
+
* **C — Home-grown TUI on `readline` + ANSI escapes** — no dep. Manual cursor movement, manual double-buffer to avoid flicker, manual key-binding dispatch.
|
|
32
|
+
* **D — Do nothing, ship only A+B (one-shot + `--watch`)** — no new dep, no interactive surface. Accept that drill-down lives in separate `handoff-read <id>` / `render-todo <slice>` calls.
|
|
33
|
+
|
|
34
|
+
## Decision Outcome
|
|
35
|
+
|
|
36
|
+
**Chosen: Option D — no TUI framework, no dependency.** The existing one-shot + `--watch` dashboard (shipped alongside ADR-0008) remains the only dashboard surface. Interactive drill-down is served by composing the existing CLI commands (`handoff-list`, `handoff-read`, `render-todo`, `checkpoint show`) — no bespoke TUI.
|
|
37
|
+
|
|
38
|
+
ADR-0002's `dependencies: []` invariant (as amended by ADR-0006 for `yaml`) remains unchanged. No new runtime dependency is introduced by this ADR.
|
|
39
|
+
|
|
40
|
+
### Why Option D was chosen
|
|
41
|
+
|
|
42
|
+
1. **A+B already cover 80% of the value.** Read-only status at-a-glance is what most people mean by "dashboard". Drill-down is one CLI command away.
|
|
43
|
+
2. **The drill-down surface exists already:** `handoff-read`, `render-todo`, `checkpoint show`, `metrics record`. All callable without a custom TUI.
|
|
44
|
+
3. **Any dep is a one-way door** for `nubos-pilot`, given the install-anywhere promise. Reversing it later is a second ADR cycle.
|
|
45
|
+
4. **Interactive hotkey mutation** (e.g. `p` to park a task from the dashboard) is a convenience, not a capability — `np:park <task-id>` already exists.
|
|
46
|
+
|
|
47
|
+
### Revisiting this decision
|
|
48
|
+
|
|
49
|
+
If concrete interactive-workflow gaps emerge from real use that A+B + existing CLI cannot serve, this ADR should be **superseded** (not rewritten) by a new ADR that adopts a specific framework. The recommended order at that time:
|
|
50
|
+
|
|
51
|
+
* **B — `neo-blessed`** first: minimal transitive tree, no React, API stable, one library to audit.
|
|
52
|
+
* **A — `ink`** second: React baggage (hooks, fiber, dev-tools) for a status view is heavy. The React mental model is a force-multiplier for teams already fluent in it; standalone it is overhead.
|
|
53
|
+
* **C — home-grown** last: feasible but expensive. Cursor math, key-binding dispatch, double-buffer, Unicode-width calculation, terminal-capability detection — all must be hand-written. For a feature that's "nice to have" this trade is poor.
|
|
54
|
+
|
|
55
|
+
### Consequences per option
|
|
56
|
+
|
|
57
|
+
**If A (ink) is chosen:**
|
|
58
|
+
- Good: React-literate maintainers can iterate fast.
|
|
59
|
+
- Good: Flicker-free diff repainting via Virtual-DOM.
|
|
60
|
+
- Bad: React transitive tree (`react`, `react-reconciler`, scheduler, several small utility libs). Several MB of install payload per `npx` invocation.
|
|
61
|
+
- Bad: Ties the TUI to React's LTS cadence.
|
|
62
|
+
|
|
63
|
+
**If B (neo-blessed) is chosen:**
|
|
64
|
+
- Good: Smaller install payload, no React.
|
|
65
|
+
- Good: Battle-tested widget set.
|
|
66
|
+
- Bad: API is old-style (event-emitter + imperative layout). Not as ergonomic as ink's React model.
|
|
67
|
+
- Bad: Maintenance is best-effort; the upstream `blessed` was abandoned and `neo-blessed` is a small-volunteer fork.
|
|
68
|
+
|
|
69
|
+
**If C (home-grown) is chosen:**
|
|
70
|
+
- Good: Zero dependencies. ADR-0002 stays unbent.
|
|
71
|
+
- Good: Full control of rendering semantics.
|
|
72
|
+
- Bad: Weeks of work that duplicate what libraries already do well.
|
|
73
|
+
- Bad: Every new TUI feature is a manual re-implementation of patterns libraries give for free.
|
|
74
|
+
|
|
75
|
+
**If D (no interactive) is chosen:**
|
|
76
|
+
- Good: Status quo preserved; A+B ship as-is.
|
|
77
|
+
- Good: One less future-breakage risk.
|
|
78
|
+
- Bad: Users who wanted keyboard-driven drill-down get a CLI-composition answer instead.
|
|
79
|
+
|
|
80
|
+
## Consequences
|
|
81
|
+
|
|
82
|
+
**Good, because:**
|
|
83
|
+
- ADR-0002's `dependencies: []` invariant (as amended by ADR-0006 for `yaml`) stays intact. Install-anywhere story is unchanged.
|
|
84
|
+
- No new supply-chain surface. No CVE triage, no maintainer hand-off risk.
|
|
85
|
+
- One less future-breakage risk.
|
|
86
|
+
|
|
87
|
+
**Bad, because:**
|
|
88
|
+
- Users who want keyboard-driven drill-down get a CLI-composition answer instead. `handoff-list --for X --status open | jq …`, then `handoff-read $(…)`, then `render-todo M001-S001`.
|
|
89
|
+
- If a concrete workflow gap emerges later, we pay the full ADR-cycle cost to revisit.
|
|
90
|
+
|
|
91
|
+
## More Information
|
|
92
|
+
|
|
93
|
+
* Dashboard A+B already ships: `lib/dashboard.cjs`, `bin/np-tools/dashboard.cjs`, with `--json`, `--no-color`, `--watch [sec]` flags.
|
|
94
|
+
* Interactive drill-down composes existing commands — `handoff-list --for X --status open | jq …`, `handoff-read $(…)`, `render-todo M001-S001`.
|
|
95
|
+
* If this ADR is ever superseded to adopt a framework, `lib/dashboard.cjs` (snapshot collector) is already framework-agnostic and can be reused as-is.
|
package/lib/config-defaults.cjs
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
7
|
+
const { listMilestones, listSlices, listTasks, mId } = require('./layout.cjs');
|
|
8
|
+
|
|
9
|
+
const ANSI = Object.freeze({
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
gray: '\x1b[90m',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const STATUS_GLYPHS = Object.freeze({
|
|
22
|
+
'pending': { glyph: '[ ]', color: ANSI.gray },
|
|
23
|
+
'in-progress': { glyph: '[~]', color: ANSI.yellow },
|
|
24
|
+
'done': { glyph: '[x]', color: ANSI.green },
|
|
25
|
+
'skipped': { glyph: '[-]', color: ANSI.dim },
|
|
26
|
+
'parked': { glyph: '[!]', color: ANSI.red },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function _safeReadJson(p) {
|
|
30
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf-8')); }
|
|
31
|
+
catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _safeReadFile(p) {
|
|
35
|
+
try { return fs.readFileSync(p, 'utf-8'); }
|
|
36
|
+
catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _taskStatus(planPath) {
|
|
40
|
+
const raw = _safeReadFile(planPath);
|
|
41
|
+
if (!raw) return 'pending';
|
|
42
|
+
try {
|
|
43
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
44
|
+
return typeof frontmatter.status === 'string' ? frontmatter.status : 'pending';
|
|
45
|
+
} catch { return 'pending'; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _collectMilestones(projectRoot) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const m of listMilestones(projectRoot)) {
|
|
51
|
+
const meta = _safeReadJson(path.join(m.path, mId(m.number) + '-META.json')) || {};
|
|
52
|
+
const slices = [];
|
|
53
|
+
for (const s of listSlices(m.number, projectRoot)) {
|
|
54
|
+
const tasks = listTasks(m.number, s.number, projectRoot);
|
|
55
|
+
const counts = { total: 0, pending: 0, 'in-progress': 0, done: 0, skipped: 0, parked: 0 };
|
|
56
|
+
const statuses = [];
|
|
57
|
+
for (const t of tasks) {
|
|
58
|
+
const status = _taskStatus(t.plan_path);
|
|
59
|
+
statuses.push(status);
|
|
60
|
+
counts.total += 1;
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(counts, status)) counts[status] += 1;
|
|
62
|
+
}
|
|
63
|
+
slices.push({
|
|
64
|
+
id: s.id,
|
|
65
|
+
full_id: s.full_id,
|
|
66
|
+
counts,
|
|
67
|
+
task_statuses: statuses,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
out.push({
|
|
71
|
+
id: m.id,
|
|
72
|
+
number: m.number,
|
|
73
|
+
name: typeof meta.name === 'string' ? meta.name : null,
|
|
74
|
+
status: typeof meta.status === 'string' ? meta.status : null,
|
|
75
|
+
slices,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectSnapshot(projectRoot) {
|
|
82
|
+
const cwd = projectRoot || process.cwd();
|
|
83
|
+
return { milestones: _collectMilestones(cwd) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _summarizeCounts(c, useColor) {
|
|
87
|
+
const paint = (code, text) => useColor ? code + text + ANSI.reset : text;
|
|
88
|
+
const bits = [];
|
|
89
|
+
if (c.done) bits.push(paint(ANSI.green, c.done + ' done'));
|
|
90
|
+
if (c['in-progress']) bits.push(paint(ANSI.yellow, c['in-progress'] + ' in-progress'));
|
|
91
|
+
if (c.pending) bits.push(paint(ANSI.gray, c.pending + ' pending'));
|
|
92
|
+
if (c.skipped) bits.push(paint(ANSI.dim, c.skipped + ' skipped'));
|
|
93
|
+
if (c.parked) bits.push(paint(ANSI.red, c.parked + ' parked'));
|
|
94
|
+
return bits.join(' · ') || paint(ANSI.dim, 'no tasks');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _checkboxRow(statuses, useColor) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const s of statuses) {
|
|
100
|
+
const g = STATUS_GLYPHS[s] || STATUS_GLYPHS.pending;
|
|
101
|
+
parts.push(useColor ? g.color + g.glyph + ANSI.reset : g.glyph);
|
|
102
|
+
}
|
|
103
|
+
return parts.join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderSnapshot(snap, opts) {
|
|
107
|
+
const o = opts || {};
|
|
108
|
+
const useColor = o.color !== false;
|
|
109
|
+
const c = (code, text) => useColor ? code + text + ANSI.reset : text;
|
|
110
|
+
const lines = [];
|
|
111
|
+
|
|
112
|
+
lines.push(c(ANSI.bold + ANSI.blue, 'nubos-pilot'));
|
|
113
|
+
lines.push('');
|
|
114
|
+
|
|
115
|
+
if (!snap.milestones || snap.milestones.length === 0) {
|
|
116
|
+
lines.push(c(ANSI.dim, 'No milestones yet. Run /np:new-project or /np:new-milestone.'));
|
|
117
|
+
lines.push('');
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const m of snap.milestones) {
|
|
122
|
+
const name = m.name ? ' — ' + m.name : '';
|
|
123
|
+
const status = m.status ? ' ' + c(ANSI.dim, '[' + m.status + ']') : '';
|
|
124
|
+
lines.push(c(ANSI.bold, m.id) + name + status);
|
|
125
|
+
if (m.slices.length === 0) {
|
|
126
|
+
lines.push(' ' + c(ANSI.dim, 'no slices planned'));
|
|
127
|
+
}
|
|
128
|
+
for (const s of m.slices) {
|
|
129
|
+
lines.push(' ' + c(ANSI.bold, s.full_id) + ' ' + _summarizeCounts(s.counts, useColor));
|
|
130
|
+
if (s.task_statuses.length > 0) {
|
|
131
|
+
lines.push(' ' + _checkboxRow(s.task_statuses, useColor));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
collectSnapshot,
|
|
142
|
+
renderSnapshot,
|
|
143
|
+
ANSI,
|
|
144
|
+
STATUS_GLYPHS,
|
|
145
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const dashboard = require('./dashboard.cjs');
|
|
10
|
+
|
|
11
|
+
function _sandbox() {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-dashboard-'));
|
|
13
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _writeTask(root, mNum, sNum, tNum, status, name) {
|
|
18
|
+
const mIdStr = 'M' + String(mNum).padStart(3, '0');
|
|
19
|
+
const sIdStr = 'S' + String(sNum).padStart(3, '0');
|
|
20
|
+
const tIdStr = 'T' + String(tNum).padStart(4, '0');
|
|
21
|
+
const fullId = mIdStr + '-' + sIdStr + '-' + tIdStr;
|
|
22
|
+
const dir = path.join(root, '.nubos-pilot', 'milestones', mIdStr, 'slices', sIdStr, 'tasks', tIdStr);
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
const fm = [
|
|
25
|
+
'---',
|
|
26
|
+
'id: "' + fullId + '"',
|
|
27
|
+
'slice: "' + mIdStr + '-' + sIdStr + '"',
|
|
28
|
+
'milestone: "' + mIdStr + '"',
|
|
29
|
+
'type: execute',
|
|
30
|
+
'status: ' + status,
|
|
31
|
+
'tier: sonnet',
|
|
32
|
+
'owner: np-executor',
|
|
33
|
+
'wave: 1',
|
|
34
|
+
'depends_on: []',
|
|
35
|
+
'files_modified: []',
|
|
36
|
+
'autonomous: true',
|
|
37
|
+
'must_haves: {}',
|
|
38
|
+
'---',
|
|
39
|
+
'',
|
|
40
|
+
'# ' + fullId + ' — ' + name,
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
fs.writeFileSync(path.join(dir, tIdStr + '-PLAN.md'), fm, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _writeMeta(root, mNum, meta) {
|
|
47
|
+
const mIdStr = 'M' + String(mNum).padStart(3, '0');
|
|
48
|
+
const dir = path.join(root, '.nubos-pilot', 'milestones', mIdStr);
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
fs.writeFileSync(path.join(dir, mIdStr + '-META.json'), JSON.stringify(meta), 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('DB-1: collectSnapshot returns only { milestones } shape', () => {
|
|
54
|
+
const root = _sandbox();
|
|
55
|
+
try {
|
|
56
|
+
const snap = dashboard.collectSnapshot(root);
|
|
57
|
+
assert.deepEqual(Object.keys(snap), ['milestones']);
|
|
58
|
+
assert.equal(Array.isArray(snap.milestones), true);
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('DB-2: collectSnapshot counts task statuses per slice', () => {
|
|
65
|
+
const root = _sandbox();
|
|
66
|
+
try {
|
|
67
|
+
_writeMeta(root, 1, { name: 'Auth', status: 'active' });
|
|
68
|
+
_writeTask(root, 1, 1, 1, 'done', 'A');
|
|
69
|
+
_writeTask(root, 1, 1, 2, 'done', 'B');
|
|
70
|
+
_writeTask(root, 1, 1, 3, 'in-progress', 'C');
|
|
71
|
+
_writeTask(root, 1, 1, 4, 'pending', 'D');
|
|
72
|
+
_writeTask(root, 1, 1, 5, 'skipped', 'E');
|
|
73
|
+
const snap = dashboard.collectSnapshot(root);
|
|
74
|
+
assert.equal(snap.milestones.length, 1);
|
|
75
|
+
const m = snap.milestones[0];
|
|
76
|
+
assert.equal(m.id, 'M001');
|
|
77
|
+
assert.equal(m.name, 'Auth');
|
|
78
|
+
assert.equal(m.slices.length, 1);
|
|
79
|
+
assert.deepEqual(m.slices[0].counts, {
|
|
80
|
+
total: 5, pending: 1, 'in-progress': 1, done: 2, skipped: 1, parked: 0,
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(m.slices[0].task_statuses, ['done', 'done', 'in-progress', 'pending', 'skipped']);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('DB-3: renderSnapshot prints milestone, slice, checkbox row', () => {
|
|
89
|
+
const root = _sandbox();
|
|
90
|
+
try {
|
|
91
|
+
_writeMeta(root, 1, { name: 'Auth', status: 'active' });
|
|
92
|
+
_writeTask(root, 1, 1, 1, 'done', 'Login');
|
|
93
|
+
_writeTask(root, 1, 1, 2, 'pending', 'Logout');
|
|
94
|
+
const snap = dashboard.collectSnapshot(root);
|
|
95
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
96
|
+
assert.match(out, /^nubos-pilot/);
|
|
97
|
+
assert.match(out, /M001 — Auth/);
|
|
98
|
+
assert.match(out, /\[active\]/);
|
|
99
|
+
assert.match(out, /M001-S001/);
|
|
100
|
+
assert.match(out, /1 done/);
|
|
101
|
+
assert.match(out, /1 pending/);
|
|
102
|
+
assert.match(out, /\[x\] \[ \]/);
|
|
103
|
+
} finally {
|
|
104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('DB-4: renderSnapshot shows "No milestones yet" when none exist', () => {
|
|
109
|
+
const root = _sandbox();
|
|
110
|
+
try {
|
|
111
|
+
const snap = dashboard.collectSnapshot(root);
|
|
112
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
113
|
+
assert.match(out, /No milestones yet/);
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('DB-5: renderSnapshot with color=false emits no ANSI codes', () => {
|
|
120
|
+
const root = _sandbox();
|
|
121
|
+
try {
|
|
122
|
+
_writeMeta(root, 1, { name: 'Auth' });
|
|
123
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
124
|
+
const snap = dashboard.collectSnapshot(root);
|
|
125
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
126
|
+
assert.equal(/\x1b\[/.test(out), false, 'render with color=false must not emit ANSI');
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('DB-6: renderSnapshot with default color includes ANSI codes', () => {
|
|
133
|
+
const root = _sandbox();
|
|
134
|
+
try {
|
|
135
|
+
_writeMeta(root, 1, { name: 'Auth' });
|
|
136
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
137
|
+
const snap = dashboard.collectSnapshot(root);
|
|
138
|
+
const out = dashboard.renderSnapshot(snap);
|
|
139
|
+
assert.match(out, /\x1b\[/);
|
|
140
|
+
} finally {
|
|
141
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('DB-7: STATUS_GLYPHS covers all task-status enum values', () => {
|
|
146
|
+
for (const s of ['pending', 'in-progress', 'done', 'skipped', 'parked']) {
|
|
147
|
+
assert.ok(dashboard.STATUS_GLYPHS[s], 'missing glyph for ' + s);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('DB-8: empty slice (no tasks) renders "no tasks" indicator', () => {
|
|
152
|
+
const root = _sandbox();
|
|
153
|
+
try {
|
|
154
|
+
_writeMeta(root, 1, { name: 'Empty' });
|
|
155
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks'), { recursive: true });
|
|
156
|
+
const snap = dashboard.collectSnapshot(root);
|
|
157
|
+
const out = dashboard.renderSnapshot(snap, { color: false });
|
|
158
|
+
assert.match(out, /M001-S001/);
|
|
159
|
+
assert.match(out, /no tasks/);
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('DB-9: multiple milestones render in numeric order', () => {
|
|
166
|
+
const root = _sandbox();
|
|
167
|
+
try {
|
|
168
|
+
_writeMeta(root, 2, { name: 'Second' });
|
|
169
|
+
_writeMeta(root, 1, { name: 'First' });
|
|
170
|
+
_writeTask(root, 1, 1, 1, 'done', 'X');
|
|
171
|
+
_writeTask(root, 2, 1, 1, 'pending', 'Y');
|
|
172
|
+
const snap = dashboard.collectSnapshot(root);
|
|
173
|
+
assert.equal(snap.milestones.length, 2);
|
|
174
|
+
assert.equal(snap.milestones[0].id, 'M001');
|
|
175
|
+
assert.equal(snap.milestones[1].id, 'M002');
|
|
176
|
+
} finally {
|
|
177
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
package/lib/git.cjs
CHANGED
|
@@ -234,6 +234,26 @@ function workspaceGitInfo(cwd) {
|
|
|
234
234
|
return { is_repo: true, current_branch, remote, branches, commits };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
function runGit(args, opts) {
|
|
238
|
+
const o = opts || {};
|
|
239
|
+
const spawnOpts = { stdio: o.stdio || ['ignore', 'pipe', 'pipe'] };
|
|
240
|
+
if (o.cwd) spawnOpts.cwd = o.cwd;
|
|
241
|
+
try {
|
|
242
|
+
const stdout = execFileSync('git', args, spawnOpts);
|
|
243
|
+
return { stdout: stdout ? stdout.toString('utf-8') : '', ok: true };
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const stderr = (err && err.stderr) ? err.stderr.toString('utf-8') : '';
|
|
246
|
+
const stdout = (err && err.stdout) ? err.stdout.toString('utf-8') : '';
|
|
247
|
+
return {
|
|
248
|
+
stdout,
|
|
249
|
+
stderr,
|
|
250
|
+
status: err && typeof err.status === 'number' ? err.status : null,
|
|
251
|
+
ok: false,
|
|
252
|
+
error: err,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
237
257
|
module.exports = {
|
|
238
258
|
commitTask,
|
|
239
259
|
assertCommittablePaths,
|
|
@@ -246,4 +266,5 @@ module.exports = {
|
|
|
246
266
|
gitShowSafe,
|
|
247
267
|
gitDiffNoColor,
|
|
248
268
|
workspaceGitInfo,
|
|
269
|
+
runGit,
|
|
249
270
|
};
|