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.
Files changed (118) hide show
  1. package/.directory +2 -0
  2. package/.env.example +3 -0
  3. package/AUDIT_LANG.md +45 -0
  4. package/BOXSAFE_VERSION_NOTES.md +14 -0
  5. package/README.md +4 -0
  6. package/TODO.md +130 -0
  7. package/adapters/index.ts +27 -0
  8. package/adapters/primary/cli-adapter.ts +56 -0
  9. package/adapters/secondary/filesystem/node-filesystem.ts +307 -0
  10. package/adapters/secondary/system/configuration.ts +147 -0
  11. package/ai/caller.ts +42 -0
  12. package/ai/label.ts +33 -0
  13. package/ai/modelConfig.ts +236 -0
  14. package/ai/provider.ts +111 -0
  15. package/boxsafe.config.json +68 -0
  16. package/core/auth/dasktop/cred/CRED.md +112 -0
  17. package/core/auth/dasktop/cred/credLinux.ts +82 -0
  18. package/core/auth/dasktop/cred/credWin.ts +2 -0
  19. package/core/config/defaults/boxsafeDefaults.ts +67 -0
  20. package/core/config/defaults/index.ts +1 -0
  21. package/core/config/loadConfig.ts +133 -0
  22. package/core/loop/about.md +13 -0
  23. package/core/loop/boxConfig.ts +20 -0
  24. package/core/loop/buildExecCommand.ts +76 -0
  25. package/core/loop/cmd/execode.ts +121 -0
  26. package/core/loop/cmd/test.js +3 -0
  27. package/core/loop/execLoop.ts +341 -0
  28. package/core/loop/git/VERSIONING.md +17 -0
  29. package/core/loop/git/commands.ts +11 -0
  30. package/core/loop/git/gitClient.ts +78 -0
  31. package/core/loop/git/index.ts +99 -0
  32. package/core/loop/git/runVersionControlRunner.ts +33 -0
  33. package/core/loop/initNavigator.ts +44 -0
  34. package/core/loop/initTasksManager.ts +35 -0
  35. package/core/loop/runValidation.ts +25 -0
  36. package/core/loop/tasks/AGENT-TASKS.md +36 -0
  37. package/core/loop/tasks/index.ts +96 -0
  38. package/core/loop/toolCalls.ts +168 -0
  39. package/core/loop/toolDispatcher.ts +146 -0
  40. package/core/loop/traceLogger.ts +106 -0
  41. package/core/loop/types.ts +26 -0
  42. package/core/loop/versionControlAdapter.ts +36 -0
  43. package/core/loop/waterfall.ts +404 -0
  44. package/core/loop/writeArtifactAtomically.ts +13 -0
  45. package/core/navigate/NAVIGATE.md +186 -0
  46. package/core/navigate/about.md +128 -0
  47. package/core/navigate/examples.ts +367 -0
  48. package/core/navigate/handler.ts +148 -0
  49. package/core/navigate/index.ts +32 -0
  50. package/core/navigate/navigate.test.ts +372 -0
  51. package/core/navigate/navigator.ts +437 -0
  52. package/core/navigate/types.ts +132 -0
  53. package/core/navigate/utils.ts +146 -0
  54. package/core/paths/paths.ts +33 -0
  55. package/core/ports/index.ts +271 -0
  56. package/core/segments/CONVENTIONS.md +30 -0
  57. package/core/segments/loop/index.ts +18 -0
  58. package/core/segments/map.ts +56 -0
  59. package/core/segments/navigate/index.ts +20 -0
  60. package/core/segments/versionControl/index.ts +18 -0
  61. package/core/util/logger.ts +128 -0
  62. package/docs/AGENT-TASKS.md +36 -0
  63. package/docs/ARQUITETURA_CORRECAO.md +121 -0
  64. package/docs/CONVENTIONS.md +30 -0
  65. package/docs/CRED.md +112 -0
  66. package/docs/L_RAG.md +567 -0
  67. package/docs/NAVIGATE.md +186 -0
  68. package/docs/PRIMARY_ACTORS.md +78 -0
  69. package/docs/SECONDARY_ACTORS.md +174 -0
  70. package/docs/VERSIONING.md +17 -0
  71. package/docs/boxsafe.config.md +472 -0
  72. package/eslint.config.mts +15 -0
  73. package/main.ts +53 -0
  74. package/memo/generated/codelog.md +13 -0
  75. package/memo/state/tasks/state.json +6 -0
  76. package/memo/state/tasks/tasks/task_001.md +2 -0
  77. package/memo/states-logs/logs.txt +7 -0
  78. package/memo/states-logs/trace-mljvrxvi-9g0k4q.jsonl +11 -0
  79. package/memo/states-logs/trace-mljvvc9j-pe9ekj.jsonl +11 -0
  80. package/memo/states-logs/trace-mljvvm1c-wbnqzp.jsonl +11 -0
  81. package/memo/states-logs/trace-mljxecwn-9xh3nw.jsonl +11 -0
  82. package/memo/states-logs/trace-mljxqkfm-ipijik.jsonl +11 -0
  83. package/memo/states-logs/trace-mljxwtrw-3fanky.jsonl +11 -0
  84. package/memo/states-logs/trace-mljxzen3-m8iinh.jsonl +11 -0
  85. package/memo/states-logs/trace-mljyucef-td6odn.jsonl +11 -0
  86. package/memo/states-logs/trace-mljyuprw-b1a6f4.jsonl +11 -0
  87. package/memo/states-logs/trace-mljyvefl-b6yoce.jsonl +11 -0
  88. package/memo/states-logs/trace-mljyxjo4-n7ibj2.jsonl +13 -0
  89. package/memo/states-logs/trace-mljziez5-8drqtn.jsonl +13 -0
  90. package/memo/states-logs/trace-mljziulp-dtd03z.jsonl +13 -0
  91. package/memo/states-logs/trace-mljzjwrq-1p2krb.jsonl +13 -0
  92. package/memo/states-logs/trace-mljzl0i7-b1cqa6.jsonl +13 -0
  93. package/memo/states-logs/trace-mljzmlk6-7kdyls.jsonl +13 -0
  94. package/memo/states-logs/trace-mlk0oj25-xa3dcu.jsonl +13 -0
  95. package/memo/states-logs/trace-mlk1x59q-713huj.jsonl +14 -0
  96. package/memo/states-logs/trace-mlk22dz8-7fd6hq.jsonl +14 -0
  97. package/memo/states-logs/trace-mlk241uy-wmx907.jsonl +14 -0
  98. package/memo/states-logs/trace-mlk2bf5r-yoh1vg.jsonl +15 -0
  99. package/package.json +44 -0
  100. package/pnpm-workspace.yaml +4 -0
  101. package/prompt_improvement_example.md +55 -0
  102. package/remove.txt +1 -0
  103. package/tests/adapters.test.ts +128 -0
  104. package/tests/extractCode.test.ts +26 -0
  105. package/tests/integration.test.ts +83 -0
  106. package/tests/loadConfig.test.ts +25 -0
  107. package/tests/navigatorBoundary.test.ts +17 -0
  108. package/tests/ports.test.ts +84 -0
  109. package/tests/runAllTests.ts +49 -0
  110. package/tests/toolCalls.test.ts +149 -0
  111. package/tests/waterfall.test.ts +52 -0
  112. package/tsconfig.json +32 -0
  113. package/tsup.config.ts +17 -0
  114. package/types.d.ts +96 -0
  115. package/util/ANSI.ts +29 -0
  116. package/util/extractCode.ts +217 -0
  117. package/util/extractToolCalls.ts +80 -0
  118. package/util/logger.ts +125 -0
