nubos-pilot 1.0.3 → 1.0.4

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.
@@ -40,6 +40,16 @@ The orchestrator provides these in your prompt context. Read every path it hands
40
40
 
41
41
  ## Workflow
42
42
 
43
+ 0. **Read inbox first (ADR-0015).** You are always running on round ≥ 2; the prior critic may have addressed clarifying questions to the executor that you must answer:
44
+ ```bash
45
+ node np-tools.cjs messages-inbox --agent np-executor --task <task-id>
46
+ ```
47
+ For each `kind=request` with `expects_reply=true`, your patch should resolve the question and you must send a response (otherwise commit-phase is blocked by Layer-B):
48
+ ```bash
49
+ node np-tools.cjs messages-send --from np-executor --to <orig-from> \
50
+ --phase <task-id> --round <round> --kind response \
51
+ --subject <same-subject> --body "<resolution>" --in-reply-to <request-id>
52
+ ```
43
53
  1. **Classify** the failure from the captured output:
44
54
  - `compile` (syntax error, missing import, type error)
45
55
  - `lint` (style/quality rule violation)
@@ -145,6 +145,26 @@ If `<report_path>` is missing from your prompt or you cannot write the file, do
145
145
  - Inline the full findings JSON in the final message. The Verdict-Only Contract exists because that response replays into the orchestrator's context every round and is the dominant token sink — defeating it silently re-introduces the cost ADR-0010 §L5 was designed to remove.
146
146
  </scope_guardrail>
147
147
 
148
+ ## Inter-Agent Messaging (ADR-0015)
149
+
150
+ When a finding requires per-finding clarification from the executor that does NOT belong in the findings JSON itself (e.g. *"did you intend to delete `FoobarService`, or was that a side-effect?"*), you MAY emit an addressed `request` message via:
151
+
152
+ ```bash
153
+ node np-tools.cjs messages-send \
154
+ --from np-critic --to np-executor \
155
+ --phase <task-id> --round <current-round> \
156
+ --kind request --subject <finding-category> \
157
+ --body "<question>" --expects-reply
158
+ ```
159
+
160
+ Rules:
161
+ - Messages are **dialogue**, not findings. The findings JSON written to `<report_path>` is canonical for routing. Messages are for clarifications that bounce back to executor / build-fixer.
162
+ - Each `request` you send with `--expects-reply` blocks the next commit-phase via Layer-B (`pendingReplies(taskId) === 0` precondition). Use sparingly.
163
+ - `request` subjects should match a finding-category (`style`, `dead-code`, `missing-test`, `weak-assertion`, `unmet-criterion`, `scope-creep`).
164
+ - Inspect prior dialogue with `messages-thread <msg-id>` if a finding looks like it bounced last round.
165
+
166
+ If you have nothing to ask, send no message. Default = no dialogue.
167
+
148
168
  ## Stop Conditions
149
169
 
150
170
  Hard-stop (`Write` the full findings JSON to `<report_path>` if possible, then emit the envelope; do NOT attempt recovery):
@@ -96,6 +96,19 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
96
96
  2. **Read codebase docs** — `.nubos-pilot/codebase/INDEX.md` plus every
97
97
  module doc owning a path in `files_modified`. Pre-edit step of the
98
98
  Codebase Docs Protocol.
99
+ 2a. **Read inbox (Round 2+ only)** — when round ≥ 2, check for addressed
100
+ requests from prior critics that need a response before commit:
101
+ ```bash
102
+ node np-tools.cjs messages-inbox --agent np-executor --task <task-id>
103
+ ```
104
+ For each `kind=request` with `expects_reply=true`, your edit should resolve it.
105
+ After the edit, send a response and let it auto-archive the request:
106
+ ```bash
107
+ node np-tools.cjs messages-send --from np-executor --to <orig-from> \
108
+ --phase <task-id> --round <round> --kind response \
109
+ --subject <same-subject> --body "<resolution>" --in-reply-to <request-id>
110
+ ```
111
+ Unanswered `expects_reply=true` requests block commit-phase via Layer-B (ADR-0015).
99
112
  3. **Transition to in-progress:** `node np-tools.cjs checkpoint transition <task-id> in-progress`.
