@xn-intenton-z2a/agentic-lib 7.4.13 → 7.4.15

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 (55) hide show
  1. package/.github/agents/agent-apply-fix.md +30 -1
  2. package/.github/agents/agent-director.md +28 -7
  3. package/.github/agents/agent-discussion-bot.md +28 -0
  4. package/.github/agents/agent-implementation-review.md +21 -0
  5. package/.github/agents/agent-issue-resolution.md +32 -0
  6. package/.github/agents/agent-iterate.md +33 -0
  7. package/.github/agents/agent-maintain-features.md +34 -0
  8. package/.github/agents/agent-maintain-library.md +39 -0
  9. package/.github/agents/agent-ready-issue.md +21 -0
  10. package/.github/agents/agent-review-issue.md +16 -0
  11. package/.github/agents/agent-supervisor.md +60 -0
  12. package/.github/workflows/agentic-lib-init.yml +76 -11
  13. package/.github/workflows/agentic-lib-schedule.yml +58 -6
  14. package/.github/workflows/agentic-lib-test.yml +31 -3
  15. package/.github/workflows/agentic-lib-update.yml +20 -0
  16. package/.github/workflows/agentic-lib-workflow.yml +63 -52
  17. package/README.md +23 -12
  18. package/agentic-lib.toml +3 -3
  19. package/bin/agentic-lib.js +34 -4
  20. package/package.json +1 -1
  21. package/src/actions/agentic-step/index.js +51 -34
  22. package/src/actions/agentic-step/logging.js +7 -14
  23. package/src/actions/agentic-step/tasks/direct.js +52 -11
  24. package/src/actions/agentic-step/tasks/maintain-features.js +7 -0
  25. package/src/actions/agentic-step/tasks/maintain-library.js +10 -0
  26. package/src/actions/agentic-step/tasks/supervise.js +14 -6
  27. package/src/actions/agentic-step/tasks/transform.js +37 -1
  28. package/src/actions/commit-if-changed/action.yml +2 -1
  29. package/src/copilot/config.js +3 -3
  30. package/src/copilot/guards.js +5 -5
  31. package/src/copilot/state.js +211 -0
  32. package/src/copilot/telemetry.js +88 -10
  33. package/src/seeds/missions/1-dan-create-c64-emulator.md +13 -13
  34. package/src/seeds/missions/1-dan-create-planning-engine.md +82 -0
  35. package/src/seeds/missions/1-kyu-create-ray-tracer.md +31 -8
  36. package/src/seeds/missions/2-dan-create-self-hosted.md +67 -0
  37. package/src/seeds/missions/2-kyu-create-markdown-compiler.md +48 -0
  38. package/src/seeds/missions/2-kyu-create-plot-code-lib.md +35 -16
  39. package/src/seeds/missions/3-kyu-analyze-lunar-lander.md +13 -14
  40. package/src/seeds/missions/3-kyu-evaluate-time-series-lab.md +22 -28
  41. package/src/seeds/missions/4-kyu-analyze-json-schema-diff.md +46 -2
  42. package/src/seeds/missions/4-kyu-apply-cron-engine.md +16 -18
  43. package/src/seeds/missions/4-kyu-apply-dense-encoding.md +14 -11
  44. package/src/seeds/missions/4-kyu-apply-owl-ontology.md +47 -0
  45. package/src/seeds/missions/5-kyu-apply-ascii-face.md +40 -0
  46. package/src/seeds/missions/5-kyu-apply-string-utils.md +17 -17
  47. package/src/seeds/missions/6-kyu-understand-hamming-distance.md +12 -12
  48. package/src/seeds/missions/6-kyu-understand-roman-numerals.md +12 -12
  49. package/src/seeds/missions/8-kyu-remember-hello-world.md +10 -0
  50. package/src/seeds/zero-MISSION.md +12 -12
  51. package/src/seeds/zero-package.json +1 -1
  52. package/src/seeds/missions/2-dan-create-agi.md +0 -22
  53. package/src/seeds/missions/2-kyu-evaluate-markdown-compiler.md +0 -33
  54. package/src/seeds/missions/3-kyu-evaluate-owl-ontology.md +0 -34
  55. package/src/seeds/missions/5-kyu-create-ascii-face.md +0 -4
@@ -8,16 +8,17 @@
8
8
  import * as core from "@actions/core";
9
9
  import * as github from "@actions/github";
10
10
  import { loadConfig, getWritablePaths } from "./config-loader.js";
