You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Move the four third-party Tuval programs (cron, shell, notify, workspace) into packages/ as opt-in @kampus packages #9374
Problem: The four Tuval programs reach @kampus/tuval through link:../../../../phoenix/apps/tuval, a path that only resolves on one laptop with both checkouts side by side. Their CI is red because of it, a user's config imports them by path instead of by name, and every program-side fix they surface has to be proven by hand across two repos. Verified at the source commit: all four package.json files carry that link:, and the .claude/phoenix symlink that makes it resolve is a root-level symlink in cansirin/monorepo pointing at this checkout — it appears nowhere inside the four packages, so that acceptance criterion is about the four link: lines, not about a path to hunt for.
Arc: Tuval programs (milestone 55) — the campaign that owns the programs themselves; this is the move that lets them be worked at all.
Appetite: 2 cycles
Rabbit-holes: (1) tsconfigpaths to workspace:* is not a straight swap. Each package maps its own name and its siblings' at src/index.ts so the .tuval/ fixture and the cross-package composition proof type-check with no build; a workspace:* dep resolves through exports to dist/index.d.ts, which does not exist until the upstream builds. packages/depo already answers this with a "development": "./src/index.ts" export condition — pick that or keep the paths, but decide it before the first package lands, because every later package inherits it. (2) cron-parser is in cansirin/monorepo's root catalog at ^5.10.1 and is absent from this repo's pnpm-workspace.yaml; a new catalog entry is part of the move, not a footnote. (3) effect via catalog:tuval is 4.0.0-rc.112 in both repos and @demlik/tea is 0.12.0 in both — those two are clean, do not re-pin them. (4) Adopting effect-tsgo diagnostics --strict per packages/depo runs over a program that pulls apps/tuval's whole reachable source tree in as raw TypeScript; cron, notify and workspace each carry a second window tsconfig on top of the node one, so it is seven projects, not four, and the source tsconfigs deliberately set exactOptionalPropertyTypes: false and lib: DOM for that reason. (5) The space-to-tab reformat over ~13.3k lines of TS will bury the real diff — land it as its own commit so the conversion stays reviewable. (6) @kampus/tuval is private: true and ships raw src/*.ts with no dist; four publishConfig.access: public packages depending on it are unpublishable until that changes, which is fine but should not surprise release tooling.
No-gos: No publish to npm, and no change that makes publishing a prerequisite. Nothing under apps/tuval changes — not a registration, not an import, not a features.* flag; the never-default-on rule from #9247/#9260 holds and grep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/src stays empty. No catalog page. No rewriting a program's behaviour while moving it: the test counts at the source commit are cron 76, shell 69, notify 87, workspace 156 — verified green on 2026-09-16 — and any child that changes a count owes a reason.
Epic — awaiting plan
plan-epic appends its plan and dependency topology below.
Original brief (verbatim)
What is true now
Four third-party Tuval programs live in cansirin/monorepo under packages/: @cansirin/tuval-cron (a scheduled job, cron-parser schedules, a window), @cansirin/tuval-shell (a job that runs a command), @cansirin/tuval-notify (delivers a job's result to a Discord/webhook/ntfy target, a window), @cansirin/tuval-workspace (an isolated worktree per task with an agent inside, a window, five persona configs). All four are opt-in: a user names them in ~/<redacted>; nothing in apps/tuval knows they exist.
They depend on @kampus/tuval through "link:../../../../phoenix/apps/tuval" because @kampus/tuval is private: true and unpublished. That link is the reason their CI is red, the reason a user's config imports them by path, and the reason none of the program-side fixes they surfaced (#9227, #9229, #9230, #9292, #9295, #9365) could be proven in one repo.
Decided today by both maintainers: they move into this monorepo under packages/, as their own packages, and stay opt-in. Not into apps/tuval, not on by default, no features.* flag; the never-default-on rule from #9247/#9260 holds.
What moves
from (cansirin/monorepo)
to
name
packages/tuval-cron
packages/tuval-cron
@kampus/tuval-cron
packages/tuval-shell
packages/tuval-shell
@kampus/tuval-shell
packages/tuval-notify
packages/tuval-notify
@kampus/tuval-notify
packages/tuval-workspace
packages/tuval-workspace
@kampus/tuval-workspace
Each keeps its src/, tests, README, .tuval/tuval.config.ts fixture, examples/ (workspace), and window build. Source of record for the copy: cansirin/monorepo main at the PR's stated commit; the PR body names it so the diff can be checked against it.
What changes in the move
@kampus/tuval becomes workspace:*; the link: and the .claude/phoenix symlink hack go away.
Package shape follows packages/depo: @kampus/ scope, repository.directory, publishConfig.access: public, catalog: for shared deps (effect, react, vitest, typescript), typecheck runs effect-tsgo diagnostics --strict, tabs per this repo's Biome.
Cross-package imports (notify reads BRIEF_PORT from cron) become workspace:* deps, not tsconfig paths.
Doc comments and READMEs that say @cansirin/ or "imports by path until npm" state the new truth.
~/<redacted> user configs will import @kampus/tuval-cron etc. once published; until then the READMEs say the path form.
Not in scope
Publishing to npm (waits on @kampus/tuval itself); a catalog page; any change under apps/tuval.
Acceptance criteria
The four packages exist under packages/ with the @kampus/ names, and pnpm -r --filter './packages/tuval-*' typecheck test build is green in this repo's CI
@kampus/tuval is a workspace:* dependency in each; no link: and no .claude/phoenix reference remains anywhere in the four packages
Every test the source packages carried passes here unchanged in count (cron 76, shell 69, notify 87, workspace 156 at the source commit; the PR states the source commit)
A boot proof: a ~/<redacted>-style config naming all four boots the desk from apps/tuval with no features.* flag and no change under apps/tuval; the PR shows the boot line
No package is registered, imported, or enabled by apps/tuval; grep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/src is empty
READMEs and doc comments name @kampus/… and describe the workspace dependency, not the path link
Plan (plan-epic)
Summary
Four Tuval programs — cron, shell, notify, workspace — move from cansirin/monorepo into this
repo's packages/ as @kampus/tuval-*, one package per child, keeping their src/, tests,
README, .tuval/ fixture, examples/ and window build. @kampus/tuval becomes a workspace:*
dependency and the link:../../../../phoenix/apps/tuval line disappears. They stay opt-in:
nothing under apps/tuval changes, and no features.* flag appears.
The first child is not just "cron". It is cron plus the conventions every later package
inherits — the cron-parser catalog entry, the cross-package resolution answer, the packages/depo
package shape, and the space-to-tab reformat landing as its own commit. That is why it is alone in
phase 1.
Problem & who has it
The four packages reach @kampus/tuval through link:../../../../phoenix/apps/tuval. That path
resolves on exactly one laptop, with both checkouts side by side and a root-level .claude/phoenix
symlink. Three things follow, and all three land on the two people who maintain these programs:
their CI is red, so nothing about them is proven by machine anywhere;
a user naming them in a config imports them by filesystem path, not by package name;
The cause is that @kampus/tuval is private: true and unpublished, so no registry version exists
to depend on. Publishing is not the answer available now. Co-location is.
What changes
Four new workspace members under packages/, each a straight move with a rename:
from (cansirin/monorepo)
to
name
size at source
packages/tuval-cron
packages/tuval-cron
@kampus/tuval-cron
2,194 LOC, 76 tests, one window
packages/tuval-shell
packages/tuval-shell
@kampus/tuval-shell
1,916 LOC, 69 tests, no window
packages/tuval-notify
packages/tuval-notify
@kampus/tuval-notify
3,157 LOC, 87 tests, one window
packages/tuval-workspace
packages/tuval-workspace
@kampus/tuval-workspace
5,444 LOC, 156 tests, one window, five persona examples
Per package: @kampus/ scope, repository.directory, publishConfig.access: public, catalog: for shared deps, tabs per this repo's Biome, @kampus/tuval at workspace:*.
Root-level, once, in the first child: cron-parser: ^5.10.1 enters pnpm-workspace.yaml's root
catalog (it is absent here today; verified by grep over pnpm-workspace.yaml at 718a7ad8).
Nothing under apps/tuval changes — not a registration, not an import, not a flag.
User stories
As a Tuval program maintainer, I want the four programs to live in this repo with @kampus/tuval
as a workspace:* dependency, so that their typecheck, tests and build run in this repo's CI
instead of on one laptop with two checkouts.
As a Tuval user, I want to name the programs by their @kampus/tuval-* package names in my
config and have the desk boot, so that I am not importing a program by a filesystem path that
only exists on someone else's machine.
As the author of the fifth Tuval program, I want the first package landed to have already settled
the cross-package resolution convention, the catalog entry and the package shape, so that I copy
one answer instead of inventing a second.
As an apps/tuval maintainer, I want these programs to stay opt-in and unreferenced by the app,
so that co-locating them turns nothing on by default.
Goal / non-goals
Goal. The four programs are workspace members of this repo, green in this repo's CI, reachable
by name, still opt-in, with every test they carried passing here at the same count.
Non-goals. Publishing to npm, and any change that makes publishing a prerequisite — the four
stay unpublishable until @kampus/tuval itself changes, and that is fine. A catalog page. Any
change under apps/tuval. Any rewrite of a program's behaviour while it moves: a child that changes
a test count owes a reason in its PR body.
Both round-1 decision questions are ruled; grill read reports the frontier clear.
R1.1 — the sibling edge carries the depo export condition, not the tsconfig paths it arrived
with. Ruled by @cansirin on 2026-09-17: "ok if its the current answer inside the repo, we can
use it imo."packages/tuval-cron's exports gains "development": "./src/index.ts"; shell and
notify declare "@kampus/tuval-cron": "workspace:*" and delete the sibling paths entry pointing
at ../tuval-cron/src/index.ts.
R1.2 — the four adopt effect-tsgo diagnostics --strict in this epic, across all seven
tsconfig projects (one node project per package, plus the second window tsconfig that cron,
notify and workspace each carry). Ruled by @cansirin on 2026-09-17: "we should try to stay closer
as much as possible to current repo rules yeah?" This reverses the draft's recommendation to
defer it.
Resolved questions
Does @kampus/tuval need a development export condition for the four to consume it? No. apps/tuval/package.json's exports map already points every subpath at raw ./src/*.ts and
ships no dist, so a workspace:* dependency resolves to source with no build. The export-condition
question is about the sibling edge (shell and notify importing cron), not this one — see Approach.
Does publish-isolation-guard red on four publishConfig.access: public packages depending on
a private @kampus/tuval? No. The guard derives its scope from publish.yml's release-tag
grammar, and publish.yml matches fabrika-cli and fabrika-pi only. A package outside that set
is outside the guard's scope.
Which shared pins are already correct?effect at catalog:tuval is 4.0.0-rc.112 in both
repos and @demlik/tea is 0.12.0 in both. Neither is re-pinned by any child.
Which dependency is genuinely missing here?cron-parser only. It is ^5.10.1 in the source
repo's root catalog and absent from this one's.
Approach
The cross-package edge is settled by R1.1 and lands in phase 1. In the source repo, shell and
notify reach cron through tsconfig paths pointing at ../tuval-cron/src/index.ts, because a workspace:* dependency resolves through exports to dist/index.d.ts, which does not exist until
cron builds. packages/depo already answers this here with a "development": "./src/index.ts"
export condition, and the founder ruled that answer in: cron's exports gains the development
condition, shell and notify declare "@kampus/tuval-cron": "workspace:*" and drop the sibling paths entry. That makes the dependency visible in the manifest — where the build graph and the
lockfile can see it — rather than in a compiler lens only.
Phase 1 is one package because the conventions are not divisible. Cron carries the catalog
entry, the export condition, the depo-shaped manifest, and the tab reformat pattern. Landing a second
package beside it means two answers to each of those questions and a reconciliation nobody asked for.
The reformat is its own commit. ~13.3k lines of TS convert from spaces to tabs for this repo's
Biome. Every child lands the move in one commit and the reformat in a second, so the real diff is
reviewable against the source commit the PR body names.
effect-tsgo diagnostics --strict is adopted in this epic, per R1.2. Every package's typecheck script becomes tsc -p <project> && effect-tsgo diagnostics --project <project> --strict
for each of its projects — the node project in all four, plus the window project cron, notify and
workspace each carry, so seven projects in total. Each package takes @effect/tsgo as a devDependency at catalog:, matching packages/depo and packages/fabrika-cli.
This is the expensive part of the move and the children carry it, not a follow-up. Each package
consumes @kampus/tuval as raw TypeScript, so the app's whole reachable source tree enters the
program and is checked under the package's options — which is why the source tsconfigs deliberately
set exactOptionalPropertyTypes: false and lib: ["ES2023","DOM","DOM.Iterable"]. A child that has
to relax or tighten an option to get a clean --strict run says which option and why in its PR body;
a child that cannot get one stops and reports rather than deleting the tsgo step it was ruled
into. Phase 1 lands the pattern on cron — the smallest of the four and the one with a window project
— so the three phase-2 children copy a proven invocation instead of each inventing one.
The boot proof is its own child. It is the only acceptance criterion that cannot be met by any
single package PR — it names all four in one config. Folding it into the last package to land would
make which package that is decide where the proof lives.
Acceptance criteria
The four packages exist under packages/ with @kampus/tuval-* names and pnpm -r --filter './packages/tuval-*' typecheck test build is green in this repo's CI
@kampus/tuval is a workspace:* dependency in each; no link: and no .claude/phoenix reference remains in any of the four packages
Test counts are unchanged from the source commit: cron 76, shell 69, notify 87, workspace 156; any child that changes a count states why in its PR body
A config naming all four boots the desk from apps/tuval with no features.* flag and no change under apps/tuval; the boot line is in the PR
grep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/src is empty
Every README and doc comment in the four names @kampus/… and describes the workspace dependency, not the path link
cron-parser is a root catalog entry and every one of the four consumes its shared deps through catalog: or catalog:tuval
Each of the four runs effect-tsgo diagnostics --strict over every project it carries — seven in total — and all seven are clean in this repo's CI; any tsconfig option changed to get there is named with its reason in the child's PR body
Testing strategy
The tests already exist and move with the code; the count is the contract. Each child runs its
package's own vitest run and states the observed count against the source number in its PR body.
Each child's pnpm typecheck is the ruled two-step — tsc -p then effect-tsgo diagnostics --strict — over every project in the package, and a child's PR body reports both as run, not just
the tsc half. Nothing new is written except where the move itself changes a fact — a fixture whose import
specifier changed, a test asserting a @cansirin/ renderer ref that is now @kampus/.
.tuval/ fixtures and examples/ are type-checked, not executed, and that is deliberate: they are
real user configs, held to compile so a README cannot promise a config that does not work.
The epic's own proof is the boot in child 5: a real config naming all four, the desk starting, and
the grep over apps/tuval/src coming back empty. That is the difference between four packages that
compile and four programs a user can actually reach.
Task-split rationale
One child per package, plus one assembly child for the proof no single package can give.
Phase 1 — cron alone. It is the only package no sibling imports, and it is where the shared
answers land (catalog entry, the ruled export condition, the ruled effect-tsgo --strict typecheck
invocation over a node project and a window project, depo shape, reformat pattern). Story 1 and
story 3.
Phase 2 — shell, notify, workspace, in parallel. Shell and notify both import cron (shell's compose.unit.test.ts reads cron, jobShape; notify's notify.unit.test.ts reads BRIEF_PORT),
so both require it. Workspace imports no sibling, but it requires cron for the convention: landing
it beside cron would have it invent the resolution answer a second time. Story 1 and story 3.
Phase 3 — the boot proof. Requires all four. Story 2 and story 4.
The one file three phase-2 children share is pnpm-lock.yaml. No two of them write any other
file: each touches only its own packages/tuval-* directory, and only cron touches pnpm-workspace.yaml. A lockfile collision is a rebase-and-regenerate, not a merge conflict to
resolve by hand, so they stay parallel rather than serialized behind each other.
Vocabulary impact
Four package names enter this repo's vocabulary: @kampus/tuval-cron, @kampus/tuval-shell, @kampus/tuval-notify, @kampus/tuval-workspace. The old @cansirin/ spellings survive only as
history — no doc, comment or fixture in this repo keeps them.
No new concept is introduced. "Program", "row", "window", "desk" and "port" already mean here what
they mean in the moved source; that is what makes this a move rather than a port.
Pitch
Problem: The four Tuval programs reach
@kampus/tuvalthroughlink:../../../../phoenix/apps/tuval, a path that only resolves on one laptop with both checkouts side by side. Their CI is red because of it, a user's config imports them by path instead of by name, and every program-side fix they surface has to be proven by hand across two repos. Verified at the source commit: all four package.json files carry thatlink:, and the.claude/phoenixsymlink that makes it resolve is a root-level symlink in cansirin/monorepo pointing at this checkout — it appears nowhere inside the four packages, so that acceptance criterion is about the fourlink:lines, not about a path to hunt for.Arc: Tuval programs (milestone 55) — the campaign that owns the programs themselves; this is the move that lets them be worked at all.
Appetite: 2 cycles
Rabbit-holes: (1)
tsconfigpathstoworkspace:*is not a straight swap. Each package maps its own name and its siblings' atsrc/index.tsso the.tuval/fixture and the cross-package composition proof type-check with no build; aworkspace:*dep resolves throughexportstodist/index.d.ts, which does not exist until the upstream builds.packages/depoalready answers this with a"development": "./src/index.ts"export condition — pick that or keep the paths, but decide it before the first package lands, because every later package inherits it. (2)cron-parseris in cansirin/monorepo's root catalog at^5.10.1and is absent from this repo'spnpm-workspace.yaml; a new catalog entry is part of the move, not a footnote. (3)effectviacatalog:tuvalis4.0.0-rc.112in both repos and@demlik/teais0.12.0in both — those two are clean, do not re-pin them. (4) Adoptingeffect-tsgo diagnostics --strictperpackages/deporuns over a program that pulls apps/tuval's whole reachable source tree in as raw TypeScript; cron, notify and workspace each carry a second windowtsconfigon top of the node one, so it is seven projects, not four, and the source tsconfigs deliberately setexactOptionalPropertyTypes: falseandlib: DOMfor that reason. (5) The space-to-tab reformat over ~13.3k lines of TS will bury the real diff — land it as its own commit so the conversion stays reviewable. (6)@kampus/tuvalisprivate: trueand ships rawsrc/*.tswith nodist; fourpublishConfig.access: publicpackages depending on it are unpublishable until that changes, which is fine but should not surprise release tooling.No-gos: No publish to npm, and no change that makes publishing a prerequisite. Nothing under
apps/tuvalchanges — not a registration, not an import, not afeatures.*flag; the never-default-on rule from #9247/#9260 holds andgrep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/srcstays empty. No catalog page. No rewriting a program's behaviour while moving it: the test counts at the source commit are cron 76, shell 69, notify 87, workspace 156 — verified green on 2026-09-16 — and any child that changes a count owes a reason.Epic — awaiting plan
plan-epicappends its plan and dependency topology below.Original brief (verbatim)
What is true now
Four third-party Tuval programs live in cansirin/monorepo under
packages/:@cansirin/tuval-cron(a scheduled job, cron-parser schedules, a window),@cansirin/tuval-shell(a job that runs a command),@cansirin/tuval-notify(delivers a job's result to a Discord/webhook/ntfy target, a window),@cansirin/tuval-workspace(an isolated worktree per task with an agent inside, a window, five persona configs). All four are opt-in: a user names them in~/<redacted>; nothing inapps/tuvalknows they exist.They depend on
@kampus/tuvalthrough"link:../../../../phoenix/apps/tuval"because@kampus/tuvalisprivate: trueand unpublished. That link is the reason their CI is red, the reason a user's config imports them by path, and the reason none of the program-side fixes they surfaced (#9227, #9229, #9230, #9292, #9295, #9365) could be proven in one repo.Decided today by both maintainers: they move into this monorepo under
packages/, as their own packages, and stay opt-in. Not intoapps/tuval, not on by default, nofeatures.*flag; the never-default-on rule from #9247/#9260 holds.What moves
packages/tuval-cronpackages/tuval-cron@kampus/tuval-cronpackages/tuval-shellpackages/tuval-shell@kampus/tuval-shellpackages/tuval-notifypackages/tuval-notify@kampus/tuval-notifypackages/tuval-workspacepackages/tuval-workspace@kampus/tuval-workspaceEach keeps its
src/, tests, README,.tuval/tuval.config.tsfixture,examples/(workspace), and window build. Source of record for the copy: cansirin/monorepomainat the PR's stated commit; the PR body names it so the diff can be checked against it.What changes in the move
@kampus/tuvalbecomesworkspace:*; thelink:and the.claude/phoenixsymlink hack go away.packages/depo:@kampus/scope,repository.directory,publishConfig.access: public,catalog:for shared deps (effect, react, vitest, typescript),typecheckrunseffect-tsgo diagnostics --strict, tabs per this repo's Biome.notifyreadsBRIEF_PORTfromcron) becomeworkspace:*deps, not tsconfigpaths.@cansirin/or "imports by path until npm" state the new truth.~/<redacted>user configs will import@kampus/tuval-cronetc. once published; until then the READMEs say the path form.Not in scope
Publishing to npm (waits on
@kampus/tuvalitself); a catalog page; any change underapps/tuval.Acceptance criteria
packages/with the@kampus/names, andpnpm -r --filter './packages/tuval-*' typecheck test buildis green in this repo's CI@kampus/tuvalis aworkspace:*dependency in each; nolink:and no.claude/phoenixreference remains anywhere in the four packages~/<redacted>-style config naming all four boots the desk fromapps/tuvalwith nofeatures.*flag and no change underapps/tuval; the PR shows the boot lineapps/tuval;grep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/srcis empty@kampus/…and describe the workspace dependency, not the path linkPlan (plan-epic)
Summary
Four Tuval programs — cron, shell, notify, workspace — move from cansirin/monorepo into this
repo's
packages/as@kampus/tuval-*, one package per child, keeping theirsrc/, tests,README,
.tuval/fixture,examples/and window build.@kampus/tuvalbecomes aworkspace:*dependency and the
link:../../../../phoenix/apps/tuvalline disappears. They stay opt-in:nothing under
apps/tuvalchanges, and nofeatures.*flag appears.The first child is not just "cron". It is cron plus the conventions every later package
inherits — the
cron-parsercatalog entry, the cross-package resolution answer, thepackages/depopackage shape, and the space-to-tab reformat landing as its own commit. That is why it is alone in
phase 1.
Problem & who has it
The four packages reach
@kampus/tuvalthroughlink:../../../../phoenix/apps/tuval. That pathresolves on exactly one laptop, with both checkouts side by side and a root-level
.claude/phoenixsymlink. Three things follow, and all three land on the two people who maintain these programs:
reproduced by hand across two repositories before it can be fixed.
The cause is that
@kampus/tuvalisprivate: trueand unpublished, so no registry version existsto depend on. Publishing is not the answer available now. Co-location is.
What changes
Four new workspace members under
packages/, each a straight move with a rename:packages/tuval-cronpackages/tuval-cron@kampus/tuval-cronpackages/tuval-shellpackages/tuval-shell@kampus/tuval-shellpackages/tuval-notifypackages/tuval-notify@kampus/tuval-notifypackages/tuval-workspacepackages/tuval-workspace@kampus/tuval-workspacePer package:
@kampus/scope,repository.directory,publishConfig.access: public,catalog:for shared deps, tabs per this repo's Biome,@kampus/tuvalatworkspace:*.Root-level, once, in the first child:
cron-parser: ^5.10.1enterspnpm-workspace.yaml's rootcatalog (it is absent here today; verified by grep over
pnpm-workspace.yamlat718a7ad8).Nothing under
apps/tuvalchanges — not a registration, not an import, not a flag.User stories
@kampus/tuvalas a
workspace:*dependency, so that their typecheck, tests and build run in this repo's CIinstead of on one laptop with two checkouts.
@kampus/tuval-*package names in myconfig and have the desk boot, so that I am not importing a program by a filesystem path that
only exists on someone else's machine.
the cross-package resolution convention, the catalog entry and the package shape, so that I copy
one answer instead of inventing a second.
apps/tuvalmaintainer, I want these programs to stay opt-in and unreferenced by the app,so that co-locating them turns nothing on by default.
Goal / non-goals
Goal. The four programs are workspace members of this repo, green in this repo's CI, reachable
by name, still opt-in, with every test they carried passing here at the same count.
Non-goals. Publishing to npm, and any change that makes publishing a prerequisite — the four
stay unpublishable until
@kampus/tuvalitself changes, and that is fine. A catalog page. Anychange under
apps/tuval. Any rewrite of a program's behaviour while it moves: a child that changesa test count owes a reason in its PR body.
Founder rulings (grilling session #9378)
Both round-1 decision questions are ruled;
grill readreports the frontierclear.pathsit arrivedwith. Ruled by @cansirin on 2026-09-17: "ok if its the current answer inside the repo, we can
use it imo."
packages/tuval-cron'sexportsgains"development": "./src/index.ts"; shell andnotify declare
"@kampus/tuval-cron": "workspace:*"and delete the siblingpathsentry pointingat
../tuval-cron/src/index.ts.effect-tsgo diagnostics --strictin this epic, across all seventsconfig projects (one node project per package, plus the second window
tsconfigthat cron,notify and workspace each carry). Ruled by @cansirin on 2026-09-17: "we should try to stay closer
as much as possible to current repo rules yeah?" This reverses the draft's recommendation to
defer it.
Resolved questions
@kampus/tuvalneed adevelopmentexport condition for the four to consume it? No.apps/tuval/package.json'sexportsmap already points every subpath at raw./src/*.tsandships no
dist, so aworkspace:*dependency resolves to source with no build. The export-conditionquestion is about the sibling edge (shell and notify importing cron), not this one — see Approach.
publish-isolation-guardred on fourpublishConfig.access: publicpackages depending ona private
@kampus/tuval? No. The guard derives its scope frompublish.yml's release-taggrammar, and
publish.ymlmatchesfabrika-cliandfabrika-pionly. A package outside that setis outside the guard's scope.
effectatcatalog:tuvalis4.0.0-rc.112in bothrepos and
@demlik/teais0.12.0in both. Neither is re-pinned by any child.cron-parseronly. It is^5.10.1in the sourcerepo's root catalog and absent from this one's.
Approach
The cross-package edge is settled by R1.1 and lands in phase 1. In the source repo, shell and
notify reach cron through tsconfig
pathspointing at../tuval-cron/src/index.ts, because aworkspace:*dependency resolves throughexportstodist/index.d.ts, which does not exist untilcron builds.
packages/depoalready answers this here with a"development": "./src/index.ts"export condition, and the founder ruled that answer in: cron's
exportsgains thedevelopmentcondition, shell and notify declare
"@kampus/tuval-cron": "workspace:*"and drop the siblingpathsentry. That makes the dependency visible in the manifest — where the build graph and thelockfile can see it — rather than in a compiler lens only.
Phase 1 is one package because the conventions are not divisible. Cron carries the catalog
entry, the export condition, the depo-shaped manifest, and the tab reformat pattern. Landing a second
package beside it means two answers to each of those questions and a reconciliation nobody asked for.
The reformat is its own commit. ~13.3k lines of TS convert from spaces to tabs for this repo's
Biome. Every child lands the move in one commit and the reformat in a second, so the real diff is
reviewable against the source commit the PR body names.
effect-tsgo diagnostics --strictis adopted in this epic, per R1.2. Every package'stypecheckscript becomestsc -p <project> && effect-tsgo diagnostics --project <project> --strictfor each of its projects — the node project in all four, plus the window project cron, notify and
workspace each carry, so seven projects in total. Each package takes
@effect/tsgoas adevDependencyatcatalog:, matchingpackages/depoandpackages/fabrika-cli.This is the expensive part of the move and the children carry it, not a follow-up. Each package
consumes
@kampus/tuvalas raw TypeScript, so the app's whole reachable source tree enters theprogram and is checked under the package's options — which is why the source tsconfigs deliberately
set
exactOptionalPropertyTypes: falseandlib: ["ES2023","DOM","DOM.Iterable"]. A child that hasto relax or tighten an option to get a clean
--strictrun says which option and why in its PR body;a child that cannot get one stops and reports rather than deleting the tsgo step it was ruled
into. Phase 1 lands the pattern on cron — the smallest of the four and the one with a window project
— so the three phase-2 children copy a proven invocation instead of each inventing one.
The boot proof is its own child. It is the only acceptance criterion that cannot be met by any
single package PR — it names all four in one config. Folding it into the last package to land would
make which package that is decide where the proof lives.
Acceptance criteria
packages/with@kampus/tuval-*names andpnpm -r --filter './packages/tuval-*' typecheck test buildis green in this repo's CI@kampus/tuvalis aworkspace:*dependency in each; nolink:and no.claude/phoenixreference remains in any of the four packagesapps/tuvalwith nofeatures.*flag and no change underapps/tuval; the boot line is in the PRgrep -r "tuval-cron\|tuval-shell\|tuval-notify\|tuval-workspace" apps/tuval/srcis empty@kampus/…and describes the workspace dependency, not the path linkcron-parseris a root catalog entry and every one of the four consumes its shared deps throughcatalog:orcatalog:tuvaleffect-tsgo diagnostics --strictover every project it carries — seven in total — and all seven are clean in this repo's CI; any tsconfig option changed to get there is named with its reason in the child's PR bodyTesting strategy
The tests already exist and move with the code; the count is the contract. Each child runs its
package's own
vitest runand states the observed count against the source number in its PR body.Each child's
pnpm typecheckis the ruled two-step —tsc -ptheneffect-tsgo diagnostics --strict— over every project in the package, and a child's PR body reports both as run, not justthe
tschalf. Nothing new is written except where the move itself changes a fact — a fixture whose importspecifier changed, a test asserting a
@cansirin/renderer ref that is now@kampus/..tuval/fixtures andexamples/are type-checked, not executed, and that is deliberate: they arereal user configs, held to compile so a README cannot promise a config that does not work.
The epic's own proof is the boot in child 5: a real config naming all four, the desk starting, and
the grep over
apps/tuval/srccoming back empty. That is the difference between four packages thatcompile and four programs a user can actually reach.
Task-split rationale
One child per package, plus one assembly child for the proof no single package can give.
answers land (catalog entry, the ruled export condition, the ruled
effect-tsgo --stricttypecheckinvocation over a node project and a window project, depo shape, reformat pattern). Story 1 and
story 3.
compose.unit.test.tsreadscron, jobShape; notify'snotify.unit.test.tsreadsBRIEF_PORT),so both require it. Workspace imports no sibling, but it requires cron for the convention: landing
it beside cron would have it invent the resolution answer a second time. Story 1 and story 3.
The one file three phase-2 children share is
pnpm-lock.yaml. No two of them write any otherfile: each touches only its own
packages/tuval-*directory, and only cron touchespnpm-workspace.yaml. A lockfile collision is a rebase-and-regenerate, not a merge conflict toresolve by hand, so they stay parallel rather than serialized behind each other.
Vocabulary impact
Four package names enter this repo's vocabulary:
@kampus/tuval-cron,@kampus/tuval-shell,@kampus/tuval-notify,@kampus/tuval-workspace. The old@cansirin/spellings survive only ashistory — no doc, comment or fixture in this repo keeps them.
No new concept is introduced. "Program", "row", "window", "desk" and "port" already mean here what
they mean in the moved source; that is what makes this a move rather than a port.
Dependencies