@xandreed/core 0.3.0

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/domain/agent-failure.entity.d.ts +12 -0
  4. package/dist/domain/agent-failure.entity.functions.d.ts +5 -0
  5. package/dist/domain/agent-failure.entity.functions.js +8 -0
  6. package/dist/domain/agent-failure.entity.js +8 -0
  7. package/dist/domain/failure.entity.d.ts +12 -0
  8. package/dist/domain/failure.entity.functions.d.ts +3 -0
  9. package/dist/domain/failure.entity.functions.js +1 -0
  10. package/dist/domain/failure.entity.js +5 -0
  11. package/dist/domain/loop-event.entity.d.ts +94 -0
  12. package/dist/domain/loop-event.entity.js +0 -0
  13. package/dist/domain/message.entity.d.ts +227 -0
  14. package/dist/domain/message.entity.js +51 -0
  15. package/dist/domain/model-call-policy.entity.d.ts +12 -0
  16. package/dist/domain/model-call-policy.entity.js +5 -0
  17. package/dist/domain/model-catalog.entity.d.ts +10 -0
  18. package/dist/domain/model-catalog.entity.js +7 -0
  19. package/dist/domain/model-selection.entity.d.ts +24 -0
  20. package/dist/domain/model-selection.entity.functions.d.ts +5 -0
  21. package/dist/domain/model-selection.entity.functions.js +9 -0
  22. package/dist/domain/model-selection.entity.js +8 -0
  23. package/dist/domain/prompt-provenance.entity.d.ts +11 -0
  24. package/dist/domain/prompt-provenance.entity.js +8 -0
  25. package/dist/domain/token-usage.entity.d.ts +9 -0
  26. package/dist/domain/token-usage.entity.functions.d.ts +3 -0
  27. package/dist/domain/token-usage.entity.functions.js +11 -0
  28. package/dist/domain/token-usage.entity.js +7 -0
  29. package/dist/harness/capability.entity.d.ts +61 -0
  30. package/dist/harness/capability.entity.functions.d.ts +6 -0
  31. package/dist/harness/capability.entity.functions.js +10 -0
  32. package/dist/harness/capability.entity.js +26 -0
  33. package/dist/harness/capability.test.d.ts +1 -0
  34. package/dist/harness/config.entity.d.ts +48 -0
  35. package/dist/harness/config.entity.functions.d.ts +3 -0
  36. package/dist/harness/config.entity.functions.js +1 -0
  37. package/dist/harness/config.entity.js +18 -0
  38. package/dist/harness/plugin.adapter.d.ts +18 -0
  39. package/dist/harness/plugin.adapter.js +13 -0
  40. package/dist/harness/plugin.entity.d.ts +25 -0
  41. package/dist/harness/plugin.entity.js +9 -0
  42. package/dist/harness/session.entity.d.ts +33 -0
  43. package/dist/harness/session.entity.js +25 -0
  44. package/dist/index.d.ts +51 -0
  45. package/dist/index.js +99 -0
  46. package/dist/loop/cacheKey.d.ts +13 -0
  47. package/dist/loop/cacheKey.js +2 -0
  48. package/dist/loop/mapping.d.ts +79 -0
  49. package/dist/loop/mapping.js +154 -0
  50. package/dist/loop/mapping.test.d.ts +1 -0
  51. package/dist/loop/modelPolicy.d.ts +21 -0
  52. package/dist/loop/modelPolicy.js +2 -0
  53. package/dist/loop/promptProvenance.d.ts +8 -0
  54. package/dist/loop/promptProvenance.js +2 -0
  55. package/dist/loop/stepContext.d.ts +3 -0
  56. package/dist/loop/stepContext.js +2 -0
  57. package/dist/loop/streamFold.d.ts +31 -0
  58. package/dist/loop/streamFold.js +58 -0
  59. package/dist/loop/streamFold.test.d.ts +1 -0
  60. package/dist/mcp/bridge.d.ts +51 -0
  61. package/dist/mcp/bridge.js +70 -0
  62. package/dist/mcp/bridge.test.d.ts +1 -0
  63. package/dist/ports/auth-store.port.d.ts +50 -0
  64. package/dist/ports/auth-store.port.js +18 -0
  65. package/dist/ports/capability.port.d.ts +15 -0
  66. package/dist/ports/capability.port.js +7 -0
  67. package/dist/ports/conversation-store.port.d.ts +250 -0
  68. package/dist/ports/conversation-store.port.js +36 -0
  69. package/dist/ports/file-system.port.d.ts +26 -0
  70. package/dist/ports/file-system.port.js +10 -0
  71. package/dist/ports/harness.port.d.ts +106 -0
  72. package/dist/ports/harness.port.js +31 -0
  73. package/dist/ports/mcp-client.port.d.ts +74 -0
  74. package/dist/ports/mcp-client.port.js +24 -0
  75. package/dist/ports/model-catalog.port.d.ts +10 -0
  76. package/dist/ports/model-catalog.port.js +4 -0
  77. package/dist/ports/settings-store.port.d.ts +85 -0
  78. package/dist/ports/settings-store.port.js +36 -0
  79. package/dist/ports/shell.port.d.ts +44 -0
  80. package/dist/ports/shell.port.js +16 -0
  81. package/dist/ports/utility-llm.port.d.ts +48 -0
  82. package/dist/ports/utility-llm.port.js +16 -0
  83. package/dist/session/chassis.d.ts +59 -0
  84. package/dist/session/chassis.js +27 -0
  85. package/dist/session/chassis.test.d.ts +1 -0
  86. package/dist/spec/SpecDoc.d.ts +162 -0
  87. package/dist/spec/SpecDoc.js +59 -0
  88. package/dist/spec/codec.d.ts +30 -0
  89. package/dist/spec/codec.js +123 -0
  90. package/dist/spec/codec.test.d.ts +1 -0
  91. package/dist/spec/frontmatter.d.ts +16 -0
  92. package/dist/spec/frontmatter.js +25 -0
  93. package/dist/util/json.d.ts +19 -0
  94. package/dist/util/json.js +15 -0
  95. package/package.json +30 -0
