@moda-ai/cli 1.18.0 → 1.19.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/dist/cli.js +7 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.js +28 -0
- package/dist/harness.d.ts +45 -0
- package/dist/harness.js +203 -12
- package/dist/init/index.d.ts +39 -1
- package/dist/init/index.js +422 -10
- package/dist/init/tenant.js +7 -25
- 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/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -177,6 +177,7 @@ Commands:
|
|
|
177
177
|
prompts sync --watch Upload again when prompt files change
|
|
178
178
|
prompts promote <key> Move a dev/staging/prod label
|
|
179
179
|
prompts ab Run a judged prompt A/B replay experiment
|
|
180
|
+
prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
|
|
180
181
|
|
|
181
182
|
Skills:
|
|
182
183
|
skills gen Kick off tenant-wide skill generation
|
|
@@ -249,6 +250,10 @@ Init flags:
|
|
|
249
250
|
--no-ci-cd Skip CI/CD workflow creation
|
|
250
251
|
--autosync Create GitHub Actions workflow to sync prompts + skills to Moda on push to main
|
|
251
252
|
--no-autosync Skip autosync workflow creation
|
|
253
|
+
--harness-rescan Create GitHub Actions workflow to re-scan the harness and sync it to Moda on push to main
|
|
254
|
+
--no-harness-rescan Skip harness rescan workflow creation
|
|
255
|
+
--harness-rescan-paths=GLOBS Comma-separated path globs that scope which pushes trigger the
|
|
256
|
+
harness rescan workflow (implies --harness-rescan)
|
|
252
257
|
--offline Run final validation offline (skip the online telemetry check)
|
|
253
258
|
|
|
254
259
|
Provision flags (headless; requires an existing "moda auth login" session):
|
|
@@ -291,6 +296,7 @@ Examples:
|
|
|
291
296
|
moda harness explain --agent=agent_support
|
|
292
297
|
moda init --ci-cd
|
|
293
298
|
moda init --autosync
|
|
299
|
+
moda init --harness-rescan --harness-rescan-paths='src/agents/**'
|
|
294
300
|
moda overview --days-back=30
|
|
295
301
|
moda clusters --time-range=7d
|
|
296
302
|
moda conversations --search="error" --limit=5
|
|
@@ -706,7 +712,7 @@ export const commandRegistry = createCommandRegistry([
|
|
|
706
712
|
{
|
|
707
713
|
name: 'init',
|
|
708
714
|
description: 'Set up Moda in your project',
|
|
709
|
-
examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd', 'moda init --autosync'],
|
|
715
|
+
examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd', 'moda init --autosync', 'moda init --harness-rescan'],
|
|
710
716
|
auth: 'none',
|
|
711
717
|
outputModes: ['human', 'json', 'agent'],
|
|
712
718
|
mutability: 'write',
|
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,18 @@ export declare function saveCliConfig(update: CliConfig, options?: ProfileIOOpti
|
|
|
28
28
|
* long-lived process (or a test) sees the current value.
|
|
29
29
|
*/
|
|
30
30
|
export declare function resolveApiKey(options?: ProfileIOOptions): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Signed CLI keys embed their tenant: the format is
|
|
33
|
+
* moda_sk_<base64url("v1:keyId:tenantId:timestamp:signature")> — see
|
|
34
|
+
* createApiKey/parseAndVerifySignedKey in
|
|
35
|
+
* apps/moda-backend/src/api-keys/api-keys.service.ts. The server derives the
|
|
36
|
+
* upload destination tenant from the key alone, so this is the only
|
|
37
|
+
* client-side tenant source that matches where data actually lands
|
|
38
|
+
* (config / MODA_TENANT_ID can disagree with a shadowing env key). The HMAC
|
|
39
|
+
* signature can only be verified server-side; treat the result as
|
|
40
|
+
* informational.
|
|
41
|
+
*/
|
|
42
|
+
export declare function embeddedTenantIdFromApiKey(key: string): string | undefined;
|
|
31
43
|
/**
|
|
32
44
|
* Resolve the ingest worker base URL used by `moda workspace snapshot`.
|
|
33
45
|
* Env override -> active profile -> legacy config -> production default.
|
package/dist/config.js
CHANGED
|
@@ -40,6 +40,34 @@ export function resolveApiKey(options = {}) {
|
|
|
40
40
|
loadCliConfig(options).api_key ||
|
|
41
41
|
undefined);
|
|
42
42
|
}
|
|
43
|
+
const SIGNED_API_KEY_PREFIX = 'moda_sk_';
|
|
44
|
+
const SIGNED_API_KEY_VERSION = 'v1';
|
|
45
|
+
/**
|
|
46
|
+
* Signed CLI keys embed their tenant: the format is
|
|
47
|
+
* moda_sk_<base64url("v1:keyId:tenantId:timestamp:signature")> — see
|
|
48
|
+
* createApiKey/parseAndVerifySignedKey in
|
|
49
|
+
* apps/moda-backend/src/api-keys/api-keys.service.ts. The server derives the
|
|
50
|
+
* upload destination tenant from the key alone, so this is the only
|
|
51
|
+
* client-side tenant source that matches where data actually lands
|
|
52
|
+
* (config / MODA_TENANT_ID can disagree with a shadowing env key). The HMAC
|
|
53
|
+
* signature can only be verified server-side; treat the result as
|
|
54
|
+
* informational.
|
|
55
|
+
*/
|
|
56
|
+
export function embeddedTenantIdFromApiKey(key) {
|
|
57
|
+
if (!key.startsWith(SIGNED_API_KEY_PREFIX))
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const decoded = Buffer.from(key.slice(SIGNED_API_KEY_PREFIX.length), 'base64url').toString('utf8');
|
|
61
|
+
// Expected format: version:keyId:tenantId:timestamp:signature
|
|
62
|
+
const parts = decoded.split(':');
|
|
63
|
+
if (parts.length !== 5 || parts[0] !== SIGNED_API_KEY_VERSION)
|
|
64
|
+
return undefined;
|
|
65
|
+
return parts[2] || undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
43
71
|
/**
|
|
44
72
|
* Resolve the ingest worker base URL used by `moda workspace snapshot`.
|
|
45
73
|
* Env override -> active profile -> legacy config -> production default.
|
package/dist/harness.d.ts
CHANGED
|
@@ -390,6 +390,51 @@ declare function buildCommandScanPlan(context: CommandContext, rootDir: string):
|
|
|
390
390
|
};
|
|
391
391
|
export declare function writeHumanProgress(context: CommandContext, message: string): void;
|
|
392
392
|
export declare function readHarnessSyncState(rootDir: string): HarnessSyncState | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* The identity a sync-style upload will actually authenticate as, resolved
|
|
395
|
+
* with the same precedence as the API call itself (MODA_API_KEY env wins over
|
|
396
|
+
* the active profile / legacy config). Used for the pre-upload
|
|
397
|
+
* "Syncing as <credential> → tenant <id>" banner so a shadowed or stale key
|
|
398
|
+
* is visible BEFORE data lands in the wrong tenant.
|
|
399
|
+
*
|
|
400
|
+
* The destination tenant is derived from the WINNING KEY itself (signed CLI
|
|
401
|
+
* keys embed their tenant, and the server resolves the destination from the
|
|
402
|
+
* key alone) — never from flag/env/profile config, which is exactly what a
|
|
403
|
+
* shadowing stale key disagrees with. When the key does not embed a tenant,
|
|
404
|
+
* the config-resolved id is returned with `tenantSource: 'config'` so
|
|
405
|
+
* callers must label it unverified. Best-effort: never throws (a broken
|
|
406
|
+
* MODA_PROFILE must not abort onboarding).
|
|
407
|
+
*/
|
|
408
|
+
export declare function describeSyncIdentity(options?: {
|
|
409
|
+
profile?: string;
|
|
410
|
+
env?: NodeJS.ProcessEnv;
|
|
411
|
+
}): {
|
|
412
|
+
credential: string;
|
|
413
|
+
tenantId?: string;
|
|
414
|
+
/** 'api-key' = decoded from the winning key (authoritative); 'config' = flag/env/profile fallback (unverified). */
|
|
415
|
+
tenantSource?: 'api-key' | 'config';
|
|
416
|
+
} | undefined;
|
|
417
|
+
/**
|
|
418
|
+
* Human tenant label for the identity banner: key-derived ids are printed
|
|
419
|
+
* bare; config-derived ids are explicitly marked unverified so the banner
|
|
420
|
+
* never asserts a destination the server might not use.
|
|
421
|
+
*/
|
|
422
|
+
export declare function describeSyncTenantLabel(identity: {
|
|
423
|
+
tenantId?: string;
|
|
424
|
+
tenantSource?: 'api-key' | 'config';
|
|
425
|
+
}): string;
|
|
426
|
+
/**
|
|
427
|
+
* Reports in subdirectories that are FRESHER than the root's
|
|
428
|
+
* `.moda/harness-report.json` (a missing root report counts as infinitely
|
|
429
|
+
* old). Catches the `--path` trap: `harness analyze --path=<subdir>` writes
|
|
430
|
+
* `<subdir>/.moda/harness-report.json`, and a later root-level
|
|
431
|
+
* `harness approve`/`sync` silently targets the stale (or absent) root report.
|
|
432
|
+
* Newest first.
|
|
433
|
+
*/
|
|
434
|
+
export declare function findFresherHarnessReports(rootDir: string): Array<{
|
|
435
|
+
path: string;
|
|
436
|
+
mtimeMs: number;
|
|
437
|
+
}>;
|
|
393
438
|
export declare function writeHarnessArtifacts(graph: HarnessGraph, rootDir: string, options?: {
|
|
394
439
|
summary?: boolean;
|
|
395
440
|
}): string[];
|
package/dist/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, delimiter, dirname, extname, join, relative, resolve, sep } f
|
|
|
6
6
|
import { confirm, select } from '@inquirer/prompts';
|
|
7
7
|
import { callControlAPI } from './api-client.js';
|
|
8
8
|
import { AnalystTui, shouldUseAnalystTui } from './analyst-tui.js';
|
|
9
|
-
import { resolveApiKey, resolveTenantId } from './config.js';
|
|
9
|
+
import { embeddedTenantIdFromApiKey, resolveApiKey, resolveTenantId } from './config.js';
|
|
10
10
|
import { CliInputError, CliInputRequiredError } from './errors.js';
|
|
11
11
|
import { HARNESS_REMOTE_RUN_STATE_PATH, isRemoteHarnessAnalyze, readRemoteRunState, runHarnessPull, runRemoteHarnessAnalyst, } from './harness-remote.js';
|
|
12
12
|
import { getActiveProfile } from './profiles.js';
|
|
@@ -101,6 +101,24 @@ export async function runHarnessCommand(context) {
|
|
|
101
101
|
if (!resolveApiKey({ profile: context.profile, env: context.env }) && !context.dryRun) {
|
|
102
102
|
throw new CliInputError('No Moda API key found for harness sync.', 'Run `moda init` first, set MODA_API_KEY, or select a profile with an API key.');
|
|
103
103
|
}
|
|
104
|
+
warnFresherSubdirHarnessReports(context, rootDir, context.flags['from-report'] ?? context.flags.report);
|
|
105
|
+
if (!context.dryRun) {
|
|
106
|
+
const identity = describeSyncIdentity({ profile: context.profile, env: context.env });
|
|
107
|
+
if (identity) {
|
|
108
|
+
// The banner's tenant comes from the winning key when it embeds
|
|
109
|
+
// one. Otherwise fall back to the flag/env/profile-resolved tenant
|
|
110
|
+
// but label it unverified — that source is precisely what a
|
|
111
|
+
// shadowing stale key disagrees with, so it must never be asserted
|
|
112
|
+
// as the destination.
|
|
113
|
+
const label = identity.tenantSource === 'api-key'
|
|
114
|
+
? describeSyncTenantLabel(identity)
|
|
115
|
+
: describeSyncTenantLabel({
|
|
116
|
+
tenantId: tenant?.tenantId ?? identity.tenantId,
|
|
117
|
+
tenantSource: 'config',
|
|
118
|
+
});
|
|
119
|
+
writeHumanProgress(context, `Syncing as ${identity.credential} → tenant ${label}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
104
122
|
if (context.flags['from-report'] || context.flags.report) {
|
|
105
123
|
const reportPath = resolveHarnessReportPath(rootDir, context.flags['from-report'] ?? context.flags.report);
|
|
106
124
|
const { report, validation, approval, approved } = loadHarnessReportForSync(rootDir, reportPath);
|
|
@@ -344,6 +362,7 @@ export async function runHarnessCommand(context) {
|
|
|
344
362
|
return { exitCode: validation.status === 'pass' ? 0 : 1 };
|
|
345
363
|
}
|
|
346
364
|
case 'approve': {
|
|
365
|
+
warnFresherSubdirHarnessReports(context, rootDir, context.positionals[1] ?? context.flags.report);
|
|
347
366
|
const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
|
|
348
367
|
const approval = await approveHarnessReport({
|
|
349
368
|
rootDir,
|
|
@@ -363,7 +382,7 @@ export async function runHarnessCommand(context) {
|
|
|
363
382
|
return { exitCode: 0 };
|
|
364
383
|
}
|
|
365
384
|
case 'agents': {
|
|
366
|
-
const graph = loadOrScanHarness(rootDir, tenant);
|
|
385
|
+
const graph = loadOrScanHarness(rootDir, tenant, context);
|
|
367
386
|
context.output.writeData({
|
|
368
387
|
summary: summarizeHarness(graph),
|
|
369
388
|
agentFamilies: graph.agentFamilies,
|
|
@@ -373,12 +392,12 @@ export async function runHarnessCommand(context) {
|
|
|
373
392
|
return { exitCode: 0 };
|
|
374
393
|
}
|
|
375
394
|
case 'graph': {
|
|
376
|
-
const graph = loadOrScanHarness(rootDir, tenant);
|
|
395
|
+
const graph = loadOrScanHarness(rootDir, tenant, context);
|
|
377
396
|
context.output.writeData(graph);
|
|
378
397
|
return { exitCode: 0 };
|
|
379
398
|
}
|
|
380
399
|
case 'explain': {
|
|
381
|
-
const graph = loadOrScanHarness(rootDir, tenant);
|
|
400
|
+
const graph = loadOrScanHarness(rootDir, tenant, context);
|
|
382
401
|
if (selectedAgentId) {
|
|
383
402
|
const agent = graph.runtimeAgents.find((candidate) => candidate.id === selectedAgentId);
|
|
384
403
|
if (!agent) {
|
|
@@ -2040,15 +2059,187 @@ function selectedRuntimeAgentId(flags) {
|
|
|
2040
2059
|
return flags.agent;
|
|
2041
2060
|
return undefined;
|
|
2042
2061
|
}
|
|
2043
|
-
function loadOrScanHarness(rootDir, tenant) {
|
|
2062
|
+
function loadOrScanHarness(rootDir, tenant, context) {
|
|
2044
2063
|
const manifestPath = join(rootDir, HARNESS_PATH);
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2064
|
+
if (existsSync(manifestPath))
|
|
2065
|
+
return readHarnessGraph(manifestPath);
|
|
2066
|
+
// Provenance banner: without a persisted manifest this command output comes
|
|
2067
|
+
// from a live scan of the current directory, which is easy to mistake for
|
|
2068
|
+
// synced/account data (especially right after switching repos or profiles).
|
|
2069
|
+
if (context) {
|
|
2070
|
+
writeHarnessNotice(context, `No ${HARNESS_PATH} found; showing a live scan of ${rootDir}. ` +
|
|
2071
|
+
'This is local scan output, NOT synced or account data. ' +
|
|
2072
|
+
'Run `moda harness scan` to persist it and `moda harness sync` to upload it.');
|
|
2073
|
+
}
|
|
2074
|
+
return scanHarness({
|
|
2075
|
+
rootDir,
|
|
2076
|
+
tenantId: tenant?.tenantId,
|
|
2077
|
+
tenantSource: tenant?.source,
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Warning line that must be visible in every output mode: styled progress on
|
|
2082
|
+
* stderr for humans, a plain `Warning:` stderr line for machine modes (stdout
|
|
2083
|
+
* stays machine-readable JSON either way). Quiet suppresses it like the rest
|
|
2084
|
+
* of the harness progress surface.
|
|
2085
|
+
*/
|
|
2086
|
+
function writeHarnessNotice(context, message) {
|
|
2087
|
+
if (context.quiet && context.flags.progress !== 'true')
|
|
2088
|
+
return;
|
|
2089
|
+
if (context.outputMode === 'human') {
|
|
2090
|
+
console.error(renderHarnessProgress(message, 'warning'));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
console.error(`Warning: ${message}`);
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* The identity a sync-style upload will actually authenticate as, resolved
|
|
2097
|
+
* with the same precedence as the API call itself (MODA_API_KEY env wins over
|
|
2098
|
+
* the active profile / legacy config). Used for the pre-upload
|
|
2099
|
+
* "Syncing as <credential> → tenant <id>" banner so a shadowed or stale key
|
|
2100
|
+
* is visible BEFORE data lands in the wrong tenant.
|
|
2101
|
+
*
|
|
2102
|
+
* The destination tenant is derived from the WINNING KEY itself (signed CLI
|
|
2103
|
+
* keys embed their tenant, and the server resolves the destination from the
|
|
2104
|
+
* key alone) — never from flag/env/profile config, which is exactly what a
|
|
2105
|
+
* shadowing stale key disagrees with. When the key does not embed a tenant,
|
|
2106
|
+
* the config-resolved id is returned with `tenantSource: 'config'` so
|
|
2107
|
+
* callers must label it unverified. Best-effort: never throws (a broken
|
|
2108
|
+
* MODA_PROFILE must not abort onboarding).
|
|
2109
|
+
*/
|
|
2110
|
+
export function describeSyncIdentity(options = {}) {
|
|
2111
|
+
const apiKey = resolveApiKey(options);
|
|
2112
|
+
if (!apiKey)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
const redacted = apiKey.length > 12
|
|
2115
|
+
? `${apiKey.slice(0, 8)}…${apiKey.slice(-4)}`
|
|
2116
|
+
: `…${apiKey.slice(-4)}`;
|
|
2117
|
+
const env = options.env ?? process.env;
|
|
2118
|
+
// getActiveProfile throws when the named profile is missing (e.g. a stale
|
|
2119
|
+
// MODA_PROFILE) even though the key already resolved via legacy config —
|
|
2120
|
+
// the banner is advisory and must never abort the command around it.
|
|
2121
|
+
let profileName;
|
|
2122
|
+
try {
|
|
2123
|
+
profileName = getActiveProfile(options).name;
|
|
2124
|
+
}
|
|
2125
|
+
catch {
|
|
2126
|
+
profileName = undefined;
|
|
2127
|
+
}
|
|
2128
|
+
const credential = env.MODA_API_KEY === apiKey
|
|
2129
|
+
? `MODA_API_KEY env key (${redacted})`
|
|
2130
|
+
: profileName
|
|
2131
|
+
? `profile '${profileName}' key (${redacted})`
|
|
2132
|
+
: `stored key (${redacted})`;
|
|
2133
|
+
const keyTenantId = embeddedTenantIdFromApiKey(apiKey);
|
|
2134
|
+
if (keyTenantId) {
|
|
2135
|
+
return { credential, tenantId: keyTenantId, tenantSource: 'api-key' };
|
|
2136
|
+
}
|
|
2137
|
+
const configTenantId = resolveTenantId(options);
|
|
2138
|
+
return {
|
|
2139
|
+
credential,
|
|
2140
|
+
tenantId: configTenantId,
|
|
2141
|
+
tenantSource: configTenantId ? 'config' : undefined,
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Human tenant label for the identity banner: key-derived ids are printed
|
|
2146
|
+
* bare; config-derived ids are explicitly marked unverified so the banner
|
|
2147
|
+
* never asserts a destination the server might not use.
|
|
2148
|
+
*/
|
|
2149
|
+
export function describeSyncTenantLabel(identity) {
|
|
2150
|
+
if (!identity.tenantId)
|
|
2151
|
+
return 'unresolved';
|
|
2152
|
+
if (identity.tenantSource === 'api-key')
|
|
2153
|
+
return identity.tenantId;
|
|
2154
|
+
return `${identity.tenantId} (unverified — from local config, not the API key; the server derives the destination tenant from the key)`;
|
|
2155
|
+
}
|
|
2156
|
+
/** Bounded walk looking for `<subdir>/.moda/harness-report.json` files. */
|
|
2157
|
+
const FRESHER_REPORT_MAX_DEPTH = 4;
|
|
2158
|
+
const FRESHER_REPORT_MAX_DIRS = 4_000;
|
|
2159
|
+
/**
|
|
2160
|
+
* Reports in subdirectories that are FRESHER than the root's
|
|
2161
|
+
* `.moda/harness-report.json` (a missing root report counts as infinitely
|
|
2162
|
+
* old). Catches the `--path` trap: `harness analyze --path=<subdir>` writes
|
|
2163
|
+
* `<subdir>/.moda/harness-report.json`, and a later root-level
|
|
2164
|
+
* `harness approve`/`sync` silently targets the stale (or absent) root report.
|
|
2165
|
+
* Newest first.
|
|
2166
|
+
*/
|
|
2167
|
+
export function findFresherHarnessReports(rootDir) {
|
|
2168
|
+
let rootMtimeMs = 0;
|
|
2169
|
+
try {
|
|
2170
|
+
rootMtimeMs = lstatSync(join(rootDir, HARNESS_REPORT_PATH)).mtimeMs;
|
|
2171
|
+
}
|
|
2172
|
+
catch {
|
|
2173
|
+
rootMtimeMs = 0;
|
|
2174
|
+
}
|
|
2175
|
+
const fresher = [];
|
|
2176
|
+
let visited = 0;
|
|
2177
|
+
const walk = (dir, depth) => {
|
|
2178
|
+
if (depth > FRESHER_REPORT_MAX_DEPTH || visited >= FRESHER_REPORT_MAX_DIRS)
|
|
2179
|
+
return;
|
|
2180
|
+
visited += 1;
|
|
2181
|
+
let entries;
|
|
2182
|
+
try {
|
|
2183
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2184
|
+
}
|
|
2185
|
+
catch {
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
for (const entry of entries) {
|
|
2189
|
+
// Dot-dirs (including each dir's own `.moda`, checked explicitly below)
|
|
2190
|
+
// and scan-skipped dirs never host a user project subtree.
|
|
2191
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || SKIPPED_DIRS.has(entry.name))
|
|
2192
|
+
continue;
|
|
2193
|
+
const child = join(dir, entry.name);
|
|
2194
|
+
try {
|
|
2195
|
+
const candidate = lstatSync(join(child, HARNESS_REPORT_PATH));
|
|
2196
|
+
if (candidate.isFile() && candidate.mtimeMs > rootMtimeMs) {
|
|
2197
|
+
fresher.push({
|
|
2198
|
+
path: normalizePath(relative(rootDir, join(child, HARNESS_REPORT_PATH))),
|
|
2199
|
+
mtimeMs: candidate.mtimeMs,
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
catch {
|
|
2204
|
+
// No report in this subdir; keep walking.
|
|
2205
|
+
}
|
|
2206
|
+
walk(child, depth + 1);
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
walk(rootDir, 1);
|
|
2210
|
+
return fresher.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* `--path` trap guard for `harness approve` / `harness sync`: when the
|
|
2214
|
+
* report the command is about to use is older than a fresher
|
|
2215
|
+
* `<subdir>/.moda/harness-report.json`, point at the fresher one. Skipped
|
|
2216
|
+
* when the user explicitly named a report file (they already chose).
|
|
2217
|
+
*/
|
|
2218
|
+
function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
|
|
2219
|
+
// Only treat an explicit report as an opt-out when it points somewhere other
|
|
2220
|
+
// than the default root report. `--from-report=.moda/harness-report.json`
|
|
2221
|
+
// still targets the default report, so the fresher-subdir warning must fire.
|
|
2222
|
+
const explicitReportPath = explicitReport && explicitReport !== 'true'
|
|
2223
|
+
? normalizePath(relative(rootDir, resolve(rootDir, explicitReport)))
|
|
2224
|
+
: undefined;
|
|
2225
|
+
if (explicitReportPath && explicitReportPath !== normalizePath(HARNESS_REPORT_PATH))
|
|
2226
|
+
return;
|
|
2227
|
+
let fresher;
|
|
2228
|
+
try {
|
|
2229
|
+
fresher = findFresherHarnessReports(rootDir);
|
|
2230
|
+
}
|
|
2231
|
+
catch {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (fresher.length === 0)
|
|
2235
|
+
return;
|
|
2236
|
+
const newest = fresher[0];
|
|
2237
|
+
const subdir = normalizePath(dirname(dirname(newest.path)));
|
|
2238
|
+
const rootState = existsSync(join(rootDir, HARNESS_REPORT_PATH))
|
|
2239
|
+
? `older ${HARNESS_REPORT_PATH}`
|
|
2240
|
+
: `no ${HARNESS_REPORT_PATH}`;
|
|
2241
|
+
writeHarnessNotice(context, `A fresher harness report exists at ${newest.path} (this directory has ${rootState}). ` +
|
|
2242
|
+
`If you meant that report, re-run with --path=${subdir}.`);
|
|
2052
2243
|
}
|
|
2053
2244
|
function readHarnessGraph(manifestPath) {
|
|
2054
2245
|
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
package/dist/init/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface InitPhaseResult {
|
|
|
11
11
|
details?: string;
|
|
12
12
|
}
|
|
13
13
|
export interface InitSetupAction {
|
|
14
|
-
category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'autosync' | 'validation';
|
|
14
|
+
category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'autosync' | 'harness-rescan' | 'validation';
|
|
15
15
|
title: string;
|
|
16
16
|
status: 'planned' | 'applied' | 'skipped' | 'manual';
|
|
17
17
|
approved: boolean;
|
|
@@ -28,6 +28,44 @@ export interface InitRunOptions {
|
|
|
28
28
|
onEvent?: (event: InitProgressEvent) => void;
|
|
29
29
|
}
|
|
30
30
|
export declare function runInit(flags: Record<string, unknown>, options?: InitRunOptions): Promise<InitResult>;
|
|
31
|
+
export declare function writeCiCdWorkflow(cwd?: string): string;
|
|
32
|
+
export declare function writeAutosyncWorkflow(cwd?: string): string;
|
|
33
|
+
export declare function writeHarnessRescanWorkflow(cwd?: string, paths?: string[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* Signals from the discovered graph used by the init 0-agent quality gate:
|
|
36
|
+
* runtime agent count plus the LLM framework/provider names detected in the
|
|
37
|
+
* repo.
|
|
38
|
+
*/
|
|
39
|
+
export interface HarnessQualitySignals {
|
|
40
|
+
runtimeAgents: number;
|
|
41
|
+
frameworks: string[];
|
|
42
|
+
providers: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the gate signals from the SAME graph the auto-sync uploads: the cited
|
|
46
|
+
* report (`.moda/harness-report.json`, consumed by `harness sync --from-report`)
|
|
47
|
+
* is authoritative, so read its `graph` first. Only fall back to the standalone
|
|
48
|
+
* manifest (`.moda/harness.json`) when no report exists, otherwise a stale or
|
|
49
|
+
* missing manifest could let an empty report slip past the gate and overwrite
|
|
50
|
+
* the tenant's harness. Returns undefined when neither is readable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readHarnessQualitySignals(cwd?: string): HarnessQualitySignals | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* True when discovery found no runtime agents even though the repo carries
|
|
55
|
+
* LLM framework/provider signals — the signature of a failed discovery run,
|
|
56
|
+
* not an agent-free repo.
|
|
57
|
+
*/
|
|
58
|
+
export declare function harnessLooksIncomplete(signals: HarnessQualitySignals): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Deterministic repo-side LLM signals: well-known LLM package names found in
|
|
61
|
+
* dependency manifests. Independent of the discovery output, so a fully
|
|
62
|
+
* failed discovery (empty graph) cannot bypass the 0-agent gate.
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectRepoLlmSignals(cwd?: string): {
|
|
65
|
+
frameworks: string[];
|
|
66
|
+
providers: string[];
|
|
67
|
+
};
|
|
68
|
+
export declare function resumeFlagsForInit(flags: Record<string, unknown>): string[];
|
|
31
69
|
/**
|
|
32
70
|
* Onboarding runs harness discovery SERVER-SIDE by default so the customer
|
|
33
71
|
* needs no local coding agent. Opt out with `--local`, `--no-remote`,
|