100
113
  4. **Edit files** — only the paths listed in the task's `files_modified` frontmatter. Use `Read` + `Edit` / `Write`. No scope expansion.
101
114
  5. **Transition to verifying:** `node np-tools.cjs checkpoint transition <task-id> verifying`.
@@ -99,6 +99,22 @@ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool t
99
99
  </role>
100
100
 
101
101
  <reality_check_protocol>
102
+ ## Vector-Memory Recall (ADR-0014)
103
+
104
+ Before scaffolding tasks, query the local vector memory for prior-phase decisions matching the current milestone. This is *advisory*: the locked-decisions file (`M<NNN>-CONTEXT.md` / `RESEARCH.md`) is canonical, memory-hits inform context-injection only.
105
+
106
+ ```bash
107
+ node np-tools.cjs memory-query --text "<milestone-summary>" --k 5 --type research
108
+ node np-tools.cjs memory-query --text "<milestone-summary>" --k 5 --type learning
109
+ ```
110
+
111
+ If `memory.enabled=false` you'll get `memory-disabled` — silently skip.
112
+
113
+ For `[VERIFIED]` / `[CITED]` hits whose subject overlaps the current milestone:
114
+ - Reference them in the slice plan's `<context>` block as prior-art.
115
+ - If a prior decision conflicts with a freshly-locked decision in `M<NNN>-CONTEXT.md`, the locked decision wins. Memory is never authoritative against locked-decisions.
116
+ - Do NOT auto-promote `[ASSUMED]` hits; they remain advisory.
117
+
102
118
  ## CRITICAL: Reality-Check Before Planning (MANDATORY)
103
119
 
104
120
  Plans fail at execute-time when they encode assumptions the planner never verified against the actual repo. To stop the replan-after-execute loop, BEFORE writing any `S<NNN>-PLAN.md` you MUST empirically verify every load-bearing assumption and record the evidence inside the slice plan.
@@ -36,6 +36,23 @@ anchor points for your research — do not propose replacements without
36
36
  explicit justification. If `INDEX.md` is absent, report and stop —
37
37
  `np:scan-codebase` must run first.
38
38
 
39
+ ## Vector-Memory Pre-recall (ADR-0014)
40
+
41
+ **Before issuing external research**, query the local vector memory for prior decisions matching the current ticket. The hybrid pre-flight (`lib/knowledge-adapter.cjs`) already runs at swarm-entry; this step is the *agent-side* recall for context-injection into your `RESEARCH.md`.
42
+
43
+ ```bash
44
+ node np-tools.cjs memory-query --text "<ticket-summary>" --k 5 --type research
45
+ node np-tools.cjs memory-query --text "<ticket-summary>" --k 3 --type learning
46
+ ```
47
+
48
+ If `memory.enabled=false` you'll get `memory-disabled` — silently skip; this section is opt-in and additive.
49
+
50
+ For each hit, surface the underlying decision in your `RESEARCH.md` with the original `provenance` preserved:
51
+ - `[VERIFIED]` / `[CITED]` hits become `[CACHED:VERIFIED]` / `[CACHED:CITED]` in your output — the merged `<consensus_meta>` block carries `cache_hit: true`.
52
+ - `[ASSUMED]` hits stay `[CACHED:ASSUMED]` — flagged for plan-checker scrutiny, never auto-promoted.
53
+
54
+ A high-confidence cache match should *replace* external research for that decision, not duplicate it. Save the swarm tokens.
55
+
39
56
  ## Handoff Protocol
40
57
 
41
58
  Agent handoffs are persistent notes between phase invocations. Before researching, check handoffs addressed to `np-researcher`:
