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.
Files changed (87) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +76 -12
  3. package/dist/active-recovery.d.ts +58 -0
  4. package/dist/active-recovery.js +143 -0
  5. package/dist/agents.d.ts +34 -0
  6. package/dist/agents.js +77 -16
  7. package/dist/attempt-identity.d.ts +21 -0
  8. package/dist/attempt-identity.js +39 -0
  9. package/dist/ci-failure-result.d.ts +23 -0
  10. package/dist/ci-failure-result.js +90 -0
  11. package/dist/claude-headless.d.ts +7 -0
  12. package/dist/claude-headless.js +28 -0
  13. package/dist/cli.d.ts +17 -0
  14. package/dist/cli.js +55 -15
  15. package/dist/commands.d.ts +18 -0
  16. package/dist/commands.js +190 -85
  17. package/dist/constants.d.ts +32 -0
  18. package/dist/constants.js +3 -3
  19. package/dist/context.d.ts +24 -0
  20. package/dist/context.js +22 -6
  21. package/dist/dependency-readiness.d.ts +13 -0
  22. package/dist/dependency-readiness.js +80 -0
  23. package/dist/dispatch-scope.d.ts +18 -0
  24. package/dist/dispatch-scope.js +46 -0
  25. package/dist/done-evidence.d.ts +31 -0
  26. package/dist/done-evidence.js +216 -0
  27. package/dist/engine-config.d.ts +65 -0
  28. package/dist/engine-config.js +139 -0
  29. package/dist/fsops.d.ts +87 -0
  30. package/dist/fsops.js +117 -24
  31. package/dist/helpers.d.ts +15 -0
  32. package/dist/helpers.js +262 -45
  33. package/dist/intent-rules/admission.d.ts +10 -0
  34. package/dist/intent-rules/admission.js +35 -0
  35. package/dist/intent-rules/done-when-roles.d.ts +21 -0
  36. package/dist/intent-rules/done-when-roles.js +101 -0
  37. package/dist/intent-rules/hooks.d.ts +29 -0
  38. package/dist/intent-rules/hooks.js +23 -0
  39. package/dist/k3-codex-role.d.ts +20 -0
  40. package/dist/k3-codex-role.js +68 -0
  41. package/dist/manifest.d.ts +41 -0
  42. package/dist/manifest.js +59 -14
  43. package/dist/mcp.d.ts +18 -0
  44. package/dist/mcp.js +89 -39
  45. package/dist/opencode-result.d.ts +19 -0
  46. package/dist/opencode-result.js +136 -0
  47. package/dist/opencode-review-guard.d.ts +1 -0
  48. package/dist/opencode-review-guard.js +19 -0
  49. package/dist/opencode-session-export.d.ts +5 -0
  50. package/dist/opencode-session-export.js +28 -0
  51. package/dist/outcome-moves.d.ts +15 -0
  52. package/dist/outcome-moves.js +39 -0
  53. package/dist/planer-client.d.ts +214 -0
  54. package/dist/planer-client.js +351 -0
  55. package/dist/restart-recovery.d.ts +47 -0
  56. package/dist/restart-recovery.js +70 -0
  57. package/dist/review-source-packet.d.ts +17 -0
  58. package/dist/review-source-packet.js +44 -0
  59. package/dist/role-registry.d.ts +19 -0
  60. package/dist/role-registry.js +83 -0
  61. package/dist/role-results.d.ts +15 -0
  62. package/dist/role-results.js +42 -0
  63. package/dist/roles.d.ts +30 -0
  64. package/dist/roles.js +20 -16
  65. package/dist/seed-validation.d.ts +28 -0
  66. package/dist/seed-validation.js +294 -0
  67. package/dist/selection-result.d.ts +44 -0
  68. package/dist/selection-result.js +144 -0
  69. package/dist/sparse-policy.d.ts +8 -0
  70. package/dist/sparse-policy.js +49 -0
  71. package/dist/tester-identity.d.ts +11 -0
  72. package/dist/tester-identity.js +53 -0
  73. package/dist/worker-env.d.ts +5 -0
  74. package/dist/worker-env.js +34 -0
  75. package/docs/ENGINE-API.md +78 -0
  76. package/docs/ENGINE-CONFIG.md +115 -0
  77. package/docs/README.en.md +24 -15
  78. package/docs/README.md +26 -15
  79. package/docs/RELEASE-0.1.0.md +81 -8
  80. package/docs/RELEASE-0.1.1.md +110 -0
  81. package/helpers/ceo-client-launcher-mcp.mjs +36 -8
  82. package/helpers/client-spawn-session.sh +181 -0
  83. package/helpers/client-spawn.sh +825 -0
  84. package/package.json +9 -4
  85. package/roles/ceo-client.md +3 -2
  86. package/roles/manager-builder-client.md +18 -4
  87. package/roles/quality-officer-client.md +20 -6
