@4pm/cli 1.32.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +995 -366
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28569,6 +28569,365 @@ async function fetchCliVersion(serverUrl, version) {
28569
28569
  }
28570
28570
  }
28571
28571
 
28572
+ // vendor/@4pm/constants/ai-prompt-defaults.json
28573
+ var ai_prompt_defaults_default = {
28574
+ "server.knowledge.distill": `You are in the working directory of a software project named "{{projectName}}".
28575
+ Read the project (code, docs, config) and write a concise, shareable KNOWLEDGE ARTICLE that
28576
+ distils it for other engineers. Use Markdown and this structure:
28577
+ - a single top-level title line starting with '# '
28578
+ - a one-paragraph summary
28579
+ - '## The problem' \u2014 what problem the project solves
28580
+ - '## The approach' \u2014 the key decisions and how it works
28581
+ - '## What we learned' \u2014 reusable takeaways
28582
+ Output ONLY the Markdown article, no preamble or code fences around the whole thing.{{focusExtra}}`,
28583
+ "cli.research.policy": `You are a research assistant for the 4PM platform. Answer the user's question thoroughly and
28584
+ accurately, in Markdown. Use the read-only web tools (WebSearch / WebFetch) to find current, factual
28585
+ information when helpful, and cite sources inline where relevant.
28586
+
28587
+ STRICT CONTENT POLICY \u2014 you MUST refuse, and must not answer, if the question (or a faithful answer
28588
+ to it) would involve any of:
28589
+ - sexual, abusive, hateful, or illegal content;
28590
+ - political persuasion, campaigning, or partisan advocacy;
28591
+ - discrimination or demeaning content targeting a race, ethnicity, religion, gender, nationality, or
28592
+ other protected group;
28593
+ - probing, scanning, exploiting, or attacking the security of ANY system, network, account, device,
28594
+ or person you do not own (no vulnerability hunting, no exploit/malware creation, no credential or
28595
+ PII harvesting, no evasion of security controls). General defensive-security education is allowed.
28596
+ - any other seriously harmful activity.
28597
+
28598
+ When you must refuse, reply with EXACTLY one line starting with "REFUSED:" followed by a short,
28599
+ neutral reason, and output nothing else.
28600
+
28601
+ You have READ-ONLY web tools only. Never attempt to read local files, run shell commands, or interact
28602
+ with other systems.
28603
+
28604
+ ---
28605
+
28606
+ Question:
28607
+ {{question}}`,
28608
+ "cli.memory.compact": "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue\nacross sessions and accounts. Merge the latest exchange into the existing memory and return the\nUPDATED memory. Keep it UNDER {{budgetChars}} characters. Use terse Markdown bullets grouped\nas: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything\nnow obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.\n\n=== EXISTING MEMORY ===\n{{oldMemory}}\n\n=== LATEST USER PROMPT ===\n{{prompt}}\n\n=== LATEST ASSISTANT ANSWER ===\n{{answer}}",
28609
+ "cli.support.answer": `You are the 4PM product support assistant. Answer the user's question about how to use 4PM
28610
+ using ONLY the documentation and FAQ provided below. If the answer is not in the docs, say
28611
+ you don't have that information and suggest contacting human support \u2014 do not invent details.
28612
+ {{roleNote}}
28613
+ Answer concisely in the user's language; reference the relevant doc heading/path when useful.
28614
+
28615
+ Return ONLY a single JSON object (no markdown fences, no prose around it) with these keys:
28616
+ "onTopic": boolean \u2014 false if the question is NOT about how to use the 4PM product,
28617
+ "sensitive": boolean \u2014 true if the question is inappropriate / abusive OR asks how to hack,
28618
+ bypass security, gain unauthorized access, take over or steal another
28619
+ user's/org's account or data, obtain credentials, or any malicious intent,
28620
+ "reason": string \u2014 a short reason when onTopic is false or sensitive is true, else "",
28621
+ "answer": string \u2014 your reply to the user, in markdown (in the user's language).
28622
+ Refusal policy (ADR-0237): if the question is off-topic (onTopic false) OR sensitive, DO NOT
28623
+ answer it. Set "answer" to this EXACT same polite refusal for BOTH cases (in the user's
28624
+ language, do not reveal which category or hint at the reason): "Sorry, I can only help with
28625
+ questions about how to use 4PM. For anything else, please contact human support." Only answer
28626
+ normally when onTopic is true AND sensitive is false.
28627
+
28628
+ ===== DOCUMENTATION =====
28629
+ {{docs}}
28630
+
28631
+ ===== QUESTION =====
28632
+ {{question}}`,
28633
+ "cli.support.reply_draft": "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.\nAddress the customer's latest message, using the whole conversation for context. Be accurate,\nfriendly and concise. Write in the language the customer uses in the ticket.\nUse the documentation and FAQ below for any product facts; never invent features, prices or\ncommitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.\nFollow the admin's instructions when given. A human admin reviews and edits your draft before\nsending it.\n\nOutput ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code\nfences around the whole message.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
28634
+ "cli.support.outreach_draft": "You are drafting an outreach message FROM the 4PM platform team TO the customer organizations.\nUse the subject and any current draft below as the starting point. Be clear, friendly and concise.\nWrite in the language of the subject/draft (or of the admin's instructions when those are empty).\nUse the documentation and FAQ below for any product facts; never invent features, prices or\ncommitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.\nFollow the admin's instructions when given. A human admin reviews and edits your draft before\nsending it.\n\nOutput ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code\nfences around the whole message.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
28635
+ "cli.support.legal_draft": "You are drafting the body of one of 4PM's legal documents (Terms of Service, Privacy Policy,\nAdd-on Terms, Rented Machine Addendum, \u2026) for the platform admin to review and publish.\nWrite or revise the WHOLE document body in the target locale given in the context. Keep the\nexisting heading structure and numbering unless the admin's instructions say otherwise.\nWhen the target locale is not English and an English reference is given, follow its meaning closely.\nRules:\n- Every {{placeholder}} (e.g. {{companyName}}) must be kept verbatim \u2014 never fill or rename it.\n- Describe plans, prices, upgrades, downgrades, cancellations, renewals, refunds and add-ons ONLY\n as stated in the PLAN CATALOG and BILLING FACTS below or the documentation; never invent\n commitments, prices or legal guarantees. Where something is unknown write `[TODO: \u2026]`.\n- Plain, precise legal English (or the target language); short numbered sections and bullet lists.\n- This is a draft, not legal advice; do not add a disclaimer about that inside the document.\n\nOutput ONLY the document body in markdown \u2014 no title line, no preamble, no explanation, no code\nfences around the whole document.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
28636
+ "cli.faq.compose": "You are maintaining the 4PM product FAQ, stored as markdown in THIS git repository (your CWD).\nFrom the resolved support tickets below, distil reusable, product-general FAQ entries and add or\nupdate the appropriate markdown file(s) in this repo (e.g. an FAQ.md, or the closest existing\ntopic file \u2014 inspect the repo first and match its structure/style).\n\nRules:\n- Write generalized Q&A, NOT ticket-specific replies; never include customer names, emails, ids,\n org names or any personal data from the tickets.\n- Merge with existing entries instead of duplicating; keep the existing formatting and headings.\n- If a ticket yields no reusable FAQ value, skip it. If nothing is worth adding, make NO changes.\n- Only edit markdown documentation files; do not touch code, CI, or unrelated files.\n- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working\n tree; the surrounding tooling handles commit + PR.{{customInstruction}}\n\n===== RESOLVED TICKETS =====\n{{transcript}}\n\n===== REQUIRED FINAL OUTPUT =====\nAfter finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON\nblock summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):\n```json\n{{ticketJsonSkeleton}}\n```\nInclude exactly one entry per ticket id above; keep each summary to one or two sentences.",
28637
+ "cli.scaffold.readme": "Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:\n{{specJson}}",
28638
+ "cli.scaffold.guide": "Write a {{guideFile}} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:\n{{specJson}}{{guideInstructionsBlock}}",
28639
+ "cli.autonomous.intake": "You are the 4PM autonomous INTAKE step (headless). Edit ONLY `USER_TODO.md`, `USER_QA.md` and\n`AI_TODO.md`. Do NOT run git, do NOT touch `AI_PROGRESS.md` / `AI_DONE.md` / any code \u2014 the 4PM cli\ncommits and publishes your book edits. Write in the language the project's `CLAUDE.md` specifies (default English).\n\n{{booksIntro}}\n\n## 1 \u2014 Approved user requests \u2192 tasks\nRead `USER_TODO.md` (columns `ID | Group | Depends | Request`). For each APPROVED `REQ-\u2026` whose `Depends`\nare already handled:\n- CLEAR request \u21D2 split it into tasks appended to `AI_TODO.md` (columns\n `ID | Priority | Tag | Depends | Group | Task description | Notes`), ids `TSK-{groupid:0000}-{taskid:0000}`\n (groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in `AI_TODO.md`/`AI_DONE.md`/\n `AI_PROGRESS.md`/their git history, +1; taskid from 0001). `Depends` = the `TSK-\u2026` it needs.\n Remove the analysed request row (empty \u21D2 reset to the empty template).\n- UNCLEAR request \u21D2 do NOT guess: append a `USER_QA.md` row (`ID | Group | Depends | Original request |\n Question / options | Answer`) with a new `QA-{groupid:0000}-{qaid:0000}`, blank `Answer`; remove the request row.\nAn unapproved request stays.\n\n## 2 \u2014 Answered intake questions\nFor each `USER_QA.md` row with an `Answer`, APPROVED, and whose id is NOT in any `AI_TODO.md` `Depends`\n(those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the\nanswer into tasks (same rules), then remove that QA row.\n\n## Task size (MANDATORY \u2014 every task must be finishable in ONE run)\nSize each task by SCOPE and write `size: S` or `size: M` at the start of `Notes`:\n- **S** \u2014 one small change in one module/layer (\u2248 \u2264 {{sMaxFiles}} files, \u2248 \u2264 {{sMaxLines}} lines), e.g. fix a bug, add a field.\n- **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 {{mMaxFiles}} files, \u2248 \u2264 {{mMaxLines}} lines), e.g. one CRUD screen, one API + its UI + tests.\n- **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.\n **Never write an L task**: split it into S/M tasks chained with `Depends`, or ask in `USER_QA.md` when the split needs a decision.\nThe counts are hints, not hard limits. Each task's `Task description` states the acceptance criteria and how to test it.\n\nWhen done, reply with one short line summarising what you added.",
28640
+ "cli.autonomous.implement": 'You are the 4PM autonomous IMPLEMENT step (headless). Implement exactly ONE task, then STOP.\n\nTask {{taskId}} (group {{group}}, attempt {{attempt}}):\n{{desc}}\nNotes: {{notes}}\n{{resumedNote}}\nBranches (already checked out by the 4PM cli \u2014 stay on them):\n- project root is on `{{branch}}`\n{{subLines}}\n\nRules:\n- Follow the project\'s architecture and `CLAUDE.md`. Add/update tests; run the project\'s test command and WAIT for the result.\n- Commit your work (`git add` + `git commit`) in the root and/or inside each submodule you changed.\n- Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files\n (USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.\n- If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real\n trade-offs), do NOT guess: commit what you have and report `needs-input` with the question + options.\n\nFinish with ONLY one fenced `json` block:\n{"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}',
28641
+ "cli.autonomous.split": 'You are the 4PM autonomous SPLIT step (read-only \u2014 do not change any file). Task {{taskId}} failed to\nfinish after repeated attempts, so it is probably too big for one run. Split it into 2\u20136 smaller child tasks\nthat are each finishable in ONE run (size S or M), in execution order (each depends on the previous).\n\nTask: {{desc}}\nNotes: {{notes}}\nWhy the attempts ended: {{reasons}}\nWork already done on its WIP branch `{{branch}}` (diff against the base, may be truncated):\n```\n{{diff}}\n```\nThe first child continues from that WIP branch: note which parts are already done so they are not redone.\nEach child description must state its acceptance criteria and how to test it.\n\nReply with ONLY one fenced `json` block:\n{"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}',
28642
+ "web.generators": "You are producing {{what}} for a software project, derived strictly from its specification (the source of truth). Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Write a clear, well-structured **Markdown** document. Output ONLY the Markdown document \u2014 no preamble and no wrapping code fence.{{templateHint}}{{paramsBlock}}\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}",
28643
+ "web.guide": 'You are producing a {{file}} file \u2014 guidance and conventions for AI coding agents working in this project \u2014 derived strictly from its specification (the source of truth). Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Write a clear, well-structured **Markdown** document. Output ONLY the Markdown document \u2014 no preamble and no wrapping code fence.\nThe document MUST include a prominent, mandatory section near the top (e.g. "## Security") that instructs the agent to READ and STRICTLY FOLLOW the project\'s security policy file `AI_SECURITY.md` (at the repository root) before doing any work, and that its rules \u2014 secret handling, data masking, and commit rules \u2014 override anything else when they conflict.{{baseBlock}}{{extraBlock}}\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
28644
+ "web.draft_tasks": 'You are a senior software project manager. From the project specification below (the source of truth), produce a concise, well-ordered list of concrete, actionable engineering tasks to build this project. Prefer vertical slices, keep each task independently shippable, and order by dependency. Output ONLY a JSON array \u2014 no preamble, no code fence. Each element is an object with exactly these string fields:\n- "priority": one of "High", "Medium", "Low"\n- "group": a short phase/area label (e.g. "Setup", "Auth", "API", "UI")\n- "depends": comma-separated titles of tasks in THIS list it depends on (or "")\n- "description": the task as a clear imperative sentence\n- "notes": optional extra detail (or "")\n\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
28645
+ "web.spec.review": 'Review this software project specification WITHOUT changing it \u2014 you only advise. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Reply in EXACTLY this format:\nASSESSMENT:\n<a short overall assessment of feasibility, coherence and reasonableness, in Markdown>\nISSUES:\n<one point that is unclear, inconsistent or unreasonable per line, or "none">\nDo not rewrite or output the spec \u2014 reply with only the assessment and issues. Spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
28646
+ "web.spec.compose": `You are revising a software project specification for consistency and feasibility, guided by the review in its "meta._aiReview" field. Make MINIMAL edits: keep the author's original intent and wording, only tighten coherence, fill clear gaps and link the fields together sensibly. Do NOT rewrite drastically or add scope the author did not intend. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Then judge whether the revised spec has enough coherent information to create the project. Reply with ONLY a single-line minified JSON object (no preamble, no code fence, no pretty-printing/indentation) of the form {"changes": [{"field": "<id>", "before": "<old value>", "after": "<new value>"}], "tree": "<source directory tree>", "ok": <true|false>, "assessment": "<one short Markdown paragraph on the result>", "blockers": ["<one must-fix gap per item; empty when ok>"]}. Include a "changes" entry ONLY for each field you actually edit \u2014 do NOT echo unchanged fields and do NOT return the whole spec. Use the SAME field ids as the input; "before"/"after" carry that field's value in its native JSON type (a string for text/select fields, a JSON array of strings for multiselect fields) and MUST keep select/multiselect values within their allowed options. Set "ok" true ONLY when nothing blocks creating the project. The "tree" is a plain-text ASCII directory tree for the project's source layout, derived from the chosen architecture, source pattern, tech stack and modules (use \u251C\u2500 \u2502 \u2514\u2500 connectors; a trailing "/" marks folders). Current spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):
28647
+ {{specContext}}`,
28648
+ "web.spec.suggest": "You are helping fill a software project specification. Suggest a value for {{target}} Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. {{seed}}{{shape}} {{instructionBlock}}Context \u2014 the full spec so far as a self-describing JSON (each field has id/name/description/type/options/value; use it to stay coherent):\n{{specContext}}",
28649
+ "web.spec.group_suggest": `You are helping fill a software project specification. Suggest values for ALL fields of the "{{groupKey}}" section below, in one pass, keeping them coherent with each other and with the rest of the spec. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. For each field: if it already has a non-empty value, treat it as the author's intent \u2014 keep it and only refine/expand; otherwise fill it. For select fields return exactly one of the allowed options (or "" if none fits); for multiselect return an array of allowed options; for textarea return detailed clean Markdown; for text return a concise single-line string. Fields:
28650
+ {{fieldLines}}
28651
+ {{instructionBlock}}Reply with ONLY a single-line minified JSON object mapping each field id to its value \u2014 no preamble, no code fence, no markdown, no extra keys, no pretty-printing/indentation. Context \u2014 the spec as self-describing JSON:
28652
+ {{specContext}}`,
28653
+ "web.spec.subagents_suggest": 'You are helping declare the custom AI subagents of a software project. Each subagent becomes a ".claude/agents/<name>.md" file the coding assistant can delegate to. Propose a small, focused set (typically 2\u20135) of subagents that fit THIS project, coherent with its architecture, tech stack and modules. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Keep every already-declared subagent (refine its description if useful) and add the missing ones. Each subagent has a "name" (a short kebab-case identifier, e.g. "code-reviewer") and a "description" (one or two sentences on its role and when to use it). Already declared: {{current}}. {{instructionBlock}}Reply with ONLY a single-line minified JSON array of {"name","description"} objects \u2014 no preamble, no code fence, no markdown, no extra keys, no pretty-printing/indentation. Context \u2014 the spec as self-describing JSON:\n{{specContext}}',
28654
+ "web.spec.subagent_desc": `You are helping declare the custom AI subagents of a software project. Each subagent becomes a ".claude/agents/<name>.md" file the coding assistant can delegate to. Write the description for {{who}}: one or two sentences on its role and when to use it, specific to THIS project's architecture, tech stack and modules. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. {{seed}}{{others}}{{instructionBlock}}Reply with ONLY the description text \u2014 no preamble, no code fence, no name, no JSON, no quotes. Context \u2014 the spec as self-describing JSON:
28655
+ {{specContext}}`,
28656
+ "web.template.analyze": "You are assessing a 4PM project-template (scaffold) update WITHOUT changing anything \u2014 advisory only. This project was scaffolded from template version {{localVersion}}; the latest is {{latestVersion}}. The template changelog for the versions this project would gain:\n{{changelog}}\n\nInspect the project's current files (they may have been customized since scaffolding) and report, in clear Markdown:\n1. What the update changes (config, hooks, conventions, layout).\n2. Which of THIS project's files are affected.\n3. Likely conflicts with local customizations (call out anything risky to overwrite).\n4. Overall effort and risk.\nDo NOT edit, create, delete, or move any file, and do NOT run git write commands \u2014 output only the report.{{extras}}",
28657
+ "web.template.update": 'You are applying a 4PM project-template (scaffold) update as a REVIEWED change \u2014 never a blind overwrite, and never a merge into the main branch. This project was scaffolded from template version {{localVersion}}; update it to {{latestVersion}}. The template changelog for the versions to apply:\n{{changelog}}\n\nSteps (do them in order; stop and report if one cannot be completed):\n{{resolveBase}}1. Run "git fetch origin", then create and switch to a new git branch "{{branch}}" FROM "origin/{{baseRef}}" (never from whatever branch is currently checked out): git checkout -B "{{branch}}" "origin/{{baseRef}}".\n2. Apply the template changes above to this project. The files may have been customized \u2014 MERGE the changes, preserving local customizations; never clobber intentional edits. Add new template files/hooks/config as needed.\n3. Update (or create) "./.4pm/.4pm.json" so its "templateVersion" is "{{latestVersion}}", keeping any other fields.\n4. Commit everything on "{{branch}}" with a clear message, then PUSH the branch to the remote.\n5. Open a PULL REQUEST from "{{branch}}" INTO "{{baseRef}}" using the host CLI that matches the remote \u2014 "gh pr create --base {{baseRef}} --head {{branch}}" for GitHub, "glab mr create --target-branch {{baseRef}} --source-branch {{branch}}" for GitLab (do NOT merge it). Capture the created PR/MR number and its URL.\nDo NOT stop to ask for confirmation \u2014 you are authorized to run these git and PR commands.{{extras}}\n\nWhen finished, reply with ONLY a single fenced code block tagged json (no prose before or after) of the form {"status": "success" | "blocked" | "failed", "pullRequestId": "<the PR/MR number, or null>", "pullRequestUrl": "<the PR/MR URL, or null>", "branch": "<the branch you created, e.g. {{branch}}>", "message": "<one short line: what changed on success, or WHY it could not complete>"}. Use "success" ONLY if the pull request was actually created and you have its id/number; if you were blocked (e.g. a command needed approval, or "gh"/"glab" is missing or not authenticated) use "blocked"; use "failed" for an outright error. Never report "success" without a real pull request id.',
28658
+ "web.checklist.eval": `You are checking a checklist against the {{contextLabel}} of this project. You MAY read the project's files (read-only) but MUST NOT write anything.
28659
+
28660
+ Checklist "{{name}}" \u2014 decide a verdict for EACH item: pass, fail, skip, or na.
28661
+ Items:
28662
+ {{lines}}
28663
+
28664
+ {{contextLabel}}:
28665
+ {{contextText}}
28666
+
28667
+ Reply with EXACTLY one line per item, in the form:
28668
+ [<ID>] <pass|fail|skip|na> \u2014 <one-line reason>
28669
+ No other text, no code fences.`,
28670
+ "web.checklist.author": `You are helping author a reusable project checklist (a "definition of ready"-style rubric). Produce the checklist's items based on the author's instruction, keeping the result coherent and actionable. Treat the current items as the author's intent: keep the ones that should stay, refine wording where it helps, and add the missing ones.
28671
+ Current items (JSON): {{current}}
28672
+ Author's instruction:
28673
+ """
28674
+ {{instruction}}
28675
+ """
28676
+ Reply with ONLY a single-line minified JSON array of the FULL new item list \u2014 each element an object with "text" (string, required), optional "description" (string) and optional "required" (boolean). No ids, no preamble, no code fence, no pretty-printing.`,
28677
+ "web.memo.compose": `You are turning a set of a teammate's project notes into ONE community forum post for the team. The notes may reference pasted screenshots/diagrams as [Image#N] tokens \u2014 for each, use the Read tool on the file path the token was rewritten to, look at the image, and weave what it shows into the post where relevant. Write a clear, helpful, self-contained post (not a raw copy of the notes). Reply with ONLY a single fenced code block tagged json containing an object of the form {"title": "<a short title, at most 200 characters>", "body": "<the post body as plain text, at most 10000 characters>"} \u2014 no prose before or after the fence.
28678
+
28679
+ Notes:
28680
+ {{notes}}`,
28681
+ "web.verify": "You are verifying entries in this project's {{verifyBook}} book. You MAY read the project's files (read-only) but MUST NOT write anything.\n\nSelected entries (use all of them together as shared context, then answer each one):\n{{entries}}\n\nQuestion to answer for EACH entry: {{question}}\n\nReply with EXACTLY one line per entry, in the form:\n[<ID>] <concise one-line answer>\nNo other text, no code fences.",
28682
+ "web.git.resolve": "Resolve all git merge conflicts in {{repoScope}} of the current project. Edit each conflicted file to a correct merged result (remove all conflict markers), then run `git add` on each. Do NOT commit \u2014 leave the merge staged for review."
28683
+ };
28684
+
28685
+ // vendor/@4pm/constants/ai-prompts.ts
28686
+ var DEFAULTS = ai_prompt_defaults_default;
28687
+ function renderPrompt(template, vars) {
28688
+ return template.replace(
28689
+ /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g,
28690
+ (match, name) => Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
28691
+ );
28692
+ }
28693
+ var PROMPT_META = [
28694
+ {
28695
+ key: "server.knowledge.distill",
28696
+ ownerApp: "server",
28697
+ phase: 1,
28698
+ title: "Knowledge distillation",
28699
+ description: "Turns a project into a shareable knowledge article (read-only agent in the project folder).",
28700
+ requiredVars: ["projectName"]
28701
+ },
28702
+ {
28703
+ key: "cli.research.policy",
28704
+ ownerApp: "cli",
28705
+ phase: 1,
28706
+ title: "AI Research \u2014 system policy",
28707
+ description: "System instruction + content policy prepended to an org Research question.",
28708
+ requiredVars: ["question"]
28709
+ },
28710
+ {
28711
+ key: "cli.memory.compact",
28712
+ ownerApp: "cli",
28713
+ phase: 1,
28714
+ title: "Console memory compaction",
28715
+ description: "Merges the latest exchange into the rolling per-project AI memory, budget-bounded.",
28716
+ requiredVars: ["budgetChars", "oldMemory", "prompt", "answer"]
28717
+ },
28718
+ {
28719
+ key: "cli.support.answer",
28720
+ ownerApp: "cli",
28721
+ phase: 1,
28722
+ title: "AI Help \u2014 support answer",
28723
+ description: "Answers a user/admin product question from the KB docs, with the inline moderation JSON contract. Edit with care: the response must stay a single JSON object with onTopic/sensitive/reason/answer.",
28724
+ requiredVars: ["roleNote", "docs", "question"]
28725
+ },
28726
+ {
28727
+ key: "cli.support.reply_draft",
28728
+ ownerApp: "cli",
28729
+ phase: 1,
28730
+ title: "Admin draft \u2014 ticket reply",
28731
+ description: "Drafts a support-team reply to a ticket (freeform markdown).",
28732
+ requiredVars: ["docs", "context"]
28733
+ },
28734
+ {
28735
+ key: "cli.support.outreach_draft",
28736
+ ownerApp: "cli",
28737
+ phase: 1,
28738
+ title: "Admin draft \u2014 outreach message",
28739
+ description: "Drafts an outreach message to customer orgs (freeform markdown).",
28740
+ requiredVars: ["docs", "context"]
28741
+ },
28742
+ {
28743
+ key: "cli.support.legal_draft",
28744
+ ownerApp: "cli",
28745
+ phase: 1,
28746
+ title: "Admin draft \u2014 legal document body",
28747
+ description: "Drafts/revises a legal document body in a locale, keeping legal {{placeholders}} verbatim.",
28748
+ requiredVars: ["docs", "context"]
28749
+ },
28750
+ {
28751
+ key: "cli.faq.compose",
28752
+ ownerApp: "cli",
28753
+ phase: 1,
28754
+ title: "Ticket \u2192 FAQ synthesis",
28755
+ description: "Agent-write run that distils resolved tickets into FAQ markdown + a per-ticket JSON summary. Keep the required JSON output block and the {{ticketJsonSkeleton}} intact.",
28756
+ requiredVars: ["transcript", "ticketJsonSkeleton"]
28757
+ },
28758
+ {
28759
+ key: "cli.scaffold.readme",
28760
+ ownerApp: "cli",
28761
+ phase: 1,
28762
+ title: "Scaffold \u2014 README author",
28763
+ description: "Writes the initial README.md for a new project from its spec JSON (on project create).",
28764
+ requiredVars: ["specJson"]
28765
+ },
28766
+ {
28767
+ key: "cli.scaffold.guide",
28768
+ ownerApp: "cli",
28769
+ phase: 1,
28770
+ title: "Scaffold \u2014 agent guide author",
28771
+ description: "Writes the project's CLAUDE.md / AGENT.md guide from its spec JSON (on project create).",
28772
+ requiredVars: ["guideFile", "specJson"]
28773
+ },
28774
+ {
28775
+ key: "cli.autonomous.intake",
28776
+ ownerApp: "cli",
28777
+ phase: 1,
28778
+ title: "Autonomous \u2014 intake",
28779
+ description: "Analyses approved user requests + answered intake questions into sized AI tasks (books only; no git/code).",
28780
+ requiredVars: ["booksIntro", "sMaxFiles", "sMaxLines", "mMaxFiles", "mMaxLines"]
28781
+ },
28782
+ {
28783
+ key: "cli.autonomous.implement",
28784
+ ownerApp: "cli",
28785
+ phase: 1,
28786
+ title: "Autonomous \u2014 implement",
28787
+ description: "Implements exactly one task on the prepared branches (commit only). Keep the final JSON status block intact.",
28788
+ requiredVars: ["taskId", "desc", "branch", "subLines"]
28789
+ },
28790
+ {
28791
+ key: "cli.autonomous.split",
28792
+ ownerApp: "cli",
28793
+ phase: 1,
28794
+ title: "Autonomous \u2014 split",
28795
+ description: "Read-only: proposes smaller child tasks when a task repeatedly fails. Keep the final JSON children block intact.",
28796
+ requiredVars: ["taskId", "desc", "reasons", "branch", "diff"]
28797
+ },
28798
+ {
28799
+ key: "web.generators",
28800
+ ownerApp: "web",
28801
+ phase: 2,
28802
+ title: "Content generator",
28803
+ description: "Produces a report / estimation / WBS / proposal / feasibility Markdown document from the project spec.",
28804
+ requiredVars: ["what", "specContext"]
28805
+ },
28806
+ {
28807
+ key: "web.guide",
28808
+ ownerApp: "web",
28809
+ phase: 2,
28810
+ title: "AI guide (CLAUDE.md / AGENT.md)",
28811
+ description: "Writes/refines the project's agent-guide file from the spec. Keep the mandatory AI_SECURITY.md section.",
28812
+ requiredVars: ["file", "specContext"]
28813
+ },
28814
+ {
28815
+ key: "web.draft_tasks",
28816
+ ownerApp: "web",
28817
+ phase: 2,
28818
+ title: "Draft tasks from spec",
28819
+ description: "Breaks the spec into a JSON array of engineering tasks. Keep the required JSON array output shape.",
28820
+ requiredVars: ["specContext"]
28821
+ },
28822
+ {
28823
+ key: "web.spec.review",
28824
+ ownerApp: "web",
28825
+ phase: 2,
28826
+ title: "Spec review (advisory)",
28827
+ description: "Advises on the whole spec in the fixed ASSESSMENT/ISSUES format (no rewrite). Keep that reply format.",
28828
+ requiredVars: ["specContext"]
28829
+ },
28830
+ {
28831
+ key: "web.spec.compose",
28832
+ ownerApp: "web",
28833
+ phase: 2,
28834
+ title: "Spec compose (gates create)",
28835
+ description: "Revises the spec with minimal edits + judges readiness. GATES project creation \u2014 keep the exact JSON object shape (changes/tree/ok/assessment/blockers).",
28836
+ requiredVars: ["specContext"]
28837
+ },
28838
+ {
28839
+ key: "web.spec.suggest",
28840
+ ownerApp: "web",
28841
+ phase: 2,
28842
+ title: "Spec field suggest (\u2728)",
28843
+ description: "Suggests a value for one spec field. {{shape}} carries the reply-format rule the parser depends on.",
28844
+ requiredVars: ["target", "shape", "specContext"]
28845
+ },
28846
+ {
28847
+ key: "web.spec.group_suggest",
28848
+ ownerApp: "web",
28849
+ phase: 2,
28850
+ title: "Spec section suggest",
28851
+ description: "Suggests values for a whole wizard section. Keep the single-line JSON object (field id \u2192 value) output shape.",
28852
+ requiredVars: ["groupKey", "fieldLines", "specContext"]
28853
+ },
28854
+ {
28855
+ key: "web.spec.subagents_suggest",
28856
+ ownerApp: "web",
28857
+ phase: 2,
28858
+ title: "Subagents suggest",
28859
+ description: "Proposes the project's AI subagents. Keep the JSON array of {name,description} output shape.",
28860
+ requiredVars: ["current", "specContext"]
28861
+ },
28862
+ {
28863
+ key: "web.spec.subagent_desc",
28864
+ ownerApp: "web",
28865
+ phase: 2,
28866
+ title: "Subagent description suggest",
28867
+ description: "Writes one subagent's description (plain text, no JSON).",
28868
+ requiredVars: ["who", "specContext"]
28869
+ },
28870
+ {
28871
+ key: "web.template.analyze",
28872
+ ownerApp: "web",
28873
+ phase: 2,
28874
+ title: "Template update \u2014 analyze impact",
28875
+ description: "Read-only: reports what a project-template update changes + affected files + risk.",
28876
+ requiredVars: ["localVersion", "latestVersion", "changelog"]
28877
+ },
28878
+ {
28879
+ key: "web.template.update",
28880
+ ownerApp: "web",
28881
+ phase: 2,
28882
+ title: "Template update \u2014 apply (branch + PR)",
28883
+ description: "Agent-write: applies the template update on a branch + opens a PR. Keep the branch/PR steps + the final JSON status block.",
28884
+ requiredVars: ["latestVersion", "branch", "baseRef", "changelog"]
28885
+ },
28886
+ {
28887
+ key: "web.checklist.eval",
28888
+ ownerApp: "web",
28889
+ phase: 2,
28890
+ title: "Checklist evaluate",
28891
+ description: "Read-only: verdict (pass/fail/skip/na) per checklist item vs content. Keep the one-line-per-item reply format.",
28892
+ requiredVars: ["contextLabel", "name", "lines", "contextText"]
28893
+ },
28894
+ {
28895
+ key: "web.checklist.author",
28896
+ ownerApp: "web",
28897
+ phase: 2,
28898
+ title: "Checklist authoring",
28899
+ description: "Proposes the full checklist item list from an instruction + current items. Keep the JSON array output shape.",
28900
+ requiredVars: ["current", "instruction"]
28901
+ },
28902
+ {
28903
+ key: "web.memo.compose",
28904
+ ownerApp: "web",
28905
+ phase: 2,
28906
+ title: "Memo \u2192 community post",
28907
+ description: "Turns a teammate's notes (with [Image#N] tokens) into one forum post. Keep the fenced JSON {title,body} output.",
28908
+ requiredVars: ["notes"]
28909
+ },
28910
+ {
28911
+ key: "web.verify",
28912
+ ownerApp: "web",
28913
+ phase: 2,
28914
+ title: "AI Verify (book entries)",
28915
+ description: "Read-only: answers a question per selected book entry. Keep the one-line-per-entry reply format.",
28916
+ requiredVars: ["verifyBook", "entries", "question"]
28917
+ },
28918
+ {
28919
+ key: "web.git.resolve",
28920
+ ownerApp: "web",
28921
+ phase: 2,
28922
+ title: "Git \u2014 resolve conflicts",
28923
+ description: "Agent: resolves merge conflicts + stages (no commit).",
28924
+ requiredVars: ["repoScope"]
28925
+ }
28926
+ ];
28927
+ var AI_PROMPTS = Object.fromEntries(
28928
+ PROMPT_META.map((m2) => [m2.key, { ...m2, defaultTemplate: DEFAULTS[m2.key] ?? "" }])
28929
+ );
28930
+
28572
28931
  // vendor/@4pm/constants/error-codes.ts
