@ran-sh/dsh-crew 2.1.5 → 2.1.7
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/.claude-plugin/plugin.json +1 -1
- package/docs/job-contracts.md +10 -0
- package/docs/readiness-matrix.md +19 -0
- package/package.json +1 -1
- package/src/ci-evidence.mjs +196 -0
- package/src/config-readiness.mjs +8 -1
- package/src/hub/index.mjs +5 -0
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +4 -0
package/docs/job-contracts.md
CHANGED
|
@@ -22,6 +22,16 @@ changes/tests/risks, changed-file names, base revision, and candidate
|
|
|
22
22
|
fingerprint. It opens the relevant files and runs `git diff` in the isolated
|
|
23
23
|
workspace when deeper inspection is needed.
|
|
24
24
|
|
|
25
|
+
The capsule also carries a pointer to the reviewed attempt's persisted execution
|
|
26
|
+
record — the Hub session id plus the Crew harness session store — because a
|
|
27
|
+
transient change leaves nothing in the workspace to inspect once it has been
|
|
28
|
+
created, run and removed. That record is the only account of what actually ran:
|
|
29
|
+
its `tool/result` entries hold the exact bytes a `write` produced, and the
|
|
30
|
+
captured output and exit code of every command. What travels is the pointer,
|
|
31
|
+
never the record, and it is omitted entirely when either half is unknown rather
|
|
32
|
+
than naming a path that would not hold the attempt. A reviewer's own
|
|
33
|
+
reproduction is not a substitute for reading it.
|
|
34
|
+
|
|
25
35
|
The Hub keeps only the latest assistant message needed as the final Delivery
|
|
26
36
|
Report. It does not retain an ever-growing list of intermediate assistant
|
|
27
37
|
messages.
|
package/docs/readiness-matrix.md
CHANGED
|
@@ -83,3 +83,22 @@ An evidence record may contain:
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
The matrix does not fetch or trust arbitrary remote data by itself. Loading and authenticating an evidence source is the responsibility of the higher layer that calls the builder.
|
|
86
|
+
|
|
87
|
+
`src/ci-evidence.mjs` is that layer for the `ci` rows. It resolves the running
|
|
88
|
+
version's tag to the commit it was cut from, reads that commit's CI run, and
|
|
89
|
+
evidences each platform row from its own job. Three things have to hold before a
|
|
90
|
+
row may go green, because each is a way to look validated without being
|
|
91
|
+
validated: the job's runner labels must name the platform the row claims (a job
|
|
92
|
+
*name* is not a platform), the run must be one this repository pushed (a pull
|
|
93
|
+
request carries its own workflow file, so its runs are not evidence), and the
|
|
94
|
+
job itself must have concluded successfully. The commit is recorded in
|
|
95
|
+
`evidence_ref` so `version → tag → commit` stays auditable. Nothing is read from
|
|
96
|
+
or written to credentials: the public API is used anonymously.
|
|
97
|
+
|
|
98
|
+
One limit belongs where a reader of a green row will meet it. The mapping starts
|
|
99
|
+
from the version number, so the evidence describes the commit the matching tag
|
|
100
|
+
points at. A payload installed with `update --candidate <dir>` can carry local
|
|
101
|
+
edits that were never tagged while reporting the same version, and the row is
|
|
102
|
+
then green for the tagged commit rather than for the tree that is running.
|
|
103
|
+
`evidence_ref` names that commit, which is as much as this can honestly say
|
|
104
|
+
until the payload records its own revision.
|
package/package.json
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Platform-validation evidence for the readiness matrix, loaded from the
|
|
2
|
+
// project's own CI runs.
|
|
3
|
+
//
|
|
4
|
+
// `readiness-matrix.mjs` is deliberately inert — it never reads files, GitHub or
|
|
5
|
+
// the network — and `docs/readiness-matrix.md` says loading and authenticating an
|
|
6
|
+
// evidence source is the responsibility of the higher layer that calls the
|
|
7
|
+
// builder. This is that layer, and it is the only thing here that touches the
|
|
8
|
+
// network.
|
|
9
|
+
//
|
|
10
|
+
// It fails closed in every direction. A version with no tag, a tag whose commit
|
|
11
|
+
// has no run, a run this repository did not push, a missing platform job, a job
|
|
12
|
+
// that did not pass, a job whose runner is not the platform its row claims, a
|
|
13
|
+
// timeout, an HTTP error — all of them return no evidence at all, which leaves
|
|
14
|
+
// the CI rows NOT_RUN. Promoting a row on anything less than a green run at the
|
|
15
|
+
// exact commit being validated is the one failure this module exists to avoid.
|
|
16
|
+
//
|
|
17
|
+
// The mapping's limit, stated plainly: evidence is resolved *from the version
|
|
18
|
+
// number*, so it describes the commit the matching tag points at. `dsh-crew
|
|
19
|
+
// update --candidate <dir>` can install a tree that was never published while
|
|
20
|
+
// reporting the same version — its local edits are not tagged, so this module
|
|
21
|
+
// would present the tag's commit as the validated one. `evidence_ref` carries
|
|
22
|
+
// that commit so a reader can see which commit the green run covered, but it
|
|
23
|
+
// cannot see the running tree, and nothing here may claim otherwise. Widening
|
|
24
|
+
// this would need the running tree's revision, which the payload does not
|
|
25
|
+
// currently record.
|
|
26
|
+
//
|
|
27
|
+
// Authentication is optional and never required: the repository is public, so
|
|
28
|
+
// the anonymous API answers. A token is used only when the caller already has
|
|
29
|
+
// one; nothing here reads credentials from disk, and the value is never logged,
|
|
30
|
+
// returned, or included in an evidence record.
|
|
31
|
+
|
|
32
|
+
export const CI_EVIDENCE_REPO = 'Ran-sh/dsh-crew';
|
|
33
|
+
|
|
34
|
+
// The row each CI job validates. A platform with no job in the workflow cannot
|
|
35
|
+
// be evidenced by any run, so `macos_smoke` is deliberately absent rather than
|
|
36
|
+
// listed and always missing.
|
|
37
|
+
// `runner` is a gate, not a label. Matching on the job name alone would let a
|
|
38
|
+
// job called `deterministic` that ran on a macOS runner evidence the *linux*
|
|
39
|
+
// row, which is exactly the "looks validated, isn't" the matrix exists to
|
|
40
|
+
// refuse; the job's own labels have to name the platform the row claims.
|
|
41
|
+
const PLATFORM_JOBS = [
|
|
42
|
+
{ row: 'linux_deterministic', job: 'deterministic', runner: 'ubuntu' },
|
|
43
|
+
{ row: 'windows_regressions', job: 'windows-paths', runner: 'windows' },
|
|
44
|
+
{ row: 'macos_smoke', job: 'macos-smoke', runner: 'macos' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
48
|
+
// Negative results expire sooner. Caching "nothing proven" for the full window
|
|
49
|
+
// would let one transient network hiccup freeze a row at NOT_RUN for ten
|
|
50
|
+
// minutes, and the cost of asking again is bounded by the deadline.
|
|
51
|
+
const NEGATIVE_CACHE_TTL_MS = 60 * 1000;
|
|
52
|
+
const cache = new Map();
|
|
53
|
+
|
|
54
|
+
/** Test seam: drop memoized evidence between cases. */
|
|
55
|
+
export function clearCiEvidenceCache() {
|
|
56
|
+
cache.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function apiHeaders(token) {
|
|
60
|
+
const headers = {
|
|
61
|
+
accept: 'application/vnd.github+json',
|
|
62
|
+
'user-agent': 'dsh-crew-readiness',
|
|
63
|
+
};
|
|
64
|
+
if (typeof token === 'string' && token.trim() !== '') headers.authorization = `Bearer ${token.trim()}`;
|
|
65
|
+
return headers;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// One deadline covers the whole chain, not one per request: this runs on the
|
|
69
|
+
// readiness route, and three sequential 5s timeouts would be a 15s stall on a
|
|
70
|
+
// cold cache.
|
|
71
|
+
//
|
|
72
|
+
// The deadline is enforced twice on purpose. The abort signal is the polite
|
|
73
|
+
// half — it lets a well-behaved transport drop the socket — but the readiness
|
|
74
|
+
// route must not depend on the transport cooperating, so the request is also
|
|
75
|
+
// raced against the clock. A fetch that ignores its signal then returns nothing
|
|
76
|
+
// at the deadline instead of holding the route open.
|
|
77
|
+
async function getJson(fetchImpl, url, { token, deadline, now }) {
|
|
78
|
+
const remaining = deadline - now();
|
|
79
|
+
if (remaining <= 0) return null;
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
let timer;
|
|
82
|
+
try {
|
|
83
|
+
const attempt = fetchImpl(url, { headers: apiHeaders(token), signal: controller.signal })
|
|
84
|
+
.then((response) => (response?.status === 200 ? response.json() : null))
|
|
85
|
+
.catch(() => null);
|
|
86
|
+
const expiry = new Promise((resolve) => {
|
|
87
|
+
timer = setTimeout(() => { controller.abort(); resolve(null); }, remaining);
|
|
88
|
+
});
|
|
89
|
+
return await Promise.race([attempt, expiry]);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The commit a released version was cut from.
|
|
99
|
+
*
|
|
100
|
+
* `v<version>` may be a lightweight tag (the ref names the commit) or annotated
|
|
101
|
+
* (the ref names a tag object that names the commit), so both are tried. A
|
|
102
|
+
* version with no tag resolves to null and no evidence is produced: the running
|
|
103
|
+
* code is then not the code CI validated, and saying so is the honest answer.
|
|
104
|
+
*/
|
|
105
|
+
async function resolveVersionCommit(fetchImpl, { repo, version, token, deadline, now }) {
|
|
106
|
+
const tag = `v${version}`;
|
|
107
|
+
const ref = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/git/ref/tags/${encodeURIComponent(tag)}`, { token, deadline, now });
|
|
108
|
+
const object = ref?.object;
|
|
109
|
+
if (!object) return null;
|
|
110
|
+
if (object.type === 'commit' && typeof object.sha === 'string') return object.sha;
|
|
111
|
+
if (object.type === 'tag' && typeof object.sha === 'string') {
|
|
112
|
+
const annotated = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/git/tags/${object.sha}`, { token, deadline, now });
|
|
113
|
+
const sha = annotated?.object?.sha;
|
|
114
|
+
return typeof sha === 'string' ? sha : null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function evidenceFor(jobs, { runId, sha }) {
|
|
120
|
+
const evidence = {};
|
|
121
|
+
for (const { row, job, runner } of PLATFORM_JOBS) {
|
|
122
|
+
const match = jobs.find((entry) => entry?.name === job);
|
|
123
|
+
if (!match) continue;
|
|
124
|
+
if (String(match.conclusion ?? '').toLowerCase() !== 'success') continue;
|
|
125
|
+
// The job's labels must name the platform the row claims. A name is not a
|
|
126
|
+
// platform, and a runner label that does not match is treated as no
|
|
127
|
+
// evidence at all rather than as evidence for the wrong row.
|
|
128
|
+
const labels = Array.isArray(match.labels) ? match.labels : [];
|
|
129
|
+
if (!labels.some((label) => String(label).toLowerCase().includes(runner))) continue;
|
|
130
|
+
evidence[row] = {
|
|
131
|
+
status: 'PASS',
|
|
132
|
+
reason_code: 'CI_GREEN',
|
|
133
|
+
evidence_source: 'github-actions',
|
|
134
|
+
// The commit is part of the reference on purpose: version -> tag -> commit
|
|
135
|
+
// is a mapping, and the reader has to be able to audit which commit the
|
|
136
|
+
// green run actually covered.
|
|
137
|
+
evidence_ref: `run-${runId}/${job}/${runner}@${String(sha).slice(0, 12)}${labels.length ? ` (${labels.join(',')})` : ''}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return evidence;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load CI evidence for a released version. Returns `{}` when nothing can be
|
|
145
|
+
* proven, so the caller can merge the result unconditionally.
|
|
146
|
+
*/
|
|
147
|
+
export async function loadCiEvidence({
|
|
148
|
+
repo = CI_EVIDENCE_REPO,
|
|
149
|
+
version,
|
|
150
|
+
token,
|
|
151
|
+
fetchImpl = globalThis.fetch,
|
|
152
|
+
timeoutMs = 5000,
|
|
153
|
+
now = Date.now,
|
|
154
|
+
useCache = true,
|
|
155
|
+
} = {}) {
|
|
156
|
+
if (typeof version !== 'string' || version.trim() === '') return {};
|
|
157
|
+
if (typeof fetchImpl !== 'function') return {};
|
|
158
|
+
|
|
159
|
+
// The credential is part of the key: an anonymous result produced under a
|
|
160
|
+
// rate limit must not be served to a call that offered a token.
|
|
161
|
+
const key = `${repo}@${version.trim()}@${typeof token === 'string' && token.trim() !== '' ? 'auth' : 'anon'}`;
|
|
162
|
+
const hit = cache.get(key);
|
|
163
|
+
if (useCache && hit) {
|
|
164
|
+
const ttl = Object.keys(hit.evidence).length > 0 ? CACHE_TTL_MS : NEGATIVE_CACHE_TTL_MS;
|
|
165
|
+
if (now() - hit.at < ttl) return hit.evidence;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const deadline = now() + timeoutMs;
|
|
169
|
+
const evidence = await loadUncached({ repo, version: version.trim(), token, fetchImpl, deadline, now });
|
|
170
|
+
cache.set(key, { at: now(), evidence });
|
|
171
|
+
return evidence;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function loadUncached({ repo, version, token, fetchImpl, deadline, now }) {
|
|
175
|
+
const sha = await resolveVersionCommit(fetchImpl, { repo, version, token, deadline, now });
|
|
176
|
+
if (!sha) return {};
|
|
177
|
+
|
|
178
|
+
const runs = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/actions/runs?head_sha=${sha}&per_page=20`, { token, deadline, now });
|
|
179
|
+
const list = Array.isArray(runs?.workflow_runs) ? runs.workflow_runs : [];
|
|
180
|
+
// The newest CI run for this commit, and each row judged by its own job.
|
|
181
|
+
//
|
|
182
|
+
// The run's overall conclusion is deliberately not a gate: one platform's red
|
|
183
|
+
// job must not withdraw another platform's evidence, and each row asks only
|
|
184
|
+
// whether *its* validation ran and passed. The run's *identity* is a gate
|
|
185
|
+
// though. A workflow is matched by name, and a name is not an identity — a
|
|
186
|
+
// pull request can carry a workflow file of its own, so a run this repository
|
|
187
|
+
// did not push, or one belonging to a fork, is not accepted as evidence.
|
|
188
|
+
const ciRun = list.find((run) => run?.name === 'CI'
|
|
189
|
+
&& String(run?.event ?? '').toLowerCase() === 'push'
|
|
190
|
+
&& run?.head_repository?.full_name === repo);
|
|
191
|
+
if (!ciRun) return {};
|
|
192
|
+
|
|
193
|
+
const jobsBody = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/actions/runs/${ciRun.id}/jobs?per_page=50`, { token, deadline, now });
|
|
194
|
+
const jobs = Array.isArray(jobsBody?.jobs) ? jobsBody.jobs : [];
|
|
195
|
+
return evidenceFor(jobs, { runId: ciRun.id, sha });
|
|
196
|
+
}
|
package/src/config-readiness.mjs
CHANGED
|
@@ -191,6 +191,7 @@ export function buildConfigReadinessMatrix({
|
|
|
191
191
|
hubJobsChecked = false,
|
|
192
192
|
hubJobsBody = null,
|
|
193
193
|
currentSelections = null,
|
|
194
|
+
ciEvidence = null,
|
|
194
195
|
} = {}) {
|
|
195
196
|
const warnings = warningCodes(providerCatalogBody);
|
|
196
197
|
const catalogResponseOk = !!providerCatalogBody
|
|
@@ -198,7 +199,13 @@ export function buildConfigReadinessMatrix({
|
|
|
198
199
|
&& providerCatalogBody.ok !== false;
|
|
199
200
|
const catalogOk = providerCatalogChecked && catalogResponseOk && warnings.length === 0;
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
// Platform validation a higher layer actually loaded. `buildReadinessMatrix`
|
|
203
|
+
// drops any record whose status is outside the vocabulary, so this is merged
|
|
204
|
+
// as-is: an empty or malformed map leaves those rows NOT_RUN rather than
|
|
205
|
+
// widening what a row can mean.
|
|
206
|
+
const evidence = ciEvidence && typeof ciEvidence === 'object' && !Array.isArray(ciEvidence)
|
|
207
|
+
? { ...ciEvidence }
|
|
208
|
+
: {};
|
|
202
209
|
const hubJobs = hubCompatibility?.compatible === true
|
|
203
210
|
&& hubJobsChecked
|
|
204
211
|
&& hubJobsBody?.ok !== false
|
package/src/hub/index.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { normalizeReviewVerdict } from '../workflow-runtime.mjs';
|
|
|
26
26
|
import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
|
|
27
27
|
import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
|
|
28
28
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
29
|
+
import { loadCiEvidence } from '../ci-evidence.mjs';
|
|
29
30
|
import { loadRoleProfiles, resolveRoleProfile, saveRoleProfiles } from '../role-profiles.mjs';
|
|
30
31
|
import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspaceContexts, resolveWorkspaceContext, saveWorkspaceContexts } from '../workspace-context.mjs';
|
|
31
32
|
import { buildExtensionContract } from '../extension-contract.mjs';
|
|
@@ -1619,6 +1620,10 @@ export async function apply(ctx) {
|
|
|
1619
1620
|
currentSelections,
|
|
1620
1621
|
hubJobsChecked: true,
|
|
1621
1622
|
hubJobsBody: { ok: true, jobs: boundedJobs },
|
|
1623
|
+
// Platform validation this release actually passed. Resolved from the
|
|
1624
|
+
// running version's tag commit, so a row can only go green for a
|
|
1625
|
+
// commit CI really validated; anything unresolved stays NOT_RUN.
|
|
1626
|
+
ciEvidence: await loadCiEvidence({ version: runtime.runtime_version }),
|
|
1622
1627
|
});
|
|
1623
1628
|
const readinessSnapshot = buildRuntimeReadinessSnapshot({
|
|
1624
1629
|
runtime,
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.1.
|
|
39
|
+
export const RUNTIME_VERSION = '2.1.7';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
package/src/server.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { RUNTIME_VERSION, getHubRuntimeIdentity } from './runtime-identity.mjs';
|
|
|
10
10
|
import { resolveWorkerModel } from './model-routing.mjs';
|
|
11
11
|
import { runtimeActivationMetadata } from './runtime-controls.mjs';
|
|
12
12
|
import { buildConfigReadinessMatrix } from './config-readiness.mjs';
|
|
13
|
+
import { loadCiEvidence } from './ci-evidence.mjs';
|
|
13
14
|
import { buildRuntimeReadinessSnapshot, reprojectRuntimeModelCallability } from './runtime-readiness-snapshot.mjs';
|
|
14
15
|
import { classifyFailure, classifyFailureCode } from './failure-classification.mjs';
|
|
15
16
|
import {
|
|
@@ -402,6 +403,9 @@ async function buildConfigReport() {
|
|
|
402
403
|
workerProviderMode,
|
|
403
404
|
providerCatalogChecked,
|
|
404
405
|
providerCatalogBody,
|
|
406
|
+
// The same platform evidence the hub route merges, so the MCP surface does
|
|
407
|
+
// not report the CI rows differently when it has to build the matrix itself.
|
|
408
|
+
ciEvidence: await loadCiEvidence({ version: RUNTIME_VERSION }),
|
|
405
409
|
});
|
|
406
410
|
const readinessMatrix = hubReadinessSnapshot?.readiness_matrix ?? fallbackReadinessMatrix;
|
|
407
411
|
const roleProfiles = loadRoleProfiles();
|