11
- import { logActivity, generateClosingNotes, writeAgentLog } from "./logging.js";
12
- import { readFileSync, existsSync } from "fs";
11
+ import { generateClosingNotes, writeAgentLog } from "./logging.js";
12
+ import { readFileSync, existsSync, readdirSync } from "fs";
13
13
  import {
14
14
  buildMissionMetrics, buildMissionReadiness,
15
- computeTransformationCost, readCumulativeCost, buildLimitsStatus,
15
+ computeTransformationCost, buildLimitsStatus,
16
16
  } from "../../copilot/telemetry.js";
17
17
  import {
18
18
  checkInstabilityLabel, countDedicatedTests,
19
19
  countOpenIssues, countResolvedIssues, countMdFiles,
20
20
  } from "./metrics.js";
21
+ import { readState, writeState, updateStateAfterTask } from "../../copilot/state.js";
21
22
 
22
23
  // Task implementations
23
24
  import { resolveIssue } from "./tasks/resolve-issue.js";
@@ -63,9 +64,22 @@ async function run() {
63
64
  if (!handler) throw new Error(`Unknown task: ${task}. Available: ${Object.keys(TASKS).join(", ")}`);
64
65
 
65
66
  // Resolve log and screenshot paths (fetched from agentic-lib-logs branch by workflow)
66
- const logFile = config.intentionBot?.intentionFilepath || "intenti\u00F6n.md";
67
+ const logPrefix = config.intentionBot?.logPrefix || "agent-log-";
67
68
  const screenshotFile = config.intentionBot?.screenshotFile || "SCREENSHOT_INDEX.png";
68
- const logFilePath = existsSync(logFile) ? logFile : null;
69
+ // Find the most recent agent-log file matching the prefix for LLM context
70
+ const logDir = logPrefix.includes("/") ? logPrefix.substring(0, logPrefix.lastIndexOf("/")) : ".";
71
+ const logBase = logPrefix.includes("/") ? logPrefix.substring(logPrefix.lastIndexOf("/") + 1) : logPrefix;
72
+ let logFilePath = null;
73
+ try {
74
+ const logFiles = readdirSync(logDir)
75
+ .filter(f => f.startsWith(logBase) && f.endsWith(".md"))
76
+ .sort();
77
+ if (logFiles.length > 0) {
78
+ const newest = logFiles[logFiles.length - 1];
79
+ const candidate = logDir === "." ? newest : `${logDir}/${newest}`;
80
+ if (existsSync(candidate)) logFilePath = candidate;
81
+ }
82
+ } catch { /* no log files yet */ }
69
83
  const screenshotFilePath = existsSync(screenshotFile) ? screenshotFile : null;
70
84
 
71
85
  const context = {
@@ -79,6 +93,9 @@ async function run() {
79
93
  logFilePath, screenshotFilePath,
80
94
  };
81
95
 
96
+ // C1: Read persistent state from agentic-lib-state.toml
97
+ const state = readState(".");
98
+
82
99
  const startTime = Date.now();
83
100
  const result = await handler(context);
84
101
  const durationMs = Date.now() - startTime;
@@ -96,15 +113,17 @@ async function run() {
96
113
  && await checkInstabilityLabel(context, issueNumber);
97
114
  if (isInstability) core.info(`Issue #${issueNumber} has instability label — does not count against budget`);
98
115
  const transformationCost = computeTransformationCost(task, result.outcome, isInstability);
99
- const intentionFilepath = config.intentionBot?.intentionFilepath;
100
- const cumulativeCost = readCumulativeCost(intentionFilepath) + transformationCost;
101
-
102
- if (result.dedicatedTestCount == null || result.dedicatedTestCount === 0) {
103
- try {
104
- const { scanDirectory: scanDir } = await import("./copilot.js");
105
- result.dedicatedTestCount = countDedicatedTests(scanDir);
106
- } catch { /* ignore */ }
107
- }
116
+
117
+ // C1/C2: Update persistent state with this task's results
118
+ updateStateAfterTask(state, {
119
+ task,
120
+ outcome: result.outcome || "completed",
121
+ transformationCost,
122
+ tokensUsed: result.tokensUsed || 0,
123
+ });
124
+ // C2: Use cumulative cost from persistent state (not just this task)
125
+ const cumulativeCost = state.counters["cumulative-transforms"] || 0;
126
+ state.budget["transformation-budget-cap"] = config.transformationBudget || 0;
108
127
 
109
128
  const { featureIssueCount, maintenanceIssueCount } = await countOpenIssues(context);
110
129
  if (result.resolvedCount == null) result.resolvedCount = await countResolvedIssues(context);
@@ -121,39 +140,37 @@ async function run() {
121
140
  }
122
141
  }
123
142
 
124
- const missionMetrics = buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount);
125
-
126
- if (intentionFilepath) {
127
- logActivity({
128
- filepath: intentionFilepath, task, outcome: result.outcome || "completed",
129
- issueNumber, prNumber: result.prNumber, commitUrl: result.commitUrl,
130
- tokensUsed: result.tokensUsed, inputTokens: result.inputTokens,
131
- outputTokens: result.outputTokens, cost: result.cost, durationMs,
132
- model: result.model || model, details: result.details,
133
- workflowUrl: `${process.env.GITHUB_SERVER_URL}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}`,
134
- profile: config.tuning?.profileName || "unknown",
135
- changes: result.changes, contextNotes: result.contextNotes,
136
- limitsStatus, promptBudget: result.promptBudget,
137
- missionReadiness: buildMissionReadiness(missionMetrics),
138
- missionMetrics, closingNotes: result.closingNotes || generateClosingNotes(limitsStatus),
139
- transformationCost, narrative: result.narrative,
140
- });
141
- }
143
+ // C5: Pass per-task costs for split display
144
+ const taskCosts = {
145
+ transformationCost,
146
+ tokensUsed: result.tokensUsed || 0,
147
+ cumulativeTokens: state.counters["total-tokens"] || 0,
148
+ };
149
+ const missionMetrics = buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount, taskCosts);
142
150
 