28573
28932
  var ErrorCode = {
28574
28933
  // Common
@@ -28587,6 +28946,11 @@ var ErrorCode = {
28587
28946
  FEATURE_NOT_AVAILABLE: "FEATURE_NOT_AVAILABLE",
28588
28947
  // Request body exceeded the HTTP gateway's JSON size limit (see HTTP_JSON_BODY_LIMIT_BYTES).
28589
28948
  PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
28949
+ // Unified composer attachments (ADR-0388): an upload over ATTACHMENT_MAX_BYTES, a MIME outside the
28950
+ // image + file lists, or a submit carrying more than ATTACHMENT_MAX_COUNT refs.
28951
+ ATTACHMENT_TOO_LARGE: "ATTACHMENT_TOO_LARGE",
28952
+ ATTACHMENT_TYPE_UNSUPPORTED: "ATTACHMENT_TYPE_UNSUPPORTED",
28953
+ ATTACHMENT_LIMIT_EXCEEDED: "ATTACHMENT_LIMIT_EXCEEDED",
28590
28954
  INTERNAL_ERROR: "INTERNAL_ERROR",
28591
28955
  // auth
28592
28956
  INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
@@ -28715,6 +29079,9 @@ var ErrorCode = {
28715
29079
  // Change-request (ADR-0379) not found / already resolved.
28716
29080
  CHANGE_REQUEST_NOT_FOUND: "CHANGE_REQUEST_NOT_FOUND",
28717
29081
  CHANGE_REQUEST_ALREADY_RESOLVED: "CHANGE_REQUEST_ALREADY_RESOLVED",
29082
+ // org AI Research (ADR-0380): question refused by the content policy / research-guard; row not found.
29083
+ RESEARCH_REFUSED: "RESEARCH_REFUSED",
29084
+ RESEARCH_NOT_FOUND: "RESEARCH_NOT_FOUND",
28718
29085
  MACHINE_ALREADY_IN_POOL: "MACHINE_ALREADY_IN_POOL",
28719
29086
  NOT_MACHINE_USER: "NOT_MACHINE_USER",
28720
29087
  // worker · physic project
@@ -28751,6 +29118,14 @@ var ErrorCode = {
28751
29118
  // web blocks the dispatch and shows an actionable "spec too large" message instead of letting
28752
29119
  // the server reject it as a confusing VALIDATION_FAILED.
28753
29120
  AI_PROMPT_TOO_LARGE: "AI_PROMPT_TOO_LARGE",
29121
+ // Server-managed prompt catalog (ADR-0381): the key is not in the @4pm/constants registry.
29122
+ AI_PROMPT_KEY_UNKNOWN: "AI_PROMPT_KEY_UNKNOWN",
29123
+ // The submitted template is missing a required `{{var}}` the builder depends on.
29124
+ AI_PROMPT_TEMPLATE_INVALID: "AI_PROMPT_TEMPLATE_INVALID",
29125
+ // Prompt editing is disabled by the AI_PROMPTS_READONLY env flag (use the defaults).
29126
+ AI_PROMPTS_READONLY: "AI_PROMPTS_READONLY",
29127
+ // Terms & policies editing is disabled by the LEGAL_READONLY env flag.
29128
+ LEGAL_READONLY: "LEGAL_READONLY",
28754
29129
  REPO_NOT_FOUND: "REPO_NOT_FOUND",
28755
29130
  // invitation
28756
29131
  INVITATION_TOKEN_INVALID: "INVITATION_TOKEN_INVALID",
@@ -29932,8 +30307,8 @@ async function runLink(serverUrl, explicitProfile, token = null) {
29932
30307
  }
29933
30308
 
29934
30309
  // src/commands/start.ts
29935
- import { spawn as spawn11 } from "child_process";
29936
- import { join as join42 } from "path";
30310
+ import { spawn as spawn12 } from "child_process";
30311
+ import { join as join43 } from "path";
29937
30312
 
29938
30313
  // src/core/update.ts
29939
30314
  import { execSync as execSync2 } from "child_process";
@@ -33152,7 +33527,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
33152
33527
  // src/core/ws-client.ts
33153
33528
  init_wrapper();
33154
33529
  import { randomUUID as randomUUID4 } from "crypto";
33155
- import { basename as basename7, join as join37 } from "path";
33530
+ import { basename as basename7, join as join38 } from "path";
33156
33531
  import { existsSync as existsSync19, mkdirSync as mkdirSync9 } from "fs";
33157
33532
 
33158
33533
  // vendor/@4pm/utils/helpers.ts
@@ -33185,6 +33560,59 @@ function prettyResult(text) {
33185
33560
  return trimmed;
33186
33561
  }
33187
33562
 
33563
+ // vendor/@4pm/dto/attachment.ts
33564
+ var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
33565
+ var ATTACHMENT_MAX_COUNT = 10;
33566
+ var ATTACHMENT_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
33567
+ var ATTACHMENT_FILE_MIME_TYPES = [
33568
+ "application/pdf",
33569
+ "application/zip",
33570
+ "application/x-zip-compressed",
33571
+ "application/msword",
33572
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
33573
+ "application/vnd.ms-excel",
33574
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
33575
+ "text/plain",
33576
+ "text/csv",
33577
+ "application/json"
33578
+ ];
33579
+ var ATTACHMENT_FILE_EXTENSIONS = [".pdf", ".zip", ".doc", ".docx", ".xls", ".xlsx", ".txt", ".csv", ".json"];
33580
+ var ATTACHMENT_ACCEPT = [...ATTACHMENT_IMAGE_MIME_TYPES, ...ATTACHMENT_FILE_MIME_TYPES, ...ATTACHMENT_FILE_EXTENSIONS].join(",");
33581
+ function attachmentExt(mime) {
33582
+ switch (mime) {
33583
+ case "image/png":
33584
+ return "png";
33585
+ case "image/jpeg":
33586
+ return "jpg";
33587
+ case "image/webp":
33588
+ return "webp";
33589
+ case "image/gif":
33590
+ return "gif";
33591
+ case "application/pdf":
33592
+ return "pdf";
33593
+ case "application/zip":
33594
+ case "application/x-zip-compressed":
33595
+ return "zip";
33596
+ case "application/msword":
33597
+ return "doc";
33598
+ case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
33599
+ return "docx";
33600
+ case "application/vnd.ms-excel":
33601
+ return "xls";
33602
+ case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
33603
+ return "xlsx";
33604
+ case "text/plain":
33605
+ return "txt";
33606
+ case "text/csv":
33607
+ return "csv";
33608
+ case "application/json":
33609
+ return "json";
33610
+ default:
33611
+ return null;
33612
+ }
33613
+ }
33614
+ var ATTACHMENT_ID_EXT_PATTERN = "png|jpg|webp|gif|pdf|zip|doc|docx|xls|xlsx|txt|csv|json";
33615
+
33188
33616
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
33189
33617
  var external_exports = {};
33190
33618
  __export(external_exports, {
@@ -37240,23 +37668,13 @@ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v2
37240
37668
  // vendor/@4pm/dto/command.ts
37241
37669
  var COMMAND_MAX_LEN = 8e3;
37242
37670
  var AI_PROMPT_MAX_LEN = 5e5;
37243
- var COMMAND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
37244
- var COMMAND_IMAGE_MAX_COUNT = 10;
37245
- var COMMAND_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
37246
- var COMMAND_IMAGE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(png|jpg|webp|gif)$/;
37671
+ var COMMAND_IMAGE_MAX_COUNT = ATTACHMENT_MAX_COUNT;
37672
+ var COMMAND_IMAGE_MIME_TYPES = [...ATTACHMENT_IMAGE_MIME_TYPES, ...ATTACHMENT_FILE_MIME_TYPES];
37673
+ var COMMAND_IMAGE_ID_RE = new RegExp(
37674
+ `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.(${ATTACHMENT_ID_EXT_PATTERN})$`
37675
+ );
37247
37676
  function commandImageExt(mime) {
37248
- switch (mime) {
37249
- case "image/png":
37250
- return "png";
37251
- case "image/jpeg":
37252
- return "jpg";
37253
- case "image/webp":
37254
- return "webp";
37255
- case "image/gif":
37256
- return "gif";
37257
- default:
37258
- return "bin";
37259
- }
37677
+ return attachmentExt(mime) ?? "bin";
37260
37678
  }
37261
37679
  var commandImageRefSchema = external_exports.object({
37262
37680
  // Constrained to `<uuidv4>.<ext>` (the only id the upload mints) so a client-supplied id can never
@@ -37264,7 +37682,9 @@ var commandImageRefSchema = external_exports.object({
37264
37682
  id: external_exports.string().regex(COMMAND_IMAGE_ID_RE),
37265
37683
  placeholder: external_exports.string().min(1).max(40),
37266
37684
  name: external_exports.string().max(255),
37267
- mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES)
37685
+ mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES),
37686
+ /** Image or file (ADR-0388); omitted by older clients ⇒ derived from `mime`. */
37687
+ category: external_exports.enum(["image", "file"]).optional()
37268
37688
  });
37269
37689
  var aiRunConfigSchema = external_exports.object({
37270
37690
  /** `--model` override for this run (must match the resolved provider's model id). */
@@ -37441,6 +37861,14 @@ var putGitCredentialRequestSchema = external_exports.object({
37441
37861
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["host"], message: "host is required for GitHub Enterprise" });
37442
37862
  }
37443
37863
  });
37864
+ var putGitLabCredentialRequestSchema = external_exports.object({
37865
+ host: hostSchema.optional(),
37866
+ group: external_exports.string().trim().min(1).max(200),
37867
+ /** The access token; omit to keep the stored one (required on first save — enforced server-side). */
37868
+ token: external_exports.string().trim().min(1).max(512).optional(),
37869
+ apiBaseUrl: external_exports.string().trim().url().startsWith("https://").max(500).nullable().optional(),
37870
+ caCert: external_exports.string().trim().max(32768).nullable().optional()
37871
+ });
37444
37872
  function parseRepoUrl(url) {
37445
37873
  const raw = url.trim();
37446
37874
  const scp = /^[\w.-]+@([^:/\s]+):([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
@@ -38074,6 +38502,23 @@ import { tmpdir as tmpdir2 } from "os";
38074
38502
  import { dirname as dirname3, join as join6, resolve } from "path";
38075
38503
  import { fileURLToPath as fileURLToPath3 } from "url";
38076
38504
  import { promisify } from "util";
38505
+
38506
+ // src/core/prompt-overrides.ts
38507
+ var overrides = {};
38508
+ function setPromptOverrides(map) {
38509
+ overrides = map ?? {};
38510
+ }
38511
+ function getPromptOverride(key) {
38512
+ const byLocale = overrides[key];
38513
+ if (!byLocale) return null;
38514
+ return byLocale[getLocale()] ?? byLocale.en ?? null;
38515
+ }
38516
+ function resolveCliPrompt(key, vars) {
38517
+ const template = getPromptOverride(key) ?? AI_PROMPTS[key]?.defaultTemplate ?? "";
38518
+ return renderPrompt(template, vars);
38519
+ }
38520
+
38521
+ // src/core/scaffold.ts
38077
38522
  var run = promisify(execFile);
38078
38523
  function reposOf(spec) {
38079
38524
  const repos = spec?.repos;
@@ -38489,18 +38934,22 @@ ${(sa.description || "").trim()}
38489
38934
  generate3,
38490
38935
  join6(target, "README.md"),
38491
38936
  "README.md",
38492
- `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
38493
- ${specJson}`
38937
+ // Admin override (ADR-0381) for `cli.scaffold.readme`, else the shared registry default.
38938
+ resolveCliPrompt("cli.scaffold.readme", { specJson })
38494
38939
  );
38495
38940
  await generateFile(
38496
38941
  generate3,
38497
38942
  join6(target, guideFile),
38498
38943
  guideFile,
38499
- `Write a ${guideFile} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
38500
- ${specJson}` + (guideInstructions ? `
38944
+ // Admin override (ADR-0381) for `cli.scaffold.guide`, else the shared registry default.
38945
+ resolveCliPrompt("cli.scaffold.guide", {
38946
+ guideFile,
38947
+ specJson,
38948
+ guideInstructionsBlock: guideInstructions ? `
38501
38949
  Also incorporate these additional instructions/content:
38502
38950
  ${guideInstructions}
38503
- ` : ""),
38951
+ ` : ""
38952
+ }),
38504
38953
  guideInstructions
38505
38954
  );
38506
38955
  } finally {
@@ -39022,6 +39471,15 @@ var WsChannels = {
39022
39471
  // tools, empty dir — like support-answer) and replies with the raw output for the web to parse. No
39023
39472
  // project is involved (checklists are org-scoped).
39024
39473
  CHECKLIST_AUTHOR: "checklist.author",
39474
+ // Org-level "AI Research" (ADR-0380): server (picks an idle cli of the org AI pool) → cli
39475
+ // request–reply. The server forwards the user's question; the worker runs a one-shot research agent
39476
+ // (read-only web/search tools) under a content policy + a research-guard post-check, and replies with
39477
+ // the markdown answer (or a structured refusal) plus the run's token usage so the server meters it to
39478
+ // the org (projectId=null). No project is involved.
39479
+ RESEARCH_ASK: "research.ask",
39480
+ // Org AI Research live progress (ADR-0380): cli → server one-way stream of answer chunks / status for
39481
+ // a running research question, keyed by the query id, relayed to the web over SSE.
39482
+ RESEARCH_PROGRESS: "research.progress",
39025
39483
  // Shared AI memory (ADR-0245): cli → server one-way write-back of the compacted rolling memory for
39026
39484
  // the project it serves (persisted per project × machine-user link). The read direction rides
39027
39485
  // `ws_token` (seeds the cli cache on connect); this channel only carries updates.
@@ -40268,7 +40726,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
40268
40726
  continue;
40269
40727
  }
40270
40728
  const ext = commandImageExt(reply.mime ?? img.mime);
40271
- const file = join12(dir2, `img-${n}.${ext}`);
40729
+ const file = join12(dir2, `attachment-${n}.${ext}`);
40272
40730
  writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
40273
40731
  map.set(img.placeholder, file);
40274
40732
  } catch (err) {
@@ -40526,13 +40984,13 @@ var ENV_ALLOW = /* @__PURE__ */ new Set([
40526
40984
  "PROGRAMFILES"
40527
40985
  ]);
40528
40986
  var ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "AWS_", "GOOGLE_", "CLOUD_ML_", "VERTEX_"];
40529
- function agentEnv(extraEnv, overrides) {
40987
+ function agentEnv(extraEnv, overrides2) {
40530
40988
  const env3 = {};
40531
40989
  for (const [key, value] of Object.entries(process.env)) {
40532
40990
  if (value === void 0) continue;
40533
40991
  if (ENV_ALLOW.has(key) || ENV_ALLOW_PREFIXES.some((p2) => key.startsWith(p2))) env3[key] = value;
40534
40992
  }
40535
- return { ...env3, ...extraEnv, ...overrides };
40993
+ return { ...env3, ...extraEnv, ...overrides2 };
40536
40994
  }
40537
40995
  function gitAuthArgs(user, password) {
40538
40996
  if (!user && !password) return [];
@@ -40628,106 +41086,46 @@ ${body}`;
40628
41086
  }
40629
41087
  function buildPrompt(docs, question, askerRole) {
40630
41088
  const roleNote = askerRole === "admin" ? "The asker is a platform admin." : "The asker is a regular user \u2014 do NOT reveal admin-only features.";
40631
- return [
40632
- "You are the 4PM product support assistant. Answer the user's question about how to use 4PM",
40633
- "using ONLY the documentation and FAQ provided below. If the answer is not in the docs, say",
40634
- "you don't have that information and suggest contacting human support \u2014 do not invent details.",
40635
- roleNote,
40636
- "Answer concisely in the user's language; reference the relevant doc heading/path when useful.",
40637
- "",
40638
- // Structured output for inline moderation (ADR-0237): the same run also classifies the question.
40639
- "Return ONLY a single JSON object (no markdown fences, no prose around it) with these keys:",
40640
- ' "onTopic": boolean \u2014 false if the question is NOT about how to use the 4PM product,',
40641
- ' "sensitive": boolean \u2014 true if the question is inappropriate / abusive OR asks how to hack,',
40642
- " bypass security, gain unauthorized access, take over or steal another",
40643
- " user's/org's account or data, obtain credentials, or any malicious intent,",
40644
- ' "reason": string \u2014 a short reason when onTopic is false or sensitive is true, else "",',
40645
- ` "answer": string \u2014 your reply to the user, in markdown (in the user's language).`,
40646
- "Refusal policy (ADR-0237): if the question is off-topic (onTopic false) OR sensitive, DO NOT",
40647
- `answer it. Set "answer" to this EXACT same polite refusal for BOTH cases (in the user's`,
40648
- 'language, do not reveal which category or hint at the reason): "Sorry, I can only help with',
40649
- 'questions about how to use 4PM. For anything else, please contact human support." Only answer',
40650
- "normally when onTopic is true AND sensitive is false.",
40651
- "",
40652
- "===== DOCUMENTATION =====",
40653
- docs,
40654
- "",
40655
- "===== QUESTION =====",
40656
- question
40657
- ].join("\n");
41089
+ return resolveCliPrompt("cli.support.answer", { roleNote, docs, question });
40658
41090
  }
40659
41091
  function buildDraftPrompt(docs, context, task) {
40660
- if (task === "legal_draft") return buildLegalDraftPrompt(docs, context);
40661
- const goal = task === "reply_draft" ? [
40662
- "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.",
40663
- "Address the customer's latest message, using the whole conversation for context. Be accurate,",
40664
- "friendly and concise. Write in the language the customer uses in the ticket."
40665
- ] : [
40666
- "You are drafting an outreach message FROM the 4PM platform team TO the customer organizations.",
40667
- "Use the subject and any current draft below as the starting point. Be clear, friendly and concise.",
40668
- "Write in the language of the subject/draft (or of the admin's instructions when those are empty)."
40669
- ];
41092
+ if (task === "prompt_generate") return buildPromptGeneratePrompt(context);
41093
+ if (task === "prompt_translate") return buildPromptTranslatePrompt(context);
41094
+ if (task === "legal_draft") return resolveCliPrompt("cli.support.legal_draft", { docs, context });
41095
+ const key = task === "reply_draft" ? "cli.support.reply_draft" : "cli.support.outreach_draft";
41096
+ return resolveCliPrompt(key, { docs, context });
41097
+ }
41098
+ function buildPromptGeneratePrompt(context) {
40670
41099
  return [
40671
- ...goal,
40672
- "Use the documentation and FAQ below for any product facts; never invent features, prices or",
40673
- "commitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.",
40674
- "Follow the admin's instructions when given. A human admin reviews and edits your draft before",
40675
- "sending it.",
40676
- "",
40677
- "Output ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code",
40678
- "fences around the whole message.",
40679
- "",
40680
- "===== DOCUMENTATION =====",
40681
- docs,
41100
+ "You are writing a reusable AI PROMPT TEMPLATE for the 4PM platform. Based on the specification",
41101
+ "below, write a clear, effective prompt. Include each required {{placeholder}} verbatim where",
41102
+ "appropriate \u2014 never rename or invent placeholders. A human admin reviews your draft before it",
41103
+ "goes live.",
41104
+ "Output ONLY the prompt template text \u2014 no preamble, no explanation, no code fences.",
40682
41105
  "",
40683
- "===== CONTEXT =====",
41106
+ "===== SPECIFICATION =====",
40684
41107
  context
40685
41108
  ].join("\n");
40686
41109
  }
40687
- function buildLegalDraftPrompt(docs, context) {
41110
+ function buildPromptTranslatePrompt(context) {
40688
41111
  return [
40689
- "You are drafting the body of one of 4PM's legal documents (Terms of Service, Privacy Policy,",
40690
- "Add-on Terms, Rented Machine Addendum, \u2026) for the platform admin to review and publish.",
40691
- "Write or revise the WHOLE document body in the target locale given in the context. Keep the",
40692
- "existing heading structure and numbering unless the admin's instructions say otherwise.",
40693
- "When the target locale is not English and an English reference is given, follow its meaning closely.",
40694
- "Rules:",
40695
- "- Every {{placeholder}} (e.g. {{companyName}}) must be kept verbatim \u2014 never fill or rename it.",
40696
- "- Describe plans, prices, upgrades, downgrades, cancellations, renewals, refunds and add-ons ONLY",
40697
- " as stated in the PLAN CATALOG and BILLING FACTS below or the documentation; never invent",
40698
- " commitments, prices or legal guarantees. Where something is unknown write `[TODO: \u2026]`.",
40699
- "- Plain, precise legal English (or the target language); short numbered sections and bullet lists.",
40700
- "- This is a draft, not legal advice; do not add a disclaimer about that inside the document.",
41112
+ "You are translating a reusable AI PROMPT TEMPLATE for the 4PM platform into the target language",
41113
+ "given below. Preserve EVERY {{placeholder}} exactly \u2014 never translate, rename, or remove a",
41114
+ "placeholder. Keep the same structure, intent and formatting. A human admin reviews your draft.",
41115
+ "Output ONLY the translated template text \u2014 no preamble, no explanation, no code fences.",
40701
41116
  "",
40702
- "Output ONLY the document body in markdown \u2014 no title line, no preamble, no explanation, no code",
40703
- "fences around the whole document.",
40704
- "",
40705
- "===== DOCUMENTATION =====",
40706
- docs,
40707
- "",
40708
- "===== CONTEXT =====",
41117
+ "===== SPECIFICATION =====",
40709
41118
  context
40710
41119
  ].join("\n");
40711
41120
  }
40712
41121
  function imageExt(mime) {
40713
- switch (mime) {
40714
- case "image/png":
40715
- return "png";
40716
- case "image/jpeg":
40717
- return "jpg";
40718
- case "image/webp":
40719
- return "webp";
40720
- case "image/gif":
40721
- return "gif";
40722
- default:
40723
- return "png";
40724
- }
41122
+ return commandImageExt(mime);
40725
41123
  }
40726
41124
  function materializeImages2(images, question) {
40727
41125
  const dir2 = mkdtempSync2(join14(tmpdir3(), "4pm-help-images-"));
40728
41126
  let rewritten = question;
40729
41127
  images.forEach((img, i) => {
40730
- const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
41128
+ const file = join14(dir2, `attachment-${i + 1}.${imageExt(img.mime)}`);
40731
41129
  writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
40732
41130
  if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
40733
41131
  });
@@ -40823,12 +41221,12 @@ async function runClaudeWithFailover(prompt, ai2, imageDir, timeoutMs = ANSWER_T
40823
41221
  }
40824
41222
  async function runChecklistAuthor(req, ai2) {
40825
41223
  try {
40826
- const { text } = await runClaudeWithFailover(req.prompt, ai2);
40827
- if (!text) return { error: "empty output" };
40828
- return { output: text };
41224
+ const { text, usage } = await runClaudeWithFailover(req.prompt, ai2);
41225
+ if (!text) return { reply: { error: "empty output" }, usage };
41226
+ return { reply: { output: text }, usage };
40829
41227
  } catch (err) {
40830
41228
  logger.warn("checklist.author.failed", { error: String(err) });
40831
- return { error: String(err) };
41229
+ return { reply: { error: String(err) }, usage: { ...NO_USAGE } };
40832
41230
  }
40833
41231
  }
40834
41232
  async function runSupportAnswer(req, ai2, profileDir2) {
@@ -41880,6 +42278,24 @@ fi
41880
42278
  exec "${realGh}" "$@"
41881
42279
  `;
41882
42280
  }
42281
+ function findRealGlab(pathValue, shimDir) {
42282
+ for (const dir2 of pathValue.split(delimiter)) {
42283
+ if (!dir2 || dir2 === shimDir) continue;
42284
+ const candidate = join20(dir2, process.platform === "win32" ? "glab.exe" : "glab");
42285
+ if (existsSync13(candidate)) return candidate;
42286
+ }
42287
+ return null;
42288
+ }
42289
+ function glabShimScript(realGlab, cli) {
42290
+ return `#!/bin/sh
42291
+ # 4PM glab shim (ADR-0382) \u2014 injects the served project's GitLab group token.
42292
+ if [ -z "$GITLAB_TOKEN" ]; then
42293
+ T="$(${cli} git-token 2>/dev/null)"
42294
+ if [ -n "$T" ]; then export GITLAB_HOST="$${GIT_HOST_ENV}" GITLAB_TOKEN="$T"; fi
42295
+ fi
42296
+ exec "${realGlab}" "$@"
42297
+ `;
42298
+ }
41883
42299
  function clearEnv() {
41884
42300
  if (!state.baseEnv) return;
41885
42301
  const { count, path } = state.baseEnv;
@@ -41896,7 +42312,8 @@ function clearEnv() {
41896
42312
  }
41897
42313
  function configureGitAuth(method, host, profileDir2, transport) {
41898
42314
  try {
41899
- const active = method === "github-app" && !!host;
42315
+ const isGitlab = method === "gitlab-group-token";
42316
+ const active = (method === "github-app" || isGitlab) && !!host;
41900
42317
  if (!active || state.host && state.host !== host) {
41901
42318
  if (state.host) void revokeAll();
41902
42319
  clearEnv();
@@ -41925,22 +42342,26 @@ function configureGitAuth(method, host, profileDir2, transport) {
41925
42342
  process.env.TSX_TSCONFIG_PATH = devTsconfig;
41926
42343
  }
41927
42344
  process.env[GIT_HOST_ENV] = host;
41928
- process.env[GIT_HOST_KIND_ENV] = githubHostKind(host);
42345
+ process.env[GIT_HOST_KIND_ENV] = isGitlab ? "gitlab" : githubHostKind(host);
41929
42346
  if (process.platform !== "win32") {
41930
42347
  const shimDir = join20(profileDir2, "bin");
41931
- const realGh = findRealGh(state.baseEnv.path, shimDir);
41932
- if (realGh) {
42348
+ const toolName = isGitlab ? "glab" : "gh";
42349
+ const otherTool = isGitlab ? "gh" : "glab";
42350
+ const realTool = isGitlab ? findRealGlab(state.baseEnv.path, shimDir) : findRealGh(state.baseEnv.path, shimDir);
42351
+ if (realTool) {
41933
42352
  mkdirSync6(shimDir, { recursive: true });
41934
- const shim = join20(shimDir, "gh");
41935
- writeFileSync8(shim, shimScript(realGh, cliInvocation()), { encoding: "utf8", mode: 493 });
42353
+ const shim = join20(shimDir, toolName);
42354
+ const shimContent = isGitlab ? glabShimScript(realTool, cliInvocation()) : shimScript(realTool, cliInvocation());
42355
+ writeFileSync8(shim, shimContent, { encoding: "utf8", mode: 493 });
41936
42356
  chmodSync3(shim, 493);
41937
42357
  process.env.PATH = `${shimDir}${delimiter}${state.baseEnv.path}`;
41938
42358
  } else {
41939
- rmSync6(join20(shimDir, "gh"), { force: true });
42359
+ rmSync6(join20(shimDir, toolName), { force: true });
41940
42360
  }
42361
+ rmSync6(join20(shimDir, otherTool), { force: true });
41941
42362
  }
41942
42363
  state.host = host;
41943
- logger.info("git.auth.github-app", { host });
42364
+ logger.info(isGitlab ? "git.auth.gitlab" : "git.auth.github-app", { host });
41944
42365
  return true;
41945
42366
  } catch (err) {
41946
42367
  logger.warn("git.auth.configure.failed", { error: String(err) });
@@ -42826,22 +43247,12 @@ function parseJsonResult(out) {
42826
43247
  }
42827
43248
  var COMPACT_TIMEOUT_MS = 6e4;
42828
43249
  function buildPrompt2(input) {
42829
- return [
42830
- "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
42831
- "across sessions and accounts. Merge the latest exchange into the existing memory and return the",
42832
- `UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
42833
- "as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
42834
- "now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
42835
- "",
42836
- "=== EXISTING MEMORY ===",
42837
- input.oldMemory || "(empty)",
42838
- "",
42839
- "=== LATEST USER PROMPT ===",
42840
- input.prompt,
42841
- "",
42842
- "=== LATEST ASSISTANT ANSWER ===",
42843
- input.answer
42844
- ].join("\n");
43250
+ return resolveCliPrompt("cli.memory.compact", {
43251
+ budgetChars: input.budgetChars,
43252
+ oldMemory: input.oldMemory || "(empty)",
43253
+ prompt: input.prompt,
43254
+ answer: input.answer
43255
+ });
42845
43256
  }
42846
43257
  function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
42847
43258
  return new Promise((resolve13, reject) => {
@@ -43019,10 +43430,10 @@ function activeAiRunCount() {
43019
43430
  return activeRuns.size;
43020
43431
  }
43021
43432
  function cancelActiveRun(commandId) {
43022
- const run15 = activeRuns.get(commandId);
43023
- if (!run15) return { ok: false, state: "unknown" };
43024
- run15.controller.abort();
43025
- return { ok: true, state: run15.state };
43433
+ const run16 = activeRuns.get(commandId);
43434
+ if (!run16) return { ok: false, state: "unknown" };
43435
+ run16.controller.abort();
43436
+ return { ok: true, state: run16.state };
43026
43437
  }
43027
43438
  function handleCommandChannels(ctx, message, payload) {
43028
43439
  if (message.channel === WsChannels.COMMAND_CANCEL) {
@@ -43120,16 +43531,16 @@ async function runLocalCommand(ctx, input) {
43120
43531
  }
43121
43532
  var REFUSED = { exitCode: 1, cancelled: false, exhausted: false, neverStarted: true, output: "" };
43122
43533
  async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
43123
- const run15 = { controller: new AbortController(), state: "queued" };
43124
- activeRuns.set(commandId, run15);
43534
+ const run16 = { controller: new AbortController(), state: "queued" };
43535
+ activeRuns.set(commandId, run16);
43125
43536
  try {
43126
- return await runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot, images, aiConfig, readOnly, bypass);
43537
+ return await runAiPromptInner(ctx, prompt, commandId, origin, run16, oneShot, images, aiConfig, readOnly, bypass);
43127
43538
  } finally {
43128
43539
  activeRuns.delete(commandId);
43129
43540
  if (!readOnly) requestGitSnapshot("ai-run");
43130
43541
  }
43131
43542
  }
43132
- async function runAiPromptInner(ctx, prompt, commandId, origin, run15, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
43543
+ async function runAiPromptInner(ctx, prompt, commandId, origin, run16, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
43133
43544
  const config = readProfileConfig(ctx.profileDir);
43134
43545
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
43135
43546
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -43312,7 +43723,7 @@ ${guardedPrompt}`;
43312
43723
  queue: { position, limit, running: running2 }
43313
43724
  });
43314
43725
  },
43315
- run15.controller.signal
43726
+ run16.controller.signal
43316
43727
  );
43317
43728
  if (slot.kind === "timeout") {
43318
43729
  neverStarted = true;
@@ -43333,9 +43744,9 @@ ${guardedPrompt}`;
43333
43744
  result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
43334
43745
  } else {
43335
43746
  if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
43336
- run15.state = "running";
43747
+ run16.state = "running";
43337
43748
  try {
43338
- result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run15.controller.signal);
43749
+ result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run16.controller.signal);
43339
43750
  } finally {
43340
43751
  slot.handle.release();
43341
43752
  }
@@ -44100,79 +44511,37 @@ empty template. The single source of truth for approval is \`.claude/.autonomous
44100
44511
  NEVER act on an unapproved row.`;
44101
44512
  function buildIntakePrompt(cfg) {
44102
44513
  const h = cfg.taskSizeHints;
44103
- return `You are the 4PM autonomous INTAKE step (headless). Edit ONLY \`USER_TODO.md\`, \`USER_QA.md\` and
44104
- \`AI_TODO.md\`. Do NOT run git, do NOT touch \`AI_PROGRESS.md\` / \`AI_DONE.md\` / any code \u2014 the 4PM cli
44105
- commits and publishes your book edits. Write in the language the project's \`CLAUDE.md\` specifies (default English).
44106
-
44107
- ${BOOKS_INTRO}
44108
-
44109
- ## 1 \u2014 Approved user requests \u2192 tasks
44110
- Read \`USER_TODO.md\` (columns \`ID | Group | Depends | Request\`). For each APPROVED \`REQ-\u2026\` whose \`Depends\`
44111
- are already handled:
44112
- - CLEAR request \u21D2 split it into tasks appended to \`AI_TODO.md\` (columns
44113
- \`ID | Priority | Tag | Depends | Group | Task description | Notes\`), ids \`TSK-{groupid:0000}-{taskid:0000}\`
44114
- (groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in \`AI_TODO.md\`/\`AI_DONE.md\`/
44115
- \`AI_PROGRESS.md\`/their git history, +1; taskid from 0001). \`Depends\` = the \`TSK-\u2026\` it needs.
44116
- Remove the analysed request row (empty \u21D2 reset to the empty template).
44117
- - UNCLEAR request \u21D2 do NOT guess: append a \`USER_QA.md\` row (\`ID | Group | Depends | Original request |
44118
- Question / options | Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\`, blank \`Answer\`; remove the request row.
44119
- An unapproved request stays.
44120
-
44121
- ## 2 \u2014 Answered intake questions
44122
- For each \`USER_QA.md\` row with an \`Answer\`, APPROVED, and whose id is NOT in any \`AI_TODO.md\` \`Depends\`
44123
- (those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the
44124
- answer into tasks (same rules), then remove that QA row.
44125
-
44126
- ## Task size (MANDATORY \u2014 every task must be finishable in ONE run)
44127
- Size each task by SCOPE and write \`size: S\` or \`size: M\` at the start of \`Notes\`:
44128
- - **S** \u2014 one small change in one module/layer (\u2248 \u2264 ${h.sMaxFiles} files, \u2248 \u2264 ${h.sMaxLines} lines), e.g. fix a bug, add a field.
44129
- - **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 ${h.mMaxFiles} files, \u2248 \u2264 ${h.mMaxLines} lines), e.g. one CRUD screen, one API + its UI + tests.
44130
- - **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.
44131
- **Never write an L task**: split it into S/M tasks chained with \`Depends\`, or ask in \`USER_QA.md\` when the split needs a decision.
44132
- The counts are hints, not hard limits. Each task's \`Task description\` states the acceptance criteria and how to test it.
44133
-
44134
- When done, reply with one short line summarising what you added.`;
44514
+ return resolveCliPrompt("cli.autonomous.intake", {
44515
+ booksIntro: BOOKS_INTRO,
44516
+ sMaxFiles: h.sMaxFiles,
44517
+ sMaxLines: h.sMaxLines,
44518
+ mMaxFiles: h.mMaxFiles,
44519
+ mMaxLines: h.mMaxLines
44520
+ });
44135
44521
  }
44136
44522
  function buildImplementPrompt(mine, subs, resumed) {
44137
44523
  const subLines = subs.length ? subs.map((s3) => `- submodule \`${s3.dir}\` is on branch \`${s3.branch}\` \u2014 commit changes to it INSIDE \`${s3.dir}\``).join("\n") : "- (no submodules)";
44138
- return `You are the 4PM autonomous IMPLEMENT step (headless). Implement exactly ONE task, then STOP.
44139
-
44140
- Task ${mine.id} (group ${mine.task.group}, attempt ${mine.attempt}):
44141
- ${mine.task.desc}
44142
- Notes: ${mine.task.notes || "(none)"}
44143
- ${resumed ? "\nThis is a CONTINUATION: earlier work for this task is already committed on the branches below \u2014 review it (git log / git diff against the base) and continue from it; do not start over.\n" : ""}
44144
- Branches (already checked out by the 4PM cli \u2014 stay on them):
44145
- - project root is on \`${mine.branch}\`
44146
- ${subLines}
44147
-
44148
- Rules:
44149
- - Follow the project's architecture and \`CLAUDE.md\`. Add/update tests; run the project's test command and WAIT for the result.
44150
- - Commit your work (\`git add\` + \`git commit\`) in the root and/or inside each submodule you changed.
44151
- - Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files
44152
- (USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.
44153
- - If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real
44154
- trade-offs), do NOT guess: commit what you have and report \`needs-input\` with the question + options.
44155
-
44156
- Finish with ONLY one fenced \`json\` block:
44157
- {"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}`;
44524
+ const resumedNote = resumed ? "\nThis is a CONTINUATION: earlier work for this task is already committed on the branches below \u2014 review it (git log / git diff against the base) and continue from it; do not start over.\n" : "";
44525
+ return resolveCliPrompt("cli.autonomous.implement", {
44526
+ taskId: mine.id,
44527
+ group: mine.task.group,
44528
+ attempt: mine.attempt,
44529
+ desc: mine.task.desc,
44530
+ notes: mine.task.notes || "(none)",
44531
+ resumedNote,
44532
+ branch: mine.branch,
44533
+ subLines
44534
+ });
44158
44535
  }
44159
44536
  function buildSplitPrompt(mine, reasons, diff2) {
44160
- return `You are the 4PM autonomous SPLIT step (read-only \u2014 do not change any file). Task ${mine.id} failed to
44161
- finish after repeated attempts, so it is probably too big for one run. Split it into 2\u20136 smaller child tasks
44162
- that are each finishable in ONE run (size S or M), in execution order (each depends on the previous).
44163
-
44164
- Task: ${mine.task.desc}
44165
- Notes: ${mine.task.notes || "(none)"}
44166
- Why the attempts ended: ${reasons || "(unknown)"}
44167
- Work already done on its WIP branch \`${mine.branch}\` (diff against the base, may be truncated):
44168
- \`\`\`
44169
- ${diff2 || "(no changes yet)"}
44170
- \`\`\`
44171
- The first child continues from that WIP branch: note which parts are already done so they are not redone.
44172
- Each child description must state its acceptance criteria and how to test it.
44173
-
44174
- Reply with ONLY one fenced \`json\` block:
44175
- {"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}`;
44537
+ return resolveCliPrompt("cli.autonomous.split", {
44538
+ taskId: mine.id,
44539
+ desc: mine.task.desc,
44540
+ notes: mine.task.notes || "(none)",
44541
+ reasons: reasons || "(unknown)",
44542
+ branch: mine.branch,
44543
+ diff: diff2 || "(no changes yet)"
44544
+ });
44176
44545
  }
44177
44546
  function parseJsonReply(text) {
44178
44547
  const fenced = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)].map((m2) => m2[1].trim());
@@ -45587,8 +45956,256 @@ function applyGitAuth(ctx, method, host) {
45587
45956
  );
45588
45957
  }
45589
45958
 
45590
- // src/core/knowledge-compose.ts
45959
+ // src/core/research-run.ts
45591
45960
  import { spawn as spawn8 } from "child_process";
45961
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
45962
+ import { tmpdir as tmpdir5 } from "os";
45963
+ import { join as join29 } from "path";
45964
+ var RESEARCH_TIMEOUT_MS = 9 * 60 * 1e3;
45965
+ var RESEARCH_MAX_TURNS = 16;
45966
+ var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
45967
+ var RESEARCH_ALLOWED_TOOLS = "WebSearch,WebFetch";
45968
+ var RESEARCH_ATTACH_TOOLS = "WebSearch,WebFetch,Read,Glob";
45969
+ var RESEARCH_DENIED_BASE = [
45970
+ "Bash",
45971
+ "Edit",
45972
+ "Write",
45973
+ "Read",
45974
+ "Glob",
45975
+ "Grep",
45976
+ "NotebookEdit",
45977
+ "Task",
45978
+ "SlashCommand",
45979
+ "Skill",
45980
+ "ToolSearch",
45981
+ "TaskCreate",
45982
+ "TaskGet",
45983
+ "TaskList",
45984
+ "TaskOutput",
45985
+ "TaskStop",
45986
+ "TaskUpdate",
45987
+ "Monitor",
45988
+ "DesignSync",
45989
+ "CronCreate",
45990
+ "CronDelete",
45991
+ "CronList",
45992
+ "EnterWorktree",
45993
+ "ExitWorktree",
45994
+ "RemoteTrigger",
45995
+ "ScheduleWakeup",
45996
+ "SendMessage",
45997
+ "PushNotification"
45998
+ ];
45999
+ function safeAttachmentName(name, index) {
46000
+ const base = (name || `file-${index + 1}`).replace(/[/\\]/g, "_").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
46001
+ return base || `file-${index + 1}`;
46002
+ }
46003
+ function buildResearchPrompt(question) {
46004
+ return resolveCliPrompt("cli.research.policy", { question });
46005
+ }
46006
+ function runClaudeOnce2(cmd, profile, prompt, extraEnv, onChunk, denyDirs, attachments) {
46007
+ return new Promise((resolve13, reject) => {
46008
+ const isClaude = cmd.includes("claude");
46009
+ const cwd2 = mkdtempSync3(join29(tmpdir5(), "4pm-research-"));
46010
+ const hasAttachments = attachments.length > 0;
46011
+ let finalPrompt = prompt;
46012
+ if (hasAttachments) {
46013
+ const lines = [];
46014
+ attachments.forEach((a, i) => {
46015
+ const file = join29(cwd2, safeAttachmentName(a.name, i));
46016
+ try {
46017
+ writeFileSync9(file, Buffer.from(a.dataBase64, "base64"));
46018
+ lines.push(`- \`${file}\``);
46019
+ } catch {
46020
+ }
46021
+ });
46022
+ if (lines.length > 0) {
46023
+ finalPrompt = `${prompt}
46024
+
46025
+ The user attached these files \u2014 use the Read tool to read them before answering:
46026
+ ${lines.join("\n")}`;
46027
+ }
46028
+ }
46029
+ const args = [
46030
+ "-p",
46031
+ ...profile?.model ? ["--model", profile.model] : [],
46032
+ ...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
46033
+ // Secret-path deny rules (ADR-0347) — reads are not confined to the working dir.
46034
+ ...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
46035
+ // Web-only agent (+ Read/Glob of the scratch dir when attachments are present): allow the tool set,
46036
+ // deny everything else, bound the loop. `--permission-mode default` is the non-variadic terminator
46037
+ // (so the variadic tool flags consume only their own comma-token — same trick as the other runners).
46038
+ ...isClaude ? [
46039
+ `--max-turns=${RESEARCH_MAX_TURNS}`,
46040
+ "--allowedTools",
46041
+ hasAttachments ? RESEARCH_ATTACH_TOOLS : RESEARCH_ALLOWED_TOOLS,
46042
+ "--disallowedTools",
46043
+ (hasAttachments ? RESEARCH_DENIED_BASE.filter((t2) => t2 !== "Read" && t2 !== "Glob") : RESEARCH_DENIED_BASE).join(","),
46044
+ "--permission-mode",
46045
+ "default"
46046
+ ] : []
46047
+ ];
46048
+ const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
46049
+ const cleanup = () => {
46050
+ try {
46051
+ rmSync8(cwd2, { recursive: true, force: true });
46052
+ } catch {
46053
+ }
46054
+ };
46055
+ const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3, cwd: cwd2 });
46056
+ const parser = createAiStreamParser(cmd);
46057
+ let out = "";
46058
+ let err = "";
46059
+ const timer = setTimeout(() => {
46060
+ child.kill("SIGKILL");
46061
+ reject(new Error("research run timed out"));
46062
+ }, RESEARCH_TIMEOUT_MS);
46063
+ timer.unref();
46064
+ child.stdout.on("data", (d) => {
46065
+ const delta = parser.push(d.toString());
46066
+ if (delta) {
46067
+ out += delta;
46068
+ onChunk(delta);
46069
+ }
46070
+ });
46071
+ child.stderr.on("data", (d) => err += d.toString());
46072
+ child.on("error", (e) => {
46073
+ clearTimeout(timer);
46074
+ cleanup();
46075
+ reject(e);
46076
+ });
46077
+ child.on("close", (code) => {
46078
+ clearTimeout(timer);
46079
+ cleanup();
46080
+ const tail = parser.flush();
46081
+ if (tail) {
46082
+ out += tail;
46083
+ onChunk(tail);
46084
+ }
46085
+ resolve13({ code: code ?? -1, out, err, usage: parser.usage() });
46086
+ });
46087
+ child.stdin.write(finalPrompt);
46088
+ child.stdin.end();
46089
+ });
46090
+ }
46091
+ async function runResearch(question, ai2, onChunk, attachments = []) {
46092
+ const prompt = buildResearchPrompt(question);
46093
+ const denyDirs = ai2.profiles.map((p2) => p2.dir);
46094
+ const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
46095
+ let lastReason = "no attempt";
46096
+ for (let i = 0; i < attempts.length; i++) {
46097
+ try {
46098
+ const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, ai2.env, onChunk, denyDirs, attachments);
46099
+ if (code === 0 && out.trim()) {
46100
+ const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE2, tokens: estimateTokens(out) };
46101
+ return { text: out.trim(), usage: u2 };
46102
+ }
46103
+ lastReason = `exited ${code}: ${(err || out).slice(0, 300)}`;
46104
+ } catch (e) {
46105
+ lastReason = e instanceof Error ? e.message : String(e);
46106
+ }
46107
+ }
46108
+ return { text: "", usage: { ...NO_USAGE2 }, error: lastReason };
46109
+ }
46110
+
46111
+ // src/core/research-guard.ts
46112
+ var REFUSAL_PREFIX = "REFUSED:";
46113
+ var ATTACK_INTENT = [
46114
+ /\b(hack|break|breare|breach|compromise|exploit|gain (unauthorized )?access to|take over|take control of|ddos|dos attack)\b[^.?!]{0,60}\b(into|someone|their|his|her|another|the victim'?s|a target|account|server|network|website|wifi|camera|phone|system|database)\b/i,
46115
+ /\b(write|create|build|generate|give me)\b[^.?!]{0,40}\b(malware|ransomware|a virus|a keylogger|a trojan|spyware|a rootkit|a worm|a botnet|an exploit|a phishing (kit|page|email|site))\b/i,
46116
+ /\bbypass\b[^.?!]{0,40}\b(authentication|login|the firewall|a paywall|security|2fa|mfa|licen[sc]e|drm)\b/i,
46117
+ /\b(steal|harvest|exfiltrate|dump|crack)\b[^.?!]{0,40}\b(credentials|passwords|password hashes|credit cards?|personal data|pii|session tokens)\b/i,
46118
+ /\bbrute[- ]?force\b[^.?!]{0,40}\b(login|password|account|credential)/i
46119
+ ];
46120
+ function isModelRefusal(text) {
46121
+ return text.trimStart().toUpperCase().startsWith(REFUSAL_PREFIX);
46122
+ }
46123
+ function refusalReasonOf(text) {
46124
+ const after = text.trim().slice(REFUSAL_PREFIX.length).trim();
46125
+ return (after || "This question is outside what Research can answer.").slice(0, 300);
46126
+ }
46127
+ function screenQuestion(question) {
46128
+ if (ATTACK_INTENT.some((re2) => re2.test(question))) {
46129
+ return { blocked: true, reason: "The question asks to attack or compromise a system, which Research does not do." };
46130
+ }
46131
+ return { blocked: false };
46132
+ }
46133
+ function screenAnswer(answer) {
46134
+ if (isModelRefusal(answer)) return { blocked: true, reason: refusalReasonOf(answer) };
46135
+ if (ATTACK_INTENT.some((re2) => re2.test(answer))) {
46136
+ return { blocked: true, reason: "The answer was withheld by the content policy." };
46137
+ }
46138
+ return { blocked: false };
46139
+ }
46140
+
46141
+ // src/core/ws-client/handlers/research.ts
46142
+ function handleResearchChannels(ctx, message, payload) {
46143
+ if (message.channel !== WsChannels.RESEARCH_ASK) return false;
46144
+ const req = payload;
46145
+ const config = readProfileConfig(ctx.profileDir);
46146
+ const cmd = config.aiCli || "claude";
46147
+ ctx.bus.push({ source: "server", kind: "aireq", text: `${cmd} \u2039 research` });
46148
+ ctx.bus.startBusy("research");
46149
+ void run14(ctx, message, req, config, cmd).finally(() => ctx.bus.endBusy("research"));
46150
+ return true;
46151
+ }
46152
+ async function run14(ctx, message, req, config, cmd) {
46153
+ const emit = (p2) => ctx.send(WsChannels.RESEARCH_PROGRESS, { queryId: req.queryId, ...p2 });
46154
+ const reply = (r) => ctx.send(WsChannels.RESEARCH_ASK, r, message.id);
46155
+ const pre = screenQuestion(req.question);
46156
+ if (pre.blocked) {
46157
+ reply({ refused: true, refusalReason: pre.reason });
46158
+ return;
46159
+ }
46160
+ const slot = await acquireRunSlot(ctx, (info) => emit({ message: `queued #${info.position}` }));
46161
+ if (slot.kind !== "granted") {
46162
+ reply({ error: slot.kind === "denied" ? "storage full" : "no idle run slot" });
46163
+ return;
46164
+ }
46165
+ try {
46166
+ const ai2 = {
46167
+ cmd,
46168
+ profiles: resolveClaudeProfiles(config, getWorkingProfile(ctx.profileDir, cmd)),
46169
+ env: config.aiEnv
46170
+ };
46171
+ const result = await runResearch(req.question, ai2, (delta) => emit({ chunk: delta }), req.attachments ?? []);
46172
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
46173
+ const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
46174
+ if (result.usage.tokens > 0) {
46175
+ events.push({
46176
+ metric: UsageMetric.AI_TOKENS,
46177
+ amount: result.usage.tokens,
46178
+ occurredAt,
46179
+ inputTokens: result.usage.input,
46180
+ outputTokens: result.usage.output,
46181
+ cacheReadTokens: result.usage.cacheRead,
46182
+ cacheCreationTokens: result.usage.cacheCreation,
46183
+ provider: aiProviderOf(cmd),
46184
+ authMode: resolveClaudeAuthMode(config)
46185
+ });
46186
+ }
46187
+ ctx.reportUsage(events, null);
46188
+ if (result.error || !result.text) {
46189
+ ctx.bus.push({ source: "server", kind: "log", text: `research failed: ${result.error ?? "empty"}`, level: "warn" });
46190
+ reply({ error: result.error || "empty answer", tokens: result.usage.tokens });
46191
+ return;
46192
+ }
46193
+ const post2 = screenAnswer(result.text);
46194
+ if (post2.blocked) {
46195
+ reply({ refused: true, refusalReason: post2.reason, tokens: result.usage.tokens });
46196
+ return;
46197
+ }
46198
+ ctx.bus.push({ source: "server", kind: "aires", text: `${cmd} \u203A` });
46199
+ reply({ answer: result.text, tokens: result.usage.tokens });
46200
+ } catch (err) {
46201
+ reply({ error: err instanceof Error ? err.message : String(err) });
46202
+ } finally {
46203
+ slot.handle.release();
46204
+ }
46205
+ }
46206
+
46207
+ // src/core/knowledge-compose.ts
46208
+ import { spawn as spawn9 } from "child_process";
45592
46209
  var COMPOSE_TIMEOUT_MS = 18e4;
45593
46210
  function buildPrompt3(req) {
45594
46211
  const focus = req.prompt ? `
@@ -45614,7 +46231,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
45614
46231
  ...extraEnv,
45615
46232
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
45616
46233
  };
45617
- const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
46234
+ const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
45618
46235
  let out = "";
45619
46236
  let err = "";
45620
46237
  const timer = setTimeout(() => {
@@ -45660,14 +46277,14 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
45660
46277
  }
45661
46278
 
45662
46279
  // src/core/faq-compose.ts
45663
- import { spawn as spawn9, execFile as execFile14 } from "child_process";
45664
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync8 } from "fs";
46280
+ import { spawn as spawn10, execFile as execFile14 } from "child_process";
46281
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync9 } from "fs";
45665
46282
  import { randomUUID as randomUUID3 } from "crypto";
45666
- import { join as join29 } from "path";
45667
- import { tmpdir as tmpdir5 } from "os";
46283
+ import { join as join30 } from "path";
46284
+ import { tmpdir as tmpdir6 } from "os";
45668
46285
  import { promisify as promisify14 } from "util";
45669
46286
  var execFileAsync2 = promisify14(execFile14);
45670
- var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
46287
+ var NO_USAGE3 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
45671
46288
  var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
45672
46289
  var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
45673
46290
  function buildPrompt4(tickets, customPrompt) {
@@ -45677,33 +46294,12 @@ function buildPrompt4(tickets, customPrompt) {
45677
46294
  ${thread}`;
45678
46295
  }).join("\n\n");
45679
46296
  const ids = tickets.map((t2) => t2.id);
45680
- return [
45681
- "You are maintaining the 4PM product FAQ, stored as markdown in THIS git repository (your CWD).",
45682
- "From the resolved support tickets below, distil reusable, product-general FAQ entries and add or",
45683
- "update the appropriate markdown file(s) in this repo (e.g. an FAQ.md, or the closest existing",
45684
- "topic file \u2014 inspect the repo first and match its structure/style).",
45685
- "",
45686
- "Rules:",
45687
- "- Write generalized Q&A, NOT ticket-specific replies; never include customer names, emails, ids,",
45688
- " org names or any personal data from the tickets.",
45689
- "- Merge with existing entries instead of duplicating; keep the existing formatting and headings.",
45690
- "- If a ticket yields no reusable FAQ value, skip it. If nothing is worth adding, make NO changes.",
45691
- "- Only edit markdown documentation files; do not touch code, CI, or unrelated files.",
45692
- "- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working",
45693
- " tree; the surrounding tooling handles commit + PR.",
45694
- ...customPrompt?.trim() ? ["", "Additional instruction from the operator (follow it too):", customPrompt.trim()] : [],
45695
- "",
45696
- "===== RESOLVED TICKETS =====",
45697
- transcript,
45698
- "",
45699
- "===== REQUIRED FINAL OUTPUT =====",
45700
- "After finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON",
45701
- "block summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):",
45702
- "```json",
45703
- `{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`,
45704
- "```",
45705
- "Include exactly one entry per ticket id above; keep each summary to one or two sentences."
45706
- ].join("\n");
46297
+ const ticketJsonSkeleton = `{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`;
46298
+ const customInstruction = customPrompt?.trim() ? `
46299
+
46300
+ Additional instruction from the operator (follow it too):
46301
+ ${customPrompt.trim()}` : "";
46302
+ return resolveCliPrompt("cli.faq.compose", { transcript, ticketJsonSkeleton, customInstruction });
45707
46303
  }
45708
46304
  function parsePerTicket(text) {
45709
46305
  const idx = text.lastIndexOf('"perTicket"');
@@ -45730,7 +46326,7 @@ function parsePerTicket(text) {
45730
46326
  }
45731
46327
  return [];
45732
46328
  }
45733
- function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
46329
+ function runClaudeOnce3(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
45734
46330
  return new Promise((resolve13, reject) => {
45735
46331
  const isClaude = cmd.includes("claude");
45736
46332
  const args = [
@@ -45746,7 +46342,7 @@ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
45746
46342
  ...cmd.includes("codex") ? ["--sandbox", "workspace-write"] : []
45747
46343
  ];
45748
46344
  const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
45749
- const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
46345
+ const child = spawn10(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
45750
46346
  const parser = createAiStreamParser(cmd);
45751
46347
  let out = "";
45752
46348
  let err = "";
@@ -45774,7 +46370,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
45774
46370
  const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
45775
46371
  let lastReason = "no attempt";
45776
46372
  for (let i = 0; i < attempts.length; i++) {
45777
- const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
46373
+ const { code, out, err, usage } = await runClaudeOnce3(ai2.cmd, attempts[i], prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
45778
46374
  if (code === 0) return { usage, text: out.trim() };
45779
46375
  lastReason = `exited ${code}: ${(err || out).slice(0, 500)}`;
45780
46376
  if (i === attempts.length - 1) break;
@@ -45784,7 +46380,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
45784
46380
  var MAX_OUTPUT_CHARS = 8e3;
45785
46381
  async function runFaqCompose(req, ai2) {
45786
46382
  if (!req.repo.token) return { prUrl: "", error: "no write token" };
45787
- const workDir = join29(tmpdir5(), "4pm-faq-sync", randomUUID3());
46383
+ const workDir = join30(tmpdir6(), "4pm-faq-sync", randomUUID3());
45788
46384
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
45789
46385
  const auth = gitAuthArgs("x-access-token", req.repo.token);
45790
46386
  try {
@@ -45814,7 +46410,7 @@ async function runFaqCompose(req, ai2) {
45814
46410
  } catch (err) {
45815
46411
  logger.warn("faq.compose.pr.failed", { error: String(err) });
45816
46412
  }
45817
- const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE2 };
46413
+ const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE3 };
45818
46414
  return {
45819
46415
  prUrl,
45820
46416
  branch: req.headBranch,
@@ -45830,7 +46426,7 @@ async function runFaqCompose(req, ai2) {
45830
46426
  } finally {
45831
46427
  if (existsSync16(workDir)) {
45832
46428
  try {
45833
- rmSync8(workDir, { recursive: true, force: true });
46429
+ rmSync9(workDir, { recursive: true, force: true });
45834
46430
  } catch {
45835
46431
  }
45836
46432
  }
@@ -45926,15 +46522,45 @@ function handleSupportChannels(ctx, message, payload) {
45926
46522
  };
45927
46523
  ctx.bus.push({ source: "server", kind: "aireq", text: `${ccmd} \u2039 author checklist items` });
45928
46524
  ctx.bus.startBusy("checklist-author");
45929
- void runChecklistAuthor(creq, cai).then((reply) => {
45930
- if (reply.error) {
45931
- ctx.bus.push({ source: "server", kind: "log", text: `checklist-author failed: ${reply.error}`, level: "warn" });
45932
- } else {
45933
- ctx.bus.push({ source: "server", kind: "aires", text: `${ccmd} \u203A` });
46525
+ void (async () => {
46526
+ const slot = await acquireRunSlot(ctx, () => void 0);
46527
+ if (slot.kind !== "granted") {
46528
+ ctx.send(
46529
+ WsChannels.CHECKLIST_AUTHOR,
46530
+ { error: slot.kind === "denied" ? "storage full" : "no idle run slot" },
46531
+ message.id
46532
+ );
46533
+ return;
46534
+ }
46535
+ try {
46536
+ const { reply, usage } = await runChecklistAuthor(creq, cai);
46537
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
46538
+ const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
46539
+ if (usage.tokens > 0) {
46540
+ events.push({
46541
+ metric: UsageMetric.AI_TOKENS,
46542
+ amount: usage.tokens,
46543
+ occurredAt,
46544
+ inputTokens: usage.input,
46545
+ outputTokens: usage.output,
46546
+ cacheReadTokens: usage.cacheRead,
46547
+ cacheCreationTokens: usage.cacheCreation,
46548
+ provider: aiProviderOf(ccmd),
46549
+ authMode: resolveClaudeAuthMode(ccfg)
46550
+ });
46551
+ }
46552
+ ctx.reportUsage(events, null);
46553
+ if (reply.error) {
46554
+ ctx.bus.push({ source: "server", kind: "log", text: `checklist-author failed: ${reply.error}`, level: "warn" });
46555
+ } else {
46556
+ ctx.bus.push({ source: "server", kind: "aires", text: `${ccmd} \u203A` });
46557
+ }
46558
+ reportToolResult(ccmd, !reply.error, reply.error);
46559
+ ctx.send(WsChannels.CHECKLIST_AUTHOR, reply, message.id);
46560
+ } finally {
46561
+ slot.handle.release();
45934
46562
  }
45935
- reportToolResult(ccmd, !reply.error, reply.error);
45936
- ctx.send(WsChannels.CHECKLIST_AUTHOR, reply, message.id);
45937
- }).finally(() => ctx.bus.endBusy("checklist-author"));
46563
+ })().finally(() => ctx.bus.endBusy("checklist-author"));
45938
46564
  return true;
45939
46565
  }
45940
46566
  default:
@@ -46035,7 +46661,7 @@ function handleToolsChannels(ctx, message, payload) {
46035
46661
 
46036
46662
  // src/core/agents.ts
46037
46663
  import { readFile as readFile14, writeFile as writeFile10, mkdir as mkdir8, readdir as readdir6, rm as rm7 } from "fs/promises";
46038
- import { join as join30 } from "path";
46664
+ import { join as join31 } from "path";
46039
46665
  var AGENTS_REL = ".claude/agents";
46040
46666
  var SKILLS_REL = ".claude/skills";
46041
46667
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -46059,17 +46685,17 @@ async function listAgents(root) {
46059
46685
  const subagents = [];
46060
46686
  const skills = [];
46061
46687
  try {
46062
- for (const e of await readdir6(join30(root, AGENTS_REL), { withFileTypes: true })) {
46688
+ for (const e of await readdir6(join31(root, AGENTS_REL), { withFileTypes: true })) {
46063
46689
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
46064
- const fm = frontmatter(await readText2(join30(root, AGENTS_REL, e.name)));
46690
+ const fm = frontmatter(await readText2(join31(root, AGENTS_REL, e.name)));
46065
46691
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
46066
46692
  }
46067
46693
  } catch {
46068
46694
  }
46069
46695
  try {
46070
- for (const e of await readdir6(join30(root, SKILLS_REL), { withFileTypes: true })) {
46696
+ for (const e of await readdir6(join31(root, SKILLS_REL), { withFileTypes: true })) {
46071
46697
  if (!e.isDirectory()) continue;
46072
- const fm = frontmatter(await readText2(join30(root, SKILLS_REL, e.name, "SKILL.md")));
46698
+ const fm = frontmatter(await readText2(join31(root, SKILLS_REL, e.name, "SKILL.md")));
46073
46699
  skills.push({ name: e.name, description: fm.description });
46074
46700
  }
46075
46701
  } catch {
@@ -46080,15 +46706,15 @@ async function listAgents(root) {
46080
46706
  }
46081
46707
  function itemPath(root, kind, name) {
46082
46708
  const n = safeName(name);
46083
- return kind === "subagent" ? join30(root, AGENTS_REL, `${n}.md`) : join30(root, SKILLS_REL, n, "SKILL.md");
46709
+ return kind === "subagent" ? join31(root, AGENTS_REL, `${n}.md`) : join31(root, SKILLS_REL, n, "SKILL.md");
46084
46710
  }
46085
46711
  async function readAgent(root, kind, name) {
46086
46712
  const content = await readText2(itemPath(root, kind, name));
46087
46713
  if (kind !== "subagent") return { content };
46088
46714
  const n = safeName(name);
46089
46715
  const [short, long] = await Promise.all([
46090
- readText2(join30(root, AGENTS_REL, "short-memory", `${n}.md`)),
46091
- readText2(join30(root, AGENTS_REL, "long-memory", `${n}.md`))
46716
+ readText2(join31(root, AGENTS_REL, "short-memory", `${n}.md`)),
46717
+ readText2(join31(root, AGENTS_REL, "long-memory", `${n}.md`))
46092
46718
  ]);
46093
46719
  return { content, memory: { short, long } };
46094
46720
  }
@@ -46098,12 +46724,12 @@ async function writeAgent(root, req) {
46098
46724
  try {
46099
46725
  if (req.action === "delete") {
46100
46726
  if (req.kind === "subagent") await rm7(itemPath(root, "subagent", n), { force: true });
46101
- else await rm7(join30(root, SKILLS_REL, n), { recursive: true, force: true });
46727
+ else await rm7(join31(root, SKILLS_REL, n), { recursive: true, force: true });
46102
46728
  return { ok: true };
46103
46729
  }
46104
46730
  if (req.content === void 0) return { ok: false, error: "content is required" };
46105
46731
  const path = itemPath(root, req.kind, n);
46106
- await mkdir8(join30(path, ".."), { recursive: true });
46732
+ await mkdir8(join31(path, ".."), { recursive: true });
46107
46733
  await writeFile10(path, req.content, "utf8");
46108
46734
  return { ok: true };
46109
46735
  } catch (err) {
@@ -46113,7 +46739,7 @@ async function writeAgent(root, req) {
46113
46739
 
46114
46740
  // src/core/secrets.ts
46115
46741
  import { readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
46116
- import { join as join31 } from "path";
46742
+ import { join as join32 } from "path";
46117
46743
  var SECURITY_REL = "AI_SECURITY.md";
46118
46744
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
46119
46745
  var SECRETS_REL = "project.secrets.json";
@@ -46126,7 +46752,7 @@ async function readText3(path, fallback = "") {
46126
46752
  }
46127
46753
  async function readSecretsObject(root) {
46128
46754
  try {
46129
- const v2 = JSON.parse(await readFile15(join31(root, SECRETS_REL), "utf8"));
46755
+ const v2 = JSON.parse(await readFile15(join32(root, SECRETS_REL), "utf8"));
46130
46756
  return v2 && typeof v2 === "object" ? v2 : {};
46131
46757
  } catch {
46132
46758
  return {};
@@ -46145,8 +46771,8 @@ function placeholderKeys(text) {
46145
46771
  }
46146
46772
  async function readSecrets(root) {
46147
46773
  const [security, placeholder, secrets] = await Promise.all([
46148
- readText3(join31(root, SECURITY_REL)),
46149
- readText3(join31(root, PLACEHOLDER_REL)),
46774
+ readText3(join32(root, SECURITY_REL)),
46775
+ readText3(join32(root, PLACEHOLDER_REL)),
46150
46776
  readSecretsObject(root)
46151
46777
  ]);
46152
46778
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -46159,14 +46785,14 @@ async function readSecrets(root) {
46159
46785
  async function writeSecrets(root, req) {
46160
46786
  try {
46161
46787
  if (req.kind === "security") {
46162
- await writeFile11(join31(root, SECURITY_REL), req.content, "utf8");
46788
+ await writeFile11(join32(root, SECURITY_REL), req.content, "utf8");
46163
46789
  } else if (req.kind === "placeholder") {
46164
- await writeFile11(join31(root, PLACEHOLDER_REL), req.content, "utf8");
46790
+ await writeFile11(join32(root, PLACEHOLDER_REL), req.content, "utf8");
46165
46791
  } else {
46166
46792
  const secrets = await readSecretsObject(root);
46167
46793
  if (req.kind === "secret") secrets[req.key] = req.value;
46168
46794
  else delete secrets[req.key];
46169
- await writeFile11(join31(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
46795
+ await writeFile11(join32(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
46170
46796
  }
46171
46797
  return { ok: true };
46172
46798
  } catch (err) {
@@ -46176,12 +46802,12 @@ async function writeSecrets(root, req) {
46176
46802
 
46177
46803
  // src/core/agent-tools.ts
46178
46804
  import { readFile as readFile16, writeFile as writeFile12, mkdir as mkdir9 } from "fs/promises";
46179
- import { dirname as dirname9, join as join32 } from "path";
46805
+ import { dirname as dirname9, join as join33 } from "path";
46180
46806
  var SHARED_REL = ".claude/settings.json";
46181
46807
  var LOCAL_REL = ".claude/settings.local.json";
46182
46808
  var SECRETS_DENY = "Read(./project.secrets.json)";
46183
46809
  function fileFor(root, scope) {
46184
- return join32(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
46810
+ return join33(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
46185
46811
  }
46186
46812
  async function readSettingsObject(path) {
46187
46813
  try {
@@ -46218,7 +46844,7 @@ async function writeAgentTools(root, scope, permissions) {
46218
46844
  // src/core/packages.ts
46219
46845
  import { createHash as createHash4 } from "crypto";
46220
46846
  import { readFile as readFile17, writeFile as writeFile13, mkdir as mkdir10, readdir as readdir7, rm as rm8, stat as stat5 } from "fs/promises";
46221
- import { dirname as dirname10, join as join33, relative } from "path";
46847
+ import { dirname as dirname10, join as join34, relative } from "path";
46222
46848
  var AGENTS_REL2 = ".claude/agents";
46223
46849
  var SKILLS_REL2 = ".claude/skills";
46224
46850
  var MANIFEST_REL = ".claude/.4pm-packages.json";
@@ -46242,7 +46868,7 @@ async function listFilesRel(dir2) {
46242
46868
  return;
46243
46869
  }
46244
46870
  for (const e of entries2) {
46245
- const full = join33(d, e.name);
46871
+ const full = join34(d, e.name);
46246
46872
  if (e.isDirectory()) await walk2(full);
46247
46873
  else if (e.isFile()) out.push(relative(dir2, full));
46248
46874
  }
@@ -46253,16 +46879,16 @@ async function listFilesRel(dir2) {
46253
46879
  async function readArtifactFiles(root, kind, slug) {
46254
46880
  const files = [];
46255
46881
  if (kind === "subagent") {
46256
- const agent = await readMaybe(join33(root, AGENTS_REL2, `${slug}.md`));
46882
+ const agent = await readMaybe(join34(root, AGENTS_REL2, `${slug}.md`));
46257
46883
  if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
46258
- const short = await readMaybe(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`));
46884
+ const short = await readMaybe(join34(root, AGENTS_REL2, "short-memory", `${slug}.md`));
46259
46885
  if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
46260
- const long = await readMaybe(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`));
46886
+ const long = await readMaybe(join34(root, AGENTS_REL2, "long-memory", `${slug}.md`));
46261
46887
  if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
46262
46888
  } else {
46263
- const base = join33(root, SKILLS_REL2, slug);
46889
+ const base = join34(root, SKILLS_REL2, slug);
46264
46890
  for (const rel of await listFilesRel(base)) {
46265
- const buf = await readMaybe(join33(base, rel));
46891
+ const buf = await readMaybe(join34(base, rel));
46266
46892
  if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
46267
46893
  }
46268
46894
  }
@@ -46280,7 +46906,7 @@ async function artifactSha256(root, kind, slug) {
46280
46906
  return h.digest("hex");
46281
46907
  }
46282
46908
  async function artifactExists(root, kind, slug) {
46283
- const target = kind === "subagent" ? join33(root, AGENTS_REL2, `${slug}.md`) : join33(root, SKILLS_REL2, slug);
46909
+ const target = kind === "subagent" ? join34(root, AGENTS_REL2, `${slug}.md`) : join34(root, SKILLS_REL2, slug);
46284
46910
  try {
46285
46911
  await stat5(target);
46286
46912
  return true;
@@ -46289,7 +46915,7 @@ async function artifactExists(root, kind, slug) {
46289
46915
  }
46290
46916
  }
46291
46917
  async function readManifest(root) {
46292
- const buf = await readMaybe(join33(root, MANIFEST_REL));
46918
+ const buf = await readMaybe(join34(root, MANIFEST_REL));
46293
46919
  if (!buf) return {};
46294
46920
  try {
46295
46921
  return JSON.parse(buf.toString("utf8"));
@@ -46298,7 +46924,7 @@ async function readManifest(root) {
46298
46924
  }
46299
46925
  }
46300
46926
  async function writeManifest(root, manifest) {
46301
- const path = join33(root, MANIFEST_REL);
46927
+ const path = join34(root, MANIFEST_REL);
46302
46928
  await mkdir10(dirname10(path), { recursive: true });
46303
46929
  await writeFile13(path, JSON.stringify(manifest, null, 2), "utf8");
46304
46930
  }
@@ -46307,24 +46933,24 @@ async function writeArtifactFiles(root, kind, slug, files) {
46307
46933
  if (kind === "subagent") {
46308
46934
  const agent = byPath.get("agent.md");
46309
46935
  if (agent) {
46310
- await mkdir10(join33(root, AGENTS_REL2), { recursive: true });
46311
- await writeFile13(join33(root, AGENTS_REL2, `${slug}.md`), agent);
46936
+ await mkdir10(join34(root, AGENTS_REL2), { recursive: true });
46937
+ await writeFile13(join34(root, AGENTS_REL2, `${slug}.md`), agent);
46312
46938
  }
46313
46939
  const short = byPath.get("short-memory.md");
46314
46940
  if (short) {
46315
- await mkdir10(join33(root, AGENTS_REL2, "short-memory"), { recursive: true });
46316
- await writeFile13(join33(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
46941
+ await mkdir10(join34(root, AGENTS_REL2, "short-memory"), { recursive: true });
46942
+ await writeFile13(join34(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
46317
46943
  }
46318
46944
  const long = byPath.get("long-memory.md");
46319
46945
  if (long) {
46320
- await mkdir10(join33(root, AGENTS_REL2, "long-memory"), { recursive: true });
46321
- await writeFile13(join33(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
46946
+ await mkdir10(join34(root, AGENTS_REL2, "long-memory"), { recursive: true });
46947
+ await writeFile13(join34(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
46322
46948
  }
46323
46949
  } else {
46324
- const base = join33(root, SKILLS_REL2, slug);
46950
+ const base = join34(root, SKILLS_REL2, slug);
46325
46951
  for (const [rel, buf] of byPath) {
46326
46952
  if (rel.includes("..") || rel.startsWith("/")) continue;
46327
- const dest = join33(base, rel);
46953
+ const dest = join34(base, rel);
46328
46954
  await mkdir10(dirname10(dest), { recursive: true });
46329
46955
  await writeFile13(dest, buf);
46330
46956
  }
@@ -46390,11 +47016,11 @@ async function removePackage(root, slug) {
46390
47016
  const entry = manifest[n];
46391
47017
  if (!entry) return { ok: false, error: "not installed" };
46392
47018
  if (entry.kind === "subagent") {
46393
- await rm8(join33(root, AGENTS_REL2, `${n}.md`), { force: true });
46394
- await rm8(join33(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
46395
- await rm8(join33(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
47019
+ await rm8(join34(root, AGENTS_REL2, `${n}.md`), { force: true });
47020
+ await rm8(join34(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
47021
+ await rm8(join34(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
46396
47022
  } else {
46397
- await rm8(join33(root, SKILLS_REL2, n), { recursive: true, force: true });
47023
+ await rm8(join34(root, SKILLS_REL2, n), { recursive: true, force: true });
46398
47024
  }
46399
47025
  delete manifest[n];
46400
47026
  await writeManifest(root, manifest);
@@ -46406,7 +47032,7 @@ async function removePackage(root, slug) {
46406
47032
 
46407
47033
  // src/core/graph.ts
46408
47034
  import { readFile as readFile18, readdir as readdir8 } from "fs/promises";
46409
- import { join as join34, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
47035
+ import { join as join35, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
46410
47036
  var MAX_FILES = 2e3;
46411
47037
  var MAX_NODES = 3e3;
46412
47038
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -46426,7 +47052,7 @@ async function walk(root, accept) {
46426
47052
  if (e.name.startsWith(".") && e.name !== ".claude") {
46427
47053
  if (e.isDirectory()) continue;
46428
47054
  }
46429
- const full = join34(dir2, e.name);
47055
+ const full = join35(dir2, e.name);
46430
47056
  if (e.isDirectory()) {
46431
47057
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
46432
47058
  } else if (accept(full)) {
@@ -46547,15 +47173,15 @@ async function buildGraph(root, mode) {
46547
47173
  // src/core/rag.ts
46548
47174
  import { readFile as readFile19, writeFile as writeFile14, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
46549
47175
  import { existsSync as existsSync17 } from "fs";
46550
- import { execFile as execFile15, spawn as spawn10 } from "child_process";
47176
+ import { execFile as execFile15, spawn as spawn11 } from "child_process";
46551
47177
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
46552
- import { join as join35 } from "path";
47178
+ import { join as join36 } from "path";
46553
47179
  import { promisify as promisify15 } from "util";
46554
- var run14 = promisify15(execFile15);
47180
+ var run15 = promisify15(execFile15);
46555
47181
  var RAG_REL = ".claude/rag";
46556
47182
  async function tryRun(cmd, args) {
46557
47183
  try {
46558
- const { stdout } = await run14(cmd, args, { timeout: 8e3 });
47184
+ const { stdout } = await run15(cmd, args, { timeout: 8e3 });
46559
47185
  return { ok: true, out: stdout.trim() };
46560
47186
  } catch {
46561
47187
  return { ok: false, out: "" };
@@ -46570,7 +47196,7 @@ async function detectPython() {
46570
47196
  }
46571
47197
  async function installLog(root) {
46572
47198
  try {
46573
- const text = await readFile19(join35(root, RAG_REL, "install.log"), "utf8");
47199
+ const text = await readFile19(join36(root, RAG_REL, "install.log"), "utf8");
46574
47200
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
46575
47201
  } catch {
46576
47202
  return [];
@@ -46632,14 +47258,14 @@ async function ragStatus(root) {
46632
47258
  };
46633
47259
  let modelName = "";
46634
47260
  try {
46635
- modelName = (await readFile19(join35(root, RAG_REL, ".installed"), "utf8")).trim();
47261
+ modelName = (await readFile19(join36(root, RAG_REL, ".installed"), "utf8")).trim();
46636
47262
  } catch {
46637
47263
  modelName = "";
46638
47264
  }
46639
47265
  const modelPresent = modelName.length > 0;
46640
47266
  return {
46641
47267
  installed: fastembed && sqliteVec && modelPresent,
46642
- installing: existsSync17(join35(root, RAG_REL, ".installing")),
47268
+ installing: existsSync17(join36(root, RAG_REL, ".installing")),
46643
47269
  python: { found: py.found, version: py.version },
46644
47270
  deps: { pip, fastembed, sqliteVec },
46645
47271
  model: { present: modelPresent, name: modelName },
@@ -46650,9 +47276,9 @@ async function ragStatus(root) {
46650
47276
  };
46651
47277
  }
46652
47278
  async function readIndexMeta(root) {
46653
- const indexing = existsSync17(join35(root, RAG_REL, ".indexing"));
47279
+ const indexing = existsSync17(join36(root, RAG_REL, ".indexing"));
46654
47280
  try {
46655
- const m2 = JSON.parse(await readFile19(join35(root, RAG_REL, ".index.json"), "utf8"));
47281
+ const m2 = JSON.parse(await readFile19(join36(root, RAG_REL, ".index.json"), "utf8"));
46656
47282
  return {
46657
47283
  present: true,
46658
47284
  indexing,
@@ -46719,14 +47345,14 @@ if __name__ == "__main__":
46719
47345
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
46720
47346
  `;
46721
47347
  async function ensureEngine(root) {
46722
- await mkdir11(join35(root, RAG_REL), { recursive: true }).catch(() => void 0);
46723
- await writeFile14(join35(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
47348
+ await mkdir11(join36(root, RAG_REL), { recursive: true }).catch(() => void 0);
47349
+ await writeFile14(join36(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
46724
47350
  }
46725
47351
  async function ragReindex(root) {
46726
47352
  const py = await detectPython();
46727
47353
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
46728
- if (!existsSync17(join35(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
46729
- if (existsSync17(join35(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
47354
+ if (!existsSync17(join36(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
47355
+ if (existsSync17(join36(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
46730
47356
  await ensureEngine(root);
46731
47357
  const script = `
46732
47358
  set +e
@@ -46734,7 +47360,7 @@ touch "${RAG_REL}/.indexing"
46734
47360
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
46735
47361
  rm -f "${RAG_REL}/.indexing"
46736
47362
  `;
46737
- const child = spawn10("bash", ["-c", script], {
47363
+ const child = spawn11("bash", ["-c", script], {
46738
47364
  cwd: root,
46739
47365
  env: { ...process.env, RAG_PY: py.bin },
46740
47366
  detached: true,
@@ -46746,12 +47372,12 @@ rm -f "${RAG_REL}/.indexing"
46746
47372
  async function ragQuery(root, queryText, k2 = 8) {
46747
47373
  const py = await detectPython();
46748
47374
  if (!py.found) return { results: [], error: "python3 not found" };
46749
- if (!existsSync17(join35(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
47375
+ if (!existsSync17(join36(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
46750
47376
  await ensureEngine(root);
46751
47377
  try {
46752
- const { stdout } = await run14(
47378
+ const { stdout } = await run15(
46753
47379
  py.bin,
46754
- [join35(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
47380
+ [join36(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
46755
47381
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
46756
47382
  );
46757
47383
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -46766,9 +47392,9 @@ async function ragInstall(root, model) {
46766
47392
  }
46767
47393
  const py = await detectPython();
46768
47394
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
46769
- if (existsSync17(join35(root, RAG_REL, ".installing"))) return { ok: true, started: true };
47395
+ if (existsSync17(join36(root, RAG_REL, ".installing"))) return { ok: true, started: true };
46770
47396
  try {
46771
- await mkdir11(join35(root, RAG_REL), { recursive: true });
47397
+ await mkdir11(join36(root, RAG_REL), { recursive: true });
46772
47398
  } catch {
46773
47399
  }
46774
47400
  const script = `
@@ -46784,7 +47410,7 @@ rm -f "${RAG_REL}/.installed"
46784
47410
  } >> "${RAG_REL}/install.log" 2>&1
46785
47411
  rm -f "${RAG_REL}/.installing"
46786
47412
  `;
46787
- const child = spawn10("bash", ["-c", script], {
47413
+ const child = spawn11("bash", ["-c", script], {
46788
47414
  cwd: root,
46789
47415
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
46790
47416
  detached: true,
@@ -46796,7 +47422,7 @@ rm -f "${RAG_REL}/.installing"
46796
47422
 
46797
47423
  // src/core/config-sync.ts
46798
47424
  import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
46799
- import { join as join36 } from "path";
47425
+ import { join as join37 } from "path";
46800
47426
  var SERVER_MANAGED_KEYS = [
46801
47427
  "physicPath",
46802
47428
  "sessionSwitchPct",
@@ -46813,7 +47439,7 @@ var SERVER_MANAGED_KEYS = [
46813
47439
  "projectAiMemoryBudgetChars"
46814
47440
  ];
46815
47441
  function readConfigText(profileDir2) {
46816
- const path = join36(profileDir2, "config.json");
47442
+ const path = join37(profileDir2, "config.json");
46817
47443
  if (existsSync18(path)) {
46818
47444
  try {
46819
47445
  return readFileSync16(path, "utf8");
@@ -47439,6 +48065,7 @@ var WsClient = class {
47439
48065
  if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
47440
48066
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
47441
48067
  this.restrictToFolder = token.aiRestrictToFolder === true;
48068
+ setPromptOverrides(token.promptOverrides ?? null);
47442
48069
  configureGitAuth(
47443
48070
  token.gitAuth ?? null,
47444
48071
  token.gitAuthHost ?? null,
@@ -47604,6 +48231,7 @@ var WsClient = class {
47604
48231
  if (handleToolsChannels(this.hctx, message, payload)) return;
47605
48232
  if (handleGitChannels(this.hctx, message, payload)) return;
47606
48233
  if (handleSupportChannels(this.hctx, message, payload)) return;
48234
+ if (handleResearchChannels(this.hctx, message, payload)) return;
47607
48235
  if (handleMiscChannels(this.hctx, message, payload)) return;
47608
48236
  this.bus.log(t("error.unsupportedChannel", { channel: message.channel }), "warn");
47609
48237
  }
@@ -47722,7 +48350,7 @@ var WsClient = class {
47722
48350
  */
47723
48351
  physicFolderPath(projectName) {
47724
48352
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
47725
- return safe ? join37(this.context.profileDir, safe) : null;
48353
+ return safe ? join38(this.context.profileDir, safe) : null;
47726
48354
  }
47727
48355
  /**
47728
48356
  * Report machine.status: version + the state of the physic project being served
@@ -47834,7 +48462,7 @@ var WsClient = class {
47834
48462
  */
47835
48463
  uploadLog() {
47836
48464
  try {
47837
- const upload = readLogUpload(join37(this.context.profileDir, "logs"));
48465
+ const upload = readLogUpload(join38(this.context.profileDir, "logs"));
47838
48466
  if (!upload) return;
47839
48467
  const sig = `${upload.fileName}:${upload.totalBytes}`;
47840
48468
  if (sig === this.lastLogSig) return;
@@ -47996,10 +48624,10 @@ var WsClient = class {
47996
48624
  };
47997
48625
 
47998
48626
  // src/core/instance-lock.ts
47999
- import { readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
48000
- import { join as join38 } from "path";
48627
+ import { readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync10 } from "fs";
48628
+ import { join as join39 } from "path";
48001
48629
  function lockPath(profileDir2) {
48002
- return join38(profileDir2, "start.lock");
48630
+ return join39(profileDir2, "start.lock");
48003
48631
  }
48004
48632
  function isAlive(pid) {
48005
48633
  try {
@@ -48019,13 +48647,13 @@ function lockHolder(profileDir2) {
48019
48647
  }
48020
48648
  function acquireInstanceLock(profileDir2) {
48021
48649
  if (lockHolder(profileDir2) !== null) return false;
48022
- writeFileSync9(lockPath(profileDir2), String(process.pid), "utf8");
48650
+ writeFileSync10(lockPath(profileDir2), String(process.pid), "utf8");
48023
48651
  return true;
48024
48652
  }
48025
48653
  function releaseInstanceLock(profileDir2) {
48026
48654
  try {
48027
48655
  const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
48028
- if (pid === process.pid) rmSync9(lockPath(profileDir2), { force: true });
48656
+ if (pid === process.pid) rmSync10(lockPath(profileDir2), { force: true });
48029
48657
  } catch {
48030
48658
  }
48031
48659
  }
@@ -48320,8 +48948,8 @@ var SessionBus = class {
48320
48948
 
48321
48949
  // src/core/control-server.ts
48322
48950
  import { createServer } from "net";
48323
- import { chmodSync as chmodSync5, rmSync as rmSync10 } from "fs";
48324
- import { join as join40 } from "path";
48951
+ import { chmodSync as chmodSync5, rmSync as rmSync11 } from "fs";
48952
+ import { join as join41 } from "path";
48325
48953
 
48326
48954
  // src/core/control-protocol.ts
48327
48955
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -48350,16 +48978,16 @@ function createFrameParser() {
48350
48978
  }
48351
48979
 
48352
48980
  // src/core/control-token.ts
48353
- import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
48981
+ import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
48354
48982
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
48355
- import { join as join39 } from "path";
48983
+ import { join as join40 } from "path";
48356
48984
  function controlTokenPath(profileDir2) {
48357
- return join39(profileDir2, CONTROL_TOKEN_FILE);
48985
+ return join40(profileDir2, CONTROL_TOKEN_FILE);
48358
48986
  }
48359
48987
  function writeControlToken(profileDir2) {
48360
48988
  const token = randomBytes3(32).toString("hex");
48361
48989
  const path = controlTokenPath(profileDir2);
48362
- writeFileSync10(path, token, { mode: 384 });
48990
+ writeFileSync11(path, token, { mode: 384 });
48363
48991
  try {
48364
48992
  chmodSync4(path, 384);
48365
48993
  } catch {
@@ -48383,9 +49011,9 @@ function tokenMatches(expected, given) {
48383
49011
 
48384
49012
  // src/core/control-server.ts
48385
49013
  function startControlServer(bus, profileDir2, info) {
48386
- const socketPath = join40(profileDir2, CONTROL_SOCKET_FILE);
49014
+ const socketPath = join41(profileDir2, CONTROL_SOCKET_FILE);
48387
49015
  try {
48388
- rmSync10(socketPath, { force: true });
49016
+ rmSync11(socketPath, { force: true });
48389
49017
  } catch {
48390
49018
  }
48391
49019
  const clients = /* @__PURE__ */ new Set();
@@ -48476,8 +49104,8 @@ function startControlServer(bus, profileDir2, info) {
48476
49104
  clients.clear();
48477
49105
  server.close();
48478
49106
  try {
48479
- rmSync10(socketPath, { force: true });
48480
- rmSync10(controlTokenPath(profileDir2), { force: true });
49107
+ rmSync11(socketPath, { force: true });
49108
+ rmSync11(controlTokenPath(profileDir2), { force: true });
48481
49109
  } catch {
48482
49110
  }
48483
49111
  };
@@ -50456,7 +51084,7 @@ var src_default = Yoga;
50456
51084
 
50457
51085
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
50458
51086
  var import_react_reconciler = __toESM(require_react_reconciler(), 1);
50459
- var import_constants12 = __toESM(require_constants2(), 1);
51087
+ var import_constants15 = __toESM(require_constants2(), 1);
50460
51088
  import process3 from "process";
50461
51089
 
50462
51090
  // ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
@@ -51698,7 +52326,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
51698
52326
  scheduleTimeout: setTimeout,
51699
52327
  cancelTimeout: clearTimeout,
51700
52328
  noTimeout: -1,
51701
- getCurrentEventPriority: () => import_constants12.DefaultEventPriority,
52329
+ getCurrentEventPriority: () => import_constants15.DefaultEventPriority,
51702
52330
  beforeActiveInstanceBlur() {
51703
52331
  },
51704
52332
  afterActiveInstanceBlur() {
@@ -54140,11 +54768,11 @@ var import_react21 = __toESM(require_react(), 1);
54140
54768
  var import_react26 = __toESM(require_react(), 1);
54141
54769
 
54142
54770
  // src/core/input-history.ts
54143
- import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
54144
- import { join as join41 } from "path";
54771
+ import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
54772
+ import { join as join42 } from "path";
54145
54773
  var MAX_HISTORY = 200;
54146
54774
  function historyPath(profileDir2) {
54147
- return join41(profileDir2, "input-history.json");
54775
+ return join42(profileDir2, "input-history.json");
54148
54776
  }
54149
54777
  function loadInputHistory(profileDir2) {
54150
54778
  const path = historyPath(profileDir2);
@@ -54161,7 +54789,7 @@ function appendInputHistory(profileDir2, line, current2) {
54161
54789
  if (!trimmed || current2[current2.length - 1] === trimmed) return current2;
54162
54790
  const next = [...current2, trimmed].slice(-MAX_HISTORY);
54163
54791
  try {
54164
- writeFileSync11(historyPath(profileDir2), JSON.stringify(next), "utf8");
54792
+ writeFileSync12(historyPath(profileDir2), JSON.stringify(next), "utf8");
54165
54793
  } catch {
54166
54794
  }
54167
54795
  return next;
@@ -54758,7 +55386,7 @@ async function runStart(profileDir2, profileName) {
54758
55386
  return;
54759
55387
  }
54760
55388
  logger.configure({
54761
- dir: join42(profileDir2, "logs"),
55389
+ dir: join43(profileDir2, "logs"),
54762
55390
  level: resolveLogLevel(),
54763
55391
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
54764
55392
  });
@@ -54778,7 +55406,7 @@ async function runStart(profileDir2, profileName) {
54778
55406
  if (result.action === "updated") {
54779
55407
  logger.info("update.updated", { version: result.version });
54780
55408
  console.log(t("start.restarting"));
54781
- const child = spawn11(process.execPath, process.argv.slice(1), {
55409
+ const child = spawn12(process.execPath, process.argv.slice(1), {
54782
55410
  stdio: "inherit",
54783
55411
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
54784
55412
  });
@@ -54868,7 +55496,7 @@ async function runStart(profileDir2, profileName) {
54868
55496
  }
54869
55497
 
54870
55498
  // src/commands/attach.ts
54871
- import { join as join43 } from "path";
55499
+ import { join as join44 } from "path";
54872
55500
 
54873
55501
  // src/core/control-client.ts
54874
55502
  import { connect as connect2 } from "net";
@@ -54977,7 +55605,7 @@ async function runAttach(profileDir2, profileName) {
54977
55605
  process.exitCode = 1;
54978
55606
  return;
54979
55607
  }
54980
- const socketPath = join43(profileDir2, CONTROL_SOCKET_FILE);
55608
+ const socketPath = join44(profileDir2, CONTROL_SOCKET_FILE);
54981
55609
  const bus = new SessionBus();
54982
55610
  let conn;
54983
55611
  try {
@@ -55008,11 +55636,11 @@ async function runAttach(profileDir2, profileName) {
55008
55636
 
55009
55637
  // src/commands/auto-run.ts
55010
55638
  import { connect as connect3 } from "net";
55011
- import { join as join44 } from "path";
55639
+ import { join as join45 } from "path";
55012
55640
  var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
55013
55641
  async function runAutoRun(profileDir2, profileName) {
55014
55642
  initI18n(readProfileConfig(profileDir2).locale);
55015
- const socketPath = join44(profileDir2, CONTROL_SOCKET_FILE);
55643
+ const socketPath = join45(profileDir2, CONTROL_SOCKET_FILE);
55016
55644
  const token = readControlToken(profileDir2);
55017
55645
  const code = await new Promise((resolve13) => {
55018
55646
  let settled = false;
@@ -55069,7 +55697,7 @@ async function runAutoRun(profileDir2, profileName) {
55069
55697
 
55070
55698
  // src/commands/git-credential.ts
55071
55699
  import { connect as connect4 } from "net";
55072
- import { join as join45 } from "path";
55700
+ import { join as join46 } from "path";
55073
55701
  var REQUEST_TIMEOUT_MS = 2e4;
55074
55702
  function requestToken(host, path) {
55075
55703
  const profileDir2 = process.env[PROFILE_DIR_ENV];
@@ -55084,7 +55712,7 @@ function requestToken(host, path) {
55084
55712
  socket.destroy();
55085
55713
  resolve13(value);
55086
55714
  };
55087
- const socket = connect4(join45(profileDir2, CONTROL_SOCKET_FILE));
55715
+ const socket = connect4(join46(profileDir2, CONTROL_SOCKET_FILE));
55088
55716
  socket.setEncoding("utf8");
55089
55717
  const timer = setTimeout(() => finish(null), REQUEST_TIMEOUT_MS);
55090
55718
  socket.on("connect", () => {
@@ -55125,7 +55753,8 @@ async function runGitCredential(action) {
55125
55753
  const host = (input.host ?? "").toLowerCase();
55126
55754
  if (input.protocol !== "https" || !host || host !== (process.env[GIT_HOST_ENV] ?? "").toLowerCase()) return;
55127
55755
  const token = await requestToken(host, input.path ?? "");
55128
- if (token) process.stdout.write(`username=x-access-token
55756
+ const username = (process.env[GIT_HOST_KIND_ENV] ?? "") === "gitlab" ? "oauth2" : "x-access-token";
55757
+ if (token) process.stdout.write(`username=${username}
55129
55758
  password=${token}
55130
55759
  `);
55131
55760
  }
@@ -55138,12 +55767,12 @@ async function runGitToken() {
55138
55767
  }
55139
55768
 
55140
55769
  // src/commands/ai-login.ts
55141
- import { spawn as spawn12 } from "child_process";
55770
+ import { spawn as spawn13 } from "child_process";
55142
55771
  import { mkdirSync as mkdirSync10 } from "fs";
55143
55772
  import { basename as basename8 } from "path";
55144
55773
  function runProviderLogin(cmd, envVar, dir2) {
55145
55774
  return new Promise((resolve13) => {
55146
- const child = spawn12(cmd, [], {
55775
+ const child = spawn13(cmd, [], {
55147
55776
  stdio: "inherit",
55148
55777
  env: { ...process.env, [envVar]: dir2 }
55149
55778
  });
@@ -55188,7 +55817,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
55188
55817
  }
55189
55818
 
55190
55819
  // src/commands/unlink.ts
55191
- import { rmSync as rmSync11 } from "fs";
55820
+ import { rmSync as rmSync12 } from "fs";
55192
55821
  async function runUnlink(profileDir2, profileName) {
55193
55822
  initI18n(readProfileConfig(profileDir2).locale);
55194
55823
  const cred = readCredential(profileDir2, profileName);
@@ -55202,7 +55831,7 @@ async function runUnlink(profileDir2, profileName) {
55202
55831
  } catch (err) {
55203
55832
  console.log(t("unlink.revokeFailed", { message: err.message }));
55204
55833
  }
55205
- rmSync11(profileDir2, { recursive: true, force: true });
55834
+ rmSync12(profileDir2, { recursive: true, force: true });
55206
55835
  clearDefaultProfileIf(profileName);
55207
55836
  console.log(t("unlink.deleted"));
55208
55837
  }