@@ -0,0 +1,42 @@
1
+ // GPT-6 Sol (Z53, 2026-09-22) pogrubia linie STATUS/WERDYKT; dopasowujemy na kopii bez * i backtickow, raport zostaje oryginalny.
2
+ export function plainMarkers(text) {
3
+ return text.replace(/[*`]/g, "");
4
+ }
5
+ /** Wyciąga sekcję RAPORT MASZYNISTY z wyjścia workera. Brak sekcji = UNKNOWN (nigdy nie zgaduj sukcesu). */
6
+ export function parseWorkerOutput(output) {
7
+ const idx = output.lastIndexOf("### RAPORT MASZYNISTY");
8
+ if (idx < 0)
9
+ return { status: "UNKNOWN", report: output.slice(-1500) };
10
+ const report = output.slice(idx);
11
+ const plain = plainMarkers(report);
12
+ // Szablon z briefu ("STATUS: WORKER_FINISHED | ALREADY_DONE | ...") NIE jest raportem — zmierzone 2026-09-10
13
+ // (karta 3999702e): worker zakonczyl PYTANIEM bez raportu, a wynik helpera zawieral tresc briefu ->
14
+ // Maszynista odczytal szablon jako WORKER_FINISHED i przeniosl karte do in_review bez zadnego diffu.
15
+ if (/STATUS:\s*WORKER_FINISHED\s*\|/.test(plain) || plain.includes("(WORKER_FINISHED = "))
16
+ return { status: "UNKNOWN", report: report.slice(0, 1500) };
17
+ if (/^[-*]?\s*STATUS:\s*ACTIONABLE_BLOCKER\b(?!\s*\|)/m.test(plain))
18
+ return { status: "ACTIONABLE_BLOCKER", report };
19
+ const m = plain.match(/STATUS:\s*(WORKER_FINISHED|DONE|ALREADY_DONE|FAILED|NEEDS_HUMAN)\b(?!\s*\|)/);
20
+ const raw = m?.[1];
21
+ const status = raw === "DONE" ? "WORKER_FINISHED" : (raw ?? "UNKNOWN");
22
+ return { status, report };
23
+ }
24
+ /** Ostatnie wystąpienie wygrywa, także gdy jest niepoprawne. Szablon nie jest PASS. */
25
+ export function parseReviewVerdict(text) {
26
+ // Szablon z prompta ("VERDICT: PASS albo VERDICT: FAIL - <powody>") nie jest werdyktem - usun go przed czytaniem.
27
+ const cleaned = text.replace(/VERDICT:\s*PASS\s+albo\s+VERDICT:\s*FAIL[^\n]*/gi, "");
28
+ // Zmierzone 2026-09-10 (karta 973a2a79, runda 2): codex review pisze werdykt W SRODKU akapitu
29
+ // ("...delivered cards. VERDICT: FAIL - 1. Tester reads..."), nie na poczatku linii -> ostatnie wystapienie
30
+ // PASS/FAIL po "VERDICT:" wygrywa, niezaleznie od pozycji w linii.
31
+ const idx = cleaned.lastIndexOf("VERDICT:");
32
+ if (idx >= 0) {
33
+ const last = cleaned.slice(idx).match(/^VERDICT:\s*([A-Z_]+)/)?.[1]; // OSTATNI werdykt rozstrzyga; PASS/FAIL, cokolwiek innego = niepoprawny format
34
+ return last === "PASS" || last === "FAIL" ? last : "UNAVAILABLE";
35
+ }
36
+ // Bez linii VERDICT: lista znalezisk "[P1] ..."/"[P2] ..." = FAIL; jawne "brak uwag" = PASS; reszta = UNAVAILABLE.
37
+ if (/\[P[0-2]\]/.test(cleaned))
38
+ return "FAIL";
39
+ if (/no (issues|findings|problems) found|brak (uwag|zastrzezen|zastrzeżeń|znalezisk)|nie znaleziono (problem|blęd|błęd)/i.test(cleaned))
40
+ return "PASS";
41
+ return "UNAVAILABLE";
42
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The three client roles, copied verbatim from the repo's `.claude/agents/*-client.md`
3
+ * (scripts/sync-assets.mjs; drift test in test/assets-drift.test.ts). Bodies are never rewritten —
4
+ * only the frontmatter is adapted to each agent's format, plus one adapter section in front.
5
+ */
6
+ export declare const ROLE_FILES: readonly ["ceo-client.md", "manager-builder-client.md", "quality-officer-client.md"];
7
+ export type RoleKind = "ceo" | "builder" | "quality";
8
+ export interface Role {
9
+ file: (typeof ROLE_FILES)[number];
10
+ name: string;
11
+ kind: RoleKind;
12
+ label: string;
13
+ description: string;
14
+ frontmatter: string;
15
+ body: string;
16
+ }
17
+ export declare function rolesDir(): string;
18
+ export declare function parseRole(file: Role["file"], text: string): Role;
19
+ export declare function loadRoles(dir?: string): Role[];
20
+ export declare const MANAGED_LINE = "<!-- robisie:managed 0.1.1 \u2014 plik zarz\u0105dzany przez `npx robisie`; `npx robisie uninstall` go usunie -->";
21
+ export type AgentFlavor = "claude" | "codex" | "opencode";
22
+ /** How the operator starts the Builder for one card in its own session (the card id is not a secret). */
23
+ export declare function builderStartCommand(flavor: AgentFlavor, codexRolePath?: string): string;
24
+ /** The one section robisie adds: how the unchanged contract maps onto this agent. */
25
+ export declare function adapterSection(flavor: AgentFlavor, codexRolePath?: string): string;
26
+ /** Claude agent frontmatter: the hosted `robisie` entry stays; each stdio helper (repo-only script) runs from this package. */
27
+ export declare function claudeFrontmatter(frontmatter: string): string;
28
+ export declare function renderClaudeAgent(role: Role): string;
29
+ export declare function renderOpenCodeAgent(role: Role): string;
30
+ export declare function renderCodexRole(role: Role, builderRolePath?: string): string;
package/dist/roles.js CHANGED
@@ -14,26 +14,27 @@ export function rolesDir() {
14
14
  // src/roles.ts and dist/roles.js both sit one level below the package root.
15
15
  return join(dirname(fileURLToPath(import.meta.url)), "..", "roles");
16
16
  }
17
- function kindOf(name) {
18
- if (name.startsWith("ceo"))
19
- return "ceo";
20
- if (name.startsWith("quality"))
21
- return "quality";
22
- return "builder";
23
- }
17
+ const KINDS = {
18
+ "ceo-client.md": "ceo",
19
+ "manager-builder-client.md": "builder",
20
+ "quality-officer-client.md": "quality",
21
+ };
24
22
  export function parseRole(file, text) {
25
- const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
23
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(text);
26
24
  if (!match)
27
- throw new RobisieError(`Rola ${file} nie ma nagłówka (frontmatter).`, "Przeinstaluj otrzymany plik .tgz w katalogu instalacji: `npm i ./robisie-0.1.0.tgz` (użyj nazwy otrzymanego pliku), potem lokalne `npx robisie init` i `npx robisie doctor`.");
28
- const frontmatter = match[1] ?? "";
25
+ throw new RobisieError(`Rola ${file} nie ma nagłówka (frontmatter).`, "W katalogu projektu wykonaj `npm uninstall robisie`, potem `npm i robisie@<wersja-zgodna-z-kontraktem-serwera>` (zastąp placeholder numerem zgodnego wydania), potem lokalne `npx robisie init` i `npx robisie doctor`.");
26
+ const frontmatter = (match[1] ?? "").replace(/\r\n/g, "\n");
29
27
  const field = (key) => {
30
28
  const line = frontmatter.split("\n").find((l) => l.startsWith(`${key}:`));
31
29
  return line ? line.slice(key.length + 1).trim().replace(/^"(.*)"$/, "$1") : "";
32
30
  };
33
31
  const name = field("name");
34
32
  if (!name)
35
- throw new RobisieError(`Rola ${file} nie ma nazwy.`, "Przeinstaluj otrzymany plik .tgz w katalogu instalacji: `npm i ./robisie-0.1.0.tgz` (użyj nazwy otrzymanego pliku), potem lokalne `npx robisie init` i `npx robisie doctor`.");
36
- const kind = kindOf(name);
33
+ throw new RobisieError(`Rola ${file} nie ma nazwy.`, "W katalogu projektu wykonaj `npm uninstall robisie`, potem `npm i robisie@<wersja-zgodna-z-kontraktem-serwera>` (zastąp placeholder numerem zgodnego wydania), potem lokalne `npx robisie init` i `npx robisie doctor`.");
34
+ if (frontmatter.split("\n").filter((line) => /^name[ \t]*:/.test(line)).length !== 1 || name !== file.slice(0, -3)) {
35
+ throw new RobisieError(`Rola ${file} ma niezgodną lub niejednoznaczną nazwę.`, "W katalogu projektu wykonaj `npm uninstall robisie`, potem `npm i robisie@<wersja-zgodna-z-kontraktem-serwera>` (zastąp placeholder numerem zgodnego wydania), potem lokalne `npx robisie init` i `npx robisie doctor`.");
36
+ }
37
+ const kind = KINDS[file];
37
38
  return { file, name, kind, label: LABELS[kind], description: field("description"), frontmatter, body: match[2] ?? "" };
38
39
  }
39
40
  export function loadRoles(dir = rolesDir()) {
@@ -43,7 +44,7 @@ export function loadRoles(dir = rolesDir()) {
43
44
  text = readFileSync(join(dir, file), "utf8");
44
45
  }
45
46
  catch (error) {
46
- throw new RobisieError(`Brak pliku roli ${file} w paczce: ${errorMessage(error)}`, "Przeinstaluj otrzymany plik .tgz w katalogu instalacji: `npm i ./robisie-0.1.0.tgz` (użyj nazwy otrzymanego pliku), potem lokalne `npx robisie init` i `npx robisie doctor`.");
47
+ throw new RobisieError(`Brak pliku roli ${file} w paczce: ${errorMessage(error)}`, "W katalogu projektu wykonaj `npm uninstall robisie`, potem `npm i robisie@<wersja-zgodna-z-kontraktem-serwera>` (zastąp placeholder numerem zgodnego wydania), potem lokalne `npx robisie init` i `npx robisie doctor`.");
47
48
  }
48
49
  return parseRole(file, text);
49
50
  });
@@ -72,9 +73,11 @@ export function adapterSection(flavor, codexRolePath) {
72
73
  "",
73
74
  `- Board tools come from the MCP server \`robisie\`; the local helper servers ${HELPER_SERVERS.map((name) => `\`${name}\``).join(" and ")} are installed too (started as \`npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} mcp <server>\`). ${TOOL_NAMING[flavor]}`,
74
75
  "- Run every role from the customer project's directory: npx must find the installed package in cwd. ROBISIE_PROJECT_CONTEXT does not change cwd; the helpers select that context only after startup. Doctor probes the helper file via node, not the agent's npx lookup. In the agent session, list tools from both client-launch-context and ceo-client-launcher before work.",
75
- "- The Builder reads its card from `ROBISIE_CARD_REF`, which the operator sets when starting the Builder's session.",
76
- `- \`spawn_builder\` needs a project launcher that a customer project normally does not have, so it fails closed and the card stays \`ready\`. Then give the operator this one command to run in a new terminal in the project directory, with the real card id: \`${builderStartCommand(flavor, codexRolePath)}\`. After the operator says it is running, do the one post-launch \`get_task\` read the contract requires.`,
76
+ "- The Builder reads its card from `ROBISIE_CARD_REF`, which the packaged launcher sets when starting the Builder's session.",
77
+ "- Use `spawn_builder` to launch the Builder with the launcher shipped in this package; no project-local scripts are required. It selects the Builder provider saved by `npx robisie init` (claude, codex or opencode), keeps the project cwd/environment and confirms the card is `in_progress` with an assignee before reporting success. This does not establish support for mixed-provider review; keep the doctor checks and independent-review requirements.",
78
+ `- If \`spawn_builder\` fails, report its error and resolve the cause before retrying. Only if the operator explicitly chooses manual startup, give this command for a new terminal in the project directory, with the real card id: \`${builderStartCommand(flavor, codexRolePath)}\`. After the operator says it is running, do the one post-launch \`get_task\` read the contract requires.`,
77
79
  "- Every role runs in its own session. Never act as two roles in one session.",
80
+ ...(flavor === "opencode" ? ["- OpenCode: brak wspieranego protokołu recenzji. Lokalny plugin `plugins/robisie-review-guard.js` odmawia wywołań `robisie_create_task` i `robisie_update_task` ze statusem `delivered` przed wysłaniem do MCP; taka próba nie zmienia karty. Wynik `task` z accept/VERIFIED, nazwa quality-officer-client, opis Agent ani JSON receiptu nie odblokowują przejścia. Hash receiptu nie dowodzi niezależnej sesji recenzenta."] : []),
78
81
  "- The credential comes from the `ROBISIE_KEY` environment variable. Never ask for it in chat and never write it into a card, a file or a command line.",
79
82
  "",
80
83
  ].join("\n");
@@ -112,7 +115,8 @@ export function renderClaudeAgent(role) {
112
115
  export function renderOpenCodeAgent(role) {
113
116
  // The Builder runs in its own session (`opencode run --agent …`); only the Quality Officer is a subagent.
114
117
  const mode = role.kind === "quality" ? "subagent" : "primary";
115
- return `---\ndescription: ${JSON.stringify(role.description)}\nmode: ${mode}\n---\n${MANAGED_LINE}\n\n${adapterSection("opencode")}\n${role.body}`;
118
+ const permission = role.kind === "builder" ? "" : `permission:\n edit: deny\n bash: deny\n${role.kind === "quality" ? " task: deny\n" : ""}`;
119
+ return `---\ndescription: ${JSON.stringify(role.description)}\nmode: ${mode}\n${permission}---\n${MANAGED_LINE}\n\n${adapterSection("opencode")}\n${role.body}`;
116
120
  }
117
121
  export function renderCodexRole(role, builderRolePath) {
118
122
  return `${MANAGED_LINE}\n\n# robisie role: ${role.name}\n\n> ${role.description}\n\n${adapterSection("codex", builderRolePath)}\n${role.body}`;
@@ -0,0 +1,28 @@
1
+ export declare const SEED_FILES: {
2
+ readonly scope: "20-scope.md";
3
+ readonly spec: "25-spec.md";
4
+ readonly brief: "30-brief.md";
5
+ };
6
+ export declare const VALIDATE_USAGE = "u\u017Cycie: robisie-intent validate-seed <katalog-pakietu> [--repo katalog-repo] [--config robisie.config.json] [--efekt-dla <dope\u0142niacz>]";
7
+ /** Domyślny odbiorca efektu w SCOPE (dopełniacz), gdy instancja nie poda własnego (roles.owner.genitive). */
8
+ export declare const DEFAULT_EFFECT_FOR = "w\u0142a\u015Bciciela";
9
+ /** Marker SCOPE „Efekt dla <odbiorca>” — to samo źródło dla szablonu Siewcy ({{WLASCICIEL}}) i walidatora. */
10
+ export declare function effectMarker(effectFor?: string): string;
11
+ export interface SeedValidation {
12
+ /** 0 = komplet, 2 = luka (jak w oryginale). */
13
+ exitCode: 0 | 2;
14
+ gaps: string[];
15
+ warnings: string[];
16
+ stdout: string;
17
+ stderr: string;
18
+ }
19
+ export interface SeedValidateOptions {
20
+ /** Repo, którego dotyczy write-set (w oryginale: repo zawierające skrypt). Domyślnie bieżący katalog. */
21
+ repo?: string;
22
+ /** Odbiorca efektu w markerze SCOPE „Efekt dla …” (dopełniacz; roles.owner.genitive). Oryginał miał go wbitego na sztywno. */
23
+ effectFor?: string;
24
+ }
25
+ /** Waliduje trzy artefakty siania (kolejność kanoniczna SCOPE → SPEC → BRIEF). */
26
+ export declare function validateSeedFiles(scope: string, spec: string, brief: string, options?: SeedValidateOptions): SeedValidation;
27
+ /** Waliduje katalog pakietu karty: 20-scope.md, 25-spec.md, 30-brief.md. */
28
+ export declare function validateSeedPackage(directory: string, options?: SeedValidateOptions): SeedValidation;
@@ -0,0 +1,294 @@
1
+ // Walidator pakietu karty siewcy — odpowiednik docs/zrodla/walidator/seed-card-validate.sh w Node.
2
+ // Te same kontrole, progi i komunikaty; bez bash/awk/grep. Jedyna różnica: odbiorca w markerze „Efekt dla …”
3
+ // pochodzi z konfiguracji (effectFor), a nie jest wbity na sztywno. Git jest wołany bezpośrednio (execFileSync, bez powłoki)
4
+ // tylko w kontroli §5, gdy SCOPE ma sekcję **Write-set.
5
+ //
6
+ // Świadoma granica (jak T1): sprawdza OBECNOŚĆ + FORMAT (nasze markery), NIE głębię/mądrość.
7
+ import { execFileSync } from 'node:child_process';
8
+ import { accessSync, constants, readFileSync, statSync } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
+ export const SEED_FILES = { scope: '20-scope.md', spec: '25-spec.md', brief: '30-brief.md' };
11
+ export const VALIDATE_USAGE = 'użycie: robisie-intent validate-seed <katalog-pakietu> [--repo katalog-repo] [--config robisie.config.json] [--efekt-dla <dopełniacz>]';
12
+ /** Domyślny odbiorca efektu w SCOPE (dopełniacz), gdy instancja nie poda własnego (roles.owner.genitive). */
13
+ export const DEFAULT_EFFECT_FOR = 'właściciela';
14
+ /** Marker SCOPE „Efekt dla <odbiorca>” — to samo źródło dla szablonu Siewcy ({{WLASCICIEL}}) i walidatora. */
15
+ export function effectMarker(effectFor = DEFAULT_EFFECT_FOR) {
16
+ return `Efekt dla ${effectFor}`;
17
+ }
18
+ function readable(file) {
19
+ try {
20
+ if (!statSync(file).isFile())
21
+ return false;
22
+ accessSync(file, constants.R_OK);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Linie pliku tak, jak widzi je grep/awk (bez pustego segmentu po końcowym \n). */
30
+ function fileLines(file) {
31
+ const text = readFileSync(file, 'utf8');
32
+ const lines = text.split('\n');
33
+ if (lines.length && lines[lines.length - 1] === '')
34
+ lines.pop();
35
+ return lines;
36
+ }
37
+ function hasFixed(file, needle) {
38
+ return readable(file) && fileLines(file).some(line => line.includes(needle));
39
+ }
40
+ function hasRegex(file, pattern) {
41
+ return readable(file) && fileLines(file).some(line => pattern.test(line));
42
+ }
43
+ // [[:space:]] w lokalizacji C (mawk/grep bez UTF-8): spacja, \t, \n, \v, \f, \r.
44
+ const SPACE = ' \\t\\n\\v\\f\\r';
45
+ const BLANK_LINE = new RegExp(`^[${SPACE}]*$`);
46
+ function git(root, args) {
47
+ try {
48
+ const stdout = execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: false });
49
+ return { ok: true, stdout };
50
+ }
51
+ catch {
52
+ return { ok: false, stdout: '' };
53
+ }
54
+ }
55
+ function gitRoot(repo) {
56
+ const result = git(repo, ['rev-parse', '--show-toplevel']);
57
+ return result.ok ? result.stdout.replace(/\n+$/, '') : '';
58
+ }
59
+ /** Kolejność `sort -u` w lokalizacji C (po jednostkach kodu). */
60
+ function sortUnique(values) {
61
+ return [...new Set(values)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
62
+ }
63
+ function writeSetPaths(scopeLines) {
64
+ const paths = [];
65
+ let inside = false;
66
+ for (const line of scopeLines) {
67
+ if (/^\*\*Write-set/.test(line)) {
68
+ inside = true;
69
+ continue;
70
+ }
71
+ if (inside && (/^\*\*/.test(line) || /^## /.test(line)))
72
+ break;
73
+ if (inside && /^- /.test(line)) {
74
+ for (const match of line.matchAll(/`[^`]+`/g)) {
75
+ const token = match[0].slice(1, -1);
76
+ if (!new RegExp(`[${SPACE}]`).test(token) && /[/.]/.test(token))
77
+ paths.push(token);
78
+ }
79
+ }
80
+ }
81
+ return paths;
82
+ }
83
+ function outText(scopeLines) {
84
+ const out = [];
85
+ let inside = false;
86
+ for (const line of scopeLines) {
87
+ if (/^\*\*Świadomie POZA/.test(line)) {
88
+ inside = true;
89
+ continue;
90
+ }
91
+ if (inside && /^## /.test(line))
92
+ break;
93
+ if (inside)
94
+ out.push(line);
95
+ }
96
+ return out.join('\n');
97
+ }
98
+ function envTokens(lines) {
99
+ const tokens = [];
100
+ for (const line of lines) {
101
+ const envline = /(^|[^A-Za-z0-9_])\.?env([^A-Za-z0-9_]|$)|zmienn/.test(line.toLowerCase());
102
+ for (const match of line.matchAll(/[A-Z][A-Z0-9]*(_[A-Z0-9]+)+/g)) {
103
+ const t = match[0];
104
+ if (envline || line.includes('process.env.' + t) || line.includes(t + '='))
105
+ tokens.push(t);
106
+ }
107
+ }
108
+ return sortUnique(tokens);
109
+ }
110
+ function runtimeNames(lines, root) {
111
+ const kept = [];
112
+ const token = new RegExp(`\`[^\`]+\`|[^${SPACE}\`]+`, 'g');
113
+ const externalPath = /^\/([^/]+\/)*[A-Z][A-Z0-9_-]*\.(md|json|jsonl|log|txt)$/;
114
+ const notExternal = new RegExp(`\\/\\.\\.?\\/|[${SPACE}]\\/|\\/[${SPACE}]`);
115
+ for (const line of lines) {
116
+ const raws = [];
117
+ let last = 0;
118
+ for (const match of line.matchAll(token)) {
119
+ const raw = match[0];
120
+ const prefix = line.slice(last, match.index);
121
+ let path = raw;
122
+ if (/^`.*`$/.test(path))
123
+ path = path.slice(1, -1);
124
+ else
125
+ path = path.replace(/[.,;:)]*$/, '');
126
+ const external = !/`$/.test(prefix) && externalPath.test(path) && !notExternal.test(path) && !path.startsWith(root + '/');
127
+ if (!external)
128
+ raws.push(raw);
129
+ last = match.index + raw.length;
130
+ }
131
+ const joined = raws.map(raw => raw + ' ').join('');
132
+ // Alternatywa jsonl przed json: grep -o wybiera najdłuższe dopasowanie, JS — pierwsze.
133
+ for (const match of joined.matchAll(/`maszynista\/[A-Za-z0-9._-]+`|[A-Z][A-Z0-9_-]*\.(md|jsonl|json|log|txt)|`\.[a-z][a-z0-9-]+`/g)) {
134
+ kept.push(match[0].split('`').join(''));
135
+ }
136
+ }
137
+ return sortUnique(kept);
138
+ }
139
+ function doneWhenNumbers(scopeLines) {
140
+ const numbers = [];
141
+ let inside = false;
142
+ for (const line of scopeLines) {
143
+ if (new RegExp(`^##[${SPACE}]*done.?when`).test(line.toLowerCase())) {
144
+ inside = true;
145
+ continue;
146
+ }
147
+ if (inside && /^## /.test(line))
148
+ break;
149
+ const match = inside ? /^[0-9]+\./.exec(line) : null;
150
+ if (match)
151
+ numbers.push(match[0].slice(0, -1));
152
+ }
153
+ return numbers;
154
+ }
155
+ function mapNumbers(specLines) {
156
+ const numbers = [];
157
+ let inside = false;
158
+ const row = new RegExp(`^\\|[${SPACE}]*[0-9]+[${SPACE}]*\\|`);
159
+ const nonBlank = new RegExp(`[^${SPACE}]`);
160
+ for (const line of specLines) {
161
+ if (/[Mm]apa kryterium/.test(line))
162
+ inside = true;
163
+ if (!inside || !row.test(line))
164
+ continue;
165
+ const cols = line.split('|');
166
+ // awk: cols[3..5] = kolumny kryterium, rozwiązanie, sprawdzenie (JS: indeksy 2..4).
167
+ if ([2, 3, 4].some(i => !nonBlank.test(cols[i] ?? '')))
168
+ continue;
169
+ numbers.push(line.replace(new RegExp(`^\\|[${SPACE}]*`), '').replace(new RegExp(`[${SPACE}]*\\|[\\s\\S]*`), ''));
170
+ }
171
+ return numbers;
172
+ }
173
+ /** Waliduje trzy artefakty siania (kolejność kanoniczna SCOPE → SPEC → BRIEF). */
174
+ export function validateSeedFiles(scope, spec, brief, options = {}) {
175
+ const gaps = [];
176
+ const warnings = [];
177
+ // ── 1. Istnienie trzech artefaktów (czytelny plik; katalog/nieczytelny = jawna luka) ──
178
+ if (!readable(scope))
179
+ gaps.push(`SCOPE nie istnieje lub nie jest czytelnym plikiem: ${scope}`);
180
+ if (!readable(brief))
181
+ gaps.push(`BRIEF nie istnieje lub nie jest czytelnym plikiem: ${brief}`);
182
+ if (!readable(spec))
183
+ gaps.push(`SPEC nie istnieje lub nie jest czytelnym plikiem: ${spec}`);
184
+ // ── 2. BRIEF — kanon MB ──
185
+ if (readable(brief)) {
186
+ if (!hasFixed(brief, 'scopeRef:'))
187
+ gaps.push("BRIEF: brak 'scopeRef:' (brama T1 rozwija scopeRef → SCOPE)");
188
+ if (!hasFixed(brief, 'designRef:'))
189
+ gaps.push("BRIEF: brak 'designRef:' (brama T1 rozwija designRef → SPEC)");
190
+ const markers = ['specialist:', 'acceptance_criteria_wiring:', '## Skill (REQUIRED)', '## Done When', 'Example Mapping'];
191
+ const miss = markers.filter(marker => !hasFixed(brief, marker));
192
+ const hits = markers.length - miss.length;
193
+ if (hits < 3)
194
+ gaps.push(`BRIEF: za mało markerów kanonu MB (${hits}/5, wymagane ≥3) — brakuje: ${miss.join(' ')}`);
195
+ }
196
+ // ── 3. SCOPE — Wzór scope (≥4 z 6) ──
197
+ if (readable(scope)) {
198
+ const checks = [
199
+ ['Cel', hasFixed(scope, 'Cel')],
200
+ [effectMarker(options.effectFor), hasFixed(scope, effectMarker(options.effectFor))],
201
+ // „Granice + lane:” liczone jako jeden marker; 'lane[: =]' by NIE łapać „Planer”.
202
+ ['Granice/lane', hasRegex(scope, new RegExp(`lane[${SPACE}]*[:=]`, 'i')) || hasFixed(scope, 'Granice')],
203
+ ['Done-When', hasRegex(scope, /Done.?When/i)],
204
+ ['specialist', hasRegex(scope, /specialist/i)],
205
+ // servesGoal = SŁOWO 'servesGoal', nie samo '#N'.
206
+ ['servesGoal', hasFixed(scope, 'servesGoal')],
207
+ ];
208
+ const miss = checks.filter(([, hit]) => !hit).map(([name]) => name);
209
+ const hits = checks.length - miss.length;
210
+ if (hits < 4)
211
+ gaps.push(`SCOPE: za mało markerów Wzoru scope (${hits}/6, wymagane ≥4) — brakuje: ${miss.join(' ')}`);
212
+ }
213
+ // ── 4. SPEC — istnienie + niepustość (≥15 linii treści) ──
214
+ if (readable(spec)) {
215
+ const specLines = fileLines(spec).filter(line => !BLANK_LINE.test(line)).length;
216
+ if (specLines < 15)
217
+ gaps.push(`SPEC: za krótki (${specLines} linii treści, wymagane ≥15) — design rationale, nie zaślepka`);
218
+ }
219
+ // ── 5. Spójność write-setu z treścią SPEC/BRIEF; to NIE marker T1. ──
220
+ if (readable(scope) && readable(spec) && readable(brief)) {
221
+ const scopeLines = fileLines(scope);
222
+ if (scopeLines.some(line => /^\*\*Write-set/.test(line))) {
223
+ const root = gitRoot(resolve(options.repo ?? '.'));
224
+ if (!root || !readable(join(root, '.env.example')) || !git(root, ['ls-files']).ok) {
225
+ gaps.push('write-set: nie mogę odczytać repo lub .env.example — sprawdź git i repo');
226
+ }
227
+ else {
228
+ const ws = writeSetPaths(scopeLines);
229
+ const inWs = (path) => ws.includes(path);
230
+ const out = outText(scopeLines);
231
+ if (!ws.length)
232
+ gaps.push('write-set: sekcja Write-set bez ścieżek plikowych — wpisz ścieżki w backtickach');
233
+ const contentLines = [...fileLines(spec), ...fileLines(brief)];
234
+ const envExample = fileLines(join(root, '.env.example'));
235
+ for (const token of envTokens(contentLines)) {
236
+ const inExample = envExample.some(line => new RegExp(`^#? *${token}(=|$)`).test(line));
237
+ if (!inWs('.env.example') && !inExample
238
+ && !git(root, ['grep', '-qF', '-e', token, '--', '.', ':!.env.example', ':!docs', ':!*.md']).ok) {
239
+ gaps.push(`write-set: nowa zmienna env ${token} nie ma wpisu w .env.example — dopisz .env.example do write-setu`);
240
+ }
241
+ }
242
+ for (const name of runtimeNames(contentLines, root)) {
243
+ const [path, bare] = name.startsWith('maszynista/') ? [name, ''] : [`maszynista/${name}`, name];
244
+ if (inWs(path))
245
+ continue;
246
+ if (git(root, ['ls-files', '--error-unmatch', '--', path]).ok)
247
+ continue;
248
+ if (git(root, ['check-ignore', '-q', '--', path]).ok)
249
+ continue;
250
+ if (bare && git(root, ['ls-files', '--', `*/${bare}`, bare]).stdout !== '')
251
+ continue;
252
+ if (!inWs('.gitignore'))
253
+ gaps.push(`write-set: nowy plik runtime ${path} nie jest ignorowany przez git — dopisz .gitignore do write-setu`);
254
+ }
255
+ if (contentLines.some(line => /`[^`]*(State|parseState)[^`]*`/.test(line)) && !out.includes('parseState') && !inWs('maszynista/lib.ts')) {
256
+ gaps.push('write-set: stan taktu (State/parseState) zmieniany bez maszynista/lib.ts — parser gubi nowe pola; dopisz maszynista/lib.ts albo wymień parseState w OUT');
257
+ }
258
+ }
259
+ }
260
+ }
261
+ // ── 6. Mapa kryterium → rozwiązanie → sprawdzenie ──
262
+ if (readable(scope) && readable(spec)) {
263
+ const dw = doneWhenNumbers(fileLines(scope));
264
+ if (dw.length) {
265
+ const map = mapNumbers(fileLines(spec));
266
+ const missing = dw.filter(n => !map.includes(n));
267
+ if (missing.length) {
268
+ const list = missing.join(',');
269
+ if (hasFixed(scope, 'reksio/propozycja')) {
270
+ gaps.push(`mapa: brak wiersza mapy kryterium→rozwiązanie→sprawdzenie dla done_when #${list} (SPEC.md, sekcja 'Mapa kryterium')`);
271
+ }
272
+ else {
273
+ warnings.push(`⚠️ mapa: brak wiersza mapy dla done_when #${list} (SCOPE bez markera reksio/propozycja — ostrzeżenie, nie blokuje)`);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ // ── Werdykt ──
279
+ const warningText = warnings.map(w => w + '\n').join('');
280
+ if (gaps.length) {
281
+ const stderr = warningText + [
282
+ '🐾 STOP — kroku 4 (sekretarz) NIE odpalać: komplet artefaktów niespełniony.',
283
+ `Braki (${gaps.length}):`,
284
+ ...gaps.map(g => ` ✗ ${g}`),
285
+ 'Uzupełnij brakujące artefakty/markery (kroki 2-3), potem ponów walidator.',
286
+ ].map(line => line + '\n').join('');
287
+ return { exitCode: 2, gaps, warnings, stdout: '', stderr };
288
+ }
289
+ return { exitCode: 0, gaps, warnings, stdout: '✓ komplet 3 artefaktów + markery NASZYCH szablonów — gotowe na sekretarza (krok 4).\n', stderr: warningText };
290
+ }
291
+ /** Waliduje katalog pakietu karty: 20-scope.md, 25-spec.md, 30-brief.md. */
292
+ export function validateSeedPackage(directory, options = {}) {
293
+ return validateSeedFiles(join(directory, SEED_FILES.scope), join(directory, SEED_FILES.spec), join(directory, SEED_FILES.brief), options);
294
+ }
@@ -0,0 +1,44 @@
1
+ export type Goal = {
2
+ canon: string;
3
+ };
4
+ export type CardQualification = {
5
+ effort: "low" | "medium" | "high" | null;
6
+ trackName: string | null;
7
+ reasons: string[];
8
+ };
9
+ export type QualifyCard = (card: {
10
+ id: string;
11
+ title: string;
12
+ details: {
13
+ scope: string[];
14
+ };
15
+ }) => CardQualification;
16
+ export type Proposal = {
17
+ title: string;
18
+ servesGoal: string;
19
+ servesNS: string | null;
20
+ ns_proof: string;
21
+ goal_ref: string;
22
+ why_now: string;
23
+ done_when: string[];
24
+ scope: string[];
25
+ out: string[];
26
+ codex_effort: NonNullable<CardQualification["effort"]>;
27
+ proposed_track: NonNullable<CardQualification["trackName"]>;
28
+ qualification: CardQualification;
29
+ };
30
+ export declare function stripFence(text: string): string;
31
+ export declare function extractJsonObject(raw: string): string | null;
32
+ export type ProposalParseResult = {
33
+ ok: Proposal[];
34
+ rejected: Array<{
35
+ title: string;
36
+ reason: string;
37
+ }>;
38
+ warnings: string[];
39
+ rawCount: number;
40
+ prose: string;
41
+ empty_reason?: string;
42
+ error?: string;
43
+ };
44
+ export declare function parseProposals(raw: string, goals: Goal[], nsIds: string[] | undefined, qualifyCard: QualifyCard): ProposalParseResult;