boxsafe 1.0.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/.directory +2 -0
- package/.env.example +3 -0
- package/AUDIT_LANG.md +45 -0
- package/BOXSAFE_VERSION_NOTES.md +14 -0
- package/README.md +4 -0
- package/TODO.md +130 -0
- package/adapters/index.ts +27 -0
- package/adapters/primary/cli-adapter.ts +56 -0
- package/adapters/secondary/filesystem/node-filesystem.ts +307 -0
- package/adapters/secondary/system/configuration.ts +147 -0
- package/ai/caller.ts +42 -0
- package/ai/label.ts +33 -0
- package/ai/modelConfig.ts +236 -0
- package/ai/provider.ts +111 -0
- package/boxsafe.config.json +68 -0
- package/core/auth/dasktop/cred/CRED.md +112 -0
- package/core/auth/dasktop/cred/credLinux.ts +82 -0
- package/core/auth/dasktop/cred/credWin.ts +2 -0
- package/core/config/defaults/boxsafeDefaults.ts +67 -0
- package/core/config/defaults/index.ts +1 -0
- package/core/config/loadConfig.ts +133 -0
- package/core/loop/about.md +13 -0
- package/core/loop/boxConfig.ts +20 -0
- package/core/loop/buildExecCommand.ts +76 -0
- package/core/loop/cmd/execode.ts +121 -0
- package/core/loop/cmd/test.js +3 -0
- package/core/loop/execLoop.ts +341 -0
- package/core/loop/git/VERSIONING.md +17 -0
- package/core/loop/git/commands.ts +11 -0
- package/core/loop/git/gitClient.ts +78 -0
- package/core/loop/git/index.ts +99 -0
- package/core/loop/git/runVersionControlRunner.ts +33 -0
- package/core/loop/initNavigator.ts +44 -0
- package/core/loop/initTasksManager.ts +35 -0
- package/core/loop/runValidation.ts +25 -0
- package/core/loop/tasks/AGENT-TASKS.md +36 -0
- package/core/loop/tasks/index.ts +96 -0
- package/core/loop/toolCalls.ts +168 -0
- package/core/loop/toolDispatcher.ts +146 -0
- package/core/loop/traceLogger.ts +106 -0
- package/core/loop/types.ts +26 -0
- package/core/loop/versionControlAdapter.ts +36 -0
- package/core/loop/waterfall.ts +404 -0
- package/core/loop/writeArtifactAtomically.ts +13 -0
- package/core/navigate/NAVIGATE.md +186 -0
- package/core/navigate/about.md +128 -0
- package/core/navigate/examples.ts +367 -0
- package/core/navigate/handler.ts +148 -0
- package/core/navigate/index.ts +32 -0
- package/core/navigate/navigate.test.ts +372 -0
- package/core/navigate/navigator.ts +437 -0
- package/core/navigate/types.ts +132 -0
- package/core/navigate/utils.ts +146 -0
- package/core/paths/paths.ts +33 -0
- package/core/ports/index.ts +271 -0
- package/core/segments/CONVENTIONS.md +30 -0
- package/core/segments/loop/index.ts +18 -0
- package/core/segments/map.ts +56 -0
- package/core/segments/navigate/index.ts +20 -0
- package/core/segments/versionControl/index.ts +18 -0
- package/core/util/logger.ts +128 -0
- package/docs/AGENT-TASKS.md +36 -0
- package/docs/ARQUITETURA_CORRECAO.md +121 -0
- package/docs/CONVENTIONS.md +30 -0
- package/docs/CRED.md +112 -0
- package/docs/L_RAG.md +567 -0
- package/docs/NAVIGATE.md +186 -0
- package/docs/PRIMARY_ACTORS.md +78 -0
- package/docs/SECONDARY_ACTORS.md +174 -0
- package/docs/VERSIONING.md +17 -0
- package/docs/boxsafe.config.md +472 -0
- package/eslint.config.mts +15 -0
- package/main.ts +53 -0
- package/memo/generated/codelog.md +13 -0
- package/memo/state/tasks/state.json +6 -0
- package/memo/state/tasks/tasks/task_001.md +2 -0
- package/memo/states-logs/logs.txt +7 -0
- package/memo/states-logs/trace-mljvrxvi-9g0k4q.jsonl +11 -0
- package/memo/states-logs/trace-mljvvc9j-pe9ekj.jsonl +11 -0
- package/memo/states-logs/trace-mljvvm1c-wbnqzp.jsonl +11 -0
- package/memo/states-logs/trace-mljxecwn-9xh3nw.jsonl +11 -0
- package/memo/states-logs/trace-mljxqkfm-ipijik.jsonl +11 -0
- package/memo/states-logs/trace-mljxwtrw-3fanky.jsonl +11 -0
- package/memo/states-logs/trace-mljxzen3-m8iinh.jsonl +11 -0
- package/memo/states-logs/trace-mljyucef-td6odn.jsonl +11 -0
- package/memo/states-logs/trace-mljyuprw-b1a6f4.jsonl +11 -0
- package/memo/states-logs/trace-mljyvefl-b6yoce.jsonl +11 -0
- package/memo/states-logs/trace-mljyxjo4-n7ibj2.jsonl +13 -0
- package/memo/states-logs/trace-mljziez5-8drqtn.jsonl +13 -0
- package/memo/states-logs/trace-mljziulp-dtd03z.jsonl +13 -0
- package/memo/states-logs/trace-mljzjwrq-1p2krb.jsonl +13 -0
- package/memo/states-logs/trace-mljzl0i7-b1cqa6.jsonl +13 -0
- package/memo/states-logs/trace-mljzmlk6-7kdyls.jsonl +13 -0
- package/memo/states-logs/trace-mlk0oj25-xa3dcu.jsonl +13 -0
- package/memo/states-logs/trace-mlk1x59q-713huj.jsonl +14 -0
- package/memo/states-logs/trace-mlk22dz8-7fd6hq.jsonl +14 -0
- package/memo/states-logs/trace-mlk241uy-wmx907.jsonl +14 -0
- package/memo/states-logs/trace-mlk2bf5r-yoh1vg.jsonl +15 -0
- package/package.json +44 -0
- package/pnpm-workspace.yaml +4 -0
- package/prompt_improvement_example.md +55 -0
- package/remove.txt +1 -0
- package/tests/adapters.test.ts +128 -0
- package/tests/extractCode.test.ts +26 -0
- package/tests/integration.test.ts +83 -0
- package/tests/loadConfig.test.ts +25 -0
- package/tests/navigatorBoundary.test.ts +17 -0
- package/tests/ports.test.ts +84 -0
- package/tests/runAllTests.ts +49 -0
- package/tests/toolCalls.test.ts +149 -0
- package/tests/waterfall.test.ts +52 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +17 -0
- package/types.d.ts +96 -0
- package/util/ANSI.ts +29 -0
- package/util/extractCode.ts +217 -0
- package/util/extractToolCalls.ts +80 -0
- package/util/logger.ts +125 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Implements an iterative agent loop that coordinates LLM code generation,
|
|
4
|
+
* execution, and validation until a successful result is achieved.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* The loop follows a deterministic pipeline:
|
|
8
|
+
* 1. Send feedback to an LLM instance.
|
|
9
|
+
* 2. Read the generated markdown artifact.
|
|
10
|
+
* 3. Extract language-specific code blocks.
|
|
11
|
+
* 4. Write the extracted code to an output file.
|
|
12
|
+
* 5. Execute the output via a system command.
|
|
13
|
+
* 6. Validate execution results using a waterfall validator.
|
|
14
|
+
* 7. Generate structured feedback on failure and retry.
|
|
15
|
+
*
|
|
16
|
+
* The process repeats until validation succeeds.
|
|
17
|
+
*
|
|
18
|
+
* This module is designed for automated agent workflows.
|
|
19
|
+
* Logs and artifacts are intended for machine consumption, not humans.
|
|
20
|
+
*
|
|
21
|
+
* @module core/loop
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFile } from "node:fs/promises";
|
|
25
|
+
import { pathToCode } from "@core/paths/paths";
|
|
26
|
+
import { createLLM } from "@ai/provider";
|
|
27
|
+
import { runLLM } from "@ai/caller";
|
|
28
|
+
import { LService, LModel } from "@ai/label";
|
|
29
|
+
import { extractCode } from "../../util/extractCode";
|
|
30
|
+
import type { CommandRun } from "../../types";
|
|
31
|
+
import { execode } from "./cmd/execode";
|
|
32
|
+
import { ANSI } from "@util/ANSI";
|
|
33
|
+
import { createNavigator } from "@core/navigate";
|
|
34
|
+
import type { Navigator } from "@core/navigate";
|
|
35
|
+
import TasksManager from '@core/loop/tasks';
|
|
36
|
+
import { loadBoxConfig, getVersionControlFlags } from '@core/loop/boxConfig';
|
|
37
|
+
import { createVersionControlAttemptRunner } from '@core/loop/versionControlAdapter';
|
|
38
|
+
import { dispatchToolCalls } from '@core/loop/toolDispatcher';
|
|
39
|
+
import { initTasksManager } from '@core/loop/initTasksManager';
|
|
40
|
+
import { initNavigator } from '@core/loop/initNavigator';
|
|
41
|
+
import { writeArtifactAtomically } from '@core/loop/writeArtifactAtomically';
|
|
42
|
+
import { buildExecCommand } from '@core/loop/buildExecCommand';
|
|
43
|
+
import { runValidation } from '@core/loop/runValidation';
|
|
44
|
+
import type { LoopOptions, LoopResult } from '@core/loop/types';
|
|
45
|
+
import { createTraceLogger } from '@core/loop/traceLogger';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a navigator instance with the given workspace.
|
|
49
|
+
* Useful for creating navigator before loop or injecting custom config.
|
|
50
|
+
* @param workspace - path to workspace directory
|
|
51
|
+
* @param maxFileSize - optional maximum file size in bytes (default 10MB)
|
|
52
|
+
* @returns Navigator instance
|
|
53
|
+
*/
|
|
54
|
+
export const createWorkspaceNavigator = (workspace: string, maxFileSize?: number): Navigator => {
|
|
55
|
+
return createNavigator({
|
|
56
|
+
workspace,
|
|
57
|
+
maxFileSize: maxFileSize ?? 10 * 1024 * 1024, // maximum in bytes
|
|
58
|
+
});
|
|
59
|
+
}; // Summary: Get the working path, adjust it to absolute, and get the maximum size in MB for a file.
|
|
60
|
+
|
|
61
|
+
// Reads the markdown file generated by the model (path injectable for testability)
|
|
62
|
+
const readMarkdown = async (markdownPath: string) => {
|
|
63
|
+
return readFile(markdownPath, "utf-8");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const loop = async (
|
|
67
|
+
{
|
|
68
|
+
service,
|
|
69
|
+
model,
|
|
70
|
+
initialPrompt,
|
|
71
|
+
cmd,
|
|
72
|
+
lang,
|
|
73
|
+
pathOutput,
|
|
74
|
+
maxIterations = 10,
|
|
75
|
+
limit,
|
|
76
|
+
signal,
|
|
77
|
+
pathGeneratedMarkdown = pathToCode,
|
|
78
|
+
navigator: injectedNavigator,
|
|
79
|
+
workspace,
|
|
80
|
+
} : LoopOptions
|
|
81
|
+
): Promise<LoopResult> => {
|
|
82
|
+
const llm = createLLM(service, model);
|
|
83
|
+
const trace = createTraceLogger({});
|
|
84
|
+
const log = trace.wrapLogger();
|
|
85
|
+
await trace.emit('loop.start', { runId: trace.runId }, {
|
|
86
|
+
service: String(service),
|
|
87
|
+
model: String(model),
|
|
88
|
+
lang: String(lang),
|
|
89
|
+
maxIterations: Number(maxIterations),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Version control: configurable commit messages and behavior
|
|
93
|
+
// Top-level variable for the 'before' commit message so it's easy to find and change.
|
|
94
|
+
const BEFORE_COMMIT_MESSAGE = process.env.BOXSAFE_BEFORE_MSG ?? 'save agent';
|
|
95
|
+
|
|
96
|
+
const boxConfig = loadBoxConfig();
|
|
97
|
+
const { vcBefore, vcAfter, vcGenerateNotes, vcAutoPushConfig } = getVersionControlFlags(boxConfig);
|
|
98
|
+
const attemptVersionControl = createVersionControlAttemptRunner();
|
|
99
|
+
|
|
100
|
+
const configuredTimeoutMs = typeof boxConfig.commands?.timeoutMs === 'number' ? boxConfig.commands.timeoutMs : undefined;
|
|
101
|
+
|
|
102
|
+
// If configured, run a one-time 'before' commit when the agent starts
|
|
103
|
+
if (vcBefore) {
|
|
104
|
+
try {
|
|
105
|
+
await trace.emit('versionControl.before.start', { runId: trace.runId });
|
|
106
|
+
const res = await attemptVersionControl({ repoPath: workspace ?? process.cwd(), commitMessage: BEFORE_COMMIT_MESSAGE, autoPush: vcAutoPushConfig, generateNotes: vcGenerateNotes });
|
|
107
|
+
log.info(`${ANSI.Cyan}[VersionControl]${ANSI.Reset} before-commit completed: ${JSON.stringify(res)}`);
|
|
108
|
+
await trace.emit('versionControl.before.ok', { runId: trace.runId }, { result: res as any });
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
log.warn(`${ANSI.Yellow}[VersionControl]${ANSI.Reset} before-commit failed after retries: ${err?.message ?? err}`);
|
|
111
|
+
await trace.emit('versionControl.before.error', { runId: trace.runId }, { error: err?.message ?? String(err) });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tasksManager: TasksManager | null = await initTasksManager(boxConfig);
|
|
116
|
+
|
|
117
|
+
// Determine effective workspace (from arg, config or cwd) and initialize navigator
|
|
118
|
+
log.info(`${ANSI.Cyan}[InitNavigator]${ANSI.Reset} Initializing navigator...`);
|
|
119
|
+
const { effectiveWorkspace, navigator } = initNavigator({
|
|
120
|
+
...(workspace ? { workspaceArg: workspace } : {}),
|
|
121
|
+
...(boxConfig.project?.workspace ? { configWorkspace: boxConfig.project.workspace } : {}),
|
|
122
|
+
...(injectedNavigator ? { injectedNavigator } : {}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
log.info(`${ANSI.Cyan}[InitNavigator]${ANSI.Reset} Navigator initialized: ${!!navigator ? 'YES' : 'NO'}, Workspace: ${effectiveWorkspace}`);
|
|
126
|
+
|
|
127
|
+
const effectiveLimit = typeof limit === "number" ? limit : maxIterations;
|
|
128
|
+
// feedback starts from tasks manager current task if available, otherwise initial prompt
|
|
129
|
+
let feedback = initialPrompt;
|
|
130
|
+
if (tasksManager && !tasksManager.isFinished()) {
|
|
131
|
+
const t = tasksManager.getCurrentTask();
|
|
132
|
+
if (t) feedback = t;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
136
|
+
|
|
137
|
+
for (limit = 1; limit <= effectiveLimit; limit++) {
|
|
138
|
+
const iterCtx = { runId: trace.runId, iter: limit };
|
|
139
|
+
const iterLog = trace.wrapLogger(iterCtx);
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
iterLog.error(`aborted`);
|
|
142
|
+
await trace.emit('loop.aborted', iterCtx);
|
|
143
|
+
return { ok: false, iterations: limit, navigator };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
iterLog.info(`iteration ${limit}`);
|
|
147
|
+
await trace.emit('iteration.start', iterCtx);
|
|
148
|
+
|
|
149
|
+
// Run the LLM with the current feedback (with a small retry/backoff for transient LLM errors)
|
|
150
|
+
let llmAttempts = 0;
|
|
151
|
+
const maxLlmAttempts = 3;
|
|
152
|
+
while (true) {
|
|
153
|
+
try {
|
|
154
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
155
|
+
const promptToSend = `${feedback}\n\nYou may optionally emit tool calls as JSON fenced blocks (\`\`\`json-tool ...\`\`\`) BEFORE the final code block. If you do, set tool=\"navigate\" and params.op=\"write\" to create files. Then, ALWAYS end your response with exactly ONE code block in the language: ${lang}`;
|
|
156
|
+
await trace.emit('llm.run.start', iterCtx);
|
|
157
|
+
await runLLM(promptToSend, llm, { service, model, outputPath: pathGeneratedMarkdown });
|
|
158
|
+
await trace.emit('llm.run.ok', iterCtx, { outputPath: pathGeneratedMarkdown });
|
|
159
|
+
break;
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
llmAttempts++;
|
|
162
|
+
iterLog.error(`${err?.message ?? err}`);
|
|
163
|
+
await trace.emit('llm.run.error', iterCtx, { error: err?.message ?? String(err), attempt: llmAttempts });
|
|
164
|
+
if (llmAttempts >= maxLlmAttempts) {
|
|
165
|
+
await trace.emit('iteration.failed', iterCtx, { layer: 'llm' });
|
|
166
|
+
return { ok: false, iterations: limit, navigator };
|
|
167
|
+
}
|
|
168
|
+
const backoff = 200 * Math.pow(2, llmAttempts - 1);
|
|
169
|
+
await sleep(backoff);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Read the generated markdown
|
|
174
|
+
let markdown: string;
|
|
175
|
+
try {
|
|
176
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
177
|
+
markdown = await readMarkdown(pathGeneratedMarkdown);
|
|
178
|
+
await trace.emit('markdown.read.ok', iterCtx, { path: pathGeneratedMarkdown, bytes: markdown.length });
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
iterLog.error(`${ANSI.Red}[ReadMarkdown]${ANSI.Reset} ${err?.message ?? err}`);
|
|
181
|
+
await trace.emit('markdown.read.error', iterCtx, { error: err?.message ?? String(err), path: pathGeneratedMarkdown });
|
|
182
|
+
// give feedback to the model and retry
|
|
183
|
+
feedback = "Could not read the generated markdown. Please emit markdown artifact.";
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extract code blocks for the target language
|
|
188
|
+
let codeBlocks: string[];
|
|
189
|
+
try {
|
|
190
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
191
|
+
codeBlocks = await extractCode(markdown, lang, {
|
|
192
|
+
throwOnNotFound: true,
|
|
193
|
+
});
|
|
194
|
+
await trace.emit('extractCode.ok', iterCtx, { blocks: codeBlocks.length, lang: String(lang) });
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
iterLog.warn(`${ANSI.Yellow}[ExtractCode]${ANSI.Reset} ${err?.message ?? err}`);
|
|
197
|
+
await trace.emit('extractCode.error', iterCtx, { error: err?.message ?? String(err), lang: String(lang) });
|
|
198
|
+
feedback = "No code blocks were found. Generate valid code for the requested language.";
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Tool call handling: detect JSON tool blocks in the generated markdown ---
|
|
203
|
+
iterLog.info(`${ANSI.Cyan}[ToolCalls]${ANSI.Reset} Processing tool calls from markdown...`);
|
|
204
|
+
await dispatchToolCalls({
|
|
205
|
+
markdown,
|
|
206
|
+
navigator,
|
|
207
|
+
boxConfig,
|
|
208
|
+
ANSI,
|
|
209
|
+
attemptVersionControl,
|
|
210
|
+
vcAutoPushConfig,
|
|
211
|
+
traceEmit: trace.emit.bind(trace),
|
|
212
|
+
traceCtx: iterCtx,
|
|
213
|
+
});
|
|
214
|
+
iterLog.info(`${ANSI.Cyan}[ToolCalls]${ANSI.Reset} Tool call processing completed`);
|
|
215
|
+
|
|
216
|
+
if (!codeBlocks || codeBlocks.length === 0) {
|
|
217
|
+
feedback = "No code blocks were found. Generate valid code.";
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Atomic write: write to temp and rename
|
|
222
|
+
const tmpPath = `${pathOutput}.tmp`;
|
|
223
|
+
try {
|
|
224
|
+
await writeArtifactAtomically({
|
|
225
|
+
tmpPath,
|
|
226
|
+
pathOutput,
|
|
227
|
+
content: codeBlocks.join("\n\n"),
|
|
228
|
+
...(signal ? { signal } : {}),
|
|
229
|
+
});
|
|
230
|
+
await trace.emit('artifact.write.ok', iterCtx, { path: pathOutput, bytes: codeBlocks.join("\n\n").length });
|
|
231
|
+
} catch (err: any) {
|
|
232
|
+
iterLog.error(`${ANSI.Red}[WriteFile]${ANSI.Reset} ${err?.message ?? err}`);
|
|
233
|
+
await trace.emit('artifact.write.error', iterCtx, { error: err?.message ?? String(err), path: pathOutput });
|
|
234
|
+
feedback = "Failed to write output file. Ensure filesystem permissions are correct.";
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Execute the generated code
|
|
239
|
+
let execResult: any;
|
|
240
|
+
try {
|
|
241
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
242
|
+
const execCmd = await buildExecCommand({ cmd, lang, pathOutput });
|
|
243
|
+
await trace.emit('exec.start', iterCtx, { cmd: Array.isArray(execCmd) ? execCmd[0] : execCmd });
|
|
244
|
+
execResult = await execode(execCmd, {
|
|
245
|
+
...(configuredTimeoutMs !== undefined ? { timeoutMs: configuredTimeoutMs } : {}),
|
|
246
|
+
});
|
|
247
|
+
await trace.emit('exec.ok', iterCtx, { exitCode: execResult.exitCode });
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
iterLog.error(`${ANSI.Red}[Execode]${ANSI.Reset} ${err?.message ?? err}`);
|
|
250
|
+
await trace.emit('exec.error', iterCtx, { error: err?.message ?? String(err) });
|
|
251
|
+
feedback = `Execution failed: ${err?.message ?? "unknown"}`;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Debug: log exec output and the written artifact to diagnose failures
|
|
256
|
+
try {
|
|
257
|
+
iterLog.info(`${ANSI.Cyan}[Execode]${ANSI.Reset} exit=${execResult.exitCode}`);
|
|
258
|
+
iterLog.info(`${ANSI.Cyan}[Execode]${ANSI.Reset} stdout=${String(execResult.stdout).slice(0,1000)}`);
|
|
259
|
+
iterLog.info(`${ANSI.Cyan}[Execode]${ANSI.Reset} stderr=${String(execResult.stderr).slice(0,1000)}`);
|
|
260
|
+
try {
|
|
261
|
+
const outFile = await readFile(pathOutput, 'utf-8');
|
|
262
|
+
iterLog.info(`${ANSI.Cyan}[OutputFile]${ANSI.Reset} ${outFile.slice(0,1000)}`);
|
|
263
|
+
} catch (err: any) {
|
|
264
|
+
iterLog.info(`${ANSI.Cyan}[OutputFile]${ANSI.Reset} could not read output file: ${err?.message ?? err}`);
|
|
265
|
+
}
|
|
266
|
+
} catch (errAny) {
|
|
267
|
+
// swallow logging errors
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Validate execution results using the waterfall pipeline
|
|
271
|
+
let verdict: any;
|
|
272
|
+
try {
|
|
273
|
+
await trace.emit('validation.start', iterCtx);
|
|
274
|
+
verdict = await runValidation({
|
|
275
|
+
execResult,
|
|
276
|
+
pathOutput,
|
|
277
|
+
...(signal ? { signal } : {}),
|
|
278
|
+
});
|
|
279
|
+
await trace.emit('validation.ok', iterCtx, {
|
|
280
|
+
ok: Boolean(verdict?.ok),
|
|
281
|
+
score: typeof verdict?.score === 'number' ? verdict.score : undefined,
|
|
282
|
+
layer: verdict?.layer ?? undefined,
|
|
283
|
+
});
|
|
284
|
+
} catch (err: any) {
|
|
285
|
+
await trace.emit('validation.error', iterCtx, { error: err?.message ?? String(err) });
|
|
286
|
+
feedback = `Validation pipeline failed: ${err?.message ?? "unknown"}`;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Stop or advance if the execution is valid
|
|
291
|
+
if (verdict?.ok) {
|
|
292
|
+
iterLog.info(`success for current task/iteration`);
|
|
293
|
+
await trace.emit('iteration.success', iterCtx, {
|
|
294
|
+
score: typeof verdict?.score === 'number' ? verdict.score : undefined,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// If task manager is active, mark current done and continue to next
|
|
298
|
+
if (tasksManager) {
|
|
299
|
+
await tasksManager.markCurrentDone();
|
|
300
|
+
if (!tasksManager.isFinished()) {
|
|
301
|
+
const next = tasksManager.getCurrentTask();
|
|
302
|
+
feedback = next ?? feedback;
|
|
303
|
+
log.info(`${ANSI.Cyan}[Tasks]${ANSI.Reset} advanced to next task`);
|
|
304
|
+
// continue loop to process next task
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// all tasks done — proceed to after-success flow below
|
|
308
|
+
log.info(`${ANSI.Cyan}[Tasks]${ANSI.Reset} all tasks completed`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// After-success versioning (commit explaining what was done)
|
|
312
|
+
if (vcAfter) {
|
|
313
|
+
const afterMessage = `agent: completed successfully in ${limit} iterations`;
|
|
314
|
+
try {
|
|
315
|
+
await trace.emit('versionControl.after.start', iterCtx);
|
|
316
|
+
const res = await attemptVersionControl({ repoPath: workspace ?? process.cwd(), commitMessage: afterMessage, autoPush: vcAutoPushConfig, generateNotes: vcGenerateNotes });
|
|
317
|
+
log.info(`${ANSI.Cyan}[VersionControl]${ANSI.Reset} after-commit completed: ${JSON.stringify(res)}`);
|
|
318
|
+
await trace.emit('versionControl.after.ok', iterCtx, { result: res as any });
|
|
319
|
+
} catch (err: any) {
|
|
320
|
+
log.warn(`${ANSI.Yellow}[VersionControl]${ANSI.Reset} after-commit failed after retries: ${err?.message ?? err}`);
|
|
321
|
+
await trace.emit('versionControl.after.error', iterCtx, { error: err?.message ?? String(err) });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { ok: true, iterations: limit, verdict, artifacts: { outputFile: pathOutput }, navigator };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Build structured feedback for the next iteration
|
|
329
|
+
feedback = `Layer: ${verdict?.layer ?? "unknown"}\nReason: ${verdict?.reason ?? "unknown"}\nDetails: ${verdict?.details ?? "n/a"}`;
|
|
330
|
+
|
|
331
|
+
iterLog.warn(`${ANSI.Yellow}[Waterfall]${ANSI.Reset} failed at ${verdict?.layer ?? "unknown"}`);
|
|
332
|
+
await trace.emit('iteration.failed', iterCtx, {
|
|
333
|
+
layer: verdict?.layer ?? 'unknown',
|
|
334
|
+
reason: verdict?.reason ?? 'unknown',
|
|
335
|
+
score: typeof verdict?.score === 'number' ? verdict.score : undefined,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// If the loop completes without a successful verdict
|
|
340
|
+
return { ok: false, iterations: limit, navigator };
|
|
341
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Git Versioning Module
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
- Lightweight helper for staging, committing and optionally pushing changes to Git/GitHub.
|
|
5
|
+
|
|
6
|
+
Behavior
|
|
7
|
+
- Finds repository root (searches up from working directory)
|
|
8
|
+
- Stages all changes and commits with a sensible default message
|
|
9
|
+
- Optionally creates `BOXSAFE_VERSION_NOTES.md` describing the commit
|
|
10
|
+
- Attempts to push to `origin` and will try a token from keyring (`gh-token`) or `PASSWORD_GIT` env var if push fails due to auth
|
|
11
|
+
|
|
12
|
+
How to use
|
|
13
|
+
- Programmatic: import `runVersionControl` from `@core/loop/git` and call with options `{ autoPush?: boolean, generateNotes?: boolean, commitMessage?: string }`.
|
|
14
|
+
- CLI / manual: this module is intended to be invoked by the agent runtime.
|
|
15
|
+
|
|
16
|
+
Security
|
|
17
|
+
- Token injection only used as a fallback for a single push attempt. Prefer using native credential helpers or keyring (see `core/auth/dasktop/cred`).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Centralized git command templates for easy maintenance
|
|
2
|
+
export const GIT_STATUS = 'git status --porcelain';
|
|
3
|
+
export const GIT_ADD_ALL = 'git add -A';
|
|
4
|
+
export const GIT_GET_BRANCH = 'git rev-parse --abbrev-ref HEAD';
|
|
5
|
+
export const GIT_REMOTE_GET_URL = 'git remote get-url origin';
|
|
6
|
+
export const GIT_SHOW_COMMIT = 'git show --name-only --pretty=format:"%B" HEAD';
|
|
7
|
+
|
|
8
|
+
export const gitCommitCmd = (message: string) => `git commit -m "${String(message).replace(/"/g, '\\"')}"`;
|
|
9
|
+
export const gitPushOriginCmd = (branch?: string) => branch ? `git push origin ${branch}` : 'git push origin HEAD';
|
|
10
|
+
export const gitPushSetUpstreamCmd = (branch: string) => `git push --set-upstream origin ${branch}`;
|
|
11
|
+
export const gitPushToUrlCmd = (remoteUrl: string, branch: string) => `git push "${remoteUrl}" HEAD:refs/heads/${branch}`;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
GIT_STATUS,
|
|
6
|
+
GIT_ADD_ALL,
|
|
7
|
+
GIT_GET_BRANCH,
|
|
8
|
+
GIT_REMOTE_GET_URL,
|
|
9
|
+
GIT_SHOW_COMMIT,
|
|
10
|
+
gitCommitCmd,
|
|
11
|
+
gitPushOriginCmd,
|
|
12
|
+
gitPushSetUpstreamCmd,
|
|
13
|
+
gitPushToUrlCmd,
|
|
14
|
+
} from './commands';
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
export async function runCommand(cmd: string, cwd?: string) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await execAsync(cmd, { cwd, env: process.env });
|
|
21
|
+
return { stdout: res.stdout?.toString() ?? '', stderr: res.stderr?.toString() ?? '', code: 0 };
|
|
22
|
+
} catch (err: any) {
|
|
23
|
+
return { stdout: err.stdout?.toString() ?? '', stderr: err.stderr?.toString() ?? err.message, code: err.code ?? 1 };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function hasUnstagedChanges(repoPath: string) {
|
|
28
|
+
const r = await runCommand(GIT_STATUS, repoPath);
|
|
29
|
+
return (r.stdout || r.stderr).trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function stageAll(repoPath: string) {
|
|
33
|
+
return runCommand(GIT_ADD_ALL, repoPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function commitAll(repoPath: string, message = 'chore: alterações automáticas do agente') {
|
|
37
|
+
const status = await hasUnstagedChanges(repoPath);
|
|
38
|
+
if (!status) return { ok: false, reason: 'no-changes' };
|
|
39
|
+
const r = await runCommand(gitCommitCmd(message), repoPath);
|
|
40
|
+
return { ok: r.code === 0, out: r.stdout, err: r.stderr };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getRemoteUrl(repoPath: string) {
|
|
44
|
+
const r = await runCommand(GIT_REMOTE_GET_URL, repoPath);
|
|
45
|
+
if (r.code !== 0) return null;
|
|
46
|
+
return (r.stdout || '').trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function injectTokenInHttpsUrl(remoteUrl: string, token: string) {
|
|
50
|
+
if (!remoteUrl.startsWith('https://')) return null;
|
|
51
|
+
// https://github.com/owner/repo.git -> https://{token}@github.com/owner/repo.git
|
|
52
|
+
const withoutProto = remoteUrl.replace('https://', '');
|
|
53
|
+
return `https://${encodeURIComponent(token)}@${withoutProto}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function pushOrigin(repoPath: string) {
|
|
57
|
+
// prefer explicit branch push to allow clearer error handling upstream
|
|
58
|
+
const branchRes = await runCommand(GIT_GET_BRANCH, repoPath);
|
|
59
|
+
const branch = (branchRes.stdout || '').trim() || 'HEAD';
|
|
60
|
+
return runCommand(gitPushOriginCmd(branch), repoPath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function pushWithToken(remoteUrl: string, repoPath: string, token: string) {
|
|
64
|
+
const injected = injectTokenInHttpsUrl(remoteUrl, token);
|
|
65
|
+
if (!injected) return { code: 1, stderr: 'remote-not-https' };
|
|
66
|
+
// get current branch
|
|
67
|
+
const branchRes = await runCommand(GIT_GET_BRANCH, repoPath);
|
|
68
|
+
const branch = (branchRes.stdout || '').trim();
|
|
69
|
+
if (!branch) return { code: 1, stderr: 'could-not-detect-branch' };
|
|
70
|
+
const cmd = gitPushToUrlCmd(injected, branch);
|
|
71
|
+
return runCommand(cmd, repoPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getCommitSummary(repoPath: string) {
|
|
75
|
+
const r = await runCommand(GIT_SHOW_COMMIT, repoPath);
|
|
76
|
+
if (r.code !== 0) return null;
|
|
77
|
+
return r.stdout.trim();
|
|
78
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub versioning helper
|
|
3
|
+
* - stages and commits changes
|
|
4
|
+
* - optionally generates notes
|
|
5
|
+
* - attempts push; falls back to keyring token when available
|
|
6
|
+
*
|
|
7
|
+
* Design goals: minimal, robust, descriptive names and clear behavior.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { runCommand, stageAll, commitAll, getRemoteUrl, pushOrigin, pushWithToken, getCommitSummary } from './gitClient';
|
|
12
|
+
import { getCredLinux } from '@core/auth/dasktop/cred/credLinux';
|
|
13
|
+
|
|
14
|
+
async function findRepoRoot(start = process.cwd()) {
|
|
15
|
+
let cur = start;
|
|
16
|
+
for (let i = 0; i < 8; i++) {
|
|
17
|
+
if (fs.existsSync(path.join(cur, '.git'))) return cur;
|
|
18
|
+
const parent = path.dirname(cur);
|
|
19
|
+
if (parent === cur) break;
|
|
20
|
+
cur = parent;
|
|
21
|
+
}
|
|
22
|
+
return start; // fallback
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VersionControlOptions {
|
|
26
|
+
repoPath?: string;
|
|
27
|
+
commitMessage?: string;
|
|
28
|
+
autoPush?: boolean;
|
|
29
|
+
generateNotes?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runVersionControl(opts: VersionControlOptions = {}) {
|
|
33
|
+
const repoRoot = opts.repoPath ? path.resolve(opts.repoPath) : await findRepoRoot();
|
|
34
|
+
const message = opts.commitMessage ?? 'chore: alterações automáticas do agente';
|
|
35
|
+
|
|
36
|
+
// ensure git user config exists (if not, use EMAIL_GIT env var)
|
|
37
|
+
const emailCheck = await runCommand('git config user.email', repoRoot);
|
|
38
|
+
if (!emailCheck.stdout?.trim() && process.env.EMAIL_GIT) {
|
|
39
|
+
await runCommand(`git config user.email "${process.env.EMAIL_GIT}"`, repoRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Stage and commit
|
|
43
|
+
await stageAll(repoRoot);
|
|
44
|
+
const commitResult = await commitAll(repoRoot, message);
|
|
45
|
+
if (!commitResult.ok && commitResult.reason === 'no-changes') {
|
|
46
|
+
return { committed: false, reason: 'no-changes' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Optionally generate notes
|
|
50
|
+
if (opts.generateNotes || process.env.BOXSAFE_GENERATE_NOTES === '1') {
|
|
51
|
+
const summary = (await getCommitSummary(repoRoot)) ?? '';
|
|
52
|
+
const notesPath = path.join(repoRoot, 'BOXSAFE_VERSION_NOTES.md');
|
|
53
|
+
const notes = `# BOXSAFE Versioning Notes\n\nCommit message:\n\n${message}\n\nSummary:\n\n${summary}\n`;
|
|
54
|
+
try {
|
|
55
|
+
fs.writeFileSync(notesPath, notes, { encoding: 'utf-8' });
|
|
56
|
+
await runCommand(`git add "${notesPath}"`, repoRoot);
|
|
57
|
+
await runCommand(`git commit -m "chore: add versioning notes by boxsafe agent"`, repoRoot);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// ignore note failures
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Attempt push if requested
|
|
64
|
+
if (opts.autoPush) {
|
|
65
|
+
const remote = await getRemoteUrl(repoRoot);
|
|
66
|
+
if (!remote) return { committed: true, pushed: false, reason: 'no-remote' };
|
|
67
|
+
|
|
68
|
+
// try to push normally first
|
|
69
|
+
const pushResp = await pushOrigin(repoRoot);
|
|
70
|
+
if (pushResp.code === 0) return { committed: true, pushed: true };
|
|
71
|
+
|
|
72
|
+
// If push failed, attempt to detect branch/upstream issues and retry with set-upstream
|
|
73
|
+
const branchRes = await runCommand('git rev-parse --abbrev-ref HEAD', repoRoot);
|
|
74
|
+
const branch = (branchRes.stdout || '').trim();
|
|
75
|
+
const stderr = String(pushResp.stderr || '').toLowerCase();
|
|
76
|
+
|
|
77
|
+
const needsSetUpstream = /no upstream|set upstream|no tracking information|failed to push some refs/.test(stderr) && branch;
|
|
78
|
+
if (needsSetUpstream) {
|
|
79
|
+
try {
|
|
80
|
+
const setRes = await runCommand(`git push --set-upstream origin ${branch}`, repoRoot);
|
|
81
|
+
if (setRes.code === 0) return { committed: true, pushed: true, note: 'set-upstream' };
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// swallow and fall through to token fallback
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If push failed (likely auth), try token from keyring or env
|
|
88
|
+
const token = (await getCredLinux({ account: 'gh-token' })) ?? process.env.PASSWORD_GIT ?? process.env.GITHUB_TOKEN;
|
|
89
|
+
if (!token) return { committed: true, pushed: false, reason: 'auth-needed', pushStdErr: pushResp.stderr };
|
|
90
|
+
|
|
91
|
+
const tryPushWithToken = await pushWithToken(remote, repoRoot, token);
|
|
92
|
+
if (tryPushWithToken.code === 0) return { committed: true, pushed: true, note: 'pushed-with-token' };
|
|
93
|
+
return { committed: true, pushed: false, reason: 'push-failed', pushStdErr: String(tryPushWithToken.stderr || tryPushWithToken.stderr) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { committed: true, pushed: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default { runVersionControl };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { runVersionControl } from '@core/loop/git';
|
|
6
|
+
import { Logger } from '@core/util/logger';
|
|
7
|
+
|
|
8
|
+
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
9
|
+
|
|
10
|
+
const logger = Logger.createModuleLogger('VersionControlRunner');
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const repoRoot = path.resolve(__dirname, '../../../');
|
|
15
|
+
|
|
16
|
+
const configPath = path.join(repoRoot, 'boxsafe.config.json');
|
|
17
|
+
let bsConfig: any = {};
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
try {
|
|
20
|
+
bsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
bsConfig = {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const autoPush = bsConfig.project?.versionControl?.after ?? (process.env.BOXSAFE_AUTO_PUSH === '1');
|
|
27
|
+
const generateNotes = bsConfig.project?.versionControl?.generateNotes ?? (process.env.BOXSAFE_GENERATE_NOTES === '1');
|
|
28
|
+
|
|
29
|
+
const res = await runVersionControl({ repoPath: repoRoot, autoPush, generateNotes, commitMessage: 'chore: add git versioning module (boxsafe agent)' });
|
|
30
|
+
logger.info(`Version control result: ${JSON.stringify(res)}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch((e) => { logger.error(`Runner error: ${e}`); process.exit(1); });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createNavigator } from '@core/navigate';
|
|
2
|
+
import type { Navigator } from '@core/navigate';
|
|
3
|
+
import { Logger } from '@core/util/logger';
|
|
4
|
+
|
|
5
|
+
const logger = Logger.createModuleLogger('InitNavigator');
|
|
6
|
+
|
|
7
|
+
type InitNavigatorArgs = {
|
|
8
|
+
workspaceArg?: string;
|
|
9
|
+
configWorkspace?: string;
|
|
10
|
+
injectedNavigator?: Navigator;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function initNavigator({ workspaceArg, configWorkspace, injectedNavigator }: InitNavigatorArgs): {
|
|
14
|
+
effectiveWorkspace: string;
|
|
15
|
+
navigator: Navigator | null;
|
|
16
|
+
} {
|
|
17
|
+
const effectiveWorkspace = workspaceArg ?? configWorkspace ?? process.cwd();
|
|
18
|
+
|
|
19
|
+
logger.info(`Initializing navigator with workspace: ${effectiveWorkspace}`);
|
|
20
|
+
logger.debug(`Workspace sources - Arg: ${workspaceArg}, Config: ${configWorkspace}, CWD: ${process.cwd()}`);
|
|
21
|
+
|
|
22
|
+
if (injectedNavigator) {
|
|
23
|
+
logger.info('Using injected navigator');
|
|
24
|
+
return { effectiveWorkspace, navigator: injectedNavigator };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!effectiveWorkspace) {
|
|
28
|
+
logger.error('No workspace available for navigator');
|
|
29
|
+
return { effectiveWorkspace: process.cwd(), navigator: null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const navigator = createNavigator({
|
|
34
|
+
workspace: effectiveWorkspace,
|
|
35
|
+
logger: Logger.createModuleLogger('Navigator')
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.info(`Navigator created successfully for workspace: ${effectiveWorkspace}`);
|
|
39
|
+
return { effectiveWorkspace, navigator };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error(`Failed to create navigator: ${error}`);
|
|
42
|
+
return { effectiveWorkspace: process.cwd(), navigator: null };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import TasksManager from './tasks';
|
|
4
|
+
import { TASKS_STATE_DIR } from '@core/paths/paths';
|
|
5
|
+
import { Logger } from '@core/util/logger';
|
|
6
|
+
|
|
7
|
+
const logger = Logger.createModuleLogger('InitTasksManager');
|
|
8
|
+
|
|
9
|
+
export async function initTasksManager(boxConfig: any): Promise<TasksManager | null> {
|
|
10
|
+
let tasksManager: TasksManager | null = null;
|
|
11
|
+
try {
|
|
12
|
+
const disableTasks = String(process.env.BOXSAFE_DISABLE_TASKS ?? '').toLowerCase();
|
|
13
|
+
if (disableTasks === 'true' || disableTasks === '1' || disableTasks === 'yes') {
|
|
14
|
+
logger.info(`tasks manager disabled by BOXSAFE_DISABLE_TASKS`);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const todoCfg = boxConfig.project?.todo ? String(boxConfig.project?.todo).trim() : '';
|
|
19
|
+
if (todoCfg) {
|
|
20
|
+
const todoPath = path.resolve(todoCfg);
|
|
21
|
+
if (fs.existsSync(todoPath) && fs.statSync(todoPath).isFile()) {
|
|
22
|
+
tasksManager = new TasksManager(todoPath, TASKS_STATE_DIR);
|
|
23
|
+
await tasksManager.init();
|
|
24
|
+
logger.info(`loaded ${tasksManager.total()} tasks`);
|
|
25
|
+
} else {
|
|
26
|
+
logger.info(`todo path not found or not a file: ${todoCfg}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.warn(`failed to init tasks: ${error}`);
|
|
31
|
+
tasksManager = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return tasksManager;
|
|
35
|
+
}
|