Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ new version heading in the same commit.

## [Unreleased]

## [0.447.2] - 2026-09-18
### Fixed
- **An app review card no longer outlives the review.** The Apps console acts on the app, never on the
card, so nothing ever closed an `app.proposed` card: an app published weeks ago still read "awaiting
review" in Needs you, with no button to clear it (live: five cards on one tenant, the oldest two months
old — the same bug the skills lane fixed in v0.404.1). Publishing an app now closes its card, deleting
one closes it too, and a boot sweep heals cards the old routes left behind, re-deriving each from the
apps store — published reads as approved, gone as rejected. A card whose app is still sitting
unpublished is left open, because that one is genuinely pending; unpublishing doesn't close a card
either, since an agent's edit to a live app unpublishes it precisely to ask for a re-review. Every
review card also gained a **Dismiss** button — the manual escape hatch for one whose subject was
handled outside the console (a per-member hide, exactly what "dismiss all" already did in bulk; the
proposal itself stays open on its own page for everyone else).
**For users:** Publishing or deleting an app now clears its "awaiting review" card from your Inbox, old
stragglers are cleaned up on the next restart, and any review card can be dismissed on the spot.
[Open Inbox](#/inbox)

## [0.447.1] - 2026-09-17
### Fixed
- **The unattended-run brief no longer tells agents three things that stopped being true.** It said
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.447.1",
"version": "0.447.2",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand All @@ -27,7 +27,7 @@
"check-deps": "bash scripts/install-deps.sh --check",
"dev": "ts-node src/cli.ts serve",
"demo:dev": "ts-node src/demo.ts",
"test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs",
"test:governance": "node scripts/version-sync-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs",
"test:alert-staleness": "node scripts/alert-staleness-test.cjs",
"test:deps": "node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-login-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs",
"test:dm-continuity": "node scripts/dm-continuity-test.cjs",
Expand Down
103 changes: 103 additions & 0 deletions scripts/app-card-close-test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env node
/* An `app.proposed` review card must not outlive the review.
*
* The Apps routes act on the APP (publish / delete), never on the card, so before this fix nothing ever
* closed one: a card whose app had been live for weeks still read "awaiting review" in Needs you — five
* of them on one live tenant, the oldest two months old. Pins the three ways out:
* 1. publishing the app closes its card (publishing IS what the card asks for);
* 2. deleting the app closes it too — it can never be acted on again;
* 3. UNPUBLISHING does not: an agent's edit to a live app unpublishes it and raises a re-review card,
* and that card is still waiting for a human;
* 4. the boot sweep heals cards left open by the old routes, and leaves a genuinely pending one alone.
*/
const fs = require('fs');
const os = require('os');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'aos-app-card-test-'));
process.env.AGENT_OS_HOME = HOME;
process.env.AGENT_OS_TENANT = 'testco';
process.env.AOS_NO_TTYD = '1';

let pass = 0, fail = 0;
const assert = (c, name, d) => c ? (pass++, console.log(` \x1b[32m✓\x1b[0m ${name}`)) : (fail++, console.log(` \x1b[31m✗ ${name}\x1b[0m${d !== undefined ? ' — ' + JSON.stringify(d).slice(0, 400) : ''}`));

