nubos-pilot 0.9.8 → 1.0.0
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-critic-acceptance.md +7 -5
- package/agents/np-critic-style.md +2 -1
- package/agents/np-critic-tests.md +2 -1
- package/agents/np-critic.md +128 -0
- package/agents/np-plan-checker.md +2 -0
- package/bin/np-tools/commit-task.cjs +54 -3
- package/bin/np-tools/commit-task.test.cjs +61 -5
- package/bin/np-tools/commit.cjs +9 -1
- package/bin/np-tools/commit.test.cjs +28 -0
- package/bin/np-tools/doctor.cjs +10 -1
- package/bin/np-tools/loop-audit-tool-use.cjs +16 -0
- package/bin/np-tools/loop-commands.test.cjs +643 -38
- package/bin/np-tools/loop-run-round.cjs +258 -38
- package/bin/np-tools/loop-state-record.cjs +41 -7
- package/bin/np-tools/resolve-model.cjs +24 -2
- package/bin/np-tools/resolve-model.test.cjs +33 -7
- package/bin/researcher-merge.cjs +103 -0
- package/bin/researcher-merge.test.cjs +142 -0
- package/docs/adr/0010-nubosloop.md +61 -15
- package/docs/agent-frontmatter-schema.md +22 -2
- package/lib/agents.cjs +39 -1
- package/lib/agents.test.cjs +97 -6
- package/lib/git.cjs +35 -17
- package/lib/git.test.cjs +78 -10
- package/lib/nubosloop.cjs +182 -30
- package/lib/nubosloop.test.cjs +161 -7
- package/package.json +1 -1
- package/workflows/add-tests.md +2 -2
- package/workflows/architect-phase.md +1 -1
- package/workflows/execute-phase.md +202 -39
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: np-critic-acceptance
|
|
3
|
-
description:
|
|
3
|
+
description: Audit-surface module for the Acceptance axis of np-critic. NOT spawned independently — loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for per-success_criterion verdict, locked-decision conformance, scope-creep, stuck-detection, and infrastructure-mismatch. ADR-0010 §Single-Critic Revision 2026-05-05.
|
|
4
|
+
module: true
|
|
4
5
|
tier: sonnet
|
|
5
6
|
tools: Read, Bash, Grep, Glob
|
|
6
7
|
color: "#A855F7"
|
|
@@ -52,7 +53,7 @@ The orchestrator provides these paths in your prompt context. Read every path it
|
|
|
52
53
|
2. **Locked-decision conformance** — the diff does not violate any locked decision in `M<NNN>-CONTEXT.md`. Violations are findings of category `locked-decision-violation`.
|
|
53
54
|
3. **Scope creep** — the diff does not edit files outside `files_modified`. Out-of-scope edits are findings of category `scope-creep`.
|
|
54
55
|
4. **Stuck-marker check** — if the task is on round 3 with no progress between rounds, you flag `stuck-detected` so the orchestrator escalates.
|
|
55
|
-
5. **Infrastructure-mismatch detection** — if the verify output indicates an infrastructure failure (container exited, runtime version skew, missing service: `php -v` mismatch, `docker exec` errors, port-not-bound, DB-unreachable), do NOT downgrade affected criteria to `Unsatisfied` or `Satisfied`. Mark them `Information-Missing`
|
|
56
|
+
5. **Infrastructure-mismatch detection** — if the verify output indicates an infrastructure failure (container exited, runtime version skew, missing service: `php -v` mismatch, `docker exec` errors, port-not-bound, DB-unreachable), do NOT downgrade affected criteria to `Unsatisfied` or `Satisfied`. Mark them `Information-Missing` for the criterion verdict, AND emit a finding of category `infrastructure-mismatch` whose `remediation` names the specific environment delta (e.g., `composer requires php ^8.5, container runs 8.4 — Dockerfile bump required outside this milestone`). The orchestrator routes `infrastructure-mismatch` directly to plan-checker (Container/PHP-skew is rarely researcher-fixable; the milestone-level infra config is what changes). The code is not at fault.
|
|
56
57
|
|
|
57
58
|
## Output Schema
|
|
58
59
|
|
|
@@ -75,7 +76,7 @@ Emit a single JSON object as your final response (no prose, no markdown wrapper
|
|
|
75
76
|
"findings": [
|
|
76
77
|
{
|
|
77
78
|
"id": "ACC-001",
|
|
78
|
-
"category": "unmet-criterion | locked-decision-violation | scope-creep | information-missing | question-to-user | stuck-detected",
|
|
79
|
+
"category": "unmet-criterion | locked-decision-violation | scope-creep | information-missing | infrastructure-mismatch | question-to-user | stuck-detected",
|
|
79
80
|
"severity": "fail | risk | nit",
|
|
80
81
|
"criterion_id": "SC-3",
|
|
81
82
|
"remediation": "Add an integration test that asserts the WWW-Authenticate header value.",
|
|
@@ -86,12 +87,13 @@ Emit a single JSON object as your final response (no prose, no markdown wrapper
|
|
|
86
87
|
}
|
|
87
88
|
```
|
|
88
89
|
|
|
89
|
-
Categories MUST be one of: `unmet-criterion`, `locked-decision-violation`, `scope-creep`, `information-missing`, `question-to-user`, `stuck-detected`. The orchestrator's routing engine maps these:
|
|
90
|
+
Categories MUST be one of: `unmet-criterion`, `locked-decision-violation`, `scope-creep`, `information-missing`, `infrastructure-mismatch`, `question-to-user`, `stuck-detected`. The orchestrator's routing engine maps these:
|
|
90
91
|
|
|
91
92
|
- `unmet-criterion` / `scope-creep` → Executor / Build-Fixer (next round).
|
|
92
93
|
- `information-missing` → Researcher-Schwarm (next research round).
|
|
94
|
+
- `infrastructure-mismatch` → plan-checker (env/container delta the milestone owns, not the executor).
|
|
93
95
|
- `question-to-user` → `askuser` (Temporal-style signal-wait when integrated).
|
|
94
|
-
- `locked-decision-violation` →
|
|
96
|
+
- `locked-decision-violation` → plan-checker escalation.
|
|
95
97
|
- `stuck-detected` → loop terminates with `stuck` state in STATE.md.
|
|
96
98
|
|
|
97
99
|
`verdict` is `passed` only when every criterion in `criteria[]` is `Satisfied` AND `findings.length === 0`. Otherwise `issues_found`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: np-critic-style
|
|
3
|
-
description:
|
|
3
|
+
description: Audit-surface module for the Style axis of np-critic. NOT spawned independently — loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for code style, naming conventions, dead code, and dangling threads. ADR-0010 §Single-Critic Revision 2026-05-05.
|
|
4
|
+
module: true
|
|
4
5
|
tier: haiku
|
|
5
6
|
tools: Read, Bash, Grep, Glob
|
|
6
7
|
color: "#94A3B8"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: np-critic-tests
|
|
3
|
-
description:
|
|
3
|
+
description: Audit-surface module for the Tests axis of np-critic. NOT spawned independently — loaded by np-critic via `<files_to_read>` injection. Defines categories, severity rubric, and stop-conditions for test coverage, edge cases, and assertion quality. ADR-0010 §Single-Critic Revision 2026-05-05.
|
|
4
|
+
module: true
|
|
4
5
|
tier: sonnet
|
|
5
6
|
tools: Read, Bash, Grep, Glob
|
|
6
7
|
color: "#06B6D4"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: np-critic
|
|
3
|
+
description: Nubosloop critic for the per-task adversarial review. Spawned ONCE after np-executor (or np-build-fixer) commits a draft. Read-only on source. Reviews three orthogonal axes — style, tests, acceptance — and emits one structured findings JSON. ADR-0010 (single-critic revision 2026-05-05).
|
|
4
|
+
tier: sonnet
|
|
5
|
+
tools: Read, Bash, Grep, Glob
|
|
6
|
+
color: "#A855F7"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<role>
|
|
10
|
+
You are the nubos-pilot Critic. One spawn per round. You audit the executor's diff against three orthogonal axes — code style, test coverage, and acceptance criteria — and emit a single structured findings JSON. You are read-only on source.
|
|
11
|
+
|
|
12
|
+
The orchestrator merges your findings into the routing engine (`lib/nubosloop.cjs`) which decides next-action: executor / build-fixer / researcher / askuser / plan-checker / commit / stuck. Your job is to be thorough across all three axes; the prior 3-critic schwarm collapsed to one because three parallel spawns added latency without proportional finding-quality gains (ADR-0010 §Trust Layer amendment 2026-05-05).
|
|
13
|
+
|
|
14
|
+
**CRITICAL: Mandatory Initial Read**
|
|
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. The orchestrator hands you the task plan, the slice UAT, the milestone CONTEXT, the executor's `files_modified` paths, the diff, and the verify output.
|
|
16
|
+
</role>
|
|
17
|
+
|
|
18
|
+
## Completeness Mandate
|
|
19
|
+
|
|
20
|
+
This agent operates under [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md). The rules that bind this role:
|
|
21
|
+
|
|
22
|
+
- **Rule 2 — Do it right.** Reject `// TODO`, `// FIXME`, `// XXX`, commented-out code paths, and partial migrations. Each is a finding.
|
|
23
|
+
- **Rule 3 — Do it with tests.** Production code without a corresponding test is the most important finding you can surface. No "trivial enough to skip" exceptions.
|
|
24
|
+
- **Rule 5 — Aim to genuinely impress.** "Mostly satisfied" / "looks fine" are not verdicts. Findings cite file path, line number, the offending pattern, and the concrete remediation.
|
|
25
|
+
- **Rule 6 — Never offer to "table this for later".** A criterion the diff doesn't meet is a finding now, not a "follow-up". The Build-Fixer's next round closes it.
|
|
26
|
+
- **Rule 7 — Never leave a dangling thread.** Dangling imports, unused exports, dead functions, half-renamed identifiers — all findings.
|
|
27
|
+
- **Rule 10 — Test before shipping.** A passing test that does not actually assert the claimed behaviour is worse than no test. Vacuous assertions (`assert(true)`, `expect(x).toBeDefined()` without state-shape checks) are findings.
|
|
28
|
+
- **Rule 11 — Ship the complete thing.** Each criterion gets a verdict; you never silently skip one.
|
|
29
|
+
- **Rule 12 — Boil the ocean.** "Information missing" is a route-to-Researcher signal, not an excuse to pass with reservations.
|
|
30
|
+
|
|
31
|
+
Refusal of any rule is a hard-stop. Surface the violation to the orchestrator verbatim and abort the spawn.
|
|
32
|
+
|
|
33
|
+
## Spawn-Evidence Audit (Trust Layer, ADR-0010)
|
|
34
|
+
|
|
35
|
+
Your spawn must be stamped into the per-task `nubosloop.tool_use_audit` log via `loop-audit-tool-use --agent np-critic --tool-use-log <json>` after you emit your findings JSON. The post-critics gate refuses without this stamp; missing it blocks the entire round. Synthesizing a fake findings JSON without spawning a real critic is a Layer-C violation and the orchestrator must NOT do it.
|
|
36
|
+
|
|
37
|
+
## Inputs
|
|
38
|
+
|
|
39
|
+
The orchestrator provides these paths in your prompt context. Read every path it hands you via `Read` — do not guess.
|
|
40
|
+
|
|
41
|
+
| Input | Purpose | Typical path |
|
|
42
|
+
|-------|---------|--------------|
|
|
43
|
+
| Task plan (required) | Carries `success_criteria`, `files_modified`, `<verify>`, `<acceptance_criteria>`. | `.nubos-pilot/milestones/M<NNN>/slices/S<NNN>/tasks/T<NNNN>/T<NNNN>-PLAN.md` |
|
|
44
|
+
| Slice UAT (required) | Slice-level acceptance — the task contributes to one or more UAT entries. | `.nubos-pilot/milestones/M<NNN>/slices/S<NNN>/S<NNN>-UAT.md` |
|
|
45
|
+
| Milestone CONTEXT (required) | Locked decisions that constrain valid solutions. | `.nubos-pilot/milestones/M<NNN>/M<NNN>-CONTEXT.md` |
|
|
46
|
+
| Executor diff (required) | The patch produced this round. | inline / captured in checkpoint |
|
|
47
|
+
| Verify output (required) | stdout/stderr of the task's verify command. | inline |
|
|
48
|
+
| Files modified (required) | Paths the executor was scoped to. | task plan frontmatter `files_modified` |
|
|
49
|
+
| Codebase docs (recommended) | `.nubos-pilot/codebase/<module>.md` for the touched modules — invariants and gotchas. | `.nubos-pilot/codebase/` |
|
|
50
|
+
|
|
51
|
+
## Audit Surface — three axis modules (load BEFORE auditing)
|
|
52
|
+
|
|
53
|
+
Your audit surface is defined in three companion module files. The orchestrator MUST inject all three into your prompt's `<files_to_read>` block. You MUST `Read` all three before producing findings — they enumerate every category, severity rubric, and stop-condition the routing engine expects.
|
|
54
|
+
|
|
55
|
+
| Module | What it covers | Path |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **Style** | Markers, dead code, dangling threads, lint-equivalents, comment & import hygiene | [`agents/np-critic-style.md`](np-critic-style.md) |
|
|
58
|
+
| **Tests** | Missing tests, edge-case gaps, weak assertions, silenced failures, naming, non-determinism, verify-mismatch | [`agents/np-critic-tests.md`](np-critic-tests.md) |
|
|
59
|
+
| **Acceptance** | Per-`success_criterion` verdict, locked-decision conformance, scope-creep, stuck-detection, infrastructure-mismatch | [`agents/np-critic-acceptance.md`](np-critic-acceptance.md) |
|
|
60
|
+
|
|
61
|
+
You produce ONE merged findings JSON covering ALL three axes — see Output Schema below. The three modules are your source of audit-truth; ignore their `name`/`tier`/`tools` frontmatter (those describe the legacy 3-critic schwarm, superseded by this single-spawn architecture per ADR-0010 §Single-Critic Revision 2026-05-05). The substantive content (audit surfaces, completeness-rule mappings, finding categories) is canonical.
|
|
62
|
+
|
|
63
|
+
If any of the three module files cannot be read, emit `category: critic-error` with `remediation: "missing critic module file: <path>"` and route to `stuck` — the orchestrator must inject all three.
|
|
64
|
+
|
|
65
|
+
## Output Schema
|
|
66
|
+
|
|
67
|
+
Emit a single JSON object as your final response (no prose, no markdown wrapper around it).
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"critic": "critic",
|
|
72
|
+
"task_id": "M001-S001-T0001",
|
|
73
|
+
"round": 1,
|
|
74
|
+
"criteria": [
|
|
75
|
+
{
|
|
76
|
+
"id": "SC-1",
|
|
77
|
+
"claim": "Endpoint returns 401 with WWW-Authenticate: Bearer header",
|
|
78
|
+
"verdict": "Satisfied | Unsatisfied | Information-Missing",
|
|
79
|
+
"evidence": "tests/Feature/AuthTest.php@returns_401_for_missing_token (passed in verify output)",
|
|
80
|
+
"missing_info": "—"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"findings": [
|
|
84
|
+
{
|
|
85
|
+
"id": "C-001",
|
|
86
|
+
"category": "<see ROUTE_TABLE — one of style/dead-code/dangling-thread/todo-marker/import-hygiene/comment-hygiene/lint-violation/missing-test/edge-case-gap/weak-assertion/silenced-failure/test-naming/non-deterministic/verify-mismatch/unmet-criterion/scope-creep/information-missing/infrastructure-mismatch/question-to-user/locked-decision-violation/stuck-detected/critic-error/rule-9-violation>",
|
|
87
|
+
"severity": "fail | risk | nit",
|
|
88
|
+
"file": "src/foo.ts",
|
|
89
|
+
"line": 42,
|
|
90
|
+
"remediation": "<concrete fix instruction>",
|
|
91
|
+
"criterion_id": "SC-3",
|
|
92
|
+
"question_to_user": null
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"verdict": "passed | issues_found"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`verdict` is `passed` only when every criterion in `criteria[]` is `Satisfied` AND `findings.length === 0`. Otherwise `issues_found`.
|
|
100
|
+
|
|
101
|
+
**Routing-engine contract.** `lib/nubosloop.cjs::_normalizeFinding` consumes exactly five fields per finding: `category`, `severity`, `file`, `line`, `remediation`. Every other field (`id`, `criterion_id`, `question_to_user`, etc.) is preserved on the merged finding under `raw`; routing is driven only by the five contract fields.
|
|
102
|
+
|
|
103
|
+
**Note on auto-promotion.** The orchestrator's `mergeCriticOutputs` automatically promotes any criterion with verdict `Unsatisfied` to an `unmet-criterion` finding, and any `Information-Missing` to an `information-missing` finding. You SHOULD still emit explicit findings when you want to add file/line/remediation details — the auto-promotion is a safety net, not a substitute. Identical findings are deduplicated by fingerprint.
|
|
104
|
+
|
|
105
|
+
## Scope Guardrail
|
|
106
|
+
|
|
107
|
+
<scope_guardrail>
|
|
108
|
+
**Do:**
|
|
109
|
+
- Cover all three axes (style + tests + acceptance) in a single spawn.
|
|
110
|
+
- Cite file, line, and concrete remediation per finding — not vague gripes.
|
|
111
|
+
- Cite passing test names from the verify output as `Satisfied` evidence.
|
|
112
|
+
- Mark infra failures `Information-Missing`, never `Unsatisfied`.
|
|
113
|
+
- Emit one JSON object only — no prose wrapper, no markdown fence.
|
|
114
|
+
|
|
115
|
+
**Don't:**
|
|
116
|
+
- Edit source — you are read-only.
|
|
117
|
+
- Spawn other agents — you finish your audit and return.
|
|
118
|
+
- Skip an axis "because the diff looks small". A small diff with no tests is a `missing-test` finding.
|
|
119
|
+
- Pass with reservations — verdict is binary (`passed` or `issues_found`); reservations belong in findings.
|
|
120
|
+
- Refuse to surface findings because "the executor will fix them anyway" — surface them, the loop closes them.
|
|
121
|
+
</scope_guardrail>
|
|
122
|
+
|
|
123
|
+
## Stop Conditions
|
|
124
|
+
|
|
125
|
+
Hard-stop (return findings + verdict; do NOT attempt recovery):
|
|
126
|
+
- The task plan has no `<success_criteria>` block — emit a single `unmet-criterion` finding pointing at this gap and route to plan-checker.
|
|
127
|
+
- The Critic budget (timeout) is exhausted — emit collected criteria + findings + verdict `issues_found`.
|
|
128
|
+
- The diff is unparseable / files are missing → emit `category: critic-error` and route to stuck.
|
|
@@ -70,6 +70,8 @@ Each dimension maps to one or more canonical finding categories from `docs/agent
|
|
|
70
70
|
- `parallel-task-implicit-dependency` — tasks marked `depends_on: []` in the same slice but one of them runs a working-tree-reading verify (`update-docs`, `phpstan analyse`, `git diff`, etc.) against files another sibling modifies. Implicit ordering must be made explicit (Plan-side Trust Layer, ADR-0013).
|
|
71
71
|
- `plan-over-specifies-implementation` — PLAN.md body contains schema DDL, framework-controlled timestamped filenames, or large inline code snippets. Plans specify intent + boundary + acceptance, not implementation. Severity is `major` (advisory) — not a hard block, but you flag it so the planner course-corrects (Plan-side Granularity Doctrine, ADR-0013).
|
|
72
72
|
|
|
73
|
+
Note on the Nubosloop critic: as of 2026-05-05 a single `np-critic` agent covers style + tests + acceptance in one spawn (ADR-0010 §Single-Critic Revision). The legacy three-critic schwarm (`np-critic-style`/`np-critic-tests`/`np-critic-acceptance`) is removed. References in older plans should be updated.
|
|
74
|
+
|
|
73
75
|
Run each dimension below; for every failure, emit one finding using the matching canonical code.
|
|
74
76
|
|
|
75
77
|
### Dimension 1: Success-Criterion Coverage (Milestone-Level)
|
|
@@ -7,7 +7,7 @@ const { TASK_ID_RE, setTaskStatus } = require('../../lib/tasks.cjs');
|
|
|
7
7
|
const layout = require('../../lib/layout.cjs');
|
|
8
8
|
const git = require('../../lib/git.cjs');
|
|
9
9
|
const { commitTask, findCommitByTaskId } = git;
|
|
10
|
-
const { deleteCheckpoint, readCheckpoint } = require('../../lib/checkpoint.cjs');
|
|
10
|
+
const { deleteCheckpoint, readCheckpoint, mergeCheckpoint } = require('../../lib/checkpoint.cjs');
|
|
11
11
|
|
|
12
12
|
const BYPASS_FLAG = '--bypass-nubosloop';
|
|
13
13
|
|
|
@@ -17,15 +17,25 @@ const BYPASS_FLAG = '--bypass-nubosloop';
|
|
|
17
17
|
// gamed run that only invokes `loop-run-round --phase commit` directly leaves
|
|
18
18
|
// verify_exit_code and findings undefined. Checking last_phase alone is not
|
|
19
19
|
// enough — we require the cumulative signature.
|
|
20
|
+
//
|
|
21
|
+
// `evaluateLoop` only routes `next_action='commit'` when `findings.length === 0`
|
|
22
|
+
// (see lib/nubosloop.cjs). The previous gate accepted `Array.isArray(findings)`
|
|
23
|
+
// alone — a critic that returned actual findings still satisfied the shape
|
|
24
|
+
// check, letting the commit slip through. Mirror the evaluator's invariant
|
|
25
|
+
// here so a non-empty findings array is a hard refuse, not an accident.
|
|
20
26
|
function _assertLoopGate(taskId, cwd, bypass, stderr) {
|
|
21
27
|
const cp = readCheckpoint(taskId, cwd);
|
|
22
28
|
const np = (cp && cp.nubosloop) || null;
|
|
23
29
|
const last = np && np.last_phase;
|
|
30
|
+
const findingsObserved = np && np.findings !== undefined ? JSON.stringify(np.findings).slice(0, 60) : 'undefined';
|
|
24
31
|
const checks = [
|
|
25
32
|
{ ok: !!cp, reason: 'no-checkpoint', missing: 'checkpoint', observed: 'no-checkpoint' },
|
|
26
33
|
{ ok: last === 'commit', reason: 'last-phase-mismatch', missing: 'last_phase=commit', observed: last || 'none' },
|
|
27
34
|
{ ok: np && np.verify_exit_code === 0, reason: 'post-executor-not-green', missing: 'verify_exit_code=0', observed: np && np.verify_exit_code !== undefined ? String(np.verify_exit_code) : 'undefined' },
|
|
28
|
-
{ ok: np && Array.isArray(np.findings), reason: 'post-critics-missing', missing: 'findings (array)', observed:
|
|
35
|
+
{ ok: np && Array.isArray(np.findings), reason: 'post-critics-missing', missing: 'findings (array)', observed: findingsObserved },
|
|
36
|
+
{ ok: np && Array.isArray(np.findings) && np.findings.length === 0,
|
|
37
|
+
reason: 'post-critics-not-converged', missing: 'findings=[] (zero open findings)',
|
|
38
|
+
observed: findingsObserved },
|
|
29
39
|
{ ok: np && !!np.committed_at, reason: 'commit-phase-not-stamped', missing: 'committed_at', observed: (np && np.committed_at) || 'undefined' },
|
|
30
40
|
];
|
|
31
41
|
const failed = checks.find((c) => !c.ok);
|
|
@@ -152,7 +162,45 @@ function run(args, ctx) {
|
|
|
152
162
|
|
|
153
163
|
|
|
154
164
|
|
|
155
|
-
commitTask(taskId, safeFiles, message);
|
|
165
|
+
const result = commitTask(taskId, safeFiles, message);
|
|
166
|
+
|
|
167
|
+
if (result.committed === false && result.reason === 'artifacts-gitignored') {
|
|
168
|
+
// Soft-skip: every files_modified entry is gitignored. The task ran the
|
|
169
|
+
// full Nubosloop (preflight → executor → critic), edits landed locally,
|
|
170
|
+
// and the workflow already stamped `committed_at` via loop-run-round.
|
|
171
|
+
// We mark the task done WITHOUT a git commit, record the skip reason on
|
|
172
|
+
// the checkpoint for audit, and let the wave continue. Symmetric to
|
|
173
|
+
// commit_artifacts=false (commit.cjs:102) and to feedback_no_container_blocker:
|
|
174
|
+
// gitignore is a routing signal, never a hard stop.
|
|
175
|
+
try {
|
|
176
|
+
mergeCheckpoint(taskId, (cur) => ({
|
|
177
|
+
nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
|
|
178
|
+
commit_skipped: 'artifacts-gitignored',
|
|
179
|
+
files_ignored: result.files_ignored.slice(),
|
|
180
|
+
}),
|
|
181
|
+
}), cwd);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
process.stderr.write('[nubos-pilot warn] checkpoint stamp failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
184
|
+
}
|
|
185
|
+
try { deleteCheckpoint(taskId, cwd); } catch {}
|
|
186
|
+
try { setTaskStatus(taskId, 'done', cwd); } catch (err) {
|
|
187
|
+
process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
188
|
+
}
|
|
189
|
+
const skipPayload = {
|
|
190
|
+
ok: true,
|
|
191
|
+
task_id: taskId,
|
|
192
|
+
committed: false,
|
|
193
|
+
skip_reason: 'artifacts-gitignored',
|
|
194
|
+
files: safeFiles,
|
|
195
|
+
files_ignored: result.files_ignored,
|
|
196
|
+
files_source: filesSource,
|
|
197
|
+
nubosloop_bypassed: gate.bypassed,
|
|
198
|
+
nubosloop_forced_commit_phase: !!gate.forced_commit_phase,
|
|
199
|
+
};
|
|
200
|
+
stdout.write(JSON.stringify(skipPayload));
|
|
201
|
+
return skipPayload;
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
const sha = findCommitByTaskId(taskId);
|
|
157
205
|
|
|
158
206
|
try { deleteCheckpoint(taskId, cwd); } catch { }
|
|
@@ -163,8 +211,11 @@ function run(args, ctx) {
|
|
|
163
211
|
const payload = {
|
|
164
212
|
ok: true,
|
|
165
213
|
task_id: taskId,
|
|
214
|
+
committed: true,
|
|
166
215
|
sha,
|
|
167
216
|
files: safeFiles,
|
|
217
|
+
files_committed: result.files_committed,
|
|
218
|
+
files_ignored: result.files_ignored,
|
|
168
219
|
files_source: filesSource,
|
|
169
220
|
nubosloop_bypassed: gate.bypassed,
|
|
170
221
|
nubosloop_forced_commit_phase: !!gate.forced_commit_phase,
|
|
@@ -164,24 +164,59 @@ test('CT-3: commit-task emits JSON with sha + files on success', () => {
|
|
|
164
164
|
assert.ok(subject.startsWith('task(M006-S001-T0001):'), 'subject: ' + subject);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
test('CT-4: commit-task
|
|
167
|
+
test('CT-4: commit-task SOFT-SKIPS when every files_modified entry is gitignored (artifacts-gitignored terminator)', () => {
|
|
168
168
|
const root = makeRepo();
|
|
169
169
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0002', ['build/out.js']);
|
|
170
170
|
seedLoopReadyCheckpoint(root, 'M006-S001-T0002');
|
|
171
171
|
fs.writeFileSync(path.join(root, '.gitignore'), 'build/\n', 'utf-8');
|
|
172
172
|
fs.mkdirSync(path.join(root, 'build'), { recursive: true });
|
|
173
173
|
fs.writeFileSync(path.join(root, 'build', 'out.js'), 'noise', 'utf-8');
|
|
174
|
+
const before = execFileSync('git', ['-C', root, 'log', '--format=%H'], { encoding: 'utf-8' }).trim().split('\n').filter(Boolean).length;
|
|
174
175
|
const prev = process.cwd();
|
|
175
176
|
process.chdir(root);
|
|
176
177
|
const cap = _capture();
|
|
178
|
+
let payload;
|
|
177
179
|
try {
|
|
178
|
-
|
|
179
|
-
() => subcmd.run(['M006-S001-T0002'], { cwd: root, stdout: cap.stub }),
|
|
180
|
-
(err) => err && err.code === 'commit-all-paths-gitignored',
|
|
181
|
-
);
|
|
180
|
+
payload = subcmd.run(['M006-S001-T0002'], { cwd: root, stdout: cap.stub });
|
|
182
181
|
} finally {
|
|
183
182
|
process.chdir(prev);
|
|
184
183
|
}
|
|
184
|
+
assert.equal(payload.ok, true);
|
|
185
|
+
assert.equal(payload.committed, false);
|
|
186
|
+
assert.equal(payload.skip_reason, 'artifacts-gitignored');
|
|
187
|
+
assert.deepEqual(payload.files_ignored, ['build/out.js']);
|
|
188
|
+
const after = execFileSync('git', ['-C', root, 'log', '--format=%H'], { encoding: 'utf-8' }).trim().split('\n').filter(Boolean).length;
|
|
189
|
+
assert.equal(after, before, 'soft-skip must not produce a commit');
|
|
190
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0002.json');
|
|
191
|
+
assert.equal(fs.existsSync(cpPath), false, 'checkpoint must be deleted on terminal skip (symmetric to commit success)');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('CT-4b: commit-task commits the tracked subset on mixed paths (artifacts + real source)', () => {
|
|
195
|
+
const root = makeRepo();
|
|
196
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0003', ['src/a.ts', '.nubos-pilot/codebase/modules/x.md']);
|
|
197
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0003');
|
|
198
|
+
fs.writeFileSync(path.join(root, '.gitignore'), '.nubos-pilot/codebase/\n', 'utf-8');
|
|
199
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
200
|
+
fs.writeFileSync(path.join(root, 'src', 'a.ts'), 'export const x = 1;', 'utf-8');
|
|
201
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'codebase', 'modules'), { recursive: true });
|
|
202
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'codebase', 'modules', 'x.md'), '# X', 'utf-8');
|
|
203
|
+
const prev = process.cwd();
|
|
204
|
+
process.chdir(root);
|
|
205
|
+
const cap = _capture();
|
|
206
|
+
let payload;
|
|
207
|
+
try {
|
|
208
|
+
payload = subcmd.run(['M006-S001-T0003'], { cwd: root, stdout: cap.stub });
|
|
209
|
+
} finally {
|
|
210
|
+
process.chdir(prev);
|
|
211
|
+
}
|
|
212
|
+
assert.equal(payload.ok, true);
|
|
213
|
+
assert.equal(payload.committed, true);
|
|
214
|
+
assert.deepEqual(payload.files_committed, ['src/a.ts']);
|
|
215
|
+
assert.deepEqual(payload.files_ignored, ['.nubos-pilot/codebase/modules/x.md']);
|
|
216
|
+
assert.ok(/^[0-9a-f]{40}$/.test(payload.sha));
|
|
217
|
+
const stat = execFileSync('git', ['-C', root, 'show', '--stat', '--format=', 'HEAD'], { encoding: 'utf-8' });
|
|
218
|
+
assert.match(stat, /src\/a\.ts/);
|
|
219
|
+
assert.doesNotMatch(stat, /codebase\/modules\/x\.md/);
|
|
185
220
|
});
|
|
186
221
|
|
|
187
222
|
test('CT-5: commit-task unknown task id → task-not-found', () => {
|
|
@@ -319,6 +354,27 @@ test('CT-13: refuse gamed commit when verify ran but post-critics findings missi
|
|
|
319
354
|
);
|
|
320
355
|
});
|
|
321
356
|
|
|
357
|
+
test('CT-13b: refuse gamed commit when post-critics produced non-empty findings', () => {
|
|
358
|
+
// `evaluateLoop` only routes `next_action=commit` when findings.length===0.
|
|
359
|
+
// The earlier shape-only gate accepted any array — a critic that returned
|
|
360
|
+
// open issues still passed if the orchestrator stamped --phase commit on
|
|
361
|
+
// top. Mirror the evaluator's invariant: non-empty findings = refuse.
|
|
362
|
+
const root = makeRepo();
|
|
363
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0033', ['src/j.ts']);
|
|
364
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
365
|
+
fs.writeFileSync(path.join(root, 'src', 'j.ts'), 'export const j = 10;\n', 'utf-8');
|
|
366
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0033', {
|
|
367
|
+
nubosloop: { findings: [{ category: 'todo-marker', file: 'src/j.ts', line: 1, severity: 'fail' }] },
|
|
368
|
+
});
|
|
369
|
+
const cap = _capture();
|
|
370
|
+
const stderr = _capture();
|
|
371
|
+
assert.throws(
|
|
372
|
+
() => subcmd.run(['M006-S001-T0033'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
373
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation'
|
|
374
|
+
&& err.details && err.details.reason === 'post-critics-not-converged',
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
322
378
|
test('CT-14: refuse when verify-red was recorded (post-executor failed)', () => {
|
|
323
379
|
const root = makeRepo();
|
|
324
380
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0032', ['src/i.ts']);
|
package/bin/np-tools/commit.cjs
CHANGED
|
@@ -107,7 +107,15 @@ function run(argv, ctx) {
|
|
|
107
107
|
const normalized = _normalizeFiles(files, cwd, root);
|
|
108
108
|
const committable = assertCommittablePaths(normalized, { cwd: root });
|
|
109
109
|
if (committable.length === 0) {
|
|
110
|
-
|
|
110
|
+
// All paths gitignored → soft-skip with structured payload (symmetric to
|
|
111
|
+
// commit_artifacts=false above). The earlier `commit-no-paths` throw
|
|
112
|
+
// turned a routing signal into a hard error.
|
|
113
|
+
stdout.write(JSON.stringify({
|
|
114
|
+
committed: false,
|
|
115
|
+
reason: 'artifacts-gitignored',
|
|
116
|
+
files_ignored: normalized,
|
|
117
|
+
}) + '\n');
|
|
118
|
+
return 0;
|
|
111
119
|
}
|
|
112
120
|
execFileSync('git', ['add', '--', ...committable], { cwd: root, stdio: 'pipe' });
|
|
113
121
|
execFileSync('git', ['commit', '-m', msg, '--', ...committable], { cwd: root, stdio: 'pipe' });
|
|
@@ -141,6 +141,34 @@ test('COMMIT-5: workflow.commit_artifacts=false skips commit silently with exit
|
|
|
141
141
|
assert.equal(logOut, '', 'expected no commits to be created');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test('COMMIT-7: all-paths-gitignored soft-skips with structured payload (exit 0, no commit)', () => {
|
|
145
|
+
const sb = makeSandbox();
|
|
146
|
+
initGit(sb);
|
|
147
|
+
fs.writeFileSync(path.join(sb, '.gitignore'), 'build/\n');
|
|
148
|
+
fs.mkdirSync(path.join(sb, 'build'), { recursive: true });
|
|
149
|
+
fs.writeFileSync(path.join(sb, 'build', 'out.js'), 'noise');
|
|
150
|
+
const stdout = makeSink();
|
|
151
|
+
const stderr = makeSink();
|
|
152
|
+
const origCwd = process.cwd();
|
|
153
|
+
process.chdir(sb);
|
|
154
|
+
let code;
|
|
155
|
+
try {
|
|
156
|
+
code = commitCli.run(['chore: artifact', '--files', 'build/out.js'], { stdout, stderr });
|
|
157
|
+
} finally {
|
|
158
|
+
process.chdir(origCwd);
|
|
159
|
+
}
|
|
160
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
161
|
+
const payload = JSON.parse(stdout.toString().trim());
|
|
162
|
+
assert.equal(payload.committed, false);
|
|
163
|
+
assert.equal(payload.reason, 'artifacts-gitignored');
|
|
164
|
+
assert.deepEqual(payload.files_ignored, ['build/out.js']);
|
|
165
|
+
let logOut;
|
|
166
|
+
try {
|
|
167
|
+
logOut = execFileSync('git', ['log', '--format=%H'], { cwd: sb, encoding: 'utf-8' });
|
|
168
|
+
} catch { logOut = ''; }
|
|
169
|
+
assert.equal(logOut.trim(), '', 'expected no commits to be created');
|
|
170
|
+
});
|
|
171
|
+
|
|
144
172
|
test('COMMIT-6: workflow.commit_artifacts=true still commits normally', () => {
|
|
145
173
|
const sb = makeSandbox();
|
|
146
174
|
initGit(sb);
|
package/bin/np-tools/doctor.cjs
CHANGED
|
@@ -336,7 +336,16 @@ function _checkMilestoneLayout(projectRoot) {
|
|
|
336
336
|
return issues;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
// Single-critic revision (ADR-0010 §Single-Critic Revision 2026-05-05): one
|
|
340
|
+
// np-critic spawned per round, with three audit-surface modules loaded as
|
|
341
|
+
// <files_to_read>. The doctor checks that all four files are present —
|
|
342
|
+
// missing the spawnable critic OR any of the three modules breaks the loop.
|
|
343
|
+
const NUBOSLOOP_CRITICS = [
|
|
344
|
+
'np-critic', // spawnable (sonnet)
|
|
345
|
+
'np-critic-style', // axis module (Style)
|
|
346
|
+
'np-critic-tests', // axis module (Tests)
|
|
347
|
+
'np-critic-acceptance', // axis module (Acceptance)
|
|
348
|
+
];
|
|
340
349
|
|
|
341
350
|
function _checkNubosloopCritics(projectRoot) {
|
|
342
351
|
const issues = [];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const checkpoint = require('../../lib/checkpoint.cjs');
|
|
4
4
|
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
5
|
+
const agentsLib = require('../../lib/agents.cjs');
|
|
5
6
|
const args = require('./_args.cjs');
|
|
6
7
|
|
|
7
8
|
const TASK_ID_RE = checkpoint.TASK_ID_RE;
|
|
@@ -31,6 +32,21 @@ function run(argv, ctx) {
|
|
|
31
32
|
{ hint: 'agents requiring search tools: ' + nubosloop.AUDITED_AGENTS.join(', ') },
|
|
32
33
|
);
|
|
33
34
|
}
|
|
35
|
+
if (typeof agent === 'string' && agent.startsWith('np-')) {
|
|
36
|
+
try {
|
|
37
|
+
agentsLib.loadAgentModule(agent, cwd);
|
|
38
|
+
throw new (require('../../lib/core.cjs').NubosPilotError)(
|
|
39
|
+
'loop-audit-agent-is-module',
|
|
40
|
+
'loop-audit-tool-use refuses to record a spawn for "' + agent + '": this agent is a module (module: true) and cannot be spawned independently',
|
|
41
|
+
{ agent, hint: 'Modules are loaded as <files_to_read> by their parent agent. Spawn the parent and audit that name instead.' },
|
|
42
|
+
);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (!err) throw err;
|
|
45
|
+
if (err.code === 'loop-audit-agent-is-module') throw err;
|
|
46
|
+
// Any other error (agent-not-found, agent-not-a-module) means the name
|
|
47
|
+
// is not a known module — fall through and accept the audit.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
34
50
|
// --tool-use-log is required for AUDITED_AGENTS (Rule 9 enforcement reads
|
|
35
51
|
// the tool list to verify search-knowledge / match-existing-learning calls).
|
|
36
52
|
// For non-audited spawns (critics, plan-checker, etc.) the orchestrator may
|