@@ -0,0 +1,25 @@
1
+ import { waterfall } from '@core/loop/waterfall';
2
+ import { Logger } from '@core/util/logger';
3
+
4
+ const logger = Logger.createModuleLogger('RunValidation');
5
+
6
+ type RunValidationArgs = {
7
+ execResult: any;
8
+ pathOutput: string;
9
+ signal?: AbortSignal;
10
+ };
11
+
12
+ export async function runValidation({ execResult, pathOutput, signal }: RunValidationArgs): Promise<any> {
13
+ if (signal?.aborted) throw new Error('Aborted');
14
+ try {
15
+ return await waterfall({
16
+ exec: execResult,
17
+ artifacts: {
18
+ outputFile: pathOutput,
19
+ },
20
+ });
21
+ } catch (err: any) {
22
+ logger.error(`Waterfall error: ${err?.message ?? err}`);
23
+ throw err;
24
+ }
25
+ }
@@ -0,0 +1,36 @@
1
+ **Tasks module for BoxSafe — core/loop/tasks**
2
+
3
+ Overview
4
+ - The `TasksManager` provides a minimal, predictable mechanism to load a TODO file,
5
+ split it into small tasks, persist task artifacts and a lightweight runtime state
6
+ under `memo/state/tasks`.
7
+
8
+ Design goals
9
+ - Keep implementation tiny and synchronous-friendly (async IO only).
10
+ - Avoid complex formats: accept either `- item` lines or paragraphs separated by
11
+ blank lines in the TODO file.
12
+ - Persist each task as `memo/state/tasks/tasks/task_###.md` and keep state in
13
+ `memo/state/tasks/state.json` with `current` and `done[]` fields.
14
+
15
+ How it is used
16
+ - The `execLoop` integrates `TasksManager` when `boxsafe.config.json` contains
17
+ `project.todo` pointing to a TODO file. The loop will use the current task as
18
+ the agent prompt, and when the agent completes a task it marks it done and
19
+ moves to the next. Only when all tasks complete does the loop perform the
20
+ normal after-success flow (versioning, notes, etc.).
21
+
22
+ Files
23
+ - `index.ts` — exports `TasksManager` class.
24
+
25
+ Behavior summary
26
+ - `TasksManager.init()` creates `memo/state/tasks` and writes individual task
27
+ files and `state.json` when needed.
28
+ - `getCurrentTask()` returns the current task prompt or `null` when finished.
29
+ - `markCurrentDone()` marks current task as done and advances to the next.
30
+ - `isFinished()` signals completion of all tasks.
31
+
32
+ Notes
33
+ - The module intentionally keeps parsing and behavior simple to be robust and
34
+ easy to audit. If you want more advanced scheduling, priorities or parallel
35
+ execution, we can extend it, but this initial version focuses on correctness
36
+ and minimal surface area.
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import { TASKS_STATE_DIR, TASKS_TASKS_DIR, TASKS_STATE_FILE } from '@core/paths/paths';
5
+
6
+ export class TasksManager {
7
+ todoPath: string;
8
+ baseDir: string;
9
+ tasksDir: string;
10
+ statePath: string;
11
+ tasks: string[] = [];
12
+ state: { current: number; done: boolean[] } = { current: 0, done: [] };
13
+
14
+ constructor(todoPath: string, baseDir?: string) {
15
+ this.todoPath = path.resolve(todoPath);
16
+ this.baseDir = baseDir ? path.resolve(baseDir) : TASKS_STATE_DIR;
17
+ this.tasksDir = baseDir ? path.join(this.baseDir, 'tasks') : TASKS_TASKS_DIR;
18
+ this.statePath = baseDir ? path.join(this.baseDir, 'state.json') : TASKS_STATE_FILE;
19
+ }
20
+
21
+ async init() {
22
+ // ensure directories
23
+ await fs.mkdir(this.tasksDir, { recursive: true });
24
+
25
+ // load or create tasks
26
+ if (fsSync.existsSync(this.statePath)) {
27
+ try {
28
+ const raw = await fs.readFile(this.statePath, 'utf-8');
29
+ this.state = JSON.parse(raw);
30
+ // load tasks files if present
31
+ const files = await fs.readdir(this.tasksDir);
32
+ this.tasks = [];
33
+ for (const f of files.sort()) {
34
+ const content = await fs.readFile(path.join(this.tasksDir, f), 'utf-8');
35
+ this.tasks.push(content);
36
+ }
37
+ } catch (e) {
38
+ // fallback to re-scan todo
39
+ await this._prepareFromTodo();
40
+ }
41
+ } else {
42
+ await this._prepareFromTodo();
43
+ }
44
+ }
45
+
46
+ private async _prepareFromTodo() {
47
+ this.tasks = [];
48
+ this.state = { current: 0, done: [] };
49
+ if (!fsSync.existsSync(this.todoPath)) return;
50
+ const raw = await fs.readFile(this.todoPath, 'utf-8');
51
+ // parse simple list format: lines starting with '-' or split by double-newline
52
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
53
+ let items: string[] = [];
54
+ const dashItems = lines.filter((l) => l.startsWith('- ')).map((l) => l.replace(/^-\s+/, ''));
55
+ if (dashItems.length > 0) items = dashItems;
56
+ else items = raw.split(/\n\s*\n/).map(s=>s.trim()).filter(Boolean);
57
+
58
+ // write tasks files
59
+ for (let i = 0; i < items.length; i++) {
60
+ const filename = `task_${String(i+1).padStart(3,'0')}.md`;
61
+ const item = items[i] ?? '';
62
+ await fs.writeFile(path.join(this.tasksDir, filename), item, 'utf-8');
63
+ this.tasks.push(item);
64
+ this.state.done.push(false);
65
+ }
66
+
67
+ await this._saveState();
68
+ }
69
+
70
+ private async _saveState() {
71
+ await fs.writeFile(this.statePath, JSON.stringify(this.state, null, 2), 'utf-8');
72
+ }
73
+
74
+ getCurrentTask(): string | null {
75
+ if (this.state.current >= this.tasks.length) return null;
76
+ return this.tasks[this.state.current] ?? null;
77
+ }
78
+
79
+ async markCurrentDone() {
80
+ if (this.state.current >= this.tasks.length) return;
81
+ this.state.done[this.state.current] = true;
82
+ // advance to next not-done
83
+ let idx = this.state.current + 1;
84
+ while (idx < this.tasks.length && this.state.done[idx]) idx++;
85
+ this.state.current = idx;
86
+ await this._saveState();
87
+ }
88
+
89
+ isFinished(): boolean {
90
+ return this.state.current >= this.tasks.length;
91
+ }
92
+
93
+ total(): number { return this.tasks.length; }
94
+ }
95
+
96
+ export default TasksManager;
@@ -0,0 +1,168 @@
1
+ import { Logger } from '@core/util/logger';
2
+
3
+ const logger = Logger.createModuleLogger('ToolCalls');
4
+
5
+ export type ToolName = 'navigate' | 'versionControl';
6
+
7
+ export type NavigateOp = 'list' | 'read' | 'write' | 'mkdir' | 'delete' | 'stat';
8
+
9
+ export type NavigateToolCall = {
10
+ tool: 'navigate';
11
+ params: {
12
+ op: NavigateOp;
13
+ path?: string;
14
+ content?: string;
15
+ writeOptions?: { append?: boolean; createDirs?: boolean };
16
+ mkdirOptions?: { recursive?: boolean };
17
+ deleteOptions?: { recursive?: boolean };
18
+ };
19
+ };
20
+
21
+ export type VersionControlToolCall = {
22
+ tool: 'versionControl';
23
+ params: Record<string, unknown>;
24
+ };
25
+
26
+ export type ToolCall = NavigateToolCall | VersionControlToolCall;
27
+
28
+ export type ToolCallParseError = {
29
+ ok: false;
30
+ error: string;
31
+ fence?: string;
32
+ };
33
+
34
+ export type ToolCallParseResult = {
35
+ ok: true;
36
+ calls: ToolCall[];
37
+ errors: ToolCallParseError[];
38
+ };
39
+
40
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
41
+ return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
42
+ }
43
+
44
+ function isParseError(v: ToolCall | ToolCallParseError): v is ToolCallParseError {
45
+ return isPlainObject(v) && (v as any).ok === false;
46
+ }
47
+
48
+ function isNavigateOp(v: unknown): v is NavigateOp {
49
+ return (
50
+ v === 'list' ||
51
+ v === 'read' ||
52
+ v === 'write' ||
53
+ v === 'mkdir' ||
54
+ v === 'delete' ||
55
+ v === 'stat'
56
+ );
57
+ }
58
+
59
+ function parseOneToolCall(obj: unknown): ToolCall | ToolCallParseError {
60
+ if (!isPlainObject(obj)) return { ok: false, error: 'tool call must be an object' };
61
+
62
+ const tool = obj.tool;
63
+ if (tool !== 'navigate' && tool !== 'versionControl') {
64
+ return { ok: false, error: `unknown tool: ${String(tool)}` };
65
+ }
66
+
67
+ const paramsRaw = obj.params;
68
+ const params = isPlainObject(paramsRaw) ? paramsRaw : {};
69
+
70
+ if (tool === 'navigate') {
71
+ const op = params.op;
72
+ if (!isNavigateOp(op)) {
73
+ return { ok: false, error: `navigate.op must be one of list|read|write|mkdir|delete|stat (got: ${String(op)})` };
74
+ }
75
+
76
+ const path = typeof params.path === 'string' ? params.path : undefined;
77
+ const content = typeof params.content === 'string' ? params.content : undefined;
78
+
79
+ if ((op === 'read' || op === 'write' || op === 'mkdir' || op === 'delete' || op === 'stat') && !path) {
80
+ return { ok: false, error: `navigate.op=${op} requires params.path` };
81
+ }
82
+
83
+ if (op === 'write' && content === undefined) {
84
+ return { ok: false, error: `navigate.op=write requires params.content` };
85
+ }
86
+
87
+ const writeOptions = isPlainObject(params.writeOptions) ? params.writeOptions : undefined;
88
+ const mkdirOptions = isPlainObject(params.mkdirOptions) ? params.mkdirOptions : undefined;
89
+ const deleteOptions = isPlainObject(params.deleteOptions) ? params.deleteOptions : undefined;
90
+
91
+ return {
92
+ tool: 'navigate',
93
+ params: {
94
+ op,
95
+ ...(path ? { path } : {}),
96
+ ...(content !== undefined ? { content } : {}),
97
+ ...(writeOptions
98
+ ? {
99
+ writeOptions: {
100
+ ...(typeof writeOptions.append === 'boolean' ? { append: writeOptions.append } : {}),
101
+ ...(typeof writeOptions.createDirs === 'boolean' ? { createDirs: writeOptions.createDirs } : {}),
102
+ },
103
+ }
104
+ : {}),
105
+ ...(mkdirOptions
106
+ ? {
107
+ mkdirOptions: {
108
+ ...(typeof mkdirOptions.recursive === 'boolean' ? { recursive: mkdirOptions.recursive } : {}),
109
+ },
110
+ }
111
+ : {}),
112
+ ...(deleteOptions
113
+ ? {
114
+ deleteOptions: {
115
+ ...(typeof deleteOptions.recursive === 'boolean' ? { recursive: deleteOptions.recursive } : {}),
116
+ },
117
+ }
118
+ : {}),
119
+ },
120
+ };
121
+ }
122
+
123
+ return {
124
+ tool: 'versionControl',
125
+ params,
126
+ };
127
+ }
128
+
129
+ export function parseToolCallsFromMarkdown(markdown: string): ToolCallParseResult {
130
+ const calls: ToolCall[] = [];
131
+ const errors: ToolCallParseError[] = [];
132
+
133
+ logger.debug(`Parsing markdown for tool calls (${markdown.length} chars)`);
134
+
135
+ // FIXED: Changed regex to specifically target json-tool blocks
136
+ // The old regex /```(?:json|json-tool)\s*([\s\S]*?)\s*```/g was capturing the wrong group
137
+ const jsonFenceRe = /```json-tool\s*([\s\S]*?)\s*```/g;
138
+ let m: RegExpExecArray | null;
139
+
140
+ while ((m = jsonFenceRe.exec(markdown)) !== null) {
141
+ const fence = (m[1] ?? '').trim();
142
+ logger.debug(`Found potential tool call: ${fence.substring(0, 100)}...`);
143
+
144
+ try {
145
+ const obj = JSON.parse(fence);
146
+ logger.debug(`JSON parsed successfully: ${JSON.stringify(obj)}`);
147
+
148
+ const parsed = parseOneToolCall(obj);
149
+
150
+ if (isParseError(parsed)) {
151
+ logger.warn(`Invalid tool call: ${parsed.error}`);
152
+ errors.push({ ...parsed, fence });
153
+ } else {
154
+ logger.info(`Valid tool call detected: ${parsed.tool}`);
155
+ calls.push(parsed);
156
+ }
157
+ } catch (e: any) {
158
+ logger.error(`JSON parse failed: ${e?.message ?? String(e)}`);
159
+ errors.push({ ok: false, error: `invalid JSON: ${e?.message ?? String(e)}`, fence });
160
+ }
161
+ }
162
+
163
+ logger.info(`Tool calls parsed: ${calls.length} valid, ${errors.length} errors`);
164
+ return { ok: true, calls, errors };
165
+ }
166
+
167
+ // Export helper functions for reuse
168
+ export { parseOneToolCall, isParseError };
@@ -0,0 +1,146 @@
1
+ import type { Navigator } from '@core/navigate';
2
+ import { parseToolCallsFromMarkdown } from './toolCalls';
3
+ import type { ToolCall } from './toolCalls';
4
+ import type { TraceCtx } from './traceLogger';
5
+ import { Logger } from '@core/util/logger';
6
+
7
+ const logger = Logger.createModuleLogger('ToolDispatcher');
8
+
9
+ type AnsiLike = { Cyan: string; Yellow: string; Red: string; Reset: string };
10
+
11
+ type AttemptVersionControl = (opts: any) => Promise<any>;
12
+
13
+ type TraceEmit = (event: string, ctx?: TraceCtx, data?: Record<string, unknown>) => Promise<void>;
14
+
15
+ type DispatchArgs = {
16
+ markdown: string;
17
+ navigator: Navigator | null;
18
+ boxConfig: any;
19
+ ANSI: AnsiLike;
20
+ attemptVersionControl: AttemptVersionControl;
21
+ vcAutoPushConfig: boolean;
22
+ traceEmit?: TraceEmit;
23
+ traceCtx?: TraceCtx;
24
+ };
25
+
26
+ export async function dispatchToolCalls({
27
+ markdown,
28
+ navigator,
29
+ boxConfig,
30
+ ANSI,
31
+ attemptVersionControl,
32
+ vcAutoPushConfig,
33
+ traceEmit,
34
+ traceCtx,
35
+ }: DispatchArgs): Promise<void> {
36
+ try {
37
+ logger.debug(`Dispatching tool calls from markdown (${markdown.length} chars)`);
38
+ logger.debug(`Navigator available: ${!!navigator}`);
39
+
40
+ const parsed = parseToolCallsFromMarkdown(markdown);
41
+ await traceEmit?.('toolcalls.parsed', traceCtx, {
42
+ calls: parsed.calls.length,
43
+ errors: parsed.errors.length,
44
+ });
45
+
46
+ logger.info(`Tool calls parsed: ${parsed.calls.length} valid, ${parsed.errors.length} errors`);
47
+
48
+ for (const e of parsed.errors) {
49
+ logger.warn(`Invalid tool call: ${e.error}`);
50
+ await traceEmit?.('toolcall.invalid', traceCtx, { error: e.error });
51
+ }
52
+
53
+ if (parsed.calls.length === 0) {
54
+ logger.debug('No valid tool calls to execute');
55
+ return;
56
+ }
57
+
58
+ logger.info(`Executing ${parsed.calls.length} tool calls...`);
59
+ for (const call of parsed.calls) {
60
+ await executeOneToolCall(call);
61
+ }
62
+ } catch (toolErr: any) {
63
+ logger.error(`Tool parsing failed: ${toolErr?.message ?? toolErr}`);
64
+ await traceEmit?.('toolcalls.failed', traceCtx, { error: toolErr?.message ?? String(toolErr) });
65
+ }
66
+
67
+ async function executeOneToolCall(call: ToolCall): Promise<void> {
68
+ logger.debug(`Executing tool call: ${JSON.stringify(call)}`);
69
+ logger.info(`detected tool call: ${call.tool}`);
70
+ await traceEmit?.('toolcall.detected', traceCtx, { tool: call.tool });
71
+
72
+ if (call.tool === 'navigate') {
73
+ logger.debug(`Processing navigate tool call with op: ${call.params.op}`);
74
+
75
+ if (!navigator) {
76
+ logger.warn(`navigate requested but navigator is not initialized`);
77
+ await traceEmit?.('toolcall.navigate.skipped', traceCtx, { reason: 'navigator_not_initialized' });
78
+ return;
79
+ }
80
+
81
+ const { op } = call.params;
82
+ let toolRes: any = null;
83
+
84
+ try {
85
+ if (op === 'write') {
86
+ logger.debug(`Writing file: ${call.params.path}`);
87
+ toolRes = await navigator.writeFile(call.params.path!, call.params.content!, call.params.writeOptions);
88
+ } else if (op === 'mkdir') {
89
+ logger.debug(`Creating directory: ${call.params.path}`);
90
+ toolRes = await navigator.createDirectory(call.params.path!, call.params.mkdirOptions);
91
+ } else if (op === 'delete') {
92
+ logger.debug(`Deleting: ${call.params.path}`);
93
+ toolRes = await navigator.delete(call.params.path!, call.params.deleteOptions);
94
+ } else if (op === 'read') {
95
+ logger.debug(`Reading file: ${call.params.path}`);
96
+ toolRes = await navigator.readFile(call.params.path!);
97
+ } else if (op === 'list') {
98
+ logger.debug(`Listing directory: ${call.params.path ?? '.'}`);
99
+ toolRes = await navigator.listDirectory(call.params.path ?? '.');
100
+ } else if (op === 'stat') {
101
+ logger.debug(`Getting metadata: ${call.params.path}`);
102
+ toolRes = await navigator.getMetadata(call.params.path!);
103
+ } else {
104
+ logger.warn(`unknown navigate op: ${String(op)}`);
105
+ await traceEmit?.('toolcall.navigate.unknown_op', traceCtx, { op: String(op) });
106
+ }
107
+
108
+ logger.info(`Navigate operation ${op} completed successfully`);
109
+ } catch (innerErr: any) {
110
+ logger.error(`navigate execution error: ${innerErr?.message ?? innerErr}`);
111
+ await traceEmit?.('toolcall.navigate.error', traceCtx, { error: innerErr?.message ?? String(innerErr) });
112
+ }
113
+
114
+ logger.debug(`Navigate result: ${JSON.stringify(toolRes ?? { ok: false })}`);
115
+ logger.info(`navigate result: ${JSON.stringify(toolRes ?? { ok: false })}`);
116
+ await traceEmit?.('toolcall.navigate.result', traceCtx, {
117
+ op,
118
+ ok: Boolean(toolRes?.ok),
119
+ });
120
+ return;
121
+ }
122
+
123
+ if (call.tool === 'versionControl') {
124
+ const allowed = Boolean(boxConfig.project?.versionControl?.before) || Boolean(boxConfig.project?.versionControl?.after);
125
+ if (!allowed) {
126
+ logger.warn(`versionControl requested but not authorized in config`);
127
+ await traceEmit?.('toolcall.versionControl.skipped', traceCtx, { reason: 'not_authorized' });
128
+ return;
129
+ }
130
+
131
+ try {
132
+ const params: any = { ...(call.params ?? {}) };
133
+ if (params.autoPush === undefined) params.autoPush = vcAutoPushConfig;
134
+ const vcRes = await attemptVersionControl(params);
135
+ logger.info(`versionControl result: ${JSON.stringify(vcRes ?? { ok: true })}`);
136
+ await traceEmit?.('toolcall.versionControl.result', traceCtx, { ok: true });
137
+ } catch (vcErr: any) {
138
+ logger.error(`versionControl error: ${vcErr?.message ?? vcErr}`);
139
+ await traceEmit?.('toolcall.versionControl.error', traceCtx, { error: vcErr?.message ?? String(vcErr) });
140
+ }
141
+ return;
142
+ }
143
+
144
+ logger.warn(`unhandled tool: ${String((call as any).tool)}`);
145
+ }
146
+ }
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+ import { mkdir, appendFile, readdir, stat, unlink } from 'node:fs/promises';
3
+ import { STATES_LOGS_DIR } from '@core/paths/paths';
4
+ import { Logger } from '@core/util/logger';
5
+
6
+ export type TraceCtx = {
7
+ runId: string;
8
+ iter?: number;
9
+ };
10
+
11
+ export type TraceEvent = {
12
+ ts: string;
13
+ runId: string;
14
+ iter?: number;
15
+ event: string;
16
+ data?: Record<string, unknown>;
17
+ };
18
+
19
+ type TraceOptions = {
20
+ runId: string;
21
+ retain?: number;
22
+ };
23
+
24
+ function safeJsonStringify(v: unknown): string {
25
+ try {
26
+ return JSON.stringify(v);
27
+ } catch {
28
+ return JSON.stringify({ error: 'unstringifiable' });
29
+ }
30
+ }
31
+
32
+ function makeRunId(): string {
33
+ const rand = Math.random().toString(36).slice(2, 8);
34
+ return `${Date.now().toString(36)}-${rand}`;
35
+ }
36
+
37
+ async function applyRetention(retain: number): Promise<void> {
38
+ const entries = await readdir(STATES_LOGS_DIR, { withFileTypes: true }).catch(() => []);
39
+ const files = entries
40
+ .filter((e) => e.isFile() && e.name.startsWith('trace-') && e.name.endsWith('.jsonl'))
41
+ .map((e) => e.name);
42
+
43
+ const withTimes = await Promise.all(
44
+ files.map(async (name) => {
45
+ const p = path.join(STATES_LOGS_DIR, name);
46
+ const s = await stat(p).catch(() => null);
47
+ return { name, mtimeMs: s?.mtimeMs ?? 0 };
48
+ })
49
+ );
50
+
51
+ withTimes.sort((a, b) => b.mtimeMs - a.mtimeMs);
52
+ const toDelete = withTimes.slice(retain);
53
+ await Promise.all(toDelete.map((f) => unlink(path.join(STATES_LOGS_DIR, f.name)).catch(() => null)));
54
+ }
55
+
56
+ export function createTraceLogger(opts?: Partial<TraceOptions>) {
57
+ const runId = opts?.runId ?? process.env.BOXSAFE_RUN_ID ?? makeRunId();
58
+ const retain = typeof opts?.retain === 'number' ? opts.retain : Number(process.env.BOXSAFE_TRACE_RETAIN ?? 20);
59
+ const filePath = path.join(STATES_LOGS_DIR, `trace-${runId}.jsonl`);
60
+
61
+ let initialized = false;
62
+
63
+ async function ensureInit() {
64
+ if (initialized) return;
65
+ await mkdir(STATES_LOGS_DIR, { recursive: true });
66
+ if (Number.isFinite(retain) && retain > 0) {
67
+ await applyRetention(retain);
68
+ }
69
+ initialized = true;
70
+ }
71
+
72
+ async function emit(event: string, ctx?: TraceCtx, data?: Record<string, unknown>) {
73
+ await ensureInit();
74
+ const entry: TraceEvent = {
75
+ ts: new Date().toISOString(),
76
+ runId,
77
+ ...(typeof ctx?.iter === 'number' ? { iter: ctx.iter } : {}),
78
+ event,
79
+ ...(data ? { data } : {}),
80
+ };
81
+ await appendFile(filePath, `${safeJsonStringify(entry)}\n`, 'utf8');
82
+ }
83
+
84
+ function prefix(ctx?: TraceCtx): string {
85
+ const parts = [`run=${runId}`];
86
+ if (typeof ctx?.iter === 'number') parts.push(`iter=${ctx.iter}`);
87
+ return `[${parts.join('][')}]`;
88
+ }
89
+
90
+ function wrapLogger(ctx?: TraceCtx) {
91
+ const logger = Logger.createModuleLogger('Trace');
92
+ const p = prefix(ctx);
93
+ return {
94
+ info: (...a: any[]) => logger.info(`${p} ${a.join(' ')}`),
95
+ warn: (...a: any[]) => logger.warn(`${p} ${a.join(' ')}`),
96
+ error: (...a: any[]) => logger.error(`${p} ${a.join(' ')}`),
97
+ };
98
+ }
99
+
100
+ return {
101
+ runId,
102
+ filePath,
103
+ emit,
104
+ wrapLogger,
105
+ };
106
+ }
@@ -0,0 +1,26 @@
1
+ import type { LModel, LService } from '@ai/label';
2
+ import type { CommandRun } from "../../types";
3
+ import type { Navigator } from '@core/navigate';
4
+
5
+ export interface LoopOptions {
6
+ service: LService;
7
+ model: LModel;
8
+ initialPrompt: string;
9
+ cmd: CommandRun;
10
+ lang: string;
11
+ pathOutput: string;
12
+ maxIterations?: number;
13
+ limit?: number;
14
+ signal?: AbortSignal;
15
+ pathGeneratedMarkdown?: string;
16
+ navigator?: Navigator;
17
+ workspace?: string;
18
+ }
19
+
20
+ export interface LoopResult {
21
+ ok: boolean;
22
+ iterations: number;
23
+ verdict?: unknown;
24
+ artifacts?: { outputFile?: string };
25
+ navigator?: Navigator | null;
26
+ }
@@ -0,0 +1,36 @@
1
+ import { runVersionControl } from '@core/loop/git';
2
+ import { Logger } from '@core/util/logger';
3
+
4
+ const logger = Logger.createModuleLogger('VersionControlAdapter');
5
+
6
+ type AttemptOptions = {
7
+ repoPath: string;
8
+ commitMessage: string;
9
+ autoPush: boolean;
10
+ generateNotes: boolean;
11
+ };
12
+
13
+ export function createVersionControlAttemptRunner() {
14
+ return async function attemptVersionControl(opts: AttemptOptions) {
15
+ const maxAttempts = 3;
16
+ let attempt = 0;
17
+ let lastErr: any = null;
18
+ logger.info(`attempting runVersionControl with opts=${JSON.stringify(opts)}`);
19
+ while (attempt < maxAttempts) {
20
+ attempt++;
21
+ try {
22
+ logger.debug(`attempt ${attempt}`);
23
+ const res = await runVersionControl(opts);
24
+ logger.info(`result (attempt ${attempt}): ${JSON.stringify(res)}`);
25
+ return res;
26
+ } catch (err: any) {
27
+ lastErr = err;
28
+ logger.warn(`attempt ${attempt} failed: ${err?.message ?? err}`);
29
+ const backoff = 300 * Math.pow(2, attempt - 1);
30
+ await new Promise((r) => setTimeout(r, backoff));
31
+ }
32
+ }
33
+ logger.error(`all ${maxAttempts} attempts failed: ${lastErr?.message ?? lastErr}`);
34
+ throw lastErr;
35
+ };
36
+ }