@@ -59,6 +59,15 @@ const COMMANDS = [
59
59
  { name: 'handoff-read', category: 'Capture', description: 'Read a single handoff by id (returns frontmatter + body as JSON)', description_de: 'Liest einzelnen Handoff per ID (gibt Frontmatter + Body als JSON zurück)' },
60
60
  { name: 'handoff-list', category: 'Capture', description: 'List handoffs (JSON array); filter with --for AGENT, --milestone M<NNN>, --status STATUS, --global', description_de: 'Listet Handoffs (JSON-Array); filtert mit --for AGENT, --milestone M<NNN>, --status STATUS, --global' },
61
61
  { name: 'handoff-status', category: 'Capture', description: 'Update a handoff status (open|read|acted|archived)', description_de: 'Aktualisiert Handoff-Status (open|read|acted|archived)' },
62
+ { name: 'messages-send', category: 'Capture', description: 'Send addressed inter-agent message (request|response|notify) to .nubos-pilot/messages/inbox/<to>/. ADR-0015', description_de: 'Sendet adressierte Inter-Agent-Nachricht (request|response|notify) an .nubos-pilot/messages/inbox/<to>/. ADR-0015' },
63
+ { name: 'messages-inbox', category: 'Capture', description: 'List ungelesen messages addressed to an agent (filterable by --kind, --since, --task)', description_de: 'Listet ungelesen Messages für einen Agent (filterbar via --kind, --since, --task)' },
64
+ { name: 'messages-archive', category: 'Capture', description: 'Move an inbox message to archive/; refuses request+expects_reply without prior response', description_de: 'Verschiebt Inbox-Message nach archive/; weigert sich bei request+expects_reply ohne Reply' },
65
+ { name: 'messages-thread', category: 'Capture', description: 'Print full reply-chain for a message id (causal order)', description_de: 'Gibt vollständige Reply-Chain für eine Message-ID aus (kausale Reihenfolge)' },
66
+ { name: 'memory-index', category: 'Capture', description: 'Bulk-index records into vector memory (--records JSON or --records-file JSONL). Opt-in via memory.enabled=true. ADR-0014', description_de: 'Bulk-Index für Records ins Vector-Memory (--records JSON oder --records-file JSONL). Opt-in via memory.enabled=true. ADR-0014' },
67
+ { name: 'memory-query', category: 'Utility', description: 'Query vector memory by text; returns top-k JSON hits with score + record. Filter by --type, --phase, --tags', description_de: 'Fragt Vector-Memory by Text; liefert Top-k JSON-Hits mit Score + Record. Filter via --type, --phase, --tags' },
68
+ { name: 'memory-add', category: 'Capture', description: 'Add a single record to vector memory (--type, --title, --body, optional --tags / --provenance / --phase / --id)', description_de: 'Fügt einzelnen Record in Vector-Memory ein (--type, --title, --body, optional --tags / --provenance / --phase / --id)' },
69
+ { name: 'memory-rebuild', category: 'Utility', description: 'Force full re-embed from records.jsonl; required after embedding-model change. ADR-0014', description_de: 'Erzwingt komplettes Re-Embed aus records.jsonl; erforderlich nach Embedding-Model-Wechsel. ADR-0014' },
70
+ { name: 'memory-stats', category: 'Utility', description: 'Print vector-memory stats (count, dim, model, schema_version, created_at)', description_de: 'Gibt Vector-Memory-Stats aus (count, dim, model, schema_version, created_at)' },
62
71
  { 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/', description_de: 'Erstellt isoliertes Git-Worktree für eine Slice (Branch np/<mid>-<sid> vom aktuellen HEAD) unter .nubos-pilot/worktrees/' },
63
72
  { name: 'worktree-remove', category: 'Execution', description: 'Remove a slice worktree + delete its branch (--force / --keep-branch)', description_de: 'Entfernt Slice-Worktree + löscht zugehörigen Branch (--force / --keep-branch)' },
64
73
  { name: 'worktree-list', category: 'Execution', description: 'List all nubos-pilot-managed slice worktrees (np/<mid>-<sid> only) as JSON', description_de: 'Listet alle nubos-pilot-verwalteten Slice-Worktrees (nur np/<mid>-<sid>) als JSON' },
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { readConfigPath } = require('../../lib/config.cjs');
5
+ const { createMemory } = require('../../lib/memory.cjs');
6
+
7
+ function resolveMemory(opts) {
8
+ const o = opts || {};
9
+ const cwd = o.cwd || process.cwd();
10
+
11
+ if (o.provider && o.indexEngine) {
12
+ return createMemory({
13
+ provider: o.provider,
14
+ indexEngine: o.indexEngine,
15
+ cwd,
16
+ alpha: o.alpha,
17
+ });
18
+ }
19
+
20
+ const enabled = readConfigPath(cwd, 'memory.enabled', false);
21
+ if (!enabled) {
22
+ throw new NubosPilotError(
23
+ 'memory-disabled',
24
+ 'memory layer is disabled. Set "memory": { "enabled": true } in .nubos-pilot/config.json (ADR-0014).',
25
+ {},
26
+ );
27
+ }
28
+
29
+ const model = readConfigPath(cwd, 'memory.model', 'Xenova/bge-small-en-v1.5');
30
+ const alpha = readConfigPath(cwd, 'memory.alpha', 0.6);
31
+
32
+ const { createLocalProvider } = require('../../lib/memory-provider-local.cjs');
33
+ const { createUsearchIndex } = require('../../lib/memory-index-usearch.cjs');
34
+
35
+ const provider = createLocalProvider({ model });
36
+ const indexEngine = createUsearchIndex({ dim: provider.dim });
37
+
38
+ return createMemory({ provider, indexEngine, cwd, alpha });
39
+ }
40
+
41
+ module.exports = { resolveMemory };
@@ -670,6 +670,132 @@ test('LCLI-RR-11: phase=commit --force-commit-phase bypasses preconditions and s
670
670
  assert.equal(cp.nubosloop.forced_commit_phase, true);
671
671
  });
672
672
 
673
+ test('LCLI-RR-MSG-1: phase=commit refused while pendingReplies > 0 (ADR-0015)', () => {
674
+ const r = _mkRoot();
675
+ checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
676
+ const nubosloop = require('../../lib/nubosloop.cjs');
677
+ nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
678
+ checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
679
+ nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
680
+ verify_exit_code: 0,
681
+ findings: [],
682
+ }),
683
+ }), r);
684
+ const messaging = require('../../lib/messaging.cjs');
685
+ messaging.send({
686
+ from: 'np-critic', to: 'np-executor',
687
+ phase: 'M001-S001-T0001', kind: 'request',
688
+ subject: 'fix-x', body: 'please fix', expects_reply: true,
689
+ }, r);
690
+
691
+ const loopRunRound = require('./loop-run-round.cjs');
692
+ assert.throws(
693
+ () => loopRunRound.run(['M001-S001-T0001', '--phase', 'commit'], { cwd: r, stdout: _cap().stub }),
694
+ (err) => err && err.code === 'loop-commit-precondition-missing'
695
+ && err.details && err.details.missing === 'pending-replies-cleared'
696
+ && err.details.observed_pending_replies === 1,
697
+ );
698
+ });
699
+
700
+ test('LCLI-RR-MSG-2: phase=commit succeeds and sweeps task messages once replies are archived', () => {
701
+ const r = _mkRoot();
702
+ checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
703
+ const nubosloop = require('../../lib/nubosloop.cjs');
704
+ nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
705
+ checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
706
+ nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
707
+ verify_exit_code: 0,
708
+ findings: [],
709
+ }),
710
+ }), r);
711
+ const messaging = require('../../lib/messaging.cjs');
712
+ const req = messaging.send({
713
+ from: 'np-critic', to: 'np-executor',
714
+ phase: 'M001-S001-T0001', kind: 'request',
715
+ subject: 'fix-x', body: 'please fix', expects_reply: true,
716
+ }, r);
717
+ messaging.send({
718
+ from: 'np-executor', to: 'np-critic',
719
+ phase: 'M001-S001-T0001', kind: 'response',
720
+ subject: 'fix-x', body: 'done', in_reply_to: req.id,
721
+ }, r);
722
+ messaging.archive(req.id, r);
723
+
724
+ const cap = _cap();
725
+ const loopRunRound = require('./loop-run-round.cjs');
726
+ loopRunRound.run(['M001-S001-T0001', '--phase', 'commit'], { cwd: r, stdout: cap.stub });
727
+ const out = JSON.parse(cap.get());
728
+ assert.equal(out.next_action, 'commit-task');
729
+ assert.equal(out.messages_swept, 2);
730
+
731
+ const inboxLeft = messaging.inbox('np-critic', { phase: 'M001-S001-T0001' }, r);
732
+ assert.equal(inboxLeft.length, 0);
733
+ const archived = path.join(r, '.nubos-pilot', 'messages', 'archive', 'by-task', 'M001-S001-T0001');
734
+ assert.ok(fs.existsSync(archived));
735
+ });
736
+
737
+ test('LCLI-RR-MEM-1: phase=commit indexes the just-logged learning into memory when memory.enabled=true', () => {
738
+ const r = _mkRoot();
739
+ fs.writeFileSync(
740
+ path.join(r, '.nubos-pilot', 'config.json'),
741
+ JSON.stringify({ memory: { enabled: true, model: 'mock-v1', alpha: 0.6 } }),
742
+ 'utf-8',
743
+ );
744
+ checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
745
+ const nubosloop = require('../../lib/nubosloop.cjs');
746
+ nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
747
+ checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
748
+ nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
749
+ verify_exit_code: 0,
750
+ findings: [],
751
+ }),
752
+ }), r);
753
+
754
+ const cap = _cap();
755
+ const loopRunRound = require('./loop-run-round.cjs');
756
+ loopRunRound.run(
757
+ ['M001-S001-T0001', '--phase', 'commit',
758
+ '--learning-pattern', 'use jose for jwt',
759
+ '--learning-outcome', 'verified'],
760
+ { cwd: r, stdout: cap.stub },
761
+ );
762
+ const out = JSON.parse(cap.get());
763
+ assert.equal(out.next_action, 'commit-task');
764
+ assert.ok(
765
+ out.memory_skip_reason === 'memory-transformers-not-installed'
766
+ || out.memory_skip_reason === 'memory-usearch-not-installed'
767
+ || out.memory_added !== null,
768
+ 'memory write-back should either succeed or skip with a known dep-missing code; got: '
769
+ + JSON.stringify({ memory_added: out.memory_added, memory_skip_reason: out.memory_skip_reason }),
770
+ );
771
+ });
772
+
773
+ test('LCLI-RR-MEM-2: phase=commit reports memory_skip_reason=memory-disabled when feature off', () => {
774
+ const r = _mkRoot();
775
+ checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
776
+ const nubosloop = require('../../lib/nubosloop.cjs');
777
+ nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
778
+ checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
779
+ nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
780
+ verify_exit_code: 0,
781
+ findings: [],
782
+ }),
783
+ }), r);
784
+
785
+ const cap = _cap();
786
+ const loopRunRound = require('./loop-run-round.cjs');
787
+ loopRunRound.run(
788
+ ['M001-S001-T0001', '--phase', 'commit',
789
+ '--learning-pattern', 'use jose for jwt',
790
+ '--learning-outcome', 'verified'],
791
+ { cwd: r, stdout: cap.stub },
792
+ );
793
+ const out = JSON.parse(cap.get());
794
+ assert.equal(out.memory_added, null);
795
+ assert.equal(out.memory_skip_reason, 'memory-disabled');
796
+ assert.equal(out.messages_swept, 0);
797
+ });
798
+
673
799
  // Layer C — audit-trail evidence enforcement -------------------------------
