Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Make the demo seed opt-in, default off, and add pnpm demo - #88

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in
Sep 1, 2026
Merged

Make the demo seed opt-in, default off, and add pnpm demo#88
os-warren merged 3 commits into
mainfrom
claude/issue-73-demo-seed-opt-in

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#73

Implements the five-item scope adjudicated on the card.

Duly is meant to be a general product, and a general product does not install 459 rows of a fictional manufacturer's org chart into every fresh deployment. Someone evaluating Duly for their own company wants an empty app to put their own duties into; someone evaluating the idea wants the demo. Those are two intentions, and now they are two commands. The login bug on a clean git clone && pnpm dev falls out of that for free — with no sys_user rows in the default seed the database is zero-user at kernel:ready, so plugin-auth mints the dev admin exactly as documented.

What changed

Scope itemWhere
1. Gate dulySeeds on an explicit opt-in, default off, genuinely emptysrc/data/index.ts — the demo array is exported as demoSeeds, and dulySeeds is demoSeedRequested() ? demoSeeds : []. The gate wraps the whole array rather than filtering rows, so "off" is empty, not "empty of users".
2. pnpm demo, working on a clean checkout in one commandscripts/demo.mjs + package.json. Boots once quietly with the flag off, verifies the handover with a real sign-in, stops, then boots with the flag on in the foreground.
3. README documents both as first-class choicesREADME.md — a two-row table under Quick start, no mention of the platform interaction.
4. A test that fails if the default seed is non-emptytest/demo-seed-opt-in.test.ts (12 cases). test/seed.test.ts now opts in explicitly.
5. A comment at the gate naming objectstack#14157src/data/index.ts, including what collapses when it lands: pnpm demo becomes a single boot, and the gate stays.

Two things worth a reviewer's attention

Both dev and demo now boot with --compile. The seed is baked into dist/objectstack.json at compile time, and os dev's --compile defaults to false — it auto-compiles only when the artifact is missing. Without this, pnpm demo's second boot would serve the artifact its own priming boot just built (the one with no seed) and print success over an empty app; and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Measured: default build → 0 datasets / 0 rows in the artifact; DULY_DEMO_SEED=1 build → 10 datasets / 459 rows.

The handover is verified, not assumed.scripts/demo.mjs polls POST /api/v1/auth/sign-in/email against the priming server until it returns 200, rather than watching for a log line or sleeping. A seeded database with no login is worse than the bug it replaces because it looks like it worked, so the failure path is loud, names the remedy, and never reaches the second boot.

Verification

Every run below deleted .objectstack/data (and dist/) immediately before booting, since a directory that already holds a minted admin makes both paths look fine whatever the code says — the exact mistake that hid this for a round. Servers ran on ports 3173-3175; port 3000 belongs to another agent.

Acceptance — pnpm dev on an empty database

### deleting the data directory immediately before this run
exists after delete? NO
### ready after 11s
🔑 Dev admin: admin@objectos.ai / admin123 ← printed; no `Seeds:` line at all
sign-in HTTP 200
sys_user -> 1 rows duly_task -> 0 rows duly_duty -> 0 rows
duly_catalog_item -> 0 rows duly_log_entry -> 0 rows

Acceptance — pnpm demo on an empty database, one command

### deleting the data directory immediately before this run
exists after delete? NO
### demo server ready 24s after `pnpm demo` started (elapsed 26s)
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
sign-in HTTP 200
sys_business_unit 6 · sys_user 13 · duly_catalog_item 20 · duly_duty 31
duly_assignment 2 · duly_task 186 · duly_log_entry 15 → 273 distinct rows

273 read back against 459 declared is the expected shape, not a shortfall: the two mode: 'update' backdate passes re-walk the same 186 task rows (273 + 186 = 459), which is what test/seed.test.ts has always asserted.

Reverse verification — the new guard actually goes red. Gate mutated on disk to export const dulySeeds: Seed[] = demoSeeds; (drift back into the default path), confirmed on disk by grep before running (gated=0 drift=1), restored by an EXIT trap. No rebuild is involved: both suites read the TypeScript sources, and seed.test.ts already points artifactPath at a file that cannot exist so no dist/ artifact is ever loaded. Result — 9 failed | 33 passed, all nine in demo-seed-opt-in.test.ts; seed.test.ts stayed green, which is the correct direction (the demo still lands, it is only the default that broke). Tree verified clean afterwards (0 modified).

Reverse verification — the loud failure path.scripts/demo.mjs mutated to a 20s priming budget and a --no-seed-admin priming boot, so no admin can ever be minted:

### pnpm demo EXIT=1
1/2 preparing an admin account (quiet, a few seconds)…
❌ pnpm demo failed — no account could sign in after 20s, so the demo was NOT loaded.
... Start over: rm -rf .objectstack/data && pnpm demo
Nothing was seeded by this run — the database is exactly as it was.
### did the demo ever boot? Seeds: lines in log = 0