143
- // Write standalone agent log file (pushed to agentic-lib-logs branch by workflow)
151
+ // C4: Write standalone agent log file with sequence number
152
+ const seq = state.counters["log-sequence"] || 0;
144
153
  try {
145
154
  const agentLogFile = writeAgentLog({
146
155
  task, outcome: result.outcome || "completed",
147
156
  model: result.model || model, durationMs, tokensUsed: result.tokensUsed,
148
157
  narrative: result.narrative, contextNotes: result.contextNotes,
149
158
  reviewTable: result.reviewTable, completenessAdvice: result.completenessAdvice,
150
- missionMetrics,
159
+ missionMetrics, sequence: seq,
151
160
  });
152
161
  core.info(`Agent log written: ${agentLogFile}`);
153
162
  } catch (err) {
154
163
  core.warning(`Could not write agent log: ${err.message}`);
155
164
  }
156
165
 
166
+ // C1: Write updated state back to agentic-lib-state.toml
167
+ try {
168
+ writeState(".", state);
169
+ core.info(`State written: seq=${seq}, transforms=${cumulativeCost}, nops=${state.counters["cumulative-nop-cycles"]}`);
170
+ } catch (err) {
171
+ core.warning(`Could not write state: ${err.message}`);
172
+ }
173
+
157
174
  core.info(`agentic-step completed: outcome=${result.outcome}`);
