ai-dev-workspace 0.1.0b5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. ai_dev_workspace-0.1.0b5.dist-info/METADATA +74 -0
  2. ai_dev_workspace-0.1.0b5.dist-info/RECORD +130 -0
  3. ai_dev_workspace-0.1.0b5.dist-info/WHEEL +5 -0
  4. ai_dev_workspace-0.1.0b5.dist-info/entry_points.txt +3 -0
  5. ai_dev_workspace-0.1.0b5.dist-info/licenses/LICENSE +21 -0
  6. ai_dev_workspace-0.1.0b5.dist-info/top_level.txt +1 -0
  7. ws/LICENSE +21 -0
  8. ws/__init__.py +0 -0
  9. ws/assist.py +232 -0
  10. ws/audit.py +101 -0
  11. ws/bin/publish-release +34 -0
  12. ws/bin/release-smoke +322 -0
  13. ws/bin/ws +11 -0
  14. ws/bin/ws-plugin +10 -0
  15. ws/bin/ws-python +8 -0
  16. ws/cards.py +104 -0
  17. ws/cli.py +935 -0
  18. ws/codeburn.py +79 -0
  19. ws/core.py +3396 -0
  20. ws/demo.py +57 -0
  21. ws/discipline.py +56 -0
  22. ws/docs/ADVANCED.md +95 -0
  23. ws/docs/AGENT-ORCHESTRATION.md +145 -0
  24. ws/docs/ARCHITECTURE.md +66 -0
  25. ws/docs/ASSIST-FEATURES.md +11 -0
  26. ws/docs/CLIENT-VERIFICATION.md +40 -0
  27. ws/docs/COMMANDS.md +309 -0
  28. ws/docs/CONCEPTS.md +26 -0
  29. ws/docs/DEMO.md +118 -0
  30. ws/docs/MAINTAINING.md +46 -0
  31. ws/docs/MCP-WORKFLOWS.md +15 -0
  32. ws/docs/MCP.md +20 -0
  33. ws/docs/MEASUREMENTS.md +55 -0
  34. ws/docs/OVERVIEW.md +151 -0
  35. ws/docs/PACKS.md +42 -0
  36. ws/docs/PARALLEL-WORKERS.md +132 -0
  37. ws/docs/PRINCIPLES.md +11 -0
  38. ws/docs/PRIVATE-PACKS.md +14 -0
  39. ws/docs/REPO-START.md +40 -0
  40. ws/docs/ROADMAP.md +43 -0
  41. ws/docs/SESSION-SEARCH.md +19 -0
  42. ws/docs/SETUP.md +139 -0
  43. ws/docs/TEAM-WORKSPACE.md +82 -0
  44. ws/docs/TEAM.md +43 -0
  45. ws/docs/TUTORIAL.md +184 -0
  46. ws/docs/UPGRADE-CHECK.md +22 -0
  47. ws/docs/WINDOWS.md +25 -0
  48. ws/docs/demo.tape +25 -0
  49. ws/docs/examples/frameworks/README.md +42 -0
  50. ws/docs/examples/frameworks/adk-requirements.txt +2 -0
  51. ws/docs/examples/frameworks/adk_agent.py +47 -0
  52. ws/docs/examples/frameworks/common.py +40 -0
  53. ws/docs/examples/frameworks/langgraph-requirements.txt +4 -0
  54. ws/docs/examples/frameworks/langgraph_agent.py +31 -0
  55. ws/docs/examples/frameworks/proof.json +42 -0
  56. ws/docs/examples/frameworks/prove.py +37 -0
  57. ws/docs/examples/mcp-protocol-proof.json +1 -0
  58. ws/folder_io.py +107 -0
  59. ws/folder_publish.py +77 -0
  60. ws/guards.py +719 -0
  61. ws/kit.json +1 -0
  62. ws/knowledge.py +193 -0
  63. ws/mcp/server.py +342 -0
  64. ws/memory_import.py +104 -0
  65. ws/native.py +185 -0
  66. ws/orchestration.py +347 -0
  67. ws/packaging.py +6 -0
  68. ws/packs/codex-worker/.gitignore +1 -0
  69. ws/packs/codex-worker/AGENTS.snippet.md +4 -0
  70. ws/packs/codex-worker/investigate.sh +19 -0
  71. ws/packs/codex-worker/pack.json +7 -0
  72. ws/packs/codex-worker/patch_modes.py +31 -0
  73. ws/packs/codex-worker/selftest.sh +22 -0
  74. ws/packs/codex-worker/setup.sh +9 -0
  75. ws/packs/discipline/AGENTS.snippet.md +2 -0
  76. ws/packs/discipline/pack.json +4 -0
  77. ws/packs/guards/AGENTS.snippet.md +2 -0
  78. ws/packs/guards/pack.json +5 -0
  79. ws/packs/ios/AGENTS.snippet.md +6 -0
  80. ws/packs/ios/pack.json +5 -0
  81. ws/packs/ios/vault/Runbooks/App Store review replies.md +7 -0
  82. ws/packs/ios/vault/Runbooks/iOS Simulator debugging.md +10 -0
  83. ws/packs/ios/vault/Runbooks/iOS build triage.md +8 -0
  84. ws/packs/library/AGENTS.snippet.md +5 -0
  85. ws/packs/library/pack.json +3 -0
  86. ws/packs/local-llm/AGENTS.snippet.md +3 -0
  87. ws/packs/local-llm/pack.json +3 -0
  88. ws/packs/local-llm/summarize.py +38 -0
  89. ws/packs/ml/AGENTS.snippet.md +5 -0
  90. ws/packs/ml/pack.json +3 -0
  91. ws/packs/obsidian/AGENTS.snippet.md +3 -0
  92. ws/packs/obsidian/pack.json +3 -0
  93. ws/packs/obsidian/vault/.obsidian/app.json +7 -0
  94. ws/packs/obsidian/vault/.obsidian/core-plugins.json +1 -0
  95. ws/packs/obsidian/vault/.obsidian/templates.json +3 -0
  96. ws/packs/web/AGENTS.snippet.md +5 -0
  97. ws/packs/web/pack.json +3 -0
  98. ws/packs.py +125 -0
  99. ws/plugin_runtime.py +44 -0
  100. ws/repo.py +648 -0
  101. ws/scope.py +53 -0
  102. ws/setup.py +116 -0
  103. ws/sharing.py +222 -0
  104. ws/skills/handoff/SKILL.md +8 -0
  105. ws/skills/lesson/SKILL.md +7 -0
  106. ws/skills/pickup/SKILL.md +7 -0
  107. ws/skills/thinkbeforeact/SKILL.md +25 -0
  108. ws/team.py +131 -0
  109. ws/team_auto.py +87 -0
  110. ws/team_folder.py +167 -0
  111. ws/team_status.py +87 -0
  112. ws/team_sync.py +169 -0
  113. ws/team_ui.py +87 -0
  114. ws/template/.gitignore +2 -0
  115. ws/template/AGENTS.md +21 -0
  116. ws/template/CLAUDE.md +1 -0
  117. ws/template/routing-presets.json +10 -0
  118. ws/template/routing.json +17 -0
  119. ws/template/vault/Analysis/INDEX.md +5 -0
  120. ws/template/vault/Feedback.md +2 -0
  121. ws/template/vault/Learnings.md +2 -0
  122. ws/template/vault/Product/INDEX.md +2 -0
  123. ws/template/vault/Project/INDEX.md +2 -0
  124. ws/template/vault/README.md +14 -0
  125. ws/template/vault/Runbooks/Delegation.md +15 -0
  126. ws/template/vault/Templates/Task.md +35 -0
  127. ws/tools.json +10 -0
  128. ws/transcripts.py +78 -0
  129. ws/upgrade.py +216 -0
  130. ws/workers.py +397 -0
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-dev-workspace
3
+ Version: 0.1.0b5
4
+ Summary: Shared task memory for AI coding assistants: Claude Code, Codex, Cursor, Copilot, Gemini CLI
5
+ Home-page: https://github.com/Alenroyfeild/ai-dev-workspace
6
+ License: MIT
7
+ Project-URL: Documentation, https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/OVERVIEW.md
8
+ Project-URL: Changelog, https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/Alenroyfeild/ai-dev-workspace/issues
10
+ Project-URL: Source, https://github.com/Alenroyfeild/ai-dev-workspace
11
+ Keywords: ai coding assistant memory claude-code codex cursor copilot gemini-cli mcp agents context handoff
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: project-url
32
+ Dynamic: requires-python
33
+ Dynamic: summary
34
+
35
+ # AI Dev Workspace
36
+
37
+ Switch between Claude Code and Codex without re-explaining your decisions. Your assistant remembers work between sessions in Markdown you own. It keeps decisions, the next step and what not to retry, and warns when files it names changed or disappeared. Try it before connecting a repo: `ws demo` · [5-minute tutorial](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/TUTORIAL.md) · [remove it](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/TUTORIAL.md#remove-it)
38
+ <!-- Demo GIF -->
39
+ ## Quick start
40
+
41
+ ```bash
42
+ pipx install ai-dev-workspace --pip-args='--pre' # beta; omit --pip-args after a stable release
43
+ cd ~/code/myapp && ws start
44
+ ```
45
+ Without pipx: `python3 -m venv ~/.local/share/ai-dev-workspace && ~/.local/share/ai-dev-workspace/bin/pip install --pre ai-dev-workspace`; put its `bin` directory on `PATH` before `ws start`. Prefer a Claude Code plugin? See the [plugin preview](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/ADVANCED.md#claude-code-plugin-preview).
46
+ ## Example
47
+ ```text
48
+ Session 1 — You: “Empty email returns missing_email; leave signup unchanged.”
49
+ Assistant: “I’ll trace login and keep the decision with this task.”
50
+ You: “Stop here; I’ll resume tomorrow.”
51
+ Session 2 — You: “Continue the login fix.”
52
+ Assistant: “The saved note says return missing_email and leave signup unchanged.”
53
+ You: “Yes, continue.”
54
+ ```
55
+ ## Results (small synthetic samples, not general productivity)
56
+ | Session 1 → session 2 | Without workspace | With workspace |
57
+ |---|---:|---:|
58
+ | Claude Code → Claude Code | 0/5 | **5/5** |
59
+ | Claude Code → Codex | 0/5 | **5/5** |
60
+ | Codex → Codex | 0/3 | **3/3** |
61
+
62
+ Later Codex → Codex runs (n=5 each) were less favourable: the workspace passed 1/5 and 2/5, while a plain `HANDOFF.md` control passed 4/5 and 5/5. See [bench/README.md](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/bench/README.md).
63
+ ## Team sharing
64
+ Your notes stay private. Share only what you approve, and your teammates' assistants start with it: after `ws start`, `ws team init <private-repo> --attest-private` (`--yes` in scripts) · `ws know add "Payments API rejects floats" --category domain --team` · `ws sync`. A teammate runs `ws team link <private-repo>`; their next brief shows the note, labelled as shared team context, and their own decisions still win. [Team quick start](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/TEAM-WORKSPACE.md#quick-start)
65
+ ## Compatibility
66
+ | Client | macOS | Linux/Windows |
67
+ |---|---|---|
68
+ | Claude Code | supported, tested live on macOS | CI-tested |
69
+ | Codex | supported, tested live on macOS | CI-tested |
70
+ | Cursor | supported, fixture-tested; live reports welcome | CI-tested |
71
+ | Copilot (VS Code) | supported, fixture-tested; live reports welcome | CI-tested |
72
+ | Gemini CLI | supported, fixture-tested; live reports welcome | CI-tested |
73
+
74
+ Testers on Cursor, Copilot (VS Code), or Gemini CLI: please share problems through [Something broke](https://github.com/Alenroyfeild/ai-dev-workspace/issues/new?template=bug.yml). · [Overview](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/OVERVIEW.md) · [Setup](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/SETUP.md) · [Commands](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/COMMANDS.md) · [Keeping corrections](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/COMMANDS.md#save-a-correction) · [Measurements](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/MEASUREMENTS.md) · [Client verification](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/CLIENT-VERIFICATION.md) · [Packs](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/PACKS.md) · [Team use](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/TEAM.md) · [Windows](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/WINDOWS.md) · [Roadmap](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/docs/ROADMAP.md) · [License](https://github.com/Alenroyfeild/ai-dev-workspace/blob/main/LICENSE) <!-- mcp-name: io.github.Alenroyfeild/ai-dev-workspace mcp-name: io.github.alenroyfeild/ai-dev-workspace -->
@@ -0,0 +1,130 @@
1
+ ai_dev_workspace-0.1.0b5.dist-info/licenses/LICENSE,sha256=VmThDBN3Ouh53UKIIKI2083jz-oVePvykDZyqYrC8z4,1086
2
+ ws/LICENSE,sha256=VmThDBN3Ouh53UKIIKI2083jz-oVePvykDZyqYrC8z4,1086
3
+ ws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ ws/assist.py,sha256=U_Y6EDBENrvqhjAnBfxZYoy9nrEVdY3sOBAYizz5LA4,13614
5
+ ws/audit.py,sha256=0hzWZqskQp7fBuHxZP_1cmn8Jn0IxEbwp4bp2VEBLoY,6305
6
+ ws/cards.py,sha256=McIITUScoj3uwBBzrSbeav_eEkEUKC8D1ZZFbIri_p4,6543
7
+ ws/cli.py,sha256=KdodBXAtpac4KyZCNtcmju-9OEO9iPUi26IJXaLJRMQ,70202
8
+ ws/codeburn.py,sha256=vrhiVDsnpFn_t51Zs_QUilXjpxSfqUG0O8RusHonbPA,4982
9
+ ws/core.py,sha256=3y4OM_aYJicFzLp1d1IxOK_AHfdyPELaL-vsjEXe8R4,180937
10
+ ws/demo.py,sha256=v1sjiNozbhySwid4zITLTZSuCMZWqeuNUDZ0SiJf-qk,2732
11
+ ws/discipline.py,sha256=NreQfSqEBs9JbIA8Ck859lCtZa99AzufvKnteIjxwVk,3695
12
+ ws/folder_io.py,sha256=u_TFhz046wzH_XlUlnNhzX0J1NhswjDJzr8qSgYAzsw,4593
13
+ ws/folder_publish.py,sha256=eYUJTr_IsVJbXvrGUmZo1Y2nZtL1P7xw5bq6s8fEEp8,5188
14
+ ws/guards.py,sha256=2nbeXj4Z_dWQN8ib_qgibKlz0RXeSZWSjbQ5AyFTKTg,40230
15
+ ws/kit.json,sha256=OzcAgQI1dK4NJDepTbXEetFbdSWwt1OA85EJIIm352Q,95
16
+ ws/knowledge.py,sha256=D_44PtPHEbboLuHwsk2znXpWmMzDRHTZkc1U9nldEEw,12135
17
+ ws/memory_import.py,sha256=pGsBGeGynX7vcEAYKT3k8AJzzAeUKrGZrRqAQzqfaSs,5396
18
+ ws/native.py,sha256=10kT8f3efMeMq-TJBle-dEiZxp7P-DidwXNPTKZonoc,8925
19
+ ws/orchestration.py,sha256=3sIRb7TJVSqfTnmlsCot3yGLqVKftu88m5KLJAHEv78,24717
20
+ ws/packaging.py,sha256=u5PPDKYGrr14Kxx6WrGSz4uDtY9x_uBgJW8UuAtU7ug,233
21
+ ws/packs.py,sha256=86LrZyAqRdwFj-RC9HO7sOcviOz0APWF9ZrAzsUHzaM,7766
22
+ ws/plugin_runtime.py,sha256=lsx1BSUzc2al1tps830Zpt_hvRc2KqkDFBJFaVOxabg,1723
23
+ ws/repo.py,sha256=oOwl35zxgxwafN-gxEnx2jY4QMjLdYG5Ifw3sG6Ezo4,46774
24
+ ws/scope.py,sha256=N96HdCeGGup-20uhBISbPP_tg0VCSyubrZ9j9NIrY2Q,3299
25
+ ws/setup.py,sha256=eLIdP8K47PIuKqEyu4FiUPwWbhQBpJQkRY65v5gB4PA,7839
26
+ ws/sharing.py,sha256=cVTjUEXUrKPZaspQWfSpAOlL56mLJO1CrHFa32U4TsQ,15385
27
+ ws/team.py,sha256=E2cdcWecXzEWjnRih0YwhooujVHzac-tUzyeVy1Kqw4,8747
28
+ ws/team_auto.py,sha256=dCN06NmPJUXe5sgCsyZ6ZXr7AGvtbXDG_J1DmyqohoQ,5292
29
+ ws/team_folder.py,sha256=1KIC0PMIVqoWOTNH72j1MPSUECxDo2xqUy4-E247sWI,12104
30
+ ws/team_status.py,sha256=90iTHXVFZZH0x_EuHxsexIvtthtEiWifvUbQfey2ofs,6356
31
+ ws/team_sync.py,sha256=-Dv8xIGgpQOpayum27wXyqD-EZlhXsA8kpdY2lpEF4E,11012
32
+ ws/team_ui.py,sha256=FW3EXCey4QXarBHb9LSZOgmKa50rLCfArw0Ndgj9aXs,4289
33
+ ws/tools.json,sha256=Cu5ePl0Bl_iOqj1NaMdmxpYuucsQlVMwhCBJ9qWQ6mk,3544
34
+ ws/transcripts.py,sha256=uwygj22sMF5Ve8aN-CRBIkAMUIxc1AXvSSGi-2GHDXk,5315
35
+ ws/upgrade.py,sha256=rbt4GBsmPhCgedka3Q-KRBSMeWmg26H5iyGmuNeFrgs,13538
36
+ ws/workers.py,sha256=dQpCJf4UEJHLg0_4bhoK-b9a2Pnx9Po3oDshb7NcVZ4,29328
37
+ ws/bin/publish-release,sha256=y6IckmC69LD6NqnrBOUGTJdSXcOSVz6ApuObzaJjpzQ,967
38
+ ws/bin/release-smoke,sha256=dRmsSO9U9kScju2FeBas6pSmjC2wxHIFCC6kAQw5N6A,16796
39
+ ws/bin/ws,sha256=KXjWUVdLuQEltNi9v59cXvGiXuuMj0-nVKzirW4uLrw,412
40
+ ws/bin/ws-plugin,sha256=G3L52WubOOukkced-nNldvhd9ArqLZ5EF_NaX6RdRHE,374
41
+ ws/bin/ws-python,sha256=IBZ4oZ_GNiEdWUSaSoPQtLrfOVma1seqVZvHbKCLRUI,469
42
+ ws/docs/ADVANCED.md,sha256=mz_dTsH9NarwmtcfxwlP4WN9VP7kZjfnnP2OWbuyqhI,10550
43
+ ws/docs/AGENT-ORCHESTRATION.md,sha256=orYX2ZvGj99ocyTHUDAkdefR40zArueYTgb__wKZu1s,9026
44
+ ws/docs/ARCHITECTURE.md,sha256=NbCDWIzsQwQ8JCepo43jvkpNj2eSKa7S9FlCf8qtplk,5119
45
+ ws/docs/ASSIST-FEATURES.md,sha256=vzJJgrk3Zebppnx2WWb4nkXlnK4sUwQ7809UrdJEx8c,668
46
+ ws/docs/CLIENT-VERIFICATION.md,sha256=s4I439kTFmUtAF4VRYi3sb1mO_GQgfgupLJCQQL6G0M,7596
47
+ ws/docs/COMMANDS.md,sha256=2aF-mMhyCJdyTjfCf8-tgpecTk4GvcWY3xGEZfkFJT8,37952
48
+ ws/docs/CONCEPTS.md,sha256=8pMD_ybe4k7gww0aQmC9s9dcb4KJLX1EL_c997OThfA,2819
49
+ ws/docs/DEMO.md,sha256=a06fDLFtYxEssxoNRUNsNeDDuI41MqdVQweeRzm-xbY,3730
50
+ ws/docs/MAINTAINING.md,sha256=TNov6c9zHk2eRoRBrBqXGM5kTkh61X39VL8SvLhH14c,4338
51
+ ws/docs/MCP-WORKFLOWS.md,sha256=FrP_Ob1KqE23nWHaH3fDp_9i5Om-aLIxSq3t8Edelnw,940
52
+ ws/docs/MCP.md,sha256=8f6jwz15n5PC1DAJyDQCeNN2Sc_HN_TlEWEaDMmFzwM,2592
53
+ ws/docs/MEASUREMENTS.md,sha256=FX6KX_K2Snboxlc0EsGUYmDcxoiPwf_ak6nvQOiokqs,5007
54
+ ws/docs/OVERVIEW.md,sha256=H8duD1RYj5hw097iUVe5Ha6bCyfdqtgsxN9GScbqTNA,12875
55
+ ws/docs/PACKS.md,sha256=wxQ3N2PIMHVcQvOa2UO5Fbys-NvS7rILS2PbTfulvdI,2348
56
+ ws/docs/PARALLEL-WORKERS.md,sha256=BrN4yPfElydDBuC7sXZeka7_yvATiHZE7rXAIHC_xa0,8264
57
+ ws/docs/PRINCIPLES.md,sha256=vIvv1KS8GUSenFOpGwYnJe0Lc2RZ8pOM2ofRdfUF9y8,1103
58
+ ws/docs/PRIVATE-PACKS.md,sha256=FUc0qtf6P5RY4HQLAGRMNPenNlPXnow9IzCTfJI1QYA,859
59
+ ws/docs/REPO-START.md,sha256=6XZt8b6EXZfUm7AEn566pno0S4GsuzI3dyJtgHbml5c,2902
60
+ ws/docs/ROADMAP.md,sha256=wmTjaOAPL3qvehJCF71oQs2eZe--WGl_i9XzYfd4xyQ,2783
61
+ ws/docs/SESSION-SEARCH.md,sha256=br-5-X0yVN21SlvZQ4XgMQPejJv0Qmd8WUkpKsyPNSE,1275
62
+ ws/docs/SETUP.md,sha256=giocL2Va8XLT9PkaBIh3Dyg0UESaQTXZpj9ixZ_AjXA,16270
63
+ ws/docs/TEAM-WORKSPACE.md,sha256=o695f_NoDj_9xtFCQAnLKqEanR1n23KWEUkuv2KrUnI,4233
64
+ ws/docs/TEAM.md,sha256=ZV0lUSfkpURnythTO17-4t_RPpZj9CRSiwdQ0n4Xj_c,2684
65
+ ws/docs/TUTORIAL.md,sha256=lHu76vVGyIWUctYCFLw-QQlkLw7DfAp03bArL6NWDFA,5611
66
+ ws/docs/UPGRADE-CHECK.md,sha256=ojPTRQzHjlMbwK21es0dCnwM258mwLSye1MIKdtIJX8,2227
67
+ ws/docs/WINDOWS.md,sha256=kSkaZPiX5xZdtSjgfsKbsHMbNBEolP9pq6E6AN1_gC8,1733
68
+ ws/docs/demo.tape,sha256=wyydxz4fuNubolQ_BQd82CMQ2eWy7fzZzGRG0sMJ4Gk,834
69
+ ws/docs/examples/mcp-protocol-proof.json,sha256=3S4_0vrJhKoKUBf_zZIGWzXYPX3nT8rDACfQ5ADdw84,504
70
+ ws/docs/examples/frameworks/README.md,sha256=RcaxodmPlcvYte_TAmtGvXsrcbkTWYg9xhCzWBWFzDY,4142
71
+ ws/docs/examples/frameworks/adk-requirements.txt,sha256=gvO5gzifMdc63AOZkKdJWTKZMA_59PTvyegR6gTU-fE,35
72
+ ws/docs/examples/frameworks/adk_agent.py,sha256=9UYqtM2c8ijSAP86rQh-lFLb2HsUcLPIdIRzup59y_Q,2283
73
+ ws/docs/examples/frameworks/common.py,sha256=itBd3r7y_xEIAALQl1f8znqnt7js0adtxY2kJ_9gaQY,1850
74
+ ws/docs/examples/frameworks/langgraph-requirements.txt,sha256=eNcR6gBmIFxMKr8iv-HKGtflczAJGFBN13BcD3SgnMc,82
75
+ ws/docs/examples/frameworks/langgraph_agent.py,sha256=9BA8mEUWdvUbC-IELeV1uTVhQxCMdEIS7kD_1uVWM8s,1653
76
+ ws/docs/examples/frameworks/proof.json,sha256=WMIszYypd1qVgGOPgEisz5QxWZWigDBaQwvBetw64ls,1247
77
+ ws/docs/examples/frameworks/prove.py,sha256=5bzIERpz7w219Kovg3kkVtAELT-EgVE5Cm4ktHeXWVE,2615
78
+ ws/mcp/server.py,sha256=DuM_vbuU1LqR2yYd_7umbS7xFzwWMKf1PYe9qADUNi8,20681
79
+ ws/packs/codex-worker/.gitignore,sha256=_c0Bg6S6vYlOfM6D7Ie9JPOFxCTMg1cQCe0HqzRyLZ8,9
80
+ ws/packs/codex-worker/AGENTS.snippet.md,sha256=UNAmYF70B9ui68OMf1_qaY4bDeZLOWn4h3zQPU209WI,333
81
+ ws/packs/codex-worker/investigate.sh,sha256=j7ztOpUWs6hl8AaFFxwr6I2WdaGmjXAYGKEhyYIAny8,1838
82
+ ws/packs/codex-worker/pack.json,sha256=bdBfqbpBZKOU8IIlK8ZOJaR6L3_0fD9oWgwdWw5eD9g,634
83
+ ws/packs/codex-worker/patch_modes.py,sha256=iYHPxDuJ93wDmVJOMZbKKLPwBhpmfwjz3ilVGyeXf3A,1544
84
+ ws/packs/codex-worker/selftest.sh,sha256=Ydfkxr8j9A4FqEP_kmROFKvsatIUDIYtY7qtfvq3jPA,1601
85
+ ws/packs/codex-worker/setup.sh,sha256=jTkFoJYZZZ7UIzcvgGhGux3wMTm4WritGN_gTIysptc,650
86
+ ws/packs/discipline/AGENTS.snippet.md,sha256=HxfMvaq6hsAhOIYHsh7qD_IlNxnHkr8UfxzPVVu6-uw,193
87
+ ws/packs/discipline/pack.json,sha256=pGh_1_-OmJS0M09oD1pgvsif2uKUOg-9AnT5KDiyX3Y,372
88
+ ws/packs/guards/AGENTS.snippet.md,sha256=WyaqLZGs4VORSIJRNwyg1tc_NMY7NHScZDb1rbRg0qs,290
89
+ ws/packs/guards/pack.json,sha256=vPbwS_kAJpHsMSbja6etEjLIPbH4RS8_qySvQ4cxM4E,475
90
+ ws/packs/ios/AGENTS.snippet.md,sha256=24atlXkFNImvhQPqzINdQf2TiGCHBzNcha_auC2xarI,627
91
+ ws/packs/ios/pack.json,sha256=jP7A0GwONjt7lZmIBDhgdehgfYD7qAKdbMLcyYFq3uk,412
92
+ ws/packs/ios/vault/Runbooks/App Store review replies.md,sha256=e1B8WuMAd-PFgtyVZzYCBxmuPtAuRz2E6uRviO_yTYM,639
93
+ ws/packs/ios/vault/Runbooks/iOS Simulator debugging.md,sha256=vtKKBldoeeKI9ZlsH6Lu7OX1WfOVFg1GaswMcK02My0,798
94
+ ws/packs/ios/vault/Runbooks/iOS build triage.md,sha256=tYH07OJt0mWFNSnWq6qRw3g5n1-eeofqVxd6wRn3MiE,806
95
+ ws/packs/library/AGENTS.snippet.md,sha256=y27A-Aoa3LljlZXNWW_U6JmchjqOfs-wtinNlspckns,348
96
+ ws/packs/library/pack.json,sha256=ee9QTEH53nxMGu_GzFT533Gs_gVlfs1AqCl7-Zk25kU,197
97
+ ws/packs/local-llm/AGENTS.snippet.md,sha256=T5DaECzKpoILmXZmlkc0gZ2T6S3jzt64-gIDg2BeJ14,213
98
+ ws/packs/local-llm/pack.json,sha256=P1dCha-TrKU08RXLw_rwF2eI5nPkBk0TY3qbmAfybsU,361
99
+ ws/packs/local-llm/summarize.py,sha256=sNvAq2_g6kMYJ2lykqAtyfW1qpOCNKijN2kL-I_DF3Y,2180
100
+ ws/packs/ml/AGENTS.snippet.md,sha256=HEAO29-Zamk5Tw2XokMsaZ_kJILSGYIkJFLDgUJrcnE,380
101
+ ws/packs/ml/pack.json,sha256=-D3OebuxFoj0_5_ifvAKpXAYGR52TZudxbQs_TXRtP8,215
102
+ ws/packs/obsidian/AGENTS.snippet.md,sha256=Y17khpTcLy0BRru06ht0BgdfmaOvZgZUnVkD3gA9Yn8,186
103
+ ws/packs/obsidian/pack.json,sha256=FVPzT6lEr6XK3ZcDB6eui7fE1GBfPrdF7FfRhQ6YG18,359
104
+ ws/packs/obsidian/vault/.obsidian/app.json,sha256=CdGnLqKDfEtNAOeC0WJYrSa7H5pDKx-38Q8tXBWMhG8,179
105
+ ws/packs/obsidian/vault/.obsidian/core-plugins.json,sha256=1h6jrUvQlD6p4YZfkD50roW5gnpNv_GHJBVJbLpdi4k,128
106
+ ws/packs/obsidian/vault/.obsidian/templates.json,sha256=OT-W1jMt_AXyXrGdwMs8dfjJJGlrGzU0nkxmcuPDpxI,28
107
+ ws/packs/web/AGENTS.snippet.md,sha256=xITAcWvYlUfXQ_TdWl5krPBoC1tjh7Lx4T6cKVhE6AU,366
108
+ ws/packs/web/pack.json,sha256=R5mvndn67Udj3dl7rlGA4eQarpjVRHg7E0JMjn_2V2I,302
109
+ ws/skills/handoff/SKILL.md,sha256=k1_smNEiWMxZy5iORGfBs1ODyXRxnoTs5ArFfWcLgPY,851
110
+ ws/skills/lesson/SKILL.md,sha256=nueCd6GSeSnZaM2aY8_X3h5w1eRa5_3ulMD-AOq0X0o,721
111
+ ws/skills/pickup/SKILL.md,sha256=xsFtt2GdYjZzVgnSLPvs4oAlnWQcpbNt-8U9ykIr24k,962
112
+ ws/skills/thinkbeforeact/SKILL.md,sha256=WU3OBPTFfABoxXgIyokcBAJzGZHOdTSRSVwbtcM6OHo,2031
113
+ ws/template/.gitignore,sha256=VkfiZs7hHe80sIOkHZp-tHtrksOVJgMh2_YuTY2LgJ0,16
114
+ ws/template/AGENTS.md,sha256=joLRGnhSIU4PURVIHA9ii0Ccht4zW5rgnBUiWAFQXz0,2321
115
+ ws/template/CLAUDE.md,sha256=M2zE-_Gb6q2nzPmYZBT6kYUajXoH37PMvoAKae7Qq0k,11
116
+ ws/template/routing-presets.json,sha256=IHnkCXfvF7FTHEsQ-UwPPQZq7L_r72u_MmX7kvgYTf8,442
117
+ ws/template/routing.json,sha256=7EYTkpk51KAjrf3OaMboR0w8h38PE8a9ROIcqlkBRxo,3663
118
+ ws/template/vault/Feedback.md,sha256=PLfPxTTirDhdVkqIa_fGF-WguAx2zLEEzD15Hwo-EEw,120
119
+ ws/template/vault/Learnings.md,sha256=XaA9x3f4a0jIT9sMC2bcPBf8e1mFcxo2ndDr8ciYWu8,89
120
+ ws/template/vault/README.md,sha256=2FZzxHJN3r8Uzq87rxpkiypfsfAkySZTRdvlJEnvGU4,862
121
+ ws/template/vault/Analysis/INDEX.md,sha256=jrmj5orQyYp46ah0ZmwkvzDzlzcl77EZzkjiGjuRrao,145
122
+ ws/template/vault/Product/INDEX.md,sha256=5GYnJZNsmeya-kdfslbff-dn8bWUPKKm_IvYThgOY1s,114
123
+ ws/template/vault/Project/INDEX.md,sha256=_mP4dWzrfR4cHOt0JEsootr8Fn6MSPXQGme6M-PZSMY,107
124
+ ws/template/vault/Runbooks/Delegation.md,sha256=Pq0DcUcgCWyhK3dbgsQxU-sTrDuyGNSf9GgwSwAuuvc,1009
125
+ ws/template/vault/Templates/Task.md,sha256=yhwAkDSJIKwdnzA5PgMhJSKLQngATKMT2W7T2zz_A8Y,342
126
+ ai_dev_workspace-0.1.0b5.dist-info/METADATA,sha256=em9ijQen1tnMgd6XFS7Tb5CYZcxU-0acVs83R5HzuWo,5639
127
+ ai_dev_workspace-0.1.0b5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
128
+ ai_dev_workspace-0.1.0b5.dist-info/entry_points.txt,sha256=hejj-VP_6McbNhDHe0Rha0VU_uoOMyLYTMzWl_-l_as,70
129
+ ai_dev_workspace-0.1.0b5.dist-info/top_level.txt,sha256=hJ3XTAUWCz6_-y1VRxZDN6IEt_iNAyLSXhhoTAotJIo,3
130
+ ai_dev_workspace-0.1.0b5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ai-dev-workspace = ws.cli:mcp_main
3
+ ws = ws.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Dev Workspace contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ws/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Dev Workspace contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ws/__init__.py ADDED
File without changes
ws/assist.py ADDED
@@ -0,0 +1,232 @@
1
+ """Local suggestions and explicit permission; never install or edit client config."""
2
+ import datetime
3
+ import json
4
+ import re
5
+ import shlex
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+ from . import core
10
+
11
+
12
+ def state(root):
13
+ path = root / '.ws/assist.json'
14
+ return core.load_json(path.read_text(encoding='utf-8'), '.ws/assist.json', 'delete it (only snooze and offer history is lost)') if path.exists() else {}
15
+
16
+
17
+ def save(root, data):
18
+ core.atomic_write(root / '.ws/assist.json', json.dumps(data) + '\n')
19
+
20
+
21
+ def cost_data():
22
+ if not core.shutil.which('codeburn'):
23
+ return {}, {}
24
+ # notices runs at every session start: reuse one Codeburn scan per day instead of ~30 s of subprocesses.
25
+ cache = Path.home() / '.cache' / 'ai-dev-workspace' / 'codeburn-assist.json'
26
+ try:
27
+ cached = json.loads(cache.read_text(encoding='utf-8'))
28
+ if cached.get('day') == core.now()[:10]:
29
+ return _dict(cached['report']), _dict(cached['usage'])
30
+ except (OSError, ValueError, KeyError, AttributeError):
31
+ pass
32
+ report, usage = _scan()
33
+ if report or usage:
34
+ cache.parent.mkdir(parents=True, exist_ok=True)
35
+ core.atomic_write(cache, json.dumps({'day': core.now()[:10], 'report': report, 'usage': usage}))
36
+ return report, usage
37
+
38
+
39
+ def _dict(value):
40
+ return value if isinstance(value, dict) else {} # Codeburn output of any other shape is ignored
41
+
42
+
43
+ def _rows(value):
44
+ return [row for row in value if isinstance(row, dict)] if isinstance(value, list) else []
45
+
46
+
47
+ def _scan():
48
+ executable = core.shutil.which('codeburn') or 'codeburn' # Windows npm shims are codeburn.CMD; only the full path launches
49
+ try:
50
+ report = subprocess.run([executable, 'optimize', '--format', 'json'], capture_output=True, encoding='utf-8', errors='replace', timeout=15)
51
+ since = (datetime.date.fromisoformat(core.now()[:10]) - datetime.timedelta(days=30)).isoformat()
52
+ with tempfile.TemporaryDirectory() as tmp:
53
+ path = Path(tmp) / 'usage.json'
54
+ export = subprocess.run([executable, 'export', '-f', 'json', '-o', str(path), '--from', since], capture_output=True, encoding='utf-8', errors='replace', timeout=15)
55
+ return _dict(json.loads(report.stdout)), _dict(json.loads(path.read_text(encoding='utf-8')) if export.returncode == 0 else {})
56
+ except (OSError, ValueError, RecursionError, subprocess.TimeoutExpired):
57
+ return {}, {}
58
+
59
+
60
+ def observe(root, payload):
61
+ if not payload.get('session_id') and not payload.get('transcript_path'):
62
+ return
63
+ with core.lock(root):
64
+ data = state(root)
65
+ if payload.get('hook_event_name') in ('SessionStart', 'sessionStart') and payload.get('session_id'):
66
+ data.pop('long_session_bytes', None)
67
+ current = core.sole_local_claim(root)
68
+ bindings = data.setdefault('session_tasks', {}); sid = core.digest_text(str(payload['session_id']))
69
+ previous = bindings.pop(sid, current)
70
+ bindings[sid] = current if previous == current else ''
71
+ while len(bindings) > 200:
72
+ del bindings[next(iter(bindings))]
73
+ for task in core.task_list(root):
74
+ if task['id'] != current:
75
+ continue
76
+ meta = core.task_read(root, task['id'], ['Next action'])['meta']
77
+ count = meta.get('checkpoint_count', '0')
78
+ record = data.setdefault('sessions', {}).setdefault(task['id'], {})
79
+ sid = core.digest_text(str(payload['session_id']))
80
+ if record.get('last') != sid:
81
+ record.update(last=sid, count=(record.get('count', 0) if record.get('checkpoint') == count else 0) + 1, checkpoint=count)
82
+ try:
83
+ transcript = payload.get('transcript_path')
84
+ size = Path(transcript).stat().st_size if isinstance(transcript, str) and transcript else 0
85
+ if size > 2000000:
86
+ data['long_session_bytes'] = size
87
+ except OSError:
88
+ pass
89
+ save(root, data)
90
+
91
+
92
+ def session_task(root, session_id):
93
+ if not session_id: return None # Legacy hooks without a session ID retain claim selection.
94
+ try:
95
+ sid = core.digest_text(str(session_id))
96
+ return state(root).get('session_tasks', {}).get(sid, '')
97
+ except (OSError, ValueError, AttributeError): return '' # Unknown bindings cannot authorize capture.
98
+
99
+
100
+ def feature_suggestions(root, add):
101
+ if core.upgrade_due(root):
102
+ add('upgrade', 'Kit is newer than this workspace or .ws-new proposals await review', 'ws upgrade --dry-run', 'local-reversible')
103
+ from . import orchestration
104
+ path = root / '.ws/delegate-selftests.json'
105
+ try: records = json.loads(path.read_text(encoding='utf-8')) if path.is_file() else {}
106
+ except (OSError, ValueError): records = {}
107
+ if not isinstance(records, dict): records = {}
108
+ seen = set()
109
+ # Only users who delegate need a sandbox selftest; new workspaces get no paid-run pitch.
110
+ for role in ('worker', 'explorer', 'reviewer') if (root / '.ws/briefs').is_dir() else ():
111
+ try: binding = orchestration.route(root, role)
112
+ except core.WsError: continue
113
+ provider = binding.get('provider')
114
+ record = records.get(provider, {})
115
+ if (binding.get('available') and provider in ('codex', 'claude') and provider not in seen
116
+ and core._detected(provider) and not (isinstance(record, dict) and record.get('executed'))):
117
+ add('selftest-' + provider, 'Routed read-only provider has no executed selftest: ' + provider,
118
+ 'ws delegate --selftest --provider ' + provider, 'paid-run' if provider == 'codex' else 'local-reversible')
119
+ seen.add(provider)
120
+ for client, checks in (('cursor', ['cursor', 'app:Cursor']), ('vscode', ['code', 'app:Visual Studio Code']), ('gemini', ['gemini'])):
121
+ if core._detected(checks) and not core.client_connected(root, client):
122
+ add('connect-' + client, 'Installed client has no matching workspace connection: ' + client, 'ws connect ' + client, 'changes-config')
123
+
124
+
125
+ def _workspace_established(root):
126
+ checkpoints = 0
127
+ for task in core.task_list(root):
128
+ try:
129
+ checkpoints += int(core.task_read(root, task['id'], ['Next action'])['meta'].get('checkpoint_count', 0))
130
+ except (TypeError, ValueError):
131
+ pass
132
+ if checkpoints >= 3:
133
+ return True
134
+ try:
135
+ created = datetime.date.fromisoformat(str(core.config(root).get('created', ''))[:10])
136
+ today = datetime.date.fromisoformat(core.now()[:10])
137
+ return today - created >= datetime.timedelta(days=7)
138
+ except ValueError:
139
+ return False
140
+
141
+
142
+ def suggestions(root, include_hidden=False):
143
+ data, items = state(root), []
144
+ def add(identifier, why, command, safety, saving='unmeasured'):
145
+ items.append(dict(id=identifier, why=' '.join(core.redact(why).split()), command=command, safety=safety, estimated_saving=saving))
146
+ feature_suggestions(root, add)
147
+ repos = core.config(root).get('repos', [])
148
+ mapped = core.vault(root) / 'Project/Codebase map.md'
149
+ if repos and (not mapped.exists() or core.time.time() - mapped.stat().st_mtime > 14 * 86400):
150
+ add('map', 'Configured repository map missing or older than 14 days', 'ws map ' + shlex.quote(repos[0]), 'local-reversible')
151
+ if repos and core.shutil.which('graphify') and not (Path(repos[0]) / 'graphify-out').exists():
152
+ add('graph', 'Graphify installed; configured repository has no graphify-out directory', 'graphify ' + shlex.quote(repos[0]), 'external')
153
+ tasks = core.task_list(root)
154
+ if not core.lesson_search(root, '') and sum(int(core.task_read(root, t['id'], ['Next action'])['meta'].get('checkpoint_count', 0)) for t in tasks) >= 5:
155
+ add('lesson', 'No lessons after at least 5 recorded checkpoints', 'ws lesson add "<what happened → rule>"', 'local-reversible')
156
+ for task in tasks:
157
+ record = data.get('sessions', {}).get(task['id'], {})
158
+ count = core.task_read(root, task['id'], ['Next action'])['meta'].get('checkpoint_count', '0')
159
+ if task['claimed_by'] and record.get('checkpoint') == count and record.get('count', 0) >= 3:
160
+ add('checkpoint-' + task['id'], 'Claim has spanned 3 sessions without a new checkpoint: ' + task['id'], '/handoff (Claude) or $handoff (Codex)', 'local-reversible')
161
+ if _workspace_established(root):
162
+ for tool in core.tools(root):
163
+ if (tool['level'] == 'recommended' and not tool['installed']
164
+ and core.tool_policy(root, tool)['mode'] != 'off'):
165
+ add('tool-' + tool['name'],
166
+ 'Recommended tool is missing: ' + tool['name'] + '. Review options and ask before installing.',
167
+ 'ws tools', 'changes-config')
168
+ if data.get('long_session_bytes'):
169
+ add('long-session', f"Transcript exceeded 2000000 bytes ({data['long_session_bytes']}); start fresh after handoff", '/handoff (Claude) or $handoff (Codex)', 'local-reversible')
170
+ guard = core.repeat_guard(root)
171
+ if guard:
172
+ add('trace', guard, 'ws trace ' + guard.split()[1].rstrip(':'), 'local-reversible')
173
+ items.insert(0, items.pop())
174
+ report, usage = map(_dict, cost_data())
175
+ used = [row['Server'].lower() for row in _rows(usage.get('mcp')) if isinstance(row.get('Server'), str)
176
+ and isinstance(row.get('Calls'), (int, float)) and not isinstance(row.get('Calls'), bool) and row['Calls'] > 0]
177
+ pinned = [name.lower() for name, value in core.config(root).get('tool_overrides', {}).items() if value == 'on']
178
+ for finding in _rows(report.get('findings')):
179
+ text = json.dumps(finding).lower()
180
+ if any(name in text for name in ['ccd_', 'claude-in-chrome', 'ai-dev-workspace', 'workspace', *used, *pinned]):
181
+ continue
182
+ if 'mcp' in text and 'mcp' not in usage:
183
+ continue # Missing usage evidence: fail closed for connector advice.
184
+ if finding.get('class') == 'keep':
185
+ continue
186
+ identifier = str(finding.get('id', ''))
187
+ if re.fullmatch(r'[a-zA-Z0-9_-]+', identifier):
188
+ try: saving = float(finding.get('estimatedSavingsUSD') or 0)
189
+ except (TypeError, ValueError): saving = 0.0
190
+ add('cost-' + identifier, 'Review Codeburn: ' + str(finding.get('title') or '') + '; ' + str(finding.get('explanation') or '')[:180],
191
+ 'codeburn optimize --format json', 'changes-config', f'up to ${saving:.2f} in the scanned period (estimate)')
192
+ today = core.now()[:10]
193
+ return [item for item in items if include_hidden or data.get('permissions', {}).get(item['id'], {}).get('until', '') <= today][:2 if not include_hidden else len(items)]
194
+
195
+
196
+ def decide(root, identifier, decision, until=None):
197
+ items = {item['id']: item for item in suggestions(root, True)}
198
+ if identifier not in items or decision not in ('accepted', 'declined', 'snoozed', 'always'):
199
+ raise core.WsError('Choose a current suggestion and accepted, declined, snoozed or always.')
200
+ if decision == 'always' and identifier not in ('map', 'trace'):
201
+ raise core.WsError('Always is allowed only for map and trace, never installs or configuration changes.')
202
+ if decision == 'snoozed':
203
+ # Strict YYYY-MM-DD: Python 3.11+ also accepts 20261001 or 2026-W43-1, which would compare wrongly as text.
204
+ if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', until or ''): raise core.WsError('Snoozed requires --until YYYY-MM-DD.')
205
+ try: day = datetime.date.fromisoformat(until)
206
+ except ValueError: raise core.WsError('Snoozed requires --until YYYY-MM-DD.')
207
+ if day <= datetime.date.fromisoformat(core.now()[:10]): raise core.WsError(f'--until {until} is not in the future; pick a later date.')
208
+ if decision == 'declined':
209
+ until = (datetime.date.fromisoformat(core.now()[:10]) + datetime.timedelta(days=30)).isoformat()
210
+ with core.lock(root):
211
+ data = state(root); data.setdefault('permissions', {})[identifier] = dict(decision=decision, until=until or '')
212
+ save(root, data)
213
+ return {'id': identifier, 'decision': decision, 'until': until}
214
+
215
+
216
+ def apply(root, identifier):
217
+ permission = state(root).get('permissions', {}).get(identifier, {}).get('decision')
218
+ items = {item['id']: item for item in suggestions(root, True)}
219
+ if identifier not in items or identifier not in ('map', 'trace') or permission not in ('accepted', 'always'):
220
+ raise core.WsError('Accept a current map/trace suggestion first; run other commands yourself after review.')
221
+ return core.codebase_map(root) if identifier == 'map' else core.trace(root, items['trace']['command'].split()[-1])
222
+
223
+
224
+ def notices(root, limit, existing=()):
225
+ candidates = [dict(id='notice-' + core.digest_text(json.dumps(item)), notice=item) for item in existing] + suggestions(root, True)
226
+ with core.lock(root):
227
+ data = state(root); seen = data.get('offered', [])
228
+ items = [item for item in candidates if item['id'] not in seen and data.get('permissions', {}).get(item['id'], {}).get('until', '') <= core.now()[:10]][:limit]
229
+ data['offered'] = seen + [item['id'] for item in items]; save(root, data)
230
+ return [item['notice'] if 'notice' in item else dict(kind='toolbox' if item['id'].startswith('tool-') else 'assist', tool=item['id'].removeprefix('tool-'),
231
+ message=item['why'] + (' Review options with ws tools before installing.' if item['id'].startswith('tool-') else ' Ask before applying.'),
232
+ suggest=item['command']) for item in items]
ws/audit.py ADDED
@@ -0,0 +1,101 @@
1
+ """ws audit: read-only scorecard of a repo's AI assistant setup with the top fixes; no network, no writes."""
2
+ import datetime
3
+ import json
4
+ import re
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from . import core
9
+
10
+ INSTRUCTIONS = ('CLAUDE.md', 'AGENTS.md', '.github/copilot-instructions.md', 'GEMINI.md')
11
+ BIG = 2000 # words loaded into every session before the user types anything
12
+ MCP_SERVERS = 5
13
+ EXTENSIONLESS = {'Makefile', 'Dockerfile', 'Justfile', 'Procfile', 'Gemfile', 'Rakefile', 'Brewfile', 'Vagrantfile', 'LICENSE', 'CODEOWNERS'} # @Name is a person otherwise
14
+ STALE_DAYS = 14
15
+
16
+
17
+ def _repo(path):
18
+ path = Path(path or '.').resolve()
19
+ try:
20
+ top = subprocess.run(['git', '-C', str(path), 'rev-parse', '--show-toplevel'], capture_output=True, encoding='utf-8', errors='replace', timeout=5)
21
+ return Path(top.stdout.strip()).resolve() if top.returncode == 0 else path
22
+ except (OSError, subprocess.SubprocessError):
23
+ return path
24
+
25
+
26
+ def _refs(text):
27
+ """Files an instruction file points at: @imports, Markdown links and backticked relative paths."""
28
+ imports = [r.rstrip('.') for r in re.findall(r'(?m)(?:^|\s)@([\w.-]+(?:/[\w.-]+)*)', text)]
29
+ # @AGENTS.md is a file import; @openai/agents (no extension, with a slash) is a package name; @Makefile (capitalised, no slash) is a file.
30
+ found = ([r for r in imports if re.search(r'\.\w{1,8}$', r) or r.split('/')[-1] in EXTENSIONLESS] + re.findall(r'\]\(([^)#\s]+)', text)
31
+ + re.findall(r'`((?:[\w.-]+/)+[\w.-]+\.\w{1,8}|[\w-]+\.(?:md|py|json|toml|ya?ml|sh|js|ts))`', text))
32
+ return [r for r in dict.fromkeys(found) if '://' not in r and '*' not in r and not r.startswith(('mailto:', '/', '~', '$'))]
33
+
34
+
35
+ def _session_start_command(path):
36
+ """True when a hooks file has at least one SessionStart command (not just an empty list)."""
37
+ try: entries = core.json_loads(path.read_text(encoding='utf-8')).get('hooks', {}).get('SessionStart')
38
+ except (OSError, ValueError, AttributeError): return False
39
+ if not isinstance(entries, list): return False
40
+ hooks = [h for e in entries if isinstance(e, dict) for h in (e.get('hooks') if isinstance(e.get('hooks'), list) else [e])]
41
+ return any(isinstance(h, dict) and isinstance(h.get('command'), str) and h['command'].strip() for h in hooks)
42
+
43
+
44
+ def _mcp_servers(repo):
45
+ names = set()
46
+ try: servers = core.json_loads((repo / '.mcp.json').read_text(encoding='utf-8')).get('mcpServers')
47
+ except (OSError, ValueError, AttributeError): servers = None
48
+ if isinstance(servers, dict): names |= set(servers)
49
+ try: names |= set(re.findall(r'(?m)^\[mcp_servers\.([^\].]+)\]', (repo / '.codex/config.toml').read_text(encoding='utf-8')))
50
+ except OSError: pass
51
+ return sorted(names)
52
+
53
+
54
+ def audit(path=None):
55
+ repo = _repo(path)
56
+ checks = []
57
+ def check(name, ok, detail, fix):
58
+ checks.append({'check': name, 'ok': bool(ok), 'detail': detail, 'fix': '' if ok else fix})
59
+
60
+ present = [n for n in INSTRUCTIONS if (repo / n).is_file()]
61
+ texts = {n: (repo / n).read_text(encoding='utf-8', errors='replace') for n in present}
62
+ words = sum(len(t.split()) for t in texts.values())
63
+ check('Instructions', {'CLAUDE.md', 'AGENTS.md'} & set(present), ', '.join(present) or 'none',
64
+ 'Add AGENTS.md with the build and test commands and repo rules; CLAUDE.md can import it with @AGENTS.md.')
65
+ check('Instruction size', words <= BIG, f'{words:,} words loaded every session',
66
+ f'Trim the instruction files under {BIG:,} words; move rarely needed detail into linked docs.')
67
+ broken = [f'{n}: {r}' for n, t in texts.items() for r in _refs(t) if not ((repo / n).parent / r).exists() and not (repo / r).exists()]
68
+ check('References', not broken, ', '.join(broken[:5]) + (f' (+{len(broken) - 5} more)' if len(broken) > 5 else '') or 'all resolve',
69
+ 'Fix or remove references to missing files: ' + ', '.join(broken[:3]) + '.')
70
+
71
+ root = None
72
+ try: root = core.find_root(repo) if (repo / '.ws-start.json').is_file() else None
73
+ except (core.WsError, OSError, ValueError): pass
74
+ check('Memory', root, 'connected' if root else 'not connected', 'Run ws start in this repo to give every session the saved decisions and next step.')
75
+ hooks = [label for label, rel in (('Claude Code', '.claude/settings.local.json'), ('Claude Code', '.claude/settings.json'), ('Codex', '.codex/hooks.json'))
76
+ if _session_start_command(repo / rel)]
77
+ check('Session hooks', hooks, ', '.join(dict.fromkeys(hooks)) or 'none',
78
+ 'Run ws start: it adds SessionStart hooks so Claude Code and Codex load memory without being asked.')
79
+ servers = _mcp_servers(repo)
80
+ profile = core.config(root).get('mcp_tools', 'core') if root else 'core'
81
+ check('MCP budget', len(servers) <= MCP_SERVERS and profile != 'all', f"{len(servers)} servers" + (', ws lists all tools' if profile == 'all' else ''),
82
+ f'Each MCP server adds tool descriptions to every session; keep {MCP_SERVERS} or fewer'
83
+ + (' and set "mcp_tools": "core" in workspace.json.' if profile == 'all' else '.'))
84
+ if root:
85
+ cutoff = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=STALE_DAYS)).strftime('%Y-%m-%d')
86
+ stale = [t['id'] for t in core.task_list(root) if t['status'] == 'in_progress'
87
+ and str(core.task_read(root, t['id'], ['Next action'])['meta'].get('updated', ''))[:10] < cutoff]
88
+ check('Stale tasks', not stale, ', '.join(stale[:5]) or 'none',
89
+ f'Checkpoint or close tasks idle for {STALE_DAYS}+ days: ' + ', '.join(stale[:3]) + '.')
90
+ passed = sum(c['ok'] for c in checks)
91
+ fixes = []
92
+ for fix in (c['fix'] for c in checks if c['fix']):
93
+ if not (fix.startswith('Run ws start') and any(f.startswith('Run ws start') for f in fixes)): fixes.append(fix) # one start fixes both
94
+ return {'repo': repo.name, 'score': f'{passed}/{len(checks)}', 'checks': checks, 'fixes': fixes[:3]}
95
+
96
+
97
+ def text(report):
98
+ lines = [f"AI setup audit for {report['repo']}: {report['score']}"]
99
+ lines += [f" {'ok ' if c['ok'] else 'fix '} {c['check']}: {c['detail']}" for c in report['checks']]
100
+ if report['fixes']: lines += ['Top fixes:'] + [f'{i}. {fix}' for i, fix in enumerate(report['fixes'], 1)]
101
+ return '\n'.join(lines)