@moda-ai/cli 1.17.2 → 1.18.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/dist/analyst-tui.js +1 -1
- package/dist/cli.js +4 -0
- package/dist/harness-remote.d.ts +102 -0
- package/dist/harness-remote.js +314 -52
- package/dist/harness.d.ts +35 -0
- package/dist/harness.js +140 -22
- package/dist/init/index.js +469 -185
- package/dist/init/progress-board.d.ts +80 -0
- package/dist/init/progress-board.js +225 -0
- package/dist/init/sdk-integration.d.ts +9 -1
- package/dist/init/sdk-integration.js +12 -2
- package/dist/manifest.js +1 -0
- package/dist/prompts-ab.d.ts +53 -0
- package/dist/prompts-ab.js +80 -10
- package/dist/prompts-propose.d.ts +8 -3
- package/dist/prompts-propose.js +145 -9
- package/dist/schemas.d.ts +2 -2
- package/dist/tui.d.ts +12 -4
- package/dist/tui.js +24 -17
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/harness.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AnalystTuiEvent } from './analyst-tui.js';
|
|
1
2
|
import type { CommandContext } from './command-context.js';
|
|
2
3
|
import type { HarnessReport } from './harness-report.js';
|
|
3
4
|
declare const SCHEMA_VERSION = "harness.v0.1";
|
|
@@ -271,6 +272,34 @@ export declare function writeHarnessAnalyzeResult(context: CommandContext, paylo
|
|
|
271
272
|
*/
|
|
272
273
|
export declare function emitWrittenArtifacts(context: CommandContext, written: string[], description: string): void;
|
|
273
274
|
export declare function repairExternalAnalystReport(report: HarnessReport, rootDir: string): HarnessReport;
|
|
275
|
+
/**
|
|
276
|
+
* A parsed coding-agent stream event handed to `onAgentEvent` consumers.
|
|
277
|
+
* Extends the analyst TUI event with an optional `detail` (file path or
|
|
278
|
+
* command extracted from tool input) so callers can render richer one-line
|
|
279
|
+
* progress cards. The analyst TUI ignores the extra field.
|
|
280
|
+
*/
|
|
281
|
+
export type CodingAgentEvent = AnalystTuiEvent & {
|
|
282
|
+
detail?: string;
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Translate a parsed coding-agent stream event into a one-line progress card
|
|
286
|
+
* ("editing src/llm/client.ts", "running bun add moda-ai", trimmed agent text).
|
|
287
|
+
* Returns undefined for low-signal events (system noise) so they never reach
|
|
288
|
+
* the user. Shared by the init progress board (via sdk-integration) and the
|
|
289
|
+
* agent-stream analyst progress events below.
|
|
290
|
+
*/
|
|
291
|
+
export declare function describeCodingAgentEvent(event: CodingAgentEvent): string | undefined;
|
|
292
|
+
/**
|
|
293
|
+
* Map a parsed analyst stream event to the `progress` agent event the CLI
|
|
294
|
+
* writes in agent-stream mode. Tool/command events get a `tool_use` phase so
|
|
295
|
+
* downstream consumers (the hosted analyze runner's progress parser counts
|
|
296
|
+
* phases starting with "tool") can track tool-call activity; agent text gets
|
|
297
|
+
* the generic `analyst` phase. Low-signal events map to undefined.
|
|
298
|
+
*/
|
|
299
|
+
export declare function analystEventProgress(event: CodingAgentEvent): {
|
|
300
|
+
phase: string;
|
|
301
|
+
message: string;
|
|
302
|
+
} | undefined;
|
|
274
303
|
export declare function sleep(ms: number): Promise<void>;
|
|
275
304
|
export declare function summarizeInvalidAnalystReport(report: unknown): Record<string, unknown>;
|
|
276
305
|
/**
|
|
@@ -306,6 +335,12 @@ export declare function runCodingAgent(options: {
|
|
|
306
335
|
timeoutMs?: number;
|
|
307
336
|
progressToStderr?: boolean;
|
|
308
337
|
onProgress?: (message: string, phase?: string) => void;
|
|
338
|
+
/**
|
|
339
|
+
* Receive parsed agent stream events incrementally while output stays
|
|
340
|
+
* buffered. Requires the streaming invocation, so passing it defaults
|
|
341
|
+
* `stream` to true when not set explicitly.
|
|
342
|
+
*/
|
|
343
|
+
onAgentEvent?: (event: CodingAgentEvent) => void;
|
|
309
344
|
}): Promise<CodingAgentRun>;
|
|
310
345
|
export declare function truncateText(value: string, maxLength: number): string;
|
|
311
346
|
export declare function scanHarness(options?: ScanOptions): HarnessGraph;
|
package/dist/harness.js
CHANGED
|
@@ -8,7 +8,7 @@ import { callControlAPI } from './api-client.js';
|
|
|
8
8
|
import { AnalystTui, shouldUseAnalystTui } from './analyst-tui.js';
|
|
9
9
|
import { resolveApiKey, resolveTenantId } from './config.js';
|
|
10
10
|
import { CliInputError, CliInputRequiredError } from './errors.js';
|
|
11
|
-
import { isRemoteHarnessAnalyze, runRemoteHarnessAnalyst } from './harness-remote.js';
|
|
11
|
+
import { HARNESS_REMOTE_RUN_STATE_PATH, isRemoteHarnessAnalyze, readRemoteRunState, runHarnessPull, runRemoteHarnessAnalyst, } from './harness-remote.js';
|
|
12
12
|
import { getActiveProfile } from './profiles.js';
|
|
13
13
|
import { enrichScanCaptureMetadata, annotatedSourceCandidates, extractToolBody, hydratePromptAndToolBodies, readPromptOrToolBody, resolveAnnotatedSourcePath, } from './harness-artifact-bodies.js';
|
|
14
14
|
import { approveHarnessReport, createHarnessReport, graphForReport, hashHarnessReport, hashStableJson as hashHarnessReportStableJson, HARNESS_REPORT_PATH, loadHarnessReportForSync, normalizeHarnessReport, readHarnessReport, resolveHarnessReportPath, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
|
|
@@ -325,6 +325,9 @@ export async function runHarnessCommand(context) {
|
|
|
325
325
|
});
|
|
326
326
|
return { exitCode: validation.status === 'pass' ? 0 : 1 };
|
|
327
327
|
}
|
|
328
|
+
case 'pull': {
|
|
329
|
+
return runHarnessPull({ context, rootDir, scanPlan });
|
|
330
|
+
}
|
|
328
331
|
case 'validate-report': {
|
|
329
332
|
const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
|
|
330
333
|
const report = readHarnessReport(reportPath);
|
|
@@ -411,11 +414,20 @@ export async function runHarnessCommand(context) {
|
|
|
411
414
|
});
|
|
412
415
|
const currentGraphHash = hashStableJson(graphForSync(graph));
|
|
413
416
|
const syncState = readHarnessSyncState(rootDir);
|
|
417
|
+
const remoteRun = readRemoteRunState(rootDir);
|
|
414
418
|
context.output.writeData({
|
|
415
419
|
manifest: HARNESS_PATH,
|
|
416
420
|
exists: existsSync(manifestPath),
|
|
417
421
|
summary: summarizeHarness(graph),
|
|
418
422
|
graphHash: currentGraphHash,
|
|
423
|
+
remoteRun: remoteRun
|
|
424
|
+
? {
|
|
425
|
+
statePath: HARNESS_REMOTE_RUN_STATE_PATH,
|
|
426
|
+
runId: remoteRun.run_id,
|
|
427
|
+
status: remoteRun.status,
|
|
428
|
+
startedAt: remoteRun.started_at_iso || undefined,
|
|
429
|
+
}
|
|
430
|
+
: undefined,
|
|
419
431
|
sync: syncState
|
|
420
432
|
? {
|
|
421
433
|
statePath: SYNC_STATE_PATH,
|
|
@@ -439,7 +451,7 @@ export async function runHarnessCommand(context) {
|
|
|
439
451
|
return { exitCode: 0 };
|
|
440
452
|
}
|
|
441
453
|
default:
|
|
442
|
-
throw new CliInputError(`Unknown harness command '${subcommand}'.`, 'Usage: moda harness analyze|validate-report|approve|scan|sync|agents|graph|explain|write-summary|status');
|
|
454
|
+
throw new CliInputError(`Unknown harness command '${subcommand}'.`, 'Usage: moda harness analyze|pull|validate-report|approve|scan|sync|agents|graph|explain|write-summary|status');
|
|
443
455
|
}
|
|
444
456
|
}
|
|
445
457
|
export function writeHarnessAnalyzeResult(context, payload) {
|
|
@@ -527,12 +539,17 @@ async function runExternalHarnessAnalyst(options) {
|
|
|
527
539
|
rootDir: options.rootDir,
|
|
528
540
|
tenantId: options.tenant?.tenantId,
|
|
529
541
|
});
|
|
542
|
+
// Agent-stream mode also runs the analyst child in stream-json mode so its
|
|
543
|
+
// tool activity can be forwarded as NDJSON `progress` events (consumed by
|
|
544
|
+
// the hosted analyze runner for live run-state progress). Report extraction
|
|
545
|
+
// is unaffected: the report is read from .moda/harness-report.json.
|
|
546
|
+
const streamAnalystProgress = options.context.outputMode === 'agent-stream';
|
|
530
547
|
const invocation = resolveExternalHarnessAnalystInvocation({
|
|
531
548
|
adapter: options.adapter,
|
|
532
549
|
flags: options.context.flags,
|
|
533
550
|
prompt,
|
|
534
551
|
rootDir: options.rootDir,
|
|
535
|
-
stream: options.context.outputMode === 'human',
|
|
552
|
+
stream: options.context.outputMode === 'human' || streamAnalystProgress,
|
|
536
553
|
});
|
|
537
554
|
options.context.output.writeEvent({
|
|
538
555
|
event: 'progress',
|
|
@@ -584,6 +601,22 @@ async function runExternalHarnessAnalyst(options) {
|
|
|
584
601
|
elapsed_ms: Date.now() - options.context.startedAt,
|
|
585
602
|
});
|
|
586
603
|
},
|
|
604
|
+
// Forward the analyst's parsed tool/text events as progress events so
|
|
605
|
+
// agent-stream consumers see live activity ("reading src/agent.ts"),
|
|
606
|
+
// not just the periodic still-investigating ticker.
|
|
607
|
+
onAgentEvent: streamAnalystProgress
|
|
608
|
+
? (event) => {
|
|
609
|
+
const progress = analystEventProgress(event);
|
|
610
|
+
if (!progress)
|
|
611
|
+
return;
|
|
612
|
+
options.context.output.writeEvent({
|
|
613
|
+
event: 'progress',
|
|
614
|
+
phase: progress.phase,
|
|
615
|
+
message: progress.message,
|
|
616
|
+
elapsed_ms: Date.now() - options.context.startedAt,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
: undefined,
|
|
587
620
|
});
|
|
588
621
|
if (result.error) {
|
|
589
622
|
throw new CliInputError(`Harness analyst ${options.adapter} failed to run: ${result.error.message}`, 'Check that the selected coding agent CLI is installed and authenticated, or run `moda harness analyze --analyst=local-scan` for the deterministic fallback.');
|
|
@@ -835,6 +868,68 @@ function extractTextFromAgentEvent(event) {
|
|
|
835
868
|
function escapeRegExp(value) {
|
|
836
869
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
837
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Tool cards must stay one line (same convention as ask-stream's oneLine):
|
|
873
|
+
* collapse whitespace and truncate so multi-line agent prose never spills
|
|
874
|
+
* into a progress line.
|
|
875
|
+
*/
|
|
876
|
+
const EVENT_CARD_MAX = 100;
|
|
877
|
+
function eventCardLine(text) {
|
|
878
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
879
|
+
return collapsed.length > EVENT_CARD_MAX
|
|
880
|
+
? `${collapsed.slice(0, EVENT_CARD_MAX - 1)}…`
|
|
881
|
+
: collapsed;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Translate a parsed coding-agent stream event into a one-line progress card
|
|
885
|
+
* ("editing src/llm/client.ts", "running bun add moda-ai", trimmed agent text).
|
|
886
|
+
* Returns undefined for low-signal events (system noise) so they never reach
|
|
887
|
+
* the user. Shared by the init progress board (via sdk-integration) and the
|
|
888
|
+
* agent-stream analyst progress events below.
|
|
889
|
+
*/
|
|
890
|
+
export function describeCodingAgentEvent(event) {
|
|
891
|
+
if (event.kind === 'tool') {
|
|
892
|
+
const name = event.toolName ?? 'tool';
|
|
893
|
+
const detail = event.detail;
|
|
894
|
+
if (/edit|write|patch|create|notebook/i.test(name)) {
|
|
895
|
+
return eventCardLine(detail ? `editing ${detail}` : `editing files (${name})`);
|
|
896
|
+
}
|
|
897
|
+
if (/bash|shell|exec|command|terminal|run/i.test(name)) {
|
|
898
|
+
return eventCardLine(detail ? `running ${detail}` : `running a command (${name})`);
|
|
899
|
+
}
|
|
900
|
+
if (/read|grep|glob|search|ls|find|cat/i.test(name)) {
|
|
901
|
+
return eventCardLine(detail ? `reading ${detail}` : `reading files (${name})`);
|
|
902
|
+
}
|
|
903
|
+
return eventCardLine(detail ? `using ${name}: ${detail}` : `using ${name}`);
|
|
904
|
+
}
|
|
905
|
+
if (event.kind === 'command') {
|
|
906
|
+
const command = event.detail ?? event.message.replace(/^command\s+/, '');
|
|
907
|
+
return eventCardLine(`running ${command}`);
|
|
908
|
+
}
|
|
909
|
+
if (event.kind === 'agent_text') {
|
|
910
|
+
const text = eventCardLine(event.message);
|
|
911
|
+
return text.length > 0 ? text : undefined;
|
|
912
|
+
}
|
|
913
|
+
if (event.kind === 'rate_limit')
|
|
914
|
+
return 'waiting on a provider rate limit';
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Map a parsed analyst stream event to the `progress` agent event the CLI
|
|
919
|
+
* writes in agent-stream mode. Tool/command events get a `tool_use` phase so
|
|
920
|
+
* downstream consumers (the hosted analyze runner's progress parser counts
|
|
921
|
+
* phases starting with "tool") can track tool-call activity; agent text gets
|
|
922
|
+
* the generic `analyst` phase. Low-signal events map to undefined.
|
|
923
|
+
*/
|
|
924
|
+
export function analystEventProgress(event) {
|
|
925
|
+
const card = describeCodingAgentEvent(event);
|
|
926
|
+
if (!card)
|
|
927
|
+
return undefined;
|
|
928
|
+
return {
|
|
929
|
+
phase: event.kind === 'tool' || event.kind === 'command' ? 'tool_use' : 'analyst',
|
|
930
|
+
message: card,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
838
933
|
function runExternalAnalystProcess(invocation, options) {
|
|
839
934
|
return new Promise((resolveProcess) => {
|
|
840
935
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -889,22 +984,22 @@ function runExternalAnalystProcess(invocation, options) {
|
|
|
889
984
|
stdout += text;
|
|
890
985
|
else
|
|
891
986
|
stderr += text;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
process.stderr.write(chunk);
|
|
987
|
+
const wantsEventLines = options.streamEvents && stream === 'stdout' &&
|
|
988
|
+
(options.inheritedOutput || Boolean(options.onAgentEvent));
|
|
989
|
+
if (options.inheritedOutput && options.tui && stream === 'stderr') {
|
|
990
|
+
recordExternalAgentTextChunk(text, options.tui);
|
|
991
|
+
}
|
|
992
|
+
else if (wantsEventLines) {
|
|
993
|
+
eventLineBuffer += text;
|
|
994
|
+
const lines = eventLineBuffer.split(/\r?\n/);
|
|
995
|
+
eventLineBuffer = lines.pop() ?? '';
|
|
996
|
+
for (const line of lines) {
|
|
997
|
+
recordExternalAgentEventLine(line, options);
|
|
906
998
|
}
|
|
907
999
|
}
|
|
1000
|
+
else if (options.inheritedOutput) {
|
|
1001
|
+
process.stderr.write(chunk);
|
|
1002
|
+
}
|
|
908
1003
|
if (stdout.length + stderr.length > options.maxBuffer && !maxBufferExceeded) {
|
|
909
1004
|
maxBufferExceeded = true;
|
|
910
1005
|
child.kill('SIGTERM');
|
|
@@ -994,6 +1089,9 @@ function recordExternalAgentEventLine(line, options) {
|
|
|
994
1089
|
const event = parseExternalAgentEventLine(line);
|
|
995
1090
|
if (!event)
|
|
996
1091
|
return;
|
|
1092
|
+
options.onAgentEvent?.(event);
|
|
1093
|
+
if (!options.inheritedOutput)
|
|
1094
|
+
return;
|
|
997
1095
|
options.tui?.recordEvent(event);
|
|
998
1096
|
if (options.tui)
|
|
999
1097
|
return;
|
|
@@ -1036,7 +1134,8 @@ function parseExternalAgentEvent(event) {
|
|
|
1036
1134
|
stringValue(event.toolName) ??
|
|
1037
1135
|
stringValue(call?.name);
|
|
1038
1136
|
if (toolName && /tool|call|exec|command|bash/i.test(`${type ?? ''} ${subtype ?? ''}`)) {
|
|
1039
|
-
|
|
1137
|
+
const detail = toolInputDetail(event.input ?? event.arguments ?? call?.input ?? call?.arguments);
|
|
1138
|
+
return { kind: 'tool', toolName, message: `tool ${toolName}`, ...(detail ? { detail } : {}) };
|
|
1040
1139
|
}
|
|
1041
1140
|
const directText = stringValue(event.text) ??
|
|
1042
1141
|
stringValue(event.message) ??
|
|
@@ -1063,8 +1162,9 @@ function parseExternalAgentEvent(event) {
|
|
|
1063
1162
|
return parsed;
|
|
1064
1163
|
}
|
|
1065
1164
|
const command = stringValue(event.command) ?? stringValue(event.cmd);
|
|
1066
|
-
if (command)
|
|
1067
|
-
return { kind: 'command', message: `command ${truncateText(command, 300)}
|
|
1165
|
+
if (command) {
|
|
1166
|
+
return { kind: 'command', message: `command ${truncateText(command, 300)}`, detail: truncateText(command, 300) };
|
|
1167
|
+
}
|
|
1068
1168
|
if (type && !isLowSignalEventType(type)) {
|
|
1069
1169
|
return { kind: 'system', message: subtype ? `${type}:${subtype}` : type };
|
|
1070
1170
|
}
|
|
@@ -1076,7 +1176,10 @@ function parseAgentContentPart(part) {
|
|
|
1076
1176
|
const partType = stringValue(part.type);
|
|
1077
1177
|
if (partType === 'tool_use' || partType === 'tool_call') {
|
|
1078
1178
|
const name = stringValue(part.name) ?? stringValue(part.tool_name);
|
|
1079
|
-
|
|
1179
|
+
if (!name)
|
|
1180
|
+
return undefined;
|
|
1181
|
+
const detail = toolInputDetail(part.input ?? part.arguments);
|
|
1182
|
+
return { kind: 'tool', toolName: name, message: `tool ${name}`, ...(detail ? { detail } : {}) };
|
|
1080
1183
|
}
|
|
1081
1184
|
if (partType === 'tool_result' || partType === 'tool_output')
|
|
1082
1185
|
return undefined;
|
|
@@ -1089,6 +1192,20 @@ function parseAgentContentPart(part) {
|
|
|
1089
1192
|
return { kind: 'tool', toolName: name, message: `tool ${name}` };
|
|
1090
1193
|
return undefined;
|
|
1091
1194
|
}
|
|
1195
|
+
/** Pull the most useful one-line detail (file path or command) out of a tool input payload. */
|
|
1196
|
+
function toolInputDetail(input) {
|
|
1197
|
+
if (!isRecord(input))
|
|
1198
|
+
return undefined;
|
|
1199
|
+
const value = stringValue(input.file_path) ??
|
|
1200
|
+
stringValue(input.filePath) ??
|
|
1201
|
+
stringValue(input.path) ??
|
|
1202
|
+
stringValue(input.notebook_path) ??
|
|
1203
|
+
stringValue(input.command) ??
|
|
1204
|
+
stringValue(input.cmd) ??
|
|
1205
|
+
stringValue(input.pattern) ??
|
|
1206
|
+
stringValue(input.query);
|
|
1207
|
+
return value ? truncateText(firstMeaningfulLine(value), 300) : undefined;
|
|
1208
|
+
}
|
|
1092
1209
|
function firstMeaningfulLine(value) {
|
|
1093
1210
|
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? value.trim();
|
|
1094
1211
|
}
|
|
@@ -1308,7 +1425,7 @@ export function codingAgentDisplayName(adapter) {
|
|
|
1308
1425
|
}
|
|
1309
1426
|
/** Spawn an installed coding-agent CLI with a prompt; capture its output. */
|
|
1310
1427
|
export async function runCodingAgent(options) {
|
|
1311
|
-
const stream = options.stream ??
|
|
1428
|
+
const stream = options.stream ?? Boolean(options.onAgentEvent);
|
|
1312
1429
|
const invocation = resolveExternalHarnessAnalystInvocation({
|
|
1313
1430
|
adapter: options.adapter,
|
|
1314
1431
|
flags: {},
|
|
@@ -1331,6 +1448,7 @@ export async function runCodingAgent(options) {
|
|
|
1331
1448
|
maxAttempts: externalAnalystMaxAttempts(options.adapter),
|
|
1332
1449
|
backoffMs: numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRY_BACKOFF_MS', 6_000),
|
|
1333
1450
|
onProgress: options.onProgress,
|
|
1451
|
+
onAgentEvent: options.onAgentEvent,
|
|
1334
1452
|
});
|
|
1335
1453
|
return {
|
|
1336
1454
|
adapter: invocation.adapter,
|