(async () => {
const { createHttpServer } = require(path.join(ROOT, 'dist/server.js'));
const { TenantRegistry } = require(path.join(ROOT, 'dist/tenant-registry.js'));
const registry = new TenantRegistry(ROOT, 0, path.join(ROOT, 'config/agent-os.config.json'));
registry.bootAll();
const { os: aos, tm } = registry.default();
const server = createHttpServer(registry);
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;

const owner = aos.team.listMembers().find((m) => m.role === 'owner');
const cookie = `aos_sid=${aos.team.createSession(owner.id)}`;
const get = async (p) => (await fetch(base + p, { headers: { cookie } })).json();
const post = async (p) => (await fetch(base + p, { method: 'POST', headers: { cookie, 'content-type': 'application/json' }, body: '{}' })).json();
const del = async (p) => (await fetch(base + p, { method: 'DELETE', headers: { cookie } })).json();

// The NEWEST card for a slug, straight from the row — `status` is what "Needs you" keys on. Newest
// because one app raises several cards over its life (proposed, then re-review after each agent edit).
const cardStatus = (slug) => {
const rows = aos.db.prepare(`SELECT args, status FROM messages WHERE type = 'app.proposed' ORDER BY created_at DESC, id DESC`).all();
const row = rows.find((r) => { try { return JSON.parse(r.args || '{}').slug === slug; } catch { return false; } });
return row ? row.status : null;
};
const propose = (slug) => {
aos.apps.scaffold(slug, { name: slug, createdBy: 'agent:app-builder' });
tm.postAppCard({ slug, agent: 'app-builder', title: `App proposed — ${slug}`, body: `app-builder built "${slug}". Review it and publish it to make it live.` });
};

console.log('\n\x1b[1m1) publishing the app closes its card\x1b[0m');
propose('voice-notes');
assert(cardStatus('voice-notes') === 'open', 'the card starts open — it is the review ask', cardStatus('voice-notes'));
const inbox = await get('/api/messages');
assert(inbox.some((m) => m.type === 'app.proposed' && m.args?.slug === 'voice-notes'), 'and an owner sees it in the inbox feed');
const pub = await post('/api/apps/voice-notes/publish');
assert(pub.ok === true && pub.app.published === true, 'publish succeeds', pub.error ?? pub.app?.published);
assert(cardStatus('voice-notes') === 'approved', 'the card is resolved by the act it was asking for', cardStatus('voice-notes'));

console.log('\n\x1b[1m2) unpublishing does NOT — a re-review card is still waiting\x1b[0m');
tm.postAppCard({ slug: 'voice-notes', agent: 'app-builder', title: 'App edited — voice-notes', body: 'app-builder changed the live app; re-publish to make the change live.' });
const unpub = await post('/api/apps/voice-notes/unpublish');
assert(unpub.ok === true && unpub.app.published === false, 'unpublish succeeds', unpub.error);
assert(cardStatus('voice-notes') === 'open', 'the re-review card stays open — nobody has reviewed the edit yet', cardStatus('voice-notes'));
await post('/api/apps/voice-notes/publish');
assert(cardStatus('voice-notes') === 'approved', 're-publishing closes it', cardStatus('voice-notes'));

console.log('\n\x1b[1m3) deleting the app closes its card too\x1b[0m');
propose('scratch-app');
const gone = await del('/api/apps/scratch-app');
assert(gone.ok === true, 'delete succeeds', gone.error);
assert(cardStatus('scratch-app') === 'rejected', 'the card cannot be acted on again, so it is closed', cardStatus('scratch-app'));

console.log('\n\x1b[1m4) the boot sweep heals cards the old routes left behind\x1b[0m');
// Exactly the live shape: three cards, whose apps were published / deleted / left proposed outside the
// console, all still marked open — what a tenant upgrading into this fix actually carries.
propose('stale-published'); await post('/api/apps/stale-published/publish');
propose('stale-deleted'); await del('/api/apps/stale-deleted');
propose('still-pending');
aos.db.prepare(`UPDATE messages SET status = 'open' WHERE type = 'app.proposed'`).run();
server.close();
await registry.stopAll?.();

const registry2 = new TenantRegistry(ROOT, 0, path.join(ROOT, 'config/agent-os.config.json'));
registry2.bootAll();
const aos2 = registry2.default().os;
const status2 = (slug) => {
const rows = aos2.db.prepare(`SELECT args, status FROM messages WHERE type = 'app.proposed' ORDER BY created_at DESC, id DESC`).all();
const row = rows.find((r) => { try { return JSON.parse(r.args || '{}').slug === slug; } catch { return false; } });
return row ? row.status : null;
};
assert(status2('stale-published') === 'approved', 'a card whose app is live reads as approved', status2('stale-published'));
assert(status2('stale-deleted') === 'rejected', 'a card whose app is gone reads as rejected', status2('stale-deleted'));
assert(status2('still-pending') === 'open', 'a card whose app is still sitting unpublished is left alone — that one IS pending', status2('still-pending'));
await registry2.stopAll?.();

fs.rmSync(HOME, { recursive: true, force: true });
console.log(`\n${fail ? '\x1b[31m' : '\x1b[32m'}${pass} passed, ${fail} failed\x1b[0m`);
process.exit(fail ? 1 : 0);
})();
Loading
Loading