coprogrammer 0.2.0a1__tar.gz → 0.3.0a1__tar.gz
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.
- {coprogrammer-0.2.0a1/src/coprogrammer.egg-info → coprogrammer-0.3.0a1}/PKG-INFO +30 -22
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/README.md +31 -23
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/pyproject.toml +1 -1
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/__init__.py +1 -1
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-active-sync/SKILL.md +26 -2
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/handoff.md +8 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/cli.py +9 -0
- coprogrammer-0.3.0a1/src/coprogrammer/evidence.py +161 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/manager_store.py +3 -3
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/mcp_server.py +7 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/setup.py +15 -7
- coprogrammer-0.3.0a1/src/coprogrammer/workflow.py +191 -0
- coprogrammer-0.3.0a1/src/coprogrammer/workflow_cli.py +93 -0
- coprogrammer-0.3.0a1/src/coprogrammer/workflow_mcp.py +43 -0
- coprogrammer-0.3.0a1/src/coprogrammer/workspace.py +256 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1/src/coprogrammer.egg-info}/PKG-INFO +30 -22
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer.egg-info/SOURCES.txt +7 -1
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_manager_store.py +23 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_setup.py +34 -0
- coprogrammer-0.3.0a1/tests/test_workspace_workflow.py +372 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/LICENSE +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/setup.cfg +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/__main__.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-integration-plan/SKILL.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-pr-digest-review/SKILL.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-project-covenant/SKILL.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-project-setup/SKILL.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/skills/coprogrammer-task-brief/SKILL.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/AGENTS.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/CLAUDE.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/COORDINATION.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/copilot-instructions.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/assets/templates/task-brief.md +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/briefing.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/channels.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/collaboration.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/collaboration_cli.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/collaboration_mcp.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/demo.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/integrations.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/knowledge.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/knowledge_mcp.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/providers.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/review.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/review_summary.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/scheduler.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/scheduler_cli.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer/scheduler_mcp.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer.egg-info/dependency_links.txt +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer.egg-info/entry_points.txt +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/src/coprogrammer.egg-info/top_level.txt +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_briefing.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_channels.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_cli.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_collaboration.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_collaboration_processes.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_demo.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_integrations.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_knowledge.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_knowledge_mcp.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_manager_performance.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_mcp_robustness.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_plugin_package.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_providers.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_review.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_review_summary.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_review_summary_cli.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_scheduler.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_scheduling_integration.py +0 -0
- {coprogrammer-0.2.0a1 → coprogrammer-0.3.0a1}/tests/test_upgrade_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coprogrammer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0a1
|
|
4
4
|
Summary: Open protocol and toolkit for semantic integration in multi-agent software development.
|
|
5
5
|
Author: Qiu Guanzong
|
|
6
6
|
License: MIT License
|
|
@@ -57,22 +57,26 @@ specific set of commits. Maintainers decide what reaches `main`.
|
|
|
57
57
|
[Ecosystem comparison](docs/research/ecosystem-2026-09-30.md) ·
|
|
58
58
|
[Contributing](CONTRIBUTING.md) · [MIT](LICENSE)
|
|
59
59
|
|
|
60
|
-
> **0.
|
|
61
|
-
>
|
|
62
|
-
>
|
|
63
|
-
>
|
|
60
|
+
> **v0.3 preview (`0.3.0a1`).** Check [PyPI](https://pypi.org/project/coprogrammer/)
|
|
61
|
+
> for published versions, or use the source installation below. Client configuration
|
|
62
|
+
> generators and provider adapters are implemented; live client/provider
|
|
63
|
+
> verification is still pending.
|
|
64
64
|
|
|
65
|
-
## New in
|
|
65
|
+
## New in v0.3
|
|
66
66
|
|
|
67
|
-
- **
|
|
68
|
-
|
|
69
|
-
- **
|
|
70
|
-
|
|
71
|
-
- **
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
- **Session-aware dispatch:** preview tasks compatible with this client,
|
|
68
|
+
session and worktree, then claim atomically.
|
|
69
|
+
- **Content-bound checks:** record explicit local commands and detect stale
|
|
70
|
+
results after file edits, command/runtime changes or expiry.
|
|
71
|
+
- **Portable handoffs:** compare versions, exact revisions, task scope and
|
|
72
|
+
content when moving work to another clone or computer. Local ownership remains
|
|
73
|
+
separate and receiving-machine checks are required.
|
|
74
|
+
|
|
75
|
+
See [dispatch and handoff](docs/WORKSPACE_HANDOFF.md),
|
|
76
|
+
[check receipts](docs/VALIDATION_EVIDENCE.md) and the
|
|
77
|
+
[source-backed choices](docs/upgrade-2026-10-04/research.md).
|
|
78
|
+
The offline demo, local messages, review adapters and preview-first setup remain
|
|
79
|
+
available through CLI, MCP and packaged workflows.
|
|
76
80
|
|
|
77
81
|
## When it helps
|
|
78
82
|
|
|
@@ -111,19 +115,23 @@ Requires **Python 3.10+** and **Git 2.36+**. Start with the offline demo; no mod
|
|
|
111
115
|
is needed.
|
|
112
116
|
|
|
113
117
|
```sh
|
|
114
|
-
git clone --branch codex/multi-platform-upgrade https://github.com/QIU-Guanzong/CoProgrammer.git
|
|
115
|
-
cd CoProgrammer
|
|
116
118
|
python3 -m venv .venv
|
|
117
119
|
source .venv/bin/activate
|
|
118
|
-
python -m pip install
|
|
119
|
-
|
|
120
|
+
python -m pip install --pre coprogrammer
|
|
120
121
|
coprogrammer demo
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
To work from source instead, clone the default branch and replace the package
|
|
125
|
+
install with `python -m pip install -e .`:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
git clone https://github.com/QIU-Guanzong/CoProgrammer.git
|
|
129
|
+
cd CoProgrammer
|
|
130
|
+
```
|
|
131
|
+
|
|
123
132
|
On Windows, create the environment with `python -m venv .venv` and activate it
|
|
124
|
-
in PowerShell with `.venv/Scripts/Activate.ps1`.
|
|
125
|
-
|
|
126
|
-
active, change to that project's checkout, and use its base branch.
|
|
133
|
+
in PowerShell with `.venv/Scripts/Activate.ps1`. For your own project, keep this
|
|
134
|
+
environment active, change to that project's checkout, and use its base branch.
|
|
127
135
|
|
|
128
136
|
The demo creates and cleans up a temporary repository and two worktrees. It
|
|
129
137
|
exercises the actual coordination code; the Codex/Claude names are local
|
|
@@ -16,22 +16,26 @@ specific set of commits. Maintainers decide what reaches `main`.
|
|
|
16
16
|
[Ecosystem comparison](docs/research/ecosystem-2026-09-30.md) ·
|
|
17
17
|
[Contributing](CONTRIBUTING.md) · [MIT](LICENSE)
|
|
18
18
|
|
|
19
|
-
> **0.
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
>
|
|
23
|
-
|
|
24
|
-
## New in
|
|
25
|
-
|
|
26
|
-
- **
|
|
27
|
-
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
- **
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
> **v0.3 preview (`0.3.0a1`).** Check [PyPI](https://pypi.org/project/coprogrammer/)
|
|
20
|
+
> for published versions, or use the source installation below. Client configuration
|
|
21
|
+
> generators and provider adapters are implemented; live client/provider
|
|
22
|
+
> verification is still pending.
|
|
23
|
+
|
|
24
|
+
## New in v0.3
|
|
25
|
+
|
|
26
|
+
- **Session-aware dispatch:** preview tasks compatible with this client,
|
|
27
|
+
session and worktree, then claim atomically.
|
|
28
|
+
- **Content-bound checks:** record explicit local commands and detect stale
|
|
29
|
+
results after file edits, command/runtime changes or expiry.
|
|
30
|
+
- **Portable handoffs:** compare versions, exact revisions, task scope and
|
|
31
|
+
content when moving work to another clone or computer. Local ownership remains
|
|
32
|
+
separate and receiving-machine checks are required.
|
|
33
|
+
|
|
34
|
+
See [dispatch and handoff](docs/WORKSPACE_HANDOFF.md),
|
|
35
|
+
[check receipts](docs/VALIDATION_EVIDENCE.md) and the
|
|
36
|
+
[source-backed choices](docs/upgrade-2026-10-04/research.md).
|
|
37
|
+
The offline demo, local messages, review adapters and preview-first setup remain
|
|
38
|
+
available through CLI, MCP and packaged workflows.
|
|
35
39
|
|
|
36
40
|
## When it helps
|
|
37
41
|
|
|
@@ -70,19 +74,23 @@ Requires **Python 3.10+** and **Git 2.36+**. Start with the offline demo; no mod
|
|
|
70
74
|
is needed.
|
|
71
75
|
|
|
72
76
|
```sh
|
|
73
|
-
git clone --branch codex/multi-platform-upgrade https://github.com/QIU-Guanzong/CoProgrammer.git
|
|
74
|
-
cd CoProgrammer
|
|
75
77
|
python3 -m venv .venv
|
|
76
78
|
source .venv/bin/activate
|
|
77
|
-
python -m pip install
|
|
78
|
-
|
|
79
|
+
python -m pip install --pre coprogrammer
|
|
79
80
|
coprogrammer demo
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
To work from source instead, clone the default branch and replace the package
|
|
84
|
+
install with `python -m pip install -e .`:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
git clone https://github.com/QIU-Guanzong/CoProgrammer.git
|
|
88
|
+
cd CoProgrammer
|
|
89
|
+
```
|
|
90
|
+
|
|
82
91
|
On Windows, create the environment with `python -m venv .venv` and activate it
|
|
83
|
-
in PowerShell with `.venv/Scripts/Activate.ps1`.
|
|
84
|
-
|
|
85
|
-
active, change to that project's checkout, and use its base branch.
|
|
92
|
+
in PowerShell with `.venv/Scripts/Activate.ps1`. For your own project, keep this
|
|
93
|
+
environment active, change to that project's checkout, and use its base branch.
|
|
86
94
|
|
|
87
95
|
The demo creates and cleans up a temporary repository and two worktrees. It
|
|
88
96
|
exercises the actual coordination code; the Codex/Claude names are local
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coprogrammer"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0a1"
|
|
8
8
|
description = "Open protocol and toolkit for semantic integration in multi-agent software development."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -16,8 +16,9 @@ touch overlapping code or shared contracts.
|
|
|
16
16
|
|
|
17
17
|
## Workflow
|
|
18
18
|
|
|
19
|
-
For cross-window work,
|
|
20
|
-
|
|
19
|
+
For cross-window work, check `coprogrammer --version` in each environment.
|
|
20
|
+
Use CoProgrammer 0.3+ for dispatch previews, content-bound checks and portable
|
|
21
|
+
handoffs; existing session/claim/message commands remain available. Keep one session ID per window, even when several
|
|
21
22
|
windows use the same client. Register once in that window's working tree; resume
|
|
22
23
|
with `pulse`, not another registration:
|
|
23
24
|
|
|
@@ -33,6 +34,8 @@ The inbox snapshot still includes unacknowledged messages after cursor advanceme
|
|
|
33
34
|
`freshness` measures explicit pulses, not process liveness; reads do not keep a
|
|
34
35
|
session alive. Git worktrees share Manager state; independent clones do not.
|
|
35
36
|
|
|
37
|
+
Preview eligible work with `coprogrammer manager dispatch --session <window-id>`.
|
|
38
|
+
This read does not claim or pulse; claim always rechecks constraints atomically.
|
|
36
39
|
For queued work, create scoped tasks with `manager task create`, optional
|
|
37
40
|
`--depends-on` and `--client` eligibility. Use `manager task claim` to atomically
|
|
38
41
|
claim a task and its path lease; preserve the returned `claim_id`. Do not request
|
|
@@ -53,6 +56,27 @@ they do not intercept writes or replace code review. Use a named Git branch,
|
|
|
53
56
|
not detached HEAD. Pulse before the session becomes stale and renew claims
|
|
54
57
|
before expiry; sync does neither.
|
|
55
58
|
|
|
59
|
+
Record explicit noninteractive checks with content-bound receipts before reporting
|
|
60
|
+
validation. Use an ignored directory or output outside the checkout:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
coprogrammer check run --label tests --output .coprogrammer/checks/tests.json \
|
|
64
|
+
-- python -m unittest discover -s tests
|
|
65
|
+
coprogrammer check verify --artifact .coprogrammer/checks/tests.json \
|
|
66
|
+
-- python -m unittest discover -s tests
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A failed, changed, expired or missing record cannot establish a current pass.
|
|
70
|
+
Ignored files and external inputs are outside the fingerprint; receipts do not
|
|
71
|
+
authenticate the worker. MCP `check_verify` never runs a command.
|
|
72
|
+
|
|
73
|
+
For another computer, reconcile and stop the source worker, commit intended
|
|
74
|
+
changes, and export `manager handoff --session <window-id> --task <task-id>
|
|
75
|
+
--base origin/main --check <receipt>` as JSON to an ignored/external path. The
|
|
76
|
+
receiver uses `workspace compare <bundle>` after an explicit Git fetch/checkout.
|
|
77
|
+
Context alignment transfers no ownership: rerun local checks and acquire a local
|
|
78
|
+
claim. Independent clones keep independent Managers; never copy tokens or logs.
|
|
79
|
+
|
|
56
80
|
Report completed work and actual validation with `manager task finish --summary
|
|
57
81
|
<result>`; use `task release` with a summary to requeue unfinished work. Both
|
|
58
82
|
require `--session`, `--id` and `--claim`. Completion is self-reported and does
|
|
@@ -23,3 +23,11 @@ action. Include any files that need reconciliation before another worker edits.
|
|
|
23
23
|
State whether the task was finished, released or still claimed, and whether
|
|
24
24
|
message receipt is pending. A handoff does not transfer a lease; the recipient
|
|
25
25
|
must sync and acquire their own task claim before editing.
|
|
26
|
+
|
|
27
|
+
## Portable context (CoProgrammer 0.3+)
|
|
28
|
+
|
|
29
|
+
Record `coprogrammer --version`, source HEAD/base, dirty state and content-bound
|
|
30
|
+
check statuses. Export `manager handoff` as JSON to an ignored/external path;
|
|
31
|
+
inspect it with `workspace compare` on the receiving machine. Confirm the source
|
|
32
|
+
worker stopped, run receiving-machine checks and acquire a local claim.
|
|
33
|
+
Context alignment does not transfer ownership between independent Managers.
|
|
@@ -401,12 +401,17 @@ def utc_now() -> str:
|
|
|
401
401
|
|
|
402
402
|
|
|
403
403
|
def run_git(args: list[str], cwd: Path) -> str:
|
|
404
|
+
# A caller's Git redirection must never move a requested project operation
|
|
405
|
+
# into another window's repository or index.
|
|
406
|
+
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
|
|
407
|
+
env.update(GIT_OPTIONAL_LOCKS="0", GIT_TERMINAL_PROMPT="0")
|
|
404
408
|
result = subprocess.run(
|
|
405
409
|
["git", *args],
|
|
406
410
|
cwd=cwd,
|
|
407
411
|
check=False,
|
|
408
412
|
text=True,
|
|
409
413
|
capture_output=True,
|
|
414
|
+
env=env,
|
|
410
415
|
)
|
|
411
416
|
if result.returncode != 0:
|
|
412
417
|
raise RuntimeError(result.stderr.strip() or "git command failed")
|
|
@@ -1785,6 +1790,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1785
1790
|
description="Protocol tooling for semantic branch integration.",
|
|
1786
1791
|
)
|
|
1787
1792
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1793
|
+
from . import __version__
|
|
1794
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
1788
1795
|
|
|
1789
1796
|
demo = subparsers.add_parser("demo", help="Run a real offline coordination demo in temporary Git worktrees.")
|
|
1790
1797
|
demo.add_argument("--json", action="store_true")
|
|
@@ -1961,6 +1968,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1961
1968
|
add_parsers(manager_subparsers)
|
|
1962
1969
|
from .scheduler_cli import add_parsers as add_task_parsers
|
|
1963
1970
|
add_task_parsers(manager_subparsers)
|
|
1971
|
+
from .workflow_cli import add_parsers as add_workflow_parsers
|
|
1972
|
+
add_workflow_parsers(subparsers, manager_subparsers)
|
|
1964
1973
|
|
|
1965
1974
|
briefing = manager_subparsers.add_parser("briefing", help="Summarize ready work, blockers and next actions.")
|
|
1966
1975
|
briefing.add_argument("--cwd", default=".")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Explicit local check receipts bound to working content and command hashes.
|
|
2
|
+
|
|
3
|
+
Receipts are unauthenticated local evidence. Ignored files, external inputs and
|
|
4
|
+
transient edits restored between observations are outside this boundary.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import __version__, collaboration as co, workspace as ws
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def environment() -> dict:
|
|
21
|
+
return {"tool_version": __version__, "python": platform.python_version(), "platform": sys.platform}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def command_digest(command: list[str]) -> str:
|
|
25
|
+
if not isinstance(command, list) or not 1 <= len(command) <= 128:
|
|
26
|
+
raise RuntimeError("check requires an explicit command argument vector")
|
|
27
|
+
for arg in command:
|
|
28
|
+
co.text(arg, "command argument", 4096, empty=True)
|
|
29
|
+
if "\0" in arg:
|
|
30
|
+
raise RuntimeError("invalid command argument")
|
|
31
|
+
if not command[0].strip():
|
|
32
|
+
raise RuntimeError("command executable is required")
|
|
33
|
+
return ws.digest(command)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _destination(cwd: Path, path: Path, label: str) -> Path:
|
|
37
|
+
path = path.absolute()
|
|
38
|
+
if path.is_symlink():
|
|
39
|
+
raise RuntimeError("check output must not be a symlink")
|
|
40
|
+
if path.resolve().is_relative_to(cwd):
|
|
41
|
+
relative = path.resolve().relative_to(cwd).as_posix()
|
|
42
|
+
if (ws.git(cwd, "ls-files", "--", relative).strip()
|
|
43
|
+
or not ws.git(cwd, "check-ignore", "--no-index", "--", relative, optional=True).strip()):
|
|
44
|
+
raise RuntimeError("check output must be ignored by Git or outside the repository")
|
|
45
|
+
if path.exists():
|
|
46
|
+
previous = ws.read_artifact(path)
|
|
47
|
+
if previous.get("format") != "coprogrammer.check.v1" or previous.get("label") != label:
|
|
48
|
+
raise RuntimeError("refusing to overwrite a different artifact")
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(cwd: Path, command: list[str], label: str, output: Path, timeout_seconds: int = 300) -> dict:
|
|
53
|
+
co.identity(label, "check label")
|
|
54
|
+
co.bounded(timeout_seconds, "timeout_seconds", 3600)
|
|
55
|
+
command_hash = command_digest(command)
|
|
56
|
+
cwd = ws.root(cwd)
|
|
57
|
+
output = _destination(cwd, output, label)
|
|
58
|
+
subject = ws.snapshot(cwd)
|
|
59
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
60
|
+
exit_code, timed_out, error = None, False, None
|
|
61
|
+
try:
|
|
62
|
+
# No shell interpretation, interactive input, output/argv persistence,
|
|
63
|
+
# or provider invocation implicit in the recording operation.
|
|
64
|
+
exit_code = subprocess.run(command, cwd=cwd, stdin=subprocess.DEVNULL,
|
|
65
|
+
stdout=sys.stderr, stderr=sys.stderr, timeout=timeout_seconds,
|
|
66
|
+
check=False).returncode
|
|
67
|
+
except subprocess.TimeoutExpired:
|
|
68
|
+
timed_out = True
|
|
69
|
+
error = "command timed out; inspect any surviving child processes"
|
|
70
|
+
except OSError:
|
|
71
|
+
error = "command could not start"
|
|
72
|
+
after = None
|
|
73
|
+
try:
|
|
74
|
+
after = ws.snapshot(cwd)["content"]["sha256"]
|
|
75
|
+
except (RuntimeError, OSError):
|
|
76
|
+
error = error or "workspace unavailable after command"
|
|
77
|
+
record = {"format": "coprogrammer.check.v1", "label": label,
|
|
78
|
+
"command_sha256": command_hash, "started_at": started_at,
|
|
79
|
+
"finished_at": datetime.now(timezone.utc).isoformat(),
|
|
80
|
+
"exit_code": exit_code, "timed_out": timed_out, "error": error,
|
|
81
|
+
"subject": subject, "after_sha256": after,
|
|
82
|
+
"stable": after == subject["content"]["sha256"], "environment": environment()}
|
|
83
|
+
record["integrity_sha256"] = ws.digest(record)
|
|
84
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
temporary = None
|
|
86
|
+
try:
|
|
87
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=output.parent,
|
|
88
|
+
prefix=".coprogrammer-check-", delete=False) as handle:
|
|
89
|
+
temporary = Path(handle.name)
|
|
90
|
+
handle.write(json.dumps(record, ensure_ascii=False, indent=2, allow_nan=False) + "\n")
|
|
91
|
+
handle.flush()
|
|
92
|
+
os.fsync(handle.fileno())
|
|
93
|
+
os.replace(temporary, output)
|
|
94
|
+
finally:
|
|
95
|
+
if temporary is not None:
|
|
96
|
+
temporary.unlink(missing_ok=True)
|
|
97
|
+
return record
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate(record: object) -> dict:
|
|
101
|
+
try:
|
|
102
|
+
fields = {"format", "label", "command_sha256", "started_at", "finished_at", "exit_code", "timed_out", "error", "subject", "after_sha256", "stable", "environment", "integrity_sha256"}
|
|
103
|
+
if not isinstance(record, dict) or set(record) != fields or record["format"] != "coprogrammer.check.v1":
|
|
104
|
+
raise ValueError
|
|
105
|
+
co.identity(record["label"], "check label")
|
|
106
|
+
ws.validate_snapshot(record["subject"])
|
|
107
|
+
if not ws.HASH.fullmatch(record["command_sha256"]) or not ws.HASH.fullmatch(record["integrity_sha256"]):
|
|
108
|
+
raise ValueError
|
|
109
|
+
if record["after_sha256"] is not None and not ws.HASH.fullmatch(record["after_sha256"]):
|
|
110
|
+
raise ValueError
|
|
111
|
+
if type(record["timed_out"]) is not bool or type(record["stable"]) is not bool:
|
|
112
|
+
raise ValueError
|
|
113
|
+
if record["exit_code"] is not None and (type(record["exit_code"]) is not int or not -2**31 <= record["exit_code"] < 2**31):
|
|
114
|
+
raise ValueError
|
|
115
|
+
if record["error"] is not None:
|
|
116
|
+
co.text(record["error"], "check error", 256)
|
|
117
|
+
if co.timestamp(record["finished_at"]) < co.timestamp(record["started_at"]):
|
|
118
|
+
raise ValueError
|
|
119
|
+
env = record["environment"]
|
|
120
|
+
if set(env) != {"tool_version", "python", "platform"}:
|
|
121
|
+
raise ValueError
|
|
122
|
+
for value in env.values():
|
|
123
|
+
co.text(value, "check environment", 64)
|
|
124
|
+
if record["stable"] != (record["after_sha256"] == record["subject"]["content"]["sha256"]):
|
|
125
|
+
raise ValueError
|
|
126
|
+
if record["integrity_sha256"] != ws.digest({k: v for k, v in record.items() if k != "integrity_sha256"}):
|
|
127
|
+
raise ValueError
|
|
128
|
+
except (ValueError, TypeError, KeyError, AttributeError, RuntimeError) as exc:
|
|
129
|
+
raise RuntimeError("invalid local check record or integrity mismatch") from exc
|
|
130
|
+
return record
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def verify(cwd: Path, path: Path, command: list[str] | None = None, *, current: dict | None = None,
|
|
134
|
+
max_age_seconds: int = 86400) -> dict:
|
|
135
|
+
co.bounded(max_age_seconds, "max_age_seconds", 604800)
|
|
136
|
+
record = validate(ws.read_artifact(path))
|
|
137
|
+
current = ws.snapshot(cwd) if current is None else current
|
|
138
|
+
reasons = []
|
|
139
|
+
age = (datetime.now(timezone.utc) - co.timestamp(record["finished_at"])).total_seconds()
|
|
140
|
+
if age < 0:
|
|
141
|
+
reasons.append("record_from_future")
|
|
142
|
+
elif age > max_age_seconds:
|
|
143
|
+
reasons.append("record_expired")
|
|
144
|
+
if record["exit_code"] != 0 or record["timed_out"] or record["error"]:
|
|
145
|
+
reasons.append("check_failed")
|
|
146
|
+
if not record["stable"]:
|
|
147
|
+
reasons.append("content_changed_during_check")
|
|
148
|
+
if record["subject"]["content"]["sha256"] != current["content"]["sha256"]:
|
|
149
|
+
reasons.append("content_changed")
|
|
150
|
+
if not ws.same_repository(record["subject"], current):
|
|
151
|
+
reasons.append("repository_changed")
|
|
152
|
+
if record["environment"] != environment():
|
|
153
|
+
reasons.append("environment_changed")
|
|
154
|
+
if command is not None and command_digest(command) != record["command_sha256"]:
|
|
155
|
+
reasons.append("command_changed")
|
|
156
|
+
return {"format": "coprogrammer.check-status.v1", "label": record["label"],
|
|
157
|
+
"fresh": not reasons, "reasons": reasons, "exit_code": record["exit_code"],
|
|
158
|
+
"finished_at": record["finished_at"], "command_sha256": record["command_sha256"],
|
|
159
|
+
"expected_command_checked": command is not None,
|
|
160
|
+
"content_sha256": record["subject"]["content"]["sha256"],
|
|
161
|
+
"note": "Unauthenticated local record; ignored files, dependencies and external inputs are not covered."}
|
|
@@ -206,9 +206,9 @@ def transaction(path: Path, timeout: float = DEFAULT_LOCK_TIMEOUT) -> Iterator[E
|
|
|
206
206
|
try:
|
|
207
207
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
208
208
|
with path.with_name(path.name + ".lock").open("a+b") as handle:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
# Windows permits locking beyond EOF. Bootstrap writes before the
|
|
210
|
+
# byte lock can conflict with another owner; keep even a new lock
|
|
211
|
+
# file empty and acquire ownership before accessing Manager state.
|
|
212
212
|
_lock(handle, deadline)
|
|
213
213
|
try:
|
|
214
214
|
current = EventTransaction(path)
|
|
@@ -30,6 +30,7 @@ from . import __version__
|
|
|
30
30
|
from . import collaboration_mcp
|
|
31
31
|
from . import scheduler_mcp
|
|
32
32
|
from . import knowledge_mcp
|
|
33
|
+
from . import workflow_mcp
|
|
33
34
|
|
|
34
35
|
SUPPORTED_PROTOCOL_VERSIONS = (
|
|
35
36
|
"2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25",
|
|
@@ -227,8 +228,10 @@ TOOLS: list[dict[str, Any]] = [
|
|
|
227
228
|
|
|
228
229
|
TOOLS.extend(collaboration_mcp.declarations(string_schema))
|
|
229
230
|
TOOLS.extend(scheduler_mcp.declarations(string_schema))
|
|
231
|
+
TOOLS.extend(workflow_mcp.declarations(string_schema))
|
|
230
232
|
READ_ONLY_TOOLS = {"digest_branch", "manager_status", "manager_briefing", "manager_forecast", "review_summary",
|
|
231
233
|
"manager_sync", "message_inbox", "manager_wait", "message_thread", "task_board", "task_guard"}
|
|
234
|
+
READ_ONLY_TOOLS.update({"workspace_snapshot", "task_dispatch", "manager_handoff", "handoff_check", "check_verify"})
|
|
232
235
|
for _tool in TOOLS:
|
|
233
236
|
_tool["inputSchema"]["additionalProperties"] = False
|
|
234
237
|
_tool["annotations"] = {
|
|
@@ -421,6 +424,10 @@ for _name in ("task_create", "task_claim", "task_renew", "task_finish", "task_re
|
|
|
421
424
|
TOOL_HANDLERS[_name] = lambda ctx, args, name=_name: json.dumps(
|
|
422
425
|
scheduler_mcp.dispatch(name, ctx, args), indent=2, ensure_ascii=False)
|
|
423
426
|
|
|
427
|
+
for _name in ("workspace_snapshot", "task_dispatch", "manager_handoff", "handoff_check", "check_verify"):
|
|
428
|
+
TOOL_HANDLERS[_name] = lambda ctx, args, name=_name: json.dumps(
|
|
429
|
+
workflow_mcp.dispatch(name, ctx, args), indent=2, ensure_ascii=False)
|
|
430
|
+
|
|
424
431
|
|
|
425
432
|
def validate_value(value: Any, schema: dict[str, Any], field: str = "arguments") -> None:
|
|
426
433
|
"""Validate the JSON Schema keywords used by this server's own tool schemas.
|
|
@@ -28,14 +28,22 @@ def _target(root: Path, relative: str) -> Path:
|
|
|
28
28
|
current = root
|
|
29
29
|
for part in Path(relative).parts:
|
|
30
30
|
current = current / part
|
|
31
|
-
|
|
31
|
+
try:
|
|
32
|
+
metadata = current.lstat()
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
continue
|
|
35
|
+
# Resolving an existing Windows lock file may fail to open it and
|
|
36
|
+
# retain different path casing. Inspect non-following metadata instead;
|
|
37
|
+
# junctions and other reparse points remain blocked along with symlinks.
|
|
38
|
+
if (stat.S_ISLNK(metadata.st_mode)
|
|
39
|
+
or getattr(metadata, "st_file_attributes", 0)
|
|
40
|
+
& getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)):
|
|
32
41
|
raise RuntimeError(f"unsafe linked setup path: {relative}")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
raise RuntimeError(f"setup parent is not a directory: {relative}")
|
|
42
|
+
mode = metadata.st_mode
|
|
43
|
+
if current == target and not stat.S_ISREG(mode):
|
|
44
|
+
raise RuntimeError(f"setup target is not a regular file: {relative}")
|
|
45
|
+
if current != target and not stat.S_ISDIR(mode):
|
|
46
|
+
raise RuntimeError(f"setup parent is not a directory: {relative}")
|
|
39
47
|
return target
|
|
40
48
|
|
|
41
49
|
|