@magic-spells/constellation 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/constellation/agent/AGENT-GUIDANCE.md +22 -0
- package/constellation/component/COMPONENT-EDITABLE.md +9 -0
- package/constellation/component/COMPONENT-STATUS-PILL.md +9 -0
- package/constellation/component/COMPONENT-SYNC-PILL.md +9 -0
- package/constellation/decision/DECISION-FEATURE-RELEASE-TYPES.md +50 -0
- package/constellation/decision/DECISION-NATIVE-DECISION-TYPE.md +58 -0
- package/constellation/diagram/DIAGRAM-ARCHITECTURE.md +28 -0
- package/constellation/doc/DOC-CARD-TYPES.md +39 -0
- package/constellation/doc/DOC-CHANGE-TRACKING.md +26 -0
- package/constellation/doc/DOC-CONNECTED-REPOS.md +25 -0
- package/constellation/doc/DOC-DIAGRAMS.md +22 -0
- package/constellation/doc/DOC-FILE-FORMAT.md +54 -0
- package/constellation/doc/DOC-LINT-CODES.md +35 -0
- package/constellation/doc/DOC-MCP-SERVER.md +59 -0
- package/constellation/doc/DOC-MCP-UPGRADES.md +57 -0
- package/constellation/external/EXTERNAL-AJV.md +7 -0
- package/constellation/external/EXTERNAL-GIT.md +7 -0
- package/constellation/external/EXTERNAL-GRAY-MATTER.md +7 -0
- package/constellation/external/EXTERNAL-MCP-SDK.md +7 -0
- package/constellation/external/EXTERNAL-ZOD.md +7 -0
- package/constellation/file/FILE-CLI.md +9 -0
- package/constellation/file/FILE-CODE.md +9 -0
- package/constellation/file/FILE-EXTRACT.md +9 -0
- package/constellation/file/FILE-GIT.md +9 -0
- package/constellation/file/FILE-HANDLES.md +9 -0
- package/constellation/file/FILE-INDEXER.md +11 -0
- package/constellation/file/FILE-LINT.md +9 -0
- package/constellation/file/FILE-MCP-SEARCH.md +12 -0
- package/constellation/file/FILE-MCP-SERVER.md +9 -0
- package/constellation/file/FILE-PARSE.md +9 -0
- package/constellation/file/FILE-RENAME.md +18 -0
- package/constellation/file/FILE-REPOS.md +9 -0
- package/constellation/file/FILE-RESOLVE.md +9 -0
- package/constellation/file/FILE-SCAFFOLD.md +12 -0
- package/constellation/file/FILE-SERVE.md +9 -0
- package/constellation/file/FILE-SYNC.md +9 -0
- package/constellation/file/FILE-TYPES.md +9 -0
- package/constellation/file/FILE-VALIDATE.md +11 -0
- package/constellation/file/FILE-WRITER.md +11 -0
- package/constellation/flow/FLOW-DRIFT-DETECT.md +11 -0
- package/constellation/flow/FLOW-LINT-PIPELINE.md +14 -0
- package/constellation/flow/FLOW-SYNC-PLAN.md +13 -0
- package/constellation/page/PAGE-VIEWER-CARD.md +9 -0
- package/constellation/page/PAGE-VIEWER-HOME.md +9 -0
- package/constellation/plan.md +59 -0
- package/constellation/test/TEST-SUITE.md +9 -0
- package/dist/cli/index.js +37 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/code.d.ts +4 -2
- package/dist/core/code.js +22 -7
- package/dist/core/code.js.map +1 -1
- package/dist/core/git.d.ts +2 -0
- package/dist/core/git.js +6 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/handles.d.ts +1 -1
- package/dist/core/handles.js +4 -1
- package/dist/core/handles.js.map +1 -1
- package/dist/core/indexer.js +1 -1
- package/dist/core/rename.d.ts +24 -0
- package/dist/core/rename.js +72 -0
- package/dist/core/rename.js.map +1 -0
- package/dist/core/repos.d.ts +2 -0
- package/dist/core/repos.js +5 -1
- package/dist/core/repos.js.map +1 -1
- package/dist/core/sync.d.ts +2 -0
- package/dist/core/sync.js +3 -1
- package/dist/core/sync.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +3 -2
- package/dist/core/types.js.map +1 -1
- package/dist/core/validate.js +14 -1
- package/dist/core/validate.js.map +1 -1
- package/dist/core/writer.d.ts +29 -0
- package/dist/core/writer.js +98 -3
- package/dist/core/writer.js.map +1 -1
- package/dist/mcp/search.d.ts +2 -2
- package/dist/mcp/search.js +27 -5
- package/dist/mcp/search.js.map +1 -1
- package/dist/mcp/server.js +311 -101
- package/dist/mcp/server.js.map +1 -1
- package/dist/serve/server.js +6 -7
- package/dist/serve/server.js.map +1 -1
- package/examples/constellation/decision/DECISION-NO-HARD-DELETE.md +31 -0
- package/examples/constellation/feature/FEATURE-AUTO-ASSIGNMENT.md +24 -0
- package/examples/constellation/release/RELEASE-V1-1-0.md +14 -0
- package/package.json +2 -2
- package/schemas/decision.json +14 -0
- package/schemas/doc.json +1 -1
- package/schemas/feature.json +19 -0
- package/schemas/plan.json +1 -1
- package/schemas/release.json +14 -0
- package/skill/SKILL.md +39 -7
- package/skill/methodology.md +10 -3
- package/skill/types/decision.md +59 -0
- package/skill/types/doc.md +2 -2
- package/skill/types/feature.md +53 -0
- package/skill/types/plan.md +1 -1
- package/skill/types/release.md +37 -0
- package/viewer/dist/assets/{arc-YuzGWkw8.js → arc-DcXsmPgw.js} +1 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-CXZxmtF5.js +1 -0
- package/viewer/dist/assets/{architectureDiagram-3BPJPVTR-BgksqMlF.js → architectureDiagram-3BPJPVTR-Cfq3OC2M.js} +1 -1
- package/viewer/dist/assets/{blockDiagram-GPEHLZMM-Bc6JILFK.js → blockDiagram-GPEHLZMM-CPuRJqEW.js} +1 -1
- package/viewer/dist/assets/{c4Diagram-AAUBKEIU-CRVt42df.js → c4Diagram-AAUBKEIU-CcVcItWo.js} +1 -1
- package/viewer/dist/assets/channel-DjvAjJNc.js +1 -0
- package/viewer/dist/assets/{chunk-2J33WTMH-CpuwKeX6.js → chunk-2J33WTMH-C_N821rH.js} +1 -1
- package/viewer/dist/assets/{chunk-3OPIFGDE-BG_ijJhE.js → chunk-3OPIFGDE-C5c2MY1i.js} +1 -1
- package/viewer/dist/assets/{chunk-4BX2VUAB-Bgz1tnDP.js → chunk-4BX2VUAB-BjYatXZ-.js} +1 -1
- package/viewer/dist/assets/{chunk-55IACEB6-NdWURd2K.js → chunk-55IACEB6-A3JXj2W_.js} +1 -1
- package/viewer/dist/assets/{chunk-5ZQYHXKU-wzW5x_7z.js → chunk-5ZQYHXKU-zDiW1Z3e.js} +1 -1
- package/viewer/dist/assets/{chunk-727SXJPM-BxKeJXYf.js → chunk-727SXJPM-CI3VBaXr.js} +1 -1
- package/viewer/dist/assets/{chunk-AQP2D5EJ-gPm6xhPX.js → chunk-AQP2D5EJ-DHpkJ1wF.js} +1 -1
- package/viewer/dist/assets/{chunk-BSJP7CBP-C30ZXEUD.js → chunk-BSJP7CBP-DXx1-_Zs.js} +1 -1
- package/viewer/dist/assets/{chunk-CSCIHK7Q-C9h7yfl1.js → chunk-CSCIHK7Q-DQOe6SQo.js} +6 -6
- package/viewer/dist/assets/{chunk-FMBD7UC4-BcSnkYc3.js → chunk-FMBD7UC4-DSvwBCfP.js} +1 -1
- package/viewer/dist/assets/{chunk-KSCS5N6A-rxSD5-MD.js → chunk-KSCS5N6A-C3haQOao.js} +1 -1
- package/viewer/dist/assets/{chunk-L5ZTLDWV-DGwFsVWy.js → chunk-L5ZTLDWV-DyHfVaRK.js} +1 -1
- package/viewer/dist/assets/{chunk-LZXEDZCA-BY3sTuvt.js → chunk-LZXEDZCA-CtMQqok_.js} +2 -2
- package/viewer/dist/assets/{chunk-ND2GUHAM-PqmKwdzq.js → chunk-ND2GUHAM-Cfp2zN_i.js} +1 -1
- package/viewer/dist/assets/{chunk-NZK2D7GU-BAZbzxhl.js → chunk-NZK2D7GU-Cn753oIP.js} +1 -1
- package/viewer/dist/assets/{chunk-O5CBEL6O-SvZfZ-0j.js → chunk-O5CBEL6O-BO32_B2k.js} +1 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-uc4X3bta.js +1 -0
- package/viewer/dist/assets/chunk-WU5MYG2G-D-wARmLR.js +1 -0
- package/viewer/dist/assets/{chunk-XPW4576I-1OXPbRQX.js → chunk-XPW4576I-DQFQAfwD.js} +1 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-CosoTts2.js +1 -0
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-CosoTts2.js +1 -0
- package/viewer/dist/assets/{cose-bilkent-S5V4N54A-DtCvLpah.js → cose-bilkent-S5V4N54A-Ibe8V5Cm.js} +1 -1
- package/viewer/dist/assets/{dagre-BM42HDAG-B55Ilz10.js → dagre-BM42HDAG-BlOzR9WP.js} +1 -1
- package/viewer/dist/assets/{diagram-2AECGRRQ-ClqSMarp.js → diagram-2AECGRRQ-l8omQVTU.js} +1 -1
- package/viewer/dist/assets/{diagram-5GNKFQAL-Dx74UdxY.js → diagram-5GNKFQAL-_rHQzAtO.js} +1 -1
- package/viewer/dist/assets/{diagram-KO2AKTUF-Vj0buH8e.js → diagram-KO2AKTUF-DCLRVRMY.js} +1 -1
- package/viewer/dist/assets/{diagram-LMA3HP47-DpEm8RZd.js → diagram-LMA3HP47-14HnEIqk.js} +1 -1
- package/viewer/dist/assets/{diagram-OG6HWLK6-Crl3BdwC.js → diagram-OG6HWLK6-CVKNCbkR.js} +1 -1
- package/viewer/dist/assets/{dist-C2uHOEFB.js → dist-BcVVfWxM.js} +1 -1
- package/viewer/dist/assets/{erDiagram-TEJ5UH35-B-Mf-EHP.js → erDiagram-TEJ5UH35-CNjpEfJD.js} +1 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-Dv27utnc.js +1 -0
- package/viewer/dist/assets/{flowDiagram-I6XJVG4X-B9stChWN.js → flowDiagram-I6XJVG4X-Bvekb5gu.js} +1 -1
- package/viewer/dist/assets/{ganttDiagram-6RSMTGT7-DzWlclwM.js → ganttDiagram-6RSMTGT7-2TpeWF0N.js} +1 -1
- package/viewer/dist/assets/{gitGraph-WXDBUCRP-CdVI3GTp.js → gitGraph-WXDBUCRP-CPfybYvf.js} +1 -1
- package/viewer/dist/assets/{gitGraphDiagram-PVQCEYII-Dn9yRc1N.js → gitGraphDiagram-PVQCEYII-BWZNJ9lY.js} +1 -1
- package/viewer/dist/assets/index-BEJYsfzU.css +2 -0
- package/viewer/dist/assets/{index-BsGRgBOD.js → index-Cqx2pndu.js} +3 -3
- package/viewer/dist/assets/{info-J43DQDTF-CXV3Mm9T.js → info-J43DQDTF-CxgngY4X.js} +1 -1
- package/viewer/dist/assets/{infoDiagram-5YYISTIA-zIUka_J2.js → infoDiagram-5YYISTIA-CARF_viY.js} +1 -1
- package/viewer/dist/assets/{ishikawaDiagram-YF4QCWOH-DsZwNuE3.js → ishikawaDiagram-YF4QCWOH-DcxZvtur.js} +1 -1
- package/viewer/dist/assets/{journeyDiagram-JHISSGLW-BhSTSB2s.js → journeyDiagram-JHISSGLW-Wu7zolY5.js} +1 -1
- package/viewer/dist/assets/{kanban-definition-UN3LZRKU-sCAldj86.js → kanban-definition-UN3LZRKU--LMKLf5s.js} +1 -1
- package/viewer/dist/assets/{line-D7b3fopb.js → line-DH7DV7uw.js} +1 -1
- package/viewer/dist/assets/{linear-BWlvVc_d.js → linear-C6xJ2LrZ.js} +1 -1
- package/viewer/dist/assets/{mermaid-parser.core-BdEYBvaj.js → mermaid-parser.core-DkqLwTDu.js} +2 -2
- package/viewer/dist/assets/{mermaid.core-C9bGWeJU.js → mermaid.core-H-eI9axh.js} +3 -3
- package/viewer/dist/assets/{mindmap-definition-RKZ34NQL-DbTvvku-.js → mindmap-definition-RKZ34NQL-xjt8VE8m.js} +1 -1
- package/viewer/dist/assets/{packet-YPE3B663-DRfLruyq.js → packet-YPE3B663-BX0W_wIa.js} +1 -1
- package/viewer/dist/assets/{pie-LRSECV5Y-47xXMkK-.js → pie-LRSECV5Y-B8cD_uBh.js} +1 -1
- package/viewer/dist/assets/{pieDiagram-4H26LBE5-Ckno9E1M.js → pieDiagram-4H26LBE5-DpM-20zS.js} +1 -1
- package/viewer/dist/assets/{quadrantDiagram-W4KKPZXB-XB5kJ_2c.js → quadrantDiagram-W4KKPZXB-CLS4_FQ4.js} +1 -1
- package/viewer/dist/assets/{radar-GUYGQ44K-CLwIH5Zr.js → radar-GUYGQ44K-CQKcm4b5.js} +1 -1
- package/viewer/dist/assets/{requirementDiagram-4Y6WPE33-D3fApL0X.js → requirementDiagram-4Y6WPE33-ESLvOyQ5.js} +1 -1
- package/viewer/dist/assets/{sankeyDiagram-5OEKKPKP-6UdaRhuZ.js → sankeyDiagram-5OEKKPKP-BT6w9JdP.js} +1 -1
- package/viewer/dist/assets/{sequenceDiagram-3UESZ5HK-ClIrrs0w.js → sequenceDiagram-3UESZ5HK-DJNAaEbv.js} +1 -1
- package/viewer/dist/assets/{src-BPb55SrS.js → src-ituc2uBy.js} +1 -1
- package/viewer/dist/assets/{stateDiagram-AJRCARHV-CCsrcv5R.js → stateDiagram-AJRCARHV-Bw7VoMlA.js} +1 -1
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-DZdCGd2U.js +1 -0
- package/viewer/dist/assets/{timeline-definition-PNZ67QCA-BkiWQlVI.js → timeline-definition-PNZ67QCA-Ddr17PAT.js} +1 -1
- package/viewer/dist/assets/{treeView-BLDUP644-m1mGpAE8.js → treeView-BLDUP644-mqjp8QBZ.js} +1 -1
- package/viewer/dist/assets/{treemap-LRROVOQU-Bapvwi0Q.js → treemap-LRROVOQU-BeaxV5lr.js} +1 -1
- package/viewer/dist/assets/{vennDiagram-CIIHVFJN-BlH71Nmx.js → vennDiagram-CIIHVFJN-ySQYfWol.js} +1 -1
- package/viewer/dist/assets/{wardley-L42UT6IY-CPRSoKgV.js → wardley-L42UT6IY-BvtBpxdo.js} +1 -1
- package/viewer/dist/assets/{wardleyDiagram-YWT4CUSO-C-ptFJ9t.js → wardleyDiagram-YWT4CUSO-DRZFQiXX.js} +1 -1
- package/viewer/dist/assets/{xychartDiagram-2RQKCTM6-C4rfeH4W.js → xychartDiagram-2RQKCTM6-kXPrRy2n.js} +1 -1
- package/viewer/dist/index.html +2 -2
- package/docs/001-file-format.md +0 -265
- package/docs/002-mcp.md +0 -186
- package/docs/003-mcp-upgrade-plan.md +0 -138
- package/viewer/dist/assets/architecture-7EHR7CIX-BAboPvK1.js +0 -1
- package/viewer/dist/assets/channel-D8YnBgqu.js +0 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-ClYnv9g5.js +0 -1
- package/viewer/dist/assets/chunk-WU5MYG2G-CvL2Yleq.js +0 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-B8YUd1jx.js +0 -1
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-B8YUd1jx.js +0 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-CZ5gxAxG.js +0 -1
- package/viewer/dist/assets/index-Bqs39Itl.css +0 -2
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-C1phrPNi.js +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -9,12 +9,13 @@ import { loadPlan } from '../core/indexer.js';
|
|
|
9
9
|
import { lintPlan } from '../core/lint.js';
|
|
10
10
|
import { resolvePlanDir } from '../core/resolve.js';
|
|
11
11
|
import { TYPE_NAMES } from '../core/types.js';
|
|
12
|
-
import { changedFilesSince, diffPlan, headSha, planDirty, planLog, readSyncPoint, writeSyncPoint, } from '../core/git.js';
|
|
12
|
+
import { changedFilesSince, diffPlan, headSha, planDirty, planLog, readSyncPoint, resolveCommit, writeSyncPoint, } from '../core/git.js';
|
|
13
13
|
import { computeSyncStatus } from '../core/sync.js';
|
|
14
14
|
import { boundPathsForCard, resolveCodeForCard } from '../core/code.js';
|
|
15
15
|
import { searchCards } from './search.js';
|
|
16
|
-
import { applyCardPatch, bodyHeadingTexts, createCardFile, deepMerge, relPathForHandle, replaceBodySection, reservedFieldKeys,
|
|
17
|
-
import {
|
|
16
|
+
import { applyCardPatch, bodyHeadingTexts, createCardFile, deepMerge, mutateCardFile, relPathForHandle, replaceBodySection, reservedFieldKeys, withAppendedNote, } from '../core/writer.js';
|
|
17
|
+
import { renameCard, RenameCardError } from '../core/rename.js';
|
|
18
|
+
import { connectedRepoToFm, connectedReposFromFrontmatter, listConnectedRepos, readConnectedRepos, removeConnectedRepoEntry, resolveConnectedRepo, upsertConnectedRepo, } from '../core/repos.js';
|
|
18
19
|
const INSTRUCTIONS = `# Constellation MCP
|
|
19
20
|
|
|
20
21
|
Constellation is this project's durable, cross-session memory for AI agents — treat it as
|
|
@@ -40,9 +41,9 @@ Retrieval is hydrated: get_card / search / traverse can return connected cards w
|
|
|
40
41
|
their FULL frontmatter and body in one call (connected: "full"). Use that when you are
|
|
41
42
|
about to work on an area; use "summary" for orientation. get_card can also hand back the
|
|
42
43
|
CODE a card is bound to — code: "paths" returns the resolved file paths of its connected
|
|
43
|
-
FILE cards (path:) plus its own code_refs; code: "direct" attaches their contents (
|
|
44
|
-
binaries/lockfiles/generated skipped) so a background coder starts from
|
|
45
|
-
code in one call. assemble turns a delta (or a handle set) into a work package: the changed
|
|
44
|
+
FILE cards (path:) plus its own code_refs; code: "direct" attaches their contents (over-cap
|
|
45
|
+
files truncated, binaries/lockfiles/generated skipped) so a background coder starts from
|
|
46
|
+
intent + current code in one call. assemble turns a delta (or a handle set) into a work package: the changed
|
|
46
47
|
cards + their neighborhood (full) + bound code + a heuristic build order + FILE-DISJOINT
|
|
47
48
|
units you can fan out one sub-agent per, with no two touching the same file.
|
|
48
49
|
|
|
@@ -52,10 +53,17 @@ Body-only updates never reformat frontmatter. Prefer SMALL, cheap writes over re
|
|
|
52
53
|
whole card — make the honest update the easy one: append_note adds an append-only typed note
|
|
53
54
|
(decision / gotcha / state / deviation / verified) with no full-body rewrite; edit_section
|
|
54
55
|
replaces a single ## section in place. Reach for these to record a correction the moment you
|
|
55
|
-
learn it, so cards stay true instead of drifting.
|
|
56
|
+
learn it, so cards stay true instead of drifting. Notes are retrievable memory: search
|
|
57
|
+
matches note text, and list_notes lists them across cards by kind — every gotcha or decision
|
|
58
|
+
in one call. A decision note is for a choice local to that one card; a decision that shaped
|
|
59
|
+
SEVERAL cards gets its own DECISION card, connected to every card it shaped (the successor
|
|
60
|
+
of a replaced decision points at it via supersedes — never delete decision history). To
|
|
61
|
+
rename a handle, use rename_card: it moves the file and rewrites every
|
|
62
|
+
reference plan-wide (connections, frontmatter values, [[links]], mermaid node IDs) as whole
|
|
63
|
+
tokens — never delete-and-recreate to rename.
|
|
56
64
|
|
|
57
65
|
describe_type is the type reference, served by this server: call it with no args for the
|
|
58
|
-
catalog of all
|
|
66
|
+
catalog of all 20 card types, or with a type (e.g. describe_type PAGE) for that type's
|
|
59
67
|
frontmatter schema + a golden example. Consult it before authoring a type you haven't used
|
|
60
68
|
this session — you don't need the authoring skill loaded to get the fields right.
|
|
61
69
|
|
|
@@ -91,11 +99,17 @@ the affected neighborhood (get_card / traverse / search, connected: "full"), the
|
|
|
91
99
|
update the cards so they describe the desired END STATE (work that isn't built yet is
|
|
92
100
|
status: planned), wiring every connection between the affected cards. Show that set of card
|
|
93
101
|
changes as the proposal; on approval, bring the CODE up to match via the sync loop above.
|
|
102
|
+
Past the initial build, model iteration explicitly: a coherent slice of future work is a
|
|
103
|
+
FEATURE card connected to every card it adds or touches (intent/scope/acceptance in the
|
|
104
|
+
body, branch: while in flight, status planned → building → built as it merges), optionally
|
|
105
|
+
targeting a RELEASE card (a version milestone; features point at it via release:). A RELEASE
|
|
106
|
+
body is theme + upgrade notes, NEVER a changelog — what shipped is git's job. Neither is a
|
|
107
|
+
ticket tracker: work that is one card's status flip needs no FEATURE card.
|
|
94
108
|
FINISH by reconciling — re-read the touched cards against the code, run check_integrity so
|
|
95
109
|
no affected card is left an orphan and every connection is set, bump status (planned →
|
|
96
110
|
building → built → verified), commit, and set_sync_point. In plan mode the write tools are
|
|
97
|
-
unavailable by design (the read tools — get_card, list_cards, search, traverse,
|
|
98
|
-
describe_type, check_integrity, diff_plan, plan_log, stale_report, check_sync,
|
|
111
|
+
unavailable by design (the read tools — get_card, list_cards, list_notes, search, traverse,
|
|
112
|
+
assemble, describe_type, check_integrity, diff_plan, plan_log, stale_report, check_sync,
|
|
99
113
|
list_connected_repos — are marked read-only and stay available), so spend plan mode READING:
|
|
100
114
|
pull in as much of the relevant plan as you can
|
|
101
115
|
(traverse from the entry points, connected: "full") to build a strong model of the project
|
|
@@ -106,7 +120,11 @@ For migrations or large scaffolds, use create_cards and add_connections (batched
|
|
|
106
120
|
lint pass) instead of many single calls — connections between cards in the same batch
|
|
107
121
|
resolve without transient "does not resolve" errors. A card is created even when issues
|
|
108
122
|
are returned (issues are lint state, not failure). check_integrity reports orphans
|
|
109
|
-
(zero-connection cards), and list_cards connected:false lists them.
|
|
123
|
+
(zero-connection cards), and list_cards connected:false lists them. The BACKLOG view —
|
|
124
|
+
everything not yet built — is list_cards status: ["planned", "building", "none"] ("none" =
|
|
125
|
+
cards with no status set, which usually means unbuilt). traverse takes the same status
|
|
126
|
+
filter as a post-filter: the walk still passes through built cards, so a built hub never
|
|
127
|
+
hides the planned work behind it.
|
|
110
128
|
|
|
111
129
|
The plan folder is found by walking up from the working directory, BOUNDED by the repo
|
|
112
130
|
root (it never adopts another repo's plan). If no plan exists in this repo (tools return
|
|
@@ -281,9 +299,7 @@ async function computeStaleCards(root, index, base) {
|
|
|
281
299
|
marker = null;
|
|
282
300
|
}
|
|
283
301
|
const fallback = base ?? marker ?? null;
|
|
284
|
-
const
|
|
285
|
-
const noBaseline = [];
|
|
286
|
-
let checked = 0;
|
|
302
|
+
const claims = [];
|
|
287
303
|
for (const card of index.cards.values()) {
|
|
288
304
|
const verifiedSha = typeof card.frontmatter.verified_sha === 'string'
|
|
289
305
|
? card.frontmatter.verified_sha
|
|
@@ -293,20 +309,39 @@ async function computeStaleCards(root, index, base) {
|
|
|
293
309
|
continue;
|
|
294
310
|
if (boundPathsForCard(index, card).length === 0)
|
|
295
311
|
continue;
|
|
296
|
-
checked++;
|
|
297
|
-
const baseline = verifiedSha ?? fallback ?? undefined;
|
|
298
312
|
const resolved = await resolveCodeForCard(root, index, card, 'paths');
|
|
299
|
-
|
|
300
|
-
|
|
313
|
+
claims.push({
|
|
314
|
+
card,
|
|
315
|
+
baseline: verifiedSha ?? fallback,
|
|
316
|
+
baseline_source: verifiedSha ? 'verified_sha' : base ? 'argument' : 'sync-marker',
|
|
317
|
+
paths: resolved.files.map((f) => f.path),
|
|
318
|
+
missing: resolved.files.filter((f) => !f.exists).map((f) => f.path),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// Pass 2: one git call per DISTINCT baseline (usually just the sync marker),
|
|
322
|
+
// not one per card — a plan with 100 verified cards must not spawn 100 diffs.
|
|
323
|
+
const changedBy = new Map();
|
|
324
|
+
const baselines = new Set(claims.map((c) => c.baseline).filter((b) => Boolean(b)));
|
|
325
|
+
for (const baseline of baselines) {
|
|
326
|
+
const union = [
|
|
327
|
+
...new Set(claims.filter((c) => c.baseline === baseline).flatMap((c) => c.paths)),
|
|
328
|
+
];
|
|
329
|
+
try {
|
|
330
|
+
changedBy.set(baseline, await changedFilesSince(root, baseline, union));
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
changedBy.set(baseline, 'unreachable');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const stale = [];
|
|
337
|
+
const noBaseline = [];
|
|
338
|
+
for (const { card, baseline, baseline_source, paths, missing } of claims) {
|
|
301
339
|
if (!baseline) {
|
|
302
340
|
noBaseline.push({ handle: card.handle, status: card.status ?? null, files: paths });
|
|
303
341
|
continue;
|
|
304
342
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
changed = await changedFilesSince(root, baseline, paths);
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
343
|
+
const changed = changedBy.get(baseline);
|
|
344
|
+
if (changed === 'unreachable') {
|
|
310
345
|
noBaseline.push({
|
|
311
346
|
handle: card.handle,
|
|
312
347
|
status: card.status ?? null,
|
|
@@ -322,13 +357,13 @@ async function computeStaleCards(root, index, base) {
|
|
|
322
357
|
name: card.name ?? null,
|
|
323
358
|
status: card.status ?? null,
|
|
324
359
|
baseline: baseline.slice(0, 12),
|
|
325
|
-
baseline_source
|
|
360
|
+
baseline_source,
|
|
326
361
|
changed_files: changedFiles,
|
|
327
362
|
missing_files: missing,
|
|
328
363
|
});
|
|
329
364
|
}
|
|
330
365
|
}
|
|
331
|
-
return { checked, stale, no_baseline: noBaseline };
|
|
366
|
+
return { checked: claims.length, stale, no_baseline: noBaseline };
|
|
332
367
|
}
|
|
333
368
|
// Rough dependency tiers for assemble's suggested build order: data first, then
|
|
334
369
|
// contracts, then surfaces. Connections are undirected, so this is a heuristic
|
|
@@ -387,6 +422,17 @@ function partitionByFiles(seeds, filesBy) {
|
|
|
387
422
|
const detailSchema = z.enum(['none', 'summary', 'full']);
|
|
388
423
|
const typeSchema = z.enum(TYPE_NAMES);
|
|
389
424
|
const statusSchema = z.enum(['planned', 'building', 'built', 'verified']);
|
|
425
|
+
// Filter variant: "none" selects cards with no status at all — unset usually
|
|
426
|
+
// means nobody has claimed the card is built, so backlog queries want it.
|
|
427
|
+
const statusFilterSchema = z.enum(['planned', 'building', 'built', 'verified', 'none']);
|
|
428
|
+
const statusesSchema = z
|
|
429
|
+
.union([statusFilterSchema, z.array(statusFilterSchema).min(1)])
|
|
430
|
+
.optional();
|
|
431
|
+
function statusSetOf(status) {
|
|
432
|
+
if (status === undefined)
|
|
433
|
+
return null;
|
|
434
|
+
return new Set(Array.isArray(status) ? status : [status]);
|
|
435
|
+
}
|
|
390
436
|
const noteKindSchema = z.enum(['decision', 'gotcha', 'state', 'deviation', 'verified']);
|
|
391
437
|
const codeModeSchema = z.enum(['none', 'paths', 'direct']);
|
|
392
438
|
const repoSchema = z
|
|
@@ -504,7 +550,7 @@ export function buildServer(options = {}) {
|
|
|
504
550
|
});
|
|
505
551
|
server.registerTool('get_card', {
|
|
506
552
|
annotations: { readOnlyHint: true },
|
|
507
|
-
description: 'Fetch one card by handle, optionally with all connected cards hydrated. connected: "full" returns the complete frontmatter and body of every connected card — use it when about to work on an area. code: "paths" returns the resolved file paths the card is bound to (connected FILE cards plus code_refs); code: "direct" attaches their contents (
|
|
553
|
+
description: 'Fetch one card by handle, optionally with all connected cards hydrated. connected: "full" returns the complete frontmatter and body of every connected card — use it when about to work on an area. code: "paths" returns the resolved file paths the card is bound to (connected FILE cards plus code_refs); code: "direct" attaches their contents (over-cap files truncated with truncated:true; binaries/lockfiles/generated skipped) so a background coder starts from intent + current code in one call.',
|
|
508
554
|
inputSchema: {
|
|
509
555
|
handle: z.string(),
|
|
510
556
|
connected: detailSchema.optional().describe('default: summary'),
|
|
@@ -548,11 +594,11 @@ export function buildServer(options = {}) {
|
|
|
548
594
|
}));
|
|
549
595
|
server.registerTool('list_cards', {
|
|
550
596
|
annotations: { readOnlyHint: true },
|
|
551
|
-
description: 'Catalog of cards filtered by type, kind, status, and/or connectedness. connected:false returns orphans (cards with zero connections). Returns summaries (handle, type, kind, name, status).',
|
|
597
|
+
description: 'Catalog of cards filtered by type, kind, status, and/or connectedness. status takes one value or a list; "none" selects cards with no status at all — status: ["planned", "building", "none"] is the backlog view (everything not yet built). connected:false returns orphans (cards with zero connections). Returns summaries (handle, type, kind, name, status).',
|
|
552
598
|
inputSchema: {
|
|
553
599
|
types: z.array(typeSchema).optional(),
|
|
554
600
|
kind: z.string().optional(),
|
|
555
|
-
status:
|
|
601
|
+
status: statusesSchema.describe('one status or a list; "none" = cards with no status set'),
|
|
556
602
|
connected: z
|
|
557
603
|
.boolean()
|
|
558
604
|
.optional()
|
|
@@ -563,11 +609,12 @@ export function buildServer(options = {}) {
|
|
|
563
609
|
}, withPlan(async (root, { types, kind, status, connected, limit }) => {
|
|
564
610
|
const index = await loadPlan(root);
|
|
565
611
|
const typeFilter = types && types.length > 0 ? new Set(types) : null;
|
|
612
|
+
const statusFilter = statusSetOf(status);
|
|
566
613
|
const isConnected = (h) => (index.connectedHandles.get(h)?.size ?? 0) > 0;
|
|
567
614
|
const matched = [...index.cards.values()]
|
|
568
615
|
.filter((c) => !typeFilter || typeFilter.has(c.type))
|
|
569
616
|
.filter((c) => !kind || c.kind === kind)
|
|
570
|
-
.filter((c) => !
|
|
617
|
+
.filter((c) => !statusFilter || statusFilter.has(c.status ?? 'none'))
|
|
571
618
|
.filter((c) => connected === undefined || isConnected(c.handle) === connected)
|
|
572
619
|
.sort((a, b) => a.handle.localeCompare(b.handle));
|
|
573
620
|
return ok({
|
|
@@ -599,15 +646,16 @@ export function buildServer(options = {}) {
|
|
|
599
646
|
}));
|
|
600
647
|
server.registerTool('traverse', {
|
|
601
648
|
annotations: { readOnlyHint: true },
|
|
602
|
-
description: 'Breadth-first walk of the connection graph from one or more starting handles. Seed it with diff_plan output for impact analysis. detail: "full" includes frontmatter and body of every reached card.',
|
|
649
|
+
description: 'Breadth-first walk of the connection graph from one or more starting handles. Seed it with diff_plan output for impact analysis. detail: "full" includes frontmatter and body of every reached card. status filters the RESULT only — the walk still passes through non-matching cards, so a built hub never hides the planned work behind it (status: ["planned", "building", "none"] = open work in this neighborhood). types, by contrast, prunes the walk itself.',
|
|
603
650
|
inputSchema: {
|
|
604
651
|
start: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
605
652
|
depth: z.number().int().min(0).max(5).optional().describe('default: 2'),
|
|
606
653
|
types: z.array(typeSchema).optional(),
|
|
654
|
+
status: statusesSchema.describe('post-filter on returned cards; "none" = no status set. The walk passes through non-matching cards.'),
|
|
607
655
|
detail: z.enum(['summary', 'full']).optional().describe('default: summary'),
|
|
608
656
|
repo: repoSchema,
|
|
609
657
|
},
|
|
610
|
-
}, withPlan(async (root, { start, depth, types, detail }) => {
|
|
658
|
+
}, withPlan(async (root, { start, depth, types, status, detail }) => {
|
|
611
659
|
const index = await loadPlan(root);
|
|
612
660
|
const starts = (Array.isArray(start) ? start : [start]).map((s) => s.toUpperCase());
|
|
613
661
|
const missing = starts.filter((s) => !index.cards.has(s));
|
|
@@ -637,7 +685,12 @@ export function buildServer(options = {}) {
|
|
|
637
685
|
}
|
|
638
686
|
frontier = next;
|
|
639
687
|
}
|
|
688
|
+
// Status is a post-filter: the walk above passed THROUGH every card, so
|
|
689
|
+
// a built hub in the middle never hides open work behind it.
|
|
690
|
+
const statusFilter = statusSetOf(status);
|
|
640
691
|
const cards = [...distance.entries()]
|
|
692
|
+
.filter(([handle]) => !statusFilter ||
|
|
693
|
+
statusFilter.has(index.cards.get(handle).status ?? 'none'))
|
|
641
694
|
.map(([handle, dist]) => {
|
|
642
695
|
const card = index.cards.get(handle);
|
|
643
696
|
return {
|
|
@@ -646,7 +699,8 @@ export function buildServer(options = {}) {
|
|
|
646
699
|
};
|
|
647
700
|
})
|
|
648
701
|
.sort((a, b) => a.distance - b.distance || a.handle.localeCompare(b.handle));
|
|
649
|
-
const
|
|
702
|
+
const surviving = new Set(cards.map((c) => c.handle));
|
|
703
|
+
const connections = index.connections.filter((c) => surviving.has(c.a) && surviving.has(c.b));
|
|
650
704
|
return ok({ cards, connections, not_found: missing });
|
|
651
705
|
}));
|
|
652
706
|
server.registerTool('assemble', {
|
|
@@ -674,9 +728,12 @@ export function buildServer(options = {}) {
|
|
|
674
728
|
}, withPlan(async (root, { handles, base, depth, code }) => {
|
|
675
729
|
const index = await loadPlan(root);
|
|
676
730
|
let seeds;
|
|
731
|
+
let notFound = [];
|
|
677
732
|
let delta = null;
|
|
678
733
|
if (handles && handles.length > 0) {
|
|
679
|
-
|
|
734
|
+
const requested = [...new Set(handles.map((h) => h.toUpperCase()))];
|
|
735
|
+
seeds = requested.filter((h) => index.cards.has(h));
|
|
736
|
+
notFound = requested.filter((h) => !index.cards.has(h));
|
|
680
737
|
}
|
|
681
738
|
else {
|
|
682
739
|
let diff;
|
|
@@ -693,6 +750,7 @@ export function buildServer(options = {}) {
|
|
|
693
750
|
return ok({
|
|
694
751
|
base: delta,
|
|
695
752
|
seeds: [],
|
|
753
|
+
not_found: notFound,
|
|
696
754
|
units: [],
|
|
697
755
|
note: handles
|
|
698
756
|
? 'None of the given handles exist in the plan.'
|
|
@@ -744,6 +802,7 @@ export function buildServer(options = {}) {
|
|
|
744
802
|
return ok({
|
|
745
803
|
base: delta,
|
|
746
804
|
seeds,
|
|
805
|
+
not_found: notFound,
|
|
747
806
|
reached_handles: reached,
|
|
748
807
|
suggested_order: suggestedOrder,
|
|
749
808
|
units,
|
|
@@ -757,7 +816,7 @@ export function buildServer(options = {}) {
|
|
|
757
816
|
}));
|
|
758
817
|
server.registerTool('describe_type', {
|
|
759
818
|
annotations: { readOnlyHint: true },
|
|
760
|
-
description: 'The card-type reference, served straight from this package. Call with no args for the catalog — all
|
|
819
|
+
description: 'The card-type reference, served straight from this package. Call with no args for the catalog — all 20 types with their prefix, folder, and one-line purpose. Call with a type for everything needed to author one: the frontmatter JSON Schema (fields, which are required, descriptions) plus the golden example and authoring guidance. Use it before writing a card of a type you have not authored this session — it is the contract create_card/create_cards/update_card validate against (W002/W003), so you do not need the authoring skill loaded to get the fields right.',
|
|
761
820
|
inputSchema: {
|
|
762
821
|
type: typeSchema.optional().describe('omit for the full catalog'),
|
|
763
822
|
},
|
|
@@ -960,12 +1019,17 @@ export function buildServer(options = {}) {
|
|
|
960
1019
|
const touched = new Set();
|
|
961
1020
|
for (const [src, targets] of additions) {
|
|
962
1021
|
const card = index.cards.get(src);
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1022
|
+
// Merge against the file's CURRENT list inside the lock, not the index
|
|
1023
|
+
// snapshot — a concurrent write must not be clobbered.
|
|
1024
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1025
|
+
const existingList = Array.isArray(current.frontmatter.connections)
|
|
1026
|
+
? current.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
1027
|
+
: [];
|
|
1028
|
+
const merged = [...new Set([...existingList, ...targets])];
|
|
1029
|
+
return {
|
|
1030
|
+
frontmatter: applyCardPatch(current.frontmatter, { connections: merged }),
|
|
1031
|
+
};
|
|
1032
|
+
});
|
|
969
1033
|
touched.add(card.relPath);
|
|
970
1034
|
added += targets.size;
|
|
971
1035
|
}
|
|
@@ -1010,10 +1074,12 @@ export function buildServer(options = {}) {
|
|
|
1010
1074
|
return fail('STALE', `${card.handle} changed on disk`);
|
|
1011
1075
|
}
|
|
1012
1076
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1077
|
+
// Apply the patch to the file's CURRENT frontmatter inside the lock —
|
|
1078
|
+
// patch semantics compose with a concurrent write instead of undoing it.
|
|
1079
|
+
await mutateCardFile(card.filePath, (current) => ({
|
|
1080
|
+
frontmatter: patch ? applyCardPatch(current.frontmatter, patch) : undefined,
|
|
1081
|
+
body,
|
|
1082
|
+
}));
|
|
1017
1083
|
const lint = await lintPlan(root);
|
|
1018
1084
|
const updated = lint.index.cards.get(card.handle);
|
|
1019
1085
|
return ok({
|
|
@@ -1041,8 +1107,11 @@ export function buildServer(options = {}) {
|
|
|
1041
1107
|
const note = { kind, text };
|
|
1042
1108
|
if (sha)
|
|
1043
1109
|
note.sha = sha;
|
|
1044
|
-
|
|
1045
|
-
|
|
1110
|
+
// Append to the CURRENT notes list inside the lock so two concurrent
|
|
1111
|
+
// appends both land — the memory tool must never lose a note.
|
|
1112
|
+
await mutateCardFile(card.filePath, (current) => ({
|
|
1113
|
+
frontmatter: withAppendedNote(current.frontmatter, note),
|
|
1114
|
+
}));
|
|
1046
1115
|
const lint = await lintPlan(root);
|
|
1047
1116
|
const updated = lint.index.cards.get(card.handle);
|
|
1048
1117
|
return ok({
|
|
@@ -1053,6 +1122,56 @@ export function buildServer(options = {}) {
|
|
|
1053
1122
|
issues: issuesForFile(lint.issues, card.relPath),
|
|
1054
1123
|
});
|
|
1055
1124
|
}));
|
|
1125
|
+
server.registerTool('list_notes', {
|
|
1126
|
+
annotations: { readOnlyHint: true },
|
|
1127
|
+
description: 'All typed notes across the plan (the memory recorded via append_note), in handle order and newest-last within each card. Filter by kind — "show me every gotcha" / "every decision" in one call — and/or by handles. The cross-card view of the plan\'s memory; for one card\'s notes use get_card notes_kind/notes_limit.',
|
|
1128
|
+
inputSchema: {
|
|
1129
|
+
kind: noteKindSchema.optional().describe('return only notes of this kind'),
|
|
1130
|
+
handles: z
|
|
1131
|
+
.array(z.string())
|
|
1132
|
+
.optional()
|
|
1133
|
+
.describe('restrict to these cards; omit for the whole plan'),
|
|
1134
|
+
limit: z
|
|
1135
|
+
.number()
|
|
1136
|
+
.int()
|
|
1137
|
+
.min(1)
|
|
1138
|
+
.max(500)
|
|
1139
|
+
.optional()
|
|
1140
|
+
.describe('default 200'),
|
|
1141
|
+
repo: repoSchema,
|
|
1142
|
+
},
|
|
1143
|
+
}, withPlan(async (root, { kind, handles, limit }) => {
|
|
1144
|
+
const index = await loadPlan(root);
|
|
1145
|
+
const wanted = handles && handles.length > 0
|
|
1146
|
+
? new Set(handles.map((h) => h.toUpperCase()))
|
|
1147
|
+
: null;
|
|
1148
|
+
const notes = [];
|
|
1149
|
+
const sorted = [...index.cards.values()].sort((a, b) => a.handle.localeCompare(b.handle));
|
|
1150
|
+
for (const card of sorted) {
|
|
1151
|
+
if (wanted && !wanted.has(card.handle))
|
|
1152
|
+
continue;
|
|
1153
|
+
const list = Array.isArray(card.frontmatter.notes)
|
|
1154
|
+
? card.frontmatter.notes
|
|
1155
|
+
: [];
|
|
1156
|
+
for (const n of list) {
|
|
1157
|
+
if (!n || typeof n !== 'object')
|
|
1158
|
+
continue;
|
|
1159
|
+
const note = n;
|
|
1160
|
+
if (typeof note.text !== 'string')
|
|
1161
|
+
continue;
|
|
1162
|
+
const noteKind = typeof note.kind === 'string' ? note.kind : 'note';
|
|
1163
|
+
if (kind && noteKind !== kind)
|
|
1164
|
+
continue;
|
|
1165
|
+
notes.push({
|
|
1166
|
+
handle: card.handle,
|
|
1167
|
+
kind: noteKind,
|
|
1168
|
+
text: note.text,
|
|
1169
|
+
...(typeof note.sha === 'string' ? { sha: note.sha } : {}),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return ok({ total: notes.length, notes: notes.slice(0, limit ?? 200) });
|
|
1174
|
+
}));
|
|
1056
1175
|
server.registerTool('edit_section', {
|
|
1057
1176
|
description: 'Replace the content under one markdown heading in a card\'s body, keeping every other section byte-for-byte — a cheap, surgical alternative to rewriting the whole body. Match the heading by its text (case-insensitive, no #). Errors if no such heading exists (use update_card to set the whole body or add a section).',
|
|
1058
1177
|
inputSchema: {
|
|
@@ -1068,17 +1187,25 @@ export function buildServer(options = {}) {
|
|
|
1068
1187
|
const card = index.cards.get(handle.toUpperCase());
|
|
1069
1188
|
if (!card)
|
|
1070
1189
|
return fail('NOT_FOUND', `No card with handle ${handle}`);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1190
|
+
// Replace against the CURRENT body inside the lock — concurrent edits to
|
|
1191
|
+
// different sections compose instead of the later one clobbering.
|
|
1192
|
+
let failure = null;
|
|
1193
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1194
|
+
const body = replaceBodySection(current.body, section, text);
|
|
1195
|
+
if (body === null) {
|
|
1196
|
+
const headings = bodyHeadingTexts(current.body);
|
|
1197
|
+
const target = section.trim().replace(/^#+\s*/, '').toLowerCase();
|
|
1198
|
+
const matchCount = headings.filter((h) => h.toLowerCase() === target).length;
|
|
1199
|
+
failure =
|
|
1200
|
+
matchCount > 1
|
|
1201
|
+
? fail('AMBIGUOUS_SECTION', `${card.handle} has ${matchCount} headings called "${section}"; edit_section can't tell them apart. Use update_card to set the whole body.`)
|
|
1202
|
+
: fail('SECTION_NOT_FOUND', `No heading "${section}" in ${card.handle}. Headings present: ${headings.length ? headings.join(', ') : '(none)'}. Use update_card to set the whole body or add the section.`);
|
|
1203
|
+
return null;
|
|
1078
1204
|
}
|
|
1079
|
-
return
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1205
|
+
return { body };
|
|
1206
|
+
});
|
|
1207
|
+
if (failure)
|
|
1208
|
+
return failure;
|
|
1082
1209
|
const lint = await lintPlan(root);
|
|
1083
1210
|
const updated = lint.index.cards.get(card.handle);
|
|
1084
1211
|
return ok({
|
|
@@ -1105,36 +1232,65 @@ export function buildServer(options = {}) {
|
|
|
1105
1232
|
const card = index.cards.get(handle.toUpperCase());
|
|
1106
1233
|
if (!card)
|
|
1107
1234
|
return fail('NOT_FOUND', `No card with handle ${handle}`);
|
|
1235
|
+
const warnings = [];
|
|
1108
1236
|
let resolvedSha = sha;
|
|
1109
|
-
|
|
1110
|
-
|
|
1237
|
+
if (resolvedSha) {
|
|
1238
|
+
// Normalize to the full commit sha; a typo'd baseline would otherwise
|
|
1239
|
+
// surface only later, as an unreachable baseline in stale_report.
|
|
1240
|
+
try {
|
|
1241
|
+
resolvedSha = await resolveCommit(root, resolvedSha);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
warnings.push(`sha ${resolvedSha} does not resolve to a commit in this repo; stamped as given — stale_report will report this card's baseline as unreachable until it does.`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1111
1248
|
try {
|
|
1112
1249
|
resolvedSha = await headSha(root);
|
|
1113
1250
|
}
|
|
1114
1251
|
catch {
|
|
1115
|
-
|
|
1116
|
-
|
|
1252
|
+
warnings.push('Not a git repo (or no commits): stamped verified_at + status only. Drift detection needs a verified_sha baseline — pass sha or commit first.');
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// A verified_sha is a claim about COMMITTED code. Uncommitted edits to the
|
|
1256
|
+
// bound files are not covered by it — mirror set_sync_point's dirty warning.
|
|
1257
|
+
if (resolvedSha) {
|
|
1258
|
+
try {
|
|
1259
|
+
const bound = boundPathsForCard(index, card).map((b) => b.path);
|
|
1260
|
+
const dirty = await changedFilesSince(root, 'HEAD', bound);
|
|
1261
|
+
const dirtyBound = bound.filter((p) => dirty.has(p));
|
|
1262
|
+
if (dirtyBound.length > 0) {
|
|
1263
|
+
warnings.push(`Bound file(s) have uncommitted changes the baseline does not include: ${dirtyBound.join(', ')}. Commit first, then set_verified.`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
// Best-effort: no git repo or nothing bound — nothing to warn about.
|
|
1117
1268
|
}
|
|
1118
1269
|
}
|
|
1119
1270
|
const verifiedAt = new Date().toISOString();
|
|
1120
1271
|
const fields = { verified_at: verifiedAt };
|
|
1121
1272
|
if (resolvedSha)
|
|
1122
1273
|
fields.verified_sha = resolvedSha;
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1274
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1275
|
+
let frontmatter = applyCardPatch(current.frontmatter, {
|
|
1276
|
+
status: 'verified',
|
|
1277
|
+
fields,
|
|
1278
|
+
});
|
|
1279
|
+
if (note) {
|
|
1280
|
+
const n = { kind: 'verified', text: note };
|
|
1281
|
+
if (resolvedSha)
|
|
1282
|
+
n.sha = resolvedSha;
|
|
1283
|
+
frontmatter = withAppendedNote(frontmatter, n);
|
|
1284
|
+
}
|
|
1285
|
+
return { frontmatter };
|
|
1286
|
+
});
|
|
1131
1287
|
const lint = await lintPlan(root);
|
|
1132
1288
|
const updated = lint.index.cards.get(card.handle);
|
|
1133
1289
|
return ok({
|
|
1134
1290
|
card: updated ? full(updated) : null,
|
|
1135
1291
|
verified_sha: resolvedSha ?? null,
|
|
1136
1292
|
verified_at: verifiedAt,
|
|
1137
|
-
warning,
|
|
1293
|
+
warning: warnings.length > 0 ? warnings.join(' ') : undefined,
|
|
1138
1294
|
issues: issuesForFile(lint.issues, card.relPath),
|
|
1139
1295
|
});
|
|
1140
1296
|
}));
|
|
@@ -1158,6 +1314,38 @@ export function buildServer(options = {}) {
|
|
|
1158
1314
|
issues: lint.issues.filter((i) => handleToken.test(i.message)),
|
|
1159
1315
|
});
|
|
1160
1316
|
}));
|
|
1317
|
+
server.registerTool('rename_card', {
|
|
1318
|
+
description: 'Rename a card\'s handle and rewrite every reference to it across the plan — connections lists, handle-shaped frontmatter values, [[links]], mermaid nodes, and prose mentions (whole-token matches only; API-USER never touches API-USERS). The file moves to the new handle\'s path, so a cross-type rename (new prefix) also moves folders and the card\'s fields are then validated against the new type\'s schema. Returns the handles whose references were rewritten plus lint issues for every touched file.',
|
|
1319
|
+
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
1320
|
+
}, withPlan(async (root, args) => {
|
|
1321
|
+
let result;
|
|
1322
|
+
try {
|
|
1323
|
+
result = await renameCard(root, args.from, args.to);
|
|
1324
|
+
}
|
|
1325
|
+
catch (err) {
|
|
1326
|
+
if (err instanceof RenameCardError)
|
|
1327
|
+
return fail(err.code, err.message);
|
|
1328
|
+
throw err;
|
|
1329
|
+
}
|
|
1330
|
+
if (result.noop) {
|
|
1331
|
+
return ok({ renamed: null, note: 'from and to are the same handle — nothing to do.' });
|
|
1332
|
+
}
|
|
1333
|
+
const lint = await lintPlan(root);
|
|
1334
|
+
const touched = new Set([result.file]);
|
|
1335
|
+
for (const h of result.references_updated) {
|
|
1336
|
+
const rel = lint.index.cards.get(h)?.relPath;
|
|
1337
|
+
if (rel)
|
|
1338
|
+
touched.add(rel);
|
|
1339
|
+
}
|
|
1340
|
+
const renamed = lint.index.cards.get(result.to);
|
|
1341
|
+
return ok({
|
|
1342
|
+
renamed: { from: result.from, to: result.to },
|
|
1343
|
+
file: result.file,
|
|
1344
|
+
references_updated: result.references_updated,
|
|
1345
|
+
card: renamed ? full(renamed) : null,
|
|
1346
|
+
issues: lint.issues.filter((i) => touched.has(i.file)),
|
|
1347
|
+
});
|
|
1348
|
+
}));
|
|
1161
1349
|
server.registerTool('add_connection', {
|
|
1162
1350
|
description: 'Connect two cards by appending `to` to `from`’s connections list. No-op if they are already connected through any source.',
|
|
1163
1351
|
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
@@ -1171,13 +1359,18 @@ export function buildServer(options = {}) {
|
|
|
1171
1359
|
if (index.connectedHandles.get(from.handle)?.has(to.handle)) {
|
|
1172
1360
|
return ok({ already_connected: true, between: [from.handle, to.handle] });
|
|
1173
1361
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1362
|
+
await mutateCardFile(from.filePath, (current) => {
|
|
1363
|
+
const existing = Array.isArray(current.frontmatter.connections)
|
|
1364
|
+
? current.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
1365
|
+
: [];
|
|
1366
|
+
if (existing.includes(to.handle))
|
|
1367
|
+
return null; // raced: already added
|
|
1368
|
+
return {
|
|
1369
|
+
frontmatter: deepMerge(current.frontmatter, {
|
|
1370
|
+
connections: [...existing, to.handle],
|
|
1371
|
+
}),
|
|
1372
|
+
};
|
|
1179
1373
|
});
|
|
1180
|
-
await updateCardFile(from.filePath, { frontmatter });
|
|
1181
1374
|
const lint = await lintPlan(root);
|
|
1182
1375
|
return ok({
|
|
1183
1376
|
connected: [from.handle, to.handle],
|
|
@@ -1200,17 +1393,24 @@ export function buildServer(options = {}) {
|
|
|
1200
1393
|
[cardA, cardB.handle],
|
|
1201
1394
|
[cardB, cardA.handle],
|
|
1202
1395
|
]) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1396
|
+
let removed = false;
|
|
1397
|
+
await mutateCardFile(card.filePath, (current) => {
|
|
1398
|
+
// Keep malformed (non-string) entries as-is — lint owns reporting them.
|
|
1399
|
+
const list = Array.isArray(current.frontmatter.connections)
|
|
1400
|
+
? current.frontmatter.connections
|
|
1401
|
+
: [];
|
|
1402
|
+
if (!list.includes(other))
|
|
1403
|
+
return null;
|
|
1404
|
+
removed = true;
|
|
1207
1405
|
const next = list.filter((h) => h !== other);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1406
|
+
return {
|
|
1407
|
+
frontmatter: deepMerge(current.frontmatter, {
|
|
1408
|
+
connections: next.length > 0 ? next : null,
|
|
1409
|
+
}),
|
|
1410
|
+
};
|
|
1411
|
+
});
|
|
1412
|
+
if (removed)
|
|
1212
1413
|
removedFrom.push(card.handle);
|
|
1213
|
-
}
|
|
1214
1414
|
}
|
|
1215
1415
|
const lint = await lintPlan(root);
|
|
1216
1416
|
const after = lint.index;
|
|
@@ -1285,11 +1485,15 @@ export function buildServer(options = {}) {
|
|
|
1285
1485
|
return fail('NO_PLAN_PROJECT', 'No plan.md (PLAN-PROJECT) at the plan root to record connected_repos on.');
|
|
1286
1486
|
}
|
|
1287
1487
|
const entry = { name, path: repoPath, description };
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1488
|
+
let next = [];
|
|
1489
|
+
await mutateCardFile(planCard.filePath, (current) => {
|
|
1490
|
+
next = upsertConnectedRepo(connectedReposFromFrontmatter(current.frontmatter), entry);
|
|
1491
|
+
return {
|
|
1492
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1493
|
+
fields: { connected_repos: next.map(connectedRepoToFm) },
|
|
1494
|
+
}),
|
|
1495
|
+
};
|
|
1291
1496
|
});
|
|
1292
|
-
await updateCardFile(planCard.filePath, { frontmatter });
|
|
1293
1497
|
let reciprocated;
|
|
1294
1498
|
if (reciprocate) {
|
|
1295
1499
|
const target = await resolveConnectedRepo(root, repoPath);
|
|
@@ -1313,12 +1517,13 @@ export function buildServer(options = {}) {
|
|
|
1313
1517
|
path: path.relative(target.repoRoot, homeRepoRoot) || '.',
|
|
1314
1518
|
description: reverse_description,
|
|
1315
1519
|
};
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1520
|
+
await mutateCardFile(targetPlan.filePath, (current) => ({
|
|
1521
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1522
|
+
fields: {
|
|
1523
|
+
connected_repos: upsertConnectedRepo(connectedReposFromFrontmatter(current.frontmatter), reverseEntry).map(connectedRepoToFm),
|
|
1524
|
+
},
|
|
1320
1525
|
}),
|
|
1321
|
-
});
|
|
1526
|
+
}));
|
|
1322
1527
|
reciprocated = {
|
|
1323
1528
|
ok: true,
|
|
1324
1529
|
repo_root: target.repoRoot,
|
|
@@ -1346,11 +1551,16 @@ export function buildServer(options = {}) {
|
|
|
1346
1551
|
if (!existing.some((r) => r.name === name)) {
|
|
1347
1552
|
return ok({ removed: false, connected_repos: existing.map(connectedRepoToFm) });
|
|
1348
1553
|
}
|
|
1349
|
-
|
|
1350
|
-
await
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1554
|
+
let next = [];
|
|
1555
|
+
await mutateCardFile(planCard.filePath, (current) => {
|
|
1556
|
+
next = removeConnectedRepoEntry(connectedReposFromFrontmatter(current.frontmatter), name);
|
|
1557
|
+
return {
|
|
1558
|
+
frontmatter: applyCardPatch(current.frontmatter, {
|
|
1559
|
+
fields: {
|
|
1560
|
+
connected_repos: next.length > 0 ? next.map(connectedRepoToFm) : null,
|
|
1561
|
+
},
|
|
1562
|
+
}),
|
|
1563
|
+
};
|
|
1354
1564
|
});
|
|
1355
1565
|
const lint = await lintPlan(root);
|
|
1356
1566
|
return ok({
|
|
@@ -1448,9 +1658,9 @@ export function buildServer(options = {}) {
|
|
|
1448
1658
|
repo: repoSchema,
|
|
1449
1659
|
},
|
|
1450
1660
|
}, withPlan(async (root, { base }) => {
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1453
|
-
const r = await computeStaleCards(root, index, base);
|
|
1661
|
+
const lint = await lintPlan(root);
|
|
1662
|
+
const status = await computeSyncStatus(root, { lint });
|
|
1663
|
+
const r = await computeStaleCards(root, lint.index, base);
|
|
1454
1664
|
return ok({
|
|
1455
1665
|
advisory: 'Advisory only — the MCP server reports sync state, it cannot block. Use as a definition-of-done gate before calling work complete.',
|
|
1456
1666
|
state: status.state,
|