@nanocollective/roster 0.1.0-alpha.41 → 0.1.0-alpha.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.41",
3
+ "version": "0.1.0-alpha.42",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,23 @@
16
16
  "engines": {
17
17
  "node": ">=20"
18
18
  },
19
+ "scripts": {
20
+ "build": "tsup src/cli.ts --format esm --target node20 --clean",
21
+ "dev": "tsx src/cli.ts",
22
+ "test": "tsx --test test/*.test.ts",
23
+ "test:all": "pnpm test:format && pnpm test:lint && pnpm test:types && pnpm test:knip && pnpm test",
24
+ "test:ava:coverage": "c8 --reporter=text --reporter=json-summary tsx --test test/*.test.ts",
25
+ "test:format": "biome check --no-errors-on-unmatched .",
26
+ "test:lint": "biome lint .",
27
+ "test:lint:fix": "biome check --write .",
28
+ "test:types": "tsc --noEmit",
29
+ "test:knip": "knip",
30
+ "test:audit": "pnpm audit --audit-level=high",
31
+ "test:security": "semgrep scan --config auto --error",
32
+ "typecheck": "tsc --noEmit",
33
+ "format": "biome check --write .",
34
+ "prepublishOnly": "pnpm test:all && pnpm build"
35
+ },
19
36
  "devDependencies": {
20
37
  "@biomejs/biome": "^2.5.12",
21
38
  "@types/node": "^22.10.2",
@@ -35,24 +52,9 @@
35
52
  "url": "https://github.com/Nano-Collective/roster/issues"
36
53
  },
37
54
  "author": "Nano Collective",
55
+ "packageManager": "pnpm@11.0.9",
38
56
  "publishConfig": {
39
57
  "access": "public",
40
58
  "tag": "latest"
41
- },
42
- "scripts": {
43
- "build": "tsup src/cli.ts --format esm --target node20 --clean",
44
- "dev": "tsx src/cli.ts",
45
- "test": "tsx --test test/*.test.ts",
46
- "test:all": "pnpm test:format && pnpm test:lint && pnpm test:types && pnpm test:knip && pnpm test",
47
- "test:ava:coverage": "c8 --reporter=text --reporter=json-summary tsx --test test/*.test.ts",
48
- "test:format": "biome check --no-errors-on-unmatched .",
49
- "test:lint": "biome lint .",
50
- "test:lint:fix": "biome check --write .",
51
- "test:types": "tsc --noEmit",
52
- "test:knip": "knip",
53
- "test:audit": "pnpm audit --audit-level=high",
54
- "test:security": "semgrep scan --config auto --error",
55
- "typecheck": "tsc --noEmit",
56
- "format": "biome check --write ."
57
59
  }
58
- }
60
+ }
@@ -79,6 +79,14 @@ jobs:
79
79
  # secrets are not usable in a step-level `if`, so the presence check is hoisted here.
80
80
  env:
81
81
  HAS_PUBLIC_APP: ${{ secrets.PUBLIC_APP_ID != '' }}
82
+ # Nothing is listening once the agent ends its turn: the session is over and a background
83
+ # job dies with the runner. Claude Code moves any command past its timeout (ten minutes by
84
+ # default) into the background, and an agent told "you will be notified" ends its turn to
85
+ # wait, so a slow test suite cost the whole run, unpushed, reported as a success. With
86
+ # background tasks off a slow command stays in the foreground, and the higher cap lets a
87
+ # long gate finish inside the turn. timeout-minutes is still the real bound.
88
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
89
+ BASH_MAX_TIMEOUT_MS: "2700000"
82
90
 
83
91
  steps:
84
92
  # For the run record at the end. A job has no start time of its own to read back.
@@ -341,6 +349,37 @@ jobs:
341
349
  fi
342
350
  eval "$RUN"
343
351
 
352
+ # A session can exit cleanly without doing what it was asked: the success above only means
353
+ # the agent stopped. A mention is answered by a reply in its thread, or by the issue being
354
+ # closed when the request said to answer somewhere else. When neither happened the job
355
+ # fails, so the notice below tells the human instead of leaving them waiting.
356
+ #
357
+ # Never fails on its own account: if the issue cannot be read, nothing is claimed.
358
+ - name: Check the request was answered
359
+ id: answered
360
+ if: >-
361
+ inputs.kind == 'mention' && inputs.issue_number != ''
362
+ && (steps.session_action.outcome == 'success' || steps.session_cli.outcome == 'success')
363
+ env:
364
+ GH_TOKEN: ${{ steps.private.outputs.token }}
365
+ BOT: ${{ steps.private.outputs.app-slug }}[bot]
366
+ ISSUE: ${{ inputs.issue_number }}
367
+ run: |
368
+ set -uo pipefail
369
+ since=$(date -u -d "@$ROSTER_STARTED" +%Y-%m-%dT%H:%M:%SZ)
370
+ if ! state=$(gh api "repos/${{ github.repository }}/issues/$ISSUE" --jq .state) ||
371
+ ! replies=$(gh api --paginate "repos/${{ github.repository }}/issues/$ISSUE/comments?since=$since" \
372
+ --jq ".[] | select(.user.login == \"$BOT\" and .created_at >= \"$since\") | .id"); then
373
+ echo "::warning::could not read #$ISSUE, so whether it was answered is unknown"
374
+ exit 0
375
+ fi
376
+ if [ "$state" = "closed" ] || [ -n "$replies" ]; then
377
+ exit 0
378
+ fi
379
+ echo "unanswered=true" >> "$GITHUB_OUTPUT"
380
+ echo "::error::the session ended without replying on #$ISSUE or closing it"
381
+ exit 1
382
+
344
383
  # What this run was and what it cost, in the job summary and as an artifact with a stable
345
384
  # name, which is what the portal's Runs screen and `roster doctor` read back. Never fatal:
346
385
  # a missing record costs a row in a table, and failing the job over it would cost the run.
@@ -354,6 +393,7 @@ jobs:
354
393
  MODEL: ${{ steps.agent.outputs.model || inputs.model }}
355
394
  AGENT_OUTCOME: ${{ steps.session_action.outcome != 'skipped' && steps.session_action.outcome || steps.session_cli.outcome }}
356
395
  JOB_STATUS: ${{ job.status }}
396
+ UNANSWERED: ${{ steps.answered.outputs.unanswered }}
357
397
  RESULT_FILE: ${{ steps.session_action.outputs.execution_file || format('{0}/.roster-run/agent-result.json', github.workspace) }}
358
398
  run: node roster-ops/run-record.mjs --out .roster-run/run.json
359
399
 
@@ -383,6 +423,7 @@ jobs:
383
423
  BRAIN_DIR: ${{ steps.plan.outputs.brain_dir }}
384
424
  BRAIN_REPO: ${{ steps.plan.outputs.brain_repo || github.repository }}
385
425
  ISSUE: ${{ inputs.issue_number }}
426
+ UNANSWERED: ${{ steps.answered.outputs.unanswered }}
386
427
  RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
387
428
  run: |
388
429
  set -uo pipefail
@@ -402,6 +443,9 @@ jobs:
402
443
  fi
403
444
 
404
445
  body="This run did not finish, so there is no answer coming. [Run ${GITHUB_RUN_ID}]($RUN_URL) has the error."
446
+ if [ "$UNANSWERED" = "true" ]; then
447
+ body="This run stopped without replying here or closing the issue, so there is no answer coming. [Run ${GITHUB_RUN_ID}]($RUN_URL) shows where it stopped."
448
+ fi
405
449
  if [ -n "$APP_TOKEN" ] && GH_TOKEN="$APP_TOKEN" gh issue comment "$issue" --repo "$BRAIN_REPO" --body "$body"; then
406
450
  exit 0
407
451
  fi
@@ -14,6 +14,10 @@ question instead of doing work has wasted its slot.
14
14
  not stand still.
15
15
  - **Never end a run blocked.** If everything on the list is genuinely blocked, do the most useful
16
16
  unblocked thing you can find and say so in the report.
17
+ - **Finish inside the run.** Once you stop, the run is over: nothing will wake you, and anything
18
+ left running dies with it. Never end a turn to wait for a command; wait for it in the same call.
19
+ **Push your branch before a slow check** such as a full test suite, so the work survives if the
20
+ run is cut short, and push again once it passes.
17
21
 
18
22
  ## Choosing work
19
23
 
@@ -64,9 +64,11 @@ export function readResult(text) {
64
64
  * What happened, in one word.
65
65
  *
66
66
  * The agent step's own outcome when it ran. When it never ran, the job's status says whether
67
- * that was a cancel (a timeout is one) or a failure somewhere in the setup before it.
67
+ * that was a cancel (a timeout is one) or a failure somewhere in the setup before it. A mention
68
+ * the agent exited from cleanly without answering is not a success, whatever the agent says.
68
69
  */
69
- export function outcomeOf(agentOutcome, jobStatus) {
70
+ export function outcomeOf(agentOutcome, jobStatus, unanswered = false) {
71
+ if (agentOutcome === "success" && unanswered) return "unanswered";
70
72
  if (["success", "failure", "cancelled"].includes(agentOutcome)) return agentOutcome;
71
73
  if (jobStatus === "cancelled") return "cancelled";
72
74
  return "setup-failure";
@@ -79,7 +81,7 @@ export function buildRecord(env, resultText, now = Date.now()) {
79
81
  v: 1,
80
82
  staff: env.STAFF ?? "",
81
83
  kind: env.KIND ?? "",
82
- outcome: outcomeOf(env.AGENT_OUTCOME, env.JOB_STATUS),
84
+ outcome: outcomeOf(env.AGENT_OUTCOME, env.JOB_STATUS, env.UNANSWERED === "true"),
83
85
  started: started ? new Date(started * 1000).toISOString() : null,
84
86
  duration_s: started ? Math.max(0, Math.round(now / 1000 - started)) : null,
85
87
  agent: env.AGENT_ID || null,