@dsdenes/devboard 0.1.560 → 0.1.562
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 +709 -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,1870 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
13
|
+
import { frontmatterStatus, isPendingStatus, setFrontmatterStatus } from "./adr-source";
|
|
14
|
+
import { GOAL_SLUG_PROMPT_MARKER } from "../goal-slug";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A deterministic ACP peer for the interface's browser verification and tests.
|
|
18
|
+
*
|
|
19
|
+
* It answers the protocol handshake, streams a little activity, optionally
|
|
20
|
+
* interviews the developer, writes `status: accepted` into the frontmatter of
|
|
21
|
+
* the record named in the prompt, and ends the turn. With `--discover` it
|
|
22
|
+
* instead stages the sheet, glossary, and five Discover skills named by
|
|
23
|
+
* `/discover <staging-dir>`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
function argument(name: string): string | undefined {
|
|
27
|
+
const prefix = `--${name}=`;
|
|
28
|
+
return process.argv.slice(2).find((value) => value.startsWith(prefix))?.slice(prefix.length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function argumentAll(name: string): string[] {
|
|
32
|
+
const prefix = `--${name}=`;
|
|
33
|
+
return process.argv
|
|
34
|
+
.slice(2)
|
|
35
|
+
.filter((value) => value.startsWith(prefix))
|
|
36
|
+
.map((value) => value.slice(prefix.length));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Parses repeated `--mode=<id>:<value>` arguments into an id-keyed map. */
|
|
40
|
+
function idOptions(name: string): Map<string, string> {
|
|
41
|
+
const options = new Map<string, string>();
|
|
42
|
+
for (const entry of argumentAll(name)) {
|
|
43
|
+
const split = entry.indexOf(":");
|
|
44
|
+
if (split < 0) continue;
|
|
45
|
+
options.set(entry.slice(0, split), entry.slice(split + 1));
|
|
46
|
+
}
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Deterministic per-epic timing and one-line activity overrides. */
|
|
51
|
+
const laneDelays = idOptions("lane-delay");
|
|
52
|
+
const laneActivities = idOptions("lane-activity");
|
|
53
|
+
const blockerEpics = new Set(argumentAll("blocker-epic"));
|
|
54
|
+
const questionEpics = new Set(argumentAll("question-epic"));
|
|
55
|
+
/** JSON list of open epics the verification server's tracker reads. */
|
|
56
|
+
const fixtureTracker = argument("fixture-tracker");
|
|
57
|
+
/** Repository whose branches and `main` a fixture lane merges into. */
|
|
58
|
+
const fixtureRepo = argument("fixture-repo");
|
|
59
|
+
const noClose = process.argv.includes("--no-close");
|
|
60
|
+
const generatedEpics = argumentAll("generate-epic");
|
|
61
|
+
const generationBudget = Number(argument("generation-budget") ?? generatedEpics.length);
|
|
62
|
+
const laneDelayMs = Number(argument("lane-delay-ms") ?? 0);
|
|
63
|
+
const laneSettleMs = Number(argument("lane-settle-ms") ?? 0);
|
|
64
|
+
const logPath = argument("log");
|
|
65
|
+
const delayMs = Number(argument("delay-ms") ?? 0);
|
|
66
|
+
const forcedQuestions = argument("questions");
|
|
67
|
+
/** Omits the schema default so the fixture question has no recommended option. */
|
|
68
|
+
const questionsUnrecommended = process.argv.includes("--questions-unrecommended");
|
|
69
|
+
const reviseGoal = process.argv.includes("--revise-goal");
|
|
70
|
+
const createGoal = process.argv.includes("--create-goal");
|
|
71
|
+
const createGoalWrite = !process.argv.includes("--no-create-goal-write");
|
|
72
|
+
const goalFile = argument("goal");
|
|
73
|
+
const prepareSkills = process.argv.includes("--prepare-skills");
|
|
74
|
+
const prepareSkillsInvalid = process.argv.includes("--prepare-skills-invalid");
|
|
75
|
+
const prepareSkillsEmpty = process.argv.includes("--prepare-skills-empty");
|
|
76
|
+
const prepareSkillsConflict = process.argv.includes("--prepare-skills-conflict");
|
|
77
|
+
const prepareSkillsCollision = process.argv.includes("--prepare-skills-collision");
|
|
78
|
+
const prepareSkillsError = process.argv.includes("--prepare-skills-error");
|
|
79
|
+
const collisionName = argument("collision-name") ?? "custom-skill";
|
|
80
|
+
const resolveRecord = !process.argv.includes("--no-resolve");
|
|
81
|
+
const removeRecord = process.argv.includes("--remove-record");
|
|
82
|
+
const busyContinuations = Number(argument("busy-continuations") ?? 0);
|
|
83
|
+
let busyRemaining = busyContinuations;
|
|
84
|
+
|
|
85
|
+
/** Flow Mode: the goal, implementation, analyst, and widget-conversation turns. */
|
|
86
|
+
const flowGoal = process.argv.includes("--flow-goal");
|
|
87
|
+
const flowImplement = process.argv.includes("--flow-implement");
|
|
88
|
+
const flowChange = process.argv.includes("--flow-change");
|
|
89
|
+
const flowWidget = process.argv.includes("--flow-widget");
|
|
90
|
+
const flowGoalEmpty = process.argv.includes("--flow-goal-empty");
|
|
91
|
+
const flowGoalFail = process.argv.includes("--flow-goal-fail");
|
|
92
|
+
const flowGoalText =
|
|
93
|
+
argument("flow-goal-text") ?? "# Flow goal\n\nMake the fixture behavior observable.";
|
|
94
|
+
/** Overrides the goal text while this file exists, so turns can differ. */
|
|
95
|
+
const flowGoalTextFile = argument("flow-goal-text-file");
|
|
96
|
+
/** Stalls this long after writing goal.md, before the turn ends. */
|
|
97
|
+
const flowGoalSettleMs = Number(argument("flow-goal-settle") ?? 0);
|
|
98
|
+
/** While this file exists the interview stalls; `partial` first writes a draft. */
|
|
99
|
+
const flowGoalHang = argument("flow-goal-hang");
|
|
100
|
+
const flowGoalPartial = argument("flow-goal-partial");
|
|
101
|
+
const flowGoalDraft = argument("flow-goal-draft") ?? "# Flow goal\n\nAn unfinished draft";
|
|
102
|
+
const flowCheckpoints = Math.max(1, Number(argument("flow-checkpoints") ?? 1));
|
|
103
|
+
const flowStepDelay = Number(argument("flow-step-delay") ?? 0);
|
|
104
|
+
const flowBlocked = process.argv.includes("--flow-blocked");
|
|
105
|
+
const flowQuestion = process.argv.includes("--flow-question");
|
|
106
|
+
const flowVerification = argument("flow-verification") ?? "passed";
|
|
107
|
+
const flowAnalystPartial = process.argv.includes("--flow-analyst-partial");
|
|
108
|
+
const flowAnalystFail = process.argv.includes("--flow-analyst-fail");
|
|
109
|
+
const flowAnalystEmpty = process.argv.includes("--flow-analyst-empty");
|
|
110
|
+
const flowAnalystForeign = process.argv.includes("--flow-analyst-foreign");
|
|
111
|
+
const flowSilentCheckpoints = process.argv.includes("--flow-silent-checkpoints");
|
|
112
|
+
const flowPreCommitDelay = Number(argument("flow-pre-commit-delay") ?? 0);
|
|
113
|
+
/** File this stub waits for before its second and later checkpoints. */
|
|
114
|
+
const flowWaitFile = argument("flow-wait-file");
|
|
115
|
+
const flowNoVerification = process.argv.includes("--flow-no-verification");
|
|
116
|
+
|
|
117
|
+
/** Discover Mode: the crawl staging turn and its validation-failure variants. */
|
|
118
|
+
const discover = process.argv.includes("--discover");
|
|
119
|
+
const discoverQuestion = process.argv.includes("--discover-question");
|
|
120
|
+
const discoverProse =
|
|
121
|
+
argument("discover-prose") ??
|
|
122
|
+
"A fixture host with a small order domain used to verify the Discover crawl";
|
|
123
|
+
const discoverInvalidSheet = process.argv.includes("--discover-invalid-sheet");
|
|
124
|
+
const discoverSheetAbsolute = process.argv.includes("--discover-sheet-absolute");
|
|
125
|
+
const discoverSheetNoEvidence = process.argv.includes("--discover-sheet-no-evidence");
|
|
126
|
+
const discoverSheetSecret = process.argv.includes("--discover-sheet-secret");
|
|
127
|
+
const discoverSheetTwoSentences = process.argv.includes("--discover-sheet-two-sentences");
|
|
128
|
+
/** Stages a level without `happyPath`, the shape an older prompt wrote. */
|
|
129
|
+
const discoverSheetNoHappyPath = process.argv.includes("--discover-sheet-no-happy-path");
|
|
130
|
+
/** Stages a happy-path count larger than the level's test count. */
|
|
131
|
+
const discoverSheetHappyOverflow = process.argv.includes("--discover-sheet-happy-overflow");
|
|
132
|
+
const discoverGlossaryInvalid = process.argv.includes("--discover-glossary-invalid");
|
|
133
|
+
const discoverGlossaryGrounding = process.argv.includes("--discover-glossary-grounding");
|
|
134
|
+
/** Omits a concept from the staged glossary, so a published one is preserved unscored. */
|
|
135
|
+
const discoverGlossaryDrop = process.argv.includes("--discover-glossary-drop");
|
|
136
|
+
/** The staged significance sidecar defect, or "" for a valid sidecar. */
|
|
137
|
+
const discoverSignificance = argument("discover-significance") ?? "";
|
|
138
|
+
const discoverMissingArtifact = process.argv.includes("--discover-missing-artifact");
|
|
139
|
+
const discoverExtraArtifact = process.argv.includes("--discover-extra-artifact");
|
|
140
|
+
const discoverWriteOutside = process.argv.includes("--discover-write-outside");
|
|
141
|
+
const discoverError = process.argv.includes("--discover-error");
|
|
142
|
+
const discoverNoExit = process.argv.includes("--discover-no-exit");
|
|
143
|
+
|
|
144
|
+
/** Ask mode: a read-only answer, preceded by thought and tool noise. */
|
|
145
|
+
const ask = process.argv.includes("--ask");
|
|
146
|
+
const askDelayMs = Number(argument("ask-delay-ms") ?? 0);
|
|
147
|
+
const askFail = process.argv.includes("--ask-fail");
|
|
148
|
+
const askExit = process.argv.includes("--ask-exit");
|
|
149
|
+
/** Refuses the ACP initialize of the first Ask peer; later peers succeed. */
|
|
150
|
+
const askRefuseInitialize = process.argv.includes("--ask-refuse-initialize");
|
|
151
|
+
/** Answers the first session/new without a session id; later peers succeed. */
|
|
152
|
+
const askNoSession = process.argv.includes("--ask-no-session");
|
|
153
|
+
if (ask) log(`ask-start ${process.pid}`);
|
|
154
|
+
|
|
155
|
+
/** True once per flag, so only the first peer fails that handshake step. */
|
|
156
|
+
function firstAskHandshake(flag: string): boolean {
|
|
157
|
+
if (logPath === undefined) return false;
|
|
158
|
+
const marker = `${logPath}.${flag}`;
|
|
159
|
+
if (existsSync(marker)) return false;
|
|
160
|
+
writeFileSync(marker, "");
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Draft elaboration mode: simulate the goal-draft command's observable work. */
|
|
165
|
+
const goalDraft = process.argv.includes("--goal-draft");
|
|
166
|
+
const goalDraftFinish = argument("goal-draft-finish");
|
|
167
|
+
/** Draft naming mode: answer a naming turn with this text, verbatim. */
|
|
168
|
+
const goalSlug = argument("goal-slug");
|
|
169
|
+
const ASK_INSTRUCTION = "This conversation is read-only.";
|
|
170
|
+
const ASK_ANSWER_ONE =
|
|
171
|
+
"The issues indicator shows the lifecycle the detached worker published for this repository. " +
|
|
172
|
+
"It turns amber while a run is working and shows failed when the last run failed. " +
|
|
173
|
+
"It sits in the status bar and opens the Issues mode.";
|
|
174
|
+
const ASK_ANSWER_TWO =
|
|
175
|
+
"The status bar renders that label in StatusBar.svelte from the issues snapshot the server sends. " +
|
|
176
|
+
"The count is the open issues the tracker returned for the host repository. " +
|
|
177
|
+
"No other status item opens a mode.";
|
|
178
|
+
let askTurns = 0;
|
|
179
|
+
const DISCOVER_SECRET = "ghp_0123456789abcdefghijklmnopqrstuvwxyz";
|
|
180
|
+
const CREATED_GOAL = "# Created goal\n\nThe agent wrote this.";
|
|
181
|
+
const SETTLE_DELAY_MS = 400;
|
|
182
|
+
|
|
183
|
+
function log(line: string) {
|
|
184
|
+
if (logPath !== undefined) appendFileSync(logPath, `${line}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function write(message: unknown) {
|
|
188
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
189
|
+
process.stdout.write(line);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const pending = new Map<string, (result: unknown) => void>();
|
|
193
|
+
let nextRequestId = 1;
|
|
194
|
+
/** One prompt turn at a time, so a queued message answers in order. */
|
|
195
|
+
let promptChain: Promise<void> = Promise.resolve();
|
|
196
|
+
|
|
197
|
+
function request(method: string, params: unknown): Promise<unknown> {
|
|
198
|
+
const id = nextRequestId++;
|
|
199
|
+
const { promise, resolve } = Promise.withResolvers<unknown>();
|
|
200
|
+
pending.set(String(id), resolve);
|
|
201
|
+
write({ jsonrpc: "2.0", id, method, params });
|
|
202
|
+
return promise;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pendingCount(directory: string): number {
|
|
206
|
+
try {
|
|
207
|
+
return readdirSync(directory)
|
|
208
|
+
.filter((file) => file.endsWith(".md"))
|
|
209
|
+
.filter((file) => isPendingStatus(frontmatterStatus(readFileSync(join(directory, file), "utf8"))))
|
|
210
|
+
.length;
|
|
211
|
+
} catch {
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function markAccepted(file: string) {
|
|
217
|
+
const source = readFileSync(file, "utf8");
|
|
218
|
+
if (frontmatterStatus(source) === undefined) return;
|
|
219
|
+
writeFileSync(file, setFrontmatterStatus(source, "accepted"));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface FixtureEpic {
|
|
223
|
+
number: number;
|
|
224
|
+
title: string;
|
|
225
|
+
url: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readFixtureTracker(path: string): FixtureEpic[] {
|
|
229
|
+
try {
|
|
230
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
231
|
+
if (!Array.isArray(parsed)) return [];
|
|
232
|
+
return parsed.filter(
|
|
233
|
+
(entry): entry is FixtureEpic =>
|
|
234
|
+
typeof entry === "object" &&
|
|
235
|
+
entry !== null &&
|
|
236
|
+
"number" in entry &&
|
|
237
|
+
typeof entry.number === "number",
|
|
238
|
+
);
|
|
239
|
+
} catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeFixtureTracker(path: string, list: FixtureEpic[]) {
|
|
245
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
246
|
+
writeFileSync(temporary, `${JSON.stringify(list, null, 2)}\n`);
|
|
247
|
+
renameSync(temporary, path);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Serializes the stub's own tracker and merge writes across lane processes. */
|
|
251
|
+
function withLock<T>(lockPath: string, action: () => T): T {
|
|
252
|
+
const started = Date.now();
|
|
253
|
+
for (;;) {
|
|
254
|
+
try {
|
|
255
|
+
mkdirSync(lockPath);
|
|
256
|
+
break;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
259
|
+
if (Date.now() - started > 10_000) {
|
|
260
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
return action();
|
|
268
|
+
} finally {
|
|
269
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function closeFixtureEpic(id: string) {
|
|
274
|
+
if (fixtureTracker === undefined) return;
|
|
275
|
+
withLock(`${fixtureTracker}.lock`, () => {
|
|
276
|
+
writeFixtureTracker(
|
|
277
|
+
fixtureTracker,
|
|
278
|
+
readFixtureTracker(fixtureTracker).filter((epic) => String(epic.number) !== id),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
log(`closed ${id}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function fileFixtureEpic(entry: string): boolean {
|
|
285
|
+
if (fixtureTracker === undefined) return false;
|
|
286
|
+
const split = entry.indexOf(":");
|
|
287
|
+
if (split < 0) return false;
|
|
288
|
+
const number = Number(entry.slice(0, split));
|
|
289
|
+
const title = entry.slice(split + 1);
|
|
290
|
+
if (!Number.isInteger(number)) return false;
|
|
291
|
+
|
|
292
|
+
let filed = false;
|
|
293
|
+
withLock(`${fixtureTracker}.lock`, () => {
|
|
294
|
+
const counterPath = `${fixtureTracker}.generation-count`;
|
|
295
|
+
let count = 0;
|
|
296
|
+
try {
|
|
297
|
+
count = Number(readFileSync(counterPath, "utf8").trim());
|
|
298
|
+
} catch {
|
|
299
|
+
count = 0;
|
|
300
|
+
}
|
|
301
|
+
if (Number.isFinite(count) && count >= generationBudget) return;
|
|
302
|
+
const list = readFixtureTracker(fixtureTracker);
|
|
303
|
+
if (list.some((epic) => epic.number === number)) return;
|
|
304
|
+
list.push({ number, title, url: `https://fixture.test/issues/${number}` });
|
|
305
|
+
writeFixtureTracker(fixtureTracker, list);
|
|
306
|
+
writeFileSync(counterPath, `${count + 1}\n`);
|
|
307
|
+
filed = true;
|
|
308
|
+
});
|
|
309
|
+
if (filed) log(`filed ${number}`);
|
|
310
|
+
return filed;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function gitTeardown(repo: string, args: string[]): { code: number; stderr: string } {
|
|
314
|
+
const result = Bun.spawnSync({
|
|
315
|
+
cmd: ["git", "-C", repo, ...args],
|
|
316
|
+
stdout: "pipe",
|
|
317
|
+
stderr: "pipe",
|
|
318
|
+
});
|
|
319
|
+
return { code: result.exitCode ?? -1, stderr: result.stderr.toString() };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Simulates the peer's own merge: branch, sentinel commit, merge, cleanup. */
|
|
323
|
+
function mergeFixtureLane(id: string) {
|
|
324
|
+
if (fixtureRepo === undefined) return;
|
|
325
|
+
const worktree = join(fixtureRepo, ".worktree", `stub-epic-${id}`);
|
|
326
|
+
const branch = `devboard/stub-epic-${id}`;
|
|
327
|
+
mkdirSync(join(fixtureRepo, ".worktree"), { recursive: true });
|
|
328
|
+
gitTeardown(fixtureRepo, ["worktree", "remove", "--force", worktree]);
|
|
329
|
+
gitTeardown(fixtureRepo, ["branch", "-D", branch]);
|
|
330
|
+
const added = gitTeardown(fixtureRepo, ["worktree", "add", worktree, "-b", branch, "main"]);
|
|
331
|
+
if (added.code !== 0) {
|
|
332
|
+
log(`merge-failed ${id} worktree ${added.stderr.trim()}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
writeFileSync(join(worktree, `epic-${id}.txt`), `epic ${id}\n`);
|
|
336
|
+
gitTeardown(worktree, ["add", "-A"]);
|
|
337
|
+
gitTeardown(worktree, ["commit", "-m", `feat: epic ${id}`]);
|
|
338
|
+
|
|
339
|
+
withLock(join(fixtureRepo, ".git", "stub-merge.lock"), () => {
|
|
340
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
341
|
+
const merged = gitTeardown(fixtureRepo, [
|
|
342
|
+
"merge",
|
|
343
|
+
"--no-ff",
|
|
344
|
+
branch,
|
|
345
|
+
"-m",
|
|
346
|
+
`merge epic ${id}`,
|
|
347
|
+
]);
|
|
348
|
+
if (merged.code === 0) {
|
|
349
|
+
log(`merged ${id}`);
|
|
350
|
+
const pushed = gitTeardown(fixtureRepo, ["push", "origin", "main"]);
|
|
351
|
+
if (pushed.code === 0) log(`pushed ${id}`);
|
|
352
|
+
else log(`push-failed ${id}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
356
|
+
}
|
|
357
|
+
log(`merge-failed ${id}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
gitTeardown(fixtureRepo, ["worktree", "remove", "--force", worktree]);
|
|
361
|
+
gitTeardown(fixtureRepo, ["branch", "-D", branch]);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function implementationEpic(text: string): string | undefined {
|
|
365
|
+
const match = /^\/implement-next-epic(?:\s+(\d+))?/.exec(text);
|
|
366
|
+
if (match === null) return undefined;
|
|
367
|
+
if (match[1] !== undefined) return match[1];
|
|
368
|
+
const open = fixtureTracker === undefined ? [] : readFixtureTracker(fixtureTracker);
|
|
369
|
+
return open.length === 1 && open[0] !== undefined ? String(open[0].number) : undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function runImplementationLane(id: string) {
|
|
373
|
+
const activity = laneActivities.get(id) ?? `Working on epic ${id}`;
|
|
374
|
+
write({
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
method: "session/update",
|
|
377
|
+
params: {
|
|
378
|
+
sessionId: "stub-session",
|
|
379
|
+
update: {
|
|
380
|
+
sessionUpdate: "agent_message_chunk",
|
|
381
|
+
content: { type: "text", text: activity },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const override = laneDelays.get(id);
|
|
387
|
+
const delay = override !== undefined ? Number(override) : laneDelayMs > 0 ? laneDelayMs : delayMs;
|
|
388
|
+
if (Number.isFinite(delay) && delay > 0) await Bun.sleep(delay);
|
|
389
|
+
|
|
390
|
+
const questionCount = questionEpics.has(id)
|
|
391
|
+
? 1
|
|
392
|
+
: forcedQuestions !== undefined
|
|
393
|
+
? Number(forcedQuestions)
|
|
394
|
+
: 0;
|
|
395
|
+
if (questionCount > 0) {
|
|
396
|
+
await askQuestions(questionCount);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (blockerEpics.has(id)) {
|
|
400
|
+
const message = `DEVBOARD-BLOCKED: fixture blocker for epic ${id}`;
|
|
401
|
+
log(`blocked ${id}`);
|
|
402
|
+
write({
|
|
403
|
+
jsonrpc: "2.0",
|
|
404
|
+
method: "session/update",
|
|
405
|
+
params: {
|
|
406
|
+
sessionId: "stub-session",
|
|
407
|
+
update: {
|
|
408
|
+
sessionUpdate: "agent_message_chunk",
|
|
409
|
+
content: { type: "text", text: message },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (noClose) return;
|
|
417
|
+
if (laneSettleMs > 0) await Bun.sleep(laneSettleMs);
|
|
418
|
+
mergeFixtureLane(id);
|
|
419
|
+
closeFixtureEpic(id);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function runGenerationLane() {
|
|
423
|
+
write({
|
|
424
|
+
jsonrpc: "2.0",
|
|
425
|
+
method: "session/update",
|
|
426
|
+
params: {
|
|
427
|
+
sessionId: "stub-session",
|
|
428
|
+
update: {
|
|
429
|
+
sessionUpdate: "agent_message_chunk",
|
|
430
|
+
content: { type: "text", text: "Choosing the next epics" },
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
if (delayMs > 0) await Bun.sleep(delayMs);
|
|
435
|
+
for (const entry of generatedEpics) {
|
|
436
|
+
if (fileFixtureEpic(entry)) {
|
|
437
|
+
const number = entry.slice(0, entry.indexOf(":"));
|
|
438
|
+
write({
|
|
439
|
+
jsonrpc: "2.0",
|
|
440
|
+
method: "session/update",
|
|
441
|
+
params: {
|
|
442
|
+
sessionId: "stub-session",
|
|
443
|
+
update: {
|
|
444
|
+
sessionUpdate: "agent_message_chunk",
|
|
445
|
+
content: { type: "text", text: `Filed epic ${number}` },
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (laneSettleMs > 0) await Bun.sleep(laneSettleMs);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function writeSkill(
|
|
455
|
+
name: string,
|
|
456
|
+
description: string,
|
|
457
|
+
body: string,
|
|
458
|
+
header = `name: ${name}\ndescription: ${description}`,
|
|
459
|
+
) {
|
|
460
|
+
const directory = join(process.cwd(), ".omp", "skills", name);
|
|
461
|
+
mkdirSync(directory, { recursive: true });
|
|
462
|
+
writeFileSync(
|
|
463
|
+
join(directory, "SKILL.md"),
|
|
464
|
+
`---\n${header}\nmetadata:\n devboard-managed: "true"\n---\n\n${body}\n`,
|
|
465
|
+
);
|
|
466
|
+
log(`skill ${name}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function writePreparedSkills() {
|
|
470
|
+
if (prepareSkillsEmpty) return;
|
|
471
|
+
|
|
472
|
+
writeSkill(
|
|
473
|
+
"dependency-direction",
|
|
474
|
+
"Choose dependency direction and boundary ownership for any layered design.",
|
|
475
|
+
"# Dependency direction\n\n- Always declare dependencies in one direction.\n- Keep policy separate from delivery and storage concerns.\n",
|
|
476
|
+
);
|
|
477
|
+
writeSkill(
|
|
478
|
+
"cohesive-units",
|
|
479
|
+
"Organize units of code so each owns one cohesive responsibility.",
|
|
480
|
+
"# Cohesive units\n\n- Prefer small units with one responsibility.\n- Keep each public interface narrow and explicit.\n",
|
|
481
|
+
);
|
|
482
|
+
writeSkill(
|
|
483
|
+
"behavior-first-testing",
|
|
484
|
+
"Verify observable behavior with focused automated tests.",
|
|
485
|
+
"# Behavior-first testing\n\n- Test observable behavior at the public boundary.\n- Prefer focused tests that fail for one reason.\n",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (prepareSkillsConflict) {
|
|
489
|
+
writeSkill(
|
|
490
|
+
"dependency-direction",
|
|
491
|
+
"Choose dependency direction and boundary ownership for any layered design.",
|
|
492
|
+
"# Dependency direction\n\n- Always use dependency injection.\n- Keep policy separate from delivery and storage concerns.\n",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (prepareSkillsCollision) {
|
|
497
|
+
writeSkill(
|
|
498
|
+
collisionName,
|
|
499
|
+
"A generated skill placed at an unmanaged skill's name.",
|
|
500
|
+
"# Generated skill\n\n- Keep one writer for every installed skill.\n",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (prepareSkillsInvalid) {
|
|
505
|
+
writeSkill(
|
|
506
|
+
"invalid-skill",
|
|
507
|
+
"",
|
|
508
|
+
"# Invalid skill\n\n- Missing a description.\n",
|
|
509
|
+
"name: Invalid Name\ndescription:",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function askQuestions(count: number, options: { recommended?: boolean } = {}) {
|
|
515
|
+
const hasDefault = options.recommended ?? !questionsUnrecommended;
|
|
516
|
+
for (let index = 0; index < count; index += 1) {
|
|
517
|
+
const response = await request("elicitation/create", {
|
|
518
|
+
mode: "form",
|
|
519
|
+
sessionId: "stub-session",
|
|
520
|
+
message: index === 0 ? "Choose an answer" : `Choose an answer ${index + 1}`,
|
|
521
|
+
requestedSchema: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
value: {
|
|
525
|
+
type: "string",
|
|
526
|
+
title: index === 0 ? "Choose an answer" : `Choose an answer ${index + 1}`,
|
|
527
|
+
oneOf: [
|
|
528
|
+
{ const: "Alpha", title: "Alpha", description: "Alpha leads to the first outcome." },
|
|
529
|
+
{ const: "Beta", title: "Beta", description: "Beta leads to the second outcome." },
|
|
530
|
+
],
|
|
531
|
+
...(hasDefault ? { default: "Alpha" } : {}),
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
required: ["value"],
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
const content =
|
|
538
|
+
typeof response === "object" && response !== null && "content" in response
|
|
539
|
+
? response.content
|
|
540
|
+
: undefined;
|
|
541
|
+
const value =
|
|
542
|
+
typeof content === "object" && content !== null && "value" in content
|
|
543
|
+
? content.value
|
|
544
|
+
: undefined;
|
|
545
|
+
log(`answer ${String(value ?? "")}`);
|
|
546
|
+
write({
|
|
547
|
+
jsonrpc: "2.0",
|
|
548
|
+
method: "session/update",
|
|
549
|
+
params: {
|
|
550
|
+
sessionId: "stub-session",
|
|
551
|
+
update: {
|
|
552
|
+
sessionUpdate: "agent_message_chunk",
|
|
553
|
+
content: { type: "text", text: `Answer received: ${String(value ?? "")}` },
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The `/goal-draft` turn. The draft's own bytes choose the scenario:
|
|
562
|
+
* `STUB-QUESTION` asks one question first, `STUB-HANG` waits for Stop, and
|
|
563
|
+
* `STUB-FAIL` reports a failed label verification. A successful turn writes
|
|
564
|
+
* the five-section spec and removes the draft, exactly as the prompt requires.
|
|
565
|
+
*/
|
|
566
|
+
async function runGoalDraft(relativePath: string) {
|
|
567
|
+
log(`goal-draft ${relativePath}`);
|
|
568
|
+
const draftPath = join(process.cwd(), relativePath);
|
|
569
|
+
let source = "";
|
|
570
|
+
try {
|
|
571
|
+
source = readFileSync(draftPath, "utf8");
|
|
572
|
+
} catch {
|
|
573
|
+
// A missing draft still produces a deterministic failure.
|
|
574
|
+
}
|
|
575
|
+
stream("Reading the draft and researching the repository");
|
|
576
|
+
|
|
577
|
+
if (source.includes("STUB-QUESTION")) {
|
|
578
|
+
await askQuestions(1, { recommended: false });
|
|
579
|
+
}
|
|
580
|
+
if (source.includes("STUB-FAIL")) {
|
|
581
|
+
throw new Error("goal label verification failed");
|
|
582
|
+
}
|
|
583
|
+
if (source.includes("STUB-HANG")) {
|
|
584
|
+
while (goalDraftFinish === undefined || !existsSync(goalDraftFinish)) {
|
|
585
|
+
await Bun.sleep(50);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const name = basename(relativePath, ".md");
|
|
590
|
+
const spec = join(process.cwd(), ".devboard", "goals", `${name}.md`);
|
|
591
|
+
mkdirSync(dirname(spec), { recursive: true });
|
|
592
|
+
writeFileSync(
|
|
593
|
+
spec,
|
|
594
|
+
[
|
|
595
|
+
"## Objective",
|
|
596
|
+
"",
|
|
597
|
+
`Finalized ${name}.`,
|
|
598
|
+
"",
|
|
599
|
+
"## Success criteria",
|
|
600
|
+
"",
|
|
601
|
+
"- The stub wrote this spec.",
|
|
602
|
+
"",
|
|
603
|
+
"## Verification",
|
|
604
|
+
"",
|
|
605
|
+
"- Read this file.",
|
|
606
|
+
"",
|
|
607
|
+
"## Boundaries",
|
|
608
|
+
"",
|
|
609
|
+
"- The fixture only.",
|
|
610
|
+
"",
|
|
611
|
+
"## Stop conditions",
|
|
612
|
+
"",
|
|
613
|
+
"- Halt on a missing prerequisite.",
|
|
614
|
+
"",
|
|
615
|
+
].join("\n"),
|
|
616
|
+
);
|
|
617
|
+
log(`goal-draft-verified ${relativePath}`);
|
|
618
|
+
unlinkSync(draftPath);
|
|
619
|
+
stream("Filed the goal issue and verified the goal label");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function promptText(params: unknown): string | undefined {
|
|
623
|
+
if (typeof params !== "object" || params === null || !("prompt" in params)) return undefined;
|
|
624
|
+
const blocks = Array.isArray(params.prompt) ? params.prompt : [];
|
|
625
|
+
return blocks
|
|
626
|
+
.map((block) => {
|
|
627
|
+
if (typeof block !== "object" || block === null || !("text" in block)) return undefined;
|
|
628
|
+
const value = block.text;
|
|
629
|
+
return typeof value === "string" ? value : undefined;
|
|
630
|
+
})
|
|
631
|
+
.find((candidate): candidate is string => typeof candidate === "string");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** JSON lines of a Flow session file, tolerant of an absent or partial file. */
|
|
635
|
+
function readLines(path: string): Record<string, unknown>[] {
|
|
636
|
+
let source = "";
|
|
637
|
+
try {
|
|
638
|
+
source = readFileSync(path, "utf8");
|
|
639
|
+
} catch {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
const entries: Record<string, unknown>[] = [];
|
|
643
|
+
for (const line of source.split("\n")) {
|
|
644
|
+
if (line.trim().length === 0) continue;
|
|
645
|
+
try {
|
|
646
|
+
const value: unknown = JSON.parse(line);
|
|
647
|
+
if (typeof value === "object" && value !== null) entries.push(value as Record<string, unknown>);
|
|
648
|
+
} catch {
|
|
649
|
+
// A partial tail costs one entry.
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return entries;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function writeJsonFile(path: string, value: unknown) {
|
|
656
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
657
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function flowGit(repo: string, args: string[]) {
|
|
661
|
+
return Bun.spawnSync({
|
|
662
|
+
cmd: [
|
|
663
|
+
"git",
|
|
664
|
+
"-C",
|
|
665
|
+
repo,
|
|
666
|
+
"-c",
|
|
667
|
+
"user.name=Flow Stub",
|
|
668
|
+
"-c",
|
|
669
|
+
"user.email=stub@example.test",
|
|
670
|
+
"-c",
|
|
671
|
+
"commit.gpgsign=false",
|
|
672
|
+
...args,
|
|
673
|
+
],
|
|
674
|
+
stdout: "pipe",
|
|
675
|
+
stderr: "pipe",
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function stream(text: string, append = false) {
|
|
680
|
+
write({
|
|
681
|
+
jsonrpc: "2.0",
|
|
682
|
+
method: "session/update",
|
|
683
|
+
params: {
|
|
684
|
+
sessionId: "stub-session",
|
|
685
|
+
update: {
|
|
686
|
+
sessionUpdate: "agent_message_chunk",
|
|
687
|
+
content: { type: "text", text },
|
|
688
|
+
...(append ? { messageId: "flow-stream" } : {}),
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Reads new steering notes and acknowledges each one before a checkpoint. */
|
|
695
|
+
function consumeSteering(directory: string): Record<string, unknown>[] {
|
|
696
|
+
const notes = readLines(join(directory, "steering.jsonl"));
|
|
697
|
+
const acked = new Set(
|
|
698
|
+
readLines(join(directory, "steering-ack.jsonl")).map((entry) => String(entry.id ?? "")),
|
|
699
|
+
);
|
|
700
|
+
const fresh = notes.filter((note) => !acked.has(String(note.id ?? "")));
|
|
701
|
+
for (const note of fresh) {
|
|
702
|
+
mkdirSync(directory, { recursive: true });
|
|
703
|
+
appendFileSync(join(directory, "steering-ack.jsonl"), `${JSON.stringify({ id: note.id })}\n`);
|
|
704
|
+
log(`flow-ack ${String(note.id ?? "")}`);
|
|
705
|
+
appendFileSync(join(process.cwd(), "steering-applied.txt"), `${String(note.text ?? "")}\n`);
|
|
706
|
+
}
|
|
707
|
+
log(`flow-steering-read ${fresh.length}`);
|
|
708
|
+
return fresh;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function runFlowGoal(directory: string) {
|
|
712
|
+
log(`flow-goal ${directory}`);
|
|
713
|
+
stream("Interviewing the developer about the Flow goal");
|
|
714
|
+
if (flowGoalHang !== undefined && existsSync(flowGoalHang)) {
|
|
715
|
+
log(`flow-goal-hung ${directory}`);
|
|
716
|
+
await new Promise<never>(() => {});
|
|
717
|
+
}
|
|
718
|
+
if (flowGoalPartial !== undefined && existsSync(flowGoalPartial)) {
|
|
719
|
+
mkdirSync(directory, { recursive: true });
|
|
720
|
+
writeFileSync(join(directory, "goal.md"), flowGoalDraft);
|
|
721
|
+
log(`flow-goal-partial ${directory}`);
|
|
722
|
+
await new Promise<never>(() => {});
|
|
723
|
+
}
|
|
724
|
+
if (delayMs > 0) await Bun.sleep(delayMs);
|
|
725
|
+
await askQuestions(1);
|
|
726
|
+
if (flowGoalFail) {
|
|
727
|
+
log("flow-goal-failed");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
mkdirSync(directory, { recursive: true });
|
|
731
|
+
let text = flowGoalText;
|
|
732
|
+
if (flowGoalTextFile !== undefined) {
|
|
733
|
+
try {
|
|
734
|
+
text = readFileSync(flowGoalTextFile, "utf8");
|
|
735
|
+
} catch {
|
|
736
|
+
// The file is a one-shot override; absent means keep the default.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
writeFileSync(join(directory, "goal.md"), flowGoalEmpty ? "" : text);
|
|
740
|
+
log(`flow-goal-written ${directory}`);
|
|
741
|
+
if (flowGoalSettleMs > 0) await Bun.sleep(flowGoalSettleMs);
|
|
742
|
+
stream("Wrote the Flow goal");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function runFlowImplementation(directory: string) {
|
|
746
|
+
log(`flow-implement ${directory} cwd=${process.cwd()}`);
|
|
747
|
+
stream("Reading the plan");
|
|
748
|
+
|
|
749
|
+
if (flowQuestion) await askQuestions(1);
|
|
750
|
+
if (flowPreCommitDelay > 0) await Bun.sleep(flowPreCommitDelay);
|
|
751
|
+
|
|
752
|
+
for (let index = 1; index <= flowCheckpoints; index += 1) {
|
|
753
|
+
try {
|
|
754
|
+
const decisions = readFileSync(join(directory, "decisions.md"), "utf8");
|
|
755
|
+
log(`flow-decisions-read ${index} ${decisions.length}`);
|
|
756
|
+
} catch {
|
|
757
|
+
log(`flow-decisions-read ${index} 0`);
|
|
758
|
+
}
|
|
759
|
+
if (index > 1) {
|
|
760
|
+
if (flowWaitFile !== undefined) {
|
|
761
|
+
const deadline = Date.now() + 30_000;
|
|
762
|
+
while (!existsSync(flowWaitFile) && Date.now() < deadline) await Bun.sleep(25);
|
|
763
|
+
log(`flow-wait-done ${existsSync(flowWaitFile) ? "yes" : "timeout"}`);
|
|
764
|
+
}
|
|
765
|
+
consumeSteering(directory);
|
|
766
|
+
}
|
|
767
|
+
mkdirSync(join(process.cwd(), "src"), { recursive: true });
|
|
768
|
+
writeFileSync(join(process.cwd(), "src", "app.txt"), `version ${index}\n`);
|
|
769
|
+
writeFileSync(
|
|
770
|
+
join(process.cwd(), "src", "config.json"),
|
|
771
|
+
`${JSON.stringify({ version: index }, null, 2)}\n`,
|
|
772
|
+
);
|
|
773
|
+
if (index > 1) writeFileSync(join(process.cwd(), "src", `extra-${index}.txt`), `extra ${index}\n`);
|
|
774
|
+
|
|
775
|
+
flowGit(process.cwd(), ["add", "-A"]);
|
|
776
|
+
const committed = flowGit(process.cwd(), [
|
|
777
|
+
"commit",
|
|
778
|
+
"-m",
|
|
779
|
+
`checkpoint ${index}: fixture change`,
|
|
780
|
+
]);
|
|
781
|
+
if ((committed.exitCode ?? 1) !== 0) {
|
|
782
|
+
log(`flow-commit-failed ${committed.stderr.toString().trim()}`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const revision = flowGit(process.cwd(), ["rev-parse", "--short=7", "HEAD"]);
|
|
786
|
+
const commit = revision.stdout.toString().trim();
|
|
787
|
+
const verifiedAt = new Date().toISOString();
|
|
788
|
+
if (!flowNoVerification) {
|
|
789
|
+
writeJsonFile(join(directory, "verification.json"), {
|
|
790
|
+
command: "bun test",
|
|
791
|
+
status: flowVerification,
|
|
792
|
+
timestamp: verifiedAt,
|
|
793
|
+
output:
|
|
794
|
+
flowVerification === "failed"
|
|
795
|
+
? "1 failing test: fixture verification failed\n"
|
|
796
|
+
: "0 failing tests: fixture verification passed\n",
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
mkdirSync(directory, { recursive: true });
|
|
800
|
+
if (!flowSilentCheckpoints) {
|
|
801
|
+
appendFileSync(
|
|
802
|
+
join(directory, "checkpoints.jsonl"),
|
|
803
|
+
`${JSON.stringify({
|
|
804
|
+
id: `cp-${index}`,
|
|
805
|
+
timestamp: verifiedAt,
|
|
806
|
+
summary: `checkpoint ${index} — fixture change`,
|
|
807
|
+
files: index > 1 ? 3 : 2,
|
|
808
|
+
commit,
|
|
809
|
+
verification: flowVerification,
|
|
810
|
+
})}\n`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
log(`flow-checkpoint ${index} ${commit}`);
|
|
814
|
+
stream(`Committed checkpoint ${index}`);
|
|
815
|
+
if (index < flowCheckpoints && flowStepDelay > 0) await Bun.sleep(flowStepDelay);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (flowBlocked) {
|
|
819
|
+
const message = "DEVBOARD-BLOCKED: the fixture cannot continue past this checkpoint";
|
|
820
|
+
log("flow-blocked");
|
|
821
|
+
stream(message);
|
|
822
|
+
mkdirSync(directory, { recursive: true });
|
|
823
|
+
appendFileSync(
|
|
824
|
+
join(directory, "blockers.jsonl"),
|
|
825
|
+
`${JSON.stringify({ id: "blocker-fixture", timestamp: new Date().toISOString(), text: message.slice("DEVBOARD-BLOCKED: ".length) })}\n`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function runFlowChange(directory: string) {
|
|
831
|
+
log(`flow-change ${directory}`);
|
|
832
|
+
stream("Classifying the changed files");
|
|
833
|
+
const request = JSON.parse(readFileSync(join(directory, "request.json"), "utf8")) as {
|
|
834
|
+
files?: { path?: unknown }[];
|
|
835
|
+
};
|
|
836
|
+
const files = (request.files ?? [])
|
|
837
|
+
.map((file) => file.path)
|
|
838
|
+
.filter((path): path is string => typeof path === "string");
|
|
839
|
+
if (flowAnalystFail) {
|
|
840
|
+
writeFileSync(join(directory, "widgets.json"), "this is not widget json");
|
|
841
|
+
log("flow-widgets-failed");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (flowAnalystEmpty) {
|
|
845
|
+
writeJsonFile(join(directory, "widgets.json"), { widgets: [] });
|
|
846
|
+
log("flow-widgets 0");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const selected = flowAnalystPartial ? files.slice(0, 1) : files;
|
|
850
|
+
const widgets: unknown[] = [];
|
|
851
|
+
if (selected.length > 0) {
|
|
852
|
+
widgets.push({
|
|
853
|
+
category: "contract",
|
|
854
|
+
concept: "Application contract",
|
|
855
|
+
summary: "The application's observable contract changed.",
|
|
856
|
+
rationale: "Callers observe a different result.",
|
|
857
|
+
files: [selected[0]],
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (selected.length > 1) {
|
|
861
|
+
widgets.push({
|
|
862
|
+
category: "data",
|
|
863
|
+
concept: "Stored configuration",
|
|
864
|
+
summary: "The stored configuration changed.",
|
|
865
|
+
rationale: "Persisted values have a new shape.",
|
|
866
|
+
files: selected.slice(1),
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (flowAnalystForeign) {
|
|
870
|
+
widgets.push({
|
|
871
|
+
category: "entity",
|
|
872
|
+
concept: "A file outside the diff",
|
|
873
|
+
summary: "This widget invents a file.",
|
|
874
|
+
rationale: "It must be rejected by the server's validation.",
|
|
875
|
+
files: ["src/not-in-the-diff.txt"],
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
writeJsonFile(join(directory, "widgets.json"), { widgets });
|
|
879
|
+
log(`flow-widgets ${widgets.length}`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function runFlowWidget(rest: string) {
|
|
883
|
+
const split = /^(\S+)\s+(\S+)/.exec(rest);
|
|
884
|
+
log(`flow-widget ${split?.[1] ?? ""} ${split?.[2] ?? ""}`);
|
|
885
|
+
stream("Reviewing the widget and its diff");
|
|
886
|
+
await askQuestions(1);
|
|
887
|
+
stream("The widget conversation is read-only");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
type DiscoverConfidence = "observed" | "declared" | "inferred" | "unknown";
|
|
891
|
+
|
|
892
|
+
interface DiscoverLabel {
|
|
893
|
+
language: "en" | "de";
|
|
894
|
+
text: string;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
interface DiscoverConcept {
|
|
898
|
+
id: string;
|
|
899
|
+
prefLabels: DiscoverLabel[];
|
|
900
|
+
altLabels: DiscoverLabel[];
|
|
901
|
+
hiddenLabels: Array<DiscoverLabel & { reason: string; rejectedOn: string }>;
|
|
902
|
+
definitions: DiscoverLabel[];
|
|
903
|
+
scopeNote: null;
|
|
904
|
+
example: null;
|
|
905
|
+
historyNote: null;
|
|
906
|
+
status: "unconfirmed" | "proposed";
|
|
907
|
+
origin: "owner" | "llm";
|
|
908
|
+
standardRef: null;
|
|
909
|
+
kind: "entity" | "value";
|
|
910
|
+
grounding: string[];
|
|
911
|
+
broader: null;
|
|
912
|
+
related: string[];
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const discoverEvidenceCandidates = [
|
|
916
|
+
"README.md",
|
|
917
|
+
"package.json",
|
|
918
|
+
"src",
|
|
919
|
+
"test",
|
|
920
|
+
".github/workflows",
|
|
921
|
+
];
|
|
922
|
+
|
|
923
|
+
/** Existing repo-relative evidence paths, in candidate order, plus a safe fallback. */
|
|
924
|
+
function discoverEvidencePaths() {
|
|
925
|
+
const available = discoverEvidenceCandidates.filter((path) =>
|
|
926
|
+
existsSync(join(process.cwd(), path)),
|
|
927
|
+
);
|
|
928
|
+
const fallback = available.length > 0 ? available : ["."];
|
|
929
|
+
return {
|
|
930
|
+
available,
|
|
931
|
+
primary: fallback[0] ?? ".",
|
|
932
|
+
manifest: existsSync(join(process.cwd(), "package.json")) ? "package.json" : fallback[0] ?? ".",
|
|
933
|
+
source: existsSync(join(process.cwd(), "src")) ? "src" : fallback[0] ?? ".",
|
|
934
|
+
tests: existsSync(join(process.cwd(), "test")) ? "test" : fallback[0] ?? ".",
|
|
935
|
+
ci: existsSync(join(process.cwd(), ".github", "workflows")) ? ".github/workflows" : undefined,
|
|
936
|
+
hasManifest: existsSync(join(process.cwd(), "package.json")),
|
|
937
|
+
hasTests: existsSync(join(process.cwd(), "test")),
|
|
938
|
+
hasSource: existsSync(join(process.cwd(), "src")),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function discoverLeaf(
|
|
943
|
+
value: unknown,
|
|
944
|
+
evidence: string[],
|
|
945
|
+
confidence: DiscoverConfidence = "observed",
|
|
946
|
+
): Record<string, unknown> {
|
|
947
|
+
return { value, evidence: [...new Set(evidence)].sort(), confidence };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function discoverUnknown(reason: string): Record<string, unknown> {
|
|
951
|
+
return { value: null, evidence: [], confidence: "unknown", reason };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** One-line, one-sentence bounded prose no longer than `limit` characters. */
|
|
955
|
+
function discoverOneSentence(text: string, limit: number): string {
|
|
956
|
+
const flat = text.replace(/\s+/g, " ").trim().replace(/[.!?]+$/g, "").replace(/[.!?]\s+/g, " ");
|
|
957
|
+
if (flat.length > limit - 1) return `${flat.slice(0, limit - 1).trimEnd()}.`;
|
|
958
|
+
return `${flat}.`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function discoverSheet(): Record<string, unknown> {
|
|
962
|
+
const paths = discoverEvidencePaths();
|
|
963
|
+
const evidence = [paths.primary];
|
|
964
|
+
const sheet: Record<string, unknown> = {
|
|
965
|
+
schemaVersion: 1,
|
|
966
|
+
definition: discoverLeaf(
|
|
967
|
+
discoverSheetTwoSentences
|
|
968
|
+
? "First sentence describes the fixture project. Second sentence describes the crawl."
|
|
969
|
+
: discoverOneSentence(discoverProse, 240),
|
|
970
|
+
evidence,
|
|
971
|
+
"declared",
|
|
972
|
+
),
|
|
973
|
+
stack: discoverLeaf(
|
|
974
|
+
["Bun", "TypeScript"],
|
|
975
|
+
[paths.manifest],
|
|
976
|
+
paths.hasManifest ? "declared" : "inferred",
|
|
977
|
+
),
|
|
978
|
+
deployment: {
|
|
979
|
+
class: discoverLeaf(
|
|
980
|
+
paths.hasManifest ? "package" : "none",
|
|
981
|
+
[paths.manifest],
|
|
982
|
+
"observed",
|
|
983
|
+
),
|
|
984
|
+
artifact: discoverLeaf("fixture-host", [paths.manifest], "declared"),
|
|
985
|
+
target: discoverUnknown("No publication target was found in the crawled files."),
|
|
986
|
+
pipelineSources:
|
|
987
|
+
paths.ci === undefined
|
|
988
|
+
? discoverUnknown("No CI configuration was found in the crawled files.")
|
|
989
|
+
: discoverLeaf([paths.ci], [paths.ci], "observed"),
|
|
990
|
+
},
|
|
991
|
+
environments: discoverLeaf(
|
|
992
|
+
[{ name: "production", purpose: "Changes reach production through a direct push." }],
|
|
993
|
+
evidence,
|
|
994
|
+
"inferred",
|
|
995
|
+
),
|
|
996
|
+
commands: {
|
|
997
|
+
install: paths.hasManifest
|
|
998
|
+
? discoverLeaf("bun install", [paths.manifest], "declared")
|
|
999
|
+
: discoverUnknown("No install command was found in the crawled files."),
|
|
1000
|
+
build: paths.hasManifest
|
|
1001
|
+
? discoverLeaf("bun run build", [paths.manifest], "declared")
|
|
1002
|
+
: discoverUnknown("No build command was found in the crawled files."),
|
|
1003
|
+
test: paths.hasManifest
|
|
1004
|
+
? discoverLeaf("bun test", [paths.manifest], "declared")
|
|
1005
|
+
: discoverUnknown("No test command was found in the crawled files."),
|
|
1006
|
+
lint: discoverUnknown("No lint command was found in the crawled files."),
|
|
1007
|
+
typecheck: discoverUnknown("No typecheck command was found in the crawled files."),
|
|
1008
|
+
dev: paths.hasManifest
|
|
1009
|
+
? discoverLeaf("bun run dev", [paths.manifest], "declared")
|
|
1010
|
+
: discoverUnknown("No dev command was found in the crawled files."),
|
|
1011
|
+
tryOut: discoverUnknown("No try-out command was found in the crawled files."),
|
|
1012
|
+
release: discoverUnknown("No release command was found in the crawled files."),
|
|
1013
|
+
},
|
|
1014
|
+
branching: {
|
|
1015
|
+
defaultBranch: discoverLeaf("main", ["git branch"], "observed"),
|
|
1016
|
+
mergeMethod: discoverLeaf("merge commit", ["git log"], "inferred"),
|
|
1017
|
+
releaseBranches: discoverLeaf([], ["git branch"], "observed"),
|
|
1018
|
+
model: discoverLeaf("pull-request", ["git log"], "inferred"),
|
|
1019
|
+
},
|
|
1020
|
+
testing: {
|
|
1021
|
+
levels: paths.hasTests
|
|
1022
|
+
? discoverLeaf(
|
|
1023
|
+
[
|
|
1024
|
+
discoverSheetNoHappyPath
|
|
1025
|
+
? {
|
|
1026
|
+
level: "unit",
|
|
1027
|
+
tests: "1",
|
|
1028
|
+
paths: [paths.tests],
|
|
1029
|
+
command: "npm test",
|
|
1030
|
+
fixtures: [],
|
|
1031
|
+
}
|
|
1032
|
+
: {
|
|
1033
|
+
level: "unit",
|
|
1034
|
+
tests: "1",
|
|
1035
|
+
happyPath: discoverSheetHappyOverflow ? "2" : "1",
|
|
1036
|
+
paths: [paths.tests],
|
|
1037
|
+
command: "npm run test:unit",
|
|
1038
|
+
fixtures: [],
|
|
1039
|
+
},
|
|
1040
|
+
],
|
|
1041
|
+
[paths.tests],
|
|
1042
|
+
"observed",
|
|
1043
|
+
)
|
|
1044
|
+
: discoverUnknown("No test files were found in the crawled files."),
|
|
1045
|
+
pyramid: paths.hasTests
|
|
1046
|
+
? discoverLeaf("Unit tests are the only observed level.", [paths.tests], "inferred")
|
|
1047
|
+
: discoverUnknown("No test files were found in the crawled files."),
|
|
1048
|
+
},
|
|
1049
|
+
architecture: {
|
|
1050
|
+
contexts: paths.hasSource
|
|
1051
|
+
? discoverLeaf(
|
|
1052
|
+
[
|
|
1053
|
+
{
|
|
1054
|
+
name: "Checkout",
|
|
1055
|
+
path: "src/checkout",
|
|
1056
|
+
responsibility: "Turns a cart into a placed order.",
|
|
1057
|
+
entryPoints: ["src/checkout/flow.ts"],
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "Orders",
|
|
1061
|
+
path: "src",
|
|
1062
|
+
responsibility: "Owns order records and order totals.",
|
|
1063
|
+
entryPoints: ["src"],
|
|
1064
|
+
},
|
|
1065
|
+
],
|
|
1066
|
+
["src"],
|
|
1067
|
+
"observed",
|
|
1068
|
+
)
|
|
1069
|
+
: discoverUnknown("No source directory was found in the crawled files."),
|
|
1070
|
+
dependencies: paths.hasSource
|
|
1071
|
+
? discoverLeaf([{ from: "Checkout", to: "Orders" }], ["src"], "observed")
|
|
1072
|
+
: discoverUnknown("No source directory was found in the crawled files."),
|
|
1073
|
+
dependencyDirection: paths.hasSource
|
|
1074
|
+
? discoverLeaf(
|
|
1075
|
+
"Dependencies point inward toward the Orders domain code.",
|
|
1076
|
+
["src"],
|
|
1077
|
+
"inferred",
|
|
1078
|
+
)
|
|
1079
|
+
: discoverUnknown("No source directory was found in the crawled files."),
|
|
1080
|
+
externalBoundaries: paths.hasSource
|
|
1081
|
+
? discoverLeaf([], ["src"], "observed")
|
|
1082
|
+
: discoverUnknown("No source directory was found in the crawled files."),
|
|
1083
|
+
sharedKernels: paths.hasSource
|
|
1084
|
+
? discoverLeaf([], ["src"], "observed")
|
|
1085
|
+
: discoverUnknown("No source directory was found in the crawled files."),
|
|
1086
|
+
},
|
|
1087
|
+
ci: {
|
|
1088
|
+
provider:
|
|
1089
|
+
paths.ci === undefined
|
|
1090
|
+
? discoverLeaf("none detected", evidence, "observed")
|
|
1091
|
+
: discoverLeaf("GitHub Actions", [paths.ci], "observed"),
|
|
1092
|
+
triggers:
|
|
1093
|
+
paths.ci === undefined
|
|
1094
|
+
? discoverLeaf([], evidence, "observed")
|
|
1095
|
+
: discoverLeaf(["push"], [paths.ci], "observed"),
|
|
1096
|
+
jobs:
|
|
1097
|
+
paths.ci === undefined
|
|
1098
|
+
? discoverLeaf([], evidence, "observed")
|
|
1099
|
+
: discoverLeaf(["test"], [paths.ci], "observed"),
|
|
1100
|
+
qualityGates:
|
|
1101
|
+
paths.ci === undefined
|
|
1102
|
+
? discoverLeaf([], evidence, "observed")
|
|
1103
|
+
: discoverLeaf(
|
|
1104
|
+
[{ name: "bun test", checks: "Runs the whole fixture test suite in CI." }],
|
|
1105
|
+
[paths.ci],
|
|
1106
|
+
"declared",
|
|
1107
|
+
),
|
|
1108
|
+
artifacts:
|
|
1109
|
+
paths.ci === undefined
|
|
1110
|
+
? discoverLeaf([], evidence, "observed")
|
|
1111
|
+
: discoverLeaf([], [paths.ci], "observed"),
|
|
1112
|
+
},
|
|
1113
|
+
runtime: {
|
|
1114
|
+
languages: discoverLeaf(["TypeScript"], [paths.manifest], "declared"),
|
|
1115
|
+
runtimes: discoverLeaf(["Bun"], [paths.manifest], "declared"),
|
|
1116
|
+
packageManagers: discoverLeaf(["bun"], [paths.manifest], "declared"),
|
|
1117
|
+
versions: discoverLeaf([], [paths.manifest], "observed"),
|
|
1118
|
+
manifests: paths.hasManifest
|
|
1119
|
+
? discoverLeaf(["package.json"], [paths.manifest], "observed")
|
|
1120
|
+
: discoverUnknown("No manifest was found in the crawled files."),
|
|
1121
|
+
},
|
|
1122
|
+
dependencyMap: {
|
|
1123
|
+
binaries: discoverLeaf([], [paths.manifest], "observed"),
|
|
1124
|
+
packages: discoverLeaf([], [paths.manifest], "observed"),
|
|
1125
|
+
routes: discoverLeaf([], [paths.manifest], "observed"),
|
|
1126
|
+
jobs: discoverLeaf([], [paths.manifest], "observed"),
|
|
1127
|
+
externalServices: discoverLeaf([], [paths.manifest], "observed"),
|
|
1128
|
+
dataStores: discoverLeaf([], [paths.manifest], "observed"),
|
|
1129
|
+
keyDependencies: discoverLeaf([], [paths.manifest], "declared"),
|
|
1130
|
+
},
|
|
1131
|
+
delivery: {
|
|
1132
|
+
artifactTarget: discoverLeaf("package", [paths.manifest], "declared"),
|
|
1133
|
+
releaseChannels: discoverLeaf([], [paths.manifest], "declared"),
|
|
1134
|
+
versionScheme: discoverUnknown("No versioning scheme was found in the crawled files."),
|
|
1135
|
+
migrationPolicy: discoverUnknown("No migration policy was found in the crawled files."),
|
|
1136
|
+
},
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// Both leaf shapes were constructed above; the casts only narrow the variant mutations.
|
|
1140
|
+
const stack = sheet.stack as { value: unknown; evidence: string[] };
|
|
1141
|
+
const deployment = sheet.deployment as { artifact: { value: unknown } };
|
|
1142
|
+
if (discoverInvalidSheet) stack.value = ["x".repeat(200)];
|
|
1143
|
+
if (discoverSheetAbsolute) stack.evidence = ["/etc/passwd"];
|
|
1144
|
+
if (discoverSheetNoEvidence) stack.evidence = [];
|
|
1145
|
+
if (discoverSheetSecret) deployment.artifact.value = DISCOVER_SECRET;
|
|
1146
|
+
|
|
1147
|
+
return sheet;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function discoverLabelLines(field: string, labels: DiscoverLabel[]): string[] {
|
|
1151
|
+
if (labels.length === 0) return [` ${field}: []`];
|
|
1152
|
+
return [
|
|
1153
|
+
` ${field}:`,
|
|
1154
|
+
...labels.flatMap((label) => [
|
|
1155
|
+
` - language: ${label.language}`,
|
|
1156
|
+
` text: ${JSON.stringify(label.text)}`,
|
|
1157
|
+
]),
|
|
1158
|
+
];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function discoverConceptLines(concept: DiscoverConcept): string[] {
|
|
1162
|
+
const hidden =
|
|
1163
|
+
concept.hiddenLabels.length === 0
|
|
1164
|
+
? [" hiddenLabels: []"]
|
|
1165
|
+
: [
|
|
1166
|
+
" hiddenLabels:",
|
|
1167
|
+
...concept.hiddenLabels.flatMap((label) => [
|
|
1168
|
+
` - language: ${label.language}`,
|
|
1169
|
+
` text: ${JSON.stringify(label.text)}`,
|
|
1170
|
+
` reason: ${JSON.stringify(label.reason)}`,
|
|
1171
|
+
` rejectedOn: ${JSON.stringify(label.rejectedOn)}`,
|
|
1172
|
+
]),
|
|
1173
|
+
];
|
|
1174
|
+
return [
|
|
1175
|
+
` - id: ${concept.id}`,
|
|
1176
|
+
...discoverLabelLines("prefLabels", concept.prefLabels),
|
|
1177
|
+
...discoverLabelLines("altLabels", concept.altLabels),
|
|
1178
|
+
...hidden,
|
|
1179
|
+
...discoverLabelLines("definitions", concept.definitions),
|
|
1180
|
+
" scopeNote: null",
|
|
1181
|
+
" example: null",
|
|
1182
|
+
" historyNote: null",
|
|
1183
|
+
` status: ${concept.status}`,
|
|
1184
|
+
` origin: ${concept.origin}`,
|
|
1185
|
+
" standardRef: null",
|
|
1186
|
+
` kind: ${concept.kind}`,
|
|
1187
|
+
" grounding:",
|
|
1188
|
+
...concept.grounding.map((entry) => ` - ${JSON.stringify(entry)}`),
|
|
1189
|
+
" broader: null",
|
|
1190
|
+
` related: ${JSON.stringify(concept.related)}`,
|
|
1191
|
+
];
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function discoverGlossary(): { yaml: string; concepts: DiscoverConcept[] } {
|
|
1195
|
+
const paths = discoverEvidencePaths();
|
|
1196
|
+
const available = paths.available.length > 0 ? paths.available : ["."];
|
|
1197
|
+
const firstGrounding = `code:${available[0] ?? "."}`;
|
|
1198
|
+
const secondGrounding = `code:${available[1] ?? available[0] ?? "."}`;
|
|
1199
|
+
const seed = discoverOneSentence(discoverProse, 152).replace(/\.$/, "");
|
|
1200
|
+
const order: DiscoverConcept = {
|
|
1201
|
+
id: "order",
|
|
1202
|
+
prefLabels: [{ language: "en", text: "Order" }],
|
|
1203
|
+
altLabels: [],
|
|
1204
|
+
hiddenLabels: [],
|
|
1205
|
+
definitions: [
|
|
1206
|
+
{ language: "en", text: discoverOneSentence(`${seed} names one customer order request`, 160) },
|
|
1207
|
+
],
|
|
1208
|
+
scopeNote: null,
|
|
1209
|
+
example: null,
|
|
1210
|
+
historyNote: null,
|
|
1211
|
+
status: "unconfirmed",
|
|
1212
|
+
origin: "owner",
|
|
1213
|
+
standardRef: null,
|
|
1214
|
+
kind: "entity",
|
|
1215
|
+
grounding: [firstGrounding],
|
|
1216
|
+
broader: null,
|
|
1217
|
+
related: [],
|
|
1218
|
+
};
|
|
1219
|
+
const orderTotal: DiscoverConcept = {
|
|
1220
|
+
id: discoverGlossaryInvalid ? "order" : "order-total",
|
|
1221
|
+
prefLabels: [{ language: "en", text: "Order Total" }],
|
|
1222
|
+
altLabels: [],
|
|
1223
|
+
hiddenLabels: [],
|
|
1224
|
+
definitions: [
|
|
1225
|
+
{
|
|
1226
|
+
language: "en",
|
|
1227
|
+
text: discoverOneSentence(`${seed} sums the lines of one order`, 160),
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
scopeNote: null,
|
|
1231
|
+
example: null,
|
|
1232
|
+
historyNote: null,
|
|
1233
|
+
status: "proposed",
|
|
1234
|
+
origin: "llm",
|
|
1235
|
+
standardRef: null,
|
|
1236
|
+
kind: "value",
|
|
1237
|
+
grounding: [discoverGlossaryGrounding ? "code:src/does-not-exist.ts" : secondGrounding],
|
|
1238
|
+
broader: null,
|
|
1239
|
+
related: [],
|
|
1240
|
+
};
|
|
1241
|
+
const option: DiscoverConcept = {
|
|
1242
|
+
id: "option",
|
|
1243
|
+
prefLabels: [{ language: "en", text: "Option" }],
|
|
1244
|
+
altLabels: [],
|
|
1245
|
+
hiddenLabels: [],
|
|
1246
|
+
definitions: [
|
|
1247
|
+
{
|
|
1248
|
+
language: "en",
|
|
1249
|
+
text: discoverOneSentence(`${seed} names an entry of a choice list`, 160),
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
scopeNote: null,
|
|
1253
|
+
example: null,
|
|
1254
|
+
historyNote: null,
|
|
1255
|
+
status: "unconfirmed",
|
|
1256
|
+
origin: "owner",
|
|
1257
|
+
standardRef: null,
|
|
1258
|
+
kind: "entity",
|
|
1259
|
+
grounding: [secondGrounding],
|
|
1260
|
+
broader: null,
|
|
1261
|
+
related: [],
|
|
1262
|
+
};
|
|
1263
|
+
const concepts = discoverGlossaryDrop ? [order] : [order, option, orderTotal];
|
|
1264
|
+
const lines = [
|
|
1265
|
+
"boundedContext: orders",
|
|
1266
|
+
"titles:",
|
|
1267
|
+
" - language: en",
|
|
1268
|
+
` text: ${JSON.stringify("Orders")}`,
|
|
1269
|
+
"concepts:",
|
|
1270
|
+
...concepts.flatMap(discoverConceptLines),
|
|
1271
|
+
];
|
|
1272
|
+
return { yaml: `${lines.join("\n")}\n`, concepts };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/** Scores the fixture concepts; the generic `option` scores lowest on purpose. */
|
|
1276
|
+
const DISCOVER_SIGNIFICANCE: Record<string, { score: number; reason: string }> = {
|
|
1277
|
+
order: { score: 80, reason: "Every fixture checkout creates one order record." },
|
|
1278
|
+
option: { score: 10, reason: "A framework option token, not a domain term." },
|
|
1279
|
+
"order-total": { score: 90, reason: "The order total is the domain's central summed value." },
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
function discoverSignificanceSidecar(
|
|
1283
|
+
concepts: DiscoverConcept[],
|
|
1284
|
+
): Record<string, unknown> {
|
|
1285
|
+
const ids = [...new Set(concepts.map((concept) => concept.id))];
|
|
1286
|
+
const entries: Record<string, { score: number; reason: string }> = Object.fromEntries(
|
|
1287
|
+
ids.map((id) => [
|
|
1288
|
+
id,
|
|
1289
|
+
{ ...(DISCOVER_SIGNIFICANCE[id] ?? { score: 50, reason: "Observed in the fixture host." }) },
|
|
1290
|
+
]),
|
|
1291
|
+
);
|
|
1292
|
+
const first = ids[0];
|
|
1293
|
+
if (discoverSignificance === "unknown-concept") delete entries[ids[1] ?? first ?? ""];
|
|
1294
|
+
if (discoverSignificance === "extra-concept" && first !== undefined) {
|
|
1295
|
+
entries["ghost-term"] = { score: 50, reason: "A concept that is not staged." };
|
|
1296
|
+
}
|
|
1297
|
+
if (discoverSignificance === "score") entries.option = { score: 101, reason: "Out of range." };
|
|
1298
|
+
if (discoverSignificance === "reason-empty") entries.option = { score: 10, reason: " " };
|
|
1299
|
+
if (discoverSignificance === "reason-multiline") {
|
|
1300
|
+
entries.option = { score: 10, reason: "One line.\nA second line." };
|
|
1301
|
+
}
|
|
1302
|
+
if (discoverSignificance === "reason-absolute") {
|
|
1303
|
+
entries.option = { score: 10, reason: "/home/operator/secret.ts defines it." };
|
|
1304
|
+
}
|
|
1305
|
+
if (discoverSignificance === "reason-secret") {
|
|
1306
|
+
entries.option = { score: 10, reason: `Pinned by ${DISCOVER_SECRET}` };
|
|
1307
|
+
}
|
|
1308
|
+
const contexts: Record<string, unknown> = { orders: entries };
|
|
1309
|
+
if (discoverSignificance === "unknown-context") {
|
|
1310
|
+
contexts["other-context"] = { "another-term": { score: 50, reason: "Not a staged context." } };
|
|
1311
|
+
}
|
|
1312
|
+
if (discoverSignificance === "extra-key") return { schemaVersion: 1, contexts, note: "extra" };
|
|
1313
|
+
if (discoverSignificance === "schema") return { schemaVersion: 2, contexts };
|
|
1314
|
+
if (discoverSignificance === "score-object") {
|
|
1315
|
+
return { schemaVersion: 1, contexts: { orders: { order: { score: "80", reason: "Text score." } } } };
|
|
1316
|
+
}
|
|
1317
|
+
return { schemaVersion: 1, contexts };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
interface DiscoverSkill {
|
|
1321
|
+
name: string;
|
|
1322
|
+
role: string;
|
|
1323
|
+
description: string;
|
|
1324
|
+
body: string[];
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function discoverSkills(): DiscoverSkill[] {
|
|
1328
|
+
const glossaryPath = ".omp/discover/glossary/orders.glossary.yaml";
|
|
1329
|
+
return [
|
|
1330
|
+
{
|
|
1331
|
+
name: "discover-testing-strategy",
|
|
1332
|
+
role: "testing-strategy",
|
|
1333
|
+
description:
|
|
1334
|
+
"Apply the observed unit-test level and the project's recorded test command before requesting review.",
|
|
1335
|
+
body: [
|
|
1336
|
+
"- Run `bun test` from the repository root before review.",
|
|
1337
|
+
"- Treat the unit level as the only implemented test level; add new cases there.",
|
|
1338
|
+
"- Add a failing case first and keep one assertion focus per test file.",
|
|
1339
|
+
"- Report untested areas as unknown instead of inventing an integration or end-to-end suite.",
|
|
1340
|
+
],
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
name: "discover-naming-conventions",
|
|
1344
|
+
role: "naming-conventions",
|
|
1345
|
+
description:
|
|
1346
|
+
"Name code after the grounded glossary terms and keep the observed casing across the project.",
|
|
1347
|
+
body: [
|
|
1348
|
+
`- Name domain types and functions after the preferred labels in ${glossaryPath}.`,
|
|
1349
|
+
"- Keep file names kebab-case and exported types PascalCase.",
|
|
1350
|
+
"- Use camelCase for functions, variables, and flags.",
|
|
1351
|
+
`- Reject aliases the glossary does not define; propose new terms through the glossary process in ${glossaryPath}.`,
|
|
1352
|
+
],
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "discover-coding-conventions",
|
|
1356
|
+
role: "coding-conventions",
|
|
1357
|
+
description:
|
|
1358
|
+
"Keep typed, explicit code that reuses the dependencies the manifest already records.",
|
|
1359
|
+
body: [
|
|
1360
|
+
"- Keep exported functions typed; never leave an implicit any boundary.",
|
|
1361
|
+
"- Handle recoverable errors at the caller; never swallow a failure silently.",
|
|
1362
|
+
"- Prefer an already-installed dependency over adding a new one.",
|
|
1363
|
+
"- Run `bun test` before claiming a change is complete.",
|
|
1364
|
+
],
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
name: "discover-architecture-boundaries",
|
|
1368
|
+
role: "architecture-boundaries",
|
|
1369
|
+
description:
|
|
1370
|
+
"Keep domain behavior inside its bounded context and preserve the observed dependency direction.",
|
|
1371
|
+
body: [
|
|
1372
|
+
"- Keep domain behavior inside its bounded context.",
|
|
1373
|
+
"- Point imports inward toward domain modules; never import delivery code from the domain.",
|
|
1374
|
+
`- Do not add a cross-context import without recording the boundary in ${glossaryPath}.`,
|
|
1375
|
+
"- Treat an external service as absent until a manifest or source file declares one.",
|
|
1376
|
+
],
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
name: "discover-glossary",
|
|
1380
|
+
role: "glossary",
|
|
1381
|
+
description:
|
|
1382
|
+
"Use the canonical glossary, honor human decisions, and propose new terms explicitly.",
|
|
1383
|
+
body: [
|
|
1384
|
+
`- Treat ${glossaryPath} as the canonical glossary for every naming decision.`,
|
|
1385
|
+
"- Never write a confirmed status into a generated glossary; only a human confirm decision may do that.",
|
|
1386
|
+
"- Reject a proposed term with a non-empty reason instead of deleting it.",
|
|
1387
|
+
"- Propose new terms through Discover with grounding and let a human confirm them.",
|
|
1388
|
+
"- Never label code with an invalidated preferred label; no concept is invalidated in this baseline.",
|
|
1389
|
+
],
|
|
1390
|
+
},
|
|
1391
|
+
];
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function discoverSkillText(skill: DiscoverSkill): string {
|
|
1395
|
+
return [
|
|
1396
|
+
"---",
|
|
1397
|
+
`name: ${skill.name}`,
|
|
1398
|
+
`description: ${skill.description}`,
|
|
1399
|
+
"metadata:",
|
|
1400
|
+
' devboard-managed: "true"',
|
|
1401
|
+
' devboard-family: "discover"',
|
|
1402
|
+
` devboard-skill: ${skill.role}`,
|
|
1403
|
+
"---",
|
|
1404
|
+
"",
|
|
1405
|
+
...skill.body,
|
|
1406
|
+
"",
|
|
1407
|
+
].join("\n");
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function runDiscover(staging: string) {
|
|
1411
|
+
stream("Scanning manifests");
|
|
1412
|
+
stream("Reading tests");
|
|
1413
|
+
stream("Mapping architecture");
|
|
1414
|
+
if (discoverQuestion) await askQuestions(1);
|
|
1415
|
+
|
|
1416
|
+
if (discoverError) {
|
|
1417
|
+
stream("Crawl failed while reading manifests");
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
mkdirSync(staging, { recursive: true });
|
|
1422
|
+
writeFileSync(join(staging, "sheet.json"), `${JSON.stringify(discoverSheet(), null, 2)}\n`);
|
|
1423
|
+
|
|
1424
|
+
mkdirSync(join(staging, "glossary"), { recursive: true });
|
|
1425
|
+
const glossary = discoverGlossary();
|
|
1426
|
+
writeFileSync(join(staging, "glossary", "orders.glossary.yaml"), glossary.yaml);
|
|
1427
|
+
if (discoverSignificance !== "missing") {
|
|
1428
|
+
const sidecar =
|
|
1429
|
+
discoverSignificance === "invalid"
|
|
1430
|
+
? "{ not json"
|
|
1431
|
+
: discoverSignificance === "array"
|
|
1432
|
+
? "[]"
|
|
1433
|
+
: `${JSON.stringify(discoverSignificanceSidecar(glossary.concepts), null, 2)}\n`;
|
|
1434
|
+
writeFileSync(join(staging, "glossary", "significance.json"), sidecar);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const skills = discoverSkills();
|
|
1438
|
+
for (const [index, skill] of skills.entries()) {
|
|
1439
|
+
if (discoverMissingArtifact && index === skills.length - 1) continue;
|
|
1440
|
+
const directory = join(staging, "skills", skill.name);
|
|
1441
|
+
mkdirSync(directory, { recursive: true });
|
|
1442
|
+
writeFileSync(join(directory, "SKILL.md"), discoverSkillText(skill));
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (discoverExtraArtifact) {
|
|
1446
|
+
writeFileSync(join(staging, "notes.txt"), "Extra staged note.\n");
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (discoverWriteOutside) {
|
|
1450
|
+
const note = join(process.cwd(), ".omp", "discover", "staging-note.txt");
|
|
1451
|
+
mkdirSync(dirname(note), { recursive: true });
|
|
1452
|
+
writeFileSync(note, "Written outside staging by the stub.\n");
|
|
1453
|
+
writeFileSync(
|
|
1454
|
+
join(process.cwd(), "GOAL.md"),
|
|
1455
|
+
"# Discover stub goal\n\nThe stub wrote outside staging.\n",
|
|
1456
|
+
);
|
|
1457
|
+
// The host fingerprint ignores .omp/discover, and the failure drill must
|
|
1458
|
+
// leave the published Discover tree byte-identical; GOAL.md still proves
|
|
1459
|
+
// the outside write.
|
|
1460
|
+
unlinkSync(note);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (discoverNoExit) await Bun.sleep(delayMs > 0 ? delayMs : 60_000);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* The peer is announced with the prompt that starts its first turn, so a test
|
|
1468
|
+
* that counts peers sees the announcement together with the turn it started.
|
|
1469
|
+
*/
|
|
1470
|
+
let announced = false;
|
|
1471
|
+
|
|
1472
|
+
async function promptTurn(params: unknown) {
|
|
1473
|
+
if (!announced) {
|
|
1474
|
+
announced = true;
|
|
1475
|
+
log(`spawn ${process.pid}`);
|
|
1476
|
+
}
|
|
1477
|
+
const text = promptText(params);
|
|
1478
|
+
const file = typeof text === "string" ? /(?:^|\s)(\S+\.md)(?:\s|$)/.exec(text)?.[1] : undefined;
|
|
1479
|
+
const firstLine = text?.split("\n")[0] ?? "";
|
|
1480
|
+
|
|
1481
|
+
if (goalSlug !== undefined && text !== undefined && text.includes(GOAL_SLUG_PROMPT_MARKER)) {
|
|
1482
|
+
log(`goal-slug ${firstLine}`);
|
|
1483
|
+
write({
|
|
1484
|
+
jsonrpc: "2.0",
|
|
1485
|
+
method: "session/update",
|
|
1486
|
+
params: {
|
|
1487
|
+
sessionId: "stub-session",
|
|
1488
|
+
update: {
|
|
1489
|
+
sessionUpdate: "agent_thought_chunk",
|
|
1490
|
+
content: { type: "text", text: "Considering the report." },
|
|
1491
|
+
messageId: "goal-slug-thought",
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
stream(goalSlug);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
if (ask && text !== undefined && text.includes(ASK_INSTRUCTION)) {
|
|
1500
|
+
askTurns += 1;
|
|
1501
|
+
const question = /(?:^|\n)Question: ([^\n]*)/.exec(text)?.[1] ?? "";
|
|
1502
|
+
log(`ask-prompt ${askTurns} ${question}`);
|
|
1503
|
+
log(`ask-text ${askTurns} ${JSON.stringify(text)}`);
|
|
1504
|
+
// Thoughts and tool activity precede the answer; the panel must drop them.
|
|
1505
|
+
write({
|
|
1506
|
+
jsonrpc: "2.0",
|
|
1507
|
+
method: "session/update",
|
|
1508
|
+
params: {
|
|
1509
|
+
sessionId: "stub-session",
|
|
1510
|
+
update: {
|
|
1511
|
+
sessionUpdate: "agent_thought_chunk",
|
|
1512
|
+
content: { type: "text", text: "Considering the captured element before answering." },
|
|
1513
|
+
messageId: `ask-thought-${askTurns}`,
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
});
|
|
1517
|
+
write({
|
|
1518
|
+
jsonrpc: "2.0",
|
|
1519
|
+
method: "session/update",
|
|
1520
|
+
params: {
|
|
1521
|
+
sessionId: "stub-session",
|
|
1522
|
+
update: {
|
|
1523
|
+
sessionUpdate: "tool_call",
|
|
1524
|
+
toolCallId: `ask-tool-${askTurns}`,
|
|
1525
|
+
title: "Reading the captured element",
|
|
1526
|
+
kind: "read",
|
|
1527
|
+
status: "pending",
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
});
|
|
1531
|
+
if (askDelayMs > 0) await Bun.sleep(askDelayMs);
|
|
1532
|
+
// The first peer of a conversation exits; its replacement, which carries
|
|
1533
|
+
// the earlier turns, answers. A new process cannot see ask turns alone.
|
|
1534
|
+
const firstPeer = !text.includes("Earlier in this conversation:");
|
|
1535
|
+
if (askExit && firstPeer) {
|
|
1536
|
+
log("ask-exit");
|
|
1537
|
+
process.exit(0);
|
|
1538
|
+
}
|
|
1539
|
+
if (askFail && askTurns === 1) throw new Error("The ask agent failed.");
|
|
1540
|
+
const answer = askTurns === 1 ? ASK_ANSWER_ONE : ASK_ANSWER_TWO;
|
|
1541
|
+
const split = answer.indexOf(". ") + 2;
|
|
1542
|
+
stream(answer.slice(0, split).trim());
|
|
1543
|
+
if (askDelayMs > 0) await Bun.sleep(askDelayMs);
|
|
1544
|
+
stream(answer.slice(split).trim());
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (goalDraft && firstLine.startsWith("/goal-draft ")) {
|
|
1549
|
+
await runGoalDraft(firstLine.slice("/goal-draft ".length).trim());
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (flowGoal && firstLine.startsWith("/flow-goal ")) {
|
|
1553
|
+
log(`prompt ${firstLine}`);
|
|
1554
|
+
await runFlowGoal(firstLine.slice("/flow-goal ".length).trim());
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (flowImplement && firstLine.startsWith("/flow-implement ")) {
|
|
1558
|
+
log(`prompt ${firstLine}`);
|
|
1559
|
+
await runFlowImplementation(firstLine.slice("/flow-implement ".length).trim());
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (flowChange && firstLine.startsWith("/flow-change ")) {
|
|
1563
|
+
log(`prompt ${firstLine}`);
|
|
1564
|
+
await runFlowChange(firstLine.slice("/flow-change ".length).trim());
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (flowWidget && firstLine.startsWith("/flow-widget ")) {
|
|
1568
|
+
log(`prompt ${firstLine}`);
|
|
1569
|
+
await runFlowWidget(firstLine.slice("/flow-widget ".length).trim());
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (
|
|
1573
|
+
flowWidget &&
|
|
1574
|
+
text !== undefined &&
|
|
1575
|
+
!firstLine.startsWith("/") &&
|
|
1576
|
+
!firstLine.startsWith("Revise the goal")
|
|
1577
|
+
) {
|
|
1578
|
+
log(`flow-conversation ${firstLine}`);
|
|
1579
|
+
stream(`Developer: ${firstLine}`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (discover && firstLine.startsWith("/discover ")) {
|
|
1584
|
+
log(`prompt ${firstLine}`);
|
|
1585
|
+
await runDiscover(firstLine.slice("/discover ".length).trim());
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
const creating = createGoal && firstLine === "/goal-outcome";
|
|
1590
|
+
const revising = reviseGoal && firstLine.startsWith("Revise the goal");
|
|
1591
|
+
log(`prompt ${firstLine}`);
|
|
1592
|
+
if (revising) log(`request ${text ?? ""}`);
|
|
1593
|
+
|
|
1594
|
+
if (prepareSkills && firstLine === "/prepare-skills") {
|
|
1595
|
+
write({
|
|
1596
|
+
jsonrpc: "2.0",
|
|
1597
|
+
method: "session/update",
|
|
1598
|
+
params: {
|
|
1599
|
+
sessionId: "stub-session",
|
|
1600
|
+
update: {
|
|
1601
|
+
sessionUpdate: "agent_message_chunk",
|
|
1602
|
+
content: {
|
|
1603
|
+
type: "text",
|
|
1604
|
+
text: "Researching public standards, architecture patterns, and testing practice",
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
if (delayMs > 0) await Bun.sleep(delayMs);
|
|
1611
|
+
await askQuestions(forcedQuestions !== undefined ? Number(forcedQuestions) : 0);
|
|
1612
|
+
|
|
1613
|
+
if (prepareSkillsError) process.exit(1);
|
|
1614
|
+
|
|
1615
|
+
writePreparedSkills();
|
|
1616
|
+
log("prepared");
|
|
1617
|
+
write({
|
|
1618
|
+
jsonrpc: "2.0",
|
|
1619
|
+
method: "session/update",
|
|
1620
|
+
params: {
|
|
1621
|
+
sessionId: "stub-session",
|
|
1622
|
+
update: {
|
|
1623
|
+
sessionUpdate: "agent_message_chunk",
|
|
1624
|
+
content: { type: "text", text: "Reviewed every installed skill; no contradictions" },
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const laneEpic =
|
|
1632
|
+
firstLine.startsWith("/implement-next-epic") && !creating && !revising
|
|
1633
|
+
? implementationEpic(firstLine)
|
|
1634
|
+
: undefined;
|
|
1635
|
+
if (laneEpic !== undefined) {
|
|
1636
|
+
await runImplementationLane(laneEpic);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (firstLine === "/epic-next" && !creating && !revising) {
|
|
1641
|
+
await runGenerationLane();
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (creating) {
|
|
1646
|
+
write({
|
|
1647
|
+
jsonrpc: "2.0",
|
|
1648
|
+
method: "session/update",
|
|
1649
|
+
params: {
|
|
1650
|
+
sessionId: "stub-session",
|
|
1651
|
+
update: {
|
|
1652
|
+
sessionUpdate: "agent_message_chunk",
|
|
1653
|
+
content: { type: "text", text: "Creating the goal" },
|
|
1654
|
+
},
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
} else if (revising) {
|
|
1658
|
+
write({
|
|
1659
|
+
jsonrpc: "2.0",
|
|
1660
|
+
method: "session/update",
|
|
1661
|
+
params: {
|
|
1662
|
+
sessionId: "stub-session",
|
|
1663
|
+
update: {
|
|
1664
|
+
sessionUpdate: "agent_message_chunk",
|
|
1665
|
+
content: { type: "text", text: "Revising the goal" },
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
} else if (file === undefined) {
|
|
1670
|
+
write({
|
|
1671
|
+
jsonrpc: "2.0",
|
|
1672
|
+
method: "session/update",
|
|
1673
|
+
params: {
|
|
1674
|
+
sessionId: "stub-session",
|
|
1675
|
+
update: {
|
|
1676
|
+
sessionUpdate: "agent_message_chunk",
|
|
1677
|
+
content: { type: "text", text: `Working on ${firstLine}` },
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
if (delayMs > 0) await Bun.sleep(delayMs);
|
|
1684
|
+
|
|
1685
|
+
if (file !== undefined && !revising) {
|
|
1686
|
+
write({
|
|
1687
|
+
jsonrpc: "2.0",
|
|
1688
|
+
method: "session/update",
|
|
1689
|
+
params: {
|
|
1690
|
+
sessionId: "stub-session",
|
|
1691
|
+
update: {
|
|
1692
|
+
sessionUpdate: "agent_message_chunk",
|
|
1693
|
+
content: { type: "text", text: `Inspecting ${basename(file)}` },
|
|
1694
|
+
},
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const questionCount = creating || revising
|
|
1700
|
+
? 1
|
|
1701
|
+
: forcedQuestions !== undefined
|
|
1702
|
+
? Number(forcedQuestions)
|
|
1703
|
+
: file !== undefined && pendingCount(dirname(file)) === 1
|
|
1704
|
+
? 1
|
|
1705
|
+
: 0;
|
|
1706
|
+
|
|
1707
|
+
await askQuestions(questionCount);
|
|
1708
|
+
|
|
1709
|
+
if (creating) {
|
|
1710
|
+
if (goalFile !== undefined && createGoalWrite) {
|
|
1711
|
+
writeFileSync(goalFile, CREATED_GOAL);
|
|
1712
|
+
log(`created ${goalFile}`);
|
|
1713
|
+
write({
|
|
1714
|
+
jsonrpc: "2.0",
|
|
1715
|
+
method: "session/update",
|
|
1716
|
+
params: {
|
|
1717
|
+
sessionId: "stub-session",
|
|
1718
|
+
update: {
|
|
1719
|
+
sessionUpdate: "agent_message_chunk",
|
|
1720
|
+
content: { type: "text", text: "Created the goal" },
|
|
1721
|
+
},
|
|
1722
|
+
},
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (revising) {
|
|
1729
|
+
if (goalFile !== undefined) {
|
|
1730
|
+
writeFileSync(goalFile, "# Revised goal\n\nThe agent rewrote this.");
|
|
1731
|
+
log(`revised ${goalFile}`);
|
|
1732
|
+
write({
|
|
1733
|
+
jsonrpc: "2.0",
|
|
1734
|
+
method: "session/update",
|
|
1735
|
+
params: {
|
|
1736
|
+
sessionId: "stub-session",
|
|
1737
|
+
update: {
|
|
1738
|
+
sessionUpdate: "agent_message_chunk",
|
|
1739
|
+
content: { type: "text", text: "Revised the goal" },
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (file !== undefined) {
|
|
1748
|
+
await Bun.sleep(SETTLE_DELAY_MS);
|
|
1749
|
+
if (removeRecord) {
|
|
1750
|
+
unlinkSync(file);
|
|
1751
|
+
log(`removed ${file}`);
|
|
1752
|
+
} else if (resolveRecord) {
|
|
1753
|
+
markAccepted(file);
|
|
1754
|
+
log(`resolved ${file}`);
|
|
1755
|
+
write({
|
|
1756
|
+
jsonrpc: "2.0",
|
|
1757
|
+
method: "session/update",
|
|
1758
|
+
params: {
|
|
1759
|
+
sessionId: "stub-session",
|
|
1760
|
+
update: {
|
|
1761
|
+
sessionUpdate: "agent_message_chunk",
|
|
1762
|
+
content: { type: "text", text: `Updated ${basename(file)}` },
|
|
1763
|
+
},
|
|
1764
|
+
},
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function handleMessage(message: Record<string, unknown>) {
|
|
1771
|
+
if (typeof message.method === "string" && message.id !== undefined) {
|
|
1772
|
+
if (message.method === "initialize") {
|
|
1773
|
+
if (askRefuseInitialize && firstAskHandshake("initialize")) {
|
|
1774
|
+
write({
|
|
1775
|
+
jsonrpc: "2.0",
|
|
1776
|
+
id: message.id,
|
|
1777
|
+
error: { code: -32603, message: "The Ask agent refused the handshake." },
|
|
1778
|
+
});
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
write({
|
|
1782
|
+
jsonrpc: "2.0",
|
|
1783
|
+
id: message.id,
|
|
1784
|
+
result: {
|
|
1785
|
+
protocolVersion: 1,
|
|
1786
|
+
agentInfo: { name: "stub-agent", version: "0.1.0" },
|
|
1787
|
+
},
|
|
1788
|
+
});
|
|
1789
|
+
} else if (message.method === "session/new") {
|
|
1790
|
+
if (askNoSession && firstAskHandshake("session")) {
|
|
1791
|
+
write({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
write({ jsonrpc: "2.0", id: message.id, result: { sessionId: "stub-session" } });
|
|
1795
|
+
} else if (message.method === "session/prompt") {
|
|
1796
|
+
const text = promptText(message.params) ?? "";
|
|
1797
|
+
if (busyRemaining > 0 && text.startsWith("The implementation turn ended")) {
|
|
1798
|
+
busyRemaining -= 1;
|
|
1799
|
+
log("busy");
|
|
1800
|
+
write({
|
|
1801
|
+
jsonrpc: "2.0",
|
|
1802
|
+
id: message.id,
|
|
1803
|
+
error: {
|
|
1804
|
+
code: -32003,
|
|
1805
|
+
message: "Session is busy",
|
|
1806
|
+
data: { reason: "session_busy", hint: "steer|followUp|wait" },
|
|
1807
|
+
},
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
promptChain = promptChain.then(async () => {
|
|
1812
|
+
try {
|
|
1813
|
+
await promptTurn(message.params);
|
|
1814
|
+
write({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
write({
|
|
1817
|
+
jsonrpc: "2.0",
|
|
1818
|
+
id: message.id,
|
|
1819
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
} else if (message.method === "session/cancel") {
|
|
1824
|
+
// Nothing to do for the stub.
|
|
1825
|
+
} else {
|
|
1826
|
+
write({
|
|
1827
|
+
jsonrpc: "2.0",
|
|
1828
|
+
id: message.id,
|
|
1829
|
+
error: { code: -32601, message: `Method not found: ${message.method}` },
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
if (message.id === undefined) return;
|
|
1836
|
+
pending.get(String(message.id))?.(message.result);
|
|
1837
|
+
pending.delete(String(message.id));
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// A killed peer records that it left exactly once, so a test can prove the close.
|
|
1841
|
+
let loggedExit = false;
|
|
1842
|
+
function logExit() {
|
|
1843
|
+
if (loggedExit) return;
|
|
1844
|
+
loggedExit = true;
|
|
1845
|
+
log(`exit ${process.pid}`);
|
|
1846
|
+
}
|
|
1847
|
+
process.on("exit", logExit);
|
|
1848
|
+
process.on("SIGTERM", () => {
|
|
1849
|
+
logExit();
|
|
1850
|
+
process.exit(0);
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
const decoder = new TextDecoder();
|
|
1854
|
+
let buffer = "";
|
|
1855
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
1856
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1857
|
+
let newline = buffer.indexOf("\n");
|
|
1858
|
+
while (newline >= 0) {
|
|
1859
|
+
const line = buffer.slice(0, newline).trim();
|
|
1860
|
+
buffer = buffer.slice(newline + 1);
|
|
1861
|
+
if (line.length > 0) {
|
|
1862
|
+
try {
|
|
1863
|
+
handleMessage(JSON.parse(line) as Record<string, unknown>);
|
|
1864
|
+
} catch {
|
|
1865
|
+
// Ignore malformed frames.
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
newline = buffer.indexOf("\n");
|
|
1869
|
+
}
|
|
1870
|
+
}
|