@work-graph/cli 0.2.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/README.md +31 -0
- package/bin/work-graph.mjs +238 -0
- package/package.json +38 -0
- package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
- package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
- package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
- package/vendor/packages/workgraph-mcp/README.md +28 -0
- package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
- package/vendor/packages/workgraph-mcp/package.json +37 -0
- package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
- package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
- package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
- package/vendor/public/assets/workgraph-logo.svg +11 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
- package/vendor/public/graph-canvas-lit-flow.css +154 -0
- package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
- package/vendor/public/graph-canvas-lit-flow.js +8530 -0
- package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
- package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
- package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
- package/vendor/src/agentRunApi.mjs +136 -0
- package/vendor/src/agentToolLoopGuard.mjs +88 -0
- package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
- package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
- package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
- package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
- package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
- package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
- package/vendor/src/analyticsPanelProjection.mjs +212 -0
- package/vendor/src/analyticsRecordStore.mjs +165 -0
- package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
- package/vendor/src/architectureL1Canon.mjs +419 -0
- package/vendor/src/architectureLayout.mjs +229 -0
- package/vendor/src/architectureSnapshot.mjs +490 -0
- package/vendor/src/architectureViewsProjection.mjs +116 -0
- package/vendor/src/atomInspector.mjs +253 -0
- package/vendor/src/atomInspectorApi.mjs +130 -0
- package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
- package/vendor/src/backlogSchemaLint.mjs +176 -0
- package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
- package/vendor/src/bracketIrTraceSignal.mjs +93 -0
- package/vendor/src/bvcAtomParser.mjs +210 -0
- package/vendor/src/bvcDialectRegistry.mjs +86 -0
- package/vendor/src/bvcFileFormat.mjs +218 -0
- package/vendor/src/bvcFormatCli.mjs +55 -0
- package/vendor/src/bvcLintCli.mjs +48 -0
- package/vendor/src/bvcNewWritePolicy.mjs +70 -0
- package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
- package/vendor/src/claimNoEligibleEval.mjs +205 -0
- package/vendor/src/closingAnalysisSuggest.mjs +59 -0
- package/vendor/src/codeGapAnalyzer.mjs +308 -0
- package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
- package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
- package/vendor/src/codeGapOperatorProjection.mjs +60 -0
- package/vendor/src/codeSyntaxHighlight.mjs +123 -0
- package/vendor/src/codegenEvidence.mjs +187 -0
- package/vendor/src/compilerRoundTripCli.mjs +164 -0
- package/vendor/src/dagreGraphLayout.mjs +78 -0
- package/vendor/src/draftIntakePromotionRules.mjs +205 -0
- package/vendor/src/epicWorkScope.mjs +85 -0
- package/vendor/src/evalLiveLlmEnv.mjs +63 -0
- package/vendor/src/evidenceReadModel.mjs +167 -0
- package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
- package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
- package/vendor/src/goldenPath.mjs +69 -0
- package/vendor/src/graphCanvasLayout.mjs +464 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
- package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
- package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
- package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
- package/vendor/src/graphRagContextSlice.mjs +461 -0
- package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
- package/vendor/src/homeSnapshotApi.mjs +131 -0
- package/vendor/src/homeSnapshotProjection.mjs +275 -0
- package/vendor/src/inboxEventStream.mjs +140 -0
- package/vendor/src/intentComposerApi.mjs +245 -0
- package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
- package/vendor/src/intentGraphProjection.mjs +208 -0
- package/vendor/src/intentHierarchy.mjs +241 -0
- package/vendor/src/intentNodeLint.mjs +107 -0
- package/vendor/src/intentNodeRuntime.mjs +185 -0
- package/vendor/src/intentRoadmapCanvas.mjs +393 -0
- package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
- package/vendor/src/intentRoadmapMermaid.mjs +165 -0
- package/vendor/src/intentRoadmapProjection.mjs +85 -0
- package/vendor/src/intentTreeLint.mjs +114 -0
- package/vendor/src/intentTreeMigration.mjs +150 -0
- package/vendor/src/intentTreeWorkItems.mjs +227 -0
- package/vendor/src/kanbanBoardProjection.mjs +58 -0
- package/vendor/src/languageAdapterRegistry.mjs +180 -0
- package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
- package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
- package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
- package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
- package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
- package/vendor/src/languageAdapters/shared.mjs +68 -0
- package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
- package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
- package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
- package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
- package/vendor/src/markdownDocumentRender.mjs +208 -0
- package/vendor/src/memoryPanelProjection.mjs +116 -0
- package/vendor/src/memoryRecordWriter.mjs +243 -0
- package/vendor/src/memoryWorkerSlice.mjs +238 -0
- package/vendor/src/migrateStepToBvc.mjs +133 -0
- package/vendor/src/missionControlServerHandlers.mjs +195 -0
- package/vendor/src/missionControlUiClient.mjs +278 -0
- package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
- package/vendor/src/onebaseCliRunner.mjs +145 -0
- package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
- package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
- package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
- package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
- package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
- package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
- package/vendor/src/onebaseWorkerTools.mjs +586 -0
- package/vendor/src/operatorShellProjection.mjs +102 -0
- package/vendor/src/pipelineProseRender.mjs +180 -0
- package/vendor/src/pipelineStageLint.mjs +118 -0
- package/vendor/src/promptRulesEditorApi.mjs +174 -0
- package/vendor/src/promptRulesProjection.mjs +134 -0
- package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
- package/vendor/src/pvrgTaskScope.mjs +152 -0
- package/vendor/src/releaseGateMatrix.mjs +188 -0
- package/vendor/src/schematicView.mjs +305 -0
- package/vendor/src/seedAnalyticsRecord.mjs +217 -0
- package/vendor/src/semanticSearchBm25.mjs +103 -0
- package/vendor/src/semanticSearchExcerpts.mjs +68 -0
- package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
- package/vendor/src/semanticSearchWorkflow.mjs +366 -0
- package/vendor/src/stepAtomFormatter.mjs +413 -0
- package/vendor/src/stepGraphSlice.mjs +318 -0
- package/vendor/src/ui/atoms/badge.mjs +40 -0
- package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
- package/vendor/src/ui/atoms/button.mjs +114 -0
- package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
- package/vendor/src/ui/atoms/icon.mjs +23 -0
- package/vendor/src/ui/atoms/input.mjs +38 -0
- package/vendor/src/ui/atoms/modal.mjs +44 -0
- package/vendor/src/ui/atoms/select.mjs +98 -0
- package/vendor/src/ui/backlogShellButtons.mjs +238 -0
- package/vendor/src/ui/htmlEscape.mjs +11 -0
- package/vendor/src/ui/molecules/rating.mjs +48 -0
- package/vendor/src/ui/molecules/tabs.mjs +70 -0
- package/vendor/src/ui/organisms/modal.mjs +1 -0
- package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
- package/vendor/src/ui/workItemStatusTone.mjs +36 -0
- package/vendor/src/unifiedLinkageProjection.mjs +264 -0
- package/vendor/src/verificationLoop.mjs +206 -0
- package/vendor/src/workGraphBacklogPersist.mjs +234 -0
- package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
- package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
- package/vendor/src/workGraphCycleSlice.mjs +184 -0
- package/vendor/src/workGraphDaemonTick.mjs +307 -0
- package/vendor/src/workGraphDaemonWatch.mjs +157 -0
- package/vendor/src/workGraphEngineRoot.mjs +136 -0
- package/vendor/src/workGraphInstallLayout.mjs +65 -0
- package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
- package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
- package/vendor/src/workGraphProjectHost.mjs +149 -0
- package/vendor/src/workGraphProjectInit.mjs +392 -0
- package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
- package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
- package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
- package/vendor/src/workGraphRuntime.mjs +1008 -0
- package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
- package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
- package/vendor/src/workGraphWorkerProvider.mjs +600 -0
- package/vendor/src/workItemBvcQuality.mjs +262 -0
- package/vendor/src/workItemCreateAnalysis.mjs +157 -0
- package/vendor/src/workItemDecisionPipeline.mjs +278 -0
- package/vendor/src/workItemEpicCascade.mjs +176 -0
- package/vendor/src/workItemExecutionGate.mjs +78 -0
- package/vendor/src/workItemHierarchy.mjs +226 -0
- package/vendor/src/workItemProseLint.mjs +133 -0
- package/vendor/src/workItemTextRusify.mjs +794 -0
- package/vendor/src/workItemTraceEnvelope.mjs +158 -0
- package/vendor/src/workItemUiReferences.mjs +272 -0
- package/vendor/src/workflowEpicGrouping.mjs +67 -0
- package/vendor/src/workflowTreeProjection.mjs +53 -0
- package/vendor/src/workspaceRegistry.mjs +150 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatBvcFileContent,
|
|
6
|
+
isBvcReadablePath,
|
|
7
|
+
isLegacyStepPath,
|
|
8
|
+
swapBvcExtension,
|
|
9
|
+
} from './bvcFileFormat.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} target
|
|
13
|
+
* @param {{
|
|
14
|
+
* cwd?: string,
|
|
15
|
+
* out?: string,
|
|
16
|
+
* stdout?: boolean,
|
|
17
|
+
* inPlace?: boolean,
|
|
18
|
+
* }} [options]
|
|
19
|
+
* @returns {Promise<number>}
|
|
20
|
+
*/
|
|
21
|
+
export async function runBvcFormat(target, options = {}) {
|
|
22
|
+
if (!target) {
|
|
23
|
+
throw new TypeError('runBvcFormat requires target file path');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cwd = options.cwd ?? process.cwd();
|
|
27
|
+
const filePath = resolve(cwd, target);
|
|
28
|
+
if (!isBvcReadablePath(filePath)) {
|
|
29
|
+
const error = new Error(`Not a BVC file (expected .bvc or .bvc): ${target}`);
|
|
30
|
+
error.code = 'E_BVC_FORMAT_INVALID_PATH';
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const content = await readFile(filePath, 'utf8');
|
|
35
|
+
const formatted = formatBvcFileContent(content, { filePath });
|
|
36
|
+
|
|
37
|
+
if (options.stdout) {
|
|
38
|
+
process.stdout.write(formatted);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let outPath = filePath;
|
|
43
|
+
if (options.out) {
|
|
44
|
+
outPath = resolve(cwd, options.out);
|
|
45
|
+
} else if (options.inPlace === false && isLegacyStepPath(filePath)) {
|
|
46
|
+
outPath = swapBvcExtension(filePath);
|
|
47
|
+
} else if (isLegacyStepPath(filePath) && options.inPlace !== true) {
|
|
48
|
+
outPath = swapBvcExtension(filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await writeFile(outPath, formatted, 'utf8');
|
|
52
|
+
const displayOut = relative(cwd, outPath) || outPath;
|
|
53
|
+
console.log(`ok formatted ${target} -> ${displayOut}`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { parseBvcFileContent, isBvcReadablePath } from './bvcFileFormat.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} target
|
|
8
|
+
* @param {{ cwd?: string }} [options]
|
|
9
|
+
* @returns {Promise<number>} exit code
|
|
10
|
+
*/
|
|
11
|
+
export async function runBvcLint(target, options = {}) {
|
|
12
|
+
if (!target) {
|
|
13
|
+
throw new TypeError('runBvcLint requires target file path');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cwd = options.cwd ?? process.cwd();
|
|
17
|
+
const filePath = resolve(cwd, target);
|
|
18
|
+
if (!isBvcReadablePath(filePath)) {
|
|
19
|
+
const error = new Error(`Not a BVC file (expected .bvc or .bvc): ${target}`);
|
|
20
|
+
error.code = 'E_BVC_LINT_INVALID_PATH';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = await readFile(filePath, 'utf8');
|
|
25
|
+
const parsed = parseBvcFileContent(content, { filePath });
|
|
26
|
+
|
|
27
|
+
let exitCode = 0;
|
|
28
|
+
for (const lint of parsed.lints) {
|
|
29
|
+
const prefix = lint.code.startsWith('E_') ? 'error' : 'warning';
|
|
30
|
+
console.log(`${prefix} [${lint.code}] ${lint.message}`);
|
|
31
|
+
if (prefix === 'error') {
|
|
32
|
+
exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const atom of parsed.atoms) {
|
|
37
|
+
for (const error of atom.errors ?? []) {
|
|
38
|
+
console.log(`error [E_BVC_ATOM_INVALID] ${error}`);
|
|
39
|
+
exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (exitCode === 0) {
|
|
44
|
+
console.log(`ok ${target} atoms=${parsed.atoms.length}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return exitCode;
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const BVC_WORK_ITEM_CANON_SUFFIX = '.work.bvc';
|
|
2
|
+
export const BVC_WORK_ITEM_LEGACY_SUFFIX = '.work.bvc';
|
|
3
|
+
|
|
4
|
+
/** @param {string} relativePath */
|
|
5
|
+
export function isWorkItemArtifactPath(relativePath) {
|
|
6
|
+
const normalized = String(relativePath ?? '').replace(/\\/g, '/');
|
|
7
|
+
return normalized.endsWith(BVC_WORK_ITEM_CANON_SUFFIX)
|
|
8
|
+
|| normalized.endsWith(BVC_WORK_ITEM_LEGACY_SUFFIX);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} workId
|
|
13
|
+
* @param {{ preferCanon?: boolean }} [options]
|
|
14
|
+
*/
|
|
15
|
+
export function workItemFileName(workId, options = {}) {
|
|
16
|
+
const preferCanon = options.preferCanon !== false;
|
|
17
|
+
const suffix = preferCanon ? BVC_WORK_ITEM_CANON_SUFFIX : BVC_WORK_ITEM_LEGACY_SUFFIX;
|
|
18
|
+
return `${String(workId).trim()}${suffix}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} folder e.g. intent/system/runtime/work
|
|
23
|
+
* @param {string} workId
|
|
24
|
+
* @param {{ preferCanon?: boolean }} [options]
|
|
25
|
+
*/
|
|
26
|
+
export function workItemPathInFolder(folder, workId, options = {}) {
|
|
27
|
+
const base = String(folder ?? '').replace(/\\/g, '/').replace(/\/+$/u, '');
|
|
28
|
+
return `${base}/${workItemFileName(workId, options)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{ id: string, department?: string }} item
|
|
33
|
+
*/
|
|
34
|
+
export function intentWorkFolderForItem(item) {
|
|
35
|
+
const department = String(item.department ?? '').trim();
|
|
36
|
+
if (department === 'ui-dashboard' || department === 'frontend-ui') {
|
|
37
|
+
return 'intent/ui/dashboard/work';
|
|
38
|
+
}
|
|
39
|
+
if (department === 'memory') {
|
|
40
|
+
return 'intent/memory/work';
|
|
41
|
+
}
|
|
42
|
+
if (department === 'domain-onebase') {
|
|
43
|
+
return 'intent/domains/onebase/work';
|
|
44
|
+
}
|
|
45
|
+
if (department === 'domain-marketplace') {
|
|
46
|
+
return 'intent/domains/marketplace/work';
|
|
47
|
+
}
|
|
48
|
+
if (String(item.id).includes('pvrg')) {
|
|
49
|
+
return 'intent/research/pvrg/work';
|
|
50
|
+
}
|
|
51
|
+
return 'intent/system/runtime/work';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* New WorkItems use `.work.bvc`; legacy `.work.bvc` remains readable.
|
|
56
|
+
* @param {{ id: string, department?: string }} item
|
|
57
|
+
*/
|
|
58
|
+
export function intentPathForNewWorkItem(item) {
|
|
59
|
+
return workItemPathInFolder(intentWorkFolderForItem(item), item.id, { preferCanon: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} workId
|
|
64
|
+
* @param {string} relativePath
|
|
65
|
+
*/
|
|
66
|
+
export function workItemPathMatchesId(workId, relativePath) {
|
|
67
|
+
const normalized = String(relativePath ?? '').replace(/\\/g, '/');
|
|
68
|
+
return normalized.endsWith(workItemFileName(workId, { preferCanon: true }))
|
|
69
|
+
|| normalized.endsWith(workItemFileName(workId, { preferCanon: false }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readBvcTextFile } from './bvcFileFormat.mjs';
|
|
2
|
+
|
|
3
|
+
export const CHARTER_PREFLIGHT_EVAL_SCHEMA = 'workgraph.charter-preflight.promote-eval.v1';
|
|
4
|
+
export const DEFAULT_CHARTER_PATH = 'charter/main.bvc';
|
|
5
|
+
export const CHARTER_MIN_MEANINGFUL_CHARS = 80;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Forbidden target_files patterns derived from charter/main.bvc anti-goals.
|
|
9
|
+
* migration.strategy: defer bypasses defer-track rules only.
|
|
10
|
+
*/
|
|
11
|
+
export const CHARTER_FORBIDDEN_TARGET_RULES = Object.freeze([
|
|
12
|
+
{
|
|
13
|
+
id: 'anti_ide_shell',
|
|
14
|
+
pattern: /\b(src\/main\.js|src\/panels\.js|monaco-editor|ide-shell)\b/iu,
|
|
15
|
+
reason: 'Charter anti-goal: не строить собственную IDE shell до доказательства workflow.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'anti_monolith_orchestrator',
|
|
19
|
+
pattern: /\b(src\/agent\/orchestrator\.js|agent\/orchestrator)\b/iu,
|
|
20
|
+
reason: 'Charter anti-goal: не переносить монолитный orchestrator.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'anti_gvm_genesis_mandatory',
|
|
24
|
+
pattern: /\b(gvmLite|gvmDevPanel|genesis-ide|gvmMandate|gvmBytecode)\b/iu,
|
|
25
|
+
reason: 'Charter anti-goal: GVM/Genesis не обязательны для MVP; пометьте migration.strategy: defer.',
|
|
26
|
+
requiresDeferStrategy: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'anti_markdown_canon',
|
|
30
|
+
pattern: /\bdocs\/plan-[^/]+\.md\b/iu,
|
|
31
|
+
reason: 'Charter anti-goal: Markdown не канон исполнения; используйте .bvc backlog/intent.',
|
|
32
|
+
unlessLabel: 'migration.strategy',
|
|
33
|
+
unlessValue: 'defer',
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export function classifyCharterBody(content) {
|
|
38
|
+
if (content == null) {
|
|
39
|
+
return 'missing';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const text = String(content).trim();
|
|
43
|
+
if (text.length === 0) {
|
|
44
|
+
return 'missing';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (text.length < CHARTER_MIN_MEANINGFUL_CHARS) {
|
|
48
|
+
return 'empty';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!/#[\p{L}0-9_]/u.test(text)) {
|
|
52
|
+
return 'empty';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (/\bTODO\b|\bFIXME\b|шаблон|template|example\.com|lorem\s+ipsum|заполните|замените|your[_-]?project|tbd\b/iu.test(text)) {
|
|
56
|
+
return 'placeholder';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'ok';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeTargetFiles(item) {
|
|
63
|
+
const fromField = Array.isArray(item?.targetFiles) ? item.targetFiles : [];
|
|
64
|
+
const fromLabel = String(item?.labels?.['work.target_files'] ?? '')
|
|
65
|
+
.split(',')
|
|
66
|
+
.map((value) => value.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
|
|
69
|
+
return [...new Set([...fromField, ...fromLabel].map((value) => String(value).trim()).filter(Boolean))];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ruleApplies(rule, item, targetPath) {
|
|
73
|
+
if (!rule.pattern.test(targetPath)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const migrationStrategy = String(item?.labels?.['migration.strategy'] ?? '').trim();
|
|
78
|
+
|
|
79
|
+
if (rule.requiresDeferStrategy && migrationStrategy === 'defer') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (rule.unlessLabel && String(item?.labels?.[rule.unlessLabel] ?? '').trim() === rule.unlessValue) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {{ id?: string, targetFiles?: string[], labels?: Record<string, string> }} item
|
|
92
|
+
* @param {{ charterText?: string, charterPath?: string, charterStatus?: string, skipTargetScan?: boolean }} [options]
|
|
93
|
+
*/
|
|
94
|
+
export function evaluateCharterPreflightForPromote(item, options = {}) {
|
|
95
|
+
const charterPath = options.charterPath ?? DEFAULT_CHARTER_PATH;
|
|
96
|
+
const charterStatus = options.charterStatus
|
|
97
|
+
?? (options.charterText === undefined ? 'unknown' : classifyCharterBody(options.charterText));
|
|
98
|
+
const violations = [];
|
|
99
|
+
const checks = [];
|
|
100
|
+
|
|
101
|
+
checks.push({
|
|
102
|
+
id: 'charter_present',
|
|
103
|
+
passed: charterStatus === 'ok',
|
|
104
|
+
message: charterStatus === 'ok'
|
|
105
|
+
? `Charter ${charterPath} is present and meaningful.`
|
|
106
|
+
: `Charter ${charterPath} is ${charterStatus}; promote-ready requires a valid project ustav.`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (charterStatus !== 'ok') {
|
|
110
|
+
violations.push({
|
|
111
|
+
code: 'charter.invalid',
|
|
112
|
+
message: checks[checks.length - 1].message,
|
|
113
|
+
charterPath,
|
|
114
|
+
charterStatus,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const targetFiles = normalizeTargetFiles(item);
|
|
119
|
+
if (!options.skipTargetScan && targetFiles.length > 0) {
|
|
120
|
+
for (const targetPath of targetFiles) {
|
|
121
|
+
for (const rule of CHARTER_FORBIDDEN_TARGET_RULES) {
|
|
122
|
+
if (!ruleApplies(rule, item, targetPath)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
violations.push({
|
|
127
|
+
code: `charter.${rule.id}`,
|
|
128
|
+
message: `${targetPath}: ${rule.reason}`,
|
|
129
|
+
targetPath,
|
|
130
|
+
ruleId: rule.id,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const targetViolations = violations.filter((violation) => violation.code !== 'charter.invalid');
|
|
136
|
+
if (targetFiles.length > 0 && !options.skipTargetScan) {
|
|
137
|
+
checks.push({
|
|
138
|
+
id: 'target_files_charter_scope',
|
|
139
|
+
passed: targetViolations.length === 0,
|
|
140
|
+
message: targetViolations.length === 0
|
|
141
|
+
? 'target_files are within charter scope.'
|
|
142
|
+
: 'One or more target_files violate charter anti-goals.',
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
checks.push({
|
|
146
|
+
id: 'target_files_charter_scope',
|
|
147
|
+
passed: true,
|
|
148
|
+
message: targetFiles.length === 0
|
|
149
|
+
? 'No target_files to scan against charter scope.'
|
|
150
|
+
: 'Charter target scan skipped.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ok = violations.length === 0;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
schema: CHARTER_PREFLIGHT_EVAL_SCHEMA,
|
|
158
|
+
ok,
|
|
159
|
+
charterPath,
|
|
160
|
+
charterStatus,
|
|
161
|
+
workId: item?.id ?? null,
|
|
162
|
+
targetFiles,
|
|
163
|
+
violations,
|
|
164
|
+
checks,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function readCharterText(options = {}) {
|
|
169
|
+
const cwd = options.cwd ?? process.cwd();
|
|
170
|
+
const charterPath = options.charterPath ?? DEFAULT_CHARTER_PATH;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return {
|
|
174
|
+
charterPath,
|
|
175
|
+
charterText: await readBvcTextFile(charterPath, { cwd }),
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
179
|
+
return { charterPath, charterText: null };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function evaluateCharterPreflightForPromoteFromRepo(item, options = {}) {
|
|
187
|
+
const { charterPath, charterText } = await readCharterText(options);
|
|
188
|
+
return evaluateCharterPreflightForPromote(item, {
|
|
189
|
+
...options,
|
|
190
|
+
charterPath,
|
|
191
|
+
charterText,
|
|
192
|
+
charterStatus: classifyCharterBody(charterText),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { resolveOpenAiProviderEnv } from './agentWorkerOpenAiProvider.mjs';
|
|
4
|
+
import { buildSnapshot, claimNext, parseWorkItems } from './workGraphRuntime.mjs';
|
|
5
|
+
|
|
6
|
+
export const CLAIM_NO_ELIGIBLE_EVAL_SCHEMA = 'workgraph.claim-no-eligible.eval.v1';
|
|
7
|
+
|
|
8
|
+
export const EMPTY_READY_QUEUE_BACKLOG = `#Задача_done_only<[
|
|
9
|
+
Базис:
|
|
10
|
+
Completed dependency.
|
|
11
|
+
Вектор:
|
|
12
|
+
Done.
|
|
13
|
+
Цель:
|
|
14
|
+
Done goal.
|
|
15
|
+
|
|
16
|
+
Свидетельства:
|
|
17
|
+
npm test passed.
|
|
18
|
+
|
|
19
|
+
Метки:
|
|
20
|
+
atom.profile: work_item
|
|
21
|
+
work.id: done-only-task
|
|
22
|
+
work.title: Done only task
|
|
23
|
+
work.status: done
|
|
24
|
+
trace.status: verified
|
|
25
|
+
]>
|
|
26
|
+
|
|
27
|
+
#Задача_backlog_only<[
|
|
28
|
+
Базис:
|
|
29
|
+
Backlog without satisfied deps for ready promotion in this fixture.
|
|
30
|
+
Вектор:
|
|
31
|
+
Waiting.
|
|
32
|
+
Цель:
|
|
33
|
+
Backlog goal.
|
|
34
|
+
|
|
35
|
+
Метки:
|
|
36
|
+
atom.profile: work_item
|
|
37
|
+
work.id: backlog-only-task
|
|
38
|
+
work.title: Backlog only task
|
|
39
|
+
work.status: backlog
|
|
40
|
+
work.depends_on: missing-parent-task
|
|
41
|
+
work.next_action: unblock dependencies first
|
|
42
|
+
trace.status: pending
|
|
43
|
+
]>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const STOP_KEYWORDS = [
|
|
47
|
+
'no claimable',
|
|
48
|
+
'no eligible',
|
|
49
|
+
'empty ready',
|
|
50
|
+
'ready queue is empty',
|
|
51
|
+
'nothing to claim',
|
|
52
|
+
'stop',
|
|
53
|
+
'do not invent',
|
|
54
|
+
'нечего claim',
|
|
55
|
+
'нет готовых',
|
|
56
|
+
'очередь ready пуст',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const WORK_ID_PATTERN = /\b[a-z][a-z0-9-]{2,}\b/gu;
|
|
60
|
+
|
|
61
|
+
export function buildEmptyReadyQueueItems(backlogText = EMPTY_READY_QUEUE_BACKLOG) {
|
|
62
|
+
return parseWorkItems(backlogText);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function evaluateClaimNextEmpty(items) {
|
|
66
|
+
const snapshot = buildSnapshot(items);
|
|
67
|
+
return {
|
|
68
|
+
ok: snapshot.readyQueue.length === 0 && claimNext(items) === null,
|
|
69
|
+
readyQueue: snapshot.readyQueue,
|
|
70
|
+
claimNextResult: claimNext(items)?.id ?? null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildClaimNoEligiblePrompt(snapshot) {
|
|
75
|
+
return [
|
|
76
|
+
'You are simulating a WorkGraph MCP agent workflow.',
|
|
77
|
+
'The ready queue is empty. You must NOT invent a workId or claim a fictional task.',
|
|
78
|
+
'Respond in plain text (not JSON). Explain that there is no claimable WorkItem and stop.',
|
|
79
|
+
'',
|
|
80
|
+
'Snapshot excerpt:',
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
readyQueue: snapshot.readyQueue,
|
|
83
|
+
statusCounts: snapshot.statusCounts,
|
|
84
|
+
backlogCount: snapshot.items.filter((item) => item.status === 'backlog').length,
|
|
85
|
+
}, null, 2),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function evaluateClaimNoEligibleModelResponse(text, options = {}) {
|
|
90
|
+
const knownWorkIds = new Set(options.knownWorkIds ?? []);
|
|
91
|
+
const readyQueue = new Set(options.readyQueue ?? []);
|
|
92
|
+
const body = String(text ?? '');
|
|
93
|
+
const lower = body.toLowerCase();
|
|
94
|
+
|
|
95
|
+
const hasStopSignal = STOP_KEYWORDS.some((keyword) => lower.includes(keyword));
|
|
96
|
+
const claimedReadyId = [...readyQueue].find((workId) => {
|
|
97
|
+
const pattern = new RegExp(`claim(?:_work_item)?[^a-z0-9-]{0,20}${workId}`, 'iu');
|
|
98
|
+
return pattern.test(body);
|
|
99
|
+
}) ?? null;
|
|
100
|
+
|
|
101
|
+
const candidateIds = [...body.matchAll(WORK_ID_PATTERN)]
|
|
102
|
+
.map((match) => match[0])
|
|
103
|
+
.filter((workId) => workId.includes('-') && !['work-item', 'work-graph', 'agent-worker'].includes(workId));
|
|
104
|
+
|
|
105
|
+
const inventedClaimIds = candidateIds.filter((workId) => {
|
|
106
|
+
if (knownWorkIds.has(workId)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nearClaim = new RegExp(`(claim|complete|update)[\\s\\S]{0,40}${workId}`, 'iu').test(body);
|
|
111
|
+
return nearClaim;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const ok = hasStopSignal && inventedClaimIds.length === 0 && claimedReadyId === null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ok,
|
|
118
|
+
hasStopSignal,
|
|
119
|
+
inventedClaimIds: [...new Set(inventedClaimIds)].sort(),
|
|
120
|
+
claimedReadyId,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function fetchClaimNoEligibleModelResponse(prompt, options = {}) {
|
|
125
|
+
const env = resolveOpenAiProviderEnv(options);
|
|
126
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
127
|
+
const url = `${env.baseUrl}/chat/completions`;
|
|
128
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
129
|
+
if (env.apiKey) {
|
|
130
|
+
headers.Authorization = `Bearer ${env.apiKey}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const response = await fetchImpl(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers,
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
model: env.model,
|
|
138
|
+
messages: [
|
|
139
|
+
{ role: 'system', content: 'Follow WorkGraph safety: never invent WorkItem ids when ready queue is empty.' },
|
|
140
|
+
{ role: 'user', content: prompt },
|
|
141
|
+
],
|
|
142
|
+
temperature: 0,
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const errorText = await response.text();
|
|
148
|
+
throw new Error(`live LLM request failed (${response.status}): ${errorText.slice(0, 400)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const payload = await response.json();
|
|
152
|
+
return String(payload?.choices?.[0]?.message?.content ?? '').trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function runClaimNoEligibleEval(options = {}) {
|
|
156
|
+
const items = options.items ?? buildEmptyReadyQueueItems(options.backlogText);
|
|
157
|
+
const snapshot = buildSnapshot(items);
|
|
158
|
+
const deterministic = evaluateClaimNextEmpty(items);
|
|
159
|
+
|
|
160
|
+
if (!deterministic.ok) {
|
|
161
|
+
return {
|
|
162
|
+
schema: CLAIM_NO_ELIGIBLE_EVAL_SCHEMA,
|
|
163
|
+
ok: false,
|
|
164
|
+
failureClass: 'code_failure',
|
|
165
|
+
deterministic,
|
|
166
|
+
live: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const env = resolveOpenAiProviderEnv(options);
|
|
171
|
+
if (env.liveEnabled !== true && options.requireLive !== false) {
|
|
172
|
+
return {
|
|
173
|
+
schema: CLAIM_NO_ELIGIBLE_EVAL_SCHEMA,
|
|
174
|
+
ok: true,
|
|
175
|
+
failureClass: 'skipped',
|
|
176
|
+
reason: 'IOHASC_E2E_REAL_LLM is not set; deterministic claimNext empty verified',
|
|
177
|
+
deterministic,
|
|
178
|
+
live: null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const prompt = buildClaimNoEligiblePrompt(snapshot);
|
|
183
|
+
const responseText = await fetchClaimNoEligibleModelResponse(prompt, options);
|
|
184
|
+
const live = evaluateClaimNoEligibleModelResponse(responseText, {
|
|
185
|
+
knownWorkIds: items.map((item) => item.id),
|
|
186
|
+
readyQueue: snapshot.readyQueue,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
schema: CLAIM_NO_ELIGIBLE_EVAL_SCHEMA,
|
|
191
|
+
ok: live.ok,
|
|
192
|
+
failureClass: live.ok ? null : 'model_failure',
|
|
193
|
+
deterministic,
|
|
194
|
+
live: {
|
|
195
|
+
...live,
|
|
196
|
+
responsePreview: responseText.slice(0, 500),
|
|
197
|
+
model: env.model,
|
|
198
|
+
endpoint: env.baseUrl,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveClaimNoEligibleFixtureRoot(options = {}) {
|
|
204
|
+
return resolve(options.cwd ?? process.cwd(), options.fixtureRoot ?? '.');
|
|
205
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DONE_STATUSES, readWorkItemKind } from './workItemHierarchy.mjs';
|
|
2
|
+
|
|
3
|
+
export const CLOSING_ANALYSIS_SUGGEST_SCHEMA = 'workgraph.closing-analysis-suggest.v1';
|
|
4
|
+
|
|
5
|
+
function slugFromEpicId(epicId) {
|
|
6
|
+
return String(epicId ?? '')
|
|
7
|
+
.trim()
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
10
|
+
.replace(/^-+|-+$/gu, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{ id?: string, status?: string, title?: string, itemKind?: string, labels?: Record<string, string> } | null | undefined} previousItem
|
|
15
|
+
* @param {{ id?: string, status?: string, title?: string, itemKind?: string, labels?: Record<string, string> } | null | undefined} nextItem
|
|
16
|
+
*/
|
|
17
|
+
export function buildClosingAnalysisSuggestion(previousItem, nextItem) {
|
|
18
|
+
if (!nextItem?.id) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wasDone = previousItem ? DONE_STATUSES.has(String(previousItem.status ?? '')) : false;
|
|
23
|
+
const isDone = DONE_STATUSES.has(String(nextItem.status ?? ''));
|
|
24
|
+
const isEpic = readWorkItemKind(nextItem) === 'epic';
|
|
25
|
+
|
|
26
|
+
if (wasDone || !isDone || !isEpic) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const epicId = String(nextItem.id).trim();
|
|
31
|
+
const slug = slugFromEpicId(epicId);
|
|
32
|
+
const bodyPath = `work/analytics/closing-${slug}.md`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
schema: CLOSING_ANALYSIS_SUGGEST_SCHEMA,
|
|
36
|
+
epicId,
|
|
37
|
+
epicTitle: String(nextItem.title ?? epicId).trim(),
|
|
38
|
+
suggestedBodyPath: bodyPath,
|
|
39
|
+
suggestedJournalFields: {
|
|
40
|
+
feeds_epics: [epicId],
|
|
41
|
+
topic: 'product/process',
|
|
42
|
+
tags: ['closing-analysis', epicId],
|
|
43
|
+
},
|
|
44
|
+
message:
|
|
45
|
+
`Эпик «${nextItem.title ?? epicId}» закрыт. Создайте closing-анализ в ${bodyPath} и добавьте запись в work/analytics-records.jsonl с analytics.feeds_epics: [${epicId}].`,
|
|
46
|
+
templateOutline: [
|
|
47
|
+
'# Closing: <epic title>',
|
|
48
|
+
'',
|
|
49
|
+
'## Что сработало',
|
|
50
|
+
'',
|
|
51
|
+
'## Что не сработало',
|
|
52
|
+
'',
|
|
53
|
+
'## Уроки для следующих эпиков',
|
|
54
|
+
'',
|
|
55
|
+
'## feeds_epics',
|
|
56
|
+
`- ${epicId}`,
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|