--- frozen: true step: KN-WORK-PATH-LIST-a model: "Thinking (thinking-high)" date: 2026-08-18 branch: feat/kn-work-path-list-a status: thinking-freeze-2026-08-18 supersedes: "Freezes Knowtation learning-path persist + list/get so Scooling My Work can later show real Path rows and later Home→Work bind. Consumes Scooling F13 (current-state card overlay shipped; tip feat/ux-work-redirect-b) and the AGENT-WORK-CHAT-HOME-BIND park. Does not invent Live on Scooling My Work, Auto BRAIN-PAIR-b, put chat on Home, or replace the Home prompt. This freeze does not authorize KN-WORK-PATH-LIST-b Auto until freeze-review pass." evidence: "Scooling PRIMARY 2026-08-18 (ROADMAP F14; OVERSEER-HANDOVER NEXT Thinking KN-WORK-PATH-LIST-a). UX-WORK-REDIRECT-b BV r1 pass sha256:8c71d27d0e1ed5e0f61587cc4fa00c566cf872453a395eb10b44dd1075c20ee4. AGENT-WORK-CHAT park AGENT-WORK-CHAT-HOME-BIND in ~/scooling/docs/reviews/2026-08-14-agent-work-chat.md. Board freeze parked persist+list in ~/scooling/docs/reviews/2026-08-14-ux-work-board.md §3.1. Path drafts have no list or get today (createPrivateLearnerLoopDraft is one-shot)." --- # KN-WORK-PATH-LIST — Knowtation path persist + list/get **Ground truth** for KN-WORK-PATH-LIST-b Auto. Downstream Auto may treat this document as ground truth without re-deriving. It does **not** edit Scooling, invent Live on Scooling My Work, put chat on Home, replace the Home prompt, start AGENT-WORK-CHAT-HOME-BIND, Auto BRAIN-PAIR-b, start KN-WORK-RUN-LIST, invent a conversation vault, flip `PATH_WRITES_ENABLED` to on, or admit path kinds to T5 self-apply. ```yaml phase: KN-WORK-PATH-LIST-a outputs: - id: kn-work-path-list path: docs/KN-WORK-PATH-LIST-FREEZE.md frozen: true notes: Dedicated learning_paths[] in hub_flow_store.json; list/get REST; Review-before-write persist gated default off. No Scooling harvest. No Home bind. No Live chip. frozen_inputs: - id: scooling-roadmap-f14 path: ~/scooling/docs/ROADMAP.md notes: F14 owns this build; AGENT-WORK-CHAT-HOME-BIND stays parked until list/get exists - id: scooling-handover-next path: ~/scooling/docs/OVERSEER-HANDOVER.md notes: PRIMARY Thinking KN-WORK-PATH-LIST-a; F13 current-state overlay shipped - id: ux-work-redirect path: ~/scooling/docs/reviews/2026-08-18-ux-work-redirect.md notes: F13 freeze-review pass sha256:8c71d27d…; this-visit overlay; cold GET shows saved thread; T8 forbade path list/get in that tip - id: ux-work-redirect-bv path: ~/scooling/docs/reviews/2026-08-18-ux-work-redirect-b-bv-round1-pass.md notes: F13b BV r1 pass; tip feat/ux-work-redirect-b; KN-WORK-PATH-LIST not started - id: agent-work-chat path: ~/scooling/docs/reviews/2026-08-14-agent-work-chat.md notes: Parks AGENT-WORK-CHAT-HOME-BIND until KN-WORK-PATH-LIST; Path drafts have no list or get - id: ux-work-board path: ~/scooling/docs/reviews/2026-08-14-ux-work-board.md notes: §3.1 Path drafts no list/get; parks KN-WORK-PATH-LIST persist+list; kinds Path·Run·Loop·Helper - id: ux-work-live-rows path: ~/scooling/docs/reviews/2026-08-14-ux-work-live-rows.md notes: Session-bound Loop/Helper harvest pattern; samples fallback; Preview chip; no Path list - id: launch-finish path: ~/scooling/docs/reviews/2026-08-18-launch-finish-assessment.md notes: KN-WORK-PATH-LIST unlocks real Path rows and Home→Work bind - id: task-loop-store path: docs/TASK-LOOP-STORE-CONTRACT-2G-c.md notes: List/get + hub_flow_store.json vault-bucket pattern to copy; do not invent a second store file - id: flow-store path: lib/flow/flow-store.mjs notes: getVaultFlowStore must default learning_paths to [] - id: flow-store-merge path: hub/bridge/external-agent-blob-store.mjs notes: mergeFlowStoreJson must union-merge learning_paths by path_id (CAPTURE-STORE-STALE-MERGE) - id: task-loop-handlers path: lib/task/task-loop-handlers.mjs notes: Scope resolve + 404-not-leak list/get handler shape - id: task-writes-gate path: lib/task/task-write.mjs notes: Env tri-state pattern only. getPathWritesEnabled lives in lib/path/path-write.mjs; do not edit this file. - id: self-apply-kinds path: lib/hub-proposal-personal-self-apply.mjs notes: Path kinds stay off ADMITTED_* lists this Auto - id: hosted-task-apply path: hub/gateway/task-approve-hosted.mjs notes: maybeApplyHostedTaskAfterApprove is the hook pattern to copy for paths - id: learning-path-draft path: ~/scooling/src/adapters/types.ts notes: Scooling LearningPathDraft is title/summary/steps/runtimeDisclosure — not a store - id: private-learner-loop path: ~/scooling/src/learnerLoop/privateLearnerLoop.ts notes: One-shot draft; no persist - id: work-open-parser path: ~/scooling/src/work/workBoardSurface.ts notes: parseWorkOpenQuery accepts sample-path, sample-run, LOOP_ID_RE, DELEGATION_GRANT_ID_RE only — Scooling widens later - id: work-copy path: ~/scooling/src/siteGuide/workCopy.ts notes: pathListHonesty is Path list is not ready.; pageStatus preview - id: home-copy path: ~/scooling/src/siteGuide/homeCopy.ts notes: Home prompt stays What's on your mind? / Start a path review_stamp: reviewed_at: '2026-08-18T17:20:17Z' verdict: pass reviewer_mode: agent reviewer_model: thinking-high reviewer_provider: local kit_version: 0.1.0 artifact_digest: sha256:1354ed45531fc4dac329e989727deb9f9f4eb1ed17936a5d65c83b25cb8a1506 downstream: - id: KN-WORK-PATH-LIST-b model: Auto consumes_as_ground_truth: true notes: Implement store + list/get + gated propose/apply. Starts only after freeze-review pass. Auto does not edit Scooling. Auto does not flip PATH_WRITES_ENABLED on. Auto does not Auto BRAIN-PAIR-b. - id: AGENT-WORK-CHAT-HOME-BIND model: Thinking → Auto consumes_as_ground_truth: true notes: Later Scooling tip. Harvest Path rows + Home draft → work?open=path_…. Not this Knowtation tip. - id: KN-WORK-RUN-LIST model: Thinking → Auto consumes_as_ground_truth: false notes: Parked. This freeze does not define run list/get. tier3_gates: - T1 Muse main or muse-mirror to GitHub main (SD-14) outside SD-21 land hygiene - T2 Setting PATH_WRITES_ENABLED to an enabled literal in source or live env - T3 Admitting path_create / path_update / path_archive to T5 personal self-apply - T4 Flipping Scooling work pageStatus to live or inventing a Live chip - T5 Home prompt replacement, a chat box on Home, or Home body work href (HOME-BIND) - T6 BRAIN-PAIR-b pairing Auto - T7 Any SCOOLING_STUDIO, SCOOLING_MEDIA, or SCOOLING_TASK_LOOP enabled assign - T8 Feature to GitHub main / non-muse-mirror head ``` Auto must not build until freeze review **pass**. This Thinking tip does **not** implement routes. This Thinking tip does **not** flip any env. ## Review record | Round | Reviewer | Verdict | Resolution | | --- | --- | --- | --- | | 0 | Thinking (this session) | draft | Freeze authored from Scooling F14 + F13 BV pass + AGENT-WORK-CHAT-HOME-BIND park + task-loop list/get + flow-store merge | | 1 | Freeze-review loop (thinking) | findings | R1-F1–F5 fixed below. CLI dry-run was already pass. | | 2 | Freeze-review loop (thinking) | findings | R2-F1 fixed below. | | 3 | Freeze-review loop (thinking) + `ok review --freeze` | **pass** | R1–R2 hold. Stale D3 frozen_input note aligned. CLI C checklist clean. Cleared for KN-WORK-PATH-LIST-b Auto. Auto must not edit Scooling, flip PATH_WRITES_ENABLED, or Auto BRAIN-PAIR-b. **No human escalation.** | ### Round 1 findings (cited — file+line) | ID | Sev | Cat | Citation | Finding | Fix | | --- | --- | --- | --- | --- | --- | | R1-F1 | MAJOR | completeness | docs/KN-WORK-PATH-LIST-FREEZE.md:300 (prior “existing approve path”) | Self-hosted apply named a function but not the insert site. Auto could miss `hub/server.mjs` approve. | §4.4 now locks the path branch next to `reconcileApprovedTaskProposal` on `POST api/v1/proposals/:id/approve`. | | R1-F2 | MAJOR | completeness | docs/KN-WORK-PATH-LIST-FREEZE.md:278 (prior path_update) | Update index vs existing/replaced `steps` unbound. Auto could write `current_step_index >= step_count`. | path_update now validates index against patched or existing steps; else `PATH_STEP_INDEX_INVALID`. | | R1-F3 | MAJOR | consistency | docs/KN-WORK-PATH-LIST-FREEZE.md:175 (prior D3) | D3 said copy the tri-state “in `lib/task/task-write.mjs`”. Auto could edit task writes. | D3: `getPathWritesEnabled` in `lib/path/path-write.mjs` only. | | R1-F4 | MINOR | completeness | docs/KN-WORK-PATH-LIST-FREEZE.md:199 (prior steps) | `source_document_ids` omit vs required unbound. | Omit or empty → `[]`. | | R1-F5 | MINOR | completeness | docs/KN-WORK-PATH-LIST-FREEZE.md:175 (prior D2/D3) | Concurrent update / `base_state_id` unbound. Auto could invent task lineage 409. | D12 last-`updated` wins; no `base_state_id`. | ### Round 2 findings (cited — file+line) | ID | Sev | Cat | Citation | Finding | Fix | | --- | --- | --- | --- | --- | --- | | R2-F1 | MAJOR | completeness | hub/server.mjs:3264-3305; docs/KN-WORK-PATH-LIST-FREEZE.md:306 (prior review_queue-only) | Self-hosted approve dispatches on `proposal.source` (`TASK_PROPOSAL_SOURCE`), not `review_queue`. Auto using only `review_queue` would never precheck/reconcile. | `PATH_PROPOSAL_SOURCE = "learning_path"`; precheck before `writeNote`; reconcile after; hosted hook uses the same `source`. | ## Citation discipline Every freeze-review finding MUST cite **file+line** (OVERSEER-KIT-SPEC §6). Do not trust uncited review output. HTTP routes in this doc omit the leading slash (`api/v1/…`) so the freeze mechanical gate does not treat them as absolute machine paths. Cross-repo paths use `~/scooling/…`. Never leading-slash absolute paths. --- ## 0. Job (lock) Knowtation becomes the filing cabinet for learning paths. A signed-in learner can later see real Path rows on Scooling My Work because Knowtation can list and get those paths. A later Home→Work bind can open one by id. Scooling stays Preview. Home stays the one-shot start. Chat stays on My Work. In one sentence: persist structured paths in Knowtation, then list and get them, so Scooling does not have to invent a second ledger. --- ## 1. What is already true (do not re-derive) | Fact | Where | | --- | --- | | Path drafts are one-shot | `createPrivateLearnerLoopDraft` in `~/scooling/src/learnerLoop/privateLearnerLoop.ts` — no list, no get | | Draft shape | `learningPathDraftSchema` in `~/scooling/src/adapters/types.ts` — `title`, `summary`, `steps[]`, `runtimeDisclosure` | | Goal cap on Scooling | `validateLearningGoal` max 180 in `~/scooling/src/learnerLoop/prompt.ts` | | My Work kinds | **Path · Run · Loop · Helper** only (`WORK_COPY.kinds`) | | Path rows today | Sample Path **Algebra through music** only (`?open=sample-path`) | | Loop/Helper harvest | Session-bound; samples fallback; Preview chip (`~/scooling/docs/reviews/2026-08-14-ux-work-live-rows.md`) | | `open` parser | `sample-path`, `sample-run`, `LOOP_ID_RE`, `DELEGATION_GRANT_ID_RE` only (`~/scooling/src/work/workBoardSurface.ts`) | | F13 card overlay | This-visit `reviewReady` / `sentToReview` + last learner turn; Who's helping stays harvest; cold GET shows the saved thread | | Home prompt | **What's on your mind?** / **Start a path** — cold-start only | | HOME-BIND park | Carrying a Home draft onto a My Work thread waits for this list/get (`~/scooling/docs/reviews/2026-08-14-agent-work-chat.md`) | | Path-list honesty | **Path list is not ready.** (`WORK_COPY.pathListHonesty`) | | Durable notes | Review → Knowtation. Scooling is not a second library | | Structured list/get pattern | Task loops in `hub_flow_store.json` + `GET api/v1/task-loops` + `GET api/v1/task-loops/:loop_id` | | Blob merge lesson | `mergeFlowStoreJson` must union-merge new arrays by id or a warm lambda masks blob rows | Do not scan vault notes and call them Path rows. Review-approved markdown stays a note. The path record is a separate structured object that may point at a note. --- ## 2. Decisions (lock — do not reopen in Auto) | ID | Decision | Recorded default | | --- | --- | --- | | **D1** | Store | Dedicated `learning_paths[]` on the same `hub_flow_store.json` vault bucket as tasks / task_loops. **No** second JSON file. **No** note-search list. | | **D2** | Id | `path_` with `PATH_ID_RE` `^path_[a-z0-9_]{1,48}$`. Distinct from `loop_`, `dgrnt_`, `run_`, `sample-path`. Server mints on create as `path_` plus 16 lowercase hex from `crypto.randomBytes(8)`. Retry mint if that id already exists in the vault. Client cannot supply `path_id` on `path_create`. | | **D3** | Reads vs writes | List/get always authorized (JWT + vault + scope). Writes Review-before-write, gated `PATH_WRITES_ENABLED` default **off**. Implement `getPathWritesEnabled` in `lib/path/path-write.mjs` using the same env tri-state helper tasks use (`1`/`true` on, unset/false off). Do **not** edit `lib/task/task-write.mjs`. No path policy file. | | **D4** | T5 | Path kinds are **not** admitted to personal self-apply this Auto. Human approve → apply-approved hook. No `scooling.path:` fingerprint admit. | | **D5** | Who's helping | **Not stored** on the path record. F13 lock: Who's helping stays harvest. Get does not invent helper grants. | | **D6** | Transcript | **Not stored.** Turns stay Scooling request-carried this-visit. Path record is structured work state only. | | **D7** | Seed | **No** production starter fixture. Tests seed via store upsert. Empty list is honest. | | **D8** | MCP / CLI / Hub chrome | Parked. Hub REST + shared handlers only. | | **D9** | Scooling harvest / HOME-BIND | Parked. This Auto does not edit `~/scooling/`. Consumer DTO in §8 is the later contract edge. | | **D10** | Scope / workspace on create | Default `scope=personal`, `workspace_id=ws-personal` (existing Scooling personal id). Project/org require matching visible scope else `PATH_SCOPE_DENIED`. v0 cannot change scope or workspace after create (`PATH_SCOPE_IMMUTABLE`). | | **D11** | Archived | List default omits `archived`. Get of an authorized archived path still returns the record. Archive does not delete the row or any vault note. | | **D12** | Concurrency | v0 last-`updated` wins on the same `path_id`. **No** `base_state_id` / lineage 409 this Auto. | --- ## 3. Canonical record — `knowtation.learning_path/v0` | Field | Type | Req | Rule | | --- | --- | --- | --- | | `schema` | const | yes | `knowtation.learning_path/v0` | | `path_id` | string | yes | `PATH_ID_RE` | | `scope` | enum | yes | `personal` \| `project` \| `org` | | `status` | enum | yes | `active` \| `paused` \| `archived` | | `title` | string | yes | Untrusted display; trim; min 1; max **200** | | `summary` | string | yes | Untrusted; trim; min 1; max **2000** | | `goal` | string | yes | Untrusted learning goal; trim; min 1; max **180** (Scooling `validateLearningGoal` cap) | | `steps` | array | yes | Min **1**, max **20**. Each step: `title` (1–240), `objective` (1–240), `source_document_ids` string[] (omit or empty → `[]`; max 16 items, each `^[A-Za-z0-9._:-]{1,128}$`) | | `current_step_index` | int | yes | `>= 0` and `< steps.length`. Default **0** on create | | `step_count` | int | yes | Server-derived `steps.length`. Never client-trusted as the only source | | `next_step_title` | string | yes | Server-derived `steps[current_step_index].title` | | `active_decisions` | string | yes | Untrusted; max **240**; empty string allowed (Scooling later shows **No decisions waiting.**) | | `workspace_id` | string | yes | `WORKSPACE_ID_RE` `^[A-Za-z0-9._:-]{1,64}$`. Create default `ws-personal` | | `note_path` | string \| null | yes | Optional vault-relative note pointer. Null when absent. See §3.1 | | `created` / `updated` | ISO8601 | yes | Server clock | `runtimeDisclosure` from the Scooling draft is **not** stored. It is mock-loop honesty, not path state. ### 3.1 `note_path` (optional pointer) When present, `note_path` MUST be a vault-relative path: - no leading `/`, no `~`, no `\`, no `://`, no `..` segment, no drive letter - max 256 chars - matches `^[A-Za-z0-9._/-]+\.md$` Invalid → `400 PATH_NOTE_PATH_INVALID`. Path routes **never** call `writeNote`. They do not create, edit, or delete the note. Archive keeps the note. ### 3.2 Client projections **List summary** (`learningPathSummaryForClient`) — no `steps`, no `summary`, no `note_path`: `schema`, `path_id`, `scope`, `status`, `title`, `goal`, `current_step_index`, `step_count`, `next_step_title`, `active_decisions`, `workspace_id`, `updated` **Get** (`learningPathForClient`) — full `knowtation.learning_path/v0` including `steps` and `note_path`. List response: ``` schema: knowtation.learning_path_list/v0 vault_id effective_scope paths: summary[] truncated: boolean ``` Sort: `updated` descending, then `path_id` ascending (stable). Cap `MAX_LEARNING_PATH_SUMMARIES` = **200**. `limit` query if present must be an integer 1–200; invalid → treat as 200. --- ## 4. Routes (lock) Mount on self-hosted Hub, hosted bridge, and gateway proxy. Shared handlers in `lib/path/path-handlers.mjs`. Store in `lib/path/path-store.mjs`. Writes in `lib/path/path-write.mjs`. | Method | Route | Role | Gate | | --- | --- | --- | --- | | GET | `api/v1/learning-paths` | viewer, editor, admin, evaluator | JWT + vault. No write gate | | GET | `api/v1/learning-paths/:path_id` | viewer, editor, admin, evaluator | JWT + vault. No write gate | | POST | `api/v1/learning-paths/proposals` | viewer, editor, admin, evaluator | JWT + vault + `PATH_WRITES_ENABLED`. Propose only — apply stays after approve | | POST | `api/v1/learning-paths/proposals/:proposal_id/apply-approved` | editor, admin (hosted bridge; session-bound like task apply-approved) | JWT + vault + `PATH_WRITES_ENABLED`. Called by the approve hook, not by Scooling | Do not add `route("work/:id")` anywhere. Do not add Scooling routes. ### 4.1 List query Reuse task-loop scope resolve (`resolveHandlerVisibleScopes` + `resolveFlowScopeQuery` from `lib/flow/flow-scope.mjs` / `lib/flow/flow-handlers.mjs`). Ambiguous scope → `400 PATH_SCOPE_AMBIGUOUS`. Unauthorized requested `scope` → `403 PATH_SCOPE_DENIED`. | Query | Rule | | --- | --- | | `scope` | optional `personal` \| `project` \| `org` | | `workspace_id` | optional; must match `WORKSPACE_ID_RE` or `400 BAD_REQUEST` | | `status` | optional `active` \| `paused` \| `archived`. If omitted, return `active` and `paused` only | | `limit` | optional integer 1–200 | No `q=` search. No client-supplied uid, vault field, or Bearer as a query. ### 4.2 Get `path_id` that fails `PATH_ID_RE`, is missing, or is outside visible scope → **404** `{ code: "PATH_NOT_FOUND" }`. Same code for all three. Do not distinguish "exists but not yours." ### 4.3 Propose body `proposal_kind` closed allowlist: `path_create` \| `path_update` \| `path_archive`. Missing kind on POST defaults to `path_create`. Any other value → `400 BAD_REQUEST`. No store write. **`path_create`** - Fields: `title`, `summary`, `goal`, `steps` (required); `current_step_index`, `active_decisions`, `scope`, `workspace_id`, `note_path`, `external_ref` (optional) - Server mints `path_id` - Client `path_id` present → `400 PATH_ID_NOT_ALLOWED` - Default scope `personal`, workspace `ws-personal`, `current_step_index` 0, `active_decisions` `""`, `note_path` null, `status` `active` - `external_ref` if present MUST match `^scooling\.path:[A-Za-z0-9._:-]{1,200}$` else `400 PATH_EXTERNAL_REF_INVALID`. Persist when valid. Absence is allowed. Do **not** treat a valid ref as T5 admission. **`path_update`** - `path_id` required (body). Must exist in scope or 404 `PATH_NOT_FOUND` (no leak) - Allowed patches: `title`, `summary`, `goal`, `steps`, `current_step_index`, `active_decisions`, `status` (`active` \| `paused` only), `note_path` - `scope` or `workspace_id` in the body → `400 PATH_SCOPE_IMMUTABLE` - `status=archived` on update → `400 BAD_REQUEST` (use `path_archive`) - If `steps` is patched, re-derive `step_count` and `next_step_title`. If `current_step_index` is omitted, keep the existing index when it still fits; if it no longer fits → `400 PATH_STEP_INDEX_INVALID` - If `current_step_index` is patched without `steps`, validate against the existing record's `steps` **`path_archive`** - `path_id` required. Sets `status=archived`. Idempotent if already archived. Does not delete the row or the note. Gate off → `403 PATH_WRITES_DISABLED`. No proposal row. No store write. No canister create. Propose roles: viewer, editor, admin, evaluator — same closed set as `TASK_WRITE_ROLES` in `hub/server.mjs` (viewer may propose; apply still waits for approve). Unauthenticated → 401. Proposal path: `meta/learning-paths/proposals/{proposal_id}.json` (never `pending`). `review_queue: learning-path`. `source: learning_path` (`PATH_PROPOSAL_SOURCE`, copy `TASK_PROPOSAL_SOURCE = "task"`). Reuse existing `createProposal` / `createProposalWithSession` (`session_bound` from `isSessionBoundActor`). Do not invent a second proposal store or a new canister schema. `title`, `summary`, `goal`, step `title` / `objective`, and `active_decisions` must contain no `U+0000`–`U+001F` control characters after trim → else `400 PATH_TEXT_INVALID`. `current_step_index` missing `steps` or `current_step_index >= steps.length` or `< 0` → `400 PATH_STEP_INDEX_INVALID`. ### 4.4 Apply Self-hosted: `hub/server.mjs` `POST api/v1/proposals/:id/approve` already prechecks when `proposal.source === TASK_PROPOSAL_SOURCE` then reconciles after `writeNote`. Auto adds a path pair **next to that task pair**: if `proposal.source === PATH_PROPOSAL_SOURCE` (`learning_path`), run `precheckApprovedPathProposal` **before** `writeNote` (refuse the approve on precheck fail, same as tasks) and `reconcileApprovedPathProposal` after. Non-path proposals skip the path branch. Do not fold path apply into `reconcileApprovedTaskProposal`. Hosted classify uses the same `source` plus `review_queue === "learning-path"` and the §4.3 kind allowlist. Hosted: `maybeApplyHostedPathAfterApprove` in `hub/gateway/path-approve-hosted.mjs`, wired next to the existing task/capture/media hooks in `hub/gateway/server.mjs`. The hook returns `null` unless the fetched proposal is a path proposal (`review_queue === "learning-path"` and `proposal_kind` in the §4.3 allowlist). It must not steal task / capture / media apply. On match it POSTs `api/v1/learning-paths/proposals/:proposal_id/apply-approved` on the bridge (copy `maybeApplyHostedTaskAfterApprove`). Bridge create + apply live in `lib/path/path-hosted-proposal.mjs`. Mutating bridge routes wrap `withExternalProtocolBlobSync` (same `hub_flow_store.json` file — do not add a new blob filename). Apply when `PATH_WRITES_ENABLED` is off → `403 PATH_WRITES_DISABLED` and no store write (including leftover proposals). `mergeFlowStoreJson` in `hub/bridge/external-agent-blob-store.mjs` MUST add: `learning_paths: mergeById(localVault.learning_paths, blobVaultObj.learning_paths, 'path_id')` `getVaultFlowStore` MUST default missing `learning_paths` to `[]` (same as `task_loops`). Do not "fix" `orchestrator_graphs` merge in this Auto. --- ## 5. Fail-closed rules 1. No JWT → 401. No vault access → 403. Do not leak whether a `path_id` exists. 2. Out-of-scope or unknown get → 404 `PATH_NOT_FOUND`. 3. Write gate off → 403 `PATH_WRITES_DISABLED` on propose **and** apply-approved; zero store I/O beyond the gate read. 4. Client-supplied `path_id` on create → 400. Client-supplied uid / vault / Bearer as a body field → ignore; never persist. 5. Injection in `title` / `summary` / `goal` / step strings / `active_decisions` is stored as untrusted text only. No HTML render. No eval. No query concatenation. 6. `note_path` traversal / absolute / URL → 400. Path routes never `writeNote`. 7. `source_document_ids` are stored; list/get do not fetch those notes. 8. Path kinds stay off `ADMITTED_TASK_PROPOSAL_KINDS` / flow / media allowlists. T5 path fingerprint → `SELF_APPLY_NOT_ADMITTED` (or the existing seam refuse). Do not add a path admit function. 9. JSON.stringify of request, proposal, store row, and response MUST contain no tokens, cookies, Bearer, refresh material, or provider secrets. 10. Do not print adapter mode, env names, or raw session material on any path response. 11. Archive is not a delete. There is no `path_delete` kind this Auto. 12. Empty list is `paths: []`, `truncated: false` — never a 500 and never a fabricated sample path. --- ## 6. What Auto must not change - Any path under `~/scooling/` - Home prompt, Home body work-href lock, My Work Preview chip - F13 `resolveWorkVisitCurrentState` / this-visit overlay - Task / flow / media / docs-sync / delegation routes and gates - `TASK_WRITES_ENABLED`, `FLOW_*`, `MEDIA_*`, `DOCS_OAUTH_*`, `SCOOLING_*` - `LOOP_ID_RE`, `DELEGATION_GRANT_ID_RE` - Conversation vault, session-cookie transcript, `localStorage` turns - BRAIN-PAIR-b, presence, pairing, MuseHub F7, Parentier.org - Hub wizard chrome / MCP / CLI path commands - Production starter seed that would show as a learner Path --- ## 7. Test matrix (seven-tier) — KN-WORK-PATH-LIST-b consumes this New files `test/path-list-*.test.mjs` (unit, integration, e2e, stress, data-integrity, performance, security). Command: `node --test test/path-list-*.test.mjs`. | Tier | File | Must prove | | --- | --- | --- | | unit | `test/path-list-unit.test.mjs` | `PATH_ID_RE`; mint is `path_` + 16 hex; reject client `path_id`; field caps; control-char `PATH_TEXT_INVALID`; `PATH_STEP_INDEX_INVALID`; `note_path` reject `../` and `https://`; default list omits archived; get returns archived; `next_step_title` / `step_count` derived; `PATH_SCOPE_IMMUTABLE`; unknown kind 400 | | integration | `test/path-list-integration.test.mjs` | upsert → list → get; scope deny; workspace filter; gate off propose 403 and store unchanged; gate on create propose then apply → get; update then archive; empty vault lists `[]` | | e2e | `test/path-list-e2e.test.mjs` | Hub/bridge/gateway walkthrough with fakes: GET list/get; POST propose; approve apply writes one path; blob hydrate then get; no Scooling file import | | stress | `test/path-list-stress.test.mjs` | 200+ rows truncate; concurrent upsert same `path_id` last-`updated` wins; 20-step cap | | data-integrity | `test/path-list-data-integrity.test.mjs` | `mergeFlowStoreJson` union by `path_id` (stale local must not mask blob); apply twice idempotent; archive keeps the row and does not delete `note_path`; restart load | | performance | `test/path-list-performance.test.mjs` | List 200 + get 1 within local budget; no unbounded note scan | | security | `test/path-list-security.test.mjs` | No token/Bearer in JSON; get unknown vs out-of-scope both 404 `PATH_NOT_FOUND`; create with foreign `path_id` rejected; `note_path` traversal 400; write gate off → no store write, no canister create, apply-approved also 403; path kinds not in T5 admit lists; `external_ref` malformed 400; no `writeNote` from path modules; hook returns null for task/capture/media proposals | Security tier MUST fail against a pre-fix stub that (a) returns 403 for out-of-scope get while 404 for missing, or (b) writes the store when `PATH_WRITES_ENABLED` is unset, or (c) includes a Bearer in the list JSON. --- ## 8. Later Scooling consumer (not this Auto) Frozen so HOME-BIND / Path-row harvest does not re-derive the Hub shape. **Do not implement in KN-WORK-PATH-LIST-b.** | Later need | Contract | | --- | --- | | Open id | Scooling widens `parseWorkOpenQuery` to accept `PATH_ID_RE` in a later tip. Sample-path still always resolves. | | Harvest row | `LivePathRow`: `openQuery` (the Hub `path_id` after `PATH_ID_RE`), `title`, `scope`, `status` (`active` \| `paused`), `goal`, `currentStepIndex`, `stepCount`, `nextStepTitle`, `activeDecisions`. Omit archived. Cap 8 is Scooling-side. | | Saved thread (cold GET) | Goal = `goal`; Active decisions = `active_decisions` or **No decisions waiting.**; Next step = `next_step_title`; Who's helping = existing harvest. F13 overlay still this-visit only and does not write the path. | | Samples fallback | Unchanged F1 rule: auth off / signed out / empty harvest → two samples. Never mix samples and live Path rows. Preview chip stays. | | HOME-BIND | After a `path_create` apply, open `work?open=`. Home prompt stays **What's on your mind?** / **Start a path**. No chat box on Home. | | Honesty | Later Scooling tip may replace **Path list is not ready.** — not this Auto. | --- ## 9. Auto file allowlist (KN-WORK-PATH-LIST-b) | Path | Why | | --- | --- | | `lib/path/**` | store, handlers, write, hosted proposal | | `hub/gateway/path-approve-hosted.mjs` | `maybeApplyHostedPathAfterApprove` | | `hub/gateway/server.mjs` | Proxy GET/POST prefixes + wire the approve hook | | `hub/bridge/path-routes.mjs` | Bridge list/get/propose + blob wrap | | `hub/bridge/server.mjs` | Register path routes | | `hub/bridge/external-agent-blob-store.mjs` | `learning_paths` mergeById only | | `lib/flow/flow-store.mjs` | Default `learning_paths: []` | | `lib/hub-proposal-personal-self-apply.mjs` | Refuse path kinds (no admit) | | `hub/server.mjs` | Self-hosted mounts | | `docs/HUB-API.md` | Honesty — new routes + gate default off | | `docs/openapi.yaml` | Route shapes | | `docs/PROPOSAL-LIFECYCLE.md` | `learning-path` queue note; T5 not admitted | | `docs/ROADMAP.md` | Status | | `docs/OVERSEER-HANDOVER.md` | NEXT | | `test/path-list-*.test.mjs` | Seven-tier | | `.env.example` | `PATH_WRITES_ENABLED` name only, if that file exists on the tip | **Forbidden in this Auto** - Any path under `~/scooling/` - `PATH_WRITES_ENABLED` assigned enabled in source - T5 path fingerprint / admit - `writeNote` from `lib/path/**` - New blob filename - MCP / CLI / Hub wizard - `orchestrator_graphs` merge rewrite - BRAIN-PAIR / presence / Live chip / Home chat - KN-WORK-RUN-LIST --- ## 10. Out of scope - AGENT-WORK-CHAT-HOME-BIND (Scooling) - Scooling Path-row harvest / `parseWorkOpenQuery` widen - KN-WORK-RUN-LIST / `getRun` / live Run rows - Live model or agent runtime on My Work - Chat on Home / Home prompt replacement - BRAIN-PAIR-b (device not ready) - SOCIAL-OPEN-RANGE / Live threads - MuseHub F7 - Conversation vault / cookie / `localStorage` transcript - Backfill of already-approved Review notes into `learning_paths[]` - Path delete / hard purge - Project/org harvest on Scooling (store supports the scopes; consumer stays personal later) --- ## 11. Definition of Done (KN-WORK-PATH-LIST-b) - [ ] Freeze-review **pass** before Auto starts - [ ] D1–D12 + §3–§5 implemented; `PATH_WRITES_ENABLED` default off - [ ] Seven-tier `test/path-list-*.test.mjs` green locally - [ ] Build verification **pass** before ROADMAP → DONE - [ ] No Scooling file edits - [ ] No secrets committed - [ ] Both governance docs updated together - [ ] Feature-branch hygiene; merge remains Tier 3 (or SD-21 land with no live flip) --- ## 12. Simple summary / technical summary **Simple:** Knowtation will keep a real list of learning paths — the things you start from “what's on your mind?” — so My Work can later show your actual paths instead of only the sample. Starting a path on Home still does not jump to My Work in this step. Chat stays on My Work. Nothing is marked Live. **Technical:** Add `knowtation.learning_path/v0` rows in `hub_flow_store.json` `learning_paths[]`. Ship `GET api/v1/learning-paths`, `GET api/v1/learning-paths/:path_id`, and gated `POST api/v1/learning-paths/proposals` (`path_create` \| `path_update` \| `path_archive`). Hosted apply copies the task approve hook. Blob merge unions by `path_id`. T5 stays refused. Scooling harvest and HOME-BIND stay later tips. **Recommendation:** Freeze this, pass review, then Auto KN-WORK-PATH-LIST-b on Knowtation only. Do not start Scooling harvest or Home bind until the seven-tier list/get is green and BV passes. Do not Auto pairing.