nubos-pilot 1.0.2 → 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.
- package/agents/np-build-fixer.md +10 -0
- package/agents/np-critic.md +20 -0
- package/agents/np-executor.md +13 -0
- package/agents/np-planner.md +16 -0
- package/agents/np-researcher.md +17 -0
- package/bin/np-tools/_commands.cjs +9 -0
- package/bin/np-tools/_memory-resolve.cjs +41 -0
- package/bin/np-tools/loop-commands.test.cjs +126 -0
- package/bin/np-tools/loop-run-round.cjs +55 -0
- package/bin/np-tools/memory-add.cjs +57 -0
- package/bin/np-tools/memory-index.cjs +77 -0
- package/bin/np-tools/memory-query.cjs +39 -0
- package/bin/np-tools/memory-rebuild.cjs +16 -0
- package/bin/np-tools/memory-stats.cjs +15 -0
- package/bin/np-tools/memory.test.cjs +241 -0
- package/bin/np-tools/messages-archive.cjs +35 -0
- package/bin/np-tools/messages-inbox.cjs +34 -0
- package/bin/np-tools/messages-send.cjs +77 -0
- package/bin/np-tools/messages-thread.cjs +35 -0
- package/bin/np-tools/messages.test.cjs +183 -0
- package/docs/adr/0014-vector-memory-layer.md +174 -0
- package/docs/adr/0015-named-agent-messaging.md +162 -0
- package/docs/adr/README.md +2 -0
- package/lib/knowledge-adapter.cjs +61 -4
- package/lib/knowledge-adapter.test.cjs +69 -0
- package/lib/memory-index-usearch.cjs +111 -0
- package/lib/memory-provider-local.cjs +60 -0
- package/lib/memory.cjs +312 -0
- package/lib/memory.test.cjs +323 -0
- package/lib/messaging.cjs +403 -0
- package/lib/messaging.test.cjs +309 -0
- package/lib/researcher-swarm.cjs +21 -5
- package/np-tools.cjs +9 -0
- package/package.json +5 -1
package/agents/np-build-fixer.md
CHANGED
|
@@ -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)
|
package/agents/np-critic.md
CHANGED
|
@@ -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):
|
package/agents/np-executor.md
CHANGED
|
@@ -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`.
|
package/agents/np-planner.md
CHANGED
|
@@ -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.
|
package/agents/np-researcher.md
CHANGED
|
@@ -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 };
|