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.
- ai_dev_workspace-0.1.0b5.dist-info/METADATA +74 -0
- ai_dev_workspace-0.1.0b5.dist-info/RECORD +130 -0
- ai_dev_workspace-0.1.0b5.dist-info/WHEEL +5 -0
- ai_dev_workspace-0.1.0b5.dist-info/entry_points.txt +3 -0
- ai_dev_workspace-0.1.0b5.dist-info/licenses/LICENSE +21 -0
- ai_dev_workspace-0.1.0b5.dist-info/top_level.txt +1 -0
- ws/LICENSE +21 -0
- ws/__init__.py +0 -0
- ws/assist.py +232 -0
- ws/audit.py +101 -0
- ws/bin/publish-release +34 -0
- ws/bin/release-smoke +322 -0
- ws/bin/ws +11 -0
- ws/bin/ws-plugin +10 -0
- ws/bin/ws-python +8 -0
- ws/cards.py +104 -0
- ws/cli.py +935 -0
- ws/codeburn.py +79 -0
- ws/core.py +3396 -0
- ws/demo.py +57 -0
- ws/discipline.py +56 -0
- ws/docs/ADVANCED.md +95 -0
- ws/docs/AGENT-ORCHESTRATION.md +145 -0
- ws/docs/ARCHITECTURE.md +66 -0
- ws/docs/ASSIST-FEATURES.md +11 -0
- ws/docs/CLIENT-VERIFICATION.md +40 -0
- ws/docs/COMMANDS.md +309 -0
- ws/docs/CONCEPTS.md +26 -0
- ws/docs/DEMO.md +118 -0
- ws/docs/MAINTAINING.md +46 -0
- ws/docs/MCP-WORKFLOWS.md +15 -0
- ws/docs/MCP.md +20 -0
- ws/docs/MEASUREMENTS.md +55 -0
- ws/docs/OVERVIEW.md +151 -0
- ws/docs/PACKS.md +42 -0
- ws/docs/PARALLEL-WORKERS.md +132 -0
- ws/docs/PRINCIPLES.md +11 -0
- ws/docs/PRIVATE-PACKS.md +14 -0
- ws/docs/REPO-START.md +40 -0
- ws/docs/ROADMAP.md +43 -0
- ws/docs/SESSION-SEARCH.md +19 -0
- ws/docs/SETUP.md +139 -0
- ws/docs/TEAM-WORKSPACE.md +82 -0
- ws/docs/TEAM.md +43 -0
- ws/docs/TUTORIAL.md +184 -0
- ws/docs/UPGRADE-CHECK.md +22 -0
- ws/docs/WINDOWS.md +25 -0
- ws/docs/demo.tape +25 -0
- ws/docs/examples/frameworks/README.md +42 -0
- ws/docs/examples/frameworks/adk-requirements.txt +2 -0
- ws/docs/examples/frameworks/adk_agent.py +47 -0
- ws/docs/examples/frameworks/common.py +40 -0
- ws/docs/examples/frameworks/langgraph-requirements.txt +4 -0
- ws/docs/examples/frameworks/langgraph_agent.py +31 -0
- ws/docs/examples/frameworks/proof.json +42 -0
- ws/docs/examples/frameworks/prove.py +37 -0
- ws/docs/examples/mcp-protocol-proof.json +1 -0
- ws/folder_io.py +107 -0
- ws/folder_publish.py +77 -0
- ws/guards.py +719 -0
- ws/kit.json +1 -0
- ws/knowledge.py +193 -0
- ws/mcp/server.py +342 -0
- ws/memory_import.py +104 -0
- ws/native.py +185 -0
- ws/orchestration.py +347 -0
- ws/packaging.py +6 -0
- ws/packs/codex-worker/.gitignore +1 -0
- ws/packs/codex-worker/AGENTS.snippet.md +4 -0
- ws/packs/codex-worker/investigate.sh +19 -0
- ws/packs/codex-worker/pack.json +7 -0
- ws/packs/codex-worker/patch_modes.py +31 -0
- ws/packs/codex-worker/selftest.sh +22 -0
- ws/packs/codex-worker/setup.sh +9 -0
- ws/packs/discipline/AGENTS.snippet.md +2 -0
- ws/packs/discipline/pack.json +4 -0
- ws/packs/guards/AGENTS.snippet.md +2 -0
- ws/packs/guards/pack.json +5 -0
- ws/packs/ios/AGENTS.snippet.md +6 -0
- ws/packs/ios/pack.json +5 -0
- ws/packs/ios/vault/Runbooks/App Store review replies.md +7 -0
- ws/packs/ios/vault/Runbooks/iOS Simulator debugging.md +10 -0
- ws/packs/ios/vault/Runbooks/iOS build triage.md +8 -0
- ws/packs/library/AGENTS.snippet.md +5 -0
- ws/packs/library/pack.json +3 -0
- ws/packs/local-llm/AGENTS.snippet.md +3 -0
- ws/packs/local-llm/pack.json +3 -0
- ws/packs/local-llm/summarize.py +38 -0
- ws/packs/ml/AGENTS.snippet.md +5 -0
- ws/packs/ml/pack.json +3 -0
- ws/packs/obsidian/AGENTS.snippet.md +3 -0
- ws/packs/obsidian/pack.json +3 -0
- ws/packs/obsidian/vault/.obsidian/app.json +7 -0
- ws/packs/obsidian/vault/.obsidian/core-plugins.json +1 -0
- ws/packs/obsidian/vault/.obsidian/templates.json +3 -0
- ws/packs/web/AGENTS.snippet.md +5 -0
- ws/packs/web/pack.json +3 -0
- ws/packs.py +125 -0
- ws/plugin_runtime.py +44 -0
- ws/repo.py +648 -0
- ws/scope.py +53 -0
- ws/setup.py +116 -0
- ws/sharing.py +222 -0
- ws/skills/handoff/SKILL.md +8 -0
- ws/skills/lesson/SKILL.md +7 -0
- ws/skills/pickup/SKILL.md +7 -0
- ws/skills/thinkbeforeact/SKILL.md +25 -0
- ws/team.py +131 -0
- ws/team_auto.py +87 -0
- ws/team_folder.py +167 -0
- ws/team_status.py +87 -0
- ws/team_sync.py +169 -0
- ws/team_ui.py +87 -0
- ws/template/.gitignore +2 -0
- ws/template/AGENTS.md +21 -0
- ws/template/CLAUDE.md +1 -0
- ws/template/routing-presets.json +10 -0
- ws/template/routing.json +17 -0
- ws/template/vault/Analysis/INDEX.md +5 -0
- ws/template/vault/Feedback.md +2 -0
- ws/template/vault/Learnings.md +2 -0
- ws/template/vault/Product/INDEX.md +2 -0
- ws/template/vault/Project/INDEX.md +2 -0
- ws/template/vault/README.md +14 -0
- ws/template/vault/Runbooks/Delegation.md +15 -0
- ws/template/vault/Templates/Task.md +35 -0
- ws/tools.json +10 -0
- ws/transcripts.py +78 -0
- ws/upgrade.py +216 -0
- 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,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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ws
|
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)
|