robisie 0.1.0 → 0.1.1
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/CHANGELOG.md +69 -0
- package/README.md +76 -12
- package/dist/active-recovery.d.ts +58 -0
- package/dist/active-recovery.js +143 -0
- package/dist/agents.d.ts +34 -0
- package/dist/agents.js +77 -16
- package/dist/attempt-identity.d.ts +21 -0
- package/dist/attempt-identity.js +39 -0
- package/dist/ci-failure-result.d.ts +23 -0
- package/dist/ci-failure-result.js +90 -0
- package/dist/claude-headless.d.ts +7 -0
- package/dist/claude-headless.js +28 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +55 -15
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +190 -85
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +3 -3
- package/dist/context.d.ts +24 -0
- package/dist/context.js +22 -6
- package/dist/dependency-readiness.d.ts +13 -0
- package/dist/dependency-readiness.js +80 -0
- package/dist/dispatch-scope.d.ts +18 -0
- package/dist/dispatch-scope.js +46 -0
- package/dist/done-evidence.d.ts +31 -0
- package/dist/done-evidence.js +216 -0
- package/dist/engine-config.d.ts +65 -0
- package/dist/engine-config.js +139 -0
- package/dist/fsops.d.ts +87 -0
- package/dist/fsops.js +117 -24
- package/dist/helpers.d.ts +15 -0
- package/dist/helpers.js +262 -45
- package/dist/intent-rules/admission.d.ts +10 -0
- package/dist/intent-rules/admission.js +35 -0
- package/dist/intent-rules/done-when-roles.d.ts +21 -0
- package/dist/intent-rules/done-when-roles.js +101 -0
- package/dist/intent-rules/hooks.d.ts +29 -0
- package/dist/intent-rules/hooks.js +23 -0
- package/dist/k3-codex-role.d.ts +20 -0
- package/dist/k3-codex-role.js +68 -0
- package/dist/manifest.d.ts +41 -0
- package/dist/manifest.js +59 -14
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +89 -39
- package/dist/opencode-result.d.ts +19 -0
- package/dist/opencode-result.js +136 -0
- package/dist/opencode-review-guard.d.ts +1 -0
- package/dist/opencode-review-guard.js +19 -0
- package/dist/opencode-session-export.d.ts +5 -0
- package/dist/opencode-session-export.js +28 -0
- package/dist/outcome-moves.d.ts +15 -0
- package/dist/outcome-moves.js +39 -0
- package/dist/planer-client.d.ts +214 -0
- package/dist/planer-client.js +351 -0
- package/dist/restart-recovery.d.ts +47 -0
- package/dist/restart-recovery.js +70 -0
- package/dist/review-source-packet.d.ts +17 -0
- package/dist/review-source-packet.js +44 -0
- package/dist/role-registry.d.ts +19 -0
- package/dist/role-registry.js +83 -0
- package/dist/role-results.d.ts +15 -0
- package/dist/role-results.js +42 -0
- package/dist/roles.d.ts +30 -0
- package/dist/roles.js +20 -16
- package/dist/seed-validation.d.ts +28 -0
- package/dist/seed-validation.js +294 -0
- package/dist/selection-result.d.ts +44 -0
- package/dist/selection-result.js +144 -0
- package/dist/sparse-policy.d.ts +8 -0
- package/dist/sparse-policy.js +49 -0
- package/dist/tester-identity.d.ts +11 -0
- package/dist/tester-identity.js +53 -0
- package/dist/worker-env.d.ts +5 -0
- package/dist/worker-env.js +34 -0
- package/docs/ENGINE-API.md +78 -0
- package/docs/ENGINE-CONFIG.md +115 -0
- package/docs/README.en.md +24 -15
- package/docs/README.md +26 -15
- package/docs/RELEASE-0.1.0.md +81 -8
- package/docs/RELEASE-0.1.1.md +110 -0
- package/helpers/ceo-client-launcher-mcp.mjs +36 -8
- package/helpers/client-spawn-session.sh +181 -0
- package/helpers/client-spawn.sh +825 -0
- package/package.json +9 -4
- package/roles/ceo-client.md +3 -2
- package/roles/manager-builder-client.md +18 -4
- package/roles/quality-officer-client.md +20 -6
package/dist/commands.js
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
|
-
import { readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
-
import { AGENT_LABELS, SHELL_EXPORT, agentPaths, agentOnPath, detectAgents, planAgent, removeCodexTables, shellProfile } from "./agents.js";
|
|
1
|
+
import { lstatSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { AGENT_IDS, AGENT_LABELS, SHELL_EXPORT, agentPaths, agentOnPath, detectAgents, planAgent, removeCodexTables, shellProfile } from "./agents.js";
|
|
3
3
|
import { HELPER_SERVERS, KEY_ENV, KEY_PATTERN, PACKAGE_NAME, PACKAGE_VERSION } from "./constants.js";
|
|
4
4
|
import { RobisieError, errorMessage } from "./context.js";
|
|
5
|
-
import { applyOp, assertCodexChange, capture, entryPresent, prepareBlock, refusePrivateSymlink, restore, undoEntry } from "./fsops.js";
|
|
5
|
+
import { applyOp, assertCodexChange, checkRecovery, preserveFileStep, capture, entryPresent, fingerprint, prepareBlock, refusePrivateSymlink, renderBlock, restore, undoEntry } from "./fsops.js";
|
|
6
6
|
import { HELPER_TOOLS, probeHelper } from "./helpers.js";
|
|
7
7
|
import { clientPaths, ensurePrivateDir, readKey, readManifest, writeKey, writeManifest, roleProviders, ROLE_IDS, ROLE_STEP } from "./manifest.js";
|
|
8
8
|
import { assertCompatible, checkConnection, fetchContractVersion } from "./mcp.js";
|
|
9
9
|
import { loadRoles, rolesDir } from "./roles.js";
|
|
10
10
|
const NO_KEY_STEP = "Wygeneruj klucz worker na https://robisie.app/dashboard/connect; w zsh/bash wykonaj `unset ROBISIE_KEY`, potem lokalne `npx robisie init` i wklej klucz w ukrytym pytaniu (poza rozmową).";
|
|
11
|
+
const KEY_FORMAT_STEP = `Skopiuj klucz jeszcze raz z https://robisie.app/dashboard/connect; uruchom lokalne \`npx robisie init\` i wklej klucz w ukrytym pytaniu (poza rozmową). Alternatywnie przekaż klucz przez stdin z jawną flagą: \`npx robisie init --key-stdin\` (poza rozmową i historią poleceń). Następnie wczytaj zapisany klucz do bieżącej sesji POSIX z tym samym XDG_CONFIG_HOME: \`${SHELL_EXPORT}\`, potem uruchom \`npx robisie doctor\`.`;
|
|
12
|
+
const ENV_KEY_FORMAT_STEP = `W zsh/bash wykonaj \`unset ROBISIE_KEY\` — ENV ma pierwszeństwo przed pytaniem, stdin i plikiem. ${KEY_FORMAT_STEP}`;
|
|
13
|
+
/** Advice chooses an executable CLI, not merely a leftover configuration directory. */
|
|
14
|
+
export function initRecoveryStep(ctx, preferred) {
|
|
15
|
+
let prior;
|
|
16
|
+
try {
|
|
17
|
+
prior = readManifest(clientPaths(agentPaths(ctx).clientDir).manifest);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return error instanceof RobisieError ? error.nextStep : `Nie mogę sprawdzić ewidencji instalacji: ${errorMessage(error)}. Sprawdź jej plik i uprawnienia przed ponowieniem init.`;
|
|
21
|
+
}
|
|
22
|
+
const missing = [...new Set(prior?.agents ?? [])].filter((id) => !agentOnPath(ctx, id));
|
|
23
|
+
const historyStep = missing.map((id) => `Zainstaluj program ${id} lub dodaj go do PATH.`).join(" ");
|
|
24
|
+
const provider = preferred && agentOnPath(ctx, preferred) ? preferred : AGENT_IDS.find((id) => agentOnPath(ctx, id));
|
|
25
|
+
if (!provider)
|
|
26
|
+
return `${historyStep || "Zainstaluj zewnętrznie Claude Code, Codex CLI albo OpenCode i dodaj program do PATH."} Potem ponów \`npx robisie doctor\`, aby przypisać role ceo, builder, quality.`;
|
|
27
|
+
return `${historyStep ? `${historyStep} Zachowana historia instalacji nadal wymaga tych programów. Potem: ` : ""}Uruchom \`npx robisie init --agent ${provider} --role ceo=${provider} --role builder=${provider} --role quality=${provider}\` (role: ceo, builder, quality; dostawcy: claude, codex, opencode).`;
|
|
28
|
+
}
|
|
29
|
+
export function recoveryStep(ctx, step) {
|
|
30
|
+
return step.includes(ROLE_STEP) ? step.replace(ROLE_STEP, initRecoveryStep(ctx)) : step;
|
|
31
|
+
}
|
|
32
|
+
function ceoProjectStep(count) {
|
|
33
|
+
return (count === 0
|
|
34
|
+
? "Sprawdź gotowość projektu na https://robisie.app/dashboard/plugins/planer."
|
|
35
|
+
: "Uporządkuj widoczność projektów w discovery do 1; sam wybór projektu w panelu nie zmienia discovery.")
|
|
36
|
+
+ " Potem uruchom `npx robisie doctor`.";
|
|
37
|
+
}
|
|
11
38
|
function entryId(entry) {
|
|
12
39
|
return `${entry.kind}:${entry.path}:${entry.keyPath?.join(".") ?? ""}`;
|
|
13
40
|
}
|
|
14
41
|
async function obtainKey(ctx, keyStdin) {
|
|
15
42
|
let key = ctx.env[KEY_ENV]?.trim() ?? "";
|
|
16
|
-
|
|
43
|
+
let source = KEY_ENV;
|
|
44
|
+
if (!key && ctx.askSecret) {
|
|
45
|
+
source = "ukrytego pytania";
|
|
17
46
|
key = (await ctx.askSecret("Wklej klucz robisie (rob_…, nie będzie widoczny): ")).trim();
|
|
18
|
-
|
|
47
|
+
}
|
|
48
|
+
if (!key && keyStdin && ctx.readStdin) {
|
|
49
|
+
source = "stdin";
|
|
19
50
|
key = (await ctx.readStdin()).trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
51
|
+
}
|
|
20
52
|
if (!key)
|
|
21
|
-
throw new RobisieError("Brak klucza robisie.",
|
|
53
|
+
throw new RobisieError("Brak klucza robisie.", ctx.askSecret === null && !keyStdin
|
|
54
|
+
? `${NO_KEY_STEP} Brak terminala do ukrytego pytania; uruchom init w terminalu albo przekaż klucz przez stdin z jawną flagą --key-stdin (poza rozmową).`
|
|
55
|
+
: NO_KEY_STEP);
|
|
22
56
|
if (!KEY_PATTERN.test(key))
|
|
23
|
-
throw new RobisieError(
|
|
57
|
+
throw new RobisieError(`Klucz z ${source} nie wygląda na klucz robisie (oczekuję rob_ i 64 znaków 0-9a-f).`, source === KEY_ENV ? ENV_KEY_FORMAT_STEP : KEY_FORMAT_STEP);
|
|
24
58
|
return key;
|
|
25
59
|
}
|
|
26
60
|
export async function init(ctx, options) {
|
|
@@ -36,8 +70,9 @@ export async function init(ctx, options) {
|
|
|
36
70
|
map = roleProviders({ agents: [], roleProviders: merged });
|
|
37
71
|
}
|
|
38
72
|
const selected = [...new Set([...Object.values(map ?? {}), ...detected])];
|
|
73
|
+
const mapChanged = prior !== null && ROLE_IDS.some((role) => previousMap?.[role] !== map?.[role]);
|
|
39
74
|
if (selected.length === 0) {
|
|
40
|
-
throw new RobisieError("Nie znalazłem na tym komputerze Claude Code, Codex CLI ani OpenCode.",
|
|
75
|
+
throw new RobisieError("Nie znalazłem na tym komputerze Claude Code, Codex CLI ani OpenCode.", initRecoveryStep(ctx));
|
|
41
76
|
}
|
|
42
77
|
const key = await obtainKey(ctx, options.keyStdin);
|
|
43
78
|
// Refuse before writing anything: incompatible server, or a key that does not open the board.
|
|
@@ -53,14 +88,16 @@ export async function init(ctx, options) {
|
|
|
53
88
|
const profile = options.shell ? shellProfile(ctx) : null;
|
|
54
89
|
if (profile)
|
|
55
90
|
planned.push({ owner: "shell", op: { kind: "block", path: profile, comment: "hash", body: SHELL_EXPORT } });
|
|
91
|
+
const priorById = new Map((prior?.entries ?? []).map((entry) => [entryId(entry), entry]));
|
|
56
92
|
// Before writing anything, so a refusal leaves no half-done install behind.
|
|
57
93
|
for (const { op } of planned) {
|
|
58
94
|
refusePrivateSymlink(op);
|
|
95
|
+
const before = capture(op.path);
|
|
59
96
|
if (op.kind === "block" && op.clean)
|
|
60
|
-
prepareBlock(
|
|
97
|
+
prepareBlock(before.bytes?.toString("utf8") ?? null, op.comment, op.clean, op.body);
|
|
98
|
+
checkRecovery(op, priorById.get(entryId(op)));
|
|
61
99
|
}
|
|
62
100
|
ensurePrivateDir(paths.dir);
|
|
63
|
-
const priorById = new Map((prior?.entries ?? []).map((entry) => [entryId(entry), entry]));
|
|
64
101
|
const done = new Map(priorById);
|
|
65
102
|
const agents = [...new Set([...(prior?.agents ?? []), ...selected])];
|
|
66
103
|
let changed = 0;
|
|
@@ -99,14 +136,18 @@ export async function init(ctx, options) {
|
|
|
99
136
|
changed += 1;
|
|
100
137
|
}
|
|
101
138
|
ctx.out(`robisie ${PACKAGE_VERSION}: połączono z tablicą (projektów: ${connection.projectCount}).`);
|
|
139
|
+
if (connection.projectCount !== 1)
|
|
140
|
+
ctx.out(`! CEO nie jest gotowy: wymaga dokładnie 1 projektu (projektów: ${connection.projectCount}). ${ceoProjectStep(connection.projectCount)}`);
|
|
102
141
|
for (const id of selected)
|
|
103
142
|
ctx.out(`✓ ${AGENT_LABELS[id]}: serwer MCP robisie i role klienta (CEO, Builder, Quality Officer) zainstalowane.`);
|
|
104
|
-
ctx.out(changed === 0 ? "Nic się nie zmieniło — wszystko było już zainstalowane." : `Zmienione pliki: ${changed} (kopie zapasowe: ${paths.backups}).`);
|
|
143
|
+
ctx.out(changed === 0 ? (mapChanged ? "✓ mapa ról zmieniona; konfiguracja agentów bez zmian." : "Nic się nie zmieniło — wszystko było już zainstalowane.") : `Zmienione pliki: ${changed} (kopie zapasowe: ${paths.backups}).`);
|
|
105
144
|
if (profile)
|
|
106
145
|
ctx.out(`✓ Klucz będzie dostępny jako ${KEY_ENV} w nowych terminalach (wpis w ${profile}).`);
|
|
107
|
-
else
|
|
108
|
-
|
|
109
|
-
|
|
146
|
+
else {
|
|
147
|
+
const reason = !options.shell ? "--no-shell" : !ctx.env.SHELL ? "brak SHELL" : "niewspierana powłoka/platforma";
|
|
148
|
+
ctx.out(`! Pominięto profil: ${reason}. Ustaw sam zmienną ${KEY_ENV} dla agenta, np.: ${SHELL_EXPORT}`);
|
|
149
|
+
}
|
|
150
|
+
ctx.out(`Następny krok: ${!map || map.ceo !== map.builder || map.ceo !== map.quality ? initRecoveryStep(ctx, map?.ceo) : "otwórz nowy terminal i uruchom `npx robisie doctor`."}`);
|
|
110
151
|
}
|
|
111
152
|
// ── doctor ───────────────────────────────────────────────────────────────────
|
|
112
153
|
/** Returns the process exit code: 0 = all good, 1 = something needs one action. */
|
|
@@ -114,7 +155,7 @@ export async function doctor(ctx) {
|
|
|
114
155
|
const problems = [];
|
|
115
156
|
const fail = (line, nextStep) => {
|
|
116
157
|
ctx.out(`✗ ${line}`);
|
|
117
|
-
problems.push(nextStep);
|
|
158
|
+
problems.push(recoveryStep(ctx, nextStep));
|
|
118
159
|
};
|
|
119
160
|
const guard = async (action) => {
|
|
120
161
|
try {
|
|
@@ -129,6 +170,11 @@ export async function doctor(ctx) {
|
|
|
129
170
|
};
|
|
130
171
|
ctx.out(`Paczka ${PACKAGE_NAME} ${PACKAGE_VERSION}`);
|
|
131
172
|
const paths = clientPaths(agentPaths(ctx).clientDir);
|
|
173
|
+
// Ledger recovery must remain the next step even when network checks fail.
|
|
174
|
+
let manifest = null;
|
|
175
|
+
await guard(async () => {
|
|
176
|
+
manifest = readManifest(paths.manifest);
|
|
177
|
+
});
|
|
132
178
|
await guard(async () => {
|
|
133
179
|
const version = await fetchContractVersion(ctx);
|
|
134
180
|
assertCompatible(version);
|
|
@@ -136,26 +182,28 @@ export async function doctor(ctx) {
|
|
|
136
182
|
});
|
|
137
183
|
const envKey = ctx.env[KEY_ENV]?.trim() || null;
|
|
138
184
|
let key = envKey;
|
|
185
|
+
let keyRead = false;
|
|
139
186
|
await guard(async () => {
|
|
140
187
|
key = envKey ?? readKey(paths.dir);
|
|
188
|
+
keyRead = true;
|
|
141
189
|
});
|
|
142
190
|
const usableKey = key;
|
|
143
|
-
if (!usableKey)
|
|
144
|
-
|
|
191
|
+
if (!usableKey) {
|
|
192
|
+
if (keyRead)
|
|
193
|
+
fail("Brak klucza robisie.", NO_KEY_STEP);
|
|
194
|
+
}
|
|
145
195
|
else if (!KEY_PATTERN.test(usableKey))
|
|
146
|
-
fail("
|
|
196
|
+
fail(`${envKey ? "Klucz z ROBISIE_KEY" : "Klucz w pliku"} ma zły format.`, envKey ? ENV_KEY_FORMAT_STEP : KEY_FORMAT_STEP);
|
|
147
197
|
else {
|
|
148
198
|
await guard(async () => {
|
|
149
199
|
const connection = await checkConnection(ctx, usableKey);
|
|
150
200
|
if (connection.contractVersion)
|
|
151
201
|
assertCompatible(connection.contractVersion);
|
|
152
202
|
ctx.out(`✓ Połączenie działa: klucz otwiera tablicę (projektów: ${connection.projectCount}).`);
|
|
203
|
+
if (connection.projectCount !== 1)
|
|
204
|
+
fail(`CEO wymaga dokładnie 1 projektu (projektów: ${connection.projectCount}).`, ceoProjectStep(connection.projectCount));
|
|
153
205
|
});
|
|
154
206
|
}
|
|
155
|
-
let manifest = null;
|
|
156
|
-
await guard(async () => {
|
|
157
|
-
manifest = readManifest(paths.manifest);
|
|
158
|
-
});
|
|
159
207
|
const installed = manifest;
|
|
160
208
|
let map = null;
|
|
161
209
|
if (installed)
|
|
@@ -164,19 +212,45 @@ export async function doctor(ctx) {
|
|
|
164
212
|
for (const role of ROLE_IDS)
|
|
165
213
|
ctx.out(`${role} → ${assignment?.[role] ?? "nie przypisano"}`);
|
|
166
214
|
if (!installed || installed.agents.length === 0) {
|
|
167
|
-
fail("robisie nie jest zainstalowane dla żadnego agenta.",
|
|
215
|
+
fail("robisie nie jest zainstalowane dla żadnego agenta.", initRecoveryStep(ctx));
|
|
168
216
|
}
|
|
169
217
|
else {
|
|
218
|
+
for (const id of new Set(assignment ? Object.values(assignment) : installed.agents)) {
|
|
219
|
+
if (!agentOnPath(ctx, id)) {
|
|
220
|
+
const assigned = assignment ? ROLE_IDS.filter((role) => assignment[role] === id).join(", ") : "nie przypisano";
|
|
221
|
+
fail(`${assigned} → ${id}: ${detectAgents(ctx).includes(id) ? "katalog jest, programu nie ma w PATH" : "brak programu w PATH i katalogu dostawcy"}.`, `Zainstaluj program ${id} lub dodaj go do PATH i ponów \`npx robisie doctor\`.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
170
224
|
const byId = new Map(installed.entries.map((entry) => [entryId(entry), entry]));
|
|
171
225
|
for (const id of new Set([...installed.agents, ...Object.values(assignment ?? {})])) {
|
|
172
|
-
const repair = `Uruchom \`npx robisie init --agent ${id}
|
|
226
|
+
const repair = `Uruchom \`npx robisie init --agent ${id}\`${id === "claude" ? " z właściwym ROBISIE_KEY w środowisku" : ""} — uzupełni braki.`;
|
|
173
227
|
await guard(async () => {
|
|
174
228
|
// A complete install = every change init plans for this agent is recorded AND still in place.
|
|
175
229
|
const expected = planAgent(ctx, id, loadRoles(), usableKey ?? `rob_${"0".repeat(64)}`);
|
|
176
230
|
const missing = expected.filter((op) => {
|
|
177
231
|
const entry = byId.get(entryId(op));
|
|
232
|
+
try {
|
|
233
|
+
checkRecovery(op, entry);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!(error instanceof RobisieError))
|
|
237
|
+
throw error;
|
|
238
|
+
const detail = assignment ? ROLE_IDS.filter((role) => assignment[role] === id).map((role) => `${role} → ${id}: niepełna instalacja`).join(", ") : "";
|
|
239
|
+
throw new RobisieError(`${AGENT_LABELS[id]}: ${error.message}${detail ? ` Role: ${detail}.` : ""}`, error.nextStep);
|
|
240
|
+
}
|
|
178
241
|
if (!entry || entry.owner !== id || !entryPresent(entry))
|
|
179
242
|
return true;
|
|
243
|
+
if (op.kind === "file" && capture(op.path).bytes?.toString("utf8") !== op.content)
|
|
244
|
+
return true;
|
|
245
|
+
if (op.kind === "block" && op.comment === "html") {
|
|
246
|
+
const current = (capture(op.path).bytes?.toString("utf8") ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
247
|
+
const planned = renderBlock(op.comment, op.body).trimEnd().split("\n");
|
|
248
|
+
const start = current.indexOf(planned[0] ?? "");
|
|
249
|
+
if (start < 0 || current.slice(start, start + planned.length).join("\n") !== planned.join("\n"))
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (id === "claude" && op.kind === "json" && (entry.kind !== "json" || fingerprint(op.value) !== entry.fingerprint))
|
|
253
|
+
return true;
|
|
180
254
|
if (op.kind === "block" && op.clean) {
|
|
181
255
|
try {
|
|
182
256
|
const current = capture(op.path).bytes?.toString("utf8") ?? "";
|
|
@@ -201,16 +275,13 @@ export async function doctor(ctx) {
|
|
|
201
275
|
}
|
|
202
276
|
});
|
|
203
277
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
fail(`${assigned} → ${id}: ${detectAgents(ctx).includes(id) ? "katalog jest, programu nie ma w PATH" : "brak programu w PATH i katalogu dostawcy"}.`, `Zainstaluj program ${id} lub dodaj go do PATH i ponów \`npx robisie doctor\`.`);
|
|
208
|
-
}
|
|
278
|
+
if (!assignment) {
|
|
279
|
+
const provider = installed.agents[0];
|
|
280
|
+
fail("Role: nie przypisano dostawców.", `Uruchom \`npx robisie init --agent ${provider} --role ceo=${provider} --role builder=${provider} --role quality=${provider}\`. Aby wybrać innego zainstalowanego dostawcę (${installed.agents.join(", ")}), zastąp ${provider} jego nazwą we wszystkich czterech miejscach.`);
|
|
209
281
|
}
|
|
210
|
-
if (!assignment)
|
|
211
|
-
fail("Role: nie przypisano dostawców.", `Uruchom npx robisie init z pełną mapą. ${ROLE_STEP}`);
|
|
212
282
|
else if (assignment.builder !== assignment.ceo || assignment.quality !== assignment.ceo) {
|
|
213
|
-
|
|
283
|
+
const provider = assignment.ceo;
|
|
284
|
+
fail(`Mieszana mapa: Quality Officer jako podagent u dostawcy CEO (${assignment.ceo}); polecenie Buildera w formacie dostawcy CEO (${assignment.ceo}). Wykonanie mieszanych ról to następny zakres.`, `Wybierz jednego dostawcę dla trzech ról przez \`npx robisie init --agent ${provider} --role ceo=${provider} --role builder=${provider} --role quality=${provider}\`. Aby wybrać innego zainstalowanego dostawcę (${installed.agents.join(", ")}), zastąp ${provider} jego nazwą we wszystkich czterech miejscach albo zachowaj mapę i poczekaj na obsługę mieszanych ról.`);
|
|
214
285
|
}
|
|
215
286
|
for (const name of HELPER_SERVERS) {
|
|
216
287
|
await guard(async () => {
|
|
@@ -219,11 +290,11 @@ export async function doctor(ctx) {
|
|
|
219
290
|
tools = await probeHelper(name, ctx.env, ctx.cwd);
|
|
220
291
|
}
|
|
221
292
|
catch (error) {
|
|
222
|
-
throw new RobisieError(`Pomocnik ${name} nie działa na tym komputerze: ${errorMessage(error)}`, "Sprawdź Node.js (wymagany 20+). Przeinstaluj
|
|
293
|
+
throw new RobisieError(`Pomocnik ${name} nie działa na tym komputerze: ${errorMessage(error)}`, "Sprawdź Node.js (wymagany 20+). Przeinstaluj paczkę z npm w katalogu instalacji: `npm i robisie@latest`, potem lokalne `npx robisie init` i `npx robisie doctor`.");
|
|
223
294
|
}
|
|
224
295
|
const absent = HELPER_TOOLS[name].filter((tool) => !tools.includes(tool));
|
|
225
296
|
if (absent.length > 0)
|
|
226
|
-
throw new RobisieError(`Pomocnik ${name} nie ma narzędzi: ${absent.join(", ")}.`, "Przeinstaluj
|
|
297
|
+
throw new RobisieError(`Pomocnik ${name} nie ma narzędzi: ${absent.join(", ")}.`, "Przeinstaluj paczkę z npm w katalogu instalacji: `npm i robisie@latest`, potem lokalne `npx robisie init` i `npx robisie doctor`.");
|
|
227
298
|
ctx.out(`✓ Pomocnik ${name} działa (${tools.length} narzędzi).`);
|
|
228
299
|
});
|
|
229
300
|
}
|
|
@@ -250,69 +321,103 @@ function removeEmptyDir(path) {
|
|
|
250
321
|
return true;
|
|
251
322
|
if (code === "ENOTEMPTY" || code === "EEXIST")
|
|
252
323
|
return false;
|
|
253
|
-
throw new RobisieError(`Nie mogę usunąć katalogu ${path}: ${errorMessage(error)}`, `
|
|
324
|
+
throw new RobisieError(`Nie mogę usunąć katalogu ${path}: ${errorMessage(error)}`, `Sprawdź uprawnienia i usuń przeszkodę uniemożliwiającą opróżnienie katalogu ${path}, zachowując ewidencję instalacji i kopie użytkownika. Potem ponów \`npx robisie uninstall\`.`);
|
|
254
325
|
}
|
|
255
326
|
}
|
|
256
327
|
export function uninstall(ctx) {
|
|
257
328
|
const agent = agentPaths(ctx);
|
|
258
329
|
const paths = clientPaths(agent.clientDir);
|
|
259
|
-
const manifest = readManifest(paths.manifest);
|
|
260
|
-
let hasBackups = false;
|
|
261
330
|
try {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new RobisieError(`Nie mogę sprawdzić kopii zapasowych: ${errorMessage(error)}`, `Sprawdź uprawnienia do ${paths.backups} i ponów polecenie.`);
|
|
331
|
+
const manifest = readManifest(paths.manifest);
|
|
332
|
+
let hasBackups = false;
|
|
333
|
+
try {
|
|
334
|
+
hasBackups = readdirSync(paths.backups).length > 0;
|
|
267
335
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
ctx.out("Nie znaleziono zapisu instalacji — nic nie zmieniono (0 zmian).");
|
|
272
|
-
ctx.out(`Jeśli wpis robisie pozostał, usuń go ręcznie z konfiguracji swojego agenta: ${agent.claudeJson}, ${agent.codexConfig} lub ${agent.opencodeJson} (albo ${agent.opencodeJsonc}).`);
|
|
273
|
-
if (backupNotice)
|
|
274
|
-
ctx.out(backupNotice);
|
|
275
|
-
return 0;
|
|
276
|
-
}
|
|
277
|
-
const pending = new Set(manifest.entries);
|
|
278
|
-
// The manifest records the installed location; CODEX_HOME may have changed since init.
|
|
279
|
-
const codexConfigs = new Set(manifest.entries.filter((entry) => entry.kind === "block" && entry.comment === "hash" && (entry.owner === "codex" || entry.path === agent.codexConfig)).map((entry) => entry.path));
|
|
280
|
-
const evidence = `${backupNotice ? `${backupNotice} ` : ""}Pozostałe zmiany: ${paths.manifest}.`;
|
|
281
|
-
try {
|
|
282
|
-
// Refuse unsafe Codex edits before deleting the key, undoing roles or recording progress.
|
|
283
|
-
for (const entry of manifest.entries) {
|
|
284
|
-
if (entry.kind === "block" && codexConfigs.has(entry.path)) {
|
|
285
|
-
prepareBlock(capture(entry.path).bytes?.toString("utf8") ?? null, entry.comment, (text) => removeCodexTables(text, true));
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error.code !== "ENOENT") {
|
|
338
|
+
throw new RobisieError(`Nie mogę sprawdzić kopii zapasowych: ${errorMessage(error)}`, `Sprawdź uprawnienia do ${paths.backups} i ponów polecenie.`);
|
|
286
339
|
}
|
|
287
340
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
ctx.out(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
341
|
+
const backupNotice = hasBackups ? `Kopie zapasowe pozostają w ${paths.backups}.` : "";
|
|
342
|
+
if (!manifest) {
|
|
343
|
+
ctx.out("Nie znaleziono zapisu instalacji — nic nie zmieniono (0 zmian).");
|
|
344
|
+
ctx.out(`Jeśli wpis robisie pozostał, usuń go ręcznie z konfiguracji swojego agenta: ${agent.claudeJson}, ${agent.codexConfig} lub ${agent.opencodeJson} (albo ${agent.opencodeJsonc}).`);
|
|
345
|
+
if (backupNotice)
|
|
346
|
+
ctx.out(backupNotice);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
const pending = new Set(manifest.entries);
|
|
350
|
+
// The manifest records the installed location; CODEX_HOME may have changed since init.
|
|
351
|
+
const codexConfigs = new Set(manifest.entries.filter((entry) => entry.kind === "block" && entry.comment === "hash" && (entry.owner === "codex" || entry.path === agent.codexConfig)).map((entry) => entry.path));
|
|
352
|
+
const evidence = `${backupNotice ? `${backupNotice} ` : ""}Pozostałe zmiany: ${paths.manifest}.`;
|
|
353
|
+
const leftovers = [];
|
|
354
|
+
try {
|
|
355
|
+
// Refuse missing role originals and unsafe Codex edits before any uninstall mutation.
|
|
356
|
+
for (const entry of manifest.entries) {
|
|
357
|
+
if (entry.kind === "file" && entry.backup && capture(entry.backup).bytes === null) {
|
|
358
|
+
throw new RobisieError(`Brak kopii oryginału ${entry.backup} dla ${entry.path}.`, `Przywróć tę kopię z własnego archiwum do ${entry.backup}; bez niej zatrzymaj się, nie usuwaj pliku ${entry.path} i nie ponawiaj uninstall. Po przywróceniu kopii ponów \`npx robisie uninstall\`.`);
|
|
359
|
+
}
|
|
360
|
+
if (entry.kind === "block" && codexConfigs.has(entry.path)) {
|
|
361
|
+
prepareBlock(capture(entry.path).bytes?.toString("utf8") ?? null, entry.comment, (text) => removeCodexTables(text, true));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// The key is not needed to finish an uninstall, even after a warning or a failure.
|
|
365
|
+
rmSync(paths.key, { force: true });
|
|
366
|
+
for (const entry of [...manifest.entries].reverse()) {
|
|
367
|
+
const warning = undoEntry(entry, codexConfigs.has(entry.path) ? (text) => removeCodexTables(text, true) : undefined);
|
|
368
|
+
if (warning)
|
|
369
|
+
ctx.out(`! ${warning} ${evidence}`);
|
|
370
|
+
else {
|
|
371
|
+
pending.delete(entry);
|
|
372
|
+
// A later init must not mistake an already restored user role for an edited installed one.
|
|
373
|
+
writeManifest(paths.dir, { agents: manifest.agents, roleProviders: manifest.roleProviders, entries: [...pending] });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (pending.size > 0) {
|
|
377
|
+
ctx.out(`! Nie udało się cofnąć wszystkich zmian. ${evidence}`);
|
|
378
|
+
const next = [...pending].map((entry) => `${preserveFileStep(entry.path)}${entry.backup ? ` Przywróć oryginał z kopii ${entry.backup} do ${entry.path}, zachowując tę kopię.` : ""}`).join(" ");
|
|
379
|
+
ctx.out(`Następny krok: ${next} Potem ponów \`npx robisie uninstall\`.`);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
for (const dir of [paths.codexRoles, paths.roles])
|
|
383
|
+
if (!removeEmptyDir(dir))
|
|
384
|
+
leftovers.push(dir);
|
|
385
|
+
// The ledger lives inside paths.dir, so only the final rmdir requires removing it first.
|
|
386
|
+
try {
|
|
387
|
+
rmSync(paths.manifest, { force: true });
|
|
388
|
+
if (!removeEmptyDir(paths.dir))
|
|
389
|
+
leftovers.push(paths.dir);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
try {
|
|
393
|
+
writeManifest(paths.dir, { agents: manifest.agents, roleProviders: manifest.roleProviders, entries: [...pending] });
|
|
394
|
+
}
|
|
395
|
+
catch (restoreError) {
|
|
396
|
+
throw new RobisieError(`${errorMessage(error)} Nie udało się odtworzyć ewidencji ${paths.manifest}: ${errorMessage(restoreError)}`, `Zachowaj katalog ${paths.dir} i kopie użytkownika. ${restoreError instanceof RobisieError ? restoreError.nextStep : "Sprawdź uprawnienia do katalogu ewidencji."} Nie ponawiaj \`npx robisie uninstall\` bez poprawnej ewidencji instalacji.`);
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
298
399
|
}
|
|
299
400
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
ctx.out("Następny krok: porównaj pozostawione pliki z kopiami, zachowaj swoje zmiany i ponów `npx robisie uninstall`.");
|
|
303
|
-
return 1;
|
|
401
|
+
catch (error) {
|
|
402
|
+
throw new RobisieError(`Nie udało się dokończyć odinstalowania: ${errorMessage(error)}`, `${error instanceof RobisieError ? error.nextStep : "Rozwiąż problem i ponów `npx robisie uninstall`."} ${evidence}`);
|
|
304
403
|
}
|
|
305
|
-
|
|
404
|
+
ctx.out(`✓ Usunięto wpisy i bloki robisie oraz cofnięto instalację ról (${manifest.entries.length} zmian).`);
|
|
405
|
+
if (backupNotice)
|
|
406
|
+
ctx.out(backupNotice);
|
|
407
|
+
if (leftovers.length > 0)
|
|
408
|
+
ctx.out(`Zostawiłem niepuste katalogi: ${leftovers.join(", ")}.`);
|
|
409
|
+
ctx.out("Następny krok: otwórz nowy terminal, żeby zmienna ROBISIE_KEY zniknęła z sesji.");
|
|
410
|
+
return 0;
|
|
306
411
|
}
|
|
307
|
-
|
|
308
|
-
|
|
412
|
+
finally {
|
|
413
|
+
let state = "pozostaje";
|
|
414
|
+
try {
|
|
415
|
+
lstatSync(paths.key);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
const code = error.code;
|
|
419
|
+
state = code === "ENOENT" ? "nie istnieje" : `nie można ustalić (${typeof code === "string" && /^E[A-Z0-9_]{1,39}$/.test(code) ? code : "błąd metadanych"})`;
|
|
420
|
+
}
|
|
421
|
+
ctx.out(`Lokalny plik klucza: ${state} — ${paths.key}.`);
|
|
309
422
|
}
|
|
310
|
-
const leftovers = [paths.codexRoles, paths.roles, paths.dir].filter((dir) => !removeEmptyDir(dir));
|
|
311
|
-
ctx.out(`✓ Usunięto wpisy i bloki robisie oraz cofnięto instalację ról (${manifest.entries.length} zmian).`);
|
|
312
|
-
if (backupNotice)
|
|
313
|
-
ctx.out(backupNotice);
|
|
314
|
-
if (leftovers.length > 0)
|
|
315
|
-
ctx.out(`Zostawiłem niepuste katalogi: ${leftovers.join(", ")}.`);
|
|
316
|
-
ctx.out("Następny krok: otwórz nowy terminal, żeby zmienna ROBISIE_KEY zniknęła z sesji.");
|
|
317
|
-
return 0;
|
|
318
423
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Package identity and the server contract it speaks. */
|
|
2
|
+
export declare const PACKAGE_NAME = "robisie";
|
|
3
|
+
export declare const PACKAGE_VERSION = "0.1.1";
|
|
4
|
+
/**
|
|
5
|
+
* MAJOR of the agent-API contract this package understands.
|
|
6
|
+
* PARITY: src/lib/mcp/contract-version.ts (AGENT_API_CONTRACT_VERSION) on the server.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SUPPORTED_CONTRACT_MAJOR = 1;
|
|
9
|
+
export declare const DEFAULT_ORIGIN = "https://robisie.app";
|
|
10
|
+
export declare const MCP_SERVER_NAME = "robisie";
|
|
11
|
+
export declare const KEY_ENV = "ROBISIE_KEY";
|
|
12
|
+
/**
|
|
13
|
+
* Local stdio helpers the client roles call (`mcp__client-launch-context__…`, `mcp__ceo-client-launcher__…`).
|
|
14
|
+
* Shipped in `helpers/` (verbatim copies of the repo's scripts/) and started as `npx -y robisie@<version> mcp <name>`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HELPER_SERVERS: readonly ["client-launch-context", "ceo-client-launcher"];
|
|
17
|
+
export type HelperServer = (typeof HELPER_SERVERS)[number];
|
|
18
|
+
/** Environment the helpers read: credential, Builder card, project and installed configuration. */
|
|
19
|
+
export declare const HELPER_ENV: readonly ["ROBISIE_KEY", "ROBISIE_CARD_REF", "ROBISIE_PROJECT_CONTEXT", "XDG_CONFIG_HOME", "CODEX_HOME"];
|
|
20
|
+
export declare function helperCommand(name: HelperServer): {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare const KEY_PATTERN: RegExp;
|
|
25
|
+
export declare const CONTRACT_VERSION_HEADER = "x-robisie-contract-version";
|
|
26
|
+
/** Text markers delimiting what `init` wrote. `uninstall` removes only what sits between them. */
|
|
27
|
+
export declare const MARK_BEGIN = ">>> robisie (zarz\u0105dzane przez `npx robisie`, nie edytuj) >>>";
|
|
28
|
+
export declare const MARK_END = "<<< robisie <<<";
|
|
29
|
+
/** First-line marker of every role file `init` owns. */
|
|
30
|
+
export declare const FILE_MARK = "robisie:managed";
|
|
31
|
+
export declare function mcpUrl(origin: string): string;
|
|
32
|
+
export declare function wellKnownUrl(origin: string): string;
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Package identity and the server contract it speaks. */
|
|
2
2
|
export const PACKAGE_NAME = "robisie";
|
|
3
|
-
export const PACKAGE_VERSION = "0.1.
|
|
3
|
+
export const PACKAGE_VERSION = "0.1.1"; // x-release-please-version
|
|
4
4
|
/**
|
|
5
5
|
* MAJOR of the agent-API contract this package understands.
|
|
6
6
|
* PARITY: src/lib/mcp/contract-version.ts (AGENT_API_CONTRACT_VERSION) on the server.
|
|
@@ -14,8 +14,8 @@ export const KEY_ENV = "ROBISIE_KEY";
|
|
|
14
14
|
* Shipped in `helpers/` (verbatim copies of the repo's scripts/) and started as `npx -y robisie@<version> mcp <name>`.
|
|
15
15
|
*/
|
|
16
16
|
export const HELPER_SERVERS = ["client-launch-context", "ceo-client-launcher"];
|
|
17
|
-
/** Environment the helpers read: credential,
|
|
18
|
-
export const HELPER_ENV = [KEY_ENV, "ROBISIE_CARD_REF", "ROBISIE_PROJECT_CONTEXT"];
|
|
17
|
+
/** Environment the helpers read: credential, Builder card, project and installed configuration. */
|
|
18
|
+
export const HELPER_ENV = [KEY_ENV, "ROBISIE_CARD_REF", "ROBISIE_PROJECT_CONTEXT", "XDG_CONFIG_HOME", "CODEX_HOME"];
|
|
19
19
|
export function helperCommand(name) {
|
|
20
20
|
return { command: "npx", args: ["-y", `${PACKAGE_NAME}@${PACKAGE_VERSION}`, "mcp", name] };
|
|
21
21
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Everything a command touches outside its own logic — injectable, so tests run on a temp HOME. */
|
|
2
|
+
export interface Ctx {
|
|
3
|
+
home: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
platform: NodeJS.Platform;
|
|
7
|
+
fetch: typeof fetch;
|
|
8
|
+
out: (line: string) => void;
|
|
9
|
+
err: (line: string) => void;
|
|
10
|
+
/** Interactive hidden prompt; null when there is no terminal to ask on. */
|
|
11
|
+
askSecret: ((question: string) => Promise<string>) | null;
|
|
12
|
+
/** Whole stdin when it is piped (not a terminal); null otherwise. */
|
|
13
|
+
readStdin: (() => Promise<string>) | null;
|
|
14
|
+
origin: string;
|
|
15
|
+
}
|
|
16
|
+
/** A failure the client can act on: what happened + exactly one next step (both in Polish). */
|
|
17
|
+
export declare class RobisieError extends Error {
|
|
18
|
+
readonly nextStep: string;
|
|
19
|
+
constructor(message: string, nextStep: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function errorMessage(error: unknown): string;
|
|
22
|
+
/** One POSIX shell word: single-quoted, each `'` written as `'\''` (paths like `/Users/O'Connor`). */
|
|
23
|
+
export declare function shellQuote(value: string): string;
|
|
24
|
+
export declare function nodeCtx(): Ctx;
|
package/dist/context.js
CHANGED
|
@@ -29,15 +29,31 @@ function hiddenPrompt(question) {
|
|
|
29
29
|
});
|
|
30
30
|
const rl = createInterface({ input: process.stdin, output, terminal: true });
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
rl.question(question, (answer) => {
|
|
37
|
-
rl.close();
|
|
32
|
+
let settled = false;
|
|
33
|
+
let inputFailed = false;
|
|
34
|
+
const accept = (answer) => {
|
|
38
35
|
process.stdout.write("\n");
|
|
39
36
|
resolve(answer);
|
|
37
|
+
};
|
|
38
|
+
const finish = (answer) => {
|
|
39
|
+
if (settled)
|
|
40
|
+
return;
|
|
41
|
+
settled = true;
|
|
42
|
+
rl.close();
|
|
43
|
+
if (answer !== undefined && !inputFailed)
|
|
44
|
+
return accept(answer);
|
|
45
|
+
reject(new RobisieError("Przerwano.", "Uruchom ponownie `npx robisie init`."));
|
|
46
|
+
};
|
|
47
|
+
const cancel = () => finish();
|
|
48
|
+
rl.on("SIGINT", cancel);
|
|
49
|
+
rl.on("close", cancel);
|
|
50
|
+
rl.on("error", () => {
|
|
51
|
+
// setRawMode can emit EIO synchronously while close is cleaning up a lost PTY.
|
|
52
|
+
inputFailed = true;
|
|
53
|
+
// Let that close finish before cancellation attempts another cleanup.
|
|
54
|
+
queueMicrotask(cancel);
|
|
40
55
|
});
|
|
56
|
+
rl.question(question, finish);
|
|
41
57
|
muted = true;
|
|
42
58
|
});
|
|
43
59
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CardLike = {
|
|
2
|
+
seedBriefDigest?: string | null;
|
|
3
|
+
detailsReadError?: string | null;
|
|
4
|
+
id: string;
|
|
5
|
+
status?: string | null;
|
|
6
|
+
details?: Record<string, unknown> | null;
|
|
7
|
+
dependsOn?: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare const HOLD_DETAILS_KEYS: readonly ["reksio_withheld", "hold", "seed_obligation", "seed_brief"];
|
|
10
|
+
export declare const FACTORY_DEPENDENCY_KEYS: readonly ["landed", "test", "review", "maszynista", "reksio_withheld", "hold", "seed_obligation"];
|
|
11
|
+
export declare function factoryDependenciesReady(card: CardLike & {
|
|
12
|
+
depsReady?: boolean;
|
|
13
|
+
}, predecessors?: ReadonlyMap<string, CardLike>): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const HOLD_DETAILS_KEYS = ["reksio_withheld", "hold", "seed_obligation", "seed_brief"];
|
|
3
|
+
export const FACTORY_DEPENDENCY_KEYS = ["landed", "test", "review", "maszynista", "reksio_withheld", "hold", "seed_obligation"];
|
|
4
|
+
function briefSha(details, digest) {
|
|
5
|
+
if (digest !== undefined)
|
|
6
|
+
return digest;
|
|
7
|
+
const brief = details?.seed_brief;
|
|
8
|
+
return nonempty(brief) ? createHash("sha256").update(brief).digest("hex") : null;
|
|
9
|
+
}
|
|
10
|
+
function holdActive(details, digest) {
|
|
11
|
+
const hold = details?.hold;
|
|
12
|
+
if (hold === undefined || hold === null)
|
|
13
|
+
return false;
|
|
14
|
+
if (!record(hold) || hold.release !== "packet_changed")
|
|
15
|
+
return true;
|
|
16
|
+
// Zlecenie zasiewu bez potwierdzonego odbioru (pending) trzyma hold niezależnie od briefu: częściowo osadzony albo niezgodny
|
|
17
|
+
// pakiet nie wychodzi do promocji, dopóki most nie zamknie zlecenia po udanym read-backu.
|
|
18
|
+
const obligation = details?.seed_obligation;
|
|
19
|
+
if (record(obligation) && obligation.status === "pending")
|
|
20
|
+
return true;
|
|
21
|
+
const sha = hold.seed_brief_sha256;
|
|
22
|
+
if (sha != null && (typeof sha !== "string" || !/^[0-9a-f]{64}$/.test(sha)))
|
|
23
|
+
return true;
|
|
24
|
+
return (hold.seed_brief_sha256 ?? null) === briefSha(details, digest);
|
|
25
|
+
}
|
|
26
|
+
function draftHeld(details, digest) {
|
|
27
|
+
return details?.reksio_withheld === true || holdActive(details, digest);
|
|
28
|
+
}
|
|
29
|
+
export function factoryDependenciesReady(card, predecessors) {
|
|
30
|
+
if (card.detailsReadError)
|
|
31
|
+
return false;
|
|
32
|
+
if (predecessors === undefined || card.dependsOn === undefined)
|
|
33
|
+
return card.depsReady === true; // legacy callers
|
|
34
|
+
if (!Array.isArray(card.dependsOn))
|
|
35
|
+
return false;
|
|
36
|
+
return card.dependsOn.every(id => {
|
|
37
|
+
const dep = predecessors?.get(id);
|
|
38
|
+
if (!dep || dep.id !== id || dep.detailsReadError)
|
|
39
|
+
return false;
|
|
40
|
+
if (dep.status === "done")
|
|
41
|
+
return true;
|
|
42
|
+
if (dep.status !== "delivered")
|
|
43
|
+
return false;
|
|
44
|
+
const d = dep.details ?? {};
|
|
45
|
+
const land = asRecord(d.landed), test = asRecord(d.test);
|
|
46
|
+
const at = Date.parse(String(test.at)), landedAt = Date.parse(String(land.at));
|
|
47
|
+
if (!landedSha(dep) || test.sha !== land.sha || !["PASS", "PASS_NEEDS_VALIDATOR"].includes(String(test.verdict))
|
|
48
|
+
|| !timestamp(test.at) || !timestamp(land.at) || at < landedAt
|
|
49
|
+
|| at > Date.now() || draftHeld(d, dep.seedBriefDigest)
|
|
50
|
+
|| (d.review != null && !record(d.review)) || (d.maszynista != null && !record(d.maszynista)))
|
|
51
|
+
return false;
|
|
52
|
+
const machine = asRecord(d.maszynista), review = asRecord(d.review);
|
|
53
|
+
// Old failed attempts survive delivery; only a later (or undated) conflict blocks it.
|
|
54
|
+
const newer = (value) => !Number.isFinite(Date.parse(String(value))) || Date.parse(String(value)) >= landedAt;
|
|
55
|
+
if (["worker", "review", "landing", "repair", "blocked"].includes(String(machine.phase)) && newer(machine.at))
|
|
56
|
+
return false;
|
|
57
|
+
if (review.verdict === "FAIL" && newer(review.at))
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function validTestSha(value) {
|
|
63
|
+
return typeof value === "string" && /^[a-f0-9]{7,40}$/i.test(value);
|
|
64
|
+
}
|
|
65
|
+
function landedSha(card) {
|
|
66
|
+
const landed = card.details?.landed;
|
|
67
|
+
return record(landed) && validTestSha(landed.sha) ? landed.sha : null;
|
|
68
|
+
}
|
|
69
|
+
function record(value) {
|
|
70
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
function asRecord(value) {
|
|
73
|
+
return record(value) ? value : {};
|
|
74
|
+
}
|
|
75
|
+
function nonempty(value) {
|
|
76
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
77
|
+
}
|
|
78
|
+
function timestamp(value) {
|
|
79
|
+
return nonempty(value) && Number.isFinite(Date.parse(value));
|
|
80
|
+
}
|