674
800
 
675
801
  test('LCLI-RR-12: post-executor refuses without np-executor audit (R1)', () => {
@@ -17,6 +17,8 @@ const path = require('node:path');
17
17
  const { NubosPilotError, safeAssign } = require('../../lib/core.cjs');
18
18
  const checkpoint = require('../../lib/checkpoint.cjs');
19
19
  const nubosloop = require('../../lib/nubosloop.cjs');
20
+ const messaging = require('../../lib/messaging.cjs');
21
+ const config = require('../../lib/config.cjs');
20
22
  const args = require('./_args.cjs');
21
23
 
22
24
  const TASK_ID_RE = checkpoint.TASK_ID_RE;
@@ -475,6 +477,23 @@ function _runCommit(taskId, list, cwd) {
475
477
  { taskId, missing: 'findings=[]', observed_findings_count: np.findings.length },
476
478
  );
477
479
  }
480
+ let pendingReplies;
481
+ try { pendingReplies = messaging.pendingReplies(taskId, cwd); }
482
+ catch { pendingReplies = []; }
483
+ if (pendingReplies.length > 0) {
484
+ throw new NubosPilotError(
485
+ 'loop-commit-precondition-missing',
486
+ 'phase=commit refused: ' + pendingReplies.length +
487
+ ' inter-agent message(s) with expects_reply=true are unarchived for ' + taskId +
488
+ '. Each request needs a response (kind=response, --in-reply-to <id>) before commit. ADR-0015.',
489
+ {
490
+ taskId,
491
+ missing: 'pending-replies-cleared',
492
+ observed_pending_replies: pendingReplies.length,
493
+ pending_subjects: pendingReplies.map((m) => m.subject),
494
+ },
495
+ );
496
+ }
478
497
  }
