severally-mcp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG.md +4 -2
- package/README.ja.md +28 -15
- package/README.md +29 -15
- package/docs/how-it-works.ja.md +7 -1
- package/docs/how-it-works.md +7 -1
- package/docs/install.ja.md +13 -1
- package/docs/install.md +16 -2
- package/package.json +2 -2
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +4 -2
- package/plugins/severally/.mcp.json +2 -1
- package/plugins/severally/dist/severally-mcp.mjs +327 -94
- package/plugins/severally/skills/_template/SKILL.md.tmpl +5 -3
- package/plugins/severally/skills/_template/reference.md.tmpl +12 -6
- package/plugins/severally/skills/antigravity/severally/SKILL.md +5 -3
- package/plugins/severally/skills/antigravity/severally/reference.md +12 -6
- package/plugins/severally/skills/claude/severally/SKILL.md +5 -3
- package/plugins/severally/skills/claude/severally/reference.md +12 -6
- package/plugins/severally/skills/codex/severally/SKILL.md +5 -3
- package/plugins/severally/skills/codex/severally/reference.md +12 -6
- package/plugins/severally/skills/opencode/severally/SKILL.md +5 -3
- package/plugins/severally/skills/opencode/severally/reference.md +12 -6
- package/scripts/init-config.mjs +1 -1
- package/scripts/install.mjs +35 -5
- package/src/adapters/antigravity-sandbox.mjs +5 -1
- package/src/adapters/codex.mjs +17 -6
- package/src/brief.mjs +38 -16
- package/src/expose-paths.mjs +68 -5
- package/src/jobs.mjs +171 -45
- package/src/policy.mjs +5 -4
- package/src/redact.mjs +20 -1
- package/src/schema.mjs +5 -4
- package/src/server.mjs +44 -16
|
@@ -22,8 +22,9 @@ see your session, or consult anyone else. In practice they know only what you pu
|
|
|
22
22
|
}})
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits
|
|
26
|
-
the
|
|
25
|
+
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits for the answer by itself,
|
|
26
|
+
up to a limit the server sets (45 seconds, or ten minutes where the client allows it); call it again while
|
|
27
|
+
`status` is `running`. To work on something independent meanwhile, ask for a shorter wait (`reference.md`).
|
|
27
28
|
3. **Check one point, report, and record it once** (see **Before you hand the decision back**).
|
|
28
29
|
|
|
29
30
|
That is the whole routine. The rest of the record — more verdicts, a prediction and reflection, follow-ups,
|
|
@@ -82,7 +83,8 @@ Pass `initiator: "user"` when the user asked for the consultation; leave it out
|
|
|
82
83
|
and ask the one whose answer changes what you do next.
|
|
83
84
|
- **Hand over the code.** The consultant starts in an empty working directory and is not told where your
|
|
84
85
|
repository is. Name the files by absolute path in `context.expose_paths` in this first call (at most 20
|
|
85
|
-
entries, 500 files, 5 MB; never your home directory or one holding keys)
|
|
86
|
+
entries, 500 files, 5 MB; never your home directory or one holding keys). A repository root works: only the
|
|
87
|
+
files git lists are copied, nothing `.gitignore` excludes. Or pass
|
|
86
88
|
`context.expose_diff: { repo: "<absolute path>" }` for a change you are making. When `consult_start` answers
|
|
87
89
|
`files_exposed: 0` and the question is about code, `consult_cancel` and start again with `expose_paths`.
|
|
88
90
|
- **Put the load-bearing facts in `context.facts`**, and test output or a log in `context.artifacts`.
|
|
@@ -70,10 +70,14 @@ names. Your own CLI is a fresh session but not another lineage, and its `quality
|
|
|
70
70
|
consultants of one vendor.
|
|
71
71
|
|
|
72
72
|
Send **one** `targets` call: every consultant receives the byte-identical brief, which is what makes the answers
|
|
73
|
-
comparable. Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
-
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap (nine by default,
|
|
75
|
-
every severally session on this machine
|
|
76
|
-
|
|
73
|
+
comparable (on a follow-up to the group, the same new question, with each one's own earlier rounds). Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
+
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap per consultant (nine by default,
|
|
75
|
+
shared by every severally session on this machine; the group starts only if every member gets one) and one
|
|
76
|
+
consultation's quota per consultant. To ask every member again, follow up the group: `followup_to: "<group_id>"`
|
|
77
|
+
with no `target` or `targets` (see Follow-ups).
|
|
78
|
+
|
|
79
|
+
While you wait, `consult_get` blocks for up to its limit (ten minutes where the client allows it). To work on
|
|
80
|
+
something independent in between, pass a shorter `wait_ms` and come back.
|
|
77
81
|
|
|
78
82
|
`comparison.by_target` holds, per consultant: stance, summary, confidence, evidence basis, finding points (each
|
|
79
83
|
with its `id` and severity), `severity_counts`, `decision_changers`, alternatives, unknowns (`u1` …),
|
|
@@ -93,7 +97,7 @@ Reading them against each other:
|
|
|
93
97
|
4. **Read `decision_changers` before spending a follow-up**: a consultant that named the condition under which
|
|
94
98
|
it would change its mind has told you what evidence to go get.
|
|
95
99
|
5. **Spend a follow-up only on a divergence that would change your decision**, sent only to the consultant
|
|
96
|
-
whose reading it belongs to.
|
|
100
|
+
whose reading it belongs to, or to the whole group (`followup_to: "<group_id>"`) when it concerns all of them.
|
|
97
101
|
6. **A stance every consultant shares is not evidence that the plan was checked.** Look for a premise they all
|
|
98
102
|
rely on and none could verify; name it only if you can actually identify one.
|
|
99
103
|
|
|
@@ -123,7 +127,9 @@ in `failure.detail`, which tells you whether to retry with a longer budget or a
|
|
|
123
127
|
|
|
124
128
|
By default a chain has five rounds: one initial round plus up to four follow-ups
|
|
125
129
|
(`followup_to: "<previous job_id>"`); the operator can configure 1–20 (`server.max_rounds`), and
|
|
126
|
-
`rounds_remaining` says what is left.
|
|
130
|
+
`rounds_remaining` says what is left. `followup_to: "<group_id>"`, with no `target` or `targets`, sends the same new
|
|
131
|
+
question to every member of a fan-out, each continuing from its own latest round with its own history; a member
|
|
132
|
+
that is still running, never answered or has used its rounds is listed under `skipped` and not asked. A follow-up starts in a fresh working directory with no files: pass
|
|
127
133
|
`context.expose_from_parent: true` to copy again, as they are now, the paths the earlier round exposed (a changed
|
|
128
134
|
or vanished entry is marked), and leave it off when the follow-up does not need them.
|
|
129
135
|
|
|
@@ -29,8 +29,9 @@ copied read-only into its working directory. The Codex consultant additionally h
|
|
|
29
29
|
}})
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits
|
|
33
|
-
the
|
|
32
|
+
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits for the answer by itself,
|
|
33
|
+
up to a limit the server sets (45 seconds, or ten minutes where the client allows it); call it again while
|
|
34
|
+
`status` is `running`. To work on something independent meanwhile, ask for a shorter wait (`reference.md`).
|
|
34
35
|
3. **Check one point, report, and record it once** (see **Before you hand the decision back**).
|
|
35
36
|
|
|
36
37
|
That is the whole routine. The rest of the record — more verdicts, a prediction and reflection, follow-ups,
|
|
@@ -93,7 +94,8 @@ Pass `initiator: "user"` when the user asked for the consultation; leave it out
|
|
|
93
94
|
and ask the one whose answer changes what you do next.
|
|
94
95
|
- **Hand over the code.** The consultant starts in an empty working directory and is not told where your
|
|
95
96
|
repository is. Name the files by absolute path in `context.expose_paths` in this first call (at most 20
|
|
96
|
-
entries, 500 files, 5 MB; never your home directory or one holding keys)
|
|
97
|
+
entries, 500 files, 5 MB; never your home directory or one holding keys). A repository root works: only the
|
|
98
|
+
files git lists are copied, nothing `.gitignore` excludes. Or pass
|
|
97
99
|
`context.expose_diff: { repo: "<absolute path>" }` for a change you are making. When `consult_start` answers
|
|
98
100
|
`files_exposed: 0` and the question is about code, `consult_cancel` and start again with `expose_paths`.
|
|
99
101
|
- **Put the load-bearing facts in `context.facts`**, and test output or a log in `context.artifacts`.
|
|
@@ -70,10 +70,14 @@ names. Your own CLI is a fresh session but not another lineage, and its `quality
|
|
|
70
70
|
consultants of one vendor.
|
|
71
71
|
|
|
72
72
|
Send **one** `targets` call: every consultant receives the byte-identical brief, which is what makes the answers
|
|
73
|
-
comparable. Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
-
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap (nine by default,
|
|
75
|
-
every severally session on this machine
|
|
76
|
-
|
|
73
|
+
comparable (on a follow-up to the group, the same new question, with each one's own earlier rounds). Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
+
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap per consultant (nine by default,
|
|
75
|
+
shared by every severally session on this machine; the group starts only if every member gets one) and one
|
|
76
|
+
consultation's quota per consultant. To ask every member again, follow up the group: `followup_to: "<group_id>"`
|
|
77
|
+
with no `target` or `targets` (see Follow-ups).
|
|
78
|
+
|
|
79
|
+
While you wait, `consult_get` blocks for up to its limit (ten minutes where the client allows it). To work on
|
|
80
|
+
something independent in between, pass a shorter `wait_ms` and come back.
|
|
77
81
|
|
|
78
82
|
`comparison.by_target` holds, per consultant: stance, summary, confidence, evidence basis, finding points (each
|
|
79
83
|
with its `id` and severity), `severity_counts`, `decision_changers`, alternatives, unknowns (`u1` …),
|
|
@@ -93,7 +97,7 @@ Reading them against each other:
|
|
|
93
97
|
4. **Read `decision_changers` before spending a follow-up**: a consultant that named the condition under which
|
|
94
98
|
it would change its mind has told you what evidence to go get.
|
|
95
99
|
5. **Spend a follow-up only on a divergence that would change your decision**, sent only to the consultant
|
|
96
|
-
whose reading it belongs to.
|
|
100
|
+
whose reading it belongs to, or to the whole group (`followup_to: "<group_id>"`) when it concerns all of them.
|
|
97
101
|
6. **A stance every consultant shares is not evidence that the plan was checked.** Look for a premise they all
|
|
98
102
|
rely on and none could verify; name it only if you can actually identify one.
|
|
99
103
|
|
|
@@ -123,7 +127,9 @@ in `failure.detail`, which tells you whether to retry with a longer budget or a
|
|
|
123
127
|
|
|
124
128
|
By default a chain has five rounds: one initial round plus up to four follow-ups
|
|
125
129
|
(`followup_to: "<previous job_id>"`); the operator can configure 1–20 (`server.max_rounds`), and
|
|
126
|
-
`rounds_remaining` says what is left.
|
|
130
|
+
`rounds_remaining` says what is left. `followup_to: "<group_id>"`, with no `target` or `targets`, sends the same new
|
|
131
|
+
question to every member of a fan-out, each continuing from its own latest round with its own history; a member
|
|
132
|
+
that is still running, never answered or has used its rounds is listed under `skipped` and not asked. A follow-up starts in a fresh working directory with no files: pass
|
|
127
133
|
`context.expose_from_parent: true` to copy again, as they are now, the paths the earlier round exposed (a changed
|
|
128
134
|
or vanished entry is marked), and leave it off when the follow-up does not need them.
|
|
129
135
|
|
|
@@ -30,8 +30,9 @@ copied read-only into its working directory. The Codex consultant additionally h
|
|
|
30
30
|
}})
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits
|
|
34
|
-
the
|
|
33
|
+
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits for the answer by itself,
|
|
34
|
+
up to a limit the server sets (45 seconds, or ten minutes where the client allows it); call it again while
|
|
35
|
+
`status` is `running`. To work on something independent meanwhile, ask for a shorter wait (`reference.md`).
|
|
35
36
|
3. **Check one point, report, and record it once** (see **Before you hand the decision back**).
|
|
36
37
|
|
|
37
38
|
That is the whole routine. The rest of the record — more verdicts, a prediction and reflection, follow-ups,
|
|
@@ -94,7 +95,8 @@ Pass `initiator: "user"` when the user asked for the consultation; leave it out
|
|
|
94
95
|
and ask the one whose answer changes what you do next.
|
|
95
96
|
- **Hand over the code.** The consultant starts in an empty working directory and is not told where your
|
|
96
97
|
repository is. Name the files by absolute path in `context.expose_paths` in this first call (at most 20
|
|
97
|
-
entries, 500 files, 5 MB; never your home directory or one holding keys)
|
|
98
|
+
entries, 500 files, 5 MB; never your home directory or one holding keys). A repository root works: only the
|
|
99
|
+
files git lists are copied, nothing `.gitignore` excludes. Or pass
|
|
98
100
|
`context.expose_diff: { repo: "<absolute path>" }` for a change you are making. When `consult_start` answers
|
|
99
101
|
`files_exposed: 0` and the question is about code, `consult_cancel` and start again with `expose_paths`.
|
|
100
102
|
- **Put the load-bearing facts in `context.facts`**, and test output or a log in `context.artifacts`.
|
|
@@ -70,10 +70,14 @@ names. Your own CLI is a fresh session but not another lineage, and its `quality
|
|
|
70
70
|
consultants of one vendor.
|
|
71
71
|
|
|
72
72
|
Send **one** `targets` call: every consultant receives the byte-identical brief, which is what makes the answers
|
|
73
|
-
comparable. Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
-
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap (nine by default,
|
|
75
|
-
every severally session on this machine
|
|
76
|
-
|
|
73
|
+
comparable (on a follow-up to the group, the same new question, with each one's own earlier rounds). Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
+
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap per consultant (nine by default,
|
|
75
|
+
shared by every severally session on this machine; the group starts only if every member gets one) and one
|
|
76
|
+
consultation's quota per consultant. To ask every member again, follow up the group: `followup_to: "<group_id>"`
|
|
77
|
+
with no `target` or `targets` (see Follow-ups).
|
|
78
|
+
|
|
79
|
+
While you wait, `consult_get` blocks for up to its limit (ten minutes where the client allows it). To work on
|
|
80
|
+
something independent in between, pass a shorter `wait_ms` and come back.
|
|
77
81
|
|
|
78
82
|
`comparison.by_target` holds, per consultant: stance, summary, confidence, evidence basis, finding points (each
|
|
79
83
|
with its `id` and severity), `severity_counts`, `decision_changers`, alternatives, unknowns (`u1` …),
|
|
@@ -93,7 +97,7 @@ Reading them against each other:
|
|
|
93
97
|
4. **Read `decision_changers` before spending a follow-up**: a consultant that named the condition under which
|
|
94
98
|
it would change its mind has told you what evidence to go get.
|
|
95
99
|
5. **Spend a follow-up only on a divergence that would change your decision**, sent only to the consultant
|
|
96
|
-
whose reading it belongs to.
|
|
100
|
+
whose reading it belongs to, or to the whole group (`followup_to: "<group_id>"`) when it concerns all of them.
|
|
97
101
|
6. **A stance every consultant shares is not evidence that the plan was checked.** Look for a premise they all
|
|
98
102
|
rely on and none could verify; name it only if you can actually identify one.
|
|
99
103
|
|
|
@@ -123,7 +127,9 @@ in `failure.detail`, which tells you whether to retry with a longer budget or a
|
|
|
123
127
|
|
|
124
128
|
By default a chain has five rounds: one initial round plus up to four follow-ups
|
|
125
129
|
(`followup_to: "<previous job_id>"`); the operator can configure 1–20 (`server.max_rounds`), and
|
|
126
|
-
`rounds_remaining` says what is left.
|
|
130
|
+
`rounds_remaining` says what is left. `followup_to: "<group_id>"`, with no `target` or `targets`, sends the same new
|
|
131
|
+
question to every member of a fan-out, each continuing from its own latest round with its own history; a member
|
|
132
|
+
that is still running, never answered or has used its rounds is listed under `skipped` and not asked. A follow-up starts in a fresh working directory with no files: pass
|
|
127
133
|
`context.expose_from_parent: true` to copy again, as they are now, the paths the earlier round exposed (a changed
|
|
128
134
|
or vanished entry is marked), and leave it off when the follow-up does not need them.
|
|
129
135
|
|
|
@@ -29,8 +29,9 @@ copied read-only into its working directory.
|
|
|
29
29
|
}})
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits
|
|
33
|
-
the
|
|
32
|
+
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits for the answer by itself,
|
|
33
|
+
up to a limit the server sets (45 seconds, or ten minutes where the client allows it); call it again while
|
|
34
|
+
`status` is `running`. To work on something independent meanwhile, ask for a shorter wait (`reference.md`).
|
|
34
35
|
3. **Check one point, report, and record it once** (see **Before you hand the decision back**).
|
|
35
36
|
|
|
36
37
|
That is the whole routine. The rest of the record — more verdicts, a prediction and reflection, follow-ups,
|
|
@@ -93,7 +94,8 @@ Pass `initiator: "user"` when the user asked for the consultation; leave it out
|
|
|
93
94
|
and ask the one whose answer changes what you do next.
|
|
94
95
|
- **Hand over the code.** The consultant starts in an empty working directory and is not told where your
|
|
95
96
|
repository is. Name the files by absolute path in `context.expose_paths` in this first call (at most 20
|
|
96
|
-
entries, 500 files, 5 MB; never your home directory or one holding keys)
|
|
97
|
+
entries, 500 files, 5 MB; never your home directory or one holding keys). A repository root works: only the
|
|
98
|
+
files git lists are copied, nothing `.gitignore` excludes. Or pass
|
|
97
99
|
`context.expose_diff: { repo: "<absolute path>" }` for a change you are making. When `consult_start` answers
|
|
98
100
|
`files_exposed: 0` and the question is about code, `consult_cancel` and start again with `expose_paths`.
|
|
99
101
|
- **Put the load-bearing facts in `context.facts`**, and test output or a log in `context.artifacts`.
|
|
@@ -70,10 +70,14 @@ names. Your own CLI is a fresh session but not another lineage, and its `quality
|
|
|
70
70
|
consultants of one vendor.
|
|
71
71
|
|
|
72
72
|
Send **one** `targets` call: every consultant receives the byte-identical brief, which is what makes the answers
|
|
73
|
-
comparable. Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
-
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap (nine by default,
|
|
75
|
-
every severally session on this machine
|
|
76
|
-
|
|
73
|
+
comparable (on a follow-up to the group, the same new question, with each one's own earlier rounds). Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
+
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap per consultant (nine by default,
|
|
75
|
+
shared by every severally session on this machine; the group starts only if every member gets one) and one
|
|
76
|
+
consultation's quota per consultant. To ask every member again, follow up the group: `followup_to: "<group_id>"`
|
|
77
|
+
with no `target` or `targets` (see Follow-ups).
|
|
78
|
+
|
|
79
|
+
While you wait, `consult_get` blocks for up to its limit (ten minutes where the client allows it). To work on
|
|
80
|
+
something independent in between, pass a shorter `wait_ms` and come back.
|
|
77
81
|
|
|
78
82
|
`comparison.by_target` holds, per consultant: stance, summary, confidence, evidence basis, finding points (each
|
|
79
83
|
with its `id` and severity), `severity_counts`, `decision_changers`, alternatives, unknowns (`u1` …),
|
|
@@ -93,7 +97,7 @@ Reading them against each other:
|
|
|
93
97
|
4. **Read `decision_changers` before spending a follow-up**: a consultant that named the condition under which
|
|
94
98
|
it would change its mind has told you what evidence to go get.
|
|
95
99
|
5. **Spend a follow-up only on a divergence that would change your decision**, sent only to the consultant
|
|
96
|
-
whose reading it belongs to.
|
|
100
|
+
whose reading it belongs to, or to the whole group (`followup_to: "<group_id>"`) when it concerns all of them.
|
|
97
101
|
6. **A stance every consultant shares is not evidence that the plan was checked.** Look for a premise they all
|
|
98
102
|
rely on and none could verify; name it only if you can actually identify one.
|
|
99
103
|
|
|
@@ -123,7 +127,9 @@ in `failure.detail`, which tells you whether to retry with a longer budget or a
|
|
|
123
127
|
|
|
124
128
|
By default a chain has five rounds: one initial round plus up to four follow-ups
|
|
125
129
|
(`followup_to: "<previous job_id>"`); the operator can configure 1–20 (`server.max_rounds`), and
|
|
126
|
-
`rounds_remaining` says what is left.
|
|
130
|
+
`rounds_remaining` says what is left. `followup_to: "<group_id>"`, with no `target` or `targets`, sends the same new
|
|
131
|
+
question to every member of a fan-out, each continuing from its own latest round with its own history; a member
|
|
132
|
+
that is still running, never answered or has used its rounds is listed under `skipped` and not asked. A follow-up starts in a fresh working directory with no files: pass
|
|
127
133
|
`context.expose_from_parent: true` to copy again, as they are now, the paths the earlier round exposed (a changed
|
|
128
134
|
or vanished entry is marked), and leave it off when the follow-up does not need them.
|
|
129
135
|
|
|
@@ -29,8 +29,9 @@ copied read-only into its working directory. The Codex consultant additionally h
|
|
|
29
29
|
}})
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits
|
|
33
|
-
the
|
|
32
|
+
2. **`consult_get({ group_id })`** (`consult_get({ job_id })` for one consultant). It waits for the answer by itself,
|
|
33
|
+
up to a limit the server sets (45 seconds, or ten minutes where the client allows it); call it again while
|
|
34
|
+
`status` is `running`. To work on something independent meanwhile, ask for a shorter wait (`reference.md`).
|
|
34
35
|
3. **Check one point, report, and record it once** (see **Before you hand the decision back**).
|
|
35
36
|
|
|
36
37
|
That is the whole routine. The rest of the record — more verdicts, a prediction and reflection, follow-ups,
|
|
@@ -93,7 +94,8 @@ Pass `initiator: "user"` when the user asked for the consultation; leave it out
|
|
|
93
94
|
and ask the one whose answer changes what you do next.
|
|
94
95
|
- **Hand over the code.** The consultant starts in an empty working directory and is not told where your
|
|
95
96
|
repository is. Name the files by absolute path in `context.expose_paths` in this first call (at most 20
|
|
96
|
-
entries, 500 files, 5 MB; never your home directory or one holding keys)
|
|
97
|
+
entries, 500 files, 5 MB; never your home directory or one holding keys). A repository root works: only the
|
|
98
|
+
files git lists are copied, nothing `.gitignore` excludes. Or pass
|
|
97
99
|
`context.expose_diff: { repo: "<absolute path>" }` for a change you are making. When `consult_start` answers
|
|
98
100
|
`files_exposed: 0` and the question is about code, `consult_cancel` and start again with `expose_paths`.
|
|
99
101
|
- **Put the load-bearing facts in `context.facts`**, and test output or a log in `context.artifacts`.
|
|
@@ -70,10 +70,14 @@ names. Your own CLI is a fresh session but not another lineage, and its `quality
|
|
|
70
70
|
consultants of one vendor.
|
|
71
71
|
|
|
72
72
|
Send **one** `targets` call: every consultant receives the byte-identical brief, which is what makes the answers
|
|
73
|
-
comparable. Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
-
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap (nine by default,
|
|
75
|
-
every severally session on this machine
|
|
76
|
-
|
|
73
|
+
comparable (on a follow-up to the group, the same new question, with each one's own earlier rounds). Poll the single group with `consult_get({ group_id })` and stop it with
|
|
74
|
+
`consult_cancel({ group_id })`. A fan-out takes one slot of the concurrency cap per consultant (nine by default,
|
|
75
|
+
shared by every severally session on this machine; the group starts only if every member gets one) and one
|
|
76
|
+
consultation's quota per consultant. To ask every member again, follow up the group: `followup_to: "<group_id>"`
|
|
77
|
+
with no `target` or `targets` (see Follow-ups).
|
|
78
|
+
|
|
79
|
+
While you wait, `consult_get` blocks for up to its limit (ten minutes where the client allows it). To work on
|
|
80
|
+
something independent in between, pass a shorter `wait_ms` and come back.
|
|
77
81
|
|
|
78
82
|
`comparison.by_target` holds, per consultant: stance, summary, confidence, evidence basis, finding points (each
|
|
79
83
|
with its `id` and severity), `severity_counts`, `decision_changers`, alternatives, unknowns (`u1` …),
|
|
@@ -93,7 +97,7 @@ Reading them against each other:
|
|
|
93
97
|
4. **Read `decision_changers` before spending a follow-up**: a consultant that named the condition under which
|
|
94
98
|
it would change its mind has told you what evidence to go get.
|
|
95
99
|
5. **Spend a follow-up only on a divergence that would change your decision**, sent only to the consultant
|
|
96
|
-
whose reading it belongs to.
|
|
100
|
+
whose reading it belongs to, or to the whole group (`followup_to: "<group_id>"`) when it concerns all of them.
|
|
97
101
|
6. **A stance every consultant shares is not evidence that the plan was checked.** Look for a premise they all
|
|
98
102
|
rely on and none could verify; name it only if you can actually identify one.
|
|
99
103
|
|
|
@@ -123,7 +127,9 @@ in `failure.detail`, which tells you whether to retry with a longer budget or a
|
|
|
123
127
|
|
|
124
128
|
By default a chain has five rounds: one initial round plus up to four follow-ups
|
|
125
129
|
(`followup_to: "<previous job_id>"`); the operator can configure 1–20 (`server.max_rounds`), and
|
|
126
|
-
`rounds_remaining` says what is left.
|
|
130
|
+
`rounds_remaining` says what is left. `followup_to: "<group_id>"`, with no `target` or `targets`, sends the same new
|
|
131
|
+
question to every member of a fan-out, each continuing from its own latest round with its own history; a member
|
|
132
|
+
that is still running, never answered or has used its rounds is listed under `skipped` and not asked. A follow-up starts in a fresh working directory with no files: pass
|
|
127
133
|
`context.expose_from_parent: true` to copy again, as they are now, the paths the earlier round exposed (a changed
|
|
128
134
|
or vanished entry is marked), and leave it off when the follow-up does not need them.
|
|
129
135
|
|
package/scripts/init-config.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export function renderConfig() {
|
|
|
53
53
|
' // ranking: optional, higher first -- which consultant a name that fits several goes to,',
|
|
54
54
|
' // before the built-in rules (opus over sonnet, newer version). e.g. "ranking": ["sol", "opus"]',
|
|
55
55
|
' //',
|
|
56
|
-
' // server: optional -- max_rounds (5, 1-20), max_concurrent (9, 1-9), timeout_ms (
|
|
56
|
+
' // server: optional -- max_rounds (5, 1-20), max_concurrent (9, 1-9), timeout_ms (1800000),',
|
|
57
57
|
' // max_wait_ms (45000), max_jobs_retained, kill_grace_ms: see CONFIG.md',
|
|
58
58
|
' "clis": {},',
|
|
59
59
|
' "consultants": {',
|
package/scripts/install.mjs
CHANGED
|
@@ -243,6 +243,14 @@ if (mode === 'plugin') {
|
|
|
243
243
|
log(` server command: ${JSON.stringify(serverCommand)}`);
|
|
244
244
|
|
|
245
245
|
const codexHome = process.env.CODEX_HOME ?? path.join(home, '.codex');
|
|
246
|
+
// consult_get waits 45 s by default, under the 60 s that some MCP clients
|
|
247
|
+
// allow a tool call. A client known to wait longer is registered with this,
|
|
248
|
+
// so one consult_get usually covers a whole consultation. The plugin's own
|
|
249
|
+
// .mcp.json carries the same value for Claude Code, and its Codex manifest
|
|
250
|
+
// with tool_timeout_sec 660 (codex 0.163 stops a tool call at 300 s by
|
|
251
|
+
// default, progress notwithstanding). `codex mcp add` cannot set that
|
|
252
|
+
// timeout, so a --manual Codex keeps 45 s.
|
|
253
|
+
const LONG_WAIT = 'SEVERALLY_MAX_WAIT_MS=600000';
|
|
246
254
|
const claudeSkillsDir = path.join(home, '.claude', 'skills');
|
|
247
255
|
|
|
248
256
|
// ---------------------------------------------------------------- clients
|
|
@@ -260,7 +268,9 @@ const CLIENTS = [
|
|
|
260
268
|
// `claude mcp get` exits non-zero when the server is not registered.
|
|
261
269
|
isRegistered: () => tryRun('claude', ['mcp', 'get', 'severally'], { real: true }).ok,
|
|
262
270
|
remove: () => tryRun('claude', ['mcp', 'remove', '--scope', 'user', 'severally']),
|
|
263
|
-
|
|
271
|
+
// Claude Code waits on a tool call far longer than consult_get's ten
|
|
272
|
+
// minutes, so its consult_get may wait that long (see LONG_WAIT).
|
|
273
|
+
add: (bin) => tryRun('claude', ['mcp', 'add', '--scope', 'user', 'severally', '--env', LONG_WAIT, '--', ...bin]),
|
|
264
274
|
skillFrom: path.join(PLUGIN, 'skills', 'claude', 'severally'),
|
|
265
275
|
skillTo: path.join(claudeSkillsDir, 'severally'),
|
|
266
276
|
},
|
|
@@ -349,6 +359,16 @@ function registrationCommandMatches(client, bin) {
|
|
|
349
359
|
return false;
|
|
350
360
|
}
|
|
351
361
|
|
|
362
|
+
// The list right after an add can be briefly empty, so it is asked up to
|
|
363
|
+
// three times, 1.5 s apart, before the add is judged.
|
|
364
|
+
function listedAfterAdd(client) {
|
|
365
|
+
for (let i = 0; i < 3; i++) {
|
|
366
|
+
if (i) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1500);
|
|
367
|
+
if (client.isRegistered()) return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
352
372
|
// Registers one client's MCP server directly (through its own `mcp add`) and
|
|
353
373
|
// copies its skill, backing up everything it touches first. Shared by plugin
|
|
354
374
|
// mode (Antigravity only) and --manual mode (all three clients).
|
|
@@ -367,13 +387,23 @@ function installClientDirectly(client, bin) { step(`Registering with ${client.l
|
|
|
367
387
|
if (!r.ok) {
|
|
368
388
|
log(` failed: ${r.out}`);
|
|
369
389
|
process.exitCode = 1;
|
|
370
|
-
} else if (!dryRun && client.verifyAdd && !client
|
|
390
|
+
} else if (!dryRun && client.verifyAdd && !listedAfterAdd(client)) {
|
|
371
391
|
// opencode prints usage and exits 0 on flags a future version renames,
|
|
372
392
|
// so a zero exit from `mcp add` proves nothing: confirm the entry is
|
|
373
393
|
// actually listed, or the server silently never reaches the client.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
394
|
+
// Its list has also come back empty with the server in its config
|
|
395
|
+
// (twice, on the first run after an upgrade), so a config
|
|
396
|
+
// holding exactly this install's command is a registration, noted.
|
|
397
|
+
const said = ` the list said: ${lastListOutput.slice(0, 500) || '(nothing)'}`;
|
|
398
|
+
if (registrationCommandMatches(client, bin)) {
|
|
399
|
+
log(` registered in ${client.label}'s global config, but \`${client.bin} mcp list\` does not show it yet`);
|
|
400
|
+
log(said);
|
|
401
|
+
log(` check with \`${client.bin} mcp list\` after restarting it`);
|
|
402
|
+
} else {
|
|
403
|
+
log(' add exited 0 but the server is not listed; registration failed');
|
|
404
|
+
log(said);
|
|
405
|
+
process.exitCode = 1;
|
|
406
|
+
}
|
|
377
407
|
} else if (!dryRun && client.verifyAdd && !registrationCommandMatches(client, bin)) {
|
|
378
408
|
log(' registration not verified: the listed server command does not match this install');
|
|
379
409
|
process.exitCode = 1;
|
|
@@ -55,7 +55,11 @@ const WEB_TOOLS = new Set(['search_web', 'read_url_content']);
|
|
|
55
55
|
// call through. So the hook always answers, allow included, and anything it
|
|
56
56
|
// cannot read or check -- broken JSON, a stdin error, arguments nested past
|
|
57
57
|
// what it walks, an exception of its own -- is denied. A hook that never runs
|
|
58
|
-
// or is killed answers nothing, and that stays agy's call
|
|
58
|
+
// or is killed answers nothing, and that stays agy's call: agy 1.4.2 fails such
|
|
59
|
+
// a call rather than running it ("JSON hook ... failed: command failed: ..."),
|
|
60
|
+
// checked live on 2026-10-20 with invoke_subagent against a hook that exited 1,
|
|
61
|
+
// exited 2, outlived the 10 s timeout, and whose script was missing -- each
|
|
62
|
+
// time the step ended in ERROR and no child agent started. For the file-reading tool
|
|
59
63
|
// and for a file: URL, every path-like parameter is also checked against the
|
|
60
64
|
// protected roots and file names, before and after resolving links, with ~
|
|
61
65
|
// expanded: a consultant that can browse must not read a credential and
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -167,11 +167,16 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
const errorMsg = errorEvents
|
|
171
|
+
.map((e) => e.message ?? e.error?.message ?? JSON.stringify(e))
|
|
172
|
+
.join(' | ');
|
|
173
|
+
// An error the turn recovered from (the stream still ends in
|
|
174
|
+
// turn.completed, with an answer) does not throw the answer away; the lead
|
|
175
|
+
// is told about it instead. A turn that ends in turn.failed has failed,
|
|
176
|
+
// whatever text it left behind.
|
|
177
|
+
const recovered = errorEvents.length > 0 && Boolean(text) && events.at(-1)?.type === 'turn.completed';
|
|
178
|
+
if (errorEvents.length && !recovered) {
|
|
179
|
+
return { ok: false, failureKind: classifyMessage(errorMsg), message: errorMsg, usageRaw: usage, events: events.length };
|
|
175
180
|
}
|
|
176
181
|
if (!text) {
|
|
177
182
|
const msg = (stderr || '').trim() || `codex exited with code ${code} and produced no final message`;
|
|
@@ -183,7 +188,13 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
|
|
|
183
188
|
events: events.length,
|
|
184
189
|
};
|
|
185
190
|
}
|
|
186
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
text,
|
|
194
|
+
usageRaw: usage,
|
|
195
|
+
events: events.length,
|
|
196
|
+
recoveredError: recovered ? { failureKind: classifyMessage(errorMsg), message: errorMsg } : null,
|
|
197
|
+
};
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
/**
|
package/src/brief.mjs
CHANGED
|
@@ -184,6 +184,8 @@ function renderExposeManifest(manifest) {
|
|
|
184
184
|
const skipped = [
|
|
185
185
|
count('symlink') ? `${count('symlink')} symlink(s), or path(s) through one, were skipped, not followed.` : null,
|
|
186
186
|
count('protected') ? `${count('protected')} credential or history path(s) were left out on purpose.` : null,
|
|
187
|
+
count('ignored') ? `${count('ignored')} path(s) (.git, node_modules, whatever .gitignore excludes) were left out as not source.` : null,
|
|
188
|
+
count('submodule') ? `${count('submodule')} submodule(s) or nested repositor(ies) were left out; their files are not copied.` : null,
|
|
187
189
|
count('gone') ? `No longer there, so not copied again: ${named('gone')}.` : null,
|
|
188
190
|
count('masked') ? `Not copied again, because the record of the earlier round masked the path: ${named('masked')}.` : null,
|
|
189
191
|
count('replaced') ? `The earlier round's change is replaced by the diff given in this round: ${named('replaced')}.` : null,
|
|
@@ -204,9 +206,30 @@ function renderExposeManifest(manifest) {
|
|
|
204
206
|
// from crowding out the question it is there to inform.
|
|
205
207
|
const clip = (s, max = 400) => (s.length > max ? `${s.slice(0, max)}...` : s);
|
|
206
208
|
|
|
209
|
+
// The earlier rounds have a budget of their own, beside the request's cap:
|
|
210
|
+
// when they outgrow it, the oldest rounds are left out, and the brief says so.
|
|
211
|
+
const HISTORY_CHARS = 40_000;
|
|
212
|
+
|
|
213
|
+
function renderRound(p) {
|
|
214
|
+
// Prefixed with the round, so a point from round 1 is not taken for this one's.
|
|
215
|
+
const tag = (id) => `r${p.round}.${id}`;
|
|
216
|
+
const verdict = (x) => (x.verdict ? ` -- lead's verdict: ${x.verdict}${x.effect ? ` (${clip(x.effect)})` : ''}` : '');
|
|
217
|
+
const list = (title, items, line) => (items?.length ? [title, ...items.map(line)].join('\n') : null);
|
|
218
|
+
return [
|
|
219
|
+
`### Round ${p.round} (${p.mode})`,
|
|
220
|
+
`Question asked: ${p.question}`,
|
|
221
|
+
`Your answer, summarised: ${p.summary}`,
|
|
222
|
+
list('Points you raised:', p.findings,
|
|
223
|
+
(f) => `- ${tag(f.id)}: ${clip(f.point)}${verdict(f)}${f.grounds ? `\n grounds: ${clip(f.grounds)}` : ''}`),
|
|
224
|
+
list('Unknowns you named:', p.unknowns, (u) => `- ${tag(u.id)}: ${clip(u.point)}${verdict(u)}`),
|
|
225
|
+
list('Checks you proposed:', p.next_checks, (c) => `- ${tag(c.id)}: ${clip(c.point)}${verdict(c)}`),
|
|
226
|
+
].filter(Boolean).join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
207
229
|
/**
|
|
208
230
|
* @param {object} req validated request
|
|
209
|
-
* @param {object} chain { round, priorRounds: [{question, mode, summary, findings: [{id, point, verdict, effect}]
|
|
231
|
+
* @param {object} chain { round, priorRounds: [{question, mode, summary, findings: [{id, point, grounds, verdict, effect}],
|
|
232
|
+
* unknowns: [{id, point, verdict, effect}], next_checks: [{id, point, verdict, effect}]}] }
|
|
210
233
|
*/
|
|
211
234
|
export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeManifest = null) {
|
|
212
235
|
const parts = [];
|
|
@@ -238,22 +261,21 @@ export function renderBrief(req, chain = { round: 1, priorRounds: [] }, exposeMa
|
|
|
238
261
|
if (chain.priorRounds && chain.priorRounds.length) {
|
|
239
262
|
parts.push('');
|
|
240
263
|
parts.push('## Earlier rounds of this same consultation');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
]
|
|
253
|
-
.filter(Boolean)
|
|
254
|
-
.join('\n'),
|
|
255
|
-
);
|
|
264
|
+
parts.push(
|
|
265
|
+
'Each point carries the lead\'s own check where there is one -- confirmed: it held; not_applicable: checked, and'
|
|
266
|
+
+ ' it does not apply here; unverifiable: could not be checked; unverified, or no verdict: not checked yet.',
|
|
267
|
+
);
|
|
268
|
+
const blocks = chain.priorRounds.map(renderRound);
|
|
269
|
+
let kept = 0;
|
|
270
|
+
let used = 0;
|
|
271
|
+
// Newest first: the round being followed up always goes in.
|
|
272
|
+
for (let i = blocks.length - 1; i >= 0 && (kept === 0 || used + blocks[i].length <= HISTORY_CHARS); i--) {
|
|
273
|
+
used += blocks[i].length;
|
|
274
|
+
kept += 1;
|
|
256
275
|
}
|
|
276
|
+
const left = chain.priorRounds.slice(0, blocks.length - kept).map((p) => p.round);
|
|
277
|
+
if (left.length) parts.push(`(Round${left.length > 1 ? 's' : ''} ${left.join(', ')} left out to keep this brief within its size.)`);
|
|
278
|
+
parts.push(...blocks.slice(blocks.length - kept));
|
|
257
279
|
parts.push('');
|
|
258
280
|
parts.push(
|
|
259
281
|
'This round is a follow-up. The lead has narrowed the exchange to the divergences above. Do not restate your earlier answer; address what is new, and say plainly if the new information changes or does not change your position.',
|