@xn-intenton-z2a/agentic-lib 7.1.94 → 7.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/agentic-lib-schedule.yml +12 -0
- package/.github/workflows/agentic-lib-test.yml +26 -1
- package/.github/workflows/agentic-lib-workflow.yml +170 -5
- package/agentic-lib.toml +1 -0
- package/bin/agentic-lib.js +47 -15
- package/package.json +1 -1
- package/src/actions/agentic-step/config-loader.js +2 -1
- package/src/actions/agentic-step/index.js +65 -0
- package/src/actions/agentic-step/logging.js +16 -0
- package/src/actions/agentic-step/tasks/discussions.js +3 -16
- package/src/actions/agentic-step/tasks/supervise.js +5 -2
- package/src/agents/agent-apply-fix.md +4 -0
- package/src/agents/agent-issue-resolution.md +4 -0
- package/src/seeds/zero-package.json +1 -1
|
@@ -107,6 +107,18 @@ jobs:
|
|
|
107
107
|
let content = fs.readFileSync(workflowPath, 'utf8');
|
|
108
108
|
const cron = SCHEDULE_MAP[frequency];
|
|
109
109
|
|
|
110
|
+
// Check if the frequency is already set — skip if no-op
|
|
111
|
+
const supervisorRegex2 = /^\s*supervisor\s*=\s*"([^"]*)"/m;
|
|
112
|
+
if (fs.existsSync(tomlPath)) {
|
|
113
|
+
const currentToml = fs.readFileSync(tomlPath, 'utf8');
|
|
114
|
+
const currentMatch = currentToml.match(supervisorRegex2);
|
|
115
|
+
const currentFreq = currentMatch ? currentMatch[1] : '';
|
|
116
|
+
if (currentFreq === frequency) {
|
|
117
|
+
core.info(`Schedule already set to ${frequency} — no change needed`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
// Remove any existing schedule block
|
|
111
123
|
content = content.replace(/\n schedule:\n - cron: "[^"]*"\n/g, '\n');
|
|
112
124
|
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
name: agentic-lib-test
|
|
12
12
|
run-name: "agentic-lib-test [${{ github.ref_name }}]"
|
|
13
13
|
|
|
14
|
+
#@dist concurrency:
|
|
15
|
+
#@dist group: agentic-lib-test-${{ github.ref_name }}
|
|
16
|
+
#@dist cancel-in-progress: true
|
|
17
|
+
|
|
14
18
|
on:
|
|
15
19
|
schedule:
|
|
16
20
|
- cron: "10 * * * *"
|
|
@@ -115,12 +119,33 @@ jobs:
|
|
|
115
119
|
if: >-
|
|
116
120
|
!cancelled()
|
|
117
121
|
&& github.ref == 'refs/heads/main'
|
|
118
|
-
&& github.event_name
|
|
122
|
+
&& (github.event_name == 'push' || github.event_name == 'schedule')
|
|
119
123
|
&& github.repository != 'xn-intenton-z2a/agentic-lib'
|
|
120
124
|
&& (needs.test.result == 'failure' || needs.behaviour.result == 'failure')
|
|
121
125
|
runs-on: ubuntu-latest
|
|
122
126
|
steps:
|
|
127
|
+
- name: Check circuit breaker
|
|
128
|
+
id: breaker
|
|
129
|
+
env:
|
|
130
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
131
|
+
run: |
|
|
132
|
+
# Count agentic-lib-workflow dispatches in last 30 minutes
|
|
133
|
+
SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30M +%Y-%m-%dT%H:%M:%SZ)
|
|
134
|
+
COUNT=$(gh run list \
|
|
135
|
+
--repo "${{ github.repository }}" \
|
|
136
|
+
--workflow agentic-lib-workflow.yml \
|
|
137
|
+
--json createdAt \
|
|
138
|
+
--jq "[.[] | select(.createdAt >= \"$SINCE\")] | length")
|
|
139
|
+
echo "recent-dispatches=$COUNT"
|
|
140
|
+
if [ "$COUNT" -ge 3 ]; then
|
|
141
|
+
echo "Circuit breaker tripped: $COUNT dispatches in last 30 min"
|
|
142
|
+
echo "tripped=true" >> $GITHUB_OUTPUT
|
|
143
|
+
else
|
|
144
|
+
echo "tripped=false" >> $GITHUB_OUTPUT
|
|
145
|
+
fi
|
|
146
|
+
|
|
123
147
|
- name: Dispatch agentic-lib-workflow to fix broken build
|
|
148
|
+
if: steps.breaker.outputs.tripped != 'true'
|
|
124
149
|
env:
|
|
125
150
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
126
151
|
run: |
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
name: agentic-lib-workflow
|
|
13
13
|
run-name: "agentic-lib-workflow [${{ github.ref_name }}]"
|
|
14
|
-
concurrency:
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
#@dist concurrency:
|
|
15
|
+
#@dist group: agentic-lib-workflow
|
|
16
|
+
#@dist cancel-in-progress: false
|
|
17
17
|
|
|
18
18
|
on:
|
|
19
19
|
#@dist schedule:
|
|
@@ -123,13 +123,25 @@ jobs:
|
|
|
123
123
|
params:
|
|
124
124
|
runs-on: ubuntu-latest
|
|
125
125
|
steps:
|
|
126
|
+
- uses: actions/checkout@v6
|
|
127
|
+
with:
|
|
128
|
+
sparse-checkout: ${{ env.configPath }}
|
|
129
|
+
sparse-checkout-cone-mode: false
|
|
126
130
|
- name: Normalise params
|
|
127
131
|
id: normalise
|
|
128
132
|
shell: bash
|
|
129
133
|
run: |
|
|
130
134
|
MODEL='${{ inputs.model }}'
|
|
135
|
+
if [ -z "$MODEL" ] && [ -f "${{ env.configPath }}" ]; then
|
|
136
|
+
TOML_MODEL=$(grep '^\s*model' "${{ env.configPath }}" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
|
|
137
|
+
MODEL="${TOML_MODEL}"
|
|
138
|
+
fi
|
|
131
139
|
echo "model=${MODEL:-gpt-5-mini}" >> $GITHUB_OUTPUT
|
|
132
140
|
PROFILE='${{ inputs.profile }}'
|
|
141
|
+
if [ -z "$PROFILE" ] && [ -f "${{ env.configPath }}" ]; then
|
|
142
|
+
TOML_PROFILE=$(grep '^\s*profile' "${{ env.configPath }}" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
|
|
143
|
+
PROFILE="${TOML_PROFILE}"
|
|
144
|
+
fi
|
|
133
145
|
echo "profile=${PROFILE}" >> $GITHUB_OUTPUT
|
|
134
146
|
MODE='${{ inputs.mode }}'
|
|
135
147
|
echo "mode=${MODE:-full}" >> $GITHUB_OUTPUT
|
|
@@ -242,6 +254,52 @@ jobs:
|
|
|
242
254
|
runs-on: ubuntu-latest
|
|
243
255
|
steps:
|
|
244
256
|
- uses: actions/checkout@v6
|
|
257
|
+
|
|
258
|
+
- uses: actions/setup-node@v6
|
|
259
|
+
with:
|
|
260
|
+
node-version: 24
|
|
261
|
+
cache: "npm"
|
|
262
|
+
|
|
263
|
+
- name: Install dependencies (non-blocking)
|
|
264
|
+
id: install-deps
|
|
265
|
+
run: npm ci 2>/dev/null || echo "npm ci failed (non-blocking)"
|
|
266
|
+
|
|
267
|
+
- name: Run unit tests (non-blocking)
|
|
268
|
+
id: unit-tests
|
|
269
|
+
run: |
|
|
270
|
+
set +e
|
|
271
|
+
TEST_CMD=""
|
|
272
|
+
if [ -f agentic-lib.toml ]; then
|
|
273
|
+
TEST_CMD=$(grep '^\s*test\s*=' agentic-lib.toml 2>/dev/null | head -1 | sed 's/.*=\s*"\([^"]*\)".*/\1/')
|
|
274
|
+
fi
|
|
275
|
+
TEST_CMD="${TEST_CMD:-npm test}"
|
|
276
|
+
OUTPUT=$(eval "$TEST_CMD" 2>&1)
|
|
277
|
+
EXIT_CODE=$?
|
|
278
|
+
echo "exit-code=$EXIT_CODE" >> $GITHUB_OUTPUT
|
|
279
|
+
PASS_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ pass' | head -1 | grep -oP '\d+' || echo "0")
|
|
280
|
+
FAIL_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ fail' | head -1 | grep -oP '\d+' || echo "0")
|
|
281
|
+
echo "pass-count=$PASS_COUNT" >> $GITHUB_OUTPUT
|
|
282
|
+
echo "fail-count=$FAIL_COUNT" >> $GITHUB_OUTPUT
|
|
283
|
+
echo "$OUTPUT" | tail -100 > /tmp/unit-test-output.txt
|
|
284
|
+
exit 0
|
|
285
|
+
|
|
286
|
+
- name: Run behaviour tests (non-blocking)
|
|
287
|
+
id: behaviour-tests
|
|
288
|
+
if: hashFiles('playwright.config.js') != '' || hashFiles('playwright.config.ts') != ''
|
|
289
|
+
run: |
|
|
290
|
+
set +e
|
|
291
|
+
npx playwright install --with-deps chromium 2>/dev/null || true
|
|
292
|
+
npm run build:web 2>/dev/null || true
|
|
293
|
+
OUTPUT=$(npm run --if-present test:behaviour 2>&1)
|
|
294
|
+
EXIT_CODE=$?
|
|
295
|
+
echo "exit-code=$EXIT_CODE" >> $GITHUB_OUTPUT
|
|
296
|
+
PASS_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ passed' | head -1 | grep -oP '\d+' || echo "0")
|
|
297
|
+
FAIL_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ failed' | head -1 | grep -oP '\d+' || echo "0")
|
|
298
|
+
echo "pass-count=$PASS_COUNT" >> $GITHUB_OUTPUT
|
|
299
|
+
echo "fail-count=$FAIL_COUNT" >> $GITHUB_OUTPUT
|
|
300
|
+
echo "$OUTPUT" | tail -50 > /tmp/behaviour-test-output.txt
|
|
301
|
+
exit 0
|
|
302
|
+
|
|
245
303
|
- name: Gather telemetry
|
|
246
304
|
id: gather
|
|
247
305
|
uses: actions/github-script@v8
|
|
@@ -293,6 +351,72 @@ jobs:
|
|
|
293
351
|
// Message from bot/human
|
|
294
352
|
const message = '${{ needs.params.outputs.message }}';
|
|
295
353
|
|
|
354
|
+
// Latest external test workflow run
|
|
355
|
+
let externalTestResults = null;
|
|
356
|
+
try {
|
|
357
|
+
const testRuns = await github.rest.actions.listWorkflowRuns({
|
|
358
|
+
owner, repo,
|
|
359
|
+
workflow_id: 'agentic-lib-test.yml',
|
|
360
|
+
branch: 'main',
|
|
361
|
+
per_page: 1,
|
|
362
|
+
});
|
|
363
|
+
const latestTest = testRuns.data.workflow_runs[0];
|
|
364
|
+
if (latestTest) {
|
|
365
|
+
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
|
366
|
+
owner, repo,
|
|
367
|
+
run_id: latestTest.id,
|
|
368
|
+
});
|
|
369
|
+
externalTestResults = {
|
|
370
|
+
runId: latestTest.id,
|
|
371
|
+
conclusion: latestTest.conclusion,
|
|
372
|
+
created: latestTest.created_at,
|
|
373
|
+
jobs: jobs.data.jobs.map(j => ({
|
|
374
|
+
name: j.name, conclusion: j.conclusion,
|
|
375
|
+
})),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} catch (e) { /* ignore */ }
|
|
379
|
+
|
|
380
|
+
// Source file stats
|
|
381
|
+
let sourceStats = null;
|
|
382
|
+
try {
|
|
383
|
+
const sourcePath = 'src/lib/';
|
|
384
|
+
if (fs.existsSync(sourcePath)) {
|
|
385
|
+
const files = fs.readdirSync(sourcePath).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
|
|
386
|
+
sourceStats = files.map(f => {
|
|
387
|
+
const content = fs.readFileSync(`${sourcePath}${f}`, 'utf8');
|
|
388
|
+
const lines = content.split('\n').length;
|
|
389
|
+
const exports = [...content.matchAll(/export\s+(?:async\s+)?(?:function|const|class)\s+(\w+)/g)].map(m => m[1]);
|
|
390
|
+
return { file: f, lines, exports };
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
} catch (e) {}
|
|
394
|
+
|
|
395
|
+
// Mission complete/failed signals
|
|
396
|
+
const missionComplete = fs.existsSync('MISSION_COMPLETE.md');
|
|
397
|
+
const missionFailed = fs.existsSync('MISSION_FAILED.md');
|
|
398
|
+
|
|
399
|
+
// Activity log stats
|
|
400
|
+
let activityStats = null;
|
|
401
|
+
try {
|
|
402
|
+
const logPath = fs.existsSync('intenti\u00F6n.md') ? 'intenti\u00F6n.md' : (fs.existsSync('intention.md') ? 'intention.md' : null);
|
|
403
|
+
if (logPath) {
|
|
404
|
+
const log = fs.readFileSync(logPath, 'utf8');
|
|
405
|
+
const entries = log.split('\n## ').length - 1;
|
|
406
|
+
const costMatches = [...log.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g)];
|
|
407
|
+
const totalCost = costMatches.reduce((sum, m) => sum + parseInt(m[1], 10), 0);
|
|
408
|
+
activityStats = { entries, totalTransformCost: totalCost };
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {}
|
|
411
|
+
|
|
412
|
+
// Live test results from earlier steps
|
|
413
|
+
const unitOutput = fs.existsSync('/tmp/unit-test-output.txt')
|
|
414
|
+
? fs.readFileSync('/tmp/unit-test-output.txt', 'utf8').slice(0, 5000)
|
|
415
|
+
: '';
|
|
416
|
+
const behaviourOutput = fs.existsSync('/tmp/behaviour-test-output.txt')
|
|
417
|
+
? fs.readFileSync('/tmp/behaviour-test-output.txt', 'utf8').slice(0, 2500)
|
|
418
|
+
: '';
|
|
419
|
+
|
|
296
420
|
const telemetry = {
|
|
297
421
|
issues: issuesSummary,
|
|
298
422
|
prs: prsSummary,
|
|
@@ -300,13 +424,42 @@ jobs:
|
|
|
300
424
|
mission: mission.slice(0, 500),
|
|
301
425
|
featureFiles: features,
|
|
302
426
|
message: message || null,
|
|
427
|
+
liveTests: {
|
|
428
|
+
unit: {
|
|
429
|
+
exitCode: parseInt('${{ steps.unit-tests.outputs.exit-code }}' || '0'),
|
|
430
|
+
passCount: parseInt('${{ steps.unit-tests.outputs.pass-count }}' || '0'),
|
|
431
|
+
failCount: parseInt('${{ steps.unit-tests.outputs.fail-count }}' || '0'),
|
|
432
|
+
output: unitOutput,
|
|
433
|
+
},
|
|
434
|
+
behaviour: {
|
|
435
|
+
exitCode: parseInt('${{ steps.behaviour-tests.outputs.exit-code }}' || '-1'),
|
|
436
|
+
passCount: parseInt('${{ steps.behaviour-tests.outputs.pass-count }}' || '0'),
|
|
437
|
+
failCount: parseInt('${{ steps.behaviour-tests.outputs.fail-count }}' || '0'),
|
|
438
|
+
output: behaviourOutput,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
externalTestResults,
|
|
442
|
+
sourceStats,
|
|
443
|
+
activityStats,
|
|
444
|
+
missionComplete,
|
|
445
|
+
missionFailed,
|
|
303
446
|
};
|
|
304
447
|
|
|
305
448
|
// Write to file for downstream jobs
|
|
306
449
|
fs.writeFileSync('/tmp/telemetry.json', JSON.stringify(telemetry, null, 2));
|
|
307
|
-
// Set as output (
|
|
450
|
+
// Set as output (clipped by tuning profile)
|
|
451
|
+
let maxTelemetryChars = 30000;
|
|
452
|
+
try {
|
|
453
|
+
if (fs.existsSync('agentic-lib.toml')) {
|
|
454
|
+
const toml = fs.readFileSync('agentic-lib.toml', 'utf8');
|
|
455
|
+
const profileMatch = toml.match(/^\s*profile\s*=\s*"(\w+)"/m);
|
|
456
|
+
const profile = profileMatch ? profileMatch[1] : 'recommended';
|
|
457
|
+
const PROFILE_LIMITS = { min: 10000, recommended: 30000, max: 60000 };
|
|
458
|
+
maxTelemetryChars = PROFILE_LIMITS[profile] || 30000;
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {}
|
|
308
461
|
const summary = JSON.stringify(telemetry);
|
|
309
|
-
core.setOutput('telemetry', summary.slice(0,
|
|
462
|
+
core.setOutput('telemetry', summary.slice(0, maxTelemetryChars));
|
|
310
463
|
outputs:
|
|
311
464
|
telemetry: ${{ steps.gather.outputs.telemetry }}
|
|
312
465
|
|
|
@@ -688,6 +841,8 @@ jobs:
|
|
|
688
841
|
echo "No changes to commit"
|
|
689
842
|
exit 0
|
|
690
843
|
fi
|
|
844
|
+
git config --local user.email 'action@github.com'
|
|
845
|
+
git config --local user.name 'GitHub Actions[bot]'
|
|
691
846
|
git add -A
|
|
692
847
|
git commit -m "agentic-step: fix broken main build (run ${{ env.FIX_RUN_ID }})"
|
|
693
848
|
git push -u origin agentic-lib-fix-main-build
|
|
@@ -987,6 +1142,16 @@ jobs:
|
|
|
987
1142
|
echo "- Dry-run: ${{ needs.params.outputs.dry-run }}" >> $GITHUB_STEP_SUMMARY
|
|
988
1143
|
echo "- Website: [${SITE_URL}](${SITE_URL})" >> $GITHUB_STEP_SUMMARY
|
|
989
1144
|
|
|
1145
|
+
# ─── Post-commit validation: call test workflow to verify branch health ───
|
|
1146
|
+
post-commit-test:
|
|
1147
|
+
needs: [params, dev, fix-stuck, post-merge]
|
|
1148
|
+
if: >-
|
|
1149
|
+
!cancelled()
|
|
1150
|
+
&& needs.params.outputs.dry-run != 'true'
|
|
1151
|
+
&& needs.params.result == 'success'
|
|
1152
|
+
uses: ./.github/workflows/agentic-lib-test.yml
|
|
1153
|
+
secrets: inherit
|
|
1154
|
+
|
|
990
1155
|
# ─── Schedule change (if requested) ────────────────────────────────
|
|
991
1156
|
update-schedule:
|
|
992
1157
|
needs: [params, dev]
|
package/agentic-lib.toml
CHANGED
|
@@ -12,6 +12,7 @@ supervisor = "daily" # off | weekly | daily | hourly | continuous
|
|
|
12
12
|
mission = "test/MISSION.md" #@dist "MISSION.md"
|
|
13
13
|
source = "test/src/lib/" #@dist "src/lib/"
|
|
14
14
|
tests = "test/tests/unit/" #@dist "tests/unit/"
|
|
15
|
+
behaviour = "test/tests/behaviour/" #@dist "tests/behaviour/"
|
|
15
16
|
features = "test/features/" #@dist "features/"
|
|
16
17
|
library = "test/library/" #@dist "library/"
|
|
17
18
|
web = "test/src/web/" #@dist "src/web/"
|
package/bin/agentic-lib.js
CHANGED
|
@@ -905,6 +905,7 @@ function initReseed(initTimestamp) {
|
|
|
905
905
|
function readTomlPaths() {
|
|
906
906
|
let sourcePath = "src/lib/";
|
|
907
907
|
let testsPath = "tests/unit/";
|
|
908
|
+
let behaviourPath = "tests/behaviour/";
|
|
908
909
|
let examplesPath = "examples/";
|
|
909
910
|
let webPath = "src/web/";
|
|
910
911
|
const tomlTarget = resolve(target, "agentic-lib.toml");
|
|
@@ -913,17 +914,19 @@ function readTomlPaths() {
|
|
|
913
914
|
const tomlContent = readFileSync(tomlTarget, "utf8");
|
|
914
915
|
const sourceMatch = tomlContent.match(/^source\s*=\s*"([^"]+)"/m);
|
|
915
916
|
const testsMatch = tomlContent.match(/^tests\s*=\s*"([^"]+)"/m);
|
|
917
|
+
const behaviourMatch = tomlContent.match(/^behaviour\s*=\s*"([^"]+)"/m);
|
|
916
918
|
const examplesMatch = tomlContent.match(/^examples\s*=\s*"([^"]+)"/m);
|
|
917
919
|
const webMatch = tomlContent.match(/^web\s*=\s*"([^"]+)"/m);
|
|
918
920
|
if (sourceMatch) sourcePath = sourceMatch[1];
|
|
919
921
|
if (testsMatch) testsPath = testsMatch[1];
|
|
922
|
+
if (behaviourMatch) behaviourPath = behaviourMatch[1];
|
|
920
923
|
if (examplesMatch) examplesPath = examplesMatch[1];
|
|
921
924
|
if (webMatch) webPath = webMatch[1];
|
|
922
925
|
} catch (err) {
|
|
923
926
|
console.log(` WARN: Could not read TOML for paths, using defaults: ${err.message}`);
|
|
924
927
|
}
|
|
925
928
|
}
|
|
926
|
-
return { sourcePath, testsPath, examplesPath, webPath };
|
|
929
|
+
return { sourcePath, testsPath, behaviourPath, examplesPath, webPath };
|
|
927
930
|
}
|
|
928
931
|
|
|
929
932
|
function clearAndRecreateDir(dirPath, label) {
|
|
@@ -939,9 +942,10 @@ function clearAndRecreateDir(dirPath, label) {
|
|
|
939
942
|
function initPurge(seedsDir, missionName, initTimestamp) {
|
|
940
943
|
console.log("\n--- Purge: Reset Source Files to Seed State ---");
|
|
941
944
|
|
|
942
|
-
const { sourcePath, testsPath, examplesPath, webPath } = readTomlPaths();
|
|
945
|
+
const { sourcePath, testsPath, behaviourPath, examplesPath, webPath } = readTomlPaths();
|
|
943
946
|
clearAndRecreateDir(sourcePath, sourcePath);
|
|
944
947
|
clearAndRecreateDir(testsPath, testsPath);
|
|
948
|
+
clearAndRecreateDir(behaviourPath, behaviourPath);
|
|
945
949
|
clearAndRecreateDir(examplesPath, examplesPath);
|
|
946
950
|
clearAndRecreateDir(webPath, webPath);
|
|
947
951
|
clearAndRecreateDir("docs", "docs");
|
|
@@ -1097,25 +1101,53 @@ function initPurgeGitHub() {
|
|
|
1097
1101
|
}
|
|
1098
1102
|
if (openIssues.length === 0) console.log(" No open issues to close");
|
|
1099
1103
|
|
|
1100
|
-
//
|
|
1104
|
+
// Blank + lock ALL issues (open and closed) to prevent bleed from old missions
|
|
1101
1105
|
const allIssuesJson = ghExec(`gh api repos/${repoSlug}/issues?state=all&per_page=100`);
|
|
1102
|
-
const allIssues = JSON.parse(allIssuesJson || "[]").filter((i) => !i.pull_request
|
|
1106
|
+
const allIssues = JSON.parse(allIssuesJson || "[]").filter((i) => !i.pull_request);
|
|
1107
|
+
let blanked = 0;
|
|
1103
1108
|
for (const issue of allIssues) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1109
|
+
const needsBlank = issue.title !== "unused github issue";
|
|
1110
|
+
const needsLock = !issue.locked;
|
|
1111
|
+
if (!needsBlank && !needsLock) continue;
|
|
1112
|
+
if (needsBlank) {
|
|
1113
|
+
console.log(` BLANK: issue #${issue.number} — "${issue.title}" → "unused github issue"`);
|
|
1114
|
+
if (!dryRun) {
|
|
1115
|
+
try {
|
|
1116
|
+
ghExec(
|
|
1117
|
+
`gh api repos/${repoSlug}/issues/${issue.number} -X PATCH -f title="unused github issue" -f body="unused github issue"`,
|
|
1118
|
+
);
|
|
1119
|
+
// Remove all labels
|
|
1120
|
+
try {
|
|
1121
|
+
ghExec(`gh api repos/${repoSlug}/issues/${issue.number}/labels -X DELETE`);
|
|
1122
|
+
} catch { /* no labels to remove */ }
|
|
1123
|
+
blanked++;
|
|
1124
|
+
initChanges++;
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
console.log(` WARN: Failed to blank issue #${issue.number}: ${err.message}`);
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
blanked++;
|
|
1130
|
+
initChanges++;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (needsLock) {
|
|
1134
|
+
console.log(` LOCK: issue #${issue.number}`);
|
|
1135
|
+
if (!dryRun) {
|
|
1136
|
+
try {
|
|
1137
|
+
ghExec(
|
|
1138
|
+
`gh api repos/${repoSlug}/issues/${issue.number}/lock -X PUT -f lock_reason=resolved`,
|
|
1139
|
+
);
|
|
1140
|
+
initChanges++;
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
console.log(` WARN: Failed to lock issue #${issue.number}: ${err.message}`);
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1110
1145
|
initChanges++;
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
console.log(` WARN: Failed to lock issue #${issue.number}: ${err.message}`);
|
|
1113
1146
|
}
|
|
1114
|
-
} else {
|
|
1115
|
-
initChanges++;
|
|
1116
1147
|
}
|
|
1117
1148
|
}
|
|
1118
|
-
if (
|
|
1149
|
+
if (blanked > 0) console.log(` Blanked ${blanked} issue(s)`);
|
|
1150
|
+
if (allIssues.length === 0) console.log(" No issues to process");
|
|
1119
1151
|
} catch (err) {
|
|
1120
1152
|
console.log(` WARN: Issue cleanup failed: ${err.message}`);
|
|
1121
1153
|
}
|
package/package.json
CHANGED
|
@@ -35,13 +35,14 @@ import { parse as parseToml } from "smol-toml";
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
// Keys whose paths are writable by agents
|
|
38
|
-
const WRITABLE_KEYS = ["source", "tests", "features", "dependencies", "docs", "readme", "examples", "web"];
|
|
38
|
+
const WRITABLE_KEYS = ["source", "tests", "behaviour", "features", "dependencies", "docs", "readme", "examples", "web"];
|
|
39
39
|
|
|
40
40
|
// Default paths — every key that task handlers might access
|
|
41
41
|
const PATH_DEFAULTS = {
|
|
42
42
|
mission: "MISSION.md",
|
|
43
43
|
source: "src/lib/",
|
|
44
44
|
tests: "tests/unit/",
|
|
45
|
+
behaviour: "tests/behaviour/",
|
|
45
46
|
features: "features/",
|
|
46
47
|
docs: "docs/",
|
|
47
48
|
examples: "examples/",
|
|
@@ -34,6 +34,65 @@ const TASKS = {
|
|
|
34
34
|
"supervise": supervise,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Build mission-complete metrics array for the intentïon.md dashboard.
|
|
39
|
+
*/
|
|
40
|
+
function buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount) {
|
|
41
|
+
const openIssues = featureIssueCount + maintenanceIssueCount;
|
|
42
|
+
const budgetCap = config.transformationBudget || 0;
|
|
43
|
+
const resolvedCount = result.resolvedCount || 0;
|
|
44
|
+
const missionComplete = existsSync("MISSION_COMPLETE.md");
|
|
45
|
+
const missionFailed = existsSync("MISSION_FAILED.md");
|
|
46
|
+
|
|
47
|
+
// Count open PRs from result if available
|
|
48
|
+
const openPrs = result.openPrCount || 0;
|
|
49
|
+
|
|
50
|
+
const metrics = [
|
|
51
|
+
{ metric: "Open issues", value: String(openIssues), target: "0", status: openIssues === 0 ? "MET" : "NOT MET" },
|
|
52
|
+
{ metric: "Open PRs", value: String(openPrs), target: "0", status: openPrs === 0 ? "MET" : "NOT MET" },
|
|
53
|
+
{ metric: "Issues closed by review (RESOLVED)", value: String(resolvedCount), target: ">= 1", status: resolvedCount >= 1 ? "MET" : "NOT MET" },
|
|
54
|
+
{ metric: "Transformation budget used", value: `${cumulativeCost}/${budgetCap}`, target: budgetCap > 0 ? `< ${budgetCap}` : "unlimited", status: budgetCap > 0 && cumulativeCost >= budgetCap ? "EXHAUSTED" : "OK" },
|
|
55
|
+
{ metric: "Cumulative transforms", value: String(cumulativeCost), target: ">= 1", status: cumulativeCost >= 1 ? "MET" : "NOT MET" },
|
|
56
|
+
{ metric: "Mission complete declared", value: missionComplete ? "YES" : "NO", target: "—", status: "—" },
|
|
57
|
+
{ metric: "Mission failed declared", value: missionFailed ? "YES" : "NO", target: "—", status: "—" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return metrics;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build mission-complete readiness narrative from metrics.
|
|
65
|
+
*/
|
|
66
|
+
function buildMissionReadiness(metrics) {
|
|
67
|
+
const openIssues = parseInt(metrics.find((m) => m.metric === "Open issues")?.value || "0", 10);
|
|
68
|
+
const openPrs = parseInt(metrics.find((m) => m.metric === "Open PRs")?.value || "0", 10);
|
|
69
|
+
const resolved = parseInt(metrics.find((m) => m.metric === "Issues closed by review (RESOLVED)")?.value || "0", 10);
|
|
70
|
+
const missionComplete = metrics.find((m) => m.metric === "Mission complete declared")?.value === "YES";
|
|
71
|
+
const missionFailed = metrics.find((m) => m.metric === "Mission failed declared")?.value === "YES";
|
|
72
|
+
|
|
73
|
+
if (missionComplete) {
|
|
74
|
+
return "Mission has been declared complete.";
|
|
75
|
+
}
|
|
76
|
+
if (missionFailed) {
|
|
77
|
+
return "Mission has been declared failed.";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const conditionsMet = openIssues === 0 && openPrs === 0 && resolved >= 1;
|
|
81
|
+
const parts = [];
|
|
82
|
+
|
|
83
|
+
if (conditionsMet) {
|
|
84
|
+
parts.push("Mission complete conditions ARE met.");
|
|
85
|
+
parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) closed by review as RESOLVED.`);
|
|
86
|
+
} else {
|
|
87
|
+
parts.push("Mission complete conditions are NOT met.");
|
|
88
|
+
if (openIssues > 0) parts.push(`${openIssues} open issue(s) remain.`);
|
|
89
|
+
if (openPrs > 0) parts.push(`${openPrs} open PR(s) remain.`);
|
|
90
|
+
if (resolved < 1) parts.push("No issues have been closed by review as RESOLVED yet.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
95
|
+
|
|
37
96
|
async function run() {
|
|
38
97
|
try {
|
|
39
98
|
// Parse inputs
|
|
@@ -173,6 +232,10 @@ async function run() {
|
|
|
173
232
|
|
|
174
233
|
const closingNotes = result.closingNotes || generateClosingNotes(limitsStatus);
|
|
175
234
|
|
|
235
|
+
// Build mission-complete metrics and readiness narrative
|
|
236
|
+
const missionMetrics = buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount);
|
|
237
|
+
const missionReadiness = buildMissionReadiness(missionMetrics);
|
|
238
|
+
|
|
176
239
|
// Log to intentïon.md (commit-if-changed excludes this on non-default branches)
|
|
177
240
|
if (intentionFilepath) {
|
|
178
241
|
logActivity({
|
|
@@ -195,6 +258,8 @@ async function run() {
|
|
|
195
258
|
contextNotes: result.contextNotes,
|
|
196
259
|
limitsStatus,
|
|
197
260
|
promptBudget: result.promptBudget,
|
|
261
|
+
missionReadiness,
|
|
262
|
+
missionMetrics,
|
|
198
263
|
closingNotes,
|
|
199
264
|
transformationCost,
|
|
200
265
|
narrative: result.narrative,
|
|
@@ -35,6 +35,8 @@ import * as core from "@actions/core";
|
|
|
35
35
|
* @param {string} [options.closingNotes] - Auto-generated limit concern notes
|
|
36
36
|
* @param {number} [options.transformationCost] - Transformation cost for this entry (0 or 1)
|
|
37
37
|
* @param {string} [options.narrative] - LLM-generated narrative description of the change
|
|
38
|
+
* @param {string} [options.missionReadiness] - Mission-complete readiness narrative
|
|
39
|
+
* @param {Array} [options.missionMetrics] - Mission metrics entries { metric, value, target, status }
|
|
38
40
|
*/
|
|
39
41
|
export function logActivity({
|
|
40
42
|
filepath,
|
|
@@ -56,6 +58,8 @@ export function logActivity({
|
|
|
56
58
|
contextNotes,
|
|
57
59
|
limitsStatus,
|
|
58
60
|
promptBudget,
|
|
61
|
+
missionReadiness,
|
|
62
|
+
missionMetrics,
|
|
59
63
|
closingNotes,
|
|
60
64
|
transformationCost,
|
|
61
65
|
narrative,
|
|
@@ -108,6 +112,18 @@ export function logActivity({
|
|
|
108
112
|
parts.push(`| ${pb.section} | ${pb.size} chars | ${pb.files || "—"} | ${pb.notes || ""} |`);
|
|
109
113
|
}
|
|
110
114
|
}
|
|
115
|
+
if (missionReadiness) {
|
|
116
|
+
parts.push("", "### Mission-Complete Readiness");
|
|
117
|
+
parts.push(missionReadiness);
|
|
118
|
+
}
|
|
119
|
+
if (missionMetrics && missionMetrics.length > 0) {
|
|
120
|
+
parts.push("", "### Mission Metrics");
|
|
121
|
+
parts.push("| Metric | Value | Target | Status |");
|
|
122
|
+
parts.push("|--------|-------|--------|--------|");
|
|
123
|
+
for (const m of missionMetrics) {
|
|
124
|
+
parts.push(`| ${m.metric} | ${m.value} | ${m.target} | ${m.status} |`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
111
127
|
if (closingNotes) {
|
|
112
128
|
parts.push("", "### Closing Notes");
|
|
113
129
|
parts.push(closingNotes);
|
|
@@ -337,23 +337,10 @@ export async function discussions(context) {
|
|
|
337
337
|
// Guard: never dispatch workflows from the SDK repo itself (agentic-lib)
|
|
338
338
|
const isSdkRepo = process.env.GITHUB_REPOSITORY === "xn-intenton-z2a/agentic-lib";
|
|
339
339
|
|
|
340
|
-
// Request supervisor evaluation
|
|
340
|
+
// Request supervisor evaluation — dispatch is handled by the bot workflow's
|
|
341
|
+
// dispatch-supervisor job, so we just log the action here to avoid double dispatch.
|
|
341
342
|
if (action === "request-supervisor") {
|
|
342
|
-
|
|
343
|
-
core.info("Skipping supervisor dispatch — running in SDK repo");
|
|
344
|
-
} else {
|
|
345
|
-
try {
|
|
346
|
-
await octokit.rest.actions.createWorkflowDispatch({
|
|
347
|
-
...context.repo,
|
|
348
|
-
workflow_id: "agentic-lib-workflow.yml",
|
|
349
|
-
ref: "main",
|
|
350
|
-
inputs: { message: actionArg || "Discussion bot referral" },
|
|
351
|
-
});
|
|
352
|
-
core.info(`Dispatched supervisor with message: ${actionArg}`);
|
|
353
|
-
} catch (err) {
|
|
354
|
-
core.warning(`Failed to dispatch supervisor: ${err.message}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
343
|
+
core.info(`Supervisor requested with message: ${actionArg || "Discussion bot referral"} (dispatch handled by bot workflow)`);
|
|
357
344
|
}
|
|
358
345
|
|
|
359
346
|
// Stop automation
|
|
@@ -520,11 +520,12 @@ async function executeDispatch(octokit, repo, actionName, params, ctx) {
|
|
|
520
520
|
return `dispatched:${workflowFile}`;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
async function executeCreateIssue(octokit, repo, params) {
|
|
523
|
+
async function executeCreateIssue(octokit, repo, params, ctx) {
|
|
524
524
|
const title = params.title || "Untitled issue";
|
|
525
525
|
const labels = params.labels ? params.labels.split(",").map((l) => l.trim()) : ["automated"];
|
|
526
526
|
|
|
527
527
|
// Dedup guard: skip if a similarly-titled issue was closed in the last hour
|
|
528
|
+
// Exclude issues closed before the init timestamp (cross-scenario protection)
|
|
528
529
|
try {
|
|
529
530
|
const { data: recent } = await octokit.rest.issues.listForRepo({
|
|
530
531
|
...repo,
|
|
@@ -533,12 +534,14 @@ async function executeCreateIssue(octokit, repo, params) {
|
|
|
533
534
|
direction: "desc",
|
|
534
535
|
per_page: 5,
|
|
535
536
|
});
|
|
537
|
+
const initTimestamp = ctx?.initTimestamp;
|
|
536
538
|
const titlePrefix = title.toLowerCase().substring(0, 30);
|
|
537
539
|
const duplicate = recent.find(
|
|
538
540
|
(i) =>
|
|
539
541
|
!i.pull_request &&
|
|
540
542
|
i.title.toLowerCase().includes(titlePrefix) &&
|
|
541
|
-
Date.now() - new Date(i.closed_at).getTime() < 3600000
|
|
543
|
+
Date.now() - new Date(i.closed_at).getTime() < 3600000 &&
|
|
544
|
+
(!initTimestamp || new Date(i.closed_at) > new Date(initTimestamp)),
|
|
542
545
|
);
|
|
543
546
|
if (duplicate) {
|
|
544
547
|
core.info(`Skipping duplicate issue (similar to recently closed #${duplicate.number})`);
|
|
@@ -31,6 +31,10 @@ regardless — each failed attempt consumes transformation budget, so get it rig
|
|
|
31
31
|
**Both unit tests AND behaviour tests must pass.** The project runs `npm test` (unit tests) and
|
|
32
32
|
`npm run test:behaviour` (Playwright). Both are gated — your fix must pass both.
|
|
33
33
|
|
|
34
|
+
**IMPORTANT**: The project uses `"type": "module"` in package.json. All files must use ESM syntax:
|
|
35
|
+
- `import { test, expect } from "@playwright/test"` (NOT `const { test, expect } = require(...)`)
|
|
36
|
+
- `import { execSync } from "child_process"` (NOT `const { execSync } = require(...)`)
|
|
37
|
+
|
|
34
38
|
## Merge Conflict Resolution
|
|
35
39
|
|
|
36
40
|
When resolving merge conflicts (files containing <<<<<<< / ======= / >>>>>>> markers):
|
|
@@ -30,6 +30,10 @@ the website code that uses it, the web tests that check for its output, and any
|
|
|
30
30
|
that depend on it. A partial change that updates the library but not the tests will fail — and there will
|
|
31
31
|
be a full test run after your changes regardless, consuming budget on each failure.
|
|
32
32
|
|
|
33
|
+
**IMPORTANT**: The project uses `"type": "module"` in package.json. All files must use ESM syntax:
|
|
34
|
+
- `import { test, expect } from "@playwright/test"` (NOT `const { test, expect } = require(...)`)
|
|
35
|
+
- `import { execSync } from "child_process"` (NOT `const { execSync } = require(...)`)
|
|
36
|
+
|
|
33
37
|
## Tests Must Pass
|
|
34
38
|
|
|
35
39
|
Your changes MUST leave all existing tests passing. If you change function signatures, return values, or
|