nubos-pilot 0.6.2 → 0.6.3

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.
@@ -0,0 +1,134 @@
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 mod = require('./thread-resume.cjs');
10
+
11
+ function mkSandbox(fmLines, body) {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-tr-'));
13
+ const p = path.join(dir, 'thread.md');
14
+ const content = '---\n' + fmLines.join('\n') + '\n---\n' + (body || '# body\n');
15
+ fs.writeFileSync(p, content);
16
+ return { dir, p };
17
+ }
18
+
19
+ function captureStdout() {
20
+ const chunks = [];
21
+ return {
22
+ stream: { write: (c) => { chunks.push(c); } },
23
+ read: () => chunks.join(''),
24
+ };
25
+ }
26
+
27
+ test('TR-1: OPEN bumps to IN_PROGRESS', () => {
28
+ assert.equal(mod._bumpStatus('OPEN'), 'IN_PROGRESS');
29
+ });
30
+
31
+ test('TR-2: IN_PROGRESS stays IN_PROGRESS', () => {
32
+ assert.equal(mod._bumpStatus('IN_PROGRESS'), 'IN_PROGRESS');
33
+ });
34
+
35
+ test('TR-3: RESOLVED stays RESOLVED (monotonic)', () => {
36
+ assert.equal(mod._bumpStatus('RESOLVED'), 'RESOLVED');
37
+ });
38
+
39
+ test('TR-4: run() bumps OPEN and writes last_resumed', () => {
40
+ const { dir, p } = mkSandbox([
41
+ 'slug: foo',
42
+ 'status: OPEN',
43
+ 'created: 2026-04-01',
44
+ 'last_resumed: 2026-04-01',
45
+ ], '# Thread: foo\n');
46
+ try {
47
+ const cap = captureStdout();
48
+ const rc = mod.run([p, '--today', '2026-04-22'], { cwd: dir, stdout: cap.stream });
49
+ assert.equal(rc, 0);
50
+ const written = fs.readFileSync(p, 'utf-8');
51
+ assert.match(written, /status: IN_PROGRESS/);
52
+ assert.match(written, /last_resumed: 2026-04-22/);
53
+ const out = JSON.parse(cap.read());
54
+ assert.equal(out.status, 'IN_PROGRESS');
55
+ } finally {
56
+ fs.rmSync(dir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test('TR-5: RESOLVED stays RESOLVED across resume', () => {
61
+ const { dir, p } = mkSandbox([
62
+ 'slug: bar',
63
+ 'status: RESOLVED',
64
+ 'created: 2026-04-01',
65
+ 'last_resumed: 2026-04-10',
66
+ ], '# body\n');
67
+ try {
68
+ const cap = captureStdout();
69
+ mod.run([p, '--today', '2026-04-22'], { cwd: dir, stdout: cap.stream });
70
+ const written = fs.readFileSync(p, 'utf-8');
71
+ assert.match(written, /status: RESOLVED/);
72
+ assert.match(written, /last_resumed: 2026-04-22/);
73
+ } finally {
74
+ fs.rmSync(dir, { recursive: true, force: true });
75
+ }
76
+ });
77
+
78
+ test('TR-6: stable field ordering (slug, status, created, last_resumed)', () => {
79
+ const { dir, p } = mkSandbox([
80
+ 'last_resumed: 2026-04-01',
81
+ 'created: 2026-04-01',
82
+ 'status: OPEN',
83
+ 'slug: baz',
84
+ ], '# body\n');
85
+ try {
86
+ const cap = captureStdout();
87
+ mod.run([p, '--today', '2026-04-22'], { cwd: dir, stdout: cap.stream });
88
+ const written = fs.readFileSync(p, 'utf-8');
89
+ const fmBlock = written.split('---')[1];
90
+ const idxSlug = fmBlock.indexOf('slug:');
91
+ const idxStatus = fmBlock.indexOf('status:');
92
+ const idxCreated = fmBlock.indexOf('created:');
93
+ const idxLast = fmBlock.indexOf('last_resumed:');
94
+ assert.ok(idxSlug < idxStatus && idxStatus < idxCreated && idxCreated < idxLast);
95
+ } finally {
96
+ fs.rmSync(dir, { recursive: true, force: true });
97
+ }
98
+ });
99
+
100
+ test('TR-7: missing path throws', () => {
101
+ assert.throws(
102
+ () => mod.run([], { cwd: '/tmp', stdout: { write: () => {} } }),
103
+ (err) => err.code === 'thread-resume-missing-path',
104
+ );
105
+ });
106
+
107
+ test('TR-8: nonexistent file throws read-error', () => {
108
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-tr-'));
109
+ try {
110
+ assert.throws(
111
+ () => mod.run([path.join(dir, 'nope.md')], { cwd: dir, stdout: { write: () => {} } }),
112
+ (err) => err.code === 'thread-resume-read-error',
113
+ );
114
+ } finally {
115
+ fs.rmSync(dir, { recursive: true, force: true });
116
+ }
117
+ });
118
+
119
+ test('TR-9: default today is current ISO date', () => {
120
+ const { dir, p } = mkSandbox([
121
+ 'slug: x',
122
+ 'status: OPEN',
123
+ 'created: 2026-04-01',
124
+ 'last_resumed: 2026-04-01',
125
+ ], '');
126
+ try {
127
+ const cap = captureStdout();
128
+ mod.run([p], { cwd: dir, stdout: cap.stream });
129
+ const out = JSON.parse(cap.read());
130
+ assert.match(out.last_resumed, /^\d{4}-\d{2}-\d{2}$/);
131
+ } finally {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ }
134
+ });
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { scan } = require('../../lib/workspace-scan.cjs');
5
+
6
+ function _parseArgs(args) {
7
+ const out = { batchSize: 1000, summary: false };
8
+ for (let i = 0; i < args.length; i++) {
9
+ const a = args[i];
10
+ if (a === '--batch-size' || a === '-b') {
11
+ const n = Number(args[++i]);
12
+ if (!Number.isFinite(n) || n <= 0) {
13
+ throw new NubosPilotError('workspace-scan-invalid-batch-size',
14
+ '--batch-size must be a positive integer', { raw: args[i] });
15
+ }
16
+ out.batchSize = n;
17
+ continue;
18
+ }
19
+ if (a === '--summary' || a === '-s') { out.summary = true; continue; }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function _projectSummary(r) {
25
+ const readmeHead = r.docs && r.docs['README.md']
26
+ ? r.docs['README.md'].content.split('\n').slice(0, 20).join('\n')
27
+ : null;
28
+ return {
29
+ file_count: r.stats.file_count,
30
+ langs: r.language_distribution,
31
+ manifests: Object.keys(r.manifests),
32
+ docs: Object.keys(r.docs),
33
+ readme_head: readmeHead,
34
+ git: r.git,
35
+ };
36
+ }
37
+
38
+ function run(args, opts) {
39
+ const o = opts || {};
40
+ const cwd = o.cwd || process.cwd();
41
+ const stdout = o.stdout || process.stdout;
42
+ const parsed = _parseArgs(args || []);
43
+ const result = scan({ cwd, batchSize: parsed.batchSize });
44
+ const payload = parsed.summary ? _projectSummary(result) : result;
45
+ stdout.write(JSON.stringify(payload));
46
+ return 0;
47
+ }
48
+
49
+ module.exports = { run, _parseArgs, _projectSummary };
@@ -0,0 +1,77 @@
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 mod = require('./workspace-scan.cjs');
10
+
11
+ function mkSandbox() {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ws-'));
13
+ fs.writeFileSync(path.join(dir, 'README.md'), '# Test\n\nLine 1\nLine 2\n');
14
+ fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("hi");\n');
15
+ fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"x"}\n');
16
+ return dir;
17
+ }
18
+
19
+ function captureStdout() {
20
+ const chunks = [];
21
+ return {
22
+ stream: { write: (c) => { chunks.push(c); } },
23
+ read: () => chunks.join(''),
24
+ };
25
+ }
26
+
27
+ test('WS-1: default run emits full scan result JSON', () => {
28
+ const dir = mkSandbox();
29
+ try {
30
+ const cap = captureStdout();
31
+ const rc = mod.run([], { cwd: dir, stdout: cap.stream });
32
+ assert.equal(rc, 0);
33
+ const out = JSON.parse(cap.read());
34
+ assert.ok(out.stats);
35
+ assert.ok(out.language_distribution);
36
+ assert.equal(typeof out.stats.file_count, 'number');
37
+ } finally {
38
+ fs.rmSync(dir, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ test('WS-2: --summary emits the new-project shape', () => {
43
+ const dir = mkSandbox();
44
+ try {
45
+ const cap = captureStdout();
46
+ const rc = mod.run(['--summary'], { cwd: dir, stdout: cap.stream });
47
+ assert.equal(rc, 0);
48
+ const out = JSON.parse(cap.read());
49
+ assert.ok('file_count' in out);
50
+ assert.ok('langs' in out);
51
+ assert.ok('manifests' in out);
52
+ assert.ok('docs' in out);
53
+ assert.ok('readme_head' in out);
54
+ assert.ok('git' in out);
55
+ assert.match(out.readme_head, /^# Test/);
56
+ } finally {
57
+ fs.rmSync(dir, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test('WS-3: --batch-size accepts positive integer', () => {
62
+ assert.deepEqual(mod._parseArgs(['--batch-size', '500']), { batchSize: 500, summary: false });
63
+ });
64
+
65
+ test('WS-4: --batch-size rejects non-positive', () => {
66
+ assert.throws(
67
+ () => mod._parseArgs(['--batch-size', '0']),
68
+ (err) => err.code === 'workspace-scan-invalid-batch-size',
69
+ );
70
+ });
71
+
72
+ test('WS-5: --batch-size rejects NaN', () => {
73
+ assert.throws(
74
+ () => mod._parseArgs(['--batch-size', 'ten']),
75
+ (err) => err.code === 'workspace-scan-invalid-batch-size',
76
+ );
77
+ });
@@ -1,8 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ const DEFAULT_RESEARCH_TOOLS = Object.freeze({
4
+ WebFetch: true,
5
+ Context7: true,
6
+ });
7
+
3
8
  const DEFAULT_WORKFLOW = Object.freeze({
4
9
  commit_docs: true,
5
10
  commit_artifacts: true,
11
+ research_tools: DEFAULT_RESEARCH_TOOLS,
6
12
  });
7
13
 
8
14
  const DEFAULT_AGENTS = Object.freeze({
@@ -25,13 +31,14 @@ function buildInstallConfig(answers) {
25
31
  mcp: !!a.mcp,
26
32
  model_profile: a.model_profile || DEFAULT_MODEL_PROFILE,
27
33
  response_language: a.response_language || DEFAULT_RESPONSE_LANGUAGE,
28
- workflow: { ...DEFAULT_WORKFLOW },
34
+ workflow: { ...DEFAULT_WORKFLOW, research_tools: { ...DEFAULT_RESEARCH_TOOLS } },
29
35
  agents: { ...DEFAULT_AGENTS },
30
36
  };
31
37
  }
32
38
 
33
39
  module.exports = {
34
40
  DEFAULT_WORKFLOW,
41
+ DEFAULT_RESEARCH_TOOLS,
35
42
  DEFAULT_AGENTS,
36
43
  DEFAULT_MODEL_PROFILE,
37
44
  DEFAULT_SCOPE,
@@ -11,21 +11,11 @@ const {
11
11
  const GENERATED_HEADER = '<!-- Generated from roadmap.yaml — do not edit by hand -->';
12
12
 
13
13
  function _yamlPath(cwd) {
14
- try {
15
- return path.join(projectStateDir(cwd), 'roadmap.yaml');
16
- } catch (err) {
17
- if (!err || err.code !== 'not-in-project') throw err;
18
- return path.join(path.resolve(cwd), '.planning', 'roadmap.yaml');
19
- }
14
+ return path.join(projectStateDir(cwd), 'roadmap.yaml');
20
15
  }
21
16
 
22
17
  function _mdPath(cwd) {
23
- try {
24
- return path.join(projectStateDir(cwd), 'ROADMAP.md');
25
- } catch (err) {
26
- if (!err || err.code !== 'not-in-project') throw err;
27
- return path.join(path.resolve(cwd), '.planning', 'ROADMAP.md');
28
- }
18
+ return path.join(projectStateDir(cwd), 'ROADMAP.md');
29
19
  }
30
20
 
31
21
  function _readYaml(p) {
package/lib/roadmap.cjs CHANGED
@@ -10,12 +10,7 @@ const {
10
10
  const { renderMarkdown } = require('./roadmap-render.cjs');
11
11
 
12
12
  function roadmapPath(cwd) {
13
- try {
14
- return path.join(projectStateDir(cwd), 'roadmap.yaml');
15
- } catch (err) {
16
- if (!err || err.code !== 'not-in-project') throw err;
17
- return path.join(path.resolve(cwd), '.planning', 'roadmap.yaml');
18
- }
13
+ return path.join(projectStateDir(cwd), 'roadmap.yaml');
19
14
  }
20
15
 
21
16
  function _readRaw(p) {
@@ -159,12 +154,7 @@ const _MAX_ROADMAP_BYTES = 1024 * 1024;
159
154
  const _SLUG_RE = /^[a-z0-9-]+$/;
160
155
 
161
156
  function _mdPath(cwd) {
162
- try {
163
- return path.join(projectStateDir(cwd), 'ROADMAP.md');
164
- } catch (err) {
165
- if (!err || err.code !== 'not-in-project') throw err;
166
- return path.join(path.resolve(cwd), '.planning', 'ROADMAP.md');
167
- }
157
+ return path.join(projectStateDir(cwd), 'ROADMAP.md');
168
158
  }
169
159
 
170
160
  function _mutate(cwd, fn) {
package/np-tools.cjs CHANGED
@@ -51,6 +51,14 @@ const topLevelCommands = {
51
51
  'detect-runtime': require('./bin/np-tools/detect-runtime.cjs'),
52
52
  'template-path': require('./bin/np-tools/template-path.cjs'),
53
53
  'update-phase-meta': require('./bin/np-tools/update-phase-meta.cjs'),
54
+ 'phase-meta': require('./bin/np-tools/phase-meta.cjs'),
55
+ 'state-dir': require('./bin/np-tools/state-dir.cjs'),
56
+ 'render-template': require('./bin/np-tools/render-template.cjs'),
57
+ 'thread-resume': require('./bin/np-tools/thread-resume.cjs'),
58
+ 'state-incr': require('./bin/np-tools/state-incr.cjs'),
59
+ 'session-aggregate': require('./bin/np-tools/session-aggregate.cjs'),
60
+ 'session-pointer-write': require('./bin/np-tools/session-pointer-write.cjs'),
61
+ 'workspace-scan': require('./bin/np-tools/workspace-scan.cjs'),
54
62
  };
55
63
 
56
64
  const THRESHOLD = 16 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -7,5 +7,3 @@ Subdirectories (populated in Phase 8 and onwards):
7
7
  - `agents/` — agent `.md` files
8
8
  - `hooks/` — hook scripts
9
9
  - `templates/` — scaffolding templates
10
-
11
- See `.planning/phases/07-install-npx-flow/07-RESEARCH.md` §Recommended Project Structure for the full layout.
@@ -131,7 +131,7 @@ reads of the project state directory from this workflow would bypass
131
131
  the lock and are explicitly forbidden by the check-workflows lint.
132
132
 
133
133
  ```bash
134
- node -e "require('./lib/state.cjs').mutateState(function (doc) { doc.frontmatter.pending_todos = (doc.frontmatter.pending_todos || 0) + 1; return doc; });"
134
+ node .nubos-pilot/bin/np-tools.cjs state-incr pending_todos > /dev/null
135
135
  ```
136
136
 
137
137
  The mutator increments the `pending_todos` counter on the STATE.md
@@ -340,14 +340,7 @@ CONTEXT_PATH=$(echo "$INIT" | node -e 'let d="";process.stdin.on("data",c=>d+=c)
340
340
  mkdir -p "$MILESTONE_DIR"
341
341
  mkdir -p "$MILESTONE_DIR/slices"
342
342
 
343
- TPL_PATH=$(node .nubos-pilot/bin/np-tools.cjs template-path milestone/CONTEXT)
344
- node -e '
345
- const { render } = require("./lib/template.cjs");
346
- const fs = require("node:fs");
347
- const tpl = fs.readFileSync(process.argv[1], "utf-8");
348
- const vars = JSON.parse(process.argv[2]);
349
- process.stdout.write(render(tpl, vars));
350
- ' "$TPL_PATH" "$VARS_JSON" > "$CONTEXT_PATH"
343
+ node .nubos-pilot/bin/np-tools.cjs render-template milestone/CONTEXT --vars "$VARS_JSON" > "$CONTEXT_PATH"
351
344
  ```
352
345
 
353
346
  `$VARS_JSON` is the JSON-serialised accumulator from Steps 2–5 (keys map to
@@ -379,13 +372,8 @@ SC_START=$(node .nubos-pilot/bin/np-tools.cjs metrics start-timestamp)
379
372
  SC_MODEL=$(node .nubos-pilot/bin/np-tools.cjs resolve-model np-sc-extractor --profile balanced)
380
373
 
381
374
  REQS_PATH=".nubos-pilot/REQUIREMENTS.md"
382
- [[ -f "$REQS_PATH" ]] || REQS_PATH=".planning/REQUIREMENTS.md"
383
375
 
384
- EXISTING_SC_JSON=$(node -e '
385
- const r = require("./lib/roadmap.cjs");
386
- const p = r.getPhase(process.argv[1]);
387
- process.stdout.write(JSON.stringify(p.success_criteria || []));
388
- ' "$PHASE")
376
+ EXISTING_SC_JSON=$(node .nubos-pilot/bin/np-tools.cjs phase-meta "$PHASE" --field success_criteria)
389
377
 
390
378
  # Spawn agent=np-sc-extractor tier=haiku model=$SC_MODEL milestone=$PHASE
391
379
  # input: milestone=$PHASE, milestone_id=$MILESTONE_ID, milestone_dir=$MILESTONE_DIR,
@@ -406,11 +394,7 @@ node .nubos-pilot/bin/np-tools.cjs metrics record \
406
394
  After the spawn, sanity-check that `success_criteria` is non-empty:
407
395
 
408
396
  ```bash
409
- SC_COUNT=$(node -e '
410
- const r = require("./lib/roadmap.cjs");
411
- const p = r.getPhase(process.argv[1]);
412
- process.stdout.write(String((p.success_criteria || []).length));
413
- ' "$PHASE")
397
+ SC_COUNT=$(node .nubos-pilot/bin/np-tools.cjs phase-meta "$PHASE" --field success_criteria --length)
414
398
  if [[ "$SC_COUNT" -lt 1 ]]; then
415
399
  echo "ERROR: np-sc-extractor produced no success_criteria for $MILESTONE_ID — refusing to continue." >&2
416
400
  exit 1
@@ -75,20 +75,7 @@ still has `_TBD` placeholders.
75
75
  Probe the workspace for context before asking anything:
76
76
 
77
77
  ```bash
78
- SCAN=$(node -e '
79
- const { scan } = require("./lib/workspace-scan.cjs");
80
- const r = scan({ cwd: process.cwd(), batchSize: 1000 });
81
- process.stdout.write(JSON.stringify({
82
- file_count: r.stats.file_count,
83
- langs: r.language_distribution,
84
- manifests: Object.keys(r.manifests),
85
- docs: Object.keys(r.docs),
86
- readme_head: r.docs["README.md"]
87
- ? r.docs["README.md"].content.split("\\n").slice(0, 20).join("\\n")
88
- : null,
89
- git: r.git,
90
- }));
91
- ')
78
+ SCAN=$(node .nubos-pilot/bin/np-tools.cjs workspace-scan --summary --batch-size 1000)
92
79
  ```
93
80
 
94
81
  Show findings to the user and offer pre-filled suggestions:
package/workflows/note.md CHANGED
@@ -74,7 +74,7 @@ sidesteps that resolver and hardcodes `HOME + /.nubos-pilot/notes`.
74
74
  if [[ "$SCOPE" == "global" ]]; then
75
75
  NOTES_DIR="$HOME/.nubos-pilot/notes"
76
76
  else
77
- NOTES_DIR=$(node -e "console.log(require('./lib/core.cjs').projectStateDir(process.cwd()) + '/notes')")
77
+ NOTES_DIR=$(node .nubos-pilot/bin/np-tools.cjs state-dir --subdir notes)
78
78
  fi
79
79
  mkdir -p "$NOTES_DIR"
80
80
  DATE=$(date +%Y-%m-%d)
@@ -40,7 +40,7 @@ for arg in "$@"; do
40
40
  esac
41
41
  done
42
42
 
43
- STATE_DIR=$(node -e "console.log(require('./lib/core.cjs').projectStateDir(process.cwd()))")
43
+ STATE_DIR=$(node .nubos-pilot/bin/np-tools.cjs state-dir)
44
44
  REPORTS_DIR="${STATE_DIR}/reports"
45
45
  POINTER="${REPORTS_DIR}/.last-session"
46
46
  mkdir -p "$REPORTS_DIR"
@@ -80,21 +80,11 @@ between "read pointer" and "write new pointer" (T-10-06-02 / Pitfall
80
80
  longer hit `lock-timeout` from `lib/core.cjs.NubosPilotError`.
81
81
 
82
82
  ```bash
83
- REPORT_JSON=$(node -e '
84
- const fs = require("node:fs");
85
- const { withFileLock } = require("./lib/core.cjs");
86
- const { aggregateSession } = require("./lib/metrics-aggregate.cjs");
87
- const pointer = process.argv[1];
88
- const override = process.argv[2] || "";
89
- const done = withFileLock(pointer, async () => {
90
- let since = override || "";
91
- if (!override && fs.existsSync(pointer)) {
92
- since = fs.readFileSync(pointer, "utf-8").trim();
93
- }
94
- return aggregateSession(since || null, { cwd: process.cwd() });
95
- }, { timeoutMs: 10000 });
96
- Promise.resolve(done).then((r) => process.stdout.write(JSON.stringify(r)));
97
- ' "$POINTER" "$SINCE_OVERRIDE")
83
+ if [[ -n "$SINCE_OVERRIDE" ]]; then
84
+ REPORT_JSON=$(node .nubos-pilot/bin/np-tools.cjs session-aggregate --since "$SINCE_OVERRIDE")
85
+ else
86
+ REPORT_JSON=$(node .nubos-pilot/bin/np-tools.cjs session-aggregate)
87
+ fi
98
88
  ```
99
89
 
100
90
  The `aggregateSession` helper returns
@@ -150,14 +140,7 @@ and "update pointer" leaves the pointer STALE — the next run
150
140
  re-covers the missing period (safe-by-default).
151
141
 
152
142
  ```bash
153
- node -e '
154
- const { withFileLock, atomicWriteFileSync } = require("./lib/core.cjs");
155
- withFileLock(
156
- process.argv[1],
157
- () => atomicWriteFileSync(process.argv[1], process.argv[2]),
158
- { timeoutMs: 10000 }
159
- );
160
- ' "$POINTER" "$NOW_ISO"
143
+ node .nubos-pilot/bin/np-tools.cjs session-pointer-write "$NOW_ISO" > /dev/null
161
144
  ```
162
145
 
163
146
  Using `atomicWriteFileSync` ensures the pointer update is crash-safe
@@ -35,7 +35,7 @@ if [[ -z "$SLUG" ]]; then
35
35
  echo "Error: argument produced no slug-safe characters." >&2
36
36
  exit 1
37
37
  fi
38
- STATE_DIR=$(node -e "console.log(require('./lib/core.cjs').projectStateDir(process.cwd()))")
38
+ STATE_DIR=$(node .nubos-pilot/bin/np-tools.cjs state-dir)
39
39
  THREADS_DIR="${STATE_DIR}/threads"
40
40
  THREAD_PATH="${THREADS_DIR}/${SLUG}.md"
41
41
  TODAY=$(date +%Y-%m-%d)
@@ -119,35 +119,11 @@ order.
119
119
 
120
120
  ```bash
121
121
  if [[ "$MODE" == "resume" ]]; then
122
- node -e "
123
- const fs = require('node:fs');
124
- const { extractFrontmatter } = require('./lib/frontmatter.cjs');
125
- const { atomicWriteFileSync } = require('./lib/core.cjs');
126
- const p = process.argv[1];
127
- const today = process.argv[2];
128
- const raw = fs.readFileSync(p, 'utf-8');
129
- const { frontmatter, body } = extractFrontmatter(raw);
130
- const next = Object.assign({}, frontmatter);
131
- const cur = String(next.status || 'OPEN');
132
- if (cur === 'OPEN') next.status = 'IN_PROGRESS';
133
- next.last_resumed = today;
134
- const order = ['slug', 'status', 'created', 'last_resumed'];
135
- const seen = new Set();
136
- const lines = ['---'];
137
- for (const k of order) {
138
- if (k in next) { lines.push(k + ': ' + next[k]); seen.add(k); }
139
- }
140
- for (const k of Object.keys(next)) {
141
- if (!seen.has(k)) lines.push(k + ': ' + next[k]);
142
- }
143
- lines.push('---');
144
- const out = lines.join('\n') + '\n' + body;
145
- atomicWriteFileSync(p, out);
146
- " "$THREAD_PATH" "$TODAY"
122
+ node .nubos-pilot/bin/np-tools.cjs thread-resume "$THREAD_PATH" --today "$TODAY" > /dev/null
147
123
  echo "Thread resumed: $THREAD_PATH"
148
124
  echo ""
149
125
  echo "--- thread content ---"
150
- node -e "process.stdout.write(require('node:fs').readFileSync(process.argv[1], 'utf-8'))" "$THREAD_PATH"
126
+ cat "$THREAD_PATH"
151
127
  fi
152
128
  ```
153
129