@njeybe/nativ 2.4.1 → 2.5.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +26 -0
  3. package/dist/commands/db/db-formatter.d.ts +5 -0
  4. package/dist/commands/db/db-formatter.js +116 -0
  5. package/dist/commands/db/db-introspect.d.ts +11 -0
  6. package/dist/commands/db/db-introspect.js +84 -0
  7. package/dist/commands/db.js +4 -206
  8. package/dist/commands/profile.d.ts +3 -0
  9. package/dist/commands/profile.js +39 -0
  10. package/dist/commands/task/task-add.d.ts +11 -0
  11. package/dist/commands/task/task-add.js +169 -0
  12. package/dist/commands/task/task-common.d.ts +57 -0
  13. package/dist/commands/task/task-common.js +45 -0
  14. package/dist/commands/task/task-escalate.d.ts +7 -0
  15. package/dist/commands/task/task-escalate.js +119 -0
  16. package/dist/commands/task/task-lifecycle.d.ts +5 -0
  17. package/dist/commands/task/task-lifecycle.js +252 -0
  18. package/dist/commands/task/task-query.d.ts +10 -0
  19. package/dist/commands/task/task-query.js +269 -0
  20. package/dist/commands/task.d.ts +9 -87
  21. package/dist/commands/task.js +8 -850
  22. package/dist/commands/triage/triage-store.d.ts +56 -0
  23. package/dist/commands/triage/triage-store.js +108 -0
  24. package/dist/commands/triage/triage-ui.d.ts +14 -0
  25. package/dist/commands/triage/triage-ui.js +104 -0
  26. package/dist/commands/triage.d.ts +3 -52
  27. package/dist/commands/triage.js +8 -216
  28. package/dist/core/enforcement.d.ts +1 -0
  29. package/dist/core/enforcement.js +19 -0
  30. package/dist/core/human-gate.d.ts +2 -1
  31. package/dist/core/human-gate.js +9 -2
  32. package/dist/core/profiles.d.ts +14 -0
  33. package/dist/core/profiles.js +82 -0
  34. package/dist/core/setup/config-merger.d.ts +59 -0
  35. package/dist/core/setup/config-merger.js +107 -0
  36. package/dist/core/setup/session-context.d.ts +4 -0
  37. package/dist/core/setup/session-context.js +65 -0
  38. package/dist/core/setup/setup-planner.d.ts +14 -0
  39. package/dist/core/setup/setup-planner.js +180 -0
  40. package/dist/core/setup/template-planner.d.ts +8 -0
  41. package/dist/core/setup/template-planner.js +32 -0
  42. package/dist/core/setup/types.d.ts +65 -0
  43. package/dist/core/setup/types.js +55 -0
  44. package/dist/core/setup-assets.d.ts +8 -119
  45. package/dist/core/setup-assets.js +7 -437
  46. package/dist/core/tier1-liaison.d.ts +2 -0
  47. package/dist/core/tier1-liaison.js +64 -3
  48. package/dist/core/verifier/executor.d.ts +5 -0
  49. package/dist/core/verifier/executor.js +62 -0
  50. package/dist/core/verifier/phases.d.ts +5 -0
  51. package/dist/core/verifier/phases.js +71 -0
  52. package/dist/core/verifier/task-verifier.d.ts +37 -0
  53. package/dist/core/verifier/task-verifier.js +233 -0
  54. package/dist/core/verifier/types.d.ts +51 -0
  55. package/dist/core/verifier/types.js +1 -0
  56. package/dist/core/verifier.d.ts +5 -93
  57. package/dist/core/verifier.js +5 -359
  58. package/dist/db/introspector.js +59 -59
  59. package/dist/governor/evaluator.d.ts +4 -64
  60. package/dist/governor/evaluator.js +21 -686
  61. package/dist/governor/patch-applier.d.ts +8 -0
  62. package/dist/governor/patch-applier.js +121 -0
  63. package/dist/governor/self-healing.d.ts +24 -0
  64. package/dist/governor/self-healing.js +351 -0
  65. package/dist/governor/test-integrity.d.ts +41 -0
  66. package/dist/governor/test-integrity.js +237 -0
  67. package/dist/index.js +8 -0
  68. package/dist/mcp/capture.d.ts +4 -0
  69. package/dist/mcp/capture.js +33 -0
  70. package/dist/mcp/resources.d.ts +39 -0
  71. package/dist/mcp/resources.js +44 -0
  72. package/dist/mcp/server.d.ts +11 -0
  73. package/dist/mcp/server.js +14 -89
  74. package/dist/mcp/tools.d.ts +1 -0
  75. package/dist/mcp/tools.js +1 -0
  76. package/dist/runner/agent-supervisor.d.ts +8 -302
  77. package/dist/runner/agent-supervisor.js +41 -1030
  78. package/dist/runner/cli-config.d.ts +84 -0
  79. package/dist/runner/cli-config.js +215 -0
  80. package/dist/runner/native-driver.d.ts +18 -0
  81. package/dist/runner/native-driver.js +208 -0
  82. package/dist/runner/native-engine.d.ts +27 -0
  83. package/dist/runner/native-engine.js +184 -0
  84. package/dist/runner/native-tools.d.ts +31 -0
  85. package/dist/runner/native-tools.js +241 -0
  86. package/dist/runner/process-utils.d.ts +12 -0
  87. package/dist/runner/process-utils.js +96 -0
  88. package/dist/runner/run-lifecycle.d.ts +6 -0
  89. package/dist/runner/run-lifecycle.js +42 -0
  90. package/dist/runner/types.d.ts +215 -0
  91. package/dist/runner/types.js +41 -0
  92. package/dist/scanner/detector.js +42 -663
  93. package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
  94. package/dist/scanner/detectors/ecosystem-detector.js +354 -0
  95. package/dist/scanner/detectors/orm-detector.d.ts +5 -0
  96. package/dist/scanner/detectors/orm-detector.js +59 -0
  97. package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
  98. package/dist/scanner/detectors/workspace-detector.js +266 -0
  99. package/dist/server/cli-runner.d.ts +15 -0
  100. package/dist/server/cli-runner.js +43 -0
  101. package/dist/server/event-hub.d.ts +42 -0
  102. package/dist/server/event-hub.js +169 -0
  103. package/dist/server/http-utils.d.ts +15 -0
  104. package/dist/server/http-utils.js +56 -0
  105. package/dist/server/plan-utils.d.ts +9 -0
  106. package/dist/server/plan-utils.js +31 -0
  107. package/dist/server/routes/db-routes.d.ts +57 -0
  108. package/dist/server/routes/db-routes.js +484 -0
  109. package/dist/server/routes/task-routes.d.ts +266 -0
  110. package/dist/server/routes/task-routes.js +285 -0
  111. package/dist/server/routes/telemetry-routes.d.ts +128 -0
  112. package/dist/server/routes/telemetry-routes.js +179 -0
  113. package/dist/server/routes/triage-routes.d.ts +81 -0
  114. package/dist/server/routes/triage-routes.js +244 -0
  115. package/dist/server/routes/worktree-routes.d.ts +31 -0
  116. package/dist/server/routes/worktree-routes.js +144 -0
  117. package/dist/server/studio-server.d.ts +4 -11
  118. package/dist/server/studio-server.js +34 -1578
  119. package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
  120. package/dist/server/studio-ui/shell/triage.js +10 -0
  121. package/package.json +2 -2
  122. package/plugin/.claude-plugin/plugin.json +1 -1
  123. package/plugin/agents/architect.md +4 -2
  124. package/plugin/agents/verifier.md +1 -1
  125. package/plugin/agents/worker.md +7 -4
  126. package/plugin/skills/nativ/SKILL.md +1 -0
  127. package/templates/.claudeignore +62 -45
  128. package/templates/AGENTS.md +1 -0
  129. package/templates/CLAUDE.md +13 -0
  130. package/templates/claude-agents/architect.md +4 -2
  131. package/templates/claude-agents/verifier.md +1 -1
  132. package/templates/claude-agents/worker.md +7 -4
  133. package/templates/dot-ai/api_contracts.json +66 -66
  134. package/templates/dot-ai/context.md +38 -38
  135. package/templates/dot-ai/db_schema.json +39 -39
  136. package/templates/dot-ai/subagents/architect.md +54 -0
  137. package/dist/commands/session.d.ts +0 -22
  138. package/dist/commands/session.js +0 -106
  139. package/dist/core/session-memory.d.ts +0 -42
  140. package/dist/core/session-memory.js +0 -121
