@ran-sh/dsh-crew 2.1.5 → 2.1.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -0,0 +1,162 @@
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 that did not succeed, a missing platform job, a timeout, an
12
+ // HTTP error — all of them return no evidence at all, which leaves the CI rows
13
+ // NOT_RUN. Promoting a row on anything less than a green run at the exact commit
14
+ // being validated is the one failure this module exists to avoid.
15
+ //
16
+ // Authentication is optional and never required: the repository is public, so
17
+ // the anonymous API answers. A token is used only when the caller already has
18
+ // one; nothing here reads credentials from disk, and the value is never logged,
19
+ // returned, or included in an evidence record.
20
+
21
+ export const CI_EVIDENCE_REPO = 'Ran-sh/dsh-crew';
22
+
23
+ // The row each CI job validates. A platform with no job in the workflow cannot
24
+ // be evidenced by any run, so `macos_smoke` is deliberately absent rather than
25
+ // listed and always missing.
26
+ const PLATFORM_JOBS = [
27
+ { row: 'linux_deterministic', job: 'deterministic', runner: 'ubuntu' },
28
+ { row: 'windows_regressions', job: 'windows-paths', runner: 'windows' },
29
+ { row: 'macos_smoke', job: 'macos-smoke', runner: 'macos' },
30
+ ];
31
+
32
+ const CACHE_TTL_MS = 10 * 60 * 1000;
33
+ const cache = new Map();
34
+
35
+ /** Test seam: drop memoized evidence between cases. */
36
+ export function clearCiEvidenceCache() {
37
+ cache.clear();
38
+ }
39
+
40
+ function apiHeaders(token) {
41
+ const headers = {
42
+ accept: 'application/vnd.github+json',
43
+ 'user-agent': 'dsh-crew-readiness',
44
+ };
45
+ if (typeof token === 'string' && token.trim() !== '') headers.authorization = `Bearer ${token.trim()}`;
46
+ return headers;
47
+ }
48
+
49
+ // One deadline covers the whole chain, not one per request: this runs on the
50
+ // readiness route, and three sequential 5s timeouts would be a 15s stall on a
51
+ // cold cache.
52
+ //
53
+ // The deadline is enforced twice on purpose. The abort signal is the polite
54
+ // half — it lets a well-behaved transport drop the socket — but the readiness
55
+ // route must not depend on the transport cooperating, so the request is also
56
+ // raced against the clock. A fetch that ignores its signal then returns nothing
57
+ // at the deadline instead of holding the route open.
58
+ async function getJson(fetchImpl, url, { token, deadline }) {
59
+ const remaining = deadline - Date.now();
60
+ if (remaining <= 0) return null;
61
+ const controller = new AbortController();
62
+ let timer;
63
+ try {
64
+ const attempt = fetchImpl(url, { headers: apiHeaders(token), signal: controller.signal })
65
+ .then((response) => (response?.status === 200 ? response.json() : null))
66
+ .catch(() => null);
67
+ const expiry = new Promise((resolve) => {
68
+ timer = setTimeout(() => { controller.abort(); resolve(null); }, remaining);
69
+ });
70
+ return await Promise.race([attempt, expiry]);
71
+ } catch {
72
+ return null;
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * The commit a released version was cut from.
80
+ *
81
+ * `v<version>` may be a lightweight tag (the ref names the commit) or annotated
82
+ * (the ref names a tag object that names the commit), so both are tried. A
83
+ * version with no tag resolves to null and no evidence is produced: the running
84
+ * code is then not the code CI validated, and saying so is the honest answer.
85
+ */
86
+ async function resolveVersionCommit(fetchImpl, { repo, version, token, deadline }) {
87
+ const tag = `v${version}`;
88
+ const ref = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/git/ref/tags/${encodeURIComponent(tag)}`, { token, deadline });
89
+ const object = ref?.object;
90
+ if (!object) return null;
91
+ if (object.type === 'commit' && typeof object.sha === 'string') return object.sha;
92
+ if (object.type === 'tag' && typeof object.sha === 'string') {
93
+ const annotated = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/git/tags/${object.sha}`, { token, deadline });
94
+ const sha = annotated?.object?.sha;
95
+ return typeof sha === 'string' ? sha : null;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ function evidenceFor(jobs, { runId, sha }) {
101
+ const evidence = {};
102
+ for (const { row, job, runner } of PLATFORM_JOBS) {
103
+ const match = jobs.find((entry) => entry?.name === job);
104
+ if (!match) continue;
105
+ if (String(match.conclusion ?? '').toLowerCase() !== 'success') continue;
106
+ const labels = Array.isArray(match.labels) ? match.labels.join(',') : '';
107
+ evidence[row] = {
108
+ status: 'PASS',
109
+ reason_code: 'CI_GREEN',
110
+ evidence_source: 'github-actions',
111
+ // The commit is part of the reference on purpose: version -> tag -> commit
112
+ // is a mapping, and the reader has to be able to audit which commit the
113
+ // green run actually covered.
114
+ evidence_ref: `run-${runId}/${job}/${runner}@${String(sha).slice(0, 12)}${labels ? ` (${labels})` : ''}`,
115
+ };
116
+ }
117
+ return evidence;
118
+ }
119
+
120
+ /**
121
+ * Load CI evidence for a released version. Returns `{}` when nothing can be
122
+ * proven, so the caller can merge the result unconditionally.
123
+ */
124
+ export async function loadCiEvidence({
125
+ repo = CI_EVIDENCE_REPO,
126
+ version,
127
+ token,
128
+ fetchImpl = globalThis.fetch,
129
+ timeoutMs = 5000,
130
+ now = Date.now,
131
+ useCache = true,
132
+ } = {}) {
133
+ if (typeof version !== 'string' || version.trim() === '') return {};
134
+ if (typeof fetchImpl !== 'function') return {};
135
+
136
+ const key = `${repo}@${version.trim()}`;
137
+ const hit = cache.get(key);
138
+ if (useCache && hit && now() - hit.at < CACHE_TTL_MS) return hit.evidence;
139
+
140
+ const deadline = now() + timeoutMs;
141
+ const evidence = await loadUncached({ repo, version: version.trim(), token, fetchImpl, deadline });
142
+ cache.set(key, { at: now(), evidence });
143
+ return evidence;
144
+ }
145
+
146
+ async function loadUncached({ repo, version, token, fetchImpl, deadline }) {
147
+ const sha = await resolveVersionCommit(fetchImpl, { repo, version, token, deadline });
148
+ if (!sha) return {};
149
+
150
+ const runs = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/actions/runs?head_sha=${sha}&per_page=20`, { token, deadline });
151
+ const list = Array.isArray(runs?.workflow_runs) ? runs.workflow_runs : [];
152
+ // The newest CI run for this commit, and each row judged by its own job. The
153
+ // run's overall conclusion is deliberately not a gate: one platform's red job
154
+ // must not withdraw another platform's evidence, and each row asks only
155
+ // whether *its* validation ran and passed.
156
+ const ciRun = list.find((run) => run?.name === 'CI');
157
+ if (!ciRun) return {};
158
+
159
+ const jobsBody = await getJson(fetchImpl, `https://api.github.com/repos/${repo}/actions/runs/${ciRun.id}/jobs?per_page=50`, { token, deadline });
160
+ const jobs = Array.isArray(jobsBody?.jobs) ? jobsBody.jobs : [];
161
+ return evidenceFor(jobs, { runId: ciRun.id, sha });
162
+ }
@@ -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
- const evidence = {};
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,
@@ -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.5';
39
+ export const RUNTIME_VERSION = '2.1.6';
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();