feat(apps): create sidebar folders from a scanned project - #8924
Conversation
|
/ai-review override fable 0e9947f: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override design 0e9947f: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
|
/ai-review override first-principles 0e9947f: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the design AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Opus 4.8 Review — ✅ human override acceptedReviewed Human judgment by @CrysisDeu overrides the Opus 4.8 finding for Verdict recorded from an authorized human decision for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A real, thrice-filed gap solved at the right seam: read-only scan, preview-then-create through the single existing folder write path, fully reversible (no schema, no config, off by default). [DESIGN-REVIEWED] d47aaea |
First Principles Review (Fable 5) — ✅ human override accepted@CrysisDeu overrode this lane for |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of I have what I need: the blind read cleared every primary control, the pixels are clean, and the reconciliation leaves only secondary-copy ambiguities plus one coverage gap (the app-store listing that gates this UX-Verdict: CONCERNS Cold reader used every primary control confidently; what's unshown is the app-store card users must find first, plus two secondary-copy ambiguities. Watch
Evidence gaps
[UX-REVIEWED] d47aaea |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
A chat folder already carries a project_dir and nests via parent_id, but a monorepo or a directory of sibling repositories needs one folder per package before per-package steering loads at all — and assembling N sub-folders by hand is work nobody does. This adds a pure read-only scanner (project_scan.py) that detects packages via repository/manifest/deploy-root/workspace-member signals with two confidence tiers, honoring the project's own .gitignore with git semantics; two endpoints (scan = dry-run preview, scaffold = create the confirmed selection through the existing folder create path); and a manifest-only builtin app providing the preview/confirm UI. Detailed design notes live in docs/system-specs. The Store listing carries the fields the current app-listing contract requires: useCases and configuration key arrays on the APP_MANIFEST_KEY entry (with the strings in all 13 catalogs), and a screenshots entry pointing at a real UI capture of the scan preview — taken against a synthetic monorepo, so the image carries no real project names. One generated file outside the feature's own sources is updated: error-code-baseline.json is re-snapshotted because extracting create_folder_record moved two of chat_folders.py's refusals behind a FolderCreateError that carries a `code`, so its missing_code count IMPROVES 21 -> 19 and test_baseline_is_not_stale requires the snapshot be refreshed. Nothing is regenerated to silence a regression. Takeover of PR #5890 (author unresponsive 48h after two fix requests). This revision carries the last open reviewer finding: - create_folder_record ran _validate_project_dir (realpath + isdir + sensitive-path scan) synchronously on the event loop; the scaffold calls it once per selected directory, so a slow or network-mounted directory stalled every other request for the whole scaffold. The validator now runs via asyncio.to_thread, and a thread-identity test pins it off the loop (GPT 5.6 blocker on #5890). - Rebased onto current main: ProjectPicker keeps both main's keyboard-isolation barrier and this PR's narrow-viewport max-w clamp; error-code-baseline.json carries main's chat_handlers.py improvement alongside this PR's chat_folders.py improvement. Earlier review rounds on #5890 (scaffold write guards, redacted scanner warnings, .gitignore grammar refusal as DeclarationError, off-loop root resolution, dropped zero-consumer groups field, all-chunk budget bump 9750 -> 9800 KB) are carried unchanged. Review round 2 on #8924 (GPT 5.6 + UX lanes on 0e9947f): - Every error the page renders goes through the shared ErrorNotice (AUTOSDE errors-use-error-notice, blocking): the refused-root notice under the field, the two create-refusal notices, and the per-folder refusals in the results card. All four leave askAgent off with a comment naming the unsaved draft (typed root / ticked selection) the hand-off would unmount. Test ids and the field's aria-describedby target are preserved. - A failed re-scan no longer destroys the hand-tuned preview: scanMut clears only the root error and stale prompt up front and replaces the preview/selection/result on SUCCESS; while a scan is in flight the preview card is a disabled fieldset, so a stale preview cannot be confirmed either. Pinned by a test that hand-tunes, re-scans into a 500, and asserts the selection count is unchanged and re-enabled. - "Offered" tier badge renamed to "Possible match" across all 13 catalogs (cold read could not tell it from "Confident"). - The raw machine-readable `code` line under a failed row is gone; the server prose already carries the message. - Screenshots re-captured from HEAD via an isolated capture entry (website/capture/project-scaffolder.*, scripts/capture-project- scaffolder.mjs) that mounts the real page against a synthetic monorepo: scan-preview, nested-open, results, empty, stale, root-refused, rescan-failed. The store listing screenshot is the same new scan-preview frame. Supersedes #5890 Closes #2516 Co-authored-by: Billy Gerhard <billygerhard@gmail.com>
0e9947f to
b6cda0b
Compare
|
/ai-review override fable 33ec3c6: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override design 33ec3c6: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the design AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 33ec3c6: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
|
/ai-review override fable db55a78: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
|
/ai-review override design db55a78: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles db55a78: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
|
|
/ai-review override fable 13d3d36: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
|
/ai-review override design 13d3d36: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles 13d3d36: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
A chat folder already carries a project_dir and nests via parent_id, but a monorepo or a directory of sibling repositories needs one folder per package before per-package steering loads at all — and assembling N sub-folders by hand is work nobody does. This adds a pure read-only scanner (project_scan.py) that detects packages via repository/manifest/deploy-root/workspace-member signals with two confidence tiers, honoring the project's own .gitignore with git semantics; two endpoints (scan = dry-run preview, scaffold = create the confirmed selection through the existing folder create path); and a manifest-only builtin app providing the preview/confirm UI. Detailed design notes live in docs/system-specs. The Store listing carries the fields the current app-listing contract requires: useCases and configuration key arrays on the APP_MANIFEST_KEY entry (with the strings in all 13 catalogs), and a screenshots entry pointing at a real UI capture of the scan preview — taken against a synthetic monorepo, so the image carries no real project names. One generated file outside the feature's own sources is updated: error-code-baseline.json is re-snapshotted because extracting create_folder_record moved two of chat_folders.py's refusals behind a FolderCreateError that carries a `code`, so its missing_code count IMPROVES 21 -> 19 and test_baseline_is_not_stale requires the snapshot be refreshed. Nothing is regenerated to silence a regression. Takeover of PR #5890 (author unresponsive 48h after two fix requests). This revision carries the last open reviewer finding: - create_folder_record ran _validate_project_dir (realpath + isdir + sensitive-path scan) synchronously on the event loop; the scaffold calls it once per selected directory, so a slow or network-mounted directory stalled every other request for the whole scaffold. The validator now runs via asyncio.to_thread, and a thread-identity test pins it off the loop (GPT 5.6 blocker on #5890). - Rebased onto current main: ProjectPicker keeps both main's keyboard-isolation barrier and this PR's narrow-viewport max-w clamp; error-code-baseline.json carries main's chat_handlers.py improvement alongside this PR's chat_folders.py improvement. Earlier review rounds on #5890 (scaffold write guards, redacted scanner warnings, .gitignore grammar refusal as DeclarationError, off-loop root resolution, dropped zero-consumer groups field) are carried unchanged. The PR no longer touches scripts/check-bundle-size.mjs: main re-measured the `all` (eager i18n catalog) ceiling in #8935 after this PR's 13 catalogs tipped the drifted 10490 KB budget, so the entry's comment about this feature now lives on main. Review round 2 on #8924 (GPT 5.6 + UX lanes on 0e9947f): - Every error the page renders goes through the shared ErrorNotice (AUTOSDE errors-use-error-notice, blocking): the refused-root notice under the field, the two create-refusal notices, and the per-folder refusals in the results card. All four leave askAgent off with a comment naming the unsaved draft (typed root / ticked selection) the hand-off would unmount. Test ids and the field's aria-describedby target are preserved. - A failed re-scan no longer destroys the hand-tuned preview: scanMut clears only the root error and stale prompt up front and replaces the preview/selection/result on SUCCESS; while a scan is in flight the preview card is a disabled fieldset, so a stale preview cannot be confirmed either. Pinned by a test that hand-tunes, re-scans into a 500, and asserts the selection count is unchanged and re-enabled. - "Offered" tier badge renamed to "Possible match" across all 13 catalogs (cold read could not tell it from "Confident"). - The raw machine-readable `code` line under a failed row is gone; the server prose already carries the message. - Screenshots re-captured from HEAD via an isolated capture entry (website/capture/project-scaffolder.*, scripts/capture-project- scaffolder.mjs) that mounts the real page against a synthetic monorepo: scan-preview, nested-open, results, empty, stale, root-refused, rescan-failed. The store listing screenshot is the same new scan-preview frame. Review round 4 on #8924 (GPT 5.6 on f422e14): - _declared_members accumulated member paths with list.extend at two levels, so a declaration repeating one glob (a 512 KiB file admits tens of thousands) re-appended the same matched directories per repeat and peak memory scaled with patterns x matches. Both accumulators are now order-preserving dicts keyed on the path, so the bound is the number of unique members. Pinned by a test that declares "packages/*" 2000 times and asserts _declared_members hands back exactly the two unique members. Review round 5 on #8924 (GPT 5.6 on 895d5c1): - The stale-selection prompt originates from a rejected create (400 folder_scaffold_selection_stale), so it is an error by origin under AUTOSDE errors-use-error-notice: its sentence now renders through ErrorNotice (askAgent off, the ticked selection is unsaved), with the dropped paths and the Re-scan action beneath. Test pins the alert. - _resolve_root's ancestor-of-sensitive-root refusal now writes a SEL denied api-access event (operation chat.folder_scan_root) before raising, matching every other security refusal in the module. Test pins one denied event per endpoint. Review round 6 on #8924 (GPT 5.6 on 0c4d88c): - Create acts on scan.root, so a root typed after the scan but never scanned could confirm the PREVIOUS project's preview. The page now remembers the field value each preview was scanned from; while the field differs, both Create buttons are disabled and a hint asks for a scan. The stale prompt's Re-scan re-runs that remembered input (not the resolved root) so a `~` or symlinked spelling does not read as drift. Test pins disabled -> re-scan -> enabled; root-drifted.png. Review round 8 on #8924 (GPT 5.6 on e170e39): - The root form row (field + Browse + Scan) had no narrow provision (AUTOSDE narrow-viewport-required, blocking): at 320px with long translated labels the path field was squeezed. Narrow-first now: the field spans the row and the two actions share the row beneath it; from `sm` up all three sit in one row as before. Capture harness gained a 320px scene (narrow-320.png) asserting the field is wide and the actions sit below it. Review round 9 on #8924 (UX CONCERNS on a2e5b56): - Results card lists the already-existing paths like the created ones, so the tally can be reconciled; the capture fixture no longer invents a skip the server cannot produce (root_existing drives the one skip) and its warning uses the scanner's shipping string. - One rule for "this preview cannot be confirmed": Create is disabled while the selection is stale, as it already was while the root field had drifted; a preview kept through a failed re-scan is labelled as the last successful scan's. - folder_project_dir_moved prose reworded from machine language to "That folder was moved or replaced after the scan — re-scan and retry". - Row label "Signals" -> "Why it matched" across 13 catalogs. - Capture harness gains a 390px picker-open scene proving the shared ProjectPicker's viewport clamp (picker-open.png). Review round 10 on #8924 (UX CONCERNS on 59ee27b): - _validate_project_dir's two refusals now read "Project directory must be an absolute path / an existing directory" instead of naming the project_dir field; the manual folder flow shares the string, so parity holds. The moved-directory prose says "directory", reserving "folder" for the sidebar like every other string on the page. - Capture harness gains a create-refused scene (whole-call 500 beside the Create button through ErrorNotice). Review round 11 on #8924 (GPT 5.6 + UX on 12b51a2): - The scan root was validated on one thread and pinned (lstat) on another, leaving a window in which an ancestor swapped for a symlink would redirect the whole walk into a tree the validation never saw. _resolve_root now records the root's (st_dev, st_ino) in the same breath as the validation and hands it to scan(expected_identity=...); scan refuses with RootChangedError before its first read when the name now reaches a different inode, and both endpoints answer 400 folder_scan_root_invalid with a SEL denied audit. Pinned by unit tests (matching / mismatched / symlink-swapped identity) and an endpoint test that swaps the root between validation and scan. - UX copy: the picker trigger reads "Choose directory" (no second "Browse" beside the picker's own tab); the primary button reads "Create sidebar folders"; the counter drops "Root folder +" when the root already has its folder. The create-refused and rescan-failed capture scenes now use refusal prose the server really sends. Review round 12 on #8924 (UX on 800a517): - One `cannotConfirm` rule now gates both Create buttons: root drifted, selection stale, or the last re-scan failed (the preview on screen is the previous scan's). The two moved/replaced refusals share one sentence. The disclosure's bulk pair is labelled "Select all inside" / "Select none inside" so two identical pairs never sit on one page. New capture scenes: root-new (the "Root folder + N" counter) and scanning (in-flight, dimmed preview). Review round 13 on #8924 (UX on df6b8b7): - A create refused because the ROOT moved (folder_scan_root_invalid) is the same situation as a stale selection, so it now takes the same path: stale banner + Re-scan, Create disabled until a scan succeeds — no more live button over "re-scan and retry" text. Whole-call create refusals carry the scope title "No folders were created" so they read apart from a per-folder failure in the results. Capture fixture's create-refused case is the server's real 429 rate-limit refusal (a retryable one, where an enabled button is right); a creating scene captures the in-flight create. Review round 15 on #8924 (GPT 5.6 on d83e09c): - The scan/create endpoints are plain dashboard routes, not behind the app-backend proxy, so the proxy's enablement gate never saw them and a dashboard-user token bypasses the app-scope check: the app ships defaultEnabled:false yet both endpoints answered for a person who never turned it on. Both handlers now refuse with 403 app_not_enabled and a SEL denied audit unless is_app_enabled("project-scaffolder"), the same shape as the proxy's gate. Test fixture opens the gate for the suite; a disabled-app test pins the 403 + audit on both endpoints. Review round 16 on #8924 (GPT 5.6 on 7a680b3): - A root whose identity could not be read at validation time was handed to scan() as expected_identity=None, which scan reads as "no caller pinned this" and skips the root-swap comparison. _resolve_root now refuses such a root (400 folder_scan_root_invalid) so the endpoints never scan unpinned; test pins that no scan runs. The additive-scaffold property test builds its own state and now opens the enablement gate like the fixture does (it was the one red on the previous head). Review round 17 on #8924 (GPT 5.6 on 33ec3c6): - Create re-resolved the submitted root; with a component swapped for a symlink after the preview (and nothing selected, so the offered-set cross-check had nothing to catch), it would scan the redirected tree and persist a folder for an unpreviewed directory. Create now requires the submitted root to be a realpath fixed point: a re-resolution that lands elsewhere is refused (400 folder_scan_root_invalid + SEL denied) before any scan. Test with a root replaced by a symlink. - The empty-preview branch never rendered the stale notice, so a root-moved refusal of the root-only create just disabled the button. The notice + Re-scan action is now a shared StaleNotice rendered by both branches; test. Review round 18 on #8924 (GPT 5.6 on db55a78): - StaleNotice's ErrorNotice carries the hand-off decision comment (errors-use-error-notice): the extraction had left it at the call sites only. - The round-17 create test read the response body after the test client had closed (buffered on Linux, not on Windows) — read inside the scope. Review round 19 on #8924 (GPT 5.6 on 13d3d36): - The Windows scandir fallback stated its check-then-read window as unclosable. It is closed by HOLDING rather than racing: a CreateFile handle (BACKUP_SEMANTICS | OPEN_REPARSE_POINT, share READ|WRITE but not DELETE) is taken before the identity and resolution checks and released after the listing is consumed; while held, neither the directory nor any ancestor can be renamed or deleted, which every junction swap needs first. A hold that cannot be taken raises, so the directory is reported unread instead of read unpinned. Tests pin the hold bracketing every listed directory's checks and the fail-closed path. Supersedes #5890 Closes #2516 Co-authored-by: Billy Gerhard <billygerhard@gmail.com>
|
/ai-review override fable d47aaea: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
|
/ai-review override design d47aaea: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
/ai-review override first-principles d47aaea: Maintainer decision: project-scaffolder ships as a built-in by design. Folder scaffolding is a once-per-project setup action that must run against the gateway's own project tree and folder-create path, so it is off by default (defaultEnabled:false) and adds no permanent core-UI surface; the scan/create endpoints stay the repackaging boundary if it is later moved to the KiroCrewApps registry. Accepting the no-new-builtin-apps exception for this commit (carried over from #5890). |
Human judgment recorded@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
|
Thanks for carrying this over from #5890 — the feature is clearly wanted (three independent filers in a week), and the shape is right: read-only scan, preview-then-create, one writer through Before approving I want to raise three points. None of them are code defects; they are about proportionality and two decisions that I think need a human on record. 1. Is the defence depth proportional to the surface?A 10.5k-line diff for "create sidebar folders from a directory" is a lot, and reading the round log in the PR body, a meaningful share of The surface being protected is: a read-only walk, gated behind an app that ships I am not asking to remove it — it is written carefully and tested. I am asking whether the maintainers consider this the intended bar for a feature of this blast radius, because it sets the precedent for the next scanner-shaped PR. If the answer is "yes, host-filesystem enumeration always gets this treatment", a one-line note in 2. The
|
Problem / Motivation
A chat folder already carries a
project_dirand nests viaparent_id, so a chat opened inside one is steering- and scope-correct for its package. What is missing for a monorepo, or a directory of sibling repositories, is population: assembling N sub-folders by hand is work nobody does, so per-package steering never loads. Filed as #2516.Takeover note. This PR supersedes #5890 by @billygerhard, carried onto a same-repo branch. It is the same feature, rebased onto current
main, plus the one reviewer-requested fix still open there (theasyncio.to_threadchange increate_folder_record). The author did not respond within 48h to two fix requests (comment 1, comment 2). Authorship is preserved viaCo-authored-by. #5890 stays open for the author's reference; it is not closed by this PR.Why it matters
Multi-package workspaces are the norm for real projects (three independent filers hit this within one week: #1637, #2164, #2516), and today the folder feature's per-package steering silently never engages for them — the user would have to hand-build the tree first, so the capability that exists goes unused exactly where it helps most.
What changed (motivation → approach → change)
Goal: populate the folder tree from the filesystem, without trusting the scan and without new persistence. Approach: a pure read-only scanner plus a preview-then-create endpoint pair, with the UI as a thin surface — chosen over teaching the folder API itself to scan (couples a slow filesystem walk into the create path) and over a client-side walker (the browser cannot see the disk; the server must re-derive anyway).
Scanner (
src/kiro_crew/project_scan.py): walks the pointed-at tree read-only and returns candidate packages. Signals: the directory's own.git/.kiro, a recognized manifest (npm, Python, Rust, Go, JVM, Flutter, PHP, Ruby, Elixir, Swift, Deno, Scala), a deploy-root marker (firebase.json,vercel.json,cdk.json,Procfile, ...), or being named by a workspace declaration (workspaces,pnpm-workspace.yaml, Cargo workspace,go.work). Two confidence tiers decide what arrives pre-ticked. The project's own.gitignoreprunes with git semantics viapathspec(nested files stack,!negation, ignored directories are never entered), alongside a name-prune list (node_modules,venv,DerivedData, ...). Scanner warning reasons are redacted at construction (redact_exfiltration_urls+redact_credentials); a.gitignorepattern the grammar refuses becomes aDeclarationError(a warning + that layer dropped), never an HTTP 500.Endpoints (
dashboard/chat_folder_scaffold.py):POST /api/project-scaffold/scan(dry-run preview, creates nothing) andPOST /api/project-scaffold/create(creates the confirmed selection through the existing folder create path — one writer, no new persistence, no schema change). The scaffold re-derives candidates server-side and refuses any selected path the fresh scan did not offer, so a stale preview and a forged path are the same 400; a candidate or root swapped for a symlink after the scan is refused (folder_project_dir_moved). The scaffold write carries the same guards as the folder create route: unattributable-caller refusal, oneFOLDER_CREATErate-budget unit per internal call, and the caller's derived app identity stamped asowner_app. Additive only: re-scans offer additions, existing folders are reported and never touched, partial failure is reported rather than rolled back. Root resolution and the folder-overlap guard run off the event loop.create_folder_recordextraction (dashboard/chat_folders.py): the single folder create path, shared by the folder API and the scaffold, so neither can end up with weaker path validation, a danglingparent_id, weaker app-ownership isolation, or an unserialized store write than the other. New in this PR vs #5890:_validate_project_dir(realpath + isdir + sensitive-path scan) now runs viaasyncio.to_threadinsidecreate_folder_record. The scaffold calls it once per selected directory in a loop, so a slow or network-mounted directory previously stalled every other request for the whole scaffold.Builtin app (
project-scaffolder,defaultEnabled: false): directory picker (reusing the coreProjectPicker), grouped preview with per-group select-all, confident-first ordering, a collapsed disclosure for speculative sub-folders, result reporting. Manifest-only; it calls the two host endpoints and duplicates no logic. Delivery as a built-in app was ruled by the maintainer on #5890 (theno-new-builtin-appsexception is re-recorded on this PR via/ai-review override).Review round 2 on this PR (GPT 5.6 blocking + UX CONCERNS on
0e9947f23):ErrorNotice(AUTOSDEerrors-use-error-notice,blocking: true): the refused-root notice under the field, both create-refusal notices, and the per-folder refusals in the results card. All four leaveaskAgentoff with aNo hand-offcomment naming the unsaved draft the hand-off would unmount (the typed root / the ticked selection). Test ids and the field'saria-describedbytarget are preserved.scanMutclears only the root error and the stale prompt up front and replaces preview/selection/result on SUCCESS; while a scan is in flight the preview card is a native disabled<fieldset>(data-testid="preview-card"), so a stale preview cannot be confirmed either.codeline under a failed results row is gone; the server prose already carries the message.Round 3 (rebase only, head
f422e14e8): rebased onto main past #8935 (all-chunk ceiling) and #8951 (drops the deadpages.chatPage.dismiss_upload_errorkey that had main itself at 30 dead keys against a 29 baseline). Both reds onb6cda0b58— Bundle Size Gate anddeadKeys.test.tsin Frontend Tests (3) — were main-inherited; no code change in this round.Round 4 (GPT 5.6 on
f422e14e8, head895d5c1a3):_declared_membersinproject_scan.pyaccumulated member paths withlist.extendat two levels, so a declaration repeating one glob (a 512 KiB file admits tens of thousands) re-appended the same matched directories per repeat and peak memory scaled with patterns × matches. Both accumulators are now order-preserving dicts keyed on the path (bound = unique members; order preserved for the preview). Pinned bytest_repeated_globs_do_not_multiply_the_member_set(2000×packages/*→_declared_membershands back exactly 2). Also rebased ontod4c2cbf22.Round 5 (GPT 5.6 on
895d5c1a3, head0c4d88cb2): (1) the stale-selection prompt originates from a rejected create (400folder_scaffold_selection_stale), so undererrors-use-error-noticeit is an error by origin — its sentence now renders throughErrorNotice(askAgentoff, the ticked selection is unsaved), with the dropped paths and the Re-scan action beneath;stale.pngre-captured. (2)_resolve_root's ancestor-of-sensitive-root refusal now emits a SELdeniedapi-access event (operation=chat.folder_scan_root) before raising, matching every other security refusal inchat_folder_scaffold.py; the sensitive-root test pins one denied event per endpoint.Round 6 (GPT 5.6 on
0c4d88cb2, head4ed13d3a5): Create acts onscan.root, so a root typed after the scan but never scanned could confirm the previous project's preview. The page now remembers the field value each preview was scanned from (scannedInput); while the field differs, both Create buttons are disabled and a muted hint (root-drifted, a validation hint, not an error) asks for a scan. The stale prompt's Re-scan re-runs that remembered input rather than the resolved root, so a~or symlinked spelling does not read as drift. Test pins disabled → re-scan → enabled;root-drifted.pngadded. Also rebased onto0d65dc969(error-code-baseline.jsonre-resolved: main'schat_title.pyentry removal + this PR'schat_folders.py21 → 19; totals 1143, verified against the per-file map).Round 7 (head
e170e394f): all five AI lanes PASS on4ed13d3a5; the one red was this PR's own —jaStyle.test.tsflagged the newjahint forフォルダwithout the long-vowel mark the style guide requires. Fixed toフォルダー; no other change.Round 8 (GPT 5.6 on
e170e394f, heada2e5b569c): the root form row (field + Browse + Scan) had no narrow provision (narrow-viewport-required, blocking). Narrow-first now: the field spans the row and the two actions share the row beneath it; fromsmup all three sit in one row as before (desktop frames byte-identical). The capture harness gained a 320px scene that asserts the field is wide and the actions sit below it —narrow-320.png.Round 9 (UX CONCERNS on
a2e5b569c, head59ee27bc9): (1) the results card lists already-existing paths like created ones so the tally reconciles, and the capture fixture no longer invents a skip the server cannot produce (root_existingdrives the one skip) nor a warning string that does not ship; (2) one rule for "this preview cannot be confirmed" — Create is disabled while the selection is stale, as it already was on root drift, and a preview kept through a failed re-scan is labelled "Showing the preview from the last successful scan."; (3)folder_project_dir_movedprose reworded to "That folder was moved or replaced after the scan — re-scan and retry"; (4) row label "Signals" → "Why it matched" (13 catalogs); (5) a 390pxpicker-open.pngproves the sharedProjectPicker's viewport clamp. All frames re-captured from this head. Also rebased onto5767e0d9c.Round 10 (UX CONCERNS on
59ee27bc9, head12b51a2dd):_validate_project_dir's two refusals now read "Project directory must be an absolute path / an existing directory" (the manual folder flow shares the string, so parity holds); the moved-directory prose says "directory", reserving "folder" for the sidebar; acreate-refused.pngscene shows the whole-call create refusal beside the Create button throughErrorNotice. Rebased ontof4268fb56.Round 11 (GPT 5.6 fenced blocker + UX on
12b51a2dd, head800a517d6): (1) the scan root was validated on one thread and pinned (lstat) on another, leaving a validate→scan window in which an ancestor swapped for a symlink would redirect the whole walk into a tree the validation never saw —_resolve_rootnow records the root's(st_dev, st_ino)in the same breath as the validation and hands it toscan(expected_identity=…);scanrefuses withRootChangedErrorbefore its first read when the name reaches a different inode, and both endpoints answer 400folder_scan_root_invalidwith a SELdeniedaudit. Pinned by three unit tests (matching / mismatched / symlink-swapped identity) and an endpoint test that swaps the root between validation and scan. (2) UX: the picker trigger reads "Choose directory" (no second "Browse" beside the picker's own tab); the primary button reads "Create sidebar folders"; the counter drops "Root folder +" when the root already has its folder; thecreate-refused/rescan-failedscenes now use refusal prose the server really sends. All frames re-captured.Round 12 (UX on
800a517d6, headdf6b8b70b): onecannotConfirmrule now gates both Create buttons — root drifted, selection stale, or the last re-scan failed; the two moved/replaced refusals share one sentence ("…after the scan — re-scan and retry"); the disclosure's bulk pair reads "Select all inside" / "Select none inside"; two more states captured —root-new.png(the "Root folder + N selected" counter when the root has no folder yet) andscanning.png(in-flight, dimmed disabled preview).Round 13 (UX on
df6b8b70b, headb12230894): a create refused because the root moved (folder_scan_root_invalid) now takes the stale path — banner + Re-scan, Create disabled until a scan succeeds — instead of a live button over "re-scan and retry" text; whole-call create refusals carry the scope title "No folders were created" so they read apart from a per-folder failure in the results; thecreate-refusedscene is now the server's real 429 rate-limit refusal (retryable, so an enabled button is correct there), and acreating.pngscene captures the in-flight create. Rebased ontobe0c92942.Round 14 (rebase only, head
d83e09c2c): all five AI lanes PASS onb12230894(UX included). The one red — Backend Tests (3.12, 4)test_snapshot.pynotification-copy ordering, failing identically on a targeted re-run — is the test main fixed in #9047 ("read the copy ordering before the worker is released"); rebased ontoe992b7771to pick it up. No code change in this round.Round 15 (GPT 5.6 fenced blocker on
d83e09c2c, head7a680b3f2): the scan/create endpoints are plain dashboard routes, not behind the app-backend proxy, so the proxy's enablement gate never saw them and a dashboard-user token bypasses_enforce_app_scope— the app shipsdefaultEnabled: falseyet both endpoints answered for a person who never turned it on. Both handlers now refuse with 403app_not_enabled+ a SELdeniedaudit unlessis_app_enabled("project-scaffolder"), the same shape as the proxy's gate (_refuse_when_app_disabled, checked before the create route's attribution and rate-limit guards). The scaffold test fixture opens the gate for the suite;TestDisabledApppins the 403 + audit on both endpoints with nothing scanned or created.Round 16 (GPT 5.6 fenced blocker on
7a680b3f2, head33ec3c690): a root whose identity could not be read at validation time was handed toscan()asexpected_identity=None, whichscanreads as "no caller pinned this" and skips the root-swap comparison._resolve_rootnow refuses such a root (400folder_scan_root_invalid) so the endpoints never scan unpinned; a test pins that no scan runs. The additive-scaffold property test builds its own state and now opens the enablement gate like the fixture does — that was the one red (Windows shard 1,KeyError: 'failed'on a 403 body) on the previous head. Rebased onto9509cbbde.Round 17 (GPT 5.6, two findings on
33ec3c690, headdb55a7830): (1) create re-resolved the submitted root; with a component swapped for a symlink after the preview — and nothing selected, so the offered-set cross-check had nothing to catch — it would scan the redirected tree and persist a folder for an unpreviewed directory. Create now requires the submitted root to be arealpathfixed point (the page submits the canonicalscan.root): a re-resolution that lands elsewhere is refused with the existing root-replaced 400 + SEL denied before any scan; test with a root replaced by a symlink. (2) The empty-preview branch never rendered the stale notice, so a root-moved refusal of the root-only create just disabled the button; the notice + Re-scan action is now a sharedStaleNoticerendered by both branches, with a test that Re-scan restores the button. Rebased onto234a85e1f.Round 18 (GPT 5.6 on
db55a7830, head13d3d36f3):StaleNotice'sErrorNoticenow carries theNo hand-offdecision comment itself (errors-use-error-notice) — the extraction had left it at the call sites only. Also: the round-17 create test read the response body after the test client had closed, which Linux tolerated and Windows did not (the one red on the previous head, the 400 assertion itself had passed); the read moved inside the client's scope. Rebased onto31cd87f29.Round 19 (GPT 5.6 fenced blocker on
13d3d36f3, headd47aaea58): the Windowsscandirfallback stated its check-then-read window as unclosable (noO_NOFOLLOW, no descriptorscandir). It is now closed by holding rather than racing:_hold_directoryopens aCreateFilehandle on the directory (BACKUP_SEMANTICS | OPEN_REPARSE_POINT, shareREAD|WRITEbut notDELETE) before the identity and resolution checks and releases it only after the listing is consumed. While held, neither the directory nor any ancestor can be renamed or deleted — which every junction swap needs first — so the name the checks resolve and the name the listing reads are the same directory. A hold that cannot be taken raises, and the walker already reports such a directory as unread, so the path fails closed instead of reading unpinned. Tests pin the hold bracketing each listed directory's checks and the fail-closed path (platform-independent, via the forced fallback branch); the Windows CI shards exercise the real handle in every existing scan test. Rebased onto30f31c247.No config key is added.
scaffold.extra_manifest_signalsandscaffold.depth_capwere dropped in earlier review rounds. The scan response'sgroupsfield was removed (zero consumers). The sharedProjectPickergainsmax-w-[calc(100vw-16px)]so its fixed 400px panel clamps on narrow viewports; on rebase this is merged with main's keyboard-isolation barrier on the same element.Generated file touched outside the feature's own sources:
error-code-baseline.json— extractingcreate_folder_recordmoved two ofchat_folders.py's refusals behind aFolderCreateErrorthat carries acode, so itsmissing_codecount improves 21 → 19 andtest_baseline_is_not_stalerequires the snapshot refreshed. On rebase the file carries main's ownchat_handlers.py49 → 48 improvement alongside (totals 1150 → 1148). Nothing is regenerated to silence a regression.Bundle budget: this PR no longer touches
scripts/check-bundle-size.mjs. Its 13 catalogs (~55 KB of eager strings) tipped main's driftedallceiling (0.4% headroom); that was re-measured on main in #8935, and the entry's comment there records this feature. The app page sits behind a lazyimport()insrc/apps/builtinRegistry.ts, so nothing but strings reaches the chunk.Tests
Carried from #5890: scanner fixtures over real temp directory layouts (symlinks, permissions, depth caps, gitignore negation/nesting/scope-boundary, the SwiftPM reproduction), hypothesis property tests ("two scans of an unchanged tree compare equal", "no candidate is ever gitignore-matched", "ignoring a child prunes exactly that subtree"), and endpoint tests including byte-identical 400-body parity with the manual folder flow, post-scan symlink-swap refusal, concurrent-scaffold exactly-once creation, and route-registration guards. Frontend tests over the app page: keyboard reach, selection isolation, zero-fetch picker interaction, stale-selection rescan flow.
Round 2 (frontend,
src/test/ProjectScaffolderPage.test.tsx):keeps the hand-tuned preview and selection when a re-scan fails(Select all → re-scan into a 500 → same selected count,preview-cardre-enabled, root error shown) andrenders every error through the shared error surface(root-erroris arole="alert"contained by the#scaffolder-root-errorwrapper the field points at). The failed-row assertion now reads the alert insidefailed-rowsand asserts the raw code is NOT rendered; "Offered" assertions read "Possible match".New in this PR:
TestCreateValidatesOffTheLoop::test_project_dir_validation_runs_off_the_loop_thread(test/test_chat_folder_scaffold.py) — spies on_validate_project_dirthroughcreate_folder_recordand asserts by thread identity that it never runs on the loop thread. A refactor that inlines the validator back onto the loop fails this test.Local gates run on this head: black (diff-scoped baseline gate), isort, flake8, mypy (
src/kiro_crew/),tsc -b --force, eslint on touched frontend files,npm run i18n:check. Test suites are left to CI.Manual verification
Per #5890: exercised end to end against a directory of sibling repositories, an npm-workspaces monorepo with Firebase apps, and an iOS/SwiftPM project whose gitignored dependency store previously flooded the preview. Scan → tick → create → folders appear correctly nested; re-scan offers only additions; chats opened in created folders load that package's steering. The to_thread change is behaviour-preserving on the success path and is covered by the thread-identity test above.
Screenshots / video
All frames are captured from THIS head via an isolated capture entry (
website/capture/project-scaffolder.html+scripts/capture-project-scaffolder.mjs) that mounts the realProjectScaffolderPageagainst a synthetic monorepo, so every string shown is the string that ships (round-2 UX finding: the previous frames showed superseded copy).Scan preview — confident rows pre-ticked, "Possible match" rows offered, speculative sub-folders behind the collapsed disclosure, "Root folder + N selected":
A failed re-scan keeps the hand-tuned preview (7 selected after "Select all") and renders the refusal through
ErrorNoticeunder the field:Results card — created paths, "already existed", and a refused folder rendered through
ErrorNotice(no raw code line):More states
Disclosure expanded — "Inside {name}" chips and the section's own bulk buttons:
Empty scan — "Create the root folder only":
Root without a folder yet — the counter promises it ("Root folder + N selected"):
Re-scan in flight — "Scanning", kept preview disabled and dimmed:
Create in flight — "Creating", button disabled:
Stale-selection refusal with Re-scan:
Whole-call create refusal, rendered through
ErrorNoticebeside the Create button:Refused root, rendered through
ErrorNoticeand still the target of the field'saria-describedby:Field edited after the scan — Create disabled with a hint until the new directory is scanned:
320px viewport — the path field spans the row, Browse/Scan sit beneath it, every row wraps:
Shared
ProjectPickeropen at 390px — the panel clamps inside the viewport (max-w-[calc(100vw-16px)]):The created sidebar tree (from #5890, captured against a real dashboard):
Related Issues
Supersedes #5890 (kept open, not closed by this PR). Related: #2164 (workspace-first model this composes toward), #1637 (folded into #2164), #6611 (UX follow-ups deferred from #5890's review rounds).
Closes #2516
Pattern harvest
Rule candidate: review-prompt
Pattern: a synchronous filesystem validator (
realpath/isdir/stat) called from anasync defhandler body — especially one reached from a per-item loop — must be wrapped inasyncio.to_thread; the sibling call in the same function (_folder_project_overlap_denied) already was, which is the tell.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)