agentic-workflow-manager 9.0.2 → 9.1.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/src/commands/ledger/index.js +13 -0
- package/dist/src/commands/process/index.js +81 -0
- package/dist/src/core/dashboard/collect.js +30 -5
- package/dist/src/core/dashboard/sanitize.js +24 -2
- package/dist/src/core/process/body.js +181 -0
- package/dist/src/core/process/discover.js +94 -0
- package/dist/src/core/process/model.js +109 -0
- package/dist/src/core/process/types.js +5 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/process.test.js +88 -0
- package/dist/tests/core/dashboard/collect.test.js +25 -21
- package/dist/tests/core/dashboard/processes-section.test.js +71 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +58 -0
- package/dist/tests/core/process/body.test.js +239 -0
- package/dist/tests/core/process/discover.test.js +139 -0
- package/dist/tests/core/process/model.test.js +83 -0
- package/dist/tests/core/process/no-regression.test.js +57 -0
- package/dist/tests/integration/process-model-e2e.test.js +161 -0
- package/dist/tests/structural/process-model-single-parser.test.js +63 -0
- package/package.json +1 -1
|
@@ -6,6 +6,15 @@ const types_1 = require("../../core/ledger/types");
|
|
|
6
6
|
function archiveLabel() {
|
|
7
7
|
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, '');
|
|
8
8
|
}
|
|
9
|
+
// R1a's own retro: across this session, reviewer subagents repeatedly verified
|
|
10
|
+
// this command's flags by issuing a live `ledger add` with a placeholder
|
|
11
|
+
// --desc ("test", "test-check", "test-x", ...) instead of reading --help,
|
|
12
|
+
// polluting the real branch ledger at least six separate times despite
|
|
13
|
+
// explicit prompt instructions not to. --help already documents every flag —
|
|
14
|
+
// the missing piece was a structural backstop, since prompt text alone kept
|
|
15
|
+
// failing to prevent it. This denylist catches the exact placeholder shapes
|
|
16
|
+
// observed; it is not a general anti-test-data filter.
|
|
17
|
+
const PLACEHOLDER_DESC = /^(test|test[-_]?\w{0,10}|placeholder|foo|bar|example|todo|tbd)$/i;
|
|
9
18
|
function registerLedgerCommand(program) {
|
|
10
19
|
const ledger = program.command('ledger').description('persistent per-branch findings ledger (working memory for harness-retro)');
|
|
11
20
|
ledger
|
|
@@ -25,6 +34,10 @@ function registerLedgerCommand(program) {
|
|
|
25
34
|
if (opts.defectClass !== undefined && !types_1.DEFECT_CLASS.test(opts.defectClass)) {
|
|
26
35
|
throw new Error('--defect-class requires a lowercase kebab-case value');
|
|
27
36
|
}
|
|
37
|
+
if (PLACEHOLDER_DESC.test(opts.desc.trim())) {
|
|
38
|
+
throw new Error(`--desc "${opts.desc}" looks like a placeholder, not a real finding — ` +
|
|
39
|
+
'run "awm ledger add --help" to check flag syntax instead of a live add.');
|
|
40
|
+
}
|
|
28
41
|
const cwd = process.cwd();
|
|
29
42
|
const branch = opts.branch ?? (0, store_1.detectBranch)(cwd);
|
|
30
43
|
const entry = {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runProcessList = runProcessList;
|
|
4
|
+
exports.runProcessShow = runProcessShow;
|
|
5
|
+
exports.registerProcessCommand = registerProcessCommand;
|
|
6
|
+
const discover_1 = require("../../core/process/discover");
|
|
7
|
+
/** `source` es un path absoluto del filesystem local: identifica la máquina, no
|
|
8
|
+
* el proceso. Se usa internamente para diagnósticos, y se omite de toda salida
|
|
9
|
+
* destinada a ser compartida o consumida por otro programa. */
|
|
10
|
+
function publicView(model) {
|
|
11
|
+
const { source: _source, ...rest } = model;
|
|
12
|
+
return rest;
|
|
13
|
+
}
|
|
14
|
+
/** Formatea los diagnósticos de descubrimiento como líneas `warning: ...\n`
|
|
15
|
+
* listas para stderr. Compartido por list y show para no divergir el formato. */
|
|
16
|
+
function diagnosticsToStderr(diagnostics) {
|
|
17
|
+
return diagnostics.map((d) => `warning: ${d}\n`).join('');
|
|
18
|
+
}
|
|
19
|
+
/** Neutraliza bytes de control C0 (incluyendo ESC `\x1b`) de texto de body
|
|
20
|
+
* proveniente de un registry no confiable antes de escribirlo a una terminal
|
|
21
|
+
* humana. `\n` y `\t` se preservan porque son whitespace legítimo usado por
|
|
22
|
+
* la vista de texto; el resto de la vista JSON no pasa por acá — `JSON.stringify`
|
|
23
|
+
* ya escapa los caracteres de control como parte de producir JSON válido. Esto
|
|
24
|
+
* es un problema distinto y más simple que `sanitizeDashboardSource` (Dashboard,
|
|
25
|
+
* Task 5): ahí se canonicaliza ids/labels contra un vocabulario permitido; acá
|
|
26
|
+
* solo se neutraliza ANSI/control crudo antes de escribir a stdout. */
|
|
27
|
+
function stripControlChars(text) {
|
|
28
|
+
// eslint-disable-next-line no-control-regex -- necesitamos matchear C0 deliberadamente
|
|
29
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
30
|
+
}
|
|
31
|
+
function runProcessList(discovered) {
|
|
32
|
+
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
33
|
+
if (discovered.models.length === 0) {
|
|
34
|
+
return { code: 0, stdout: 'No process models declared by the installed registries.\n', stderr };
|
|
35
|
+
}
|
|
36
|
+
const rows = discovered.models
|
|
37
|
+
.map((m) => `${m.name} ${m.status} ${m.entryPoint ? 'entry-point' : 'phase'} -> ${m.terminatesTo}`)
|
|
38
|
+
.join('\n');
|
|
39
|
+
return { code: 0, stdout: `${rows}\n`, stderr };
|
|
40
|
+
}
|
|
41
|
+
function runProcessShow(discovered, name, json) {
|
|
42
|
+
const stderr = diagnosticsToStderr(discovered.diagnostics);
|
|
43
|
+
const found = discovered.models.find((m) => m.name === name);
|
|
44
|
+
if (!found) {
|
|
45
|
+
const available = discovered.models.map((m) => m.name).join(', ') || '(none)';
|
|
46
|
+
return { code: 2, stdout: '', stderr: `${stderr}awm process show: no process named "${name}" — available: ${available}\n` };
|
|
47
|
+
}
|
|
48
|
+
if (!json) {
|
|
49
|
+
// Vista mínima deliberada: name/status/objective/structure. appliesWhen,
|
|
50
|
+
// routing, termination y unverified no se renderizan aquí — el detalle
|
|
51
|
+
// completo del body está disponible vía --json (ver Task 4 del plan R1A).
|
|
52
|
+
const view = publicView(found);
|
|
53
|
+
const structure = view.body.structure
|
|
54
|
+
.map((sg) => [
|
|
55
|
+
`${stripControlChars(sg.id)} — ${stripControlChars(sg.text)}`,
|
|
56
|
+
...sg.operations.map((op) => ` ${stripControlChars(op.id)} — ${stripControlChars(op.text)}`),
|
|
57
|
+
].join('\n')).join('\n');
|
|
58
|
+
return { code: 0, stdout: `${view.name} (${view.status})\n\n${stripControlChars(view.body.objective)}\n\n${structure}\n`, stderr };
|
|
59
|
+
}
|
|
60
|
+
return { code: 0, stdout: `${JSON.stringify(publicView(found), null, 2)}\n`, stderr };
|
|
61
|
+
}
|
|
62
|
+
function registerProcessCommand(program) {
|
|
63
|
+
const process_ = program.command('process').description('declared process models (the CLI is their only parser)');
|
|
64
|
+
process_
|
|
65
|
+
.command('list')
|
|
66
|
+
.description('list process models declared by the installed registries')
|
|
67
|
+
.action(() => emit(runProcessList((0, discover_1.discoverProcessModels)())));
|
|
68
|
+
process_
|
|
69
|
+
.command('show <name>')
|
|
70
|
+
.description('show one process model')
|
|
71
|
+
.option('--json', 'emit the parsed model as JSON')
|
|
72
|
+
.action((name, opts) => emit(runProcessShow((0, discover_1.discoverProcessModels)(), name, opts.json === true)));
|
|
73
|
+
}
|
|
74
|
+
function emit(result) {
|
|
75
|
+
if (result.stderr)
|
|
76
|
+
process.stderr.write(result.stderr);
|
|
77
|
+
if (result.stdout)
|
|
78
|
+
process.stdout.write(result.stdout);
|
|
79
|
+
if (result.code !== 0)
|
|
80
|
+
process.exitCode = result.code;
|
|
81
|
+
}
|
|
@@ -15,6 +15,7 @@ const types_1 = require("../evidence/types");
|
|
|
15
15
|
const store_1 = require("../ledger/store");
|
|
16
16
|
const paths_1 = require("../journal/paths");
|
|
17
17
|
const store_2 = require("../journal/store");
|
|
18
|
+
const discover_1 = require("../process/discover");
|
|
18
19
|
const sanitize_1 = require("./sanitize");
|
|
19
20
|
const validate_1 = require("./validate");
|
|
20
21
|
const plan_state_1 = require("./plan-state");
|
|
@@ -29,7 +30,7 @@ exports.REMEDIATION_BY_FINDING_ID = {
|
|
|
29
30
|
};
|
|
30
31
|
const BLOCKED_CYCLE_REMEDIATION = 'awm preflight';
|
|
31
32
|
const EMPTY_ADAPTERS = {
|
|
32
|
-
machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
|
|
33
|
+
machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [],
|
|
33
34
|
};
|
|
34
35
|
const SAFE_REMEDIATIONS = new Set(['awm init', 'awm update', 'awm sync', 'awm sensors status', 'awm preflight']);
|
|
35
36
|
const HEALTHY_PROVIDER_STATES = new Set(['supported', 'healthy', 'shared', 'delivered']);
|
|
@@ -127,6 +128,7 @@ function productionDashboardAdapters(context) {
|
|
|
127
128
|
retro: history.cycles.map((cycle) => cycleFinding('retro', 'Project context', cycle, cycle.plan.state !== 'executed')),
|
|
128
129
|
};
|
|
129
130
|
},
|
|
131
|
+
processes: () => (0, discover_1.discoverProcessModels)().models.map((m) => ({ name: m.name, status: m.status })),
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
/** A journal has no durable plan reference. Use a generic fixed row so its
|
|
@@ -358,6 +360,29 @@ function collectDashboardSnapshot(options) {
|
|
|
358
360
|
const retroItems = isolatedFindings(execution?.retro);
|
|
359
361
|
const evidenceResult = optional(() => evidenceHistoryItems(root));
|
|
360
362
|
const executionUnavailable = !executionResult.failed && execution === undefined;
|
|
363
|
+
const processesResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.processes({ root })));
|
|
364
|
+
// `id`/`label`/`detail` se construyen acá, en código de aplicación, DESPUÉS de
|
|
365
|
+
// la sanitización de source — igual que `planning` hace con `classifyPlanState`:
|
|
366
|
+
// el sanitizador descarta todo `detail` que venga del adapter (sanitize.ts), así
|
|
367
|
+
// que ponerlo antes sería escribirlo para que se borre en silencio. Y como estos
|
|
368
|
+
// tres campos nunca pasan por el recorrido recursivo de sanitizeDashboardSource
|
|
369
|
+
// de arriba, se los corre por las mismas ramas de sanitize() (sanitizeDashboardId/
|
|
370
|
+
// sanitizeDashboardLabel) para sostener la segunda barrera: un `p.name` que no es
|
|
371
|
+
// slug cae al id hasheado `item-*` en vez de colarse como `process.<bad-name>`.
|
|
372
|
+
// El estado `attention` es accionable (ver validate.ts): un modelo en draft no
|
|
373
|
+
// tiene un comando que lo "arregle" — el remediation apunta a inspeccionarlo, no
|
|
374
|
+
// a resolverlo automáticamente.
|
|
375
|
+
const processItems = (processesResult.value ?? []).map((p) => ({
|
|
376
|
+
id: (0, sanitize_1.sanitizeDashboardId)(`process.${p.name}`),
|
|
377
|
+
// El literal 'Process' ya está en CANONICAL_LABELS, así que el branch
|
|
378
|
+
// [redacted] de sanitizeDashboardLabel nunca se ejecuta desde este call
|
|
379
|
+
// site hoy — el wrapper queda por simetría con sanitizeDashboardId y como
|
|
380
|
+
// guardia ante un futuro label dinámico, no porque esta llamada pueda fallar.
|
|
381
|
+
label: (0, sanitize_1.sanitizeDashboardLabel)('Process'),
|
|
382
|
+
state: p.status === 'active' ? 'ok' : 'attention',
|
|
383
|
+
detail: p.status,
|
|
384
|
+
...(p.status === 'active' ? {} : { remediation: 'awm process list' }),
|
|
385
|
+
}));
|
|
361
386
|
const sections = [
|
|
362
387
|
machineSection,
|
|
363
388
|
section('project', projectResult.failed || projectItemsResult.failed ? 'unavailable' : 'available', projectResult.failed
|
|
@@ -371,10 +396,10 @@ function collectDashboardSnapshot(options) {
|
|
|
371
396
|
section('docs', executionResult.failed || executionUnavailable || docsItems.failed ? 'unavailable' : 'available', docsItems.value),
|
|
372
397
|
section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
|
|
373
398
|
section('history', evidenceResult.failed ? 'unavailable' : 'available', evidenceResult.failed ? [] : evidenceResult.value.items),
|
|
374
|
-
// `processes`
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
section('processes', 'not_applicable', []),
|
|
399
|
+
// R1a puebla `processes` desde su adapter. Sin modelos declarados la
|
|
400
|
+
// sección conserva `not_applicable` — el mismo valor que R0 dejó
|
|
401
|
+
// reservado — para que un proyecto sin procesos se vea igual que antes.
|
|
402
|
+
section('processes', processesResult.failed ? 'unavailable' : processItems.length === 0 ? 'not_applicable' : 'available', processesResult.failed ? [] : processItems),
|
|
378
403
|
];
|
|
379
404
|
const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
|
|
380
405
|
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 2, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: evidenceResult.failed ? 'none' : evidenceResult.value.confidence, sections });
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sanitizeDashboardSource = sanitizeDashboardSource;
|
|
4
|
+
exports.sanitizeDashboardId = sanitizeDashboardId;
|
|
5
|
+
exports.sanitizeDashboardLabel = sanitizeDashboardLabel;
|
|
4
6
|
const crypto_1 = require("crypto");
|
|
5
7
|
const STATES = new Set(['ok', 'attention', 'missing', 'unavailable', 'not_applicable', 'active', 'blocked']);
|
|
6
|
-
const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'docs', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'docsComplete', 'retroComplete']);
|
|
8
|
+
const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'docs', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'docsComplete', 'retroComplete', 'processes', 'name', 'status']);
|
|
7
9
|
const CANONICAL_LABELS = new Set([
|
|
8
10
|
'Preferences', 'Registries', 'Profile', 'Sensors', 'Optional source unavailable',
|
|
9
11
|
'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight', 'Documentation',
|
|
12
|
+
'Process',
|
|
10
13
|
]);
|
|
11
14
|
const CANONICAL_FINDING_IDS = new Set([
|
|
12
15
|
'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
|
|
@@ -15,6 +18,10 @@ const CANONICAL_FINDING_IDS = new Set([
|
|
|
15
18
|
const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
16
19
|
const PROJECT_FINDING_ID = /^project\.(?:profile\.present|extensions\.configured|registry-pins\.present|bundles\.coherent|context\.present|constitution\.present|sensors\.present|preflight\.not_collected)$/;
|
|
17
20
|
const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
21
|
+
// El slug ya lo validó PROCESS_NAME en core/process/model.ts. Repetir la forma
|
|
22
|
+
// acá es deliberado: este módulo es la frontera de render y no puede confiar en
|
|
23
|
+
// que su input pasó por aquel validador — un adapter nuevo podría no hacerlo.
|
|
24
|
+
const PROCESS_FINDING_ID = /^process\.[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
18
25
|
const DANGEROUS = /(?:ghp_|sk-[A-Za-z]|<|>|\\\\[^\\\s]+\\[^\\\s]+|\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*|[A-Za-z]:\\|\b[A-Za-z_][A-Za-z0-9_]*=|token|secret|password)/iu;
|
|
19
26
|
function sanitize(value, key) {
|
|
20
27
|
if (value === null || typeof value === 'boolean')
|
|
@@ -33,7 +40,7 @@ function sanitize(value, key) {
|
|
|
33
40
|
if (key === 'id') {
|
|
34
41
|
if (value.trim() === '')
|
|
35
42
|
throw new Error('Dashboard finding id is invalid');
|
|
36
|
-
return CANONICAL_FINDING_IDS.has(value) || PROVIDER_FINDING_ID.test(value) || PROJECT_FINDING_ID.test(value)
|
|
43
|
+
return CANONICAL_FINDING_IDS.has(value) || PROVIDER_FINDING_ID.test(value) || PROJECT_FINDING_ID.test(value) || PROCESS_FINDING_ID.test(value)
|
|
37
44
|
? value : `item-${(0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 16)}`;
|
|
38
45
|
}
|
|
39
46
|
if (key === 'label' && !CANONICAL_LABELS.has(value) && !PROVIDER_LABEL.test(value))
|
|
@@ -60,3 +67,18 @@ function sanitize(value, key) {
|
|
|
60
67
|
function sanitizeDashboardSource(value) {
|
|
61
68
|
return sanitize(value);
|
|
62
69
|
}
|
|
70
|
+
// Sections that build an item's `id`/`label` in application code (rather than
|
|
71
|
+
// receiving them ready-made inside an adapter's `findings` array) never pass
|
|
72
|
+
// through `sanitizeDashboardSource`'s recursive walk — there is no `findings`
|
|
73
|
+
// array for it to walk. These wrappers let such call sites run the exact same
|
|
74
|
+
// `id`/`label` branches sanitize() applies to every other section, so a
|
|
75
|
+
// constructed value gets the same canonical-vocabulary / hashed-fallback
|
|
76
|
+
// treatment instead of skipping validation entirely.
|
|
77
|
+
/** Validates a dashboard item id against the canonical id vocabulary, falling back to a hashed id otherwise. */
|
|
78
|
+
function sanitizeDashboardId(value) {
|
|
79
|
+
return sanitize(value, 'id');
|
|
80
|
+
}
|
|
81
|
+
/** Validates a dashboard item label against the canonical label vocabulary, redacting it otherwise. */
|
|
82
|
+
function sanitizeDashboardLabel(value) {
|
|
83
|
+
return sanitize(value, 'label');
|
|
84
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseProcessBody = parseProcessBody;
|
|
4
|
+
const REQUIRED = ['Objetivo', 'Cuándo aplica', 'Estructura', 'Ruteo', 'Terminación', 'Sin verificar'];
|
|
5
|
+
const SG = /^-\s+(SG-\d+)\s+—\s+(.+)$/;
|
|
6
|
+
const OP = /^\s+-\s+(OP-(\d+)\.\d+)\s+—\s+(.+)$/;
|
|
7
|
+
// Mismo razonamiento que MAX_FIELD_LENGTH en orchestrators.ts: este texto
|
|
8
|
+
// llega al payload de contexto de un proveedor de IA, así que ningún campo
|
|
9
|
+
// puede ser ilimitado — un `## Objetivo` de 10MB en un registry no confiable
|
|
10
|
+
// debe rechazarse con diagnóstico, no aceptarse. Dos anchos, no uno, porque
|
|
11
|
+
// las secciones de prosa (Objetivo/Cuándo aplica/Terminación) son párrafos
|
|
12
|
+
// libres que legítimamente exceden una sola oración, mientras que los ítems
|
|
13
|
+
// estructurales (texto de SG-#/OP-#, celdas de la tabla Ruteo, entradas de
|
|
14
|
+
// "Sin verificar") son título-cortos por contrato — igual de acotados que los
|
|
15
|
+
// campos de orchestrators.ts.
|
|
16
|
+
const MAX_SECTION_LENGTH = 5000;
|
|
17
|
+
const MAX_ITEM_LENGTH = 500;
|
|
18
|
+
/** Corta el documento en secciones de nivel 2. Las de nivel 1 (`# Titulo`) y el
|
|
19
|
+
* contenido previo al primer `##` se descartan: el contrato vive en los `##`.
|
|
20
|
+
* Un heading repetido NO se fusiona con el anterior — eso corrompería en
|
|
21
|
+
* silencio los parsers posicionales (p.ej. `parseRouting` tomaría la fila de
|
|
22
|
+
* cabecera de la segunda sección como una fila de datos). En su lugar se
|
|
23
|
+
* registra como problema y el llamador debe abortar. */
|
|
24
|
+
function splitSections(source, problems) {
|
|
25
|
+
const out = new Map();
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
let current = null;
|
|
28
|
+
for (const line of source.split(/\r?\n/)) {
|
|
29
|
+
// Greedy capture a fin-de-línea + trim en código, no en el regex: un
|
|
30
|
+
// `(.+?)` perezoso seguido de `\s*` sobre una clase que lo solapa
|
|
31
|
+
// (`.` incluye `\s`) es ReDoS cuadrático — ver AGENTS.md
|
|
32
|
+
// "regex-cuantificador-adyacente-a-clase-que-lo-solapa" y el mismo
|
|
33
|
+
// patrón ya corregido en cli/src/commands/evidence/index.ts marker().
|
|
34
|
+
const m = /^##\s+(.*)$/.exec(line);
|
|
35
|
+
// Un `##` seguido solo de espacio (grupo capturado vacío tras el trim)
|
|
36
|
+
// NO es un heading real: la regex vieja `(.+?)\s*$` exigía al menos un
|
|
37
|
+
// carácter no-blanco antes del `\s*$` final, así que una línea así
|
|
38
|
+
// nunca matcheaba como sección. `(.*)` sí puede capturar cero
|
|
39
|
+
// caracteres — sin este guard, una línea "## " perdida dentro de p.ej.
|
|
40
|
+
// `## Ruteo` abriría una sección fantasma "" y desviaría las filas de
|
|
41
|
+
// tabla reales hacia ella en silencio (ver finding-1 del reviewer).
|
|
42
|
+
if (m && m[1].trim() !== '') {
|
|
43
|
+
const heading = m[1].trim();
|
|
44
|
+
if (seen.has(heading)) {
|
|
45
|
+
problems.push(`duplicate section heading "## ${heading}"`);
|
|
46
|
+
current = null; // deja de recolectar: no fusionar con la primera aparición
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
seen.add(heading);
|
|
50
|
+
out.set(heading, []);
|
|
51
|
+
current = heading;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (current !== null)
|
|
55
|
+
out.get(current).push(line);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function paragraph(lines) {
|
|
60
|
+
return lines.map((l) => l.trim()).filter((l) => l !== '').join(' ');
|
|
61
|
+
}
|
|
62
|
+
function parseStructure(lines, problems) {
|
|
63
|
+
const subgoals = [];
|
|
64
|
+
const seenSg = new Set();
|
|
65
|
+
for (const raw of lines) {
|
|
66
|
+
if (raw.trim() === '')
|
|
67
|
+
continue;
|
|
68
|
+
const sg = SG.exec(raw);
|
|
69
|
+
if (sg) {
|
|
70
|
+
// Un SG-# repetido produciría dos entradas con el mismo `id` en
|
|
71
|
+
// `structure` — inconsistente para cualquier consumidor que
|
|
72
|
+
// indexe por id. Mismo patrón `problems` que el resto del archivo.
|
|
73
|
+
if (seenSg.has(sg[1])) {
|
|
74
|
+
problems.push(`duplicate "Estructura" id ${sg[1]}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
seenSg.add(sg[1]);
|
|
78
|
+
const text = sg[2].trim();
|
|
79
|
+
if (text.length > MAX_ITEM_LENGTH) {
|
|
80
|
+
problems.push(`"${sg[1]}" text must be at most ${MAX_ITEM_LENGTH} characters`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
subgoals.push({ id: sg[1], text, operations: [] });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const op = OP.exec(raw);
|
|
87
|
+
if (!op) {
|
|
88
|
+
problems.push(`"Estructura" line is neither an SG-# nor an OP-#: ${JSON.stringify(raw.trim().slice(0, 80))}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const owner = subgoals[subgoals.length - 1];
|
|
92
|
+
// El prefijo numérico de la operación DEBE coincidir con su subobjetivo:
|
|
93
|
+
// sin esto, OP-9.1 colgando de SG-2 pasaría y la jerarquía HTA sería una
|
|
94
|
+
// ilusión tipográfica.
|
|
95
|
+
if (!owner) {
|
|
96
|
+
problems.push(`${op[1]} appears before any SG-#`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (`SG-${op[2]}` !== owner.id) {
|
|
100
|
+
problems.push(`${op[1]} does not belong to ${owner.id}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const opText = op[3].trim();
|
|
104
|
+
if (opText.length > MAX_ITEM_LENGTH) {
|
|
105
|
+
problems.push(`"${op[1]}" text must be at most ${MAX_ITEM_LENGTH} characters`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
owner.operations.push({ id: op[1], text: opText });
|
|
109
|
+
}
|
|
110
|
+
if (subgoals.length === 0)
|
|
111
|
+
problems.push('"Estructura" declares no SG-#');
|
|
112
|
+
return subgoals;
|
|
113
|
+
}
|
|
114
|
+
function parseRouting(lines, problems) {
|
|
115
|
+
const rows = [];
|
|
116
|
+
const tableLines = lines.map((l) => l.trim()).filter((l) => l.startsWith('|'));
|
|
117
|
+
for (const [index, line] of tableLines.entries()) {
|
|
118
|
+
// `| a | b |` -> ['a','b']: se descartan los extremos vacíos que deja el
|
|
119
|
+
// split, no las celdas internas vacías — la columna "Estado requerido"
|
|
120
|
+
// vacía es WCP18 "sin hito", un valor con significado.
|
|
121
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
122
|
+
if (index === 0) {
|
|
123
|
+
if (cells.length !== 4)
|
|
124
|
+
problems.push('"Ruteo" header must have exactly 4 columns');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Separador GFM: dashes puros (`|---|`) o con colons de alineación
|
|
128
|
+
// (`|:---|:---:|`). Se prueba celda por celda con un regex anclado y
|
|
129
|
+
// sin cuantificadores anidados (`^:?-+:?$`) — lineal, sin riesgo ReDoS
|
|
130
|
+
// — y se exige que cada celda tenga al menos un `-` real para no
|
|
131
|
+
// aceptar una fila de solo colons/vacíos como falso separador.
|
|
132
|
+
if (cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c)))
|
|
133
|
+
continue; // separador
|
|
134
|
+
if (cells.length !== 4) {
|
|
135
|
+
problems.push(`"Ruteo" row ${index} must have exactly 4 columns, found ${cells.length}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (cells[0] === '' || cells[2] === '' || cells[3] === '') {
|
|
139
|
+
problems.push(`"Ruteo" row ${index} needs Cuándo, Va a and Termina en`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (cells.some((c) => c.length > MAX_ITEM_LENGTH)) {
|
|
143
|
+
problems.push(`"Ruteo" row ${index} has a cell longer than ${MAX_ITEM_LENGTH} characters`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
rows.push({ when: cells[0], requiredState: cells[1], goesTo: cells[2], endsAt: cells[3] });
|
|
147
|
+
}
|
|
148
|
+
if (rows.length === 0)
|
|
149
|
+
problems.push('"Ruteo" declares no transitions');
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
function parseProcessBody(source, file) {
|
|
153
|
+
const problems = [];
|
|
154
|
+
const sections = splitSections(source, problems);
|
|
155
|
+
for (const heading of REQUIRED) {
|
|
156
|
+
if (!sections.has(heading))
|
|
157
|
+
problems.push(`missing required section "## ${heading}"`);
|
|
158
|
+
}
|
|
159
|
+
if (problems.length > 0)
|
|
160
|
+
return { diagnostics: [`${file}: invalid process model body — ${problems.join('; ')}`] };
|
|
161
|
+
const structure = parseStructure(sections.get('Estructura'), problems);
|
|
162
|
+
const routing = parseRouting(sections.get('Ruteo'), problems);
|
|
163
|
+
const objective = paragraph(sections.get('Objetivo'));
|
|
164
|
+
const appliesWhen = paragraph(sections.get('Cuándo aplica'));
|
|
165
|
+
const termination = paragraph(sections.get('Terminación'));
|
|
166
|
+
const unverified = sections.get('Sin verificar')
|
|
167
|
+
.map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => l.slice(2).trim());
|
|
168
|
+
for (const [label, value] of [['Objetivo', objective], ['Cuándo aplica', appliesWhen], ['Terminación', termination]]) {
|
|
169
|
+
if (value === '')
|
|
170
|
+
problems.push(`"${label}" is empty`);
|
|
171
|
+
else if (value.length > MAX_SECTION_LENGTH)
|
|
172
|
+
problems.push(`"${label}" must be at most ${MAX_SECTION_LENGTH} characters`);
|
|
173
|
+
}
|
|
174
|
+
for (const [index, entry] of unverified.entries()) {
|
|
175
|
+
if (entry.length > MAX_ITEM_LENGTH)
|
|
176
|
+
problems.push(`"Sin verificar" entry ${index} must be at most ${MAX_ITEM_LENGTH} characters`);
|
|
177
|
+
}
|
|
178
|
+
if (problems.length > 0)
|
|
179
|
+
return { diagnostics: [`${file}: invalid process model body — ${problems.join('; ')}`] };
|
|
180
|
+
return { model: { objective, appliesWhen, structure, routing, termination, unverified }, diagnostics: [] };
|
|
181
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverProcessModels = discoverProcessModels;
|
|
7
|
+
// cli/src/core/process/discover.ts
|
|
8
|
+
// Enumera modelos de proceso entre todos los registries instalados.
|
|
9
|
+
//
|
|
10
|
+
// Reusa discoverSkills (core/discovery.ts) para listar cada root: no re-camina
|
|
11
|
+
// el filesystem, delega en ella la lectura del directorio `skills/` y la
|
|
12
|
+
// descripción del SKILL.md. Pero se la llama UN ROOT A LA VEZ, no con todos
|
|
13
|
+
// los roots juntos.
|
|
14
|
+
//
|
|
15
|
+
// Motivo: discoverSkills lanza cuando dos roots tienen una skill con el MISMO
|
|
16
|
+
// nombre de carpeta y ningún override declarado — esa es la política correcta
|
|
17
|
+
// para identidad de skill, pero un modelo de proceso se identifica por su
|
|
18
|
+
// `name` de frontmatter, no por el nombre de su carpeta. Si se le pasaran
|
|
19
|
+
// todos los roots juntos, una colisión de nombre-de-carpeta entre dos
|
|
20
|
+
// registries (con o sin relación entre sí) tumbaría el descubrimiento de
|
|
21
|
+
// TODOS los modelos de proceso de TODOS los registries — justo lo que R7.1
|
|
22
|
+
// prohíbe. El dedupe real de modelos ocurre acá abajo, por `name`, con
|
|
23
|
+
// diagnóstico en vez de excepción; ver `collectDeclaredOrchestrators` para el
|
|
24
|
+
// mismo criterio ("gana el primero en orden de roots").
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const registries_1 = require("../registries");
|
|
28
|
+
const discovery_1 = require("../discovery");
|
|
29
|
+
const model_1 = require("./model");
|
|
30
|
+
const body_1 = require("./body");
|
|
31
|
+
function discoverProcessModels(roots = (0, registries_1.contentRoots)()) {
|
|
32
|
+
const models = [];
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
let skills;
|
|
37
|
+
try {
|
|
38
|
+
skills = (0, discovery_1.discoverSkills)([root]);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Con un solo root, discoverSkills no debería lanzar por colisión
|
|
42
|
+
// (no hay con qué colisionar) — pero sí puede lanzar por I/O. Un
|
|
43
|
+
// registry roto no debe impedir descubrir los demás (R7.1).
|
|
44
|
+
diagnostics.push(`${root}: process model discovery unavailable (${e instanceof Error ? e.message : String(e)})`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const skill of skills) {
|
|
48
|
+
const file = path_1.default.join(skill.path, 'SKILL.md');
|
|
49
|
+
// Same trust boundary as readDeclaredOrchestrators (orchestrators.ts):
|
|
50
|
+
// a registry is untrusted input, and SKILL.md is registry-declared content.
|
|
51
|
+
// assertRegularRegistryFile throws when the file is a symlink (or otherwise
|
|
52
|
+
// not a regular file), which would let a malicious/compromised registry point
|
|
53
|
+
// at an arbitrary local file and have its content read. This module's
|
|
54
|
+
// discipline is "never throws" — a broken/malicious skill must not block
|
|
55
|
+
// discovery of the others — so the rejection is caught here and turned into a
|
|
56
|
+
// diagnostic (naming only the path, never the symlink target's content).
|
|
57
|
+
let exists;
|
|
58
|
+
try {
|
|
59
|
+
exists = (0, registries_1.assertRegularRegistryFile)(file);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
diagnostics.push(`${file}: cannot read (${e instanceof Error ? e.message : String(e)})`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!exists) {
|
|
66
|
+
diagnostics.push(`${file}: cannot read (ENOENT: no such file)`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let source;
|
|
70
|
+
try {
|
|
71
|
+
source = fs_1.default.readFileSync(file, 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
diagnostics.push(`${file}: cannot read (${e instanceof Error ? e.message : String(e)})`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const front = (0, model_1.parseProcessFrontmatter)(source, file);
|
|
78
|
+
diagnostics.push(...front.diagnostics);
|
|
79
|
+
if (!front.model)
|
|
80
|
+
continue; // no es un modelo, o está roto
|
|
81
|
+
const body = (0, body_1.parseProcessBody)(source, file);
|
|
82
|
+
diagnostics.push(...body.diagnostics);
|
|
83
|
+
if (!body.model)
|
|
84
|
+
continue;
|
|
85
|
+
if (seen.has(front.model.name)) {
|
|
86
|
+
diagnostics.push(`${file}: process "${front.model.name}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(front.model.name);
|
|
90
|
+
models.push({ ...front.model, source: file, body: body.model });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { models, diagnostics };
|
|
94
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROCESS_NAME = void 0;
|
|
4
|
+
exports.parseProcessFrontmatter = parseProcessFrontmatter;
|
|
5
|
+
// cli/src/core/process/model.ts
|
|
6
|
+
// Lector del contrato de frontmatter del modelo durable de proceso.
|
|
7
|
+
// Disciplina heredada literal de core/orchestrators.ts: NUNCA lanza. Un modelo
|
|
8
|
+
// malformado se rechaza con diagnóstico y no invalida al registry que lo
|
|
9
|
+
// contiene ni a los demás (R7.1).
|
|
10
|
+
const frontmatter_1 = require("../frontmatter");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
/** El discriminador es literal: ningún documento se reconoce como modelo por su
|
|
13
|
+
* cuerpo, sus headings ni su nombre de archivo (R1.2). */
|
|
14
|
+
const DISCRIMINATOR = 'process-model';
|
|
15
|
+
const ALLOWED_FIELDS = ['awm', 'schema', 'name', 'status', 'entry_point', 'terminates_to', 'created', 'updated'];
|
|
16
|
+
const STATUSES = ['draft', 'active'];
|
|
17
|
+
/** El slug es lo único del modelo que puede viajar dentro de un id del Dashboard
|
|
18
|
+
* (ver sanitize.ts). Por eso se valida acá y no en la frontera de render: si
|
|
19
|
+
* admitiera rutas, markup o `=`, el id dejaría de ser seguro por construcción. */
|
|
20
|
+
exports.PROCESS_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
21
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
22
|
+
/** Lector de pares `clave: valor` de un frontmatter plano. El contrato no admite
|
|
23
|
+
* anidamiento ni block scalars, así que no se arrastra un parser YAML: cualquier
|
|
24
|
+
* línea que no sea `clave: valor` es un rechazo, no una interpretación. */
|
|
25
|
+
function readPairs(block) {
|
|
26
|
+
const pairs = new Map();
|
|
27
|
+
const problems = [];
|
|
28
|
+
for (const raw of block.split(/\r?\n/)) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (line === '' || line.startsWith('#'))
|
|
31
|
+
continue;
|
|
32
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/.exec(line);
|
|
33
|
+
if (!m) {
|
|
34
|
+
problems.push(`line is not a "key: value" pair: ${JSON.stringify(line.slice(0, 80))}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (pairs.has(m[1])) {
|
|
38
|
+
problems.push(`duplicate field ${JSON.stringify(m[1])}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
pairs.set(m[1], m[2].trim());
|
|
42
|
+
}
|
|
43
|
+
return { pairs, problems };
|
|
44
|
+
}
|
|
45
|
+
function parseProcessFrontmatter(source, file) {
|
|
46
|
+
let block;
|
|
47
|
+
try {
|
|
48
|
+
block = (0, frontmatter_1.matchFrontmatterBlock)(source);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { diagnostics: [] };
|
|
52
|
+
}
|
|
53
|
+
if (block === null)
|
|
54
|
+
return { diagnostics: [] };
|
|
55
|
+
const { pairs, problems } = readPairs(block);
|
|
56
|
+
// Sin discriminador NO es un modelo de proceso: no es un error, es otro
|
|
57
|
+
// documento. Devolver diagnóstico acá inundaría de ruido a todo registry
|
|
58
|
+
// (cada SKILL.md normal caería en esta rama).
|
|
59
|
+
if (pairs.get('awm') !== DISCRIMINATOR)
|
|
60
|
+
return { diagnostics: [] };
|
|
61
|
+
for (const key of pairs.keys()) {
|
|
62
|
+
if (!ALLOWED_FIELDS.includes(key)) {
|
|
63
|
+
problems.push(`unknown field ${JSON.stringify(key)} — the contract admits only ${ALLOWED_FIELDS.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rawSchema = pairs.get('schema');
|
|
67
|
+
const schema = rawSchema !== undefined && /^\d+$/.test(rawSchema) ? Number(rawSchema) : Number.NaN;
|
|
68
|
+
if (!Number.isInteger(schema) || schema < 1) {
|
|
69
|
+
problems.push('"schema" must be a positive integer');
|
|
70
|
+
}
|
|
71
|
+
else if (schema > types_1.KNOWN_PROCESS_SCHEMA) {
|
|
72
|
+
// R1.4: detenerse e informar. Interpretarlo como el contrato anterior es
|
|
73
|
+
// exactamente lo que este branch existe para impedir.
|
|
74
|
+
return { diagnostics: [`${file}: process model declares schema ${schema}, but this CLI understands up to ${types_1.KNOWN_PROCESS_SCHEMA} — install a newer agentic-workflow-manager to read it`] };
|
|
75
|
+
}
|
|
76
|
+
for (const field of ['name', 'status', 'entry_point', 'terminates_to', 'created', 'updated']) {
|
|
77
|
+
const value = pairs.get(field);
|
|
78
|
+
if (value === undefined || value === '')
|
|
79
|
+
problems.push(`"${field}" is required`);
|
|
80
|
+
}
|
|
81
|
+
const name = pairs.get('name') ?? '';
|
|
82
|
+
if (name !== '' && !exports.PROCESS_NAME.test(name))
|
|
83
|
+
problems.push('"name" must be a lowercase slug (a-z, 0-9, hyphen)');
|
|
84
|
+
const status = pairs.get('status') ?? '';
|
|
85
|
+
if (status !== '' && !STATUSES.includes(status))
|
|
86
|
+
problems.push(`"status" must be one of ${STATUSES.join(', ')}`);
|
|
87
|
+
const entryPointRaw = pairs.get('entry_point') ?? '';
|
|
88
|
+
if (entryPointRaw !== '' && entryPointRaw !== 'true' && entryPointRaw !== 'false')
|
|
89
|
+
problems.push('"entry_point" must be true or false');
|
|
90
|
+
const terminatesTo = pairs.get('terminates_to') ?? '';
|
|
91
|
+
if (terminatesTo !== '' && terminatesTo !== 'none' && !exports.PROCESS_NAME.test(terminatesTo)) {
|
|
92
|
+
problems.push('"terminates_to" must be a lowercase slug or "none"');
|
|
93
|
+
}
|
|
94
|
+
for (const field of ['created', 'updated']) {
|
|
95
|
+
const value = pairs.get(field) ?? '';
|
|
96
|
+
if (value !== '' && !DATE.test(value))
|
|
97
|
+
problems.push(`"${field}" must be YYYY-MM-DD`);
|
|
98
|
+
}
|
|
99
|
+
if (problems.length > 0) {
|
|
100
|
+
return { diagnostics: [`${file}: invalid process model — ${problems.join('; ')}`] };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
model: {
|
|
104
|
+
schema, name, status: status, entryPoint: entryPointRaw === 'true',
|
|
105
|
+
terminatesTo, created: pairs.get('created'), updated: pairs.get('updated'),
|
|
106
|
+
},
|
|
107
|
+
diagnostics: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const job_1 = require("./commands/job");
|
|
|
42
42
|
const watch_1 = require("./commands/watch");
|
|
43
43
|
const track_1 = require("./commands/track");
|
|
44
44
|
const evidence_1 = require("./commands/evidence");
|
|
45
|
+
const process_1 = require("./commands/process");
|
|
45
46
|
const add_1 = require("./commands/add");
|
|
46
47
|
const sync_1 = require("./commands/sync");
|
|
47
48
|
const update_1 = require("./commands/update");
|
|
@@ -735,6 +736,7 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
735
736
|
(0, watch_1.registerWatchCommand)(program);
|
|
736
737
|
(0, track_1.registerTrackCommand)(program);
|
|
737
738
|
(0, evidence_1.registerEvidenceCommand)(program);
|
|
739
|
+
(0, process_1.registerProcessCommand)(program);
|
|
738
740
|
// Commander only waits for async action handlers through parseAsync(). The CLI has
|
|
739
741
|
// async commands (including `sensors coverage`), so returning its promise keeps the
|
|
740
742
|
// process alive until their JSON/output contract has been completed.
|