@dsdenes/devboard 0.1.560 → 0.1.563
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.
- package/AGENTS.md +59 -0
- package/README.md +2497 -26
- package/bin/devboard.mjs +352 -118
- package/client/App.svelte +523 -0
- package/client/FeedbackOverlay.svelte +1125 -0
- package/client/FlowMode.svelte +1015 -0
- package/client/app.css +3922 -0
- package/client/env.d.ts +23 -0
- package/client/lib/commands.ts +836 -0
- package/client/lib/components/ActivityBar.svelte +295 -0
- package/client/lib/components/AdrTable.svelte +241 -0
- package/client/lib/components/CommandPalette.svelte +198 -0
- package/client/lib/components/ConceptKind.svelte +66 -0
- package/client/lib/components/ConfirmDialog.svelte +62 -0
- package/client/lib/components/DataTable.svelte +243 -0
- package/client/lib/components/DetailSplit.svelte +68 -0
- package/client/lib/components/DiscoverOverview.svelte +898 -0
- package/client/lib/components/DiscoverWorkspace.svelte +437 -0
- package/client/lib/components/DraftsPage.svelte +186 -0
- package/client/lib/components/GoalWorkspace.svelte +1042 -0
- package/client/lib/components/IssuesAgentFocus.svelte +117 -0
- package/client/lib/components/IssuesAgentLog.svelte +50 -0
- package/client/lib/components/IssuesQuestion.svelte +95 -0
- package/client/lib/components/IssuesWorkspace.svelte +577 -0
- package/client/lib/components/MermaidDiagram.svelte +89 -0
- package/client/lib/components/MonacoEditor.svelte +190 -0
- package/client/lib/components/NotificationRegion.svelte +195 -0
- package/client/lib/components/PaneHandle.svelte +25 -0
- package/client/lib/components/ScoutIssues.svelte +215 -0
- package/client/lib/components/ScoutWorkspace.svelte +180 -0
- package/client/lib/components/Sidebar.svelte +622 -0
- package/client/lib/components/StatusBar.svelte +118 -0
- package/client/lib/components/Terminal.svelte +193 -0
- package/client/lib/components/WatchdogWorkspace.svelte +537 -0
- package/client/lib/components/ui/button.svelte +56 -0
- package/client/lib/components/ui/icon-button.svelte +56 -0
- package/client/lib/components/ui/input.svelte +19 -0
- package/client/lib/components/ui/page-header.svelte +120 -0
- package/client/lib/components/ui/stage-track.svelte +141 -0
- package/client/lib/components/watchdog-indicator.svelte +189 -0
- package/client/lib/connection.svelte.ts +210 -0
- package/client/lib/dependency-diagram.test.ts +90 -0
- package/client/lib/dependency-diagram.ts +75 -0
- package/client/lib/discover-format.test.ts +265 -0
- package/client/lib/discover-format.ts +394 -0
- package/client/lib/discover.svelte.ts +412 -0
- package/client/lib/draft-format.test.ts +48 -0
- package/client/lib/draft-format.ts +13 -0
- package/client/lib/flow-save.test.ts +148 -0
- package/client/lib/flow.svelte.ts +445 -0
- package/client/lib/goal-reconnect.test.ts +282 -0
- package/client/lib/goal.svelte.ts +652 -0
- package/client/lib/issues.svelte.ts +439 -0
- package/client/lib/layout.svelte.ts +234 -0
- package/client/lib/notification-issue.test.ts +53 -0
- package/client/lib/notification-issue.ts +27 -0
- package/client/lib/notifications.svelte.ts +62 -0
- package/client/lib/poll-status.test.ts +141 -0
- package/client/lib/poll-status.ts +88 -0
- package/client/lib/record-status.test.ts +19 -0
- package/client/lib/record-status.ts +25 -0
- package/client/lib/scout.svelte.ts +164 -0
- package/client/lib/skill-document.test.ts +77 -0
- package/client/lib/skill-document.ts +57 -0
- package/client/lib/status-tone.test.ts +40 -0
- package/client/lib/status-tone.ts +33 -0
- package/client/lib/types.ts +578 -0
- package/client/lib/utils.ts +6 -0
- package/client/lib/viewport.svelte.ts +20 -0
- package/client/lib/watchdog.svelte.ts +69 -0
- package/client/main.ts +7 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
- package/dist/assets/arc-BZgCSX2H.js +1 -0
- package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
- package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
- package/dist/assets/channel-CgM4lcUW.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
- package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
- package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
- package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
- package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
- package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
- package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
- package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
- package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
- package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
- package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
- package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-D7JM2Yhv.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
- package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
- package/dist/assets/dist-Dce-2YbA.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
- package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
- package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
- package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
- package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-ByQ-XX38.css +1 -0
- package/dist/assets/index-CIf5TpfY.js +214 -0
- package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
- package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
- package/dist/assets/katex-DJhrJSbw.js +257 -0
- package/dist/assets/line-N9XiCREw.js +1 -0
- package/dist/assets/linear-C9hnqmJv.js +1 -0
- package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
- package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
- package/dist/assets/monaco-DjpsE8to.css +1 -0
- package/dist/assets/monaco-DqzJaNIe.js +908 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
- package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
- package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
- package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
- package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-CGp3bfio.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
- package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
- package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
- package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/assets/xterm-C8zLTNGk.js +16 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +6 -0
- package/dist/index.html +34 -0
- package/extensions/implement-goal-issue.ts +368 -0
- package/extensions/implement-next-epic.ts +835 -0
- package/index.html +28 -0
- package/install.sh +111 -0
- package/issues/control.ts +91 -0
- package/issues/guards.ts +12 -0
- package/issues/ledger.ts +122 -0
- package/issues/legacy.ts +68 -0
- package/issues/lock.ts +305 -0
- package/issues/paths.ts +76 -0
- package/issues/preflight.ts +189 -0
- package/issues/repository.ts +203 -0
- package/issues/scout/actions.ts +255 -0
- package/issues/scout/control.ts +80 -0
- package/issues/scout/github.ts +358 -0
- package/issues/scout/paths.ts +129 -0
- package/issues/scout/preflight.ts +277 -0
- package/issues/scout/recommendation.ts +400 -0
- package/issues/scout/scan.ts +143 -0
- package/issues/scout/state.ts +448 -0
- package/issues/scout/worker.ts +1282 -0
- package/issues/state.ts +584 -0
- package/issues/tracker.ts +271 -0
- package/issues/worker.ts +1065 -0
- package/lib/dependencies.ts +61 -0
- package/lib/forge-auth.ts +953 -0
- package/lib/forge.ts +141 -0
- package/lib/gitea.ts +729 -0
- package/lib/github-release.ts +318 -0
- package/lib/inventory.test.ts +79 -0
- package/lib/inventory.ts +53 -0
- package/lib/release.test.ts +269 -0
- package/lib/release.ts +801 -0
- package/omp/commands/adr-resolve.md +8 -0
- package/omp/commands/discover.md +6 -0
- package/omp/commands/epic-next.md +8 -0
- package/omp/commands/flow-change.md +8 -0
- package/omp/commands/flow-goal.md +8 -0
- package/omp/commands/flow-implement.md +8 -0
- package/omp/commands/flow-widget.md +8 -0
- package/omp/commands/goal-draft.md +8 -0
- package/omp/commands/goal-outcome.md +8 -0
- package/omp/commands/implement-issue.md +8 -0
- package/omp/commands/prepare-skills.md +6 -0
- package/omp/commands/scout-recommendations.md +8 -0
- package/omp/commands/setup-devboard.md +24 -0
- package/omp/config.yml +12 -0
- package/omp/extensions/implement-goal-issue.ts +2 -0
- package/omp/extensions/implement-next-epic.ts +2 -0
- package/omp/mcp.json +9 -0
- package/package.json +86 -7
- package/prompts/adr-resolve.md +130 -0
- package/prompts/discover.md +541 -0
- package/prompts/epic-next.md +533 -0
- package/prompts/flow-change.md +76 -0
- package/prompts/flow-goal.md +126 -0
- package/prompts/flow-implement.md +103 -0
- package/prompts/flow-widget.md +54 -0
- package/prompts/goal-draft.md +147 -0
- package/prompts/goal-outcome.md +87 -0
- package/prompts/implement-issue.md +66 -0
- package/prompts/prepare-skills.md +142 -0
- package/prompts/scout-recommendations.md +143 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/server/acp.ts +540 -0
- package/server/activity.test.ts +96 -0
- package/server/activity.ts +48 -0
- package/server/app.test.ts +882 -0
- package/server/app.ts +6179 -0
- package/server/ask.test.ts +543 -0
- package/server/ask.ts +589 -0
- package/server/dependencies.test.ts +118 -0
- package/server/discover-remote.test.ts +1602 -0
- package/server/discover-sheet.test.ts +176 -0
- package/server/discover.test.ts +1510 -0
- package/server/discover.ts +3198 -0
- package/server/feedback.test.ts +458 -0
- package/server/feedback.ts +369 -0
- package/server/fixtures/adr-source.ts +71 -0
- package/server/fixtures/hang-download.ts +14 -0
- package/server/fixtures/stub-agent.ts +1870 -0
- package/server/fixtures/stub-forge.ts +141 -0
- package/server/fixtures/stub-gh.ts +57 -0
- package/server/flow-pr.test.ts +976 -0
- package/server/flow.test.ts +1632 -0
- package/server/flow.ts +2655 -0
- package/server/forge-pr.ts +387 -0
- package/server/goal-creation.test.ts +957 -0
- package/server/goal-draft.test.ts +348 -0
- package/server/goal-drafts.ts +449 -0
- package/server/goal-proposal.test.ts +479 -0
- package/server/goal-proposal.ts +232 -0
- package/server/goal-revision.test.ts +613 -0
- package/server/goal-slug.test.ts +56 -0
- package/server/goal-slug.ts +96 -0
- package/server/goal.ts +20 -0
- package/server/index.ts +147 -0
- package/server/issue-close.test.ts +100 -0
- package/server/issue-close.ts +178 -0
- package/server/issues.test.ts +167 -0
- package/server/issues.ts +479 -0
- package/server/journal.test.ts +350 -0
- package/server/journal.ts +329 -0
- package/server/lifecycle-pause.test.ts +1795 -0
- package/server/lifecycle.test.ts +1879 -0
- package/server/parallel-lifecycle.test.ts +1029 -0
- package/server/persistence-restart.test.ts +723 -0
- package/server/persistence.test.ts +1935 -0
- package/server/records.test.ts +180 -0
- package/server/records.ts +99 -0
- package/server/reset-implementation.test.ts +997 -0
- package/server/run.test.ts +611 -0
- package/server/scout.test.ts +877 -0
- package/server/scout.ts +459 -0
- package/server/skills-preparation.test.ts +1073 -0
- package/server/state.ts +249 -0
- package/server/tracker.ts +369 -0
- package/server/updates.test.ts +444 -0
- package/server/updates.ts +198 -0
- package/server/version.test.ts +140 -0
- package/server/version.ts +151 -0
- package/server/watchdog.ts +389 -0
- package/svelte.config.js +5 -0
- package/templates/host-AGENTS.md +235 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +21 -0
- package/watchdog/cleanup-main.ts +98 -0
- package/watchdog/cleanup.ts +301 -0
- package/watchdog/collect.ts +311 -0
- package/watchdog/coordinator.ts +397 -0
- package/watchdog/delivery.ts +107 -0
- package/watchdog/discovery.ts +209 -0
- package/watchdog/findings.ts +211 -0
- package/watchdog/github.ts +189 -0
- package/watchdog/history.ts +138 -0
- package/watchdog/install.ts +103 -0
- package/watchdog/ledger.ts +188 -0
- package/watchdog/lock.ts +63 -0
- package/watchdog/main.ts +106 -0
- package/watchdog/status.ts +71 -0
- package/watchdog/systemd.ts +96 -0
- package/watchdog/uninstall.ts +30 -0
- package/watchdog/units/devboard-watchdog.service +9 -0
- package/watchdog/units/devboard-watchdog.timer +10 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposed-goal drafts for the local interface.
|
|
3
|
+
*
|
|
4
|
+
* The operator's report panel can turn one pick and one sentence into a raw
|
|
5
|
+
* goal draft. A draft is not a goal spec: it carries the operator's text
|
|
6
|
+
* verbatim, the Ask conversation when the panel has one, plus the same
|
|
7
|
+
* captured element context an issue report carries,
|
|
8
|
+
* and lives under `.devboard/goal-proposals/` until someone elaborates it with
|
|
9
|
+
* `/goal-spec`. The server never files an issue, starts a conversation, or
|
|
10
|
+
* writes a final goal.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
FeedbackRequestError,
|
|
17
|
+
conversationSection,
|
|
18
|
+
parseCapturedFields,
|
|
19
|
+
parseConversation,
|
|
20
|
+
parseReportText,
|
|
21
|
+
type CapturedFields,
|
|
22
|
+
type FeedbackTurn,
|
|
23
|
+
} from "./feedback";
|
|
24
|
+
|
|
25
|
+
/** Repository-relative directory holding every draft. */
|
|
26
|
+
export const GOAL_PROPOSALS_DIRECTORY = ".devboard/goal-proposals";
|
|
27
|
+
|
|
28
|
+
const MAX_HEADING = 200;
|
|
29
|
+
const MAX_NAME_WORDS = 6;
|
|
30
|
+
/** The bounds a draft's base name must respect, whether chosen or derived. */
|
|
31
|
+
const MAX_SLUG_WORDS = MAX_NAME_WORDS;
|
|
32
|
+
const MAX_SLUG_LENGTH = 64;
|
|
33
|
+
const FALLBACK_NAME = "ui-goal";
|
|
34
|
+
const BODY_VISIBLE_TEXT = 1500;
|
|
35
|
+
/** Bounds the `-2`, `-3`, … search; far past any real click count. */
|
|
36
|
+
const MAX_NAME_ATTEMPTS = 1000;
|
|
37
|
+
|
|
38
|
+
export interface GoalProposalSubmission extends CapturedFields {
|
|
39
|
+
report: string;
|
|
40
|
+
/** The Ask conversation the operator had about this element, if any. */
|
|
41
|
+
conversation: FeedbackTurn[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GoalProposal {
|
|
45
|
+
/** Repository-relative path of the written draft, with `/` separators. */
|
|
46
|
+
path: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Validates one proposal body: the same fields and limits as a report. */
|
|
50
|
+
export function parseGoalProposalSubmission(value: unknown): GoalProposalSubmission {
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
52
|
+
throw new FeedbackRequestError(400, "The proposal must be a JSON object.");
|
|
53
|
+
}
|
|
54
|
+
const source = value as Record<string, unknown>;
|
|
55
|
+
return {
|
|
56
|
+
report: parseReportText(source.report),
|
|
57
|
+
conversation: parseConversation(source.conversation),
|
|
58
|
+
...parseCapturedFields(source),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The report's first non-empty line, trimmed, or the empty string. */
|
|
63
|
+
function firstReportLine(report: string): string {
|
|
64
|
+
return (
|
|
65
|
+
report
|
|
66
|
+
.split("\n")
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.find((line) => line.length > 0) ?? ""
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The heading text: one line, whitespace collapsed, bounded. */
|
|
73
|
+
export function goalProposalHeading(report: string): string {
|
|
74
|
+
const line = firstReportLine(report).replace(/\s+/g, " ");
|
|
75
|
+
return line.length <= MAX_HEADING ? line : line.slice(0, MAX_HEADING);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The draft's deterministic fallback name: lowercase, hyphenated, at most six
|
|
80
|
+
* words and `MAX_SLUG_LENGTH` characters, so it is always usable as a file name.
|
|
81
|
+
*/
|
|
82
|
+
export function goalProposalName(report: string): string {
|
|
83
|
+
const slug = firstReportLine(report)
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
86
|
+
.replace(/^-+|-+$/g, "");
|
|
87
|
+
const name = slug
|
|
88
|
+
.split("-")
|
|
89
|
+
.filter((word) => word.length > 0)
|
|
90
|
+
.slice(0, MAX_NAME_WORDS)
|
|
91
|
+
.join("-");
|
|
92
|
+
if (name.length === 0) return FALLBACK_NAME;
|
|
93
|
+
if (name.length <= MAX_SLUG_LENGTH) return name;
|
|
94
|
+
return name.slice(0, MAX_SLUG_LENGTH).replace(/-+$/, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates one naming agent's answer. Every non-fence line is considered with
|
|
101
|
+
* surrounding backticks and quotes, an optional `slug:` label, a leading list
|
|
102
|
+
* marker, and trailing punctuation removed; the last line that is a complete
|
|
103
|
+
* lowercase kebab-case slug within the word and length bounds wins, so short
|
|
104
|
+
* framing like "Here is the slug:" does not discard a good answer. Returns
|
|
105
|
+
* `null` when no line qualifies, so a malformed answer falls back to the
|
|
106
|
+
* deterministic name instead of producing an unusable file name.
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeGoalSlug(value: string): string | null {
|
|
109
|
+
let slug: string | null = null;
|
|
110
|
+
for (const line of value.split("\n")) {
|
|
111
|
+
const entry = line.trim();
|
|
112
|
+
if (entry.length === 0 || entry.startsWith("```")) continue;
|
|
113
|
+
const candidate = entry
|
|
114
|
+
.replace(/^[-*]\s+/, "")
|
|
115
|
+
.replace(/^[`'"*_~]+/, "")
|
|
116
|
+
.replace(/[`'"*_~.,;:!?]+$/, "")
|
|
117
|
+
.replace(/^slug\s*[:=]\s*/i, "")
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
if (!SLUG_PATTERN.test(candidate)) continue;
|
|
121
|
+
if (candidate.length > MAX_SLUG_LENGTH) continue;
|
|
122
|
+
if (candidate.split("-").length > MAX_SLUG_WORDS) continue;
|
|
123
|
+
slug = candidate;
|
|
124
|
+
}
|
|
125
|
+
return slug;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Resolves the draft's base name from a naming agent, with a deterministic fallback. */
|
|
129
|
+
async function chooseGoalName(
|
|
130
|
+
report: string,
|
|
131
|
+
slugger: ((report: string) => Promise<string>) | undefined,
|
|
132
|
+
): Promise<string> {
|
|
133
|
+
if (slugger === undefined) return goalProposalName(report);
|
|
134
|
+
const suggestion = await slugger(report).catch(() => undefined);
|
|
135
|
+
return (suggestion === undefined ? null : normalizeGoalSlug(suggestion)) ?? goalProposalName(report);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collapse(value: string, limit: number): string {
|
|
139
|
+
const single = value.replace(/\s+/g, " ").trim();
|
|
140
|
+
if (single.length <= limit) return single;
|
|
141
|
+
return `${single.slice(0, limit - 1).trimEnd()}…`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The captured context block, rendered exactly like `feedbackBody`'s. */
|
|
145
|
+
function capturedContext(submission: GoalProposalSubmission): string[] {
|
|
146
|
+
const context: string[] = [];
|
|
147
|
+
for (const [label, value] of [
|
|
148
|
+
["Route", submission.route],
|
|
149
|
+
["URL", submission.url],
|
|
150
|
+
["Selected element", submission.selector],
|
|
151
|
+
["Viewport", submission.viewport],
|
|
152
|
+
["Captured at", submission.capturedAt],
|
|
153
|
+
] as const) {
|
|
154
|
+
// A backtick inside an inline code span would close it early.
|
|
155
|
+
if (value.trim().length > 0) {
|
|
156
|
+
context.push(`- ${label}: \`${value.trim().replace(/`/g, "'")}\``);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const visible = collapse(submission.visibleText, BODY_VISIBLE_TEXT);
|
|
161
|
+
if (visible.length > 0) {
|
|
162
|
+
context.push("", "### Visible text", "", visible);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const dom = submission.domExcerpt.trim();
|
|
166
|
+
if (dom.length > 0) {
|
|
167
|
+
const fence = dom.includes("```") ? "````" : "```";
|
|
168
|
+
context.push("", "### DOM excerpt", "", `${fence}html`, dom, fence);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return context.length === 0 ? ["No page context was captured."] : context;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The draft body: the proposed goal, the operator's text verbatim, the Ask
|
|
176
|
+
* conversation, then the captured context. It carries no notice line, success
|
|
177
|
+
* criteria, verification, boundaries, or stop conditions; elaboration does
|
|
178
|
+
* that.
|
|
179
|
+
*/
|
|
180
|
+
export function goalProposalBody(submission: GoalProposalSubmission): string {
|
|
181
|
+
const conversation = conversationSection(submission.conversation);
|
|
182
|
+
return [
|
|
183
|
+
`# Proposed goal: ${goalProposalHeading(submission.report)}`,
|
|
184
|
+
"",
|
|
185
|
+
submission.report,
|
|
186
|
+
"",
|
|
187
|
+
...(conversation.length === 0 ? [] : [...conversation, ""]),
|
|
188
|
+
"## Captured UI context",
|
|
189
|
+
"",
|
|
190
|
+
...capturedContext(submission),
|
|
191
|
+
"",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Writes the content to `name.md`, or the first free `name-2.md`, `name-3.md`,
|
|
197
|
+
* … The content is staged in a temporary file and hard-linked into place, so a
|
|
198
|
+
* failed write leaves no partial draft and an existing draft is never replaced.
|
|
199
|
+
*/
|
|
200
|
+
export async function createExclusive(directory: string, name: string, content: string): Promise<string> {
|
|
201
|
+
const temporary = join(directory, `.${name}.${crypto.randomUUID()}.tmp`);
|
|
202
|
+
try {
|
|
203
|
+
await writeFile(temporary, content, { flag: "wx" });
|
|
204
|
+
for (let index = 1; index <= MAX_NAME_ATTEMPTS; index++) {
|
|
205
|
+
const filename = index === 1 ? `${name}.md` : `${name}-${index}.md`;
|
|
206
|
+
try {
|
|
207
|
+
await link(temporary, join(directory, filename));
|
|
208
|
+
return filename;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`No free draft filename for ${name}.md.`);
|
|
214
|
+
} finally {
|
|
215
|
+
await unlink(temporary).catch(() => {});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Writes one draft under `<repoDir>/.devboard/goal-proposals/`. */
|
|
220
|
+
export async function writeGoalProposal(
|
|
221
|
+
submission: GoalProposalSubmission,
|
|
222
|
+
options: { repoDir: string; slugger?: (report: string) => Promise<string> },
|
|
223
|
+
): Promise<GoalProposal> {
|
|
224
|
+
const directory = join(options.repoDir, GOAL_PROPOSALS_DIRECTORY);
|
|
225
|
+
await mkdir(directory, { recursive: true });
|
|
226
|
+
const filename = await createExclusive(
|
|
227
|
+
directory,
|
|
228
|
+
await chooseGoalName(submission.report, options.slugger),
|
|
229
|
+
goalProposalBody(submission),
|
|
230
|
+
);
|
|
231
|
+
return { path: `${GOAL_PROPOSALS_DIRECTORY}/${filename}` };
|
|
232
|
+
}
|