nubos-pilot 1.2.2 → 1.2.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/CHANGELOG.md +18 -0
- package/README.md +16 -0
- package/agents/np-architect.md +2 -0
- package/agents/np-executor.md +1 -1
- package/agents/np-learnings-extractor.md +54 -0
- package/agents/np-planner.md +1 -1
- package/agents/np-security-reviewer.md +9 -0
- package/bin/np-tools/_commands.cjs +4 -0
- package/bin/np-tools/derive-tier.cjs +86 -0
- package/bin/np-tools/derive-tier.test.cjs +83 -0
- package/bin/np-tools/learnings.cjs +109 -0
- package/bin/np-tools/learnings.test.cjs +66 -0
- package/bin/np-tools/loop-run-round.cjs +7 -1
- package/bin/np-tools/security.cjs +3 -0
- package/bin/np-tools/skill-audit.cjs +79 -0
- package/bin/np-tools/skill-audit.test.cjs +86 -0
- package/bin/np-tools/spawn-headless.cjs +35 -1
- package/bin/np-tools/spawn-headless.test.cjs +135 -0
- package/bin/np-tools/verify-reliability.cjs +65 -0
- package/bin/np-tools/verify-reliability.test.cjs +69 -0
- package/lib/agents.test.cjs +1 -0
- package/lib/config-defaults.cjs +13 -0
- package/lib/config-schema.cjs +11 -0
- package/lib/eval-reliability.cjs +63 -0
- package/lib/eval-reliability.test.cjs +56 -0
- package/lib/headless-guard.cjs +127 -0
- package/lib/headless-guard.test.cjs +119 -0
- package/lib/install/claude-hooks-learnings.test.cjs +82 -0
- package/lib/install/claude-hooks.cjs +65 -4
- package/lib/install/claude-hooks.test.cjs +5 -2
- package/lib/learnings/capture-ledger.cjs +80 -0
- package/lib/learnings/capture-ledger.test.cjs +54 -0
- package/lib/learnings/extract.cjs +191 -0
- package/lib/learnings/extract.test.cjs +115 -0
- package/lib/nubosloop-audit.cjs +104 -0
- package/lib/nubosloop-skill-audit.test.cjs +98 -0
- package/lib/nubosloop.cjs +9 -0
- package/lib/tier-classify.cjs +67 -0
- package/lib/tier-classify.test.cjs +67 -0
- package/np-tools.cjs +4 -0
- package/package.json +1 -1
- package/skills/np-access-control/SKILL.md +42 -0
- package/skills/np-accessibility-audit/SKILL.md +41 -0
- package/skills/np-adr/SKILL.md +37 -0
- package/skills/np-api-design/SKILL.md +34 -0
- package/skills/np-caching-strategy/SKILL.md +38 -0
- package/skills/np-data-modeling/SKILL.md +37 -0
- package/skills/np-data-privacy/SKILL.md +39 -0
- package/skills/np-dependency-audit/SKILL.md +47 -0
- package/skills/np-encryption/SKILL.md +47 -0
- package/skills/np-error-handling/SKILL.md +37 -0
- package/skills/np-incident-response/SKILL.md +38 -0
- package/skills/np-llm-app-architecture/SKILL.md +50 -0
- package/skills/np-observability/SKILL.md +39 -0
- package/skills/np-performance/SKILL.md +38 -0
- package/skills/np-queue-design/SKILL.md +32 -0
- package/skills/np-rag-design/SKILL.md +43 -0
- package/skills/np-refactoring/SKILL.md +35 -0
- package/skills/np-resilience-patterns/SKILL.md +39 -0
- package/skills/np-secure-code-review/SKILL.md +46 -0
- package/skills/np-secure-design/SKILL.md +44 -0
- package/skills/np-service-boundary/SKILL.md +35 -0
- package/skills/np-system-design/SKILL.md +40 -0
- package/skills/np-test-strategy/SKILL.md +46 -0
- package/skills/np-threat-model/SKILL.md +42 -0
- package/templates/claude/payload/hooks/np-learnings-hook.cjs +56 -0
- package/templates/claude/payload/hooks/np-security-hook.cjs +1 -0
- package/workflows/architect-phase.md +21 -1
- package/workflows/execute-phase.md +66 -4
- package/workflows/verify-work.md +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to nubos-pilot are documented in this file. Format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
5
|
follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.2.4] - 2026-06-15
|
|
8
|
+
|
|
9
|
+
Fixed a recursion fault in the in-session hooks that could spawn an unbounded cascade of headless `claude -p` processes.
|
|
10
|
+
|
|
11
|
+
- The Stop-hook security review and continuous-learning capture each spawn a headless `claude -p` to do their work. That headless run re-fires the same SessionStart/Stop hooks, which spawned another headless run, and so on — a fork bomb of `claude`, `np-tools` and duplicated MCP servers that survived closing the terminal. nubos-pilot now marks every headless spawn with `NUBOS_PILOT_HEADLESS=1` and a `NUBOS_PILOT_HOOK_DEPTH` counter; the hooks no-op immediately inside a headless run, so the chain stops at exactly one level.
|
|
12
|
+
- Three independent guards back this up: the hook scripts and the `security`/`learnings` backends exit early when `NUBOS_PILOT_HEADLESS` is set; `spawn-headless` refuses to start a nested headless run (reentrancy + depth cap, default one level); and a per-agent lockfile under `.nubos-pilot/run/` bounds concurrent headless runs to one per agent even if the environment is not inherited. Headless runs already carry a hard timeout with SIGKILL, so a hung review cannot linger.
|
|
13
|
+
- Escape hatch: the guard keys off `NUBOS_PILOT_HEADLESS`, set automatically on the spawned `claude` — do not set it in your own shell or the in-session hooks will silently no-op. Raise the depth cap with `NUBOS_PILOT_MAX_HOOK_DEPTH` only if you understand the recursion risk.
|
|
14
|
+
|
|
15
|
+
## [1.2.3] — 2026-06-14
|
|
16
|
+
|
|
17
|
+
Three opt-in layers that make execution cheaper, more reliable, and self-improving.
|
|
18
|
+
|
|
19
|
+
- Cost-aware model routing: with `workflow.tier_routing` enabled, each task's executor runs at the model tier the plan assigned it — trivial work on a smaller model, structural or security-sensitive work on the strongest — instead of every task running at the top tier. The new `np:derive-tier` command suggests a tier from a task's observable signals (files touched, security/data sensitivity), so the choice is evidence-based. Off by default; behaviour is unchanged until you turn it on.
|
|
20
|
+
- Reliability checks (pass@k): set `loop.verify_runs` above 1 and nubos-pilot runs a task's verify command several times per round. A task goes green only when every run passes; a flaky task (passes sometimes, fails sometimes) is treated as red and handed to the build-fixer with a clear note, instead of slipping through on a lucky run. Defaults to a single run.
|
|
21
|
+
- Continuous learning: at the end of a session, a lightweight background reviewer reads what changed and distils reusable, durable lessons into the same learnings store the planner consults on the next similar task — so the system improves with use, not only inside the execution loop. On by default and rate-limited to bound cost; disable with `learnings.auto_capture`.
|
|
22
|
+
|
|
23
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
|
24
|
+
|
|
7
25
|
## [1.2.2] — 2026-06-05
|
|
8
26
|
|
|
9
27
|
A dependency graph for the codebase you work in, plus stricter checks on nubos-pilot's own data.
|
package/README.md
CHANGED
|
@@ -169,6 +169,22 @@ load-bearing ones for users and contributors:
|
|
|
169
169
|
See [`SECURITY.md`](./SECURITY.md) for the vulnerability disclosure policy
|
|
170
170
|
and threat model.
|
|
171
171
|
|
|
172
|
+
### Headless recursion guard
|
|
173
|
+
|
|
174
|
+
The in-session security review and continuous-learning hooks do their work in
|
|
175
|
+
a headless `claude -p` subprocess. To stop that subprocess from re-firing the
|
|
176
|
+
same hooks (which would cascade into an unbounded fork of `claude`/`np-tools`
|
|
177
|
+
processes), nubos-pilot sets `NUBOS_PILOT_HEADLESS=1` and a
|
|
178
|
+
`NUBOS_PILOT_HOOK_DEPTH` counter on every headless spawn. The hooks no-op when
|
|
179
|
+
`NUBOS_PILOT_HEADLESS` is set, `spawn-headless` refuses a nested or
|
|
180
|
+
depth-exceeded spawn, and a per-agent lockfile under `.nubos-pilot/run/` bounds
|
|
181
|
+
concurrent headless runs to one per agent.
|
|
182
|
+
|
|
183
|
+
The guard is automatic — do not export `NUBOS_PILOT_HEADLESS` in your own
|
|
184
|
+
shell, or the in-session hooks will silently do nothing. The depth cap is one
|
|
185
|
+
level; override it with `NUBOS_PILOT_MAX_HOOK_DEPTH` only if you understand the
|
|
186
|
+
recursion risk.
|
|
187
|
+
|
|
172
188
|
## Support
|
|
173
189
|
|
|
174
190
|
- Bugs / features: [GitHub issues](https://github.com/Nubos-AI/nubos-pilot/issues)
|
package/agents/np-architect.md
CHANGED
|
@@ -13,6 +13,8 @@ You are NOT a second researcher. Research is investigation; you are decision-mak
|
|
|
13
13
|
|
|
14
14
|
**CRITICAL: Mandatory Initial Read**
|
|
15
15
|
If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
|
|
16
|
+
|
|
17
|
+
**Design skills.** If the spawn prompt contains a `Use the following Nubos skills` line (injected by `/np:architect-phase` for structural/security milestones), `Read` each named skill from `.claude/skills/<skill>/SKILL.md` BEFORE committing decisions. Each skill's "Verification bar" is the standard every relevant decision must satisfy — design against it, and let the decision's stated consequences answer it. If the skills are absent (non-Claude runtime), proceed on your own judgment.
|
|
16
18
|
</role>
|
|
17
19
|
|
|
18
20
|
## Completeness Mandate
|
package/agents/np-executor.md
CHANGED
|
@@ -20,7 +20,7 @@ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool t
|
|
|
20
20
|
- Invoke commit-helper ONLY after verification passes.
|
|
21
21
|
- Never invoke `git` directly — always through the `np-tools.cjs` wrapper so the D-25 gitignore-guard runs.
|
|
22
22
|
- One task per spawn. One commit per task (D-03).
|
|
23
|
-
- If the spawn prompt contains a `Use the following Nubos skills:` line (injected by `/np:execute-phase`
|
|
23
|
+
- If the spawn prompt contains a `Use the following Nubos skills:` line (injected by `/np:execute-phase` — covers UI/frontend AND engineering concerns: API/contract, security/auth, data/migration, error-handling, resilience, caching, queue, performance, access-control, encryption, dependencies, privacy, and more), you MUST `Read` each named skill from `.claude/skills/<skill>/SKILL.md` BEFORE editing source — this is not optional. Apply each skill's quality bar; the task is NOT done until its diff satisfies every matched skill's "Verification bar", not just the test command. Treat an unmet skill bar exactly like a failing test. Immediately after reading each skill, stamp consultation via Bash: `node .nubos-pilot/bin/np-tools.cjs skill-audit ack --task <TASK_ID> --skill <skill>`. This is audited at post-critics exactly like the Rule-9 search evidence — an injected skill with no `ack` becomes a `skill-bar-unconsulted` finding that bounces the task back to you.
|
|
24
24
|
</role>
|
|
25
25
|
|
|
26
26
|
## Completeness Mandate
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: np-learnings-extractor
|
|
3
|
+
description: Read-only continuous-learning observer. Spawned headlessly by the ADR-0010 learnings Stop-hook against a single turn-diff — it reads what the session changed and returns a JSON envelope of atomic, reusable {pattern, outcome} learnings as its final message. Detection-only — never edits source, never writes files, never uses a milestone number. The orchestrator folds the returned candidates into the learnings store.
|
|
4
|
+
tier: haiku
|
|
5
|
+
tools: Read, Bash, Grep, Glob
|
|
6
|
+
color: cyan
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<role>
|
|
10
|
+
You are the nubos-pilot learnings extractor — the lightweight twin of `np-security-reviewer`'s session/diff mode, for institutional knowledge instead of security. You are spawned in the background when a session stops. You receive ONE turn's diff and a fresh context, and you return reusable learnings distilled from it. You never graded or wrote the code you are reading.
|
|
11
|
+
|
|
12
|
+
You DO NOT edit source. You DO NOT write files. You DO NOT use a milestone number. You read the supplied diff (and, only if needed, surrounding code via `Read`/`Grep`) and emit a single JSON envelope as your **final message**.
|
|
13
|
+
</role>
|
|
14
|
+
|
|
15
|
+
## Completeness Mandate
|
|
16
|
+
|
|
17
|
+
This agent operates under [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md). The rules that bind this role:
|
|
18
|
+
|
|
19
|
+
- **Rule 1 — Do the whole thing.** Read the entire supplied diff before extracting. Do not stop at the first interesting hunk.
|
|
20
|
+
- **Rule 5 — Aim to genuinely impress.** A learning must be durable and transferable — a rule a future agent on a *similar* task would thank you for. Narration of what changed is a failure.
|
|
21
|
+
- **Rule 8 — Never present a workaround when the real fix exists.** When a learning captures a fix, record the real fix as the pattern, not the band-aid.
|
|
22
|
+
- **Rule 12 — Boil the ocean, but quality over quantity.** Zero learnings is the correct, common answer for a routine turn. Never manufacture filler to fill the list — a noisy store is worse than an empty one.
|
|
23
|
+
|
|
24
|
+
Refusal of any rule is a hard-stop. Surface the violation verbatim and abort.
|
|
25
|
+
|
|
26
|
+
## Input
|
|
27
|
+
|
|
28
|
+
Triggered when the prompt contains a `<learning_capture>` block. Inside it: the list of changed files and the turn's diff. That is your entire scope — start from the diff; reach into surrounding code with `Read`/`Grep` only to confirm whether a candidate learning is real and correctly stated.
|
|
29
|
+
|
|
30
|
+
## What counts as a learning
|
|
31
|
+
|
|
32
|
+
A learning is one `{pattern, outcome}` pair:
|
|
33
|
+
|
|
34
|
+
- **pattern** — a durable, reusable, self-contained imperative rule. Good: *"use jose for JWT verification, never hand-roll HS256"*, *"batch ORM lookups in a single query to avoid N+1 in list endpoints"*. Bad: *"added a login form"* (narration), *"the UserController now has 3 methods"* (project trivia), *"renamed x to y"* (obvious from the diff).
|
|
35
|
+
- **outcome** — exactly one of `verified` | `failed` | `reverted` | `partial`: how the pattern played out in THIS turn.
|
|
36
|
+
|
|
37
|
+
Extract at most **5**. Prefer fewer, higher-signal learnings. If nothing clears the bar, return an empty list — that is expected for routine work.
|
|
38
|
+
|
|
39
|
+
## Output contract — your FINAL message MUST be exactly one JSON object, no prose, no code fence:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"learnings": [
|
|
44
|
+
{ "pattern": "reusable imperative rule, self-contained", "outcome": "verified|failed|reverted|partial" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you find nothing worth keeping, return `{"learnings":[]}`. The orchestrator dedups and folds each candidate into the learnings store (occurrence-counted, threshold-promoted) — it never blocks the session on your output.
|
|
50
|
+
|
|
51
|
+
<scope_guardrail>
|
|
52
|
+
**Do:** read the diff and surrounding code; return one JSON envelope as your final message.
|
|
53
|
+
**Don't:** edit or write any file; use a milestone number; spawn other agents; emit prose around the JSON; manufacture low-value learnings to pad the list.
|
|
54
|
+
</scope_guardrail>
|
package/agents/np-planner.md
CHANGED
|
@@ -388,7 +388,7 @@ Inside each `S<NNN>-PLAN.md`, every `<task>` tag MUST have these four attributes
|
|
|
388
388
|
> The slice number in the task ID is the authoritative wave; the T-number is per-slice. `np-plan-checker` rejects continued numbering as a `broken-dependency` critical finding (Dimension 6) — iteration-2 will then force a renumber.
|
|
389
389
|
- `depends_on="<id>[,<id>...]"` — comma-separated predecessor task full-ids, or empty string `""`. Must only reference tasks in **earlier slices** (cross-slice forward deps) or be empty (intra-slice tasks are implicitly parallel, never serial).
|
|
390
390
|
- `wave="<N>"` — integer equal to the slice number. For S001 use `wave="1"`, for S002 use `wave="2"`, etc.
|
|
391
|
-
- `tier="<haiku|sonnet|opus>"` — executor tier, picks the model via resolve-model.
|
|
391
|
+
- `tier="<haiku|sonnet|opus>"` — executor tier, picks the model via resolve-model. You are the decider, but make the call evidence-based, not by feel: run `node .nubos-pilot/bin/np-tools.cjs derive-tier --files "<comma-separated files_modified>" --name "<task title>"` and adopt its suggested tier unless you have a concrete reason to override (ADR-0013 — the tier is derived from observable signals: file count + security/data-sensitivity, never from implementation detail). The tier only changes the executor model when the project opts into `workflow.tier_routing`; otherwise every task runs at the strongest model regardless, so a wrong tier is never a correctness risk — but a right tier saves cost when routing is on.
|
|
392
392
|
|
|
393
393
|
The scaffolder (`_extractTasksFromSlicePlan` in `bin/np-tools/plan-milestone.cjs`) reads ONLY these opening-tag attributes. Without them, zero task files are scaffolded and execute-phase has nothing to dispatch.
|
|
394
394
|
|
|
@@ -44,6 +44,15 @@ Refusal of any rule is a hard-stop. Surface the violation to the orchestrator ve
|
|
|
44
44
|
|
|
45
45
|
For each path in `files_modified`, scan for indicators of the following categories. Each finding gets its own block in the report.
|
|
46
46
|
|
|
47
|
+
When the Nubos skill library is present, `Read` `.claude/skills/np-secure-code-review/SKILL.md` first and treat its checklist as the authoritative, language-agnostic expansion of the categories below. Then load the skills matching the milestone's surface and apply each one's "Verification bar" to the relevant findings:
|
|
48
|
+
|
|
49
|
+
- new trust boundary / external integration / store for credentials or PII → `np-threat-model` (STRIDE lens) and `np-secure-design` (secure-defaults / least-privilege / zero-trust design review).
|
|
50
|
+
- roles, permissions, resource-ownership, or access-rule changes → `np-access-control` (deny-by-default, object-level authz, IDOR).
|
|
51
|
+
- encryption, hashing, password storage, TLS, tokens, or key/secret management → `np-encryption`.
|
|
52
|
+
- collection, storage, or logging of personal/sensitive data → `np-data-privacy` (minimization, retention, no-PII-in-logs).
|
|
53
|
+
|
|
54
|
+
The table below is the index; the skills are the depth. If the skills are absent (non-Claude runtime), fall back to the table alone.
|
|
55
|
+
|
|
47
56
|
| Category | Look for |
|
|
48
57
|
|---------|----------|
|
|
49
58
|
| Injection | unparameterized SQL/shell/exec, string-concat queries, `eval`-style calls, untrusted input into `child_process` |
|
|
@@ -14,11 +14,15 @@ const COMMANDS = [
|
|
|
14
14
|
{ name: 'new-milestone', category: 'Planning', description: 'Append a new milestone (M<NNN>) to an existing project', description_de: 'Hängt einen neuen Milestone (M<NNN>) an ein bestehendes Projekt an' },
|
|
15
15
|
{ name: 'propose-milestones', category: 'Planning', description: 'Re-plan all not-yet-done milestones: AI proposes add/update/remove from PROJECT.md + REQUIREMENTS.md', description_de: 'Plant offene Milestones neu: KI schlägt add/update/remove aus PROJECT.md + REQUIREMENTS.md vor' },
|
|
16
16
|
{ name: 'agent-skills', category: 'Planning', description: 'Print agent_skills config for a given subagent', description_de: 'Gibt agent_skills-Konfiguration für einen Subagent aus' },
|
|
17
|
+
{ name: 'derive-tier', category: 'Planning', description: 'Advisory: derive a suggested executor tier (haiku|sonnet|opus) from a task\'s observable signals (files_modified + risk keywords). Decider stays the planner. ADR-0013.', description_de: 'Advisory: leitet aus den beobachtbaren Task-Signalen (files_modified + Risk-Keywords) einen Vorschlags-Tier (haiku|sonnet|opus) ab. Entscheider bleibt der Planner. ADR-0013.' },
|
|
17
18
|
|
|
18
19
|
{ name: 'execute-milestone', category: 'Execution', description: 'Wave-based milestone execution — slice by slice, tasks parallel within a slice', description_de: 'Wave-basierte Milestone-Ausführung — Slice für Slice, Tasks parallel innerhalb einer Slice' },
|
|
19
20
|
{ name: 'commit-task', category: 'Execution', description: 'Atomic per-task git commit via lib/git.cjs', description_de: 'Atomarer Per-Task-Git-Commit über lib/git.cjs' },
|
|
20
21
|
{ name: 'checkpoint', category: 'Execution', description: 'Per-task crash-safety checkpoint CRUD (start/transition/touch/show)', description_de: 'Per-Task-Checkpoint-CRUD für Crash-Safety (start/transition/touch/show)' },
|
|
21
22
|
{ name: 'verify-work', category: 'Execution', description: 'Two-pass goal-backward verification (milestone-level VERIFICATION.md)', description_de: 'Zweistufige Goal-Backward-Verifikation (Milestone-Ebene VERIFICATION.md)' },
|
|
23
|
+
{ name: 'verify-reliability', category: 'Execution', description: 'pass@k reliability: fold k verify-run exit codes into pass@1/pass@k/flaky + an aggregate exit code (pass^k) for loop-run-round. Opt-in via loop.verify_runs.', description_de: 'pass@k-Reliability: faltet k Verify-Exit-Codes zu pass@1/pass@k/flaky + Aggregat-Exit-Code (pass^k) für loop-run-round. Opt-in über loop.verify_runs.' },
|
|
24
|
+
{ name: 'learnings', category: 'Execution', description: 'Stop-hook continuous-learning capture (ADR-0010). Verbs: capture (rate-limited; spawns headless np-learnings-extractor over the turn diff) | reset (clears stop-streak) | run-extract (background worker). Gated by learnings.auto_capture.', description_de: 'Stop-Hook Continuous-Learning-Capture (ADR-0010). Verben: capture (rate-limited; spawnt headless np-learnings-extractor über das Turn-Diff) | reset (setzt Stop-Streak zurück) | run-extract (Background-Worker). Gated über learnings.auto_capture.' },
|
|
25
|
+
{ name: 'skill-audit', category: 'Execution', description: 'Skill-bar consultation audit (counterpart to the Rule-9 search audit). Verbs: expect --task --skills (orchestrator records injected skills) | ack --task --skill (executor stamps a consulted skill) | findings --task [--round] (list unmet bars). An unconsulted injected skill becomes a skill-bar-unconsulted finding that routes back to the executor.', description_de: 'Skill-Bar-Konsultations-Audit (Pendant zum Rule-9-Search-Audit). Verben: expect --task --skills (Orchestrator merkt injizierte Skills) | ack --task --skill (Executor stempelt konsultierten Skill) | findings --task [--round] (offene Bars). Ein nicht konsultierter injizierter Skill wird zu einem skill-bar-unconsulted-Finding und routet zurück zum Executor.' },
|
|
22
26
|
{ name: 'close-project', category: 'Review', description: 'Aggregate verification of every milestone; writes PROJECT-SUMMARY.md + sets project_status=completed', description_de: 'Aggregat-Verifikation aller Milestones; schreibt PROJECT-SUMMARY.md + setzt project_status=completed' },
|
|
23
27
|
{ name: 'archive-project', category: 'Planning', description: 'Move current .nubos-pilot/ project to archive/<slug>-<YYYYMMDD>/ (status|do|list|read)', description_de: 'Verschiebt aktuelles .nubos-pilot/-Projekt nach archive/<slug>-<YYYYMMDD>/ (status|do|list|read)' },
|
|
24
28
|
{ name: 'add-tests', category: 'Execution', description: 'Persist VERIFICATION Pass-cases as node:test UAT (Sentinel-preserving)', description_de: 'Persistiert VERIFICATION-Pass-Cases als node:test-UAT (Sentinel-erhaltend)' },
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
const { classifyTier } = require('../../lib/tier-classify.cjs');
|
|
6
|
+
const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
|
|
7
|
+
const { emitErrorEnvelope } = require('./_args.cjs');
|
|
8
|
+
|
|
9
|
+
function _usage() {
|
|
10
|
+
return [
|
|
11
|
+
'Usage:',
|
|
12
|
+
' np-tools.cjs derive-tier --files <a,b,c> [--name <text>] [--desc <text>]',
|
|
13
|
+
' np-tools.cjs derive-tier --plan <path-to-PLAN.md>',
|
|
14
|
+
'',
|
|
15
|
+
'Advisory: derives a suggested executor tier (haiku|sonnet|opus) from the',
|
|
16
|
+
'task\'s observable signals. The planner remains the decider.',
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _fromPlan(planPath) {
|
|
21
|
+
const raw = fs.readFileSync(planPath, 'utf-8');
|
|
22
|
+
const { frontmatter, body } = extractFrontmatter(raw);
|
|
23
|
+
const nameMatch = String(body || '').match(/^#\s+(?:.*?—\s*)?(.+?)\s*$/m);
|
|
24
|
+
return {
|
|
25
|
+
files_modified: Array.isArray(frontmatter.files_modified) ? frontmatter.files_modified : [],
|
|
26
|
+
name: nameMatch ? nameMatch[1] : (frontmatter.id || ''),
|
|
27
|
+
desc: String(body || ''),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function run(argv, ctx) {
|
|
32
|
+
const context = ctx || {};
|
|
33
|
+
const stdout = context.stdout || process.stdout;
|
|
34
|
+
const stderr = context.stderr || process.stderr;
|
|
35
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
36
|
+
|
|
37
|
+
let files = null;
|
|
38
|
+
let name = '';
|
|
39
|
+
let desc = '';
|
|
40
|
+
let planPath = null;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const a = args[i];
|
|
44
|
+
if (a === '-h' || a === '--help') { stdout.write(_usage() + '\n'); return 0; }
|
|
45
|
+
else if (a === '--files') { files = args[++i] || ''; }
|
|
46
|
+
else if (a.startsWith('--files=')) { files = a.slice('--files='.length); }
|
|
47
|
+
else if (a === '--name') { name = args[++i] || ''; }
|
|
48
|
+
else if (a.startsWith('--name=')) { name = a.slice('--name='.length); }
|
|
49
|
+
else if (a === '--desc') { desc = args[++i] || ''; }
|
|
50
|
+
else if (a.startsWith('--desc=')) { desc = a.slice('--desc='.length); }
|
|
51
|
+
else if (a === '--plan') { planPath = args[++i] || ''; }
|
|
52
|
+
else if (a.startsWith('--plan=')) { planPath = a.slice('--plan='.length); }
|
|
53
|
+
else {
|
|
54
|
+
stderr.write(JSON.stringify({
|
|
55
|
+
code: 'derive-tier-unknown-arg',
|
|
56
|
+
message: 'Unknown argument: ' + a,
|
|
57
|
+
details: { arg: a },
|
|
58
|
+
}) + '\n');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
let task;
|
|
65
|
+
if (planPath) {
|
|
66
|
+
task = _fromPlan(planPath);
|
|
67
|
+
} else {
|
|
68
|
+
const list = files == null
|
|
69
|
+
? []
|
|
70
|
+
: String(files).split(',').map((s) => s.trim()).filter(Boolean);
|
|
71
|
+
task = { files_modified: list, name, desc };
|
|
72
|
+
}
|
|
73
|
+
const result = classifyTier(task);
|
|
74
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
75
|
+
return 0;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
emitErrorEnvelope(err, stderr, 'derive-tier-internal-error');
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { run };
|
|
83
|
+
|
|
84
|
+
if (require.main === module) {
|
|
85
|
+
process.exit(run(process.argv.slice(2)));
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { run } = require('./derive-tier.cjs');
|
|
9
|
+
|
|
10
|
+
function _capture() {
|
|
11
|
+
const out = { text: '' };
|
|
12
|
+
const err = { text: '' };
|
|
13
|
+
return {
|
|
14
|
+
stdout: { write: (s) => { out.text += s; return true; } },
|
|
15
|
+
stderr: { write: (s) => { err.text += s; return true; } },
|
|
16
|
+
out, err,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('DT-1: --files + --name with security keyword → opus', () => {
|
|
21
|
+
const c = _capture();
|
|
22
|
+
const code = run(['--files', 'app/Auth.php', '--name', 'add login throttling'], c);
|
|
23
|
+
assert.strictEqual(code, 0);
|
|
24
|
+
const r = JSON.parse(c.out.text);
|
|
25
|
+
assert.strictEqual(r.tier, 'opus');
|
|
26
|
+
assert.strictEqual(r.size, 'large');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('DT-2: single doc file → haiku', () => {
|
|
30
|
+
const c = _capture();
|
|
31
|
+
const code = run(['--files', 'README.md', '--name', 'fix typo'], c);
|
|
32
|
+
assert.strictEqual(code, 0);
|
|
33
|
+
assert.strictEqual(JSON.parse(c.out.text).tier, 'haiku');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('DT-3: ordinary task → sonnet', () => {
|
|
37
|
+
const c = _capture();
|
|
38
|
+
const code = run(['--files', 'app/Cart.php,app/Cart.test.php', '--name', 'add discount'], c);
|
|
39
|
+
assert.strictEqual(code, 0);
|
|
40
|
+
assert.strictEqual(JSON.parse(c.out.text).tier, 'sonnet');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('DT-4: --plan reads frontmatter files + body name', () => {
|
|
44
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-derive-tier-'));
|
|
45
|
+
const plan = path.join(dir, 'T0001-PLAN.md');
|
|
46
|
+
fs.writeFileSync(plan, [
|
|
47
|
+
'---',
|
|
48
|
+
'id: M001-S001-T0001',
|
|
49
|
+
'files_modified:',
|
|
50
|
+
' - db/migrations/004_users.sql',
|
|
51
|
+
'---',
|
|
52
|
+
'',
|
|
53
|
+
'# M001-S001-T0001 — Add users table migration',
|
|
54
|
+
'',
|
|
55
|
+
'Body text.',
|
|
56
|
+
].join('\n'));
|
|
57
|
+
const c = _capture();
|
|
58
|
+
const code = run(['--plan', plan], c);
|
|
59
|
+
assert.strictEqual(code, 0);
|
|
60
|
+
assert.strictEqual(JSON.parse(c.out.text).tier, 'opus');
|
|
61
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('DT-5: unknown arg → error envelope, exit 1', () => {
|
|
65
|
+
const c = _capture();
|
|
66
|
+
const code = run(['--bogus'], c);
|
|
67
|
+
assert.strictEqual(code, 1);
|
|
68
|
+
assert.match(c.err.text, /derive-tier-unknown-arg/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('DT-6: --help → usage, exit 0', () => {
|
|
72
|
+
const c = _capture();
|
|
73
|
+
const code = run(['--help'], c);
|
|
74
|
+
assert.strictEqual(code, 0);
|
|
75
|
+
assert.match(c.out.text, /derive-tier/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('DT-7: no files → standard sonnet, no throw', () => {
|
|
79
|
+
const c = _capture();
|
|
80
|
+
const code = run(['--name', 'something'], c);
|
|
81
|
+
assert.strictEqual(code, 0);
|
|
82
|
+
assert.strictEqual(JSON.parse(c.out.text).tier, 'sonnet');
|
|
83
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const child_process = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const { tryReadConfigPath } = require('../../lib/config.cjs');
|
|
7
|
+
const ledger = require('../../lib/learnings/capture-ledger.cjs');
|
|
8
|
+
const extract = require('../../lib/learnings/extract.cjs');
|
|
9
|
+
const headlessGuard = require('../../lib/headless-guard.cjs');
|
|
10
|
+
const args = require('./_args.cjs');
|
|
11
|
+
|
|
12
|
+
function _readStdin() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
if (process.stdin.isTTY) return resolve('');
|
|
15
|
+
let buf = '';
|
|
16
|
+
process.stdin.setEncoding('utf-8');
|
|
17
|
+
const timer = setTimeout(() => { try { process.stdin.removeAllListeners(); } catch {} resolve(buf); }, 800);
|
|
18
|
+
process.stdin.on('data', (c) => { buf += c; });
|
|
19
|
+
process.stdin.on('end', () => { clearTimeout(timer); resolve(buf); });
|
|
20
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(buf); });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _safeParse(s) { try { return s ? JSON.parse(s) : {}; } catch { return {}; } }
|
|
25
|
+
|
|
26
|
+
async function _payload(argv) {
|
|
27
|
+
const inline = args.getFlag(argv, '--payload', { allowDashValues: true });
|
|
28
|
+
if (inline !== undefined) return _safeParse(inline);
|
|
29
|
+
if (argv.includes('--stdin')) return _safeParse(await _readStdin());
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _cfg(cwd) {
|
|
34
|
+
return {
|
|
35
|
+
auto_capture: tryReadConfigPath(cwd, 'learnings.auto_capture', true) !== false,
|
|
36
|
+
max_per_hour: Number(tryReadConfigPath(cwd, 'learnings.max_captures_per_hour', 10)) || 10,
|
|
37
|
+
max_in_a_row: Number(tryReadConfigPath(cwd, 'learnings.max_in_a_row', 3)) || 3,
|
|
38
|
+
timeout_ms: Number(tryReadConfigPath(cwd, 'learnings.timeout_ms', 120000)) || 120000,
|
|
39
|
+
max_files: Number(tryReadConfigPath(cwd, 'learnings.max_files', 30)) || 30,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _spawnWorker(cwd, sid) {
|
|
44
|
+
const npTools = path.join(__dirname, '..', '..', 'np-tools.cjs');
|
|
45
|
+
try {
|
|
46
|
+
const child = child_process.spawn(
|
|
47
|
+
process.execPath,
|
|
48
|
+
[npTools, 'learnings', 'run-extract', '--session', sid],
|
|
49
|
+
{ cwd, detached: true, stdio: 'ignore' },
|
|
50
|
+
);
|
|
51
|
+
child.unref();
|
|
52
|
+
return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _emit(stdout, obj) { stdout.write(JSON.stringify(obj) + '\n'); }
|
|
57
|
+
|
|
58
|
+
async function run(argv, ctx) {
|
|
59
|
+
const context = ctx || {};
|
|
60
|
+
const cwd = context.cwd || process.cwd();
|
|
61
|
+
const stdout = context.stdout || process.stdout;
|
|
62
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
63
|
+
const verb = list[0];
|
|
64
|
+
|
|
65
|
+
if (headlessGuard.isHeadless(process.env)) return 0;
|
|
66
|
+
|
|
67
|
+
const cfg = _cfg(cwd);
|
|
68
|
+
|
|
69
|
+
// 'reset' (UserPromptSubmit) and 'run-extract' (background worker) are not
|
|
70
|
+
// gated by auto_capture so they keep working coherently, but 'capture' is.
|
|
71
|
+
if (verb === 'capture') {
|
|
72
|
+
if (!cfg.auto_capture) { _emit(stdout, { captured: false, reason: 'disabled' }); return 0; }
|
|
73
|
+
const payload = await _payload(list);
|
|
74
|
+
const sid = payload.session_id || args.getFlag(list, '--session') || '';
|
|
75
|
+
if (!sid) { _emit(stdout, { captured: false, reason: 'no-session' }); return 0; }
|
|
76
|
+
const gate = ledger.tryRecordCapture(sid, { maxPerHour: cfg.max_per_hour, maxStreak: cfg.max_in_a_row });
|
|
77
|
+
if (!gate.allowed) { _emit(stdout, { captured: false, reason: gate.reason }); return 0; }
|
|
78
|
+
_spawnWorker(cwd, sid);
|
|
79
|
+
_emit(stdout, { captured: true, spawned: true });
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (verb === 'reset') {
|
|
84
|
+
const payload = await _payload(list);
|
|
85
|
+
const sid = payload.session_id || args.getFlag(list, '--session') || '';
|
|
86
|
+
if (sid) ledger.resetStreak(sid);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (verb === 'run-extract') {
|
|
91
|
+
const sid = args.getFlag(list, '--session') || '';
|
|
92
|
+
try {
|
|
93
|
+
const result = extract.runExtract({ cwd, sid, config: cfg });
|
|
94
|
+
_emit(stdout, result);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
_emit(stdout, { ran: false, reason: 'error', error: String(err && err.code || err) });
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_emit(stdout, { error: 'unknown-verb', verb: verb || null, verbs: ['capture', 'reset', 'run-extract'] });
|
|
102
|
+
return verb ? 1 : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { run };
|
|
106
|
+
|
|
107
|
+
if (require.main === module) {
|
|
108
|
+
run(process.argv.slice(3)).then((c) => process.exit(c)).catch(() => process.exit(0));
|
|
109
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { run } = require('./learnings.cjs');
|
|
9
|
+
|
|
10
|
+
function _capture() {
|
|
11
|
+
const out = { text: '' };
|
|
12
|
+
return { stdout: { write: (s) => { out.text += s; return true; } }, out };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('LV-1: capture with no session → no-session, no spawn', async () => {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lv-'));
|
|
17
|
+
try {
|
|
18
|
+
const c = _capture();
|
|
19
|
+
c.cwd = dir;
|
|
20
|
+
const code = await run(['capture'], c);
|
|
21
|
+
assert.strictEqual(code, 0);
|
|
22
|
+
assert.match(c.out.text, /no-session/);
|
|
23
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('LV-2: capture disabled via config → disabled, no spawn', async () => {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lv-'));
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(dir, '.nubos-pilot', 'config.json'),
|
|
32
|
+
JSON.stringify({ learnings: { auto_capture: false } }),
|
|
33
|
+
);
|
|
34
|
+
const c = _capture();
|
|
35
|
+
c.cwd = dir;
|
|
36
|
+
const code = await run(['capture', '--payload', JSON.stringify({ session_id: 'abc' })], c);
|
|
37
|
+
assert.strictEqual(code, 0);
|
|
38
|
+
assert.match(c.out.text, /disabled/);
|
|
39
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('LV-3: run-extract on a non-repo cwd → ran:false not-a-repo', async () => {
|
|
43
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lv-'));
|
|
44
|
+
try {
|
|
45
|
+
const c = _capture();
|
|
46
|
+
c.cwd = dir;
|
|
47
|
+
const code = await run(['run-extract', '--session', 'abc'], c);
|
|
48
|
+
assert.strictEqual(code, 0);
|
|
49
|
+
const parsed = JSON.parse(c.out.text);
|
|
50
|
+
assert.strictEqual(parsed.ran, false);
|
|
51
|
+
assert.strictEqual(parsed.reason, 'not-a-repo');
|
|
52
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('LV-4: unknown verb → error envelope, exit 1', async () => {
|
|
56
|
+
const c = _capture();
|
|
57
|
+
const code = await run(['bogus'], c);
|
|
58
|
+
assert.strictEqual(code, 1);
|
|
59
|
+
assert.match(c.out.text, /unknown-verb/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('LV-5: reset is a no-op without a session and never throws', async () => {
|
|
63
|
+
const c = _capture();
|
|
64
|
+
const code = await run(['reset'], c);
|
|
65
|
+
assert.strictEqual(code, 0);
|
|
66
|
+
});
|
|
@@ -324,21 +324,27 @@ function _runPostCritics(taskId, list, cwd) {
|
|
|
324
324
|
? nubosloop.coerceMaxRounds(override)
|
|
325
325
|
: opts.maxRounds;
|
|
326
326
|
const auditFindings = nubosloop.auditFindingsFromAudits(prev.tool_use_audit, round, taskId);
|
|
327
|
+
const skillFindings = nubosloop.skillFindingsFromState(prev, round, taskId);
|
|
328
|
+
const combinedAudit = skillFindings.length ? auditFindings.concat(skillFindings) : auditFindings;
|
|
327
329
|
evalResult = nubosloop.evaluateLoop(
|
|
328
330
|
{ round },
|
|
329
331
|
criticOutputs,
|
|
330
|
-
{ maxRounds: effectiveMax, auditFindings },
|
|
332
|
+
{ maxRounds: effectiveMax, auditFindings: combinedAudit },
|
|
331
333
|
);
|
|
332
334
|
const perRound = (prev.findings_per_round && typeof prev.findings_per_round === 'object')
|
|
333
335
|
? safeAssign({}, prev.findings_per_round)
|
|
334
336
|
: {};
|
|
335
337
|
perRound[String(round)] = evalResult.findings;
|
|
336
338
|
const routed = nubosloop.markAuditsRoutedInArray(prev.tool_use_audit, round);
|
|
339
|
+
const skillRoutedRounds = skillFindings.length
|
|
340
|
+
? nubosloop.markSkillFindingsRoutedInArray(prev.skill_routed_rounds, round)
|
|
341
|
+
: (Array.isArray(prev.skill_routed_rounds) ? prev.skill_routed_rounds : []);
|
|
337
342
|
const partial = {
|
|
338
343
|
last_phase: 'post-critics',
|
|
339
344
|
last_action: evalResult.next_action,
|
|
340
345
|
findings: evalResult.findings,
|
|
341
346
|
findings_per_round: perRound,
|
|
347
|
+
skill_routed_rounds: skillRoutedRounds,
|
|
342
348
|
tool_use_audit: routed.audits,
|
|
343
349
|
};
|
|
344
350
|
if (force) partial.forced_post_critics = true;
|
|
@@ -8,6 +8,7 @@ const { tryReadConfigPath } = require('../../lib/config.cjs');
|
|
|
8
8
|
const scan = require('../../lib/security/scan.cjs');
|
|
9
9
|
const ledger = require('../../lib/security/ledger.cjs');
|
|
10
10
|
const review = require('../../lib/security/review.cjs');
|
|
11
|
+
const headlessGuard = require('../../lib/headless-guard.cjs');
|
|
11
12
|
const args = require('./_args.cjs');
|
|
12
13
|
|
|
13
14
|
const COMMIT_RE = /\bgit\b[\s\S]*\b(commit|push)\b/;
|
|
@@ -93,6 +94,8 @@ async function run(argv, ctx) {
|
|
|
93
94
|
const list = Array.isArray(argv) ? argv : [];
|
|
94
95
|
const verb = list[0];
|
|
95
96
|
|
|
97
|
+
if (headlessGuard.isHeadless(process.env)) return 0;
|
|
98
|
+
|
|
96
99
|
const cfg = _cfg(cwd);
|
|
97
100
|
if (!cfg.enabled && verb !== 'run-review') return 0;
|
|
98
101
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
4
|
+
const checkpoint = require('../../lib/checkpoint.cjs');
|
|
5
|
+
const { TASK_ID_RE } = require('../../lib/ids.cjs');
|
|
6
|
+
const args = require('./_args.cjs');
|
|
7
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
8
|
+
|
|
9
|
+
function _usage() {
|
|
10
|
+
return [
|
|
11
|
+
'Usage:',
|
|
12
|
+
' np-tools.cjs skill-audit expect --task <id> --skills <a,b,c> (orchestrator: record injected skills)',
|
|
13
|
+
' np-tools.cjs skill-audit ack --task <id> --skill <name> (executor: stamp a consulted skill)',
|
|
14
|
+
' np-tools.cjs skill-audit findings --task <id> [--round <n>] (read-only: list unmet skill bars)',
|
|
15
|
+
'',
|
|
16
|
+
'Mechanical counterpart to the Rule-9 search audit: a skill injected as a task\'s',
|
|
17
|
+
'quality bar that the executor never consulted becomes a `skill-bar-unconsulted`',
|
|
18
|
+
'finding at post-critics, routing the task back to the executor (once per round).',
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _assertTask(taskId) {
|
|
23
|
+
args.assertMatch(taskId, TASK_ID_RE, 'skill-audit-invalid-task-id', 'taskId');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(argv, ctx) {
|
|
27
|
+
const context = ctx || {};
|
|
28
|
+
const cwd = context.cwd || process.cwd();
|
|
29
|
+
const stdout = context.stdout || process.stdout;
|
|
30
|
+
const stderr = context.stderr || process.stderr;
|
|
31
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
32
|
+
const verb = list[0];
|
|
33
|
+
const tail = list.slice(1);
|
|
34
|
+
|
|
35
|
+
if (!verb || verb === '-h' || verb === '--help') { stdout.write(_usage() + '\n'); return 0; }
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (verb === 'expect') {
|
|
39
|
+
const taskId = args.getFlag(tail, '--task');
|
|
40
|
+
_assertTask(taskId);
|
|
41
|
+
const raw = args.getFlag(tail, '--skills') || '';
|
|
42
|
+
const skills = String(raw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
43
|
+
const res = nubosloop.recordExpectedSkills(taskId, skills, cwd);
|
|
44
|
+
stdout.write(JSON.stringify(res) + '\n');
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (verb === 'ack') {
|
|
48
|
+
const taskId = args.getFlag(tail, '--task');
|
|
49
|
+
_assertTask(taskId);
|
|
50
|
+
const skill = args.getFlag(tail, '--skill');
|
|
51
|
+
if (!skill) throw new NubosPilotError('skill-audit-missing-skill', 'ack requires --skill <name>', {});
|
|
52
|
+
const res = nubosloop.recordSkillEvidence(taskId, skill, cwd);
|
|
53
|
+
stdout.write(JSON.stringify(res) + '\n');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (verb === 'findings') {
|
|
57
|
+
const taskId = args.getFlag(tail, '--task');
|
|
58
|
+
_assertTask(taskId);
|
|
59
|
+
const cp = checkpoint.readCheckpoint(taskId, cwd) || {};
|
|
60
|
+
const prev = cp.nubosloop || {};
|
|
61
|
+
const roundArg = args.getFlag(tail, '--round');
|
|
62
|
+
const round = roundArg != null ? Number(roundArg) : (Number(prev.round) || 1);
|
|
63
|
+
const findings = nubosloop.skillFindingsFromState(prev, round, taskId);
|
|
64
|
+
stdout.write(JSON.stringify({ task_id: taskId, round, findings }) + '\n');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
stderr.write(JSON.stringify({ code: 'skill-audit-unknown-verb', message: 'Unknown verb: ' + verb, details: { verb, verbs: ['expect', 'ack', 'findings'] } }) + '\n');
|
|
68
|
+
return 1;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
args.emitErrorEnvelope(err, stderr, 'skill-audit-internal-error');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { run };
|
|
76
|
+
|
|
77
|
+
if (require.main === module) {
|
|
78
|
+
process.exit(run(process.argv.slice(3)));
|
|
79
|
+
}
|