Skip to content

Write learnings/ to a teamai-learnings branch (direct push, no PR) #485

Description

@jeff-r2026

Related

Depends on / follows #484 (report data → teamai-reports). Same motivation: members should not need write access on the default branch. Different data: session notes are knowledge, not telemetry.

Problem

teamai contribute currently lands learnings/*.md on the default branch:

  • kind: git — pushRepoDirectly() / pushLearningToOrigin() onto main/master
  • kind: self — isolated knowledge worktree + PR onto main (contributeSelf)

That is the wrong gate for this content:

  • Direct-to-main requires every member to write the protected branch.
  • A PR is too heavy. Learnings are frequent, per-session notes (unique filenames, low conflict). Review cost is higher than the risk. Skills / rules / docs already have teamai push if a team wants a real review.

pull / recall / maintenance today index learnings/ from the knowledge clone (default branch). Whatever we change on write, those readers must follow.

Why not teamai-reports

#484 already owns members/ sessions/ votes/ stats/ on an orphan branch. Putting learnings there would be convenient (one worktree, one lock) and wrong:

  • Reports are high-churn YAML telemetry. Learnings are recallable knowledge (search-index, recall maintenance, KB Health, promote-to-skill).
  • An orphan history cannot later merge into main; learnings might still want that.
  • One branch cannot split “lock down stats” vs “allow notes”.
  • pull already splits knowledge vs reports; stuffing notes into the reports worktree couples recall to telemetry history.

Proposal (tentative)

New long-lived branch teamai-learnings.

  • Not orphan. Create it from the default branch (or from the existing learnings/ tree on first use) so a later merge-to-main is a normal PR, not a cross-history copy.
  • Direct push. No per-contribution PR, no review requirement. Same write model as reports: isolated worktree + lock + fetch/rebase retry.
  • Reuse the reports-branch machinery, not the branch. Extract or clone the worktree/lock/retry helper; do not share REPORTS_BRANCH or the reports worktree.
  • Git-mode worktree: sibling of the clone (e.g. <dirname(localPath)>/learnings-wt), same reason as Route report data (members/sessions/votes/stats) to teamai-reports in independent git clones #484 (reset --hard must not nest inside the clone).
  • Self mode: stop opening a knowledge PR for contribute. Write learnings/ in the learnings worktree; leave skills/ rules/ docs/ teamai.yaml on main via existing teamai push.
  • Readers: pull index rebuild, contribute immediate index rebuild, recall, viz, maintenance read learnings/ from the learnings worktree, not from the default-branch checkout. Docs / rules / skills stay on the knowledge clone.
  • Pending flush: flushPendingLearnings pushes onto teamai-learnings, not pushLearningToOrigin() on the default branch.
  • Do not migrate leftover data. Existing learnings/ on main is left as-is and not copied. New writes go to teamai-learnings. After the switch, readers only look at that branch, so old main copies are ignored (same policy as Route report data (members/sessions/votes/stats) to teamai-reports in independent git clones #484).

No new CLI command. No --target-branch flag in this issue.

Open question for the community

Is teamai-learnings the right name and shape?

Alternatives we considered:

  1. This proposal — dedicated regular branch teamai-learnings (current preference).
  2. Same orphan teamai-reports as Route report data (members/sessions/votes/stats) to teamai-reports in independent git clones #484 — fewer refs, mixes knowledge with telemetry, hard to merge later.
  3. Rename the side channel to something like teamai-data and put reports + learnings together — simpler ops, blurs the knowledge/report split.
  4. Keep contribute as PR-to-main but skip required reviewers — still needs default-branch integration, still a review-shaped workflow.

If you have a better branch name, or a reason to merge learnings into reports / a generic data branch, comment here before we lock the implementation.

Out of scope

Acceptance

  • An account without default-branch write permission can teamai contribute successfully. The file appears on origin/teamai-learnings under learnings/. No default-branch commit, no PR.
  • Same machine can teamai recall the new note without waiting for a merge to main (index reads the learnings worktree).
  • Offline contribute → next teamai pull retries onto teamai-learnings.
  • kind: self contribute no longer opens a knowledge PR.
  • Concurrent contributes from two users still succeed via rebase retry.
  • Existing learnings/ on main is not copied, not deleted, and not read after the switch.
  • kind: http unchanged.

Activity

  1. SaulMoro commented on Sep 17, 2026

    @SaulMoro
    Collaborator

    The name works, and so does the shape. A dedicated regular branch, direct push, and reuse of the worktree, lock, and retry code without sharing REPORTS_BRANCH or the reports worktree. Options 2 and 3 tie recall to telemetry history. Option 4 keeps the default branch in the write path. This proposal avoids both.

    We would add two things. Neither depends on the branch decision, so both can ship as separate PRs.

    Add one read seam before the switch

    #484 was cheap because every reader already called getReportsDir(). learnings/ has no such function. Eleven call sites build the path by hand:

    src/
    ├── pull.ts:916                    reader
    ├── recall.ts:281                  reader
    ├── digest.ts:228                  reader
    ├── viz.ts:145,170                 reader
    ├── maintenance/paths.ts:33        reader
    ├── contribute.ts:53,198           writer, plus the index rebuild
    ├── import.ts:247                  writer
    ├── import-mr.ts:291               writer
    ├── ci/extract-mr.ts:116,205       writer
    ├── maintenance/prune.ts:102       writer, learnings/_archive
    └── utils/pending-learnings.ts:86  writer
    

    A missed call site does not throw. It reads an empty directory. Recall returns fewer results, and prune archives into a branch that nobody reads.

    The same list shows that contribute is not the only writer. import, import-mr, and the maintenance commands also create and move learnings. The issue does not list them.

    We would land a pure refactor first, with no branch change:

    export interface LearningsRoots {
      write: string;    // where new learnings go
      read: string[];   // precedence order, write root first
    }
    export function getLearningsRoots(localConfig: LocalConfig): LearningsRoots;

    buildIndex then takes learningsDirs: string[] and deduplicates by relative path. The first root wins. After that, the branch switch changes one function.

    The read list also answers the "do not migrate" rule. #484 could abandon the report files on main, because the next session regenerates them. Nobody regenerates a learning. If readers stop looking at the clone's learnings/, every note written before the switch drops out of recall. Keep that directory as the last read root, and nothing is copied, deleted, or lost.

    The read list also frees the branch shape. If the old tree stays readable, teamai-learnings can be an orphan branch that holds only learnings/. The worktree then carries no second copy of skills/, rules/, docs/, and teamai.yaml, which goes stale as soon as the branch exists.

    Make contribute offline-first

    pending-learnings/ is already a durable queue outside the clone. Make it the only write path:

     contribute
    -  write into <clone>/learnings/
    -  pushRepoDirectly(...)
    -  on failure: savePendingLearning(...)   # a second, different write path
    +  savePendingLearning(...)
    +  flushPendingLearnings()                # the only code that knows the branch

    Three facts about the code today:

    • pull.ts:127 discards the return value of flushPendingLearnings, and that function logs every failure at log.debug. No message in src/ tells the user that learnings are queued. A member without push rights queues notes forever and reads "Will retry on the next pull" every time. That member is the reason for this issue.
    • contributeSelf saves nothing when the push fails, so the note is lost. This issue routes every self-mode contribution through that path.
    • Two functions do the same write: pushRepoDirectly in contribute, and pushLearningToOrigin in the flush. The issue has to move both.

    With the queue as the write path, contribute never runs git. Self mode gets the durability that git mode has. The branch target lives in one function. We would also print the queue state during pull, for example 2 learnings pending (last error: ...).

  2. jeff-r2026 commented on Sep 17, 2026

    @jeff-r2026
    CollaboratorAuthor

    Thanks — this is the right read on both fronts. Decisions, so we can lock the implementation:

    1. Name & shape — confirmed. teamai-learnings, dedicated branch, direct push, reuse the worktree/lock/retry helper from src/utils/reports-branch.ts (#484 landed) without sharing REPORTS_BRANCH or the reports worktree.

    2. Orphan branch — agreed, changing the proposal. You're right that a non-orphan branch drags a second copy of skills/ rules/ docs/ teamai.yaml that goes stale the moment the branch exists. Make teamai-learnings an orphan branch holding only learnings/. We drop the "later merge-to-main is a normal PR" argument from the body — learnings don't need that path.

    3. Keep the old tree readable — agreed, this fixes a real data-loss bug. My "not read after the switch" line was wrong: reports regenerate, learnings don't, so cutting readers off from the clone's learnings/ would silently drop every pre-switch note out of recall. Fix as you describe: keep the old learnings/ as the last read root. Precedence = new orphan worktree first, old clone dir last, dedup by relative path. Still nothing copied or deleted — just kept readable. This is also what makes the orphan shape in (2) safe.

    4. No separate refactor PR — fold the read-seam into the one PR. Don't land getLearningsRoots() as a standalone pure-refactor step first; do the multi-root read seam and the branch switch together in a single PR. The { write, read[] } shape and buildIndex(learningsDirs: string[]) are still the right mechanism (they're what makes (3) work), and yes — cover the writers I missed in the body: import, import-mr, ci/extract-mr, maintenance/prune. Just don't split it across two PRs.

    5. Offline-first / queue-only — out of scope here, please open a separate issue. Making pending-learnings/ the single write path, routing self-mode through the durable queue, and surfacing queue state during pull (N learnings pending (last error: ...)) is a real gap — contributeSelf dropping notes on push failure and flushPendingLearnings only logging at debug both need fixing. But it's a separate change from the branch move and shouldn't gate it. Could you open a dedicated issue for it and link back here? Happy to review it there.

    Net change to the issue body: (a) branch becomes orphan, learnings/ only, (b) Acceptance drops "not read after the switch" and instead requires pre-switch notes stay recallable via the old read root, (c) offline-first moves out of scope to a follow-up. Everything else stands.

  3. SaulMoro commented on Sep 17, 2026

    @SaulMoro
    Collaborator

    Agreed on all five, and thanks for the call on (3) — that was the part worth getting wrong out loud.

    Offline-first is now #615, out of this change's way. The PR for #485 and #486 lands the branch move, the multi-root read seam and the docs together, as you asked in (4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions