import { type MissingProjectStandardsDiagnostic } from '../standards/missing-standards-detector.js'; import { type StandardsLanguage } from '../standards/project-standards-service.js'; export type WorkspaceInitOptions = { projectRoot: string; sessionId: string; /** * When true, the conflict check is skipped and the new session id is * written to .peaks/.session.json even if the project is already bound * to a different (real) session. Use only with explicit user authorization * — the CLI surfaces this as `--allow-session-rebind`. */ allowSessionRebind?: boolean; /** * Slice 2.0.1-bug3-fact-forcing-bypass: opt out of writing the * consumer-project `.claude/settings.local.json` file. Default * (`false`) writes the file so the [Fact-Forcing Gate] is bypassed * for tool calls inside `.peaks/**`. The CLI surfaces this as * `--no-claude-hooks`. */ noClaudeHooks?: boolean; /** * auto-applying `peaks standards init` when the consumer project's * `.claude/rules/` is missing or empty. Default (`false`) only emits * the diagnostic; set to `true` to also scaffold the rules tree via * `executeProjectStandardsInit({ projectRoot, apply: true })`. The * CLI surfaces this as `--init-standards`. */ initStandards?: boolean; /** * Optional language override for the standards scaffold. When unset, * `detectLanguage(projectRoot)` is used (looks for tsconfig.json / * package.json / pyproject.toml / go.mod / Cargo.toml). Pass this to * force a specific language for the auto-apply path. */ language?: StandardsLanguage; }; export type WorkspaceInitReport = { sessionId: string; sessionRoot: string; created: string[]; alreadyExisted: string[]; bound: boolean; previousSessionId: string | null; /** * Slice 2.0.1-bug3-fact-forcing-bypass: what the consumer-project * `.claude/settings.local.json` materialization did this call. * - written: the file was freshly written * - refreshed: the file already existed and was rewritten to * match the current peaks-loop release's template * - already-current: the file already matched the template; no * rewrite needed * - skipped: the caller passed noClaudeHooks=true * The LLM and the user both see this in the JSON envelope so they * can decide whether the bypass is in effect. * * `offlineTemplate` sub-field, which describes the self-heal action * taken on the offline `.peaks/.claude-settings-template.json` copy. * The offline copy is ALWAYS written/checked (regardless of * noClaudeHooks) because it is the manual-recovery anchor — see * `skills/peaks-code/references/anchoring-and-session-info.md`. * - refreshed: the file existed but its parsed hooks tree * diverged from the current `buildClaudeSettingsLocalJson()`; * it was rewritten * - already-current: the file already matched; no rewrite needed */ claudeSettings: { action: 'written' | 'refreshed' | 'already-current' | 'skipped'; path: string; offlineTemplate: { action: 'written' | 'refreshed' | 'already-current'; path: string; }; }; /** * diagnostic for missing or empty `.claude/rules/{common,}/`. * Always present (the detector runs on every init); `missing: false` * means the project's rules tree is already populated and no action is * required. The CLI copies this descriptor into the JSON envelope's * `data.standardsMissing` so the LLM can read it programmatically. */ standardsMissing: MissingProjectStandardsDiagnostic; /** * passed `initStandards: true` AND the detector reported `missing: * true`, this field lists the files written by * `executeProjectStandardsInit({ projectRoot, apply: true })`. * Undefined when `initStandards` was not requested. */ standardsApplied?: { readonly language: StandardsLanguage; readonly writtenFiles: string[]; readonly skippedFiles: string[]; }; }; /** * patterns under `.peaks/_runtime//` that the lazy WRITER (peaks-qa, * peaks-rd, peaks-prd, peaks-txt, peaks-sc) creates via * `mkdir(parent, { recursive: true })` immediately before a write. When * a sibling `.peaks/_runtime//` exists on disk AND every entry below * it matches one of these patterns (AND no entry is a symlink), the * dir is treated as legitimate writer output and `initWorkspace` * re-init is tolerant — the binding is rewritten without throwing. * * Adding a new pattern here is a deliberate, reviewed change because * it widens the heuristic. The list is intentionally narrow: * - `qa/screenshots/.{png,jpg,jpeg,webp,gif}` — peaks-qa screenshots * - `/requests/.md` — RD/QA/PRD/TXT/SC artifacts * - `/findings/.md` — QA findings files * * Re-exported so unit tests can assert the pattern list directly. */ export declare const WRITER_ALLOWED_RELATIVE_PATTERNS: ReadonlyArray; export declare class InvalidSessionIdError extends Error { readonly code = "INVALID_SESSION_ID"; constructor(message: string); } export declare class ConflictingSessionError extends Error { readonly existingSessionId: string; readonly requestedSessionId: string; readonly code = "CONFLICTING_SESSION"; constructor(message: string, existingSessionId: string, requestedSessionId: string); } /** * Thrown when `peaks workspace init --change-id ` is invoked but a * 2.8.0-era legacy sibling directory `.peaks/_runtime//` already exists * at top level. Under the 2.8.0+ two-axis convention, change-id is a * logical identifier in the SESSION axis — NOT a top-level sibling dir. * * The caller is expected to: * 1. Inspect `.peaks/_runtime//` to see if it contains user-authored * content worth preserving. * 2. Migrate or delete the sibling dir. * 3. Re-run `peaks workspace init --change-id `. */ export declare class LegacyChangeIdSiblingError extends Error { readonly sessionId: string; readonly legacyPath: string; readonly code = "LEGACY_CHANGE_ID_SIBLING"; constructor(sessionId: string, legacyPath: string); } export declare function validateSessionId(sessionId: string): void; export declare function initWorkspace(options: WorkspaceInitOptions): Promise; export { materializeClaudeSettingsLocal } from './workspace-claude-settings-materializer.js'; /** * for the legacy sibling `.peaks/_runtime//`. Returns `true` ONLY when * every leaf path under the sibling matches one of * `WRITER_ALLOWED_RELATIVE_PATTERNS` AND no entry is a symlink (anywhere * in the tree). * * Heuristic rationale (Karpathy #1 — name the tradeoff): * - False negative (reject writer-shaped content): we accept this * failure mode because it preserves the 2.8.3+ hard ban. The user * gets a clear migration message and can re-run after deleting the * sibling dir. * - False positive (accept legacy residue as writer output): we * REJECT this failure mode by requiring EVERY entry to match the * writer-allowed shape. One mismatched leaf ⇒ `false`. The whole-dir * check is conservative on purpose. * * Symlink rejection (Karpathy #3 — surgical): any symlinked entry — at * the root or nested — returns `false` immediately. A symlinked * `.peaks/_runtime//qa/foo.png` could otherwise bypass the file-extension * check by resolving to user content outside the project. * * Implementation note: the helper is synchronous and uses `lstatSync` * recursively so the caller (the guard in `initWorkspace`) can decide * inline without paying an async round-trip. Tree depth is bounded by * the writer's mkdir pattern (≤3 levels: `//`). * We cap the recursion at 8 levels as a defense-in-depth limit. */ export declare function isWriterCreatedSiblingShape(siblingDir: string): boolean;