Worth recording: the first attempt at this mutated only the environment (OS_SEED_ADMIN=0) and the handover succeededos dev sets OS_SEED_ADMIN itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. That is an ineffective mutation that reads as a pass; the mutation had to move onto the spawn arguments.

Gates, all four on the pushed commit b2df2ae, after the reverse verifications had been restored (tree: 0 modified):

validate exit=0 | ✓ Validation passed (475ms)
typecheck exit=0 | > tsc --noEmit
test exit=0 | Test Files 21 passed (21) Tests 552 passed (552)
build exit=0 | ✓ Build complete (580ms)

The one validate warning is the expected hierarchy-security capability-provider notice AGENTS.md says not to silence.

Deliberately not included

No changeset. This repo has no changeset mechanism — no .changeset/ in the tree or anywhere in git log --all, no @changesets/* dependency, no script, no mention in AGENTS.md. Adding one would mint a mechanism nothing reads.

Generated by Claude Code


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 10:08
Duly is meant to be a general product, and a general product does not
install 459 rows of a fictional manufacturer's org chart into every fresh
deployment. Someone evaluating Duly for their own company wants an empty
app to put their own duties into; someone evaluating the idea wants the
demo. Those are two intentions, and now they are two commands.
- `src/data/index.ts` gates the whole demo array on `DULY_DEMO_SEED`.
Off means genuinely empty, not "empty of users".
- `scripts/demo.mjs` (`pnpm demo`) sequences the two boots this needs on a
brand-new database and VERIFIES the handover with a real sign-in, so a
half-finished run fails loudly instead of looking like it worked.
- Both `dev` and `demo` boot with `--compile`: the gate is read at compile
time and `os dev` otherwise reuses whichever artifact is on disk.
- `test/demo-seed-opt-in.test.ts` fails if the default path stops being
empty; `test/seed.test.ts` now opts in explicitly.
- README documents the two commands as first-class choices.
The login bug on a clean `git clone && pnpm dev` falls out of this for
free: with no `sys_user` rows in the default seed the database is
zero-user at `kernel:ready`, so `plugin-auth` mints the dev admin exactly
as documented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — and I ran the acceptance myself, from empty data directories. Merging.

This card hid for a whole round because a reused .objectstack/data makes both paths look fine, so I did not take the report's word for it.

pnpm dev, after rm -rf .objectstack/data dist, data dir exists? NO:

🔑 Dev admin: admin@objectos.ai / admin123
(no "Seeds:" line at all)
POST /api/v1/auth/sign-in/email → 200

pnpm demo, same wipe, data dir exists? NO, one command:

1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready.
2/2 starting Duly with the demo organisation loaded…
🔑 Dev admin: admin@objectos.ai / admin123
Seeds: ai.objectstack.duly 459 rows
POST /api/v1/auth/sign-in/email → 200 · duly_task, duly_duty, sys_user all populated

A clean git clone && pnpm dev now works. That was the whole point.

Gates on the head merged with current main (this branch was cut before #80, #81, #83, #85 and #87 landed): validate 0, typecheck 0, test 0 — Test Files 23 passed, Tests 599 passedbuild 0, no conflicts.

The finding beyond the scope is the one that would have sunk this

os dev --compile defaults to false and auto-compiles only when dist/objectstack.json is missing, while the seed is baked in at compile time. Without --compile on both scripts, pnpm demo's second boot would serve the seedless artifact its own priming boot had just built — printing every success message over an empty app — and a pnpm dev after a pnpm demo would serve the demo artifact, so the default would not be a default. Catching that is the difference between this working and this appearing to work.

Two more things worth recording:

  • The gate wraps the whole array (dulySeeds = demoSeedRequested() ? demoSeeds : []) rather than filtering rows, so "off" is genuinely empty rather than empty-of-users. That was the distinction scope item 1 turned on, and it is easy to get wrong in a way nothing would catch.
  • The handover is verified by a real sign-in returning 200, not by a log line or a sleep. The failure path exits 1, names the remedy, prints the priming log tail, and never reaches the second boot — so a broken priming step cannot produce a seeded database with no login, which is the exact state this card exists to prevent.

And the ablation note about the first mutation attempt is the most useful paragraph in the report: mutating OS_SEED_ADMIN=0 in the environment did not break the handover, because os dev sets that variable itself in its serve child from its own --seed-admin flag, so a parent-side value never reaches plugin-auth. An ineffective mutation that reads as a passing test — recognising it and moving the mutation onto the spawn arguments is exactly right.

test/seed.test.ts opting in explicitly rather than substituting the demo array by hand is also correct: it keeps the config's own wiring under test instead of testing a hand-assembled copy of it.

Noted on the report-marker sanitization — GitHub ate the HTML comment. Not worth another attempt; the second comment is legible.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A clean git clone && pnpm dev has no loginable account — make the demo seed opt-in, default off

1 participant

@os-warren