158
175
  } catch (error) {
159
176
  core.setFailed(`agentic-step failed: ${error.message}`);
@@ -5,8 +5,8 @@
5
5
  // Appends structured entries to the intentïon.md activity log,
6
6
  // including commit URLs and safety-check outcomes.
7
7
 
8
- import { writeFileSync, readFileSync, appendFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
9
- import { dirname, basename } from "path";
8
+ import { writeFileSync, readFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from "fs";
9
+ import { dirname } from "path";
10
10
  import { join } from "path";
11
11
  import * as core from "@actions/core";
12
12
 
@@ -160,16 +160,6 @@ export function logActivity({
160
160
  writeFileSync(filepath, `# intentïon Activity Log\n${entry}`);
161
161
  }
162
162
 
163
- // Write ASCII fallback copy (intention.md) for systems that can't handle UTF-8 filenames
164
- const name = basename(filepath);
165
- if (name !== "intention.md" && name.includes("intenti")) {
166
- const fallbackPath = join(dirname(filepath), "intention.md");
167
- try {
168
- copyFileSync(filepath, fallbackPath);
169
- } catch {
170
- // Best-effort — don't fail the log if the copy fails
171
- }
172
- }
173
163
  }
174
164
 
175
165
  /**
@@ -192,16 +182,19 @@ export function logActivity({
192
182
  export function writeAgentLog({
193
183
  task, outcome, model, durationMs, narrative,
194
184
  reviewTable, completenessAdvice, contextNotes,
195
- missionMetrics, tokensUsed,
185
+ missionMetrics, tokensUsed, sequence,
196
186
  }) {
197
187
  const now = new Date();
198
188
  const stamp = now.toISOString().replace(/:/g, "-").replace(/\./g, "-");
199
- const filename = `agent-log-${stamp}.md`;
189
+ // C4: Include zero-padded sequence number in filename
190
+ const seq = String(sequence || 0).padStart(3, "0");
191
+ const filename = `agent-log-${stamp}-${seq}.md`;
200
192
 
201
193
  const parts = [
202
194
  `# Agent Log: ${task} at ${now.toISOString()}`,
203
195
  "",
204
196
  "## Summary",
197
+ `**Sequence:** ${seq}`,
205
198
  `**Task:** ${task}`,
206
199
  `**Outcome:** ${outcome}`,
207
200
  ];
@@ -68,7 +68,6 @@ function detectDedicatedTests() {
68
68
  function buildMetricAssessment(ctx, config) {
69
69
  const thresholds = config.missionCompleteThresholds || {};
70
70
  const minResolved = thresholds.minResolvedIssues ?? 3;
71
- const minTests = thresholds.minDedicatedTests ?? 1;
72
71
  const maxTodos = thresholds.maxSourceTodos ?? 0;
73
72
 
74
73
  // Implementation review gaps (passed from workflow via env)
@@ -79,12 +78,13 @@ function buildMetricAssessment(ctx, config) {
79
78
  } catch { /* ignore parse errors */ }
80
79
  const criticalGaps = reviewGaps.filter((g) => g.severity === "critical");
81
80
 
81
+ // C6: Removed "Dedicated tests" metric; using cumulative transforms instead
82
82
  const metrics = [
83
83
  { metric: "Open issues", value: ctx.issuesSummary.length, target: 0, met: ctx.issuesSummary.length === 0 },
84
84
  { metric: "Open PRs", value: ctx.prsSummary.length, target: 0, met: ctx.prsSummary.length === 0 },
85
85
  { metric: "Issues resolved", value: ctx.resolvedCount, target: minResolved, met: ctx.resolvedCount >= minResolved },
86
- { metric: "Dedicated tests", value: ctx.dedicatedTestCount, target: minTests, met: ctx.dedicatedTestCount >= minTests },
87
86
  { metric: "Source TODOs", value: ctx.sourceTodoCount, target: maxTodos, met: ctx.sourceTodoCount <= maxTodos },
87
+ { metric: "Cumulative transforms", value: ctx.cumulativeTransformationCost, target: 1, met: ctx.cumulativeTransformationCost >= 1 },
88
88
  { metric: "Budget", value: ctx.cumulativeTransformationCost, target: ctx.transformationBudget || "unlimited", met: !(ctx.transformationBudget > 0 && ctx.cumulativeTransformationCost >= ctx.transformationBudget) },
89
89
  { metric: "Implementation review", value: criticalGaps.length === 0 ? "No critical gaps" : `${criticalGaps.length} critical gap(s)`, target: "No critical gaps", met: criticalGaps.length === 0 },
90
90
  ];
@@ -129,8 +129,8 @@ function buildPrompt(ctx, agentInstructions, metricAssessment) {
129
129
  `Open issues: ${ctx.issuesSummary.length}`,
130
130
  `Recently closed issues: ${ctx.recentlyClosedSummary.length}`,
131
131
  `Open PRs: ${ctx.prsSummary.length}`,
132
- `Dedicated test files: ${ctx.dedicatedTestCount}`,
133
132
  `Source TODOs: ${ctx.sourceTodoCount}`,
133
+ `Cumulative transforms: ${ctx.cumulativeTransformationCost}`,
134
134
  `Transformation budget: ${ctx.cumulativeTransformationCost}/${ctx.transformationBudget || "unlimited"}`,
135
135
  "",
136
136
  ...(process.env.REVIEW_ADVICE ? [
@@ -216,18 +216,29 @@ async function executeMissionComplete(octokit, repo, reason) {
216
216
  /**
217
217
  * Execute mission-failed: write signal file and commit via Contents API.
218
218
  */
219
- async function executeMissionFailed(octokit, repo, reason) {
219
+ async function executeMissionFailed(octokit, repo, reason, metricAssessment) {
220
+ // C16: Build a detailed reason including specific failed metrics
221
+ const metricDetail = metricAssessment?.notMet?.length > 0
222
+ ? `Failed metrics: ${metricAssessment.notMet.map(m => `${m.metric}=${m.value} (target: ${typeof m.target === "number" ? (m.metric.includes("TODO") ? `<= ${m.target}` : `>= ${m.target}`) : m.target})`).join(", ")}.`
223
+ : reason;
224
+ const metsMet = metricAssessment?.metrics?.filter(m => m.met).map(m => `${m.metric}=${m.value}`) || [];
225
+ const detailedReason = metsMet.length > 0
226
+ ? `${metricDetail} Passing metrics: ${metsMet.join(", ")}.`
227
+ : metricDetail;
228
+
220
229
  const signal = [
221
230
  "# Mission Failed",
222
231
  "",
223
232
  `- **Timestamp:** ${new Date().toISOString()}`,
224
233
  `- **Detected by:** director`,
225
- `- **Reason:** ${reason}`,
234
+ `- **Reason:** ${detailedReason}`,
226
235
  "",
227
236
  "This file was created automatically. To restart, delete this file and run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
228
237
  ].join("\n");
229
238
  writeFileSync("MISSION_FAILED.md", signal);
230
239
 
240
+ // C16: Use detailed commit message
241
+ const commitMsg = `mission-failed: ${metricDetail.substring(0, 200)}`;
231
242
  try {
232
243
  const contentBase64 = Buffer.from(signal).toString("base64");
233
244
  let existingSha;
@@ -238,7 +249,7 @@ async function executeMissionFailed(octokit, repo, reason) {
238
249
  await octokit.rest.repos.createOrUpdateFileContents({
239
250
  ...repo,
240
251
  path: "MISSION_FAILED.md",
241
- message: "mission-failed: " + reason.substring(0, 72),
252
+ message: commitMsg,
242
253
  content: contentBase64,
243
254
  branch: "main",
244
255
  ...(existingSha ? { sha: existingSha } : {}),
@@ -247,6 +258,33 @@ async function executeMissionFailed(octokit, repo, reason) {
247
258
  } catch (err) {
248
259
  core.warning(`Could not commit MISSION_FAILED.md: ${err.message}`);
249
260
  }
261
+
262
+ // C3: Auto-disable schedule on mission-failed
263
+ try {
264
+ const { readState, writeState } = await import("../../../copilot/state.js");
265
+ const state = readState(".");
266
+ state.status["mission-failed"] = true;
267
+ state.status["mission-failed-reason"] = metricDetail.substring(0, 500);
268
+ state.schedule["auto-disabled"] = true;
269
+ state.schedule["auto-disabled-reason"] = "mission-failed";
270
+ writeState(".", state);
271
+ core.info("State updated: mission-failed, schedule auto-disabled");
272
+ } catch (err) {
273
+ core.warning(`Could not update state for mission-failed: ${err.message}`);
274
+ }
275
+
276
+ // C3: Dispatch schedule change to weekly
277
+ try {
278
+ await octokit.rest.actions.createWorkflowDispatch({
279
+ ...repo,
280
+ workflow_id: "agentic-lib-schedule.yml",
281
+ ref: "main",
282
+ inputs: { frequency: "weekly" },
283
+ });
284
+ core.info("Dispatched schedule change to weekly after mission-failed");
285
+ } catch (err) {
286
+ core.warning(`Could not dispatch schedule change: ${err.message}`);
287
+ }
250
288
  }
251
289
 
252
290
  /**
@@ -261,10 +299,13 @@ export async function direct(context) {
261
299
 
262
300
  // --- Gather context (similar to supervisor but focused on metrics) ---
263
301
  const mission = readOptionalFile(config.paths.mission.path);
264
- const intentionLogFull = readOptionalFile(config.intentionBot.intentionFilepath);
265
-
266
- const costMatches = intentionLogFull.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
267
- const cumulativeTransformationCost = [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
302
+ // C2: Read cumulative cost from persistent state
303
+ let cumulativeTransformationCost = 0;
304
+ try {
305
+ const { readState } = await import("../../../copilot/state.js");
306
+ const state = readState(".");
307
+ cumulativeTransformationCost = state.counters["cumulative-transforms"] || 0;
308
+ } catch { /* state not available yet */ }
268
309
 
269
310
  const missionComplete = existsSync("MISSION_COMPLETE.md");
270
311
  const missionFailed = existsSync("MISSION_FAILED.md");
@@ -451,7 +492,7 @@ export async function direct(context) {
451
492
  outcome = "directed";
452
493
  } else if (decision === "mission-failed") {
453
494
  if (process.env.GITHUB_REPOSITORY !== "xn-intenton-z2a/agentic-lib") {
454
- await executeMissionFailed(octokit, repo, reason);
495
+ await executeMissionFailed(octokit, repo, reason, metricAssessment);
455
496
  outcome = "mission-failed";
456
497
  }
457
498
  }
@@ -58,6 +58,8 @@ export async function maintainFeatures(context) {
58
58
  return { outcome: "wip-limit-reached", details: `Maintenance WIP limit reached (${wipCheck.count}/${config.maintenanceIssuesWipLimit})` };
59
59
  }
60
60
 
61
+ const maxTokens = config.maxTokensPerMaintain || 200000;
62
+
61
63
  const mission = readOptionalFile(config.paths.mission.path);
62
64
  const featureLimit = config.paths.features.limit;
63
65
  const featureFiles = buildFileListing(config.paths.features.path, ".md");
@@ -89,6 +91,7 @@ export async function maintainFeatures(context) {
89
91
  "## Constraints",
90
92
  `- Maximum ${featureLimit} feature files`,
91
93
  "- Feature files must be markdown with a descriptive filename (e.g. HTTP_SERVER.md)",
94
+ `- Token budget: ~${maxTokens} tokens. Be concise — avoid verbose explanations or unnecessary tool calls.`,
92
95
  ].join("\n");
93
96
 
94
97
  const systemPrompt =
@@ -103,6 +106,9 @@ export async function maintainFeatures(context) {
103
106
  if (logFilePath) attachments.push({ type: "file", path: logFilePath });
104
107
  if (screenshotFilePath) attachments.push({ type: "file", path: screenshotFilePath });
105
108
 
109
+ // Derive a tool-call cap from the token budget (rough: ~5000 tokens per tool call)
110
+ const maxToolCalls = Math.max(10, Math.floor(maxTokens / 5000));
111
+
106
112
  const result = await runCopilotSession({
107
113
  workspacePath: process.cwd(),
108
114
  model,
@@ -112,6 +118,7 @@ export async function maintainFeatures(context) {
112
118
  writablePaths,
113
119
  createTools,
114
120
  attachments,
121
+ maxToolCalls,
115
122
  excludedTools: ["dispatch_workflow", "close_issue", "label_issue", "post_discussion_comment", "run_tests"],
116
123
  logger: { info: core.info, warning: core.warning, error: core.error, debug: core.debug },
117
124
  });
@@ -51,6 +51,8 @@ export async function maintainLibrary(context) {
51
51
  return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
52
52
  }
53
53
 
54
+ const maxTokens = config.maxTokensPerMaintain || 200000;
55
+
54
56
  const sourcesPath = config.paths.librarySources.path;
55
57
  const sources = readOptionalFile(sourcesPath);
56
58
  const mission = readOptionalFile(config.paths.mission.path);
@@ -83,6 +85,9 @@ export async function maintainLibrary(context) {
83
85
  `Write the URLs as a markdown list in ${sourcesPath}, keeping the existing header text.`,
84
86
  "",
85
87
  formatPathsSection(writablePaths, config.readOnlyPaths, config),
88
+ "",
89
+ "## Constraints",
90
+ `- Token budget: ~${maxTokens} tokens. Be concise — avoid verbose explanations or unnecessary tool calls.`,
86
91
  ].join("\n");
87
92
  } else {
88
93
  prompt = [
@@ -105,6 +110,7 @@ export async function maintainLibrary(context) {
105
110
  "",
106
111
  "## Constraints",
107
112
  `- Maximum ${libraryLimit} library documents`,
113
+ `- Token budget: ~${maxTokens} tokens. Be concise — avoid verbose explanations or unnecessary tool calls.`,
108
114
  ].join("\n");
109
115
  }
110
116
 
@@ -116,6 +122,9 @@ export async function maintainLibrary(context) {
116
122
  if (logFilePath) attachments.push({ type: "file", path: logFilePath });
117
123
  if (screenshotFilePath) attachments.push({ type: "file", path: screenshotFilePath });
118
124
 
125
+ // Derive a tool-call cap from the token budget (rough: ~5000 tokens per tool call)
126
+ const maxToolCalls = Math.max(10, Math.floor(maxTokens / 5000));
127
+
119
128
  const result = await runCopilotSession({
120
129
  workspacePath: process.cwd(),
121
130
  model,
@@ -124,6 +133,7 @@ export async function maintainLibrary(context) {
124
133
  userPrompt: prompt,
125
134
  writablePaths,
126
135
  attachments,
136
+ maxToolCalls,
127
137
  excludedTools: ["dispatch_workflow", "close_issue", "label_issue", "post_discussion_comment", "run_tests"],
128
138
  logger: { info: core.info, warning: core.warning, error: core.error, debug: core.debug },
129
139
  });
@@ -106,12 +106,20 @@ async function postDirectReply(octokit, repo, nodeId, body) {
106
106
 
107
107
  async function gatherContext(octokit, repo, config, t) {
108
108
  const mission = readOptionalFile(config.paths.mission.path);
109
- const intentionLogFull = readOptionalFile(config.intentionBot.intentionFilepath);
110
- const recentActivity = intentionLogFull.split("\n").slice(-20).join("\n");
111
-
112
- // Read cumulative transformation cost from the activity log
113
- const costMatches = intentionLogFull.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
114
- const cumulativeTransformationCost = [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
109
+ // Read recent activity from agent-log files
110
+ let recentActivity = "";
111
+ let cumulativeTransformationCost = 0;
112
+ try {
113
+ const { readdirSync } = await import("fs");
114
+ const logFiles = readdirSync(".").filter(f => f.startsWith("agent-log-") && f.endsWith(".md")).sort();
115
+ const recent = logFiles.slice(-5);
116
+ recentActivity = recent.map(f => readOptionalFile(f)).join("\n---\n").split("\n").slice(-40).join("\n");
117
+ for (const f of logFiles) {
118
+ const content = readOptionalFile(f);
119
+ const costMatches = content.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
120
+ cumulativeTransformationCost += [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
121
+ }
122
+ } catch { /* no agent-log files yet */ }
115
123
 
116
124
  // Check mission-complete signal
117
125
  const missionComplete = existsSync("MISSION_COMPLETE.md");
@@ -6,7 +6,7 @@
6
6
  // instead of having all context front-loaded into the prompt.
7
7
 
8
8
  import * as core from "@actions/core";
9
- import { existsSync, readdirSync, statSync } from "fs";
9
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
10
10
  import { join, resolve } from "path";
11
11
  import { readOptionalFile, formatPathsSection, extractNarrative, NARRATIVE_INSTRUCTION } from "../copilot.js";
12
12
  import { runCopilotSession } from "../../../copilot/copilot-session.js";
@@ -38,6 +38,35 @@ function buildFileListing(dirPath, extensions) {
38
38
  }
39
39
  }
40
40
 
41
+ /**
42
+ * Build a library index: filename + first 2 lines of each library doc, capped at 2000 chars.
43
+ */
44
+ function buildLibraryIndex(libraryPath) {
45
+ if (!libraryPath || !existsSync(libraryPath)) return "";
46
+ try {
47
+ const files = readdirSync(libraryPath).filter((f) => f.endsWith(".md")).sort();
48
+ if (files.length === 0) return "";
49
+ const entries = [];
50
+ let totalLen = 0;
51
+ for (const f of files) {
52
+ const fullPath = join(libraryPath, f);
53
+ try {
54
+ const content = readFileSync(fullPath, "utf8");
55
+ const lines = content.split("\n").slice(0, 2).join(" ").trim();
56
+ const entry = `- ${f}: ${lines}`;
57
+ if (totalLen + entry.length > 2000) break;
58
+ entries.push(entry);
59
+ totalLen += entry.length;
60
+ } catch {
61
+ entries.push(`- ${f}: (unreadable)`);
62
+ }
63
+ }
64
+ return entries.join("\n");
65
+ } catch {
66
+ return "";
67
+ }
68
+ }
69
+
41
70
  /**
42
71
  * Run the full transformation pipeline from mission to code.
43
72
  *
@@ -89,6 +118,7 @@ export async function transform(context) {
89
118
  const testFiles = buildFileListing(config.paths.tests.path, [".js", ".ts"]);
90
119
  const webFiles = buildFileListing(config.paths.web?.path || "src/web/", [".html", ".css", ".js"]);
91
120
  const featureFiles = buildFileListing(config.paths.features.path, [".md"]);
121
+ const libraryIndex = buildLibraryIndex(config.paths.library?.path || "library/");
92
122
 
93
123
  const prompt = [
94
124
  "## Instructions",
@@ -107,6 +137,12 @@ export async function transform(context) {
107
137
  "The website in `src/web/` uses the JS library. `src/web/lib.js` re-exports from `../lib/main.js`.",
108
138
  "When transforming source code, also update the website to use the library's new/changed features.",
109
139
  ] : []),
140
+ ...(libraryIndex ? [
141
+ "",
142
+ "## Library Index",
143
+ "Reference documents available in `library/` (use read_file for full content):",
144
+ libraryIndex,
145
+ ] : []),
110
146
  "",
111
147
  "## Your Task",
112
148
  "Analyze the mission and open issues (use list_issues tool).",
@@ -29,7 +29,8 @@ runs:
29
29
  # Unstage workflow files — GITHUB_TOKEN cannot push workflow changes
30
30
  git reset HEAD -- '.github/workflows/' 2>/dev/null || true
31
31
  # Unstage log/screenshot files — these live on the agentic-lib-logs branch
32
- git reset HEAD -- 'intentïon.md' 'intention.md' 'SCREENSHOT_INDEX.png' 2>/dev/null || true
32
+ git reset HEAD -- 'intentïon.md' 'SCREENSHOT_INDEX.png' 2>/dev/null || true
33
+ git reset HEAD -- agent-log-*.md 2>/dev/null || true
33
34
  if git diff --cached --quiet; then
34
35
  echo "No changes to commit"
35
36
  fi
@@ -240,11 +240,10 @@ export function loadConfig(configPath) {
240
240
  const bot = toml.bot || {};
241
241
 
242
242
  // Mission-complete thresholds (with safe defaults)
243
+ // C6: Removed minDedicatedTests and requireDedicatedTests
243
244
  const mc = toml["mission-complete"] || {};
244
245
  const missionCompleteThresholds = {
245
246
  minResolvedIssues: mc["min-resolved-issues"] ?? 3,
246
- minDedicatedTests: mc["min-dedicated-tests"] ?? 1,
247
- requireDedicatedTests: mc["require-dedicated-tests"] ?? true,
248
247
  maxSourceTodos: mc["max-source-todos"] ?? 0,
249
248
  };
250
249
 
@@ -261,13 +260,14 @@ export function loadConfig(configPath) {
261
260
  transformationBudget: tuning.transformationBudget,
262
261
  seeding: toml.seeding || {},
263
262
  intentionBot: {
264
- intentionFilepath: bot["log-file"] || "intentïon.md",
263
+ logPrefix: bot["log-prefix"] || "agent-log-",
265
264
  logBranch: bot["log-branch"] || "agentic-lib-logs",
266
265
  screenshotFile: bot["screenshot-file"] || "SCREENSHOT_INDEX.png",
267
266
  },
268
267
  init: toml.init || null,
269
268
  tdd: toml.tdd === true,
270
269
  missionCompleteThresholds,
270
+ maxTokensPerMaintain: resolvedLimits.maxTokensPerMaintain || 200000,
271
271
  writablePaths,
272
272
  readOnlyPaths,
273
273
  configToml: rawToml,
@@ -6,10 +6,10 @@
6
6
  // (transform.js, fix-code.js, maintain-features.js, maintain-library.js)
7
7
  // before Phase 4 convergence replaced them with unconditional runCopilotSession().
8
8
 
9
- import { existsSync } from "fs";
9
+ import { existsSync, readdirSync, readFileSync } from "fs";
10
10
  import { resolve } from "path";
11
11
  import { execSync } from "child_process";
12
- import { readCumulativeCost } from "./telemetry.js";
12
+ import { readState } from "./state.js";
13
13
 
14
14
  /**
15
15
  * Task-to-guard mapping. Each task has an ordered list of guards.
@@ -71,9 +71,9 @@ export function checkGuards(taskName, config, workspacePath, { logger } = {}) {
71
71
  case "budget-exhausted": {
72
72
  const budget = config.transformationBudget || 0;
73
73
  if (budget > 0) {
74
- const intentionFilepath = config.intentionBot?.intentionFilepath;
75
- const filePath = intentionFilepath ? resolve(wsPath, intentionFilepath) : null;
76
- const cumulativeCost = readCumulativeCost(filePath);
74
+ // C2: Read cumulative cost from persistent state (agentic-lib-state.toml)
75
+ const state = readState(wsPath);
76
+ const cumulativeCost = state.budget["transformation-budget-used"] || 0;
77
77
  if (cumulativeCost >= budget) {
78
78
  return { skip: true, reason: `Transformation budget exhausted (${cumulativeCost}/${budget})` };
79
79
  }