@xn-intenton-z2a/agentic-lib 7.1.70 → 7.1.72

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.
@@ -13,8 +13,6 @@ name: agentic-lib-init
13
13
  run-name: "agentic-lib-init [${{ github.ref_name }}]"
14
14
 
15
15
  on:
16
- #@dist schedule:
17
- #@dist - cron: "0 5 * * *"
18
16
  workflow_call:
19
17
  inputs:
20
18
  mode:
@@ -856,10 +856,30 @@ function clearDirContents(dirPath, label) {
856
856
  }
857
857
  }
858
858
 
859
- function initReseed() {
859
+ function initReseed(initTimestamp) {
860
860
  console.log("\n--- Reseed: Clear Features + Activity Log ---");
861
861
  removeFile(resolve(target, "intentïon.md"), "intentïon.md");
862
862
  removeFile(resolve(target, "MISSION_COMPLETE.md"), "MISSION_COMPLETE.md");
863
+ removeFile(resolve(target, "MISSION_FAILED.md"), "MISSION_FAILED.md");
864
+
865
+ // Write init epoch header to the activity log
866
+ const pkg = JSON.parse(readFileSync(resolve(pkgRoot, "package.json"), "utf8"));
867
+ const mode = purge ? "purge" : "reseed";
868
+ const initHeader = [
869
+ `# intentïon activity log`,
870
+ "",
871
+ `**init ${mode}** at ${initTimestamp} (agentic-lib@${pkg.version})`,
872
+ `**mission:** ${mission}`,
873
+ "",
874
+ "---",
875
+ "",
876
+ ].join("\n");
877
+ if (!dryRun) {
878
+ writeFileSync(resolve(target, "intentïon.md"), initHeader);
879
+ }
880
+ console.log(" WRITE: intentïon.md (init epoch header)");
881
+ initChanges++;
882
+
863
883
  clearDirContents(resolve(target, "features"), "features");
864
884
 
865
885
  // Clear old features location if it exists
@@ -916,7 +936,7 @@ function clearAndRecreateDir(dirPath, label) {
916
936
  if (!dryRun) mkdirSync(fullPath, { recursive: true });
917
937
  }
918
938
 
919
- function initPurge(seedsDir, missionName) {
939
+ function initPurge(seedsDir, missionName, initTimestamp) {
920
940
  console.log("\n--- Purge: Reset Source Files to Seed State ---");
921
941
 
922
942
  const { sourcePath, testsPath, examplesPath, webPath } = readTomlPaths();
@@ -980,10 +1000,46 @@ function initPurge(seedsDir, missionName) {
980
1000
  }
981
1001
  process.exit(1);
982
1002
  }
1003
+
1004
+ // Write init metadata to agentic-lib.toml
1005
+ const tomlTarget = resolve(target, "agentic-lib.toml");
1006
+ if (existsSync(tomlTarget)) {
1007
+ let toml = readFileSync(tomlTarget, "utf8");
1008
+ const pkg = JSON.parse(readFileSync(resolve(pkgRoot, "package.json"), "utf8"));
1009
+ const initSection = [
1010
+ "",
1011
+ "[init]",
1012
+ `timestamp = "${initTimestamp}"`,
1013
+ `mode = "purge"`,
1014
+ `mission = "${missionName}"`,
1015
+ `version = "${pkg.version}"`,
1016
+ ].join("\n");
1017
+ // Replace existing [init] section or append
1018
+ if (/^\[init\]/m.test(toml)) {
1019
+ toml = toml.replace(/\n?\[init\][^\[]*/, initSection);
1020
+ } else {
1021
+ toml = toml.trimEnd() + "\n" + initSection + "\n";
1022
+ }
1023
+ if (!dryRun) {
1024
+ writeFileSync(tomlTarget, toml);
1025
+ }
1026
+ console.log(" WRITE: [init] section in agentic-lib.toml");
1027
+ initChanges++;
1028
+ }
1029
+ }
1030
+
1031
+ function ghExec(cmd, input) {
1032
+ const opts = { cwd: target, encoding: "utf8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] };
1033
+ if (input) opts.input = input;
1034
+ return execSync(cmd, opts);
1035
+ }
1036
+
1037
+ function ghGraphQL(query) {
1038
+ return JSON.parse(ghExec("gh api graphql --input -", JSON.stringify({ query })));
983
1039
  }
984
1040
 
985
1041
  function initPurgeGitHub() {
986
- console.log("\n--- Purge: Close GitHub Issues + Lock Discussions ---");
1042
+ console.log("\n--- Purge: Clean Slate (Issues, PRs, Runs, Branches, Labels, Discussions) ---");
987
1043
 
988
1044
  // Detect the GitHub repo from git remote
989
1045
  let repoSlug = "";
@@ -993,7 +1049,6 @@ function initPurgeGitHub() {
993
1049
  encoding: "utf8",
994
1050
  timeout: 10000,
995
1051
  }).trim();
996
- // Parse owner/repo from git@github.com:owner/repo.git or https://github.com/owner/repo.git
997
1052
  const match = remoteUrl.match(/github\.com[:/]([^/]+\/[^/.]+)/);
998
1053
  if (match) repoSlug = match[1].replace(/\.git$/, "");
999
1054
  } catch {
@@ -1005,7 +1060,6 @@ function initPurgeGitHub() {
1005
1060
  return;
1006
1061
  }
1007
1062
 
1008
- // Check gh CLI is available
1009
1063
  try {
1010
1064
  execSync("gh --version", { encoding: "utf8", timeout: 5000, stdio: "pipe" });
1011
1065
  } catch {
@@ -1013,29 +1067,205 @@ function initPurgeGitHub() {
1013
1067
  return;
1014
1068
  }
1015
1069
 
1016
- // Close all open issues
1070
+ const [owner, repo] = repoSlug.split("/");
1071
+
1072
+ // ── A1: Close + lock ALL issues (open and closed) ──────────────────
1073
+ console.log("\n --- Issues: close open, lock all ---");
1074
+ try {
1075
+ // Close all open issues with not_planned reason
1076
+ const openIssuesJson = ghExec(`gh api repos/${repoSlug}/issues?state=open&per_page=100`);
1077
+ const openIssues = JSON.parse(openIssuesJson || "[]").filter((i) => !i.pull_request);
1078
+ for (const issue of openIssues) {
1079
+ console.log(` CLOSE: issue #${issue.number} — ${issue.title}`);
1080
+ if (!dryRun) {
1081
+ try {
1082
+ ghExec(
1083
+ `gh api repos/${repoSlug}/issues/${issue.number} -X PATCH -f state=closed -f state_reason=not_planned`,
1084
+ );
1085
+ ghExec(
1086
+ `gh api repos/${repoSlug}/issues/${issue.number}/comments -X POST -f body="Closed by init --purge (mission reset)"`,
1087
+ );
1088
+ initChanges++;
1089
+ } catch (err) {
1090
+ console.log(` WARN: Failed to close issue #${issue.number}: ${err.message}`);
1091
+ }
1092
+ } else {
1093
+ initChanges++;
1094
+ }
1095
+ }
1096
+ if (openIssues.length === 0) console.log(" No open issues to close");
1097
+
1098
+ // Lock ALL issues (open and closed) to prevent bleed
1099
+ const allIssuesJson = ghExec(`gh api repos/${repoSlug}/issues?state=all&per_page=100`);
1100
+ const allIssues = JSON.parse(allIssuesJson || "[]").filter((i) => !i.pull_request && !i.locked);
1101
+ for (const issue of allIssues) {
1102
+ console.log(` LOCK: issue #${issue.number} — ${issue.title}`);
1103
+ if (!dryRun) {
1104
+ try {
1105
+ ghExec(
1106
+ `gh api repos/${repoSlug}/issues/${issue.number}/lock -X PUT -f lock_reason=resolved`,
1107
+ );
1108
+ initChanges++;
1109
+ } catch (err) {
1110
+ console.log(` WARN: Failed to lock issue #${issue.number}: ${err.message}`);
1111
+ }
1112
+ } else {
1113
+ initChanges++;
1114
+ }
1115
+ }
1116
+ if (allIssues.length === 0) console.log(" No unlocked issues to lock");
1117
+ } catch (err) {
1118
+ console.log(` WARN: Issue cleanup failed: ${err.message}`);
1119
+ }
1120
+
1121
+ // ── A2: Close all open PRs + delete branches ──────────────────────
1122
+ console.log("\n --- PRs: close all open ---");
1123
+ try {
1124
+ const prsJson = ghExec(`gh pr list --repo ${repoSlug} --state open --json number,title,headRefName --limit 100`);
1125
+ const prs = JSON.parse(prsJson || "[]");
1126
+ for (const pr of prs) {
1127
+ console.log(` CLOSE: PR #${pr.number} — ${pr.title} (${pr.headRefName})`);
1128
+ if (!dryRun) {
1129
+ try {
1130
+ ghExec(`gh pr close ${pr.number} --repo ${repoSlug} --delete-branch`);
1131
+ initChanges++;
1132
+ } catch (err) {
1133
+ console.log(` WARN: Failed to close PR #${pr.number}: ${err.message}`);
1134
+ }
1135
+ } else {
1136
+ initChanges++;
1137
+ }
1138
+ }
1139
+ if (prs.length === 0) console.log(" No open PRs to close");
1140
+ } catch (err) {
1141
+ console.log(` WARN: PR cleanup failed: ${err.message}`);
1142
+ }
1143
+
1144
+ // ── A3: Delete old workflow runs ──────────────────────────────────
1145
+ console.log("\n --- Workflow runs: delete old ---");
1017
1146
  try {
1018
- const issuesJson = execSync(`gh issue list --repo ${repoSlug} --state open --json number,title --limit 100`, {
1147
+ const runsJson = ghExec(`gh api repos/${repoSlug}/actions/runs?per_page=100`);
1148
+ const runs = JSON.parse(runsJson || '{"workflow_runs":[]}').workflow_runs || [];
1149
+ let deleted = 0;
1150
+ for (const run of runs) {
1151
+ // Skip currently running workflows (in_progress or queued)
1152
+ if (run.status === "in_progress" || run.status === "queued") {
1153
+ console.log(` SKIP: run ${run.id} (${run.name}) — ${run.status}`);
1154
+ continue;
1155
+ }
1156
+ if (!dryRun) {
1157
+ try {
1158
+ ghExec(`gh api repos/${repoSlug}/actions/runs/${run.id} -X DELETE`);
1159
+ deleted++;
1160
+ } catch (err) {
1161
+ console.log(` WARN: Failed to delete run ${run.id}: ${err.message}`);
1162
+ }
1163
+ } else {
1164
+ deleted++;
1165
+ }
1166
+ }
1167
+ console.log(` ${deleted > 0 ? `DELETE: ${deleted} workflow run(s)` : "No workflow runs to delete"}`);
1168
+ initChanges += deleted;
1169
+ } catch (err) {
1170
+ console.log(` WARN: Workflow run cleanup failed: ${err.message}`);
1171
+ }
1172
+
1173
+ // ── A4: Delete stale remote branches ──────────────────────────────
1174
+ console.log("\n --- Branches: delete stale remotes ---");
1175
+ try {
1176
+ const branchesJson = ghExec(`gh api repos/${repoSlug}/branches?per_page=100`);
1177
+ const branches = JSON.parse(branchesJson || "[]");
1178
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
1019
1179
  cwd: target,
1020
1180
  encoding: "utf8",
1021
- timeout: 30000,
1022
- stdio: ["pipe", "pipe", "pipe"],
1023
- });
1024
- const issues = JSON.parse(issuesJson || "[]");
1025
- if (issues.length === 0) {
1026
- console.log(" No open issues to close");
1027
- } else {
1028
- for (const issue of issues) {
1029
- console.log(` CLOSE: issue #${issue.number} — ${issue.title}`);
1181
+ timeout: 5000,
1182
+ }).trim();
1183
+ const keepBranches = new Set(["main", "master", "template", "gh-pages", currentBranch]);
1184
+ let deletedBranches = 0;
1185
+ for (const branch of branches) {
1186
+ if (keepBranches.has(branch.name)) continue;
1187
+ if (branch.protected) continue;
1188
+ console.log(` DELETE: branch ${branch.name}`);
1189
+ if (!dryRun) {
1190
+ try {
1191
+ ghExec(`gh api repos/${repoSlug}/git/refs/heads/${branch.name} -X DELETE`);
1192
+ deletedBranches++;
1193
+ } catch (err) {
1194
+ console.log(` WARN: Failed to delete branch ${branch.name}: ${err.message}`);
1195
+ }
1196
+ } else {
1197
+ deletedBranches++;
1198
+ }
1199
+ }
1200
+ if (deletedBranches === 0) console.log(" No stale branches to delete");
1201
+ initChanges += deletedBranches;
1202
+ } catch (err) {
1203
+ console.log(` WARN: Branch cleanup failed: ${err.message}`);
1204
+ }
1205
+
1206
+ // ── A5: Reset labels ──────────────────────────────────────────────
1207
+ console.log("\n --- Labels: reset to pipeline defaults ---");
1208
+ const GITHUB_DEFAULT_LABELS = new Set([
1209
+ "bug",
1210
+ "documentation",
1211
+ "duplicate",
1212
+ "enhancement",
1213
+ "good first issue",
1214
+ "help wanted",
1215
+ "invalid",
1216
+ "question",
1217
+ "wontfix",
1218
+ ]);
1219
+ const PIPELINE_LABELS = [
1220
+ { name: "automated", color: "0e8a16", description: "Created by the autonomous pipeline" },
1221
+ { name: "ready", color: "0075ca", description: "Issue is ready for transformation" },
1222
+ { name: "in-progress", color: "e4e669", description: "Work in progress" },
1223
+ { name: "merged", color: "6f42c1", description: "Associated PR has been merged" },
1224
+ { name: "automerge", color: "1d76db", description: "PR should be auto-merged when checks pass" },
1225
+ ];
1226
+ try {
1227
+ const labelsJson = ghExec(`gh api repos/${repoSlug}/labels?per_page=100`);
1228
+ const labels = JSON.parse(labelsJson || "[]");
1229
+ const pipelineNames = new Set(PIPELINE_LABELS.map((l) => l.name));
1230
+ // Delete non-default, non-pipeline labels
1231
+ for (const label of labels) {
1232
+ if (GITHUB_DEFAULT_LABELS.has(label.name)) continue;
1233
+ if (pipelineNames.has(label.name)) continue;
1234
+ console.log(` DELETE: label "${label.name}"`);
1235
+ if (!dryRun) {
1236
+ try {
1237
+ ghExec(`gh api repos/${repoSlug}/labels/${encodeURIComponent(label.name)} -X DELETE`);
1238
+ initChanges++;
1239
+ } catch (err) {
1240
+ console.log(` WARN: Failed to delete label "${label.name}": ${err.message}`);
1241
+ }
1242
+ } else {
1243
+ initChanges++;
1244
+ }
1245
+ }
1246
+ // Ensure pipeline labels exist with correct config
1247
+ const existingNames = new Set(labels.map((l) => l.name));
1248
+ for (const pl of PIPELINE_LABELS) {
1249
+ if (existingNames.has(pl.name)) {
1250
+ // Update to ensure correct color/description
1030
1251
  if (!dryRun) {
1031
1252
  try {
1032
- execSync(
1033
- `gh issue close ${issue.number} --repo ${repoSlug} --comment "Closed by init --purge (mission reset)"`,
1034
- { cwd: target, encoding: "utf8", timeout: 15000, stdio: "pipe" },
1253
+ ghExec(
1254
+ `gh api repos/${repoSlug}/labels/${encodeURIComponent(pl.name)} -X PATCH -f color=${pl.color} -f description="${pl.description}"`,
1255
+ );
1256
+ } catch { /* ignore */ }
1257
+ }
1258
+ console.log(` UPDATE: label "${pl.name}"`);
1259
+ } else {
1260
+ console.log(` CREATE: label "${pl.name}"`);
1261
+ if (!dryRun) {
1262
+ try {
1263
+ ghExec(
1264
+ `gh api repos/${repoSlug}/labels -X POST -f name="${pl.name}" -f color=${pl.color} -f description="${pl.description}"`,
1035
1265
  );
1036
1266
  initChanges++;
1037
1267
  } catch (err) {
1038
- console.log(` WARN: Failed to close issue #${issue.number}: ${err.message}`);
1268
+ console.log(` WARN: Failed to create label "${pl.name}": ${err.message}`);
1039
1269
  }
1040
1270
  } else {
1041
1271
  initChanges++;
@@ -1043,23 +1273,15 @@ function initPurgeGitHub() {
1043
1273
  }
1044
1274
  }
1045
1275
  } catch (err) {
1046
- console.log(` WARN: Could not list issues: ${err.message}`);
1276
+ console.log(` WARN: Label cleanup failed: ${err.message}`);
1047
1277
  }
1048
1278
 
1049
- // Close open discussions
1050
- const [owner, repo] = repoSlug.split("/");
1279
+ // ── Discussions: close all open, create fresh one ─────────────────
1280
+ console.log("\n --- Discussions: close open, create fresh ---");
1051
1281
  try {
1052
- const query = JSON.stringify({
1053
- query: `{ repository(owner:"${owner}", name:"${repo}") { discussions(first:50, states:OPEN) { nodes { id number title } } } }`,
1054
- });
1055
- const result = execSync(`gh api graphql --input -`, {
1056
- cwd: target,
1057
- encoding: "utf8",
1058
- timeout: 30000,
1059
- input: query,
1060
- stdio: ["pipe", "pipe", "pipe"],
1061
- });
1062
- const parsed = JSON.parse(result);
1282
+ const parsed = ghGraphQL(
1283
+ `{ repository(owner:"${owner}", name:"${repo}") { discussions(first:50, states:OPEN) { nodes { id number title } } } }`,
1284
+ );
1063
1285
  const discussions = parsed?.data?.repository?.discussions?.nodes || [];
1064
1286
  if (discussions.length === 0) {
1065
1287
  console.log(" No open discussions to close");
@@ -1068,16 +1290,7 @@ function initPurgeGitHub() {
1068
1290
  console.log(` CLOSE: discussion #${disc.number} — ${disc.title}`);
1069
1291
  if (!dryRun) {
1070
1292
  try {
1071
- const mutation = JSON.stringify({
1072
- query: `mutation { closeDiscussion(input: { discussionId: "${disc.id}" }) { discussion { number } } }`,
1073
- });
1074
- execSync(`gh api graphql --input -`, {
1075
- cwd: target,
1076
- encoding: "utf8",
1077
- timeout: 15000,
1078
- input: mutation,
1079
- stdio: ["pipe", "pipe", "pipe"],
1080
- });
1293
+ ghGraphQL(`mutation { closeDiscussion(input: { discussionId: "${disc.id}" }) { discussion { number } } }`);
1081
1294
  initChanges++;
1082
1295
  } catch {
1083
1296
  console.log(` SKIP: Could not close discussion #${disc.number} (may need admin permissions)`);
@@ -1093,18 +1306,9 @@ function initPurgeGitHub() {
1093
1306
 
1094
1307
  // Create a new "Talk to the repository" discussion
1095
1308
  try {
1096
- // Get the repository node ID and "General" category ID
1097
- const repoQuery = JSON.stringify({
1098
- query: `{ repository(owner:"${owner}", name:"${repo}") { id discussionCategories(first:20) { nodes { id name } } } }`,
1099
- });
1100
- const repoResult = execSync(`gh api graphql --input -`, {
1101
- cwd: target,
1102
- encoding: "utf8",
1103
- timeout: 30000,
1104
- input: repoQuery,
1105
- stdio: ["pipe", "pipe", "pipe"],
1106
- });
1107
- const repoParsed = JSON.parse(repoResult);
1309
+ const repoParsed = ghGraphQL(
1310
+ `{ repository(owner:"${owner}", name:"${repo}") { id discussionCategories(first:20) { nodes { id name } } } }`,
1311
+ );
1108
1312
  const repoId = repoParsed?.data?.repository?.id;
1109
1313
  const categories = repoParsed?.data?.repository?.discussionCategories?.nodes || [];
1110
1314
  const generalCat = categories.find((c) => c.name === "General");
@@ -1113,17 +1317,9 @@ function initPurgeGitHub() {
1113
1317
  } else {
1114
1318
  console.log(' CREATE: discussion "Talk to the repository" in General category');
1115
1319
  if (!dryRun) {
1116
- const createMutation = JSON.stringify({
1117
- query: `mutation { createDiscussion(input: { repositoryId: "${repoId}", categoryId: "${generalCat.id}", title: "Talk to the repository", body: "This discussion is the main channel for interacting with the repository's autonomous agents.\\n\\nUse this thread to:\\n- Submit feature requests or ideas\\n- Ask questions about the project\\n- Chat with the discussions bot\\n\\n---\\n*Created by init --purge*" }) { discussion { number url } } }`,
1118
- });
1119
- const createResult = execSync(`gh api graphql --input -`, {
1120
- cwd: target,
1121
- encoding: "utf8",
1122
- timeout: 15000,
1123
- input: createMutation,
1124
- stdio: ["pipe", "pipe", "pipe"],
1125
- });
1126
- const createParsed = JSON.parse(createResult);
1320
+ const createParsed = ghGraphQL(
1321
+ `mutation { createDiscussion(input: { repositoryId: "${repoId}", categoryId: "${generalCat.id}", title: "Talk to the repository", body: "This discussion is the main channel for interacting with the repository's autonomous agents.\\n\\nUse this thread to:\\n- Submit feature requests or ideas\\n- Ask questions about the project\\n- Chat with the discussions bot\\n\\n---\\n*Created by init --purge*" }) { discussion { number url } } }`,
1322
+ );
1127
1323
  const newDisc = createParsed?.data?.createDiscussion?.discussion;
1128
1324
  if (newDisc) {
1129
1325
  console.log(` CREATED: discussion #${newDisc.number} — ${newDisc.url}`);
@@ -1137,7 +1333,7 @@ function initPurgeGitHub() {
1137
1333
  console.log(` SKIP: Could not create discussion (${err.message})`);
1138
1334
  }
1139
1335
 
1140
- // Enable GitHub Pages (serve from docs/ on main branch)
1336
+ // ── Enable GitHub Pages ───────────────────────────────────────────
1141
1337
  console.log("\n--- Enable GitHub Pages ---");
1142
1338
  try {
1143
1339
  if (!dryRun) {
@@ -1187,14 +1383,31 @@ function runInit() {
1187
1383
  console.log(`Mode: ${dryRun ? "DRY RUN" : "LIVE"}`);
1188
1384
  console.log("");
1189
1385
 
1386
+ // Capture existing init timestamp before any destructive operations (for idempotency).
1387
+ // The TOML [init] section is the authoritative record. If it matches the current
1388
+ // mode/mission/version, reuse its timestamp so that re-running init is a no-op.
1389
+ const pkg = JSON.parse(readFileSync(resolve(pkgRoot, "package.json"), "utf8"));
1390
+ let initTimestamp = null;
1391
+ const tomlPath = resolve(target, "agentic-lib.toml");
1392
+ if (existsSync(tomlPath)) {
1393
+ const tomlContent = readFileSync(tomlPath, "utf8");
1394
+ const tm = tomlContent.match(/^\[init\]\s*\ntimestamp\s*=\s*"([^"]+)"\s*\nmode\s*=\s*"([^"]+)"\s*\nmission\s*=\s*"([^"]+)"\s*\nversion\s*=\s*"([^"]+)"/m);
1395
+ const mode = purge ? "purge" : reseed ? "reseed" : null;
1396
+ if (tm && mode && tm[2] === mode && tm[3] === mission && tm[4] === pkg.version) {
1397
+ initTimestamp = tm[1];
1398
+ }
1399
+ }
1400
+ // Use a single timestamp for the entire init run (for consistency across files)
1401
+ if (!initTimestamp) initTimestamp = new Date().toISOString();
1402
+
1190
1403
  initWorkflows();
1191
1404
  initActions(agenticDir);
1192
1405
  initDirContents("agents", resolve(agenticDir, "agents"), "Agents");
1193
1406
  initDirContents("seeds", resolve(agenticDir, "seeds"), "Seeds");
1194
1407
  initScripts(agenticDir);
1195
1408
  initConfig(seedsDir);
1196
- if (reseed) initReseed();
1197
- if (purge) initPurge(seedsDir, mission);
1409
+ if (reseed) initReseed(initTimestamp);
1410
+ if (purge) initPurge(seedsDir, mission, initTimestamp);
1198
1411
  if (purge) initPurgeGitHub();
1199
1412
 
1200
1413
  console.log(`\n${initChanges} change(s)${dryRun ? " (dry run)" : ""}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.1.70",
3
+ "version": "7.1.72",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -271,6 +271,7 @@ export function loadConfig(configPath) {
271
271
  intentionBot: {
272
272
  intentionFilepath: bot["log-file"] || "intentïon.md",
273
273
  },
274
+ init: toml.init || null,
274
275
  tdd: toml.tdd === true,
275
276
  writablePaths,
276
277
  readOnlyPaths,
@@ -9,7 +9,7 @@ import * as core from "@actions/core";
9
9
  import * as github from "@actions/github";
10
10
  import { loadConfig, getWritablePaths } from "./config-loader.js";
11
11
  import { logActivity, generateClosingNotes } from "./logging.js";
12
- import { readFileSync } from "fs";
12
+ import { readFileSync, existsSync, readdirSync } from "fs";
13
13
 
14
14
  // Task implementations
15
15
  import { resolveIssue } from "./tasks/resolve-issue.js";
@@ -99,15 +99,63 @@ async function run() {
99
99
  if (result.action) core.setOutput("action", result.action);
100
100
  if (result.actionArg) core.setOutput("action-arg", result.actionArg);
101
101
 
102
- // Compute limits status for enriched logging
102
+ const profileName = config.tuning?.profileName || "unknown";
103
+
104
+ // Transformation cost: 1 for code-changing tasks, 0 otherwise
105
+ const COST_TASKS = ["transform", "fix-code", "maintain-features", "maintain-library"];
106
+ const isNop = result.outcome === "nop" || result.outcome === "error";
107
+ const transformationCost = COST_TASKS.includes(task) && !isNop ? 1 : 0;
108
+
109
+ // Read cumulative transformation cost from the activity log
110
+ const intentionFilepath = config.intentionBot?.intentionFilepath;
111
+ let cumulativeCost = 0;
112
+ if (intentionFilepath && existsSync(intentionFilepath)) {
113
+ const logContent = readFileSync(intentionFilepath, "utf8");
114
+ const costMatches = logContent.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
115
+ cumulativeCost = [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
116
+ }
117
+ cumulativeCost += transformationCost;
118
+
119
+ // Count features and library docs on disk
120
+ const featuresPath = config.paths?.features?.path;
121
+ const featuresUsed = featuresPath && existsSync(featuresPath)
122
+ ? readdirSync(featuresPath).filter((f) => f.endsWith(".md")).length
123
+ : 0;
124
+ const libraryPath = config.paths?.library?.path;
125
+ const libraryUsed = libraryPath && existsSync(libraryPath)
126
+ ? readdirSync(libraryPath).filter((f) => f.endsWith(".md")).length
127
+ : 0;
128
+
129
+ // Count open automated issues (feature vs maintenance)
130
+ let featureIssueCount = 0;
131
+ let maintenanceIssueCount = 0;
132
+ try {
133
+ const { data: openAutoIssues } = await context.octokit.rest.issues.listForRepo({
134
+ ...context.repo,
135
+ state: "open",
136
+ labels: "automated",
137
+ per_page: 50,
138
+ });
139
+ for (const oi of openAutoIssues.filter((i) => !i.pull_request)) {
140
+ const lbls = oi.labels.map((l) => l.name);
141
+ if (lbls.includes("maintenance")) maintenanceIssueCount++;
142
+ else featureIssueCount++;
143
+ }
144
+ } catch (_) { /* API not available */ }
145
+
146
+ const budgetCap = config.transformationBudget || 0;
147
+ const featCap = config.paths?.features?.limit || 4;
148
+ const libCap = config.paths?.library?.limit || 32;
149
+
150
+ // Compute limits status with actual values
103
151
  const limitsStatus = [
104
- { name: "transformation-budget", valueNum: 0, capacityNum: config.transformationBudget || 0, value: `0/${config.transformationBudget || 0}`, remaining: `${config.transformationBudget || 0}`, status: "" },
105
- { name: "max-feature-issues", valueNum: 0, capacityNum: config.featureDevelopmentIssuesWipLimit, value: `?/${config.featureDevelopmentIssuesWipLimit}`, remaining: "?", status: "" },
106
- { name: "max-maintenance-issues", valueNum: 0, capacityNum: config.maintenanceIssuesWipLimit, value: `?/${config.maintenanceIssuesWipLimit}`, remaining: "?", status: "" },
152
+ { name: "transformation-budget", valueNum: cumulativeCost, capacityNum: budgetCap, value: `${cumulativeCost}/${budgetCap}`, remaining: `${Math.max(0, budgetCap - cumulativeCost)}`, status: cumulativeCost >= budgetCap && budgetCap > 0 ? "EXHAUSTED" : "" },
153
+ { name: "max-feature-issues", valueNum: featureIssueCount, capacityNum: config.featureDevelopmentIssuesWipLimit, value: `${featureIssueCount}/${config.featureDevelopmentIssuesWipLimit}`, remaining: `${Math.max(0, config.featureDevelopmentIssuesWipLimit - featureIssueCount)}`, status: "" },
154
+ { name: "max-maintenance-issues", valueNum: maintenanceIssueCount, capacityNum: config.maintenanceIssuesWipLimit, value: `${maintenanceIssueCount}/${config.maintenanceIssuesWipLimit}`, remaining: `${Math.max(0, config.maintenanceIssuesWipLimit - maintenanceIssueCount)}`, status: "" },
107
155
  { name: "max-attempts-per-issue", valueNum: 0, capacityNum: config.attemptsPerIssue, value: `?/${config.attemptsPerIssue}`, remaining: "?", status: task === "resolve-issue" ? "" : "n/a" },
108
156
  { name: "max-attempts-per-branch", valueNum: 0, capacityNum: config.attemptsPerBranch, value: `?/${config.attemptsPerBranch}`, remaining: "?", status: task === "fix-code" ? "" : "n/a" },
109
- { name: "features", valueNum: 0, capacityNum: config.paths?.features?.limit || 4, value: `?/${config.paths?.features?.limit || 4}`, remaining: "?", status: ["maintain-features", "transform"].includes(task) ? "" : "n/a" },
110
- { name: "library", valueNum: 0, capacityNum: config.paths?.library?.limit || 32, value: `?/${config.paths?.library?.limit || 32}`, remaining: "?", status: task === "maintain-library" ? "" : "n/a" },
157
+ { name: "features", valueNum: featuresUsed, capacityNum: featCap, value: `${featuresUsed}/${featCap}`, remaining: `${Math.max(0, featCap - featuresUsed)}`, status: ["maintain-features", "transform"].includes(task) ? "" : "n/a" },
158
+ { name: "library", valueNum: libraryUsed, capacityNum: libCap, value: `${libraryUsed}/${libCap}`, remaining: `${Math.max(0, libCap - libraryUsed)}`, status: task === "maintain-library" ? "" : "n/a" },
111
159
  ];
112
160
 
113
161
  // Merge task-reported limits if available
@@ -119,15 +167,8 @@ async function run() {
119
167
  }
120
168
 
121
169
  const closingNotes = result.closingNotes || generateClosingNotes(limitsStatus);
122
- const profileName = config.tuning?.profileName || "unknown";
123
-
124
- // Transformation cost: 1 for code-changing tasks, 0 otherwise
125
- const COST_TASKS = ["transform", "fix-code", "maintain-features", "maintain-library"];
126
- const isNop = result.outcome === "nop" || result.outcome === "error";
127
- const transformationCost = COST_TASKS.includes(task) && !isNop ? 1 : 0;
128
170
 
129
171
  // Log to intentïon.md (commit-if-changed excludes this on non-default branches)
130
- const intentionFilepath = config.intentionBot?.intentionFilepath;
131
172
  if (intentionFilepath) {
132
173
  logActivity({
133
174
  filepath: intentionFilepath,
@@ -53,7 +53,7 @@ async function fetchDiscussion(octokit, discussionUrl, commentsLimit = 10) {
53
53
  }
54
54
  }
55
55
 
56
- function buildPrompt(discussionUrl, discussion, context, t) {
56
+ function buildPrompt(discussionUrl, discussion, context, t, repoContext) {
57
57
  const { config, instructions } = context;
58
58
  const { title, body, comments } = discussion;
59
59
 
@@ -99,6 +99,28 @@ function buildPrompt(discussionUrl, discussion, context, t) {
99
99
  `### Mission\n${mission}`,
100
100
  contributing ? `### Contributing\n${contributing}` : "",
101
101
  `### Current Features\n${featureNames.join(", ") || "none"}`,
102
+ );
103
+
104
+ // Add issue context
105
+ if (repoContext?.issuesSummary?.length > 0) {
106
+ parts.push(`### Open Issues (${repoContext.issuesSummary.length})`, repoContext.issuesSummary.join("\n"));
107
+ }
108
+
109
+ // Add actions-since-init context
110
+ if (repoContext?.actionsSinceInit?.length > 0) {
111
+ parts.push(
112
+ `### Actions Since Last Init${repoContext.initTimestamp ? ` (${repoContext.initTimestamp})` : ""}`,
113
+ ...repoContext.actionsSinceInit.map((a) => {
114
+ let line = `- ${a.name}: ${a.conclusion} (${a.created}) [${a.commitSha}] ${a.commitMessage}`;
115
+ if (a.prNumber) {
116
+ line += ` — PR #${a.prNumber}: +${a.additions}/-${a.deletions} in ${a.changedFiles} file(s)`;
117
+ }
118
+ return line;
119
+ }),
120
+ );
121
+ }
122
+
123
+ parts.push(
102
124
  recentActivity ? `### Recent Activity\n${recentActivity}` : "",
103
125
  config.configToml ? `### Configuration (agentic-lib.toml)\n\`\`\`toml\n${config.configToml}\n\`\`\`` : "",
104
126
  config.packageJson ? `### Dependencies (package.json)\n\`\`\`json\n${config.packageJson}\n\`\`\`` : "",
@@ -151,15 +173,72 @@ async function postReply(octokit, nodeId, replyBody) {
151
173
  * @returns {Promise<Object>} Result with outcome, action, tokensUsed, model
152
174
  */
153
175
  export async function discussions(context) {
154
- const { octokit, model, discussionUrl } = context;
155
- const t = context.config?.tuning || {};
176
+ const { octokit, model, discussionUrl, repo, config } = context;
177
+ const t = config?.tuning || {};
156
178
 
157
179
  if (!discussionUrl) {
158
180
  throw new Error("discussions task requires discussion-url input");
159
181
  }
160
182
 
183
+ // Gather repo context: issues + actions since init
184
+ const repoContext = { issuesSummary: [], actionsSinceInit: [], initTimestamp: null };
185
+ if (octokit && repo) {
186
+ try {
187
+ const { data: openIssues } = await octokit.rest.issues.listForRepo({
188
+ ...repo, state: "open", per_page: 10, sort: "created", direction: "asc",
189
+ });
190
+ repoContext.issuesSummary = openIssues
191
+ .filter((i) => !i.pull_request)
192
+ .map((i) => {
193
+ const labels = i.labels.map((l) => l.name).join(", ");
194
+ return `#${i.number}: ${i.title} [${labels || "no labels"}]`;
195
+ });
196
+ } catch (err) {
197
+ core.warning(`Could not fetch issues for discussion context: ${err.message}`);
198
+ }
199
+
200
+ const initTimestamp = config?.init?.timestamp || null;
201
+ repoContext.initTimestamp = initTimestamp;
202
+ try {
203
+ const { data: runs } = await octokit.rest.actions.listWorkflowRunsForRepo({
204
+ ...repo, per_page: 20,
205
+ });
206
+ const initDate = initTimestamp ? new Date(initTimestamp) : null;
207
+ const relevantRuns = initDate
208
+ ? runs.workflow_runs.filter((r) => new Date(r.created_at) >= initDate)
209
+ : runs.workflow_runs.slice(0, 10);
210
+
211
+ for (const run of relevantRuns) {
212
+ const commit = run.head_commit;
213
+ const entry = {
214
+ name: run.name,
215
+ conclusion: run.conclusion || run.status,
216
+ created: run.created_at,
217
+ commitMessage: commit?.message?.split("\n")[0] || "",
218
+ commitSha: run.head_sha?.substring(0, 7) || "",
219
+ };
220
+ if (run.head_branch?.startsWith("agentic-lib-issue-")) {
221
+ try {
222
+ const { data: prs } = await octokit.rest.pulls.list({
223
+ ...repo, head: `${repo.owner}:${run.head_branch}`, state: "all", per_page: 1,
224
+ });
225
+ if (prs.length > 0) {
226
+ entry.prNumber = prs[0].number;
227
+ entry.additions = prs[0].additions;
228
+ entry.deletions = prs[0].deletions;
229
+ entry.changedFiles = prs[0].changed_files;
230
+ }
231
+ } catch { /* ignore */ }
232
+ }
233
+ repoContext.actionsSinceInit.push(entry);
234
+ }
235
+ } catch (err) {
236
+ core.warning(`Could not fetch workflow runs for discussion context: ${err.message}`);
237
+ }
238
+ }
239
+
161
240
  const discussion = await fetchDiscussion(octokit, discussionUrl, t.discussionComments || 10);
162
- const prompt = buildPrompt(discussionUrl, discussion, context, t);
241
+ const prompt = buildPrompt(discussionUrl, discussion, context, t, repoContext);
163
242
  const { content, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
164
243
  model,
165
244
  systemMessage:
@@ -191,6 +270,61 @@ export async function discussions(context) {
191
270
  core.info("Mission complete signal written (MISSION_COMPLETE.md)");
192
271
  }
193
272
 
273
+ // Create issue when bot requests it
274
+ if (action === "create-issue" && actionArg) {
275
+ try {
276
+ const { data: issue } = await octokit.rest.issues.create({
277
+ ...context.repo,
278
+ title: actionArg,
279
+ labels: ["automated", "enhancement"],
280
+ });
281
+ core.info(`Created issue #${issue.number}: ${actionArg}`);
282
+ } catch (err) {
283
+ core.warning(`Failed to create issue: ${err.message}`);
284
+ }
285
+ }
286
+
287
+ // Guard: never dispatch workflows from the SDK repo itself (agentic-lib)
288
+ const isSdkRepo = process.env.GITHUB_REPOSITORY === "xn-intenton-z2a/agentic-lib";
289
+
290
+ // Request supervisor evaluation
291
+ if (action === "request-supervisor") {
292
+ if (isSdkRepo) {
293
+ core.info("Skipping supervisor dispatch — running in SDK repo");
294
+ } else {
295
+ try {
296
+ await octokit.rest.actions.createWorkflowDispatch({
297
+ ...context.repo,
298
+ workflow_id: "agentic-lib-workflow.yml",
299
+ ref: "main",
300
+ inputs: { message: actionArg || "Discussion bot referral" },
301
+ });
302
+ core.info(`Dispatched supervisor with message: ${actionArg}`);
303
+ } catch (err) {
304
+ core.warning(`Failed to dispatch supervisor: ${err.message}`);
305
+ }
306
+ }
307
+ }
308
+
309
+ // Stop automation
310
+ if (action === "stop") {
311
+ if (isSdkRepo) {
312
+ core.info("Skipping schedule dispatch — running in SDK repo");
313
+ } else {
314
+ try {
315
+ await octokit.rest.actions.createWorkflowDispatch({
316
+ ...context.repo,
317
+ workflow_id: "agentic-lib-schedule.yml",
318
+ ref: "main",
319
+ inputs: { frequency: "off" },
320
+ });
321
+ core.info("Automation stopped via discussions bot");
322
+ } catch (err) {
323
+ core.warning(`Failed to stop automation: ${err.message}`);
324
+ }
325
+ }
326
+ }
327
+
194
328
  await postReply(octokit, discussion.nodeId, replyBody);
195
329
 
196
330
  const argSuffix = actionArg ? ` (${actionArg})` : "";
@@ -60,6 +60,24 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
60
60
 
61
61
  const agentInstructions = instructions || "Review whether this issue has been resolved by the current codebase.";
62
62
 
63
+ // Gather recent commits since init for context
64
+ let recentCommitsSummary = [];
65
+ try {
66
+ const initTimestamp = config.init?.timestamp || null;
67
+ const since = initTimestamp || new Date(Date.now() - 7 * 86400000).toISOString();
68
+ const { data: commits } = await octokit.rest.repos.listCommits({
69
+ ...repo,
70
+ sha: "main",
71
+ since,
72
+ per_page: 10,
73
+ });
74
+ recentCommitsSummary = commits.map((c) => {
75
+ const msg = c.commit.message.split("\n")[0];
76
+ const sha = c.sha.substring(0, 7);
77
+ return `- [${sha}] ${msg} (${c.commit.author?.date || ""})`;
78
+ });
79
+ } catch { /* ignore */ }
80
+
63
81
  const prompt = [
64
82
  "## Instructions",
65
83
  agentInstructions,
@@ -84,6 +102,9 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
84
102
  ...(docsFiles.length > 0
85
103
  ? [`## Documentation (${docsFiles.length} files)`, ...docsFiles.map((f) => `- ${f.name}`), ""]
86
104
  : []),
105
+ ...(recentCommitsSummary.length > 0
106
+ ? [`## Recent Commits (since init)`, ...recentCommitsSummary, ""]
107
+ : []),
87
108
  config.configToml ? `## Configuration (agentic-lib.toml)\n\`\`\`toml\n${config.configToml}\n\`\`\`` : "",
88
109
  config.packageJson ? `## Dependencies (package.json)\n\`\`\`json\n${config.packageJson}\n\`\`\`` : "",
89
110
  "",
@@ -6,12 +6,17 @@
6
6
  // asks the Copilot SDK to choose multiple concurrent actions, then dispatches them.
7
7
 
8
8
  import * as core from "@actions/core";
9
- import { existsSync, readFileSync } from "fs";
9
+ import { existsSync, readFileSync, writeFileSync } from "fs";
10
10
  import { runCopilotTask, readOptionalFile, scanDirectory, filterIssues } from "../copilot.js";
11
11
 
12
12
  async function gatherContext(octokit, repo, config, t) {
13
13
  const mission = readOptionalFile(config.paths.mission.path);
14
- const recentActivity = readOptionalFile(config.intentionBot.intentionFilepath).split("\n").slice(-20).join("\n");
14
+ const intentionLogFull = readOptionalFile(config.intentionBot.intentionFilepath);
15
+ const recentActivity = intentionLogFull.split("\n").slice(-20).join("\n");
16
+
17
+ // Read cumulative transformation cost from the activity log
18
+ const costMatches = intentionLogFull.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
19
+ const cumulativeTransformationCost = [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
15
20
 
16
21
  // Check mission-complete signal
17
22
  const missionComplete = existsSync("MISSION_COMPLETE.md");
@@ -20,6 +25,13 @@ async function gatherContext(octokit, repo, config, t) {
20
25
  missionCompleteInfo = readFileSync("MISSION_COMPLETE.md", "utf8").substring(0, 500);
21
26
  }
22
27
 
28
+ // Check mission-failed signal
29
+ const missionFailed = existsSync("MISSION_FAILED.md");
30
+ let missionFailedInfo = "";
31
+ if (missionFailed) {
32
+ missionFailedInfo = readFileSync("MISSION_FAILED.md", "utf8").substring(0, 500);
33
+ }
34
+
23
35
  // Check transformation budget
24
36
  const transformationBudget = config.transformationBudget || 0;
25
37
 
@@ -55,6 +67,36 @@ async function gatherContext(octokit, repo, config, t) {
55
67
  return `#${i.number}: ${i.title} [${labels || "no labels"}] (${age}d old)`;
56
68
  });
57
69
 
70
+ // Fetch recently-closed issues for mission-complete detection and dedup
71
+ let recentlyClosedSummary = [];
72
+ try {
73
+ const { data: closedIssuesRaw } = await octokit.rest.issues.listForRepo({
74
+ ...repo,
75
+ state: "closed",
76
+ per_page: 5,
77
+ sort: "updated",
78
+ direction: "desc",
79
+ });
80
+ for (const ci of closedIssuesRaw.filter((i) => !i.pull_request)) {
81
+ let closeReason = "closed";
82
+ try {
83
+ const { data: comments } = await octokit.rest.issues.listComments({
84
+ ...repo,
85
+ issue_number: ci.number,
86
+ per_page: 1,
87
+ sort: "created",
88
+ direction: "desc",
89
+ });
90
+ if (comments.length > 0 && comments[0].body?.includes("Automated Review Result")) {
91
+ closeReason = "closed by review as RESOLVED";
92
+ }
93
+ } catch (_) { /* ignore */ }
94
+ recentlyClosedSummary.push(`#${ci.number}: ${ci.title} — ${closeReason}`);
95
+ }
96
+ } catch (err) {
97
+ core.warning(`Could not fetch recently closed issues: ${err.message}`);
98
+ }
99
+
58
100
  const { data: openPRs } = await octokit.rest.pulls.list({
59
101
  ...repo,
60
102
  state: "open",
@@ -68,13 +110,55 @@ async function gatherContext(octokit, repo, config, t) {
68
110
  return `#${pr.number}: ${pr.title} (${pr.head.ref}) [${labels || "no labels"}] (${age}d old)`;
69
111
  });
70
112
 
113
+ // Read init timestamp for epoch boundary
114
+ const initTimestamp = config.init?.timestamp || null;
115
+
71
116
  let workflowsSummary = [];
117
+ let actionsSinceInit = [];
72
118
  try {
73
119
  const { data: runs } = await octokit.rest.actions.listWorkflowRunsForRepo({
74
120
  ...repo,
75
- per_page: 10,
121
+ per_page: 20,
76
122
  });
77
123
  workflowsSummary = runs.workflow_runs.map((r) => `${r.name}: ${r.conclusion || r.status} (${r.created_at})`);
124
+
125
+ // Build detailed actions-since-init with commit context
126
+ const initDate = initTimestamp ? new Date(initTimestamp) : null;
127
+ const relevantRuns = initDate
128
+ ? runs.workflow_runs.filter((r) => new Date(r.created_at) >= initDate)
129
+ : runs.workflow_runs.slice(0, 10);
130
+
131
+ for (const run of relevantRuns) {
132
+ const commit = run.head_commit;
133
+ const entry = {
134
+ name: run.name,
135
+ conclusion: run.conclusion || run.status,
136
+ created: run.created_at,
137
+ commitMessage: commit?.message?.split("\n")[0] || "",
138
+ commitSha: run.head_sha?.substring(0, 7) || "",
139
+ branch: run.head_branch || "",
140
+ };
141
+
142
+ // For transform branches, try to get PR change stats
143
+ if (run.head_branch?.startsWith("agentic-lib-issue-")) {
144
+ try {
145
+ const { data: prs } = await octokit.rest.pulls.list({
146
+ ...repo,
147
+ head: `${repo.owner}:${run.head_branch}`,
148
+ state: "all",
149
+ per_page: 1,
150
+ });
151
+ if (prs.length > 0) {
152
+ entry.prNumber = prs[0].number;
153
+ entry.prTitle = prs[0].title;
154
+ entry.additions = prs[0].additions;
155
+ entry.deletions = prs[0].deletions;
156
+ entry.changedFiles = prs[0].changed_files;
157
+ }
158
+ } catch { /* ignore */ }
159
+ }
160
+ actionsSinceInit.push(entry);
161
+ }
78
162
  } catch (err) {
79
163
  core.warning(`Could not fetch workflow runs: ${err.message}`);
80
164
  }
@@ -90,6 +174,8 @@ async function gatherContext(octokit, repo, config, t) {
90
174
  oldestReadyIssue,
91
175
  prsSummary,
92
176
  workflowsSummary,
177
+ actionsSinceInit,
178
+ initTimestamp,
93
179
  supervisor: config.supervisor,
94
180
  configToml: config.configToml,
95
181
  packageJson: config.packageJson,
@@ -98,7 +184,11 @@ async function gatherContext(octokit, repo, config, t) {
98
184
  activeDiscussionUrl,
99
185
  missionComplete,
100
186
  missionCompleteInfo,
187
+ missionFailed,
188
+ missionFailedInfo,
101
189
  transformationBudget,
190
+ cumulativeTransformationCost,
191
+ recentlyClosedSummary,
102
192
  };
103
193
  }
104
194
 
@@ -114,6 +204,9 @@ function buildPrompt(ctx, agentInstructions) {
114
204
  `### Open Issues (${ctx.issuesSummary.length})`,
115
205
  ctx.issuesSummary.join("\n") || "none",
116
206
  "",
207
+ `### Recently Closed Issues (${ctx.recentlyClosedSummary.length})`,
208
+ ctx.recentlyClosedSummary.join("\n") || "none",
209
+ "",
117
210
  `### Open PRs (${ctx.prsSummary.length})`,
118
211
  ctx.prsSummary.join("\n") || "none",
119
212
  "",
@@ -126,6 +219,20 @@ function buildPrompt(ctx, agentInstructions) {
126
219
  `### Recent Workflow Runs`,
127
220
  ctx.workflowsSummary.join("\n") || "none",
128
221
  "",
222
+ ...(ctx.actionsSinceInit.length > 0
223
+ ? [
224
+ `### Actions Since Last Init${ctx.initTimestamp ? ` (${ctx.initTimestamp})` : ""}`,
225
+ "Each entry: workflow | outcome | commit | branch | changes",
226
+ ...ctx.actionsSinceInit.map((a) => {
227
+ let line = `- ${a.name}: ${a.conclusion} (${a.created}) [${a.commitSha}] ${a.commitMessage}`;
228
+ if (a.prNumber) {
229
+ line += ` — PR #${a.prNumber}: +${a.additions}/-${a.deletions} in ${a.changedFiles} file(s)`;
230
+ }
231
+ return line;
232
+ }),
233
+ "",
234
+ ]
235
+ : []),
129
236
  `### Recent Activity`,
130
237
  ctx.recentActivity || "none",
131
238
  "",
@@ -150,8 +257,17 @@ function buildPrompt(ctx, agentInstructions) {
150
257
  "",
151
258
  ]
152
259
  : []),
260
+ ...(ctx.missionFailed
261
+ ? [
262
+ `### Mission Status: FAILED`,
263
+ ctx.missionFailedInfo,
264
+ "The mission has been declared failed. The schedule should be set to off.",
265
+ "You may still: review/close issues, respond to discussions.",
266
+ "",
267
+ ]
268
+ : []),
153
269
  ...(ctx.transformationBudget > 0
154
- ? [`### Transformation Budget: ${ctx.transformationBudget} cycles per run`, ""]
270
+ ? [`### Transformation Budget: ${ctx.cumulativeTransformationCost}/${ctx.transformationBudget} used (${Math.max(0, ctx.transformationBudget - ctx.cumulativeTransformationCost)} remaining)`, ""]
155
271
  : []),
156
272
  `### Issue Limits`,
157
273
  `Feature development WIP limit: ${ctx.featureIssuesWipLimit}`,
@@ -176,6 +292,10 @@ function buildPrompt(ctx, agentInstructions) {
176
292
  "### Communication",
177
293
  "- `respond:discussions | message: <text> | discussion-url: <url>` — Reply via discussions bot",
178
294
  "",
295
+ "### Mission Lifecycle",
296
+ "- `mission-complete | reason: <text>` — Declare mission accomplished. Writes MISSION_COMPLETE.md and sets schedule to off. Use when: all acceptance criteria in MISSION.md are satisfied, tests pass, and recently-closed issues confirm resolution.",
297
+ "- `mission-failed | reason: <text>` — Declare mission failed. Writes MISSION_FAILED.md and sets schedule to off. Use when: transformation budget is exhausted with no progress, pipeline is stuck in a loop, or the mission is unachievable.",
298
+ "",
179
299
  "### Schedule Control",
180
300
  "- `set-schedule:<frequency>` — Change supervisor schedule (off, weekly, daily, hourly, continuous). Use `set-schedule:weekly` when mission is substantially complete, `set-schedule:continuous` to ramp up.",
181
301
  "",
@@ -227,6 +347,12 @@ async function executeDispatch(octokit, repo, actionName, params) {
227
347
  if (params["pr-number"]) inputs["pr-number"] = params["pr-number"];
228
348
  if (params["issue-number"]) inputs["issue-number"] = params["issue-number"];
229
349
 
350
+ // Guard: never dispatch workflows from the SDK repo itself (agentic-lib)
351
+ if (process.env.GITHUB_REPOSITORY === "xn-intenton-z2a/agentic-lib") {
352
+ core.info(`Skipping dispatch of ${workflowFile} — running in SDK repo`);
353
+ return `skipped:sdk-repo:${workflowFile}`;
354
+ }
355
+
230
356
  // Guard: skip transform dispatch if one is already running
231
357
  if (workflowFile === "agentic-lib-workflow.yml") {
232
358
  try {
@@ -253,6 +379,31 @@ async function executeDispatch(octokit, repo, actionName, params) {
253
379
  async function executeCreateIssue(octokit, repo, params) {
254
380
  const title = params.title || "Untitled issue";
255
381
  const labels = params.labels ? params.labels.split(",").map((l) => l.trim()) : ["automated"];
382
+
383
+ // Dedup guard: skip if a similarly-titled issue was closed in the last hour
384
+ try {
385
+ const { data: recent } = await octokit.rest.issues.listForRepo({
386
+ ...repo,
387
+ state: "closed",
388
+ sort: "updated",
389
+ direction: "desc",
390
+ per_page: 5,
391
+ });
392
+ const titlePrefix = title.toLowerCase().substring(0, 30);
393
+ const duplicate = recent.find(
394
+ (i) =>
395
+ !i.pull_request &&
396
+ i.title.toLowerCase().includes(titlePrefix) &&
397
+ Date.now() - new Date(i.closed_at).getTime() < 3600000,
398
+ );
399
+ if (duplicate) {
400
+ core.info(`Skipping duplicate issue (similar to recently closed #${duplicate.number})`);
401
+ return `skipped:duplicate-of-#${duplicate.number}`;
402
+ }
403
+ } catch (err) {
404
+ core.warning(`Dedup check failed: ${err.message}`);
405
+ }
406
+
256
407
  core.info(`Creating issue: ${title}`);
257
408
  const { data: issue } = await octokit.rest.issues.create({ ...repo, title, labels });
258
409
  return `created-issue:#${issue.number}`;
@@ -283,6 +434,10 @@ async function executeRespondDiscussions(octokit, repo, params) {
283
434
  const message = params.message || "";
284
435
  const url = params["discussion-url"] || "";
285
436
  if (message) {
437
+ if (process.env.GITHUB_REPOSITORY === "xn-intenton-z2a/agentic-lib") {
438
+ core.info("Skipping bot dispatch — running in SDK repo");
439
+ return `skipped:sdk-repo:respond-discussions`;
440
+ }
286
441
  core.info(`Dispatching discussions bot with response: ${message.substring(0, 100)}`);
287
442
  const inputs = { message };
288
443
  if (url) inputs["discussion-url"] = url;
@@ -297,11 +452,69 @@ async function executeRespondDiscussions(octokit, repo, params) {
297
452
  return "skipped:respond-no-message";
298
453
  }
299
454
 
455
+ async function executeMissionComplete(octokit, repo, params) {
456
+ const reason = params.reason || "All acceptance criteria satisfied";
457
+ const signal = [
458
+ "# Mission Complete",
459
+ "",
460
+ `- **Timestamp:** ${new Date().toISOString()}`,
461
+ `- **Detected by:** supervisor`,
462
+ `- **Reason:** ${reason}`,
463
+ "",
464
+ "This file was created automatically. To restart transformations, delete this file or run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
465
+ ].join("\n");
466
+ writeFileSync("MISSION_COMPLETE.md", signal);
467
+ core.info(`Mission complete signal written: ${reason}`);
468
+ if (process.env.GITHUB_REPOSITORY !== "xn-intenton-z2a/agentic-lib") {
469
+ try {
470
+ await octokit.rest.actions.createWorkflowDispatch({
471
+ ...repo,
472
+ workflow_id: "agentic-lib-schedule.yml",
473
+ ref: "main",
474
+ inputs: { frequency: "off" },
475
+ });
476
+ } catch (err) {
477
+ core.warning(`Could not set schedule to off: ${err.message}`);
478
+ }
479
+ }
480
+ return `mission-complete:${reason.substring(0, 100)}`;
481
+ }
482
+
483
+ async function executeMissionFailed(octokit, repo, params) {
484
+ const reason = params.reason || "Mission could not be completed";
485
+ const signal = [
486
+ "# Mission Failed",
487
+ "",
488
+ `- **Timestamp:** ${new Date().toISOString()}`,
489
+ `- **Detected by:** supervisor`,
490
+ `- **Reason:** ${reason}`,
491
+ "",
492
+ "This file was created automatically. To restart, delete this file and run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
493
+ ].join("\n");
494
+ writeFileSync("MISSION_FAILED.md", signal);
495
+ core.info(`Mission failed signal written: ${reason}`);
496
+ if (process.env.GITHUB_REPOSITORY !== "xn-intenton-z2a/agentic-lib") {
497
+ try {
498
+ await octokit.rest.actions.createWorkflowDispatch({
499
+ ...repo,
500
+ workflow_id: "agentic-lib-schedule.yml",
501
+ ref: "main",
502
+ inputs: { frequency: "off" },
503
+ });
504
+ } catch (err) {
505
+ core.warning(`Could not set schedule to off: ${err.message}`);
506
+ }
507
+ }
508
+ return `mission-failed:${reason.substring(0, 100)}`;
509
+ }
510
+
300
511
  const ACTION_HANDLERS = {
301
512
  "github:create-issue": executeCreateIssue,
302
513
  "github:label-issue": executeLabelIssue,
303
514
  "github:close-issue": executeCloseIssue,
304
515
  "respond:discussions": executeRespondDiscussions,
516
+ "mission-complete": executeMissionComplete,
517
+ "mission-failed": executeMissionFailed,
305
518
  };
306
519
 
307
520
  async function executeSetSchedule(octokit, repo, frequency) {
@@ -309,6 +522,10 @@ async function executeSetSchedule(octokit, repo, frequency) {
309
522
  if (!valid.includes(frequency)) {
310
523
  return `skipped:invalid-frequency:${frequency}`;
311
524
  }
525
+ if (process.env.GITHUB_REPOSITORY === "xn-intenton-z2a/agentic-lib") {
526
+ core.info(`Skipping schedule dispatch — running in SDK repo`);
527
+ return `skipped:sdk-repo:set-schedule:${frequency}`;
528
+ }
312
529
  core.info(`Setting supervisor schedule to: ${frequency}`);
313
530
  await octokit.rest.actions.createWorkflowDispatch({
314
531
  ...repo,
@@ -370,6 +587,17 @@ export async function supervise(context) {
370
587
  }
371
588
  }
372
589
 
590
+ // Build changes list from executed actions
591
+ const changes = results
592
+ .filter((r) => r.startsWith("created-issue:") || r.startsWith("mission-complete:") || r.startsWith("mission-failed:"))
593
+ .map((r) => {
594
+ if (r.startsWith("created-issue:")) return { action: "created-issue", file: r.replace("created-issue:", ""), sizeInfo: "" };
595
+ if (r.startsWith("mission-complete:")) return { action: "mission-complete", file: "MISSION_COMPLETE.md", sizeInfo: r.replace("mission-complete:", "") };
596
+ if (r.startsWith("mission-failed:")) return { action: "mission-failed", file: "MISSION_FAILED.md", sizeInfo: r.replace("mission-failed:", "") };
597
+ return null;
598
+ })
599
+ .filter(Boolean);
600
+
373
601
  return {
374
602
  outcome: actions.length === 0 ? "nop" : `supervised:${actions.length}-actions`,
375
603
  tokensUsed,
@@ -378,5 +606,7 @@ export async function supervise(context) {
378
606
  cost,
379
607
  model,
380
608
  details: `Actions: ${results.join(", ")}\nReasoning: ${reasoning.substring(0, 300)}`,
609
+ narrative: reasoning.substring(0, 500),
610
+ changes,
381
611
  };
382
612
  }
@@ -26,6 +26,8 @@ runs:
26
26
  git config --local user.email 'action@github.com'
27
27
  git config --local user.name 'GitHub Actions[bot]'
28
28
  git add -A
29
+ # Unstage workflow files — GITHUB_TOKEN cannot push workflow changes
30
+ git reset HEAD -- '.github/workflows/' 2>/dev/null || true
29
31
  # Unstage log files on non-default branches to avoid merge conflicts
30
32
  REF="${{ inputs.push-ref }}"
31
33
  if [ -n "$REF" ] && [ "$REF" != "main" ] && [ "$REF" != "master" ]; then
@@ -27,6 +27,8 @@ You are the supervisor of an autonomous coding repository. Your job is to advanc
27
27
  - **github:close-issue** — When an issue is clearly resolved or no longer relevant.
28
28
  - **respond:discussions** — When replying to a user request that came through the discussions bot. Include the discussion URL and a clear message.
29
29
  - **set-schedule:\<frequency\>** — Change the workflow schedule. Use `weekly` when mission is substantially achieved, `continuous` to ramp up for active development.
30
+ - **mission-complete** — When all MISSION.md acceptance criteria are verified as satisfied. Review the Recently Closed Issues — if the last 2+ issues were closed by review as RESOLVED, 0 open issues remain, and the acceptance criteria in MISSION.md match the implemented code, declare mission complete. This writes MISSION_COMPLETE.md and sets the schedule to off. Always include a reason summarising what was achieved.
31
+ - **mission-failed** — When the mission cannot be completed. Use when: transformation budget is exhausted with acceptance criteria still unmet, the pipeline is stuck in a create-close loop with no code changes, or 3+ consecutive transforms failed to produce working code. This writes MISSION_FAILED.md and sets the schedule to off. Always include a reason explaining what went wrong.
30
32
  - **nop** — When everything is running optimally: transform is active, issues are flowing, no failures.
31
33
 
32
34
  ## Stale Issue Detection
@@ -42,15 +44,16 @@ Include the website URL in the announcement — the site is at `https://<owner>.
42
44
 
43
45
  ### Mission Accomplished (bounded missions)
44
46
  When ALL of the following conditions are met, the mission is accomplished:
45
- 1. All open issues are closed
47
+ 1. All open issues are closed (check Recently Closed Issues — if the last 2+ were closed by review as RESOLVED, this is strong evidence)
46
48
  2. Tests pass (CI gates commits, so this is usually the case)
47
- 3. The MISSION.md acceptance criteria are all satisfied
48
- 4. Evidence artifacts exist under `docs/` (example outputs, test results, or walkthroughs)
49
+ 3. The MISSION.md acceptance criteria are all satisfied (verify each criterion against the Recently Closed Issues and Recent Activity)
50
+ 4. Do not create an issue if a similar issue was recently closed as resolved — check the Recently Closed Issues section
49
51
 
50
- When all conditions are met:
51
- 1. `dispatch:agentic-lib-bot` announce mission accomplished in the discussions thread. Include the website URL (`https://<owner>.github.io/<repo>/`) where users can see the finished product.
52
- 2. `set-schedule:off` — stop the workflow. The mission is done.
53
- 3. Log `mission-accomplished` in the activity log.
52
+ When all conditions are met, use the `mission-complete` action:
53
+ 1. `mission-complete | reason: <summary of what was achieved>` this writes MISSION_COMPLETE.md and sets the schedule to off
54
+ 2. `dispatch:agentic-lib-bot` — announce mission accomplished in the discussions thread. Include the website URL (`https://<owner>.github.io/<repo>/`) where users can see the finished product.
55
+
56
+ Do NOT create another issue when the mission is already accomplished. If the Recently Closed Issues show 2+ issues closed by review as RESOLVED and 0 open issues remain, the mission is done.
54
57
 
55
58
  ### Ongoing Missions
56
59
  If MISSION.md explicitly says "do not set schedule to off" or "ongoing mission", the mission never completes.
@@ -64,14 +67,33 @@ When the transform agent has implemented all major features but minor polish rem
64
67
  2. `set-schedule:weekly` — reduce to weekly maintenance check-ins
65
68
  3. Check that `docs/` contains evidence of the library working before declaring done
66
69
 
70
+ ### Mission Failed
71
+ When the mission cannot be completed, use the `mission-failed` action. Indicators of failure:
72
+ 1. **Budget exhausted** — Transformation Budget shows usage at or near capacity with acceptance criteria still unmet
73
+ 2. **Pipeline stuck** — 3+ consecutive supervisor cycles created issues that were immediately closed by review as RESOLVED, but the acceptance criteria are NOT actually met (false positives in review)
74
+ 3. **No progress** — the last 3+ transforms produced no code changes (all nop outcomes) and acceptance criteria remain unmet
75
+ 4. **Repeated failures** — transforms keep producing code that fails tests, and fix-code cannot resolve the failures
76
+ 5. **Consuming budget without results** — transformation budget is being spent but the codebase is not converging toward the acceptance criteria
77
+
78
+ When declaring mission failed:
79
+ 1. `mission-failed | reason: <what went wrong and what was achieved>` — this writes MISSION_FAILED.md and sets the schedule to off
80
+ 2. `dispatch:agentic-lib-bot` — announce the failure in the discussions thread with details of what was accomplished and what remains
81
+
67
82
  ## Prerequisites
68
83
 
69
84
  - The `set-schedule` action requires a `WORKFLOW_TOKEN` secret (classic PAT with `workflow` scope) to push workflow file changes to main.
70
85
 
71
86
  ## Stability Detection
72
87
 
73
- Check the Recent Activity log for patterns that indicate the mission is done:
74
- - If the last 2+ workflow runs produced no transform commits (only maintain-only or nop outcomes), AND all open issues are closed, the mission is likely accomplished. Follow the "Mission Accomplished" protocol above.
88
+ Check the Recent Activity log and Recently Closed Issues for patterns:
89
+
90
+ **Mission complete signals:**
91
+ - If the last 2+ issues were closed by review as RESOLVED, AND 0 open issues remain, the mission is likely accomplished. Verify against MISSION.md acceptance criteria, then use `mission-complete`.
92
+ - If the last 2+ workflow runs produced no transform commits (only maintain-only or nop outcomes), AND all open issues are closed, follow the "Mission Accomplished" protocol.
93
+
94
+ **Mission failed signals:**
95
+ - If the Transformation Budget shows usage near capacity (e.g. 28/32) and acceptance criteria are still unmet, the mission is failing. Use `mission-failed`.
96
+ - If the last 3+ cycles show the pattern: create issue → review closes as resolved → no transform → create identical issue, the pipeline is stuck. Check if acceptance criteria are truly met (use `mission-complete`) or if review is wrong (create a more specific issue). If neither works, use `mission-failed`.
75
97
  - Look for `transform: nop` or `transform: transformed` patterns in the activity log to distinguish productive iterations from idle ones.
76
98
 
77
99
  ## Discussions Awareness
@@ -14,9 +14,6 @@
14
14
  .identity { background: #f6f8fa; padding: 1rem; border-radius: 6px; margin: 1rem 0; font-family: monospace; }
15
15
  .identity dt { font-weight: bold; color: #666; }
16
16
  .identity dd { margin: 0 0 0.5rem 0; font-size: 1.1rem; }
17
- .social { display: flex; gap: 1rem; margin: 1rem 0; flex-wrap: wrap; }
18
- .social a { display: inline-block; padding: 0.4rem 0.8rem; border: 1px solid #ddd; border-radius: 4px; text-decoration: none; font-size: 0.9rem; }
19
- .social a:hover { background: #f6f8fa; }
20
17
  #demo-output { background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; font-family: monospace; white-space: pre-wrap; min-height: 2rem; }
21
18
  </style>
22
19
  </head>
@@ -32,12 +29,6 @@
32
29
  </div>
33
30
  <h2>Output</h2>
34
31
  <div id="demo-output">Loading library...</div>
35
- <h2>Links</h2>
36
- <div class="social">
37
- <a id="share-github" href="https://github.com/xn-intenton-z2a/repository0" target="_blank" rel="noopener">GitHub</a>
38
- <a id="share-twitter" href="#" target="_blank" rel="noopener">X/Twitter</a>
39
- <a id="share-linkedin" href="#" target="_blank" rel="noopener">LinkedIn</a>
40
- </div>
41
32
  <h2>About</h2>
42
33
  <p>This website uses the library. See <a href="https://github.com/xn-intenton-z2a/repository0">the repository</a> for source code and mission details.</p>
43
34
  <script type="module">
@@ -48,10 +39,6 @@
48
39
  document.getElementById('lib-description').textContent = description || '(no description)';
49
40
  document.getElementById('demo-output').textContent = JSON.stringify({ name, version, description }, null, 2);
50
41
  document.title = name + ' v' + version;
51
- const pageUrl = encodeURIComponent(window.location.href);
52
- const text = encodeURIComponent('Check out ' + name + ' v' + version + ' — ' + description);
53
- document.getElementById('share-twitter').href = 'https://x.com/intent/tweet?url=' + pageUrl + '&text=' + text;
54
- document.getElementById('share-linkedin').href = 'https://www.linkedin.com/sharing/share-offsite/?url=' + pageUrl;
55
42
  } catch (e) {
56
43
  document.getElementById('lib-version').textContent = '(build required)';
57
44
  document.getElementById('lib-description').textContent = 'Run npm run build:web to generate library metadata';
@@ -16,7 +16,7 @@
16
16
  "author": "",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@xn-intenton-z2a/agentic-lib": "^7.1.70"
19
+ "@xn-intenton-z2a/agentic-lib": "^7.1.72"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@vitest/coverage-v8": "^4.0.18",
@@ -25,9 +25,4 @@ describe("Website", () => {
25
25
  expect(html).toContain("lib-name");
26
26
  expect(html).toContain("lib-version");
27
27
  });
28
-
29
- test("index.html has social share links", () => {
30
- const html = readFileSync("src/web/index.html", "utf8");
31
- expect(html).toMatch(/share.*(twitter|x\.com|linkedin)/is);
32
- });
33
28
  });