@ran-sh/dsh-crew 2.2.7 → 2.2.9
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/readiness-matrix.md +19 -7
- package/package.json +1 -1
- package/scripts/setup.mjs +1 -1
- package/src/extension-contract.mjs +11 -5
- package/src/hub/index.mjs +4 -0
- package/src/install/install-legacy.mjs +39 -9
- package/src/install/npx-lifecycle.mjs +1 -1
- package/src/log-prune.mjs +25 -8
- package/src/readiness-matrix.mjs +37 -5
- package/src/runtime-identity.mjs +1 -1
- package/src/runtime-readiness-snapshot.mjs +9 -1
- package/windows/start-dsh-crew.cmd +16 -0
package/docs/readiness-matrix.md
CHANGED
|
@@ -13,15 +13,27 @@ The matrix is emitted by `hubStatus()` and is therefore visible inside the exist
|
|
|
13
13
|
- `BLOCKED` — the check could not run because required infrastructure or authorization was unavailable.
|
|
14
14
|
- `SKIP` — the row is intentionally not applicable for the active policy/path.
|
|
15
15
|
- `NOT_APPLICABLE` — the row asks about a route this machine does not use, and the
|
|
16
|
-
matrix says so instead of omitting it. The two built-in DeepSeek rows are the
|
|
17
|
-
|
|
18
|
-
other provider is selected, and keep
|
|
16
|
+
matrix says so instead of omitting it. The two built-in DeepSeek rows are the only
|
|
17
|
+
rows a policy may mark this way: they read `NOT_APPLICABLE` /
|
|
18
|
+
`WORKER_PROVIDER_FOLLOWS_DSH` whenever a known, other provider is selected, and keep
|
|
19
|
+
`NOT_RUN` when the selection is unknown.
|
|
19
20
|
- `NOT_RUN` — no trusted evidence has been supplied for the row.
|
|
20
21
|
|
|
21
|
-
`BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
`BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success.
|
|
23
|
+
|
|
24
|
+
Applicability is decided when the row is built and it is final for that row:
|
|
25
|
+
|
|
26
|
+
- Supplied evidence can neither create nor clear it. `NOT_APPLICABLE` is not part of
|
|
27
|
+
the evidence vocabulary (`PASS`, `FAIL`, `BLOCKED`, `SKIP`, `NOT_RUN`), so no
|
|
28
|
+
evidence source can make a required row disappear from readiness, and evidence
|
|
29
|
+
reported against a not-applicable row is kept as `reported_evidence` metadata
|
|
30
|
+
instead of flipping the status back.
|
|
31
|
+
- A required row that arrives as `NOT_APPLICABLE` anyway is a contradiction, not a
|
|
32
|
+
pass: consumers that project such a row into readiness report
|
|
33
|
+
`UNAVAILABLE` / `CHECK_NOT_APPLICABLE_ON_REQUIRED_ROW`, which cannot leave the
|
|
34
|
+
aggregate READY.
|
|
35
|
+
- The matrix declares `schema_version: 2`: a row may legally be `NOT_APPLICABLE` and
|
|
36
|
+
the summary carries a matching key, which is what a strict consumer needs to know.
|
|
25
37
|
|
|
26
38
|
## Evidence classes
|
|
27
39
|
|
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -389,7 +389,7 @@ export async function setupStatus({ log = console.log, root = ROOT, home = homed
|
|
|
389
389
|
// of the installation, which it is not.
|
|
390
390
|
const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
|
|
391
391
|
const frontend = frontendConfig?.frontend_autostart === true
|
|
392
|
-
? 'auto-
|
|
392
|
+
? 'configured to auto-start at login (dsh-crew open for a session URL)'
|
|
393
393
|
: 'available on demand (dsh-crew open)';
|
|
394
394
|
log(`Frontend (3080): ${frontend}`);
|
|
395
395
|
log(`Codex Desktop integration: ${codex}`);
|
|
@@ -29,13 +29,19 @@ function workspaceComponent(workspace) {
|
|
|
29
29
|
: { ...component('UNAVAILABLE', workspace?.code ?? 'WORKSPACE_NOT_CHECKED'), state: 'UNAVAILABLE' };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function readinessFromRow(entry, { pass = 'READY', notRun = 'DEGRADED' } = {}) {
|
|
32
|
+
function readinessFromRow(entry, { pass = 'READY', notRun = 'DEGRADED', allowNotApplicable = false } = {}) {
|
|
33
33
|
if (!entry) return component('UNAVAILABLE', 'NO_EVIDENCE');
|
|
34
34
|
if (entry.status === 'PASS') return component(pass, entry.reason_code ?? 'CHECK_PASSED');
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
35
|
+
// Every row this function is called with asks a question that applies to any Crew
|
|
36
|
+
// machine (a reachable hub, a consistent provider lifecycle, a runnable route), so
|
|
37
|
+
// "not applicable" is a contradiction here, not a pass: it is reported as missing
|
|
38
|
+
// evidence and it cannot leave the aggregate READY. Only a caller that names a row
|
|
39
|
+
// which may legitimately not apply opts in.
|
|
40
|
+
if (entry.status === 'NOT_APPLICABLE') {
|
|
41
|
+
return allowNotApplicable
|
|
42
|
+
? component('NOT_APPLICABLE', entry.reason_code ?? 'CHECK_NOT_APPLICABLE')
|
|
43
|
+
: component('UNAVAILABLE', 'CHECK_NOT_APPLICABLE_ON_REQUIRED_ROW');
|
|
44
|
+
}
|
|
39
45
|
if (entry.status === 'NOT_RUN' || entry.status === 'SKIP') return component(notRun, entry.reason_code ?? 'CHECK_NOT_RUN');
|
|
40
46
|
return component('UNAVAILABLE', entry.reason_code ?? 'CHECK_FAILED');
|
|
41
47
|
}
|
package/src/hub/index.mjs
CHANGED
|
@@ -642,6 +642,10 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
642
642
|
delivery_complete: !!job.delivery_complete,
|
|
643
643
|
allow_no_changes: job.allow_no_changes === true,
|
|
644
644
|
task_status: job.outcome?.task_status ?? null,
|
|
645
|
+
// Whether the worker's execution itself completed, kept beside the task-level
|
|
646
|
+
// verdict: a `partial` task still had a provider response, and that is what
|
|
647
|
+
// model callability asks about.
|
|
648
|
+
execution_status: job.outcome?.execution_status ?? null,
|
|
645
649
|
workspace_evidence_ok: job.outcome?.workspace_evidence_ok ?? null,
|
|
646
650
|
review_verdict: job.review?.verdict ?? null,
|
|
647
651
|
workspace_diff_available: !!job.workspaceDiff && ['git', 'filesystem-empty'].includes(job.workspaceDiff.kind),
|
|
@@ -1247,12 +1247,35 @@ export function installHudSegment({ home = homedir() } = {}) {
|
|
|
1247
1247
|
return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), 'statusLine: claude-hud now runs worker-segment.sh via --extra-cmd'] };
|
|
1248
1248
|
}
|
|
1249
1249
|
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1250
|
+
// The paths Crew has recorded as its own in this Claude host: the marketplace records
|
|
1251
|
+
// it wrote (under the current and pre-rename names, in settings and in the CLI's own
|
|
1252
|
+
// cache), the constant an older installer used, and the checkout this module runs
|
|
1253
|
+
// from. Only these are grounds for removing anything: a directory that merely happens
|
|
1254
|
+
// to be named `dsh-crew` belongs to whoever made it, and a path that is not recorded
|
|
1255
|
+
// here cannot be identified as Crew's, so it is left alone.
|
|
1256
|
+
function crewOwnedRoots({ home, settings, markets, installed }) {
|
|
1257
|
+
const roots = [];
|
|
1258
|
+
const push = (value) => { if (typeof value === 'string' && value.trim() && !roots.includes(value.trim())) roots.push(value.trim()); };
|
|
1259
|
+
for (const name of CREW_CLAUDE_MARKETPLACES) {
|
|
1260
|
+
push(settings?.extraKnownMarketplaces?.[name]?.source?.path);
|
|
1261
|
+
push(markets?.[name]?.source?.path);
|
|
1262
|
+
push(markets?.[name]?.installLocation);
|
|
1263
|
+
}
|
|
1264
|
+
for (const key of CREW_CLAUDE_PLUGIN_KEYS) {
|
|
1265
|
+
const entries = Array.isArray(installed?.plugins?.[key]) ? installed.plugins[key] : [installed?.plugins?.[key]];
|
|
1266
|
+
for (const entry of entries) push(entry?.installPath);
|
|
1267
|
+
}
|
|
1268
|
+
push(join(home, '.config', 'dsh-crew', 'marketplace'));
|
|
1269
|
+
push(ROOT);
|
|
1270
|
+
return roots;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function crewOwnedStatusLine(value, roots) {
|
|
1274
|
+
const command = typeof value?.command === 'string' ? value.command.trim() : '';
|
|
1275
|
+
const match = /^bash\s+(.+)$/i.exec(command);
|
|
1276
|
+
if (!match) return false;
|
|
1277
|
+
const path = normalizedPath(match[1].trim());
|
|
1278
|
+
return path !== null && roots.some((root) => normalizedPath(join(root, 'statusline', 'statusline.sh')) === path);
|
|
1256
1279
|
}
|
|
1257
1280
|
|
|
1258
1281
|
// The claude CLI records, per marketplace and plugin, what it materialized, in
|
|
@@ -1290,12 +1313,19 @@ function removeCrewClaudeCacheRecords({ home, actions }) {
|
|
|
1290
1313
|
export function uninstallClaudeCode({ home = homedir() } = {}) {
|
|
1291
1314
|
const actions = [];
|
|
1292
1315
|
const settingsFile = join(home, '.claude', 'settings.json');
|
|
1316
|
+
const cacheDir = join(home, '.claude', 'plugins');
|
|
1293
1317
|
const settings = readJson(settingsFile, null);
|
|
1318
|
+
const markets = readJson(join(cacheDir, 'known_marketplaces.json'), null);
|
|
1319
|
+
const installed = readJson(join(cacheDir, 'installed_plugins.json'), null);
|
|
1320
|
+
const roots = crewOwnedRoots({ home, settings, markets, installed });
|
|
1294
1321
|
if (settings) {
|
|
1295
1322
|
backup(settingsFile);
|
|
1296
|
-
const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
|
|
1297
1323
|
if (Array.isArray(settings.extraKnownMarketplaces)) {
|
|
1298
|
-
|
|
1324
|
+
// Legacy array shape: an entry is removed only when it IS one of the paths Crew
|
|
1325
|
+
// recorded as its own. What the path is spelled with is not evidence — a
|
|
1326
|
+
// marketplace under someone else's `dsh-crew` directory stays.
|
|
1327
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces
|
|
1328
|
+
.filter((entry) => !roots.some((root) => normalizedPath(entry?.path) === normalizedPath(root)));
|
|
1299
1329
|
} else if (settings.extraKnownMarketplaces) {
|
|
1300
1330
|
for (const name of CREW_CLAUDE_MARKETPLACES) delete settings.extraKnownMarketplaces[name];
|
|
1301
1331
|
}
|
|
@@ -1308,7 +1338,7 @@ export function uninstallClaudeCode({ home = homedir() } = {}) {
|
|
|
1308
1338
|
settings.permissions.allow = settings.permissions.allow.filter((rule) => typeof rule !== 'string'
|
|
1309
1339
|
|| !CREW_CLAUDE_PERMISSION_PREFIXES.some((prefix) => rule.startsWith(prefix)));
|
|
1310
1340
|
}
|
|
1311
|
-
if (crewOwnedStatusLine(settings.statusLine)) {
|
|
1341
|
+
if (crewOwnedStatusLine(settings.statusLine, roots)) {
|
|
1312
1342
|
delete settings.statusLine;
|
|
1313
1343
|
actions.push('statusLine: removed (Crew-installed)');
|
|
1314
1344
|
}
|
|
@@ -3333,7 +3333,7 @@ export function npxStatus({
|
|
|
3333
3333
|
// whether 3080 happens to be up right now does not change either answer.
|
|
3334
3334
|
const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
|
|
3335
3335
|
const frontend = frontendConfig?.frontend_autostart === true
|
|
3336
|
-
? 'auto-
|
|
3336
|
+
? 'configured to auto-start at login (dsh-crew open for a session URL)'
|
|
3337
3337
|
: 'available on demand (dsh-crew open)';
|
|
3338
3338
|
log(`Frontend (3080): ${frontend}`);
|
|
3339
3339
|
// The desktop app carries its own bridge entry, which pins a revision: after a
|
package/src/log-prune.mjs
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
// new dsh-crew-dsh-crew-3210-<stamp>.out/.err.log pair, every desktop launch a
|
|
3
3
|
// dsh-crew-web-*, every official launch a dsh-official-web-*, and the launcher
|
|
4
4
|
// appends to dsh-crew-launcher.log forever. They are the only post-mortem evidence
|
|
5
|
-
// for a crash-before-readiness, so the rule
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// for a crash-before-readiness, so the rule bounds them instead of deleting them all.
|
|
6
|
+
// A run survives when it is BOTH within its family's newest `keep` AND younger than
|
|
7
|
+
// the age bound; exceeding either one removes it, unless it was written within the
|
|
8
|
+
// recency guard (a running process owns its own log files). The newest `keep` runs
|
|
9
|
+
// therefore do not survive unconditionally — only a run that is also inside the age
|
|
10
|
+
// bound does, which is what keeps a burst of starts from filling the directory.
|
|
8
11
|
|
|
9
12
|
import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
10
13
|
import { join } from 'node:path';
|
|
@@ -14,10 +17,15 @@ export const DEFAULT_KEEP_RUNS = 10;
|
|
|
14
17
|
export const DEFAULT_MAX_AGE_DAYS = 14;
|
|
15
18
|
const RECENT_GUARD_MS = 5 * 60 * 1000;
|
|
16
19
|
|
|
20
|
+
// Per family. The hub-start pairs are what a crash-before-readiness leaves behind,
|
|
21
|
+
// and ten of them are the post-mortem window the policy names; the frontend families
|
|
22
|
+
// are launched far less often and keep the retention they already had, so one
|
|
23
|
+
// family's bound is not silently imposed on the others. An explicit `keepRuns`
|
|
24
|
+
// still applies to every family: that is an operator stating how many runs to keep.
|
|
17
25
|
const LOG_FAMILIES = [
|
|
18
|
-
{ prefix: 'dsh-crew-dsh-crew-3210-', suffix: '.out.log' },
|
|
19
|
-
{ prefix: 'dsh-crew-web-', suffix: '.out.log' },
|
|
20
|
-
{ prefix: 'dsh-official-web-', suffix: '.out.log' },
|
|
26
|
+
{ prefix: 'dsh-crew-dsh-crew-3210-', suffix: '.out.log', keep: 10 },
|
|
27
|
+
{ prefix: 'dsh-crew-web-', suffix: '.out.log', keep: 20 },
|
|
28
|
+
{ prefix: 'dsh-official-web-', suffix: '.out.log', keep: 20 },
|
|
21
29
|
];
|
|
22
30
|
|
|
23
31
|
// `dsh-crew-<profile>-<port>-<stamp>.out.log` -> the stamp identifies one run, and
|
|
@@ -28,8 +36,11 @@ function runStamp(fileName, family) {
|
|
|
28
36
|
return stamp || null;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns =
|
|
32
|
-
|
|
39
|
+
export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns = null, maxAgeDays = DEFAULT_MAX_AGE_DAYS, now = Date.now() } = {}) {
|
|
40
|
+
// `keepRuns` is an explicit operator override for every family; without one each
|
|
41
|
+
// family uses its own bound, so the default is "nothing was asked for" rather than
|
|
42
|
+
// a number that would flatten them all to the hub's ten.
|
|
43
|
+
const keepOverride = Number.isInteger(keepRuns) && keepRuns > 0 ? keepRuns : null;
|
|
33
44
|
const maxAgeMs = Number.isFinite(maxAgeDays) && maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : DEFAULT_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
34
45
|
let names;
|
|
35
46
|
try { names = readdirSync(tempDir); } catch { return { ok: true, removed: [], kept: [], skipped: 'unreadable' }; }
|
|
@@ -50,10 +61,16 @@ export function pruneCrewTempLogs({ tempDir = tmpdir(), keepRuns = DEFAULT_KEEP_
|
|
|
50
61
|
runs.set(stamp, run);
|
|
51
62
|
}
|
|
52
63
|
const ordered = [...runs.values()].sort((left, right) => right.newest - left.newest);
|
|
64
|
+
const keep = keepOverride ?? family.keep;
|
|
53
65
|
ordered.forEach((run, index) => {
|
|
54
66
|
const tooOld = now - run.newest > maxAgeMs;
|
|
55
67
|
const tooMany = index >= keep;
|
|
56
68
|
const tooRecentToTouch = now - run.newest < RECENT_GUARD_MS;
|
|
69
|
+
// Delete when EITHER bound is exceeded. The newest `keep` runs are what an
|
|
70
|
+
// operator reads, and past that a run beyond the age bound is gone regardless
|
|
71
|
+
// of its rank: keeping an eleventh run merely because it is only hours old
|
|
72
|
+
// would let a burst of starts grow without bound, which is the growth this
|
|
73
|
+
// bound exists to stop. A burst of eleven young runs still keeps ten.
|
|
57
74
|
if ((tooOld || tooMany) && !tooRecentToTouch) {
|
|
58
75
|
for (const name of run.files) removed.push(name);
|
|
59
76
|
const errName = run.files[0].replace(/\.out\.log$/, '.err.log');
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
// verification (real worker, reviewer, cancellation, timeout, etc.).
|
|
7
7
|
|
|
8
8
|
const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_APPLICABLE', 'NOT_RUN']);
|
|
9
|
+
// Statuses a trusted evidence source may report. `NOT_APPLICABLE` is deliberately
|
|
10
|
+
// absent: applicability is a fact about this machine's policy, decided when the row
|
|
11
|
+
// is built, and letting evidence declare a required row "not applicable" would be a
|
|
12
|
+
// way to make a check disappear while the matrix still reads healthy. Evidence can
|
|
13
|
+
// neither create nor clear it.
|
|
14
|
+
const EVIDENCE_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
|
|
15
|
+
// The rows this release may decide are not applicable, and only when the reason is
|
|
16
|
+
// policy rather than observation: the two built-in DeepSeek routes exist to name a
|
|
17
|
+
// provider the operator may not be using. Every other row asks a question that is
|
|
18
|
+
// always applicable to a Crew machine.
|
|
19
|
+
const POLICY_NOT_APPLICABLE_ROWS = Object.freeze(['deepseek_flash', 'deepseek_pro']);
|
|
9
20
|
|
|
10
21
|
export const READINESS_REASON_CODES = Object.freeze({
|
|
11
22
|
LIVE_CHECK_PASSED: 'LIVE_CHECK_PASSED',
|
|
@@ -59,7 +70,24 @@ function baseRow(id, category, status, reasonCode, evidenceSource = 'none', extr
|
|
|
59
70
|
|
|
60
71
|
function normalizeEvidence(row, evidence) {
|
|
61
72
|
if (!evidence || typeof evidence !== 'object') return row;
|
|
62
|
-
if (!
|
|
73
|
+
if (!EVIDENCE_STATUSES.includes(evidence.status)) return row;
|
|
74
|
+
// A row the policy marked not applicable keeps that status: reported evidence says
|
|
75
|
+
// something happened, not that the current worker route changed, and letting it
|
|
76
|
+
// flip the row is how "this route is unused" would turn into "this route passed".
|
|
77
|
+
// What was reported is kept as metadata rather than thrown away.
|
|
78
|
+
if (row.status === 'NOT_APPLICABLE') {
|
|
79
|
+
// The same sanitized fields an applied record keeps, so "the observation is not
|
|
80
|
+
// lost" is a statement the code supports rather than a claim about three of five.
|
|
81
|
+
const source = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
82
|
+
const reported = {
|
|
83
|
+
status: evidence.status,
|
|
84
|
+
...(source(evidence.reason_code) ? { reason_code: source(evidence.reason_code) } : {}),
|
|
85
|
+
...(source(evidence.evidence_source) ? { evidence_source: source(evidence.evidence_source) } : {}),
|
|
86
|
+
...(source(evidence.evidence_ref) ? { evidence_ref: source(evidence.evidence_ref) } : {}),
|
|
87
|
+
...(source(evidence.detail_code) && /^[A-Z][A-Z0-9_]{0,127}$/u.test(evidence.detail_code) ? { detail_code: evidence.detail_code } : {}),
|
|
88
|
+
};
|
|
89
|
+
return { ...row, reported_evidence: reported };
|
|
90
|
+
}
|
|
63
91
|
const reason = typeof evidence.reason_code === 'string' && evidence.reason_code.trim()
|
|
64
92
|
? evidence.reason_code.trim()
|
|
65
93
|
: READINESS_REASON_CODES.EVIDENCE_REPORTED;
|
|
@@ -171,12 +199,13 @@ export function buildReadinessMatrix({
|
|
|
171
199
|
// answered), and neither status can move the aggregate — only a FAIL, an
|
|
172
200
|
// UNPROVEN row or an UNAVAILABLE component does. An unknown selection stays
|
|
173
201
|
// NOT_RUN: this matrix is conservative, and "we could not tell" must not read as
|
|
174
|
-
// "not applicable".
|
|
202
|
+
// "not applicable". This policy decision is also final for the row: evidence
|
|
203
|
+
// applied below records what it saw without changing the status back.
|
|
175
204
|
const selectionKnown = typeof workerSelection?.provider === 'string' && workerSelection.provider.length > 0;
|
|
176
205
|
const deepseekRouteSelected = workerProviderMode === 'deepseek-official'
|
|
177
206
|
|| workerSelection?.provider === 'deepseek-official';
|
|
178
|
-
if (workerProviderMode
|
|
179
|
-
for (const id of
|
|
207
|
+
if (workerProviderMode === 'follow-dsh' && selectionKnown && !deepseekRouteSelected) {
|
|
208
|
+
for (const id of POLICY_NOT_APPLICABLE_ROWS) {
|
|
180
209
|
rows[id] = baseRow(id, 'real-execution', 'NOT_APPLICABLE', READINESS_REASON_CODES.WORKER_PROVIDER_FOLLOWS_DSH, 'runtime-policy');
|
|
181
210
|
}
|
|
182
211
|
}
|
|
@@ -192,7 +221,10 @@ export function buildReadinessMatrix({
|
|
|
192
221
|
]));
|
|
193
222
|
|
|
194
223
|
return {
|
|
195
|
-
|
|
224
|
+
// A row can now legally be NOT_APPLICABLE and the summary carries a matching key,
|
|
225
|
+
// so the vocabulary a strict consumer must accept changed: that is what the
|
|
226
|
+
// version means here, not a change in the rules that produce PASS.
|
|
227
|
+
schema_version: 2,
|
|
196
228
|
platform,
|
|
197
229
|
conservative: true,
|
|
198
230
|
summary: counts,
|
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.2.
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.9';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -108,9 +108,17 @@ function projectModelRole({ role, selected, health, jobs, runtime, now, executio
|
|
|
108
108
|
if (healthCurrent && matchingHealth.state === 'callable') {
|
|
109
109
|
return { state: 'CALLABLE', reason_code: matchingHealth.reason_code ?? 'PROVIDER_CALLABLE', selected, observed_at: matchingHealth.observed_at ?? null, expires_at: expiresAt, source: 'provider_health', last_success: null };
|
|
110
110
|
}
|
|
111
|
+
// Callability asks for positive proof that the provider answered, so the ledger
|
|
112
|
+
// fact that proves it is the one that says the execution ended normally: a job the
|
|
113
|
+
// worker recorded as `done` whose `execution_status` is `completed`. The task-level
|
|
114
|
+
// verdict is deliberately NOT part of this — a `partial` task still had a provider
|
|
115
|
+
// response — and a job whose execution failed or was never recorded is not evidence
|
|
116
|
+
// of a response, however its task was later classified. A recovery probe or a real
|
|
117
|
+
// run is what a route without such a job needs.
|
|
118
|
+
const responded = (job) => job?.execution_status === 'completed';
|
|
111
119
|
const completed = (Array.isArray(jobs) ? jobs : [])
|
|
112
120
|
.filter((job) => job?.role === role && job?.provider === selected.provider && job?.model === selected.model
|
|
113
|
-
&& job?.status === 'done' && job
|
|
121
|
+
&& job?.status === 'done' && responded(job)
|
|
114
122
|
&& sameCompleteRuntimeIdentity(job.execution_context, runtime))
|
|
115
123
|
.map((job) => ({ job, endedAt: Date.parse(job.endedAt ?? '') }))
|
|
116
124
|
.filter(({ endedAt }) => Number.isFinite(endedAt))
|
|
@@ -32,6 +32,7 @@ if not "%~1"=="" goto :invalid_argument
|
|
|
32
32
|
set "LAUNCH_HELPER=%LAUNCH_DIR%start-dsh-crew.ps1"
|
|
33
33
|
set "LAUNCH_LOG=%TEMP%\dsh-crew-launcher.log"
|
|
34
34
|
if not exist "%LAUNCH_HELPER%" (
|
|
35
|
+
call :rotate_launcher_log
|
|
35
36
|
>>"%LAUNCH_LOG%" echo [%date% %time%] ERROR Managed launcher helper is missing: %LAUNCH_HELPER%
|
|
36
37
|
echo ERROR: DSH Crew launcher helper is missing.
|
|
37
38
|
echo Repair it with: dsh-crew update
|
|
@@ -51,6 +52,21 @@ echo ERROR: Unsupported launcher arguments: %LAUNCH_REQUEST%
|
|
|
51
52
|
echo Use --open, --background, or --watch.
|
|
52
53
|
exit /b 64
|
|
53
54
|
|
|
55
|
+
:rotate_launcher_log
|
|
56
|
+
rem The PowerShell launcher bounds this log at 5 MiB before its first line, but the
|
|
57
|
+
rem emergency path that calls this is what runs when that helper is MISSING — so a
|
|
58
|
+
rem bounded log cannot depend on the helper whose absence is one of the failures the
|
|
59
|
+
rem log exists to diagnose. Same cap, same two generations, best-effort: a locked or
|
|
60
|
+
rem unreadable log is left alone rather than failing the launch.
|
|
61
|
+
set "LAUNCH_LOG_SIZE="
|
|
62
|
+
for %%A in ("%LAUNCH_LOG%") do set "LAUNCH_LOG_SIZE=%%~zA"
|
|
63
|
+
if not defined LAUNCH_LOG_SIZE goto :eof
|
|
64
|
+
if %LAUNCH_LOG_SIZE% LSS 5242880 goto :eof
|
|
65
|
+
if exist "%LAUNCH_LOG%.2" del /f /q "%LAUNCH_LOG%.2" >nul 2>&1
|
|
66
|
+
if exist "%LAUNCH_LOG%.1" move /y "%LAUNCH_LOG%.1" "%LAUNCH_LOG%.2" >nul 2>&1
|
|
67
|
+
move /y "%LAUNCH_LOG%" "%LAUNCH_LOG%.1" >nul 2>&1
|
|
68
|
+
goto :eof
|
|
69
|
+
|
|
54
70
|
:help
|
|
55
71
|
echo Usage: %~nx0 [--open ^| --background ^| --watch]
|
|
56
72
|
echo --open Open official Harness on 3080 and return once Crew is supervised;
|