@njeybe/nativ 2.3.0 → 2.3.1
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
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Studio Dispatch on Windows: task text with quotes or symbols like > could cut off the agent's instructions and send its output to a stray file.
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- The simulation endpoints (`/api/pipeline/simulation/start` and `/stop`) that fed the old Workflow Canvas.
|
|
12
|
+
|
|
3
13
|
## 2.3.0
|
|
4
14
|
|
|
5
15
|
### Changed
|
|
@@ -209,14 +209,16 @@ export declare function buildRunnerPermissions(task: Pick<MasterPlanTask, 'verif
|
|
|
209
209
|
/** Where a task's runner settings live: a subdirectory, because run history parses every top-level .json in runs/. */
|
|
210
210
|
export declare function runnerSettingsPath(runsDir: string, taskId: string): string;
|
|
211
211
|
export declare function writeRunnerSettings(runsDir: string, taskId: string, settings: RunnerPermissions): string;
|
|
212
|
+
/** The instructions text handed to the agent on its standard input. */
|
|
213
|
+
export declare function buildRunnerPrompt(task: MasterPlanTask): string;
|
|
212
214
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
+
* Arguments for Claude Code CLI to run headless. The prompt goes on stdin, never in this string:
|
|
216
|
+
* quotes or symbols in task text would break the shell.
|
|
215
217
|
*
|
|
216
218
|
* It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
|
|
217
219
|
* permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
|
|
218
220
|
*/
|
|
219
|
-
export declare function buildDefaultClaudeCommand(
|
|
221
|
+
export declare function buildDefaultClaudeCommand(_task: MasterPlanTask, model?: string | null, settingsFile?: string): string;
|
|
220
222
|
/** True when a runner command asks Claude Code for its line-delimited JSON event stream. */
|
|
221
223
|
export declare function usesClaudeStreamJson(command: string): boolean;
|
|
222
224
|
/** Final `result` event of a Claude Code stream: grounded usage for cli runs. */
|
|
@@ -213,15 +213,9 @@ export function writeRunnerSettings(runsDir, taskId, settings) {
|
|
|
213
213
|
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
214
214
|
return file;
|
|
215
215
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*
|
|
220
|
-
* It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
|
|
221
|
-
* permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
|
|
222
|
-
*/
|
|
223
|
-
export function buildDefaultClaudeCommand(task, model, settingsFile) {
|
|
224
|
-
const parts = [
|
|
216
|
+
/** The instructions text handed to the agent on its standard input. */
|
|
217
|
+
export function buildRunnerPrompt(task) {
|
|
218
|
+
return [
|
|
225
219
|
`Execute task ${task.id} (${task.title}).`,
|
|
226
220
|
task.description ? `Description: ${task.description}.` : '',
|
|
227
221
|
task.verificationCommand ? `Verify your work using: ${task.verificationCommand}.` : '',
|
|
@@ -230,11 +224,19 @@ export function buildDefaultClaudeCommand(task, model, settingsFile) {
|
|
|
230
224
|
]
|
|
231
225
|
.filter(Boolean)
|
|
232
226
|
.join(' ');
|
|
233
|
-
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Arguments for Claude Code CLI to run headless. The prompt goes on stdin, never in this string:
|
|
230
|
+
* quotes or symbols in task text would break the shell.
|
|
231
|
+
*
|
|
232
|
+
* It runs in `acceptEdits` mode with the rules in `settingsFile` (see buildRunnerPermissions); it does not bypass
|
|
233
|
+
* permissions. Without a settings file only the file-edit tools and read-only commands are pre-approved.
|
|
234
|
+
*/
|
|
235
|
+
export function buildDefaultClaudeCommand(_task, model, settingsFile) {
|
|
234
236
|
const modelFlag = model ? ` --model ${model}` : '';
|
|
235
237
|
// Plain `claude -p` prints nothing until it finishes; stream-json emits every message and tool call as it happens.
|
|
236
238
|
const settingsFlag = settingsFile ? ` --settings "${settingsFile}"` : '';
|
|
237
|
-
return `claude -p
|
|
239
|
+
return `claude -p${modelFlag} --output-format stream-json --verbose --permission-mode acceptEdits${settingsFlag}`;
|
|
238
240
|
}
|
|
239
241
|
/** True when a runner command asks Claude Code for its line-delimited JSON event stream. */
|
|
240
242
|
export function usesClaudeStreamJson(command) {
|
|
@@ -1092,7 +1094,7 @@ export class AgentSupervisor extends EventEmitter {
|
|
|
1092
1094
|
// stdio pipes, so we stay attached and let `taskkill /T` walk the tree.
|
|
1093
1095
|
detached: process.platform !== 'win32',
|
|
1094
1096
|
windowsHide: true,
|
|
1095
|
-
stdio: ['
|
|
1097
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1096
1098
|
env: {
|
|
1097
1099
|
...buildRunnerEnv(process.env, options.env),
|
|
1098
1100
|
...(record.thinking?.budgetTokens ? { MAX_THINKING_TOKENS: String(record.thinking.budgetTokens) } : {}),
|
|
@@ -1103,6 +1105,9 @@ export class AgentSupervisor extends EventEmitter {
|
|
|
1103
1105
|
});
|
|
1104
1106
|
run.child = child;
|
|
1105
1107
|
record.pid = child.pid ?? null;
|
|
1108
|
+
// A runner that exits without reading its input must not crash us or fail the run.
|
|
1109
|
+
child.stdin?.on('error', () => { });
|
|
1110
|
+
child.stdin?.end(buildRunnerPrompt(task), 'utf8');
|
|
1106
1111
|
child.stdout?.on('data', (chunk) => {
|
|
1107
1112
|
if (run.streamJson)
|
|
1108
1113
|
this.consumeStreamJson(run, chunk.toString());
|
|
@@ -1580,128 +1580,6 @@ class PipelineEventHub {
|
|
|
1580
1580
|
res.write(frame);
|
|
1581
1581
|
}
|
|
1582
1582
|
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Interactive simulation engine for Mission Control Studio.
|
|
1585
|
-
* Broadcasts realistic multi-agent execution steps over the SSE stream
|
|
1586
|
-
* without modifying workspace files or invoking external model APIs.
|
|
1587
|
-
*/
|
|
1588
|
-
class SimulationController {
|
|
1589
|
-
events;
|
|
1590
|
-
activeTimer = null;
|
|
1591
|
-
running = false;
|
|
1592
|
-
currentId = null;
|
|
1593
|
-
constructor(events) {
|
|
1594
|
-
this.events = events;
|
|
1595
|
-
}
|
|
1596
|
-
start(body = {}) {
|
|
1597
|
-
this.stop();
|
|
1598
|
-
const id = `sim-${Date.now()}`;
|
|
1599
|
-
this.currentId = id;
|
|
1600
|
-
this.running = true;
|
|
1601
|
-
const speed = Math.max(1, Math.min(5, Number(body.speedMultiplier) || 1));
|
|
1602
|
-
const scenario = String(body.scenario || 'default');
|
|
1603
|
-
const steps = [
|
|
1604
|
-
// Step 1: Foundation task (qa-tester)
|
|
1605
|
-
{ taskId: 'task-01', phase: 'spawning_worktree', log: '[qa-tester] Provisioning isolated worktree branch for task-01...\n', delay: 400 },
|
|
1606
|
-
{ taskId: 'task-01', phase: 'running', log: '[qa-tester] Validating dependencies, project manifest and baseline checks...\n', delay: 600 },
|
|
1607
|
-
{ taskId: 'task-01', phase: 'verifying', log: '[gatekeeper] Running verification command: npm test\n✔ Baseline contracts validated\n', delay: 600 },
|
|
1608
|
-
{ taskId: 'task-01', phase: 'completed', log: '[qa-tester] Task task-01 passed verification and merged.\n', delay: 400 },
|
|
1609
|
-
// Step 2: Parallel execution (backend & frontend)
|
|
1610
|
-
{ taskId: 'task-studio-sse-api', phase: 'spawning_worktree', log: '[backend] Initializing git worktree .nativ/worktrees/task-studio-sse-api...\n', delay: 300 },
|
|
1611
|
-
{ taskId: 'task-studio-light-ui', phase: 'spawning_worktree', log: '[frontend] Initializing git worktree .nativ/worktrees/task-studio-light-ui...\n', delay: 300 },
|
|
1612
|
-
{ taskId: 'task-studio-sse-api', phase: 'running', log: '[backend] Implementing real-time event pipeline fanout & simulation endpoints in studio-server.ts...\n', delay: 700 },
|
|
1613
|
-
{ taskId: 'task-studio-light-ui', phase: 'running', log: '[frontend] Constructing SVG Workflow Canvas with Bezier cables and floating playback bar...\n', delay: 700 },
|
|
1614
|
-
{ taskId: 'task-studio-sse-api', phase: 'verifying', log: '[gatekeeper] Executing test-studio-pipeline-api.mjs...\n✔ All API endpoints passed\n', delay: 600 },
|
|
1615
|
-
{ taskId: 'task-studio-sse-api', phase: 'merging', log: '[backend] Merging worktree branch to main...\n', delay: 300 },
|
|
1616
|
-
{ taskId: 'task-studio-sse-api', phase: 'completed', log: '[backend] SSE pipeline stream online.\n', delay: 400 },
|
|
1617
|
-
{ taskId: 'task-studio-light-ui', phase: 'verifying', log: '[gatekeeper] Executing test-studio-light-ui.mjs...\n✔ Zero-emoji compliance and Canvas DOM validated\n', delay: 600 },
|
|
1618
|
-
{ taskId: 'task-studio-light-ui', phase: 'merging', log: '[frontend] Merging worktree branch to main...\n', delay: 300 },
|
|
1619
|
-
{ taskId: 'task-studio-light-ui', phase: 'completed', log: '[frontend] Workflow Canvas ready.\n', delay: 400 },
|
|
1620
|
-
// Step 3: End-to-end integration & verification
|
|
1621
|
-
{ taskId: 'task-studio-e2e-verify', phase: 'spawning_worktree', log: '[qa-tester] Spawning E2E integration test runner...\n', delay: 300 },
|
|
1622
|
-
{ taskId: 'task-studio-e2e-verify', phase: 'running', log: '[qa-tester] Running comprehensive integration test matrix...\n', delay: 800 },
|
|
1623
|
-
...(scenario === 'circuit_breaker_heal'
|
|
1624
|
-
? [
|
|
1625
|
-
{ taskId: 'task-studio-e2e-verify', phase: 'failed', stream: 'stderr', log: '[qa-tester] Invariant violation detected: test mock failure. Circuit breaker tripped.\n', delay: 600 },
|
|
1626
|
-
]
|
|
1627
|
-
: [
|
|
1628
|
-
{ taskId: 'task-studio-e2e-verify', phase: 'verifying', log: '[gatekeeper] Full E2E suite passed.\n', delay: 600 },
|
|
1629
|
-
{ taskId: 'task-studio-e2e-verify', phase: 'completed', log: '[qa-tester] All milestones verified.\n', delay: 400 },
|
|
1630
|
-
]),
|
|
1631
|
-
];
|
|
1632
|
-
let stepIndex = 0;
|
|
1633
|
-
const runNext = () => {
|
|
1634
|
-
if (!this.running || stepIndex >= steps.length) {
|
|
1635
|
-
this.running = false;
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
const step = steps[stepIndex++];
|
|
1639
|
-
const record = {
|
|
1640
|
-
runId: `${id}-${step.taskId}`,
|
|
1641
|
-
taskId: step.taskId,
|
|
1642
|
-
status: step.phase,
|
|
1643
|
-
engine: 'native',
|
|
1644
|
-
model: 'claude-opus-5-5 (simulation)',
|
|
1645
|
-
thinking: { budget: 2048, effort: 'medium', budgetTokens: 2048 },
|
|
1646
|
-
usage: {
|
|
1647
|
-
model: 'claude-opus-5-5 (simulation)',
|
|
1648
|
-
turns: 3,
|
|
1649
|
-
inputTokens: 1400,
|
|
1650
|
-
outputTokens: 380,
|
|
1651
|
-
cacheCreationTokens: 500,
|
|
1652
|
-
cacheReadTokens: 4000,
|
|
1653
|
-
thinkingTokens: 120,
|
|
1654
|
-
costUsd: 0.011,
|
|
1655
|
-
},
|
|
1656
|
-
pid: 99000 + stepIndex,
|
|
1657
|
-
branch: `nativ/${step.taskId}`,
|
|
1658
|
-
worktreeDir: `.nativ/worktrees/${step.taskId}`,
|
|
1659
|
-
command: 'simulation',
|
|
1660
|
-
startedAt: new Date(Date.now() - 2000).toISOString(),
|
|
1661
|
-
endedAt: step.phase === 'completed' || step.phase === 'failed' ? new Date().toISOString() : null,
|
|
1662
|
-
durationMs: 2500,
|
|
1663
|
-
exitCode: step.phase === 'completed' ? 0 : step.phase === 'failed' ? 1 : null,
|
|
1664
|
-
signal: null,
|
|
1665
|
-
timeoutSeconds: 300,
|
|
1666
|
-
timedOut: false,
|
|
1667
|
-
logBytes: (step.log || '').length,
|
|
1668
|
-
logFile: `.nativ/logs/${step.taskId}.log`,
|
|
1669
|
-
error: step.phase === 'failed' ? (step.log || 'Task failed') : null,
|
|
1670
|
-
abortReason: null,
|
|
1671
|
-
verification: step.phase === 'completed' ? { command: 'npm test', success: true, exitCode: 0, durationMs: 400, skipped: false } : null,
|
|
1672
|
-
};
|
|
1673
|
-
this.events.publish('runner_status', runView(record));
|
|
1674
|
-
if (step.log) {
|
|
1675
|
-
this.events.publish('runner_log', {
|
|
1676
|
-
runId: `${id}-${step.taskId}`,
|
|
1677
|
-
taskId: step.taskId,
|
|
1678
|
-
stream: step.stream ?? 'stdout',
|
|
1679
|
-
chunk: step.log,
|
|
1680
|
-
at: new Date().toISOString(),
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
this.activeTimer = setTimeout(runNext, Math.max(80, Math.round(step.delay / speed)));
|
|
1684
|
-
};
|
|
1685
|
-
this.activeTimer = setTimeout(runNext, Math.round(150 / speed));
|
|
1686
|
-
return {
|
|
1687
|
-
ok: true,
|
|
1688
|
-
simulationId: id,
|
|
1689
|
-
scenario,
|
|
1690
|
-
speed,
|
|
1691
|
-
status: 'running',
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
stop() {
|
|
1695
|
-
if (this.activeTimer) {
|
|
1696
|
-
clearTimeout(this.activeTimer);
|
|
1697
|
-
this.activeTimer = null;
|
|
1698
|
-
}
|
|
1699
|
-
const wasRunning = this.running;
|
|
1700
|
-
this.running = false;
|
|
1701
|
-
this.currentId = null;
|
|
1702
|
-
return { ok: true, status: 'stopped', wasRunning };
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
1583
|
// ─── Server ────────────────────────────────────────────────────────────────────
|
|
1706
1584
|
export function createStudioServer(options = {}) {
|
|
1707
1585
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -1742,7 +1620,6 @@ export function createStudioServer(options = {}) {
|
|
|
1742
1620
|
supervisor.on('runner_status', (record) => events.publish('runner_status', runView(record)));
|
|
1743
1621
|
supervisor.on('runner_log', (entry) => events.publish('runner_log', entry));
|
|
1744
1622
|
supervisor.on('runner_token_usage', (usage) => events.publish('runner_token_usage', usage));
|
|
1745
|
-
const simulation = new SimulationController(events);
|
|
1746
1623
|
// Tier 1 strategist: settings and counters live for this server session only.
|
|
1747
1624
|
const liaison = new Tier1Liaison(root, options.triage);
|
|
1748
1625
|
const triageInFlight = new Map();
|
|
@@ -1842,10 +1719,6 @@ export function createStudioServer(options = {}) {
|
|
|
1842
1719
|
return sendJson(res, 200, handleEscalations(root, url.searchParams));
|
|
1843
1720
|
case 'POST /api/pipeline/escalations/resolve':
|
|
1844
1721
|
return sendJson(res, 200, await handleEscalationResolve(root, await readJsonBody(req)));
|
|
1845
|
-
case 'POST /api/pipeline/simulation/start':
|
|
1846
|
-
return sendJson(res, 200, simulation.start(await readJsonBody(req)));
|
|
1847
|
-
case 'POST /api/pipeline/simulation/stop':
|
|
1848
|
-
return sendJson(res, 200, simulation.stop());
|
|
1849
1722
|
case 'POST /api/pipeline/triage/evaluate':
|
|
1850
1723
|
return sendJson(res, 200, await handleTriageEvaluate(root, liaison, triageInFlight, await readJsonBody(req)));
|
|
1851
1724
|
case 'GET /api/pipeline/triage/status':
|
|
@@ -1863,8 +1736,7 @@ export function createStudioServer(options = {}) {
|
|
|
1863
1736
|
'/api/pipeline/tasks/abort', '/api/pipeline/tasks/runs', '/api/pipeline/tasks/logs', '/api/pipeline/worktrees',
|
|
1864
1737
|
'/api/pipeline/worktrees/action', '/api/pipeline/worktrees/diff', '/api/pipeline/benchmarks', '/api/pipeline/benchmarks/run',
|
|
1865
1738
|
'/api/pipeline/telemetry/detailed', '/api/pipeline/escalations', '/api/pipeline/escalations/resolve',
|
|
1866
|
-
'/api/pipeline/
|
|
1867
|
-
'/api/pipeline/triage/status', '/api/pipeline/triage/config',
|
|
1739
|
+
'/api/pipeline/triage/evaluate', '/api/pipeline/triage/status', '/api/pipeline/triage/config',
|
|
1868
1740
|
];
|
|
1869
1741
|
if (known.includes(url.pathname.replace(/\/+$/, '')))
|
|
1870
1742
|
throw new HttpError(405, 'METHOD_NOT_ALLOWED', `${req.method} not allowed on ${url.pathname}`);
|
|
@@ -1884,7 +1756,6 @@ export function createStudioServer(options = {}) {
|
|
|
1884
1756
|
// Open SSE streams and in-flight agent runners would otherwise keep close() waiting forever.
|
|
1885
1757
|
const closeServer = server.close.bind(server);
|
|
1886
1758
|
server.close = ((callback) => {
|
|
1887
|
-
simulation.stop();
|
|
1888
1759
|
supervisor.shutdown('Studio server shutting down');
|
|
1889
1760
|
events.close();
|
|
1890
1761
|
return closeServer(callback);
|
|
@@ -59,15 +59,9 @@ export const dispatchScript = String.raw ` // ─── Autonomous dispatch: PO
|
|
|
59
59
|
|
|
60
60
|
/** Mirrors buildDefaultClaudeCommand() in agent-supervisor.ts so the preview matches what the server runs. */
|
|
61
61
|
function defaultClaudeCommand(t) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
t.verificationCommand ? 'Verify your work using: ' + t.verificationCommand + '.' : '',
|
|
66
|
-
'Start by running: nativ task start ' + t.id + '. When finished and verified, run: nativ task complete ' + t.id + '.',
|
|
67
|
-
'If blocked, follow the Human-Centric Communication Protocol in CLAUDE.md: explain the user experience symptom, root cause in plain English, and clear options without technical jargon.'
|
|
68
|
-
].filter(Boolean).join(' ');
|
|
69
|
-
// No permission bypass: acceptEdits plus the per-task allowlist the server writes to .nativ/runs/permissions/<task>.json.
|
|
70
|
-
return 'claude -p "' + prompt.replace(/"/g, '\\"') + '" --output-format stream-json --verbose --permission-mode acceptEdits --settings ".nativ/runs/permissions/' + t.id + '.json"';
|
|
62
|
+
// The task prompt goes to the agent on stdin, so it is not part of the command.
|
|
63
|
+
return 'claude -p --output-format stream-json --verbose ' +
|
|
64
|
+
'--permission-mode acceptEdits --settings ".nativ/runs/permissions/' + t.id + '.json"';
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
function syncDispatchSwitches() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nativ",
|
|
3
3
|
"displayName": "nativ",
|
|
4
|
-
"version": "2.3.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"description": "Role-based multi-agent workflow for Claude Code: architect, worker and verifier agents, task-scope enforcement, and the nativ MCP server.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "JB Natividad"
|