479
498
  const pattern = args.getFlag(list, '--learning-pattern') || null;
480
499
  const outcome = args.getFlag(list, '--learning-outcome') || 'verified';
@@ -498,6 +517,39 @@ function _runCommit(taskId, list, cwd) {
498
517
  logged = { error: err && err.code ? err.code : 'auto-log-learning-failed' };
499
518
  }
500
519
  }
520
+
521
+ let memoryAdded = null;
522
+ let memorySkipReason = null;
523
+ if (config.readConfigPath(cwd, 'memory.enabled', false)) {
524
+ if (logged && !logged.error && pattern) {
525
+ try {
526
+ const { resolveMemory } = require('./_memory-resolve.cjs');
527
+ const memory = resolveMemory({ cwd });
528
+ memoryAdded = memory.add({
529
+ type: 'learning',
530
+ phase: taskId,
531
+ title: pattern,
532
+ body: typeof logged.entry === 'object' && logged.entry ? JSON.stringify(logged.entry) : String(pattern),
533
+ provenance: outcome === 'verified' ? 'VERIFIED' : null,
534
+ });
535
+ if (memoryAdded && typeof memoryAdded.then === 'function') {
536
+ memoryAdded.catch(() => {});
537
+ memoryAdded = { pending: true };
538
+ }
539
+ } catch (err) {
540
+ memorySkipReason = err && err.code ? err.code : 'memory-add-failed';
541
+ }
542
+ } else {
543
+ memorySkipReason = 'no-pattern-to-index';
544
+ }
545
+ } else {
546
+ memorySkipReason = 'memory-disabled';
547
+ }
548
+
549
+ let messagesSwept = 0;
550
+ try { messagesSwept = messaging.sweepTaskOnCommit(taskId, cwd); }
551
+ catch { messagesSwept = 0; }
552
+
501
553
  checkpoint.mergeCheckpoint(
502
554
  taskId,
503
555
  (cur) => ({
@@ -521,6 +573,9 @@ function _runCommit(taskId, list, cwd) {
521
573
  next_action: 'commit-task',
522
574
  learning_logged: logged,
523
575
  learning_skip_reason: skipReason,
576
+ memory_added: memoryAdded,
577
+ memory_skip_reason: memorySkipReason,
578
+ messages_swept: messagesSwept,
524
579
  forced: force,
525
580
  };
526
581
  }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const { NubosPilotError } = require('../../lib/core.cjs');
5
+ const { resolveMemory } = require('./_memory-resolve.cjs');
6
+
7
+ function _parseArgs(args) {
8
+ const out = {
9
+ type: null, phase: null, title: null, body: null, bodyFile: null,
10
+ tags: null, provenance: null, id: null,
11
+ };
12
+ for (let i = 0; i < args.length; i++) {
13
+ const a = args[i];
14
+ if (a === '--type') { out.type = args[++i] || null; continue; }
15
+ if (a === '--phase') { out.phase = args[++i] || null; continue; }
16
+ if (a === '--title') { out.title = args[++i] || null; continue; }
17
+ if (a === '--body') { out.body = args[++i] || null; continue; }
18
+ if (a === '--body-file') { out.bodyFile = args[++i] || null; continue; }
19
+ if (a === '--tags') { out.tags = (args[++i] || '').split(',').filter(Boolean); continue; }
20
+ if (a === '--provenance') { out.provenance = args[++i] || null; continue; }
21
+ if (a === '--id') { out.id = args[++i] || null; continue; }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ async function run(args, opts) {
27
+ const o = opts || {};
28
+ const stdout = o.stdout || process.stdout;
29
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
30
+
31
+ let body = parsed.body || '';
32
+ if (parsed.bodyFile) {
33
+ try { body = fs.readFileSync(parsed.bodyFile, 'utf-8'); }
34
+ catch (err) {
35
+ throw new NubosPilotError(
36
+ 'memory-body-file-read-failed',
37
+ 'failed to read --body-file: ' + (err && err.message),
38
+ { path: parsed.bodyFile },
39
+ );
40
+ }
41
+ }
42
+
43
+ const memory = resolveMemory(o);
44
+ const result = await memory.add({
45
+ id: parsed.id || undefined,
46
+ type: parsed.type,
47
+ phase: parsed.phase,
48
+ title: parsed.title,
49
+ body,
50
+ tags: parsed.tags || undefined,
51
+ provenance: parsed.provenance || undefined,
52
+ });
53
+ stdout.write(JSON.stringify(result) + '\n');
54
+ return 0;
55
+ }
56
+
57
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const { NubosPilotError } = require('../../lib/core.cjs');
5
+ const { resolveMemory } = require('./_memory-resolve.cjs');
6
+
7
+ function _parseArgs(args) {
8
+ const out = { recordsFile: null, recordsJson: null };
9
+ for (let i = 0; i < args.length; i++) {
10
+ const a = args[i];
11
+ if (a === '--records-file') { out.recordsFile = args[++i] || null; continue; }
12
+ if (a === '--records') { out.recordsJson = args[++i] || null; continue; }
13
+ }
14
+ return out;
15
+ }
16
+
17
+ function _readRecords(parsed) {
18
+ if (parsed.recordsFile) {
19
+ let raw;
20
+ try { raw = fs.readFileSync(parsed.recordsFile, 'utf-8'); }
21
+ catch (err) {
22
+ throw new NubosPilotError(
23
+ 'memory-records-file-read-failed',
24
+ 'failed to read --records-file: ' + (err && err.message),
25
+ { path: parsed.recordsFile },
26
+ );
27
+ }
28
+ try { return JSON.parse(raw); }
29
+ catch {
30
+ const lines = raw.split('\n').filter((l) => l.trim());
31
+ const out = [];
32
+ for (const l of lines) {
33
+ try { out.push(JSON.parse(l)); }
34
+ catch (err) {
35
+ throw new NubosPilotError(
36
+ 'memory-records-file-invalid-json',
37
+ 'records file is neither JSON array nor JSONL',
38
+ { line: l.slice(0, 80) },
39
+ );
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ }
45
+ if (parsed.recordsJson) {
46
+ try { return JSON.parse(parsed.recordsJson); }
47
+ catch (err) {
48
+ throw new NubosPilotError(
49
+ 'memory-records-arg-invalid-json',
50
+ '--records is not valid JSON',
51
+ { error: err && err.message },
52
+ );
53
+ }
54
+ }
55
+ throw new NubosPilotError(
56
+ 'memory-index-missing-records',
57
+ 'either --records <json-array> or --records-file <path> required',
58
+ {},
59
+ );
60
+ }
61
+
62
+ async function run(args, opts) {
63
+ const o = opts || {};
64
+ const stdout = o.stdout || process.stdout;
65
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
66
+ const records = _readRecords(parsed);
67
+ if (!Array.isArray(records)) {
68
+ throw new NubosPilotError('memory-records-not-array', 'records must be a JSON array', {});
69
+ }
70
+
71
+ const memory = resolveMemory(o);
72
+ const result = await memory.index(records);
73
+ stdout.write(JSON.stringify(result) + '\n');
74
+ return 0;
75
+ }
76
+
77
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { resolveMemory } = require('./_memory-resolve.cjs');
5
+
6
+ function _parseArgs(args) {
7
+ const out = { text: null, k: 8, type: null, phase: null, tags: null };
8
+ for (let i = 0; i < args.length; i++) {
9
+ const a = args[i];
10
+ if (a === '--text') { out.text = args[++i] || null; continue; }
11
+ if (a === '--k') { out.k = Number(args[++i]) || 8; continue; }
12
+ if (a === '--type') { out.type = args[++i] || null; continue; }
13
+ if (a === '--phase') { out.phase = args[++i] || null; continue; }
14
+ if (a === '--tags') { out.tags = (args[++i] || '').split(',').filter(Boolean); continue; }
15
+ if (!a.startsWith('--') && !out.text) { out.text = a; continue; }
16
+ }
17
+ return out;
18
+ }
19
+
20
+ async function run(args, opts) {
21
+ const o = opts || {};
22
+ const stdout = o.stdout || process.stdout;
23
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
24
+ if (!parsed.text) {
25
+ throw new NubosPilotError('memory-query-missing-text', 'query text required (positional or --text)', {});
26
+ }
27
+
28
+ const filter = {};
29
+ if (parsed.type) filter.type = parsed.type;
30
+ if (parsed.phase) filter.phase = parsed.phase;
31
+ if (Array.isArray(parsed.tags) && parsed.tags.length > 0) filter.tags = parsed.tags;
32
+
33
+ const memory = resolveMemory(o);
34
+ const hits = await memory.query(parsed.text, { k: parsed.k, filter });
35
+ stdout.write(JSON.stringify(hits) + '\n');
36
+ return 0;
37
+ }
38
+
39
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { resolveMemory } = require('./_memory-resolve.cjs');
4
+
5
+ function _parseArgs() { return {}; }
6
+
7
+ async function run(args, opts) {
8
+ const o = opts || {};
9
+ const stdout = o.stdout || process.stdout;
10
+ const memory = resolveMemory(o);
11
+ const result = await memory.rebuild();
12
+ stdout.write(JSON.stringify(result) + '\n');
13
+ return 0;
14
+ }
15
+
16
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const { resolveMemory } = require('./_memory-resolve.cjs');
4
+
5
+ function _parseArgs() { return {}; }
6
+
7
+ function run(args, opts) {
8
+ const o = opts || {};
9
+ const stdout = o.stdout || process.stdout;
10
+ const memory = resolveMemory(o);
11
+ stdout.write(JSON.stringify(memory.stats()) + '\n');
12
+ return 0;
13
+ }
14
+
15
+ module.exports = { run, _parseArgs };