@@ -0,0 +1,123 @@
1
+ import { Array as Arr, Data, Effect, Option, Schema } from "effect";
2
+ import { SpecDoc } from "./SpecDoc.js";
3
+ import { SpecSlug as SpecSlugSchema } from "./SpecDoc.js";
4
+ import { parseFrontmatter } from "./frontmatter.js";
5
+
6
+ export class SpecDocParseError extends Data.TaggedError("SpecDocParseError") {
7
+ }
8
+ const SECTION_HEADERS = ["Acceptance", "Checks", "Constraints", "Non-goals"], emptyBullets = {
9
+ Acceptance: [],
10
+ Checks: [],
11
+ Constraints: [],
12
+ "Non-goals": []
13
+ }, parseSections = (body) => {
14
+ const folded = body.split(`
15
+ `).reduce((acc, rawLine) => {
16
+ if (Option.isSome(acc.error))
17
+ return acc;
18
+ const line = rawLine.trimEnd();
19
+ if (line.startsWith("# "))
20
+ return line === "# Goal" ? { ...acc, current: "goal" } : { ...acc, error: Option.some(`unknown top-level section "${line}" (only "# Goal")`) };
21
+ if (line.startsWith("## ")) {
22
+ const header = line.slice(3).trim(), known = SECTION_HEADERS.find((h) => h === header);
23
+ return known === void 0 ? {
24
+ ...acc,
25
+ error: Option.some(`unknown section "## ${header}" (allowed: ${SECTION_HEADERS.join(", ")})`)
26
+ } : { ...acc, current: known };
27
+ }
28
+ if (acc.current === "goal")
29
+ return { ...acc, goalLines: [...acc.goalLines, line] };
30
+ if (acc.current !== "none" && line.trim().startsWith("- ")) {
31
+ const bullet = line.trim().slice(2).trim();
32
+ return bullet.length === 0 ? acc : {
33
+ ...acc,
34
+ bullets: {
35
+ ...acc.bullets,
36
+ [acc.current]: [...acc.bullets[acc.current], bullet]
37
+ }
38
+ };
39
+ }
40
+ if (acc.current !== "none" && line.trim().length > 0)
41
+ return {
42
+ ...acc,
43
+ error: Option.some(`section "## ${acc.current}" only takes "- " bullets (got: "${line.trim()}")`)
44
+ };
45
+ return acc;
46
+ }, { current: "none", goalLines: [], bullets: emptyBullets, error: Option.none() });
47
+ if (Option.isSome(folded.error))
48
+ return Effect.fail(new SpecDocParseError({ message: folded.error.value }));
49
+ const goal = folded.goalLines.join(`
50
+ `).trim();
51
+ if (goal.length === 0)
52
+ return Effect.fail(new SpecDocParseError({ message: 'missing "# Goal" section' }));
53
+ return Effect.succeed({ goal, bullets: folded.bullets });
54
+ }, parseCheckBullet = (bullet) => {
55
+ const colon = bullet.indexOf(":");
56
+ if (colon === -1)
57
+ return Effect.fail(new SpecDocParseError({ message: `check bullet needs "name: command" (got "${bullet}")` }));
58
+ return Effect.succeed({
59
+ name: bullet.slice(0, colon).trim(),
60
+ command: bullet.slice(colon + 1).trim()
61
+ });
62
+ };
63
+ export const decodeSpecDocText = (slug, text) => Effect.gen(function* () {
64
+ const fm = yield* Option.match(parseFrontmatter(text), {
65
+ onNone: () => Effect.fail(new SpecDocParseError({ message: "missing/unterminated frontmatter fence" })),
66
+ onSome: Effect.succeed
67
+ }), sections = yield* parseSections(fm.body), checks = yield* Effect.forEach(sections.bullets.Checks, parseCheckBullet), fields = fm.fields, numeric = (key, fallback) => {
68
+ const raw = fields[key];
69
+ return raw === void 0 || raw.length === 0 ? fallback : Number(raw);
70
+ }, candidate = {
71
+ slug,
72
+ status: fields.status ?? "draft",
73
+ created: fields.created ?? "",
74
+ ...fields.locked !== void 0 && fields.locked.length > 0 ? { locked: fields.locked } : {},
75
+ goal: sections.goal,
76
+ acceptance: sections.bullets.Acceptance,
77
+ constraints: sections.bullets.Constraints,
78
+ nonGoals: sections.bullets["Non-goals"],
79
+ checks,
80
+ limits: {
81
+ maxAttempts: numeric("maxAttempts", 3),
82
+ budgetMinutes: numeric("budgetMinutes", 15)
83
+ },
84
+ gates: {
85
+ ...fields.gatesConfig !== void 0 && fields.gatesConfig.length > 0 ? { config: fields.gatesConfig } : {},
86
+ ...fields.testCommand !== void 0 && fields.testCommand.length > 0 ? { testCommand: fields.testCommand } : {},
87
+ ...fields.noTest !== void 0 ? { noTest: fields.noTest === "true" } : {},
88
+ ...fields.judge !== void 0 ? { judge: fields.judge === "true" } : {}
89
+ }
90
+ };
91
+ return yield* Schema.decodeUnknown(SpecDoc)(candidate).pipe(Effect.mapError((error) => new SpecDocParseError({ message: String(error) })));
92
+ }), normalizeBullet = (text) => text.replace(/\s+/g, " ").trim(), encodeSpecDocText = (doc) => {
93
+ const frontmatter = [
94
+ "---",
95
+ "version: 1",
96
+ `status: ${doc.status}`,
97
+ `created: ${doc.created}`,
98
+ ...Option.match(doc.locked, { onNone: () => [], onSome: (at) => [`locked: ${at}`] }),
99
+ `maxAttempts: ${doc.limits.maxAttempts}`,
100
+ `budgetMinutes: ${doc.limits.budgetMinutes}`,
101
+ ...Option.match(doc.gates.config, { onNone: () => [], onSome: (c) => [`gatesConfig: ${c}`] }),
102
+ ...Option.match(doc.gates.testCommand, {
103
+ onNone: () => [],
104
+ onSome: (c) => [`testCommand: ${c}`]
105
+ }),
106
+ ...doc.gates.noTest ? ["noTest: true"] : [],
107
+ ...doc.gates.judge ? [] : ["judge: false"],
108
+ "---"
109
+ ].join(`
110
+ `), section = (header, items) => items.length === 0 ? "" : `
111
+
112
+ ## ${header}
113
+ ${items.map((item) => `- ${item}`).join(`
114
+ `)}`;
115
+ return `${frontmatter}
116
+
117
+ # Goal
118
+ ${doc.goal}${section("Acceptance", doc.acceptance.map(normalizeBullet))}${section("Checks", doc.checks.map((check) => `${check.name}: ${check.command}`))}${section("Constraints", doc.constraints.map(normalizeBullet))}${section("Non-goals", doc.nonGoals.map(normalizeBullet))}
119
+ `;
120
+ }, specSlug = (goal) => {
121
+ const kebab = goal.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
122
+ return SpecSlugSchema.make(kebab.length === 0 ? "spec" : kebab);
123
+ }, uniqueSlug = (base, taken) => taken(base) ? SpecSlugSchema.make(Option.getOrElse(Arr.findFirst(Arr.range(2, 99), (n) => !taken(`${base}-${n}`)).pipe(Option.map((n) => `${base}-${n}`)), () => `${base}-${Date.now()}`)) : base;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { Option } from "effect";
2
+ export interface Frontmatter {
3
+ /** Flat `key: value` pairs from the fence. No arrays / nested YAML. */
4
+ readonly fields: Record<string, string>;
5
+ /** Everything after the closing fence, leading blank lines trimmed. */
6
+ readonly body: string;
7
+ }
8
+ /**
9
+ * The one frontmatter parser shared by scopes, skills, and agent definitions:
10
+ * a `---\n…\n---` fence at the top with `key: value` lines inside, then a
11
+ * free-form body. Values are taken verbatim, trimmed, surrounding quotes
12
+ * stripped; comment (`#`) and blank lines are skipped. No arrays, no nested
13
+ * YAML, no multi-line values. Returns `None` when the fence is missing or
14
+ * unterminated. Callers decide which `fields` are required.
15
+ */
16
+ export declare const parseFrontmatter: (content: string) => Option.Option<Frontmatter>;
@@ -0,0 +1,25 @@
1
+ import { Option } from "effect";
2
+ export const parseFrontmatter = (content) => {
3
+ if (!content.startsWith("---"))
4
+ return Option.none();
5
+ const rest = content.slice(3), lfIndex = rest.indexOf(`
6
+ `);
7
+ if (lfIndex === -1)
8
+ return Option.none();
9
+ const afterFirstFence = rest.slice(lfIndex + 1), closeIndex = afterFirstFence.indexOf(`
10
+ ---`);
11
+ if (closeIndex === -1)
12
+ return Option.none();
13
+ const frontmatter = afterFirstFence.slice(0, closeIndex), body = afterFirstFence.slice(closeIndex + 4).replace(/^\n+/, ""), fields = Object.fromEntries(frontmatter.split(`
14
+ `).flatMap((line) => {
15
+ const trimmed = line.trim();
16
+ if (trimmed === "" || trimmed.startsWith("#"))
17
+ return [];
18
+ const colon = trimmed.indexOf(":");
19
+ if (colon === -1)
20
+ return [];
21
+ const key = trimmed.slice(0, colon).trim(), value = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
22
+ return [[key, value]];
23
+ }));
24
+ return Option.some({ fields, body });
25
+ };
@@ -0,0 +1,19 @@
1
+ import { Effect, Either, Option } from "effect";
2
+ /**
3
+ * Parse JSON with CORRUPT ≠ ABSENT semantics: malformed text logs a warning
4
+ * naming the source and yields `None`. Config readers fall back to their
5
+ * empty shape either way — but a corrupt `auth.json` silently reading as
6
+ * "logged out", or a corrupt `config.json` as "defaults", gives the user
7
+ * zero signal about the actual problem (audit class L8).
8
+ */
9
+ export declare const parseJsonWarn: (text: string, where: string) => Effect.Effect<Option.Option<unknown>>;
10
+ /** The record-or-empty projection every config reader wants after parse. */
11
+ export declare const asJsonRecord: (value: Option.Option<unknown>) => Record<string, unknown>;
12
+ /** SILENT parse-to-Option — for wire noise (a garbage WS frame) where
13
+ * dropping without a log is the design; configs use {@link parseJsonWarn}. */
14
+ export declare const parseJsonOption: (text: string) => Option.Option<unknown>;
15
+ /** Decode append-only JSONL text: one JSON value per line, each decoded by
16
+ * `decode`; a corrupt or undecodable LINE is skipped (append-only files
17
+ * corrupt at a line boundary — one bad row must not brick the history).
18
+ * The shared machinery of the smith memory ledger and the social ledger. */
19
+ export declare const decodeJsonLines: <A, E>(text: string, decode: (value: unknown) => Either.Either<A, E>) => ReadonlyArray<A>;
@@ -0,0 +1,15 @@
1
+ import { Effect, Either, Option } from "effect";
2
+ export const parseJsonWarn = (text, where) => Either.match(Either.try(() => JSON.parse(text)), {
3
+ onLeft: (error) => Effect.logWarning(`${where}: unreadable JSON \u2014 treating as empty: ${String(error)}`).pipe(Effect.as(Option.none())),
4
+ onRight: (value) => Effect.succeed(Option.some(value))
5
+ }), asJsonRecord = (value) => Option.match(value, {
6
+ onNone: () => ({}),
7
+ onSome: (parsed) => typeof parsed === "object" && parsed !== null ? parsed : {}
8
+ }), parseJsonOption = (text) => Either.getRight(Either.try(() => JSON.parse(text))), decodeJsonLines = (text, decode) => text.split(`
9
+ `).filter((line) => line.trim().length > 0).flatMap((line) => Either.try(() => JSON.parse(line)).pipe(Either.match({
10
+ onLeft: () => [],
11
+ onRight: (parsed) => Either.match(decode(parsed), {
12
+ onLeft: () => [],
13
+ onRight: (decoded) => [decoded]
14
+ })
15
+ })));
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@xandreed/core",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "The agent kernel of the new line: entities, ports, the provider-agnostic agent loop, and the session chassis — pure Effect, no IO, no provider SDKs.",
7
+ "main": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@effect/ai": "0.35.0",
16
+ "effect": "3.21.4"
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "skills",
22
+ "profiles",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "bun": ">=1.3.0"
28
+ },
29
+ "license": "MIT"
30
+ }