@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,178 @@
|
|
|
1
|
+
import { posix, resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const BOUNDED_TARGET_FILE_READ_SCHEMA = 'workgraph.bounded-target-file-read.v1';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MAX_BYTES_PER_FILE = 32_768;
|
|
6
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 131_072;
|
|
7
|
+
|
|
8
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
9
|
+
|
|
10
|
+
export function normalizeBoundedTargetPath(rawPath, repoRoot = '') {
|
|
11
|
+
const normalized = String(rawPath ?? '').trim().replace(/\\/gu, '/');
|
|
12
|
+
if (normalized === '') {
|
|
13
|
+
return { ok: false, error: 'empty path' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:/u.test(normalized)) {
|
|
17
|
+
return { ok: false, error: 'absolute paths are not allowed' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (normalized.split('/').includes('..')) {
|
|
21
|
+
return { ok: false, error: 'path traversal is not allowed' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cleaned = posix.normalize(normalized).replace(/^\.\//u, '');
|
|
25
|
+
if (cleaned === '..' || cleaned.startsWith('../')) {
|
|
26
|
+
return { ok: false, error: 'path traversal is not allowed' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (repoRoot) {
|
|
30
|
+
const absolute = resolve(repoRoot, cleaned);
|
|
31
|
+
const root = resolve(repoRoot);
|
|
32
|
+
if (!absolute.startsWith(root)) {
|
|
33
|
+
return { ok: false, error: 'path escapes repo root' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { ok: true, path: cleaned };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isPathAllowedForTargetFiles(path, targetFiles) {
|
|
41
|
+
const allowed = new Set(
|
|
42
|
+
(targetFiles ?? []).map((entry) => normalizeBoundedTargetPath(entry).path).filter(Boolean),
|
|
43
|
+
);
|
|
44
|
+
const normalized = normalizeBoundedTargetPath(path);
|
|
45
|
+
return normalized.ok && allowed.has(normalized.path);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readBoundedTargetFile(path, options = {}) {
|
|
49
|
+
const targetFiles = options.targetFiles ?? [];
|
|
50
|
+
const repoRoot = options.repoRoot ?? options.cwd ?? process.cwd();
|
|
51
|
+
const maxBytes = options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
|
|
52
|
+
const readFileImpl = options.readFile ?? (async (filePath) => {
|
|
53
|
+
const { readFile } = await import('node:fs/promises');
|
|
54
|
+
return readFile(filePath, 'utf8');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!isPathAllowedForTargetFiles(path, targetFiles)) {
|
|
58
|
+
return {
|
|
59
|
+
path: String(path),
|
|
60
|
+
ok: false,
|
|
61
|
+
error: 'path is not in task targetFiles allowlist',
|
|
62
|
+
content: '',
|
|
63
|
+
truncated: false,
|
|
64
|
+
byteLength: 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalized = normalizeBoundedTargetPath(path, repoRoot);
|
|
69
|
+
if (!normalized.ok) {
|
|
70
|
+
return {
|
|
71
|
+
path: String(path),
|
|
72
|
+
ok: false,
|
|
73
|
+
error: normalized.error,
|
|
74
|
+
content: '',
|
|
75
|
+
truncated: false,
|
|
76
|
+
byteLength: 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const absolutePath = resolve(repoRoot, normalized.path);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const content = await readFileImpl(absolutePath);
|
|
84
|
+
const text = String(content);
|
|
85
|
+
const byteLength = Buffer.byteLength(text, 'utf8');
|
|
86
|
+
const truncated = byteLength > maxBytes;
|
|
87
|
+
const slice = truncated ? Buffer.from(text, 'utf8').subarray(0, maxBytes).toString('utf8') : text;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
path: normalized.path,
|
|
91
|
+
ok: true,
|
|
92
|
+
error: '',
|
|
93
|
+
content: slice,
|
|
94
|
+
truncated,
|
|
95
|
+
byteLength,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return {
|
|
100
|
+
path: normalized.path,
|
|
101
|
+
ok: false,
|
|
102
|
+
error: message,
|
|
103
|
+
content: '',
|
|
104
|
+
truncated: false,
|
|
105
|
+
byteLength: 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function readBoundedTargetFiles(input, options = {}) {
|
|
111
|
+
const targetFiles = [...(options.targetFiles ?? input?.targetFiles ?? [])]
|
|
112
|
+
.map(String)
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.sort(compareText);
|
|
115
|
+
|
|
116
|
+
const maxBytesPerFile = options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
|
|
117
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
118
|
+
const repoRoot = options.repoRoot ?? options.cwd ?? process.cwd();
|
|
119
|
+
|
|
120
|
+
const files = [];
|
|
121
|
+
let totalBytes = 0;
|
|
122
|
+
|
|
123
|
+
for (const path of targetFiles) {
|
|
124
|
+
if (totalBytes >= maxTotalBytes) {
|
|
125
|
+
files.push({
|
|
126
|
+
path,
|
|
127
|
+
ok: false,
|
|
128
|
+
error: 'total byte budget exceeded',
|
|
129
|
+
content: '',
|
|
130
|
+
truncated: false,
|
|
131
|
+
byteLength: 0,
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const remaining = maxTotalBytes - totalBytes;
|
|
137
|
+
const entry = await readBoundedTargetFile(path, {
|
|
138
|
+
targetFiles,
|
|
139
|
+
repoRoot,
|
|
140
|
+
maxBytesPerFile: Math.min(maxBytesPerFile, remaining),
|
|
141
|
+
readFile: options.readFile,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
files.push(entry);
|
|
145
|
+
if (entry.ok) {
|
|
146
|
+
totalBytes += Buffer.byteLength(entry.content, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
schema: BOUNDED_TARGET_FILE_READ_SCHEMA,
|
|
152
|
+
taskId: input?.task?.id ?? '',
|
|
153
|
+
allowedPaths: targetFiles,
|
|
154
|
+
files,
|
|
155
|
+
summary: {
|
|
156
|
+
requested: targetFiles.length,
|
|
157
|
+
readOk: files.filter((entry) => entry.ok).length,
|
|
158
|
+
failed: files.filter((entry) => !entry.ok).length,
|
|
159
|
+
truncated: files.filter((entry) => entry.truncated).length,
|
|
160
|
+
totalBytes,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function formatBoundedTargetFilesForPrompt(readResult) {
|
|
166
|
+
if (!readResult?.files?.length) {
|
|
167
|
+
return 'No target file contents available.';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return readResult.files.map((entry) => {
|
|
171
|
+
if (!entry.ok) {
|
|
172
|
+
return `# ${entry.path}\n(error: ${entry.error})`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const suffix = entry.truncated ? '\n...(truncated)' : '';
|
|
176
|
+
return `# ${entry.path}\n${entry.content}${suffix}`;
|
|
177
|
+
}).join('\n\n');
|
|
178
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
2
|
+
|
|
3
|
+
export const CYCLE_SLICE_SCHEMA = 'workgraph.cycle.slice.v1';
|
|
4
|
+
export const PHASE_EPIC_PATTERN = /^phase-\d+/u;
|
|
5
|
+
export const DONE_STATUSES = new Set(['done', 'verified']);
|
|
6
|
+
export const OPERATIONAL_STATUSES = new Set(['ready', 'claimed', 'doing', 'in_progress', 'verify', 'blocked']);
|
|
7
|
+
export const DEFAULT_DONE_ARCHIVE_CAP = 12;
|
|
8
|
+
|
|
9
|
+
export function sortDoneArchiveItems(items) {
|
|
10
|
+
return [...items].sort((left, right) => compareText(right.id, left.id));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function selectDoneArchiveItems(items, cap = DEFAULT_DONE_ARCHIVE_CAP) {
|
|
14
|
+
return sortDoneArchiveItems(items.filter((item) => DONE_STATUSES.has(item.status))).slice(0, cap);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function deriveWorkCycle(item, itemById) {
|
|
18
|
+
if (!item || typeof item !== 'object') {
|
|
19
|
+
throw new TypeError('item must be an object');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const explicitCycle = String(item.labels?.['work.cycle'] ?? item.cycle ?? '').trim();
|
|
23
|
+
if (explicitCycle !== '') {
|
|
24
|
+
return explicitCycle;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (PHASE_EPIC_PATTERN.test(item.id)) {
|
|
28
|
+
return item.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const visited = new Set();
|
|
32
|
+
const queue = [...(item.dependsOn ?? [])];
|
|
33
|
+
|
|
34
|
+
while (queue.length > 0) {
|
|
35
|
+
const dependencyId = queue.shift();
|
|
36
|
+
if (visited.has(dependencyId)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
visited.add(dependencyId);
|
|
40
|
+
|
|
41
|
+
if (PHASE_EPIC_PATTERN.test(dependencyId)) {
|
|
42
|
+
return dependencyId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dependency = itemById.get(dependencyId);
|
|
46
|
+
if (dependency?.labels?.['work.cycle']) {
|
|
47
|
+
return String(dependency.labels['work.cycle']).trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const nextId of dependency?.dependsOn ?? []) {
|
|
51
|
+
if (!visited.has(nextId)) {
|
|
52
|
+
queue.push(nextId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'uncategorized';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveCurrentCycle(items, itemById) {
|
|
61
|
+
const activeItems = items.filter((item) => OPERATIONAL_STATUSES.has(item.status) && item.status !== 'backlog');
|
|
62
|
+
const cycles = activeItems.map((item) => deriveWorkCycle(item, itemById));
|
|
63
|
+
|
|
64
|
+
const phaseCycles = cycles.filter((cycle) => PHASE_EPIC_PATTERN.test(cycle));
|
|
65
|
+
if (phaseCycles.length > 0) {
|
|
66
|
+
return [...phaseCycles].sort((left, right) => comparePhaseEpic(right, left))[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const readyItem = items.find((item) => item.status === 'ready');
|
|
70
|
+
if (readyItem) {
|
|
71
|
+
return deriveWorkCycle(readyItem, itemById);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return cycles[0] ?? 'uncategorized';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildCycleSliceProjection(items, options = {}) {
|
|
78
|
+
const stableItems = [...items].sort((left, right) => compareText(left.id, right.id));
|
|
79
|
+
const itemById = new Map(stableItems.map((item) => [item.id, item]));
|
|
80
|
+
const doneArchiveCap = options.doneArchiveCap ?? DEFAULT_DONE_ARCHIVE_CAP;
|
|
81
|
+
|
|
82
|
+
const cyclesMap = new Map();
|
|
83
|
+
for (const item of stableItems) {
|
|
84
|
+
const cycleId = deriveWorkCycle(item, itemById);
|
|
85
|
+
const bucket = cyclesMap.get(cycleId) ?? {
|
|
86
|
+
id: cycleId,
|
|
87
|
+
label: formatCycleLabel(cycleId),
|
|
88
|
+
total: 0,
|
|
89
|
+
done: 0,
|
|
90
|
+
active: 0,
|
|
91
|
+
backlog: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
bucket.total += 1;
|
|
95
|
+
if (DONE_STATUSES.has(item.status)) {
|
|
96
|
+
bucket.done += 1;
|
|
97
|
+
} else if (item.status === 'backlog') {
|
|
98
|
+
bucket.backlog += 1;
|
|
99
|
+
} else {
|
|
100
|
+
bucket.active += 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cyclesMap.set(cycleId, bucket);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const currentCycle = options.currentCycle ?? resolveCurrentCycle(stableItems, itemById);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
schema: CYCLE_SLICE_SCHEMA,
|
|
110
|
+
currentCycle,
|
|
111
|
+
doneArchiveCap,
|
|
112
|
+
operationalStatuses: [...OPERATIONAL_STATUSES],
|
|
113
|
+
derivedByWorkId: Object.fromEntries(
|
|
114
|
+
stableItems.map((item) => [item.id, deriveWorkCycle(item, itemById)]),
|
|
115
|
+
),
|
|
116
|
+
cycles: [...cyclesMap.values()].sort((left, right) => compareText(left.id, right.id)),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function filterItemsByCycleSlice(items, options = {}) {
|
|
121
|
+
const stableItems = [...items].sort((left, right) => compareText(left.id, right.id));
|
|
122
|
+
const itemById = new Map(stableItems.map((item) => [item.id, item]));
|
|
123
|
+
const cycleId = options.cycleId ?? 'current';
|
|
124
|
+
const resolvedCycle = cycleId === 'current'
|
|
125
|
+
? resolveCurrentCycle(stableItems, itemById)
|
|
126
|
+
: cycleId;
|
|
127
|
+
const includeAllCycles = resolvedCycle === 'all';
|
|
128
|
+
const operationalOnly = options.operationalOnly !== false;
|
|
129
|
+
const doneArchiveCap = options.doneArchiveCap ?? DEFAULT_DONE_ARCHIVE_CAP;
|
|
130
|
+
|
|
131
|
+
const cycleMatched = includeAllCycles
|
|
132
|
+
? stableItems
|
|
133
|
+
: stableItems.filter((item) => deriveWorkCycle(item, itemById) === resolvedCycle);
|
|
134
|
+
|
|
135
|
+
if (!operationalOnly) {
|
|
136
|
+
return cycleMatched;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const operational = cycleMatched.filter((item) => OPERATIONAL_STATUSES.has(item.status));
|
|
140
|
+
const doneArchive = selectDoneArchiveItems(cycleMatched, doneArchiveCap);
|
|
141
|
+
|
|
142
|
+
return [...operational, ...doneArchive];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function partitionBoardItems(items, options = {}) {
|
|
146
|
+
const filtered = filterItemsByCycleSlice(items, options);
|
|
147
|
+
const operational = filtered.filter((item) => OPERATIONAL_STATUSES.has(item.status));
|
|
148
|
+
const doneArchive = selectDoneArchiveItems(filtered, options.doneArchiveCap ?? DEFAULT_DONE_ARCHIVE_CAP);
|
|
149
|
+
const hiddenDoneCount = Math.max(0, countDoneInCycle(items, options) - doneArchive.length);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
operational,
|
|
153
|
+
doneArchive,
|
|
154
|
+
hiddenDoneCount,
|
|
155
|
+
totalVisible: filtered.length,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function countDoneInCycle(items, options) {
|
|
160
|
+
const itemById = new Map(items.map((item) => [item.id, item]));
|
|
161
|
+
const cycleId = options.cycleId ?? 'current';
|
|
162
|
+
const resolvedCycle = cycleId === 'current'
|
|
163
|
+
? resolveCurrentCycle(items, itemById)
|
|
164
|
+
: cycleId;
|
|
165
|
+
|
|
166
|
+
if (resolvedCycle === 'all') {
|
|
167
|
+
return items.filter((item) => DONE_STATUSES.has(item.status)).length;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return items.filter((item) => DONE_STATUSES.has(item.status) && deriveWorkCycle(item, itemById) === resolvedCycle).length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatCycleLabel(cycleId) {
|
|
174
|
+
if (cycleId === 'uncategorized') {
|
|
175
|
+
return 'Без цикла';
|
|
176
|
+
}
|
|
177
|
+
return cycleId.replace(/^phase-/u, 'Фаза ').replace(/-/gu, ' · ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function comparePhaseEpic(left, right) {
|
|
181
|
+
const leftNum = Number.parseInt(String(left).match(/^phase-(\d+)/u)?.[1] ?? '0', 10);
|
|
182
|
+
const rightNum = Number.parseInt(String(right).match(/^phase-(\d+)/u)?.[1] ?? '0', 10);
|
|
183
|
+
return leftNum - rightNum;
|
|
184
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { appendFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
appendWorkerRunJournal,
|
|
7
|
+
runAgentWorkerLiveLoop,
|
|
8
|
+
} from './agentWorkerLiveLoop.mjs';
|
|
9
|
+
import { readWorkItemsFromRepo } from './intentTreeWorkItems.mjs';
|
|
10
|
+
import { buildRecoverySuggestionFromWorkerOutput } from './workGraphRecoveryPolicy.mjs';
|
|
11
|
+
|
|
12
|
+
export const DAEMON_TICK_PHASES = ['observe', 'schedule', 'run', 'recovery', 'audit', 'stop'];
|
|
13
|
+
|
|
14
|
+
const TICK_OUTPUT_SCHEMA = 'workgraph.daemon.tick.output.v1';
|
|
15
|
+
export const DAEMON_AUDIT_TAIL_SCHEMA = 'workgraph.daemon-audit.tail.v1';
|
|
16
|
+
const DEFAULT_AUDIT_PATH = 'work/daemon-audit.jsonl';
|
|
17
|
+
const DEFAULT_WORKER_RUNS_PATH = 'work/worker-runs.jsonl';
|
|
18
|
+
const DEFAULT_AUDIT_TAIL_LIMIT = 24;
|
|
19
|
+
const MAX_AUDIT_TAIL_LIMIT = 200;
|
|
20
|
+
|
|
21
|
+
export function buildDaemonTickAuditRecord(tickMeta) {
|
|
22
|
+
return {
|
|
23
|
+
tickId: tickMeta.tickId,
|
|
24
|
+
event: tickMeta.event,
|
|
25
|
+
taskId: tickMeta.taskId ?? '',
|
|
26
|
+
workerStatus: tickMeta.workerStatus ?? '',
|
|
27
|
+
recoveryClass: tickMeta.recoveryClass ?? '',
|
|
28
|
+
summary: tickMeta.summary ?? '',
|
|
29
|
+
recordedAt: tickMeta.recordedAt ?? new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runWorkGraphDaemonTick(items, options = {}) {
|
|
34
|
+
const tickId = options.tickId ?? `tick-${Date.now()}`;
|
|
35
|
+
const phases = [];
|
|
36
|
+
const schedulerPolicy = {
|
|
37
|
+
paused: false,
|
|
38
|
+
dryRun: false,
|
|
39
|
+
...(options.schedulerPolicy ?? {}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
phases.push({ phase: 'observe', detail: 'read snapshot', itemCount: items.length });
|
|
43
|
+
|
|
44
|
+
if (schedulerPolicy.paused) {
|
|
45
|
+
phases.push({ phase: 'schedule', detail: 'scheduler paused', skipped: true });
|
|
46
|
+
phases.push({ phase: 'stop', detail: 'tick skipped' });
|
|
47
|
+
return buildTickOutput({
|
|
48
|
+
ok: true,
|
|
49
|
+
tickId,
|
|
50
|
+
phases,
|
|
51
|
+
skippedReason: 'scheduler_paused',
|
|
52
|
+
auditRecord: buildDaemonTickAuditRecord({
|
|
53
|
+
tickId,
|
|
54
|
+
event: 'tick_skipped',
|
|
55
|
+
summary: 'scheduler paused',
|
|
56
|
+
recordedAt: options.recordedAt,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const loopOptions = {
|
|
62
|
+
taskId: options.taskId,
|
|
63
|
+
runId: options.runId ?? `daemon-${tickId}`,
|
|
64
|
+
workerInput: options.workerInput,
|
|
65
|
+
runWorker: options.runWorker,
|
|
66
|
+
provider: options.provider,
|
|
67
|
+
providerOptions: options.providerOptions,
|
|
68
|
+
selectionOptions: options.selectionOptions,
|
|
69
|
+
enableFallback: options.enableFallback,
|
|
70
|
+
maxFallbackAttempts: options.maxFallbackAttempts,
|
|
71
|
+
recordedAt: options.recordedAt,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (schedulerPolicy.dryRun) {
|
|
75
|
+
const pool = cloneItems(items);
|
|
76
|
+
const selected = options.taskId
|
|
77
|
+
? pool.find((item) => item.id === options.taskId)
|
|
78
|
+
: pool.find((item) => item.status === 'ready');
|
|
79
|
+
|
|
80
|
+
phases.push({
|
|
81
|
+
phase: 'schedule',
|
|
82
|
+
detail: selected ? `dry-run selected ${selected.id}` : 'no task selected',
|
|
83
|
+
taskId: selected?.id,
|
|
84
|
+
});
|
|
85
|
+
phases.push({ phase: 'stop', detail: 'dry-run complete' });
|
|
86
|
+
|
|
87
|
+
return buildTickOutput({
|
|
88
|
+
ok: true,
|
|
89
|
+
tickId,
|
|
90
|
+
phases,
|
|
91
|
+
selectedTaskId: selected?.id ?? null,
|
|
92
|
+
skippedReason: selected ? 'dry_run' : 'no_claimable_task',
|
|
93
|
+
auditRecord: buildDaemonTickAuditRecord({
|
|
94
|
+
tickId,
|
|
95
|
+
event: 'tick_skipped',
|
|
96
|
+
taskId: selected?.id,
|
|
97
|
+
summary: selected ? 'dry-run schedule only' : 'no claimable task',
|
|
98
|
+
recordedAt: options.recordedAt,
|
|
99
|
+
}),
|
|
100
|
+
recoverySuggestion: buildRecoverySuggestionFromWorkerOutput(null, {
|
|
101
|
+
noClaimableTask: !selected,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
phases.push({ phase: 'schedule', detail: 'delegate to live-loop claimNext policy' });
|
|
107
|
+
|
|
108
|
+
const loopResult = await runAgentWorkerLiveLoop(items, loopOptions);
|
|
109
|
+
|
|
110
|
+
phases.push({
|
|
111
|
+
phase: 'run',
|
|
112
|
+
detail: 'live-loop worker run',
|
|
113
|
+
taskId: loopResult.taskId,
|
|
114
|
+
workerStatus: loopResult.workerOutput?.status,
|
|
115
|
+
ok: loopResult.ok,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const recoverySuggestion = buildRecoverySuggestionFromWorkerOutput(loopResult.workerOutput ?? null, {
|
|
119
|
+
noClaimableTask: loopResult.error === 'no_claimable_task',
|
|
120
|
+
retryCount: options.retryCount,
|
|
121
|
+
maxRetries: options.maxRetries,
|
|
122
|
+
taskStatus: loopResult.finalItems?.find((item) => item.id === loopResult.taskId)?.status,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
phases.push({
|
|
126
|
+
phase: 'recovery',
|
|
127
|
+
detail: recoverySuggestion.preset,
|
|
128
|
+
action: recoverySuggestion.action,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const auditRecord = buildDaemonTickAuditRecord({
|
|
132
|
+
tickId,
|
|
133
|
+
event: loopResult.ok ? 'worker_run_finished' : 'tick_failed',
|
|
134
|
+
taskId: loopResult.taskId,
|
|
135
|
+
workerStatus: loopResult.workerOutput?.status,
|
|
136
|
+
recoveryClass: recoverySuggestion.failureClass,
|
|
137
|
+
summary: loopResult.workerOutput?.patchSummary?.summary || loopResult.error || recoverySuggestion.reason,
|
|
138
|
+
recordedAt: options.recordedAt,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
phases.push({ phase: 'audit', detail: 'build audit record', event: auditRecord.event });
|
|
142
|
+
phases.push({ phase: 'stop', detail: 'tick complete', taskId: loopResult.taskId });
|
|
143
|
+
|
|
144
|
+
return buildTickOutput({
|
|
145
|
+
ok: loopResult.ok,
|
|
146
|
+
tickId,
|
|
147
|
+
phases,
|
|
148
|
+
selectedTaskId: loopResult.taskId ?? null,
|
|
149
|
+
error: loopResult.error,
|
|
150
|
+
workerOutput: loopResult.workerOutput,
|
|
151
|
+
workerRunSummary: loopResult.workerRunSummary,
|
|
152
|
+
auditRecord,
|
|
153
|
+
recoverySuggestion,
|
|
154
|
+
finalItems: loopResult.finalItems,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runWorkGraphDaemonTickFromBacklogFile(options = {}) {
|
|
159
|
+
const items = await readWorkItemsFromRepo(options);
|
|
160
|
+
return runWorkGraphDaemonTick(items, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function appendDaemonAuditJournal(entry, options = {}) {
|
|
164
|
+
const journalPath = resolve(options.cwd ?? process.cwd(), options.auditPath ?? DEFAULT_AUDIT_PATH);
|
|
165
|
+
await appendFile(journalPath, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
166
|
+
return journalPath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function readDaemonAuditJournal(options = {}) {
|
|
170
|
+
const journalPath = resolve(options.cwd ?? process.cwd(), options.auditPath ?? DEFAULT_AUDIT_PATH);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const text = await readFile(journalPath, 'utf8');
|
|
174
|
+
return text
|
|
175
|
+
.split(/\r?\n/u)
|
|
176
|
+
.map((line) => line.trim())
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.map((line) => JSON.parse(line));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function readDaemonAuditTailResponse(options = {}) {
|
|
189
|
+
const limitRaw = options.limit;
|
|
190
|
+
const limit = Number.isInteger(limitRaw) && limitRaw > 0
|
|
191
|
+
? Math.min(limitRaw, MAX_AUDIT_TAIL_LIMIT)
|
|
192
|
+
: DEFAULT_AUDIT_TAIL_LIMIT;
|
|
193
|
+
const auditPath = options.auditPath ?? DEFAULT_AUDIT_PATH;
|
|
194
|
+
const entries = await readDaemonAuditJournal({ ...options, auditPath });
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
schema: DAEMON_AUDIT_TAIL_SCHEMA,
|
|
198
|
+
journalPath: auditPath,
|
|
199
|
+
limit,
|
|
200
|
+
totalCount: entries.length,
|
|
201
|
+
truncated: entries.length > limit,
|
|
202
|
+
entries: entries.slice(-limit).reverse(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildTickOutput(fields) {
|
|
207
|
+
return {
|
|
208
|
+
schema: TICK_OUTPUT_SCHEMA,
|
|
209
|
+
...fields,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cloneItems(items) {
|
|
214
|
+
return items.map((item) => ({
|
|
215
|
+
...item,
|
|
216
|
+
dependsOn: [...item.dependsOn],
|
|
217
|
+
evidence: [...item.evidence],
|
|
218
|
+
checks: [...item.checks],
|
|
219
|
+
labels: { ...item.labels },
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseCliArgs(argv) {
|
|
224
|
+
const args = argv.slice(2);
|
|
225
|
+
const options = {
|
|
226
|
+
once: false,
|
|
227
|
+
dryRun: false,
|
|
228
|
+
paused: false,
|
|
229
|
+
writeJournal: true,
|
|
230
|
+
taskId: undefined,
|
|
231
|
+
backlogPath: undefined,
|
|
232
|
+
auditPath: DEFAULT_AUDIT_PATH,
|
|
233
|
+
workerRunsPath: DEFAULT_WORKER_RUNS_PATH,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
237
|
+
const arg = args[index];
|
|
238
|
+
|
|
239
|
+
if (arg === '--once') {
|
|
240
|
+
options.once = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (arg === '--dry-run') {
|
|
245
|
+
options.dryRun = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (arg === '--paused') {
|
|
250
|
+
options.paused = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (arg === '--no-journal') {
|
|
255
|
+
options.writeJournal = false;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg === '--backlog') {
|
|
260
|
+
options.backlogPath = args[index + 1];
|
|
261
|
+
index += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!arg.startsWith('--') && options.taskId === undefined) {
|
|
266
|
+
options.taskId = arg;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return options;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
274
|
+
const cli = parseCliArgs(process.argv);
|
|
275
|
+
|
|
276
|
+
if (!cli.once) {
|
|
277
|
+
console.error('Usage: node src/workGraphDaemonTick.mjs --once [--dry-run] [--paused] [--backlog path/to/backlog.bvc] [taskId]');
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
} else {
|
|
280
|
+
const result = await runWorkGraphDaemonTickFromBacklogFile({
|
|
281
|
+
taskId: cli.taskId,
|
|
282
|
+
backlogPath: cli.backlogPath,
|
|
283
|
+
schedulerPolicy: { paused: cli.paused, dryRun: cli.dryRun },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (cli.writeJournal && result.auditRecord) {
|
|
287
|
+
await appendDaemonAuditJournal(result.auditRecord, { auditPath: cli.auditPath });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (cli.writeJournal && result.workerRunSummary && !cli.dryRun && !cli.paused) {
|
|
291
|
+
await appendWorkerRunJournal(result.workerRunSummary, { journalPath: cli.workerRunsPath });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(JSON.stringify({
|
|
295
|
+
ok: result.ok,
|
|
296
|
+
tickId: result.tickId,
|
|
297
|
+
selectedTaskId: result.selectedTaskId,
|
|
298
|
+
skippedReason: result.skippedReason,
|
|
299
|
+
recoverySuggestion: result.recoverySuggestion,
|
|
300
|
+
phases: result.phases?.map((step) => step.phase),
|
|
301
|
+
}, null, 2));
|
|
302
|
+
|
|
303
|
+
if (!result.ok && result.skippedReason !== 'dry_run' && result.skippedReason !== 'scheduler_paused') {
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|