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
package/agents/np-executor.md
CHANGED
|
@@ -112,6 +112,38 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
|
|
|
112
112
|
- Auto-discover files via `git status` — the plan declares scope, not the filesystem.
|
|
113
113
|
</scope_guardrail>
|
|
114
114
|
|
|
115
|
+
## Handoff Protocol
|
|
116
|
+
|
|
117
|
+
Agent handoffs are persistent notes between phase invocations — context that doesn't belong in commit messages or frontmatter. They survive across spawns and let downstream agents see non-obvious signals you discovered during execution.
|
|
118
|
+
|
|
119
|
+
**At start, check handoffs addressed to you:**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-executor --milestone M<NNN> --status open
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For each relevant entry:
|
|
126
|
+
1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
|
|
127
|
+
2. Apply the context to your work
|
|
128
|
+
3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
|
|
129
|
+
|
|
130
|
+
**At end, write a handoff ONLY for genuine cross-phase signals:**
|
|
131
|
+
|
|
132
|
+
- Non-obvious compromise the verifier must know about → `--to np-verifier`
|
|
133
|
+
- Plan flaw the next planner run should address → `--to np-planner`
|
|
134
|
+
- Trap in shared code that applies broadly → `--to "*"` (broadcast)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
138
|
+
--from np-executor \
|
|
139
|
+
--to np-verifier \
|
|
140
|
+
--topic "Short subject" \
|
|
141
|
+
--milestone M<NNN> --slice M<NNN>-S<NNN> --task M<NNN>-S<NNN>-T<NNNN> \
|
|
142
|
+
--body "What downstream needs to know"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Do NOT write handoffs for routine work. One handoff per genuine signal; noise trains future agents to ignore the channel.
|
|
146
|
+
|
|
115
147
|
## Stop Conditions
|
|
116
148
|
|
|
117
149
|
Hard-stop (report to orchestrator, do not attempt recovery):
|
package/agents/np-planner.md
CHANGED
|
@@ -14,6 +14,34 @@ Spawned by:
|
|
|
14
14
|
- `/np:plan-phase <N> --gaps` — gap closure from verification failures
|
|
15
15
|
- `/np:plan-phase <N>` in revision mode — updating plans based on plan-checker feedback
|
|
16
16
|
|
|
17
|
+
## Handoff Protocol
|
|
18
|
+
|
|
19
|
+
Agent handoffs are persistent notes between phase invocations. Before planning, check handoffs addressed to `np-planner` for this milestone:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-planner --milestone M<NNN> --status open
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For each entry:
|
|
26
|
+
1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
|
|
27
|
+
2. Integrate the signal into your plan, OR reject it with a return handoff explaining why (executors often flag plan-flaws this way; honor them or refute them — never silently ignore).
|
|
28
|
+
3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
|
|
29
|
+
|
|
30
|
+
**Write a handoff ONLY for cross-phase signals downstream needs:**
|
|
31
|
+
|
|
32
|
+
- Scope nuance that doesn't fit cleanly in the slice `PLAN.md` → `--to np-executor`
|
|
33
|
+
- SC interpretation that matters at verification time → `--to np-verifier`
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
37
|
+
--from np-planner --to <target> \
|
|
38
|
+
--topic "Short subject" \
|
|
39
|
+
--milestone M<NNN> \
|
|
40
|
+
--body "What downstream needs to know"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Do NOT write handoffs for information already captured in PLAN/ROADMAP/CONTEXT.
|
|
44
|
+
|
|
17
45
|
## Layout (MANDATORY)
|
|
18
46
|
|
|
19
47
|
Every artifact you write MUST land at exactly these paths. The orchestrator provides the absolute paths in the `<files_to_write>` block — use them verbatim.
|
package/agents/np-researcher.md
CHANGED
|
@@ -24,6 +24,34 @@ anchor points for your research — do not propose replacements without
|
|
|
24
24
|
explicit justification. If `INDEX.md` is absent, report and stop —
|
|
25
25
|
`np:scan-codebase` must run first.
|
|
26
26
|
|
|
27
|
+
## Handoff Protocol
|
|
28
|
+
|
|
29
|
+
Agent handoffs are persistent notes between phase invocations. Before researching, check handoffs addressed to `np-researcher`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-researcher --milestone M<NNN> --status open
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For each entry:
|
|
36
|
+
1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
|
|
37
|
+
2. Let the signal shape your research focus (e.g. a verifier-flagged uncertain SC steers deeper investigation in that area)
|
|
38
|
+
3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
|
|
39
|
+
|
|
40
|
+
**Write a handoff when findings apply beyond this single RESEARCH.md:**
|
|
41
|
+
|
|
42
|
+
- Evidence hint for a known-hard SC → `--to np-verifier`
|
|
43
|
+
- Cross-milestone trap future planners must see → `--to np-planner` without `--milestone` (global scope)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
47
|
+
--from np-researcher --to <target> \
|
|
48
|
+
--topic "Short subject" \
|
|
49
|
+
[--milestone M<NNN>] \
|
|
50
|
+
--body "What downstream needs to know"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Do NOT use handoffs as a replacement for RESEARCH.md content — they are for signals that transcend this milestone's research doc.
|
|
54
|
+
|
|
27
55
|
## Tool Availability Detection
|
|
28
56
|
|
|
29
57
|
On startup, before doing any research work, probe the web + MCP surface:
|
package/agents/np-verifier.md
CHANGED
|
@@ -32,6 +32,21 @@ The orchestrator provides these in your prompt context. Read every path it hands
|
|
|
32
32
|
| success_criteria (from init payload) | The list of SC strings to classify. | provided inline in prompt |
|
|
33
33
|
| Task commits | `git log --grep='^task(M<NNN>-'` → audit trail. | git history |
|
|
34
34
|
|
|
35
|
+
## Handoff Protocol (read-only)
|
|
36
|
+
|
|
37
|
+
Agent handoffs are persistent notes between phase invocations. Before classifying, check handoffs addressed to `np-verifier` for this milestone:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-verifier --milestone M<NNN> --status open
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For each entry:
|
|
44
|
+
1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
|
|
45
|
+
2. Fold the context into your evidence gathering (executors often flag compromises that would otherwise read as `Fail` — the handoff explains the compromise and may move the SC to `Pass` or `Defer`).
|
|
46
|
+
3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
|
|
47
|
+
|
|
48
|
+
**You do NOT write handoffs.** Verifier is detection-only — your findings land in `VERIFICATION.md`, never in the handoff channel. If you have no Write tool, writing handoffs is impossible anyway.
|
|
49
|
+
|
|
35
50
|
## Workflow
|
|
36
51
|
|
|
37
52
|
1. **Parse success_criteria:** read the prompt-provided SC list (from `np-tools.cjs init verify-work <N>`).
|
|
@@ -54,6 +54,16 @@ const COMMANDS = [
|
|
|
54
54
|
{ name: 'phase-meta', category: 'Planning', description: 'Read roadmap.yaml phase fields as JSON (supports --field NAME and --length for arrays)' },
|
|
55
55
|
{ name: 'state-dir', category: 'Utility', description: 'Print project-state directory (.nubos-pilot) or a validated subdir via --subdir NAME' },
|
|
56
56
|
{ name: 'render-template', category: 'Utility', description: 'Render a shipped template by name with --vars JSON (or --vars-file PATH)' },
|
|
57
|
+
{ name: 'render-todo', category: 'Utility', description: 'Render slice TODO.md rollup (checkbox view of task statuses) for a slice full-id' },
|
|
58
|
+
{ name: 'handoff-write', category: 'Capture', description: 'Write an agent-to-agent handoff note (milestone-scoped by default, global without --milestone)' },
|
|
59
|
+
{ name: 'handoff-read', category: 'Capture', description: 'Read a single handoff by id (returns frontmatter + body as JSON)' },
|
|
60
|
+
{ name: 'handoff-list', category: 'Capture', description: 'List handoffs (JSON array); filter with --for AGENT, --milestone M<NNN>, --status STATUS, --global' },
|
|
61
|
+
{ name: 'handoff-status', category: 'Capture', description: 'Update a handoff status (open|read|acted|archived)' },
|
|
62
|
+
{ name: 'worktree-create', category: 'Execution', description: 'Create an isolated git worktree for a slice (branch np/<mid>-<sid> off current HEAD) under .nubos-pilot/worktrees/' },
|
|
63
|
+
{ name: 'worktree-remove', category: 'Execution', description: 'Remove a slice worktree + delete its branch (--force / --keep-branch)' },
|
|
64
|
+
{ name: 'worktree-list', category: 'Execution', description: 'List all nubos-pilot-managed slice worktrees (np/<mid>-<sid> only) as JSON' },
|
|
65
|
+
{ name: 'worktree-ff-merge', category: 'Execution', description: 'Fast-forward merge a slice branch back to its base (fails hard on non-FF)' },
|
|
66
|
+
{ name: 'dashboard', category: 'Utility', description: 'One-shot console dashboard of milestones, slices, and tasks. Read-only; flags: --json, --no-color' },
|
|
57
67
|
{ name: 'thread-resume', category: 'Utility', description: 'Bump a thread markdown on resume (status OPEN→IN_PROGRESS, refresh last_resumed) via atomic write' },
|
|
58
68
|
{ name: 'state-incr', category: 'Capture', description: 'Increment a whitelisted STATE.md counter (e.g. pending_todos) under withFileLock' },
|
|
59
69
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { collectSnapshot, renderSnapshot } = require('../../lib/dashboard.cjs');
|
|
4
|
+
|
|
5
|
+
function _parseArgs(args) {
|
|
6
|
+
const out = { json: false, noColor: false };
|
|
7
|
+
for (const a of args) {
|
|
8
|
+
if (a === '--json') out.json = true;
|
|
9
|
+
else if (a === '--no-color') out.noColor = true;
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function run(args, opts) {
|
|
15
|
+
const o = opts || {};
|
|
16
|
+
const cwd = o.cwd || process.cwd();
|
|
17
|
+
const stdout = o.stdout || process.stdout;
|
|
18
|
+
const parsed = _parseArgs(Array.isArray(args) ? args : []);
|
|
19
|
+
|
|
20
|
+
const snap = collectSnapshot(cwd);
|
|
21
|
+
if (parsed.json) {
|
|
22
|
+
stdout.write(JSON.stringify(snap, null, 2) + '\n');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const useColor = !parsed.noColor && Boolean(stdout.isTTY);
|
|
26
|
+
stdout.write(renderSnapshot(snap, { color: useColor }) + '\n');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { run, _parseArgs };
|
package/bin/np-tools/doctor.cjs
CHANGED
|
@@ -7,15 +7,45 @@ const path = require('node:path');
|
|
|
7
7
|
const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
|
|
8
8
|
const manifestMod = require('../../lib/install/manifest.cjs');
|
|
9
9
|
const codexTomlMod = require('../../lib/install/codex-toml.cjs');
|
|
10
|
+
const runtimeAssetsMod = require('../../lib/install/runtime-assets.cjs');
|
|
10
11
|
const askuserMod = require('../../lib/askuser.cjs');
|
|
11
12
|
const codebaseManifest = require('../../lib/codebase-manifest.cjs');
|
|
12
13
|
const { scan: workspaceScan } = require('../../lib/workspace-scan.cjs');
|
|
13
14
|
|
|
14
15
|
const PAYLOAD_SUBPATH = path.join('.claude', 'nubos-pilot');
|
|
16
|
+
const STATE_SUBPATH = '.nubos-pilot';
|
|
15
17
|
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
18
|
+
const OPENCODE_LOCAL_PREFIX = '.opencode/nubos-pilot/';
|
|
19
|
+
const OPENCODE_GLOBAL_PREFIX = '~/.config/opencode/nubos-pilot/';
|
|
16
20
|
|
|
17
|
-
function
|
|
18
|
-
|
|
21
|
+
function _readScope(projectRoot) {
|
|
22
|
+
const cfgPath = path.join(projectRoot, STATE_SUBPATH, 'config.json');
|
|
23
|
+
if (!fs.existsSync(cfgPath)) return 'local';
|
|
24
|
+
try {
|
|
25
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
26
|
+
return cfg && cfg.scope === 'global' ? 'global' : 'local';
|
|
27
|
+
} catch {
|
|
28
|
+
return 'local';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _payloadBaseFor(projectRoot, scope) {
|
|
33
|
+
return scope === 'global' ? os.homedir() : projectRoot;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _payloadDirFor(projectRoot, scope) {
|
|
37
|
+
return path.join(_payloadBaseFor(projectRoot, scope), PAYLOAD_SUBPATH);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _resolveManifestEntry(rel, projectRoot, scope) {
|
|
41
|
+
if (rel.startsWith('~/')) {
|
|
42
|
+
return path.join(os.homedir(), rel.slice(2));
|
|
43
|
+
}
|
|
44
|
+
const base = _payloadBaseFor(projectRoot, scope);
|
|
45
|
+
if (runtimeAssetsMod.isAssetManifestKey(rel) || rel.startsWith(OPENCODE_LOCAL_PREFIX)) {
|
|
46
|
+
return path.join(base, rel);
|
|
47
|
+
}
|
|
48
|
+
return path.join(_payloadDirFor(projectRoot, scope), rel);
|
|
19
49
|
}
|
|
20
50
|
|
|
21
51
|
function _pkgVersion() {
|
|
@@ -26,8 +56,9 @@ function _pkgVersion() {
|
|
|
26
56
|
}
|
|
27
57
|
}
|
|
28
58
|
|
|
29
|
-
function _checkManifestIntegrity(
|
|
59
|
+
function _checkManifestIntegrity(projectRoot, scope) {
|
|
30
60
|
const issues = [];
|
|
61
|
+
const payloadDir = _payloadDirFor(projectRoot, scope);
|
|
31
62
|
let manifest = null;
|
|
32
63
|
try {
|
|
33
64
|
manifest = manifestMod.readManifest(payloadDir);
|
|
@@ -51,7 +82,7 @@ function _checkManifestIntegrity(payloadDir) {
|
|
|
51
82
|
}
|
|
52
83
|
const files = (manifest.files && typeof manifest.files === 'object') ? manifest.files : {};
|
|
53
84
|
for (const rel of Object.keys(files)) {
|
|
54
|
-
const full =
|
|
85
|
+
const full = _resolveManifestEntry(rel, projectRoot, scope);
|
|
55
86
|
if (!fs.existsSync(full)) {
|
|
56
87
|
issues.push({
|
|
57
88
|
id: 'payload-missing',
|
|
@@ -306,9 +337,10 @@ function _checkMilestoneLayout(projectRoot) {
|
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
function _audit(projectRoot) {
|
|
309
|
-
const
|
|
340
|
+
const scope = _readScope(projectRoot);
|
|
341
|
+
const payloadDir = _payloadDirFor(projectRoot, scope);
|
|
310
342
|
const issues = [];
|
|
311
|
-
const { manifest, issues: manifestIssues } = _checkManifestIntegrity(
|
|
343
|
+
const { manifest, issues: manifestIssues } = _checkManifestIntegrity(projectRoot, scope);
|
|
312
344
|
issues.push(...manifestIssues);
|
|
313
345
|
issues.push(..._checkVersionMismatch(manifest));
|
|
314
346
|
issues.push(..._checkHooksMissing(manifest, payloadDir));
|
|
@@ -85,6 +85,35 @@ test('DOC-4: flags codebase-tbd-docs for modules with _TBD Purpose', async () =>
|
|
|
85
85
|
assert.ok(tbd.details.count >= 1);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
test('DOC-6: asset manifest keys resolve to project root, not payloadDir', async () => {
|
|
89
|
+
const root = makeSandbox();
|
|
90
|
+
|
|
91
|
+
const payloadDir = path.join(root, '.claude', 'nubos-pilot');
|
|
92
|
+
fs.mkdirSync(payloadDir, { recursive: true });
|
|
93
|
+
fs.writeFileSync(path.join(payloadDir, '.manifest.json'), JSON.stringify({
|
|
94
|
+
version: '0.0.0',
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
files: {
|
|
97
|
+
'.claude/commands/np/foo.md': 'deadbeef',
|
|
98
|
+
'.claude/agents/np-bar.md': 'deadbeef',
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
const cmdDir = path.join(root, '.claude', 'commands', 'np');
|
|
102
|
+
const agentsDir = path.join(root, '.claude', 'agents');
|
|
103
|
+
fs.mkdirSync(cmdDir, { recursive: true });
|
|
104
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(cmdDir, 'foo.md'), 'x');
|
|
106
|
+
fs.writeFileSync(path.join(agentsDir, 'np-bar.md'), 'y');
|
|
107
|
+
|
|
108
|
+
const cap = captureStdout();
|
|
109
|
+
await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
|
|
110
|
+
const out = cap.json();
|
|
111
|
+
const missing = out.issues.filter((i) => i.id === 'payload-missing');
|
|
112
|
+
assert.equal(missing.length, 0,
|
|
113
|
+
'asset keys must resolve to project-root paths (found ' +
|
|
114
|
+
missing.map((m) => m.file).join(', ') + ')');
|
|
115
|
+
});
|
|
116
|
+
|
|
88
117
|
test('DOC-5: no tbd flag after prose applied', async () => {
|
|
89
118
|
const root = makeSandbox();
|
|
90
119
|
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { listHandoffs } = require('../../lib/handoff.cjs');
|
|
4
|
+
|
|
5
|
+
function _parseArgs(args) {
|
|
6
|
+
const out = { for: null, milestone: null, status: null, global: false };
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const a = args[i];
|
|
9
|
+
if (a === '--for') { out.for = args[++i] || null; continue; }
|
|
10
|
+
if (a === '--milestone') { out.milestone = args[++i] || null; continue; }
|
|
11
|
+
if (a === '--status') { out.status = args[++i] || null; continue; }
|
|
12
|
+
if (a === '--global') { out.global = true; continue; }
|
|
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
|
+
const list = listHandoffs(parsed, cwd);
|
|
23
|
+
stdout.write(JSON.stringify(list) + '\n');
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { readHandoff } = require('../../lib/handoff.cjs');
|
|
5
|
+
|
|
6
|
+
function run(args, opts) {
|
|
7
|
+
const o = opts || {};
|
|
8
|
+
const cwd = o.cwd || process.cwd();
|
|
9
|
+
const stdout = o.stdout || process.stdout;
|
|
10
|
+
const list = Array.isArray(args) ? args : [];
|
|
11
|
+
const id = list.find((a) => !a.startsWith('-'));
|
|
12
|
+
if (!id) {
|
|
13
|
+
throw new NubosPilotError('handoff-read-missing-id', 'handoff id required', {});
|
|
14
|
+
}
|
|
15
|
+
const rec = readHandoff(id, cwd);
|
|
16
|
+
stdout.write(JSON.stringify(rec) + '\n');
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { run };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { setHandoffStatus } = require('../../lib/handoff.cjs');
|
|
5
|
+
|
|
6
|
+
function run(args, opts) {
|
|
7
|
+
const o = opts || {};
|
|
8
|
+
const cwd = o.cwd || process.cwd();
|
|
9
|
+
const stdout = o.stdout || process.stdout;
|
|
10
|
+
const list = Array.isArray(args) ? args : [];
|
|
11
|
+
const positional = list.filter((a) => !a.startsWith('-'));
|
|
12
|
+
const id = positional[0];
|
|
13
|
+
const newStatus = positional[1];
|
|
14
|
+
if (!id || !newStatus) {
|
|
15
|
+
throw new NubosPilotError(
|
|
16
|
+
'handoff-status-missing-args',
|
|
17
|
+
'usage: handoff-status <id> <new-status>',
|
|
18
|
+
{ got: { id, newStatus } },
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const result = setHandoffStatus(id, newStatus, cwd);
|
|
22
|
+
stdout.write(JSON.stringify({ id, status: result }) + '\n');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { run };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
5
|
+
const { writeHandoff } = require('../../lib/handoff.cjs');
|
|
6
|
+
|
|
7
|
+
function _parseArgs(args) {
|
|
8
|
+
const out = {
|
|
9
|
+
from: null, to: null, topic: null,
|
|
10
|
+
milestone: null, slice: null, task: null,
|
|
11
|
+
body: null, bodyFile: null,
|
|
12
|
+
};
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const a = args[i];
|
|
15
|
+
if (a === '--from') { out.from = args[++i] || null; continue; }
|
|
16
|
+
if (a === '--to') { out.to = args[++i] || null; continue; }
|
|
17
|
+
if (a === '--topic') { out.topic = args[++i] || null; continue; }
|
|
18
|
+
if (a === '--milestone') { out.milestone = args[++i] || null; continue; }
|
|
19
|
+
if (a === '--slice') { out.slice = args[++i] || null; continue; }
|
|
20
|
+
if (a === '--task') { out.task = args[++i] || null; continue; }
|
|
21
|
+
if (a === '--body') { out.body = args[++i] || null; continue; }
|
|
22
|
+
if (a === '--body-file') { out.bodyFile = args[++i] || null; continue; }
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(args, opts) {
|
|
28
|
+
const o = opts || {};
|
|
29
|
+
const cwd = o.cwd || process.cwd();
|
|
30
|
+
const stdout = o.stdout || process.stdout;
|
|
31
|
+
const parsed = _parseArgs(Array.isArray(args) ? args : []);
|
|
32
|
+
|
|
33
|
+
let body = parsed.body || '';
|
|
34
|
+
if (parsed.bodyFile) {
|
|
35
|
+
try { body = fs.readFileSync(parsed.bodyFile, 'utf-8'); }
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw new NubosPilotError(
|
|
38
|
+
'handoff-body-file-read-failed',
|
|
39
|
+
'failed to read --body-file: ' + (err && err.message),
|
|
40
|
+
{ path: parsed.bodyFile },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = writeHandoff({
|
|
46
|
+
from: parsed.from,
|
|
47
|
+
to: parsed.to,
|
|
48
|
+
topic: parsed.topic,
|
|
49
|
+
milestone: parsed.milestone,
|
|
50
|
+
slice: parsed.slice,
|
|
51
|
+
task: parsed.task,
|
|
52
|
+
body,
|
|
53
|
+
}, cwd);
|
|
54
|
+
|
|
55
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { run, _parseArgs };
|
|
@@ -357,6 +357,19 @@ function _scaffoldAllTasks(mNum, cwd) {
|
|
|
357
357
|
for (const s of slices) {
|
|
358
358
|
per.push(_scaffoldSliceTasks(mNum, s.number, cwd));
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
const { renderTodoMd } = require('../../lib/todo.cjs');
|
|
362
|
+
const todos = [];
|
|
363
|
+
for (const s of slices) {
|
|
364
|
+
try {
|
|
365
|
+
todos.push(renderTodoMd(s.full_id, cwd));
|
|
366
|
+
} catch (err) {
|
|
367
|
+
process.stderr.write(
|
|
368
|
+
'[nubos-pilot warn] TODO.md render failed for ' + s.full_id + ': ' + ((err && err.message) || err) + '\n',
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
360
373
|
const total = per.reduce((acc, p) => acc + (p.task_count || 0), 0);
|
|
361
374
|
return {
|
|
362
375
|
scaffolded: per,
|
|
@@ -364,6 +377,7 @@ function _scaffoldAllTasks(mNum, cwd) {
|
|
|
364
377
|
milestone: layout.mId(mNum),
|
|
365
378
|
total_tasks: total,
|
|
366
379
|
normalized_ids: normalized.changed ? normalized.remap : {},
|
|
380
|
+
todos_rendered: todos,
|
|
367
381
|
};
|
|
368
382
|
}
|
|
369
383
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { renderTodoMd } = require('../../lib/todo.cjs');
|
|
5
|
+
|
|
6
|
+
function run(args, opts) {
|
|
7
|
+
const o = opts || {};
|
|
8
|
+
const cwd = o.cwd || process.cwd();
|
|
9
|
+
const stdout = o.stdout || process.stdout;
|
|
10
|
+
const list = Array.isArray(args) ? args : [];
|
|
11
|
+
const sliceFullId = list.find((a) => !a.startsWith('-'));
|
|
12
|
+
if (!sliceFullId) {
|
|
13
|
+
throw new NubosPilotError(
|
|
14
|
+
'render-todo-missing-slice',
|
|
15
|
+
'slice full-id required (e.g. M001-S001)',
|
|
16
|
+
{},
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const target = renderTodoMd(sliceFullId, cwd);
|
|
20
|
+
stdout.write(target + '\n');
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { run };
|
|
@@ -8,6 +8,11 @@ const { restoreFiles } = require('../../lib/git.cjs');
|
|
|
8
8
|
const { deleteCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
|
|
9
9
|
const layout = require('../../lib/layout.cjs');
|
|
10
10
|
const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
|
|
11
|
+
const {
|
|
12
|
+
hasSliceWorktree,
|
|
13
|
+
removeSliceWorktree,
|
|
14
|
+
worktreeIsolationEnabled,
|
|
15
|
+
} = require('../../lib/worktree.cjs');
|
|
11
16
|
|
|
12
17
|
function _resolveTaskId(explicit, cwd) {
|
|
13
18
|
if (explicit) {
|
|
@@ -44,13 +49,33 @@ function _readTaskFiles(taskId, cwd) {
|
|
|
44
49
|
return Array.isArray(frontmatter.files_modified) ? frontmatter.files_modified : [];
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function _maybeRemoveWorktreeForTask(taskId, cwd) {
|
|
53
|
+
if (!worktreeIsolationEnabled(cwd)) return null;
|
|
54
|
+
let parsed;
|
|
55
|
+
try { parsed = layout.parseTaskFullId(taskId); } catch { return null; }
|
|
56
|
+
const sliceFullId = layout.sliceFullId(parsed.milestone, parsed.slice);
|
|
57
|
+
let exists = false;
|
|
58
|
+
try { exists = hasSliceWorktree(sliceFullId, cwd); } catch { exists = false; }
|
|
59
|
+
if (!exists) return null;
|
|
60
|
+
try {
|
|
61
|
+
return removeSliceWorktree(sliceFullId, cwd, { force: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
'[nubos-pilot warn] removeSliceWorktree failed for ' + sliceFullId + ': ' + ((err && err.message) || err) + '\n',
|
|
65
|
+
);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
function run(args, ctx) {
|
|
48
71
|
const context = ctx || {};
|
|
49
72
|
const cwd = context.cwd || process.cwd();
|
|
50
73
|
const stdout = context.stdout || process.stdout;
|
|
51
74
|
const list = Array.isArray(args) ? args : [];
|
|
52
75
|
|
|
53
|
-
const
|
|
76
|
+
const keepWorktree = list.includes('--keep-worktree');
|
|
77
|
+
const positional = list.filter((a) => a && !a.startsWith('--'));
|
|
78
|
+
const explicit = positional[0] || null;
|
|
54
79
|
const taskId = _resolveTaskId(explicit, cwd);
|
|
55
80
|
|
|
56
81
|
if (!taskId) {
|
|
@@ -91,12 +116,16 @@ function run(args, ctx) {
|
|
|
91
116
|
return { frontmatter: fm, body: state.body };
|
|
92
117
|
}, cwd);
|
|
93
118
|
|
|
119
|
+
const worktreeRemoved = keepWorktree ? null : _maybeRemoveWorktreeForTask(taskId, cwd);
|
|
120
|
+
|
|
94
121
|
const payload = {
|
|
95
122
|
ok: true,
|
|
96
123
|
task_id: taskId,
|
|
97
124
|
restored_files: files,
|
|
98
125
|
deleted_checkpoints: [taskId],
|
|
99
|
-
|
|
126
|
+
worktree_removed: worktreeRemoved,
|
|
127
|
+
message: 'in-flight task discarded; working tree restored to HEAD'
|
|
128
|
+
+ (worktreeRemoved ? '; worktree ' + worktreeRemoved.branch + ' removed' : ''),
|
|
100
129
|
};
|
|
101
130
|
stdout.write(JSON.stringify(payload));
|
|
102
131
|
return payload;
|
|
@@ -5,6 +5,29 @@ const { readState } = require('../../lib/state.cjs');
|
|
|
5
5
|
const { readCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
|
|
6
6
|
const { TASK_ID_RE } = require('../../lib/tasks.cjs');
|
|
7
7
|
const textMode = require('../../lib/text-mode.cjs');
|
|
8
|
+
const layout = require('../../lib/layout.cjs');
|
|
9
|
+
const {
|
|
10
|
+
hasSliceWorktree,
|
|
11
|
+
sliceWorktreePath,
|
|
12
|
+
sliceBranchName,
|
|
13
|
+
worktreeIsolationEnabled,
|
|
14
|
+
listSliceWorktrees,
|
|
15
|
+
} = require('../../lib/worktree.cjs');
|
|
16
|
+
|
|
17
|
+
function _worktreeInfoForTask(taskId, cwd) {
|
|
18
|
+
if (!taskId || !worktreeIsolationEnabled(cwd)) return null;
|
|
19
|
+
let parsed;
|
|
20
|
+
try { parsed = layout.parseTaskFullId(taskId); } catch { return null; }
|
|
21
|
+
const sliceFullId = layout.sliceFullId(parsed.milestone, parsed.slice);
|
|
22
|
+
let exists = false;
|
|
23
|
+
try { exists = hasSliceWorktree(sliceFullId, cwd); } catch { exists = false; }
|
|
24
|
+
if (!exists) return null;
|
|
25
|
+
return {
|
|
26
|
+
slice_full_id: sliceFullId,
|
|
27
|
+
branch: sliceBranchName(sliceFullId),
|
|
28
|
+
path: sliceWorktreePath(sliceFullId, cwd),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
8
31
|
|
|
9
32
|
function _safeReadState(cwd) {
|
|
10
33
|
try { return readState(cwd); } catch { return null; }
|
|
@@ -74,6 +97,25 @@ function run(_args, ctx) {
|
|
|
74
97
|
payload.text_mode = tmDetail.enabled;
|
|
75
98
|
payload.text_mode_source = tmDetail.source;
|
|
76
99
|
|
|
100
|
+
const wtInfo = _worktreeInfoForTask(currentTask, cwd);
|
|
101
|
+
if (wtInfo) payload.worktree = wtInfo;
|
|
102
|
+
payload.worktree_isolation = worktreeIsolationEnabled(cwd);
|
|
103
|
+
let stale = [];
|
|
104
|
+
try {
|
|
105
|
+
stale = listSliceWorktrees(cwd).filter((w) => {
|
|
106
|
+
const b = w.branch;
|
|
107
|
+
const activeBranch = wtInfo && wtInfo.branch;
|
|
108
|
+
return b !== activeBranch;
|
|
109
|
+
});
|
|
110
|
+
} catch { stale = []; }
|
|
111
|
+
if (stale.length > 0) {
|
|
112
|
+
payload.stale_worktrees = stale.map((w) => ({
|
|
113
|
+
slice_full_id: w.slice_full_id,
|
|
114
|
+
branch: w.branch,
|
|
115
|
+
path: w.path,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
77
119
|
stdout.write(JSON.stringify(payload));
|
|
78
120
|
return payload;
|
|
79
121
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { createSliceWorktree } = require('../../lib/worktree.cjs');
|
|
5
|
+
|
|
6
|
+
function run(args, opts) {
|
|
7
|
+
const o = opts || {};
|
|
8
|
+
const cwd = o.cwd || process.cwd();
|
|
9
|
+
const stdout = o.stdout || process.stdout;
|
|
10
|
+
const list = Array.isArray(args) ? args : [];
|
|
11
|
+
const sliceFullId = list.find((a) => !a.startsWith('-'));
|
|
12
|
+
if (!sliceFullId) {
|
|
13
|
+
throw new NubosPilotError(
|
|
14
|
+
'worktree-create-missing-slice',
|
|
15
|
+
'slice full-id required (e.g. M001-S001)',
|
|
16
|
+
{},
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const result = createSliceWorktree(sliceFullId, cwd);
|
|
20
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { run };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { ffMergeSliceWorktree } = require('../../lib/worktree.cjs');
|
|
5
|
+
|
|
6
|
+
function _parseArgs(args) {
|
|
7
|
+
const out = { sliceFullId: null, target: null };
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const a = args[i];
|
|
10
|
+
if (a === '--target') { out.target = args[++i] || null; continue; }
|
|
11
|
+
if (!a.startsWith('-') && !out.sliceFullId) out.sliceFullId = a;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run(args, opts) {
|
|
17
|
+
const o = opts || {};
|
|
18
|
+
const cwd = o.cwd || process.cwd();
|
|
19
|
+
const stdout = o.stdout || process.stdout;
|
|
20
|
+
const parsed = _parseArgs(Array.isArray(args) ? args : []);
|
|
21
|
+
if (!parsed.sliceFullId) {
|
|
22
|
+
throw new NubosPilotError(
|
|
23
|
+
'worktree-ff-merge-missing-slice',
|
|
24
|
+
'slice full-id required (e.g. M001-S001)',
|
|
25
|
+
{},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const result = ffMergeSliceWorktree(parsed.sliceFullId, parsed.target, cwd);
|
|
29
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { run, _parseArgs };
|