Skip to content

feat(apps): create sidebar folders from a scanned project - #8924

Merged
iamwhatever merged 1 commit into
mainfrom
feat/create-folders-from-project
Sep 7, 2026
Merged

feat(apps): create sidebar folders from a scanned project#8924
iamwhatever merged 1 commit into
mainfrom
feat/create-folders-from-project

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

A chat folder already carries a project_dir and nests via parent_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 (the asyncio.to_thread change in create_folder_record). The author did not respond within 48h to two fix requests (comment 1, comment 2). Authorship is preserved via Co-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 .gitignore prunes with git semantics via pathspec (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 .gitignore pattern the grammar refuses becomes a DeclarationError (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) and POST /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, one FOLDER_CREATE rate-budget unit per internal call, and the caller's derived app identity stamped as owner_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_record extraction (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 dangling parent_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 via asyncio.to_thread inside create_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 core ProjectPicker), 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 (the no-new-builtin-apps exception is re-recorded on this PR via /ai-review override).

Review round 2 on this PR (GPT 5.6 blocking + UX CONCERNS on 0e9947f23):

  • Every error the page renders goes through the shared ErrorNotice (AUTOSDE errors-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 leave askAgent off with a No hand-off comment naming the unsaved draft the hand-off would unmount (the typed root / the ticked selection). 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 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.
  • The "Offered" tier badge is renamed "Possible match" across all 13 catalogs (cold read could not tell it from "Confident"); pseudolocale regenerated.
  • The raw machine-readable code line 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 dead pages.chatPage.dismiss_upload_error key that had main itself at 30 dead keys against a 29 baseline). Both reds on b6cda0b58 — Bundle Size Gate and deadKeys.test.ts in Frontend Tests (3) — were main-inherited; no code change in this round.

Round 4 (GPT 5.6 on f422e14e8, head 895d5c1a3): _declared_members in project_scan.py 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 × matches. Both accumulators are now order-preserving dicts keyed on the path (bound = unique members; order preserved for the preview). Pinned by test_repeated_globs_do_not_multiply_the_member_set (2000× packages/*_declared_members hands back exactly 2). Also rebased onto d4c2cbf22.

Round 5 (GPT 5.6 on 895d5c1a3, head 0c4d88cb2): (1) the stale-selection prompt originates from a rejected create (400 folder_scaffold_selection_stale), so under errors-use-error-notice it is an error by origin — its sentence now renders through ErrorNotice (askAgent off, the ticked selection is unsaved), with the dropped paths and the Re-scan action beneath; stale.png re-captured. (2) _resolve_root's ancestor-of-sensitive-root refusal now emits a SEL denied api-access event (operation=chat.folder_scan_root) before raising, matching every other security refusal in chat_folder_scaffold.py; the sensitive-root test pins one denied event per endpoint.

Round 6 (GPT 5.6 on 0c4d88cb2, head 4ed13d3a5): 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 (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.png added. Also rebased onto 0d65dc969 (error-code-baseline.json re-resolved: main's chat_title.py entry removal + this PR's chat_folders.py 21 → 19; totals 1143, verified against the per-file map).

Round 7 (head e170e394f): all five AI lanes PASS on 4ed13d3a5; the one red was this PR's own — jaStyle.test.ts flagged the new ja hint for フォルダ without the long-vowel mark the style guide requires. Fixed to フォルダー; no other change.

Round 8 (GPT 5.6 on e170e394f, head a2e5b569c): 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; from sm up 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, head 59ee27bc9): (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_existing drives 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_moved prose 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 390px picker-open.png proves the shared ProjectPicker's viewport clamp. All frames re-captured from this head. Also rebased onto 5767e0d9c.

Round 10 (UX CONCERNS on 59ee27bc9, head 12b51a2dd): _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; a create-refused.png scene shows the whole-call create refusal beside the Create button through ErrorNotice. Rebased onto f4268fb56.

Round 11 (GPT 5.6 fenced blocker + UX on 12b51a2dd, head 800a517d6): (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_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 reaches a different inode, and both endpoints answer 400 folder_scan_root_invalid with a SEL denied audit. 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; the create-refused / rescan-failed scenes now use refusal prose the server really sends. All frames re-captured.

Round 12 (UX on 800a517d6, head df6b8b70b): one cannotConfirm rule 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) and scanning.png (in-flight, dimmed disabled preview).

Round 13 (UX on df6b8b70b, head b12230894): 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; the create-refused scene is now the server's real 429 rate-limit refusal (retryable, so an enabled button is correct there), and a creating.png scene captures the in-flight create. Rebased onto be0c92942.

Round 14 (rebase only, head d83e09c2c): all five AI lanes PASS on b12230894 (UX included). The one red — Backend Tests (3.12, 4) test_snapshot.py notification-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 onto e992b7771 to pick it up. No code change in this round.

Round 15 (GPT 5.6 fenced blocker on d83e09c2c, head 7a680b3f2): 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 ships defaultEnabled: false yet both endpoints answered for a person who never turned it on. Both handlers now refuse with 403 app_not_enabled + a SEL denied audit unless is_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; TestDisabledApp pins the 403 + audit on both endpoints with nothing scanned or created.

Round 16 (GPT 5.6 fenced blocker on 7a680b3f2, head 33ec3c690): 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; 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 onto 9509cbbde.

Round 17 (GPT 5.6, two findings on 33ec3c690, head db55a7830): (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 a realpath fixed point (the page submits the canonical scan.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 shared StaleNotice rendered by both branches, with a test that Re-scan restores the button. Rebased onto 234a85e1f.

Round 18 (GPT 5.6 on db55a7830, head 13d3d36f3): StaleNotice's ErrorNotice now carries the No hand-off decision 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 onto 31cd87f29.

Round 19 (GPT 5.6 fenced blocker on 13d3d36f3, head d47aaea58): the Windows scandir fallback stated its check-then-read window as unclosable (no O_NOFOLLOW, no descriptor scandir). It is now closed by holding rather than racing: _hold_directory opens a CreateFile handle on the directory (BACKUP_SEMANTICS | OPEN_REPARSE_POINT, share READ|WRITE but not DELETE) 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 onto 30f31c247.

No config key is added. scaffold.extra_manifest_signals and scaffold.depth_cap were dropped in earlier review rounds. The scan response's groups field was removed (zero consumers). The shared ProjectPicker gains max-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 — 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 refreshed. On rebase the file carries main's own chat_handlers.py 49 → 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 drifted all ceiling (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 lazy import() in src/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-card re-enabled, root error shown) and renders every error through the shared error surface (root-error is a role="alert" contained by the #scaffolder-root-error wrapper the field points at). The failed-row assertion now reads the alert inside failed-rows and 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_dir through create_folder_record and 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 real ProjectScaffolderPage against 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":

scan preview

A failed re-scan keeps the hand-tuned preview (7 selected after "Select all") and renders the refusal through ErrorNotice under the field:

re-scan failed, preview kept

Results card — created paths, "already existed", and a refused folder rendered through ErrorNotice (no raw code line):

results

More states

Disclosure expanded — "Inside {name}" chips and the section's own bulk buttons:

nested open

Empty scan — "Create the root folder only":

empty

Root without a folder yet — the counter promises it ("Root folder + N selected"):

root new

Re-scan in flight — "Scanning", kept preview disabled and dimmed:

scanning

Create in flight — "Creating", button disabled:

creating

Stale-selection refusal with Re-scan:

stale

Whole-call create refusal, rendered through ErrorNotice beside the Create button:

create refused

Refused root, rendered through ErrorNotice and still the target of the field's aria-describedby:

root refused

Field edited after the scan — Create disabled with a hint until the new directory is scanned:

root drifted

320px viewport — the path field spans the row, Browse/Scan sit beneath it, every row wraps:

narrow 320px

Shared ProjectPicker open at 390px — the panel clamps inside the viewport (max-w-[calc(100vw-16px)]):

picker open

The created sidebar tree (from #5890, captured against a real dashboard):

created folders

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 an async def handler body — especially one reached from a per-item loop — must be wrapped in asyncio.to_thread; the sibling call in the same function (_folder_project_overlap_denied) already was, which is the tell.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@CrysisDeu
CrysisDeu requested a review from a team September 6, 2026 07:42
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 6, 2026 07:42
@CrysisDeu
CrysisDeu requested a review from pepmach September 6, 2026 07:42
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 6, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for 0e9947f23124bd82bed9638043c0499073012c69.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the design AI finding as false positive, not applicable, or explicitly accepted for 0e9947f23124bd82bed9638043c0499073012c69.

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).

This decision applies only to this commit. A new push requires a new judgment.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for 0e9947f23124bd82bed9638043c0499073012c69.

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).

This decision applies only to this commit. A new push requires a new judgment.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ human override accepted

Reviewed d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81 — this comment is updated in place on each push.

Human judgment by @CrysisDeu overrides the Opus 4.8 finding for d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81; the recorded reason is authoritative for this commit.

Verdict recorded from an authorized human decision for commit d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81.

False positive or not applicable? A repository writer can comment:
/ai-review override fable d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ human override accepted

@CrysisDeu overrode this lane for d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81 via /ai-review override (this commit only).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

UX-level review of d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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 defaultEnabled: false app was never screenshotted).

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

  • Three stale-state wordings overlap — "The directory above changed — scan it before creating folders" (root-drifted.png), "This preview no longer matches the directory on disk" (stale.png), "moved or replaced after the scan — re-scan and retry" (results.png); the reader: "I cannot tell when I'd get which one." Low impact (Re-scan fixes all three), every occurrence. Smallest fix: end all three with the same "scan again before creating folders" stem.
  • "Confident" vs "Possible match" pills: reader guessed the gist but "couldn't explain the difference to someone else." Persistent on every row. Smallest fix: one tooltip/helper on the pills — "Confident matches start ticked; possible matches are opt-in" (the manifest's highlight_3 already has this sentence).

Evidence gaps

  • The app-store listing (display_name, description, six highlights, hero art) that a user must find to enable this defaultEnabled: false app appears in no committed screenshot — add a store card/detail capture.

[UX-REVIEWED] d47aaea

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] d47aaea

False positive or not applicable? A repository writer can comment:
/ai-review override gpt d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 6, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 6, 2026
CrysisDeu added a commit that referenced this pull request Sep 6, 2026
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>
@CrysisDeu
CrysisDeu force-pushed the feat/create-folders-from-project branch from 0e9947f to b6cda0b Compare September 6, 2026 08:15
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 6, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for 33ec3c690bd003b64cd5580bf373d1c4aa22cb12.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the design AI finding as false positive, not applicable, or explicitly accepted for 33ec3c690bd003b64cd5580bf373d1c4aa22cb12.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for 33ec3c690bd003b64cd5580bf373d1c4aa22cb12.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • fixed — span=5d61f725813d Failed identity capture disables root-swap protection (src/kiro_crew/dashboard/chat_folder_scaffold.py:154), in 33ec3c690bd003b64cd5580bf373d1c4aa22cb12.

_resolve_root now refuses a root whose root_identity() is None with the folder validator's own 400 ("Project directory must be an existing directory", folder_scan_root_invalid) instead of returning it, and its return type narrows to tuple[str, tuple[int, int]] — so neither endpoint can ever hand scan() an expected_identity=None and take the unpinned path. Pinned by test_a_root_whose_identity_cannot_be_read_is_refused_not_scanned_unpinned: with root_identity stubbed to None, both endpoints answer 400 and a stubbed _scan_off_loop asserts it was never called.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for db55a783074e0871dbe74562940357de8345c9ed.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for db55a783074e0871dbe74562940357de8345c9ed.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • fixed — span=5d61f725813d A replaced preview root is silently accepted (src/kiro_crew/dashboard/chat_folder_scaffold.py:698), in db55a783074e0871dbe74562940357de8345c9ed.

Create now requires the submitted root to be a realpath fixed point. The page submits the canonical scan.root the preview came from, so _resolve_root landing anywhere else means a component was replaced by a symlink after the preview; the handler answers with the existing _root_changed_response (400 folder_scan_root_invalid, SEL denied "root replaced after validation", resource = where the name now leads) before _scan_off_loop runs — so the empty-selection case the offered-set cross-check cannot catch is closed too. Pinned by test_create_refuses_a_previewed_root_that_now_resolves_elsewhere: root replaced by a symlink to a sibling, selected: [], stubbed scan asserts it never ran, no folder persisted, one denied audit.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • fixed — span=be90488f54a5 Empty previews hide root-change errors (website/src/apps/project-scaffolder/ProjectScaffolderPage.tsx:522), in db55a783074e0871dbe74562940357de8345c9ed.

The stale block (ErrorNotice + dropped-path list + Re-scan) is extracted into StaleNotice and rendered by both preview branches, so a root-only create refused with folder_scan_root_invalid on the empty branch now shows the notice and the one action that recovers, instead of a silently disabled button. New test: empty scan → root-moved refusal → stale-selection visible, no create-error, button disabled → Re-scan → notice gone, button enabled.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for 13d3d36f3e455d5aec9e2a684250179a846fc995.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for 13d3d36f3e455d5aec9e2a684250179a846fc995.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • fixed — span=be90488f54a5 Stale notice omits the required hand-off decision (website/src/apps/project-scaffolder/ProjectScaffolderPage.tsx:387), in 13d3d36f3e455d5aec9e2a684250179a846fc995.

StaleNotice now carries the decision itself, as a JSX No hand-off comment directly above its ErrorNotice, naming what is unsaved (the scanned root in the field and the ticked selection in the preview — neither persisted, both unmounted by a hand-off) and the one action that recovers (Re-scan). The round-17 extraction had left the comment at the two call sites only.

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>
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the fable AI finding as false positive, not applicable, or explicitly accepted for d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/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).

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the first-principles AI finding as false positive, not applicable, or explicitly accepted for d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81.

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).

This decision applies only to this commit. A new push requires a new judgment.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author
  • fixed — span=cb4830d0dc74 Windows junction swap escapes the approved scan root (src/kiro_crew/project_scan.py:868), in d47aaea58fb42dc5cb67bfcb8e4457fbe2cefe81.

The fallback branch no longer races its checks against the read — it holds the directory across both. _hold_directory opens a CreateFile handle (FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, share READ|WRITE, deliberately not DELETE) before the identity and leaf-resolution checks and _release_directory closes it only after the listing has been consumed. Under a handle without FILE_SHARE_DELETE the object cannot be renamed or deleted, and NTFS refuses to rename a directory with an open handle beneath it, so neither the leaf nor an ancestor can be swapped for a junction while the checks and os.scandir run — the check and the use now name the same directory. OPEN_REPARSE_POINT means a pre-planted junction is pinned as itself and refused by the existing resolution check. If the hold cannot be taken the OSError propagates and the walker's existing handling reports the directory as unread — fail closed, never an unpinned read. Tests (forced fallback branch, platform-independent): the hold brackets every listed directory's checks and is released once per hold; a hold that raises leaves the directory unlisted and warned. The Windows CI shards exercise the real handle in every existing scan test.

@iamwhatever

Copy link
Copy Markdown
Collaborator

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 create_folder_record, no new persistence, off by default. Checks are green and I have no correctness findings.

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 project_scan.py (1,662 lines) is TOCTOU hardening added one layer per review round: (st_dev, st_ino) root pinning, O_NOFOLLOW/O_DIRECTORY descriptor descent, and in round 19 a Windows CreateFile handle hold (_hold_directory) to block renames during the listing.

The surface being protected is: a read-only walk, gated behind an app that ships defaultEnabled: false, reachable only by an authenticated dashboard user against the gateway host's own filesystem, whose only write is a folder record pointing at a path the same user already has access to. The attacker model that justifies descriptor-pinned descent is "someone races the filesystem between the scan and the create on the user's own machine".

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 project_scan.py's module docstring saying so would save the next author 19 rounds.

2. The no-new-builtin-apps exception is self-recorded

Both the Opus 4.8 and First Principles lanes flagged the built-in app, and both were cleared with /ai-review override by the PR author citing "Maintainer decision … carried over from #5890". I could not find the ruling itself on #5890 — could you link the exact comment? An override recorded by the author of the PR being overridden should point at the maintainer's words, not paraphrase them. If the ruling exists, this is a link away from resolved; if it doesn't, it needs a maintainer to confirm here that project-scaffolder belongs in apps/builtins/ rather than the KiroCrewApps registry.

3. UX Review 🟡 CONCERNS on d47aaea58 has three unanswered items

The final UX verdict lists:

  • three overlapping stale-state wordings ("The directory above changed…", "This preview no longer matches…", "…moved or replaced after the scan…") with no way for a reader to tell which they will get;
  • no helper text explaining "Confident" vs "Possible match" (the manifest's highlight_3 already has the sentence);
  • no screenshot of the App Store card a user has to find to enable a defaultEnabled: false app.

All advisory, none block readiness — but none has a disposition comment either. Fix or rebut, each one individually, so the round is closed rather than just green.

Happy to approve once 2 has a link and 3 has dispositions; 1 is a question for the maintainers, not a blocker.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create folder tree from a multi-package project (monorepo / workspace scaffolding)

2 participants