@@ -0,0 +1,266 @@
1
+ import { AgentSupervisor, type RunnerEngine, type RunnerRecord } from '../../runner/agent-supervisor.js';
2
+ export declare const CONTRACT_FILES: {
3
+ readonly masterPlanExists: "master_plan.json";
4
+ readonly contextExists: "context.md";
5
+ readonly dbSchemaExists: "db_schema.json";
6
+ readonly apiContractsExists: "api_contracts.json";
7
+ readonly uiSpecsExists: "ui_specs.md";
8
+ };
9
+ export declare const TASK_ACTION_STATUS: {
10
+ readonly start: "in_progress";
11
+ readonly complete: "completed";
12
+ readonly block: "blocked";
13
+ };
14
+ export type TaskAction = keyof typeof TASK_ACTION_STATUS;
15
+ export declare function packageVersion(): string;
16
+ /** GET /api/pipeline/status: contract matrix, milestone/task burndown and telemetry KPIs. */
17
+ export declare function handlePipelineStatus(root: string): {
18
+ ok: boolean;
19
+ pipeline: {
20
+ version: string;
21
+ contracts: Record<"masterPlanExists" | "contextExists" | "dbSchemaExists" | "apiContractsExists" | "uiSpecsExists", boolean>;
22
+ milestones: {
23
+ total: number;
24
+ completed: number;
25
+ inProgress: number;
26
+ pending: number;
27
+ };
28
+ tasks: {
29
+ total: number;
30
+ completed: number;
31
+ inProgress: number;
32
+ pending: number;
33
+ blocked: number;
34
+ progressPercentage: number;
35
+ };
36
+ telemetry: {
37
+ totalTokens: number;
38
+ estimatedCostUsd: number;
39
+ actualSpendUsd: number;
40
+ cacheReadTokens: number;
41
+ cacheCreationTokens: number;
42
+ thinkingTokens: number;
43
+ cacheHitRate: number;
44
+ totalTasksTracked: number;
45
+ passRate: number;
46
+ circuitBreakerTrips: number;
47
+ };
48
+ };
49
+ };
50
+ /** GET /api/pipeline/tasks: milestones with per-task dependency readiness and circuit-breaker attempts. */
51
+ export declare function handlePipelineTasks(root: string): {
52
+ ok: boolean;
53
+ projectName: string | null;
54
+ overallStatus: "pending" | "in_progress" | "completed" | null;
55
+ activeMilestoneId: string | null;
56
+ milestones: {
57
+ progressPercentage: number;
58
+ tasks: {
59
+ isAvailable: boolean;
60
+ circuitBreaker: import("../../governor/types.js").CircuitBreakerState;
61
+ id: string;
62
+ title: string;
63
+ description: string;
64
+ assignedSubagent: import("../../scanner/types.js").SubagentType;
65
+ dependencies: string[];
66
+ targetFiles: string[];
67
+ status: import("../../scanner/types.js").TaskStatus;
68
+ verificationCommand: string;
69
+ notes?: string;
70
+ fastPath?: boolean;
71
+ specRefs?: string[];
72
+ complexity?: import("../../scanner/types.js").TaskComplexity;
73
+ acceptanceCriteria?: string[];
74
+ }[];
75
+ id: string;
76
+ name: string;
77
+ status: "pending" | "in_progress" | "completed";
78
+ fastPath?: boolean;
79
+ }[];
80
+ };
81
+ /** POST /api/pipeline/tasks/action: the same lock-guarded transitions as `nativ task start|complete|block`. */
82
+ export declare function handleTaskAction(root: string, body: Record<string, unknown>): Promise<{
83
+ ok: boolean;
84
+ task: import("../../scanner/types.js").MasterPlanTask;
85
+ }>;
86
+ /** Supervisor guard rails (unmet dependencies, already running, unknown task) map to the contract's 400 envelope. */
87
+ export declare function toHttpError(err: unknown): never;
88
+ export declare function parseRunnerCommand(value: unknown): string | undefined;
89
+ export declare function parseTimeoutSeconds(value: unknown): number | undefined;
90
+ export declare function parseOptionalBoolean(value: unknown, field: string): boolean | undefined;
91
+ /** Query strings carry booleans as text: `?activeOnly=1|true|yes`. */
92
+ export declare function parseBooleanParam(value: string | null): boolean;
93
+ export declare function parseTailLines(value: string | null): number;
94
+ export declare function parseRunnerEngine(value: unknown): RunnerEngine | undefined;
95
+ export declare function parseThinkingBudget(value: unknown): number | undefined;
96
+ export declare function parseModel(value: unknown): string | undefined;
97
+ /** Runner records as served by the API: usage gains its derived cache hit rate. */
98
+ export declare function runView(run: RunnerRecord): {
99
+ usage: {
100
+ costUsd: number;
101
+ cacheHitRate: number;
102
+ model: string;
103
+ turns: number;
104
+ inputTokens: number;
105
+ outputTokens: number;
106
+ cacheCreationTokens: number;
107
+ cacheReadTokens: number;
108
+ thinkingTokens: number;
109
+ } | null;
110
+ runId: string;
111
+ taskId: string;
112
+ status: import("../../runner/types.js").RunnerStatus;
113
+ engine: RunnerEngine;
114
+ model: string | null;
115
+ thinking: import("../../runner/types.js").RunnerThinking | null;
116
+ pid: number | null;
117
+ branch: string | null;
118
+ worktreeDir: string;
119
+ command: string;
120
+ startedAt: string;
121
+ endedAt: string | null;
122
+ durationMs: number | null;
123
+ exitCode: number | null;
124
+ signal: string | null;
125
+ timeoutSeconds: number;
126
+ timedOut: boolean;
127
+ logBytes: number;
128
+ logFile: string;
129
+ error: string | null;
130
+ abortReason: string | null;
131
+ verification: {
132
+ command: string;
133
+ success: boolean;
134
+ exitCode: number;
135
+ durationMs: number;
136
+ skipped: boolean;
137
+ error?: string;
138
+ phases?: Array<{
139
+ name: string;
140
+ success: boolean;
141
+ durationMs: number;
142
+ }>;
143
+ } | null;
144
+ };
145
+ /**
146
+ * POST /api/pipeline/tasks/dispatch: hands a task to a background runner, either the native
147
+ * Messages API engine or a CLI command (Claude Code by default), in an isolated worktree.
148
+ */
149
+ export declare function handleTaskDispatch(supervisor: AgentSupervisor, body: Record<string, unknown>): Promise<{
150
+ ok: boolean;
151
+ run: {
152
+ usage: {
153
+ costUsd: number;
154
+ cacheHitRate: number;
155
+ model: string;
156
+ turns: number;
157
+ inputTokens: number;
158
+ outputTokens: number;
159
+ cacheCreationTokens: number;
160
+ cacheReadTokens: number;
161
+ thinkingTokens: number;
162
+ } | null;
163
+ runId: string;
164
+ taskId: string;
165
+ status: import("../../runner/types.js").RunnerStatus;
166
+ engine: RunnerEngine;
167
+ model: string | null;
168
+ thinking: import("../../runner/types.js").RunnerThinking | null;
169
+ pid: number | null;
170
+ branch: string | null;
171
+ worktreeDir: string;
172
+ command: string;
173
+ startedAt: string;
174
+ endedAt: string | null;
175
+ durationMs: number | null;
176
+ exitCode: number | null;
177
+ signal: string | null;
178
+ timeoutSeconds: number;
179
+ timedOut: boolean;
180
+ logBytes: number;
181
+ logFile: string;
182
+ error: string | null;
183
+ abortReason: string | null;
184
+ verification: {
185
+ command: string;
186
+ success: boolean;
187
+ exitCode: number;
188
+ durationMs: number;
189
+ skipped: boolean;
190
+ error?: string;
191
+ phases?: Array<{
192
+ name: string;
193
+ success: boolean;
194
+ durationMs: number;
195
+ }>;
196
+ } | null;
197
+ };
198
+ }>;
199
+ /** POST /api/pipeline/tasks/abort: terminates the runner's process tree and settles the run as aborted. */
200
+ export declare function handleTaskAbort(supervisor: AgentSupervisor, body: Record<string, unknown>): Promise<{
201
+ ok: boolean;
202
+ taskId: string;
203
+ status: "aborted";
204
+ message: string;
205
+ }>;
206
+ /** GET /api/pipeline/tasks/runs: active and recent runs, newest first. */
207
+ export declare function handleTaskRuns(supervisor: AgentSupervisor, searchParams: URLSearchParams): {
208
+ ok: boolean;
209
+ runs: {
210
+ usage: {
211
+ costUsd: number;
212
+ cacheHitRate: number;
213
+ model: string;
214
+ turns: number;
215
+ inputTokens: number;
216
+ outputTokens: number;
217
+ cacheCreationTokens: number;
218
+ cacheReadTokens: number;
219
+ thinkingTokens: number;
220
+ } | null;
221
+ runId: string;
222
+ taskId: string;
223
+ status: import("../../runner/types.js").RunnerStatus;
224
+ engine: RunnerEngine;
225
+ model: string | null;
226
+ thinking: import("../../runner/types.js").RunnerThinking | null;
227
+ pid: number | null;
228
+ branch: string | null;
229
+ worktreeDir: string;
230
+ command: string;
231
+ startedAt: string;
232
+ endedAt: string | null;
233
+ durationMs: number | null;
234
+ exitCode: number | null;
235
+ signal: string | null;
236
+ timeoutSeconds: number;
237
+ timedOut: boolean;
238
+ logBytes: number;
239
+ logFile: string;
240
+ error: string | null;
241
+ abortReason: string | null;
242
+ verification: {
243
+ command: string;
244
+ success: boolean;
245
+ exitCode: number;
246
+ durationMs: number;
247
+ skipped: boolean;
248
+ error?: string;
249
+ phases?: Array<{
250
+ name: string;
251
+ success: boolean;
252
+ durationMs: number;
253
+ }>;
254
+ } | null;
255
+ }[];
256
+ };
257
+ /** GET /api/pipeline/tasks/logs: tail of a runner's streaming log buffer. */
258
+ export declare function handleTaskLogs(supervisor: AgentSupervisor, searchParams: URLSearchParams): {
259
+ ok: boolean;
260
+ taskId: string;
261
+ runId: string | null;
262
+ status: import("../../runner/types.js").RunnerStatus;
263
+ totalBytes: number;
264
+ log: string;
265
+ truncated: boolean;
266
+ };
@@ -0,0 +1,285 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CircuitBreaker } from '../../governor/index.js';
4
+ import { runTaskBlock, runTaskComplete, runTaskStart } from '../../commands/task.js';
5
+ import { SupervisorError, } from '../../runner/agent-supervisor.js';
6
+ import { cacheHitRate, loadTelemetry, } from '../../core/telemetry.js';
7
+ import { HttpError } from '../http-utils.js';
8
+ import { AI_DIR, findTask, parseTaskId, planMilestones, readPlan } from '../plan-utils.js';
9
+ import { runCaptured, truncateOutput } from '../cli-runner.js';
10
+ import { aggregateActualUsage, finiteOr, percent, roundUsd, sanitizeUsage, usageView, } from './telemetry-routes.js';
11
+ export const CONTRACT_FILES = {
12
+ masterPlanExists: 'master_plan.json',
13
+ contextExists: 'context.md',
14
+ dbSchemaExists: 'db_schema.json',
15
+ apiContractsExists: 'api_contracts.json',
16
+ uiSpecsExists: 'ui_specs.md',
17
+ };
18
+ export const TASK_ACTION_STATUS = { start: 'in_progress', complete: 'completed', block: 'blocked' };
19
+ const MAX_REASON_LENGTH = 1000;
20
+ const MAX_RUNNER_COMMAND_LENGTH = 2000;
21
+ const MAX_TIMEOUT_SECONDS = 24 * 60 * 60;
22
+ const DEFAULT_TAIL_LINES = 200;
23
+ const MAX_TAIL_LINES = 5000;
24
+ const MODEL_ID_PATTERN = /^[A-Za-z0-9][\w.:@-]{0,127}$/;
25
+ const MAX_THINKING_BUDGET = 1_000_000;
26
+ let cachedVersion = null;
27
+ export function packageVersion() {
28
+ if (cachedVersion === null) {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
31
+ cachedVersion = pkg.version ?? 'unknown';
32
+ }
33
+ catch {
34
+ cachedVersion = 'unknown';
35
+ }
36
+ }
37
+ return cachedVersion;
38
+ }
39
+ /** GET /api/pipeline/status: contract matrix, milestone/task burndown and telemetry KPIs. */
40
+ export function handlePipelineStatus(root) {
41
+ const aiDir = path.join(root, AI_DIR);
42
+ const contracts = Object.fromEntries(Object.entries(CONTRACT_FILES).map(([key, file]) => [key, fs.existsSync(path.join(aiDir, file))]));
43
+ const milestones = planMilestones(readPlan(root));
44
+ const tasks = milestones.flatMap((m) => m.tasks);
45
+ const count = (items, status) => items.filter((i) => i.status === status).length;
46
+ const completedTasks = count(tasks, 'completed');
47
+ const { summary, tasks: trackedTasks } = loadTelemetry(path.join(aiDir, 'telemetry.json'));
48
+ const actual = aggregateActualUsage(trackedTasks);
49
+ return {
50
+ ok: true,
51
+ pipeline: {
52
+ version: packageVersion(),
53
+ contracts,
54
+ milestones: {
55
+ total: milestones.length,
56
+ completed: count(milestones, 'completed'),
57
+ inProgress: count(milestones, 'in_progress'),
58
+ pending: count(milestones, 'pending'),
59
+ },
60
+ tasks: {
61
+ total: tasks.length,
62
+ completed: completedTasks,
63
+ inProgress: count(tasks, 'in_progress'),
64
+ pending: count(tasks, 'pending'),
65
+ blocked: count(tasks, 'blocked'),
66
+ progressPercentage: percent(completedTasks, tasks.length),
67
+ },
68
+ telemetry: {
69
+ totalTokens: finiteOr(summary?.estimatedTotalTokens, 0),
70
+ estimatedCostUsd: finiteOr(summary?.estimatedTotalCostUsd, 0),
71
+ // Grounded in API-reported usage (native runs), summed from the task records so a
72
+ // stale summary written by an older nativ can never under-report spend.
73
+ actualSpendUsd: roundUsd(actual.costUsd),
74
+ cacheReadTokens: actual.cacheReadTokens,
75
+ cacheCreationTokens: actual.cacheCreationTokens,
76
+ thinkingTokens: actual.thinkingTokens,
77
+ cacheHitRate: cacheHitRate(actual),
78
+ totalTasksTracked: trackedTasks.length,
79
+ // Ratio in [0, 1], as stored in telemetry.json.
80
+ passRate: finiteOr(summary?.verificationPassRate, 1),
81
+ circuitBreakerTrips: finiteOr(summary?.circuitBreakerTrips, 0),
82
+ },
83
+ },
84
+ };
85
+ }
86
+ /** GET /api/pipeline/tasks: milestones with per-task dependency readiness and circuit-breaker attempts. */
87
+ export function handlePipelineTasks(root) {
88
+ const plan = readPlan(root);
89
+ const milestones = planMilestones(plan);
90
+ const completedIds = new Set(milestones.flatMap((m) => m.tasks).filter((t) => t.status === 'completed').map((t) => t.id));
91
+ return {
92
+ ok: true,
93
+ projectName: plan?.projectName ?? null,
94
+ overallStatus: plan?.overallStatus ?? null,
95
+ activeMilestoneId: plan?.activeMilestoneId ?? null,
96
+ milestones: milestones.map((m) => ({
97
+ ...m,
98
+ progressPercentage: percent(m.tasks.filter((t) => t.status === 'completed').length, m.tasks.length),
99
+ tasks: m.tasks.map((t) => ({
100
+ ...t,
101
+ // Same readiness rule as `nativ task list --available`.
102
+ isAvailable: (t.status === 'pending' || t.status === 'in_progress') &&
103
+ (t.dependencies ?? []).every((d) => completedIds.has(d)),
104
+ circuitBreaker: CircuitBreaker.getStatus(root, t.id),
105
+ })),
106
+ })),
107
+ };
108
+ }
109
+ /** POST /api/pipeline/tasks/action: the same lock-guarded transitions as `nativ task start|complete|block`. */
110
+ export async function handleTaskAction(root, body) {
111
+ const { action } = body;
112
+ if (typeof action !== 'string' || !Object.hasOwn(TASK_ACTION_STATUS, action)) {
113
+ throw new HttpError(400, 'VALIDATION_ERROR', `"action" must be one of: ${Object.keys(TASK_ACTION_STATUS).join(', ')}`);
114
+ }
115
+ const taskAction = action;
116
+ const taskId = parseTaskId(body.taskId);
117
+ if (body.reason !== undefined && body.reason !== null && typeof body.reason !== 'string') {
118
+ throw new HttpError(400, 'VALIDATION_ERROR', '"reason" must be a string');
119
+ }
120
+ const reason = typeof body.reason === 'string' ? body.reason.trim() : '';
121
+ if (reason.length > MAX_REASON_LENGTH) {
122
+ throw new HttpError(400, 'VALIDATION_ERROR', `"reason" must be at most ${MAX_REASON_LENGTH} characters`);
123
+ }
124
+ if (taskAction === 'block' && !reason)
125
+ throw new HttpError(400, 'VALIDATION_ERROR', '"reason" is required when blocking a task');
126
+ const task = findTask(readPlan(root), taskId);
127
+ if (!task)
128
+ throw new HttpError(400, 'TASK_NOT_FOUND', `Task "${taskId}" not found in .ai/master_plan.json`);
129
+ if (task.status === TASK_ACTION_STATUS[taskAction]) {
130
+ throw new HttpError(400, 'INVALID_TRANSITION', `Task "${taskId}" is already ${task.status}`);
131
+ }
132
+ // `complete` runs the task's verificationCommand as a gatekeeper, exactly like the CLI.
133
+ const { failed, output } = await runCaptured(async () => {
134
+ if (taskAction === 'start')
135
+ await runTaskStart(taskId, root);
136
+ else if (taskAction === 'complete')
137
+ await runTaskComplete(taskId, root);
138
+ else
139
+ await runTaskBlock(taskId, reason, root);
140
+ });
141
+ if (failed)
142
+ throw new HttpError(400, 'TASK_ACTION_FAILED', truncateOutput(output || `Task ${taskAction} failed`));
143
+ return { ok: true, task: findTask(readPlan(root), taskId) ?? task };
144
+ }
145
+ /** Supervisor guard rails (unmet dependencies, already running, unknown task) map to the contract's 400 envelope. */
146
+ export function toHttpError(err) {
147
+ if (err instanceof SupervisorError)
148
+ throw new HttpError(err.status, err.code, err.message);
149
+ throw err;
150
+ }
151
+ export function parseRunnerCommand(value) {
152
+ if (value === undefined || value === null || value === '')
153
+ return undefined;
154
+ if (typeof value !== 'string')
155
+ throw new HttpError(400, 'VALIDATION_ERROR', '"runnerCommand" must be a string');
156
+ const command = value.trim();
157
+ if (!command)
158
+ throw new HttpError(400, 'VALIDATION_ERROR', '"runnerCommand" must not be empty');
159
+ if (command.length > MAX_RUNNER_COMMAND_LENGTH) {
160
+ throw new HttpError(400, 'VALIDATION_ERROR', `"runnerCommand" must be at most ${MAX_RUNNER_COMMAND_LENGTH} characters`);
161
+ }
162
+ return command;
163
+ }
164
+ export function parseTimeoutSeconds(value) {
165
+ if (value === undefined || value === null)
166
+ return undefined;
167
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > MAX_TIMEOUT_SECONDS) {
168
+ throw new HttpError(400, 'VALIDATION_ERROR', `"timeoutSeconds" must be a number between 1 and ${MAX_TIMEOUT_SECONDS}`);
169
+ }
170
+ return Math.floor(value);
171
+ }
172
+ export function parseOptionalBoolean(value, field) {
173
+ if (value === undefined || value === null)
174
+ return undefined;
175
+ if (typeof value !== 'boolean')
176
+ throw new HttpError(400, 'VALIDATION_ERROR', `"${field}" must be a boolean`);
177
+ return value;
178
+ }
179
+ /** Query strings carry booleans as text: `?activeOnly=1|true|yes`. */
180
+ export function parseBooleanParam(value) {
181
+ return value !== null && ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
182
+ }
183
+ export function parseTailLines(value) {
184
+ if (value === null || value === '')
185
+ return DEFAULT_TAIL_LINES;
186
+ const tail = Number(value);
187
+ if (!Number.isFinite(tail) || tail <= 0 || tail > MAX_TAIL_LINES) {
188
+ throw new HttpError(400, 'VALIDATION_ERROR', `"tailLines" must be a number between 1 and ${MAX_TAIL_LINES}`);
189
+ }
190
+ return Math.floor(tail);
191
+ }
192
+ export function parseRunnerEngine(value) {
193
+ if (value === undefined || value === null || value === '')
194
+ return undefined;
195
+ if (value !== 'native' && value !== 'cli')
196
+ throw new HttpError(400, 'VALIDATION_ERROR', `"runnerEngine" must be 'native' or 'cli'`);
197
+ return value;
198
+ }
199
+ export function parseThinkingBudget(value) {
200
+ if (value === undefined || value === null)
201
+ return undefined;
202
+ // 0 is valid: "as little thinking as the model allows" (the Studio's None chip).
203
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > MAX_THINKING_BUDGET) {
204
+ throw new HttpError(400, 'VALIDATION_ERROR', `"thinkingBudget" must be a number between 0 and ${MAX_THINKING_BUDGET}`);
205
+ }
206
+ return Math.floor(value);
207
+ }
208
+ export function parseModel(value) {
209
+ if (value === undefined || value === null || value === '')
210
+ return undefined;
211
+ if (typeof value !== 'string' || !MODEL_ID_PATTERN.test(value.trim())) {
212
+ throw new HttpError(400, 'VALIDATION_ERROR', '"model" must be a model ID such as claude-opus-5-5');
213
+ }
214
+ return value.trim();
215
+ }
216
+ /** Runner records as served by the API: usage gains its derived cache hit rate. */
217
+ export function runView(run) {
218
+ return { ...run, usage: usageView(sanitizeUsage(run.usage)) };
219
+ }
220
+ /**
221
+ * POST /api/pipeline/tasks/dispatch: hands a task to a background runner, either the native
222
+ * Messages API engine or a CLI command (Claude Code by default), in an isolated worktree.
223
+ */
224
+ export async function handleTaskDispatch(supervisor, body) {
225
+ const taskId = parseTaskId(body.taskId);
226
+ const runnerEngine = parseRunnerEngine(body.runnerEngine);
227
+ const runnerCommand = parseRunnerCommand(body.runnerCommand);
228
+ const thinkingBudget = parseThinkingBudget(body.thinkingBudget);
229
+ const model = parseModel(body.model);
230
+ const timeoutSeconds = parseTimeoutSeconds(body.timeoutSeconds);
231
+ const useWorktree = parseOptionalBoolean(body.useWorktree, 'useWorktree');
232
+ const verify = parseOptionalBoolean(body.verify, 'verify');
233
+ const autoMerge = parseOptionalBoolean(body.autoMerge, 'autoMerge');
234
+ let run;
235
+ try {
236
+ run = await supervisor.dispatch({
237
+ taskId,
238
+ runnerEngine,
239
+ runnerCommand,
240
+ thinkingBudget,
241
+ model,
242
+ timeoutSeconds,
243
+ useWorktree,
244
+ verify,
245
+ autoMerge,
246
+ });
247
+ }
248
+ catch (err) {
249
+ toHttpError(err);
250
+ }
251
+ return { ok: true, run: runView(run) };
252
+ }
253
+ /** POST /api/pipeline/tasks/abort: terminates the runner's process tree and settles the run as aborted. */
254
+ export async function handleTaskAbort(supervisor, body) {
255
+ const taskId = parseTaskId(body.taskId);
256
+ if (body.reason !== undefined && body.reason !== null && typeof body.reason !== 'string') {
257
+ throw new HttpError(400, 'VALIDATION_ERROR', '"reason" must be a string');
258
+ }
259
+ const reason = typeof body.reason === 'string' ? body.reason.trim() : '';
260
+ if (reason.length > MAX_REASON_LENGTH) {
261
+ throw new HttpError(400, 'VALIDATION_ERROR', `"reason" must be at most ${MAX_REASON_LENGTH} characters`);
262
+ }
263
+ try {
264
+ const { status, message } = await supervisor.abort(taskId, reason || undefined);
265
+ return { ok: true, taskId, status, message };
266
+ }
267
+ catch (err) {
268
+ toHttpError(err);
269
+ }
270
+ }
271
+ /** GET /api/pipeline/tasks/runs: active and recent runs, newest first. */
272
+ export function handleTaskRuns(supervisor, searchParams) {
273
+ const rawTaskId = searchParams.get('taskId');
274
+ const taskId = rawTaskId === null || rawTaskId === '' ? undefined : parseTaskId(rawTaskId);
275
+ const runs = supervisor.listRuns({ taskId, activeOnly: parseBooleanParam(searchParams.get('activeOnly')) });
276
+ return { ok: true, runs: runs.map(runView) };
277
+ }
278
+ /** GET /api/pipeline/tasks/logs: tail of a runner's streaming log buffer. */
279
+ export function handleTaskLogs(supervisor, searchParams) {
280
+ const taskId = parseTaskId(searchParams.get('taskId'));
281
+ const { status, totalBytes, log, runId, truncated } = supervisor.getLogs(taskId, parseTailLines(searchParams.get('tailLines')));
282
+ if (status === null)
283
+ throw new HttpError(400, 'RUN_NOT_FOUND', `No runner history for task "${taskId}"`);
284
+ return { ok: true, taskId, runId, status, totalBytes, log, truncated };
285
+ }
@@ -0,0 +1,128 @@
1
+ import { type ActualTokenUsage, type TaskTelemetryRecord } from '../../core/telemetry.js';
2
+ import type { BenchmarkReport } from '../../core/benchmark.js';
3
+ import type { AgentSupervisor } from '../../runner/agent-supervisor.js';
4
+ export declare function finiteOr(value: unknown, fallback: number): number;
5
+ export declare function percent(part: number, total: number): number;
6
+ export declare function roundUsd(value: number): number;
7
+ /** Telemetry is hand-editable JSON: coerce every counter so one bad record cannot NaN the totals. */
8
+ export declare function sanitizeUsage(value: unknown): ActualTokenUsage | null;
9
+ export declare function aggregateActualUsage(records: TaskTelemetryRecord[]): ActualTokenUsage;
10
+ /** Usage as served by the API: the stored counters plus the derived cache hit rate. */
11
+ export declare function usageView(usage: ActualTokenUsage | null): {
12
+ costUsd: number;
13
+ cacheHitRate: number;
14
+ model: string;
15
+ turns: number;
16
+ inputTokens: number;
17
+ outputTokens: number;
18
+ cacheCreationTokens: number;
19
+ cacheReadTokens: number;
20
+ thinkingTokens: number;
21
+ } | null;
22
+ /** GET /api/pipeline/benchmarks: the cached report written by `nativ bench` or benchmarks/run. */
23
+ export declare function handleBenchmarks(root: string): {
24
+ ok: boolean;
25
+ report: unknown;
26
+ };
27
+ export declare function createBenchmarkRunner(root: string): () => Promise<BenchmarkReport>;
28
+ /**
29
+ * GET /api/pipeline/telemetry/detailed: per-task financial audit. Each breakdown pairs the heuristic
30
+ * estimate with API-reported usage and lists the task's runs (engine, model, per-run usage).
31
+ */
32
+ export declare function handleTelemetryDetailed(root: string, supervisor: AgentSupervisor): {
33
+ ok: boolean;
34
+ summary: {
35
+ projectName: string;
36
+ lastUpdated: string;
37
+ modelTierDefault: string;
38
+ tasksTracked: number;
39
+ totalTasksCompleted: number;
40
+ totalDurationMs: number;
41
+ estimated: {
42
+ totalTokens: number;
43
+ costUsd: number;
44
+ };
45
+ actual: {
46
+ turns: number;
47
+ inputTokens: number;
48
+ outputTokens: number;
49
+ cacheReadTokens: number;
50
+ cacheCreationTokens: number;
51
+ thinkingTokens: number;
52
+ spendUsd: number;
53
+ cacheHitRate: number;
54
+ cacheSavingsUsd: number;
55
+ };
56
+ byModel: ({
57
+ costUsd: number;
58
+ cacheHitRate: number;
59
+ model: string;
60
+ turns: number;
61
+ inputTokens: number;
62
+ outputTokens: number;
63
+ cacheCreationTokens: number;
64
+ cacheReadTokens: number;
65
+ thinkingTokens: number;
66
+ } | null)[];
67
+ verification: {
68
+ passRate: number;
69
+ runs: number;
70
+ passed: number;
71
+ };
72
+ circuitBreakerTrips: number;
73
+ };
74
+ taskBreakdowns: {
75
+ taskId: string;
76
+ title: string;
77
+ assignedSubagent: string | null;
78
+ status: import("../../scanner/types.js").TaskStatus | "failed" | null;
79
+ startedAt: string | null;
80
+ completedAt: string | null;
81
+ durationMs: number;
82
+ verification: {
83
+ command: string;
84
+ durationMs: number;
85
+ exitCode: number;
86
+ skipped: boolean;
87
+ } | null;
88
+ estimated: {
89
+ inputTokens: number;
90
+ outputTokens: number;
91
+ totalTokens: number;
92
+ costUsd: number;
93
+ } | null;
94
+ actual: {
95
+ costUsd: number;
96
+ cacheHitRate: number;
97
+ model: string;
98
+ turns: number;
99
+ inputTokens: number;
100
+ outputTokens: number;
101
+ cacheCreationTokens: number;
102
+ cacheReadTokens: number;
103
+ thinkingTokens: number;
104
+ } | null;
105
+ varianceUsd: number | null;
106
+ runs: {
107
+ runId: string;
108
+ engine: import("../../runner/types.js").RunnerEngine;
109
+ model: string | null;
110
+ status: import("../../runner/types.js").RunnerStatus;
111
+ startedAt: string;
112
+ endedAt: string | null;
113
+ durationMs: number | null;
114
+ thinking: import("../../runner/types.js").RunnerThinking | null;
115
+ usage: {
116
+ costUsd: number;
117
+ cacheHitRate: number;
118
+ model: string;
119
+ turns: number;
120
+ inputTokens: number;
121
+ outputTokens: number;
122
+ cacheCreationTokens: number;
123
+ cacheReadTokens: number;
124
+ thinkingTokens: number;
125
+ } | null;
126
+ }[];
127
+ }[];
128
+ };